squared 0.4.7 → 0.4.9

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.
@@ -14,20 +14,20 @@ module Squared
14
14
  check = ->(proj) { proj.is_a?(Project::Git) && !proj.exclude?(Project::Git.ref) && git_clone?(proj.path) }
15
15
  if uri.is_a?(Array)
16
16
  base = name
17
- uri.each { |val| repo << proj if (proj = @project[val.to_s]) && check.(proj) }
17
+ uri.each { |val| repo << proj if (proj = @project[val.to_s]) && check.call(proj) }
18
18
  elsif uri
19
19
  data[name.to_s] = uri
20
20
  elsif name.is_a?(Enumerable)
21
21
  data = name.to_h
22
22
  elsif name.is_a?(String) && name.match?(GIT_PROTO)
23
23
  base = name
24
- @project.each_value { |proj| repo << proj if !proj.parent && check.(proj) }
24
+ @project.each_value { |proj| repo << proj if !proj.parent && check.call(proj) }
25
25
  else
26
26
  warn log_message(Logger::WARN, name, subject: 'git', hint: 'invalid', pass: true) if warning
27
27
  return self
28
28
  end
29
29
  if base
30
- base = base.match?(GIT_PROTO) ? "#{base.chomp('/')}/" : @root.join(base)
30
+ base = base.match?(GIT_PROTO) ? "#{base.chomp('/')}/" : @root + base
31
31
  repo.each do |target|
32
32
  if target.is_a?(Project::Git)
33
33
  data[target.localname] = target.project
@@ -46,14 +46,14 @@ module Squared
46
46
  end
47
47
  unless uri.match?(GIT_PROTO) || Pathname.new(uri).absolute?
48
48
  if uri.start_with?('.')
49
- uri = @root.join(uri)
49
+ uri = @root + uri
50
50
  elsif base
51
- uri = base.is_a?(Pathname) ? base.join(uri) : base + uri
51
+ uri = base + uri
52
52
  else
53
53
  next
54
54
  end
55
55
  end
56
- key = task_name(key)
56
+ key = task_name key
57
57
  (GIT_REPO[main] ||= {})[key] = [uri.to_s, opts]
58
58
  (@kind[key] ||= []) << Project::Git
59
59
  end
@@ -145,115 +145,117 @@ module Squared
145
145
  module Project
146
146
  class Git < Base
