sugarjar 3.0.1 → 4.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.
@@ -6,14 +6,16 @@ require_relative 'commands/branch'
6
6
  require_relative 'commands/checks'
7
7
  require_relative 'commands/debuginfo'
8
8
  require_relative 'commands/feature'
9
+ require_relative 'commands/modernize_config'
9
10
  require_relative 'commands/pullsuggestions'
10
11
  require_relative 'commands/push'
11
12
  require_relative 'commands/smartclone'
12
13
  require_relative 'commands/smartpullrequest'
13
14
  require_relative 'commands/up'
15
+ require_relative 'forge'
16
+ require_relative 'git'
14
17
  require_relative 'log'
15
18
  require_relative 'repoconfig'
16
- require_relative 'util'
17
19
  require_relative 'version'
18
20
 
19
21
  class SugarJar
@@ -25,39 +27,21 @@ class SugarJar
25
27
 
26
28
  def initialize(options)
27
29
  SugarJar::Log.debug("Commands.initialize options: #{options}")
28
- @ignore_dirty = options['ignore_dirty']
29
- @ignore_prerun_failure = options['ignore_prerun_failure']
30
+ @config = options
30
31
  @repo_config = SugarJar::RepoConfig.config
31
32
  SugarJar::Log.debug("Repoconfig: #{@repo_config}")
32
- @color = options['color']
33
- @pr_autofill = options['pr_autofill']
34
- @pr_autostack = options['pr_autostack']
35
- @feature_prefix = options['feature_prefix']
36
- @use_forks = options['use_forks']
37
- @fork_name = options['fork_name']
33
+
38
34
  @checks = {}
39
35
  @main_branch = nil
40
36
  @main_remote_branches = {}
41
- # This is CONFIGURED host, which may be null, as opposed
42
- # to the method forge_host which will always return something
43
- @_forge_host = @repo_config['forge_host'] || options['forge_host']
44
- @repo_forge = @repo_config['forge_type'] || options['forge_type'] ||
45
- _determine_forge_type
46
-
47
- unless @repo_forge.nil?
48
- cmd = _forge_cmd
49
- unless SugarJar::Util.which_nofail(cmd)
50
- die("No '#{cmd}' found, please install it'")
51
- end
52
- end
37
+ @host_config = nil
53
38
 
54
- user_option = "#{@repo_forge}_user"
55
- @forge_user = @repo_config[user_option] || options[user_option]
56
-
57
- # Tell the cli where to talk to, if not default
58
- if @_forge_host
59
- ENV['GH_HOST'] = @_forge_host
60
- ENV['GL_HOST'] = @_forge_host
39
+ if SugarJar::Git.in_repo?
40
+ @host_config = gen_host_config
41
+ if @host_config['forge_type']
42
+ # initialize instance var here so checks run...
43
+ forge
44
+ end
61
45
  end
62
46
 
63
47
  return if options['no_change']
@@ -67,34 +51,72 @@ class SugarJar
67
51
 
68
52
  private
69
53
 
54
+ def forge
55
+ return @forge if @forge
56
+ unless @host_config['forge_type']
57
+ raise 'Attempted to create Forge object before forge-type known!'
58
+ end
59
+
60
+ @forge = SugarJar::Forge.new(
61
+ @host_config['forge_type'], @host_config['forge_host']
62
+ )
63
+ end
64
+
65
+ def gen_host_config(host = nil)
66
+ host ||= _determine_forge_host
67
+ _config_for_host(host) || {}
68
+ end
69
+
70
+ def _config_for_host(host)
71
+ SugarJar::Log.debug("config_for_host called for #{host}")
72
+ r = @config.dig('host_configs', 'default').dup || {}
73
+ r.merge!(@config.dig('host_configs', host) || {})
74
+ r['forge_host'] = host
75
+ # if people specified a forge_type, honor it
76
+ r['forge_type'] ||= _determine_forge_type(host)
77
+
78
+ # there are a few things we allow the CLI to override
79
+ r.merge!(@config['_cli_overrides'] || {})
80
+
81
+ SugarJar::Log.debug("Determined config for host: #{r}")
82
+ r
83
+ end
84
+
70
85
  def forked_repo(repo, username)
86
+ host = @host_config&.dig('forge_host') || extract_host(repo) ||
87
+ @config['default_forge_host']
88
+ raise 'Cannot determine host' unless host
89
+
71
90
  repo = extract_repo(repo)
