sugarjar 1.1.3 → 2.0.0.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,20 +4,27 @@ require_relative 'util'
4
4
  require_relative 'repoconfig'
5
5
  require_relative 'log'
6
6
  require_relative 'version'
7
+ require_relative 'commands/amend'
8
+ require_relative 'commands/bclean'
9
+ require_relative 'commands/branch'
10
+ require_relative 'commands/checks'
11
+ require_relative 'commands/debuginfo'
12
+ require_relative 'commands/feature'
13
+ require_relative 'commands/pullsuggestions'
14
+ require_relative 'commands/push'
15
+ require_relative 'commands/smartclone'
16
+ require_relative 'commands/smartpullrequest'
17
+ require_relative 'commands/up'
7
18
 
8
19
  class SugarJar
9
20
  # This is the workhorse of SugarJar. Short of #initialize, all other public
10
21
  # methods are "commands". Anything in private is internal implementation
11
22
  # details.
12
23
  class Commands
13
- include SugarJar::Util
14
-
15
24
  MAIN_BRANCHES = %w{master main}.freeze
16
25
 
17
26
  def initialize(options)
18
27
  SugarJar::Log.debug("Commands.initialize options: #{options}")
19
- @ghuser = options['github_user']
20
- @ghhost = options['github_host']
21
28
  @ignore_dirty = options['ignore_dirty']
22
29
  @ignore_prerun_failure = options['ignore_prerun_failure']
23
30
  @repo_config = SugarJar::RepoConfig.config
@@ -29,998 +36,19 @@ class SugarJar
29
36
  @checks = {}
30
37
  @main_branch = nil
31
38
  @main_remote_branches = {}
