sugarjar 0.0.9 → 0.0.11

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: 7b12b221b467eca9aeba30396d059870a82608120ce1739ded50b4aef55ce7ba
4
- data.tar.gz: c84bcab24de60fc61a0241f7c2940d6da6d78bc7416fa3fb92500528a447f9c8
3
+ metadata.gz: 7f4c70f80f34b048ba0d6b526e5eb20fcc51164b8ef78412a38ad5c953296898
4
+ data.tar.gz: 53958fab72a6c93d35617b63ffdb178cddcf1f97837a321533836896b0336c8b
5
5
  SHA512:
6
- metadata.gz: e041cd65eb939ea1d2153bf13a8a019e05f194fcf6ce4326c69d62a2de9e34e1d036cf759c5ea17b5b42d8e7e159fc99d65ee75470580a4ed15a854fffc7c87f
7
- data.tar.gz: 2c47c2920b6a774ffb83ef16761183ac7dbfcc808a6885335bbeeee5470a29ceff343df4e3748897ab4e8e681e7c7d8b9a5fc3c7ed4d44fafaa40dda6c08b300
6
+ metadata.gz: 5b3c546fe02babc8b186cf4f5de2d6a2716e371228af676dd5550d56b1f17db9d310d135f47a1905e77a98ed32ebd6716292bc19059a1e6e9e4733f725422900
7
+ data.tar.gz: 37df61e3b4161c2737dcf6392b5db3dcb8feef5d402868b48d6f28db9cb2254ddaf9b39d2719ea88071c4f37feb32efd9d327c105c3dcde428f495957e85913e
data/LICENSE CHANGED
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright [yyyy] [name of copyright owner]
189
+ Copyright 2020-present Phil Dibowitz
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
data/README.md CHANGED
@@ -3,10 +3,11 @@
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
+ Welcome to SugarJar - a git/github helper. It needs one of the GitHub CLI's:
8
+ the current default is [hub](https://hub.github.com/), but there is
9
+ experimental support for [gh](https://cli.github.com/). So you will need one of
10
+ those two installed.
10
11
 
11
12
  SugarJar is inspired by [arcanist](https://github.com/phacility/arcanist), and
12
13
  its replacement at Facebook, JellyFish. Many of the features they provide for
@@ -19,6 +20,24 @@ If you miss Mondrian or Phabricator - this is the tool for you!
19
20
 
20
21
  If you don't, there's a ton of useful stuff for everyone!
21
22
 
23
+ ## Installation
24
+
25
+ Sugarjar is packaged in a variety of Linux distributions - see if it's on the
26
+ list here, and if so, use your package manager (or `gem`) to install it:
27
+
28
+ [![Packaging status](https://repology.org/badge/vertical-allrepos/sugarjar.svg)](https://repology.org/project/sugarjar/versions)
29
+
30
+ Another option is to use our [omnibus
31
+ packages](https://github.com/jaymzh/sugarjar/releases). We keep Omnibus
32
+ packages for most distros that do not package Sugarjar. Omnibus packages are
33
+ distro packages (deb, rpm, etc.) that have all dependencies bundled up together
34
+ (including Ruby), and install in `/opt/sugarjar` allowing you to manage the
35
+ installation with your package manager, but with as a hermetically sealed app
36
+ that doesn't depend on the rest of your distro.
37
+
38
+ Finally, if none of those work for you, you can clone this repo and run it
39
+ directly from there.
40
+
22
41
  ## Auto cleanup squash-merged branches
23
42
 
24
43
  It is common for a PR to go back and forth with a variety of nits, lint fixes,
@@ -73,7 +92,7 @@ This will:
73
92
  * Clone your fork
74
93
  * Add the original as an 'upstream' remote
75
94
 
76
- Note that it takes `hub`s short-names for repos. No need to specify a full URL,
95
+ Note that it takes short names for repos. No need to specify a full URL,
77
96
  just a $org/$repo.
78
97
 
79
98
  Like `git clone`, `sj sclone` will accept an additional argument as the
@@ -198,6 +217,15 @@ smartlog` or `sj sl` for short.
198
217
 
199
218
  ![smartlog screenshot](https://github.com/jaymzh/sugarjar/blob/master/smartlog.png)
200
219
 
220
+ ## Pulling in suggestions from the web
221
+
222
+ When someone 'suggests' a change in the GitHub WebUI, once you choose to commit
223
+ them, your origin and local branches are no longer in-sync. The
224
+ `pullsuggestions` command will attempt to merge in any remote commits to your
225
+ local branch. This command will show a diff and ask for confirmation before
226
+ attempting the merge and - if allowed to continue - will use a fast-forward
227
+ merge.
228
+
201
229
  ## And more!
202
230
 
203
231
  See `sj help` for more commands!
@@ -205,8 +233,11 @@ See `sj help` for more commands!
205
233
  ## Using SugarJar as a git wrapper
206
234
 
207
235
  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.
236
+ (which passes commands **it** doesn't know to `git`). If you have configured
237
+ SugarJar to use `gh` instead of `hub`, then it will pass commands straight to
238
+ `git` since `gh` doesn't act as a `git` wrapper.
239
+
240
+ As such you can alias it to `git` and just have a super-git.
210
241
 
211
242
  ```shell
212
243
  $ alias git=sj
@@ -244,8 +275,8 @@ yaml file is a straight key-value pair of options without their '--'. For
244
275
  example:
245
276
 
246
277
  ```yaml
247
- debug: true
248
- github-user: jaymzh
278
+ log_level: debug
279
+ github_user: jaymzh
249
280
  ```
250
281
 
251
282
  In addition, the environment variable `SUGARJAR_DEBUG` can be defined to set
@@ -258,9 +289,15 @@ Sugarjar looks for a `.sugarjar.yaml` in the root of the repository to tell it
258
289
  how to handle repo-specific things. Currently there options are:
259
290
 
260
291
  * `lint` - A list of scripts to run on `sj lint`. These should be linters like
261
- rubocop or pyflake.
292
+ rubocop or pyflake. Linters will be run from the root of the repo.
293
+ * `lint_list_cmd` - A command to run which will print out linters to run, one
294
+ per line. Takes precedence over `lint`. The command (and the resulting
295
+ linters) will be run from the root of the repo.
262
296
  * `unit` - A list of scripts to run on `sj unit`. These should be unittest
263
- runners like rspec or pyunit.
297
+ runners like rspec or pyunit. Test will be run fro mthe root of the repo.
298
+ * `unit_list_cmd` - A command to run which will print out the unit tests to
299
+ run, one more line. Takes precedence over `unit`. The command (and the
300
+ resulting unit tests) will be run from the root of the repo.
264
301
  * `on_push` - A list of types (`lint`, `unit`) of checks to run before pushing.
265
302
  It is highly recommended this is only `lint`. The goal here is to allow for
266
303
  the user to get quick stylistic feedback before pushing their branch to avoid
@@ -312,6 +349,16 @@ sj clone jaymzh/sugarjar --github-host githuh.com
312
349
  We will add the `hub.host` to the `sugarjar` clone so that future `hub` or `sj`
313
350
  commands work without needing to specify..
314
351
 
352
+ ## Support for `gh`
353
+
354
+ As of version 0.11 there is experimental support for the `gh` CLI (instead of
355
+ `hub`). If you would like to use `gh`, install it and then add the following
356
+ to your configuration:
357
+
358
+ ```yaml
359
+ github_cli: gh
360
+ ```
361
+
315
362
  ## FAQ
316
363
 
317
364
  Why the name SugarJar?
@@ -324,6 +371,19 @@ and github, it seemed appropriate.
324
371
 
325
372
  Why did you use `hub` instead of the newer `gh` CLI?
326
373
 
327
- `gh` is still new and not yet as feature rich as `hub`. Also I wanted SugarJar
328
- to be able to be a git wrapper, and so wrapping `hub` allows us to do that but
329
- wrapping `gh` does not.
374
+ When I originally wrote SugarJar, `gh` was in early development, and `hub` had
375
+ many more features. Now that `gh` has matured, we have experimental support for
376
+ `gh` and will switch it to the default at some point.
377
+
378
+ In addition, I wanted SugarJar to be able to be a git wrapper, and so wrapping
379
+ `hub` allows us to do that but wrapping `gh` does not.
380
+
381
+ I'd like to package SugarJar for my favorite distro/OS, is that OK?
382
+
383
+ Of course! But I'd appreciate you emailing me to give me a heads up. Doing so
384
+ will allow me to make sure it shows up in the Repology badge above as well as
385
+ stop building Omnibus packages for whatever distro.
386
+
387
+ Does it work on Windows/Mac?
388
+
389
+ It should! Though I will admit I don't regularly test on non-Linux OSes.
data/bin/sj CHANGED
@@ -33,8 +33,13 @@ parser = OptionParser.new do |opts|
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(
37
+ '--github-cli CLI',
38
+ %w{gh cli},
39
+ 'Github CLI to use ("gh" or "hub"). Note that support for "gh" is ' +
40
+ 'currently experimental. default: "hub"',
41
+ ) do |cli|
42
+ options['github_cli'] = cli
38
43
  end
39
44
 
40
45
  opts.on(
@@ -48,6 +53,10 @@ parser = OptionParser.new do |opts|
48
53
  options['github_host'] = host
49
54
  end
50
55
 
56
+ opts.on('--github-user USER', 'Github username') do |user|
57
+ options['github_user'] = user
58
+ end
59
+
51
60
  opts.on('-h', '--help', 'Print this help message') do
52
61
  puts opts
53
62
  exit
@@ -128,6 +137,11 @@ COMMANDS:
128
137
  lint
129
138
  Run any linters configured in .sugarjar.yaml.
130
139
 
140
+ pullsuggestions, ps
141
+ Pull any suggestions *that have been committed* in the GitHub UI.
142
+ This will show the diff and prompt for confirmation before
143
+ merging. Note that a fast-forward merge will be used.
144
+
131
145
  smartclone, sclone
132
146
  A smart wrapper to "git clone" that handles forking and managing
133
147
  remotes for you.
@@ -182,19 +196,23 @@ extra_opts = []
182
196
  config = SugarJar::Config.config
183
197
 
184
198
  valid_commands = sj.public_methods - Object.public_methods
185
-
186
- is_valid_command = ARGV.any? { |arg| valid_commands.include?(arg.to_s.to_sym) }
199
+ possible_valid_command = ARGV.any? do |arg|
200
+ valid_commands.include?(arg.to_s.to_sym)
201
+ end
187
202
 
188
203
  # if we're configured to fall thru and the subcommand isn't one
189
204
  # we recognize, don't parse the options as they may be different
190
205
  # than git's. For example `git config -l` - we error because we
191
206
  # require an arguement to `-l`.
192
- if config['fallthru'] && !is_valid_command
207
+ if config['fallthru'] && !possible_valid_command
193
208
  SugarJar::Log.debug(
194
209
  'Skipping option parsing: fall-thru is set and we do not recognize ' +
195
210
  'any subcommands',
196
211
  )
197
212
  else
213
+ SugarJar::Log.debug(
214
+ 'We MIGHT have a valid command... parse-command line options',
215
+ )
198
216
  # We want to allow people to pass in extra args to be passed to
199
217
  # git commands, but OptionParser doesn't easily allow this. So we
200
218
  # loop over it, catching exceptions.
@@ -240,6 +258,7 @@ SugarJar::Log.level = options['log_level'].to_sym if options['log_level']
240
258
  sj = SugarJar::Commands.new(options)
241
259
 
242
260
  subcommand = argv_copy.reject { |x| x.start_with?('-') }.first
261
+ is_valid_command = valid_commands.include?(subcommand.to_sym)
243
262
  argv_copy.delete(subcommand)
244
263
  SugarJar::Log.debug("subcommand is #{subcommand}")
245
264
 
@@ -260,7 +279,13 @@ if is_valid_command
260
279
  sj.send(subcommand.to_sym, *extra_opts)
261
280
  elsif options['fallthru']
262
281
  SugarJar::Log.debug("Falling thru to: hub #{ARGV.join(' ')}")
263
- exec('hub', *ARGV)
282
+ if options['github_cli'] == 'hub'
283
+ exec('hub', *ARGV)
284
+ else
285
+ # If we're using 'gh', it doesn't have 'git fall thru' support, so
286
+ # we pass thru directly to 'git'
287
+ exec('git', *ARGV)
288
+ end
264
289
  else
265
290
  SugarJar::Log.error("No such subcommand: #{subcommand}")
266
291
  end
@@ -12,28 +12,34 @@ class SugarJar
12
12
  class Commands
13
13
  include SugarJar::Util
14
14
 
15
+ MAIN_BRANCHES = %w{master main}.freeze
16
+
15
17
  def initialize(options)
16
18
  SugarJar::Log.debug("Commands.initialize options: #{options}")
17
19
  @ghuser = options['github_user']
18
20
  @ghhost = options['github_host']
21
+ @cli = options['github_cli']
19
22
  @ignore_dirty = options['ignore_dirty']
20
23
  @ignore_prerun_failure = options['ignore_prerun_failure']
21
24
  @repo_config = SugarJar::RepoConfig.config
22
25
  @color = options['color']
26
+ @checks = {}
27
+ @main_branch = nil
28
+ @main_remote_branches = {}
23
29
  return if options['no_change']
24
30
 
25
- set_hub_host if @ghhost
31
+ set_hub_host
26
32
  set_commit_template if @repo_config['commit_template']
27
33
  end
28
34
 
29
35
  def feature(name, base = nil)
30
36
  assert_in_repo
31
37
  SugarJar::Log.debug("Feature: #{name}, #{base}")
32
- die("#{name} already exists!") if all_branches.include?(name)
33
- base ||= most_master
38
+ die("#{name} already exists!") if all_local_branches.include?(name)
39
+ base ||= most_main
34
40
  base_pieces = base.split('/')
35
- hub('fetch', base_pieces[0]) if base_pieces.length > 1
36
- hub('checkout', '-b', name, base)
41
+ git('fetch', base_pieces[0]) if base_pieces.length > 1
42
+ git('checkout', '-b', name, base)
37
43
  SugarJar::Log.info(
38
44
  "Created feature branch #{color(name, :green)} based on " +
39
45
  color(base, :green),
@@ -56,9 +62,9 @@ class SugarJar
56
62
  def bcleanall
57
63
  assert_in_repo
58
64
  curr = current_branch
59
- all_branches.each do |branch|
60
- if branch == 'master'
61
- SugarJar::Log.debug('Skipping master')
65
+ all_local_branches.each do |branch|
66
+ if MAIN_BRANCHES.include?(branch)
67
+ SugarJar::Log.debug("Skipping #{branch}")
62
68
  next
63
69
  end
64
70
 
@@ -73,28 +79,28 @@ class SugarJar
73
79
  end
74
80
  end
75
81
 
76
- # Return to the branch we were on, or master
77
- if all_branches.include?(curr)
78
- hub('checkout', curr)
82
+ # Return to the branch we were on, or main
83
+ if all_local_branches.include?(curr)
84
+ git('checkout', curr)
79
85
  else
80
- hub('checkout', 'master')
86
+ checkout_main_branch
81
87
  end
82
88
  end
83
89
 
84
90
  def co(*args)
85
91
  assert_in_repo
86
- s = hub('checkout', *args)
92
+ s = git('checkout', *args)
87
93
  SugarJar::Log.info(s.stderr + s.stdout.chomp)
88
94
  end
89
95
 
90
96
  def br
91
97
  assert_in_repo
92
- SugarJar::Log.info(hub('branch', '-v').stdout.chomp)
98
+ SugarJar::Log.info(git('branch', '-v').stdout.chomp)
93
99
  end
94
100
 
95
101
  def binfo
96
102
  assert_in_repo
97
- SugarJar::Log.info(hub(
103
+ SugarJar::Log.info(git(
98
104
  'log', '--graph', '--oneline', '--decorate', '--boundary',
99
105
  "#{tracked_branch}.."
100
106
  ).stdout.chomp)
@@ -103,9 +109,9 @@ class SugarJar
103
109
  # binfo for all branches
104
110
  def smartlog
105
111
  assert_in_repo
106
- SugarJar::Log.info(hub(
112
+ SugarJar::Log.info(git(
107
113
  'log', '--graph', '--oneline', '--decorate', '--boundary',
108
- '--branches', "#{most_master}.."
114
+ '--branches', "#{most_main}.."
109
115
  ).stdout.chomp)
110
116
  end
111
117
 
@@ -113,13 +119,22 @@ class SugarJar
113
119
 
114
120
  def up
115
121
  assert_in_repo
122
+ # get a copy of our current branch, if rebase fails, we won't
123
+ # be able to determine it without backing out
124
+ curr = current_branch
116
125
  result = gitup
117
- if result
118
- SugarJar::Log.info(
119
- "#{color(current_branch, :green)} rebased on #{result}",
126
+ if result['so'].error?
127
+ die(
128
+ "#{color(curr, :red)}: Failed to rebase on " +
129
+ "#{result['base']}. Leaving the repo as-is. You can get out of " +
130
+ 'this with a `git rebase --abort`. Output from failed rebase is: ' +
131
+ "\nSTDOUT:\n#{result['so'].stdout.lines.map { |x| "\t#{x}" }.join}" +
132
+ "\nSTDERR:\n#{result['so'].stderr.lines.map { |x| "\t#{x}" }.join}",
120
133
  )
121
134
  else
122
- die("#{color(current_branch, :red)}: Failed to rebase")
135
+ SugarJar::Log.info(
136
+ "#{color(current_branch, :green)} rebased on #{result['base']}",
137
+ )
123
138
  end
124
139
  end
125
140
 
@@ -131,28 +146,29 @@ class SugarJar
131
146
 
132
147
  def qamend(*args)
133
148
  assert_in_repo
134
- SugarJar::Log.info(hub('commit', '--amend', '--no-edit', *args).stdout)
149
+ SugarJar::Log.info(git('commit', '--amend', '--no-edit', *args).stdout)
135
150
  end
136
151
 
137
152
  alias amendq qamend
138
153
 
139
154
  def upall
140
155
  assert_in_repo
141
- all_branches.each do |branch|
142
- next if branch == 'master'
156
+ all_local_branches.each do |branch|
157
+ next if MAIN_BRANCHES.include?(branch)
143
158
 
144
- hub('checkout', branch)
159
+ git('checkout', branch)
145
160
  result = gitup
146
- if result
147
- SugarJar::Log.info(
148
- "#{color(branch, :green)} rebased on #{color(result, :green)}",
149
- )
150
- else
161
+ if result['so'].error?
151
162
  SugarJar::Log.error(
152
163
  "#{color(branch, :red)} failed rebase. Reverting attempt and " +
153
- 'moving to next branch',
164
+ 'moving to next branch. Try `sj up` manually on that branch.',
165
+ )
166
+ git('rebase', '--abort')
167
+ else
168
+ SugarJar::Log.info(
169
+ "#{color(branch, :green)} rebased on " +
170
+ color(result['base'], :green).to_s,
154
171
  )
155
- hub('rebase', '--abort')
156
172
  end
157
173
  end
158
174
  end
@@ -164,28 +180,57 @@ class SugarJar
164
180
 
165
181
  reponame = File.basename(repo, '.git')
166
182
  dir ||= reponame
183
+
167
184
  SugarJar::Log.info("Cloning #{reponame}...")
168
- hub('clone', canonicalize_repo(repo), dir, *args)
169
185
 
186
+ # GH's 'fork' command (with the --clone arg) will fork, if necessary,
187
+ # then clone, and then setup the remotes with the appropriate names. So
188
+ # we just let it do all the work for us and return.
189
+ if gh?
190
+ ghcli('repo', 'fork', '--clone', canonicalize_repo(repo), dir, *args)
191
+ SugarJar::Log.info('Remotes "origin" and "upstream" configured.')
192
+ return
193
+ end
194
+
195
+ # For 'hub', first we clone, using git, as 'hub' always needs a repo
196
+ # to operate on.
197
+ git('clone', canonicalize_repo(repo), dir, *args)
198
+
199
+ # Then we go into it and attempt to use the 'fork' capability
170
200
  Dir.chdir dir do
171
201
  # Now that we have a repo, if we have a hub host set it.
172
- set_hub_host if @ghhost
202
+ set_hub_host
173
203
 
174
204
  org = extract_org(repo)
175
205
  SugarJar::Log.debug("Comparing org #{org} to ghuser #{@ghuser}")
176
206
  if org == @ghuser
177
207
  puts 'Cloned forked or self-owned repo. Not creating "upstream".'
208
+ SugarJar::Log.info('Remotes "origin" and "upstream" configured.')
178
209
  return
179
210
  end
180
211
 
181
- s = hub_nofail('fork', '--remote-name=origin')
212
+ s = ghcli_nofail('repo', 'fork', '--remote-name=origin')
182
213
  if s.error?
183
- # if the fork command failed, we already have one, so we have
184
- # to swap the remote names ourselves
185
- # newer 'hub's don't fail and do the right thing...
186
- SugarJar::Log.info("Fork (#{@ghuser}/#{reponame}) detected.")
187
- hub('remote', 'rename', 'origin', 'upstream')
188
- hub('remote', 'add', 'origin', forked_repo(repo, @ghuser))
214
+ if s.stdout.include?('SAML enforcement')
215
+ SugarJar::Log.info(
216
+ 'Forking the repo failed because the repo requires SAML ' +
217
+ "authentication. Full output:\n\n\t#{s.stdout}",
218
+ )
219
+ exit(1)
220
+ else
221
+ # gh as well as old versions of hub, it would fail if the upstream
222
+ # fork already existed. If we got an error, but didn't recognize
223
+ # that, we'll assume that's what happened and try to add the remote
224
+ # ourselves.
225
+ SugarJar::Log.info("Fork (#{@ghuser}/#{reponame}) detected.")
226
+ SugarJar::Log.debug(
227
+ 'The above is a bit of a lie. "hub" failed to fork and it was ' +
228
+ 'not a SAML error, so our best guess is that a fork exists ' +
229
+ 'and so we will try to configure it.',
230
+ )
231
+ git('remote', 'rename', 'origin', 'upstream')
232
+ git('remote', 'add', 'origin', forked_repo(repo, @ghuser))
233
+ end
189
234
  else
190
235
  SugarJar::Log.info("Forked #{reponame} to #{@ghuser}")
191
236
  end
@@ -221,11 +266,15 @@ class SugarJar
221
266
 
222
267
  def version
223
268
  puts "sugarjar version #{SugarJar::VERSION}"
224
- puts hub('version').stdout
269
+ puts ghcli('version').stdout
270
+ # 'hub' prints the 'git' version, but gh doesn't, so if we're on 'gh'
271
+ # print out the git version directly
272
+ puts git('version').stdout if gh?
225
273
  end
226
274
 
227
- def smartpullrequest
275
+ def smartpullrequest(*args)
228
276
  assert_in_repo
277
+ assert_common_main_branch
229
278
  if dirty?
230
279
  SugarJar::Log.warn(
231
280
  'Your repo is dirty, so I am not going to create a pull request. ' +
@@ -233,12 +282,61 @@ class SugarJar
233
282
  )
234
283
  exit(1)
235
284
  end
236
- system(which('hub'), 'pull-request')
285
+ if gh?
286
+ SugarJar::Log.trace("Running: gh pr create #{args.join(' ')}")
287
+ system(which('gh'), 'pr', 'create', *args)
288
+ else
289
+ SugarJar::Log.trace("Running: hub pull-request #{args.join(' ')}")
290
+ system(which('hub'), 'pull-request', *args)
291
+ end
237
292
  end
238
293
 
239
294
  alias spr smartpullrequest
240
295
  alias smartpr smartpullrequest
241
296
 
297
+ def pullsuggestions
298
+ assert_in_repo
299
+
300
+ if dirty?
301
+ if @ignore_dirty
302
+ SugarJar::Log.warn(
303
+ 'Your repo is dirty, but --ignore-dirty was specified, so ' +
304
+ 'carrying on anyway.',
305
+ )
306
+ else
307
+ SugarJar::Log.error(
308
+ 'Your repo is dirty, so I am not going to push. Please commit ' +
309
+ 'or amend first.',
310
+ )
311
+ exit(1)
312
+ end
313
+ end
314
+
315
+ src = "origin/#{current_branch}"
316
+ fetch('origin')
317
+ diff = git('diff', src).stdout
318
+ return unless diff && !diff.empty?
319
+
320
+ puts "Will merge the following suggestions:\n\n#{diff}"
321
+
322
+ loop do
323
+ $stdout.print("\nAre you sure? [y/n] ")
324
+ ans = $stdin.gets.strip
325
+ case ans
326
+ when /^[Yy]$/
327
+ system(which('git'), 'merge', '--ff', "origin/#{current_branch}")
328
+ break
329
+ when /^[Nn]$/, /^[Qq](uit)?/
330
+ puts 'Not merging at user request...'
331
+ break
332
+ else
333
+ puts "Didn't understand '#{ans}'."
334
+ end
335
+ end
336
+ end
337
+
338
+ alias ps pullsuggestions
339
+
242
340
  private
243
341
 
244
342
  def _smartpush(remote, branch, force)
@@ -276,11 +374,11 @@ class SugarJar
276
374
 
277
375
  args = ['push', remote, branch]
278
376
  args << '--force-with-lease' if force
279
- puts hub(*args).stderr
377
+ puts git(*args).stderr
280
378
  end
281
379
 
282
380
  def dirty?
283
- s = hub_nofail('diff', '--quiet')
381
+ s = git_nofail('diff', '--quiet')
284
382
  s.error?
285
383
  end
286
384
 
@@ -317,9 +415,9 @@ class SugarJar
317
415
  end
318
416
 
319
417
  def set_hub_host
320
- return unless in_repo
418
+ return unless hub? && in_repo && @ghhost
321
419
 
322
- s = hub_nofail('config', '--local', '--get', 'hub.host')
420
+ s = git_nofail('config', '--local', '--get', 'hub.host')
323
421
  if s.error?
324
422
  SugarJar::Log.info("Setting repo hub.host = #{@ghhost}")
325
423
  else
@@ -337,7 +435,7 @@ class SugarJar
337
435
  end
338
436
  return
339
437
  end
340
- hub('config', '--local', '--add', 'hub.host', @ghhost)
438
+ git('config', '--local', '--add', 'hub.host', @ghhost)
341
439
  end
342
440
 
343
441
  def set_commit_template
@@ -358,7 +456,7 @@ class SugarJar
358
456
  )
359
457
  end
360
458
 
361
- s = hub_nofail('config', '--local', 'commit.template')
459
+ s = git_nofail('config', '--local', 'commit.template')
362
460
  unless s.error?
363
461
  current = s.stdout.strip
364
462
  if current == @repo_config['commit_template']
@@ -376,18 +474,60 @@ class SugarJar
376
474
  'Setting repo-specific commit template to ' +
377
475
  "#{@repo_config['commit_template']} per sugarjar repo config.",
378
476
  )
379
- hub(
477
+ git(
380
478
  'config', '--local', 'commit.template', @repo_config['commit_template']
381
479
  )
382
480
  end
383
481
 
384
- def run_check(type)
385
- unless @repo_config[type]
386
- SugarJar::Log.debug("No #{type} configured. Returning success")
387
- return true
482
+ def get_checks_from_command(type)
483
+ return nil unless @repo_config["#{type}_list_cmd"]
484
+
485
+ cmd = @repo_config["#{type}_list_cmd"]
486
+ short = cmd.split.first
487
+ unless File.exist?(short)
488
+ SugarJar::Log.error(
489
+ "Configured #{type}_list_cmd #{short} does not exist!",
490
+ )
491
+ return false
492
+ end
493
+ s = Mixlib::ShellOut.new(cmd).run_command
494
+ if s.error?
495
+ SugarJar::Log.error(
496
+ "#{type}_list_cmd (#{cmd}) failed: #{s.format_for_exception}",
497
+ )
498
+ return false
499
+ end
500
+ s.stdout.split("\n")
501
+ end
502
+
503
+ # determine if we're using the _list_cmd and if so run it to get the
504
+ # checks, or just use the directly-defined check, and cache it
505
+ def get_checks(type)
506
+ return @checks[type] if @checks[type]
507
+
508
+ ret = get_checks_from_command(type)
509
+ if ret
510
+ SugarJar::Log.debug("Found #{type}s: #{ret}")
511
+ @checks[type] = ret
512
+ # if it's explicitly false, we failed to run the command
513
+ elsif ret == false
514
+ @checks[type] = false
515
+ # otherwise, we move on (basically: it's nil, there was no _list_cmd)
516
+ else
517
+ SugarJar::Log.debug("[#{type}]: using listed linters: #{ret}")
518
+ @checks[type] = @repo_config[type] || []
388
519
  end
520
+ @checks[type]
521
+ end
522
+
523
+ def run_check(type)
389
524
  Dir.chdir repo_root do
390
- @repo_config[type].each do |check|
525
+ checks = get_checks(type)
526
+ # if we failed to determine the checks, the the checks have effectively
527
+ # failed
528
+ return false unless checks
529
+
530
+ checks.each do |check|
391
531
  SugarJar::Log.debug("Running #{type} #{check}")
392
532
 
393
533
  short = check.split.first
@@ -405,7 +545,7 @@ class SugarJar
405
545
  SugarJar::Log.warn(
406
546
  "The linter modified the repo. Here's the diff:\n",
407
547
  )
408
- puts hub('diff').stdout
548
+ puts git('diff').stdout
409
549
  loop do
410
550
  $stdout.print(
411
551
  "\nWould you like to\n\t[q]uit and inspect\n\t[a]mend the " +
@@ -457,29 +597,82 @@ class SugarJar
457
597
  exit(1)
458
598
  end
459
599
 
600
+ def assert_common_main_branch
601
+ upstream_branch = main_remote_branch(upstream)
602
+ unless main_branch == upstream_branch
603
+ die(
604
+ "The local main branch is '#{main_branch}', but the main branch " +
605
+ "of the #{upstream} remote is '#{upstream_branch}'. You probably " +
606
+ "want to rename your local branch by doing:\n\t" +
607
+ "git branch -m #{main_branch} #{upstream_branch}\n\t" +
608
+ "git fetch #{upstream}\n\t" +
609
+ "git branch -u #{upstream}/#{upstream_branch} #{upstream_branch}\n" +
610
+ "\tgit remote set-head #{upstream} -a",
611
+ )
612
+ end
613
+ return if upstream_branch == 'origin'
614
+
615
+ origin_branch = main_remote_branch('origin')
616
+ return if origin_branch == upstream_branch
617
+
618
+ die(
619
+ "The main branch of your upstream (#{upstream_branch}) and your " +
620
+ "fork/origin (#{origin_branch}) are not the same. You should go " +
621
+ "to https://#{@ghhost || 'github.com'}/#{@ghuser}/#{repo_name}/" +
622
+ 'branches/ and rename the \'default\' branch to ' +
623
+ "'#{upstream_branch}'. It will then give you some commands to " +
624
+ 'run to update this clone.',
625
+ )
626
+ end
627
+
460
628
  def assert_in_repo
461
629
  die('sugarjar must be run from inside a git repo') unless in_repo
462
630
  end
463
631
 
632
+ def determine_main_branch(branches)
633
+ branches.include?('main') ? 'main' : 'master'
634
+ end
635
+
636
+ def main_branch
637
+ @main_branch = determine_main_branch(all_local_branches)
638
+ end
639
+
640
+ def main_remote_branch(remote)
641
+ @main_remote_branches[remote] ||=
642
+ determine_main_branch(all_remote_branches(remote))
643
+ end
644
+
645
+ def checkout_main_branch
646
+ git('checkout', main_branch)
647
+ end
648
+
464
649
  def clean_branch(name)
465
- die('Cannot remove master branch') if name == 'master'
650
+ die("Cannot remove #{name} branch") if MAIN_BRANCHES.include?(name)
466
651
  SugarJar::Log.debug('Fetch relevant remote...')
467
652
  fetch_upstream
468
653
  return false unless safe_to_clean(name)
469
654
 
470
655
  SugarJar::Log.debug('branch deemed safe to delete...')
471
- hub('checkout', 'master')
472
- hub('branch', '-D', name)
656
+ checkout_main_branch
657
+ git('branch', '-D', name)
473
658
  gitup
474
659
  true
475
660
  end
476
661
 
477
- def all_branches
662
+ def all_remote_branches(remote = 'origin')
478
663
  branches = []
479
- hub('branch', '--format', '%(refname)').stdout.lines.each do |line|
480
- next if line == 'master'
664
+ git('branch', '-r', '--format', '%(refname)').stdout.lines.each do |line|
665
+ next unless line.start_with?("refs/remotes/#{remote}/")
666
+
667
+ branches << branch_from_ref(line.strip, :remote)
668
+ end
669
+ branches
670
+ end
481
671
 
482
- branches << line.strip.split('/')[2]
672
+ def all_local_branches
673
+ branches = []
674
+ git('branch', '--format', '%(refname)').stdout.lines.each do |line|
675
+ branches << branch_from_ref(line.strip)
483
676
  end
484
677
  branches
485
678
  end
@@ -488,7 +681,7 @@ class SugarJar
488
681
  # cherry -v will output 1 line per commit on the target branch
489
682
  # prefixed by a - or + - anything with a - can be dropped, anything
490
683
  # else cannot.
491
- out = hub(
684
+ out = git(
492
685
  'cherry', '-v', tracked_branch, branch
493
686
  ).stdout.lines.reject do |line|
494
687
  line.start_with?('-')
@@ -502,24 +695,24 @@ class SugarJar
502
695
 
503
696
  # if the "easy" check didn't work, it's probably because there
504
697
  # was a squash-merge. To check for that we make our own squash
505
- # merge to upstream/master and see if that has any delta
698
+ # merge to upstream/main and see if that has any delta
506
699
 
507
700
  # First we need a temp branch to work on
508
701
  tmpbranch = "_sugar_jar.#{Process.pid}"
509
702
 
510
- hub('checkout', '-b', tmpbranch, tracked_branch)
511
- s = hub_nofail('merge', '--squash', branch)
703
+ git('checkout', '-b', tmpbranch, tracked_branch)
704
+ s = git_nofail('merge', '--squash', branch)
512
705
  if s.error?
513
706
  cleanup_tmp_branch(tmpbranch, branch)
514
707
  SugarJar::Log.debug(
515
- 'Failed to merge changes into current master. This means we could ' +
708
+ 'Failed to merge changes into current main. This means we could ' +
516
709
  'not figure out if this is merged or not. Check manually and use ' +
517
710
  "'git branch -D #{branch}' if it is safe to do so.",
518
711
  )
519
712
  return false
520
713
  end
521
714
 
522
- s = hub('diff', '--staged')
715
+ s = git('diff', '--staged')
523
716
  out = s.stdout
524
717
  SugarJar::Log.debug("Squash-merged diff: #{out}")
525
718
  cleanup_tmp_branch(tmpbranch, branch)
@@ -530,34 +723,37 @@ class SugarJar
530
723
  true
531
724
  else
532
725
  SugarJar::Log.debug(
533
- 'After squash-merging, this branch is NOT fully merged to master',
726
+ 'After squash-merging, this branch is NOT fully merged to main',
534
727
  )
535
728
  false
536
729
  end
537
730
  end
538
731
 
539
732
  def cleanup_tmp_branch(tmp, backto)
540
- hub('reset', '--hard', tracked_branch)
541
- hub('checkout', backto)
542
- hub('branch', '-D', tmp)
733
+ git('reset', '--hard', tracked_branch)
734
+ git('checkout', backto)
735
+ git('branch', '-D', tmp)
543
736
  end
544
737
 
545
738
  def current_branch
546
- hub('symbolic-ref', 'HEAD').stdout.strip.split('/')[2]
739
+ branch_from_ref(git('symbolic-ref', 'HEAD').stdout.strip)
547
740
  end
548
741
 
549
742
  def fetch_upstream
550
743
  us = upstream
551
- hub('fetch', us) if us
744
+ fetch(us) if us
745
+ end
746
+
747
+ def fetch(remote)
748
+ git('fetch', remote)
552
749
  end
553
750
 
554
751
  def gitup
555
752
  SugarJar::Log.debug('Fetching upstream')
556
753
  fetch_upstream
557
754
  curr = current_branch
558
- SugarJar::Log.debug('Rebasing')
559
755
  base = tracked_branch
560
- if curr != 'master' && base == "origin/#{curr}"
756
+ if !MAIN_BRANCHES.include?(curr) && base == "origin/#{curr}"
561
757
  SugarJar::Log.warn(
562
758
  "This branch is tracking origin/#{curr}, which is probably your " +
563
759
  'downstream (where you push _to_) as opposed to your upstream ' +
@@ -566,34 +762,38 @@ class SugarJar
566
762
  'to do a "git branch -u upstream".',
567
763
  )
568
764
  end
569
- s = hub_nofail('rebase', base)
570
- s.error? ? nil : base
765
+ SugarJar::Log.debug('Rebasing')
766
+ s = git_nofail('rebase', base)
767
+ {
768
+ 'so' => s,
769
+ 'base' => base,
770
+ }
571
771
  end
572
772
 
573
773
  def tracked_branch
574
- s = hub_nofail(
774
+ s = git_nofail(
575
775
  'rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'
576
776
  )
577
777
  if s.error?
578
- most_master
778
+ most_main
579
779
  else
580
780
  s.stdout.strip
581
781
  end
582
782
  end
583
783
 
584
- def most_master
784
+ def most_main
585
785
  us = upstream
586
786
  if us
587
- "#{us}/master"
787
+ "#{us}/#{main_branch}"
588
788
  else
589
- master
789
+ main_branch
590
790
  end
591
791
  end
592
792
 
593
793
  def upstream
594
794
  return @remote if @remote
595
795
 
596
- s = hub('remote')
796
+ s = git('remote')
597
797
 
598
798
  remotes = s.stdout.lines.map(&:strip)
599
799
  SugarJar::Log.debug("remotes is #{remotes}")
@@ -611,6 +811,13 @@ class SugarJar
611
811
  @remote
612
812
  end
613
813
 
814
+ def branch_from_ref(ref, type = :local)
815
+ # local branches are refs/head/XXXX
816
+ # remote branches are refs/remotes/<remote>/XXXX
817
+ base = type == :local ? 2 : 3
818
+ ref.split('/')[base..].join('/')
819
+ end
820
+
614
821
  def color(string, *colors)
615
822
  if @color
616
823
  pastel.decorate(string, *colors)
@@ -625,5 +832,21 @@ class SugarJar
625
832
  Pastel.new
626
833
  end
627
834
  end
835
+
836
+ def hub?
837
+ @cli == 'hub'
838
+ end
839
+
840
+ def gh?
841
+ @cli == 'gh'
842
+ end
843
+
844
+ def ghcli_nofail(*args)
845
+ gh? ? gh_nofail(*args) : hub_nofail(*args)
846
+ end
847
+
848
+ def ghcli(*args)
849
+ gh? ? gh(*args) : hub(*args)
850
+ end
628
851
  end
629
852
  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' => 'hub',
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.9'.freeze
2
+ VERSION = '0.0.11'.freeze
3
3
  end
data/sugarjar.gemspec CHANGED
@@ -20,4 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.add_dependency 'mixlib-log'
21
21
  spec.add_dependency 'mixlib-shellout'
22
22
  spec.add_dependency 'pastel'
23
+ spec.metadata = {
24
+ 'rubygems_mfa_required' => 'true',
25
+ }
23
26
  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.9
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Phil Dibowitz
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-21 00:00:00.000000000 Z
11
+ date: 2022-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mixlib-log
@@ -52,7 +52,7 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- description:
55
+ description:
56
56
  email:
57
57
  - phil@ipom.com
58
58
  executables:
@@ -78,8 +78,9 @@ files:
78
78
  homepage: https://github.com/jaymzh/sugarjar
79
79
  licenses:
80
80
  - Apache-2.0
81
- metadata: {}
82
- post_install_message:
81
+ metadata:
82
+ rubygems_mfa_required: 'true'
83
+ post_install_message:
83
84
  rdoc_options: []
84
85
  require_paths:
85
86
  - lib
@@ -94,8 +95,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
95
  - !ruby/object:Gem::Version
95
96
  version: '0'
96
97
  requirements: []
97
- rubygems_version: 3.2.5
98
- signing_key:
98
+ rubygems_version: 3.3.7
99
+ signing_key:
99
100
  specification_version: 4
100
101
  summary: A git/github helper script
101
102
  test_files: []