72
- "git@#{forge_host}:#{username}/#{repo}.git"
91
+
92
+ "git@#{host}:#{username}/#{repo}.git"
73
93
  end
74
94
 
75
95
  # gh utils will default to https, but we should always default to SSH
76
96
  # unless otherwise specified since https will cause prompting.
77
97
  def canonicalize_repo(repo)
78
98
  # if they fully-qualified it, we're good
79
- return repo if repo.start_with?('http', 'git@')
99
+ return repo if repo.start_with?('http', 'git@', 'ssh://')
80
100
 
101
+ host = @host_config&.dig('forge_host') || extract_host(repo) ||
102
+ @config['default_forge_host']
81
103
  # otherwise, it's a shortname
82
- cr = "git@#{forge_host}:#{repo}.git"
104
+ cr = "git@#{host}:#{repo}.git"
83
105
  SugarJar::Log.debug("canonicalized #{repo} to #{cr}")
84
106
  cr
85
107
  end
86
108
 
87
- def forge_host
88
- # if one is specifically configured, use that
89
- return @_forge_host if @_forge_host
109
+ def _determine_forge_host
110
+ # DO NOT USE in the general case - ONLY for filling in @host_config
111
+ # (doesn't consult host_config). Use @host_config['forge_host']
112
+ # in general.
90
113
 
91
- # otherwise, if we're in a repo, use the hostname of the remote
92
- if SugarJar::Util.in_repo?
114
+ # if we're in a repo, use the hostname of the remote
115
+ if SugarJar::Git.in_repo?
93
116
  url = remote_url_map.values.first
94
117
  return extract_host(url)
95
118
  end
96
-
97
- @repo_forge == 'gitlab' ? 'gitlab.com' : 'github.com'
119
+ @config['default_forge_host']
98
120
  end
99
121
 
100
122
  def repo_shortname(repo)
@@ -107,7 +129,7 @@ class SugarJar
107
129
  end
108
130
 
109
131
  def set_commit_template
110
- unless SugarJar::Util.in_repo?
132
+ unless SugarJar::Git.in_repo?
111
133
  SugarJar::Log.debug('Skipping set_commit_template: not in repo')
112
134
  return
113
135
  end
@@ -115,7 +137,10 @@ class SugarJar
115
137
  realpath = if @repo_config['commit_template'].start_with?('/')
116
138
  @repo_config['commit_template']
117
139
  else
118
- "#{Util.repo_root}/#{@repo_config['commit_template']}"
140
+ File.join(
141
+ SugarJar::Git.repo_root,
142
+ @repo_config['commit_template'],
143
+ )
119
144
  end
120
145
  unless File.exist?(realpath)
