sugarjar 2.0.2 → 3.0.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.
@@ -22,12 +22,12 @@ class SugarJar
22
22
  exit(1)
23
23
  end
24
24
  end
25
- exit(1) unless run_check('lint')
25
+ exit(1) unless run_check('lint', false)
26
26
  end
27
27
 
28
28
  def unit
29
29
  assert_in_repo!
30
- exit(1) unless run_check('unit')
30
+ exit(1) unless run_check('unit', false)
31
31
  end
32
32
 
33
33
  def get_checks_from_command(type)
@@ -71,7 +71,13 @@ class SugarJar
71
71
  @checks[type]
72
72
  end
73
73
 
74
- def run_check(type)
74
+ # autorun is true when we're running from push, and false when someone
75
+ # ran 'lint' or 'unit' directly
76
+ #
77
+ # In the case of a autorun, if a linter changes the code, we require
78
+ # either the user amend, or bail out. If it's a manual run, then we
79
+ # allow them to just go on.
80
+ def run_check(type, autorun)
75
81
  repo_root = SugarJar::Util.repo_root
76
82
  Dir.chdir repo_root do
77
83
  checks = get_checks(type)
@@ -81,6 +87,7 @@ class SugarJar
81
87
 
82
88
  checks.each do |check|
83
89
  SugarJar::Log.debug("Running #{type} #{check}")
90
+ skip_redo = false
84
91
 
85
92
  short = check.split.first
86
93
  if short.include?('/')
@@ -104,10 +111,15 @@ class SugarJar
104
111
  )
105
112
  puts git('diff').stdout
106
113
  loop do
107
- $stdout.print(
108
- "\nWould you like to\n\t[q]uit and inspect\n\t[a]mend the " +
109
- "changes to the current commit and re-run\n > ",
110
- )
114
+ options = [
115
+ '[q]uit and inspect',
116
+ '[a]mend the changes to the current commit and re-run',
117
+ ]
118
+ options << '[i]gnore the changes and keep going' unless autorun
119
+
120
+ msg = "\nWould you like to\n\t" + options.join("\n\t") + "\n > "
121
+
122
+ $stdout.print(msg)
111
123
  ans = $stdin.gets.strip
112
124
  case ans
113
125
  when /^q/
@@ -118,9 +130,14 @@ class SugarJar
118
130
  # break here, if we get out of this loop we 'redo', assuming
119
131
  # the user chose this option
120
132
  break
133
+ when /^i/
134
+ unless autorun
135
+ skip_redo = true
136
+ break
137
+ end
121
138
  end
122
139
  end
123
- redo
140
+ redo unless skip_redo
124
141
  end
125
142
 
126
143
  if s.error?
@@ -4,7 +4,7 @@ class SugarJar
4
4
  class Commands
5
5
  def debuginfo(*args)
6
6
  puts "sugarjar version #{SugarJar::VERSION}"
7
- puts ghcli('version').stdout
7
+ puts forge('version').stdout
8
8
  puts git('version').stdout
9
9
 
10
10
  puts "Config: #{JSON.pretty_generate(args[0])}"
@@ -5,10 +5,32 @@ class SugarJar
5
5
  SugarJar::Log.debug("Feature: #{name}, #{base}")
6
6
  name = fprefix(name)
7
7
  die("#{name} already exists!") if all_local_branches.include?(name)
8
+ rel_branches = release_branches
8
9
  if base
9
- fbase = fprefix(base)
10
- base = fbase if all_local_branches.include?(fbase)
10
+ # If the user specified a base branch (sf mything base)
11
+ # we check if <base> is a release branch and if so, we make
12
+ # this track <upstream>/<base>
13
+ if rel_branches.include?(base)
14
+ newbase = "#{upstream}/#{base}"
15
+ SugarJar::Log.info(
16
+ "Base branch #{base} is a release branch, setting it to track " +
17
+ newbase,
18
+ )
19
+ base = newbase
20
+ else
21
+ fbase = fprefix(base)
22
+ base = fbase if all_local_branches.include?(fbase)
23
+ end
24
+ elsif rel_branches.include?(name)
25
+ # If the user did NOT specify a base *and* this new feature is
26
+ # a release branch, check it out tracking the upstream release
27
+ # branch instead of main
28
+ base = "#{upstream}/#{name}"
29
+ SugarJar::Log.info(
30
+ "Feature #{name} is a release branch, setting it to track #{base}",
31
+ )
11
32
  else
