sugarjar 1.1.2 → 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,973 +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
39
+ @ghuser = @repo_config['github_user'] || options['github_user']
40
+ @ghhost = @repo_config['github_host'] || options['github_host']
286
41
 
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
- num_commits = git(
338
- 'rev-list', '--count', curr, "^#{base}"
339
- ).stdout.strip.to_i
340
- if num_commits > 1
341
- SugarJar::Log.debug(
342
- "Not using --fill because there are #{num_commits} commits",
343
- )
344
- else
345
- SugarJar::Log.info('Autofilling in PR from commit message')
346
- args.unshift('--fill')
347
- end
348
- end
349
- if subfeature?(base)
350
- # nil is prompt, true is always, false is never
351
- if @pr_autostack.nil?
352
- $stdout.print(
353
- 'It looks like this is a subfeature, would you like to base ' +
354
- "this PR on #{base}? [y/n] ",
355
- )
356
- ans = $stdin.gets.strip
357
- args += ['--base', base] if %w{Y y}.include?(ans)
358
- elsif @pr_autostack
359
- args += ['--base', base]
360
- end
361
- end
362
- SugarJar::Log.trace("Running: gh pr create #{args.join(' ')}")
363
- system(which('gh'), 'pr', 'create', *args)
364
- else
365
- SugarJar::Log.trace("Running: hub pull-request #{args.join(' ')}")
366
- system(which('hub'), 'pull-request', *args)
367
- end
368
- end
369
-
370
- alias spr smartpullrequest
371
- alias smartpr smartpullrequest
372
-
373
- def pullsuggestions
374
- assert_in_repo
375
-
376
- if dirty?
377
- if @ignore_dirty
378
- SugarJar::Log.warn(
379
- 'Your repo is dirty, but --ignore-dirty was specified, so ' +
380
- 'carrying on anyway.',
381
- )
382
- else
383
- SugarJar::Log.error(
384
- 'Your repo is dirty, so I am not going to push. Please commit ' +
385
- 'or amend first.',
386
- )
387
- exit(1)
388
- end
389
- end
390
-
391
- src = "origin/#{current_branch}"
392
- fetch('origin')
393
- diff = git('diff', "..#{src}").stdout
394
- return unless diff && !diff.empty?
395
-
396
- puts "Will merge the following suggestions:\n\n#{diff}"
397
-
398
- loop do
399
- $stdout.print("\nAre you sure? [y/n] ")
400
- ans = $stdin.gets.strip
401
- case ans
402
- when /^[Yy]$/
403
- system(which('git'), 'merge', '--ff', "origin/#{current_branch}")
404
- break
405
- when /^[Nn]$/, /^[Qq](uit)?/
406
- puts 'Not merging at user request...'
407
- break
408
- else
409
- puts "Didn't understand '#{ans}'."
410
- end
411
- end
412
- end
413
-
414
- alias ps pullsuggestions
415
-
416
- private
417
-
418
- def fprefix(name)
419
- return name unless @feature_prefix
420
-
421
- return name if name.start_with?(@feature_prefix)
422
- return name if all_local_branches.include?(name)
423
-
424
- newname = "#{@feature_prefix}#{name}"
425
- SugarJar::Log.debug(
426
- "Munging feature name: #{name} -> #{newname} due to feature prefix",
427
- )
428
- newname
429
- end
430
-
431
- def _smartpush(remote, branch, force)
432
- unless remote && branch
433
- remote ||= 'origin'
434
- branch ||= current_branch
435
- end
436
-
437
- if dirty?
438
- if @ignore_dirty
439
- SugarJar::Log.warn(
440
- 'Your repo is dirty, but --ignore-dirty was specified, so ' +
441
- 'carrying on anyway.',
442
- )
443
- else
444
- SugarJar::Log.error(
445
- 'Your repo is dirty, so I am not going to push. Please commit ' +
446
- 'or amend first.',
447
- )
448
- exit(1)
449
- end
450
- end
451
-
452
- unless run_prepush
453
- if @ignore_prerun_failure
454
- SugarJar::Log.warn(
455
- 'Pre-push checks failed, but --ignore-prerun-failure was ' +
456
- 'specified, so carrying on anyway',
457
- )
458
- else
459
- SugarJar::Log.error('Pre-push checks failed. Not pushing.')
460
- exit(1)
461
- end
462
- end
463
-
464
- args = ['push', remote, branch]
465
- args << '--force-with-lease' if force
466
- puts git(*args).stderr
467
- end
468
-
469
- def dirty?
470
- s = git_nofail('diff', '--quiet')
471
- s.error?
472
- end
473
-
474
- def extract_org(repo)
475
- if repo.start_with?('http')
476
- File.basename(File.dirname(repo))
477
- elsif repo.start_with?('git@')
478
- repo.split(':')[1].split('/')[0]
479
- else
480
- # assume they passed in a hub-friendly name
481
- repo.split('/').first
482
- end
483
- end
484
-
485
- def forked_repo(repo, username)
486
- repo = if repo.start_with?('http', 'git@')
487
- File.basename(repo)
488
- else
489
- "#{File.basename(repo)}.git"
490
- end
491
- "git@#{@ghhost || 'github.com'}:#{username}/#{repo}"
492
- end
493
-
494
- # Hub will default to https, but we should always default to SSH
495
- # unless otherwise specified since https will cause prompting.
496
- def canonicalize_repo(repo)
497
- # if they fully-qualified it, we're good
498
- return repo if repo.start_with?('http', 'git@')
499
-
500
- # otherwise, ti's a shortname
501
- cr = "git@#{@ghhost || 'github.com'}:#{repo}.git"
502
- SugarJar::Log.debug("canonicalized #{repo} to #{cr}")
503
- cr
504
- end
505
-
506
- def set_hub_host
507
- return unless hub? && in_repo && @ghhost
508
-
509
- s = git_nofail('config', '--local', '--get', 'hub.host')
510
- if s.error?
511
- SugarJar::Log.info("Setting repo hub.host = #{@ghhost}")
512
- else
513
- current = s.stdout
514
- if current == @ghhost
515
- SugarJar::Log.debug('Repo hub.host already set correctly')
516
- else
517
- # Even though we have an explicit config, in most cases, it
518
- # comes from a global or user config, but the config in the
519
- # local repo we likely set. So we'd just constantly revert that.
520
- SugarJar::Log.debug(
521
- "Not overwriting repo hub.host. Already set to #{current}. " +
522
- "To change it, run `git config --local --add hub.host #{@ghhost}`",
523
- )
524
- end
525
- return
526
- end
527
- git('config', '--local', '--add', 'hub.host', @ghhost)
528
- end
42
+ die("No 'gh' found, please install 'gh'") unless gh_avail?
529
43
 
