squared 0.3.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'date'
4
+ require 'time'
5
+ require 'digest'
6
+
3
7
  module Squared
4
8
  module Workspace
5
9
  module Git
@@ -7,7 +11,9 @@ module Squared
7
11
  GIT_PROTO = %r{^(?:https?|ssh|git|file)://}i.freeze
8
12
  private_constant :GIT_REPO, :GIT_PROTO
9
13
 
10
- def git(name, uri = nil, base: nil, repo: [], options: {})
14
+ attr_reader :revfile
15
+
16
+ def git(name, uri = nil, base: nil, repo: [], options: {}, cache: nil)
11
17
  data = {}
12
18
  check = ->(proj) { proj.is_a?(Project::Git) && !proj.exclude?(Project::Git.ref) && git_clone?(proj.path) }
13
19
  if uri.is_a?(Array)
@@ -17,15 +23,15 @@ module Squared
17
23
  data[name.to_s] = uri
18
24
  elsif name.is_a?(Enumerable)
19
25
  data = name.to_h
20
- elsif name.is_a?(String) && name =~ GIT_PROTO
26
+ elsif name.is_a?(String) && name.match?(GIT_PROTO)
21
27
  base = name
22
28
  @project.each_value { |proj| repo << proj if !proj.parent && check.(proj) }
23
29
  else
24
- warn log_message(Logger::WARN, name, subject: 'git', hint: 'invalid') if warning
30
+ warn log_message(Logger::WARN, name, subject: 'git', hint: 'invalid', pass: true) if warning
25
31
  return self
26
32
  end
27
33
  if base
28
- base = base =~ GIT_PROTO ? "#{base.chomp('/')}/" : @root.join(base)
34
+ base = base.match?(GIT_PROTO) ? "#{base.chomp('/')}/" : @root.join(base)
29
35
  repo.each do |target|
30
36
  if target.is_a?(Project::Git)
31
37
  data[target.localname] = target.project
@@ -55,6 +61,23 @@ module Squared
55
61
  (GIT_REPO[main] ||= {})[key] = [uri.to_s, opts]
56
62
  (@kind[key] ||= []) << Project::Git
57
63
  end
64
+ if cache == true
65
+ revbuild
66
+ elsif cache
67
+ revbuild(file: cache)
68
+ end
69
+ self
70
+ end
71
+
72
+ def revbuild(file: nil)
73
+ @revfile = @home.join(file || "#{@main}.revb")
74
+ @revdoc = JSON.parse(@revfile.read) if @revfile.exist?
75
+ rescue StandardError => e
76
+ @revfile = nil
77
+ warn log_message(Logger::WARN, e, pass: true) if @warning
78
+ self
79
+ else
80
+ @revdoc = {} unless @revdoc.is_a?(Hash)
58
81
  self
59
82
  end
60
83
 
@@ -62,25 +85,76 @@ module Squared
62
85
  (ret = GIT_REPO[main]) && ret[name]
63
86
  end
64
87
 
88
+ def rev_entry(*keys, val: nil, create: true)
89
+ return unless @revdoc
90
+ return @revdoc.dig(*keys) unless val
91
+
92
+ data = @revdoc
93
+ last = keys.pop
94
+ for key in keys
95
+ if data[key].is_a?(Hash)
96
+ data = data[key]
97
+ elsif create
98
+ data = data[key] = {}
99
+ else
100
+ return
101
+ end
102
+ end
103
+ data[last] = val
104
+ end
105
+
106
+ def rev_timeutc(*keys)
107
+ rev_entry(*keys, val: rev_timenow)
108
+ end
109
+
110
+ def rev_timesince(*keys, clock: false)
111
+ epoch = rev_timenow - rev_entry(*keys).to_i
112
+ rescue StandardError
113
+ nil
114
+ else
115
+ time_format(epoch, clock: clock)
116
+ end
117
+
118
+ def rev_clear(name)
119
+ rev_write if rev_entry(name, 'revision', val: '', create: false)
120
+ end
121
+
122
+ def rev_write(name = nil, data = nil, utc: nil)
123
+ return unless @revfile
124
+
125
+ if name
126
+ data&.each { |key, val| rev_entry(name, key, val: val) }
127
+ rev_timeutc(name, utc) if utc
128
+ end
129
+ File.write(@revfile, JSON.pretty_generate(@revdoc))
130
+ end
131
+
65
132
  def git_clone?(path, name = nil)
66
133
  return false if name && !git_repo(name)
67
134
 
68
135
  !path.exist? || path.empty?
69
136
  end
137
+
138
+ private
139
+
140
+ def rev_timenow
141
+ DateTime.now.strftime('%Q').to_i + time_offset
142
+ end
70
143
  end
71
144
  Application.include Git
72
145
 
73
146
  module Project
74
147
  class Git < Base
75
- include Prompt
76
-
77
148
  OPT_GIT = {
149
+ common: %w[bare p|paginate P|no-pager glob-pathspecs icase-pathspecs literal-pathspecs no-optional-locks
150
+ no-replace-objects noglob-pathspecs c=q config-env=q exec-path=p namespace=p].freeze,
78
151
  branch: %w[a|all create-reflog i|ignore-case q|quiet r|remotes v|verbose abbrev=i color=b column=b
79
- contains=e format=q merged=e no-contains=e no-merged=e points-at=e u|set-upstream-to=e sort=q
152
+ contains=b format=q merged=b no-contains=b no-merged=b points-at=e u|set-upstream-to=e sort=q
80
153
  t|track=b].freeze,
81
154
  checkout: %w[l d|detach f|force ignore-other-worktrees ignore-skip-worktree-bits m|merge p|patch
82
155
  pathspec-file-nul q quiet orphan=e ours theirs conflict=b pathspec-from-file=p
83
156
  t|track=b].freeze,
157
+ clean: %w[d x X f|force i|interactive n|dry-run q|quiet e|exlcude=q].freeze,
84
158
  diff: {
85
159
  base: %w[0 1|base 2|ours 3|theirs].freeze,
86
160
  show: %w[s exit-code histogram].freeze
@@ -90,7 +164,7 @@ module Squared
90
164
  recurse-submodules-default=b].freeze,
91
165
  pull: %w[4 6 n t a|append atomic dry-run f|force k|keep n|negotiate-only prefetch p|prune q|quiet
92
166
  set-upstream unshallow update-shallow v|verbose deepen=i depth=i j|jobs=i negotiation-tip=q
93
- refmap=q o|server-option=e shallow-exclude=e shallow-since=b upload-pack=e].freeze
167
+ refmap=q o|server-option=q shallow-exclude=e shallow-since=b upload-pack=q].freeze
94
168
  }.freeze,
