sugarjar 3.0.0 → 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,37 +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']
33
+
36
34
  @checks = {}
37
35
  @main_branch = nil
38
36
  @main_remote_branches = {}
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
37
+ @host_config = nil
51
38
 
52
- user_option = "#{@repo_forge}_user"
53
- @forge_user = @repo_config[user_option] || options[user_option]
54
-
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
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
59
45
  end
60
46
 
61
47
  return if options['no_change']
@@ -65,54 +51,85 @@ class SugarJar
65
51
 
66
52
  private
67
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
+
68
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
+
69
90
  repo = extract_repo(repo)
70
- "git@#{forge_host}:#{username}/#{repo}.git"
91
+
92
+ "git@#{host}:#{username}/#{repo}.git"
71
93
  end
72
94
 
73
95
  # gh utils will default to https, but we should always default to SSH
74
96
  # unless otherwise specified since https will cause prompting.
75
97
  def canonicalize_repo(repo)
76
98
  # if they fully-qualified it, we're good
77
- return repo if repo.start_with?('http', 'git@')
99
+ return repo if repo.start_with?('http', 'git@', 'ssh://')
78
100
 
101
+ host = @host_config&.dig('forge_host') || extract_host(repo) ||
102
+ @config['default_forge_host']
79
103
  # otherwise, it's a shortname
80
- cr = "git@#{forge_host}:#{repo}.git"
104
+ cr = "git@#{host}:#{repo}.git"
81
105
  SugarJar::Log.debug("canonicalized #{repo} to #{cr}")
82
106
  cr
83
107
  end
84
108
 
85
- def forge_host
86
- # if one is specifically configured, use that
87
- 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.
88
113
 
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'
114
+ # if we're in a repo, use the hostname of the remote
115
+ if SugarJar::Git.in_repo?
116
+ url = remote_url_map.values.first
117
+ return extract_host(url)
94
118
  end
119
+ @config['default_forge_host']
95
120
  end
96
121
 
97
122
  def repo_shortname(repo)
98
123
  # if it's already a shortname, return
99
124
  return repo unless repo.start_with?('http', 'git@')
100
125
 
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}"
126
+ repo_name = extract_repo(repo)
127
+ org_name = extract_org(repo)
128
+ [org_name, repo_name].join('/')
112
129
  end
113
130
 
114
131
  def set_commit_template
115
- unless SugarJar::Util.in_repo?
132
+ unless SugarJar::Git.in_repo?
116
133
  SugarJar::Log.debug('Skipping set_commit_template: not in repo')
117
134
  return
118
135
  end
@@ -120,7 +137,10 @@ class SugarJar
120
137
  realpath = if @repo_config['commit_template'].start_with?('/')
121
138
  @repo_config['commit_template']
122
139
  else
123
- "#{Util.repo_root}/#{@repo_config['commit_template']}"
140
+ File.join(
141
+ SugarJar::Git.repo_root,
142
+ @repo_config['commit_template'],
143
+ )
124
144
  end
125
145
  unless File.exist?(realpath)