147
147
  OPT_GIT = {
148
- common: %w[bare paginate no-pager glob-pathspecs icase-pathspecs literal-pathspecs no-optional-locks
149
- no-replace-objects noglob-pathspecs c=q config-env=q exec-path=p namespace=p].freeze,
150
- branch: %w[a|all create-reflog i|ignore-case q|quiet r|remotes v|verbose abbrev=i color=b column=b
151
- contains=b format=q merged=b no-contains=b no-merged=b points-at=e u|set-upstream-to=e sort=q
148
+ common: %w[c=q bare glob-pathspecs icase-pathspecs literal-pathspecs no-optional-locks no-pager
149
+ no-replace-objects noglob-pathspecs paginate config-env=q exec-path=p namespace=p].freeze,
150
+ add: %w[A|all e|edit f|force ignore-errors ignore-missing ignore-removal i|interactive no-all
151
+ no-ignore-removal n|dry-run p|patch pathspec-file-nul renormalize sparse u|update v|verbose
152
+ chmod=b pathspec-from-file=p].freeze,
153
+ branch: %w[a|all create-reflog i|ignore-case q|quiet r|remotes v|verbose vv abbrev=i color=b column=b
154
+ contains=b format=q merged=b no-contains=b no-merged=b points-at=b u|set-upstream-to=b sort=q
152
155
  t|track=b].freeze,
153
156
  checkout: %w[l d|detach f|force ignore-other-worktrees ignore-skip-worktree-bits m|merge p|patch
154
- pathspec-file-nul q|quiet orphan=e ours theirs conflict=b pathspec-from-file=p
155
- t|track=b].freeze,
156
- clean: %w[d x X f|force i|interactive n|dry-run q|quiet e|exlcude=q].freeze,
157
+ pathspec-file-nul q|quiet ours theirs conflict=b orphan=b pathspec-from-file=p t|track=b].freeze,
157
158
  diff: {
158
159
  base: %w[0 1|base 2|ours 3|theirs].freeze,
159
160
  show: %w[s exit-code histogram].freeze
160
161
  }.freeze,
161
162
  fetch: {
162
- base: %w[multiple progress P|prune-tags refetch stdin u|update-head-ok
163
- recurse-submodules-default=b].freeze,
164
- pull: %w[4 6 n t a|append atomic dry-run f|force k|keep n|negotiate-only prefetch p|prune q|quiet
163
+ base: %w[multiple progress P|prune-tags refetch stdin u|update-head-ok recurse-submodules-default=b].freeze,
164
+ pull: %w[4 6 n t a|append atomic dry-run f|force k|keep negotiate-only prefetch p|prune q|quiet
165
165
  set-upstream unshallow update-shallow v|verbose deepen=i depth=i j|jobs=i negotiation-tip=q
166
- recurse-submodules=v refmap=q o|server-option=q shallow-exclude=e shallow-since=v
166
+ recurse-submodules=v refmap=q o|server-option=q shallow-exclude=b shallow-since=v
167
167
  upload-pack=q].freeze
168
168
  }.freeze,
169
+ git: {
170
+ add: %w[N|intent-to-add refresh].freeze,
171
+ clean: %w[d x X f|force n|dry-run i|interactive q|quiet e|exclude=q].freeze,
172
+ mv: %w[k f|force n|dry-run v|verbose].freeze,
173
+ revert: %w[e S=bm? abort continue n|no-commit quit reference skip cleanup=b gpg-sign=b? m|mainline=i
174
+ s|signoff strategy=b X|strategy-option=b].freeze,
175
+ rm: %w[r cached f|force n|dry-run ignore-unmatch pathspec-file-nul q|quiet sparse v|verbose
176
+ pathspec-from-file=p].freeze
177
+ }.freeze,
169
178
  log: {
170
- base: %w[all all-match alternate-refs author-date-order basic-regexp bisect boundary cherry cherry-mark
179
+ base: %w[L=qm all all-match alternate-refs author-date-order basic-regexp bisect boundary cherry cherry-mark
171
180
  cherry-pick clear-decorations date-order dense do-walk exclude-first-parent-only E|extended-regexp
172
181
  first-parent F|fixed-strings follow full-diff full-history ignore-missing invert-grep left-only
173
- merge log-size no-max-parents no-min-parents not P|perl-regexp reflog i|regexp-ignore-case
182
+ log-size merge no-max-parents no-min-parents not P|perl-regexp reflog i|regexp-ignore-case
174
183
  remove-empty reverse right-only simplify-by-decoration simplify-merges single-worktree show-pulls
175
184
  source sparse stdin topo-order g|walk-reflogs after=q ancestry-path=b? author=q before=q
176
185
  branches=q? committer=q decorate=b decorate-refs=q decorate-refs-exclude=q exclude=q
177
- exclude-hidden=b? glob=q grep=q grep-reflog=q L=q n|max-count=i max-parents=i min-parents=i
178
- no-walk=b? remotes=q? since=q since-as-filter=q skip=i tags=q? until=q].freeze,
186
+ exclude-hidden=b glob=q grep=q grep-reflog=q n|max-count=i max-parents=i min-parents=i no-walk=b?
187
+ remotes=q? since=q since-as-filter=q skip=i tags=q? until=q].freeze,
179
188
  format: %w[t children combined-all-paths oneline left-right no-diff-merges parents relative-date
180
- show-signature date=q diff-merges=b encoding=b expand-tabs=i format=q notes=q pretty=q?
189
+ show-signature date=q diff-merges=b encoding=b expand-tabs=i format=q notes=b pretty=q?
181
190
  show-linear-break=q?].freeze,
182
- diff: %w[p R u z l=i G=q O=q S=q binary check compact-summary cumulative find-copies-harder full-index
183
- W|function-context w|ignore-all-space ignore-blank-lines ignore-cr-at-eol ignore-space-at-eol
184
- b|ignore-space-change D|irreversible-delete graph ita-invisible-in-index minimal name-only
185
- name-status no-color-moved-ws no-prefix no-renames numstat patch-with-raw patch-with-stat patience
186
- pickaxe-all pickaxe-regex raw shortstat summary a|text abbrev=i? anchored=q B|break-rewrites=b?
187
- color=b color-moved=b color-moved-ws=b color-words=q? diff-algorithm=b diff-filter=e? X|dirstat=q?
188
- dirstat-by-file=q? dst-prefix=q C|find-copies=i? find-object=b M|find-renames=i?
189
- I|ignore-matching-lines=q ignore-submodules=b inter-hunk-context=i line-prefix=q output=p
190
- output-indicator-context=q output-indicator-new=q output-indicator-old=q relative=p rotate-to=p
191
- skip-to=p src-prefix=q stat=q? stat-width=i stat-name-width=i stat-count=i submodule=b? U|unified=i
192
- word-diff=b? word-diff-regex=q ws-error-highligt=q].freeze
191
+ diff: %w[p R u z B=bm? C=bm? l=im G=qm I=qm M=bm? O=qm S=qm U=im binary check compact-summary cumulative
192
+ find-copies-harder full-index W|function-context w|ignore-all-space ignore-blank-lines
193
+ ignore-cr-at-eol ignore-space-at-eol b|ignore-space-change D|irreversible-delete graph
194
+ ita-invisible-in-index minimal name-only name-status no-color-moved-ws no-prefix no-renames numstat
195
+ patch-with-raw patch-with-stat patience pickaxe-all pickaxe-regex raw shortstat summary a|text
196
+ abbrev=i? anchored=q break-rewrites=b? color=b color-moved=b color-moved-ws=b color-words=q?
197
+ diff-algorithm=b diff-filter=e? X|dirstat=b? dirstat-by-file=b? dst-prefix=q find-copies=i?
198
+ find-object=b find-renames=b? ignore-matching-lines=q ignore-submodules=b? inter-hunk-context=i
199
+ line-prefix=q output=p output-indicator-context=q output-indicator-new=q output-indicator-old=q
200
+ relative=p rotate-to=p skip-to=p src-prefix=q stat=b? stat-count=i stat-width=i stat-name-width=i
201
+ submodule=b? unified=i word-diff=b? word-diff-regex=q ws-error-highlight=b].freeze
193
202
  }.freeze,
194
- ls_files: %w[z debug deduplicate directory eol error-unmatch exclude-standard full-name k|killed
195
- no-empty-directory recurse-submodules sparse s|stage u|unmerged abbrev=i x|exclude=q
203
+ ls_files: %w[f t v z debug deduplicate directory eol error-unmatch exclude-standard full-name i|ignored
204
+ k|killed no-empty-directory recurse-submodules sparse s|stage u|unmerged abbrev=i x|exclude=q
196
205
  X|exclude-from=p exclude-per-directory=p format=q with-tree=q].freeze,
197
- ls_remote: %w[exit-code get-url q|quiet o|server-option=q symref sort=q upload-pack=q].freeze,
198
- mv: %w[k f|force n|dry-run v|verbose].freeze,
199
- pull: %w[e n allow-unrelated-histories ff-only S|gpg-sign=qq log=i r|rebase=v? s|strategy=b
206
+ ls_remote: %w[exit-code get-url q|quiet symref o|server-option=q sort=q upload-pack=q].freeze,
207
+ merge: %w[e n S=bm? allow-unrelated-histories ff-only m=q q|quiet v|verbose cleanup=b F|file=p gpg-sign=b?
208
+ into-name=b log=i s|strategy=b X|strategy-option=b].freeze,
209
+ pull: %w[e n S=bm? allow-unrelated-histories ff-only cleanup=b gpg-sign=b? log=i r|rebase=v? s|strategy=b
200
210
  X|strategy-option=b].freeze,
201
- merge: %w[e n allow-unrelated-histories ff-only m=q q|quiet v|verbose cleanup=b F|file=p S|gpg-sign=qq
202
- into-name=e log=i s|strategy=b X|strategy-option=b].freeze,
203
- rebase: %w[n C=i allow-empty-message apply committer-date-is-author-date edit-todo f|force-rebase ignore-date
204
- ignore-whitespace i|interactive keep-base m merge no-ff q|quiet quit r|rebase-merges=b?
205
- reset-author-date root show-current-patch signoff v|verbose empty=b S|gpg-sign=qq onto=e
206
- s|strategy=b X|strategy-option=b whitespace=b].freeze,
211
+ rebase: %w[n C=im S=bm? allow-empty-message apply committer-date-is-author-date edit-todo empty=b
212
+ f|force-rebase ignore-date ignore-whitespace i|interactive keep-base m|merge no-ff q|quiet quit
213
+ reset-author-date root show-current-patch signoff v|verbose empty=b x|exec=q gpg-sign=b? onto=b
214
+ r|rebase-merges=b s|strategy=b X|strategy-option=b whitespace=b].freeze,
207
215
  reset: %w[N pathspec-file-nul q|quiet pathspec-from-file=p].freeze,
208
- restore: %w[ignore-unmerged ignore-skip-worktree-bits m|merge ours p|patch pathspec-file-nul S|staged theirs
209
- W|worktree conflict=b pathspec-from-file=p s|source=q].freeze,
210
- revert: %w[e abort continue no-commit quit reference skip cleanup=b S|gpg-sign=qq m|mainline=i s|signoff
211
- strategy=b X|strategy-option=b].freeze,
212
- rev_parse: {
213
- output: %w[absolute-git-dir all flags git-common-dir git-dir is-bare-repository is-inside-git-dir
214
- is-inside-work-tree is-shallow-repository local-env-vars no-flags no-revs not q|quiet sq
215
- revs-only shared-index-path show-cdup show-prefix show-toplevel show-superproject-working-tree
216
- sq-quote symbolic symbolic-full-name verify abbrev-ref=b? after=q before=q default=q
217
- disambiguate=b exclude=q exclude-hidden=b glob=q git-path=p path-format=b? prefix=q branches=q?
218
- remotes=q? resolve-git-dir=p short=i? show-object-format=b? since=q tags=q? until=q].freeze,
219
- parseopt: %w[keep-dashdash stop-at-non-option stuck-long].freeze
220
- }.freeze,
221
- rm: %w[r cached f|force n|dry-run ignore-unmatch pathspec-file-nul q|quiet sparse v|verbose
222
- pathspec-from-file=p].freeze,
216
+ restore: %w[ignore-skip-worktree-bits ignore-unmerged m|merge ours p|patch pathspec-file-nul q|quiet S|staged
217
+ theirs W|worktree conflict=b pathspec-from-file=p s|source=b].freeze,
218
+ rev_parse: %w[absolute-git-dir all git-common-dir git-dir is-bare-repository is-inside-git-dir
219
+ is-inside-work-tree is-shallow-repository local-env-vars no-revs not q|quiet revs-only
220
+ shared-index-path show-cdup show-prefix show-superproject-working-tree show-toplevel sq sq-quote
221
+ symbolic symbolic-full-name verify abbrev-ref=b? after=q before=q branches=q? default=q
222
+ disambiguate=b exclude=q exclude-hidden=b git-path=p glob=q path-format=b? prefix=q remotes=q?
223
+ resolve-git-dir=p short=i? show-object-format=b? since=q tags=q? until=q].freeze,
223
224
  show: %w[t combined-all-paths no-diff-merges remerge-diff show-signature diff-merges=b encoding=b
224
225
  expand-tabs=i notes=q show-notes=q?].freeze,
225
226
  stash: {
226
227
  common: %w[q|quiet].freeze,
227
- push: %w[a|all u|include-untracked k|keep-index no-keep-index p|patch S|staged m|message=q
228
- pathspec-from-file=p].freeze,
228
+ push: %w[a|all u|include-untracked k|keep-index no-keep-index no-include-untracked pathspec-file-nul
229
+ p|patch S|staged m|message=q pathspec-from-file=p].freeze,
229
230
  pop: %w[index].freeze,
230
231
  apply: %w[index].freeze
231
232
  }.freeze,
232
- status: %w[untracked-files=b? u|ignore-submodules=m? ignored=b?],
233
- tag: %w[create-reflog column=b contains=b? format=q merged=b? n=i no-contains=b? no-merged=b? points-at=q
234
- sort=q].freeze,
233
+ status: %w[u|ignore-submodules=bm? ignored=b? untracked-files=b?],
234
+ tag: %w[n=im cleanup=b create-reflog i|ignore-case color=b? column=b contains=b? format=q merged=b?
235
+ no-contains=b? no-merged=b? points-at=q sort=q].freeze,
235
236
  no: {
237
+ branch: %w[color color-moved column track].freeze,
238
+ checkout: %w[overwrite-ignore guess overlay progress recurse-submodules track].freeze,
236
239
  fetch: {
237
240
  base: %w[auto-gc auto-maintenance write-commit-graph write-fetch-head].freeze,
238
241
  pull: %w[all ipv4 ipv6 recurse-submodules show-forced-updates tags].freeze
239
242
  },
240
243
  log: {
241
244
  base: %w[decorate mailmap merges use-mailmap].freeze,
242
- show: %w[abbrev-commit expand-tabs notes].freeze,
243
- diff: %w[color color-moved ext-diff indent-heuristic patch relative rename-empty textconv].freeze
245
+ diff: %w[color color-moved ext-diff indent-heuristic patch relative rename-empty textconv].freeze,
246
+ show: %w[abbrev-commit expand-tabs notes].freeze
244
247
  }.freeze,
245
- pull: %w[autostash commit edit ff log signoff squash stat verify verify-signatures gpg-sign rebase].freeze,
246
- tag: %w[column].freeze,
247
- branch: %w[color-moved column color track].freeze,
248
- checkout: %w[overwrite-ignore guess overlay progress recurse-submodules track].freeze,
249
- merge: %w[autostash edit ff gpg-sign log progress overwrite-ignore rerere-autoupdate signoff squash stat
248
+ merge: %w[autostash edit ff gpg-sign log overwrite-ignore progress rerere-autoupdate signoff squash stat
250
249
  verify verify-signatures].freeze,
251
- rebase: %w[autosquash autostash fork-point gpg-sign keep-empty reapply-cherry-picks reschedule-failed-exec
252
- rerere-autoupdate stat update-refs verify].freeze,
250
+ pull: %w[autostash commit edit gpg-sign ff log rebase signoff squash stat verify verify-signatures].freeze,
251
+ rebase: %w[autosquash autostash fork-point gpg-sign keep-empty reapply-cherry-picks rebase-merges
252
+ rerere-autoupdate reschedule-failed-exec stat update-refs verify].freeze,
253
253
  reset: %w[refresh].freeze,
254
254
  restore: %w[overlay progress recurse-submodules].freeze,
255
+ rev_parse: %w[flags].freeze,
255
256
  revert: %w[edit gpg-sign rerere-autoupdate].freeze,
256
- show: %w[standard-notes].freeze
257
+ show: %w[standard-notes].freeze,
258
+ tag: %w[column].freeze
257
259
  }.freeze
258
260
  }.freeze
259
261
  VAL_GIT = {
@@ -272,7 +274,7 @@ module Squared
272
274
  include Rake::DSL
273
275
 
274
276
  def populate(ws, **)
275
- return if ws.series[:pull].empty?
277
+ return if ws.series.exclude?(:pull, true)
276
278
 
277
279
  namespace(name = ws.task_name('git')) do
278
280
  all = ws.task_join(name, 'all')
@@ -297,7 +299,7 @@ module Squared
297
299
  end
298
300
 
299
301
  def tasks
300
- %i[pull rebase fetch clone stash status branch revbuild].freeze
302
+ %i[pull rebase autostash fetch clone stash status branch revbuild].freeze
301
303
  end
302
304
 
303
305
  def config?(val)
@@ -308,23 +310,24 @@ module Squared
308
310
  end
309
311
 
310
312
  subtasks({
311
- 'branch' => %i[create set delete move copy list edit current].freeze,
313
+ 'branch' => %i[create track delete move copy list current].freeze,
312
314
  'checkout' => %i[commit branch track detach path].freeze,
313
- 'commit' => %i[add all amend amend-orig].freeze,
315
+ 'commit' => %i[add all amend amend-orig fixup].freeze,
314
316
  'diff' => %i[head branch files view between contain].freeze,
315
317
  'fetch' => %i[origin remote].freeze,
316
- 'files' => %i[cached modified deleted others ignored].freeze,
317
- 'git' => %i[clean mv revert rm].freeze,
318
+ 'files' => %i[cached modified deleted others].freeze,
319
+ 'git' => %i[add clean mv revert rm].freeze,
318
320
  'log' => %i[view between contain].freeze,
319
321
  'merge' => %i[commit no-commit send].freeze,
320
322
  'pull' => %i[origin remote].freeze,
321
323
  'rebase' => %i[branch onto send].freeze,
322
324
  'refs' => %i[heads tags remote].freeze,
323
325
  'reset' => %i[commit index patch mode].freeze,
324
- 'restore' => %i[staged worktree].freeze,
325
- 'rev' => %i[commit branch output parseopt build].freeze,
326
+ 'restore' => %i[source staged worktree].freeze,
327
+ 'rev' => %i[commit build output].freeze,
326
328
  'show' => %i[format oneline textconv].freeze,
327
329
  'stash' => %i[push pop apply drop clear list].freeze,
330
+ 'switch' => %i[create detach merge].freeze,
328
331
  'tag' => %i[add sign delete list].freeze
329
332
  })
330
333
 
@@ -342,7 +345,7 @@ module Squared
342
345
  return unless ref?(Git.ref)
343
346
 
344
347
  namespace name do
345
- @@tasks[Git.ref].each do |action, flags|
348
+ Git.subtasks do |action, flags|
346
349
  next if @pass.include?(action)
347
350
 
348
351
  namespace action do
@@ -350,7 +353,7 @@ module Squared
350
353
  case action
351
354
  when 'pull', 'fetch'
352
355
  if flag == :remote
353
- format_desc(action, flag, 'remote,opts*')
356
+ format_desc action, flag, 'remote,opts*'
354
357
  task flag, [:remote] do |_, args|
355
358
  remote = param_guard(action, flag, args: args, key: :remote)
356
359
  __send__(action, flag, args.extras, remote: remote)
@@ -366,13 +369,39 @@ module Squared
366
369
  when :all
367
370
  format_desc action, flag, 'message?'
368
371
  task flag, [:message] do |_, args|
369
- commit(flag, message: args.fetch(:message, nil))
372
+ commit(flag, message: args.message)
370
373
  end
371
374
  else
372
- format_desc action, flag, 'pathspec+'
375
+ format_desc(action, flag, 'pathspec+', before: flag == :add ? 'opts*' : nil)
373
376
  task flag do |_, args|
374
- refs = param_guard(action, flag, args: args.to_a)
375
- commit(flag, refs: refs)
377
+ if flag == :fixup
378
+ ref, squash, pick = choice_commit(accept: [['Auto squash?', true]], reflog: false,
379
+ values: ['Pick [amend|reword]'])
380
+ pick = case pick&.downcase
381
+ when 'a', 'amend'
382
+ 'amend'
383
+ when 'r', 'reword'
384
+ 'reword'
385
+ end
386
+ if squash
387
+ found = false
388
+ git_spawn(git_output('log --format=%h'), stdout: false).each do |val|
389
+ if found
390
+ squash = val.chomp
391
+ break
392
+ end
393
+ found = val.chomp == ref
394
+ end
395
+ end
396
+ end
397
+ refs = pick == 'reword' ? [] : param_guard(action, flag, args: args.to_a)
398
+ if flag == :add
399
+ opts = refs
400
+ refs = []
401
+ else
402
+ opts = []
403
+ end
404
+ commit(flag, opts, refs: refs, ref: ref, squash: squash, pick: pick)
376
405
  end
377
406
  end
378
407
  when 'tag'
@@ -385,61 +414,103 @@ module Squared
385
414
  when :delete
386
415
  format_desc action, flag, 'name+'
387
416
  task flag do |_, args|
388
- refs = param_guard(action, flag, args: args.to_a)
389
- tag(flag, refs: refs)
417
+ refs = args.to_a
418
+ if refs.empty?
419
+ refs = choice_refs('Choose a tag', 'tags', multiple: true, accept: 'Delete?', series: true)
420
+ remote = choice_remote
421
+ end
422
+ tag(flag, refs: refs, remote: remote)
390
423
  end
391
424
  when :add, :sign
392
- format_desc action, flag, 'name,message?,commit?'
393
- task flag, [:name, :message, :commit] do |_, args|
394
- name = param_guard(action, flag, args: args, key: :name)
395
- tag(flag, refs: [name], message: args.message, commit: args.commit)
425
+ format_desc action, flag, 'name,message?,commit?,remote?'
426
+ task flag, [:name, :message, :commit, :remote] do |_, args|
427
+ if (name = args.name)
428
+ message = args.message
429
+ commit = commithead args.commit
430
+ remote = args.remote
431
+ else
432
+ commit, name, message = choice_commit(values: [['Enter tag name', true], 'Enter message'],
433
+ series: true, reflog: false)
434
+ remote = choice_remote
435
+ end
436
+ ret = tag(flag, refs: [name], message: message, commit: commit, remote: remote)
437
+ print_success if success?(ret) && !remote
396
438
  end
397
439
  end
398
440
  when 'stash'
399
- if flag == :list
400
- format_desc action, flag
401
- else
402
- format_desc(action, flag, '*opts', after: flag == :push ? 'pathspec*' : 'commit?')
403
- end
441
+ format_desc(action, flag, 'opts*', after: case flag
442
+ when :push then 'pathspec*'
443
+ when :list then nil
444
+ else 'commit?' end)
404
445
  task flag do |_, args|
405
446
  stash flag, args.to_a
406
447
  end
407
448
  when 'log', 'diff'
408
449
  case flag
409
450
  when :view, :between, :contain
410
- if flag == :view && action == 'log'
411
- format_desc action, flag, '(^)commit/H0*,pathspec*,opts*'
451
+ view = flag == :view
452
+ if view && action == 'log'
453
+ format_desc action, flag, '(^)commit*|:,opts*,ref?,pathspec*'
412
454
  task flag do |_, args|
413
- index = []
414
- args.to_a.each do |val|
415
- if val =~ /^H(\d+)$/
416
- index << "HEAD~#{$1}"
417
- elsif (sha = commithash(val))
418
- index << sha
419
- elsif val.start_with?('^') || (!%r{^[.\\/]}.match?(val) && !%r{[\\/]$}.match?(val))
420
- index << shell_quote(val)
455
+ args = args.to_a
456
+ if args.first == ':'
457
+ args.shift
458
+ index = choice_commit(multiple: true)
459
+ else
460
+ index = []
461
+ args.each do |val|
462
+ if matchhead(val)
463
+ index << commithead(val)
464
+ elsif (sha = commithash(val))
465
+ index << sha
466
+ elsif val.start_with?('^')
467
+ index << shell_quote(val)
468
+ else
469
+ break
470
+ end
421
471
  end
472
+ args = args.drop(index.size)
422
473
  end
423
- logx(flag, args.to_a.drop(index.size), index: index)
474
+ log!(flag, args, index: index)
424
475
  end
425
476
  else
426
- format_desc action, flag, 'commit1,commit2,pathspec*,opts*'
477
+ format_desc action, flag, 'commit1,commit2,opts*,pathspec*'
427
478
  task flag, [:commit1, :commit2] do |_, args|
428
- commit1 = param_guard(action, flag, args: args, key: :commit1)
429
- commit2 = param_guard(action, flag, args: args, key: :commit2)
430
- __send__(action == 'log' ? :logx : :diff, flag, args.extras, range: [commit1, commit2])
479
+ commit1 = commithead args.commit1
480
+ if commit1
481
+ commit2 = commithead param_guard(action, flag, args: args, key: :commit2)
482
+ args = args.extras.to_a
483
+ range = [commit1, commit2]
484
+ else
485
+ range, opts, refs = choice_commit(multiple: view ? true : 2, values: %w[Options Pathspec])
486
+ range = range.reverse
487
+ args = OptionPartition.strip(opts)
488
+ args.concat(refs.shellsplit) if refs
489
+ end
490
+ __send__(action == 'log' ? :log! : :diff, flag, args, range: range)
431
491
  end
432
492
  end
433
493
  when :head
434
- format_desc action, flag, 'commit/H0*,opts*,pathspec*'
494
+ format_desc action, flag, 'commit*|:,opts*,pathspec*'
435
495
  task flag do |_, args|
436
- index = []
437
- args.to_a.each do |val|
438
- break unless val =~ /^H(\d+)$/ || (sha = commithash(val))
439
-
440
- index << ($1 ? "HEAD~#{$1}" : sha)
496
+ args = args.to_a
497
+ if args.first == ':'
498
+ args.shift
499
+ index = choice_commit(multiple: true)
500
+ else
501
+ index = []
502
+ args.each do |val|
503
+ if matchhead(val)
504
+ index << commithead(val)
505
+ elsif (sha = commithash(val))
506
+ index << sha
507
+ else
508
+ break
509
+ end
510
+ end
511
+ args = args.drop(index.size)
441
512
  end
442
- diff(flag, args.to_a.drop(index.size), index: index)
513
+ diff(flag, args, index: index)
443
514
  end
444
515
  when :branch
445
516
  format_desc action, flag, 'name,opts*,pathspec*'
@@ -460,42 +531,61 @@ module Squared
460
531
  when :branch
461
532
  format_desc action, flag, 'name,create?=[bB],commit?,detach?=d'
462
533
  task flag, [:name, :create, :commit, :detach] do |_, args|
463
- branch = param_guard(action, flag, args: args, key: :name)
464
- create = args.create
465
- if args.commit == 'd'
466
- detach = 'd'
467
- commit = nil
468
- elsif create == 'd'
469
- create = nil
470
- commit = nil
471
- detach = 'd'
472
- elsif create && create.size > 1
473
- commit = create
474
- create = nil
475
- detach = args.commit
534
+ if (branch = args.name)
535
+ branch = param_guard(action, flag, args: args, key: :name)
536
+ create = args.create
537
+ if args.commit == 'd'
538
+ detach = 'd'
539
+ commit = nil
540
+ elsif create == 'd'
541
+ create = nil
542
+ commit = nil
543
+ detach = 'd'
544
+ elsif create && create.size > 1
545
+ commit = commithead create
546
+ create = nil
547
+ detach = args.commit
548
+ else
549
+ detach = args.detach
550
+ commit = commithead args.commit
551
+ end
552
+ param_guard(action, flag, args: { create: create }, key: :create, pat: /\Ab\z/i) if create
476
553
  else
477
- detach = args.detach
478
- commit = args.commit
554
+ branch = choice_refs 'Choose a branch to switch'
479
555
  end
480
- param_guard(action, flag, args: { create: create }, key: :create, pat: /\Ab\z/i) if create
481
556
  checkout(flag, branch: branch, create: create, commit: commit, detach: detach)
482
557
  end
483
558
  when :track
484
559
  format_desc action, flag, 'origin,(^)name?'
485
560
  task flag, [:origin, :name] do |_, args|
486
- origin = param_guard(action, flag, args: args, key: :origin)
487
- checkout(flag, branch: args.name, origin: origin)
561
+ if (origin = args.origin)
562
+ branch = args.name
563
+ else
564
+ origin, branch = choice_refs('Choose a remote', 'remotes', values: ['Enter branch name'])
565
+ end
566
+ checkout(flag, branch: branch, origin: origin)
488
567
  end
489
568
  when :commit
490
- format_desc action, flag, 'branch/commit,opts*'
569
+ format_desc action, flag, 'ref,opts*'
491
570
  task flag, [:commit] do |_, args|
492
- commit = param_guard(action, flag, args: args, key: :commit)
493
- checkout(flag, commit: commit)
571
+ commit = commithead args.commit
572
+ if commit
573
+ args = args.extras
574
+ else
575
+ commit, opts = choice_commit(values: ['Options'])
576
+ args = OptionPartition.strip(opts)
577
+ end
578
+ checkout(flag, args, commit: commit)
494
579
  end
495
580
  when :detach
496
- format_desc action, flag, 'branch/commit?'
581
+ format_desc action, flag, 'ref?'
497
582
  task flag, [:commit] do |_, args|
498
- checkout(flag, commit: args.commit)
583
+ commit = commithead args.commit
584
+ unless commit
585
+ commit, merge = choice_commit(values: ['Merge? [y|N]'])
586
+ merge = merge&.upcase == 'Y'
587
+ end
588
+ checkout(flag, commit: commit, merge: merge)
499
589
  end
500
590
  when :path
501
591
  format_desc action, flag, 'opts*,pathspec*'
@@ -506,27 +596,46 @@ module Squared
506
596
  when 'branch'
507
597
  case flag
508
598
  when :create
509
- format_desc action, flag, 'name,ref?=HEAD'
599
+ format_desc action, flag, 'name,ref?=HEAD|:'
510
600
  task flag, [:name, :ref] do |_, args|
511
601
  target = param_guard(action, flag, args: args, key: :name)
512
- branch(flag, target: target, ref: args.ref)
602
+ ref = commithead args.ref
603
+ ref, remote = choice_refs('Choose a remote', 'remotes', accept: [['Push?', true]]) if ref == ':'
604
+ branch(flag, target: target, ref: ref, remote: remote)
513
605
  end
514
- when :set
515
- format_desc(action, flag, '(^)upstream,name?')
606
+ when :track
607
+ format_desc action, flag, '(^~)upstream?,name?'
516
608
  task flag, [:upstream, :name] do |_, args|
517
- upstream = param_guard(action, flag, args: args, key: :upstream)
518
- branch(flag, target: args.name, ref: upstream)
609
+ if (ref = args.upstream)
610
+ target = args.name
611
+ if ref.start_with?('~')
612
+ ref = ref[1..-1]
613
+ remote = true
614
+ end
615
+ else
616
+ ref, remote, target = choice_refs('Choose a remote', 'remotes', accept: [['Push?', true]],
617
+ values: ['Enter branch name'])
618
+ end
619
+ branch(flag, target: target, ref: ref, remote: remote)
519
620
  end
520
621
  when :delete
521
- format_desc action, flag, '(^~)name+'
622
+ format_desc action, flag, '(^~)name*,:?'
522
623
  task flag do |_, args|
523
- refs = param_guard(action, flag, args: args.to_a)
524
- branch(flag, refs: refs)
525
- end
526
- when :edit
527
- format_desc action, flag, 'name?'
528
- task flag, [:name] do |_, args|
529
- branch(flag, target: args.name)
624
+ refs = args.to_a
625
+ if refs.empty? || (r = refs.last == ':')
626
+ accept = ['Delete?']
627
+ accept << ['Force?', true] unless r
628
+ remote = choice_refs('Choose a branch', r ? 'remotes' : 'heads', multiple: true,
629
+ accept: accept)
630
+ if r
631
+ refs.pop
632
+ else
633
+ refs = remote.first
634
+ refs.map! { |val| "^#{val}" } if remote[1]
635
+ remote = nil
636
+ end
637
+ end
638
+ branch(flag, refs: refs, remote: remote)
530
639
  end
531
640
  when :list
532
641
  format_desc action, flag, 'opts*,pattern*'
@@ -541,17 +650,55 @@ module Squared
541
650
  else
542
651
  format_desc action, flag, 'branch,oldbranch?'
543
652
  task flag, [:branch, :oldbranch] do |_, args|
544
- branch = param_guard(action, flag, args: args, key: :branch)
545
- branch(flag, refs: [args.oldbranch, branch])
653
+ if (branch = args.branch)
654
+ oldbranch = args.oldbranch
655
+ else
656
+ oldbranch, branch = choice_refs("Choose a branch to #{flag}",
657
+ values: [['Enter new branch name', true]])
658
+ end
659
+ branch(flag, refs: [oldbranch, branch])
660
+ end
661
+ end
662
+ when 'switch'
663
+ case flag
664
+ when :create
665
+ format_desc action, flag, '(^)name,ref?=HEAD|:'
666
+ task flag, [:name, :commit] do |_, args|
667
+ target = param_guard(action, flag, args: args, key: :name)
668
+ commit = commithead args.commit
669
+ commit, track = choice_commit(values: ['Track? [Y|n]'], force: false) if commit == ':'
670
+ switch(flag, target: target, commit: commit, track: track)
671
+ end
672
+ when :detach
673
+ format_desc action, flag, 'ref?=HEAD'
674
+ task flag, [:commit] do |_, args|
675
+ commit = commithead(args.commit) || choice_commit(force: false)
676
+ switch(flag, commit: commit)
677
+ end
678
+ when :merge
679
+ format_desc action, flag, 'branch?'
680
+ task flag, [:branch] do |_, args|
681
+ commit = args.branch || choice_refs('Choose a branch')
682
+ switch(flag, commit: commit)
546
683
  end
547
684
  end
548
685
  when 'reset'
549
686
  case flag
550
687
  when :commit
551
- format_desc action, flag, 'branch/commit,opts*'
688
+ format_desc action, flag, 'ref|:,opts*'
552
689
  task flag, [:commit] do |_, args|
553
- commit = param_guard(action, flag, args: args, key: :commit)
554
- reset(flag, args.extras, commit: commit)
690
+ commit = commithead args.commit
691
+ if commit && commit != ':'
692
+ args = args.extras
693
+ else
694
+ commit, mode = choice_commit(values: ['Mode [mixed|soft|hard|N]'])
695
+ args = args.extras.to_a.concat(case mode&.downcase
696
+ when 'h', 'hard' then ['hard']
697
+ when 's', 'soft' then ['soft']
698
+ when 'n', 'N' then ['mixed', 'N']
699
+ else ['mixed'] end)
700
+ end
701
+ print_success if success?(reset(flag, args, commit: commit))
555
702
  end
556
703
  when :index
557
704
  format_desc action, flag, 'opts*,pathspec*'
@@ -559,15 +706,18 @@ module Squared
559
706
  reset flag, args.to_a
560
707
  end
561
708
  when :mode
562
- format_desc action, flag, 'mode,ref?=HEAD'
709
+ format_desc action, flag, 'mode,ref?=HEAD|:'
563
710
  task flag, [:mode, :ref] do |_, args|
564
711
  mode = param_guard(action, flag, args: args, key: :mode)
565
- reset(flag, mode: mode, ref: args.ref)
712
+ ref = commithead args.ref
713
+ ref = choice_commit(reflog: false) if ref == ':'
714
+ reset(flag, mode: mode, ref: ref)
566
715
  end
567
716
  when :patch
568
- format_desc action, flag, 'ref,pathspec*'
717
+ format_desc action, flag, 'ref?=HEAD|:,pathspec*'
569
718
  task flag, [:ref] do |_, args|
570
- ref = param_guard(action, flag, args: args, key: :ref)
719
+ ref = commithead args.ref
720
+ ref = choice_commit(reflog: false) unless ref && ref != ':'
571
721
  reset(flag, refs: args.extras, ref: ref)
572
722
  end
573
723
  end
@@ -576,7 +726,7 @@ module Squared
576
726
  when :oneline
577
727
  format_desc action, flag, 'opts*,object*'
578
728
  task flag do |_, args|
579
- show flag, args.to_a.push('abbrev-commit')
729
+ show flag, args.to_a
580
730
  end
581
731
  when :format
582
732
  format_desc action, flag, 'format?,opts*,object*'
@@ -593,23 +743,42 @@ module Squared
593
743
  when 'rebase', 'merge'
594
744
  case flag
595
745
  when :branch
596
- format_desc action, flag, 'opts*,upstream?,branch?'
597
- task flag do |_, args|
598
- args = param_guard(action, flag, args: args.to_a)
599
- rebase flag, args
746
+ format_desc action, flag, 'upstream,branch?=HEAD,opts*'
747
+ task flag, [:upstream] do |_, args|
748
+ if (upstream = args.upstream)
749
+ args = args.extras
750
+ else
751
+ upstream, opts = choice_refs('Choose upstream branch', values: ['Options'])
752
+ args = OptionPartition.strip(opts)
753
+ end
754
+ rebase(flag, args, upstream: upstream)
600
755
  end
601
756
  when :onto
602
- format_desc action, flag, 'branch/commit,upstream,branch?=HEAD'
757
+ format_desc action, flag, 'ref,upstream,branch?=HEAD'
603
758
  task flag, [:commit, :upstream, :branch] do |_, args|
604
- commit = param_guard(action, flag, args: args, key: :commit)
605
- upstream = param_guard(action, flag, args: args, key: :upstream)
606
- rebase(flag, commit: commit, upstream: upstream, branch: args.branch)
759
+ commit = commithead args.commit
760
+ if commit
761
+ upstream = param_guard(action, flag, args: args, key: :upstream)
762
+ branch = args.branch
763
+ args = []
764
+ else
765
+ commit = choice_refs 'Choose "onto" branch'
766
+ target, opts = choice_commit(multiple: 2, values: ['Options'], reflog: false)
767
+ branch, upstream = target
768
+ args = OptionPartition.strip(opts)
769
+ end
770
+ rebase(flag, args, commit: commit, upstream: upstream, branch: branch)
607
771
  end
608
772
  when :commit, :'no-commit'
609
- format_desc action, flag, 'branch/commit+,opts*'
773
+ format_desc action, flag, 'refs+,opts*'
610
774
  task flag do |_, args|
611
- args = param_guard(action, flag, args: args.to_a)
612
- merge flag, args
775
+ args = args.to_a
776
+ if args.empty?
777
+ accept = "Merge with #{`#{git_output('branch --show-current')}`.chomp}?"
778
+ branch, opts = choice_refs('Choose a branch', values: ['Options'], accept: accept)
779
+ args = OptionPartition.strip(opts)
780
+ end
781
+ merge(flag, args, branch: branch)
613
782
  end
614
783
  when :send
615
784
  format_desc(action, flag, VAL_GIT[action.to_sym][:send], arg: nil)
@@ -624,25 +793,20 @@ module Squared
624
793
  when :commit
625
794
  format_desc action, flag, 'ref?=HEAD,size?'
626
795
  task flag, [:ref, :size] do |_, args|
627
- ref = args.ref
796
+ ref = commithead args.ref
628
797
  size = args.size
629
- if !size && ref.to_i > 0
798
+ if !size && ref.to_i.between?(1, 40)
630
799
  size = ref
631
800
  ref = nil
632
801
  end
633
802
  rev_parse(flag, ref: ref, size: size)
634
803
  end
635
- when :branch
636
- format_desc action, flag, 'ref?=HEAD'
637
- task flag, [:ref] do |_, args|
638
- rev_parse(flag, ref: args.ref)
639
- end
640
804
  when :build
641
805
  format_desc action, flag, OPT_GIT[:status]
642
806
  task flag do |_, args|
643
807
  revbuild flag, args.to_a
644
808
  end
645
- else
809
+ when :output
646
810
  format_desc action, flag, 'opts*,args*'
647
811
  task flag do |_, args|
648
812
  rev_parse flag, args.to_a
@@ -650,10 +814,9 @@ module Squared
650
814
  end
651
815
  when 'refs', 'files'
652
816
  if flag == :remote
653
- format_desc action, flag, 'remote,opts*,pattern*'
817
+ format_desc action, flag, 'remote?,opts*,pattern*'
654
818
  task flag, [:remote] do |_, args|
655
- remote = param_guard(action, flag, args: args, key: :remote)
656
- ls_remote(flag, args.extras, remote: remote)
819
+ ls_remote(flag, args.extras, remote: args.remote)
657
820
  end
658
821
  else
659
822
  format_desc action, flag, 'opts*,pattern*'
@@ -661,17 +824,57 @@ module Squared
661
824
  __send__(action == 'refs' ? :ls_remote : :ls_files, flag, args.to_a)
662
825
  end
663
826
  end
664
- when 'restore', 'git'
665
- format_desc(action, flag, 'opts*', before: case flag
666
- when :rm
667
- 'source+,destination'
668
- when :revert
669
- 'commit+'
670
- else
671
- 'pathspec*'
672
- end)
827
+ when 'restore'
828
+ case flag
829
+ when :source
830
+ format_desc action, flag, 'ref,opts*,pathspec*'
831
+ task flag, [:commit] do |_, args|
832
+ commit = commithead args.commit
833
+ if commit
834
+ args = args.extras
835
+ else
836
+ commit, opts, files = choice_commit(values: ['Options', ['Pathspec', true]])
837
+ args = OptionPartition.strip(opts)
838
+ files = files&.shellsplit
839
+ end
840
+ restore(flag, args, commit: commit, files: files)
841
+ end
842
+ when :staged, :worktree
843
+ format_desc action, flag, 'opts*,pathspec*,:?'
844
+ task flag do |_, args|
845
+ args = args.to_a
846
+ if args.empty? || args.last == ':'
847
+ files = []
848
+ git_spawn('status -s --porcelain', stdout: false).each do |line|
849
+ next unless line =~ /^(.)(.)\s+(.+)$/
850
+
851
+ case (flag == :staged ? $1 : $2)
852
+ when 'A', 'M'
853
+ files << $3
854
+ end
855
+ end
856
+ unless files.empty?
857
+ files = choice_index('Select a file', files, multiple: true, force: false,
858
+ accept: 'Restore?')
859
+ end
860
+ args.pop
861
+ args, glob = args.partition { |val| val.match?(/^(?:[a-z-]+=|[^*]+$)/) }
862
+ (files ||= []).concat(glob)
863
+ next if args.empty? && files.empty?
864
+ end
865
+ restore(flag, args, files: files)
866
+ end
867
+ end
868
+ when 'git'
869
+ before = case flag
870
+ when :rm then 'source+,destination'
871
+ when :revert then 'commit+' end
872
+ format_desc(action, flag, 'opts*', before: before, after: case flag
873
+ when :add, :clean, :mv
874
+ 'pathspec*'
875
+ end)
673
876
  task flag do |_, args|
674
- __send__(action, flag, args.to_a)
877
+ git flag, args.to_a
675
878
  end
676
879
  end
677
880
  end
@@ -699,15 +902,18 @@ module Squared
699
902
  cmd, opts = git_session('pull', opts: opts)
700
903
  if flag == :rebase
701
904
  cmd << '--rebase'
702
- elsif (val = option('rebase', ignore: false))
703
- cmd << case val
704
- when '0', 'false'
705
- '--no-rebase'
706
- else
707
- VAL_GIT[:rebase][:value].include?(val) ? basic_option('rebase', val) : '--rebase'
708
- end
905
+ cmd << '--autostash' if option('autostash')
906
+ else
907
+ cmd << '--autostash' if flag == :autostash
908
+ if (val = option('rebase', ignore: false))
909
+ cmd << case val
910
+ when '0', 'false'
911
+ '--no-rebase'
912
+ else
913
+ VAL_GIT[:rebase][:value].include?(val) ? basic_option('rebase', val) : '--rebase'
914
+ end
915
+ end
709
916
  end
710
- cmd << '--autostash' if option('autostash')
711
917
  append_pull(opts, OPT_GIT[:pull] + OPT_GIT[:fetch][:pull],
712
918
  no: OPT_GIT[:no][:pull] + OPT_GIT[:no][:fetch][:pull], remote: remote, flag: flag)
713
919
  source(sync: sync, sub: if verbose
@@ -725,16 +931,12 @@ module Squared
725
931
  cmd, opts = git_session('rebase', opts: opts)
726
932
  case flag
727
933
  when :branch
934
+ return unless upstream
935
+
728
936
  op = OptionPartition.new(opts, OPT_GIT[:rebase], cmd, project: self, no: OPT_GIT[:no][:rebase])
729
- case op.size
730
- when 0
731
- append_head
732
- when 1, 2
733
- op.append(delim: true)
734
- else
735
- op.append(op.pop(2), delim: true)
736
- .clear(pass: false)
737
- end
937
+ cmd << shell_escape(upstream)
938
+ append_head op.shift
939
+ op.clear(pass: false)
738
940
  when :onto
739
941
  return unless upstream
740
942
 
@@ -750,6 +952,10 @@ module Squared
750
952
  source
751
953
  end
752
954
 
955
+ def autostash(*, sync: invoked_sync?('autostash'), **)
956
+ pull(:autostash, sync: sync)
957
+ end
958
+
753
959
  def fetch(flag = nil, opts = [], sync: invoked_sync?('fetch', flag), remote: nil)
754
960
  cmd, opts = git_session('fetch', opts: opts)
755
961
  append_pull(opts, collect_hash(OPT_GIT[:fetch]), no: collect_hash(OPT_GIT[:no][:fetch]), remote: remote,
@@ -784,8 +990,12 @@ module Squared
784
990
  def stash(flag = nil, opts = [], sync: invoked_sync?('stash', flag))
785
991
  if flag
786
992
  cmd, opts = git_session('stash', flag, opts: opts)
787
- op = OptionPartition.new(opts, OPT_GIT[:stash][:common] + OPT_GIT[:stash].fetch(flag, []), cmd,
788
- project: self)
993
+ list = OPT_GIT[:stash][:common] + OPT_GIT[:stash].fetch(flag, [])
994
+ if flag == :list
995
+ list += collect_hash(OPT_GIT[:log])
996
+ no = collect_hash(OPT_GIT[:no][:log])
997
+ end
998
+ op = OptionPartition.new(opts, list, cmd, project: self, no: no, first: flag == :push ? matchpathspec : nil)
789
999
  case flag
790
1000
  when :push
791
1001
  append_pathspec op.extras
@@ -800,6 +1010,7 @@ module Squared
800
1010
  end
801
1011
  return
802
1012
  when :list
1013
+ op.clear
803
1014
  out, banner, from = source(io: true)
804
1015
  print_item banner
805
1016
  list_result(write_lines(out), 'objects', from: from)
@@ -807,8 +1018,8 @@ module Squared
807
1018
  end
808
1019
  else
809
1020
  git_session('stash', 'push', opts: opts)
810
- append_option(%w[all keep-index include-untracked staged].freeze, no: true, ignore: false)
811
- append_message option('message', 'm', ignore: false)
1021
+ append_option(OPT_GIT[:stash][:push].grep_v(/[=|]/), no: true, ignore: false)
1022
+ append_message
812
1023
  end
813
1024
  source(banner: !quiet?, sync: sync, **threadargs)
814
1025
  end
@@ -816,6 +1027,7 @@ module Squared
816
1027
  def status(*)
817
1028
  cmd = git_session 'status'
818
1029
  cmd << (option('long') ? '--long' : '--short')
1030
+ cmd << '--branch' if option('branch')
819
1031
  if (val = option('ignore-submodules', ignore: false))
820
1032
  cmd << basic_option('ignore-submodules', case val
821
1033
  when '0', 'none'
@@ -829,18 +1041,22 @@ module Squared
829
1041
  end)
830
1042
  end
831
1043
  append_pathspec
1044
+ if verbose
1045
+ r = color(:red)
1046
+ g = color(:green)
1047
+ sub = if session_arg?('short')
1048
+ [
1049
+ { pat: /^(.)([A-Z?!])(.+)$/, styles: r, index: 2 },
1050
+ { pat: /^([A-Z?!])(.+)$/, styles: g },
1051
+ { pat: /^(\?\?)(.+)$/, styles: r },
1052
+ { pat: /^(## )(.+?)(\.{3})(.+)$/, styles: [nil, g, nil, r], index: -1 }
1053
+ ]
1054
+ else
1055
+ [{ pat: /^(\t+)([a-z]+: +.+)$/, styles: r, index: 2 }]
1056
+ end
1057
+ end
832
1058
  out, banner, from = source(io: true)
833
- ret = write_lines(out, banner: banner, sub: if verbose
834
- r = color(:red)
835
- g = color(:green)
836
- [
837
- { pat: /^(.)([A-Z?!])(.+)$/, styles: r, index: 2 },
838
- { pat: /^([A-Z?!])(.+)$/, styles: g },
839
- { pat: /^(\?\?)(.+)$/, styles: r },
840
- { pat: /^(## )(.+)(\.{3})(.+)$/,
841
- styles: [nil, g, nil, r], index: -1 }
842
- ]
843
- end)
1059
+ ret = write_lines(out, banner: banner, sub: sub)
844
1060
  list_result(ret, 'files', from: from, action: 'modified')
845
1061
  end
846
1062
 
@@ -853,16 +1069,16 @@ module Squared
853
1069
  end
854
1070
  unless workspace.closed
855
1071
  if @revbuild
856
- statusargs.().each { |key, val| @revbuild[key] += val }
1072
+ statusargs.call.each { |key, val| @revbuild[key] += val }
857
1073
  else
858
- @revbuild = statusargs.()
1074
+ @revbuild = statusargs.call
859
1075
  end
860
1076
  return
861
1077
  end
862
- sha = git_spawn('rev-parse --verify HEAD').first.to_s.chomp
1078
+ sha = git_spawn('rev-parse --verify HEAD').chomp
863
1079
  return if sha.empty?
864
1080
 
865
- kwargs = kwargs.key?(:include) || kwargs.key?(:exclude) ? statusargs.() : @revbuild || {}
1081
+ kwargs = kwargs.key?(:include) || kwargs.key?(:exclude) ? statusargs.call : @revbuild || {}
866
1082
  case flag
867
1083
  when :build
868
1084
  op = OptionPartition.new(opts, OPT_GIT[:status], project: self)
@@ -904,13 +1120,14 @@ module Squared
904
1120
  case flag
905
1121
  when :commit, :index
906
1122
  op = OptionPartition.new(opts, OPT_GIT[:reset] + VAL_GIT[:reset], cmd,
907
- project: self, no: OPT_GIT[:no][:reset])
1123
+ project: self, no: OPT_GIT[:no][:reset],
1124
+ first: flag == :index ? matchpathspec : nil)
908
1125
  if flag == :commit
909
- op.append(commit, delim: true)
1126
+ op.append(commit)
910
1127
  .clear(pass: false)
911
1128
  ref = false
912
1129
  else
913
- (refs ||= []).concat(op.extras)
1130
+ refs = op.extras
914
1131
  end
915
1132
  when :mode
916
1133
  return unless VAL_GIT[:reset].include?(mode)
@@ -922,6 +1139,7 @@ module Squared
922
1139
  end
923
1140
  when :patch
924
1141
  cmd << '--patch'
1142
+ append_pathspec(refs, pass: false)
925
1143
  end
926
1144
  unless ref == false
927
1145
  append_commit(ref, head: true)
@@ -930,39 +1148,41 @@ module Squared
930
1148
  source
931
1149
  end
932
1150
 
933
- def checkout(flag, opts = [], branch: nil, origin: nil, create: nil, commit: nil, detach: nil)
1151
+ def checkout(flag, opts = [], branch: nil, origin: nil, create: nil, commit: nil, detach: nil, merge: false)
934
1152
  cmd, opts = git_session('checkout', opts: opts)
935
1153
  append_option 'force', 'merge'
936
1154
  case flag
937
1155
  when :branch
938
1156
  cmd << '--detach' if detach == 'd' || option('detach')
939
1157
  append_option('track', equals: true)
940
- cmd << (create ? shell_option(create, branch) : branch) << commit
1158
+ cmd << (create ? quote_option(create, branch) : branch) << commit
941
1159
  when :track
942
1160
  if branch
943
1161
  if branch.start_with?('^')
944
1162
  opt = 'B'
945
1163
  branch = branch[1..-1]
946
1164
  end
947
- cmd << shell_option(opt || 'b', branch)
1165
+ cmd << quote_option(opt || 'b', branch)
948
1166
  end
949
- cmd << '--track' << origin
1167
+ cmd << '--track' << shell_quote(origin)
950
1168
  when :detach
1169
+ cmd << '-m' if merge
951
1170
  cmd << '--detach' << commit
952
1171
  else
953
- op = OptionPartition.new(opts, OPT_GIT[:checkout], cmd, project: self, no: OPT_GIT[:no][:checkout])
1172
+ op = OptionPartition.new(opts, OPT_GIT[:checkout], cmd, project: self, no: OPT_GIT[:no][:checkout],
1173
+ first: flag == :path ? matchpathspec : nil)
954
1174
  if flag == :commit
955
- op.append(commit, delim: true)
1175
+ op.append(commit)
956
1176
  .clear(pass: false)
957
1177
  else
958
1178
  append_head
959
- append_pathspec op.extras
1179
+ append_pathspec(op.extras, pass: false)
960
1180
  end
961
1181
  end
962
1182
  source
963
1183
  end
964
1184
 
965
- def tag(flag, opts = [], refs: [], message: nil, commit: nil)
1185
+ def tag(flag, opts = [], refs: [], message: nil, commit: nil, remote: nil)
966
1186
  cmd, opts = git_session('tag', opts: opts)
967
1187
  case flag
968
1188
  when :add, :sign
@@ -974,31 +1194,32 @@ module Squared
974
1194
  cmd << '--force' if option('force')
975
1195
  if !commit && message && (sha = commithash(message))
976
1196
  commit = sha
977
- else
978
- append_message message
1197
+ message = nil
979
1198
  end
1199
+ append_message message
980
1200
  append_value refs
981
1201
  append_head commit
982
- when :list
1202
+ when :delete
1203
+ cmd << '--delete'
1204
+ append_value refs
1205
+ else
983
1206
  op = OptionPartition.new(opts, OPT_GIT[:tag], cmd << '--list', project: self, no: OPT_GIT[:no][:tag])
984
1207
  out, banner, from = source(io: true)
985
1208
  print_item banner
986
1209
  ret = write_lines(out, grep: op.extras)
987
1210
  list_result(ret, 'tags', from: from, grep: op.extras)
988
1211
  return
989
- when :delete
990
- cmd << '--delete'
991
- append_value refs
992
- else
993
- cmd << shell_option(flag, commit)
994
1212
  end
1213
+ remote ||= option('remote')
995
1214
  source
1215
+ git_spawn('push', flag == :delete ? '-d' : nil, remote, *refs.map { |val| shell_quote(val) }) if remote
996
1216
  end
997
1217
 
998
- def logx(flag, opts = [], range: [], index: [])
1218
+ def log!(flag, opts = [], range: [], index: [])
999
1219
  cmd, opts = git_session('log', opts: opts)
1000
1220
  op = OptionPartition.new(opts, collect_hash(OPT_GIT[:log]), cmd,
1001
- project: self, no: collect_hash(OPT_GIT[:no][:log]))
1221
+ project: self, no: collect_hash(OPT_GIT[:no][:log]),
1222
+ first: matchpathspec)
1002
1223
  case flag
1003
1224
  when :between, :contain
1004
1225
  op << shell_quote(range.join(flag == :between ? '..' : '...'))
@@ -1013,7 +1234,8 @@ module Squared
1013
1234
  def diff(flag, opts = [], refs: [], branch: nil, range: [], index: [])
1014
1235
  cmd, opts = git_session('diff', opts: opts)
1015
1236
  op = OptionPartition.new(opts, collect_hash(OPT_GIT[:diff]) + OPT_GIT[:log][:diff], cmd,
1016
- project: self, no: OPT_GIT[:no][:log][:diff])
1237
+ project: self, no: OPT_GIT[:no][:log][:diff],
1238
+ first: flag == :files ? nil : matchpathspec)
1017
1239
  case flag
1018
1240
  when :files, :view, :between, :contain
1019
1241
  op.delete('--cached')
@@ -1026,7 +1248,7 @@ module Squared
1026
1248
  case flag
1027
1249
  when :view
1028
1250
  op << '--merge-base' if option('merge-base')
1029
- op << shell_quote(range.first, quote: true) << shell_quote(range.last, quote: true)
1251
+ op.merge(range)
1030
1252
  when :between, :contain
1031
1253
  op.delete('--merge-base')
1032
1254
  op << shell_quote(range.join(flag == :between ? '..' : '...'))
@@ -1049,82 +1271,99 @@ module Squared
1049
1271
  source(exception: op.arg?('exit-code'))
1050
1272
  end
1051
1273
 
1052
- def commit(flag, *, refs: [], message: nil, pass: false)
1053
- message ||= option('message', 'm', prefix: 'git', ignore: false)
1054
- amend = flag.to_s.start_with?('amend')
1274
+ def commit(flag, opts = [], refs: [], ref: nil, squash: nil, pick: nil, message: nil, pass: false)
1275
+ fixup = flag == :fixup
1276
+ amend = !fixup && flag.to_s.start_with?('amend')
1277
+ unless flag == :add || pick == 'reword'
1278
+ pathspec = if flag == :all || ((fixup || amend) && refs.size == 1 && refs.first == '*')
1279
+ '--all'
1280
+ elsif (refs = projectmap(refs)).empty?
1281
+ raise_error 'no qualified pathspec'
1282
+ else
1283
+ "-- #{refs.join(' ')}"
1284
+ end
1285
+ end
1286
+ if fixup
1287
+ source git_session('commit', basic_option('fixup', "#{pick ? "#{pick}:" : ''}#{ref}"), pathspec)
1288
+ source git_output('rebase --autosquash', squash) if squash.is_a?(String)
1289
+ return
1290
+ end
1291
+ message ||= messageopt
1055
1292
  if !message && !amend
1056
1293
  return if pass
1057
1294
 
1058
- raise_error('missing message', hint: 'GIT_MESSAGE="description"')
1295
+ message = readline('Enter message', force: true)
1059
1296
  end
1060
- pathspec = if flag == :all || (amend && refs.size == 1 && refs.first == '*')
1061
- '--all'
1062
- elsif (refs = projectmap(refs)).empty?
1063
- raise_error 'no qualified pathspec'
1064
- else
1065
- "-- #{refs.join(' ')}"
1066
- end
1067
- origin = nil
1068
1297
  branch = nil
1298
+ origin = nil
1069
1299
  upstream = nil
1300
+ cmd, opts = git_session('add', opts: opts)
1301
+ op = OptionPartition.new(opts, OPT_GIT[:add], cmd, project: self, first: matchpathspec)
1302
+ op << '--verbose' if verbose
1303
+ dryrun = dryrun?
1304
+ format = '%(if)%(HEAD)%(then)%(refname:short)...%(upstream:short)...%(upstream:track)%(end)'
1070
1305
  git_spawn 'fetch --no-tags --quiet'
1071
- git_spawn('branch -vv --list', stdout: false).first.each do |val|
1072
- next unless (r = /^\*\s(\S+)\s+(\h+)(?:\s\[(.+?)(?=\]\s)\])?\s/.match(val))
1073
-
1074
- branch = r[1]
1075
- if r[3]
1076
- origin = r[3][%r{^(.+)/#{Regexp.escape(branch)}$}, 1]
1077
- else
1078
- unless (origin = option('repository', prefix: 'git', ignore: false))
1079
- out = git_spawn('log -n1 --format=%h%d').first
1080
- if out =~ /^#{r[2]} \(HEAD -> #{Regexp.escape(branch)}, (.+?)\)$/
1081
- split_escape($1).each do |s|
1082
- next unless s.end_with?("/#{branch}")
1083
-
1084
- origin = s[0, s.size - branch.size - 1]
1085
- break
1086
- end
1306
+ foreachref('heads', format: format).each do |line|
1307
+ next if (line = line.chomp).empty?
1308
+
1309
+ branch, origin, hint = line.split('...')
1310
+ if hint && !hint.match?(/^\[(\D+0,\D+0)\]$/)
1311
+ raise_error('work tree is not usable', hint: hint[1..-2])
1312
+ elsif origin.empty?
1313
+ return nil if pass
1314
+ break if dryrun
1315
+
1316
+ unless (origin = option('upstream', prefix: 'git', ignore: false))
1317
+ if (origin = choice_refs('Choose an upstream', 'remotes', attempts: 1, force: false))
1318
+ git_spawn 'branch', quote_option('set-upstream-to', origin)
1319
+ else
1320
+ origin = readline('Enter an upstream', force: true)
1087
1321
  end
1088
1322
  end
1089
- upstream = true if origin
1323
+ raise_error('missing remote name', hint: origin) unless origin.include?('/')
1324
+ upstream = true
1090
1325
  end
1091
1326
  break
1092
1327
  end
1093
- raise_error 'work tree is not usable' unless origin && branch
1094
- cmd = git_session('commit', option('dry-run') && '--dry-run', options: false)
1095
- if amend
1096
- cmd << '--amend'
1328
+ if pathspec
1329
+ op << pathspec
1097
1330
  else
1098
- cmd.delete('--amend')
1331
+ append_pathspec op.extras
1332
+ end
1333
+ co = git_session('commit', options: false)
1334
+ pu = git_output 'push', upstream && '--set-upstream'
1335
+ if dryrun
1336
+ op.delete('--dry-run')
1337
+ co << '--dry-run'
1338
+ pu << '--dry-run'
1099
1339
  end
1340
+ co << '--amend' if amend
1100
1341
  if message
1101
1342
  append_message message
1102
1343
  elsif flag == :'amend-orig' || option('edit', equals: '0')
1103
- cmd << '--no-edit'
1104
- end
1105
- a = git_output 'add', '--verbose'
1106
- b = git_output 'push', upstream && '--set-upstream'
1107
- if dryrun?
1108
- a << '--dry-run'
1109
- b << '--dry-run'
1344
+ co << '--no-edit'
1110
1345
  end
1111
- a << pathspec
1112
- b << '--force-with-lease' if amend
1113
- b << origin << branch
1346
+ pu << '--force-with-lease' if amend
1347
+ pu.merge(repotrack(origin, branch))
1114
1348
  puts if pass
1115
- source a
1116
- source cmd
1117
- source b
1349
+ source op
1350
+ source co
1351
+ source pu
1118
1352
  end
1119
1353
 
1120
- def merge(flag, opts = [], command: nil)
1354
+ def merge(flag, opts = [], command: nil, branch: nil)
1121
1355
  cmd, opts = git_session('merge', opts: opts)
1122
1356
  case flag
1123
1357
  when :commit, :'no-commit'
1124
1358
  op = OptionPartition.new(opts, OPT_GIT[:merge], cmd, project: self, no: OPT_GIT[:no][:merge])
1125
1359
  raise_error 'no branch/commit' if op.empty?
1126
1360
  op << "--#{flag}" << '--'
1127
- append_commit(*op.extras)
1361
+ if branch
1362
+ op << branch
1363
+ op.clear(pass: false)
1364
+ else
1365
+ append_commit(*op.extras)
1366
+ end
1128
1367
  else
1129
1368
  return unless VAL_GIT[:merge][:send].include?(command)
1130
1369
 
@@ -1133,7 +1372,7 @@ module Squared
1133
1372
  source
1134
1373
  end
1135
1374
 
1136
- def branch(flag = nil, opts = [], refs: [], ref: nil, target: nil)
1375
+ def branch(flag = nil, opts = [], refs: [], ref: nil, target: nil, remote: nil)
1137
1376
  cmd, opts = git_session('branch', opts: opts)
1138
1377
  stdout = false
1139
1378
  case flag
@@ -1149,43 +1388,46 @@ module Squared
1149
1388
  end
1150
1389
  end
1151
1390
  cmd << '--force' if option('force')
1152
- when :set
1153
- return unless ref
1154
-
1391
+ cmd << shell_quote(target)
1392
+ cmd << shell_quote(ref) if ref
1393
+ when :track
1394
+ raise_error('invalid upstream', hint: ref) unless ref.include?('/')
1155
1395
  if ref.start_with?('^')
1156
- cmd << '--unset-upstream' << shell_escape(ref[1..-1])
1157
- target = nil
1396
+ cmd << '--unset-upstream' << shell_quote(ref[1..-1])
1397
+ remote = false
1158
1398
  stdout = true
1159
1399
  else
1160
1400
  cmd << quote_option('set-upstream-to', ref)
1401
+ cmd << shell_quote(target) if target
1161
1402
  end
1162
- ref = nil
1163
1403
  when :delete
1164
- force, list = refs.partition { |val| val.match?(/^[\^~]/) }
1404
+ remote&.each do |val|
1405
+ source git_output('push', '--delete', *val.split('/', 2).map { |s| shell_quote(s) })
1406
+ end
1407
+ force, list = refs.partition { |val| val.match?(/^[~^]/) }
1165
1408
  force.each do |val|
1166
- dr = val[0, 3]
1409
+ dr = val[0, 2]
1167
1410
  d = dr.include?('^') ? '-D' : '-d'
1168
- r = dr.include?('~') ? '-r' : nil
1169
- source git_output('branch', d, r, shell_quote(val.sub(/^[\^~]+/, '')))
1411
+ r = '-r' if dr.include?('~')
1412
+ source git_output('branch', d, r, shell_quote(val.sub(/^[~^]+/, '')))
1170
1413
  end
1171
1414
  return if list.empty?
1172
1415
 
1173
1416
  cmd << '-d'
1174
- list.each { |val| cmd << shell_quote(val) }
1417
+ append_value list
1418
+ remote = nil
1175
1419
  when :move, :copy
1176
1420
  flag = "-#{flag.to_s[0]}"
1177
1421
  cmd << (option('force') ? flag.upcase : flag)
1178
1422
  refs.compact.each { |val| cmd << shell_quote(val) }
1179
1423
  stdout = true
1180
- when :edit
1181
- cmd << '--edit-description'
1182
1424
  when :current
1183
1425
  cmd << '--show-current'
1184
1426
  source(banner: verbosetype > 1, stdout: true)
1185
1427
  return
1186
1428
  when :list
1187
1429
  op = OptionPartition.new(opts, OPT_GIT[:branch], cmd << '--list',
1188
- project: self, no: OPT_GIT[:no][:branch], single: /^v+$/)
1430
+ project: self, no: OPT_GIT[:no][:branch], single: /\Av+\z/)
1189
1431
  op.each { |val| op << shell_quote(val) }
1190
1432
  out, banner, from = source(io: true)
1191
1433
  print_item banner
@@ -1196,7 +1438,7 @@ module Squared
1196
1438
  list_result(ret, 'branches', from: from)
1197
1439
  return
1198
1440
  else
1199
- head = git_spawn('rev-parse --abbrev-ref HEAD').first.chomp
1441
+ head = git_spawn('rev-parse --abbrev-ref HEAD').chomp
1200
1442
  if head.empty?
1201
1443
  ret = 0
1202
1444
  else
@@ -1212,8 +1454,8 @@ module Squared
1212
1454
  if (r1 = r[1]) && r1 =~ /^(.+):(?: ([a-z]+) (\d+),)? ([a-z]+) (\d+)$/
1213
1455
  write = ->(s1, s2) { "#{s1.capitalize.rjust(7)}: #{sub_style(s2, styles: theme[:warn])}" }
1214
1456
  r1 = $1
1215
- r2 = $2 && write.($2, $3)
1216
- r3 = write.($4, $5)
1457
+ r2 = $2 && write.call($2, $3)
1458
+ r3 = write.call($4, $5)
1217
1459
  end
1218
1460
  r1 = nil if r1 == "origin/#{data[0]}"
1219
1461
  [" Branch: #{a + (r1 ? " (#{r1})" : '')}", r2, r3, " Commit: #{b}", "Message: #{r[2]}"]
@@ -1227,15 +1469,46 @@ module Squared
1227
1469
  end
1228
1470
  return
1229
1471
  end
1230
- cmd << shell_escape(target) if target
1231
- cmd << shell_escape(ref) if ref
1232
- source(stdout: stdout)
1472
+ return unless success?(source(stdout: stdout))
1473
+
1474
+ if !ref
1475
+ print_success if flag == :create
1476
+ elsif remote && target
1477
+ source git_output('push', '-u', shell_quote(ref.split('/', 2).first), shell_quote(target))
1478
+ end
1233
1479
  end
1234
1480
 
1235
- def restore(flag, opts = [])
1236
- cmd, opts = git_session('restore', "--#{flag}", opts: opts)
1237
- op = OptionPartition.new(opts, OPT_GIT[:restore], cmd, project: self, no: OPT_GIT[:no][:restore])
1238
- append_pathspec(op.extras, pass: false)
1481
+ def switch(flag, opts = [], target: nil, commit: nil, track: nil)
1482
+ cmd = git_session('switch', opts: opts).first
1483
+ case flag
1484
+ when :create
1485
+ c = 'c'
1486
+ if target.start_with?('^')
1487
+ target = target[1..-1]
1488
+ c = c.upcase
1489
+ end
1490
+ cmd << quote_option(c, target)
1491
+ cmd << case (track ||= option('track', ignore: false))
1492
+ when 'n', 'N', '0', 'false'
1493
+ '--no-track'
1494
+ when 'y', 'Y', '1', 'true'
1495
+ '--track'
1496
+ when 'direct', 'inherit'
1497
+ basic_option('track', track)
1498
+ end
1499
+ when :detach, :merge
1500
+ cmd << "--#{flag}"
1501
+ end
1502
+ cmd << '--force' if option('force')
1503
+ append_head commit
1504
+ source
1505
+ end
1506
+
1507
+ def restore(flag, opts = [], commit: nil, files: nil)
1508
+ cmd, opts = git_session('restore', shell_option(flag, commit, escape: false, force: false), opts: opts)
1509
+ op = OptionPartition.new(opts, OPT_GIT[:restore], cmd, project: self, no: OPT_GIT[:no][:restore],
1510
+ first: matchpathspec)
1511
+ append_pathspec(op.extras + (files || []), pass: false)
1239
1512
  source(sync: false, stderr: true)
1240
1513
  end
1241
1514
 
@@ -1244,14 +1517,13 @@ module Squared
1244
1517
  case flag
1245
1518
  when :textconv
1246
1519
  cmd << '--textconv'
1247
- append_value(files.map { |val| Dir[val] }
1248
- .flatten
1520
+ append_value(files.flat_map { |val| Dir[val] }
1249
1521
  .select { |val| projectpath?(val) }
1250
1522
  .map! { |val| shell_quote("HEAD:#{val}") })
1251
1523
  source(banner: false)
1252
1524
  return
1253
1525
  when :oneline
1254
- format = flag.to_s
1526
+ format = 'oneline'
1255
1527
  end
1256
1528
  if format
1257
1529
  case (val = format.downcase)
@@ -1268,21 +1540,12 @@ module Squared
1268
1540
  op = OptionPartition.new(opts, OPT_GIT[:show] + OPT_GIT[:diff][:show] + OPT_GIT[:log][:diff], cmd,
1269
1541
  project: self,
1270
1542
  no: OPT_GIT[:no][:show] + collect_hash(OPT_GIT[:no][:log], pass: [:base]))
1271
- unless val == 'oneline' && op.arg?('abbrev-commit')
1272
- op << basic_option('abbrev', val) if (val = option('abbrev')) && val.to_i > 0
1273
- banner = true
1274
- end
1275
1543
  op.append(delim: true)
1276
- source(exception: false, banner: banner)
1544
+ source(exception: false, banner: flag != :oneline)
1277
1545
  end
1278
1546
 
1279
1547
  def rev_parse(flag, opts = [], ref: nil, size: nil)
1280
1548
  cmd, opts = git_session('rev-parse', opts: opts)
1281
- cmd << if flag == :parseopt
1282
- '--parseopt'
1283
- elsif opts.delete('sq-quote')
1284
- '--sq-quote'
1285
- end
1286
1549
  case flag
1287
1550
  when :commit
1288
1551
  cmd << ((n = size.to_i) > 0 ? basic_option('short', [n, 5].max) : '--verify')
@@ -1290,9 +1553,14 @@ module Squared
1290
1553
  when :branch
1291
1554
  cmd << '--abbrev-ref'
1292
1555
  append_commit(ref, head: true)
1293
- else
1294
- op = OptionPartition.new(opts, OPT_GIT[:rev_parse][flag], cmd, project: self)
1295
- op.append(escape: op.arg?('sq-quote'))
1556
+ when :output
1557
+ if opts.delete('sq-quote')
1558
+ cmd << '--sq-quote'
1559
+ args = true
1560
+ end
1561
+ op = OptionPartition.new(opts, OPT_GIT[:rev_parse], cmd, project: self, no: OPT_GIT[:no][:rev_parse],
1562
+ args: args)
1563
+ op.append(escape: args)
1296
1564
  end
1297
1565
  source(banner: verbosetype > 1)
1298
1566
  end
@@ -1319,18 +1587,10 @@ module Squared
1319
1587
 
1320
1588
  def git(flag, opts = [])
1321
1589
  cmd, opts = git_session(flag, opts: opts)
1322
- op = OptionPartition.new(opts, OPT_GIT[flag], cmd, project: self, no: OPT_GIT[:no][flag])
1323
- sync = false
1324
- stderr = true
1590
+ list = OPT_GIT[:git].fetch(flag, []) + OPT_GIT.fetch(flag, [])
1591
+ op = OptionPartition.new(opts, list, cmd, project: self, no: OPT_GIT[:no][flag],
1592
+ first: flag == :revert ? nil : matchpathspec)
1325
1593
  case flag
1326
- when :clean
1327
- refs = projectmap(op.extras)
1328
- unless refs.empty?
1329
- op << '--'
1330
- op.merge(refs)
1331
- end
1332
- sync = true
1333
- stderr = false
1334
1594
  when :revert
1335
1595
  if VAL_GIT[:rebase][:send].any? { |val| op.arg?(val) }
1336
1596
  op.clear
@@ -1339,14 +1599,29 @@ module Squared
1339
1599
  else
1340
1600
  append_commit(*op.extras)
1341
1601
  end
1602
+ when :add, :clean
1603
+ if flag == :add && op.empty? && !op.arg?('pathspec-from-file')
1604
+ files = []
1605
+ red = color(:red)
1606
+ git_spawn('status -s --porcelain -uall', stdout: false).each do |line|
1607
+ next unless line =~ /^.(.) (.+)$/
1608
+
1609
+ files << "#{sub_style($1, styles: red)} #{$2}"
1610
+ end
1611
+ files = choice_index('Select files', files, multiple: true, force: true, trim: /^\S+\s/, accept: 'Add?')
1612
+ end
1613
+ append_pathspec(files || op.extras)
1614
+ verbose = flag == :add && !op.arg?('verbose')
1615
+ print_success if success?(source) && verbose
1616
+ return
1342
1617
  when :mv
1343
- refs = projectmap(op.extras)
1618
+ refs = projectmap op.extras
1344
1619
  raise_error 'no source/destination' unless refs.size > 1
1345
1620
  op.merge(refs)
1346
1621
  when :rm
1347
- append_pathspec(op.extras, expect: true)
1622
+ append_pathspec(op.extras, expect: !op.arg?('pathspec-from-file'))
1348
1623
  end
1349
- source(sync: sync, stderr: stderr)
1624
+ source(sync: false, stderr: true)
1350
1625
  end
1351
1626
 
1352
1627
  def clone?
@@ -1365,43 +1640,47 @@ module Squared
1365
1640
 
1366
1641
  def source(cmd = @session, exception: true, io: false, sync: true, stdout: false, stderr: false, banner: true,
1367
1642
  multiple: false, sub: nil)
1643
+ cmd = cmd.target if cmd.is_a?(OptionPartition)
1368
1644
  banner = nil if multiple && banner
1369
1645
  if cmd.respond_to?(:done)
1370
1646
  if io && banner == false
1371
1647
  from = nil
1372
- elsif !from && (from = cmd.drop(1).find { |val| val.match?(/^[a-z][a-z\-]{2,}$/) })
1648
+ elsif !from && (from = cmd.drop(1).find { |val| val.match?(/\A[a-z][a-z\-]{2,}\z/) })
1373
1649
  from = :"git:#{from}"
1374
1650
  end
1375
1651
  banner &&= cmd.temp { |val| val.start_with?('--work-tree') || val.start_with?('--git-dir') }
1376
1652
  end
1377
- cmd = session_done(cmd)
1653
+ cmd = session_done cmd
1378
1654
  log&.info cmd
1379
1655
  on :first, from
1380
1656
  banner = if banner
1381
1657
  format_banner((banner.is_a?(String) ? banner : cmd).gsub(File.join(path, ''), ''), banner: true)
1382
1658
  end
1383
1659
  begin
1384
- return [stdout ? `#{cmd}` : IO.popen(cmd), banner, from] if io
1660
+ if io
1661
+ return `#{cmd}` if stdout
1385
1662
 
1663
+ return banner ? [IO.popen(cmd), banner, from] : IO.popen(cmd)
1664
+ end
1386
1665
  if stdin? ? sync : stdout
1387
1666
  print_item banner unless multiple
1388
1667
  ret = `#{cmd}`
1389
1668
  if !ret.empty?
1390
1669
  puts ret
1391
- elsif banner && stdout && !stdin?
1392
- puts 'Success'
1670
+ elsif success?(!banner.nil?)
1671
+ print_success
1393
1672
  end
1394
1673
  elsif sync || (!exception && !stderr)
1395
1674
  print_item banner unless multiple
1396
- shell(cmd, exception: exception)
1675
+ ret = shell(cmd, exception: exception)
1397
1676
  else
1398
1677
  require 'open3'
1399
1678
  if stderr
1400
1679
  Open3.popen3(cmd) do |_, out, err|
1401
- ret = write_lines(out, banner: banner, sub: sub, pass: true)
1402
- if ret == 0
1403
- ret = write_lines(err, banner: banner)
1404
- puts 'Success' if ret == 0 && banner && !stdin?
1680
+ n = write_lines(out, banner: banner, sub: sub, pass: true)
1681
+ if n == 0
1682
+ n = write_lines(err, banner: banner)
1683
+ print_success if success?(n == 0 && !banner.nil?)
1405
1684
  else
1406
1685
  write_lines(err, loglevel: Logger::DEBUG)
1407
1686
  end
@@ -1416,8 +1695,10 @@ module Squared
1416
1695
  raise if exception && ret != true
1417
1696
 
1418
1697
  warn log_message(Logger::WARN, e, pass: true) if warning?
1698
+ nil
1419
1699
  else
1420
1700
  on :last, from
1701
+ ret
1421
1702
  end
1422
1703
  end
1423
1704
 
@@ -1458,7 +1739,7 @@ module Squared
1458
1739
  styles = theme.fetch(:banner, []).reject { |s| s.to_s.end_with?('!') }
1459
1740
  styles << :bold if styles.size <= 1
1460
1741
  puts print_footer("#{size} #{size == 1 ? type.sub(/(?:(?<!l)e)?s\z/, '') : type}",
1461
- sub: { pat: /\A(\d+)(.+)\z/, styles: styles })
1742
+ sub: { pat: /\A(\d+)(.+)\z/m, styles: styles })
1462
1743
  else
1463
1744
  puts empty_status("No #{type} were #{action}", 'grep', grep.is_a?(Array) ? case grep.size
1464
1745
  when 0
@@ -1471,27 +1752,55 @@ module Squared
1471
1752
  on :last, from
1472
1753
  end
1473
1754
 
1474
- def status_digest(*args, algorithm: Digest::SHA256, **kwargs)
1755
+ def choice_refs(msg, *type, format: nil, sort: '-creatordate', count: true, short: true, **kwargs)
1756
+ type << 'heads' if type.empty?
1757
+ unless format
1758
+ format = +"%(refname#{short ? ':short' : ''})"
1759
+ if type.include?('heads') || type.include?('tags')
1760
+ format += '%(if)%(HEAD)%(then) *%(end)'
1761
+ trim = /\s+\*\z/
1762
+ end
1763
+ end
1764
+ args = []
1765
+ args << quote_option('sort', sort) if sort
1766
+ args << shell_option('count', env('GIT_COUNT', ARG[:CHOICE])) if count
1767
+ choice_index(msg, foreachref(type, *args, format: format), trim: trim, **kwargs)
1768
+ end
1769
+
1770
+ def choice_commit(count: true, reflog: true, force: true, **kwargs)
1771
+ kwargs[:attempts] ||= 1 unless force
1772
+ cmd = git_output(reflog && env('GIT_REFLOG') ? 'reflog' : 'log')
1773
+ cmd << quote_option('format', '(%h) %s')
1774
+ cmd << basic_option('max-count', env('GIT_COUNT', ARG[:CHOICE])) if count
1775
+ choice_index('Choose a commit', git_spawn(cmd, stdout: false), column: /\((\w+)\)/, force: force, **kwargs)
1776
+ end
1777
+
1778
+ def choice_remote(force: false, **kwargs)
1779
+ kwargs[:attempts] ||= 1 unless force
1780
+ choice_index('Select a remote', git_spawn('remote', stdout: false), force: force, **kwargs)
1781
+ end
1782
+
1783
+ def status_digest(*args, algorithm: nil, **kwargs)
1784
+ require 'digest'
1785
+ algorithm ||= Digest::SHA256
1475
1786
  glob = kwargs.fetch(:include, [])
1476
1787
  pass = kwargs.fetch(:exclude, [])
1477
1788
  ret = {}
1478
- git_spawn('status -s --porcelain', *args, stdout: false).first.each do |line|
1789
+ git_spawn('status -s --porcelain', *args, stdout: false).each do |line|
1479
1790
  next unless (file = line[/^[A-Z ?!]{3}"?(.+?)"?$/, 1])
1480
1791
  next if !glob.empty? && glob.none? { |val| File.fnmatch?(val, file, File::FNM_DOTMATCH) }
1481
1792
  next if !pass.empty? && pass.any? { |val| File.fnmatch?(val, file, File::FNM_DOTMATCH) }
1482
1793
 
1483
- ret[file] = algorithm.hexdigest(File.read(basepath(file)))
1794
+ ret[file] = algorithm.hexdigest(File.read(path + file))
1484
1795
  end
1485
1796
  ret
1486
1797
  end
1487
1798
 
1488
- def git_spawn(*cmd, stdout: true)
1489
- source(git_output(*cmd), io: true, banner: false, stdout: stdout)
1490
- end
1491
-
1492
1799
  def append_pull(opts, list, target: @session, flag: nil, no: nil, remote: nil)
1493
1800
  target << '--force' if option('force', target: target)
1494
1801
  append_submodules(target: target)
1802
+ return if !remote && opts.empty?
1803
+
1495
1804
  refspec = []
1496
1805
  op = OptionPartition.new(opts, remote ? list + ['refspec=v'] : list, target, project: self, no: no)
1497
1806
  op.each do |opt|
@@ -1543,7 +1852,7 @@ module Squared
1543
1852
  option_clear files
1544
1853
  else
1545
1854
  if files.empty? && (val = option('pathspec', target: target))
1546
- files = split_escape(val)
1855
+ files = split_escape val
1547
1856
  end
1548
1857
  files = projectmap(files, parent: parent, pass: pass)
1549
1858
  if !files.empty?
@@ -1554,12 +1863,13 @@ module Squared
1554
1863
  end
1555
1864
  end
1556
1865
 
1557
- def append_message(val, target: @session)
1558
- target << quote_option('message', val) unless val.to_s.empty?
1866
+ def append_message(val = nil, target: @session)
1867
+ val = messageopt if val.to_s.empty?
1868
+ target << quote_option('message', val) if val
1559
1869
  end
1560
1870
 
1561
1871
  def append_head(val = nil, target: @session)
1562
- return target << val if val
1872
+ return target << shell_quote(val) if val
1563
1873
 
1564
1874
  append_first('head', 'tree-ish', 'object', target: target, flag: false, ignore: false)
1565
1875
  end
@@ -1568,22 +1878,33 @@ module Squared
1568
1878
  return unless (val = option('recurse-submodules', target: target, ignore: false))
1569
1879
 
1570
1880
  if from == :clone
1571
- projectmap(split_escape(val)).each do |path|
1572
- target << basic_option('recurse-submodules', path)
1881
+ case val
1882
+ when '0', 'false'
1883
+ target << '--no-recurse-submodules'
1884
+ when '1', 'true'
1885
+ target << '--recurse-submodules'
1886
+ else
1887
+ projectmap(split_escape(val)).each do |path|
1888
+ target << basic_option('recurse-submodules', path)
1889
+ end
1573
1890
  end
1574
- target
1575
1891
  else
1576
1892
  target << case val
1577
1893
  when 'no', '0', 'false'
1578
1894
  '--no-recurse-submodules'
1579
1895
  when 'yes', 'on-demand'
1580
- "--recurse-submodules#{from == :reset ? '' : "=#{val}"}"
1896
+ "--recurse-submodules=#{val}"
1581
1897
  else
1582
1898
  '--recurse-submodules'
1583
1899
  end
1584
1900
  end
1585
1901
  end
1586
1902
 
1903
+ def foreachref(path, *args, format: nil)
1904
+ path = as_a(path).map! { |val| "refs/#{val}" }
1905
+ git_spawn('for-each-ref', format && quote_option('format', format), *args, *path, stdout: false)
1906
+ end
1907
+
1587
1908
  def git_session(*cmd, opts: nil, worktree: true, **kwargs)
1588
1909
  dir = worktree ? ["--work-tree=#{shell_quote(path)}", "--git-dir=#{shell_quote(gitpath)}"] : []
1589
1910
  return session('git', *dir, *cmd, **kwargs) unless opts
@@ -1597,10 +1918,14 @@ module Squared
1597
1918
  git_session(*cmd, main: false, options: false, **kwargs)
1598
1919
  end
1599
1920
 
1921
+ def git_spawn(*cmd, stdout: true)
1922
+ source(cmd.first.is_a?(Set) ? cmd.first : git_output(*cmd), io: true, banner: false, stdout: stdout)
1923
+ end
1924
+
1600
1925
  def dryrun?(*, target: @session, **)
1601
1926
  return false unless target
1602
1927
 
1603
- target.include?('--dry-run')
1928
+ target.include?('--dry-run') || !option('dry-run', target: target).nil?
1604
1929
  end
1605
1930
 
1606
1931
  def quiet?(target: @session)
@@ -1610,11 +1935,36 @@ module Squared
1610
1935
  end
1611
1936
 
1612
1937
  def gitpath
1613
- basepath '.git'
1938
+ path + '.git'
1939
+ end
1940
+
1941
+ def repotrack(origin, branch, quote: true)
1942
+ i = origin.index('/')
1943
+ branch = "#{branch}:#{origin[i + 1..-1]}" unless origin.end_with?("/#{branch}")
1944
+ ret = [origin[0..i - 1], branch]
1945
+ quote ? ret.map! { |val| shell_quote(val) } : ret
1614
1946
  end
1615
1947
 
1616
1948
  def commithash(val)
1617
- val[/^#\{(\h{5,40})\}$/, 1]
1949
+ val[/:(\h{5,40})\z/, 1] || val[/\A#\{(\h{5,40})\}\z/, 1]
1950
+ end
1951
+
1952
+ def commithead(val)
1953
+ return val unless (s = matchhead(val))
1954
+
1955
+ s.match?(/^\d/) ? "@~#{s}" : "@#{s}"
1956
+ end
1957
+
1958
+ def matchhead(val)
1959
+ val && val =~ /^(?:(?:HEAD|@)([~^]\d*)?|H(\d+))$/ ? $2 || $1 || '' : nil
1960
+ end
1961
+
1962
+ def matchpathspec
1963
+ [/\A[^a-z\d-]+/i, %r{\A[^=\\/*]*[\\/*]}]
1964
+ end
1965
+
1966
+ def messageopt
1967
+ option('message', 'm', prefix: 'git', ignore: false)
1618
1968
  end
1619
1969
 
1620
1970
  def threadargs