sugarjar 1.1.3 → 2.0.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.
@@ -1,8 +1,19 @@
1
1
  require 'mixlib/shellout'
2
2
 
3
- require_relative 'util'
4
- require_relative 'repoconfig'
3
+ require_relative 'commands/amend'
4
+ require_relative 'commands/bclean'
5
+ require_relative 'commands/branch'
6
+ require_relative 'commands/checks'
7
+ require_relative 'commands/debuginfo'
8
+ require_relative 'commands/feature'
9
+ require_relative 'commands/pullsuggestions'
10
+ require_relative 'commands/push'
11
+ require_relative 'commands/smartclone'
12
+ require_relative 'commands/smartpullrequest'
13
+ require_relative 'commands/up'
5
14
  require_relative 'log'
15
+ require_relative 'repoconfig'
16
+ require_relative 'util'
6
17
  require_relative 'version'
7
18
 
8
19
  class SugarJar
@@ -10,14 +21,10 @@ class SugarJar
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,472 +36,21 @@ 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
39
+ @ghuser = @repo_config['github_user'] || options['github_user']
40
+ @ghhost = @repo_config['github_host'] || options['github_host']
320
41
 
321
- def smartpullrequest(*args)
322
- assert_in_repo
323
- assert_common_main_branch
42
+ die("No 'gh' found, please install 'gh'") unless gh_avail?
324
43
 
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
44
+ # Tell the 'gh' cli where to talk to, if not github.com
45
+ ENV['GH_HOST'] = @ghhost if @ghhost
384
46
 
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}"
47
+ return if options['no_change']
406
48
 
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
49
+ set_commit_template if @repo_config['commit_template']
421
50
  end
422
51
 
423
- alias ps pullsuggestions
424
-
425
52
  private
426
53
 
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
54
  def forked_repo(repo, username)
499
55
  repo = if repo.start_with?('http', 'git@')
500
56
  File.basename(repo)
@@ -504,7 +60,7 @@ class SugarJar
504
60
  "git@#{@ghhost || 'github.com'}:#{username}/#{repo}"
505
61
  end
506
62
 
507
- # Hub will default to https, but we should always default to SSH
63
+ # gh utils will default to https, but we should always default to SSH
508
64
  # unless otherwise specified since https will cause prompting.
509
65
  def canonicalize_repo(repo)
510
66
  # if they fully-qualified it, we're good
@@ -516,32 +72,8 @@ class SugarJar
516
72
  cr
517
73
  end
518
74
 
519
- def set_hub_host
520
- return unless hub? && in_repo && @ghhost
521
-
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
75
  def set_commit_template
544
- unless in_repo
76
+ unless SugarJar::Util.in_repo?
545
77
  SugarJar::Log.debug('Skipping set_commit_template: not in repo')
546
78
  return
547
79
  end
@@ -549,7 +81,7 @@ class SugarJar
549
81
  realpath = if @repo_config['commit_template'].start_with?('/')
550
82
  @repo_config['commit_template']
551
83
  else
552
- "#{repo_root}/#{@repo_config['commit_template']}"
84
+ "#{Util.repo_root}/#{@repo_config['commit_template']}"
553
85
  end
554
86
  unless File.exist?(realpath)
555
87
  die(
@@ -581,159 +113,10 @@ class SugarJar
581
113
  )
582
114
  end
583
115
 
584
- def get_checks_from_command(type)
585
- return nil unless @repo_config["#{type}_list_cmd"]
116
+ def assert_in_repo!
117
+ return if SugarJar::Util.in_repo?
586
118
 
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
119
+ die('sugarjar must be run from inside a git repo')
737
120
  end
738
121
 
739
122
  def determine_main_branch(branches)
@@ -753,19 +136,6 @@ class SugarJar
753
136
  git('checkout', main_branch)
754
137
  end
755
138
 
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
139
  def all_remote_branches(remote = 'origin')
770
140
  branches = []
771
141
  git('branch', '-r', '--format', '%(refname)').stdout.lines.each do |line|
@@ -780,6 +150,8 @@ class SugarJar
780
150
  git(
781
151
  'branch', '--format', '%(refname)'
782
152
  ).stdout.lines.map do |line|
153
+ next if line.start_with?('(HEAD detached')
154
+
783
155
  branch_from_ref(line.strip)
784
156
  end
785
157
  end
@@ -788,62 +160,13 @@ class SugarJar
788
160
  git('remote').stdout.lines.map(&:strip)
789
161
  end