33
+ # otherwise, fallback to most-main
12
34
  base ||= most_main
13
35
  end
14
36
  # If our base is a local branch, don't try to parse it for a remote name
@@ -25,6 +47,7 @@ class SugarJar
25
47
  end
26
48
  alias f feature
27
49
 
50
+ # alias for "feature <current_branch>'
28
51
  def subfeature(name)
29
52
  assert_in_repo!
30
53
  SugarJar::Log.debug("Subfature: #{name}")
@@ -43,7 +43,7 @@ class SugarJar
43
43
  def run_prepush
44
44
  @repo_config['on_push']&.each do |item|
45
45
  SugarJar::Log.debug("Running on_push check type #{item}")
46
- unless run_check(item)
46
+ unless run_check(item, true)
47
47
  SugarJar::Log.info("[prepush]: #{item} #{color('failed', :red)}.")
48
48
  return false
49
49
  end
@@ -1,9 +1,15 @@
1
1
  class SugarJar
2
2
  class Commands
3
- def smartclone(repo, dir = nil, *)
4
- reponame = File.basename(repo, '.git')
5
- dir ||= reponame
3
+ def smartclone(repo, *args)
6
4
  org = extract_org(repo)
5
+ reponame = extract_repo(repo)
6
+ dir = if args.length.positive? && !args.first.start_with?('-')
7
+ args.shift
8
+ else
9
+ reponame
10
+ end
11
+
12
+ forkname = @fork_name || reponame
7
13
 
8
14
  SugarJar::Log.info("Cloning #{reponame}...")
9
15
 
@@ -13,18 +19,69 @@ class SugarJar
13
19
  #
14
20
  # Unless the repo is in our own org and cannot be forked, then it
15
21
  # will fail.
16
- if org == @ghuser
17
- git('clone', canonicalize_repo(repo), dir, *)
18
- else
19
- ghcli('repo', 'fork', '--clone', canonicalize_repo(repo), dir, *)
22
+ if @use_forks && @forge_user != org
23
+ if @repo_forge == 'gitlab'
24
+ _gitlab_clone(org, repo, dir, forkname, *args)
25
+ else
26
+ forge(
27
+ 'repo', 'fork', '--clone', canonicalize_repo(repo), dir,
28
+ '--fork-name', forkname, *args
29
+ )
30
+ end
31
+
20
32
  # make the main branch track upstream
21
33
  Dir.chdir dir do
22
34
  git('branch', '-u', "upstream/#{main_branch}")
23
35
  end
36
+ else
37
+ git('clone', canonicalize_repo(repo), dir, *args)
24
38
  end
25
39
 
26
40
  SugarJar::Log.info('Remotes "origin" and "upstream" configured.')
27
41
  end
28
42
  alias sclone smartclone
43
+
44
+ def _gitlab_clone(_org, repo, dir, forkname, *)
45
+ # The gitlab CLI is much less forgiving about already-forked
46
+ # repos, and it has no option to clone to a differently-named
47
+ # directory. So we have to special case it.
48
+
49
+ extract_repo(repo)
50
+
51
+ # glab requires a short-name for the fork command...
52
+ shortname = repo_shortname(repo)
53
+
54
+ # We call fork without --clone since --clone can't clone
55
+ # to another directory. Also, we must specify =false, or it
56
+ # will prompt
57
+ s = forge_nofail(
58
+ 'repo', 'fork', shortname, '--clone=false', '--name', forkname
59
+ )
60
+
61
+ # It fails with:
62
+ # 409 {message: [Project namespace name has already been taken,
63
+ # Name has already been taken, Path has already been taken]}
64
+ #
65
+ # when there's already a fork... or if you happen to have a name
66
+ # collision. There's no way to tell, so we assume it means we've
67
+ # already forked.
68
+ if s.error?
69
+ if s.stderr.include?(' 409 ')
70
+ SugarJar::Log.debug('Forking failed, probably already forked')
71
+ else
72
+ s.error!
73
+ end
74
+ end
75
+
76
+ # Now we clone ourselves...
77
+ git('clone', canonicalize_repo(repo), dir, *)
78
+ Dir.chdir dir do
79
+ # and then configure remotes properly
80
+ git('remote', 'rename', 'origin', 'upstream')
81
+
82
+ fork_url = forked_repo(repo, @forge_user)
83
+ git('remote', 'add', 'origin', fork_url)
84
+ end
85
+ end
29
86
  end
30
87
  end