126
146
  die(
@@ -153,7 +173,7 @@ class SugarJar
153
173
  end
154
174
 
155
175
  def assert_in_repo!
156
- return if SugarJar::Util.in_repo?
176
+ return if SugarJar::Git.in_repo?
157
177
 
158
178
  die('sugarjar must be run from inside a git repo')
159
179
  end
@@ -161,7 +181,7 @@ class SugarJar
161
181
  def dirty_check!
162
182
  return unless dirty?
163
183
 
164
- if @ignore_dirty
184
+ if @config['ignore_dirty']
165
185
  SugarJar::Log.warn(
166
186
  'Your repo is dirty, but --ignore-dirty was specified, so ' +
167
187
  'carrying on anyway.',
@@ -310,8 +330,14 @@ class SugarJar
310
330
  extract_org(url)
311
331
  end
312
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
+
313
339
  def color(string, *colors)
314
- if @color
340
+ if @config['color']
315
341
  pastel.decorate(string, *colors)
316
342
  else
317
343
  string
@@ -325,17 +351,13 @@ class SugarJar
325
351
  end
326
352
  end
327
353
 
328
- def forge_cli_avail?
329
- !!SugarJar::Util.which_nofail(_forge_cmd)
330
- end
331
-
332
354
  def fprefix(name)
333
- return name unless @feature_prefix
355
+ return name unless @host_config['feature_prefix']
334
356
 
335
- return name if name.start_with?(@feature_prefix)
357
+ return name if name.start_with?(@host_config['feature_prefix'])
336
358
  return name if all_local_branches.include?(name)
337
359
 
338
- newname = "#{@feature_prefix}#{name}"
360
+ newname = "#{@host_config['feature_prefix']}#{name}"
339
361
  SugarJar::Log.debug(
340
362
  "Munging feature name: #{name} -> #{newname} due to feature prefix",
341
363
  )
@@ -347,18 +369,18 @@ class SugarJar
347
369
  s.error?
348
370
  end
349
371
 
350
- def repo_name
351
- SugarJar::Util.repo_root.split('/').last
372
+ def repo_dir_name
373
+ SugarJar::Git.repo_root.split('/').last
352
374
  end
353
375
 
354
376
  def extract_org(repo)
355
377
  if repo.start_with?('http')
356
- File.basename(File.dirname(repo))
378
+ repo.split('://').last.split('/')[1..-2].join('/')
357
379
  elsif repo.start_with?('git@')
358
- repo.split(':')[1].split('/')[0]
380
+ repo.split(':').last.split('/')[0..-2].join('/')
359
381
  else
360
- # assume they passed in a ghcli-friendly name
361
- repo.split('/').first
382
+ # assume they passed in a cli-friendly shortname
383
+ repo.split('/')[0..-2].join('/')
362
384
  end
363
385
  end
364
386
 
@@ -369,6 +391,8 @@ class SugarJar
369
391
  def extract_host(repo)
370
392
  if repo.start_with?('git@')
371
393
  repo.split(':').first.split('@').last
394
+ elsif repo.start_with?('ssh://')
395
+ repo.split('/')[2].split('@').last
372
396
  elsif repo.start_with?('http')
373
397
  repo.split('/')[2]
374
398
  end
@@ -390,7 +414,7 @@ class SugarJar
390
414
  end
391
415
 
392
416
  def worktrees
393
- root = SugarJar::Util.repo_root
417
+ root = SugarJar::Git.repo_root
394
418
  s = git('worktree', 'list', '--porcelain')
395
419
  s.error!
396
420
  worktrees = {}
@@ -421,43 +445,33 @@ class SugarJar
421
445
  end
422
446
 
423
447
  def git(*)
424
- SugarJar::Util.git(*, :color => @color)
448
+ SugarJar::Git.run(*, :color => @config['color'])
425
449
  end
426
450
 
427
451
  def git_nofail(*)
428
- SugarJar::Util.git_nofail(*, :color => @color)
429
- end
430
-
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'
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
440
465
  end
441
- end
442
-
443
- def _forge_cmd
444
- @repo_forge == 'gitlab' ? 'glab' : 'gh'
445
- end
446
466
 
447
- def forge(*)
448
- if @repo_forge == 'gitlab'
449
- SugarJar::Util.glcli(*)
450
- else
451
- SugarJar::Util.ghcli(*)
467
+ if SugarJar::Git.in_repo?
468
+ gl = remote_url_map.values.any? { |x| x.include?('gitlab') }
469
+ return gl ? 'gitlab' : 'github'
452
470
  end
453
- end
454
471
 
455
- def forge_nofail(*)
456
- if @repo_forge == 'gitlab'
457
- SugarJar::Util.glcli_nofail(*)
458
- else
459
- SugarJar::Util.ghcli_nofail(*)
460
- 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!')
461
475
  end
462
476
  end
463
477
  end
@@ -1,24 +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
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
15
36
  }.freeze
16
37
 
17
38
  def self._find_ordered_files
39
+ real_files = []
18
40
  [
19
- '/etc/sugarjar/config.yaml',
20
- "#{Dir.home}/.config/sugarjar/config.yaml",
21
- ].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
22
50
  end
23
51
 
24
52
  def self.config
@@ -27,21 +55,34 @@ class SugarJar
27
55
  _find_ordered_files.each do |f|
28
56
  SugarJar::Log.debug("Loading config #{f}")
29
57
  data = YAML.safe_load_file(f)
58
+ next unless data
59
+
30
60
  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
35
- # an empty file is a `nil` which you can't merge
36
- c.merge!(YAML.safe_load_file(f)) if data
61
+ data = data.slice(*TOP_LEVEL_CONFIGS)
62
+ c.deep_merge!(data)
37
63
  SugarJar::Log.debug("Modified config: #{c}")
38
64
  end
39
65
  c
40
66
  end
41
67
 
42
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
+
43
84
  ignore_deprecated_options = data['ignore_deprecated_options'] || []
44
- %w{fallthru gh_cli}.each do |opt|
85
+ DEPRECATED_OPTIONS.each do |opt|
45
86
  next unless data.key?(opt)
46
87
 
47
88
  if ignore_deprecated_options.include?(opt)
@@ -56,29 +97,6 @@ class SugarJar
56
97
  'suppress this warning with `ignore_deprecated_options`.',
57
98
  )
58
99
  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`.',
80
- )
81
- end
82
100
  end
83
101
  end
84
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