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,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
@@ -1,9 +1,5 @@
1
1
  require 'mixlib/shellout'
2
2
 
3
- require_relative 'util'
4
- require_relative 'repoconfig'
5
- require_relative 'log'
6
- require_relative 'version'
7
3
  require_relative 'commands/amend'
8
4
  require_relative 'commands/bclean'
9
5
  require_relative 'commands/branch'
@@ -15,6 +11,10 @@ require_relative 'commands/push'
15
11
  require_relative 'commands/smartclone'
16
12
  require_relative 'commands/smartpullrequest'
17
13
  require_relative 'commands/up'
14
+ require_relative 'log'
15
+ require_relative 'repoconfig'
16
+ require_relative 'util'
17
+ require_relative 'version'
18
18
 
19
19
  class SugarJar
20
20
  # This is the workhorse of SugarJar. Short of #initialize, all other public
@@ -49,6 +49,285 @@ class SugarJar
49
49
  set_commit_template if @repo_config['commit_template']
50
50
  end
51
51
 
52
- include SugarJar::Util
52
+ private
53
+
54
+ 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}"
61
+ end
62
+
63
+ # gh utils will default to https, but we should always default to SSH
64
+ # unless otherwise specified since https will cause prompting.
65
+ def canonicalize_repo(repo)
66
+ # if they fully-qualified it, we're good
67
+ return repo if repo.start_with?('http', 'git@')
68
+
69
+ # otherwise, ti's a shortname
70
+ cr = "git@#{@ghhost || 'github.com'}:#{repo}.git"
71
+ SugarJar::Log.debug("canonicalized #{repo} to #{cr}")
72
+ cr
73
+ end
74
+
75
+ def set_commit_template
76
+ unless SugarJar::Util.in_repo?
77
+ SugarJar::Log.debug('Skipping set_commit_template: not in repo')
78
+ return
79
+ end
80
+
81
+ realpath = if @repo_config['commit_template'].start_with?('/')
82
+ @repo_config['commit_template']
83
+ else
84
+ "#{Util.repo_root}/#{@repo_config['commit_template']}"
85
+ end
86
+ unless File.exist?(realpath)
87
+ die(
88
+ "Repo config specifies #{@repo_config['commit_template']} as the " +
89
+ 'commit template, but that file does not exist.',
90
+ )
91
+ end
92
+
93
+ s = git_nofail('config', '--local', 'commit.template')
94
+ unless s.error?
95
+ current = s.stdout.strip
96
+ if current == @repo_config['commit_template']
97
+ SugarJar::Log.debug('Commit template already set correctly')
98
+ return
99
+ else
100
+ SugarJar::Log.warn(
101
+ "Updating repo-specific commit template from #{current} " +
102
+ "to #{@repo_config['commit_template']}",
103
+ )
104
+ end
105
+ end
106
+
107
+ SugarJar::Log.debug(
108
+ 'Setting repo-specific commit template to ' +
109
+ "#{@repo_config['commit_template']} per sugarjar repo config.",
110
+ )
111
+ git(
112
+ 'config', '--local', 'commit.template', @repo_config['commit_template']
113
+ )
114
+ end
115
+
116
+ def assert_in_repo!
117
+ return if SugarJar::Util.in_repo?
118
+
119
+ die('sugarjar must be run from inside a git repo')
120
+ end
121
+
122
+ def determine_main_branch(branches)
123
+ branches.include?('main') ? 'main' : 'master'
124
+ end
125
+
126
+ def main_branch
127
+ @main_branch = determine_main_branch(all_local_branches)
128
+ end
129
+
130
+ def main_remote_branch(remote)
131
+ @main_remote_branches[remote] ||=
132
+ determine_main_branch(all_remote_branches(remote))
133
+ end
134
+
135
+ def checkout_main_branch
136
+ git('checkout', main_branch)
137
+ end
138
+
139
+ def all_remote_branches(remote = 'origin')
140
+ branches = []
141
+ git('branch', '-r', '--format', '%(refname)').stdout.lines.each do |line|
142
+ next unless line.start_with?("refs/remotes/#{remote}/")
143
+
144
+ branches << branch_from_ref(line.strip, :remote)
145
+ end
146
+ branches
147
+ end
148
+
149
+ def all_local_branches
150
+ git(
151
+ 'branch', '--format', '%(refname)'
152
+ ).stdout.lines.map do |line|
153
+ next if line.start_with?('(HEAD detached')
154
+
155
+ branch_from_ref(line.strip)
156
+ end
157
+ end
158
+
159
+ def all_remotes
160
+ git('remote').stdout.lines.map(&:strip)
161
+ end
162
+
163
+ def remote_url_map
164
+ m = {}
165
+ git('remote', '-v').stdout.each_line do |line|
166
+ name, url, = line.split
167
+ m[name] = url
168
+ end
169
+ m
170
+ end
171
+
172
+ def current_branch
173
+ branch_from_ref(git('symbolic-ref', 'HEAD').stdout.strip)
174
+ end
175
+
176
+ def fetch_upstream
177
+ us = upstream
178
+ fetch(us) if us
179
+ end
180
+
181
+ def fetch(remote)
182
+ git('fetch', remote)
183
+ end
184
+
185
+ # determine if this branch is based on another local branch (i.e. is a
186
+ # subfeature). Used to figure out of we should stack the PR
187
+ def subfeature?(base)
188
+ all_local_branches.reject { |x| x == most_main }.include?(base)
189
+ end
190
+
191
+ def tracked_branch(fallback: true)
192
+ branch = nil
193
+ s = git_nofail(
194
+ 'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'
195
+ )
196
+ if s.error?
197
+ branch = fallback ? most_main : nil
198
+ SugarJar::Log.debug("No specific tracked branch, using #{branch}")
199
+ else
200
+ branch = s.stdout.strip
201
+ SugarJar::Log.debug(
202
+ "Using explicit tracked branch: #{branch}, use " +
203
+ '`git branch -u` to change',
204
+ )
205
+ end
206
+ branch
207
+ end
208
+
209
+ def most_main
210
+ us = upstream
211
+ if us
212
+ "#{us}/#{main_branch}"
213
+ else
214
+ main_branch
215
+ end
216
+ end
217
+
218
+ def upstream
219
+ return @remote if @remote
220
+
221
+ remotes = all_remotes
222
+ SugarJar::Log.debug("remotes is #{remotes}")
223
+ if remotes.empty?
224
+ @remote = nil
225
+ elsif remotes.length == 1
226
+ @remote = remotes[0]
227
+ elsif remotes.include?('upstream')
228
+ @remote = 'upstream'
229
+ elsif remotes.include?('origin')
230
+ @remote = 'origin'
231
+ else
232
+ raise 'Could not determine "upstream" remote to use...'
233
+ end
234
+ @remote
235
+ end
236
+
237
+ def upstream_org
238
+ us = upstream
239
+ remotes = remote_url_map
240
+ extract_org(remotes[us])
241
+ end
242
+
243
+ # Whatever org we push to, regardless of if this is a fork or not
244
+ def push_org
245
+ url = git('remote', 'get-url', 'origin').stdout.strip
246
+ extract_org(url)
247
+ end
248
+
249
+ def color(string, *colors)
250
+ if @color
251
+ pastel.decorate(string, *colors)
252
+ else
253
+ string
254
+ end
255
+ end
256
+
257
+ def pastel
258
+ @pastel ||= begin
259
+ require 'pastel'
260
+ Pastel.new
261
+ end
262
+ end
263
+
264
+ def gh_avail?
265
+ !!SugarJar::Util.which_nofail('gh')
266
+ end
267
+
268
+ def fprefix(name)
269
+ return name unless @feature_prefix
270
+
271
+ return name if name.start_with?(@feature_prefix)
272
+ return name if all_local_branches.include?(name)
273
+
274
+ newname = "#{@feature_prefix}#{name}"
275
+ SugarJar::Log.debug(
276
+ "Munging feature name: #{name} -> #{newname} due to feature prefix",
277
+ )
278
+ newname
279
+ end
280
+
281
+ def dirty?
282
+ s = git_nofail('diff', '--quiet')
283
+ s.error?
284
+ end
285
+
286
+ def repo_name
287
+ SugarJar::Util.repo_root.split('/').last
288
+ end
289
+
290
+ def extract_org(repo)
291
+ if repo.start_with?('http')
292
+ File.basename(File.dirname(repo))
293
+ elsif repo.start_with?('git@')
294
+ repo.split(':')[1].split('/')[0]
295
+ else
296
+ # assume they passed in a ghcli-friendly name
297
+ repo.split('/').first
298
+ end
299
+ end
300
+
301
+ def extract_repo(repo)
302
+ File.basename(repo, '.git')
303
+ end
304
+
305
+ def die(msg)
306
+ SugarJar::Log.fatal(msg)
307
+ exit(1)
308
+ end
309
+
310
+ def branch_from_ref(ref, type = :local)
311
+ # local branches are refs/head/XXXX
312
+ # remote branches are refs/remotes/<remote>/XXXX
313
+ base = type == :local ? 2 : 3
314
+ ref.split('/')[base..].join('/')
315
+ end
316
+
317
+ def git(*)
318
+ SugarJar::Util.git(*, :color => @color)
319
+ end
320
+
321
+ def git_nofail(*)
322
+ SugarJar::Util.git_nofail(*, :color => @color)
323
+ end
324
+
325
+ def ghcli(*)
326
+ SugarJar::Util.ghcli(*)
327
+ end
328
+
329
+ def ghcli_nofail(*)
330
+ SugarJar::Util.ghcli_nofail(*)
331
+ end
53
332
  end
54
333
  end
@@ -7,12 +7,10 @@ class SugarJar
7
7
  # This parses SugarJar repoconfigs (not to be confused with configs).
8
8
  # This is lint/unit/on_push configs.
9
9
  class RepoConfig
10
- extend SugarJar::Util
11
-
12
10
  CONFIG_NAME = '.sugarjar.yaml'.freeze
13
11
 
14
12
  def self.repo_config_path(config)
15
- ::File.join(repo_root, config)
13
+ ::File.join(SugarJar::Util.repo_root, config)
16
14
  end
17
15
 
18
16
  def self.hash_from_file(config_file)
@@ -27,7 +25,7 @@ class SugarJar
27
25
 
28
26
  def self.config(config = CONFIG_NAME)
29
27
  data = {}
30
- unless in_repo
28
+ unless SugarJar::Util.in_repo?
31
29
  SugarJar::Log.debug('Not in repo, skipping repoconfig load')
32
30
  return data
33
31
  end