@@ -32,6 +32,7 @@ import (
3232 "fmt"
3333 "os"
3434 "os/exec"
35+ "strconv"
3536 "strings"
3637 "time"
3738)
@@ -85,63 +86,105 @@ func tag(args []string) error {
8586 return fmt .Errorf ("invalid flags: %w" , err )
8687 }
8788
88- if len (fs .Args ()) > 1 {
89- return fmt .Errorf ("too many args: %#v" , fs .Args ())
90- }
91-
92- branch := "main"
93- if len (fs .Args ()) == 1 {
89+ var branch string
90+ switch len (fs .Args ()) {
91+ case 0 :
92+ branch = "main"
93+ case 1 :
9494 branch = fs .Arg (0 )
95- }
96-
97- switch {
98- case branch == "main" :
99- break
100- case strings .HasPrefix (branch , "release-branch-" ):
101- return fmt .Errorf ("sorry, tagging hotfix release branches is not yet supported" )
95+ if ! strings .HasPrefix (branch , "release-branch-" ) {
96+ return fmt .Errorf ("branch must be 'main' or 'release-branch-...', got %q" , branch )
97+ }
10298 default :
103- return fmt .Errorf ("branch must be 'main' or 'release-branch-...', got %q " , branch )
99+ return fmt .Errorf ("too many args: %#v " , fs . Args () )
104100 }
105101
106102 // Fetch all of the latest commits on this ref from origin, so that we can
107- // ensure we're tagging the tip of the upstream branch.
103+ // ensure we're tagging the tip of the upstream branch, and that we have all
104+ // of the extant tags along this branch if its a release branch.
108105 _ , err = git ("fetch" , "origin" , branch )
109106 if err != nil {
110107 return err
111108 }
112109
113- // We use semver's vMajor.Minor.Patch format, where the Major version is
114- // always 0 (no backwards compatibility guarantees), the Minor version is
115- // the date of the release, and the Patch number is zero for normal releases
116- // and only non-zero for hotfix releases.
117- minor := time .Now ().Format ("20060102" )
118- version := fmt .Sprintf ("v0.%s.0" , minor )
119- message := fmt .Sprintf ("Release %s" , version )
110+ var tag string
111+ switch branch {
112+ case "main" :
113+ tag = fmt .Sprintf ("v0.%s.0" , time .Now ().Format ("20060102" ))
114+ default :
115+ tag , err = nextTagOnBranch (branch )
116+ if err != nil {
117+ return fmt .Errorf ("failed to compute next hotfix tag: %w" , err )
118+ }
119+ }
120120
121121 // Produce the tag, using -s to PGP sign it. This will fail if a tag with
122122 // that name already exists.
123- _ , err = git ("tag" , "-s" , "-m" , message , version , "origin/" + branch )
123+ message := fmt .Sprintf ("Release %s" , tag )
124+ _ , err = git ("tag" , "-s" , "-m" , message , tag , "origin/" + branch )
124125 if err != nil {
125126 return err
126127 }
127128
128129 // Show the result of the tagging operation, including the tag message and
129130 // signature, and the commit hash and message, but not the diff.
130- out , err := git ("show" , "-s" , version )
131+ out , err := git ("show" , "-s" , tag )
131132 if err != nil {
132133 return err
133134 }
134135 show (out )
135136
136137 if push {
137- _ , err = git ("push" , "origin" , version )
138+ _ , err = git ("push" , "origin" , tag )
138139 if err != nil {
139140 return err
140141 }
141142 } else {
142143 fmt .Println ()
143144 fmt .Println ("Please inspect the tag above, then run:" )
144- fmt .Printf (" git push origin %s\n " , version )
145+ fmt .Printf (" git push origin %s\n " , tag )
145146 }
146147 return nil
147148}
149+
150+ func nextTagOnBranch (branch string ) (string , error ) {
151+ baseVersion := strings .TrimPrefix (branch , "release-branch-" )
152+ out , err := git ("tag" , "--list" , "--no-column" , baseVersion + ".*" )
153+ if err != nil {
154+ return "" , fmt .Errorf ("failed to list extant tags on branch %q: %w" , branch , err )
155+ }
156+
157+ maxPatch := 0
158+ for tag := range strings .SplitSeq (strings .TrimSpace (out ), "\n " ) {
159+ parts := strings .SplitN (tag , "." , 3 )
160+ if len (parts ) != 3 {
161+ return "" , fmt .Errorf ("failed to parse release tag %q as semver" , tag )
162+ }
163+
164+ major := parts [0 ]
165+ if major != "v0" {
166+ return "" , fmt .Errorf ("expected major portion of prior release tag %q to be 'v0'" , tag )
167+ }
168+
169+ minor := parts [1 ]
170+ t , err := time .Parse ("20060102" , minor )
171+ if err != nil {
172+ return "" , fmt .Errorf ("expected minor portion of prior release tag %q to be a date: %w" , tag , err )
173+ }
174+ if t .Year () < 2015 {
175+ return "" , fmt .Errorf ("minor portion of prior release tag %q appears to be an unrealistic date: %q" , tag , t .String ())
176+ }
177+
178+ patch := parts [2 ]
179+ patchInt , err := strconv .Atoi (patch )
180+ if err != nil {
181+ return "" , fmt .Errorf ("patch portion of prior release tag %q is not an integer: %w" , tag , err )
182+ }
183+
184+ if patchInt > maxPatch {
185+ maxPatch = patchInt
186+ }
187+ }
188+
189+ return fmt .Sprintf ("%s.%d" , baseVersion , maxPatch + 1 ), nil
190+ }
0 commit comments