95
169
  log: {
96
170
  base: %w[all all-match alternate-refs author-date-order basic-regexp bisect boundary cherry cherry-mark
@@ -98,7 +172,7 @@ module Squared
98
172
  first-parent F|fixed-strings follow full-diff full-history ignore-missing invert-grep left-only
99
173
  merge log-size no-max-parents no-min-parents not P|perl-regexp reflog i|regexp-ignore-case
100
174
  remove-empty reverse right-only simplify-by-decoration simplify-merges single-worktree show-pulls
101
- source sparse stdin topo-order g|walk-reflogs after=q ancestry-path=e? author=q before=q
175
+ source sparse stdin topo-order g|walk-reflogs after=q ancestry-path=b? author=q before=q
102
176
  branches=q? committer=q decorate=b decorate-refs=q decorate-refs-exclude=q exclude=q
103
177
  exclude-hidden=b? glob=q grep=q grep-reflog=q L=q n|max-count=i max-parents=i min-parents=i
104
178
  no-walk=b? remotes=q? since=q since-as-filter=q skip=i tags=q? until=q].freeze,
@@ -109,9 +183,9 @@ module Squared
109
183
  W|function-context w|ignore-all-space ignore-blank-lines ignore-cr-at-eol ignore-space-at-eol
110
184
  b|ignore-space-change D|irreversible-delete graph ita-invisible-in-index minimal name-only
111
185
  name-status no-color-moved-ws no-prefix no-renames numstat patch-with-raw patch-with-stat patience
112
- pickaxe-all pickaxe-regex raw shortstat summary a|text abbrev=i? anchored=q B|break-rewrites=e?
186
+ pickaxe-all pickaxe-regex raw shortstat summary a|text abbrev=i? anchored=q B|break-rewrites=b?
113
187
  color=b color-moved=b color-moved-ws=b color-words=q? diff-algorithm=b diff-filter=e? X|dirstat=q?
114
- dirstat-by-file=q? dst-prefix=q C|find-copies=i? find-object=e M|find-renames=i?
188
+ dirstat-by-file=q? dst-prefix=q C|find-copies=i? find-object=b M|find-renames=i?
115
189
  I|ignore-matching-lines=q ignore-submodules=b inter-hunk-context=i line-prefix=q output=p
116
190
  output-indicator-context=q output-indicator-new=q output-indicator-old=q relative=p rotate-to=p
117
191
  skip-to=p src-prefix=q stat=q? stat-width=i stat-name-width=i stat-count=i submodule=b? U|unified=i
@@ -120,25 +194,32 @@ module Squared
120
194
  ls_files: %w[z debug deduplicate directory eol error-unmatch exclude-standard full-name k|killed
121
195
  no-empty-directory recurse-submodules sparse s|stage u|unmerged abbrev=i x|exclude=q
122
196
  X|exclude-from=p exclude-per-directory=p format=q with-tree=q].freeze,
123
- ls_remote: %w[exit-code get-url q|quiet o|server-option=e symref sort=q upload-pack=e].freeze,
124
- pull: %w[e n allow-unrelated-histories ff-only S|gpg-sign=e log=i r|rebase=b? s|strategy=b
125
- X|strategy-option=e].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=b? s|strategy=b
200
+ 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,
126
203
  rebase: %w[n C=i allow-empty-message apply committer-date-is-author-date edit-todo f|force-rebase ignore-date
127
204
  ignore-whitespace i|interactive keep-base m merge no-ff q|quiet quit r|rebase-merges=b?
128
- reset-author-date root show-current-patch signoff v|verbose empty=b S|gpg-sign=b onto=e
129
- s|strategy=b X|strategy-option=b whitespace=e].freeze,
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,
130
207
  reset: %w[N pathspec-file-nul q|quiet pathspec-from-file=p].freeze,
131
208
  restore: %w[ignore-unmerged ignore-skip-worktree-bits m|merge ours p|patch pathspec-file-nul S|staged theirs
132
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,
133
212
  rev_parse: {
134
213
  output: %w[absolute-git-dir all flags git-common-dir git-dir is-bare-repository is-inside-git-dir
135
214
  is-inside-work-tree is-shallow-repository local-env-vars no-flags no-revs not q|quiet sq
136
215
  revs-only shared-index-path show-cdup show-prefix show-toplevel show-superproject-working-tree
137
- sq-quote symbolic symbolic-full-name verify abbrev-ref=b? after=q before=q default=e
216
+ sq-quote symbolic symbolic-full-name verify abbrev-ref=b? after=q before=q default=q
138
217
  disambiguate=b exclude=q exclude-hidden=b glob=q git-path=p path-format=b? prefix=q branches=q?
139
218
  remotes=q? resolve-git-dir=p short=i? show-object-format=b? since=q tags=q? until=q].freeze,
140
219
  parseopt: %w[keep-dashdash stop-at-non-option stuck-long].freeze
141
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,
142
223
  show: %w[t combined-all-paths no-diff-merges remerge-diff show-signature diff-merges=b encoding=b
143
224
  expand-tabs=i notes=q show-notes=q?].freeze,
144
225
  stash: {
@@ -148,7 +229,8 @@ module Squared
148
229
  pop: %w[index].freeze,
149
230
  apply: %w[index].freeze
150
231
  }.freeze,
151
- tag: %w[create-reflog column=b contains=e? format=q merged=e? n=i no-contains=e? no-merged=e? points-at=q
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
152
234
  sort=q].freeze,
153
235
  no: {
154
236
  fetch: {
@@ -164,20 +246,26 @@ module Squared
164
246
  tag: %w[column].freeze,
165
247
  branch: %w[color-moved column color track].freeze,
166
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
250
+ verify verify-signatures].freeze,
167
251
  rebase: %w[autosquash autostash fork-point gpg-sign keep-empty reapply-cherry-picks reschedule-failed-exec
168
252
  rerere-autoupdate stat update-refs verify].freeze,
169
253
  reset: %w[refresh].freeze,
170
254
  restore: %w[overlay progress recurse-submodules].freeze,
255
+ revert: %w[edit gpg-sign rerere-autoupdate].freeze,
171
256
  show: %w[standard-notes].freeze
172
257
  }.freeze
173
- }
258
+ }.freeze
174
259
  VAL_GIT = {
260
+ merge: {
261
+ send: %w[continue abort quit].freeze
262
+ }.freeze,
175
263
  rebase: {
176
264
  send: %w[continue skip abort quit].freeze,
177
265
  value: %w[true false merges interactive].freeze
178
266
  }.freeze,
179
267
  reset: %w[soft mixed hard merge keep recurse-submodules no-recurse-submodules].freeze
180
- }
268
+ }.freeze
181
269
  private_constant :OPT_GIT, :VAL_GIT
