squared 0.4.8 → 0.4.10

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.
@@ -27,7 +27,7 @@ module Squared
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
@@ -111,21 +111,30 @@ module Squared
111
111
  time_format(epoch, clock: clock)
112
112
  end
113
113
 
114
- def rev_clear(name)
114
+ def rev_clear(name, sync: true)
115
115
  if Dir.exist?(name) && (proj = find(name))
116
116
  name = proj.name
117
117
  end
118
- rev_write if rev_entry(name, 'revision', val: '', create: false)
118
+ rev_write(sync: sync) if rev_entry(name, 'revision', val: '', create: false)
119
119
  end
120
120
 
121
- def rev_write(name = nil, data = nil, utc: nil)
121
+ def rev_write(name = nil, data = nil, sync: true, utc: nil)
122
122
  return unless @revfile
123
123
 
124
124
  if name
125
125
  data&.each { |key, val| rev_entry(name, key, val: val) }
126
126
  rev_timeutc(name, utc) if utc
127
127
  end
128
- File.write(@revfile, JSON.pretty_generate(@revdoc))
128
+ sleep 0 while !sync && @revlock
129
+ begin
130
+ @revlock = true
131
+ File.write(@revfile, JSON.pretty_generate(@revdoc))
132
+ rescue StandardError => e
133
+ log&.debug e
134
+ warn log_message(Logger::WARN, e, pass: true) if warning?
135
+ ensure
136
+ @revlock = false
137
+ end
129
138
  end
130
139
 
131
140
  def git_clone?(path, name = nil)
@@ -145,115 +154,117 @@ module Squared
145
154
  module Project
146
155
  class Git < Base
147
156
  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
157
+ common: %w[c=q bare glob-pathspecs icase-pathspecs literal-pathspecs no-optional-locks no-pager
158
+ no-replace-objects noglob-pathspecs paginate config-env=q exec-path=p namespace=p].freeze,
159
+ add: %w[A|all e|edit f|force ignore-errors ignore-missing ignore-removal i|interactive no-all
160
+ no-ignore-removal n|dry-run p|patch pathspec-file-nul renormalize sparse u|update v|verbose
161
+ chmod=b pathspec-from-file=p].freeze,
162
+ branch: %w[a|all create-reflog i|ignore-case q|quiet r|remotes v|verbose vv abbrev=i color=b column=b
163
+ contains=b format=q merged=b no-contains=b no-merged=b points-at=b u|set-upstream-to=b sort=q
152
164
  t|track=b].freeze,
153
165
  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,
166
+ pathspec-file-nul q|quiet ours theirs conflict=b orphan=b pathspec-from-file=p t|track=b].freeze,
157
167
  diff: {
158
168
  base: %w[0 1|base 2|ours 3|theirs].freeze,
159
169
  show: %w[s exit-code histogram].freeze
160
170
  }.freeze,
161
171
  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
172
+ base: %w[multiple progress P|prune-tags refetch stdin u|update-head-ok recurse-submodules-default=b].freeze,
173
+ pull: %w[4 6 n t a|append atomic dry-run f|force k|keep negotiate-only prefetch p|prune q|quiet
165
174
  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
175
+ recurse-submodules=v refmap=q o|server-option=q shallow-exclude=b shallow-since=v
167
176
  upload-pack=q].freeze
168
177
  }.freeze,
178
+ git: {
179
+ add: %w[N|intent-to-add refresh].freeze,
180
+ clean: %w[d x X f|force n|dry-run i|interactive q|quiet e|exclude=q].freeze,
181
+ mv: %w[k f|force n|dry-run v|verbose].freeze,
182
+ revert: %w[e S=bm? abort continue n|no-commit quit reference skip cleanup=b gpg-sign=b? m|mainline=i
183
+ s|signoff strategy=b X|strategy-option=b].freeze,
184
+ rm: %w[r cached f|force n|dry-run ignore-unmatch pathspec-file-nul q|quiet sparse v|verbose
185
+ pathspec-from-file=p].freeze
186
+ }.freeze,
169
187
  log: {
170
- base: %w[all all-match alternate-refs author-date-order basic-regexp bisect boundary cherry cherry-mark
188
+ base: %w[L=qm all all-match alternate-refs author-date-order basic-regexp bisect boundary cherry cherry-mark
171
189
  cherry-pick clear-decorations date-order dense do-walk exclude-first-parent-only E|extended-regexp
172
190
  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
191
+ log-size merge no-max-parents no-min-parents not P|perl-regexp reflog i|regexp-ignore-case
174
192
  remove-empty reverse right-only simplify-by-decoration simplify-merges single-worktree show-pulls
175
193
  source sparse stdin topo-order g|walk-reflogs after=q ancestry-path=b? author=q before=q
176
194
  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,
195
+ exclude-hidden=b glob=q grep=q grep-reflog=q n|max-count=i max-parents=i min-parents=i no-walk=b?
196
+ remotes=q? since=q since-as-filter=q skip=i tags=q? until=q].freeze,
179
197
  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?
198
+ show-signature date=q diff-merges=b encoding=b expand-tabs=i format=q notes=b pretty=q?
181
199
  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
200
+ 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
201
+ find-copies-harder full-index W|function-context w|ignore-all-space ignore-blank-lines
202
+ ignore-cr-at-eol ignore-space-at-eol b|ignore-space-change D|irreversible-delete graph
203
+ ita-invisible-in-index minimal name-only name-status no-color-moved-ws no-prefix no-renames numstat
204
+ patch-with-raw patch-with-stat patience pickaxe-all pickaxe-regex raw shortstat summary a|text
205
+ abbrev=i? anchored=q break-rewrites=b? color=b color-moved=b color-moved-ws=b color-words=q?
206
+ diff-algorithm=b diff-filter=e? X|dirstat=b? dirstat-by-file=b? dst-prefix=q find-copies=i?
207
+ find-object=b find-renames=b? ignore-matching-lines=q ignore-submodules=b? inter-hunk-context=i
208
+ line-prefix=q output=p output-indicator-context=q output-indicator-new=q output-indicator-old=q
209
+ relative=p rotate-to=p skip-to=p src-prefix=q stat=b? stat-count=i stat-width=i stat-name-width=i
210
+ submodule=b? unified=i word-diff=b? word-diff-regex=q ws-error-highlight=b].freeze
193
211
  }.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
212
+ ls_files: %w[f t v z debug deduplicate directory eol error-unmatch exclude-standard full-name i|ignored
213
+ k|killed no-empty-directory recurse-submodules sparse s|stage u|unmerged abbrev=i x|exclude=q
196
214
  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
215
+ ls_remote: %w[exit-code get-url q|quiet symref o|server-option=q sort=q upload-pack=q].freeze,
216
+ 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?
217
+ into-name=b log=i s|strategy=b X|strategy-option=b].freeze,
218
+ 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
219
  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,
220
+ rebase: %w[n C=im S=bm? allow-empty-message apply committer-date-is-author-date edit-todo empty=b
221
+ f|force-rebase ignore-date ignore-whitespace i|interactive keep-base m|merge no-ff q|quiet quit
222
+ reset-author-date root show-current-patch signoff v|verbose empty=b x|exec=q gpg-sign=b? onto=b
223
+ r|rebase-merges=b s|strategy=b X|strategy-option=b whitespace=b].freeze,
207
224
  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,
225
+ restore: %w[ignore-skip-worktree-bits ignore-unmerged m|merge ours p|patch pathspec-file-nul q|quiet S|staged
226
+ theirs W|worktree conflict=b pathspec-from-file=p s|source=b].freeze,
227
+ rev_parse: %w[absolute-git-dir all git-common-dir git-dir is-bare-repository is-inside-git-dir
228
+ is-inside-work-tree is-shallow-repository local-env-vars no-revs not q|quiet revs-only
229
+ shared-index-path show-cdup show-prefix show-superproject-working-tree show-toplevel sq sq-quote
230
+ symbolic symbolic-full-name verify abbrev-ref=b? after=q before=q branches=q? default=q
231
+ disambiguate=b exclude=q exclude-hidden=b git-path=p glob=q path-format=b? prefix=q remotes=q?
232
+ resolve-git-dir=p short=i? show-object-format=b? since=q tags=q? until=q].freeze,
223
233
  show: %w[t combined-all-paths no-diff-merges remerge-diff show-signature diff-merges=b encoding=b
224
234
  expand-tabs=i notes=q show-notes=q?].freeze,
225
235
  stash: {
226
236
  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,
237
+ push: %w[a|all u|include-untracked k|keep-index no-keep-index no-include-untracked pathspec-file-nul
238
+ p|patch S|staged m|message=q pathspec-from-file=p].freeze,
229
239
  pop: %w[index].freeze,
230
240
  apply: %w[index].freeze
231
241
  }.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,
242
+ status: %w[u|ignore-submodules=bm? ignored=b? untracked-files=b?],
243
+ tag: %w[n=im cleanup=b create-reflog i|ignore-case color=b? column=b contains=b? format=q merged=b?
244
+ no-contains=b? no-merged=b? points-at=q sort=q].freeze,
235
245
  no: {
246
+ branch: %w[color color-moved column track].freeze,
247
+ checkout: %w[overwrite-ignore guess overlay progress recurse-submodules track].freeze,
236
248
  fetch: {
237
249
  base: %w[auto-gc auto-maintenance write-commit-graph write-fetch-head].freeze,
238
250
  pull: %w[all ipv4 ipv6 recurse-submodules show-forced-updates tags].freeze
239
251
  },
240
252
  log: {
241
253
  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
254
+ diff: %w[color color-moved ext-diff indent-heuristic patch relative rename-empty textconv].freeze,
255
+ show: %w[abbrev-commit expand-tabs notes].freeze
244
256
  }.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
257
+ merge: %w[autostash edit ff gpg-sign log overwrite-ignore progress rerere-autoupdate signoff squash stat
250
258
  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,
259
+ pull: %w[autostash commit edit gpg-sign ff log rebase signoff squash stat verify verify-signatures].freeze,
260
+ rebase: %w[autosquash autostash fork-point gpg-sign keep-empty reapply-cherry-picks rebase-merges
261
+ rerere-autoupdate reschedule-failed-exec stat update-refs verify].freeze,
253
262
  reset: %w[refresh].freeze,
254
263
  restore: %w[overlay progress recurse-submodules].freeze,
264
+ rev_parse: %w[flags].freeze,
255
265
  revert: %w[edit gpg-sign rerere-autoupdate].freeze,
256
- show: %w[standard-notes].freeze
266
+ show: %w[standard-notes].freeze,
267
+ tag: %w[column].freeze
257
268
  }.freeze
258
269
  }.freeze