@@ -60,10 +60,25 @@ class SugarJar
60
60
 
61
61
  # <org>:<branch> is the GH API syntax for:
62
62
  # look for a branch of name <branch>, from a fork in owner <org>
63
- args.unshift('--head', "#{push_org}:#{curr}")
64
- SugarJar::Log.trace("Running: gh pr create #{args.join(' ')}")
65
- gh = SugarJar::Util.which('gh')
66
- system(gh, 'pr', 'create', *args)
63
+ if @repo_forge == 'github'
64
+ # On GitHub, the head is the org and the *BRANCH* name to use as
65
+ # the head branch...
66
+ args.unshift('--head', "#{push_org}:#{curr}")
67
+ else
68
+ # On GitLab, the head is the repo (org/repo) to use as the head
69
+ # _repo_, and then branch is configured seperately (with -s), but
70
+ # we don't need that since it defaults to the local branch name.
71
+ #
72
+ # Then we need --yes for it to not prompt us
73
+ args.unshift('--head', "#{push_org}/#{repo_name}", '--yes')
74
+ end
75
+
76
+ bin = SugarJar::Util.which(_forge_cmd)
77
+ subcmd = _pr_cmd
78
+ SugarJar::Log.trace(
79
+ "Running: #{bin} #{subcmd} create #{args.join(' ')}",
80
+ )
81
+ system(bin, subcmd, 'create', *args)
67
82
  end
68
83
 
69
84
  alias spr smartpullrequest
@@ -71,6 +86,10 @@ class SugarJar
71
86
 
72
87
  private
73
88
 
89
+ def _pr_cmd
90
+ @repo_forge == 'gitlab' ? 'mr' : 'pr'
91
+ end
92
+
74
93
  def assert_common_main_branch!
75
94
  upstream_branch = main_remote_branch(upstream)
76
95
  unless main_branch == upstream_branch
@@ -84,10 +103,14 @@ class SugarJar
84
103
  "\tgit remote set-head #{upstream} -a",
85
104
  )
86
105
  end
87
- return if upstream_branch == 'origin'
106
+
107
+ # If we don't have an upstream and an origin, we're done
108
+ return if upstream == 'origin'
88
109
 
89
110
  origin_branch = main_remote_branch('origin')
90
- return if origin_branch == upstream_branch
111
+ # NOTE: that on GL, forks don't fork any branches by default, even
112
+ # a main one, so if it's 'nil', then ignore.
113
+ return if origin_branch.nil? || origin_branch == upstream_branch
91
114
 