182
270
 
183
271
  class << self
@@ -209,7 +297,7 @@ module Squared
209
297
  end
210
298
 
211
299
  def tasks
212
- %i[pull rebase fetch clone stash status].freeze
300
+ %i[pull rebase fetch clone stash status branch revbuild].freeze
213
301
  end
214
302
 
215
303
  def batchargs
@@ -227,24 +315,25 @@ module Squared
227
315
  'branch' => %i[create set delete move copy list edit current].freeze,
228
316
  'checkout' => %i[commit branch track detach path].freeze,
229
317
  'commit' => %i[add all amend amend-orig].freeze,
230
- 'diff' => %i[head cached branch files view between contain].freeze,
318
+ 'diff' => %i[head branch files view between contain].freeze,
231
319
  'fetch' => %i[origin remote].freeze,
232
320
  'files' => %i[cached modified deleted others ignored].freeze,
321
+ 'git' => %i[clean mv restore revert rm].freeze,
233
322
  'log' => %i[view between contain].freeze,
323
+ 'merge' => %i[commit no-commit send].freeze,
234
324
  'pull' => %i[origin remote].freeze,
235
325
  'rebase' => %i[branch onto send].freeze,
236
326
  'refs' => %i[heads tags remote].freeze,
237
327
  'reset' => %i[commit index patch mode].freeze,
238
- 'restore' => %i[source worktree staged overlay].freeze,
239
- 'rev' => %i[commit branch output parseopt].freeze,
328
+ 'rev' => %i[commit branch output parseopt build].freeze,
240
329
  'show' => %i[format oneline].freeze,
241
- 'stash' => %i[push pop apply drop clear list].freeze,
330
+ 'stash' => %i[push pop apply drop list].freeze,
242
331
  'tag' => %i[add delete list].freeze
243
332
  }.freeze
244
333
 
245
334
  def initialize(*, **)
246
335
  super
247
- initialize_ref(Git.ref) if gitpath.exist?
336
+ initialize_ref Git.ref if gitpath.exist?
248
337
  end
249
338
 
250
339
  def ref
@@ -289,19 +378,6 @@ module Squared
289
378
  commit(flag, refs: refs)
290
379
  end
291
380
  end
292
- when 'restore'
293
- if flag == :source
294
- format_desc action, flag, 'tree,opts*,pathspec*'
295
- task flag, [:tree] do |_, args|
296
- tree = param_guard(action, flag, args: args, key: :tree)
297
- restore(flag, args.extras, tree: tree)
298
- end
299
- else
300
- format_desc action, flag, 'opts*,pathspec+'
301
- task flag do |_, args|
302
- restore flag, args.to_a
303
- end
304
- end
305
381
  when 'tag'
306
382
  case flag
307
383
  when :list
@@ -335,9 +411,19 @@ module Squared
335
411
  case flag
336
412
  when :view, :between, :contain
337
413
  if flag == :view && action == 'log'
338
- format_desc action, flag, '(^)commit*,pathspec*,opts*'
414
+ format_desc action, flag, '(^)commit/H0*,pathspec*,opts*'
339
415
  task flag do |_, args|
340
- logx flag, args.to_a
416
+ index = []
417
+ args.to_a.each do |val|
418
+ if val =~ /^H(\d+)$/
419
+ index << "HEAD~#{$1}"
420
+ elsif (sha = commithash(val))
421
+ index << sha
422
+ elsif val.start_with?('^') || (!%r{^[.\\/]}.match?(val) && !%r{[\\/]$}.match?(val))
423
+ index << shell_quote(val)
424
+ end
425
+ end
426
+ logx(flag, args.to_a.drop(index.size), index: index)
341
427
  end
342
428
  else
343
429
  format_desc action, flag, 'commit1,commit2,pathspec*,opts*'
@@ -347,10 +433,16 @@ module Squared
347
433
  __send__(action == 'log' ? :logx : :diff, flag, args.extras, range: [commit1, commit2])
348
434
  end
349
435
  end
350
- when :head, :cached
351
- format_desc action, flag, 'opts*,pathspec*'
436
+ when :head
437
+ format_desc action, flag, 'commit/H0*,opts*,pathspec*'
352
438
  task flag do |_, args|
353
- diff flag, args.to_a
439
+ index = []
440
+ args.to_a.each do |val|
441
+ break unless val =~ /^H(\d+)$/ || (sha = commithash(val))
442
+
443
+ index << ($1 ? "HEAD~#{$1}" : sha)
444
+ end
445
+ diff(flag, args.to_a.drop(index.size), index: index)
354
446
  end
355
447
  when :branch
356
448
  format_desc action, flag, 'name,opts*,pathspec*'
@@ -495,7 +587,7 @@ module Squared
495
587
  show args.format, args.extras
496
588
  end
497
589
  end
498
- when 'rebase'
590
+ when 'rebase', 'merge'
499
591
  case flag
500
592
  when :branch
501
593
  format_desc action, flag, 'opts*,upstream?,branch?'
@@ -510,11 +602,18 @@ module Squared
510
602
  upstream = param_guard(action, flag, args: args, key: :upstream)
511
603
  rebase(flag, commit: commit, upstream: upstream, branch: args.branch)
512
604
  end
605
+ when :commit, :'no-commit'
606
+ format_desc action, flag, 'branch/commit+,opts*'
607
+ task flag do |_, args|
608
+ args = param_guard(action, flag, args: args.to_a)
609
+ merge flag, args
610
+ end
513
611
  when :send
514
- format_desc(action, flag, VAL_GIT[:rebase][:send], arg: nil)
612
+ format_desc(action, flag, VAL_GIT[action.to_sym][:send], arg: nil)
515
613
  task flag, [:command] do |_, args|
516
- command = param_guard(action, flag, args: args, key: :command)
517
- rebase(flag, command: command)
614
+ command = param_guard(action, flag, args: args, key: :command,
615
+ values: VAL_GIT[action.to_sym][:send])
616
+ __send__(action, flag, command: command)
518
617
  end
519
618
  end
520
619
  when 'rev'
@@ -535,6 +634,11 @@ module Squared
535
634
  task flag, [:ref] do |_, args|
536
635
  rev_parse(flag, ref: args.ref)
537
636
  end
637
+ when :build
638
+ format_desc action, flag, OPT_GIT[:status]
639
+ task flag do |_, args|
640
+ revbuild flag, args.to_a
641
+ end
538
642
  else
539
643
  format_desc action, flag, 'opts*,args*'
540
644
  task flag do |_, args|
