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.
@@ -37,8 +37,11 @@ release_branches:
37
37
  # is assumed it is a relative path and `sj` will check that the file exists.
38
38
 
39
39
  lint:
40
+ # can be a string which is just a command to run...
40
41
  - scripts/run_rubocop.sh
41
- - scripts/run_mdl.sh
42
+ # want it to have a pretty name? Entries can be a hash instead...
43
+ - name: mdl
44
+ command: scripts/run_mdl.sh
42
45
 
43
46
  # `unit` is a list of scripts to run when `sj unit` is executed (or, if
44
47
  # configured to run on `sj spush`/`sj fpush`- see `on_push` below). Regardless
@@ -47,8 +50,11 @@ lint:
47
50
  # assumed it is a relative path and `sj` will check that the file exists.
48
51
 
49
52
  unit:
53
+ # like lint, supports bare strings...
50
54
  - bundle exec rspec
51
- - scripts/run_tests.sh
55
+ # or hashes...
56
+ - name: unittests
57
+ command: scripts/run_tests.sh
52
58
 
53
59
  # `lint_list_cmd` is like `lint`, except it's a command to run which will
54
60
  # determine the proper lints to run and return them, one per line. This is
@@ -74,20 +80,3 @@ on_push: [lint] # or [lint, unit]
74
80
  # template configured.
75
81
 
76
82
  commit_template: .git_commit_template.txt
77
-
78
- # `github_user` is the user to use when talking to GitHub. Overrides any such
79
- # setting in the regular SugarJar config. Most useful when in the
80
- # `include_from` file.
81
-
82
- github_user: myuser
83
-
84
- # `gitlab_user` is the user to use when talking to GitLab. Overrides any such
85
- # setting in the regular SugarJar config. Most useful when in the
86
- # `include_from` file.
87
-
88
- gitlab_user: myuser
89
-
90
- # `forge_host` is the GitHub/GitLab host to use when talking to hosted versions
91
- # of these services.
92
-
93
- forge_host: github.sample.com
@@ -10,26 +10,29 @@ _sugarjar_completions()
10
10
 
11
11
  local -a suggestions
12
12
 
13
- # grap the feature_prefix if we have one so that we
14
- # can let the user ignore that part. If we have `yq`
15
- # we'll use it as that's going to be always 100%
16
- # reliable, but if we don't, do our best with shell
17
- # utils
18
- local prefix=''
13
+ # grap any and all feature_prefixes so that we can let the user ignore that
14
+ # part. If we have `yq` we'll use it as that's going to be always 100%
15
+ # reliable, but if we don't, do our best with shell utils
16
+ local prefixes=''
19
17
  if [ -e "$SJCONFIG" ]; then
20
18
  if type yq &>/dev/null; then
21
- prefix=$(yq .feature_prefix $SJCONFIG)
19
+ # We don't use the '// empty' syntax here as thats only in
20
+ # very new yq, so use the more standard '| select(. != null)'
21
+ yq_search='[.host_configs[].feature_prefix | select(. != null)] | unique[]'
22
+ prefixes=$(yq -r "$yq_search" $SJCONFIG | xargs)
22
23
  else
23
- # the xargs removes extra spaces
24
- prefix=$(grep feature_prefix $SJCONFIG | cut -f2 -d: | xargs)
24
+ prefixes=$(
25
+ grep feature_prefix $SJCONFIG | cut -f2 -d: | sort -u | xargs
26
+ )
25
27
  fi
26
28
  fi
27
29
 
28
30
  case "${COMP_WORDS[1]}" in
29
31
  co|checkout|bclean)
30
32
  local branches=$(git branch | sed -e 's/* //g' | xargs)
31
- if [ -n "$prefix" ]; then
32
- local branches=$(echo $branches | sed -e "s!$prefix!!g")
33
+ if [ -n "$prefixes" ]; then
34
+ regex=$(printf '%s\n' $prefixes | paste -sd'|')
35
+ local branches=$(echo $branches | sed -E "s!($regex)!!g")
33
36
  fi
34
37
  suggestions=($(compgen -W "$branches" -- "${COMP_WORDS[2]}"))
35
38
  COMPREPLY=("${suggestions[@]}")
@@ -1,4 +1,4 @@
1
- require_relative '../util'
1
+ require_relative '../git'
2
2
 
3
3
  class SugarJar
4
4
  class Commands
@@ -7,7 +7,7 @@ class SugarJar
7
7
 
8
8
  # does not use dirty_check! as we want a custom message
9
9
  if dirty?