92
115
  die(
93
116
  "The main branch of your upstream (#{upstream_branch}) and your " +
@@ -53,21 +53,53 @@ class SugarJar
53
53
  end
54
54
  end
55
55
 
56
- def sync
56
+ def fsync
57
+ sync(:force => true)
58
+ end
59
+ alias forcesync fsync
60
+
61
+ def sync(force: false)
57
62
  assert_in_repo!
58
63
  dirty_check!
59
64
 
60
65
  src = "origin/#{current_branch}"
61
66
  fetch('origin')
62
- s = git_nofail('merge-base', '--is-ancestor', 'HEAD', src)
63
- if s.error?
64
- SugarJar::Log.debug(
65
- "Choosing rebase sync since this isn't a direct ancestor",
66
- )
67
- rebase(src)
68
- else
69
- SugarJar::Log.debug('Choosing reset sync since this is an ancestor')
67
+ want_reset = false
68
+ if force
69
+ SugarJar::Log.debug('Forcing reset instead of rebase at user request')
70
+ want_reset = true
71
+ end
72
+
73
+ unless force
74
+ s = git_nofail('merge-base', '--is-ancestor', 'HEAD', src)
75
+ # if this IS an ancestor, we can just force reset.
76
+ #
77
+ # otherwise, we attempt a rebase to not lose anything (unless
78
+ # force is set)
79
+ if s.error?
80
+ SugarJar::Log.debug(
81
+ "Choosing rebase sync since this isn't a direct ancestor",
82
+ )
83
+ else
84
+ SugarJar::Log.debug('Choosing reset sync since this is an ancestor')
85
+ want_reset = true
86
+ end
87
+ end
88
+
89
+ if want_reset
70
90
  git('reset', '--hard', src)
91
+ else
92
+ rebase(src)
93
+ s = git_nofail('rev-parse', '--verify', 'REBASE_HEAD')
94
+ unless s.error?
95
+ SugarJar::Log.info(
96
+ 'Rebase required input. You may continue the rebase from' +
97
+ ' here normally, or you may abort (`git rebase --abort`)' +
98
+ ' and instead to `sj fsync` to skip a rebase and force' +
99
+ ' reset to the remote branch.',
100
+ )
101
+ return
102
+ end
71
103
  end
72
104
  SugarJar::Log.info("Synced to #{src}.")
73
105
  end
@@ -85,7 +117,7 @@ class SugarJar
85
117
  # rubocop:enable Style/HashSyntax
86
118
  unless base
87
119
  SugarJar::Log.info(
88
- 'The brach we were tracking is gone, resetting tracking to ' +
120
+ 'The branch we were tracking is gone, resetting tracking to ' +
89
121
  most_main,
90
122
  )
91
123
  git('branch', '-u', most_main)
@@ -33,16 +33,32 @@ class SugarJar
33
33
  @pr_autofill = options['pr_autofill']
34
34
  @pr_autostack = options['pr_autostack']
35
35
  @feature_prefix = options['feature_prefix']
36
+ @use_forks = options['use_forks']
37
+ @fork_name = options['fork_name']
36
38
  @checks = {}
37
39
  @main_branch = nil
38
40
  @main_remote_branches = {}
39
- @ghuser = @repo_config['github_user'] || options['github_user']
40
- @ghhost = @repo_config['github_host'] || options['github_host']
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
41
53
 
42
- die("No 'gh' found, please install 'gh'") unless gh_avail?
54
+ user_option = "#{@repo_forge}_user"
55
+ @forge_user = @repo_config[user_option] || options[user_option]
43
56
 
44
- # Tell the 'gh' cli where to talk to, if not github.com
45
- ENV['GH_HOST'] = @ghhost if @ghhost
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
61
+ end
46
62
 
47
63
  return if options['no_change']
48
64
 
@@ -52,12 +68,8 @@ class SugarJar
52
68
  private
53
69
 
54
70
  def forked_repo(repo, username)
55
- repo = if repo.start_with?('http', 'git@')
56
- File.basename(repo)
57
- else
58
- "#{File.basename(repo)}.git"
59
- end
60
- "git@#{@ghhost || 'github.com'}:#{username}/#{repo}"
71
+ repo = extract_repo(repo)
72
+ "git@#{forge_host}:#{username}/#{repo}.git"
61
73
  end
62
74
 
63
75
  # gh utils will default to https, but we should always default to SSH
@@ -66,12 +78,34 @@ class SugarJar
66
78
  # if they fully-qualified it, we're good
67
79
  return repo if repo.start_with?('http', 'git@')
68
80
 
69
- # otherwise, ti's a shortname
70
- cr = "git@#{@ghhost || 'github.com'}:#{repo}.git"
81
+ # otherwise, it's a shortname
82
+ cr = "git@#{forge_host}:#{repo}.git"
71
83
  SugarJar::Log.debug("canonicalized #{repo} to #{cr}")
72
84
  cr
73
85
  end
74
86
 
87
+ def forge_host
88
+ # if one is specifically configured, use that
89
+ return @_forge_host if @_forge_host
90
+
91
+ # otherwise, if we're in a repo, use the hostname of the remote
92
+ if SugarJar::Util.in_repo?
93
+ url = remote_url_map.values.first
94
+ return extract_host(url)
95
+ end
96
+
97
+ @repo_forge == 'gitlab' ? 'gitlab.com' : 'github.com'
98
+ end
99
+
100
+ def repo_shortname(repo)
101
+ # if it's already a shortname, return
102
+ return repo unless repo.start_with?('http', 'git@')
103
+
104
+ repo_name = extract_repo(repo)
105
+ org_name = extract_org(repo)
106
+ [org_name, repo_name].join('/')
107
+ end
108
+
75
109
  def set_commit_template
76
110
  unless SugarJar::Util.in_repo?
77
111
  SugarJar::Log.debug('Skipping set_commit_template: not in repo')
@@ -137,7 +171,7 @@ class SugarJar
137
171
  end
138
172
 
139
173
  def determine_main_branch(branches)
140
- branches.include?('main') ? 'main' : 'master'
174
+ branches.include?('master') ? 'master' : 'main'
141
175
  end
142
176
 
143
177
  def main_branch
@@ -263,7 +297,7 @@ class SugarJar
263
297
 
264
298
  # Whatever org we push to, regardless of if this is a fork or not
265
299
  def push_org
266
- url = git('remote', 'get-url', 'origin').stdout.strip
300
+ url = remote_url_map['origin']
267
301
  extract_org(url)
268
302
  end
269
303
 
@@ -282,8 +316,8 @@ class SugarJar
282
316
  end
283
317
  end
284
318
 
285
- def gh_avail?
286
- !!SugarJar::Util.which_nofail('gh')
319
+ def forge_cli_avail?
320
+ !!SugarJar::Util.which_nofail(_forge_cmd)
287
321
  end
288
322
 
289
323
  def fprefix(name)
@@ -310,12 +344,12 @@ class SugarJar
310
344
 
311
345
  def extract_org(repo)
312
346
  if repo.start_with?('http')
313
- File.basename(File.dirname(repo))
347
+ repo.split('://').last.split('/')[1..-2].join('/')
314
348
  elsif repo.start_with?('git@')
315
- repo.split(':')[1].split('/')[0]
349
+ repo.split(':').last.split('/')[0..-2].join('/')
316
350
  else
317
- # assume they passed in a ghcli-friendly name
318
- repo.split('/').first
351
+ # assume they passed in a cli-friendly shortname
352
+ repo.split('/')[0..-2].join('/')
319
353
  end
320
354
  end
321
355
 
@@ -323,11 +357,23 @@ class SugarJar
323
357
  File.basename(repo, '.git')
324
358
  end
325
359
 
360
+ def extract_host(repo)
361
+ if repo.start_with?('git@')
362
+ repo.split(':').first.split('@').last
363
+ elsif repo.start_with?('http')
364
+ repo.split('/')[2]
365
+ end
366
+ end
367
+
326
368
  def die(msg)
327
369
  SugarJar::Log.fatal(msg)
328
370
  exit(1)
329
371
  end
330
372
 
373
+ def release_branches
374
+ @repo_config['release_branches'] || []
375
+ end
376
+
331
377
  def worktree_branches
332
378
  worktrees.values.map do |wt|
333
379
  branch_from_ref(wt['branch'])
@@ -373,12 +419,34 @@ class SugarJar
373
419
  SugarJar::Util.git_nofail(*, :color => @color)
374
420
  end
375
421
 
376
- def ghcli(*)
377
- SugarJar::Util.ghcli(*)
422
+ def _determine_forge_type
423
+ if SugarJar::Util.in_repo?
424
+ gl = remote_url_map.values.any? { |x| x.include?('gitlab') }
425
+ return gl ? 'gitlab' : 'github'
426
+ end
427
+
428
+ # if all else fails, guess GH
429
+ 'github'
430
+ end
431
+
432
+ def _forge_cmd
433
+ @repo_forge == 'gitlab' ? 'glab' : 'gh'
378
434
  end
379
435
 
380
- def ghcli_nofail(*)
381
- SugarJar::Util.ghcli_nofail(*)
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
382
450
  end
383
451
  end
384
452
  end
@@ -7,9 +7,11 @@ 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,
14
+ 'use_forks' => true,
13
15
  'ignore_deprecated_options' => [],
14
16
  }.freeze