32
- return if options['no_change']
33
-
34
- # technically this doesn't "change" things, but we won't have this
35
- # option on the no_change call
36
- @cli = determine_cli(options['github_cli'])
37
-
38
- set_hub_host
39
- set_commit_template if @repo_config['commit_template']
40
- end
41
-
42
- def feature(name, base = nil)
43
- assert_in_repo
44
- SugarJar::Log.debug("Feature: #{name}, #{base}")
45
- name = fprefix(name)
46
- die("#{name} already exists!") if all_local_branches.include?(name)
47
- base ||= most_main
48
- # If our base is a local branch, don't try to parse it for a remote name
49
- unless all_local_branches.include?(base)
50
- base_pieces = base.split('/')
51
- git('fetch', base_pieces[0]) if base_pieces.length > 1
52
- end
53
- git('checkout', '-b', name, base)
54
- git('branch', '-u', base)
55
- SugarJar::Log.info(
56
- "Created feature branch #{color(name, :green)} based on " +
57
- color(base, :green),
58
- )
59
- end
60
- alias f feature
61
-
62
- def subfeature(name)
63
- assert_in_repo
64
- SugarJar::Log.debug("Subfature: #{name}")
65
- feature(name, current_branch)
66
- end
67
- alias sf subfeature
68
-
69
- def bclean(name = nil)
70
- assert_in_repo
71
- name ||= current_branch
72
- name = fprefix(name)
73
- if clean_branch(name)
74
- SugarJar::Log.info("#{name}: #{color('reaped', :green)}")
75
- else
76
- die(
77
- "#{color("Cannot clean #{name}", :red)}! there are unmerged " +
78
- "commits; use 'git branch -D #{name}' to forcefully delete it.",
79
- )
80
- end
81
- end
82
-
83
- def bcleanall
84
- assert_in_repo
85
- curr = current_branch
86
- all_local_branches.each do |branch|
87
- if MAIN_BRANCHES.include?(branch)
88
- SugarJar::Log.debug("Skipping #{branch}")
89
- next
90
- end
91
-
92
- if clean_branch(branch)
93
- SugarJar::Log.info("#{branch}: #{color('reaped', :green)}")
94
- else
95
- SugarJar::Log.info("#{branch}: skipped")
96
- SugarJar::Log.debug(
97
- "There are unmerged commits; use 'git branch -D #{branch}' to " +
98
- 'forcefully delete it)',
99
- )
100
- end
101
- end
102
-
103
- # Return to the branch we were on, or main
104
- if all_local_branches.include?(curr)
105
- git('checkout', curr)
106
- else
107
- checkout_main_branch
108
- end
109
- end
110
-
111
- def co(*args)
112
- assert_in_repo
113
- # Pop the last arguement, which is _probably_ a branch name
114
- # and then add any featureprefix, and if _that_ is a branch
115
- # name, replace the last arguement with that
116
- name = args.last
117
- bname = fprefix(name)
118
- if all_local_branches.include?(bname)
119
- SugarJar::Log.debug("Featurepefixing #{name} -> #{bname}")
120
- args[-1] = bname
121
- end
122
- s = git('checkout', *args)
123
- SugarJar::Log.info(s.stderr + s.stdout.chomp)
124
- end
125
-
126
- def br
127
- assert_in_repo
128
- SugarJar::Log.info(git('branch', '-v').stdout.chomp)
129
- end
130
-
131
- def binfo
132
- assert_in_repo
133
- SugarJar::Log.info(git(
134
- 'log', '--graph', '--oneline', '--decorate', '--boundary',
135
- "#{tracked_branch}.."
136
- ).stdout.chomp)
137
- end
138
-
139
- # binfo for all branches
140
- def smartlog
141
- assert_in_repo
142
- SugarJar::Log.info(git(
143
- 'log', '--graph', '--oneline', '--decorate', '--boundary',
144
- '--branches', "#{most_main}.."
145
- ).stdout.chomp)
146
- end
147
-
148
- alias sl smartlog
149
-
150
- def up(branch = nil)
151
- assert_in_repo
152
- branch ||= current_branch
153
- branch = fprefix(branch)
154
- # get a copy of our current branch, if rebase fails, we won't
155
- # be able to determine it without backing out
156
- curr = current_branch
157
- git('checkout', branch)
158
- result = gitup
159
- if result['so'].error?
160
- backout = ''
161
- if rebase_in_progress?
162
- backout = ' You can get out of this with a `git rebase --abort`.'
163
- end
164
-
165
- die(
166
- "#{color(curr, :red)}: Failed to rebase on " +
167
- "#{result['base']}. Leaving the repo as-is.#{backout} " +
168
- 'Output from failed rebase is: ' +
169
- "\nSTDOUT:\n#{result['so'].stdout.lines.map { |x| "\t#{x}" }.join}" +
170
- "\nSTDERR:\n#{result['so'].stderr.lines.map { |x| "\t#{x}" }.join}",
171
- )
172
- else
173
- SugarJar::Log.info(
174
- "#{color(current_branch, :green)} rebased on #{result['base']}",
175
- )
176
- # go back to where we were if we rebased a different branch
177
- git('checkout', curr) if branch != curr
178
- end
179
- end
180
-
181
- def amend(*args)
182
- assert_in_repo
183
- # This cannot use shellout since we need a full terminal for the editor
184
- exit(system(which('git'), 'commit', '--amend', *args))
185
- end
186
-
187
- def qamend(*args)
188
- assert_in_repo
189
- SugarJar::Log.info(git('commit', '--amend', '--no-edit', *args).stdout)
190
- end
191
-
192
- alias amendq qamend
193
-
194
- def upall
195
- assert_in_repo
196
- all_local_branches.each do |branch|
197
- next if MAIN_BRANCHES.include?(branch)
198
-
199
- git('checkout', branch)
200
- result = gitup
201
- if result['so'].error?
202
- SugarJar::Log.error(
203
- "#{color(branch, :red)} failed rebase. Reverting attempt and " +
204
- 'moving to next branch. Try `sj up` manually on that branch.',
205
- )
206
- git('rebase', '--abort') if rebase_in_progress?
207
- else
208
- SugarJar::Log.info(
209
- "#{color(branch, :green)} rebased on " +
210
- color(result['base'], :green).to_s,
211
- )
212
- end
213
- end
214
- end
215
-
216
- def smartclone(repo, dir = nil, *args)
217
- # If the user has specified a hub host, set the environment variable
218
- # since we don't have a repo to configure yet
219
- ENV['GITHUB_HOST'] = @ghhost if @ghhost
220
-
221
- reponame = File.basename(repo, '.git')
222
- dir ||= reponame
223
- org = extract_org(repo)
224
-
225
- SugarJar::Log.info("Cloning #{reponame}...")
226
-
227
- # GH's 'fork' command (with the --clone arg) will fork, if necessary,
228
- # then clone, and then setup the remotes with the appropriate names. So
229
- # we just let it do all the work for us and return.
230
- #
231
- # Unless the repo is in our own org and cannot be forked, then it
232
- # will fail.
233
- if gh? && org != @ghuser
234
- ghcli('repo', 'fork', '--clone', canonicalize_repo(repo), dir, *args)
235
- SugarJar::Log.info('Remotes "origin" and "upstream" configured.')
236
- return
237
- end
238
-
239
- # For 'hub' first we clone, using git, as 'hub' always needs a repo to
240
- # operate on.
241
- #
242
- # Or for 'gh' when we can't fork...
243
- git('clone', canonicalize_repo(repo), dir, *args)
244
-
245
- # Then we go into it and attempt to use the 'fork' capability
246
- # or if not
247
- Dir.chdir dir do
248
- # Now that we have a repo, if we have a hub host set it.
249
- set_hub_host
250
-
251
- SugarJar::Log.debug("Comparing org #{org} to ghuser #{@ghuser}")
252
- if org == @ghuser
253
- puts 'Cloned forked or self-owned repo. Not creating "upstream".'
254
- SugarJar::Log.info('Remotes "origin" and "upstream" configured.')
255
- return
256
- end
257
-
258
- s = ghcli_nofail('repo', 'fork', '--remote-name=origin')
259
- if s.error?
260
- if s.stdout.include?('SAML enforcement')
261
- SugarJar::Log.info(
262
- 'Forking the repo failed because the repo requires SAML ' +
263
- "authentication. Full output:\n\n\t#{s.stdout}",
264
- )
265
- exit(1)
266
- else
267
- # gh as well as old versions of hub, it would fail if the upstream
268
- # fork already existed. If we got an error, but didn't recognize
269
- # that, we'll assume that's what happened and try to add the remote
270
- # ourselves.
271
- SugarJar::Log.info("Fork (#{@ghuser}/#{reponame}) detected.")
272
- SugarJar::Log.debug(
273
- 'The above is a bit of a lie. "hub" failed to fork and it was ' +
274
- 'not a SAML error, so our best guess is that a fork exists ' +
275
- 'and so we will try to configure it.',
276
- )
277
- git('remote', 'rename', 'origin', 'upstream')
278
- git('remote', 'add', 'origin', forked_repo(repo, @ghuser))
279
- end
280
- else
281
- SugarJar::Log.info("Forked #{reponame} to #{@ghuser}")
282
- end
283
- SugarJar::Log.info('Remotes "origin" and "upstream" configured.')
284
- end
285
- end
286
-
287
- alias sclone smartclone
288
-
289
- def lint
290
- assert_in_repo
291
- exit(1) unless run_check('lint')
292
- end
293
-
294
- def unit
295
- assert_in_repo
296
- exit(1) unless run_check('unit')
297
- end
298
-
299
- def smartpush(remote = nil, branch = nil)
300
- assert_in_repo
301
- _smartpush(remote, branch, false)
302
- end
303
-
304
- alias spush smartpush
305
-
306
- def forcepush(remote = nil, branch = nil)
307
- assert_in_repo
308
- _smartpush(remote, branch, true)
309
- end
310
-
311
- alias fpush forcepush
312
-
313
- def version
314
- puts "sugarjar version #{SugarJar::VERSION}"
315
- puts ghcli('version').stdout
316
- # 'hub' prints the 'git' version, but gh doesn't, so if we're on 'gh'
317
- # print out the git version directly
318
- puts git('version').stdout if gh?
319
- end
320
-
321
- def smartpullrequest(*args)
322
- assert_in_repo
323
- assert_common_main_branch
324
-
325
- if dirty?
326
- SugarJar::Log.warn(
327
- 'Your repo is dirty, so I am not going to create a pull request. ' +
328
- 'You should commit or amend and push it to your remote first.',
329
- )
330
- exit(1)
331
- end
332
-
333
- if gh?
334
- curr = current_branch
335
- base = tracked_branch
336
- if @pr_autofill
337
- SugarJar::Log.info('Autofilling in PR from commit message')
338
- num_commits = git(
339
- 'rev-list', '--count', curr, "^#{base}"
340
- ).stdout.strip.to_i
341
- if num_commits > 1
342
- args.unshift('--fill-first')
343
- else
344
- args.unshift('--fill')
345
- end
346
- end
347
- if subfeature?(base)
348
- if upstream != push_org
349
- SugarJar::Log.warn(
350
- 'Unfortunately you cannot based one PR on another PR when' +
351
- " using fork-based PRs. We will base this on #{most_main}." +
352
- ' This just means the PR "Changes" tab will show changes for' +
353
- ' the full stack until those other PRs are merged and this PR' +
354
- ' PR is rebased.',
355
- )
356
- # nil is prompt, true is always, false is never
357
- elsif @pr_autostack.nil?
358
- $stdout.print(
359
- 'It looks like this is a subfeature, would you like to base ' +
360
- "this PR on #{base}? [y/n] ",
361
- )
362
- ans = $stdin.gets.strip
363
- args.unshift('--base', base) if %w{Y y}.include?(ans)
364
- elsif @pr_autostack
365
- args.unshift('--base', base)
366
- end
367
- end
368
- # <org>:<branch> is the GH API syntax for:
369
- # look for a branch of name <branch>, from a fork in owner <org>
370
- args.unshift('--head', "#{push_org}:#{curr}")
371
- SugarJar::Log.trace("Running: gh pr create #{args.join(' ')}")
372
- system(which('gh'), 'pr', 'create', *args)
373
- else
374
- SugarJar::Log.trace("Running: hub pull-request #{args.join(' ')}")
375
- system(which('hub'), 'pull-request', *args)
376
- end
377
- end
378
-
379
- alias spr smartpullrequest
380
- alias smartpr smartpullrequest
381
-
382
- def pullsuggestions
383
- assert_in_repo
384
-
385
- if dirty?
386
- if @ignore_dirty
387
- SugarJar::Log.warn(
388
- 'Your repo is dirty, but --ignore-dirty was specified, so ' +
389
- 'carrying on anyway.',
390
- )
391
- else
392
- SugarJar::Log.error(
393
- 'Your repo is dirty, so I am not going to push. Please commit ' +
394
- 'or amend first.',
395
- )
396
- exit(1)
397
- end
398
- end
399
-
400
- src = "origin/#{current_branch}"
401
- fetch('origin')
402
- diff = git('diff', "..#{src}").stdout
403
- return unless diff && !diff.empty?
404
-
405
- puts "Will merge the following suggestions:\n\n#{diff}"
406
-
407
- loop do
408
- $stdout.print("\nAre you sure? [y/n] ")
409
- ans = $stdin.gets.strip
410
- case ans
411
- when /^[Yy]$/
412
- system(which('git'), 'merge', '--ff', "origin/#{current_branch}")
413
- break
414
- when /^[Nn]$/, /^[Qq](uit)?/
415
- puts 'Not merging at user request...'
416
- break
417
- else
418
- puts "Didn't understand '#{ans}'."
419
- end
420
- end
421
- end
422
-
423
- alias ps pullsuggestions
424
-
425
- private
426
-
427
- def fprefix(name)
428
- return name unless @feature_prefix
429
-
430
- return name if name.start_with?(@feature_prefix)
431
- return name if all_local_branches.include?(name)
432
-
433
- newname = "#{@feature_prefix}#{name}"
434
- SugarJar::Log.debug(
435
- "Munging feature name: #{name} -> #{newname} due to feature prefix",
436
- )
437
- newname
438
- end
439
-
440
- def _smartpush(remote, branch, force)
441
- unless remote && branch
442
- remote ||= 'origin'
443
- branch ||= current_branch
444
- end
445
-
446
- if dirty?
447
- if @ignore_dirty
448
- SugarJar::Log.warn(
449
- 'Your repo is dirty, but --ignore-dirty was specified, so ' +
450
- 'carrying on anyway.',
451
- )
452
- else
453
- SugarJar::Log.error(
454
- 'Your repo is dirty, so I am not going to push. Please commit ' +
455
- 'or amend first.',
456
- )
457
- exit(1)
458
- end
459
- end
460
-
461
- unless run_prepush
462
- if @ignore_prerun_failure
463
- SugarJar::Log.warn(
464
- 'Pre-push checks failed, but --ignore-prerun-failure was ' +
465
- 'specified, so carrying on anyway',
466
- )
467
- else
468
- SugarJar::Log.error('Pre-push checks failed. Not pushing.')
469
- exit(1)
470
- end
471
- end
472
-
473
- args = ['push', remote, branch]
474
- args << '--force-with-lease' if force
475
- puts git(*args).stderr
476
- end
477
-
478
- def dirty?
479
- s = git_nofail('diff', '--quiet')
480
- s.error?
481
- end
482
-
483
- def extract_org(repo)
484
- if repo.start_with?('http')
485
- File.basename(File.dirname(repo))
486
- elsif repo.start_with?('git@')
487
- repo.split(':')[1].split('/')[0]
488
- else
489
- # assume they passed in a hub-friendly name
490
- repo.split('/').first
491
- end
492
- end
493
-
494
- def extract_repo(repo)
495
- File.basename(repo, '.git')
496
- end
497
-
498
- def forked_repo(repo, username)
499
- repo = if repo.start_with?('http', 'git@')
500
- File.basename(repo)
501
- else
502
- "#{File.basename(repo)}.git"
503
- end
504
- "git@#{@ghhost || 'github.com'}:#{username}/#{repo}"
505
- end
506
-
507
- # Hub will default to https, but we should always default to SSH
508
- # unless otherwise specified since https will cause prompting.
509
- def canonicalize_repo(repo)
510
- # if they fully-qualified it, we're good
511
- return repo if repo.start_with?('http', 'git@')
512
-
513
- # otherwise, ti's a shortname
514
- cr = "git@#{@ghhost || 'github.com'}:#{repo}.git"
515
- SugarJar::Log.debug("canonicalized #{repo} to #{cr}")
516
- cr
517
- end
518
-
519
- def set_hub_host
520
- return unless hub? && in_repo && @ghhost
39
+ @ghuser = @repo_config['github_user'] || options['github_user']
40
+ @ghhost = @repo_config['github_host'] || options['github_host']
521
41
 