10
- if @ignore_dirty
10
+ if @config['ignore_dirty']
11
11
  SugarJar::Log.warn(
12
12
  'Your repo is dirty, but --ignore-dirty was specified, so ' +
13
13
  'carrying on anyway. If the linter autocorrects, the displayed ' +
@@ -48,7 +48,21 @@ class SugarJar
48
48
  )
49
49
  return false
50
50
  end
51
- s.stdout.split("\n")
51
+ output = s.stdout
52
+ if output.start_with?("---\n")
53
+ begin
54
+ checks = YAML.safe_load(s.stdout)
55
+ return checks
56
+ rescue Psych::SyntaxError => e
57
+ SugarJar::Log.error(
58
+ "#{type}_list_cmd (#{cmd}) produced invalid YAML: #{e.message}",
59
+ )
60
+ return false
61
+ end
62
+ end
63
+
64
+ # otherwise it's one line per command
65
+ output.split("\n")
52
66
  end
53
67
 
54
68
  # determine if we're using the _list_cmd and if so run it to get the
@@ -78,18 +92,26 @@ class SugarJar
78
92
  # either the user amend, or bail out. If it's a manual run, then we
79
93
  # allow them to just go on.
80
94
  def run_check(type, autorun)
81
- repo_root = SugarJar::Util.repo_root
95
+ repo_root = SugarJar::Git.repo_root
82
96
  Dir.chdir repo_root do
83
97
  checks = get_checks(type)
84
- # if we failed to determine the checks, the the checks have effectively
98
+ # if we failed to determine the checks, then the checks have effectively
85
99
  # failed
86
100
  return false unless checks
87
101
 
88
102
  checks.each do |check|
89
- SugarJar::Log.debug("Running #{type} #{check}")
103
+ if check.is_a?(Hash)
104
+ command = check['command']
105
+ name = check['name'] || command.split.first
106
+ else
107
+ command = check
108
+ name = check.split.first
109
+ end
110
+
111
+ SugarJar::Log.debug("Running #{type} #{name}: #{command}")
90
112
  skip_redo = false
91
113
 
92
- short = check.split.first
114
+ short = command.split.first
93
115
  if short.include?('/')
94
116
  short = File.join(repo_root, short) unless short.start_with?('/')
95
117
  unless File.exist?(short)
@@ -99,12 +121,12 @@ class SugarJar
99
121
  SugarJar::Log.error("Configured #{type} #{short} does not exist!")
100
122
  return false
101
123
  end
102
- s = Mixlib::ShellOut.new(check).run_command
124
+ s = Mixlib::ShellOut.new(command).run_command
103
125
 
104
126
  # Linters auto-correct, lets handle that gracefully
105
127
  if type == 'lint' && dirty?
106
128
  SugarJar::Log.info(
107
- "[#{type}] #{short}: #{color('Corrected', :yellow)}",
129
+ "[#{type}] #{name}: #{color('Corrected', :yellow)}",
108
130
  )
