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.
- checksums.yaml +4 -4
- data/README.md +164 -104
- data/bin/sj +65 -111
- data/lib/sugarjar/commands.rb +19 -966
- data/lib/sugarjar/config.rb +22 -2
- data/lib/sugarjar/util.rb +298 -66
- data/lib/sugarjar/version.rb +1 -1
- metadata +4 -4
data/lib/sugarjar/commands.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
531
|
-
|
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
|
-
|
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
|
-
|
994
|
-
gh? ? gh_nofail(*args) : hub_nofail(*args)
|
49
|
+
set_commit_template if @repo_config['commit_template']
|
995
50
|
end
|
996
51
|
|
997
|
-
|
998
|
-
gh? ? gh(*args) : hub(*args)
|
999
|
-
end
|
52
|
+
include SugarJar::Util
|
1000
53
|
end
|
1001
54
|
end
|