522
- s = git_nofail('config', '--local', '--get', 'hub.host')
523
- if s.error?
524
- SugarJar::Log.info("Setting repo hub.host = #{@ghhost}")
525
- else
526
- current = s.stdout
527
- if current == @ghhost
528
- SugarJar::Log.debug('Repo hub.host already set correctly')
529
- else
530
- # Even though we have an explicit config, in most cases, it
531
- # comes from a global or user config, but the config in the
532
- # local repo we likely set. So we'd just constantly revert that.
533
- SugarJar::Log.debug(
534
- "Not overwriting repo hub.host. Already set to #{current}. " +
535
- "To change it, run `git config --local --add hub.host #{@ghhost}`",
536
- )
537
- end
538
- return
539
- end
540
- git('config', '--local', '--add', 'hub.host', @ghhost)
541
- end
542
-
543
- def set_commit_template
544
- unless in_repo
545
- SugarJar::Log.debug('Skipping set_commit_template: not in repo')
546
- return
547
- end
548
-
549
- realpath = if @repo_config['commit_template'].start_with?('/')
550
- @repo_config['commit_template']
551
- else
552
- "#{repo_root}/#{@repo_config['commit_template']}"
553
- end
554
- unless File.exist?(realpath)
555
- die(
556
- "Repo config specifies #{@repo_config['commit_template']} as the " +
557
- 'commit template, but that file does not exist.',
558
- )
559
- end
560
-
561
- s = git_nofail('config', '--local', 'commit.template')
562
- unless s.error?
563
- current = s.stdout.strip
564
- if current == @repo_config['commit_template']
565
- SugarJar::Log.debug('Commit template already set correctly')
566
- return
567
- else
568
- SugarJar::Log.warn(
569
- "Updating repo-specific commit template from #{current} " +
570
- "to #{@repo_config['commit_template']}",
571
- )
572
- end
573
- end
574
-
575
- SugarJar::Log.debug(
576
- 'Setting repo-specific commit template to ' +
577
- "#{@repo_config['commit_template']} per sugarjar repo config.",
578
- )
579
- git(
580
- 'config', '--local', 'commit.template', @repo_config['commit_template']
581
- )
582
- end
583
-
584
- def get_checks_from_command(type)
585
- return nil unless @repo_config["#{type}_list_cmd"]
586
-
587
- cmd = @repo_config["#{type}_list_cmd"]
588
- short = cmd.split.first
589
- unless File.exist?(short)
590
- SugarJar::Log.error(
591
- "Configured #{type}_list_cmd #{short} does not exist!",
592
- )
593
- return false
594
- end
595
- s = Mixlib::ShellOut.new(cmd).run_command
596
- if s.error?
597
- SugarJar::Log.error(
598
- "#{type}_list_cmd (#{cmd}) failed: #{s.format_for_exception}",
599
- )
600
- return false
601
- end
602
- s.stdout.split("\n")
603
- end
604
-
605
- # determine if we're using the _list_cmd and if so run it to get the
606
- # checks, or just use the directly-defined check, and cache it
607
- def get_checks(type)
608
- return @checks[type] if @checks[type]
609
-
610
- ret = get_checks_from_command(type)
611
- if ret
612
- SugarJar::Log.debug("Found #{type}s: #{ret}")
613
- @checks[type] = ret
614
- # if it's explicitly false, we failed to run the command
615
- elsif ret == false
616
- @checks[type] = false
617
- # otherwise, we move on (basically: it's nil, there was no _list_cmd)
618
- else
619
- SugarJar::Log.debug("[#{type}]: using listed linters: #{ret}")
620
- @checks[type] = @repo_config[type] || []
621
- end
622
- @checks[type]
623
- end
624
-
625
- def run_check(type)
626
- Dir.chdir repo_root do
627
- checks = get_checks(type)
628
- # if we failed to determine the checks, the the checks have effectively
629
- # failed
630
- return false unless checks
631
-
632
- checks.each do |check|
633
- SugarJar::Log.debug("Running #{type} #{check}")
634
-
635
- short = check.split.first
636
- if short.include?('/')
637
- short = File.join(repo_root, short) unless short.start_with?('/')
638
- unless File.exist?(short)
639
- SugarJar::Log.error("Configured #{type} #{short} does not exist!")
640
- end
641
- elsif !which_nofail(short)
642
- SugarJar::Log.error("Configured #{type} #{short} does not exist!")
643
- return false
644
- end
645
- s = Mixlib::ShellOut.new(check).run_command
646
-
647
- # Linters auto-correct, lets handle that gracefully
648
- if type == 'lint' && dirty?
649
- SugarJar::Log.info(
650
- "[#{type}] #{short}: #{color('Corrected', :yellow)}",
651
- )
652
- SugarJar::Log.warn(
653
- "The linter modified the repo. Here's the diff:\n",
654
- )
655
- puts git('diff').stdout
656
- loop do
657
- $stdout.print(
658
- "\nWould you like to\n\t[q]uit and inspect\n\t[a]mend the " +
659
- "changes to the current commit and re-run\n > ",
660
- )
661
- ans = $stdin.gets.strip
662
- case ans
663
- when /^q/
664
- SugarJar::Log.info('Exiting at user request.')
665
- exit(1)
666
- when /^a/
667
- qamend('-a')
668
- # break here, if we get out of this loop we 'redo', assuming
669
- # the user chose this option
670
- break
671
- end
672
- end
673
- redo
674
- end
675
-
676
- if s.error?
677
- SugarJar::Log.info(
678
- "[#{type}] #{short} #{color('failed', :red)}, output follows " +
679
- "(see debug for more)\n#{s.stdout}",
680
- )
681
- SugarJar::Log.debug(s.format_for_exception)
682
- return false
683
- end
684
- SugarJar::Log.info(
685
- "[#{type}] #{short}: #{color('OK', :green)}",
686
- )
687
- end
688
- end
689
- end
690
-
691
- def run_prepush
692
- @repo_config['on_push']&.each do |item|
693
- SugarJar::Log.debug("Running on_push check type #{item}")
694
- unless send(:run_check, item)
695
- SugarJar::Log.info("[prepush]: #{item} #{color('failed', :red)}.")
696
- return false
697
- end
698
- end
699
- true
700
- end
701
-
702
- def die(msg)
703
- SugarJar::Log.fatal(msg)
704
- exit(1)
705
- end
706
-
707
- def assert_common_main_branch
708
- upstream_branch = main_remote_branch(upstream)
709
- unless main_branch == upstream_branch
710
- die(
711
- "The local main branch is '#{main_branch}', but the main branch " +
712
- "of the #{upstream} remote is '#{upstream_branch}'. You probably " +
713
- "want to rename your local branch by doing:\n\t" +
714
- "git branch -m #{main_branch} #{upstream_branch}\n\t" +
715
- "git fetch #{upstream}\n\t" +
716
- "git branch -u #{upstream}/#{upstream_branch} #{upstream_branch}\n" +
717
- "\tgit remote set-head #{upstream} -a",
718
- )
719
- end
720
- return if upstream_branch == 'origin'
721
-
722
- origin_branch = main_remote_branch('origin')
723
- return if origin_branch == upstream_branch
724
-
725
- die(
726
- "The main branch of your upstream (#{upstream_branch}) and your " +
727
- "fork/origin (#{origin_branch}) are not the same. You should go " +
728
- "to https://#{@ghhost || 'github.com'}/#{@ghuser}/#{repo_name}/" +
729
- 'branches/ and rename the \'default\' branch to ' +
730
- "'#{upstream_branch}'. It will then give you some commands to " +
731
- 'run to update this clone.',
732
- )
733
- end
734
-
735
- def assert_in_repo
736
- die('sugarjar must be run from inside a git repo') unless in_repo
737
- end
738
-
739
- def determine_main_branch(branches)
740
- branches.include?('main') ? 'main' : 'master'
741
- end
742
-
743
- def main_branch
744
- @main_branch = determine_main_branch(all_local_branches)
745
- end
746
-
747
- def main_remote_branch(remote)
748
- @main_remote_branches[remote] ||=
749
- determine_main_branch(all_remote_branches(remote))
750
- end
751
-
752
- def checkout_main_branch
753
- git('checkout', main_branch)
754
- end
755
-
756
- def clean_branch(name)
757
- die("Cannot remove #{name} branch") if MAIN_BRANCHES.include?(name)
758
- SugarJar::Log.debug('Fetch relevant remote...')
759
- fetch_upstream
760
- return false unless safe_to_clean(name)
761
-
762
- SugarJar::Log.debug('branch deemed safe to delete...')
763
- checkout_main_branch
764
- git('branch', '-D', name)
765
- gitup
766
- true
767
- end
768
-
769
- def all_remote_branches(remote = 'origin')
770
- branches = []
771
- git('branch', '-r', '--format', '%(refname)').stdout.lines.each do |line|
772
- next unless line.start_with?("refs/remotes/#{remote}/")
773
-
774
- branches << branch_from_ref(line.strip, :remote)
775
- end
776
- branches
777
- end
778
-
779
- def all_local_branches
780
- git(
781
- 'branch', '--format', '%(refname)'
782
- ).stdout.lines.map do |line|
783
- branch_from_ref(line.strip)
784
- end
785
- end
786
-
787
- def all_remotes
788
- git('remote').stdout.lines.map(&:strip)
789
- end
790
-
791
- def safe_to_clean(branch)
792
- # cherry -v will output 1 line per commit on the target branch
793
- # prefixed by a - or + - anything with a - can be dropped, anything
794
- # else cannot.
795
- out = git(
796
- 'cherry', '-v', tracked_branch, branch
797
- ).stdout.lines.reject do |line|
798
- line.start_with?('-')
799
- end
800
- if out.empty?
801
- SugarJar::Log.debug(
802
- "cherry-pick shows branch #{branch} obviously safe to delete",
803
- )
804
- return true
805
- end
42
+ die("No 'gh' found, please install 'gh'") unless gh_avail?
806
43
 
