sugarjar 0.0.10 → 1.0.0

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