@@ -554,6 +658,18 @@ module Squared
554
658
  __send__(action == 'refs' ? :ls_remote : :ls_files, flag, args.to_a)
555
659
  end
556
660
  end
661
+ when 'git'
662
+ format_desc(action, flag, 'opts*', before: case flag
663
+ when :rm
664
+ 'source+,destination'
665
+ when :revert
666
+ 'commit+'
667
+ else
668
+ 'pathspec*'
669
+ end)
670
+ task flag do |_, args|
671
+ git(flag, args.to_a)
672
+ end
557
673
  end
558
674
  end
559
675
  end
@@ -566,8 +682,18 @@ module Squared
566
682
  super
567
683
  end
568
684
 
685
+ def depend(*, **)
686
+ workspace.rev_clear name
687
+ super
688
+ end
689
+
690
+ def clean(*, **)
691
+ workspace.rev_clear name
692
+ super
693
+ end
694
+
569
695
  def pull(flag = nil, opts = [], sync: invoked_sync?('pull', flag), remote: nil)
570
- cmd = git_session 'pull', flag && "--#{flag}"
696
+ cmd, opts = git_session('pull', flag && "--#{flag}", opts: opts)
571
697
  if (val = option('rebase', ignore: false))
572
698
  cmd << case val
573
699
  when '0'
@@ -580,8 +706,8 @@ module Squared
580
706
  no: OPT_GIT[:no][:pull] + OPT_GIT[:no][:fetch][:pull], remote: remote, flag: flag)
581
707
  source(sync: sync, sub: if verbose
582
708
  [
583
- { pat: /^(.+)(\|\s+\d+\s+)([^-]*)(-+)(.*)$/, styles: :red, index: 4 },
584
- { pat: /^(.+)(\|\s+\d+\s+)(\++)(-*)(.*)$/, styles: :green, index: 3 }
709
+ { pat: /^(.+)(\|\s+\d+\s+)([^-]*)(-+)(.*)$/, styles: color(:red), index: 4 },
710
+ { pat: /^(.+)(\|\s+\d+\s+)(\++)(-*)(.*)$/, styles: color(:green), index: 3 }
585
711
  ]
586
712
  end, **threadargs)
587
713
  end
@@ -590,7 +716,7 @@ module Squared
590
716
  command: nil)
591
717
  return pull(:rebase, sync: sync) unless flag
592
718
 
593
- cmd = git_session 'rebase'
719
+ cmd, opts = git_session('rebase', opts: opts)
594
720
  case flag
595
721
  when :branch
596
722
  branch = option_sanitize(opts, OPT_GIT[:rebase], no: OPT_GIT[:no][:rebase]).first
@@ -619,7 +745,7 @@ module Squared
619
745
  end
620
746
 
621
747
  def fetch(flag = nil, opts = [], sync: invoked_sync?('fetch', flag), remote: nil)
622
- cmd = git_session 'fetch'
748
+ cmd, opts = git_session('fetch', opts: opts)
623
749
  cmd << '--all' if !remote && !opts.include?('multiple') && option('all')
624
750
  cmd << '--verbose' if verbose && !opts.include?('quiet')