15
17
 
@@ -27,6 +29,10 @@ class SugarJar
27
29
  SugarJar::Log.debug("Loading config #{f}")
28
30
  data = YAML.safe_load_file(f)
29
31
  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
30
36
  # an empty file is a `nil` which you can't merge
31
37
  c.merge!(YAML.safe_load_file(f)) if data
32
38
  SugarJar::Log.debug("Modified config: #{c}")
@@ -41,14 +47,37 @@ class SugarJar
41
47
 
42
48
  if ignore_deprecated_options.include?(opt)
43
49
  SugarJar::Log.debug(
44
- "Not warning about deprecated option '#{opt}' in #{fname} due to " +
45
- '"ignore_deprecated_options" in that file.',
50
+ "#{fname}: Not warning about deprecated option `#{opt}` due to " +
51
+ '`ignore_deprecated_options` in that file.',
46
52
  )
47
53
  next
48
54
  end
49
55
  SugarJar::Log.warn(
50
- "Config file #{fname} contains deprecated option #{opt}. You can " +
51
- 'suppress this warning with ignore_deprecated_options.',
56
+ "#{fname}: contains deprecated option `#{opt}`. You can " +
57
+ 'suppress this warning with `ignore_deprecated_options`.',
58
+ )
59
+ 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`.',
52
81
  )
53
82
  end
54
83
  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