@@ -7,6 +7,7 @@ package main
7
7
import (
8
8
"context"
9
9
"fmt"
10
+ "log"
10
11
"net/http"
11
12
"os"
12
13
"os/exec"
@@ -18,8 +19,11 @@ import (
18
19
19
20
"github.com/google/go-github/v53/github"
20
21
"github.com/urfave/cli/v2"
22
+ "gopkg.in/yaml.v3"
21
23
)
22
24
25
+ const defaultVersion = "v1.18" // to backport to
26
+
23
27
func main () {
24
28
app := cli .NewApp ()
25
29
app .Name = "backport"
@@ -50,6 +54,16 @@ func main() {
50
54
Name : "backport-branch" ,
51
55
Usage : "Backport branch to backport on to (default: backport-<pr>-<version>" ,
52
56
},
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
+ },
53
67
& cli.BoolFlag {
54
68
Name : "no-fetch" ,
55
69
Usage : "Set this flag to prevent fetch of remote branches" ,
@@ -58,6 +72,18 @@ func main() {
58
72
Name : "no-amend-message" ,
59
73
Usage : "Set this flag to prevent automatic amendment of the commit message" ,
60
74
},
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
+ },
61
87
}
62
88
cli .AppHelpTemplate = `NAME:
63
89
{{.Name}} - {{.Usage}}
@@ -75,24 +101,49 @@ OPTIONS:
75
101
app .Action = runBackport
76
102
77
103
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 )
79
105
}
80
106
}
81
107
82
108
func runBackport (c * cli.Context ) error {
83
109
ctx , cancel := installSignals ()
84
110
defer cancel ()
85
111
112
+ continuing := c .Bool ("continue" )
113
+
114
+ var pr string
115
+
86
116
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
+ }
87
125
if version == "" {
88
- return fmt .Errorf ("Provide a version to backport to" )
126
+ version = readVersion ()
127
+ }
128
+ if version == "" {
129
+ version = defaultVersion
89
130
}
90
131
91
132
upstream := c .String ("upstream" )
92
133
if upstream == "" {
93
134
upstream = "origin"
94
135
}
95
136
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
+
96
147
upstreamReleaseBranch := c .String ("release-branch" )
97
148
if upstreamReleaseBranch == "" {
98
149
upstreamReleaseBranch = path .Join ("release" , version )
@@ -101,12 +152,14 @@ func runBackport(c *cli.Context) error {
101
152
localReleaseBranch := path .Join (upstream , upstreamReleaseBranch )
102
153
103
154
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 ]
108
162
}
109
- pr := args [0 ]
110
163
111
164
backportBranch := c .String ("backport-branch" )
112
165
if backportBranch == "" {
@@ -133,8 +186,10 @@ func runBackport(c *cli.Context) error {
133
186
}
134
187
}
135
188
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
+ }
138
193
}
139
194
140
195
if err := cherrypick (ctx , sha ); err != nil {
@@ -147,8 +202,41 @@ func runBackport(c *cli.Context) error {
147
202
}
148
203
}
149
204
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
+ }
151
232
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
+ }
152
240
return nil
153
241
}
154
242
@@ -179,6 +267,18 @@ func amendCommit(ctx context.Context, pr string) error {
179
267
}
180
268
181
269
func 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
+
182
282
fmt .Printf ("* Attempting git cherry-pick %s\n " , sha )
183
283
out , err := exec .CommandContext (ctx , "git" , "cherry-pick" , sha ).Output ()
184
284
if err != nil {
@@ -189,8 +289,22 @@ func cherrypick(ctx context.Context, sha string) error {
189
289
}
190
290
191
291
func 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
+ }
194
308
195
309
fmt .Printf ("* `git checkout -b %s %s`\n " , backportBranch , releaseBranch )
196
310
return exec .CommandContext (ctx , "git" , "checkout" , "-b" , backportBranch , releaseBranch ).Run ()
@@ -203,17 +317,116 @@ func fetchRemoteAndMain(ctx context.Context, remote, releaseBranch string) error
203
317
fmt .Println (string (out ))
204
318
return fmt .Errorf ("unable to fetch %s from %s: %w" , "main" , remote , err )
205
319
}
320
+ fmt .Println (string (out ))
206
321
207
322
fmt .Printf ("* `git fetch %s %s`\n " , remote , releaseBranch )
208
323
out , err = exec .CommandContext (ctx , "git" , "fetch" , remote , releaseBranch ).Output ()
209
324
if err != nil {
210
325
fmt .Println (string (out ))
211
326
return fmt .Errorf ("unable to fetch %s from %s: %w" , releaseBranch , remote , err )
212
327
}
328
+ fmt .Println (string (out ))
213
329
214
330
return nil
215
331
}
216
332
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 ,
"[email protected] :" ) {
359
+ forkUser = strings .
TrimPrefix (
remote ,
"[email protected] :" )
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
+
217
430
func determineSHAforPR (ctx context.Context , prStr string ) (string , error ) {
218
431
prNum , err := strconv .Atoi (prStr )
219
432
if err != nil {
0 commit comments