@@ -7,6 +7,7 @@ package main
77import (
88 "context"
99 "fmt"
10+ "log"
1011 "net/http"
1112 "os"
1213 "os/exec"
@@ -18,8 +19,11 @@ import (
1819
1920 "github.com/google/go-github/v53/github"
2021 "github.com/urfave/cli/v2"
22+ "gopkg.in/yaml.v3"
2123)
2224
25+ const defaultVersion = "v1.18" // to backport to
26+
2327func main () {
2428 app := cli .NewApp ()
2529 app .Name = "backport"
@@ -50,6 +54,16 @@ func main() {
5054 Name : "backport-branch" ,
5155 Usage : "Backport branch to backport on to (default: backport-<pr>-<version>" ,
5256 },
57+ & cli.StringFlag {
58+ Name : "remote" ,
59+ Value : "" ,
60+ Usage : "Remote for your fork of the Gitea upstream" ,
61+ },
62+ & cli.StringFlag {
63+ Name : "fork-user" ,
64+ Value : "" ,
65+ Usage : "Forked user name on Github" ,
66+ },
5367 & cli.BoolFlag {
5468 Name : "no-fetch" ,
5569 Usage : "Set this flag to prevent fetch of remote branches" ,
@@ -58,6 +72,18 @@ func main() {
5872 Name : "no-amend-message" ,
5973 Usage : "Set this flag to prevent automatic amendment of the commit message" ,
6074 },
75+ & cli.BoolFlag {
76+ Name : "no-push" ,
77+ Usage : "Set this flag to prevent pushing the backport up to your fork" ,
78+ },
79+ & cli.BoolFlag {
80+ Name : "no-xdg-open" ,
81+ Usage : "Set this flag to not use xdg-open to open the PR URL" ,
82+ },
83+ & cli.BoolFlag {
84+ Name : "continue" ,
85+ Usage : "Set this flag to continue from a git cherry-pick that has broken" ,
86+ },
6187 }
6288 cli .AppHelpTemplate = `NAME:
6389 {{.Name}} - {{.Usage}}
@@ -75,24 +101,49 @@ OPTIONS:
75101 app .Action = runBackport
76102
77103 if err := app .Run (os .Args ); err != nil {
78- fmt .Fprintf (os .Stderr , "%v\n " , err )
104+ fmt .Fprintf (os .Stderr , "Unable to backport: %v\n " , err )
79105 }
80106}
81107
82108func runBackport (c * cli.Context ) error {
83109 ctx , cancel := installSignals ()
84110 defer cancel ()
85111
112+ continuing := c .Bool ("continue" )
113+
114+ var pr string
115+
86116 version := c .String ("version" )
117+ if version == "" && continuing {
118+ // determine version from current branch name
119+ var err error
120+ pr , version , err = readCurrentBranch (ctx )
121+ if err != nil {
122+ return err
123+ }
124+ }
87125 if version == "" {
88- return fmt .Errorf ("Provide a version to backport to" )
126+ version = readVersion ()
127+ }
128+ if version == "" {
129+ version = defaultVersion
89130 }
90131
91132 upstream := c .String ("upstream" )
92133 if upstream == "" {
93134 upstream = "origin"
94135 }
95136
137+ forkUser := c .String ("fork-user" )
138+ remote := c .String ("remote" )
139+ if remote == "" && ! c .Bool ("--no-push" ) {
140+ var err error
141+ remote , forkUser , err = determineRemote (ctx , forkUser )
142+ if err != nil {
143+ return err
144+ }
145+ }
146+
96147 upstreamReleaseBranch := c .String ("release-branch" )
97148 if upstreamReleaseBranch == "" {
98149 upstreamReleaseBranch = path .Join ("release" , version )
@@ -101,12 +152,14 @@ func runBackport(c *cli.Context) error {
101152 localReleaseBranch := path .Join (upstream , upstreamReleaseBranch )
102153
103154 args := c .Args ().Slice ()
104- if len (args ) == 0 {
105- return fmt .Errorf ("Provide a PR number to backport" )
106- } else if len (args ) != 1 {
107- return fmt .Errorf ("Only a single PR can be backported at a time" )
155+ if len (args ) == 0 && pr == "" {
156+ return fmt .Errorf ("no PR number provided\n Provide a PR number to backport" )
157+ } else if len (args ) != 1 && pr == "" {
158+ return fmt .Errorf ("multiple PRs provided %v\n Only a single PR can be backported at a time" , args )
159+ }
160+ if pr == "" {
161+ pr = args [0 ]
108162 }
109- pr := args [0 ]
110163
111164 backportBranch := c .String ("backport-branch" )
112165 if backportBranch == "" {
@@ -133,8 +186,10 @@ func runBackport(c *cli.Context) error {
133186 }
134187 }
135188
136- if err := checkoutBackportBranch (ctx , backportBranch , localReleaseBranch ); err != nil {
137- return err
189+ if ! continuing {
190+ if err := checkoutBackportBranch (ctx , backportBranch , localReleaseBranch ); err != nil {
191+ return err
192+ }
138193 }
139194
140195 if err := cherrypick (ctx , sha ); err != nil {
@@ -147,8 +202,41 @@ func runBackport(c *cli.Context) error {
147202 }
148203 }
149204
150- fmt .Printf ("Backport done! You can now push it with `git push <your remote> %s`\n " , backportBranch )
205+ if ! c .Bool ("no-push" ) {
206+ url := "https://github.com/go-gitea/gitea/compare/" + upstreamReleaseBranch + "..." + forkUser + ":" + backportBranch
207+
208+ if err := gitPushUp (ctx , remote , backportBranch ); err != nil {
209+ return err
210+ }
211+
212+ if ! c .Bool ("no-xdg-open" ) {
213+ if err := xdgOpen (ctx , url ); err != nil {
214+ return err
215+ }
216+ } else {
217+ fmt .Printf ("* Navigate to %s to open PR\n " , url )
218+ }
219+ }
220+ return nil
221+ }
222+
223+ func xdgOpen (ctx context.Context , url string ) error {
224+ fmt .Printf ("* `xdg-open %s`\n " , url )
225+ out , err := exec .CommandContext (ctx , "xdg-open" , url ).Output ()
226+ if err != nil {
227+ fmt .Fprintf (os .Stderr , "%s" , string (out ))
228+ return fmt .Errorf ("unable to xdg-open to %s: %w" , url , err )
229+ }
230+ return nil
231+ }
151232
233+ func gitPushUp (ctx context.Context , remote , backportBranch string ) error {
234+ fmt .Printf ("* `git push -u %s %s`\n " , remote , backportBranch )
235+ out , err := exec .CommandContext (ctx , "git" , "push" , "-u" , remote , backportBranch ).Output ()
236+ if err != nil {
237+ fmt .Fprintf (os .Stderr , "%s" , string (out ))
238+ return fmt .Errorf ("unable to push up to %s: %w" , remote , err )
239+ }
152240 return nil
153241}
154242
@@ -179,6 +267,18 @@ func amendCommit(ctx context.Context, pr string) error {
179267}
180268
181269func cherrypick (ctx context.Context , sha string ) error {
270+ // Check if a CHERRY_PICK_HEAD exists
271+ if _ , err := os .Stat (".git/CHERRY_PICK_HEAD" ); err == nil {
272+ // Assume that we are in the middle of cherry-pick - continue it
273+ fmt .Println ("* Attempting git cherry-pick --continue" )
274+ out , err := exec .CommandContext (ctx , "git" , "cherry-pick" , "--continue" ).Output ()
275+ if err != nil {
276+ fmt .Fprintf (os .Stderr , "git cherry-pick --continue failed:\n %s\n " , string (out ))
277+ return fmt .Errorf ("unable to continue cherry-pick: %w" , err )
278+ }
279+ return nil
280+ }
281+
182282 fmt .Printf ("* Attempting git cherry-pick %s\n " , sha )
183283 out , err := exec .CommandContext (ctx , "git" , "cherry-pick" , sha ).Output ()
184284 if err != nil {
@@ -189,8 +289,22 @@ func cherrypick(ctx context.Context, sha string) error {
189289}
190290
191291func checkoutBackportBranch (ctx context.Context , backportBranch , releaseBranch string ) error {
192- fmt .Printf ("* `git branch -D %s`\n " , backportBranch )
193- _ = exec .CommandContext (ctx , "git" , "branch" , "-D" , backportBranch ).Run ()
292+ out , err := exec .CommandContext (ctx , "git" , "branch" , "--show-current" ).Output ()
293+ if err != nil {
294+ return fmt .Errorf ("unable to check current branch %w" , err )
295+ }
296+
297+ currentBranch := strings .TrimSpace (string (out ))
298+ fmt .Printf ("* Current branch is %s\n " , currentBranch )
299+ if currentBranch == backportBranch {
300+ fmt .Printf ("* Current branch is %s - not checking out\n " , currentBranch )
301+ return nil
302+ }
303+
304+ if _ , err := exec .CommandContext (ctx , "git" , "rev-list" , "-1" , backportBranch ).Output (); err == nil {
305+ fmt .Printf ("* Branch %s already exists. Checking it out...\n " , backportBranch )
306+ return exec .CommandContext (ctx , "git" , "checkout" , "-f" , backportBranch ).Run ()
307+ }
194308
195309 fmt .Printf ("* `git checkout -b %s %s`\n " , backportBranch , releaseBranch )
196310 return exec .CommandContext (ctx , "git" , "checkout" , "-b" , backportBranch , releaseBranch ).Run ()
@@ -203,17 +317,116 @@ func fetchRemoteAndMain(ctx context.Context, remote, releaseBranch string) error
203317 fmt .Println (string (out ))
204318 return fmt .Errorf ("unable to fetch %s from %s: %w" , "main" , remote , err )
205319 }
320+ fmt .Println (string (out ))
206321
207322 fmt .Printf ("* `git fetch %s %s`\n " , remote , releaseBranch )
208323 out , err = exec .CommandContext (ctx , "git" , "fetch" , remote , releaseBranch ).Output ()
209324 if err != nil {
210325 fmt .Println (string (out ))
211326 return fmt .Errorf ("unable to fetch %s from %s: %w" , releaseBranch , remote , err )
212327 }
328+ fmt .Println (string (out ))
213329
214330 return nil
215331}
216332
333+ func determineRemote (ctx context.Context , forkUser string ) (string , string , error ) {
334+ out , err := exec .CommandContext (ctx , "git" , "remote" , "-v" ).Output ()
335+ if err != nil {
336+ fmt .Fprintf (os .Stderr , "Unable to list git remotes:\n %s\n " , string (out ))
337+ return "" , "" , fmt .Errorf ("unable to determine forked remote: %w" , err )
338+ }
339+ lines := strings .Split (string (out ), "\n " )
340+ for _ , line := range lines {
341+ fields := strings .Split (line , "\t " )
342+ name , remote := fields [0 ], fields [1 ]
343+ // only look at pushers
344+ if ! strings .HasSuffix (remote , " (push)" ) {
345+ continue
346+ }
347+ // only look at github.com pushes
348+ if ! strings .Contains (remote , "github.com" ) {
349+ continue
350+ }
351+ // ignore go-gitea/gitea
352+ if strings .Contains (remote , "go-gitea/gitea" ) {
353+ continue
354+ }
355+ if ! strings .Contains (remote , forkUser ) {
356+ continue
357+ }
358+ if strings .HasPrefix (remote , "git@github.com:" ) {
359+ forkUser = strings .TrimPrefix (remote , "git@github.com:" )
360+ } else if strings .HasPrefix (remote , "https://github.com/" ) {
361+ forkUser = strings .TrimPrefix (remote , "https://github.com/" )
362+ } else if strings .HasPrefix (remote , "https://www.github.com/" ) {
363+ forkUser = strings .TrimPrefix (remote , "https://www.github.com/" )
364+ } else if forkUser == "" {
365+ return "" , "" , fmt .Errorf ("unable to extract forkUser from remote %s: %s" , name , remote )
366+ }
367+ idx := strings .Index (forkUser , "/" )
368+ if idx >= 0 {
369+ forkUser = forkUser [:idx ]
370+ }
371+ return name , forkUser , nil
372+ }
373+ return "" , "" , fmt .Errorf ("unable to find appropriate remote in:\n %s" , string (out ))
374+ }
375+
376+ func readCurrentBranch (ctx context.Context ) (pr , version string , err error ) {
377+ out , err := exec .CommandContext (ctx , "git" , "branch" , "--show-current" ).Output ()
378+ if err != nil {
379+ fmt .Fprintf (os .Stderr , "Unable to read current git branch:\n %s\n " , string (out ))
380+ return "" , "" , fmt .Errorf ("unable to read current git branch: %w" , err )
381+ }
382+ parts := strings .Split (strings .TrimSpace (string (out )), "-" )
383+
384+ if len (parts ) != 3 || parts [0 ] != "backport" {
385+ fmt .Fprintf (os .Stderr , "Unable to continue from git branch:\n %s\n " , string (out ))
386+ return "" , "" , fmt .Errorf ("unable to continue from git branch:\n %s" , string (out ))
387+ }
388+
389+ return parts [1 ], parts [2 ], nil
390+ }
391+
392+ func readVersion () string {
393+ bs , err := os .ReadFile ("docs/config.yaml" )
394+ if err != nil {
395+ if err == os .ErrNotExist {
396+ log .Println ("`docs/config.yaml` not present" )
397+ return ""
398+ }
399+ fmt .Fprintf (os .Stderr , "Unable to read `docs/config.yaml`: %v\n " , err )
400+ return ""
401+ }
402+
403+ type params struct {
404+ Version string
405+ }
406+ type docConfig struct {
407+ Params params
408+ }
409+ dc := & docConfig {}
410+ if err := yaml .Unmarshal (bs , dc ); err != nil {
411+ fmt .Fprintf (os .Stderr , "Unable to read `docs/config.yaml`: %v\n " , err )
412+ return ""
413+ }
414+
415+ if dc .Params .Version == "" {
416+ fmt .Fprintf (os .Stderr , "No version in `docs/config.yaml`" )
417+ return ""
418+ }
419+
420+ version := dc .Params .Version
421+ if version [0 ] != 'v' {
422+ version = "v" + version
423+ }
424+
425+ split := strings .SplitN (version , "." , 3 )
426+
427+ return strings .Join (split [:2 ], "." )
428+ }
429+
217430func determineSHAforPR (ctx context.Context , prStr string ) (string , error ) {
218431 prNum , err := strconv .Atoi (prStr )
219432 if err != nil {
0 commit comments