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.
@@ -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 (i.e. we may be linked to as 'git'
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
- # if it exists, and it is executable and is not us...
15
- if File.exist?(p) && File.executable?(p) &&
16
- File.basename(File.realpath(p)) != 'sj'
17
- return p
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 hub_nofail(*args)
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 gh(*args)
124
- s = gh_nofail(*args)
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
@@ -1,3 +1,3 @@
1
1
  class SugarJar
2
- VERSION = '1.1.3'.freeze
2
+ VERSION = '2.0.0.beta.1'.freeze
3
3
  end
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: 1.1.3
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-02-20 00:00:00.000000000 Z
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: '0'
115
+ version: 1.3.1
116
116
  requirements: []
117
117
  rubygems_version: 3.3.7
118
118
  signing_key: