sugarjar 2.0.0.beta.1 → 2.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.
@@ -0,0 +1,24 @@
1
+ # This is a sample SugarJar config
2
+ #
3
+ # SugarJar will look for this config in:
4
+ #
5
+ # - /etc/sugarjar/config.yaml
6
+ # - ~/.config/sugarjar/config.yaml
7
+ #
8
+ # The latter will overwrite anything in the former.
9
+ #
10
+
11
+ # NOTE: This file does NOT document ALL options since any command-line option
12
+ # to SugarJar is a valid configuration in this file, so see `sj help` for full
13
+ # details.
14
+
15
+ # Autofill in my PRs from my commit message (default: true)
16
+ pr_autofile: true
17
+
18
+ # Auto stack PRs when subfeatures are detected (default is `nil`, which prompts,
19
+ # but use `true` or `false` to force an option without prompting)
20
+ pr_autostack: true
21
+
22
+ # Don't warn about deprecated config file options if they are in this
23
+ # list
24
+ ignore_deprecated_options: [ 'gh_cli' ]
@@ -0,0 +1,77 @@
1
+ # This is a sample `repoconfig` for SugarJar
2
+ #
3
+ # Configs should be named `.sugarjar.yaml` and placed in the root
4
+ # of your repository.
5
+ #
6
+
7
+ # `include_from` is a meta config wich will read from an additional
8
+ # configuration file and merge anything from the file onto whatever is in the
9
+ # primary file. This is helpful to have a repo configuration that applies to
10
+ # all/most developers, but allow individual developers to add to over overwrite
11
+ # specific configurations for themselves. If the file does not exist, this
12
+ # configuration is ignored.
13
+
14
+ include_from: .sugarjar_local.yaml
15
+
16
+ # `overwrite_from` is a meta config which works much like `include_from`,
17
+ # except that if the file is found, everything else in this configuration file
18
+ # will be ignored and the configuration will be entirely read from the
19
+ # referenced file. If the file does not exist, this configuration is ignored.
20
+
21
+ overwrite_from: .sugarjar_local_overwrite.yaml
22
+
23
+ # `lint` is a list of scripts to run when `sj lint` is executed (or, if
24
+ # configured, to run on `sj spush`/`sj fpush` - see `on_push` below).
25
+ # Regardless of where `sj` is run from, these scripts will be run from the root
26
+ # of the repo. If a slash is detected in the first 'word' of the command, it
27
+ # is assumed it is a relative path and `sj` will check that the file exists.
28
+
29
+ lint:
30
+ - scripts/run_rubocop.sh
31
+ - scripts/run_mdl.sh
32
+
33
+ # `unit` is a list of scripts to run when `sj unit` is executed (or, if
34
+ # configured to run on `sj spush`/`sj fpush`- see `on_push` below). Regardless
35
+ # of where `sj` is run from, these scripts will be run from the root of the
36
+ # repo. If a slash is detected in the first 'word' of the command, it is
37
+ # assumed it is a relative path and `sj` will check that the file exists.
38
+
39
+ unit:
40
+ - bundle exec rspec
41
+ - scripts/run_tests.sh
42
+
43
+ # `lint_list_cmd` is like `lint`, except it's a command to run which will
44
+ # determine the proper lints to run and return them, one per line. This is
45
+ # useful, for example, when you want to only run lints relevant to the changed
46
+ # files.
47
+
48
+ lint_list_cmd: scripts/determine_linters.sh
49
+
50
+ # `unit_list_cmd` is like `unit`, except it's a command to run which will
51
+ # determine the proper units to run and return them, one per line. This is
52
+ # useful, for example, when you want to only run tests relevant to the changed
53
+ # files.
54
+
55
+ unit_list_cmd: scripts/determine_tests.sh
56
+
57
+ # `on_push` determines what checks should be run when pushing a repo. Valid
58
+ # options are `lint` and/or `unit` (or nothing, of course).
59
+
60
+ on_push: [lint] # or [lint, unit]
61
+
62
+ # `commit_template` points to a file to set the git `commit.template` config
63
+ # to. This is really useful for ensuring that everyone has the same
64
+ # template configured.
65
+
66
+ commit_template: .git_commit_template.txt
67
+
68
+ # `github_user` is the user to use when talking to GitHub. Overrides any such
69
+ # setting in the regular SugarJar config. Most useful when in the
70
+ # `include_from` file.
71
+
72
+ github_user: myuser
73
+
74
+ # `github_host` is the GitHub host to use when talking to GitHub (for hosted
75
+ # GHE). See `github_user`.
76
+
77
+ github_host: github.sample.com
@@ -0,0 +1,17 @@
1
+ require_relative '../util'
2
+
3
+ class SugarJar
4
+ class Commands
5
+ def amend(*)
6
+ assert_in_repo!
7
+ # This cannot use shellout since we need a full terminal for the editor
8
+ exit(system(SugarJar::Util.which('git'), 'commit', '--amend', *))
9
+ end
10
+
11
+ def qamend(*)
12
+ assert_in_repo!
13
+ SugarJar::Log.info(git('commit', '--amend', '--no-edit', *).stdout)
14
+ end
15
+ alias amendq qamend
16
+ end
17
+ end
@@ -0,0 +1,118 @@
1
+ class SugarJar
2
+ class Commands
3
+ def bclean(name = nil)
4
+ assert_in_repo!
5
+ name ||= current_branch
6
+ name = fprefix(name)
7
+ if clean_branch(name)
8
+ SugarJar::Log.info("#{name}: #{color('reaped', :green)}")
9
+ else
10
+ die(
11
+ "#{color("Cannot clean #{name}", :red)}! there are unmerged " +
12
+ "commits; use 'git branch -D #{name}' to forcefully delete it.",
13
+ )
14
+ end
15
+ end
16
+
17
+ def bcleanall
18
+ assert_in_repo!
19
+ curr = current_branch
20
+ all_local_branches.each do |branch|
21
+ if MAIN_BRANCHES.include?(branch)
22
+ SugarJar::Log.debug("Skipping #{branch}")
23
+ next
24
+ end
25
+
26
+ if clean_branch(branch)
27
+ SugarJar::Log.info("#{branch}: #{color('reaped', :green)}")
28
+ else
29
+ SugarJar::Log.info("#{branch}: skipped")
30
+ SugarJar::Log.debug(
31
+ "There are unmerged commits; use 'git branch -D #{branch}' to " +
32
+ 'forcefully delete it)',
33
+ )
34
+ end
35
+ end
36
+
37
+ # Return to the branch we were on, or main
38
+ if all_local_branches.include?(curr)
39
+ git('checkout', curr)
40
+ else
41
+ checkout_main_branch
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def clean_branch(name)
48
+ die("Cannot remove #{name} branch") if MAIN_BRANCHES.include?(name)
49
+ SugarJar::Log.debug('Fetch relevant remote...')
50
+ fetch_upstream
51
+ return false unless safe_to_clean(name)
52
+
53
+ SugarJar::Log.debug('branch deemed safe to delete...')
54
+ checkout_main_branch
55
+ git('branch', '-D', name)
56
+ rebase
57
+ true
58
+ end
59
+
60
+ def safe_to_clean(branch)
61
+ # cherry -v will output 1 line per commit on the target branch
62
+ # prefixed by a - or + - anything with a - can be dropped, anything
63
+ # else cannot.
64
+ out = git(
65
+ 'cherry', '-v', tracked_branch, branch
66
+ ).stdout.lines.reject do |line|
67
+ line.start_with?('-')
68
+ end
69
+ if out.empty?
70
+ SugarJar::Log.debug(
71
+ "cherry-pick shows branch #{branch} obviously safe to delete",
72
+ )
73
+ return true
74
+ end
75
+
76
+ # if the "easy" check didn't work, it's probably because there
77
+ # was a squash-merge. To check for that we make our own squash
78
+ # merge to upstream/main and see if that has any delta
79
+
80
+ # First we need a temp branch to work on
81
+ tmpbranch = "_sugar_jar.#{Process.pid}"
82
+
83
+ git('checkout', '-b', tmpbranch, tracked_branch)
84
+ s = git_nofail('merge', '--squash', branch)
85
+ if s.error?
86
+ cleanup_tmp_branch(tmpbranch, branch)
87
+ SugarJar::Log.debug(
88
+ 'Failed to merge changes into current main. This means we could ' +
89
+ 'not figure out if this is merged or not. Check manually and use ' +
90
+ "'git branch -D #{branch}' if it is safe to do so.",
91
+ )
92
+ return false
93
+ end
94
+
95
+ s = git('diff', '--staged')
96
+ out = s.stdout
97
+ SugarJar::Log.debug("Squash-merged diff: #{out}")
98
+ cleanup_tmp_branch(tmpbranch, branch)
99
+ if out.empty?
100
+ SugarJar::Log.debug(
101
+ 'After squash-merging, this branch appears safe to delete',
102
+ )
103
+ true
104
+ else
105
+ SugarJar::Log.debug(
106
+ 'After squash-merging, this branch is NOT fully merged to main',
107
+ )
108
+ false
109
+ end
110
+ end
111
+
112
+ def cleanup_tmp_branch(tmp, backto)
113
+ git('reset', '--hard', tracked_branch)
114
+ git('checkout', backto)
115
+ git('branch', '-D', tmp)
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,42 @@
1
+ class SugarJar
2
+ class Commands
3
+ def checkout(*args)
4
+ assert_in_repo!
5
+ # Pop the last arguement, which is _probably_ a branch name
6
+ # and then add any featureprefix, and if _that_ is a branch
7
+ # name, replace the last arguement with that
8
+ name = args.last
9
+ bname = fprefix(name)
10
+ if all_local_branches.include?(bname)
11
+ SugarJar::Log.debug("Featurepefixing #{name} -> #{bname}")
12
+ args[-1] = bname
13
+ end
14
+ s = git('checkout', *args)
15
+ SugarJar::Log.info(s.stderr + s.stdout.chomp)
16
+ end
17
+ alias co checkout
18
+
19
+ def br
20
+ assert_in_repo!
21
+ SugarJar::Log.info(git('branch', '-v').stdout.chomp)
22
+ end
23
+
24
+ def binfo
25
+ assert_in_repo!
26
+ SugarJar::Log.info(git(
27
+ 'log', '--graph', '--oneline', '--decorate', '--boundary',
28
+ "#{tracked_branch}.."
29
+ ).stdout.chomp)
30
+ end
31
+
32
+ # binfo for all branches
33
+ def smartlog
34
+ assert_in_repo!
35
+ SugarJar::Log.info(git(
36
+ 'log', '--graph', '--oneline', '--decorate', '--boundary',
37
+ '--branches', "#{most_main}.."
38
+ ).stdout.chomp)
39
+ end
40
+ alias sl smartlog
41
+ end
42
+ end
@@ -0,0 +1,139 @@
1
+ require_relative '../util'
2
+
3
+ class SugarJar
4
+ class Commands
5
+ def lint
6
+ assert_in_repo!
7
+ if dirty?
8
+ if @ignore_dirty
9
+ SugarJar::Log.warn(
10
+ 'Your repo is dirty, but --ignore-dirty was specified, so ' +
11
+ 'carrying on anyway. If the linter autocorrects, the displayed ' +
12
+ 'diff will be misleading',
13
+ )
14
+ else
15
+ SugarJar::Log.error(
16
+ 'Your repo is dirty, but --ignore-dirty was not specified. ' +
17
+ 'Refusing to run lint. This is to ensure that if the linter ' +
18
+ 'autocorrects, we can show the correct diff.',
19
+ )
20
+ exit(1)
21
+ end
22
+ end
23
+ exit(1) unless run_check('lint')
24
+ end
25
+
26
+ def unit
27
+ assert_in_repo!
28
+ exit(1) unless run_check('unit')
29
+ end
30
+
31
+ def get_checks_from_command(type)
32
+ return nil unless @repo_config["#{type}_list_cmd"]
33
+
34
+ cmd = @repo_config["#{type}_list_cmd"]
35
+ short = cmd.split.first
36
+ unless File.exist?(short)
37
+ SugarJar::Log.error(
38
+ "Configured #{type}_list_cmd #{short} does not exist!",
39
+ )
40
+ return false
41
+ end
42
+ s = Mixlib::ShellOut.new(cmd).run_command
43
+ if s.error?
44
+ SugarJar::Log.error(
45
+ "#{type}_list_cmd (#{cmd}) failed: #{s.format_for_exception}",
46
+ )
47
+ return false
48
+ end
49
+ s.stdout.split("\n")
50
+ end
51
+
52
+ # determine if we're using the _list_cmd and if so run it to get the
53
+ # checks, or just use the directly-defined check, and cache it
54
+ def get_checks(type)
55
+ return @checks[type] if @checks[type]
56
+
57
+ ret = get_checks_from_command(type)
58
+ if ret
59
+ SugarJar::Log.debug("Found #{type}s: #{ret}")
60
+ @checks[type] = ret
61
+ # if it's explicitly false, we failed to run the command
62
+ elsif ret == false
63
+ @checks[type] = false
64
+ # otherwise, we move on (basically: it's nil, there was no _list_cmd)
65
+ else
66
+ SugarJar::Log.debug("[#{type}]: using listed linters: #{ret}")
67
+ @checks[type] = @repo_config[type] || []
68
+ end
69
+ @checks[type]
70
+ end
71
+
72
+ def run_check(type)
73
+ repo_root = SugarJar::Util.repo_root
74
+ Dir.chdir repo_root do
75
+ checks = get_checks(type)
76
+ # if we failed to determine the checks, the the checks have effectively
77
+ # failed
78
+ return false unless checks
79
+
80
+ checks.each do |check|
81
+ SugarJar::Log.debug("Running #{type} #{check}")
82
+
83
+ short = check.split.first
84
+ if short.include?('/')
85
+ short = File.join(repo_root, short) unless short.start_with?('/')
86
+ unless File.exist?(short)
87
+ SugarJar::Log.error("Configured #{type} #{short} does not exist!")
88
+ end
89
+ elsif !SugarJar::Util.which_nofail(short)
90
+ SugarJar::Log.error("Configured #{type} #{short} does not exist!")
91
+ return false
92
+ end
93
+ s = Mixlib::ShellOut.new(check).run_command
94
+
95
+ # Linters auto-correct, lets handle that gracefully
96
+ if type == 'lint' && dirty?
97
+ SugarJar::Log.info(
98
+ "[#{type}] #{short}: #{color('Corrected', :yellow)}",
99
+ )
100
+ SugarJar::Log.warn(
101
+ "The linter modified the repo. Here's the diff:\n",
102
+ )
103
+ puts git('diff').stdout
104
+ loop do
105
+ $stdout.print(
106
+ "\nWould you like to\n\t[q]uit and inspect\n\t[a]mend the " +
107
+ "changes to the current commit and re-run\n > ",
108
+ )
109
+ ans = $stdin.gets.strip
110
+ case ans
111
+ when /^q/
112
+ SugarJar::Log.info('Exiting at user request.')
113
+ exit(1)
114
+ when /^a/
115
+ qamend('-a')
116
+ # break here, if we get out of this loop we 'redo', assuming
117
+ # the user chose this option
118
+ break
119
+ end
120
+ end
121
+ redo
122
+ end
123
+
124
+ if s.error?
125
+ SugarJar::Log.info(
126
+ "[#{type}] #{short} #{color('failed', :red)}, output follows " +
127
+ "(see debug for more)\n#{s.stdout}",
128
+ )
129
+ SugarJar::Log.debug(s.format_for_exception)
130
+ return false
131
+ end
132
+ SugarJar::Log.info(
133
+ "[#{type}] #{short}: #{color('OK', :green)}",
134
+ )
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,16 @@
1
+ require 'json'
2
+
3
+ class SugarJar
4
+ class Commands
5
+ def debuginfo(*args)
6
+ puts "sugarjar version #{SugarJar::VERSION}"
7
+ puts ghcli('version').stdout
8
+ puts git('version').stdout
9
+
10
+ puts "Config: #{JSON.pretty_generate(args[0])}"
11
+ return unless @repo_config
12
+
13
+ puts "Repo config: #{JSON.pretty_generate(@repo_config)}"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ class SugarJar
2
+ class Commands
3
+ def feature(name, base = nil)
4
+ assert_in_repo!
5
+ SugarJar::Log.debug("Feature: #{name}, #{base}")
6
+ name = fprefix(name)
7
+ die("#{name} already exists!") if all_local_branches.include?(name)
8
+ if base
9
+ fbase = fprefix(base)
10
+ base = fbase if all_local_branches.include?(fbase)
11
+ else
12
+ base ||= most_main
13
+ end
14
+ # If our base is a local branch, don't try to parse it for a remote name
15
+ unless all_local_branches.include?(base)
16
+ base_pieces = base.split('/')
17
+ git('fetch', base_pieces[0]) if base_pieces.length > 1
18
+ end
19
+ git('checkout', '-b', name, base)
20
+ git('branch', '-u', base)
21
+ SugarJar::Log.info(
22
+ "Created feature branch #{color(name, :green)} based on " +
23
+ color(base, :green),
24
+ )
25
+ end
26
+ alias f feature
27
+
28
+ def subfeature(name)
29
+ assert_in_repo!
30
+ SugarJar::Log.debug("Subfature: #{name}")
31
+ feature(name, current_branch)
32
+ end
33
+ alias sf subfeature
34
+ end
35
+ end
@@ -0,0 +1,48 @@
1
+ require_relative '../util'
2
+
3
+ class SugarJar
4
+ class Commands
5
+ def pullsuggestions
6
+ assert_in_repo!
7
+
8
+ if dirty?
9
+ if @ignore_dirty
10
+ SugarJar::Log.warn(
11
+ 'Your repo is dirty, but --ignore-dirty was specified, so ' +
12
+ 'carrying on anyway.',
13
+ )
14
+ else
15
+ SugarJar::Log.error(
16
+ 'Your repo is dirty, so I am not going to push. Please commit ' +
17
+ 'or amend first.',
18
+ )
19
+ exit(1)
20
+ end
21
+ end
22
+
23
+ src = "origin/#{current_branch}"
24
+ fetch('origin')
25
+ diff = git('diff', "..#{src}").stdout
26
+ return unless diff && !diff.empty?
27
+
28
+ puts "Will merge the following suggestions:\n\n#{diff}"
29
+
30
+ loop do
31
+ $stdout.print("\nAre you sure? [y/n] ")
32
+ ans = $stdin.gets.strip
33
+ case ans
34
+ when /^[Yy]$/
35
+ git = SugarJar::Util.which('git')
36
+ system(git, 'merge', '--ff', "origin/#{current_branch}")
37
+ break
38
+ when /^[Nn]$/, /^[Qq](uit)?/
39
+ puts 'Not merging at user request...'
40
+ break
41
+ else
42
+ puts "Didn't understand '#{ans}'."
43
+ end
44
+ end
45
+ end
46
+ alias ps pullsuggestions
47
+ end
48
+ end
@@ -0,0 +1,66 @@
1
+ class SugarJar
2
+ class Commands
3
+ def smartpush(remote = nil, branch = nil)
4
+ assert_in_repo!
5
+ _smartpush(remote, branch, false)
6
+ end
7
+ alias spush smartpush
8
+
9
+ def forcepush(remote = nil, branch = nil)
10
+ assert_in_repo!
11
+ _smartpush(remote, branch, true)
12
+ end
13
+ alias fpush forcepush
14
+
15
+ private
16
+
17
+ def _smartpush(remote, branch, force)
18
+ unless remote && branch
19
+ remote ||= 'origin'
20
+ branch ||= current_branch
21
+ end
22
+
23
+ if dirty?
24
+ if @ignore_dirty
25
+ SugarJar::Log.warn(
26
+ 'Your repo is dirty, but --ignore-dirty was specified, so ' +
27
+ 'carrying on anyway.',
28
+ )
29
+ else
30
+ SugarJar::Log.error(
31
+ 'Your repo is dirty, so I am not going to push. Please commit ' +
32
+ 'or amend first.',
33
+ )
34
+ exit(1)
35
+ end
36
+ end
37
+
38
+ unless run_prepush
39
+ if @ignore_prerun_failure
40
+ SugarJar::Log.warn(
41
+ 'Pre-push checks failed, but --ignore-prerun-failure was ' +
42
+ 'specified, so carrying on anyway',
43
+ )
44
+ else
45
+ SugarJar::Log.error('Pre-push checks failed. Not pushing.')
46
+ exit(1)
47
+ end
48
+ end
49
+
50
+ args = ['push', remote, branch]
51
+ args << '--force-with-lease' if force
52
+ puts git(*args).stderr
53
+ end
54
+
55
+ def run_prepush
56
+ @repo_config['on_push']&.each do |item|
57
+ SugarJar::Log.debug("Running on_push check type #{item}")
58
+ unless run_check(item)
59
+ SugarJar::Log.info("[prepush]: #{item} #{color('failed', :red)}.")
60
+ return false
61
+ end
62
+ end
63
+ true
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,30 @@
1
+ class SugarJar
2
+ class Commands
3
+ def smartclone(repo, dir = nil, *)
4
+ reponame = File.basename(repo, '.git')
5
+ dir ||= reponame
6
+ org = extract_org(repo)
7
+
8
+ SugarJar::Log.info("Cloning #{reponame}...")
9
+
10
+ # GH's 'fork' command (with the --clone arg) will fork, if necessary,
11
+ # then clone, and then setup the remotes with the appropriate names. So
12
+ # we just let it do all the work for us and return.
13
+ #
14
+ # Unless the repo is in our own org and cannot be forked, then it
15
+ # will fail.
16
+ if org == @ghuser
17
+ git('clone', canonicalize_repo(repo), dir, *)
18
+ else
19
+ ghcli('repo', 'fork', '--clone', canonicalize_repo(repo), dir, *)
20
+ # make the main branch track upstream
21
+ Dir.chdir dir do
22
+ git('branch', '-u', "upstream/#{main_branch}")
23
+ end
24
+ end
25
+
26
+ SugarJar::Log.info('Remotes "origin" and "upstream" configured.')
27
+ end
28
+ alias sclone smartclone
29
+ end
30
+ end