121
146
  die(
@@ -148,7 +173,7 @@ class SugarJar
148
173
  end
149
174
 
150
175
  def assert_in_repo!
151
- return if SugarJar::Util.in_repo?
176
+ return if SugarJar::Git.in_repo?
152
177
 
153
178
  die('sugarjar must be run from inside a git repo')
154
179
  end
@@ -156,7 +181,7 @@ class SugarJar
156
181
  def dirty_check!
157
182
  return unless dirty?
158
183
 
159
- if @ignore_dirty
184
+ if @config['ignore_dirty']
160
185
  SugarJar::Log.warn(
161
186
  'Your repo is dirty, but --ignore-dirty was specified, so ' +
162
187
  'carrying on anyway.',
@@ -171,7 +196,11 @@ class SugarJar
171
196
  end
172
197
 
173
198
  def determine_main_branch(branches)
174
- branches.include?('master') ? 'master' : 'main'
199
+ if branches.include?('main')
200
+ 'main'
201
+ elsif branches.include?('master')
202
+ 'master'
203
+ end
175
204
  end
176
205
 
177
206
  def main_branch
@@ -301,8 +330,14 @@ class SugarJar
301
330
  extract_org(url)
302
331
  end
303
332
 
333
+ def repo_name
334
+ # Take the url of the most-upstream remote, then pull the repo
335
+ # name from that
336
+ extract_repo(remote_url_map[upstream])
337
+ end
338
+
304
339
  def color(string, *colors)
305
- if @color
340
+ if @config['color']
306
341
  pastel.decorate(string, *colors)
307
342
  else
308
343
  string
@@ -316,17 +351,13 @@ class SugarJar
316
351
  end
317
352
  end
318
353
 
319
- def forge_cli_avail?
320
- !!SugarJar::Util.which_nofail(_forge_cmd)
321
- end
322
-
323
354
  def fprefix(name)
324
- return name unless @feature_prefix
355
+ return name unless @host_config['feature_prefix']
325
356
 
326
- return name if name.start_with?(@feature_prefix)
357
+ return name if name.start_with?(@host_config['feature_prefix'])
327
358
  return name if all_local_branches.include?(name)
328
359
 
329
- newname = "#{@feature_prefix}#{name}"
360
+ newname = "#{@host_config['feature_prefix']}#{name}"
330
361
  SugarJar::Log.debug(
331
362
  "Munging feature name: #{name} -> #{newname} due to feature prefix",
332
363
  )
@@ -338,8 +369,8 @@ class SugarJar
338
369
  s.error?
339
370
  end
340
371
 
341
- def repo_name
342
- SugarJar::Util.repo_root.split('/').last
372
+ def repo_dir_name
373
+ SugarJar::Git.repo_root.split('/').last
343
374
  end
344
375
 
345
376
  def extract_org(repo)
@@ -360,6 +391,8 @@ class SugarJar
360
391
  def extract_host(repo)
361
392
  if repo.start_with?('git@')
362
393
  repo.split(':').first.split('@').last
394
+ elsif repo.start_with?('ssh://')
395
+ repo.split('/')[2].split('@').last
363
396
  elsif repo.start_with?('http')
364
397
  repo.split('/')[2]
365
398
  end
@@ -381,7 +414,7 @@ class SugarJar
381
414
  end
382
415
 
383
416
  def worktrees
384
- root = SugarJar::Util.repo_root
417
+ root = SugarJar::Git.repo_root
385
418
  s = git('worktree', 'list', '--porcelain')
386
419
  s.error!
387
420
  worktrees = {}
@@ -412,41 +445,33 @@ class SugarJar
412
445
  end
413
446
 
414
447
  def git(*)
415
- SugarJar::Util.git(*, :color => @color)
448
+ SugarJar::Git.run(*, :color => @config['color'])
416
449
  end
417
450
 
418
451
  def git_nofail(*)
419
- SugarJar::Util.git_nofail(*, :color => @color)
420
- end
452
+ SugarJar::Git.run_nofail(*, :color => @config['color'])
453
+ end
454
+
455
+ def _determine_forge_type(host = nil)
456
+ if host
457
+ case host
458
+ when /gitlab/
459
+ return 'gitlab'
460
+ when /github/
461
+ return 'github'
462
+ when /forgejo|codeberg/
463
+ return 'forgejo'
464
+ end
465
+ end
421
466
 
422
- def _determine_forge_type
423
- if SugarJar::Util.in_repo?
467
+ if SugarJar::Git.in_repo?
424
468
  gl = remote_url_map.values.any? { |x| x.include?('gitlab') }
425
469
  return gl ? 'gitlab' : 'github'
426
470
  end
427
471
 
428
- # if all else fails, guess GH
429
- 'github'
430
- end
431
-
432
- def _forge_cmd
433
- @repo_forge == 'gitlab' ? 'glab' : 'gh'
434
- end
435
-
436
- def forge(*)
437
- if @repo_forge == 'gitlab'
438
- SugarJar::Util.glcli(*)
439
- else
440
- SugarJar::Util.ghcli(*)
441
- end
442
- end
443
-
444
- def forge_nofail(*)
445
- if @repo_forge == 'gitlab'
446
- SugarJar::Util.glcli_nofail(*)
447
- else
448
- SugarJar::Util.ghcli_nofail(*)
449
- end
472
+ # in smartclone mode, when creating the object, we will in fact
473
+ # be unable to determine this, that's expacted
474
+ SugarJar::Log.debug('Unable to determine forge_type!')
450
475
  end
451
476
  end
452
477
  end
@@ -1,25 +1,52 @@
1
1
  require 'yaml'
2
2
  require_relative 'log'
3
+ require 'deep_merge'
3
4
 
4
5
  class SugarJar
5
6
  # This parses SugarJar configs (not to be confused with repoconfigs).
6
7
  # This is stuff like log level, github-user, etc.
7
8
  class Config
8
9
  DEFAULTS = {
9
- 'github_user' => ENV.fetch('USER'),
10
- 'gitlab_user' => ENV.fetch('USER'),
10
+ 'default_forge_host' => nil,
11
11
  'pr_autofill' => true,
12
12
  'pr_autostack' => nil,
13
13
  'color' => true,
14
- 'use_forks' => true,
15
14
  'ignore_deprecated_options' => [],
15
+ 'host_configs' => {
16
+ 'default' => {
17
+ 'user' => ENV.fetch('USER'),
18
+ 'use_forks' => true,
19
+ },
20
+ },
21
+ }.freeze
22
+
23
+ # things we have a command-line option for, but doesn't make sense
24
+ # in a config file.
25
+ EXTRA_CONFIGS = %w{fork_name force_forge_type}.freeze
26
+
27
+ TOP_LEVEL_CONFIGS = (DEFAULTS.keys + EXTRA_CONFIGS).freeze
28
+
29
+ DEPRECATED_OPTIONS = %w{
30
+ fallthru
31
+ gh_cli
32
+ github_user
33
+ gitlab_user
34
+ github_host
35
+ gitlab_host
16
36
  }.freeze
17
37
 
18
38
  def self._find_ordered_files
39
+ real_files = []
19
40
  [
20
- '/etc/sugarjar/config.yaml',
21
- "#{Dir.home}/.config/sugarjar/config.yaml",
22
- ].select { |f| File.exist?(f) }
41
+ '/etc/sugarjar',
42
+ "#{Dir.home}/.config/sugarjar",
43
+ ].each do |dir|
44
+ %w{yaml yml}.each do |ext|
45
+ f = File.join(dir, "config.#{ext}")
46
+ real_files << f if File.exist?(f)
47
+ end
48
+ end
49
+ real_files
23
50
  end
24
51
 
25
52
  def self.config
@@ -28,21 +55,34 @@ class SugarJar
28
55
  _find_ordered_files.each do |f|
29
56
  SugarJar::Log.debug("Loading config #{f}")
30
57
  data = YAML.safe_load_file(f)
58
+ next unless data
59
+
31
60
  warn_on_deprecated_configs(data, f)
32
- if data['github_host']
33
- data['forge_host'] = data['github_host'] if data['forge_host'].nil?
34
- data.delete('github_host')
35
- end
36
- # an empty file is a `nil` which you can't merge
37
- c.merge!(YAML.safe_load_file(f)) if data
61
+ data = data.slice(*TOP_LEVEL_CONFIGS)
62
+ c.deep_merge!(data)
38
63
  SugarJar::Log.debug("Modified config: #{c}")
39
64
  end
40
65
  c
41
66
  end
42
67
 
43
68
  def self.warn_on_deprecated_configs(data, fname)
69
+ # if it doesn't have host_configs **and** it has one of the
70
+ # previous user options, that were effectively required before
71
+ # and have now moved to host_configs, then we treat it as an
72
+ # old config
73
+ if %w{github_user gitlab_user}.any? { |useropt| data.key?(useropt) } &&
74
+ !data['host_configs']
75
+ SugarJar::Log.warn(
76
+ "#{fname} appears to be an old-style config.\nIt likely will " +
77
+ 'not result in the behavior you want. You should run' +
78
+ "\n\n\tsj modernizeconfig #{fname}\n\n" +
79
+ "To automatically convert it.\n",
80
+ )
81
+ return
82
+ end
83
+
44
84
  ignore_deprecated_options = data['ignore_deprecated_options'] || []
45
- %w{fallthru gh_cli}.each do |opt|
85
+ DEPRECATED_OPTIONS.each do |opt|
46
86
  next unless data.key?(opt)
47
87
 
48
88
  if ignore_deprecated_options.include?(opt)
@@ -57,29 +97,6 @@ class SugarJar
57
97
  'suppress this warning with `ignore_deprecated_options`.',
58
98
  )