807
- # if the "easy" check didn't work, it's probably because there
808
- # was a squash-merge. To check for that we make our own squash
809
- # merge to upstream/main and see if that has any delta
44
+ # Tell the 'gh' cli where to talk to, if not github.com
45
+ ENV['GH_HOST'] = @ghhost if @ghhost
810
46
 
811
- # First we need a temp branch to work on
812
- tmpbranch = "_sugar_jar.#{Process.pid}"
813
-
814
- git('checkout', '-b', tmpbranch, tracked_branch)
815
- s = git_nofail('merge', '--squash', branch)
816
- if s.error?
817
- cleanup_tmp_branch(tmpbranch, branch)
818
- SugarJar::Log.debug(
819
- 'Failed to merge changes into current main. This means we could ' +
820
- 'not figure out if this is merged or not. Check manually and use ' +
821
- "'git branch -D #{branch}' if it is safe to do so.",
822
- )
823
- return false
824
- end
825
-
826
- s = git('diff', '--staged')
827
- out = s.stdout
828
- SugarJar::Log.debug("Squash-merged diff: #{out}")
829
- cleanup_tmp_branch(tmpbranch, branch)
830
- if out.empty?
831
- SugarJar::Log.debug(
832
- 'After squash-merging, this branch appears safe to delete',
833
- )
834
- true
835
- else
836
- SugarJar::Log.debug(
837
- 'After squash-merging, this branch is NOT fully merged to main',
838
- )
839
- false
840
- end
841
- end
842
-
843
- def cleanup_tmp_branch(tmp, backto)
844
- git('reset', '--hard', tracked_branch)
845
- git('checkout', backto)
846
- git('branch', '-D', tmp)
847
- end
848
-
849
- def current_branch
850
- branch_from_ref(git('symbolic-ref', 'HEAD').stdout.strip)
851
- end
852
-
853
- def fetch_upstream
854
- us = upstream
855
- fetch(us) if us
856
- end
857
-
858
- def fetch(remote)
859
- git('fetch', remote)
860
- end
861
-
862
- def gitup
863
- SugarJar::Log.debug('Fetching upstream')
864
- fetch_upstream
865
- curr = current_branch
866
- # this isn't a hash, it's a named param, silly rubocop
867
- # rubocop:disable Style/HashSyntax
868
- base = tracked_branch(fallback: false)
869
- # rubocop:enable Style/HashSyntax
870
- unless base
871
- SugarJar::Log.info(
872
- 'The brach we were tracking is gone, resetting tracking to ' +
873
- most_main,
874
- )
875
- git('branch', '-u', most_main)
876
- base = most_main
877
- end
878
- # If this is a subfeature based on a local branch which has since
879
- # been deleted, 'tracked branch' will automatically return <most_main>
880
- # so we don't need any special handling for that
881
- if !MAIN_BRANCHES.include?(curr) && base == "origin/#{curr}"
882
- SugarJar::Log.warn(
883
- "This branch is tracking origin/#{curr}, which is probably your " +
884
- 'downstream (where you push _to_) as opposed to your upstream ' +
885
- '(where you pull _from_). This means that "sj up" is probably ' +
886
- 'rebasing on the wrong thing and doing nothing. You probably want ' +
887
- "to do a 'git branch -u #{most_main}'.",
888
- )
889
- end
890
- SugarJar::Log.debug('Rebasing')
891
- s = git_nofail('rebase', base)
892
- {
893
- 'so' => s,
894
- 'base' => base,
895
- }
896
- end
897
-
898
- # determine if this branch is based on another local branch (i.e. is a
899
- # subfeature). Used to figure out of we should stack the PR
900
- def subfeature?(base)
901
- all_local_branches.reject { |x| x == most_main }.include?(base)
902
- end
903
-
904
- def rebase_in_progress?
905
- # for rebase without -i
906
- rebase_file = git('rev-parse', '--git-path', 'rebase-apply').stdout.strip
907
- # for rebase -i
908
- rebase_merge_file = git('rev-parse', '--git-path', 'rebase-merge').
909
- stdout.strip
910
- File.exist?(rebase_file) || File.exist?(rebase_merge_file)
911
- end
912
-
913
- def tracked_branch(fallback: true)
914
- branch = nil
915
- s = git_nofail(
916
- 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'
917
- )
918
- if s.error?
919
- branch = fallback ? most_main : nil
920
- SugarJar::Log.debug("No specific tracked branch, using #{branch}")
921
- else
922
- branch = s.stdout.strip
923
- SugarJar::Log.debug(
924
- "Using explicit tracked branch: #{branch}, use " +
925
- '`git branch -u` to change',
926
- )
927
- end
928
- branch
929
- end
930
-
931
- def most_main
932
- us = upstream
933
- if us
934
- "#{us}/#{main_branch}"
935
- else
936
- main_branch
937
- end
938
- end
939
-
940
- def upstream
941
- return @remote if @remote
942
-
943
- remotes = all_remotes
944
- SugarJar::Log.debug("remotes is #{remotes}")
945
- if remotes.empty?
946
- @remote = nil
947
- elsif remotes.length == 1
948
- @remote = remotes[0]
949
- elsif remotes.include?('upstream')
950
- @remote = 'upstream'
951
- elsif remotes.include?('origin')
952
- @remote = 'origin'
953
- else
954
- raise 'Could not determine "upstream" remote to use...'
955
- end
956
- @remote
957
- end
958
-
959
- # Whatever org we push to, regardless of if this is a fork or not
960
- def push_org
961
- url = git('remote', 'get-url', 'origin').stdout.strip
962
- extract_org(url)
963
- end
964
-
965
- def branch_from_ref(ref, type = :local)
966
- # local branches are refs/head/XXXX
967
- # remote branches are refs/remotes/<remote>/XXXX
968
- base = type == :local ? 2 : 3
969
- ref.split('/')[base..].join('/')
970
- end
971
-
972
- def color(string, *colors)
973
- if @color
974
- pastel.decorate(string, *colors)
975
- else
976
- string
977
- end
978
- end
979
-
980
- def pastel
981
- @pastel ||= begin
982
- require 'pastel'
983
- Pastel.new
984
- end
985
- end
986
-
987
- def determine_cli(cli)
988
- return cli if %w{gh hub}.include?(cli)
989
-
990
- die("'github_cli' has unknown setting: #{cli}") unless cli == 'auto'
991
-
992
- SugarJar::Log.debug('github_cli set to auto')
993
-
994
- if which_nofail('gh')
995
- SugarJar::Log.debug('Found "gh"')
996
- return 'gh'
997
- end
998
- if which_nofail('hub')
999
- SugarJar::Log.debug('Did not find "gh" but did find "hub"')
1000
- return 'hub'
1001
- end
1002
-
1003
- die(
1004
- 'Neither "gh" nor "hub" found in PATH, please ensure at least one ' +
1005
- 'of these utilities is in the PATH. If both are available you can ' +
1006
- 'specify which to use with --github-cli',
1007
- )
1008
- end
1009
-
1010
- def hub?
1011
- @cli == 'hub'
1012
- end
1013
-
1014
- def gh?
1015
- @cli == 'gh'
1016
- end
47
+ return if options['no_change']
1017
48
 
1018
- def ghcli_nofail(*args)
1019
- gh? ? gh_nofail(*args) : hub_nofail(*args)
49
+ set_commit_template if @repo_config['commit_template']
1020
50
  end
1021
51
 
1022
- def ghcli(*args)
1023
- gh? ? gh(*args) : hub(*args)
1024
- end
52
+ include SugarJar::Util
1025
53
  end
1026
54
  end