sugarjar 1.1.1 → 1.1.3

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 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