sugarjar 1.1.1 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc16295007c8698083a182a521c68481b3cb421644def0115aa04286867dd1eb
4
- data.tar.gz: c901d92e5d7d5287472f52cf56a83d29a5b1b4b0248232278b36e1706bc7117d
3
+ metadata.gz: c689945af0d2bfb7b38477d19da9f58503a15b7be931ab021c4a121a07e6198c
4
+ data.tar.gz: e5fb66f22db68dce1ef31199ea688ff768d753b5df65bc8479744c010d4072c3
5
5
  SHA512:
6
- metadata.gz: 6d64d758a56feac59e5617544cdde17341c804a8831524c49222f6397e47e886ed16f42280d60584a5231d279410839bdacebda66827ae77bb115addb24f3b6d
7
- data.tar.gz: e65b75d83d852906c713f00d6424a76897177f39eb1f2eaa0b5d17896b6d2fb288de3d1cf0946f5587f5674819a074782279ecdb26094c8038d221fcd7ceab0f
6
+ metadata.gz: 9979e5aa13fd2a176cc5a391b9aef8353c1ddb7e63392ab6291f1781c3a99c39f9e4415a3c8cf02720ed2e0c6683a470978ba4ded3be9f2bb993120980a34863
7
+ data.tar.gz: 81ba9f2079a9cc6a88e9052092795c722f9a48bd6f9d763ddc33dddd48fbde65049a16dca2ca66ad39d2a33f179d175a712d22517e6e8103a5089a8d26d9592d
data/README.md CHANGED
@@ -123,34 +123,142 @@ to `git clone` under the hood.
123
123
  ## Work with stacked branches more easily
124
124
 
125
125
  It's important to break changes into reviewable chunks, but working with
126
- stacked branches can be confusing. Enter `binfo` - it gives you a view of your
127
- current branch all the way up to master. In this example imagine we have a
128
- branch structure like:
126
+ stacked branches can be confusing. SugarJar provides several tools to make this
127
+ easier.
129
128
 
130
- ```text
131
- +- test2.1
132
- /
133
- master --- test --- test2 --- test3
129
+ First, and foremost, is `feature` and `subfeature`. Regardless of stacking, the
130
+ way to create a new feature bracnh with sugarjar is with `sj feature` (or `sj
131
+ f` for short):
132
+
133
+ ```shell
134
+ $ sj feature mynewthing
135
+ Created feature branch mynewthing based on origin/main
134
136
  ```
135
137
 
136
- This is what `binfo` on test3 looks like:
138
+ A "feature" in SugarJar parliance just means that the branch is always created
139
+ from "most_main" - this is usually "upstream/main", but SJ will figure out
140
+ which remote is the "upstream", even if it's "origin", and then will determine
141
+ the primary branch ("main" or for older repos "master"). It's also smart enough
142
+ to fetch that remote first to make sure you're working on the latest HEAD.
143
+
144
+ When you want to create a stacked PR, you can create "subfeature", which, at
145
+ its core is just a branch created from the current branch:
137
146
 
138
147
  ```shell
