sugarjar 0.0.10 → 1.0.0

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: a1c65749738fac53cfb69c3ec42007433f7598e3e43dd36e33217c8c0efbbb2e
4
- data.tar.gz: 50288abc1d21962e793731a937a8dd886bd2674db269ed62fbb0f89bb4121850
3
+ metadata.gz: 95e4979017c0ed58f99c446d9215a6b64aec7d5ebc22694a95903710d5af9d7e
4
+ data.tar.gz: b4b7a648bc7e8002e46dfbaa73cc52749cd38258b5782a2b33a6d66db98facfc
5
5
  SHA512:
6
- metadata.gz: 97ca2433bccc9e15d8c86ab141fa74e8f441f62cdb2be16e3131e2dcf71b37319ed0399990a96cb8c34ee4c55e75f1eca8a652cd5c7031f272493eec3fdb0f9a
7
- data.tar.gz: 5395379c1ca16bc50c900cd75a4b72665c0079dfe48f8717d38640d4ba610cc571c5f407333e088c0c387f19dc620039e3a406f03d6d6ea4a72a7348b98e7a41
6
+ metadata.gz: 0e34735382f6646bf5fe1577eb6637ad7aaceaa9ccf2d2dad024d4a95f3e2a0e8ec080fe086fd5dae7354b3f15f83d26e6ae8b067a1fae9fb5b3da777f9e1f86
7
+ data.tar.gz: 155d1aa1c7148f2d7181678f64ef64b17fb5efc90386ce3bd27c2bcc7b256113680b4e69a16558790ab3c77080325fc36a56c9f3cc76cf4962f8f575cd00d5d7
data/README.md CHANGED
@@ -3,10 +3,17 @@
3
3
  [![Lint](https://github.com/jaymzh/sugarjar/workflows/Lint/badge.svg)](https://github.com/jaymzh/sugarjar/actions?query=workflow%3ALint)
4
4
  [![Unittest](https://github.com/jaymzh/sugarjar/workflows/Unittests/badge.svg)](https://github.com/jaymzh/sugarjar/actions?query=workflow%3AUnittests)
5
5
  [![DCO](https://github.com/jaymzh/sugarjar/workflows/DCO%20Check/badge.svg)](https://github.com/jaymzh/sugarjar/actions?query=workflow%3A%22DCO+Check%22)
6
- [![Gem Version](https://badge.fury.io/rb/sugarjar.svg)](https://badge.fury.io/rb/sugarjar)
7
6
 
8
- Welcome to SugarJar - a git/github helper. It leverages the amazing GitHub cli,
9
- [hub](https://hub.github.com/), so you'll need that installed.
7
+ > [!IMPORTANT]
8
+ > You probably want to use [Sapling](https://sapling-scm.com/)
9
+ > instead of this! This was written as a stop-gap to approximate some features
10
+ > of the Facebook/Meta internal development tools before they were released.
11
+ > Now that those tools have been open-sourced and work with GitHub, this is
12
+ > likely to be deprecated at some point. Sapling is far more feature rich,
13
+ > faster, and have more development resources behind them.
14
+
15
+ Welcome to SugarJar - a git/github helper. It needs one of the GitHub CLI's:
16
+ either [gh](https://cli.github.com/) or the older [hub](https://hub.github.com/).
10
17
 
11
18
  SugarJar is inspired by [arcanist](https://github.com/phacility/arcanist), and
12
19
  its replacement at Facebook, JellyFish. Many of the features they provide for
@@ -19,6 +26,30 @@ If you miss Mondrian or Phabricator - this is the tool for you!
19
26
 
20
27
  If you don't, there's a ton of useful stuff for everyone!
21
28
 
29
+ ## Installation
30
+
31
+ Sugarjar is packaged in a variety of Linux distributions - see if it's on the
32
+ list here, and if so, use your package manager (or `gem`) to install it:
33
+
34
+ [![Packaging status](https://repology.org/badge/vertical-allrepos/sugarjar.svg)](https://repology.org/project/sugarjar/versions)
35
+
36
+ For Ubuntu users, Sugarjar will likely be available starting in the upcoming
37
+ 23.04 release. Until then, for supported LTS releases starting 20.04 (focal)
38
+ and the latest non-LTS release, you can use [this PPA](
39
+ https://launchpad.net/~michel-slm/+archive/ubuntu/sugarjar) maintained by the
40
+ Debian package maintainer.
41
+
42
+ Another option is to use our [omnibus
43
+ packages](https://github.com/jaymzh/sugarjar/releases). We keep Omnibus
44
+ packages for most distros that do not package Sugarjar. Omnibus packages are
45
+ distro packages (deb, rpm, etc.) that have all dependencies bundled up together
46
+ (including Ruby), and install in `/opt/sugarjar` allowing you to manage the
47
+ installation with your package manager, but with as a hermetically sealed app
48
+ that doesn't depend on the rest of your distro.
49
+
50
+ Finally, if none of those work for you, you can clone this repo and run it
51
+ directly from there.
52
+
22
53
  ## Auto cleanup squash-merged branches
23
54
 
24
55
  It is common for a PR to go back and forth with a variety of nits, lint fixes,
@@ -73,7 +104,7 @@ This will:
73
104
  * Clone your fork
74
105
  * Add the original as an 'upstream' remote
75
106
 
76
- Note that it takes `hub`s short-names for repos. No need to specify a full URL,
107
+ Note that it takes short names for repos. No need to specify a full URL,
77
108
  just a $org/$repo.
78
109
 
79
110
  Like `git clone`, `sj sclone` will accept an additional argument as the
@@ -198,6 +229,15 @@ smartlog` or `sj sl` for short.
198
229
 
199
230
  ![smartlog screenshot](https://github.com/jaymzh/sugarjar/blob/master/smartlog.png)
200
231
 
232
+ ## Pulling in suggestions from the web
233
+
234
+ When someone 'suggests' a change in the GitHub WebUI, once you choose to commit
235
+ them, your origin and local branches are no longer in-sync. The
236
+ `pullsuggestions` command will attempt to merge in any remote commits to your
237
+ local branch. This command will show a diff and ask for confirmation before
238
+ attempting the merge and - if allowed to continue - will use a fast-forward
239
+ merge.
240
+
201
241
  ## And more!
202
242
 
203
243
  See `sj help` for more commands!
@@ -205,8 +245,11 @@ See `sj help` for more commands!
205
245
  ## Using SugarJar as a git wrapper
206
246
 
207
247
  SugarJar, by default, will pass any command it doesn't know straight to `hub`
208
- (which passes commands **it** doesn't know to `git`). As such you can alias it
209
- to `git` and just have a super-git.
248
+ (which passes commands **it** doesn't know to `git`). If you have configured
249
+ SugarJar to use `gh` instead of `hub`, then it will pass commands straight to
250
+ `git` since `gh` doesn't act as a `git` wrapper.
251
+
252
+ As such you can alias it to `git` and just have a super-git.
210
253
 
211
254
  ```shell
212
255
  $ alias git=sj
@@ -248,8 +291,8 @@ log_level: debug
248
291
  github_user: jaymzh
249
292
  ```
250
293
 
251
- In addition, the environment variable `SUGARJAR_DEBUG` can be defined to set
252
- debug on. This is primarily used as a way to turn debug on earlier in order to
294
+ In addition, the environment variable `SUGARJAR_LOGLEVEL` can be defined to set
295
+ a log level. This is primarily used as a way to turn debug on earlier in order to
253
296
  troubleshoot configuration parsing.
254
297
 
255
298
  ## Repository Configuration
@@ -258,9 +301,15 @@ Sugarjar looks for a `.sugarjar.yaml` in the root of the repository to tell it
258
301
  how to handle repo-specific things. Currently there options are:
259
302
 
260
303
  * `lint` - A list of scripts to run on `sj lint`. These should be linters like
261
- rubocop or pyflake.
304
+ rubocop or pyflake. Linters will be run from the root of the repo.
305
+ * `lint_list_cmd` - A command to run which will print out linters to run, one
306
+ per line. Takes precedence over `lint`. The command (and the resulting
307
+ linters) will be run from the root of the repo.
262
308
  * `unit` - A list of scripts to run on `sj unit`. These should be unittest
263
- runners like rspec or pyunit.
309
+ runners like rspec or pyunit. Test will be run from the root of the repo.
310
+ * `unit_list_cmd` - A command to run which will print out the unit tests to
311
+ run, one more line. Takes precedence over `unit`. The command (and the
312
+ resulting unit tests) will be run from the root of the repo.
264
313
  * `on_push` - A list of types (`lint`, `unit`) of checks to run before pushing.
265
314
  It is highly recommended this is only `lint`. The goal here is to allow for
266
315
  the user to get quick stylistic feedback before pushing their branch to avoid
@@ -312,34 +361,40 @@ sj clone jaymzh/sugarjar --github-host githuh.com
312
361
  We will add the `hub.host` to the `sugarjar` clone so that future `hub` or `sj`
313
362
  commands work without needing to specify..
314
363
 
315
- ## Installing
316
-
317
- There are many ways to install SugarJar. The easiest is to use one of the
318
- packages we provide in the
319
- [releases](https://github.com/jaymzh/sugarjar/releases) section. Currently we
320
- provide packages for Fedora, CentOS, Debian, and Ubuntu, but if you want
321
- others, file an Issue. Since these packages are
322
- [omnibus](https://github.com/chef/omnibus) packages which means they are bundled
323
- with all of their dependencies. This means these packages will likely work as-is
324
- on later releases of these distros or any similar distros.
364
+ ## Choosing a GitHub CLI
325
365
 
326
- We also distribute SugarJar via [RubyGems](https://rubygems.org/gems/sugarjar/),
327
- so you can install it as a gem either via system ruby or via rvm/rbenv.
328
-
329
- Finally you can clone the git repo and run it from within the repo if you'd like.
366
+ SugarJar will use `gh` if it is available or otherwise fall back to `hub`. You
367
+ can override this by specifying `--github-cli` on the command line or setting
368
+ `github_cli` to either `gh` or `hub` (it defaults to `auto`) in your
369
+ configuration.
330
370
 
331
371
  ## FAQ
332
372
 
333
- Why the name SugarJar?
373
+ **Why the name SugarJar?**
334
374
 
335
- It's mostly a backranym. Like jellyfish, I wanted two letters that were on home
375
+ It's mostly a backronym. Like jellyfish, I wanted two letters that were on home
336
376
  row on different sides of the keyboard to make it easy to type. I looked at the
337
377
  possible options that where there and not taken and tried to find one I could
338
378
  make an appropriate name out of. Since this utility adds lots of sugar to git
339
379
  and github, it seemed appropriate.
340
380
 
341
- Why did you use `hub` instead of the newer `gh` CLI?
381
+ **Why did you originally use `hub` instead of the newer `gh` CLI?**
382
+
383
+ When I originally wrote SugarJar, `gh` was in early development, and `hub` had
384
+ many more features. In addition, I wrote SugarJar to be a wrapper for git/hub,
385
+ and `hub` allows this but `gh` does not.
386
+
387
+ When `gh` matured, we added experimental `gh` support in 0.0.11, and switched the
388
+ default to prefer `gh` in 1.0.0.
389
+
390
+ **I'd like to package SugarJar for my favorite distro/OS, is that OK?**
391
+
392
+ Of course! But I'd appreciate you emailing me to give me a heads up. Doing so
393
+ will allow me to make sure it shows up in the Repology badge above as well as
394
+ stop building Omnibus packages for whatever distro.
395
+
396
+ **What platforms does it work on?**
342
397
 
343
- `gh` is still new and not yet as feature rich as `hub`. Also I wanted SugarJar
344
- to be able to be a git wrapper, and so wrapping `hub` allows us to do that but
345
- wrapping `gh` does not.
398
+ Since it's Ruby, it should work across all platforms, however, it's developed
399
+ and primarily tested on Linux as well as regularly used on Mac. I've not tested
400
+ it on Windows, but I'll happily accept patches for Windows compatibility.
data/bin/sj CHANGED
@@ -29,12 +29,22 @@ parser = OptionParser.new do |opts|
29
29
  opts.separator ''
30
30
  opts.separator 'OPTIONS:'
31
31
 
32
- opts.on('--[no-]fallthru', 'Fall-thru to git') do |fallthru|
32
+ opts.on('--[no-]fallthru', 'Fall-thru to git. [default: true]') do |fallthru|
33
33
  options['fallthru'] = fallthru
34
34
  end
35
35
 
36
- opts.on('--github-user USER', 'Github username') do |user|
37
- options['github_user'] = user
36
+ opts.on('--feature-prefix', 'Prefix to use for feature branches') do |prefix|
37
+ options['feature_prefix'] = prefix
38
+ end
39
+
40
+ opts.on(
41
+ '--github-cli CLI',
42
+ %w{gh cli},
43
+ 'Github CLI to use ("gh" or "hub" or "auto"). Auto (the default) will ' +
44
+ 'prefer "gh" if it is available but will fall back to "hub." ' +
45
+ '[default: "auto"]',
46
+ ) do |cli|
47
+ options['github_cli'] = cli
38
48
  end
39
49
 
40
50
  opts.on(
@@ -48,6 +58,10 @@ parser = OptionParser.new do |opts|
48
58
  options['github_host'] = host
49
59
  end
50
60
 
61
+ opts.on('--github-user USER', 'Github username') do |user|
62
+ options['github_user'] = user
63
+ end
64
+
51
65
  opts.on('-h', '--help', 'Print this help message') do
52
66
  puts opts
53
67
  exit
@@ -55,22 +69,24 @@ parser = OptionParser.new do |opts|
55
69
 
56
70
  opts.on(
57
71
  '--ignore-dirty',
58
- 'Tell command that check for a dirty repo to carry on anyway.',
72
+ 'Tell command that check for a dirty repo to carry on anyway. ' +
73
+ '[default: false]',
59
74
  ) do
60
75
  options['ignore_dirty'] = true
61
76
  end
62
77
 
63
78
  opts.on(
64
79
  '--ignore-prerun-failure',
65
- 'Ignore preprun failure on *push commands.',
80
+ 'Ignore preprun failure on *push commands. [default: false]',
66
81
  ) do
67
82
  options['ignore_prerun_failure'] = true
68
83
  end
69
84
 
70
85
  opts.on(
71
86
  '--log-level LEVEL',
72
- 'Set logging level (fatal, error, warning, info, debug, trace). Default: ' +
73
- 'info',
87
+ 'Set logging level (fatal, error, warning, info, debug, trace). This can ' +
88
+ 'also be set via the SUGARJAR_LOGLEVEL environment variable. [default: ' +
89
+ 'info]',
74
90
  ) do |level|
75
91
  options['log_level'] = level
76
92
  end
@@ -118,6 +134,12 @@ COMMANDS:
118
134
  of preference it will be upstream/master, origin/master, master,
119
135
  depending upon what remotes are available.
120
136
 
137
+ Note that you can specify "--feature-prefix" (or add
138
+ "feature_prefix" to your config) to have all features created
139
+ with a prefix. This is useful for branch-based workflows where
140
+ developers are expected to create branches names that, for
141
+ example, start with their username.
142
+
121
143
  forcepush, fpush
122
144
  The same as "smartpush", but uses "--force-with-lease". This is
123
145
  a "safer" way of doing force-pushes and is the recommended way
@@ -128,6 +150,11 @@ COMMANDS:
128
150
  lint
129
151
  Run any linters configured in .sugarjar.yaml.
130
152
 
153
+ pullsuggestions, ps
154
+ Pull any suggestions *that have been committed* in the GitHub UI.
155
+ This will show the diff and prompt for confirmation before
156
+ merging. Note that a fast-forward merge will be used.
157
+
131
158
  smartclone, sclone
132
159
  A smart wrapper to "git clone" that handles forking and managing
133
160
  remotes for you.
@@ -162,6 +189,10 @@ COMMANDS:
162
189
  version
163
190
  Print the version of sugarjar, and then run 'hub version'
164
191
  to show the hub and git versions.
192
+
193
+ Be sure to checkout Sapling (https://sapling-scm.com/)! This was written as
194
+ a stop-gap to get Sapling features before it was open-sourced, and as such
195
+ I may deprecate this in the future.
165
196
  COMMANDS
166
197
 
167
198
  # rubocop:enable Layout/HeredocIndentation
@@ -232,7 +263,9 @@ else
232
263
  end
233
264
  end
234
265
 
235
- if ARGV.empty?
266
+ subcommand = argv_copy.reject { |x| x.start_with?('-') }.first
267
+
268
+ if ARGV.empty? || !subcommand
236
269
  puts parser
237
270
  exit
238
271
  end
@@ -243,7 +276,6 @@ options = config.merge(options)
243
276
  SugarJar::Log.level = options['log_level'].to_sym if options['log_level']
244
277
  sj = SugarJar::Commands.new(options)
245
278
 
246
- subcommand = argv_copy.reject { |x| x.start_with?('-') }.first
247
279
  is_valid_command = valid_commands.include?(subcommand.to_sym)
248
280
  argv_copy.delete(subcommand)
249
281
  SugarJar::Log.debug("subcommand is #{subcommand}")
@@ -265,7 +297,13 @@ if is_valid_command
265
297
  sj.send(subcommand.to_sym, *extra_opts)
266
298
  elsif options['fallthru']
267
299
  SugarJar::Log.debug("Falling thru to: hub #{ARGV.join(' ')}")
268
- exec('hub', *ARGV)
300
+ if options['github_cli'] == 'hub'
301
+ exec('hub', *ARGV)
302
+ else
303
+ # If we're using 'gh', it doesn't have 'git fall thru' support, so
304
+ # we pass thru directly to 'git'
305
+ exec('git', *ARGV)
306
+ end
269
307
  else
270
308
  SugarJar::Log.error("No such subcommand: #{subcommand}")
271
309
  end
@@ -22,20 +22,29 @@ class SugarJar
22
22
  @ignore_prerun_failure = options['ignore_prerun_failure']
23
23
  @repo_config = SugarJar::RepoConfig.config
24
24
  @color = options['color']
25
+ @feature_prefix = options['feature_prefix']
26
+ @checks = {}
27
+ @main_branch = nil
28
+ @main_remote_branches = {}
25
29
  return if options['no_change']
26
30
 
27
- set_hub_host if @ghhost
31
+ # technically this doesn't "change" things, but we won't have this
32
+ # option on the no_change call
33
+ @cli = determine_cli(options['github_cli'])
34
+
35
+ set_hub_host
28
36
  set_commit_template if @repo_config['commit_template']
29
37
  end
30
38
 
31
39
  def feature(name, base = nil)
32
40
  assert_in_repo
33
41
  SugarJar::Log.debug("Feature: #{name}, #{base}")
34
- die("#{name} already exists!") if all_branches.include?(name)
42
+ name = fprefix(name)
43
+ die("#{name} already exists!") if all_local_branches.include?(name)
35
44
  base ||= most_main
36
45
  base_pieces = base.split('/')
37
- hub('fetch', base_pieces[0]) if base_pieces.length > 1
38
- hub('checkout', '-b', name, base)
46
+ git('fetch', base_pieces[0]) if base_pieces.length > 1
47
+ git('checkout', '-b', name, base)
39
48
  SugarJar::Log.info(
40
49
  "Created feature branch #{color(name, :green)} based on " +
41
50
  color(base, :green),
@@ -45,6 +54,7 @@ class SugarJar
45
54
  def bclean(name = nil)
46
55
  assert_in_repo
47
56
  name ||= current_branch
57
+ name = fprefix(name) unless all_local_branches.include?(name)
48
58
  if clean_branch(name)
49
59
  SugarJar::Log.info("#{name}: #{color('reaped', :green)}")
50
60
  else
@@ -58,7 +68,7 @@ class SugarJar
58
68
  def bcleanall
59
69
  assert_in_repo
60
70
  curr = current_branch
61
- all_branches.each do |branch|
71
+ all_local_branches.each do |branch|
62
72
  if MAIN_BRANCHES.include?(branch)
63
73
  SugarJar::Log.debug("Skipping #{branch}")
64
74
  next
@@ -76,8 +86,8 @@ class SugarJar
76
86
  end
77
87
 
78
88
  # Return to the branch we were on, or main
79
- if all_branches.include?(curr)
80
- hub('checkout', curr)
89
+ if all_local_branches.include?(curr)
90
+ git('checkout', curr)
81
91
  else
82
92
  checkout_main_branch
83
93
  end
@@ -85,18 +95,18 @@ class SugarJar
85
95
 
86
96
  def co(*args)
87
97
  assert_in_repo
88
- s = hub('checkout', *args)
98
+ s = git('checkout', *args)
89
99
  SugarJar::Log.info(s.stderr + s.stdout.chomp)
90
100
  end
91
101
 
92
102
  def br
93
103
  assert_in_repo
94
- SugarJar::Log.info(hub('branch', '-v').stdout.chomp)
104
+ SugarJar::Log.info(git('branch', '-v').stdout.chomp)
95
105
  end
96
106
 
97
107
  def binfo
98
108
  assert_in_repo
99
- SugarJar::Log.info(hub(
109
+ SugarJar::Log.info(git(
100
110
  'log', '--graph', '--oneline', '--decorate', '--boundary',
101
111
  "#{tracked_branch}.."
102
112
  ).stdout.chomp)
@@ -105,7 +115,7 @@ class SugarJar
105
115
  # binfo for all branches
106
116
  def smartlog
107
117
  assert_in_repo
108
- SugarJar::Log.info(hub(
118
+ SugarJar::Log.info(git(
109
119
  'log', '--graph', '--oneline', '--decorate', '--boundary',
110
120
  '--branches', "#{most_main}.."
111
121
  ).stdout.chomp)
@@ -142,24 +152,24 @@ class SugarJar
142
152
 
143
153
  def qamend(*args)
144
154
  assert_in_repo
145
- SugarJar::Log.info(hub('commit', '--amend', '--no-edit', *args).stdout)
155
+ SugarJar::Log.info(git('commit', '--amend', '--no-edit', *args).stdout)
146
156
  end
147
157
 
148
158
  alias amendq qamend
149
159
 
150
160
  def upall
151
161
  assert_in_repo
152
- all_branches.each do |branch|
162
+ all_local_branches.each do |branch|
153
163
  next if MAIN_BRANCHES.include?(branch)
154
164
 
155
- hub('checkout', branch)
165
+ git('checkout', branch)
156
166
  result = gitup
157
167
  if result['so'].error?
158
168
  SugarJar::Log.error(
159
169
  "#{color(branch, :red)} failed rebase. Reverting attempt and " +
160
170
  'moving to next branch. Try `sj up` manually on that branch.',
161
171
  )
162
- hub('rebase', '--abort')
172
+ git('rebase', '--abort')
163
173
  else
164
174
  SugarJar::Log.info(
165
175
  "#{color(branch, :green)} rebased on " +
@@ -176,21 +186,42 @@ class SugarJar
176
186
 
177
187
  reponame = File.basename(repo, '.git')
178
188
  dir ||= reponame
189
+ org = extract_org(repo)
190
+
179
191
  SugarJar::Log.info("Cloning #{reponame}...")
180
- hub('clone', canonicalize_repo(repo), dir, *args)
181
192
 
193
+ # GH's 'fork' command (with the --clone arg) will fork, if necessary,
194
+ # then clone, and then setup the remotes with the appropriate names. So
195
+ # we just let it do all the work for us and return.
196
+ #
197
+ # Unless the repo is in our own org and cannot be forked, then it
198
+ # will fail.
199
+ if gh? && org != @ghuser
200
+ ghcli('repo', 'fork', '--clone', canonicalize_repo(repo), dir, *args)
201
+ SugarJar::Log.info('Remotes "origin" and "upstream" configured.')
202
+ return
203
+ end
204
+
205
+ # For 'hub' first we clone, using git, as 'hub' always needs a repo to
206
+ # operate on.
207
+ #
208
+ # Or for 'gh' when we can't fork...
209
+ git('clone', canonicalize_repo(repo), dir, *args)
210
+
211
+ # Then we go into it and attempt to use the 'fork' capability
212
+ # or if not
182
213
  Dir.chdir dir do
183
214
  # Now that we have a repo, if we have a hub host set it.
184
- set_hub_host if @ghhost
215
+ set_hub_host
185
216
 
186
- org = extract_org(repo)
187
217
  SugarJar::Log.debug("Comparing org #{org} to ghuser #{@ghuser}")
188
218
  if org == @ghuser
189
219
  puts 'Cloned forked or self-owned repo. Not creating "upstream".'
220
+ SugarJar::Log.info('Remotes "origin" and "upstream" configured.')
190
221
  return
191
222
  end
192
223
 
193
- s = hub_nofail('fork', '--remote-name=origin')
224
+ s = ghcli_nofail('repo', 'fork', '--remote-name=origin')
194
225
  if s.error?
195
226
  if s.stdout.include?('SAML enforcement')
196
227
  SugarJar::Log.info(
@@ -199,18 +230,18 @@ class SugarJar
199
230
  )
200
231
  exit(1)
201
232
  else
202
- # In old versions of hub, it would fail if the upstream fork
203
- # already existed. If we got an error, but didn't recognize
204
- # that, we'll assume that's what happened and try to add the
205
- # remote ourselves.
233
+ # gh as well as old versions of hub, it would fail if the upstream
234
+ # fork already existed. If we got an error, but didn't recognize
235
+ # that, we'll assume that's what happened and try to add the remote
236
+ # ourselves.
206
237
  SugarJar::Log.info("Fork (#{@ghuser}/#{reponame}) detected.")
207
238
  SugarJar::Log.debug(
208
239
  'The above is a bit of a lie. "hub" failed to fork and it was ' +
209
240
  'not a SAML error, so our best guess is that a fork exists ' +
210
241
  'and so we will try to configure it.',
211
242
  )
212
- hub('remote', 'rename', 'origin', 'upstream')
213
- hub('remote', 'add', 'origin', forked_repo(repo, @ghuser))
243
+ git('remote', 'rename', 'origin', 'upstream')
244
+ git('remote', 'add', 'origin', forked_repo(repo, @ghuser))
214
245
  end
215
246
  else
216
247
  SugarJar::Log.info("Forked #{reponame} to #{@ghuser}")
@@ -247,11 +278,15 @@ class SugarJar
247
278
 
248
279
  def version
249
280
  puts "sugarjar version #{SugarJar::VERSION}"
250
- puts hub('version').stdout
281
+ puts ghcli('version').stdout
282
+ # 'hub' prints the 'git' version, but gh doesn't, so if we're on 'gh'
283
+ # print out the git version directly
284
+ puts git('version').stdout if gh?
251
285
  end
252
286
 
253
- def smartpullrequest
287
+ def smartpullrequest(*args)
254
288
  assert_in_repo
289
+ assert_common_main_branch
255
290
  if dirty?
256
291
  SugarJar::Log.warn(
257
292
  'Your repo is dirty, so I am not going to create a pull request. ' +
@@ -259,14 +294,73 @@ class SugarJar
259
294
  )
260
295
  exit(1)
261
296
  end
262
- system(which('hub'), 'pull-request')
297
+ if gh?
298
+ SugarJar::Log.trace("Running: gh pr create #{args.join(' ')}")
299
+ system(which('gh'), 'pr', 'create', *args)
300
+ else
301
+ SugarJar::Log.trace("Running: hub pull-request #{args.join(' ')}")
302
+ system(which('hub'), 'pull-request', *args)
303
+ end
263
304
  end
264
305
 
265
306
  alias spr smartpullrequest
266
307
  alias smartpr smartpullrequest
267
308
 
309
+ def pullsuggestions
310
+ assert_in_repo
311
+
312
+ if dirty?
313
+ if @ignore_dirty
314
+ SugarJar::Log.warn(
315
+ 'Your repo is dirty, but --ignore-dirty was specified, so ' +
316
+ 'carrying on anyway.',
317
+ )
318
+ else
319
+ SugarJar::Log.error(
320
+ 'Your repo is dirty, so I am not going to push. Please commit ' +
321
+ 'or amend first.',
322
+ )
323
+ exit(1)
324
+ end
325
+ end
326
+
327
+ src = "origin/#{current_branch}"
328
+ fetch('origin')
329
+ diff = git('diff', src).stdout
330
+ return unless diff && !diff.empty?
331
+
332
+ puts "Will merge the following suggestions:\n\n#{diff}"
333
+
334
+ loop do
335
+ $stdout.print("\nAre you sure? [y/n] ")
336
+ ans = $stdin.gets.strip
337
+ case ans
338
+ when /^[Yy]$/
339
+ system(which('git'), 'merge', '--ff', "origin/#{current_branch}")
340
+ break
341
+ when /^[Nn]$/, /^[Qq](uit)?/
342
+ puts 'Not merging at user request...'
343
+ break
344
+ else
345
+ puts "Didn't understand '#{ans}'."
346
+ end
347
+ end
348
+ end
349
+
350
+ alias ps pullsuggestions
351
+
268
352
  private
269
353
 
354
+ def fprefix(name)
355
+ return name unless @feature_prefix
356
+
357
+ newname = "#{@feature_prefix}#{name}"
358
+ SugarJar::Log.debug(
359
+ "Munging feature name: #{name} -> #{newname} due to feature prefix",
360
+ )
361
+ newname
362
+ end
363
+
270
364
  def _smartpush(remote, branch, force)
271
365
  unless remote && branch
272
366
  remote ||= 'origin'
@@ -302,11 +396,11 @@ class SugarJar
302
396
 
303
397
  args = ['push', remote, branch]
304
398
  args << '--force-with-lease' if force
305
- puts hub(*args).stderr
399
+ puts git(*args).stderr
306
400
  end
307
401
 
308
402
  def dirty?
309
- s = hub_nofail('diff', '--quiet')
403
+ s = git_nofail('diff', '--quiet')
310
404
  s.error?
311
405
  end
312
406
 
@@ -343,9 +437,9 @@ class SugarJar
343
437
  end
344
438
 
345
439
  def set_hub_host
346
- return unless in_repo
440
+ return unless hub? && in_repo && @ghhost
347
441
 
348
- s = hub_nofail('config', '--local', '--get', 'hub.host')
442
+ s = git_nofail('config', '--local', '--get', 'hub.host')
349
443
  if s.error?
350
444
  SugarJar::Log.info("Setting repo hub.host = #{@ghhost}")
351
445
  else
@@ -363,7 +457,7 @@ class SugarJar
363
457
  end
364
458
  return
365
459
  end
366
- hub('config', '--local', '--add', 'hub.host', @ghhost)
460
+ git('config', '--local', '--add', 'hub.host', @ghhost)
367
461
  end
368
462
 
369
463
  def set_commit_template
@@ -384,7 +478,7 @@ class SugarJar
384
478
  )
385
479
  end
386
480
 
387
- s = hub_nofail('config', '--local', 'commit.template')
481
+ s = git_nofail('config', '--local', 'commit.template')
388
482
  unless s.error?
389
483
  current = s.stdout.strip
390
484
  if current == @repo_config['commit_template']
@@ -402,18 +496,60 @@ class SugarJar
402
496
  'Setting repo-specific commit template to ' +
403
497
  "#{@repo_config['commit_template']} per sugarjar repo config.",
404
498
  )
405
- hub(
499
+ git(
406
500
  'config', '--local', 'commit.template', @repo_config['commit_template']
407
501
  )
408
502
  end
409
503
 
410
- def run_check(type)
411
- unless @repo_config[type]
412
- SugarJar::Log.debug("No #{type} configured. Returning success")
413
- return true
504
+ def get_checks_from_command(type)
505
+ return nil unless @repo_config["#{type}_list_cmd"]
506
+
507
+ cmd = @repo_config["#{type}_list_cmd"]
508
+ short = cmd.split.first
509
+ unless File.exist?(short)
510
+ SugarJar::Log.error(
511
+ "Configured #{type}_list_cmd #{short} does not exist!",
512
+ )
513
+ return false
514
+ end
515
+ s = Mixlib::ShellOut.new(cmd).run_command
516
+ if s.error?
517
+ SugarJar::Log.error(
518
+ "#{type}_list_cmd (#{cmd}) failed: #{s.format_for_exception}",
519
+ )
520
+ return false
521
+ end
522
+ s.stdout.split("\n")
523
+ end
524
+
525
+ # determine if we're using the _list_cmd and if so run it to get the
526
+ # checks, or just use the directly-defined check, and cache it
527
+ def get_checks(type)
528
+ return @checks[type] if @checks[type]
529
+
530
+ ret = get_checks_from_command(type)
531
+ if ret
532
+ SugarJar::Log.debug("Found #{type}s: #{ret}")
533
+ @checks[type] = ret
534
+ # if it's explicitly false, we failed to run the command
535
+ elsif ret == false
536
+ @checks[type] = false
537
+ # otherwise, we move on (basically: it's nil, there was no _list_cmd)
538
+ else
539
+ SugarJar::Log.debug("[#{type}]: using listed linters: #{ret}")
540
+ @checks[type] = @repo_config[type] || []
414
541
  end
542
+ @checks[type]
543
+ end
544
+
545
+ def run_check(type)
415
546
  Dir.chdir repo_root do
416
- @repo_config[type].each do |check|
547
+ checks = get_checks(type)
548
+ # if we failed to determine the checks, the the checks have effectively
549
+ # failed
550
+ return false unless checks
551
+
552
+ checks.each do |check|
417
553
  SugarJar::Log.debug("Running #{type} #{check}")
418
554
 
419
555
  short = check.split.first
@@ -431,7 +567,7 @@ class SugarJar
431
567
  SugarJar::Log.warn(
432
568
  "The linter modified the repo. Here's the diff:\n",
433
569
  )
434
- puts hub('diff').stdout
570
+ puts git('diff').stdout
435
571
  loop do
436
572
  $stdout.print(
437
573
  "\nWould you like to\n\t[q]uit and inspect\n\t[a]mend the " +
@@ -483,16 +619,53 @@ class SugarJar
483
619
  exit(1)
484
620
  end
485
621
 
622
+ def assert_common_main_branch
623
+ upstream_branch = main_remote_branch(upstream)
624
+ unless main_branch == upstream_branch
625
+ die(
626
+ "The local main branch is '#{main_branch}', but the main branch " +
627
+ "of the #{upstream} remote is '#{upstream_branch}'. You probably " +
628
+ "want to rename your local branch by doing:\n\t" +
629
+ "git branch -m #{main_branch} #{upstream_branch}\n\t" +
630
+ "git fetch #{upstream}\n\t" +
631
+ "git branch -u #{upstream}/#{upstream_branch} #{upstream_branch}\n" +
632
+ "\tgit remote set-head #{upstream} -a",
633
+ )
634
+ end
635
+ return if upstream_branch == 'origin'
636
+
637
+ origin_branch = main_remote_branch('origin')
638
+ return if origin_branch == upstream_branch
639
+
640
+ die(
641
+ "The main branch of your upstream (#{upstream_branch}) and your " +
642
+ "fork/origin (#{origin_branch}) are not the same. You should go " +
643
+ "to https://#{@ghhost || 'github.com'}/#{@ghuser}/#{repo_name}/" +
644
+ 'branches/ and rename the \'default\' branch to ' +
645
+ "'#{upstream_branch}'. It will then give you some commands to " +
646
+ 'run to update this clone.',
647
+ )
648
+ end
649
+
486
650
  def assert_in_repo
487
651
  die('sugarjar must be run from inside a git repo') unless in_repo
488
652
  end
489
653
 
654
+ def determine_main_branch(branches)
655
+ branches.include?('main') ? 'main' : 'master'
656
+ end
657
+
490
658
  def main_branch
491
- @main_branch = all_branches.include?('main') ? 'main' : 'master'
659
+ @main_branch = determine_main_branch(all_local_branches)
660
+ end
661
+
662
+ def main_remote_branch(remote)
663
+ @main_remote_branches[remote] ||=
664
+ determine_main_branch(all_remote_branches(remote))
492
665
  end
493
666
 
494
667
  def checkout_main_branch
495
- hub('checkout', main_branch)
668
+ git('checkout', main_branch)
496
669
  end
497
670
 
498
671
  def clean_branch(name)
@@ -503,15 +676,25 @@ class SugarJar
503
676
 
504
677
  SugarJar::Log.debug('branch deemed safe to delete...')
505
678
  checkout_main_branch
506
- hub('branch', '-D', name)
679
+ git('branch', '-D', name)
507
680
  gitup
508
681
  true
509
682
  end
510
683
 
511
- def all_branches
684
+ def all_remote_branches(remote = 'origin')
685
+ branches = []
686
+ git('branch', '-r', '--format', '%(refname)').stdout.lines.each do |line|
687
+ next unless line.start_with?("refs/remotes/#{remote}/")
688
+
689
+ branches << branch_from_ref(line.strip, :remote)
690
+ end
691
+ branches
692
+ end
693
+
694
+ def all_local_branches
512
695
  branches = []
513
- hub('branch', '--format', '%(refname)').stdout.lines.each do |line|
514
- branches << line.strip.split('/')[2]
696
+ git('branch', '--format', '%(refname)').stdout.lines.each do |line|
697
+ branches << branch_from_ref(line.strip)
515
698
  end
516
699
  branches
517
700
  end
@@ -520,12 +703,12 @@ class SugarJar
520
703
  # cherry -v will output 1 line per commit on the target branch
521
704
  # prefixed by a - or + - anything with a - can be dropped, anything
522
705
  # else cannot.
523
- out = hub(
706
+ out = git(
524
707
  'cherry', '-v', tracked_branch, branch
525
708
  ).stdout.lines.reject do |line|
526
709
  line.start_with?('-')
527
710
  end
528
- if out.length.zero?
711
+ if out.empty?
529
712
  SugarJar::Log.debug(
530
713
  "cherry-pick shows branch #{branch} obviously safe to delete",
531
714
  )
@@ -539,8 +722,8 @@ class SugarJar
539
722
  # First we need a temp branch to work on
540
723
  tmpbranch = "_sugar_jar.#{Process.pid}"
541
724
 
542
- hub('checkout', '-b', tmpbranch, tracked_branch)
543
- s = hub_nofail('merge', '--squash', branch)
725
+ git('checkout', '-b', tmpbranch, tracked_branch)
726
+ s = git_nofail('merge', '--squash', branch)
544
727
  if s.error?
545
728
  cleanup_tmp_branch(tmpbranch, branch)
546
729
  SugarJar::Log.debug(
@@ -551,7 +734,7 @@ class SugarJar
551
734
  return false
552
735
  end
553
736
 
554
- s = hub('diff', '--staged')
737
+ s = git('diff', '--staged')
555
738
  out = s.stdout
556
739
  SugarJar::Log.debug("Squash-merged diff: #{out}")
557
740
  cleanup_tmp_branch(tmpbranch, branch)
@@ -569,18 +752,22 @@ class SugarJar
569
752
  end
570
753
 
571
754
  def cleanup_tmp_branch(tmp, backto)
572
- hub('reset', '--hard', tracked_branch)
573
- hub('checkout', backto)
574
- hub('branch', '-D', tmp)
755
+ git('reset', '--hard', tracked_branch)
756
+ git('checkout', backto)
757
+ git('branch', '-D', tmp)
575
758
  end
576
759
 
577
760
  def current_branch
578
- hub('symbolic-ref', 'HEAD').stdout.strip.split('/')[2]
761
+ branch_from_ref(git('symbolic-ref', 'HEAD').stdout.strip)
579
762
  end
580
763
 
581
764
  def fetch_upstream
582
765
  us = upstream
583
- hub('fetch', us) if us
766
+ fetch(us) if us
767
+ end
768
+
769
+ def fetch(remote)
770
+ git('fetch', remote)
584
771
  end
585
772
 
586
773
  def gitup
@@ -598,7 +785,7 @@ class SugarJar
598
785
  )
599
786
  end
600
787
  SugarJar::Log.debug('Rebasing')
601
- s = hub_nofail('rebase', base)
788
+ s = git_nofail('rebase', base)
602
789
  {
603
790
  'so' => s,
604
791
  'base' => base,
@@ -606,7 +793,7 @@ class SugarJar
606
793
  end
607
794
 
608
795
  def tracked_branch
609
- s = hub_nofail(
796
+ s = git_nofail(
610
797
  'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'
611
798
  )
612
799
  if s.error?
@@ -628,7 +815,7 @@ class SugarJar
628
815
  def upstream
629
816
  return @remote if @remote
630
817
 
631
- s = hub('remote')
818
+ s = git('remote')
632
819
 
633
820
  remotes = s.stdout.lines.map(&:strip)
634
821
  SugarJar::Log.debug("remotes is #{remotes}")
@@ -646,6 +833,13 @@ class SugarJar
646
833
  @remote
647
834
  end
648
835
 
836
+ def branch_from_ref(ref, type = :local)
837
+ # local branches are refs/head/XXXX
838
+ # remote branches are refs/remotes/<remote>/XXXX
839
+ base = type == :local ? 2 : 3
840
+ ref.split('/')[base..].join('/')
841
+ end
842
+
649
843
  def color(string, *colors)
650
844
  if @color
651
845
  pastel.decorate(string, *colors)
@@ -660,5 +854,44 @@ class SugarJar
660
854
  Pastel.new
661
855
  end
662
856
  end
857
+
858
+ def determine_cli(cli)
859
+ return cli if %w{gh hub}.include?(cli)
860
+
861
+ die("'github_cli' has unknown setting: #{cli}") unless cli == 'auto'
862
+
863
+ SugarJar::Log.debug('github_cli set to auto')
864
+
865
+ if which_nofail('gh')
866
+ SugarJar::Log.debug('Found "gh"')
867
+ return 'gh'
868
+ end
869
+ if which_nofail('hub')
870
+ SugarJar::Log.debug('Did not find "gh" but did find "hub"')
871
+ return 'hub'
872
+ end
873
+
874
+ die(
875
+ 'Neither "gh" nor "hub" found in PATH, please ensure at least one ' +
876
+ 'of these utilities is in the PATH. If both are available you can ' +
877
+ 'specify which to use with --github-cli',
878
+ )
879
+ end
880
+
881
+ def hub?
882
+ @cli == 'hub'
883
+ end
884
+
885
+ def gh?
886
+ @cli == 'gh'
887
+ end
888
+
889
+ def ghcli_nofail(*args)
890
+ gh? ? gh_nofail(*args) : hub_nofail(*args)
891
+ end
892
+
893
+ def ghcli(*args)
894
+ gh? ? gh(*args) : hub(*args)
895
+ end
663
896
  end
664
897
  end
@@ -6,14 +6,15 @@ class SugarJar
6
6
  # This is stuff like log level, github-user, etc.
7
7
  class Config
8
8
  DEFAULTS = {
9
- 'github_user' => ENV['USER'],
9
+ 'github_cli' => 'auto',
10
+ 'github_user' => ENV.fetch('USER'),
10
11
  'fallthru' => true,
11
12
  }.freeze
12
13
 
13
14
  def self._find_ordered_files
14
15
  [
15
16
  '/etc/sugarjar/config.yaml',
16
- "#{ENV['HOME']}/.config/sugarjar/config.yaml",
17
+ "#{Dir.home}/.config/sugarjar/config.yaml",
17
18
  ].select { |f| File.exist?(f) }
18
19
  end
19
20
 
data/lib/sugarjar/util.rb CHANGED
@@ -26,11 +26,25 @@ class SugarJar
26
26
  exit(1)
27
27
  end
28
28
 
29
- def hub_nofail(*args)
29
+ def git_nofail(*args)
30
30
  if %w{diff log grep branch}.include?(args[0]) &&
31
31
  args.none? { |x| x.include?('color') }
32
32
  args << (@color ? '--color' : '--no-color')
33
33
  end
34
+ SugarJar::Log.trace("Running: git #{args.join(' ')}")
35
+ Mixlib::ShellOut.new([which('git')] + args).run_command
36
+ end
37
+
38
+ def git(*args)
39
+ s = git_nofail(*args)
40
+ s.error!
41
+ s
42
+ end
43
+
44
+ def hub_nofail(*args)
45
+ # this allows us to use 'hub' stuff that's top-level, but is under
46
+ # repo for this.
47
+ args.delete_at(0) if args[0] == 'repo'
34
48
  SugarJar::Log.trace("Running: hub #{args.join(' ')}")
35
49
  s = Mixlib::ShellOut.new([which('hub')] + args).run_command
36
50
  if s.error?
@@ -84,13 +98,43 @@ class SugarJar
84
98
  s
85
99
  end
86
100
 
101
+ def gh_nofail(*args)
102
+ SugarJar::Log.trace("Running: gh #{args.join(' ')}")
103
+ s = Mixlib::ShellOut.new([which('gh')] + args).run_command
104
+ if s.error? && s.stderr.include?('gh auth')
105
+ SugarJar::Log.info(
106
+ 'gh was run but no github token exists. Will run "gh auth login" ' +
107
+ "to force\ngh to authenticate...",
108
+ )
109
+ unless system(which('gh'), 'auth', 'login', '-p', 'ssh')
110
+ SugarJar::Log.fatal(
111
+ 'That failed, I will bail out. Hub needs to get a github ' +
112
+ 'token. Try running "gh auth login" (will list info about ' +
113
+ 'your account) and try this again when that works.',
114
+ )
115
+ exit(1)
116
+ end
117
+ end
118
+ s
119
+ end
120
+
121
+ def gh(*args)
122
+ s = gh_nofail(*args)
123
+ s.error!
124
+ s
125
+ end
126
+
87
127
  def in_repo
88
- s = hub_nofail('rev-parse', '--is-inside-work-tree')
128
+ s = git_nofail('rev-parse', '--is-inside-work-tree')
89
129
  !s.error? && s.stdout.strip == 'true'
90
130
  end
91
131
 
92
132
  def repo_root
93
- hub('rev-parse', '--show-toplevel').stdout.strip
133
+ git('rev-parse', '--show-toplevel').stdout.strip
134
+ end
135
+
136
+ def repo_name
137
+ repo_root.split('/').last
94
138
  end
95
139
  end
96
140
  end
@@ -1,3 +1,3 @@
1
1
  class SugarJar
2
- VERSION = '0.0.10'.freeze
2
+ VERSION = '1.0.0'.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: 0.0.10
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Phil Dibowitz
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-23 00:00:00.000000000 Z
11
+ date: 2023-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mixlib-log
@@ -95,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  requirements: []
98
- rubygems_version: 3.3.15
98
+ rubygems_version: 3.3.7
99
99
  signing_key:
100
100
  specification_version: 4
101
101
  summary: A git/github helper script