625
751
  append_pull(opts, collect_hash(OPT_GIT[:fetch]), no: collect_hash(OPT_GIT[:no][:fetch]),
@@ -651,22 +777,14 @@ module Squared
651
777
 
652
778
  def stash(flag = nil, opts = [], sync: invoked_sync?('stash', flag))
653
779
  if flag
654
- cmd = git_session 'stash', flag
655
- list = OPT_GIT[:stash][:common] + OPT_GIT[:stash].fetch(flag, [])
656
- refs = option_sanitize(opts, list).first
780
+ cmd, opts = git_session('stash', flag, opts: opts)
781
+ refs = option_sanitize(opts, OPT_GIT[:stash][:common] + OPT_GIT[:stash].fetch(flag, [])).first
657
782
  case flag
658
783
  when :push
659
784
  append_pathspec refs
660
785
  when :pop, :apply, :drop
661
- unless refs.empty?
662
- cmd << shell_escape(refs.pop)
663
- option_clear refs
664
- end
665
- when :clear
666
- if confirm("Remove #{sub_style('all', styles: theme[:active])} the stash entries? [y/N] ", 'N')
667
- source(stdout: true)
668
- end
669
- return
786
+ cmd << shell_quote(refs.pop)
787
+ option_clear refs
670
788
  when :list
671
789
  out, banner, from = source(io: true)
672
790
  print_item banner
@@ -675,14 +793,14 @@ module Squared
675
793
  return
676
794
  end
677
795
  else
678
- git_session 'stash', 'push'
796
+ git_session('stash', 'push', opts: opts)
679
797
  append_option(%w[all keep-index include-untracked staged].freeze, no: true, ignore: false)
680
798
  append_message option('message', 'm', ignore: false)
681
799
  end
682
800
  source(banner: !quiet?, sync: sync, **threadargs)
683
801
  end
684
802
 
685
- def status(*, sync: invoked_sync?('status'), **)
803
+ def status(*)
686
804
  cmd = git_session 'status'
687
805
  cmd << (option('long') ? '--long' : '--short')
688
806
  if (val = option('ignore-submodules', ignore: false))
@@ -699,24 +817,76 @@ module Squared
699
817
  end
700
818
  append_pathspec
701
819
  out, banner, from = source(io: true)
702
- if sync
703
- print_item banner
704
- banner = nil
705
- end
706
820
  ret = write_lines(out, banner: banner, sub: if verbose
821
+ r = color(:red)
822
+ g = color(:green)
707
823
  [
708
- { pat: /^(.)([A-Z?!])(.+)$/, styles: :red, index: 2 },
709
- { pat: /^([A-Z?!])(.+)$/, styles: :green },
710
- { pat: /^(\?\?)(.+)$/, styles: :red },
824
+ { pat: /^(.)([A-Z?!])(.+)$/, styles: r, index: 2 },
825
+ { pat: /^([A-Z?!])(.+)$/, styles: g },
826
+ { pat: /^(\?\?)(.+)$/, styles: r },
711
827
  { pat: /^(## )(.+)(\.{3})(.+)$/,
712
- styles: [nil, :green, nil, :red], index: -1 }
828
+ styles: [nil, g, nil, r], index: -1 }
713
829
  ]
714
830
  end)
715
831
  list_result(ret, 'files', from: from, action: 'modified')
716
832
  end
717
833
 
834
+ def revbuild(flag = nil, opts = [], sync: invoked_sync?('revbuild', flag), **kwargs)
835
+ statusargs = lambda do
836
+ {
837
+ include: relativepath(as_a(kwargs[:include]), all: true),
838
+ exclude: relativepath(as_a(kwargs[:exclude]), all: true)
839
+ }
840
+ end
841
+ unless workspace.closed
842
+ if @revbuild
843
+ statusargs.().each { |key, val| @revbuild[key] += val }
844
+ else
845
+ @revbuild = statusargs.()
846
+ end
847
+ return
848
+ end
849
+ sha = source(git_output('rev-parse --verify HEAD'), io: true, banner: false, stdout: true).first.to_s.chomp
850
+ return if sha.empty?
851
+
852
+ args = []
853
+ kwargs = kwargs.key?(:include) || kwargs.key?(:exclude) ? statusargs.() : @revbuild || {}
854
+ case flag
855
+ when :build
856
+ opts = option_sanitize(opts, OPT_GIT[:status], target: args).first
857
+ option_clear opts
858
+ else
859
+ args << basic_option('untracked-files', flag) if (flag = option('untracked-files', prefix: 'git'))
860
+ args << basic_option('ignore-submodules', flag) if (flag = option('ignore-submodules', prefix: 'git'))
861
+ args << basic_option('ignored', flag) if (flag = option('ignored', prefix: 'git'))
862
+ end
863
+ if (cur = workspace.rev_entry(name)) && cur['revision'] == sha
864
+ files = status_digest(*args, **kwargs)
865
+ if cur['files'].size == files.size && cur['files'].find { |key, val| files[key] != val }.nil?
866
+ if verbose
867
+ if (since = workspace.rev_timesince(name, 'build'))
868
+ puts log_message(Logger::INFO, name, 'no changes', subject: 'revbuild', hint: "#{since} ago")
869
+ else
870
+ workspace.rev_timeutc(name, 'build')
871
+ end
872
+ end
873
+ return
874
+ end
875
+ end
876
+ start = epochtime
877
+ build(@output, sync: sync, from: :'git:revbuild')
878
+ rescue StandardError => e
879
+ warn log_message(Logger::WARN, e, pass: true) if warning?
880
+ else
881
+ if verbose
882
+ msg = sub_style('completed', styles: theme[:active])
883
+ puts log_message(Logger::INFO, name, msg, subject: 'revbuild', hint: time_format(epochtime - start))
884
+ end
885
+ workspace.rev_write(name, { 'revision' => sha, 'files' => status_digest(*args, **kwargs) }, utc: 'build')
886
+ end
887
+
718
888
  def reset(flag, opts = [], refs: nil, ref: nil, mode: nil, commit: nil)
719
- cmd = git_session 'reset'
889
+ cmd, opts = git_session('reset', opts: opts)
720
890
  case flag
721
891
  when :commit, :index
722
892
  out = option_sanitize(opts, OPT_GIT[:reset] + VAL_GIT[:reset], no: OPT_GIT[:no][:reset]).first
@@ -741,21 +911,19 @@ module Squared
741
911
  return
742
912
  end
743
913
  unless ref == false
744
- append_commit ref
914
+ append_commit(ref, head: true)
745
915
  append_pathspec refs if refs
746
916
  end
747
917
  source
748
918
  end
749
919
 
750
920
  def checkout(flag, opts = [], branch: nil, origin: nil, create: nil, commit: nil, detach: nil)
751
- cmd = git_session 'checkout'
921
+ cmd, opts = git_session('checkout', opts: opts)
752
922
  append_option 'force', 'merge'
753
923
  case flag
754
924
  when :branch
755
925
  cmd << '--detach' if detach == 'd' || option('detach')
756
- if (val = option('track'))
757
- cmd << shell_option('track', val)
758
- end
926
+ append_option('track', equals: true)
759
927
  cmd << if create
760
928
  shell_option(create, branch)
761
929
  else
@@ -787,7 +955,7 @@ module Squared
787
955
  end
788
956
 
789
957
  def tag(flag, opts = [], refs: [], message: nil, commit: nil)
790
- cmd = git_session 'tag'
958
+ cmd, opts = git_session('tag', opts: opts)
791
959
  case flag
792
960
  when :add
793
961
  if option('sign')
@@ -795,8 +963,8 @@ module Squared
795
963
  elsif !session_arg?('s', 'sign', 'u', 'local-user')
796
964
  cmd << '--annotate'
797
965
  end
798
- if !commit && message && (hash = commithash(message))
799
- commit = hash
966
+ if !commit && message && (sha = commithash(message))
967
+ commit = sha
800
968
  else
801
969
  append_message message
802
970
  end
@@ -819,41 +987,27 @@ module Squared
819
987
  source
820
988
  end
821
989
 
822
- def logx(flag, opts = [], range: [])
823
- cmd = git_session 'log'
824
- files = option_sanitize(opts, collect_hash(OPT_GIT[:log]), no: collect_hash(OPT_GIT[:no][:log])).first
990
+ def logx(flag, opts = [], range: [], index: [])
991
+ cmd, opts = git_session('log', opts: opts)
992
+ refs = option_sanitize(opts, collect_hash(OPT_GIT[:log]), no: collect_hash(OPT_GIT[:no][:log])).first
825
993
  case flag
826
994
  when :between, :contain
827
995
  cmd << shell_quote(range.join(flag == :between ? '..' : '...'))
828
996
  else
829
- commit, files = files.partition do |val|
830
- val.start_with?('^') || (!%r{^[.\\/]}.match?(val) && !%r{[\\/]$}.match?(val)) || commithash(val)
831
- end
832
- cmd.merge(commit.map { |val| commithash(val) || shell_quote(val) }) unless commit.empty?
997
+ cmd.merge(index)
833
998
  end
834
999
  append_nocolor
835
- append_pathspec files
1000
+ append_pathspec refs
836
1001
  source(exception: false)
837
1002
  end
838
1003
 
839
- def diff(flag, opts = [], refs: [], branch: nil, range: [])
840
- cmd = git_session 'diff'
1004
+ def diff(flag, opts = [], refs: [], branch: nil, range: [], index: [])
1005
+ cmd, opts = git_session('diff', opts: opts)
841
1006
  files = option_sanitize(opts, collect_hash(OPT_GIT[:diff]) + OPT_GIT[:log][:diff],
842
1007
  no: OPT_GIT[:no][:log][:diff]).first
843
1008
  case flag
844
1009
  when :files, :view, :between, :contain
845
1010
  cmd.delete('--cached')
846
- else
847
- items = files.dup
848
- sha = nil
849
- files.clear
850
- items.each do |val|
851
- if (s = commithash(val))
852
- (sha ||= []).push(s)
853
- else
854
- files << val
855
- end
856
- end
857
1011
  end
858
1012
  append_nocolor
859
1013
  if flag == :files
@@ -868,15 +1022,14 @@ module Squared
868
1022
  cmd.delete('--merge-base')
869
1023
  cmd << shell_quote(range.join(flag == :between ? '..' : '...'))
870
1024
  else
871
- cmd << '--cached' if flag == :cached
872
1025
  cmd << '--merge-base' if option('merge-base')
873
1026
  cmd << shell_quote(branch) if branch
874
- if sha
1027
+ if !index.empty?
875
1028
  if session_arg?('cached')
876
- raise_error('diff', sha.join(', '), hint: 'one commit') if sha.size > 1
877
- cmd << sha.first
1029
+ raise_error("one commit only: #{index.join(', ')}", hint: '--cached') if index.size > 1
1030
+ cmd << index.first
878
1031
  else
879
- cmd.merge(sha)
1032
+ cmd.merge(index)
880
1033
  end
881
1034
  elsif (n = option('index'))
882
1035
  cmd << "HEAD~#{n}"
@@ -893,12 +1046,12 @@ module Squared
893
1046
  if !message && !amend
894
1047
  return if pass
895
1048
 
896
- raise_error('commit', 'GIT_MESSAGE="description"', hint: 'missing')
1049
+ raise_error('missing message', hint: 'GIT_MESSAGE="description"')
897
1050
  end
898
1051
  pathspec = if flag == :all || (amend && refs.size == 1 && refs.first == '*')
899
1052
  '--all'
900
1053
  elsif (refs = projectmap(refs)).empty?
901
- raise_error('commit', 'pathspec', hint: 'missing')
1054
+ raise_error 'no qualified pathspec'
902
1055
  else
903
1056
  "-- #{refs.join(' ')}"
904
1057
  end
@@ -907,13 +1060,15 @@ module Squared
907
1060
  upstream = nil
908
1061
  source(git_output('fetch --no-tags --quiet'), io: true, banner: false)
909
1062
  source(git_output('branch -vv --list'), io: true, banner: false).first.each do |val|
910
- next unless (data = /^\*\s(\S+)\s+(\h+)(?:\s\[(.+?)(?=\]\s)\])?\s/.match(val))
1063
+ next unless (r = /^\*\s(\S+)\s+(\h+)(?:\s\[(.+?)(?=\]\s)\])?\s/.match(val))
911
1064
 