530
- def set_commit_template
531
- unless in_repo
532
- SugarJar::Log.debug('Skipping set_commit_template: not in repo')
533
- return
534
- end
44
+ # Tell the 'gh' cli where to talk to, if not github.com
45
+ ENV['GH_HOST'] = @ghhost if @ghhost
535
46
 
536
- realpath = if @repo_config['commit_template'].start_with?('/')
537
- @repo_config['commit_template']
538
- else
539
- "#{repo_root}/#{@repo_config['commit_template']}"
540
- end
541
- unless File.exist?(realpath)
542
- die(
543
- "Repo config specifies #{@repo_config['commit_template']} as the " +
544
- 'commit template, but that file does not exist.',
545
- )
546
- end
547
-
548
- s = git_nofail('config', '--local', 'commit.template')
549
- unless s.error?
550
- current = s.stdout.strip
551
- if current == @repo_config['commit_template']
552
- SugarJar::Log.debug('Commit template already set correctly')
553
- return
554
- else
555
- SugarJar::Log.warn(
556
- "Updating repo-specific commit template from #{current} " +
557
- "to #{@repo_config['commit_template']}",
558
- )
559
- end
560
- end
561
-
562
- SugarJar::Log.debug(
563
- 'Setting repo-specific commit template to ' +
564
- "#{@repo_config['commit_template']} per sugarjar repo config.",
565
- )
566
- git(
567
- 'config', '--local', 'commit.template', @repo_config['commit_template']
568
- )
569
- end
570
-
571
- def get_checks_from_command(type)
572
- return nil unless @repo_config["#{type}_list_cmd"]
573
-
574
- cmd = @repo_config["#{type}_list_cmd"]
575
- short = cmd.split.first
576
- unless File.exist?(short)
577
- SugarJar::Log.error(
578
- "Configured #{type}_list_cmd #{short} does not exist!",
579
- )
580
- return false
581
- end
582
- s = Mixlib::ShellOut.new(cmd).run_command
583
- if s.error?
584
- SugarJar::Log.error(
585
- "#{type}_list_cmd (#{cmd}) failed: #{s.format_for_exception}",
586
- )
587
- return false
588
- end
589
- s.stdout.split("\n")
590
- end
591
-
592
- # determine if we're using the _list_cmd and if so run it to get the
593
- # checks, or just use the directly-defined check, and cache it
594
- def get_checks(type)
595
- return @checks[type] if @checks[type]
596
-
597
- ret = get_checks_from_command(type)
598
- if ret
599
- SugarJar::Log.debug("Found #{type}s: #{ret}")
600
- @checks[type] = ret
601
- # if it's explicitly false, we failed to run the command
602
- elsif ret == false
603
- @checks[type] = false
604
- # otherwise, we move on (basically: it's nil, there was no _list_cmd)
605
- else
606
- SugarJar::Log.debug("[#{type}]: using listed linters: #{ret}")
607
- @checks[type] = @repo_config[type] || []
608
- end
609
- @checks[type]
610
- end
611
-
612
- def run_check(type)
613
- Dir.chdir repo_root do
614
- checks = get_checks(type)
615
- # if we failed to determine the checks, the the checks have effectively
616
- # failed
617
- return false unless checks
618
-
619
- checks.each do |check|
620
- SugarJar::Log.debug("Running #{type} #{check}")
621
-
622
- short = check.split.first
623
- if short.include?('/')
624
- short = File.join(repo_root, short) unless short.start_with?('/')
625
- unless File.exist?(short)
626
- SugarJar::Log.error("Configured #{type} #{short} does not exist!")
627
- end
628
- elsif !which_nofail(short)
629
- SugarJar::Log.error("Configured #{type} #{short} does not exist!")
630
- return false
631
- end
632
- s = Mixlib::ShellOut.new(check).run_command
633
-
634
- # Linters auto-correct, lets handle that gracefully
635
- if type == 'lint' && dirty?
636
- SugarJar::Log.info(
637
- "[#{type}] #{short}: #{color('Corrected', :yellow)}",
638
- )
639
- SugarJar::Log.warn(
640
- "The linter modified the repo. Here's the diff:\n",
641
- )
642
- puts git('diff').stdout
643
- loop do
644
- $stdout.print(
645
- "\nWould you like to\n\t[q]uit and inspect\n\t[a]mend the " +
646
- "changes to the current commit and re-run\n > ",
647
- )
648
- ans = $stdin.gets.strip
649
- case ans
650
- when /^q/
651
- SugarJar::Log.info('Exiting at user request.')
652
- exit(1)
653
- when /^a/
654
- qamend('-a')
655
- # break here, if we get out of this loop we 'redo', assuming
656
- # the user chose this option
657
- break
658
- end
659
- end
660
- redo
661
- end
662
-
663
- if s.error?
664
- SugarJar::Log.info(
665
- "[#{type}] #{short} #{color('failed', :red)}, output follows " +
666
- "(see debug for more)\n#{s.stdout}",
667
- )
668
- SugarJar::Log.debug(s.format_for_exception)
669
- return false
670
- end
671
- SugarJar::Log.info(
672
- "[#{type}] #{short}: #{color('OK', :green)}",
673
- )
674
- end
675
- end
676
- end
677
-
678
- def run_prepush
679
- @repo_config['on_push']&.each do |item|
680
- SugarJar::Log.debug("Running on_push check type #{item}")
681
- unless send(:run_check, item)
682
- SugarJar::Log.info("[prepush]: #{item} #{color('failed', :red)}.")
683
- return false
684
- end
685
- end
686
- true
687
- end
688
-
689
- def die(msg)
690
- SugarJar::Log.fatal(msg)
691
- exit(1)
692
- end
693
-
694
- def assert_common_main_branch
695
- upstream_branch = main_remote_branch(upstream)
696
- unless main_branch == upstream_branch
697
- die(
698
- "The local main branch is '#{main_branch}', but the main branch " +
699
- "of the #{upstream} remote is '#{upstream_branch}'. You probably " +
700
- "want to rename your local branch by doing:\n\t" +
701
- "git branch -m #{main_branch} #{upstream_branch}\n\t" +
702
- "git fetch #{upstream}\n\t" +
703
- "git branch -u #{upstream}/#{upstream_branch} #{upstream_branch}\n" +
704
- "\tgit remote set-head #{upstream} -a",
705
- )
706
- end
707
- return if upstream_branch == 'origin'
708
-
709
- origin_branch = main_remote_branch('origin')
710
- return if origin_branch == upstream_branch
711
-
712
- die(
713
- "The main branch of your upstream (#{upstream_branch}) and your " +
714
- "fork/origin (#{origin_branch}) are not the same. You should go " +
715
- "to https://#{@ghhost || 'github.com'}/#{@ghuser}/#{repo_name}/" +
716
- 'branches/ and rename the \'default\' branch to ' +
717
- "'#{upstream_branch}'. It will then give you some commands to " +
718
- 'run to update this clone.',
719
- )
720
- end
721
-
722
- def assert_in_repo
723
- die('sugarjar must be run from inside a git repo') unless in_repo
724
- end
725
-
726
- def determine_main_branch(branches)
727
- branches.include?('main') ? 'main' : 'master'
728
- end
729
-
730
- def main_branch
731
- @main_branch = determine_main_branch(all_local_branches)
732
- end
733
-
734
- def main_remote_branch(remote)
735
- @main_remote_branches[remote] ||=
736
- determine_main_branch(all_remote_branches(remote))
737
- end
738
-
739
- def checkout_main_branch
740
- git('checkout', main_branch)
741
- end
742
-
743
- def clean_branch(name)
744
- die("Cannot remove #{name} branch") if MAIN_BRANCHES.include?(name)
745
- SugarJar::Log.debug('Fetch relevant remote...')
746
- fetch_upstream
747
- return false unless safe_to_clean(name)
748
-
749
- SugarJar::Log.debug('branch deemed safe to delete...')
750
- checkout_main_branch
751
- git('branch', '-D', name)
752
- gitup
753
- true
754
- end
755
-
756
- def all_remote_branches(remote = 'origin')
757
- branches = []
758
- git('branch', '-r', '--format', '%(refname)').stdout.lines.each do |line|
759
- next unless line.start_with?("refs/remotes/#{remote}/")
760
-
761
- branches << branch_from_ref(line.strip, :remote)
762
- end
763
- branches
764
- end
765
-
766
- def all_local_branches
767
- git(
768
- 'branch', '--format', '%(refname)'
769
- ).stdout.lines.map do |line|
770
- branch_from_ref(line.strip)
771
- end
772
- end
773
-
774
- def all_remotes
775
- git('remote').stdout.lines.map(&:strip)
776
- end
777
-
778
- def safe_to_clean(branch)
779
- # cherry -v will output 1 line per commit on the target branch
780
- # prefixed by a - or + - anything with a - can be dropped, anything
781
- # else cannot.
782
- out = git(
783
- 'cherry', '-v', tracked_branch, branch
784
- ).stdout.lines.reject do |line|
785
- line.start_with?('-')
786
- end
787
- if out.empty?
788
- SugarJar::Log.debug(
789
- "cherry-pick shows branch #{branch} obviously safe to delete",
790
- )
791
- return true
792
- end
793
-
794
- # if the "easy" check didn't work, it's probably because there
795
- # was a squash-merge. To check for that we make our own squash
796
- # merge to upstream/main and see if that has any delta
797
-
798
- # First we need a temp branch to work on
799
- tmpbranch = "_sugar_jar.#{Process.pid}"
800
-
801
- git('checkout', '-b', tmpbranch, tracked_branch)
802
- s = git_nofail('merge', '--squash', branch)
803
- if s.error?
804
- cleanup_tmp_branch(tmpbranch, branch)
805
- SugarJar::Log.debug(
806
- 'Failed to merge changes into current main. This means we could ' +
807
- 'not figure out if this is merged or not. Check manually and use ' +
808
- "'git branch -D #{branch}' if it is safe to do so.",
809
- )
810
- return false
811
- end
812
-
813
- s = git('diff', '--staged')
814
- out = s.stdout
815
- SugarJar::Log.debug("Squash-merged diff: #{out}")
816
- cleanup_tmp_branch(tmpbranch, branch)
817
- if out.empty?
818
- SugarJar::Log.debug(
819
- 'After squash-merging, this branch appears safe to delete',
820
- )
821
- true
822
- else
823
- SugarJar::Log.debug(
824
- 'After squash-merging, this branch is NOT fully merged to main',
825
- )
826
- false
827
- end
828
- end
829
-
830
- def cleanup_tmp_branch(tmp, backto)
831
- git('reset', '--hard', tracked_branch)
832
- git('checkout', backto)
833
- git('branch', '-D', tmp)
834
- end
835
-
836
- def current_branch
837
- branch_from_ref(git('symbolic-ref', 'HEAD').stdout.strip)
838
- end
839
-
840
- def fetch_upstream
841
- us = upstream
842
- fetch(us) if us
843
- end
844
-
845
- def fetch(remote)
846
- git('fetch', remote)
847
- end
848
-
849
- def gitup
850
- SugarJar::Log.debug('Fetching upstream')
851
- fetch_upstream
852
- curr = current_branch
853
- # this isn't a hash, it's a named param, silly rubocop
854
- # rubocop:disable Style/HashSyntax
855
- base = tracked_branch(fallback: false)
856
- # rubocop:enable Style/HashSyntax
857
- unless base
858
- SugarJar::Log.info(
859
- 'The brach we were tracking is gone, resetting tracking to ' +
860
- most_main,
861
- )
862
- git('branch', '-u', most_main)
863
- base = most_main
864
- end
865
- # If this is a subfeature based on a local branch which has since
866
- # been deleted, 'tracked branch' will automatically return <most_main>
867
- # so we don't need any special handling for that
868
- if !MAIN_BRANCHES.include?(curr) && base == "origin/#{curr}"
869
- SugarJar::Log.warn(
870
- "This branch is tracking origin/#{curr}, which is probably your " +
871
- 'downstream (where you push _to_) as opposed to your upstream ' +
872
- '(where you pull _from_). This means that "sj up" is probably ' +
873
- 'rebasing on the wrong thing and doing nothing. You probably want ' +
874
- "to do a 'git branch -u #{most_main}'.",
875
- )
876
- end
877
- SugarJar::Log.debug('Rebasing')
878
- s = git_nofail('rebase', base)
879
- {
880
- 'so' => s,
881
- 'base' => base,
882
- }
883
- end
884
-
885
- # determine if this branch is based on another local branch (i.e. is a
886
- # subfeature). Used to figure out of we should stack the PR
887
- def subfeature?(base)
888
- all_local_branches.reject { |x| x == most_main }.include?(base)
889
- end
890
-
891
- def rebase_in_progress?
892
- # for rebase without -i
893
- rebase_file = git('rev-parse', '--git-path', 'rebase-apply').stdout.strip
894
- # for rebase -i
895
- rebase_merge_file = git('rev-parse', '--git-path', 'rebase-merge').
896
- stdout.strip
897
- File.exist?(rebase_file) || File.exist?(rebase_merge_file)
898
- end
899
-
900
- def tracked_branch(fallback: true)
901
- s = git_nofail(
902
- 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'
903
- )
904
- if s.error?
905
- fallback ? most_main : nil
906
-
907
- else
908
- s.stdout.strip
909
- end
910
- end
911
-
912
- def most_main
913
- us = upstream
914
- if us
915
- "#{us}/#{main_branch}"
916
- else
917
- main_branch
918
- end
919
- end
920
-
921
- def upstream
922
- return @remote if @remote
923
-
924
- remotes = all_remotes
925
- SugarJar::Log.debug("remotes is #{remotes}")
926
- if remotes.empty?
927
- @remote = nil
928
- elsif remotes.length == 1
929
- @remote = remotes[0]
930
- elsif remotes.include?('upstream')
931
- @remote = 'upstream'
932
- elsif remotes.include?('origin')
933
- @remote = 'origin'
934
- else
935
- raise 'Could not determine "upstream" remote to use...'
936
- end
937
- @remote
938
- end
939
-
940
- def branch_from_ref(ref, type = :local)
941
- # local branches are refs/head/XXXX
942
- # remote branches are refs/remotes/<remote>/XXXX
943
- base = type == :local ? 2 : 3
944
- ref.split('/')[base..].join('/')
945
- end
946
-
947
- def color(string, *colors)
948
- if @color
949
- pastel.decorate(string, *colors)
950
- else
951
- string
952
- end
953
- end
954
-
955
- def pastel
956
- @pastel ||= begin
957
- require 'pastel'
958
- Pastel.new
959
- end
960
- end
961
-
962
- def determine_cli(cli)
963
- return cli if %w{gh hub}.include?(cli)
964
-
965
- die("'github_cli' has unknown setting: #{cli}") unless cli == 'auto'
966
-
967
- SugarJar::Log.debug('github_cli set to auto')
968
-
969
- if which_nofail('gh')
970
- SugarJar::Log.debug('Found "gh"')
971
- return 'gh'
972
- end
973
- if which_nofail('hub')
974
- SugarJar::Log.debug('Did not find "gh" but did find "hub"')
975
- return 'hub'
976
- end
977
-
978
- die(
979
- 'Neither "gh" nor "hub" found in PATH, please ensure at least one ' +
980
- 'of these utilities is in the PATH. If both are available you can ' +
981
- 'specify which to use with --github-cli',
982
- )
983
- end
984
-
985
- def hub?
986
- @cli == 'hub'
987
- end
988
-
989
- def gh?
990
- @cli == 'gh'
991
- end
47
+ return if options['no_change']
992
48
 
993
- def ghcli_nofail(*args)
994
- gh? ? gh_nofail(*args) : hub_nofail(*args)
49
+ set_commit_template if @repo_config['commit_template']
995
50
  end
996
51
 
997
- def ghcli(*args)
998
- gh? ? gh(*args) : hub(*args)
999
- end
52
+ include SugarJar::Util
1000
53
  end
1001
54
  end