139
- $ sj binfo
140
- * e451865 (HEAD -> test3) test3
141
- * e545b41 (test2) test2
142
- * c808eae (test1) test1
143
- o 44cf9e2 (origin/master, origin/HEAD, master) Lint/gemspec cleanups
148
+ $ sj subfeature dependentnewthing
149
+ Created feature branch dependentnewthing based on mynewthing
144
150
  ```
145
151
 
146
- while `binfo` on test2.1 looks like:
152
+ If you create branches like this then sugarjar can now make several things
153
+ much easier:
147
154
 
148
- ```shell
149
- $ sj binfo
150
- * 36d0136 (HEAD -> test2.1) test2.1
151
- * e545b41 (test2) test2
152
- * c808eae (test1) test1
153
- o 44cf9e2 (origin/master, origin/HEAD, master) Lint/gemspec cleanups
155
+ * `sj up` will rebase intelligently
156
+ * After an `sj bclean` of a branch earlier in the tree, `sj up` will update
157
+ the tracked branch to "most_main"
158
+
159
+ There are two commands that will show you the state of your stacked branches:
160
+
161
+ * `sj binfo` - shows the current branch and its ancestors up to your primary branch
162
+ * `sj smartlist` (aka `sj sl`) - shows you the whole tree.
163
+
164
+ To continue with the example above, my `smartlist` might look like:
165
+
166
+ ```text
167
+ $ sj sl
168
+ * 59c0522 (HEAD -> dependentnewthing) anothertest
169
+ * 6ebaa28 (mynewthing) test
170
+ o 7a0ffd0 (tag: v1.1.2, origin/main, origin/HEAD, main) Version bump (#160)
171
+ ```
172
+
173
+ This is simple. Now lets make a different feature stack:
174
+
175
+ ```text
176
+ $ sj feature anotherfeature
177
+ Created feature branch anotherfeature based on origin/main
178
+ # do stuff
179
+ $ sj subfeature dependent2
180
+ Created feature branch dependent2 based on anotherfeature
181
+ # do stuff
182
+ ```
183
+
184
+ The `smartlist` will now show us this tree, and it's a bit more interesting:
185
+
186
+ ```text
187
+ $ sj sl
188
+ * af6f143 (HEAD -> dependent2) morestuff
189
+ * 028c7f4 (anotherfeature) stuff
190
+ | * 59c0522 (dependentnewthing) anothertest
191
+ | * 6ebaa28 (mynewthing) test
192
+ |/
193
+ o 7a0ffd0 (tag: v1.1.2, origin/main, origin/HEAD, main) Version bump (#160)
194
+ ```
195
+
196
+ Now, what happens if I make a change to `mynewthing`?
197
+
198
+ ```text
199
+ $ sj co mynewthing
200
+ Switched to branch 'mynewthing'
201
+ Your branch is ahead of 'origin/main' by 1 commit.
202
+ (use "git push" to publish your local commits)
203
+ $ echo 'randomchange' >> README.md
204
+ $ git commit -a -m change
205
+ [mynewthing d33e082] change
206
+ 1 file changed, 1 insertion(+)
207
+ $ sj sl
208
+ * d33e082 (HEAD -> mynewthing) change
209
+ | * af6f143 (dependent2) morestuff
210
+ | * 028c7f4 (anotherfeature) stuff
211
+ | | * 59c0522 (dependentnewthing) anothertest
212
+ | |/
213
+ |/|
214
+ * | 6ebaa28 test
215
+ |/
216
+ o 7a0ffd0 (tag: v1.1.2, origin/main, origin/HEAD, main) Version bump (#160)
217
+ ```
218
+
219
+ We can see here now that `dependentnewthing`, is based off a commit that _used_
220
+ to be `mynewthing`, but `mynewthing` has moved. But SugarJar will handle this
221
+ all correctly when we ask it to update the branch:
222
+
223
+ ```text
224
+ $ sj co dependentnewthing
225
+ Switched to branch 'dependentnewthing'
226
+ Your branch and 'mynewthing' have diverged,
227
+ and have 1 and 1 different commits each, respectively.
228
+ (use "git pull" if you want to integrate the remote branch with yours)
229
+ $ sj up
230
+ dependentnewthing rebased on mynewthing
231
+ $ sj sl
232
+ * 93ed585 (HEAD -> dependentnewthing) anothertest
233
+ * d33e082 (mynewthing) change
234
+ * 6ebaa28 test
235
+ | * af6f143 (dependent2) morestuff
236
+ | * 028c7f4 (anotherfeature) stuff
237
+ |/
238
+ o 7a0ffd0 (tag: v1.1.2, origin/main, origin/HEAD, main) Version bump (#160)
239
+ ```
240
+
241
+ Now, lets say that `mynewthing` gets merged and we use `bclean` to clean it all
242
+ up, what happens then?
243
+
244
+ ```text
245
+ $ sj up
246
+ The brach we were tracking is gone, resetting tracking to origin/main
247
+ dependentnewthing rebased on origin/main
248
+ ```
249
+
250
+ ### Creating Stacked PRs with subfeatures
251
+
252
+ When dependent branches are created with `subfeature`, when you create a PR,
253
+ SugarJar will automatically set the 'base' of the PR to the parent branch. By
254
+ default it'll prompt you about this, but you can set `pr_autostack` to `true`
255
+ in your config to tell it to always do this (or `false` to never do this):
256
+
257
+ ```text
258
+ $ sj spr
259
+ Autofilling in PR from commit message
260
+ It looks like this is a subfeature, would you like to base this PR on mynewthing? [y/n] y
261
+ ...
154
262
  ```