59
99
  end
60
-
61
- # github_host has special handling
62
- return unless data['github_host']
63
-
64
- if ignore_deprecated_options.include?('github_host')
65
- SugarJar::Log.debug(
66
- "#{fname}: Deprecated option `github_host` found, but not " +
67
- 'warning due to `ignore_deprecated_options` in that file.',
68
- )
69
- elsif data.key?('forge_host')
70
- SugarJar::Log.warn(
71
- "#{fname}: Deprecated option `github_host` found. " +
72
- 'Ignoring in favor of newer `force_host` option. You can ' +
73
- 'suppress this warning with `ignore_deprecated_options`.',
74
- )
75
- else
76
- SugarJar::Log.warn(
77
- "#{fname}: Deprecated option `github_host` found. " +
78
- 'Treating it as if it was `forge_host` for now. Please update ' +
79
- 'your config file to use this new option. You can suppress ' +
80
- 'this warning with `ignore_deprecated_options`.',
81
- )
82
- end
83
100
  end
84
101
  end
85
102
  end
@@ -0,0 +1,98 @@
1
+ require 'mixlib/shellout'
2
+ require_relative 'util'
3
+ require_relative 'log'
4
+
5
+ class SugarJar
6
+ class Forge
7
+ attr_reader :type
8
+
9
+ CMD_MAP = {
10
+ 'github' => 'gh',
11
+ 'gitlab' => 'glab',
12
+ 'forgejo' => 'fj',
13
+ }.freeze
14
+
15
+ def initialize(type, host)
16
+ @type = type
17
+ @host = host
18
+ assert_cli_avail!
19
+ end
20
+
21
+ def cmd
22
+ @cmd ||= CMD_MAP[@type]
23
+ end
24
+
25
+ def all_versions
26
+ CMD_MAP.values.map do |cli|
27
+ bin = SugarJar::Util.which_nofail(cli)
28
+ _forge_nofail(cli, 'version').stdout if bin
29
+ end.compact
30
+ end
31
+
32
+ def run_nofail(*)
33
+ _forge_nofail(cmd, *)
34
+ end
35
+
36
+ def run(*)
37
+ s = _forge_nofail(cmd, *)
38
+ s.error!
39
+ s
40
+ end
41
+
42
+ def run_with_system(*args)
43
+ SugarJar::Log.trace("Running: #{cmd} #{args.join(' ')}")
44
+ system(cmd, *args)
45
+ end
46
+
47
+ private
48
+
49
+ def _forge_nofail(cli, *args)
50
+ SugarJar::Log.trace("Running: #{cli} #{args.join(' ')}")
51
+ bin = SugarJar::Util.which_nofail(cli)
52
+ s = Mixlib::ShellOut.new([bin] + args).run_command
53
+ if s.error? && ["#{cli} auth", 'Unauthorized'].any? do |err|
54
+ s.stderr.include?(err)
55
+ end
56
+ SugarJar::Log.info(
57
+ "#{cli} was run but no gitlab token exists. Will run " +
58
+ "'#{cli} auth login' to force\n#{cli} to authenticate...",
59
+ )
60
+ args = if cli == 'fj'
61
+ [bin, '-H', @host, 'auth', 'login']
62
+ else
63
+ [bin, 'auth', 'login', '-p', 'ssh']
64
+ end
65
+ unless system(*args)
66
+ SugarJar::Log.fatal(
67
+ "That failed, I will bail out. #{cli} needs to get a " +
68
+ "token. Try running '#{cli} auth login' (will list info about " +
69
+ 'your account) and try this again when that works.',
70
+ )
71
+ exit(1)
72
+ end
73
+
74
+ if cli == 'fj'
75
+ fj = Mixlib::ShellOut.new([bin, 'auth', 'list']).run_command
76
+ unless fj.stdout.include?(@host)
77
+ SugarJar::Log.fatal(
78
+ 'Logging in for you failed - this usually means you need to ' +
79
+ 'create a token manually - the instructions should have been ' +
80
+ 'printed above. Setup the token and then try again.',
81
+ )
82
+ exit(1)
83
+ end
84
+ end
85
+ end
86
+ s
87
+ end
88
+
89
+ def assert_cli_avail!
90
+ return if SugarJar::Util.which_nofail(cmd)
91
+
92
+ SugarJar::Log.error(
93
+ "Forge CLI #{cmd} is unavailable, please install",
94
+ )
95
+ exit 1
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,29 @@
1
+ require_relative 'util'
2
+
3
+ class SugarJar
4
+ module Git
5
+ def self.run_nofail(*args, color: true)
6
+ if %w{diff log grep branch}.include?(args[0]) &&
7
+ args.none? { |x| x.include?('color') }
8
+ args << (color ? '--color' : '--no-color')
9
+ end
10
+ SugarJar::Log.trace("Running: git #{args.join(' ')}")
11
+ Mixlib::ShellOut.new([SugarJar::Util.which('git')] + args).run_command
12
+ end
13
+
14
+ def self.run(*, color: true)
15
+ s = run_nofail(*, :color => color)
16
+ s.error!
17
+ s
18
+ end
19
+
20
+ def self.in_repo?
21
+ s = run_nofail('rev-parse', '--is-inside-work-tree')
22
+ !s.error? && s.stdout.strip == 'true'
23
+ end
24
+
25
+ def self.repo_root
26
+ run('rev-parse', '--show-toplevel').stdout.strip
27
+ end
28
+ end
29
+ end