sugarjar 1.1.3 → 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 +36 -84
- data/bin/sj +62 -108
- data/lib/sugarjar/commands.rb +19 -991
- 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/config.rb
CHANGED
@@ -6,11 +6,11 @@ class SugarJar
|
|
6
6
|
# This is stuff like log level, github-user, etc.
|
7
7
|
class Config
|
8
8
|
DEFAULTS = {
|
9
|
-
'github_cli' => 'auto',
|
10
9
|
'github_user' => ENV.fetch('USER'),
|
11
|
-
'fallthru' => true,
|
12
10
|
'pr_autofill' => true,
|
13
11
|
'pr_autostack' => nil,
|
12
|
+
'color' => true,
|
13
|
+
'ignore_deprecated_options' => [],
|
14
14
|
}.freeze
|
15
15
|
|
16
16
|
def self._find_ordered_files
|
@@ -26,11 +26,31 @@ class SugarJar
|
|
26
26
|
_find_ordered_files.each do |f|
|
27
27
|
SugarJar::Log.debug("Loading config #{f}")
|
28
28
|
data = YAML.safe_load_file(f)
|
29
|
+
warn_on_deprecated_configs(data, f)
|
29
30
|
# an empty file is a `nil` which you can't merge
|
30
31
|
c.merge!(YAML.safe_load_file(f)) if data
|
31
32
|
SugarJar::Log.debug("Modified config: #{c}")
|
32
33
|
end
|
33
34
|
c
|
34
35
|
end
|
36
|
+
|
37
|
+
def self.warn_on_deprecated_configs(data, fname)
|
38
|
+
ignore_deprecated_options = data['ignore_deprecated_options'] || []
|
39
|
+
%w{fallthru gh_cli}.each do |opt|
|
40
|
+
next unless data.key?(opt)
|
41
|
+
|
42
|
+
if ignore_deprecated_options.include?(opt)
|
43
|
+
SugarJar::Log.debug(
|
44
|
+
"Not warning about deprecated option '#{opt}' in #{fname} due to " +
|
45
|
+
'"ignore_deprecated_options" in that file.',
|
46
|
+
)
|
47
|
+
next
|
48
|
+
end
|
49
|
+
SugarJar::Log.warn(
|
50
|
+
"Config file #{fname} contains deprecated option #{opt}. You can " +
|
51
|
+
'suppress this warning with ignore_deprecated_options.',
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
35
55
|
end
|
36
56
|
end
|
data/lib/sugarjar/util.rb
CHANGED
@@ -5,17 +5,296 @@ require 'mixlib/shellout'
|
|
5
5
|
class SugarJar
|
6
6
|
# Some common methods needed by other classes
|
7
7
|
module Util
|
8
|
+
def extract_org(repo)
|
9
|
+
if repo.start_with?('http')
|
10
|
+
File.basename(File.dirname(repo))
|
11
|
+
elsif repo.start_with?('git@')
|
12
|
+
repo.split(':')[1].split('/')[0]
|
13
|
+
else
|
14
|
+
# assume they passed in a ghcli-friendly name
|
15
|
+
repo.split('/').first
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def extract_repo(repo)
|
20
|
+
File.basename(repo, '.git')
|
21
|
+
end
|
22
|
+
|
23
|
+
def forked_repo(repo, username)
|
24
|
+
repo = if repo.start_with?('http', 'git@')
|
25
|
+
File.basename(repo)
|
26
|
+
else
|
27
|
+
"#{File.basename(repo)}.git"
|
28
|
+
end
|
29
|
+
"git@#{@ghhost || 'github.com'}:#{username}/#{repo}"
|
30
|
+
end
|
31
|
+
|
32
|
+
# gh utils will default to https, but we should always default to SSH
|
33
|
+
# unless otherwise specified since https will cause prompting.
|
34
|
+
def canonicalize_repo(repo)
|
35
|
+
# if they fully-qualified it, we're good
|
36
|
+
return repo if repo.start_with?('http', 'git@')
|
37
|
+
|
38
|
+
# otherwise, ti's a shortname
|
39
|
+
cr = "git@#{@ghhost || 'github.com'}:#{repo}.git"
|
40
|
+
SugarJar::Log.debug("canonicalized #{repo} to #{cr}")
|
41
|
+
cr
|
42
|
+
end
|
43
|
+
|
44
|
+
def set_commit_template
|
45
|
+
unless in_repo
|
46
|
+
SugarJar::Log.debug('Skipping set_commit_template: not in repo')
|
47
|
+
return
|
48
|
+
end
|
49
|
+
|
50
|
+
realpath = if @repo_config['commit_template'].start_with?('/')
|
51
|
+
@repo_config['commit_template']
|
52
|
+
else
|
53
|
+
"#{repo_root}/#{@repo_config['commit_template']}"
|
54
|
+
end
|
55
|
+
unless File.exist?(realpath)
|
56
|
+
die(
|
57
|
+
"Repo config specifies #{@repo_config['commit_template']} as the " +
|
58
|
+
'commit template, but that file does not exist.',
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
s = git_nofail('config', '--local', 'commit.template')
|
63
|
+
unless s.error?
|
64
|
+
current = s.stdout.strip
|
65
|
+
if current == @repo_config['commit_template']
|
66
|
+
SugarJar::Log.debug('Commit template already set correctly')
|
67
|
+
return
|
68
|
+
else
|
69
|
+
SugarJar::Log.warn(
|
70
|
+
"Updating repo-specific commit template from #{current} " +
|
71
|
+
"to #{@repo_config['commit_template']}",
|
72
|
+
)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
SugarJar::Log.debug(
|
77
|
+
'Setting repo-specific commit template to ' +
|
78
|
+
"#{@repo_config['commit_template']} per sugarjar repo config.",
|
79
|
+
)
|
80
|
+
git(
|
81
|
+
'config', '--local', 'commit.template', @repo_config['commit_template']
|
82
|
+
)
|
83
|
+
end
|
84
|
+
|
85
|
+
def run_prepush
|
86
|
+
@repo_config['on_push']&.each do |item|
|
87
|
+
SugarJar::Log.debug("Running on_push check type #{item}")
|
88
|
+
unless send(:run_check, item)
|
89
|
+
SugarJar::Log.info("[prepush]: #{item} #{color('failed', :red)}.")
|
90
|
+
return false
|
91
|
+
end
|
92
|
+
end
|
93
|
+
true
|
94
|
+
end
|
95
|
+
|
96
|
+
def die(msg)
|
97
|
+
SugarJar::Log.fatal(msg)
|
98
|
+
exit(1)
|
99
|
+
end
|
100
|
+
|
101
|
+
def assert_common_main_branch
|
102
|
+
upstream_branch = main_remote_branch(upstream)
|
103
|
+
unless main_branch == upstream_branch
|
104
|
+
die(
|
105
|
+
"The local main branch is '#{main_branch}', but the main branch " +
|
106
|
+
"of the #{upstream} remote is '#{upstream_branch}'. You probably " +
|
107
|
+
"want to rename your local branch by doing:\n\t" +
|
108
|
+
"git branch -m #{main_branch} #{upstream_branch}\n\t" +
|
109
|
+
"git fetch #{upstream}\n\t" +
|
110
|
+
"git branch -u #{upstream}/#{upstream_branch} #{upstream_branch}\n" +
|
111
|
+
"\tgit remote set-head #{upstream} -a",
|
112
|
+
)
|
113
|
+
end
|
114
|
+
return if upstream_branch == 'origin'
|
115
|
+
|
116
|
+
origin_branch = main_remote_branch('origin')
|
117
|
+
return if origin_branch == upstream_branch
|
118
|
+
|
119
|
+
die(
|
120
|
+
"The main branch of your upstream (#{upstream_branch}) and your " +
|
121
|
+
"fork/origin (#{origin_branch}) are not the same. You should go " +
|
122
|
+
"to https://#{@ghhost || 'github.com'}/#{@ghuser}/#{repo_name}/" +
|
123
|
+
'branches/ and rename the \'default\' branch to ' +
|
124
|
+
"'#{upstream_branch}'. It will then give you some commands to " +
|
125
|
+
'run to update this clone.',
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
def assert_in_repo
|
130
|
+
die('sugarjar must be run from inside a git repo') unless in_repo
|
131
|
+
end
|
132
|
+
|
133
|
+
def determine_main_branch(branches)
|
134
|
+
branches.include?('main') ? 'main' : 'master'
|
135
|
+
end
|
136
|
+
|
137
|
+
def main_branch
|
138
|
+
@main_branch = determine_main_branch(all_local_branches)
|
139
|
+
end
|
140
|
+
|
141
|
+
def main_remote_branch(remote)
|
142
|
+
@main_remote_branches[remote] ||=
|
143
|
+
determine_main_branch(all_remote_branches(remote))
|
144
|
+
end
|
145
|
+
|
146
|
+
def checkout_main_branch
|
147
|
+
git('checkout', main_branch)
|
148
|
+
end
|
149
|
+
|
150
|
+
def all_remote_branches(remote = 'origin')
|
151
|
+
branches = []
|
152
|
+
git('branch', '-r', '--format', '%(refname)').stdout.lines.each do |line|
|
153
|
+
next unless line.start_with?("refs/remotes/#{remote}/")
|
154
|
+
|
155
|
+
branches << branch_from_ref(line.strip, :remote)
|
156
|
+
end
|
157
|
+
branches
|
158
|
+
end
|
159
|
+
|
160
|
+
def all_local_branches
|
161
|
+
git(
|
162
|
+
'branch', '--format', '%(refname)'
|
163
|
+
).stdout.lines.map do |line|
|
164
|
+
next if line.start_with?('(HEAD detached')
|
165
|
+
|
166
|
+
branch_from_ref(line.strip)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def all_remotes
|
171
|
+
git('remote').stdout.lines.map(&:strip)
|
172
|
+
end
|
173
|
+
|
174
|
+
def current_branch
|
175
|
+
branch_from_ref(git('symbolic-ref', 'HEAD').stdout.strip)
|
176
|
+
end
|
177
|
+
|
178
|
+
def fetch_upstream
|
179
|
+
us = upstream
|
180
|
+
fetch(us) if us
|
181
|
+
end
|
182
|
+
|
183
|
+
def fetch(remote)
|
184
|
+
git('fetch', remote)
|
185
|
+
end
|
186
|
+
|
187
|
+
# determine if this branch is based on another local branch (i.e. is a
|
188
|
+
# subfeature). Used to figure out of we should stack the PR
|
189
|
+
def subfeature?(base)
|
190
|
+
all_local_branches.reject { |x| x == most_main }.include?(base)
|
191
|
+
end
|
192
|
+
|
193
|
+
def tracked_branch(fallback: true)
|
194
|
+
branch = nil
|
195
|
+
s = git_nofail(
|
196
|
+
'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'
|
197
|
+
)
|
198
|
+
if s.error?
|
199
|
+
branch = fallback ? most_main : nil
|
200
|
+
SugarJar::Log.debug("No specific tracked branch, using #{branch}")
|
201
|
+
else
|
202
|
+
branch = s.stdout.strip
|
203
|
+
SugarJar::Log.debug(
|
204
|
+
"Using explicit tracked branch: #{branch}, use " +
|
205
|
+
'`git branch -u` to change',
|
206
|
+
)
|
207
|
+
end
|
208
|
+
branch
|
209
|
+
end
|
210
|
+
|
211
|
+
def most_main
|
212
|
+
us = upstream
|
213
|
+
if us
|
214
|
+
"#{us}/#{main_branch}"
|
215
|
+
else
|
216
|
+
main_branch
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def upstream
|
221
|
+
return @remote if @remote
|
222
|
+
|
223
|
+
remotes = all_remotes
|
224
|
+
SugarJar::Log.debug("remotes is #{remotes}")
|
225
|
+
if remotes.empty?
|
226
|
+
@remote = nil
|
227
|
+
elsif remotes.length == 1
|
228
|
+
@remote = remotes[0]
|
229
|
+
elsif remotes.include?('upstream')
|
230
|
+
@remote = 'upstream'
|
231
|
+
elsif remotes.include?('origin')
|
232
|
+
@remote = 'origin'
|
233
|
+
else
|
234
|
+
raise 'Could not determine "upstream" remote to use...'
|
235
|
+
end
|
236
|
+
@remote
|
237
|
+
end
|
238
|
+
|
239
|
+
# Whatever org we push to, regardless of if this is a fork or not
|
240
|
+
def push_org
|
241
|
+
url = git('remote', 'get-url', 'origin').stdout.strip
|
242
|
+
extract_org(url)
|
243
|
+
end
|
244
|
+
|
245
|
+
def branch_from_ref(ref, type = :local)
|
246
|
+
# local branches are refs/head/XXXX
|
247
|
+
# remote branches are refs/remotes/<remote>/XXXX
|
248
|
+
base = type == :local ? 2 : 3
|
249
|
+
ref.split('/')[base..].join('/')
|
250
|
+
end
|
251
|
+
|
252
|
+
def color(string, *colors)
|
253
|
+
if @color
|
254
|
+
pastel.decorate(string, *colors)
|
255
|
+
else
|
256
|
+
string
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
def pastel
|
261
|
+
@pastel ||= begin
|
262
|
+
require 'pastel'
|
263
|
+
Pastel.new
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def gh_avail?
|
268
|
+
!!which_nofail('gh')
|
269
|
+
end
|
270
|
+
|
271
|
+
def fprefix(name)
|
272
|
+
return name unless @feature_prefix
|
273
|
+
|
274
|
+
return name if name.start_with?(@feature_prefix)
|
275
|
+
return name if all_local_branches.include?(name)
|
276
|
+
|
277
|
+
newname = "#{@feature_prefix}#{name}"
|
278
|
+
SugarJar::Log.debug(
|
279
|
+
"Munging feature name: #{name} -> #{newname} due to feature prefix",
|
280
|
+
)
|
281
|
+
newname
|
282
|
+
end
|
283
|
+
|
8
284
|
# Finds the first entry in the path for a binary and checks
|
9
|
-
# to make sure it's not us
|
10
|
-
# or 'hub', but when we are calling that, we don't want ourselves.
|
285
|
+
# to make sure it's not us. Warn if it is us as that won't work in 2.x
|
11
286
|
def which_nofail(cmd)
|
12
287
|
ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir|
|
13
288
|
p = File.join(dir, cmd)
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
289
|
+
next unless File.exist?(p) && File.executable?(p)
|
290
|
+
|
291
|
+
if File.basename(File.realpath(p)) == 'sj'
|
292
|
+
SugarJar::Log.error(
|
293
|
+
"'#{cmd}' is linked to 'sj' which is no longer supported.",
|
294
|
+
)
|
295
|
+
next
|
18
296
|
end
|
297
|
+
return p
|
19
298
|
end
|
20
299
|
false
|
21
300
|
end
|
@@ -43,64 +322,7 @@ class SugarJar
|
|
43
322
|
s
|
44
323
|
end
|
45
324
|
|
46
|
-
def
|
47
|
-
# this allows us to use 'hub' stuff that's top-level, but is under
|
48
|
-
# repo for this.
|
49
|
-
args.delete_at(0) if args[0] == 'repo'
|
50
|
-
SugarJar::Log.trace("Running: hub #{args.join(' ')}")
|
51
|
-
s = Mixlib::ShellOut.new([which('hub')] + args).run_command
|
52
|
-
if s.error?
|
53
|
-
# depending on hub version and possibly other things, STDERR
|
54
|
-
# is either "Requires authentication" or "Must authenticate"
|
55
|
-
case s.stderr
|
56
|
-
when /^(Must|Requires) authenticat/
|
57
|
-
SugarJar::Log.info(
|
58
|
-
'Hub was run but no github token exists. Will run "hub api user" ' +
|
59
|
-
"to force\nhub to authenticate...",
|
60
|
-
)
|
61
|
-
unless system(which('hub'), 'api', 'user')
|
62
|
-
SugarJar::Log.fatal(
|
63
|
-
'That failed, I will bail out. Hub needs to get a github ' +
|
64
|
-
'token. Try running "hub api user" (will list info about ' +
|
65
|
-
'your account) and try this again when that works.',
|
66
|
-
)
|
67
|
-
exit(1)
|
68
|
-
end
|
69
|
-
SugarJar::Log.info('Re-running original hub command...')
|
70
|
-
s = Mixlib::ShellOut.new([which('hub')] + args).run_command
|
71
|
-
when /^fatal: could not read Username/, /Anonymous access denied/
|
72
|
-
|
73
|
-
# On http(s) URLs, git may prompt for username/passwd
|
74
|
-
SugarJar::Log.info(
|
75
|
-
'Hub was run but git prompted for authentication. This probably ' +
|
76
|
-
"means you have\nused an http repo URL instead of an ssh one. It " +
|
77
|
-
"is recommended you reclone\nusing 'sj sclone' to setup your " +
|
78
|
-
"remotes properly. However, in the meantime,\nwe'll go ahead " +
|
79
|
-
"and re-run the command in a shell so you can type in the\n" +
|
80
|
-
'credentials.',
|
81
|
-
)
|
82
|
-
unless system(which('hub'), *args)
|
83
|
-
SugarJar::Log.fatal(
|
84
|
-
'That failed, I will bail out. You can either manually change ' +
|
85
|
-
'your remotes, or simply create a fresh clone with ' +
|
86
|
-
'"sj smartclone".',
|
87
|
-
)
|
88
|
-
exit(1)
|
89
|
-
end
|
90
|
-
SugarJar::Log.info('Re-running original hub command...')
|
91
|
-
s = Mixlib::ShellOut.new([which('hub')] + args).run_command
|
92
|
-
end
|
93
|
-
end
|
94
|
-
s
|
95
|
-
end
|
96
|
-
|
97
|
-
def hub(*args)
|
98
|
-
s = hub_nofail(*args)
|
99
|
-
s.error!
|
100
|
-
s
|
101
|
-
end
|
102
|
-
|
103
|
-
def gh_nofail(*args)
|
325
|
+
def ghcli_nofail(*args)
|
104
326
|
SugarJar::Log.trace("Running: gh #{args.join(' ')}")
|
105
327
|
s = Mixlib::ShellOut.new([which('gh')] + args).run_command
|
106
328
|
if s.error? && s.stderr.include?('gh auth')
|
@@ -108,6 +330,11 @@ class SugarJar
|
|
108
330
|
'gh was run but no github token exists. Will run "gh auth login" ' +
|
109
331
|
"to force\ngh to authenticate...",
|
110
332
|
)
|
333
|
+
ENV['GITHUB_HOST'] = @ghhost if @ghhost
|
334
|
+
args = [
|
335
|
+
which('gh'), 'auth', 'login', '-p', 'ssh'
|
336
|
+
]
|
337
|
+
args + ['--hostname', @ghhost] if @ghhost
|
111
338
|
unless system(which('gh'), 'auth', 'login', '-p', 'ssh')
|
112
339
|
SugarJar::Log.fatal(
|
113
340
|
'That failed, I will bail out. Hub needs to get a github ' +
|
@@ -120,8 +347,8 @@ class SugarJar
|
|
120
347
|
s
|
121
348
|
end
|
122
349
|
|
123
|
-
def
|
124
|
-
s =
|
350
|
+
def ghcli(*args)
|
351
|
+
s = ghcli_nofail(*args)
|
125
352
|
s.error!
|
126
353
|
s
|
127
354
|
end
|
@@ -131,6 +358,11 @@ class SugarJar
|
|
131
358
|
!s.error? && s.stdout.strip == 'true'
|
132
359
|
end
|
133
360
|
|
361
|
+
def dirty?
|
362
|
+
s = git_nofail('diff', '--quiet')
|
363
|
+
s.error?
|
364
|
+
end
|
365
|
+
|
134
366
|
def repo_root
|
135
367
|
git('rev-parse', '--show-toplevel').stdout.strip
|
136
368
|
end
|
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: 2.0.0.beta.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Phil Dibowitz
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-04-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: deep_merge
|
@@ -110,9 +110,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
110
110
|
version: '3.0'
|
111
111
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
112
|
requirements:
|
113
|
-
- - "
|
113
|
+
- - ">"
|
114
114
|
- !ruby/object:Gem::Version
|
115
|
-
version:
|
115
|
+
version: 1.3.1
|
116
116
|
requirements: []
|
117
117
|
rubygems_version: 3.3.7
|
118
118
|
signing_key:
|