155
263
 
156
264
  ## Have a better lint/unittest experience!
data/bin/sj CHANGED
@@ -91,6 +91,24 @@ parser = OptionParser.new do |opts|
91
91
  options['log_level'] = level
92
92
  end
93
93
 
94
+ opts.on(
95
+ '--[no-]pr-autofill',
96
+ 'When creating a PR, auto fill the title & description from the top ' +
97
+ 'commit if we are using "gh". [default: true]',
98
+ ) do |autofill|
99
+ options['pr_autofill'] = autofill
100
+ end
101
+
102
+ opts.on(
103
+ '--[no-]pr-autostack',
104
+ 'When creating a PR, if this is a subfeature, should we make it a ' +
105
+ 'PR on the PR for the parent feature. If not specified, we prompt ' +
106
+ 'when this happens, when true always do this, when false never do ' +
107
+ 'this. Only applicable when usiing "gh" and on branch-based PRs.',
108
+ ) do |autostack|
109
+ options['pr_autostack'] = autostack
110
+ end
111
+
94
112
  opts.on('--[no-]use-color', 'Enable color. [default: true]') do |color|
95
113
  options['color'] = color
96
114
  end
@@ -112,10 +130,10 @@ COMMANDS:
112
130
  Same as "amend" but without changing the message. Alias for
113
131
  "git commit --amend --no-edit".
114
132
 
115
- bclean
116
- If safe, delete the current branch. Unlike "git branch -d",
117
- bclean can handle squash-merged branches. Think of it as
118
- a smarter "git branch -d".
133
+ bclean [<branch>]
134
+ If safe, delete the current branch (or the specified branch).
135
+ Unlike "git branch -d", bclean can handle squash-merged branches.
136
+ Think of it as a smarter "git branch -d".
119
137
 
120
138
  bcleanall
121
139
  Walk all branches, and try to delete them if it's safe. See
@@ -127,7 +145,7 @@ COMMANDS:
127
145
  br
128
146
  Verbose branch list. An alias for "git branch -v".
129
147
 
130
- feature
148
+ feature, f <branch_name>
131
149
  Create a "feature" branch. It's morally equivalent to
132
150
  "git checkout -b" except it defaults to creating it based on
133
151
  some form of 'master' instead of your current branch. In order
@@ -177,11 +195,17 @@ COMMANDS:
177
195
  A smart wrapper to "git push" that runs whatever is defined in
178
196
  "on_push" in .sugarjar.yml, and only pushes if they succeed.
179
197
 
198
+ subfeature, sf <feature>
199
+ An alias for 'sj feature <feature> <current_branch>'
200
+
180
201
  unit
181
202
  Run any unitests configured in .sugarjar.yaml.
182
203
 
183
- up
184
- Rebase the current branch on upstream/master or origin/master.
204
+ up [<branch>]
205
+ Rebase the current branch (or specified branch) intelligently.
206
+ In most causes this will check for a main (or master) branch on
207
+ upstream, then origin. If a branch explicitly tracks something
208
+ else, then that will be used, instead.
185
209
 
186
210
  upall
187
211
  Same as "up", but for all branches.
@@ -23,6 +23,8 @@ class SugarJar
23
23
  @repo_config = SugarJar::RepoConfig.config
24
24
  SugarJar::Log.debug("Repoconfig: #{@repo_config}")