259
270
  VAL_GIT = {
@@ -272,7 +283,7 @@ module Squared
272
283
  include Rake::DSL
273
284
 
274
285
  def populate(ws, **)
275
- return if ws.series[:pull].empty?
286
+ return if ws.series.exclude?(:pull, true)
276
287
 
277
288
  namespace(name = ws.task_name('git')) do
278
289
  all = ws.task_join(name, 'all')
@@ -308,23 +319,24 @@ module Squared
308
319
  end
309
320
 
310
321
  subtasks({
311
- 'branch' => %i[create set delete move copy list edit current].freeze,
322
+ 'branch' => %i[create track delete move copy list current].freeze,
312
323
  'checkout' => %i[commit branch track detach path].freeze,
313
- 'commit' => %i[add all amend amend-orig].freeze,
324
+ 'commit' => %i[add all amend amend-orig fixup].freeze,
314
325
  'diff' => %i[head branch files view between contain].freeze,
315
326
  'fetch' => %i[origin remote].freeze,
316
- 'files' => %i[cached modified deleted others ignored].freeze,
317
- 'git' => %i[clean mv revert rm].freeze,
327
+ 'files' => %i[cached modified deleted others].freeze,
328
+ 'git' => %i[add clean mv revert rm].freeze,
318
329
  'log' => %i[view between contain].freeze,
319
330
  'merge' => %i[commit no-commit send].freeze,
320
331
  'pull' => %i[origin remote].freeze,
321
332
  'rebase' => %i[branch onto send].freeze,
322
333
  'refs' => %i[heads tags remote].freeze,
323
334
  'reset' => %i[commit index patch mode].freeze,
324
- 'restore' => %i[staged worktree].freeze,
325
- 'rev' => %i[commit branch output parseopt build].freeze,
335
+ 'restore' => %i[source staged worktree].freeze,
336
+ 'rev' => %i[commit build output].freeze,
326
337
  'show' => %i[format oneline textconv].freeze,
327
338
  'stash' => %i[push pop apply drop clear list].freeze,
339
+ 'switch' => %i[create detach merge].freeze,
328
340
  'tag' => %i[add sign delete list].freeze
329
341
  })
330
342
 
@@ -350,10 +362,15 @@ module Squared
350
362
  case action
351
363
  when 'pull', 'fetch'
352
364
  if flag == :remote
353
- format_desc(action, flag, 'remote,opts*')
365
+ format_desc action, flag, 'remote,opts*'
354
366
  task flag, [:remote] do |_, args|
355
- remote = param_guard(action, flag, args: args, key: :remote)
356
- __send__(action, flag, args.extras, remote: remote)
367
+ if (remote = args.remote)
368
+ args = args.extras
369
+ else
370
+ remote = choice_remote
371
+ args = args.to_a.drop(1)
372
+ end
373
+ __send__(action, flag, args, remote: remote)
357
374
  end
358
375
  else
359
376
  format_desc action, flag, 'opts*'
@@ -366,13 +383,39 @@ module Squared
366
383
  when :all
367
384
  format_desc action, flag, 'message?'
368
385
  task flag, [:message] do |_, args|
369
- commit(flag, message: args.fetch(:message, nil))
386
+ commit(flag, message: args.message)
370
387
  end
371
388
  else
372
- format_desc action, flag, 'pathspec+'
389
+ format_desc(action, flag, 'pathspec+', before: flag == :add ? 'opts*' : nil)
373
390
  task flag do |_, args|
374
- refs = param_guard(action, flag, args: args.to_a)
375
- commit(flag, refs: refs)
391
+ if flag == :fixup
392
+ ref, squash, pick = choice_commit(accept: [['Auto squash?', true]], reflog: false,
393
+ values: ['Pick [amend|reword]'])
394
+ pick = case pick&.downcase
395
+ when 'a', 'amend'
396
+ 'amend'
397
+ when 'r', 'reword'
398
+ 'reword'
399
+ end
400
+ if squash
401
+ found = false
402
+ git_spawn(git_output('log --format=%h'), stdout: false).each do |val|
403
+ if found
404
+ squash = val.chomp
405
+ break
406
+ end
407
+ found = val.chomp == ref
408
+ end
409
+ end
410
+ end
411
+ refs = pick == 'reword' ? [] : param_guard(action, flag, args: args.to_a)
412
+ if flag == :add
413
+ opts = refs
414
+ refs = []
415
+ else
416
+ opts = []
417
+ end
418
+ commit(flag, opts, refs: refs, ref: ref, squash: squash, pick: pick)
376
419
  end
377
420
  end
378
421
  when 'tag'
@@ -383,65 +426,105 @@ module Squared
383
426
  tag flag, args.to_a
384
427
  end
385
428
  when :delete
386
- format_desc action, flag, 'name+?'
429
+ format_desc action, flag, 'name+'
387
430
  task flag do |_, args|
388
- if (refs = args.to_a).empty?
389
- refs = choice_refs('Choose a tag', 'tags', multiple: true, accept: 'Delete?')
431
+ refs = args.to_a
432
+ if refs.empty?
433
+ refs = choice_refs('Choose a tag', 'tags', multiple: true, accept: 'Delete?', series: true)
434
+ remote = choice_remote
390
435
  end
391
- tag(flag, refs: refs)
436
+ tag(flag, refs: refs, remote: remote)
392
437
  end
393
438
  when :add, :sign
394
- format_desc action, flag, 'name,message?,commit?'
395
- task flag, [:name, :message, :commit] do |_, args|
396
- name = param_guard(action, flag, args: args, key: :name)
397
- tag(flag, refs: [name], message: args.message, commit: args.commit)
439
+ format_desc action, flag, 'name,message?,commit?,remote?'
440
+ task flag, [:name, :message, :commit, :remote] do |_, args|
441
+ if (name = args.name)
442
+ message = args.message
443
+ commit = commithead args.commit
444
+ remote = args.remote
445
+ else
446
+ commit, name, message = choice_commit(values: [['Enter tag name', true], 'Enter message'],
447
+ series: true, reflog: false)
448
+ remote = choice_remote
449
+ end
450
+ ret = tag(flag, refs: [name], message: message, commit: commit, remote: remote)
451
+ print_success if success?(ret) && !remote
398
452
  end
399
453
  end
400
454
  when 'stash'
401
- if flag == :list
402
- format_desc action, flag
403
- else
404
- format_desc(action, flag, '*opts', after: flag == :push ? 'pathspec*' : 'commit?')
405
- end
455
+ format_desc(action, flag, 'opts*', after: case flag
456
+ when :push then 'pathspec*'
457
+ when :list then nil
458
+ else 'commit?' end)
406
459
  task flag do |_, args|
407
460
  stash flag, args.to_a
408
461
  end
409
462
  when 'log', 'diff'
410
463
  case flag
411
464
  when :view, :between, :contain
412
- if flag == :view && action == 'log'
413
- format_desc action, flag, '(^)commit/H0*,opts*,pathspec*'
465
+ view = flag == :view
466
+ if view && action == 'log'
467
+ format_desc action, flag, '(^)commit*|:,opts*,ref?,pathspec*'
414
468
  task flag do |_, args|
415
- index = []
416
- args.to_a.each do |val|
417
- if val =~ /^H(\d+)$/
418
- index << "HEAD~#{$1}"
419
- elsif (sha = commithash(val))
420
- index << sha
421
- elsif val.start_with?('^')
422
- index << shell_quote(val)
469
+ args = args.to_a
470
+ if args.first == ':'
471
+ args.shift
472
+ index = choice_commit(multiple: true)
473
+ else
474
+ index = []
475
+ args.each do |val|
476
+ if matchhead(val)
477
+ index << commithead(val)
478
+ elsif (sha = commithash(val))
479
+ index << sha
480
+ elsif val.start_with?('^')
481
+ index << shell_quote(val)
482
+ else
483
+ break
484
+ end
423
485
  end
486
+ args = args.drop(index.size)
424
487
  end
425
- log!(flag, args.to_a.drop(index.size), index: index)
488
+ log!(flag, args, index: index)
426
489
  end
427
490
  else
428
491
  format_desc action, flag, 'commit1,commit2,opts*,pathspec*'
429
492
  task flag, [:commit1, :commit2] do |_, args|
430
- commit1 = param_guard(action, flag, args: args, key: :commit1)
431
- commit2 = param_guard(action, flag, args: args, key: :commit2)
432
- __send__(action == 'log' ? :log! : :diff, flag, args.extras, range: [commit1, commit2])
493
+ commit1 = commithead args.commit1
494
+ if commit1
495
+ commit2 = commithead param_guard(action, flag, args: args, key: :commit2)
496
+ args = args.extras.to_a
497
+ range = [commit1, commit2]
498
+ else
499
+ range, opts, refs = choice_commit(multiple: view ? true : 2, values: %w[Options Pathspec])
500
+ range = range.reverse
501
+ args = OptionPartition.strip(opts)
502
+ args.concat(refs.shellsplit) if refs
503
+ end
504
+ __send__(action == 'log' ? :log! : :diff, flag, args, range: range)
433
505
  end
434
506
  end
435
507
  when :head
436
- format_desc action, flag, 'commit/H0*,opts*,pathspec*'
508
+ format_desc action, flag, 'commit*|:,opts*,pathspec*'
437
509
  task flag do |_, args|
438
- index = []
439
- args.to_a.each do |val|
440
- break unless val =~ /^H(\d+)$/ || (sha = commithash(val))
441
-
442
- index << ($1 ? "HEAD~#{$1}" : sha)
510
+ args = args.to_a
511
+ if args.first == ':'
512
+ args.shift
513
+ index = choice_commit(multiple: true)
514
+ else
515
+ index = []
516
+ args.each do |val|
517
+ if matchhead(val)
518
+ index << commithead(val)
519
+ elsif (sha = commithash(val))
520
+ index << sha
521
+ else
522
+ break
523
+ end
524
+ end
525
+ args = args.drop(index.size)
443
526
  end
444
- diff(flag, args.to_a.drop(index.size), index: index)
527
+ diff(flag, args, index: index)
445
528
  end
446
529
  when :branch
447
530
  format_desc action, flag, 'name,opts*,pathspec*'
@@ -460,7 +543,7 @@ module Squared
460
543
  when 'checkout'
461
544
  case flag
462
545
  when :branch
463
- format_desc action, flag, 'name?,create?=[bB],commit?,detach?=d'
546
+ format_desc action, flag, 'name,create?=[bB],commit?,detach?=d'
464
547
  task flag, [:name, :create, :commit, :detach] do |_, args|
465
548
  if (branch = args.name)
466
549
  branch = param_guard(action, flag, args: args, key: :name)
@@ -473,21 +556,21 @@ module Squared
473
556
  commit = nil
474
557
  detach = 'd'
475
558
  elsif create && create.size > 1
476
- commit = create
559
+ commit = commithead create
477
560
  create = nil
478
561
  detach = args.commit
479
562
  else
480
563
  detach = args.detach
481
- commit = args.commit
564
+ commit = commithead args.commit
482
565
  end
483
566
  param_guard(action, flag, args: { create: create }, key: :create, pat: /\Ab\z/i) if create
484
567
  else
485
- branch = choice_refs 'Choose a branch to switch', 'heads'
568
+ branch = choice_refs 'Choose a branch to switch'
486
569
  end
487
570
  checkout(flag, branch: branch, create: create, commit: commit, detach: detach)
488
571
  end
489
572
  when :track
490
- format_desc action, flag, 'origin?,(^)name?'
573
+ format_desc action, flag, 'origin,(^)name?'
491
574
  task flag, [:origin, :name] do |_, args|
492
575
  if (origin = args.origin)
493
576
  branch = args.name
@@ -497,15 +580,26 @@ module Squared
497
580
  checkout(flag, branch: branch, origin: origin)
498
581
  end
499
582
  when :commit
500
- format_desc action, flag, 'branch/commit,opts*'
583
+ format_desc action, flag, 'ref,opts*'
501
584
  task flag, [:commit] do |_, args|
502
- commit = param_guard(action, flag, args: args, key: :commit)
503
- checkout(flag, commit: commit)
585
+ commit = commithead args.commit
586
+ if commit
587
+ args = args.extras
588
+ else
589
+ commit, opts = choice_commit(values: ['Options'])
590
+ args = OptionPartition.strip(opts)
591
+ end
592
+ checkout(flag, args, commit: commit)
504
593
  end
505
594
  when :detach
506
- format_desc action, flag, 'branch/commit?'
595
+ format_desc action, flag, 'ref?'
507
596
  task flag, [:commit] do |_, args|
508
- checkout(flag, commit: args.commit)
597
+ commit = commithead args.commit
598
+ unless commit
599
+ commit, merge = choice_commit(values: ['Merge? [y|N]'])
600
+ merge = merge&.upcase == 'Y'
601
+ end
602
+ checkout(flag, commit: commit, merge: merge)
509
603
  end
510
604
  when :path
511
605
  format_desc action, flag, 'opts*,pathspec*'
@@ -516,33 +610,46 @@ module Squared
516
610
  when 'branch'
517
611
  case flag
518
612
  when :create
519
- format_desc action, flag, 'name,ref?=HEAD'
613
+ format_desc action, flag, 'name,ref?=HEAD|:'
520
614
  task flag, [:name, :ref] do |_, args|
521
615
  target = param_guard(action, flag, args: args, key: :name)
522
- branch(flag, target: target, ref: args.ref)
616
+ ref = commithead args.ref
617
+ ref, remote = choice_refs('Choose a remote', 'remotes', accept: [['Push?', true]]) if ref == ':'
618
+ branch(flag, target: target, ref: ref, remote: remote)
523
619
  end
524
- when :set
525
- format_desc(action, flag, '(^)upstream?,name?')
620
+ when :track
621
+ format_desc action, flag, '(^~)upstream?,name?'
526
622
  task flag, [:upstream, :name] do |_, args|
527
623
  if (ref = args.upstream)
528
624
  target = args.name
625
+ if ref.start_with?('~')
626
+ ref = ref[1..-1]
627
+ remote = true
628
+ end
529
629
  else
530
- ref, target = choice_refs('Choose a remote', 'remotes', values: ['Enter branch name'])
630
+ ref, remote, target = choice_refs('Choose a remote', 'remotes', accept: [['Push?', true]],
631
+ values: ['Enter branch name'])
531
632
  end
532
- branch(flag, target: target, ref: ref)
633
+ branch(flag, target: target, ref: ref, remote: remote)
533
634
  end
534
635
  when :delete
535
- format_desc action, flag, '(^~)name+?'
636
+ format_desc action, flag, '(^~)name*,:?'
536
637
  task flag do |_, args|
537
- if (refs = args.to_a).empty?
538
- refs = choice_refs('Choose a branch', 'heads', multiple: true, accept: 'Delete?')
638
+ refs = args.to_a
639
+ if refs.empty? || (r = refs.last == ':')
640
+ accept = ['Delete?']
641
+ accept << ['Force?', true] unless r
642
+ remote = choice_refs('Choose a branch', r ? 'remotes' : 'heads', multiple: true,
643
+ accept: accept)
644
+ if r
645
+ refs.pop
646
+ else
647
+ refs = remote.first
648
+ refs.map! { |val| "^#{val}" } if remote[1]
649
+ remote = nil
650
+ end
539
651
  end
540
- branch(flag, refs: refs)
541
- end
542
- when :edit
543
- format_desc action, flag, 'name?'
544
- task flag, [:name] do |_, args|
545
- branch(flag, target: args.name)
652
+ branch(flag, refs: refs, remote: remote)
546
653
  end
547
654
  when :list
548
655
  format_desc action, flag, 'opts*,pattern*'
@@ -557,17 +664,55 @@ module Squared
557
664
  else
558
665
  format_desc action, flag, 'branch,oldbranch?'
559
666
  task flag, [:branch, :oldbranch] do |_, args|
560
- branch = param_guard(action, flag, args: args, key: :branch)
561
- branch(flag, refs: [args.oldbranch, branch])
667
+ if (branch = args.branch)
668
+ oldbranch = args.oldbranch
669
+ else
670
+ oldbranch, branch = choice_refs("Choose a branch to #{flag}",
671
+ values: [['Enter new branch name', true]])
672
+ end
673
+ branch(flag, refs: [oldbranch, branch])
674
+ end
675
+ end
676
+ when 'switch'
677
+ case flag
678
+ when :create
679
+ format_desc action, flag, '(^)name,ref?=HEAD|:'
680
+ task flag, [:name, :commit] do |_, args|
681
+ target = param_guard(action, flag, args: args, key: :name)
682
+ commit = commithead args.commit
683
+ commit, track = choice_commit(values: ['Track? [Y|n]'], force: false) if commit == ':'
684
+ switch(flag, target: target, commit: commit, track: track)
685
+ end
686
+ when :detach
687
+ format_desc action, flag, 'ref?=HEAD'
688
+ task flag, [:commit] do |_, args|
689
+ commit = commithead(args.commit) || choice_commit(force: false)
690
+ switch(flag, commit: commit)
691
+ end
692
+ when :merge
693
+ format_desc action, flag, 'branch?'
694
+ task flag, [:branch] do |_, args|
695
+ commit = args.branch || choice_refs('Choose a branch')
696
+ switch(flag, commit: commit)
562
697
  end
563
698
  end
564
699
  when 'reset'
565
700
  case flag
566
701
  when :commit
567
- format_desc action, flag, 'branch/commit,opts*'
702
+ format_desc action, flag, 'ref|:,opts*'
568
703
  task flag, [:commit] do |_, args|
569
- commit = param_guard(action, flag, args: args, key: :commit)
570
- reset(flag, args.extras, commit: commit)
704
+ commit = commithead args.commit
705
+ if commit && commit != ':'
706
+ args = args.extras
707
+ else
708
+ commit, mode = choice_commit(values: ['Mode [mixed|soft|hard|N]'])
709
+ args = args.extras.to_a.concat(case mode&.downcase
710
+ when 'h', 'hard' then ['hard']
711
+ when 's', 'soft' then ['soft']
712
+ when 'n', 'N' then ['mixed', 'N']
713
+ else ['mixed'] end)
714
+ end
715
+ print_success if success?(reset(flag, args, commit: commit))
571
716
  end
572
717
  when :index
573
718
  format_desc action, flag, 'opts*,pathspec*'
@@ -575,15 +720,18 @@ module Squared
575
720
  reset flag, args.to_a
576
721
  end
577
722
  when :mode
578
- format_desc action, flag, 'mode,ref?=HEAD'
723
+ format_desc action, flag, 'mode,ref?=HEAD|:'
579
724
  task flag, [:mode, :ref] do |_, args|
580
725
  mode = param_guard(action, flag, args: args, key: :mode)
581
- reset(flag, mode: mode, ref: args.ref)
726
+ ref = commithead args.ref
727
+ ref = choice_commit(reflog: false) if ref == ':'
728
+ reset(flag, mode: mode, ref: ref)
582
729
  end
583
730
  when :patch
584
- format_desc action, flag, 'ref,pathspec*'
731
+ format_desc action, flag, 'ref?=HEAD|:,pathspec*'
585
732
  task flag, [:ref] do |_, args|
586
- ref = param_guard(action, flag, args: args, key: :ref)
733
+ ref = commithead args.ref
734
+ ref = choice_commit(reflog: false) unless ref && ref != ':'
587
735
  reset(flag, refs: args.extras, ref: ref)
588
736
  end
589
737
  end
@@ -592,7 +740,7 @@ module Squared
592
740
  when :oneline
593
741
  format_desc action, flag, 'opts*,object*'
594
742
  task flag do |_, args|
595
- show flag, args.to_a.push('abbrev-commit')
743
+ show flag, args.to_a
596
744
  end
597
745
  when :format
598
746
  format_desc action, flag, 'format?,opts*,object*'
@@ -609,23 +757,42 @@ module Squared
609
757
  when 'rebase', 'merge'
610
758
  case flag
611
759
  when :branch
612
- format_desc action, flag, 'opts*,upstream?,branch?'
613
- task flag do |_, args|
614
- args = param_guard(action, flag, args: args.to_a)
615
- rebase flag, args
760
+ format_desc action, flag, 'upstream,branch?=HEAD,opts*'
761
+ task flag, [:upstream] do |_, args|
762
+ if (upstream = args.upstream)
763
+ args = args.extras
764
+ else
765
+ upstream, opts = choice_refs('Choose upstream branch', values: ['Options'])
766
+ args = OptionPartition.strip(opts)
767
+ end
768
+ rebase(flag, args, upstream: upstream)
616
769
  end
617
770
  when :onto
618
- format_desc action, flag, 'branch/commit,upstream,branch?=HEAD'
771
+ format_desc action, flag, 'ref,upstream,branch?=HEAD'
619
772
  task flag, [:commit, :upstream, :branch] do |_, args|
620
- commit = param_guard(action, flag, args: args, key: :commit)
621
- upstream = param_guard(action, flag, args: args, key: :upstream)
622
- rebase(flag, commit: commit, upstream: upstream, branch: args.branch)
773
+ commit = commithead args.commit
774
+ if commit
775
+ upstream = param_guard(action, flag, args: args, key: :upstream)
776
+ branch = args.branch
777
+ args = []
778
+ else
779
+ commit = choice_refs 'Choose "onto" branch'
780
+ target, opts = choice_commit(multiple: 2, values: ['Options'], reflog: false)
781
+ branch, upstream = target
782
+ args = OptionPartition.strip(opts)
783
+ end
784
+ rebase(flag, args, commit: commit, upstream: upstream, branch: branch)
623
785
  end
624
786
  when :commit, :'no-commit'
625
- format_desc action, flag, 'branch/commit+,opts*'
787
+ format_desc action, flag, 'refs+,opts*'
626
788
  task flag do |_, args|
627
- args = param_guard(action, flag, args: args.to_a)
628
- merge flag, args
789
+ args = args.to_a
790
+ if args.empty?
791
+ accept = "Merge with #{`#{git_output('branch --show-current')}`.chomp}?"
792
+ branch, opts = choice_refs('Choose a branch', values: ['Options'], accept: accept)
793
+ args = OptionPartition.strip(opts)
794
+ end
795
+ merge(flag, args, branch: branch)
629
796
  end
630
797
  when :send
631
798
  format_desc(action, flag, VAL_GIT[action.to_sym][:send], arg: nil)
@@ -640,25 +807,20 @@ module Squared
640
807
  when :commit
641
808
  format_desc action, flag, 'ref?=HEAD,size?'
642
809
  task flag, [:ref, :size] do |_, args|
643
- ref = args.ref
810
+ ref = commithead args.ref
644
811
  size = args.size
645
- if !size && ref.to_i > 0
812
+ if !size && ref.to_i.between?(1, 40)
646
813
  size = ref
647
814
  ref = nil
648
815
  end
649
816
  rev_parse(flag, ref: ref, size: size)
650
817
  end
651
- when :branch
652
- format_desc action, flag, 'ref?=HEAD'
653
- task flag, [:ref] do |_, args|
654
- rev_parse(flag, ref: args.ref)
655
- end
656
818
  when :build
657
819
  format_desc action, flag, OPT_GIT[:status]
658
820
  task flag do |_, args|
659
821
  revbuild flag, args.to_a
660
822
  end
661
- else
823
+ when :output
662
824
  format_desc action, flag, 'opts*,args*'
663
825
  task flag do |_, args|
664
826
  rev_parse flag, args.to_a
@@ -666,10 +828,9 @@ module Squared
666
828
  end
667
829
  when 'refs', 'files'
668
830
  if flag == :remote
669
- format_desc action, flag, 'remote,opts*,pattern*'
831
+ format_desc action, flag, 'remote?,opts*,pattern*'
670
832
  task flag, [:remote] do |_, args|
671
- remote = param_guard(action, flag, args: args, key: :remote)
672
- ls_remote(flag, args.extras, remote: remote)
833
+ ls_remote(flag, args.extras, remote: args.remote)
673
834
  end
674
835
  else
675
836
  format_desc action, flag, 'opts*,pattern*'
@@ -677,17 +838,54 @@ module Squared
677
838
  __send__(action == 'refs' ? :ls_remote : :ls_files, flag, args.to_a)
678
839
  end
679
840
  end
680
- when 'restore', 'git'
681
- format_desc(action, flag, 'opts*', before: case flag
682
- when :rm
683
- 'source+,destination'
684
- when :revert
685
- 'commit+'
686
- else
687
- 'pathspec*'
688
- end)
841
+ when 'restore'
842
+ case flag
843
+ when :source
844
+ format_desc action, flag, 'ref,opts*,pathspec*'
845
+ task flag, [:commit] do |_, args|
846
+ commit = commithead args.commit
847
+ if commit
848
+ args = args.extras
849
+ else
850
+ commit, opts, files = choice_commit(values: ['Options', ['Pathspec', true]])
851
+ args = OptionPartition.strip(opts)
852
+ files = files&.shellsplit
853
+ end
854
+ restore(flag, args, commit: commit, files: files)
855
+ end
856
+ when :staged, :worktree
857
+ format_desc action, flag, 'opts*,pathspec*,:?'
858
+ task flag do |_, args|
859
+ args = args.to_a
860
+ if args.empty? || args.last == ':'
861
+ files = []
862
+ status_data.each do |line|
863
+ case (flag == :staged ? line[2] : line[1])
864
+ when /[AMDRTC]/
865
+ files << line[0]
866
+ end
867
+ end
868
+ unless files.empty?
869
+ files = choice_index('Select a file', files, multiple: true, force: false,
870
+ accept: 'Restore?')
871
+ end
872
+ args.pop
873
+ args, glob = args.partition { |val| val.match?(/^(?:[a-z-]+=|[^*]+$)/) }
874
+ (files ||= []).concat(glob)
875
+ next if args.empty? && files.empty?
876
+ end
877
+ restore(flag, args, files: files)
878
+ end
879
+ end
880
+ when 'git'
881
+ before = case flag
882
+ when :mv then 'source+,destination'
883
+ when :revert then 'commit+' end
884
+ format_desc(action, flag, 'opts*', before: before, after: case flag
885
+ when :add then 'pathspec*|:pattern:*'
886
+ when :clean, :rm then 'pathspec*' end)
689
887
  task flag do |_, args|
690
- __send__(action, flag, args.to_a)
888
+ git flag, args.to_a
691
889
  end
692
890
  end
693
891
  end
@@ -701,13 +899,13 @@ module Squared
701
899
  super
702
900
  end
703
901
 
704
- def depend(*, **)
705
- workspace.rev_clear(name)
902
+ def depend(*, sync: invoked_sync?('depend'), **)
903
+ workspace.rev_clear(name, sync: sync)
706
904
  super
707
905
  end
708
906
 
709
- def clean(*, **)
710
- workspace.rev_clear(name)
907
+ def clean(*, sync: invoked_sync?('clean'), **)
908
+ workspace.rev_clear(name, sync: sync)
711
909
  super
712
910
  end
713
911
 
@@ -728,7 +926,7 @@ module Squared
728
926
  end
729
927
  end
730
928
  append_pull(opts, OPT_GIT[:pull] + OPT_GIT[:fetch][:pull],
731
- no: OPT_GIT[:no][:pull] + OPT_GIT[:no][:fetch][:pull], remote: remote, flag: flag)
929
+ no: OPT_GIT[:no][:pull] + OPT_GIT[:no][:fetch][:pull], remote: remote, flag: flag, from: :pull)
732
930
  source(sync: sync, sub: if verbose
733
931
  [
734
932
  { pat: /^(.+)(\|\s+\d+\s+)([^-]*)(-+)(.*)$/, styles: color(:red), index: 4 },
@@ -744,16 +942,12 @@ module Squared
744
942
  cmd, opts = git_session('rebase', opts: opts)
745
943
  case flag
746
944
  when :branch
945
+ return unless upstream
946
+
747
947
  op = OptionPartition.new(opts, OPT_GIT[:rebase], cmd, project: self, no: OPT_GIT[:no][:rebase])
748
- case op.size
749
- when 0
750
- append_head
751
- when 1, 2
752
- op.append(delim: true)
753
- else
754
- op.append(op.pop(2), delim: true)
755
- .clear(pass: false)
756
- end
948
+ cmd << shell_escape(upstream)
949
+ append_head op.shift
950
+ op.clear(pass: false)
757
951
  when :onto
758
952
  return unless upstream
759
953
 
@@ -774,11 +968,9 @@ module Squared
774
968
  end
775
969
 
776
970
  def fetch(flag = nil, opts = [], sync: invoked_sync?('fetch', flag), remote: nil)
777
- cmd, opts = git_session('fetch', opts: opts)
778
- append_pull(opts, collect_hash(OPT_GIT[:fetch]),
779
- no: collect_hash(OPT_GIT[:no][:fetch]), remote: remote, flag: flag)
780
- cmd << '--all' if !remote && !session_arg?('multiple') && option('all')
781
- cmd << '--verbose' if verbose && !session_arg?('quiet')
971
+ opts = git_session('fetch', opts: opts).last
972
+ append_pull(opts, collect_hash(OPT_GIT[:fetch]), no: collect_hash(OPT_GIT[:no][:fetch]),
973
+ remote: remote, flag: flag, from: :fetch)
782
974
  source(sync: sync, **threadargs)
783
975
  end
784
976
 
@@ -797,7 +989,7 @@ module Squared
797
989
  opts[:origin] = val if (val = option('origin', ignore: false))
798
990
  opts[:branch] = val if (val = option('branch', strict: true))
799
991
  opts[:local] = val != '0' if (val = option('local', strict: true))
800
- opts.delete(:'recurse-submodules') || opts.delete(:'no-recurse-submodules') if append_submodules(:clone)
992
+ opts.delete(:'recurse-submodules') || opts.delete(:'no-recurse-submodules') if append_submodules(from: :clone)
801
993
  append_hash opts
802
994
  cmd << '--quiet' unless verbose
803
995
  append_value(data[0], path, delim: true)
@@ -807,8 +999,12 @@ module Squared
807
999
  def stash(flag = nil, opts = [], sync: invoked_sync?('stash', flag))
808
1000
  if flag
809
1001
  cmd, opts = git_session('stash', flag, opts: opts)
810
- op = OptionPartition.new(opts, OPT_GIT[:stash][:common] + OPT_GIT[:stash].fetch(flag, []), cmd,
811
- project: self)
1002
+ list = OPT_GIT[:stash][:common] + OPT_GIT[:stash].fetch(flag, [])
1003
+ if flag == :list
1004
+ list += collect_hash(OPT_GIT[:log])
1005
+ no = collect_hash(OPT_GIT[:no][:log])
1006
+ end
1007
+ op = OptionPartition.new(opts, list, cmd, project: self, no: no, first: flag == :push ? matchpathspec : nil)
812
1008
  case flag
813
1009
  when :push
814
1010
  append_pathspec op.extras
@@ -823,6 +1019,7 @@ module Squared
823
1019
  end
824
1020
  return
825
1021
  when :list
1022
+ op.clear
826
1023
  out, banner, from = source(io: true)
827
1024
  print_item banner
828
1025
  list_result(write_lines(out), 'objects', from: from)
@@ -830,8 +1027,8 @@ module Squared
830
1027
  end
831
1028
  else
832
1029
  git_session('stash', 'push', opts: opts)
833
- append_option(%w[all keep-index include-untracked staged].freeze, no: true, ignore: false)
834
- append_message option('message', 'm', ignore: false)
1030
+ append_option(OPT_GIT[:stash][:push].grep_v(/[=|]/), no: true, ignore: false)
1031
+ append_message
835
1032
  end
836
1033
  source(banner: !quiet?, sync: sync, **threadargs)
837
1034
  end
@@ -839,6 +1036,7 @@ module Squared
839
1036
  def status(*)
840
1037
  cmd = git_session 'status'
841
1038
  cmd << (option('long') ? '--long' : '--short')
1039
+ cmd << '--branch' if option('branch')
842
1040
  if (val = option('ignore-submodules', ignore: false))
843
1041
  cmd << basic_option('ignore-submodules', case val
844
1042
  when '0', 'none'
@@ -852,18 +1050,22 @@ module Squared
852
1050
  end)
853
1051
  end
854
1052
  append_pathspec
1053
+ if verbose
1054
+ r = color(:red)
1055
+ g = color(:green)
1056
+ sub = if session_arg?('short')
1057
+ [
1058
+ { pat: /^(.)([A-Z?!])(.+)$/, styles: r, index: 2 },
1059
+ { pat: /^([A-Z?!])(.+)$/, styles: g },
1060
+ { pat: /^(\?\?)(.+)$/, styles: r },
1061
+ { pat: /^(## )(.+?)(\.{3})(.+)$/, styles: [nil, g, nil, r], index: -1 }
1062
+ ]
1063
+ else
1064
+ [{ pat: /^(\t+)([a-z]+: +.+)$/, styles: r, index: 2 }]
1065
+ end
1066
+ end
855
1067
  out, banner, from = source(io: true)
856
- ret = write_lines(out, banner: banner, sub: if verbose
857
- r = color(:red)
858
- g = color(:green)
859
- [
860
- { pat: /^(.)([A-Z?!])(.+)$/, styles: r, index: 2 },
861
- { pat: /^([A-Z?!])(.+)$/, styles: g },
862
- { pat: /^(\?\?)(.+)$/, styles: r },
863
- { pat: /^(## )(.+)(\.{3})(.+)$/,
864
- styles: [nil, g, nil, r], index: -1 }
865
- ]
866
- end)
1068
+ ret = write_lines(out, banner: banner, sub: sub)
867
1069
  list_result(ret, 'files', from: from, action: 'modified')
868
1070
  end
869
1071
 
@@ -919,7 +1121,8 @@ module Squared
919
1121
  msg = sub_style('completed', styles: theme[:active])
920
1122
  puts log_message(Logger::INFO, name, msg, subject: 'revbuild', hint: time_format(epochtime - start))
921
1123
  end
922
- workspace.rev_write(name, { 'revision' => sha, 'files' => status_digest(*args, **kwargs) }, utc: 'build')
1124
+ workspace.rev_write(name, { 'revision' => sha, 'files' => status_digest(*args, **kwargs) },
1125
+ sync: sync, utc: 'build')
923
1126
  end
924
1127
 
925
1128
  def reset(flag, opts = [], refs: nil, ref: nil, mode: nil, commit: nil)
@@ -927,13 +1130,14 @@ module Squared
927
1130
  case flag
928
1131
  when :commit, :index
929
1132
  op = OptionPartition.new(opts, OPT_GIT[:reset] + VAL_GIT[:reset], cmd,
930
- project: self, no: OPT_GIT[:no][:reset])
1133
+ project: self, no: OPT_GIT[:no][:reset],
1134
+ first: flag == :index ? matchpathspec : nil)
931
1135
  if flag == :commit
932
- op.append(commit, delim: true)
1136
+ op.append(commit)
933
1137
  .clear(pass: false)
934
1138
  ref = false
935
1139
  else
936
- (refs ||= []).concat(op.extras)
1140
+ refs = op.extras
937
1141
  end
938
1142
  when :mode
939
1143
  return unless VAL_GIT[:reset].include?(mode)
@@ -945,6 +1149,7 @@ module Squared
945
1149
  end
946
1150
  when :patch
947
1151
  cmd << '--patch'
1152
+ append_pathspec(refs, pass: false)
948
1153
  end
949
1154
  unless ref == false
950
1155
  append_commit(ref, head: true)
@@ -953,7 +1158,7 @@ module Squared
953
1158
  source
954
1159
  end
955
1160
 
956
- def checkout(flag, opts = [], branch: nil, origin: nil, create: nil, commit: nil, detach: nil)
1161
+ def checkout(flag, opts = [], branch: nil, origin: nil, create: nil, commit: nil, detach: nil, merge: false)
957
1162
  cmd, opts = git_session('checkout', opts: opts)
958
1163
  append_option 'force', 'merge'
959
1164
  case flag
@@ -971,21 +1176,23 @@ module Squared
971
1176
  end
972
1177
  cmd << '--track' << shell_quote(origin)
973
1178
  when :detach
1179
+ cmd << '-m' if merge
974
1180
  cmd << '--detach' << commit
975
1181
  else
976
- op = OptionPartition.new(opts, OPT_GIT[:checkout], cmd, project: self, no: OPT_GIT[:no][:checkout])
1182
+ op = OptionPartition.new(opts, OPT_GIT[:checkout], cmd, project: self, no: OPT_GIT[:no][:checkout],
1183
+ first: flag == :path ? matchpathspec : nil)
977
1184
  if flag == :commit
978
- op.append(commit, delim: true)
1185
+ op.append(commit)
979
1186
  .clear(pass: false)
980
1187
  else
981
1188
  append_head
982
- append_pathspec op.extras
1189
+ append_pathspec(op.extras, pass: false)
983
1190
  end
984
1191
  end
985
1192
  source
986
1193
  end
987
1194
 
988
- def tag(flag, opts = [], refs: [], message: nil, commit: nil)
1195
+ def tag(flag, opts = [], refs: [], message: nil, commit: nil, remote: nil)
989
1196
  cmd, opts = git_session('tag', opts: opts)
990
1197
  case flag
991
1198
  when :add, :sign
@@ -997,31 +1204,32 @@ module Squared
997
1204
  cmd << '--force' if option('force')
998
1205
  if !commit && message && (sha = commithash(message))
999
1206
  commit = sha
1000
- else
1001
- append_message message
1207
+ message = nil
1002
1208
  end
1209
+ append_message message
1003
1210
  append_value refs
1004
1211
  append_head commit
1005
- when :list
1212
+ when :delete
1213
+ cmd << '--delete'
1214
+ append_value refs
1215
+ else
1006
1216
  op = OptionPartition.new(opts, OPT_GIT[:tag], cmd << '--list', project: self, no: OPT_GIT[:no][:tag])
1007
1217
  out, banner, from = source(io: true)
1008
1218
  print_item banner
1009
1219
  ret = write_lines(out, grep: op.extras)
1010
1220
  list_result(ret, 'tags', from: from, grep: op.extras)
1011
1221
  return
1012
- when :delete
1013
- cmd << '--delete'
1014
- append_value refs
1015
- else
1016
- cmd << shell_option(flag, commit)
1017
1222
  end
1223
+ remote ||= option('remote')
1018
1224
  source
1225
+ git_spawn('push', flag == :delete ? '-d' : nil, remote, *refs.map { |val| shell_quote(val) }) if remote
1019
1226
  end
1020
1227
 
1021
1228
  def log!(flag, opts = [], range: [], index: [])
1022
1229
  cmd, opts = git_session('log', opts: opts)
1023
1230
  op = OptionPartition.new(opts, collect_hash(OPT_GIT[:log]), cmd,
1024
- project: self, no: collect_hash(OPT_GIT[:no][:log]))
1231
+ project: self, no: collect_hash(OPT_GIT[:no][:log]),
1232
+ first: matchpathspec)
1025
1233
  case flag
1026
1234
  when :between, :contain
1027
1235
  op << shell_quote(range.join(flag == :between ? '..' : '...'))
@@ -1036,7 +1244,8 @@ module Squared
1036
1244
  def diff(flag, opts = [], refs: [], branch: nil, range: [], index: [])
1037
1245
  cmd, opts = git_session('diff', opts: opts)
1038
1246
  op = OptionPartition.new(opts, collect_hash(OPT_GIT[:diff]) + OPT_GIT[:log][:diff], cmd,
1039
- project: self, no: OPT_GIT[:no][:log][:diff])
1247
+ project: self, no: OPT_GIT[:no][:log][:diff],
1248
+ first: flag == :files ? nil : matchpathspec)
1040
1249
  case flag
1041
1250
  when :files, :view, :between, :contain
1042
1251
  op.delete('--cached')
@@ -1049,7 +1258,7 @@ module Squared
1049
1258
  case flag
1050
1259
  when :view
1051
1260
  op << '--merge-base' if option('merge-base')
1052
- op << shell_quote(range.first, quote: true) << shell_quote(range.last, quote: true)
1261
+ op.merge(range)
1053
1262
  when :between, :contain
1054
1263
  op.delete('--merge-base')
1055
1264
  op << shell_quote(range.join(flag == :between ? '..' : '...'))
@@ -1072,82 +1281,99 @@ module Squared
1072
1281
  source(exception: op.arg?('exit-code'))
1073
1282
  end
1074
1283
 
1075
- def commit(flag, *, refs: [], message: nil, pass: false)
1076
- message ||= option('message', 'm', prefix: 'git', ignore: false)
1077
- amend = flag.to_s.start_with?('amend')
1284
+ def commit(flag, opts = [], refs: [], ref: nil, squash: nil, pick: nil, message: nil, pass: false)
1285
+ fixup = flag == :fixup
1286
+ amend = !fixup && flag.to_s.start_with?('amend')
1287
+ unless flag == :add || pick == 'reword'
1288
+ pathspec = if flag == :all || ((fixup || amend) && refs.size == 1 && refs.first == '*')
1289
+ '--all'
1290
+ elsif (refs = projectmap(refs)).empty?
1291
+ raise_error 'no qualified pathspec'
1292
+ else
1293
+ "-- #{refs.join(' ')}"
1294
+ end
1295
+ end
1296
+ if fixup
1297
+ source git_session('commit', basic_option('fixup', "#{pick ? "#{pick}:" : ''}#{ref}"), pathspec)
1298
+ source git_output('rebase --autosquash', squash) if squash.is_a?(String)
1299
+ return
1300
+ end
1301
+ message ||= messageopt
1078
1302
  if !message && !amend
1079
1303
  return if pass
1080
1304
 
1081
- raise_error('missing message', hint: 'GIT_MESSAGE="description"')
1305
+ message = readline('Enter message', force: true)
1082
1306
  end
1083
- pathspec = if flag == :all || (amend && refs.size == 1 && refs.first == '*')
1084
- '--all'
1085
- elsif (refs = projectmap(refs)).empty?
1086
- raise_error 'no qualified pathspec'
1087
- else
1088
- "-- #{refs.join(' ')}"
1089
- end
1090
- origin = nil
1091
1307
  branch = nil
1308
+ origin = nil
1092
1309
  upstream = nil
1310
+ cmd, opts = git_session('add', opts: opts)
1311
+ op = OptionPartition.new(opts, OPT_GIT[:add], cmd, project: self, first: matchpathspec)
1312
+ op << '--verbose' if verbose
1313
+ dryrun = dryrun?
1314
+ format = '%(if)%(HEAD)%(then)%(refname:short)...%(upstream:short)...%(upstream:track)%(end)'
1093
1315
  git_spawn 'fetch --no-tags --quiet'
1094
- git_spawn('branch -vv --list', stdout: false).each do |val|
1095
- next unless (r = /^\*\s(\S+)\s+(\h+)(?:\s\[(.+?)(?=\]\s)\])?\s/.match(val))
1096
-
1097
- branch = r[1]
1098
- if r[3]
1099
- origin = r[3][%r{^(.+)/#{Regexp.escape(branch)}$}, 1]
1100
- else
1101
- unless (origin = option('repository', prefix: 'git', ignore: false))
1102
- out = git_spawn 'log -n1 --format=%h%d'
1103
- if out =~ /^#{r[2]} \(HEAD -> #{Regexp.escape(branch)}, (.+?)\)$/
1104
- split_escape($1).each do |s|
1105
- next unless s.end_with?("/#{branch}")
1106
-
1107
- origin = s[0, s.size - branch.size - 1]
1108
- break
1109
- end
1316
+ foreachref('heads', format: format).each do |line|
1317
+ next if (line = line.chomp).empty?
1318
+
1319
+ branch, origin, hint = line.split('...')
1320
+ if hint && !hint.match?(/^\[(\D+0,\D+0)\]$/)
1321
+ raise_error('work tree is not usable', hint: hint[1..-2])
1322
+ elsif origin.empty?
1323
+ return nil if pass
1324
+ break if dryrun
1325
+
1326
+ unless (origin = option('upstream', prefix: 'git', ignore: false))
1327
+ if (origin = choice_refs('Choose an upstream', 'remotes', attempts: 1, force: false))
1328
+ git_spawn 'branch', quote_option('set-upstream-to', origin)
1329
+ else
1330
+ origin = readline('Enter an upstream', force: true)
1110
1331
  end
1111
1332
  end
1112
- upstream = true if origin
1333
+ raise_error('missing remote name', hint: origin) unless origin.include?('/')
1334
+ upstream = true
1113
1335
  end
1114
1336
  break
1115
1337
  end
1116
- raise_error 'work tree is not usable' unless origin && branch
1117
- cmd = git_session('commit', option('dry-run') && '--dry-run', options: false)
1118
- if amend
1119
- cmd << '--amend'
1338
+ if pathspec
1339
+ op << pathspec
1120
1340
  else
1121
- cmd.delete('--amend')
1341
+ append_pathspec op.extras
1342
+ end
1343
+ co = git_session('commit', options: false)
1344
+ pu = git_output 'push', upstream && '--set-upstream'
1345
+ if dryrun
1346
+ op.delete('--dry-run')
1347
+ co << '--dry-run'
1348
+ pu << '--dry-run'
1122
1349
  end
1350
+ co << '--amend' if amend
1123
1351
  if message
1124
1352
  append_message message
1125
1353
  elsif flag == :'amend-orig' || option('edit', equals: '0')
1126
- cmd << '--no-edit'
1127
- end
1128
- a = git_output 'add', '--verbose'
1129
- b = git_output 'push', upstream && '--set-upstream'
1130
- if dryrun?
1131
- a << '--dry-run'
1132
- b << '--dry-run'
1354
+ co << '--no-edit'
1133
1355
  end
1134
- a << pathspec
1135
- b << '--force-with-lease' if amend
1136
- b << origin << branch
1356
+ pu << '--force-with-lease' if amend
1357
+ pu.merge(repotrack(origin, branch))
1137
1358
  puts if pass
1138
- source a
1139
- source cmd
1140
- source b
1359
+ source op
1360
+ source co
1361
+ source pu
1141
1362
  end
1142
1363
 
1143
- def merge(flag, opts = [], command: nil)
1364
+ def merge(flag, opts = [], command: nil, branch: nil)
1144
1365
  cmd, opts = git_session('merge', opts: opts)
1145
1366
  case flag
1146
1367
  when :commit, :'no-commit'
1147
1368
  op = OptionPartition.new(opts, OPT_GIT[:merge], cmd, project: self, no: OPT_GIT[:no][:merge])
1148
1369
  raise_error 'no branch/commit' if op.empty?
1149
1370
  op << "--#{flag}" << '--'
1150
- append_commit(*op.extras)
1371
+ if branch
1372
+ op << branch
1373
+ op.clear(pass: false)
1374
+ else
1375
+ append_commit(*op.extras)
1376
+ end
1151
1377
  else
1152
1378
  return unless VAL_GIT[:merge][:send].include?(command)
1153
1379
 
@@ -1156,7 +1382,7 @@ module Squared
1156
1382
  source
1157
1383
  end
1158
1384
 
1159
- def branch(flag = nil, opts = [], refs: [], ref: nil, target: nil)
1385
+ def branch(flag = nil, opts = [], refs: [], ref: nil, target: nil, remote: nil)
1160
1386
  cmd, opts = git_session('branch', opts: opts)
1161
1387
  stdout = false
1162
1388
  case flag
@@ -1172,43 +1398,46 @@ module Squared
1172
1398
  end
1173
1399
  end
1174
1400
  cmd << '--force' if option('force')
1175
- when :set
1176
- return unless ref
1177
-
1401
+ cmd << shell_quote(target)
1402
+ cmd << shell_quote(ref) if ref
1403
+ when :track
1404
+ raise_error('invalid upstream', hint: ref) unless ref.include?('/')
1178
1405
  if ref.start_with?('^')
1179
- cmd << '--unset-upstream' << shell_escape(ref[1..-1])
1180
- target = nil
1406
+ cmd << '--unset-upstream' << shell_quote(ref[1..-1])
1407
+ remote = false
1181
1408
  stdout = true
1182
1409
  else
1183
1410
  cmd << quote_option('set-upstream-to', ref)
1411
+ cmd << shell_quote(target) if target
1184
1412
  end
1185
- ref = nil
1186
1413
  when :delete
1414
+ remote&.each do |val|
1415
+ source git_output('push', '--delete', *val.split('/', 2).map { |s| shell_quote(s) })
1416
+ end
1187
1417
  force, list = refs.partition { |val| val.match?(/^[~^]/) }
1188
1418
  force.each do |val|
1189
- dr = val[0, 3]
1419
+ dr = val[0, 2]
1190
1420
  d = dr.include?('^') ? '-D' : '-d'
1191
1421
  r = '-r' if dr.include?('~')
1192
- source git_output('branch', d, r, shell_quote(val.sub(/^[\^~]+/, '')))
1422
+ source git_output('branch', d, r, shell_quote(val.sub(/^[~^]+/, '')))
1193
1423
  end
1194
1424
  return if list.empty?
1195
1425
 
1196
1426
  cmd << '-d'
1197
1427
  append_value list
1428
+ remote = nil
1198
1429
  when :move, :copy
1199
1430
  flag = "-#{flag.to_s[0]}"
1200
1431
  cmd << (option('force') ? flag.upcase : flag)
1201
1432
  refs.compact.each { |val| cmd << shell_quote(val) }
1202
1433
  stdout = true
1203
- when :edit
1204
- cmd << '--edit-description'
1205
1434
  when :current
1206
1435
  cmd << '--show-current'
1207
1436
  source(banner: verbosetype > 1, stdout: true)
1208
1437
  return
1209
1438
  when :list
1210
1439
  op = OptionPartition.new(opts, OPT_GIT[:branch], cmd << '--list',
1211
- project: self, no: OPT_GIT[:no][:branch], single: /^v+$/)
1440
+ project: self, no: OPT_GIT[:no][:branch], single: /\Av+\z/)
1212
1441
  op.each { |val| op << shell_quote(val) }
1213
1442
  out, banner, from = source(io: true)
1214
1443
  print_item banner
@@ -1250,15 +1479,46 @@ module Squared
1250
1479
  end
1251
1480
  return
1252
1481
  end
1253
- cmd << shell_escape(target) if target
1254
- cmd << shell_escape(ref) if ref
1255
- source(stdout: stdout)
1482
+ return unless success?(source(stdout: stdout))
1483
+
1484
+ if !ref
1485
+ print_success if flag == :create
1486
+ elsif remote && target
1487
+ source git_output('push', '-u', shell_quote(ref.split('/', 2).first), shell_quote(target))
1488
+ end
1489
+ end
1490
+
1491
+ def switch(flag, opts = [], target: nil, commit: nil, track: nil)
1492
+ cmd = git_session('switch', opts: opts).first
1493
+ case flag
1494
+ when :create
1495
+ c = 'c'
1496
+ if target.start_with?('^')
1497
+ target = target[1..-1]
1498
+ c = c.upcase
1499
+ end
1500
+ cmd << quote_option(c, target)
1501
+ cmd << case (track ||= option('track', ignore: false))
1502
+ when 'n', 'N', '0', 'false'
1503
+ '--no-track'
1504
+ when 'y', 'Y', '1', 'true'
1505
+ '--track'
1506
+ when 'direct', 'inherit'
1507
+ basic_option('track', track)
1508
+ end
1509
+ when :detach, :merge
1510
+ cmd << "--#{flag}"
1511
+ end
1512
+ cmd << '--force' if option('force')
1513
+ append_head commit
1514
+ source
1256
1515
  end
1257
1516
 
1258
- def restore(flag, opts = [])
1259
- cmd, opts = git_session('restore', "--#{flag}", opts: opts)
1260
- op = OptionPartition.new(opts, OPT_GIT[:restore], cmd, project: self, no: OPT_GIT[:no][:restore])
1261
- append_pathspec(op.extras, pass: false)
1517
+ def restore(flag, opts = [], commit: nil, files: nil)
1518
+ cmd, opts = git_session('restore', shell_option(flag, commit, escape: false, force: false), opts: opts)
1519
+ op = OptionPartition.new(opts, OPT_GIT[:restore], cmd, project: self, no: OPT_GIT[:no][:restore],
1520
+ first: matchpathspec)
1521
+ append_pathspec(op.extras + (files || []), pass: false)
1262
1522
  source(sync: false, stderr: true)
1263
1523
  end
1264
1524
 
@@ -1267,14 +1527,13 @@ module Squared
1267
1527
  case flag
1268
1528
  when :textconv
1269
1529
  cmd << '--textconv'
1270
- append_value(files.map { |val| Dir[val] }
1271
- .flatten
1530
+ append_value(files.flat_map { |val| Dir[val] }
1272
1531
  .select { |val| projectpath?(val) }
1273
1532
  .map! { |val| shell_quote("HEAD:#{val}") })
1274
1533
  source(banner: false)
1275
1534
  return
1276
1535
  when :oneline
1277
- format = flag.to_s
1536
+ format = 'oneline'
1278
1537
  end
1279
1538
  if format
1280
1539
  case (val = format.downcase)
@@ -1291,21 +1550,12 @@ module Squared
1291
1550
  op = OptionPartition.new(opts, OPT_GIT[:show] + OPT_GIT[:diff][:show] + OPT_GIT[:log][:diff], cmd,
1292
1551
  project: self,
1293
1552
  no: OPT_GIT[:no][:show] + collect_hash(OPT_GIT[:no][:log], pass: [:base]))
1294
- unless val == 'oneline' && op.arg?('abbrev-commit')
1295
- op << basic_option('abbrev', val) if (val = option('abbrev')) && val.to_i > 0
1296
- banner = true
1297
- end
1298
1553
  op.append(delim: true)
1299
- source(exception: false, banner: banner)
1554
+ source(exception: false, banner: flag != :oneline)
1300
1555
  end
1301
1556
 
1302
1557
  def rev_parse(flag, opts = [], ref: nil, size: nil)
1303
1558
  cmd, opts = git_session('rev-parse', opts: opts)
1304
- cmd << if flag == :parseopt
1305
- '--parseopt'
1306
- elsif opts.delete('sq-quote')
1307
- '--sq-quote'
1308
- end
1309
1559
  case flag
1310
1560
  when :commit
1311
1561
  cmd << ((n = size.to_i) > 0 ? basic_option('short', [n, 5].max) : '--verify')
@@ -1313,9 +1563,14 @@ module Squared
1313
1563
  when :branch
1314
1564
  cmd << '--abbrev-ref'
1315
1565
  append_commit(ref, head: true)
1316
- else
1317
- op = OptionPartition.new(opts, OPT_GIT[:rev_parse][flag], cmd, project: self)
1318
- op.append(escape: op.arg?('sq-quote'))
1566
+ when :output
1567
+ if opts.delete('sq-quote')
1568
+ cmd << '--sq-quote'
1569
+ args = true
1570
+ end
1571
+ op = OptionPartition.new(opts, OPT_GIT[:rev_parse], cmd, project: self, no: OPT_GIT[:no][:rev_parse],
1572
+ args: args)
1573
+ op.append(escape: args)
1319
1574
  end
1320
1575
  source(banner: verbosetype > 1)
1321
1576
  end
@@ -1342,18 +1597,10 @@ module Squared
1342
1597
 
1343
1598
  def git(flag, opts = [])
1344
1599
  cmd, opts = git_session(flag, opts: opts)
1345
- op = OptionPartition.new(opts, OPT_GIT[flag], cmd, project: self, no: OPT_GIT[:no][flag])
1346
- sync = false
1347
- stderr = true
1600
+ list = OPT_GIT[:git].fetch(flag, []) + OPT_GIT.fetch(flag, [])
1601
+ op = OptionPartition.new(opts, list, cmd, project: self, no: OPT_GIT[:no][flag],
1602
+ first: flag == :revert ? nil : matchpathspec)
1348
1603
  case flag
1349
- when :clean
1350
- refs = projectmap(op.extras)
1351
- unless refs.empty?
1352
- op << '--'
1353
- op.merge(refs)
1354
- end
1355
- sync = true
1356
- stderr = false
1357
1604
  when :revert
1358
1605
  if VAL_GIT[:rebase][:send].any? { |val| op.arg?(val) }
1359
1606
  op.clear
@@ -1362,14 +1609,37 @@ module Squared
1362
1609
  else
1363
1610
  append_commit(*op.extras)
1364
1611
  end
1612
+ when :add, :clean
1613
+ if flag == :add && !op.arg?('pathspec-from-file')
1614
+ grep, list = op.extras.partition { |val| val.start_with?(':') && val.end_with?(':') }
1615
+ if list.empty? || !grep.empty?
1616
+ red = color(:red)
1617
+ grep.map! { |val| Regexp.new(val[1..-2]) }
1618
+ files = status_data.map! do |a, b|
1619
+ next unless grep.empty? || grep.any? { |pat| pat.match?(a) }
1620
+
1621
+ "#{sub_style(b, styles: red)} #{a}"
1622
+ end
1623
+ .compact
1624
+ unless files.empty?
1625
+ files = choice_index('Select files', files, multiple: true, force: true, trim: /^\S+\s/,
1626
+ accept: 'Add?')
1627
+ end
1628
+ op.swap(list + files)
1629
+ end
1630
+ end
1631
+ append_pathspec(op.extras)
1632
+ verbose = flag == :add && !op.arg?('verbose')
1633
+ print_success if success?(source) && verbose
1634
+ return
1365
1635
  when :mv
1366
- refs = projectmap(op.extras)
1636
+ refs = projectmap op.extras
1367
1637
  raise_error 'no source/destination' unless refs.size > 1
1368
1638
  op.merge(refs)
1369
1639
  when :rm
1370
1640
  append_pathspec(op.extras, expect: true)
1371
1641
  end
1372
- source(sync: sync, stderr: stderr)
1642
+ source(sync: false, stderr: true)
1373
1643
  end
1374
1644
 
1375
1645
  def clone?
@@ -1387,17 +1657,18 @@ module Squared
1387
1657
  private
1388
1658
 
1389
1659
  def source(cmd = @session, exception: true, io: false, sync: true, stdout: false, stderr: false, banner: true,
1390
- multiple: false, sub: nil)
1660
+ multiple: false, **kwargs)
1661
+ cmd = cmd.target if cmd.is_a?(OptionPartition)
1391
1662
  banner = nil if multiple && banner
1392
1663
  if cmd.respond_to?(:done)
1393
1664
  if io && banner == false
1394
1665
  from = nil
1395
- elsif !from && (from = cmd.drop(1).find { |val| val.match?(/^[a-z][a-z\-]{2,}$/) })
1666
+ elsif !from && (from = cmd.drop(1).find { |val| val.match?(/\A[a-z]{1,2}[a-z\-]*\z/) })
1396
1667
  from = :"git:#{from}"
1397
1668
  end
1398
1669
  banner &&= cmd.temp { |val| val.start_with?('--work-tree') || val.start_with?('--git-dir') }
1399
1670
  end
1400
- cmd = session_done(cmd)
1671
+ cmd = session_done cmd
1401
1672
  log&.info cmd
1402
1673
  on :first, from
1403
1674
  banner = if banner
@@ -1414,26 +1685,26 @@ module Squared
1414
1685
  ret = `#{cmd}`
1415
1686
  if !ret.empty?
1416
1687
  puts ret
1417
- elsif banner && stdout && !stdin?
1418
- puts 'Success'
1688
+ elsif success?(!banner.nil?)
1689
+ print_success
1419
1690
  end
1420
1691
  elsif sync || (!exception && !stderr)
1421
1692
  print_item banner unless multiple
1422
- shell(cmd, exception: exception)
1693
+ ret = shell(cmd, exception: exception)
1423
1694
  else
1424
1695
  require 'open3'
1425
1696
  if stderr
1426
1697
  Open3.popen3(cmd) do |_, out, err|
1427
- ret = write_lines(out, banner: banner, sub: sub, pass: true)
1428
- if ret == 0
1429
- ret = write_lines(err, banner: banner)
1430
- puts 'Success' if ret == 0 && banner && !stdin?
1698
+ n = write_lines(out, banner: banner, pass: true, **kwargs)
1699
+ if n == 0
1700
+ n = write_lines(err, banner: banner)
1701
+ print_success if success?(n == 0 && !banner.nil?)
1431
1702
  else
1432
1703
  write_lines(err, loglevel: Logger::DEBUG)
1433
1704
  end
1434
1705
  end
1435
1706
  else
1436
- Open3.popen2e(cmd) { |_, out| write_lines(out, banner: banner) }
1707
+ Open3.popen2e(cmd) { |_, out| write_lines(out, banner: banner, **kwargs) }
1437
1708
  end
1438
1709
  end
1439
1710
  rescue StandardError => e
@@ -1442,19 +1713,16 @@ module Squared
1442
1713
  raise if exception && ret != true
1443
1714
 
1444
1715
  warn log_message(Logger::WARN, e, pass: true) if warning?
1716
+ nil
1445
1717
  else
1446
1718
  on :last, from
1719
+ ret
1447
1720
  end
1448
1721
  end
1449
1722
 
1450
1723
  def write_lines(data, banner: nil, loglevel: nil, grep: nil, sub: nil, pass: false, first: false)
1451
- grep = as_a(grep).map do |val|
1452
- next val if val.is_a?(Regexp)
1453
-
1454
- Regexp.new(val == '*' ? '.+' : val.to_s)
1455
- end
1456
- grep = nil if grep.empty?
1457
- sub = nil if stdin?
1724
+ grep &&= as_a(grep).yield_self { |a| a.empty? || a.include?('*') ? nil : a.map { |val| Regexp.new(val) } }
1725
+ sub &&= stdin? ? nil : as_a(sub)
1458
1726
  ret = 0
1459
1727
  out = []
1460
1728
  data.each do |line|
@@ -1484,7 +1752,7 @@ module Squared
1484
1752
  styles = theme.fetch(:banner, []).reject { |s| s.to_s.end_with?('!') }
1485
1753
  styles << :bold if styles.size <= 1
1486
1754
  puts print_footer("#{size} #{size == 1 ? type.sub(/(?:(?<!l)e)?s\z/, '') : type}",
1487
- sub: { pat: /\A(\d+)(.+)\z/, styles: styles })
1755
+ sub: { pat: /^(\d+)(.+)$/, styles: styles })
1488
1756
  else
1489
1757
  puts empty_status("No #{type} were #{action}", 'grep', grep.is_a?(Array) ? case grep.size
1490
1758
  when 0
@@ -1497,20 +1765,32 @@ module Squared
1497
1765
  on :last, from
1498
1766
  end
1499
1767
 
1500
- def choice_refs(msg, type, format: nil, sort: '-creatordate', count: ARG[:CHOICE], short: true, **kwargs)
1768
+ def choice_refs(msg, *type, format: nil, sort: '-creatordate', count: true, short: true, **kwargs)
1769
+ type << 'heads' if type.empty?
1501
1770
  unless format
1502
1771
  format = +"%(refname#{short ? ':short' : ''})"
1503
- case type
1504
- when 'heads', 'tags'
1772
+ if type.include?('heads') || type.include?('tags')
1505
1773
  format += '%(if)%(HEAD)%(then) *%(end)'
1506
1774
  trim = /\s+\*\z/
1507
1775
  end
1508
1776
  end
1509
- cmd = git_output 'for-each-ref', quote_option('format', format)
1510
- cmd << quote_option('sort', sort) if sort
1511
- cmd << shell_option('count', count) if count
1512
- cmd << "refs/#{type}"
1513
- choice_index(msg, source(cmd, io: true, banner: false), trim: trim, **kwargs)
1777
+ args = []
1778
+ args << quote_option('sort', sort) if sort
1779
+ args << shell_option('count', env('GIT_COUNT', ARG[:CHOICE])) if count
1780
+ choice_index(msg, foreachref(type, *args, format: format), trim: trim, **kwargs)
1781
+ end
1782
+
1783
+ def choice_commit(count: true, reflog: true, force: true, **kwargs)
1784
+ kwargs[:attempts] ||= 1 unless force
1785
+ cmd = git_output(reflog && env('GIT_REFLOG') ? 'reflog' : 'log')
1786
+ cmd << quote_option('format', '(%h) %s')
1787
+ cmd << basic_option('max-count', env('GIT_COUNT', ARG[:CHOICE])) if count
1788
+ choice_index('Choose a commit', git_spawn(cmd, stdout: false), column: /\((\w+)\)/, force: force, **kwargs)
1789
+ end
1790
+
1791
+ def choice_remote(force: false, **kwargs)
1792
+ kwargs[:attempts] ||= 1 unless force
1793
+ choice_index('Select a remote', git_spawn('remote', stdout: false), force: force, **kwargs)
1514
1794
  end
1515
1795
 
1516
1796
  def status_digest(*args, algorithm: nil, **kwargs)
@@ -1519,23 +1799,29 @@ module Squared
1519
1799
  glob = kwargs.fetch(:include, [])
1520
1800
  pass = kwargs.fetch(:exclude, [])
1521
1801
  ret = {}
1522
- git_spawn('status -s --porcelain', *args, stdout: false).each do |line|
1523
- next unless (file = line[/^[A-Z ?!]{3}"?(.+?)"?$/, 1])
1802
+ status_data(*args).each do |line|
1803
+ file = line.first
1524
1804
  next if !glob.empty? && glob.none? { |val| File.fnmatch?(val, file, File::FNM_DOTMATCH) }
1525
1805
  next if !pass.empty? && pass.any? { |val| File.fnmatch?(val, file, File::FNM_DOTMATCH) }
1526
1806
 
1527
- ret[file] = algorithm.hexdigest(File.read(basepath(file)))
1807
+ ret[file] = algorithm.hexdigest(File.read(path + file))
1528
1808
  end
1529
1809
  ret
1530
1810
  end
1531
1811
 
1532
- def git_spawn(*cmd, stdout: true)
1533
- source(git_output(*cmd), io: true, banner: false, stdout: stdout)
1812
+ def status_data(*args)
1813
+ ret = []
1814
+ git_spawn('status -z -uall', *args).split("\x0").each do |line|
1815
+ next unless line =~ /^(.)(.) (.+)$/
1816
+
1817
+ ret << [$3, $2, $1]
1818
+ end
1819
+ ret
1534
1820
  end
1535
1821
 
1536
- def append_pull(opts, list, target: @session, flag: nil, no: nil, remote: nil)
1822
+ def append_pull(opts, list, target: @session, flag: nil, no: nil, remote: nil, from: nil)
1537
1823
  target << '--force' if option('force', target: target)
1538
- append_submodules(target: target)
1824
+ append_submodules(target: target, from: from)
1539
1825
  return if !remote && opts.empty?
1540
1826
 
1541
1827
  refspec = []
@@ -1560,6 +1846,7 @@ module Squared
1560
1846
  op.errors << opt
1561
1847
  end
1562
1848
  end
1849
+ op << '--verbose' if (flag || from == :fetch) && verbose && !op.arg?('quiet')
1563
1850
  if remote
1564
1851
  op.append(remote, delim: true)
1565
1852
  if (val = option('refspec', target: target, strict: true))
@@ -1571,6 +1858,8 @@ module Squared
1571
1858
  elsif op.arg?('--multiple')
1572
1859
  op.swap.merge(op.map! { |opt| shell_escape(opt, quote: true) })
1573
1860
  return
1861
+ elsif option('all')
1862
+ cmd << '--all'
1574
1863
  end
1575
1864
  op.clear(errors: true, subject: flag.to_s) if flag
1576
1865
  end
@@ -1589,7 +1878,7 @@ module Squared
1589
1878
  option_clear files
1590
1879
  else
1591
1880
  if files.empty? && (val = option('pathspec', target: target))
1592
- files = split_escape(val)
1881
+ files = split_escape val
1593
1882
  end
1594
1883
  files = projectmap(files, parent: parent, pass: pass)
1595
1884
  if !files.empty?
@@ -1600,36 +1889,48 @@ module Squared
1600
1889
  end
1601
1890
  end
1602
1891
 
1603
- def append_message(val, target: @session)
1604
- target << quote_option('message', val) unless val.to_s.empty?
1892
+ def append_message(val = nil, target: @session)
1893
+ val = messageopt if val.to_s.empty?
1894
+ target << quote_option('message', val) if val
1605
1895
  end
1606
1896
 
1607
1897
  def append_head(val = nil, target: @session)
1608
- return target << val if val
1898
+ return target << shell_quote(val) if val
1609
1899
 
1610
1900
  append_first('head', 'tree-ish', 'object', target: target, flag: false, ignore: false)
1611
1901
  end
1612
1902
 
1613
- def append_submodules(from = nil, target: @session)
1903
+ def append_submodules(target: @session, from: nil)
1614
1904
  return unless (val = option('recurse-submodules', target: target, ignore: false))
1615
1905
 
1616
1906
  if from == :clone
1617
- projectmap(split_escape(val)).each do |path|
1618
- target << basic_option('recurse-submodules', path)
1907
+ case val
1908
+ when '0', 'false'
1909
+ target << '--no-recurse-submodules'
1910
+ when '1', 'true'
1911
+ target << '--recurse-submodules'
1912
+ else
1913
+ projectmap(split_escape(val)).each do |path|
1914
+ target << basic_option('recurse-submodules', path)
1915
+ end
1619
1916
  end
1620
- target
1621
1917
  else
1622
1918
  target << case val
1623
1919
  when 'no', '0', 'false'
1624
1920
  '--no-recurse-submodules'
1625
1921
  when 'yes', 'on-demand'
1626
- "--recurse-submodules#{from == :reset ? '' : "=#{val}"}"
1922
+ "--recurse-submodules=#{val}"
1627
1923
  else
1628
1924
  '--recurse-submodules'
1629
1925
  end
1630
1926
  end
1631
1927
  end
1632
1928
 
1929
+ def foreachref(path, *args, format: nil)
1930
+ path = as_a(path).map! { |val| "refs/#{val}" }
1931
+ git_spawn('for-each-ref', format && quote_option('format', format), *args, *path, stdout: false)
1932
+ end
1933
+
1633
1934
  def git_session(*cmd, opts: nil, worktree: true, **kwargs)
1634
1935
  dir = worktree ? ["--work-tree=#{shell_quote(path)}", "--git-dir=#{shell_quote(gitpath)}"] : []
1635
1936
  return session('git', *dir, *cmd, **kwargs) unless opts
@@ -1643,24 +1944,53 @@ module Squared
1643
1944
  git_session(*cmd, main: false, options: false, **kwargs)
1644
1945
  end
1645
1946
 
1947
+ def git_spawn(*cmd, stdout: true)
1948
+ source(cmd.first.is_a?(Set) ? cmd.first : git_output(*cmd), io: true, banner: false, stdout: stdout)
1949
+ end
1950
+
1646
1951
  def dryrun?(*, target: @session, **)
1647
1952
  return false unless target
1648
1953
 
1649
- target.include?('--dry-run')
1954
+ target.include?('--dry-run') || !option('dry-run', target: target).nil?
1650
1955
  end
1651
1956
 
1652
- def quiet?(target: @session)
1957
+ def quiet?(*, target: @session, **)
1653
1958
  return false unless target
1654
1959
 
1655
1960
  target.include?('--quiet') || (target.include?('-q') && stripext(target.first) == 'git')
1656
1961
  end
1657
1962
 
1658
1963
  def gitpath
1659
- basepath '.git'
1964
+ path + '.git'
1965
+ end
1966
+
1967
+ def repotrack(origin, branch, quote: true)
1968
+ i = origin.index('/')
1969
+ branch = "#{branch}:#{origin[i + 1..-1]}" unless origin.end_with?("/#{branch}")
1970
+ ret = [origin[0..i - 1], branch]
1971
+ quote ? ret.map! { |val| shell_quote(val) } : ret
1660
1972
  end
1661
1973
 
1662
1974
  def commithash(val)
1663
- val[/^#\{(\h{5,40})\}$/, 1]
1975
+ val[/:(\h{5,40})\z/, 1] || val[/\A#\{(\h{5,40})\}\z/, 1]
1976
+ end
1977
+
1978
+ def commithead(val)
1979
+ return val unless (s = matchhead(val))
1980
+
1981
+ s.match?(/^\d/) ? "@~#{s}" : "@#{s}"
1982
+ end
1983
+
1984
+ def matchhead(val)
1985
+ val && val =~ /^(?:(?:HEAD|@)([~^]\d*)?|H(\d+))$/ ? $2 || $1 || '' : nil
1986
+ end
1987
+
1988
+ def matchpathspec
1989
+ [/\A[^a-z\d-]+/i, %r{\A[^=\\/*]*[\\/*]}]
1990
+ end
1991
+
1992
+ def messageopt
1993
+ option('message', 'm', prefix: 'git', ignore: false)
1664
1994
  end
1665
1995
 
1666
1996
  def threadargs