sugarjar 2.0.1 → 3.0.0
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 +28 -0
- data/README.md +151 -55
- data/bin/sj +96 -23
- data/examples/sample_config.yaml +6 -0
- data/examples/sample_repoconfig.yaml +19 -3
- data/lib/sugarjar/commands/bclean.rb +147 -17
- data/lib/sugarjar/commands/checks.rb +27 -8
- data/lib/sugarjar/commands/debuginfo.rb +1 -1
- data/lib/sugarjar/commands/feature.rb +25 -2
- data/lib/sugarjar/commands/pullsuggestions.rb +2 -15
- data/lib/sugarjar/commands/push.rb +4 -15
- data/lib/sugarjar/commands/smartclone.rb +47 -3
- data/lib/sugarjar/commands/smartpullrequest.rb +27 -5
- data/lib/sugarjar/commands/up.rb +57 -4
- data/lib/sugarjar/commands.rb +155 -25
- data/lib/sugarjar/config.rb +32 -4
- data/lib/sugarjar/log.rb +2 -0
- data/lib/sugarjar/util.rb +28 -14
- data/lib/sugarjar/version.rb +1 -1
- metadata +2 -2
data/lib/sugarjar/commands.rb
CHANGED
|
@@ -36,13 +36,27 @@ class SugarJar
|
|
|
36
36
|
@checks = {}
|
|
37
37
|
@main_branch = nil
|
|
38
38
|
@main_remote_branches = {}
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
# This is CONFIGURED host, which may be null, as opposed
|
|
40
|
+
# to the method forge_host which will always return something
|
|
41
|
+
@_forge_host = @repo_config['forge_host'] || options['forge_host']
|
|
42
|
+
@repo_forge = @repo_config['forge_type'] || options['forge_type'] ||
|
|
43
|
+
_determine_forge_type
|
|
44
|
+
|
|
45
|
+
unless @repo_forge.nil?
|
|
46
|
+
cmd = _forge_cmd
|
|
47
|
+
unless SugarJar::Util.which_nofail(cmd)
|
|
48
|
+
die("No '#{cmd}' found, please install it'")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
41
51
|
|
|
42
|
-
|
|
52
|
+
user_option = "#{@repo_forge}_user"
|
|
53
|
+
@forge_user = @repo_config[user_option] || options[user_option]
|
|
43
54
|
|
|
44
|
-
# Tell the
|
|
45
|
-
|
|
55
|
+
# Tell the cli where to talk to, if not default
|
|
56
|
+
if @_forge_host
|
|
57
|
+
ENV['GH_HOST'] = @_forge_host
|
|
58
|
+
ENV['GL_HOST'] = @_forge_host
|
|
59
|
+
end
|
|
46
60
|
|
|
47
61
|
return if options['no_change']
|
|
48
62
|
|
|
@@ -52,12 +66,8 @@ class SugarJar
|
|
|
52
66
|
private
|
|
53
67
|
|
|
54
68
|
def forked_repo(repo, username)
|
|
55
|
-
repo =
|
|
56
|
-
|
|
57
|
-
else
|
|
58
|
-
"#{File.basename(repo)}.git"
|
|
59
|
-
end
|
|
60
|
-
"git@#{@ghhost || 'github.com'}:#{username}/#{repo}"
|
|
69
|
+
repo = extract_repo(repo)
|
|
70
|
+
"git@#{forge_host}:#{username}/#{repo}.git"
|
|
61
71
|
end
|
|
62
72
|
|
|
63
73
|
# gh utils will default to https, but we should always default to SSH
|
|
@@ -66,12 +76,41 @@ class SugarJar
|
|
|
66
76
|
# if they fully-qualified it, we're good
|
|
67
77
|
return repo if repo.start_with?('http', 'git@')
|
|
68
78
|
|
|
69
|
-
# otherwise,
|
|
70
|
-
cr = "git@#{
|
|
79
|
+
# otherwise, it's a shortname
|
|
80
|
+
cr = "git@#{forge_host}:#{repo}.git"
|
|
71
81
|
SugarJar::Log.debug("canonicalized #{repo} to #{cr}")
|
|
72
82
|
cr
|
|
73
83
|
end
|
|
74
84
|
|
|
85
|
+
def forge_host
|
|
86
|
+
# if one is specifically configured, use that
|
|
87
|
+
return @_forge_host if @_forge_host
|
|
88
|
+
|
|
89
|
+
# otherwise, if we're in a repo, use the hostname of the remote
|
|
90
|
+
if SugarJar::Util.in_repo?
|
|
91
|
+
extract_host(remote_url_map['origin'])
|
|
92
|
+
else
|
|
93
|
+
@repo_forge == 'gitlab' ? 'gitlab.com' : 'github.com'
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def repo_shortname(repo)
|
|
98
|
+
# if it's already a shortname, return
|
|
99
|
+
return repo unless repo.start_with?('http', 'git@')
|
|
100
|
+
|
|
101
|
+
# otherwise, parse it
|
|
102
|
+
if repo.start_with?('http')
|
|
103
|
+
bits = repo.split('/')
|
|
104
|
+
elsif repo.start_with?('git@')
|
|
105
|
+
relevant = repo.split(':').last
|
|
106
|
+
bits = relevant.split('/')
|
|
107
|
+
end
|
|
108
|
+
repo = bits[-1].gsub('.git', '')
|
|
109
|
+
org = bits[-2]
|
|
110
|
+
|
|
111
|
+
"#{org}/#{repo}"
|
|
112
|
+
end
|
|
113
|
+
|
|
75
114
|
def set_commit_template
|
|
76
115
|
unless SugarJar::Util.in_repo?
|
|
77
116
|
SugarJar::Log.debug('Skipping set_commit_template: not in repo')
|
|
@@ -119,8 +158,29 @@ class SugarJar
|
|
|
119
158
|
die('sugarjar must be run from inside a git repo')
|
|
120
159
|
end
|
|
121
160
|
|
|
161
|
+
def dirty_check!
|
|
162
|
+
return unless dirty?
|
|
163
|
+
|
|
164
|
+
if @ignore_dirty
|
|
165
|
+
SugarJar::Log.warn(
|
|
166
|
+
'Your repo is dirty, but --ignore-dirty was specified, so ' +
|
|
167
|
+
'carrying on anyway.',
|
|
168
|
+
)
|
|
169
|
+
else
|
|
170
|
+
SugarJar::Log.error(
|
|
171
|
+
'Your repo is dirty, so I am refusing to continue. Please commit ' +
|
|
172
|
+
'or amend first (or use --ignore-dirty to override).',
|
|
173
|
+
)
|
|
174
|
+
exit(1)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
122
178
|
def determine_main_branch(branches)
|
|
123
|
-
branches.include?('main')
|
|
179
|
+
if branches.include?('main')
|
|
180
|
+
'main'
|
|
181
|
+
elsif branches.include?('master')
|
|
182
|
+
'master'
|
|
183
|
+
end
|
|
124
184
|
end
|
|
125
185
|
|
|
126
186
|
def main_branch
|
|
@@ -150,8 +210,10 @@ class SugarJar
|
|
|
150
210
|
git(
|
|
151
211
|
'branch', '--format', '%(refname)'
|
|
152
212
|
).stdout.lines.map do |line|
|
|
153
|
-
|
|
154
|
-
|
|
213
|
+
if line.start_with?('(')
|
|
214
|
+
SugarJar::Log.debug("Skipping meta-branch: #{line.strip}")
|
|
215
|
+
next
|
|
216
|
+
end
|
|
155
217
|
branch_from_ref(line.strip)
|
|
156
218
|
end
|
|
157
219
|
end
|
|
@@ -188,11 +250,13 @@ class SugarJar
|
|
|
188
250
|
all_local_branches.reject { |x| x == most_main }.include?(base)
|
|
189
251
|
end
|
|
190
252
|
|
|
191
|
-
def tracked_branch(fallback: true)
|
|
192
|
-
|
|
253
|
+
def tracked_branch(branch = nil, fallback: true)
|
|
254
|
+
curr = current_branch
|
|
255
|
+
git('checkout', branch) if branch && branch != curr
|
|
193
256
|
s = git_nofail(
|
|
194
257
|
'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'
|
|
195
258
|
)
|
|
259
|
+
git('checkout', curr) if branch && branch != curr
|
|
196
260
|
if s.error?
|
|
197
261
|
branch = fallback ? most_main : nil
|
|
198
262
|
SugarJar::Log.debug("No specific tracked branch, using #{branch}")
|
|
@@ -242,7 +306,7 @@ class SugarJar
|
|
|
242
306
|
|
|
243
307
|
# Whatever org we push to, regardless of if this is a fork or not
|
|
244
308
|
def push_org
|
|
245
|
-
url =
|
|
309
|
+
url = remote_url_map['origin']
|
|
246
310
|
extract_org(url)
|
|
247
311
|
end
|
|
248
312
|
|
|
@@ -261,8 +325,8 @@ class SugarJar
|
|
|
261
325
|
end
|
|
262
326
|
end
|
|
263
327
|
|
|
264
|
-
def
|
|
265
|
-
!!SugarJar::Util.which_nofail(
|
|
328
|
+
def forge_cli_avail?
|
|
329
|
+
!!SugarJar::Util.which_nofail(_forge_cmd)
|
|
266
330
|
end
|
|
267
331
|
|
|
268
332
|
def fprefix(name)
|
|
@@ -302,11 +366,47 @@ class SugarJar
|
|
|
302
366
|
File.basename(repo, '.git')
|
|
303
367
|
end
|
|
304
368
|
|
|
369
|
+
def extract_host(repo)
|
|
370
|
+
if repo.start_with?('git@')
|
|
371
|
+
repo.split(':').first.split('@').last
|
|
372
|
+
elsif repo.start_with?('http')
|
|
373
|
+
repo.split('/')[2]
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
305
377
|
def die(msg)
|
|
306
378
|
SugarJar::Log.fatal(msg)
|
|
307
379
|
exit(1)
|
|
308
380
|
end
|
|
309
381
|
|
|
382
|
+
def release_branches
|
|
383
|
+
@repo_config['release_branches'] || []
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def worktree_branches
|
|
387
|
+
worktrees.values.map do |wt|
|
|
388
|
+
branch_from_ref(wt['branch'])
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def worktrees
|
|
393
|
+
root = SugarJar::Util.repo_root
|
|
394
|
+
s = git('worktree', 'list', '--porcelain')
|
|
395
|
+
s.error!
|
|
396
|
+
worktrees = {}
|
|
397
|
+
# each entry is separated by a double newline
|
|
398
|
+
s.stdout.split("\n\n").each do |entry|
|
|
399
|
+
# then each key/val is split by a new line with the key and
|
|
400
|
+
# the value themselves split by a whitespace
|
|
401
|
+
tree = entry.split("\n").to_h(&:split)
|
|
402
|
+
# Skip the one
|
|
403
|
+
next if tree['worktree'] == root
|
|
404
|
+
|
|
405
|
+
worktrees[tree['worktree']] = tree
|
|
406
|
+
end
|
|
407
|
+
worktrees
|
|
408
|
+
end
|
|
409
|
+
|
|
310
410
|
def branch_from_ref(ref, type = :local)
|
|
311
411
|
# local branches are refs/head/XXXX
|
|
312
412
|
# remote branches are refs/remotes/<remote>/XXXX
|
|
@@ -314,6 +414,12 @@ class SugarJar
|
|
|
314
414
|
ref.split('/')[base..].join('/')
|
|
315
415
|
end
|
|
316
416
|
|
|
417
|
+
def remote_from_ref(ref)
|
|
418
|
+
return nil unless ref.start_with?('refs/remotes/')
|
|
419
|
+
|
|
420
|
+
ref.split('/')[2]
|
|
421
|
+
end
|
|
422
|
+
|
|
317
423
|
def git(*)
|
|
318
424
|
SugarJar::Util.git(*, :color => @color)
|
|
319
425
|
end
|
|
@@ -322,12 +428,36 @@ class SugarJar
|
|
|
322
428
|
SugarJar::Util.git_nofail(*, :color => @color)
|
|
323
429
|
end
|
|
324
430
|
|
|
325
|
-
def
|
|
326
|
-
SugarJar::Util.
|
|
431
|
+
def _determine_forge_type
|
|
432
|
+
return nil unless SugarJar::Util.in_repo?
|
|
433
|
+
|
|
434
|
+
if remote_url_map.values.any? do |x|
|
|
435
|
+
x.include?('gitlab')
|
|
436
|
+
end
|
|
437
|
+
'gitlab'
|
|
438
|
+
else
|
|
439
|
+
'github'
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def _forge_cmd
|
|
444
|
+
@repo_forge == 'gitlab' ? 'glab' : 'gh'
|
|
327
445
|
end
|
|
328
446
|
|
|
329
|
-
def
|
|
330
|
-
|
|
447
|
+
def forge(*)
|
|
448
|
+
if @repo_forge == 'gitlab'
|
|
449
|
+
SugarJar::Util.glcli(*)
|
|
450
|
+
else
|
|
451
|
+
SugarJar::Util.ghcli(*)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def forge_nofail(*)
|
|
456
|
+
if @repo_forge == 'gitlab'
|
|
457
|
+
SugarJar::Util.glcli_nofail(*)
|
|
458
|
+
else
|
|
459
|
+
SugarJar::Util.ghcli_nofail(*)
|
|
460
|
+
end
|
|
331
461
|
end
|
|
332
462
|
end
|
|
333
463
|
end
|
data/lib/sugarjar/config.rb
CHANGED
|
@@ -7,6 +7,7 @@ class SugarJar
|
|
|
7
7
|
class Config
|
|
8
8
|
DEFAULTS = {
|
|
9
9
|
'github_user' => ENV.fetch('USER'),
|
|
10
|
+
'gitlab_user' => ENV.fetch('USER'),
|
|
10
11
|
'pr_autofill' => true,
|
|
11
12
|
'pr_autostack' => nil,
|
|
12
13
|
'color' => true,
|
|
@@ -27,6 +28,10 @@ class SugarJar
|
|
|
27
28
|
SugarJar::Log.debug("Loading config #{f}")
|
|
28
29
|
data = YAML.safe_load_file(f)
|
|
29
30
|
warn_on_deprecated_configs(data, f)
|
|
31
|
+
if data['github_host']
|
|
32
|
+
data['forge_host'] = data['github_host'] if data['forge_host'].nil?
|
|
33
|
+
data.delete('github_host')
|
|
34
|
+
end
|
|
30
35
|
# an empty file is a `nil` which you can't merge
|
|
31
36
|
c.merge!(YAML.safe_load_file(f)) if data
|
|
32
37
|
SugarJar::Log.debug("Modified config: #{c}")
|
|
@@ -41,14 +46,37 @@ class SugarJar
|
|
|
41
46
|
|
|
42
47
|
if ignore_deprecated_options.include?(opt)
|
|
43
48
|
SugarJar::Log.debug(
|
|
44
|
-
"Not warning about deprecated option
|
|
45
|
-
'
|
|
49
|
+
"#{fname}: Not warning about deprecated option `#{opt}` due to " +
|
|
50
|
+
'`ignore_deprecated_options` in that file.',
|
|
46
51
|
)
|
|
47
52
|
next
|
|
48
53
|
end
|
|
49
54
|
SugarJar::Log.warn(
|
|
50
|
-
"
|
|
51
|
-
'suppress this warning with ignore_deprecated_options
|
|
55
|
+
"#{fname}: contains deprecated option `#{opt}`. You can " +
|
|
56
|
+
'suppress this warning with `ignore_deprecated_options`.',
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# github_host has special handling
|
|
61
|
+
return unless data['github_host']
|
|
62
|
+
|
|
63
|
+
if ignore_deprecated_options.include?('github_host')
|
|
64
|
+
SugarJar::Log.debug(
|
|
65
|
+
"#{fname}: Deprecated option `github_host` found, but not " +
|
|
66
|
+
'warning due to `ignore_deprecated_options` in that file.',
|
|
67
|
+
)
|
|
68
|
+
elsif data.key?('forge_host')
|
|
69
|
+
SugarJar::Log.warn(
|
|
70
|
+
"#{fname}: Deprecated option `github_host` found. " +
|
|
71
|
+
'Ignoring in favor of newer `force_host` option. You can ' +
|
|
72
|
+
'suppress this warning with `ignore_deprecated_options`.',
|
|
73
|
+
)
|
|
74
|
+
else
|
|
75
|
+
SugarJar::Log.warn(
|
|
76
|
+
"#{fname}: Deprecated option `github_host` found. " +
|
|
77
|
+
'Treating it as if it was `forge_host` for now. Please update ' +
|
|
78
|
+
'your config file to use this new option. You can suppress ' +
|
|
79
|
+
'this warning with `ignore_deprecated_options`.',
|
|
52
80
|
)
|
|
53
81
|
end
|
|
54
82
|
end
|
data/lib/sugarjar/log.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'mixlib/log'
|
|
2
2
|
|
|
3
|
+
# rubocop:disable Style/OneClassPerFile
|
|
3
4
|
module Mixlib
|
|
4
5
|
module Log
|
|
5
6
|
# A simple formatter so that 'info' is just like 'puts'
|
|
@@ -22,3 +23,4 @@ class SugarJar
|
|
|
22
23
|
extend Mixlib::Log
|
|
23
24
|
end
|
|
24
25
|
end
|
|
26
|
+
# rubocop:enable Style/OneClassPerFile
|
data/lib/sugarjar/util.rb
CHANGED
|
@@ -46,16 +46,36 @@ class SugarJar
|
|
|
46
46
|
s
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
def self.ghcli_nofail(*
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
def self.ghcli_nofail(*)
|
|
50
|
+
forge_nofail('gh', *)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.ghcli(*)
|
|
54
|
+
s = ghcli_nofail(*)
|
|
55
|
+
s.error!
|
|
56
|
+
s
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.glcli_nofail(*)
|
|
60
|
+
forge_nofail('glab', *)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.glcli(*)
|
|
64
|
+
s = glcli_nofail(*)
|
|
65
|
+
s.error!
|
|
66
|
+
s
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.forge_nofail(cli, *args)
|
|
70
|
+
SugarJar::Log.trace("Running: #{cli} #{args.join(' ')}")
|
|
71
|
+
bin = which(cli)
|
|
72
|
+
s = Mixlib::ShellOut.new([bin] + args).run_command
|
|
73
|
+
if s.error? && s.stderr.include?("#{cli} auth")
|
|
54
74
|
SugarJar::Log.info(
|
|
55
|
-
'
|
|
56
|
-
"to force\ngh to authenticate...
|
|
75
|
+
'glab was run but no gitlab token exists. Will run ' +
|
|
76
|
+
'"glab auth login" to force\ngh to authenticate...',
|
|
57
77
|
)
|
|
58
|
-
unless system(
|
|
78
|
+
unless system(bin, 'auth', 'login', '-p', 'ssh')
|
|
59
79
|
SugarJar::Log.fatal(
|
|
60
80
|
'That failed, I will bail out. Hub needs to get a github ' +
|
|
61
81
|
'token. Try running "gh auth login" (will list info about ' +
|
|
@@ -67,12 +87,6 @@ class SugarJar
|
|
|
67
87
|
s
|
|
68
88
|
end
|
|
69
89
|
|
|
70
|
-
def self.ghcli(*)
|
|
71
|
-
s = ghcli_nofail(*)
|
|
72
|
-
s.error!
|
|
73
|
-
s
|
|
74
|
-
end
|
|
75
|
-
|
|
76
90
|
def self.in_repo?
|
|
77
91
|
s = git_nofail('rev-parse', '--is-inside-work-tree')
|
|
78
92
|
!s.error? && s.stdout.strip == 'true'
|
data/lib/sugarjar/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sugarjar
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Phil Dibowitz
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-06-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: deep_merge
|