sugarjar 1.1.3 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +161 -0
- data/CONTRIBUTING.md +37 -0
- data/README.md +142 -264
- data/bin/sj +62 -108
- data/examples/sample_config.yaml +24 -0
- data/examples/sample_repoconfig.yaml +77 -0
- data/lib/sugarjar/commands/amend.rb +17 -0
- data/lib/sugarjar/commands/bclean.rb +118 -0
- data/lib/sugarjar/commands/branch.rb +42 -0
- data/lib/sugarjar/commands/checks.rb +139 -0
- data/lib/sugarjar/commands/debuginfo.rb +16 -0
- data/lib/sugarjar/commands/feature.rb +35 -0
- data/lib/sugarjar/commands/pullsuggestions.rb +48 -0
- data/lib/sugarjar/commands/push.rb +66 -0
- data/lib/sugarjar/commands/smartclone.rb +30 -0
- data/lib/sugarjar/commands/smartpullrequest.rb +101 -0
- data/lib/sugarjar/commands/up.rb +103 -0
- data/lib/sugarjar/commands.rb +94 -787
- data/lib/sugarjar/config.rb +22 -2
- data/lib/sugarjar/repoconfig.rb +2 -4
- data/lib/sugarjar/util.rb +33 -90
- data/lib/sugarjar/version.rb +1 -1
- data/sugarjar.gemspec +11 -5
- metadata +24 -5
@@ -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
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require_relative '../util'
|
2
|
+
|
3
|
+
class SugarJar
|
4
|
+
class Commands
|
5
|
+
def smartpullrequest(*args)
|
6
|
+
assert_in_repo!
|
7
|
+
assert_common_main_branch!
|
8
|
+
|
9
|
+
if dirty?
|
10
|
+
SugarJar::Log.warn(
|
11
|
+
'Your repo is dirty, so I am not going to create a pull request. ' +
|
12
|
+
'You should commit or amend and push it to your remote first.',
|
13
|
+
)
|
14
|
+
exit(1)
|
15
|
+
end
|
16
|
+
|
17
|
+
user_specified_base = args.include?('-B') || args.include?('--base')
|
18
|
+
|
19
|
+
curr = current_branch
|
20
|
+
base = tracked_branch
|
21
|
+
if @pr_autofill
|
22
|
+
SugarJar::Log.info('Autofilling in PR from commit message')
|
23
|
+
num_commits = git(
|
24
|
+
'rev-list', '--count', curr, "^#{base}"
|
25
|
+
).stdout.strip.to_i
|
26
|
+
if num_commits > 1
|
27
|
+
args.unshift('--fill-first')
|
28
|
+
else
|
29
|
+
args.unshift('--fill')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
unless user_specified_base
|
33
|
+
if subfeature?(base)
|
34
|
+
if upstream_org != push_org
|
35
|
+
SugarJar::Log.warn(
|
36
|
+
'Unfortunately you cannot based one PR on another PR when' +
|
37
|
+
" using fork-based PRs. We will base this on #{most_main}." +
|
38
|
+
' This just means the PR "Changes" tab will show changes for' +
|
39
|
+
' the full stack until those other PRs are merged and this PR' +
|
40
|
+
' PR is rebased.',
|
41
|
+
)
|
42
|
+
# nil is prompt, true is always, false is never
|
43
|
+
elsif @pr_autostack.nil?
|
44
|
+
$stdout.print(
|
45
|
+
'It looks like this is a subfeature, would you like to base ' +
|
46
|
+
"this PR on #{base}? [y/n] ",
|
47
|
+
)
|
48
|
+
ans = $stdin.gets.strip
|
49
|
+
args.unshift('--base', base) if %w{Y y}.include?(ans)
|
50
|
+
elsif @pr_autostack
|
51
|
+
args.unshift('--base', base)
|
52
|
+
end
|
53
|
+
elsif base.include?('/') && base != most_main
|
54
|
+
# If our base is a remote branch, then use that as the
|
55
|
+
# base branch of the PR
|
56
|
+
args.unshift('--base', base.split('/').last)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# <org>:<branch> is the GH API syntax for:
|
61
|
+
# look for a branch of name <branch>, from a fork in owner <org>
|
62
|
+
args.unshift('--head', "#{push_org}:#{curr}")
|
63
|
+
SugarJar::Log.trace("Running: gh pr create #{args.join(' ')}")
|
64
|
+
gh = SugarJar::Util.which('gh')
|
65
|
+
system(gh, 'pr', 'create', *args)
|
66
|
+
end
|
67
|
+
|
68
|
+
alias spr smartpullrequest
|
69
|
+
alias smartpr smartpullrequest
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def assert_common_main_branch!
|
74
|
+
upstream_branch = main_remote_branch(upstream)
|
75
|
+
unless main_branch == upstream_branch
|
76
|
+
die(
|
77
|
+
"The local main branch is '#{main_branch}', but the main branch " +
|
78
|
+
"of the #{upstream} remote is '#{upstream_branch}'. You probably " +
|
79
|
+
"want to rename your local branch by doing:\n\t" +
|
80
|
+
"git branch -m #{main_branch} #{upstream_branch}\n\t" +
|
81
|
+
"git fetch #{upstream}\n\t" +
|
82
|
+
"git branch -u #{upstream}/#{upstream_branch} #{upstream_branch}\n" +
|
83
|
+
"\tgit remote set-head #{upstream} -a",
|
84
|
+
)
|
85
|
+
end
|
86
|
+
return if upstream_branch == 'origin'
|
87
|
+
|
88
|
+
origin_branch = main_remote_branch('origin')
|
89
|
+
return if origin_branch == upstream_branch
|
90
|
+
|
91
|
+
die(
|
92
|
+
"The main branch of your upstream (#{upstream_branch}) and your " +
|
93
|
+
"fork/origin (#{origin_branch}) are not the same. You should go " +
|
94
|
+
"to https://#{@ghhost || 'github.com'}/#{@ghuser}/#{repo_name}/" +
|
95
|
+
'branches/ and rename the \'default\' branch to ' +
|
96
|
+
"'#{upstream_branch}'. It will then give you some commands to " +
|
97
|
+
'run to update this clone.',
|
98
|
+
)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
class SugarJar
|
2
|
+
class Commands
|
3
|
+
def up(branch = nil)
|
4
|
+
assert_in_repo!
|
5
|
+
branch ||= current_branch
|
6
|
+
branch = fprefix(branch)
|
7
|
+
# get a copy of our current branch, if rebase fails, we won't
|
8
|
+
# be able to determine it without backing out
|
9
|
+
curr = current_branch
|
10
|
+
git('checkout', branch)
|
11
|
+
result = rebase
|
12
|
+
if result['so'].error?
|
13
|
+
backout = ''
|
14
|
+
if rebase_in_progress?
|
15
|
+
backout = ' You can get out of this with a `git rebase --abort`.'
|
16
|
+
end
|
17
|
+
|
18
|
+
die(
|
19
|
+
"#{color(curr, :red)}: Failed to rebase on " +
|
20
|
+
"#{result['base']}. Leaving the repo as-is.#{backout} " +
|
21
|
+
'Output from failed rebase is: ' +
|
22
|
+
"\nSTDOUT:\n#{result['so'].stdout.lines.map { |x| "\t#{x}" }.join}" +
|
23
|
+
"\nSTDERR:\n#{result['so'].stderr.lines.map { |x| "\t#{x}" }.join}",
|
24
|
+
)
|
25
|
+
else
|
26
|
+
SugarJar::Log.info(
|
27
|
+
"#{color(current_branch, :green)} rebased on #{result['base']}",
|
28
|
+
)
|
29
|
+
# go back to where we were if we rebased a different branch
|
30
|
+
git('checkout', curr) if branch != curr
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def upall
|
35
|
+
assert_in_repo!
|
36
|
+
all_local_branches.each do |branch|
|
37
|
+
next if MAIN_BRANCHES.include?(branch)
|
38
|
+
|
39
|
+
git('checkout', branch)
|
40
|
+
result = rebase
|
41
|
+
if result['so'].error?
|
42
|
+
SugarJar::Log.error(
|
43
|
+
"#{color(branch, :red)} failed rebase. Reverting attempt and " +
|
44
|
+
'moving to next branch. Try `sj up` manually on that branch.',
|
45
|
+
)
|
46
|
+
git('rebase', '--abort') if rebase_in_progress?
|
47
|
+
else
|
48
|
+
SugarJar::Log.info(
|
49
|
+
"#{color(branch, :green)} rebased on " +
|
50
|
+
color(result['base'], :green).to_s,
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def rebase
|
59
|
+
SugarJar::Log.debug('Fetching upstream')
|
60
|
+
fetch_upstream
|
61
|
+
curr = current_branch
|
62
|
+
# this isn't a hash, it's a named param, silly rubocop
|
63
|
+
# rubocop:disable Style/HashSyntax
|
64
|
+
base = tracked_branch(fallback: false)
|
65
|
+
# rubocop:enable Style/HashSyntax
|
66
|
+
unless base
|
67
|
+
SugarJar::Log.info(
|
68
|
+
'The brach we were tracking is gone, resetting tracking to ' +
|
69
|
+
most_main,
|
70
|
+
)
|
71
|
+
git('branch', '-u', most_main)
|
72
|
+
base = most_main
|
73
|
+
end
|
74
|
+
# If this is a subfeature based on a local branch which has since
|
75
|
+
# been deleted, 'tracked branch' will automatically return <most_main>
|
76
|
+
# so we don't need any special handling for that
|
77
|
+
if !MAIN_BRANCHES.include?(curr) && base == "origin/#{curr}"
|
78
|
+
SugarJar::Log.warn(
|
79
|
+
"This branch is tracking origin/#{curr}, which is probably your " +
|
80
|
+
'downstream (where you push _to_) as opposed to your upstream ' +
|
81
|
+
'(where you pull _from_). This means that "sj up" is probably ' +
|
82
|
+
'rebasing on the wrong thing and doing nothing. You probably want ' +
|
83
|
+
"to do a 'git branch -u #{most_main}'.",
|
84
|
+
)
|
85
|
+
end
|
86
|
+
SugarJar::Log.debug('Rebasing')
|
87
|
+
s = git_nofail('rebase', base)
|
88
|
+
{
|
89
|
+
'so' => s,
|
90
|
+
'base' => base,
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
def rebase_in_progress?
|
95
|
+
# for rebase without -i
|
96
|
+
rebase_file = git('rev-parse', '--git-path', 'rebase-apply').stdout.strip
|
97
|
+
# for rebase -i
|
98
|
+
rebase_merge_file = git('rev-parse', '--git-path', 'rebase-merge').
|
99
|
+
stdout.strip
|
100
|
+
File.exist?(rebase_file) || File.exist?(rebase_merge_file)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|