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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +161 -0
- data/CONTRIBUTING.md +37 -0
- data/README.md +142 -264
- data/bin/sj +62 -108
- data/examples/sample_config.yaml +24 -0
- data/examples/sample_repoconfig.yaml +77 -0
- data/lib/sugarjar/commands/amend.rb +17 -0
- data/lib/sugarjar/commands/bclean.rb +118 -0
- data/lib/sugarjar/commands/branch.rb +42 -0
- data/lib/sugarjar/commands/checks.rb +139 -0
- data/lib/sugarjar/commands/debuginfo.rb +16 -0
- data/lib/sugarjar/commands/feature.rb +35 -0
- data/lib/sugarjar/commands/pullsuggestions.rb +48 -0
- data/lib/sugarjar/commands/push.rb +66 -0
- data/lib/sugarjar/commands/smartclone.rb +30 -0
- data/lib/sugarjar/commands/smartpullrequest.rb +101 -0
- data/lib/sugarjar/commands/up.rb +103 -0
- data/lib/sugarjar/commands.rb +94 -787
- data/lib/sugarjar/config.rb +22 -2
- data/lib/sugarjar/repoconfig.rb +2 -4
- data/lib/sugarjar/util.rb +33 -90
- data/lib/sugarjar/version.rb +1 -1
- data/sugarjar.gemspec +11 -5
- metadata +24 -5
data/lib/sugarjar/commands.rb
CHANGED
@@ -1,8 +1,19 @@
|
|
1
1
|
require 'mixlib/shellout'
|
2
2
|
|
3
|
-
require_relative '
|
4
|
-
require_relative '
|
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
|
-
|
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
|
-
|
322
|
-
assert_in_repo
|
323
|
-
assert_common_main_branch
|
42
|
+
die("No 'gh' found, please install 'gh'") unless gh_avail?
|
324
43
|
|
325
|
-
if
|
326
|
-
|
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
|
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
|
-
|
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
|
-
#
|
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
|
585
|
-
return
|
116
|
+
def assert_in_repo!
|
117
|
+
return if SugarJar::Util.in_repo?
|
586
118
|
|
587
|
-
|
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
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
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
|
-
|
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
|
988
|
-
|
264
|
+
def gh_avail?
|
265
|
+
!!SugarJar::Util.which_nofail('gh')
|
266
|
+
end
|
989
267
|
|
990
|
-
|
268
|
+
def fprefix(name)
|
269
|
+
return name unless @feature_prefix
|
991
270
|
|
992
|
-
|
271
|
+
return name if name.start_with?(@feature_prefix)
|
272
|
+
return name if all_local_branches.include?(name)
|
993
273
|
|
994
|
-
|
995
|
-
|
996
|
-
|
997
|
-
|
998
|
-
|
999
|
-
|
1000
|
-
|
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
|
-
|
1004
|
-
|
1005
|
-
|
1006
|
-
|
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
|
1011
|
-
|
317
|
+
def git(*)
|
318
|
+
SugarJar::Util.git(*, :color => @color)
|
1012
319
|
end
|
1013
320
|
|
1014
|
-
def
|
1015
|
-
|
321
|
+
def git_nofail(*)
|
322
|
+
SugarJar::Util.git_nofail(*, :color => @color)
|
1016
323
|
end
|
1017
324
|
|
1018
|
-
def
|
1019
|
-
|
325
|
+
def ghcli(*)
|
326
|
+
SugarJar::Util.ghcli(*)
|
1020
327
|
end
|
1021
328
|
|
1022
|
-
def
|
1023
|
-
|
329
|
+
def ghcli_nofail(*)
|
330
|
+
SugarJar::Util.ghcli_nofail(*)
|
1024
331
|
end
|
1025
332
|
end
|
1026
333
|
end
|