912
- branch = data[1]
913
- if !data[3]
1065
+ branch = r[1]
1066
+ if r[3]
1067
+ origin = r[3][%r{^(.+)/#{Regexp.escape(branch)}$}, 1]
1068
+ else
914
1069
  unless (origin = option('repository', prefix: 'git', ignore: false))
915
- out = source(git_output('log -n1 --format=%h%d'), io: true, stdout: true, banner: false).first
916
- if out =~ /^#{data[2]} \(HEAD -> #{Regexp.escape(branch)}, (.+?)\)$/
1070
+ out = source(git_output('log -n1 --format=%h%d'), io: true, banner: false, stdout: true).first
1071
+ if out =~ /^#{r[2]} \(HEAD -> #{Regexp.escape(branch)}, (.+?)\)$/
917
1072
  split_escape($1).each do |val|
918
1073
  next unless val.end_with?("/#{branch}")
919
1074
 
@@ -923,12 +1078,10 @@ module Squared
923
1078
  end
924
1079
  end
925
1080
  upstream = true if origin
926
- elsif data[3] =~ %r{^(.+)/#{Regexp.escape(branch)}$}
927
- origin = $1
928
1081
  end
929
1082
  break
930
1083
  end
931
- raise_error('commit', 'work tree is not usable') unless origin && branch
1084
+ raise_error 'work tree is not usable' unless origin && branch
932
1085
  cmd = git_session('commit', option('dry-run') && '--dry-run', options: false)
933
1086
  if amend
934
1087
  cmd << '--amend'
@@ -955,8 +1108,24 @@ module Squared
955
1108
  source b
956
1109
  end
957
1110
 
958
- def branch(flag, opts = [], refs: [], ref: nil, target: nil)
959
- cmd = git_session 'branch'
1111
+ def merge(flag, opts = [], command: nil)
1112
+ cmd, opts = git_session('merge', opts: opts)
1113
+ case flag
1114
+ when :commit, :'no-commit'
1115
+ refs = option_sanitize(opts, OPT_GIT[:merge], no: OPT_GIT[:no][:merge]).first
1116
+ raise_error 'no branch/commit' if refs.empty?
1117
+ cmd << "--#{flag}" << '--'
1118
+ append_commit(*refs)
1119
+ else
1120
+ return unless VAL_GIT[:merge][:send].include?(command)
1121
+
1122
+ cmd << "--#{command}"
1123
+ end
1124
+ source
1125
+ end
1126
+
1127
+ def branch(flag = nil, opts = [], refs: [], ref: nil, target: nil)
1128
+ cmd, opts = git_session('branch', opts: opts)
960
1129
  stdout = false
961
1130
  case flag
962
1131
  when :create
@@ -983,7 +1152,7 @@ module Squared
983
1152
  end
984
1153
  ref = nil
985
1154
  when :delete
986
- force, list = refs.partition { |val| val =~ /^[\^~]/ }
1155
+ force, list = refs.partition { |val| val.match?(/^[\^~]/) }
987
1156
  force.each do |val|
988
1157
  dr = val[0, 3]
989
1158
  d = dr.include?('^') ? '-D' : '-d'
@@ -1003,10 +1172,9 @@ module Squared
1003
1172
  cmd << '--edit-description'
1004
1173
  when :current
1005
1174
  cmd << '--show-current'
1006
- else
1007
- opts = option_sanitize(opts, OPT_GIT[:branch], no: OPT_GIT[:no][:branch]).first
1175
+ when :list
1008
1176
  grep = []
1009
- opts.each do |opt|
1177
+ option_sanitize(opts, OPT_GIT[:branch], no: OPT_GIT[:no][:branch]).first.each do |opt|
1010
1178
  if opt =~ /^(v+)$/
1011
1179
  cmd << "-#{$1}"
1012
1180
  else
@@ -1018,42 +1186,46 @@ module Squared
1018
1186
  out, banner, from = source(io: true)
1019
1187
  print_item banner
1020
1188
  ret = write_lines(out, sub: [
1021
- { pat: /^(\*\s+)(\S+)(\s*)$/, styles: :green, index: 2 },
1022
- { pat: %r{^(\s*)(remotes/\S+)(.*)$}, styles: :red, index: 2 }
1189
+ { pat: /^(\*\s+)(\S+)(\s*)$/, styles: color(:green), index: 2 },
1190
+ { pat: %r{^(\s*)(remotes/\S+)(.*)$}, styles: color(:red), index: 2 }
1023
1191
  ])
1024
1192
  list_result(ret, 'branches', from: from)
1025
1193
  return
1194
+ else
1195
+ head = source(git_output('rev-parse --abbrev-ref HEAD'), io: true, banner: false, stdout: true).first.chomp
1196
+ if head.empty?
1197
+ ret = 0
1198
+ else
1199
+ out, banner, from = source(cmd << '-vv --no-abbrev --list', io: true)
1200
+ ret = write_lines(out, grep: /^\*\s+#{Regexp.escape(head)}\s/, banner: banner, first: true) do |line|
1201
+ next line if stdin?
1202
+
1203
+ data = line.sub(/^\*\s+/, '').split(/\s+/)
1204
+ a = sub_style(data[0], styles: theme[:inline])
1205
+ b = sub_style(data[1], styles: theme[:extra])
1206
+ r = /\A(?:\[(.+?)(?=\]\s)\]\s)?(.+)\z/m.match(data[2..-1].join(' '))
1207
+ [" Branch: #{a + (r[1] ? " (#{r[1]})" : '')}", " Commit: #{b}", "Message: #{r[2]}"].compact.join("\n")
1208
+ end
1209
+ on :last, from
1210
+ end
1211
+ if ret == 0
1212
+ warn log_message(Logger::WARN, name, 'no ref found', subject: 'branch', hint: 'head', pass: true)
1213
+ end
1214
+ return
1026
1215
  end
1027
1216
  cmd << shell_escape(target) if target
1028
1217
  cmd << shell_escape(ref) if ref
1029
1218
  source(stdout: stdout)
1030
1219
  end
1031
1220
 
1032
- def restore(flag, opts = [], tree: nil)
1033
- cmd = git_session 'restore'
1034
- refs = option_sanitize(opts, OPT_GIT[:restore], no: OPT_GIT[:no][:restore]).first
1035
- if flag == :source
1036
- cmd << '--patch' if refs.empty?
1037
- cmd << shell_option('source', tree)
1038
- else
1039
- cmd << "--#{flag}"
1040
- end
1041
- if session_arg?('p', 'patch')
1042
- option_clear refs
1043
- else
1044
- append_pathspec(refs, expect: true)
1045
- end
1046
- source(sync: false, stderr: true)
1047
- end
1048
-
1049
1221
  def show(format, opts = [])
1050
- cmd = git_session 'show'
1222
+ cmd, opts = git_session('show', opts: opts)
1051
1223
  if format
1052
1224
  case (val = format.downcase)
1053
1225
  when 'oneline', 'short', 'medium', 'full', 'fuller', 'reference', 'email', 'raw'
1054
1226
  cmd << basic_option('format', val)
1055
1227
  else
1056
- if format =~ /^t?format:/ || format.include?('%')
1228
+ if format.match?(/^t?format:/) || format.include?('%')
1057
1229
  cmd << quote_option('pretty', format)
1058
1230
  else
1059
1231
  opts << format
@@ -1071,18 +1243,19 @@ module Squared
1071
1243
  end
1072
1244
 
1073
1245
  def rev_parse(flag, opts = [], ref: nil, size: nil)
1074
- cmd = git_session 'rev-parse', if flag == :parseopt
1075
- '--parseopt'
1076
- elsif opts.delete('sq-quote')
1077
- '--sq-quote'
1078
- end
1246
+ cmd, opts = git_session('rev-parse', opts: opts)
1247
+ cmd << if flag == :parseopt
1248
+ '--parseopt'
1249
+ elsif opts.delete('sq-quote')
1250
+ '--sq-quote'
1251
+ end
1079
1252
  case flag
1080
1253
  when :commit
1081
1254
  cmd << ((n = size.to_i) > 0 ? basic_option('short', [n, 5].max) : '--verify')
1082
- append_commit ref
1255
+ append_commit(ref, head: true)
1083
1256
  when :branch
1084
1257
  cmd << '--abbrev-ref'
1085
- append_commit ref
1258
+ append_commit(ref, head: true)
1086
1259
  else
1087
1260
  args = option_sanitize(opts, OPT_GIT[:rev_parse][flag]).first
1088
1261
  append_value(args, escape: session_arg?('sq-quote'))
@@ -1091,7 +1264,7 @@ module Squared
1091
1264
  end
1092
1265
 
1093
1266
  def ls_remote(flag, opts = [], remote: nil)
1094
- cmd = git_session 'ls-remote', '--refs'
1267
+ cmd, opts = git_session('ls-remote', '--refs', opts: opts)
1095
1268
  cmd << "--#{flag}" unless flag == :remote
1096
1269
  grep = option_sanitize(opts, OPT_GIT[:ls_remote]).first
1097
1270
  cmd << shell_quote(remote) if remote
@@ -1102,7 +1275,7 @@ module Squared
1102
1275
  end
1103
1276
 
1104
1277
  def ls_files(flag, opts = [])
1105
- git_session 'ls-files', "--#{flag}"
1278
+ opts = git_session('ls-files', "--#{flag}", opts: opts).last
1106
1279
  grep = option_sanitize(opts, OPT_GIT[:ls_files]).first
1107
1280
  out, banner, from = source(io: true)
1108
1281
  print_item banner
@@ -1110,10 +1283,51 @@ module Squared
1110
1283
  list_result(ret, 'files', from: from, grep: grep)
1111
1284
  end
1112
1285
 
1286
+ def git(flag, opts = [])
1287
+ cmd, opts = git_session(flag, opts: opts)
1288
+ refs = option_sanitize(opts, OPT_GIT[flag], no: OPT_GIT[:no][flag]).first
1289
+ refs = projectmap(refs) unless flag == :revert
1290
+ sync = false
1291
+ stderr = true
1292
+ case flag
1293
+ when :clean
1294
+ unless refs.empty?
1295
+ cmd << '--'
1296
+ cmd.merge(refs)
1297
+ end
1298
+ sync = true
1299
+ stderr = false
1300
+ when :restore
1301
+ if session_arg?('p', 'patch')
1302
+ option_clear refs
1303
+ else
1304
+ append_pathspec(refs, expect: true)
1305
+ end
1306
+ when :revert
1307
+ if VAL_GIT[:rebase][:send].any? { |val| session_arg?(val) }
1308
+ option_clear refs
1309
+ elsif refs.empty?
1310
+ raise_error 'no commit given'
1311
+ else
1312
+ append_commit(*refs)
1313
+ end
1314
+ when :mv
1315
+ raise_error 'no source/destination' unless refs.size > 1
1316
+ cmd.merge(refs)
1317
+ when :rm
1318
+ append_pathspec(refs, expect: true)
1319
+ end
1320
+ source(sync: sync, stderr: stderr)
1321
+ end
1322
+
1113
1323
  def clone?
1114
1324
  ref?(workspace.baseref) && workspace.git_clone?(path, name) ? 1 : false
1115
1325
  end
1116
1326
 
1327
+ def revbuild?
1328
+ build? && !!workspace.revfile
1329
+ end
1330
+
1117
1331
  def enabled?(*, **kwargs)
1118
1332
  super || (kwargs[:base] == false && !!clone?)
1119
1333
  end
@@ -1126,7 +1340,7 @@ module Squared
1126
1340
  if cmd.respond_to?(:done)
1127
1341
  if io && banner == false
1128
1342
  from = nil
1129
- elsif !from && (from = cmd.drop(1).find { |val| val =~ /^[a-z][a-z\-]{2,}$/ })
1343
+ elsif !from && (from = cmd.drop(1).find { |val| val.match?(/^[a-z][a-z\-]{2,}$/) })
1130
1344
  from = :"git:#{from}"
1131
1345
  end
1132
1346
  banner &&= cmd.temp { |val| val.start_with?('--work-tree') || val.start_with?('--git-dir') }
@@ -1172,13 +1386,13 @@ module Squared
1172
1386
  ret = on(:error, from, e)
1173
1387
  raise if exception && ret != true
1174
1388
 
1175
- warn log_message(Logger::WARN, e) if warning?
1389
+ warn log_message(Logger::WARN, e, pass: true) if warning?
1176
1390
  else
1177
1391
  on :last, from
1178
1392
  end
1179
1393
  end
1180
1394
 
1181
- def write_lines(data, banner: nil, loglevel: nil, grep: nil, sub: nil, pass: false)
1395
+ def write_lines(data, banner: nil, loglevel: nil, grep: nil, sub: nil, pass: false, first: false)
1182
1396
  grep = as_a(grep).map do |val|
1183
1397
  next val if val.is_a?(Regexp)
1184
1398
 
@@ -1191,6 +1405,7 @@ module Squared
1191
1405
  data.each do |line|
1192
1406
  next if grep&.none? { |pat| pat.match?(line) }
1193
1407
 
1408
+ line = yield line if block_given?
1194
1409
  if loglevel
1195
1410
  log&.add loglevel, line
1196
1411
  else
@@ -1202,8 +1417,9 @@ module Squared
1202
1417
  end
1203
1418
  end
1204
1419
  ret += 1
1420
+ break if first
1205
1421
  end
1206
- print_item banner, out if banner && (ret > 0 || !pass)
1422
+ print_item banner, out if banner && (ret > 0 || (!pass && !first))
1207
1423
  ret
1208
1424
  end
1209
1425
 
@@ -1226,6 +1442,21 @@ module Squared
1226
1442
  on :last, from
1227
1443
  end
1228
1444
 
1445
+ def status_digest(*args, algorithm: Digest::SHA256, **kwargs)
1446
+ glob = kwargs.fetch(:include, [])
1447
+ pass = kwargs.fetch(:exclude, [])
1448
+ ret = {}
1449
+ out = source(git_output('status -s --porcelain', *args), io: true, banner: false).first
1450
+ out.each do |line|
1451
+ next unless (file = line[/^[A-Z ?!]{3}"?(.+?)"?$/, 1])
1452
+ next if !glob.empty? && glob.none? { |val| File.fnmatch?(val, file, File::FNM_DOTMATCH) }
1453
+ next if !pass.empty? && pass.any? { |val| File.fnmatch?(val, file, File::FNM_DOTMATCH) }
1454
+
1455
+ ret[file] = algorithm.hexdigest(File.read(basepath(file)))
1456
+ end
1457
+ ret
1458
+ end
1459
+
1229
1460
  def append_pull(opts, list, target: @session, no: nil, flag: nil, remote: nil)
1230
1461
  cmd << '--force' if option('force')
1231
1462
  rsm = append_submodules(target: target)
@@ -1265,9 +1496,13 @@ module Squared
1265
1496
  option_clear(out, target: target, subject: flag.to_s) if flag
1266
1497
  end
1267
1498
 
1268
- def append_commit(val, target: @session)
1269
- val = val.to_s.strip
1270
- target << (val.empty? ? append_head(target: target) || 'HEAD' : val)
1499
+ def append_commit(*val, target: @session, head: false)
1500
+ val.compact!
1501
+ if !val.empty?
1502
+ val.each { |ref| target << (commithash(ref) || shell_quote(ref)) }
1503
+ elsif head
1504
+ target << (append_head(target: target) || 'HEAD')
1505
+ end
1271
1506
  end
1272
1507
 
1273
1508
  def append_pathspec(files = [], target: @session, expect: false, parent: false)
@@ -1279,9 +1514,9 @@ module Squared
1279
1514
  end
1280
1515
  files = projectmap(files, parent: parent)
1281
1516
  if !files.empty?
1282
- target << "-- #{files.join(' ')}"
1517
+ target << '--' << files.join(' ')
1283
1518
  elsif expect
1284
- raise_error(parent ? 'pathspec not present' : 'pathspec not within worktree', hint: 'invalid')
1519
+ raise_error(parent ? 'pathspec not present' : 'pathspec not within worktree')
1285
1520
  end
1286
1521
  end
1287
1522
  end
@@ -1316,9 +1551,13 @@ module Squared
1316
1551
  end
1317
1552
  end
1318
1553
 
1319
- def git_session(*cmd, worktree: true, **kwargs)
1554
+ def git_session(*cmd, opts: nil, worktree: true, **kwargs)
1320
1555
  dir = worktree ? ["--work-tree=#{shell_quote(path)}", "--git-dir=#{shell_quote(gitpath)}"] : []
1321
- session('git', *dir, *cmd, **kwargs)
1556
+ ret = session('git', *dir, **kwargs)
1557
+ return ret.merge(cmd) unless opts
1558
+
1559
+ opts = option_sanitize(opts, OPT_GIT[:common]).first
1560
+ [ret.merge(cmd), opts]
1322
1561
  end
1323
1562
 
1324
1563
  def git_output(*cmd, **kwargs)
@@ -1336,11 +1575,11 @@ module Squared
1336
1575
  end
1337
1576
 
1338
1577
  def gitpath
1339
- basepath('.git')
1578
+ basepath '.git'
1340
1579
  end
1341
1580
 
1342
1581
  def commithash(val)
1343
- val =~ /\A#\{(\h{5,40})\}\z/ ? $1 : nil
1582
+ val[/^#\{(\h{5,40})\}$/, 1]
1344
1583
  end
1345
1584
 
1346
1585
  def threadargs