790
162
 
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?('-')
163
+ def remote_url_map
164
+ m = {}
165
+ git('remote', '-v').stdout.each_line do |line|
166
+ name, url, = line.split
167
+ m[name] = url
799
168
  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
806
-
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
810
-
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)
169
+ m
847
170
  end
848
171
 
849
172
  def current_branch
@@ -859,57 +182,12 @@ class SugarJar
859
182
  git('fetch', remote)
860
183
  end
861
184
 
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
185
  # determine if this branch is based on another local branch (i.e. is a
899
186
  # subfeature). Used to figure out of we should stack the PR
900
187
  def subfeature?(base)
901
188
  all_local_branches.reject { |x| x == most_main }.include?(base)
902
189
  end
903
190
 
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
191
  def tracked_branch(fallback: true)
914
192
  branch = nil
915
193
  s = git_nofail(
@@ -956,19 +234,18 @@ class SugarJar
956
234
  @remote
957
235
  end
958
236
 
237
+ def upstream_org
238
+ us = upstream
239
+ remotes = remote_url_map
240
+ extract_org(remotes[us])
241
+ end
242
+
959
243
  # Whatever org we push to, regardless of if this is a fork or not
960
244
  def push_org
961
245
  url = git('remote', 'get-url', 'origin').stdout.strip
962
246
  extract_org(url)
963
247
  end
964
248
 
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
249
  def color(string, *colors)
973
250
  if @color
974
251
  pastel.decorate(string, *colors)
@@ -984,43 +261,73 @@ class SugarJar
984
261
  end
985
262
  end
986
263
 
987
- def determine_cli(cli)
988
- return cli if %w{gh hub}.include?(cli)
264
+ def gh_avail?
265
+ !!SugarJar::Util.which_nofail('gh')
266
+ end
989
267
 
990
- die("'github_cli' has unknown setting: #{cli}") unless cli == 'auto'
268
+ def fprefix(name)
269
+ return name unless @feature_prefix
991
270
 
992
- SugarJar::Log.debug('github_cli set to auto')
271
+ return name if name.start_with?(@feature_prefix)
272
+ return name if all_local_branches.include?(name)
993
273
 
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'
274
+ newname = "#{@feature_prefix}#{name}"
275
+ SugarJar::Log.debug(
276
+ "Munging feature name: #{name} -> #{newname} due to feature prefix",
277
+ )
278
+ newname
279
+ end
280
+
281
+ def dirty?
282
+ s = git_nofail('diff', '--quiet')
283
+ s.error?
284
+ end
285
+
286
+ def repo_name
287
+ SugarJar::Util.repo_root.split('/').last
288
+ end
289
+
290
+ def extract_org(repo)
291
+ if repo.start_with?('http')
292
+ File.basename(File.dirname(repo))
293
+ elsif repo.start_with?('git@')
294
+ repo.split(':')[1].split('/')[0]
295
+ else
296
+ # assume they passed in a ghcli-friendly name
297
+ repo.split('/').first
1001
298
  end
299
+ end
1002
300
 
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
- )
301
+ def extract_repo(repo)
302
+ File.basename(repo, '.git')
303
+ end
304
+
305
+ def die(msg)
306
+ SugarJar::Log.fatal(msg)
307
+ exit(1)
308
+ end
309
+
310
+ def branch_from_ref(ref, type = :local)
311
+ # local branches are refs/head/XXXX
312
+ # remote branches are refs/remotes/<remote>/XXXX
313
+ base = type == :local ? 2 : 3
314
+ ref.split('/')[base..].join('/')
1008
315
  end
1009
316
 
1010
- def hub?
1011
- @cli == 'hub'
317
+ def git(*)
318
+ SugarJar::Util.git(*, :color => @color)
1012
319
  end
1013
320
 
1014
- def gh?
1015
- @cli == 'gh'
321
+ def git_nofail(*)
322
+ SugarJar::Util.git_nofail(*, :color => @color)
1016
323
  end
1017
324
 
1018
- def ghcli_nofail(*args)
1019
- gh? ? gh_nofail(*args) : hub_nofail(*args)
325
+ def ghcli(*)
326
+ SugarJar::Util.ghcli(*)
1020
327
  end
1021
328
 
1022
- def ghcli(*args)
1023
- gh? ? gh(*args) : hub(*args)
329
+ def ghcli_nofail(*)
330
+ SugarJar::Util.ghcli_nofail(*)
1024
331
  end
1025
332
  end
1026
333
  end