1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
|
#!/usr/bin/env ruby
# SugarJar
require 'optparse'
require 'mixlib/shellout'
require_relative '../lib/sugarjar/commands'
require_relative '../lib/sugarjar/config'
require_relative '../lib/sugarjar/log'
require_relative '../lib/sugarjar/util'
require_relative '../lib/sugarjar/version'
SugarJar::Log.level = Logger::INFO
# Don't put defaults here, put them in SugarJar::Config - otherwise
# these defaults overwrite whatever is in config files.
options = {}
# If ENV['SUGARJAR_DEBUG'] is set, it overrides the config file,
# but not the command line options, so set that one here. Also
# start the logger at that level, in case we are debugging option loading
# itself
if ENV['SUGARJAR_LOGLEVEL']
options['log_level'] = SugarJar::Log.level = ENV['SUGARJAR_LOGLEVEL'].to_sym
end
parser = OptionParser.new do |opts|
opts.banner = 'Usage: sj <command> [<args>] [<options>]'
opts.separator ''
opts.separator 'Command, args, and options, can appear in any order.'
opts.separator ''
opts.separator 'OPTIONS:'
opts.on('--feature-prefix', 'Prefix to use for feature branches') do |prefix|
options['feature_prefix'] = prefix
end
opts.on(
'--github-host HOST',
'The host for "hub". Note that we will set this in the local repo ' +
'config so there is no need to have multiple config files for multiple ' +
'github servers. Put your default one in your config file, and simply ' +
'specify this option the first time you clone or touch a repo and it ' +
'will be part of that repo until changed.',
) do |host|
options['github_host'] = host
end
opts.on('--github-user USER', 'Github username') do |user|
options['github_user'] = user
end
opts.on('-h', '--help', 'Print this help message') do
puts opts
exit
end
opts.on(
'--ignore-dirty',
'Tell command that check for a dirty repo to carry on anyway. ' +
'[default: false]',
) do
options['ignore_dirty'] = true
end
opts.on(
'--ignore-prerun-failure',
'Ignore preprun failure on *push commands. [default: false]',
) do
options['ignore_prerun_failure'] = true
end
opts.on(
'--log-level LEVEL',
'Set logging level (fatal, error, warning, info, debug, trace). This can ' +
'also be set via the SUGARJAR_LOGLEVEL environment variable. [default: ' +
'info]',
) do |level|
options['log_level'] = level
end
opts.on(
'--[no-]pr-autofill',
'When creating a PR, auto fill the title & description from the top ' +
'commit if we are using "gh". [default: true]',
) do |autofill|
options['pr_autofill'] = autofill
end
opts.on(
'--[no-]pr-autostack',
'When creating a PR, if this is a subfeature, should we make it a ' +
'PR on the PR for the parent feature. If not specified, we prompt ' +
'when this happens, when true always do this, when false never do ' +
'this. Only applicable when usiing "gh" and on branch-based PRs.',
) do |autostack|
options['pr_autostack'] = autostack
end
opts.on('--[no-]color', 'Enable color. [default: true]') do |color|
options['color'] = color
end
opts.on('--version') do
puts SugarJar::VERSION
exit
end
# rubocop:disable Layout/HeredocIndentation
opts.separator <<COMMANDTEXT
COMMANDS:
amend
Amend the current commit. Alias for "git commit --amend".
Accepts other arguments such as "-a" or files.
amendq, qamend
Same as "amend" but without changing the message. Alias for
"git commit --amend --no-edit".
binfo
Verbose information about the current branch.
br
Verbose branch list. An alias for "git branch -v".
debuginfo
Prints out a bunch of version and config information useful for
including in bug reports.
feature, f <branch_name>
Create a "feature" branch. It's morally equivalent to
"git checkout -b" except it defaults to creating it based on
some form of 'master' instead of your current branch. In order
of preference it will be upstream/master, origin/master, master,
depending upon what remotes are available.
Note that you can specify "--feature-prefix" (or add
"feature_prefix" to your config) to have all features created
with a prefix. This is useful for branch-based workflows where
developers are expected to create branches names that, for
example, start with their username.
forcepush, fpush
The same as "smartpush", but uses "--force-with-lease". This is
a "safer" way of doing force-pushes and is the recommended way
to push after rebasing or amending. Never do this to shared
branches. Very convenient for keeping the branch behind a pull-
request clean.
globalbranchclean, gbclean [<branch>] [<remote>]
WARNING: EXPERIMENTAL COMMAND.
Combination of "lbclean" and "rbclean". Cleans up
both local and remote branches safely. See those commands for
details.
globalbranchcleanall, gbcleanall [<remote>]
WARNING: EXPERIMENTAL COMMAND.
Safely clean all branches, both local and remote. See "gbclean"
for details.
lint
Run any linters configured in .sugarjar.yaml.
localbranchclean, lbclean [<branch>]
If safe, delete the current branch (or the specified branch).
Unlike "git branch -d", lbclean can handle squash-merged branches.
Think of it as a smarter "git branch -d".
Aliased to 'bclean' for backwards compatibility.
localbranchcleanall, lbcleanall
Walk all branches, and try to delete them if it's safe. See
"lbclean" for details.
Aliased to 'bcleanall' for backwards compatibility.
pullsuggestions, ps
Pull any suggestions *that have been committed* in the GitHub UI.
This will show the diff and prompt for confirmation before
merging. Note that a fast-forward merge will be used.
remotebranchclean, rbclean [<branch>] [<remote>]
WARNING: EXPERIMENTAL COMMAND.
Similar to lbclean, except safely cleans up remote branches.
Unlike many git commands, <remote> comes after <branch> so
that you can specify a branch and the remote defaults to 'origin'.
This means you can do "sj rclean" to clean the remote branch with
the same name as the local one. Note that you probably want
"sclean", which will do both local and remote cleaning in one
command.
WARNING: This command cannot differentiate release branches
that are fully merged but still need to be kept around for future
work. So if main contains everything that 2.0-devel and 3.0-devel
has, then those branches will be deleted. Use with caution.
remotebranchcleanall, rbcleanall [<remote>]
WARNING: EXPERIMENTAL COMMAND.
Walk all remote branches, and try to delete them if it's safe. See
"rbclean" for details.
smartclone, sclone
A smart wrapper to "git clone" that handles forking and managing
remotes for you.
It will clone a git repository using hub-style short name
("$org/$repo"). If the org of the repository is not the same
as your github-user then it will fork the repo for you to
your account (if not already done) and then setup your remotes
so that "origin" is your fork and "upstream" is the upstream.
smartlog, sl
Inspired by Facebook's "sl" extension to Mercurial, this command
will show you a tree of all your local branches relative to your
upstream.
smartpullrequest, smartpr, spr
A smart wrapper to "hub pull-request" that checks if your repo
is dirty before creating the pull request.
smartpush, spush
A smart wrapper to "git push" that runs whatever is defined in
"on_push" in .sugarjar.yml, and only pushes if they succeed.
subfeature, sf <feature>
An alias for 'sj feature <feature> <current_branch>'
sync
Similar to `up`, except instead of rebasing on a tracked branch
(usually `upstream` remote), rebases to wherever our remote push
target is (usually `origin` remote). Useful for syncing work
across different machines.
For example, if you do some work on feature `foo` on machine1 and
push to `origin/foo` (intending to eventually merge to
`upstream/main`), then on machine2, you pull that branch, do more
work, which you also push to `origin/foo`, then on machine1, you
can do `sj sync` to pull down the changes from `origin/foo`. If
you have local changes, that are not already on `origin/foo`,
those will be rebased on top of the changes from `origin/foo`.
unit
Run any unitests configured in .sugarjar.yaml.
up [<branch>]
Rebase the current branch (or specified branch) intelligently.
In most causes this will check for a main (or master) branch on
upstream, then origin. If a branch explicitly tracks something
else, then that will be used, instead.
upall
Same as "up", but for all branches.
COMMANDTEXT
# rubocop:enable Layout/HeredocIndentation
end
extra_opts = []
argv_copy = ARGV.dup
# We want to allow people to pass in extra args to be passed to commands (like
# `amend`), but OptionParser doesn't easily allow this. So we loop over it,
# catching exceptions.
begin
# HOWEVER, anytime it throws an exception, for some reason, it clears
# out all of ARGV, or whatever you passed to as ARGV.
#
# This not only prevents further parsing, but also means we lose
# any non-option arguements (like the subcommand!)
#
# So we save a copy, and if we throw an exception, save the option that
# caused it, remove that option from our copy, and then re-populate argv
# with what's left.
#
# By doing this we not only get to parse all the options properly and
# save unknown ones, but non-option arguements, which OptionParser
# normally leaves in ARGV stay in ARGV.
saved_argv = argv_copy.dup
parser.parse!(argv_copy)
rescue OptionParser::InvalidOption => e
SugarJar::Log.debug("Saving unknown argument #{e.args}")
extra_opts += e.args
# e.args is an array, but it's only ever one arguement per exception
saved_argv.delete(e.args.first)
argv_copy = saved_argv.dup
SugarJar::Log.debug(
"Continuing option parsing with remaining ARGV: #{argv_copy}",
)
retry
end
options = SugarJar::Config.config.merge(options)
SugarJar::Log.level = options['log_level'].to_sym if options['log_level']
subcommand = argv_copy.reject { |x| x.start_with?('-') }.first
if ARGV.empty? || !subcommand
puts parser
exit
end
SugarJar::Log.debug("Final config: #{options}")
sj = SugarJar::Commands.new(options)
valid_commands = sj.public_methods - Object.public_methods
is_valid_command = valid_commands.include?(subcommand.to_sym)
# We can't do .delete(subcommand) because someone could, for example
# have a branch called 'co' and then do 'sj co co' - which will then
# remove _all_ instances of 'co'. So find the first instance and remove
# that.
argv_copy.delete_at(argv_copy.find_index(subcommand))
SugarJar::Log.debug("subcommand is #{subcommand}")
# Extra options we got, plus any left over arguements are what we
# pass to Commands so they can be passed to git as necessary
extra_opts += argv_copy
SugarJar::Log.debug("extra unknown options: #{extra_opts}")
case subcommand
when 'help'
puts parser
exit
when 'debuginfo'
extra_opts = [options]
end
unless is_valid_command
SugarJar::Log.fatal("No such subcommand: #{subcommand}")
exit 1
end
SugarJar::Log.debug(
"running #{subcommand}; extra opts: #{extra_opts.join(', ')}",
)
sj.send(subcommand.to_sym, *extra_opts)
|