25
25
  @color = options['color']
26
+ @pr_autofill = options['pr_autofill']
27
+ @pr_autostack = options['pr_autostack']
26
28
  @feature_prefix = options['feature_prefix']
27
29
  @checks = {}
28
30
  @main_branch = nil
@@ -43,19 +45,31 @@ class SugarJar
43
45
  name = fprefix(name)
44
46
  die("#{name} already exists!") if all_local_branches.include?(name)
45
47
  base ||= most_main
46
- base_pieces = base.split('/')
47
- git('fetch', base_pieces[0]) if base_pieces.length > 1
48
+ # If our base is a local branch, don't try to parse it for a remote name
49
+ unless all_local_branches.include?(base)
50
+ base_pieces = base.split('/')
51
+ git('fetch', base_pieces[0]) if base_pieces.length > 1
52
+ end
48
53
  git('checkout', '-b', name, base)
54
+ git('branch', '-u', base)
49
55
  SugarJar::Log.info(
50
56
  "Created feature branch #{color(name, :green)} based on " +
51
57
  color(base, :green),
52
58
  )
53
59
  end
60
+ alias f feature
61
+
62
+ def subfeature(name)
63
+ assert_in_repo
64
+ SugarJar::Log.debug("Subfature: #{name}")
65
+ feature(name, current_branch)
66
+ end
67
+ alias sf subfeature
54
68
 
55
69
  def bclean(name = nil)
56
70
  assert_in_repo
57
71
  name ||= current_branch
58
- name = fprefix(name) unless all_local_branches.include?(name)
72
+ name = fprefix(name)
59
73
  if clean_branch(name)
60
74
  SugarJar::Log.info("#{name}: #{color('reaped', :green)}")
61
75
  else
@@ -100,7 +114,7 @@ class SugarJar
100
114
  # and then add any featureprefix, and if _that_ is a branch
101
115
  # name, replace the last arguement with that
102
116
  name = args.last
103
- bname = fprefix(name) unless all_local_branches.include?(name)
117
+ bname = fprefix(name)
104
118
  if all_local_branches.include?(bname)
105
119
  SugarJar::Log.debug("Featurepefixing #{name} -> #{bname}")
106
120
  args[-1] = bname
@@ -133,11 +147,14 @@ class SugarJar
133
147
 
134
148
  alias sl smartlog
135
149
 
136
- def up
150
+ def up(branch = nil)
137
151
  assert_in_repo
152
+ branch ||= current_branch
153
+ branch = fprefix(branch)
138
154
  # get a copy of our current branch, if rebase fails, we won't
139
155
  # be able to determine it without backing out
140
156
  curr = current_branch
157
+ git('checkout', branch)
141
158
  result = gitup
142
159
  if result['so'].error?
143
160
  backout = ''
@@ -156,6 +173,8 @@ class SugarJar
156
173
  SugarJar::Log.info(
157
174
  "#{color(current_branch, :green)} rebased on #{result['base']}",
158
175
  )
176
+ # go back to where we were if we rebased a different branch
177
+ git('checkout', curr) if branch != curr
159
178
  end
160
179
  end
161
180
 
@@ -302,6 +321,7 @@ class SugarJar
302
321
  def smartpullrequest(*args)
303
322
  assert_in_repo
304
323
  assert_common_main_branch
324
+
305
325
  if dirty?
306
326
  SugarJar::Log.warn(
307
327
  'Your repo is dirty, so I am not going to create a pull request. ' +
@@ -309,9 +329,47 @@ class SugarJar
309
329
  )
310
330
  exit(1)
311
331
  end
332
+
312
333
  if gh?