109
131
  SugarJar::Log.warn(
110
132
  "The linter modified the repo. Here's the diff:\n",
@@ -142,14 +164,14 @@ class SugarJar
142
164
 
143
165
  if s.error?
144
166
  SugarJar::Log.info(
145
- "[#{type}] #{short} #{color('failed', :red)}, output follows " +
167
+ "[#{type}] #{name} #{color('failed', :red)}, output follows " +
146
168
  "(see debug for more)\n#{s.stdout}",
147
169
  )
148
170
  SugarJar::Log.debug(s.format_for_exception)
149
171
  return false
150
172
  end
151
173
  SugarJar::Log.info(
152
- "[#{type}] #{short}: #{color('OK', :green)}",
174
+ "[#{type}] #{name}: #{color('OK', :green)}",
153
175
  )
154
176
  end
155
177
  end
@@ -4,7 +4,9 @@ class SugarJar
4
4
  class Commands
5
5
  def debuginfo(*args)
6
6
  puts "sugarjar version #{SugarJar::VERSION}"
7
- puts forge('version').stdout
7
+ forge.all_versions.each do |v|
8
+ puts v
9
+ end
8
10
  puts git('version').stdout
9
11
 
10
12
  puts "Config: #{JSON.pretty_generate(args[0])}"
@@ -0,0 +1,119 @@
1
+ class SugarJar
2
+ class Commands
3
+ def modernizeconfig(config)
4
+ require 'tempfile'
5
+ require 'diffy'
6
+
7
+ data = YAML.safe_load_file(config)
8
+ data = _modernize(data).to_yaml
9
+ tfile = Tempfile.new('sugarjar')
10
+ tfile.write(data)
11
+ tfile.close
12
+ diff = Diffy::Diff.new(
13
+ config,
14
+ tfile.path,
15
+ :source => 'files',
16
+ :context => 2,
17
+ :include_diff_info => true,
18
+ :format => @config['color'] ? 'color' : 'text',
19
+ ).to_s
20
+ puts "Here is the diff from your existing config to the new one:\n"
21
+ puts diff
22
+ options = [
23
+ '[u]pdate my config, saving my old one to .bak',
24
+ '[w]rite new config to a temporary file for me to inspect',
25
+ ]
26
+ msg = "\nWould you like to\n\t" + options.join("\n\t") + "\n > "
27
+ loop do
28
+ $stdout.print(msg)
29
+ ans = $stdin.gets.strip
30
+ case ans
31
+ when /^u/
32
+ old = "#{config}.bak"
33
+ File.rename(config, old)
34
+ File.write(config, data)
35
+ puts "#{config} updated. Old config in #{old}"
36
+ break
37
+ when /^w/
38
+ new = "#{config}.new"
39
+ File.write(new, data)
40
+ puts "New config written to #{new}"
41
+ break
42
+ end
43
+ end
44
+ tfile.unlink
45
+ end
46
+ alias fixconfig modernizeconfig
47
+
48
+ def _modernize(data)
49
+ data['host_configs'] ||= {}
50
+ data['host_configs']['default'] ||= {}
51
+
52
+ # First, take anything that used to be a top-level config
53
+ # that's now a host config and move it to host_configs->default
54
+ %w{use_forks feature_prefix}.each do |tlc|
55
+ # this needs to return true even if the value is false-y
56
+ next unless data.key?(tlc)
57
+
58
+ if data['host_configs']['default'][tlc]
59
+ if data['host_configs']['default'][tlc] != data[tlc]
60
+ SugarJar::Log.warn(
61
+ "host_configs->default->#{tlc} already set to something" +
62
+ " different than deprecated top-level #{tlc}, so just deleting" +
63
+ ' old top-level config.',
64
+ )
65
+ end
66
+ else
67
+ data['host_configs']['default'][tlc] = data[tlc]
68
+ end
69
+
70
+ # we delete it either way, just only warn if it's different
71
+ data.delete(tlc)
72
+ end
73
+
74
+ # Users move into the default config, plus other userless hosts
75
+ # of that type
76
+ %w{github gitlab}.each do |forge|
77
+ tlc = "#{forge}_user"
78
+ site = "#{forge}.com"
79
+ next unless data[tlc]
80
+
81
+ # start with the "default site" for this user (github.com/gitlab.com)
82
+ if data.dig('host_configs', site, 'user')
83
+ if data['host_configs'][site]['user'] != data[tlc]
84
+ SugarJar::Log.warn(
85
+ "host_configs->#{site}->user already set to something" +
86
+ " different than deprecated top-level #{tlc}, so just" +
87
+ ' deleting old top-level config.',
88
+ )
89
+ end
90
+ else
91
+ data['host_configs'][site] ||= {}
92
+ data['host_configs'][site]['user'] = data[tlc]
93
+ end
94
+
95
+ # now loop through any existing hosts that have been configured that
96
+ # are NOT those default hosts... IFthey don't have a user, populate
97
+ # with the relevant top-level user as in the old world, we would have
98
+ # fallen back to ${type}_user
99
+ data['host_configs'].each do |host, cfg|
100
+ next if host == site
101
+ next unless host.include?(forge)
102
+ next if cfg && cfg['user']
103
+
104
+ data['host_configs'][host] ||= {}
105
+ data['host_configs'][host]['user'] = data[tlc]
106
+ end
107
+
108
+ # and finally, delete the top-level config
109
+ data.delete(tlc)
110
+ end
111
+
112
+ # Remove any unused/deprecated configs.
113
+ #
114
+ # we don't use TOP_LEVEL_CONFIGS here as that includes things
115
+ # that are cli-only
116
+ data.slice(*SugarJar::Config::DEFAULTS.keys)
117
+ end
118
+ end
119
+ end
@@ -23,7 +23,7 @@ class SugarJar
23
23
  dirty_check!
24
24
 
25
25
  unless run_prepush
26
- if @ignore_prerun_failure
26
+ if @config['ignore_prerun_failure']
27
27
  SugarJar::Log.warn(
28
28
  'Pre-push checks failed, but --ignore-prerun-failure was ' +
29
29
  'specified, so carrying on anyway',
@@ -1,9 +1,34 @@
1
1
  class SugarJar
2
2
  class Commands
3
- def smartclone(repo, dir = nil, *)
4
- reponame = extract_repo(repo)
5
- dir ||= reponame
3
+ def smartclone(repo, *args)
6
4
  org = extract_org(repo)
5
+ reponame = extract_repo(repo)
6
+ host = extract_host(repo) || @config['default_forge_host']
7
+ unless host
8
+ raise 'Failed to determine host. Please specify --default-forge-host,' +
9
+ ' and file a bug.'
10
+ end
11
+ @host_config = gen_host_config(host)
12
+ raise 'Failed to determine forge_type' unless @host_config['forge_type']
13
+
14
+ # force object recreation - the initial creation would not
15
+ # have had the right data. Plus, we want it to check for the
16
+ # right cli
17
+ @forge = nil
18
+ forge
19
+
20
+ # set env vars of the host, just for completeness
21
+ # since it's short-lived we just set both vars
22
+ ENV['GH_HOST'] = host
23
+ ENV['GL_HOST'] = host
24
+
25
+ dir = if args.length.positive? && !args.first.start_with?('-')
26
+ args.shift
27
+ else
28
+ reponame
29
+ end
30
+
31
+ forkname = @config['fork_name'] || reponame
7
32
 
8
33
  SugarJar::Log.info("Cloning #{reponame}...")
9
34
 
@@ -13,37 +38,49 @@ class SugarJar
13
38
  #
14
39
  # Unless the repo is in our own org and cannot be forked, then it
15
40
  # will fail.
16
- if org == @forge_user
17
- git('clone', canonicalize_repo(repo), dir, *)
18
- else
19
- if @repo_forge == 'gitlab'
20
- _gitlab_clone(org, repo, dir, *)
41
+ if @host_config['use_forks'] && @host_config['user'] != org
42
+ case forge.type
43
+ when 'gitlab', 'forgejo'
44
+ _separate_fork_clone(org, repo, dir, forkname, *args)
21
45
  else
22
- forge('repo', 'fork', '--clone', canonicalize_repo(repo), dir, *)
46
+ forge.run(
47
+ 'repo', 'fork', '--clone', canonicalize_repo(repo), dir,
48
+ '--fork-name', forkname, *args
49
+ )
23
50
  end
24
51
 
25
52
  # make the main branch track upstream
26
53
  Dir.chdir dir do
27
54
  git('branch', '-u', "upstream/#{main_branch}")
28
55
  end
56
+ else
57
+ git('clone', canonicalize_repo(repo), dir, *args)
29
58
  end
30
59
 
31
60
  SugarJar::Log.info('Remotes "origin" and "upstream" configured.')
32
61
  end
33
62
  alias sclone smartclone
34
63
 
35
- def _gitlab_clone(_org, repo, dir, *)
64
+ def _separate_fork_clone(_org, repo, dir, forkname, *)
36
65
  # The gitlab CLI is much less forgiving about already-forked
37
66
  # repos, and it has no option to clone to a differently-named
38
67
  # directory. So we have to special case it.
39
68
 
69
+ extract_repo(repo)
70
+
40
71
  # glab requires a short-name for the fork command...
41
72
  shortname = repo_shortname(repo)
42
73
 
43
- # We call fork without --clone since --clone can't clone
44
- # to another directory. Also, we must specify =false, or it
45
- # will prompt
46
- s = forge_nofail('repo', 'fork', shortname, '--clone=false')
74
+ # For both gitlab and forgejo we fork without clone:
75
+ # * gitlab - it cannot clone to a chosen directory
76
+ # * forgjo - it doesn't have an option to fork and clone in one
77
+ #
78
+ # Also note that for gitlab we must be explicit, if we don't
79
+ # set --clone=false it will prompt.
80
+ repo_ref = forge.type == 'gitlab' ? shortname : repo
81
+ args = ['repo', 'fork', repo_ref, '--name', forkname]
82
+ args << '--clone=false' if forge.type == 'gitlab'
83
+ s = forge.run_nofail(*args)
47
84
 
48
85
  # It fails with:
49
86
  # 409 {message: [Project namespace name has already been taken,
@@ -53,7 +90,8 @@ class SugarJar
53
90
  # collision. There's no way to tell, so we assume it means we've
54
91
  # already forked.
55
92
  if s.error?
56
- if s.stderr.include?(' 409 ')
93
+ if %w{gitlab forgejo}.include?(forge.type) &&
94
+ s.stderr.include?(' 409 ')
57
95
  SugarJar::Log.debug('Forking failed, probably already forked')
58
96
  else
59
97
  s.error!
@@ -66,7 +104,7 @@ class SugarJar
66
104
  # and then configure remotes properly
67
105
  git('remote', 'rename', 'origin', 'upstream')
68
106
 
69
- fork_url = forked_repo(repo, @forge_user)
107
+ fork_url = forked_repo(repo, @host_config['user'])
70
108
  git('remote', 'add', 'origin', fork_url)
71
109
  end
72
110
  end
@@ -19,17 +19,24 @@ class SugarJar
19
19
 
20
20
  curr = current_branch
21
21
  base = tracked_branch
22
- if @pr_autofill
22
+ if @config['pr_autofill']
23
23
  SugarJar::Log.info('Autofilling in PR from commit message')
24
24
  num_commits = git(
25
25
  'rev-list', '--count', curr, "^#{base}"
26
26
  ).stdout.strip.to_i
27
- if num_commits > 1
27
+ if num_commits > 1 && forge.type == 'github'
28
28
  args.unshift('--fill-first')
29
+ elsif forge.type == 'forgejo'
30
+ args += _forgejo_pr_opts
29
31
  else
30
32
  args.unshift('--fill')
31
33
  end
32
34
  end
35
+ base_opt = if forge.type == 'gitlab'
36
+ '--target-branch'
37
+ else
38
+ '--base'
39
+ end
33
40
  unless user_specified_base
34
41
  if subfeature?(base)
35
42
  if upstream_org != push_org
@@ -41,44 +48,39 @@ class SugarJar
41
48
  ' PR is rebased.',
42
49
  )
43
50
  # nil is prompt, true is always, false is never
44
- elsif @pr_autostack.nil?
51
+ elsif @config['pr_autostack'].nil?
45
52
  $stdout.print(
46
53
  'It looks like this is a subfeature, would you like to base ' +
47
54
  "this PR on #{base}? [y/n] ",
48
55
  )
49
56
  ans = $stdin.gets.strip
50
- args.unshift('--base', base) if %w{Y y}.include?(ans)
51
- elsif @pr_autostack
52
- args.unshift('--base', base)
57
+ args.unshift(base_opt, base) if %w{Y y}.include?(ans)
58
+ elsif @config['pr_autostack']
59
+ args.unshift(base_opt, base)
53
60
  end
54
61
  elsif base.include?('/') && base != most_main
55
62
  # If our base is a remote branch, then use that as the
56
63
  # base branch of the PR
57
- args.unshift('--base', base.split('/').last)
64
+ args.unshift(base_opt, base.split('/').last)
58
65
  end
59
66
  end
60
67
 
61
68
  # <org>:<branch> is the GH API syntax for:
62
69
  # look for a branch of name <branch>, from a fork in owner <org>
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
70
+ if forge.type == 'gitlab'
68
71
  # On GitLab, the head is the repo (org/repo) to use as the head
69
72
  # _repo_, and then branch is configured seperately (with -s), but
70
73
  # we don't need that since it defaults to the local branch name.
71
74
  #
72
75
  # Then we need --yes for it to not prompt us
73
- args.unshift('--head', "#{push_org}/#{reponame}", '--yes')
76
+ args.unshift('--head', "#{push_org}/#{repo_name}", '--yes')
77
+ else
78
+ # On GitHub/Forgeji, the head is the org and the *BRANCH* name to use as
79
+ # the head branch...
80
+ args.unshift('--head', "#{push_org}:#{curr}")
74
81
  end
75
82
 
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)
83
+ forge.run_with_system(_pr_cmd, 'create', *args)
82
84
  end
83
85
 
84
86
  alias spr smartpullrequest
@@ -87,7 +89,7 @@ class SugarJar
87
89
  private
88
90
 
89
91
  def _pr_cmd
90
- @repo_forge == 'gitlab' ? 'mr' : 'pr'
92
+ forge.type == 'gitlab' ? 'mr' : 'pr'
91
93
  end
92
94
 
93
95
  def assert_common_main_branch!
@@ -103,7 +105,9 @@ class SugarJar
103
105
  "\tgit remote set-head #{upstream} -a",
104
106
  )
105
107
  end
106
- return if upstream_branch == 'origin'
108
+
109
+ # If we don't have an upstream and an origin, we're done
110
+ return if upstream == 'origin'
107
111
 
108
112
  origin_branch = main_remote_branch('origin')
109
113
  # NOTE: that on GL, forks don't fork any branches by default, even
@@ -119,5 +123,11 @@ class SugarJar
119
123
  'run to update this clone.',
120
124
  )
121
125
  end
126
+
127
+ def _forgejo_pr_opts
128
+ title = git('log', '-1', '--pretty=format:%s').stdout
129
+ body = git('show', '-s', '--format=%b').stdout
130
+ [title, "--body=#{body}"]
131
+ end
122
132
  end
123
133
  end