334
+ curr = current_branch
335
+ base = tracked_branch
336
+ if @pr_autofill
337
+ SugarJar::Log.info('Autofilling in PR from commit message')
338
+ num_commits = git(
339
+ 'rev-list', '--count', curr, "^#{base}"
340
+ ).stdout.strip.to_i
341
+ if num_commits > 1
342
+ args.unshift('--fill-first')
343
+ else
344
+ args.unshift('--fill')
345
+ end
346
+ end
347
+ if subfeature?(base)
348
+ if upstream != push_org
349
+ SugarJar::Log.warn(
350
+ 'Unfortunately you cannot based one PR on another PR when' +
351
+ " using fork-based PRs. We will base this on #{most_main}." +
352
+ ' This just means the PR "Changes" tab will show changes for' +
353
+ ' the full stack until those other PRs are merged and this PR' +
354
+ ' PR is rebased.',
355
+ )
356
+ # nil is prompt, true is always, false is never
357
+ elsif @pr_autostack.nil?
358
+ $stdout.print(
359
+ 'It looks like this is a subfeature, would you like to base ' +
360
+ "this PR on #{base}? [y/n] ",
361
+ )
362
+ ans = $stdin.gets.strip
363
+ args.unshift('--base', base) if %w{Y y}.include?(ans)
364
+ elsif @pr_autostack
365
+ args.unshift('--base', base)
366
+ end
367
+ end
368
+ # <org>:<branch> is the GH API syntax for:
369
+ # look for a branch of name <branch>, from a fork in owner <org>
370
+ args.unshift('--head', "#{push_org}:#{curr}")
313
371
  SugarJar::Log.trace("Running: gh pr create #{args.join(' ')}")
314
- system(which('gh'), 'pr', 'create', '--fill', *args)
372
+ system(which('gh'), 'pr', 'create', *args)
315
373
  else
316
374
  SugarJar::Log.trace("Running: hub pull-request #{args.join(' ')}")
317
375
  system(which('hub'), 'pull-request', *args)
@@ -341,7 +399,7 @@ class SugarJar
341
399
 
342
400
  src = "origin/#{current_branch}"
343
401
  fetch('origin')
344
- diff = git('diff', src).stdout
402
+ diff = git('diff', "..#{src}").stdout
345
403
  return unless diff && !diff.empty?
346
404
 
347
405
  puts "Will merge the following suggestions:\n\n#{diff}"
@@ -369,6 +427,9 @@ class SugarJar
369
427
  def fprefix(name)
370
428
  return name unless @feature_prefix
371
429
 
430
+ return name if name.start_with?(@feature_prefix)
431
+ return name if all_local_branches.include?(name)
432
+
372
433
  newname = "#{@feature_prefix}#{name}"
373
434
  SugarJar::Log.debug(
374
435
  "Munging feature name: #{name} -> #{newname} due to feature prefix",
@@ -430,6 +491,10 @@ class SugarJar
430
491
  end
431
492
  end
432
493
 
494
+ def extract_repo(repo)
495
+ File.basename(repo, '.git')
496
+ end
497
+
433
498
  def forked_repo(repo, username)
434
499
  repo = if repo.start_with?('http', 'git@')
435
500
  File.basename(repo)
@@ -712,11 +777,15 @@ class SugarJar
712
777
  end
713
778
 
714
779
  def all_local_branches
715
- branches = []
716
- git('branch', '--format', '%(refname)').stdout.lines.each do |line|
717
- branches << branch_from_ref(line.strip)
780
+ git(
781
+ 'branch', '--format', '%(refname)'
782
+ ).stdout.lines.map do |line|
783
+ branch_from_ref(line.strip)
718
784
  end
719
- branches
785
+ end
786
+
787
+ def all_remotes
788
+ git('remote').stdout.lines.map(&:strip)
720
789
  end
721
790
 
722
791
  def safe_to_clean(branch)
@@ -794,14 +863,28 @@ class SugarJar
794
863
  SugarJar::Log.debug('Fetching upstream')
795
864
  fetch_upstream
796
865
  curr = current_branch
797
- base = tracked_branch
866
+ # this isn't a hash, it's a named param, silly rubocop
867
+ # rubocop:disable Style/HashSyntax
868
+ base = tracked_branch(fallback: false)
869
+ # rubocop:enable Style/HashSyntax
870
+ unless base
871
+ SugarJar::Log.info(
872
+ 'The brach we were tracking is gone, resetting tracking to ' +
873
+ most_main,
874
+ )
875
+ git('branch', '-u', most_main)
876
+ base = most_main
877
+ end
878
+ # If this is a subfeature based on a local branch which has since
879
+ # been deleted, 'tracked branch' will automatically return <most_main>
880
+ # so we don't need any special handling for that
798
881
  if !MAIN_BRANCHES.include?(curr) && base == "origin/#{curr}"
799
882
  SugarJar::Log.warn(
800
883
  "This branch is tracking origin/#{curr}, which is probably your " +
801
884
  'downstream (where you push _to_) as opposed to your upstream ' +
802
885
  '(where you pull _from_). This means that "sj up" is probably ' +
803
886
  'rebasing on the wrong thing and doing nothing. You probably want ' +
804
- 'to do a "git branch -u upstream".',
887
+ "to do a 'git branch -u #{most_main}'.",
805
888
  )
806
889
  end
807
890
  SugarJar::Log.debug('Rebasing')
@@ -812,6 +895,12 @@ class SugarJar
812
895
  }
813
896
  end
814
897
 
898
+ # determine if this branch is based on another local branch (i.e. is a
899
+ # subfeature). Used to figure out of we should stack the PR
900
+ def subfeature?(base)
901
+ all_local_branches.reject { |x| x == most_main }.include?(base)
902
+ end
903
+
815
904
  def rebase_in_progress?
816
905
  # for rebase without -i
817
906
  rebase_file = git('rev-parse', '--git-path', 'rebase-apply').stdout.strip
@@ -821,15 +910,22 @@ class SugarJar
821
910
  File.exist?(rebase_file) || File.exist?(rebase_merge_file)
822
911
  end
823
912
 
824
- def tracked_branch
913
+ def tracked_branch(fallback: true)
914
+ branch = nil
825
915
  s = git_nofail(
826
916
  'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'
827
917
  )
828
918
  if s.error?
829
- most_main
919
+ branch = fallback ? most_main : nil
920
+ SugarJar::Log.debug("No specific tracked branch, using #{branch}")
830
921
  else
831
- s.stdout.strip
922
+ branch = s.stdout.strip
923
+ SugarJar::Log.debug(
924
+ "Using explicit tracked branch: #{branch}, use " +
925
+ '`git branch -u` to change',
926
+ )
832
927
  end
928
+ branch
833
929
  end
834
930
 
835
931
  def most_main
@@ -844,9 +940,7 @@ class SugarJar
844
940
  def upstream
845
941
  return @remote if @remote
846
942
 
847
- s = git('remote')
848
-
849
- remotes = s.stdout.lines.map(&:strip)
943
+ remotes = all_remotes
850
944
  SugarJar::Log.debug("remotes is #{remotes}")
851
945
  if remotes.empty?
852
946
  @remote = nil
@@ -862,6 +956,12 @@ class SugarJar
862
956
  @remote
863
957
  end
864
958
 
959
+ # Whatever org we push to, regardless of if this is a fork or not
960
+ def push_org
961
+ url = git('remote', 'get-url', 'origin').stdout.strip
962
+ extract_org(url)
963
+ end
964
+
865
965
  def branch_from_ref(ref, type = :local)
866
966
  # local branches are refs/head/XXXX
867
967
  # remote branches are refs/remotes/<remote>/XXXX
@@ -9,6 +9,8 @@ class SugarJar
9
9
  'github_cli' => 'auto',
10
10
  'github_user' => ENV.fetch('USER'),
11
11
  'fallthru' => true,
12
+ 'pr_autofill' => true,
13
+ 'pr_autostack' => nil,
12
14
  }.freeze
13
15
 
14
16
  def self._find_ordered_files
@@ -1,3 +1,3 @@
1
1
  class SugarJar
2
- VERSION = '1.1.1'.freeze
2
+ VERSION = '1.1.3'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sugarjar
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Phil Dibowitz
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-02-13 00:00:00.000000000 Z
11
+ date: 2025-02-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: deep_merge