squared 0.3.6 → 0.4.1

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,11 +85,61 @@ 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
 
@@ -75,12 +148,15 @@ module Squared
75
148
  include Prompt
76
149
 
77
150
  OPT_GIT = {
151
+ common: %w[bare p|paginate P|no-pager glob-pathspecs icase-pathspecs literal-pathspecs no-optional-locks
152
+ no-replace-objects noglob-pathspecs c=q config-env=q exec-path=p namespace=p].freeze,
78
153
  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
154
+ contains=b format=q merged=b no-contains=b no-merged=b points-at=e u|set-upstream-to=e sort=q
80
155
  t|track=b].freeze,
81
156
  checkout: %w[l d|detach f|force ignore-other-worktrees ignore-skip-worktree-bits m|merge p|patch
82
157
  pathspec-file-nul q quiet orphan=e ours theirs conflict=b pathspec-from-file=p
83
158
  t|track=b].freeze,
159
+ clean: %w[d x X f|force i|interactive n|dry-run q|quiet e|exlcude=q].freeze,
84
160
  diff: {
85
161
  base: %w[0 1|base 2|ours 3|theirs].freeze,
86
162
  show: %w[s exit-code histogram].freeze
@@ -90,7 +166,7 @@ module Squared
90
166
  recurse-submodules-default=b].freeze,
91
167
  pull: %w[4 6 n t a|append atomic dry-run f|force k|keep n|negotiate-only prefetch p|prune q|quiet
92
168
  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
169
+ refmap=q o|server-option=q shallow-exclude=e shallow-since=b upload-pack=q].freeze
94
170
  }.freeze,
95
171
  log: {
96
172
  base: %w[all all-match alternate-refs author-date-order basic-regexp bisect boundary cherry cherry-mark
@@ -98,7 +174,7 @@ module Squared
98
174
  first-parent F|fixed-strings follow full-diff full-history ignore-missing invert-grep left-only
99
175
  merge log-size no-max-parents no-min-parents not P|perl-regexp reflog i|regexp-ignore-case
100
176
  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
177
+ source sparse stdin topo-order g|walk-reflogs after=q ancestry-path=b? author=q before=q
102
178
  branches=q? committer=q decorate=b decorate-refs=q decorate-refs-exclude=q exclude=q
103
179
  exclude-hidden=b? glob=q grep=q grep-reflog=q L=q n|max-count=i max-parents=i min-parents=i
104
180
  no-walk=b? remotes=q? since=q since-as-filter=q skip=i tags=q? until=q].freeze,
@@ -109,9 +185,9 @@ module Squared
109
185
  W|function-context w|ignore-all-space ignore-blank-lines ignore-cr-at-eol ignore-space-at-eol
110
186
  b|ignore-space-change D|irreversible-delete graph ita-invisible-in-index minimal name-only
111
187
  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?
188
+ pickaxe-all pickaxe-regex raw shortstat summary a|text abbrev=i? anchored=q B|break-rewrites=b?
113
189
  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?
190
+ dirstat-by-file=q? dst-prefix=q C|find-copies=i? find-object=b M|find-renames=i?
115
191
  I|ignore-matching-lines=q ignore-submodules=b inter-hunk-context=i line-prefix=q output=p
116
192
  output-indicator-context=q output-indicator-new=q output-indicator-old=q relative=p rotate-to=p
117
193
  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 +196,32 @@ module Squared
120
196
  ls_files: %w[z debug deduplicate directory eol error-unmatch exclude-standard full-name k|killed
121
197
  no-empty-directory recurse-submodules sparse s|stage u|unmerged abbrev=i x|exclude=q
122
198
  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,
199
+ ls_remote: %w[exit-code get-url q|quiet o|server-option=q symref sort=q upload-pack=q].freeze,
200
+ mv: %w[k f|force n|dry-run v|verbose].freeze,
201
+ pull: %w[e n allow-unrelated-histories ff-only S|gpg-sign=qq log=i r|rebase=b? s|strategy=b
202
+ X|strategy-option=b].freeze,
203
+ merge: %w[e n allow-unrelated-histories ff-only m=q q|quiet v|verbose cleanup=b F|file=p S|gpg-sign=qq
204
+ into-name=e log=i s|strategy=b X|strategy-option=b].freeze,
126
205
  rebase: %w[n C=i allow-empty-message apply committer-date-is-author-date edit-todo f|force-rebase ignore-date
127
206
  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,
207
+ reset-author-date root show-current-patch signoff v|verbose empty=b S|gpg-sign=qq onto=e
208
+ s|strategy=b X|strategy-option=b whitespace=b].freeze,
130
209
  reset: %w[N pathspec-file-nul q|quiet pathspec-from-file=p].freeze,
131
210
  restore: %w[ignore-unmerged ignore-skip-worktree-bits m|merge ours p|patch pathspec-file-nul S|staged theirs
132
211
  W|worktree conflict=b pathspec-from-file=p s|source=q].freeze,
212
+ revert: %w[e abort continue no-commit quit reference skip cleanup=b S|gpg-sign=qq m|mainline=i s|signoff
213
+ strategy=b X|strategy-option=b].freeze,
133
214
  rev_parse: {
134
215
  output: %w[absolute-git-dir all flags git-common-dir git-dir is-bare-repository is-inside-git-dir
135
216
  is-inside-work-tree is-shallow-repository local-env-vars no-flags no-revs not q|quiet sq
136
217
  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
218
+ sq-quote symbolic symbolic-full-name verify abbrev-ref=b? after=q before=q default=q
138
219
  disambiguate=b exclude=q exclude-hidden=b glob=q git-path=p path-format=b? prefix=q branches=q?
139
220
  remotes=q? resolve-git-dir=p short=i? show-object-format=b? since=q tags=q? until=q].freeze,
140
221
  parseopt: %w[keep-dashdash stop-at-non-option stuck-long].freeze
141
222
  }.freeze,
223
+ rm: %w[r cached f|force n|dry-run ignore-unmatch pathspec-file-nul q|quiet sparse v|verbose
224
+ pathspec-from-file=p].freeze,
142
225
  show: %w[t combined-all-paths no-diff-merges remerge-diff show-signature diff-merges=b encoding=b
143
226
  expand-tabs=i notes=q show-notes=q?].freeze,
144
227
  stash: {
@@ -148,7 +231,8 @@ module Squared
148
231
  pop: %w[index].freeze,
149
232
  apply: %w[index].freeze
150
233
  }.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
234
+ status: %w[untracked-files=b? u|ignore-submodules=m? ignored=b?],
235
+ tag: %w[create-reflog column=b contains=b? format=q merged=b? n=i no-contains=b? no-merged=b? points-at=q
152
236
  sort=q].freeze,
153
237
  no: {
154
238
  fetch: {
@@ -164,20 +248,26 @@ module Squared
164
248
  tag: %w[column].freeze,
165
249
  branch: %w[color-moved column color track].freeze,
166
250
  checkout: %w[overwrite-ignore guess overlay progress recurse-submodules track].freeze,
251
+ merge: %w[autostash edit ff gpg-sign log progress overwrite-ignore rerere-autoupdate signoff squash stat
252
+ verify verify-signatures].freeze,
167
253
  rebase: %w[autosquash autostash fork-point gpg-sign keep-empty reapply-cherry-picks reschedule-failed-exec
168
254
  rerere-autoupdate stat update-refs verify].freeze,
169
255
  reset: %w[refresh].freeze,
170
256
  restore: %w[overlay progress recurse-submodules].freeze,
257
+ revert: %w[edit gpg-sign rerere-autoupdate].freeze,
171
258
  show: %w[standard-notes].freeze
172
259
  }.freeze
173
- }
260
+ }.freeze
174
261
  VAL_GIT = {
262
+ merge: {
263
+ send: %w[continue abort quit].freeze
264
+ }.freeze,
175
265
  rebase: {
176
266
  send: %w[continue skip abort quit].freeze,
177
267
  value: %w[true false merges interactive].freeze
178
268
  }.freeze,
179
269
  reset: %w[soft mixed hard merge keep recurse-submodules no-recurse-submodules].freeze
180
- }
270
+ }.freeze
181
271
  private_constant :OPT_GIT, :VAL_GIT
182
272
 
183
273
  class << self
@@ -209,11 +299,7 @@ module Squared
209
299
  end
210
300
 
211
301
  def tasks
212
- %i[pull rebase fetch clone stash status].freeze
213
- end
214
-
215
- def batchargs
216
- [ref, { 'pull+s': %i[stash pull], 'rebase+s': %i[stash rebase] }]
302
+ %i[pull rebase fetch clone stash status branch revbuild].freeze
217
303
  end
218
304
 
219
305
  def config?(val)
@@ -227,24 +313,25 @@ module Squared
227
313
  'branch' => %i[create set delete move copy list edit current].freeze,
228
314
  'checkout' => %i[commit branch track detach path].freeze,
229
315
  'commit' => %i[add all amend amend-orig].freeze,
230
- 'diff' => %i[head cached branch files view between contain].freeze,
316
+ 'diff' => %i[head branch files view between contain].freeze,
231
317
  'fetch' => %i[origin remote].freeze,
232
318
  'files' => %i[cached modified deleted others ignored].freeze,
319
+ 'git' => %i[clean mv restore revert rm].freeze,
233
320
  'log' => %i[view between contain].freeze,
321
+ 'merge' => %i[commit no-commit send].freeze,
234
322
  'pull' => %i[origin remote].freeze,
235
323
  'rebase' => %i[branch onto send].freeze,
236
324
  'refs' => %i[heads tags remote].freeze,
237
325
  'reset' => %i[commit index patch mode].freeze,
238
- 'restore' => %i[source worktree staged overlay].freeze,
239
- 'rev' => %i[commit branch output parseopt].freeze,
240
- 'show' => %i[format oneline].freeze,
326
+ 'rev' => %i[commit branch output parseopt build].freeze,
327
+ 'show' => %i[format oneline textconv].freeze,
241
328
  'stash' => %i[push pop apply drop clear list].freeze,
242
329
  'tag' => %i[add delete list].freeze
243
330
  }.freeze
244
331
 
245
332
  def initialize(*, **)
246
333
  super
247
- initialize_ref(Git.ref) if gitpath.exist?
334
+ initialize_ref Git.ref if gitpath.exist?
248
335
  end
249
336
 
250
337
  def ref
@@ -289,19 +376,6 @@ module Squared
289
376
  commit(flag, refs: refs)
290
377
  end
291
378
  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
379
  when 'tag'
306
380
  case flag
307
381
  when :list
@@ -335,9 +409,19 @@ module Squared
335
409
  case flag
336
410
  when :view, :between, :contain
337
411
  if flag == :view && action == 'log'
338
- format_desc action, flag, '(^)commit*,pathspec*,opts*'
412
+ format_desc action, flag, '(^)commit/H0*,pathspec*,opts*'
339
413
  task flag do |_, args|
340
- logx flag, args.to_a
414
+ index = []
415
+ args.to_a.each do |val|
416
+ if val =~ /^H(\d+)$/
417
+ index << "HEAD~#{$1}"
418
+ elsif (sha = commithash(val))
419
+ index << sha
420
+ elsif val.start_with?('^') || (!%r{^[.\\/]}.match?(val) && !%r{[\\/]$}.match?(val))
421
+ index << shell_quote(val)
422
+ end
423
+ end
424
+ logx(flag, args.to_a.drop(index.size), index: index)
341
425
  end
342
426
  else
343
427
  format_desc action, flag, 'commit1,commit2,pathspec*,opts*'
@@ -347,10 +431,16 @@ module Squared
347
431
  __send__(action == 'log' ? :logx : :diff, flag, args.extras, range: [commit1, commit2])
348
432
  end
349
433
  end
350
- when :head, :cached
351
- format_desc action, flag, 'opts*,pathspec*'
434
+ when :head
435
+ format_desc action, flag, 'commit/H0*,opts*,pathspec*'
352
436
  task flag do |_, args|
353
- diff flag, args.to_a
437
+ index = []
438
+ args.to_a.each do |val|
439
+ break unless val =~ /^H(\d+)$/ || (sha = commithash(val))
440
+
441
+ index << ($1 ? "HEAD~#{$1}" : sha)
442
+ end
443
+ diff(flag, args.to_a.drop(index.size), index: index)
354
444
  end
355
445
  when :branch
356
446
  format_desc action, flag, 'name,opts*,pathspec*'
@@ -487,15 +577,21 @@ module Squared
487
577
  when :oneline
488
578
  format_desc action, flag, 'opts*,object*'
489
579
  task flag do |_, args|
490
- show 'oneline', args.to_a.push('abbrev-commit')
580
+ show flag, args.to_a.push('abbrev-commit')
491
581
  end
492
582
  when :format
493
583
  format_desc action, flag, 'format?,opts*,object*'
494
584
  task flag, [:format] do |_, args|
495
- show args.format, args.extras
585
+ show(flag, args.extras, format: args.format)
586
+ end
587
+ when :textconv
588
+ format_desc action, flag, 'files+'
589
+ task flag do |_, args|
590
+ files = param_guard(action, flag, args: args.to_a)
591
+ show(flag, files: files)
496
592
  end
497
593
  end
498
- when 'rebase'
594
+ when 'rebase', 'merge'
499
595
  case flag
500
596
  when :branch
501
597
  format_desc action, flag, 'opts*,upstream?,branch?'
@@ -510,11 +606,18 @@ module Squared
510
606
  upstream = param_guard(action, flag, args: args, key: :upstream)
511
607
  rebase(flag, commit: commit, upstream: upstream, branch: args.branch)
512
608
  end
609
+ when :commit, :'no-commit'
610
+ format_desc action, flag, 'branch/commit+,opts*'
611
+ task flag do |_, args|
612
+ args = param_guard(action, flag, args: args.to_a)
613
+ merge flag, args
614
+ end
513
615
  when :send
514
- format_desc(action, flag, VAL_GIT[:rebase][:send], arg: nil)
616
+ format_desc(action, flag, VAL_GIT[action.to_sym][:send], arg: nil)
515
617
  task flag, [:command] do |_, args|
516
- command = param_guard(action, flag, args: args, key: :command)
517
- rebase(flag, command: command)
618
+ command = param_guard(action, flag, args: args, key: :command,
619
+ values: VAL_GIT[action.to_sym][:send])
620
+ __send__(action, flag, command: command)
518
621
  end
519
622
  end
520
623
  when 'rev'
@@ -535,6 +638,11 @@ module Squared
535
638
  task flag, [:ref] do |_, args|
536
639
  rev_parse(flag, ref: args.ref)
537
640
  end
641
+ when :build
642
+ format_desc action, flag, OPT_GIT[:status]
643
+ task flag do |_, args|
644
+ revbuild flag, args.to_a
645
+ end
538
646
  else
539
647
  format_desc action, flag, 'opts*,args*'
540
648
  task flag do |_, args|
@@ -554,6 +662,18 @@ module Squared
554
662
  __send__(action == 'refs' ? :ls_remote : :ls_files, flag, args.to_a)
555
663
  end
556
664
  end
665
+ when 'git'
666
+ format_desc(action, flag, 'opts*', before: case flag
667
+ when :rm
668
+ 'source+,destination'
669
+ when :revert
670
+ 'commit+'
671
+ else
672
+ 'pathspec*'
673
+ end)
674
+ task flag do |_, args|
675
+ git(flag, args.to_a)
676
+ end
557
677
  end
558
678
  end
559
679
  end
@@ -566,8 +686,18 @@ module Squared
566
686
  super
567
687
  end
568
688
 
689
+ def depend(*, **)
690
+ workspace.rev_clear name
691
+ super
692
+ end
693
+
694
+ def clean(*, **)
695
+ workspace.rev_clear name
696
+ super
697
+ end
698
+
569
699
  def pull(flag = nil, opts = [], sync: invoked_sync?('pull', flag), remote: nil)
570
- cmd = git_session 'pull', flag && "--#{flag}"
700
+ cmd, opts = git_session('pull', flag && "--#{flag}", opts: opts)
571
701
  if (val = option('rebase', ignore: false))
572
702
  cmd << case val
573
703
  when '0'
@@ -580,8 +710,8 @@ module Squared
580
710
  no: OPT_GIT[:no][:pull] + OPT_GIT[:no][:fetch][:pull], remote: remote, flag: flag)
581
711
  source(sync: sync, sub: if verbose
582
712
  [
583
- { pat: /^(.+)(\|\s+\d+\s+)([^-]*)(-+)(.*)$/, styles: :red, index: 4 },
584
- { pat: /^(.+)(\|\s+\d+\s+)(\++)(-*)(.*)$/, styles: :green, index: 3 }
713
+ { pat: /^(.+)(\|\s+\d+\s+)([^-]*)(-+)(.*)$/, styles: color(:red), index: 4 },
714
+ { pat: /^(.+)(\|\s+\d+\s+)(\++)(-*)(.*)$/, styles: color(:green), index: 3 }
585
715
  ]
586
716
  end, **threadargs)
587
717
  end
@@ -590,7 +720,7 @@ module Squared
590
720
  command: nil)
591
721
  return pull(:rebase, sync: sync) unless flag
592
722
 
593
- cmd = git_session 'rebase'
723
+ cmd, opts = git_session('rebase', opts: opts)
594
724
  case flag
595
725
  when :branch
596
726
  branch = option_sanitize(opts, OPT_GIT[:rebase], no: OPT_GIT[:no][:rebase]).first
@@ -619,7 +749,7 @@ module Squared
619
749
  end
620
750
 
621
751
  def fetch(flag = nil, opts = [], sync: invoked_sync?('fetch', flag), remote: nil)
622
- cmd = git_session 'fetch'
752
+ cmd, opts = git_session('fetch', opts: opts)
623
753
  cmd << '--all' if !remote && !opts.include?('multiple') && option('all')
624
754
  cmd << '--verbose' if verbose && !opts.include?('quiet')
625
755
  append_pull(opts, collect_hash(OPT_GIT[:fetch]), no: collect_hash(OPT_GIT[:no][:fetch]),
@@ -651,9 +781,8 @@ module Squared
651
781
 
652
782
  def stash(flag = nil, opts = [], sync: invoked_sync?('stash', flag))
653
783
  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
784
+ cmd, opts = git_session('stash', flag, opts: opts)
785
+ refs = option_sanitize(opts, OPT_GIT[:stash][:common] + OPT_GIT[:stash].fetch(flag, [])).first
657
786
  case flag
658
787
  when :push
659
788
  append_pathspec refs
@@ -675,14 +804,14 @@ module Squared
675
804
  return
676
805
  end
677
806
  else
678
- git_session 'stash', 'push'
807
+ git_session('stash', 'push', opts: opts)
679
808
  append_option(%w[all keep-index include-untracked staged].freeze, no: true, ignore: false)
680
809
  append_message option('message', 'm', ignore: false)
681
810
  end
682
811
  source(banner: !quiet?, sync: sync, **threadargs)
683
812
  end
684
813
 
685
- def status(*, sync: invoked_sync?('status'), **)
814
+ def status(*)
686
815
  cmd = git_session 'status'
687
816
  cmd << (option('long') ? '--long' : '--short')
688
817
  if (val = option('ignore-submodules', ignore: false))
@@ -699,24 +828,76 @@ module Squared
699
828
  end
700
829
  append_pathspec
701
830
  out, banner, from = source(io: true)
702
- if sync
703
- print_item banner
704
- banner = nil
705
- end
706
831
  ret = write_lines(out, banner: banner, sub: if verbose
832
+ r = color(:red)
833
+ g = color(:green)
707
834
  [
708
- { pat: /^(.)([A-Z?!])(.+)$/, styles: :red, index: 2 },
709
- { pat: /^([A-Z?!])(.+)$/, styles: :green },
710
- { pat: /^(\?\?)(.+)$/, styles: :red },
835
+ { pat: /^(.)([A-Z?!])(.+)$/, styles: r, index: 2 },
836
+ { pat: /^([A-Z?!])(.+)$/, styles: g },
837
+ { pat: /^(\?\?)(.+)$/, styles: r },
711
838
  { pat: /^(## )(.+)(\.{3})(.+)$/,
712
- styles: [nil, :green, nil, :red], index: -1 }
839
+ styles: [nil, g, nil, r], index: -1 }
713
840
  ]
714
841
  end)
715
842
  list_result(ret, 'files', from: from, action: 'modified')
716
843
  end
717
844
 
845
+ def revbuild(flag = nil, opts = [], sync: invoked_sync?('revbuild', flag), **kwargs)
846
+ statusargs = lambda do
847
+ {
848
+ include: relativepath(as_a(kwargs[:include]), all: true),
849
+ exclude: relativepath(as_a(kwargs[:exclude]), all: true)
850
+ }
851
+ end
852
+ unless workspace.closed
853
+ if @revbuild
854
+ statusargs.().each { |key, val| @revbuild[key] += val }
855
+ else
856
+ @revbuild = statusargs.()
857
+ end
858
+ return
859
+ end
860
+ sha = source(git_output('rev-parse --verify HEAD'), io: true, banner: false, stdout: true).first.to_s.chomp
861
+ return if sha.empty?
862
+
863
+ args = []
864
+ kwargs = kwargs.key?(:include) || kwargs.key?(:exclude) ? statusargs.() : @revbuild || {}
865
+ case flag
866
+ when :build
867
+ opts = option_sanitize(opts, OPT_GIT[:status], target: args).first
868
+ option_clear opts
869
+ else
870
+ args << basic_option('untracked-files', flag) if (flag = option('untracked-files', prefix: 'git'))
871
+ args << basic_option('ignore-submodules', flag) if (flag = option('ignore-submodules', prefix: 'git'))
872
+ args << basic_option('ignored', flag) if (flag = option('ignored', prefix: 'git'))
873
+ end
874
+ if (cur = workspace.rev_entry(name)) && cur['revision'] == sha
875
+ files = status_digest(*args, **kwargs)
876
+ if cur['files'].size == files.size && cur['files'].find { |key, val| files[key] != val }.nil?
877
+ if verbose
878
+ if (since = workspace.rev_timesince(name, 'build'))
879
+ puts log_message(Logger::INFO, name, 'no changes', subject: 'revbuild', hint: "#{since} ago")
880
+ else
881
+ workspace.rev_timeutc(name, 'build')
882
+ end
883
+ end
884
+ return
885
+ end
886
+ end
887
+ start = epochtime
888
+ build(@output, sync: sync, from: :'git:revbuild')
889
+ rescue StandardError => e
890
+ warn log_message(Logger::WARN, e, pass: true) if warning?
891
+ else
892
+ if verbose
893
+ msg = sub_style('completed', styles: theme[:active])
894
+ puts log_message(Logger::INFO, name, msg, subject: 'revbuild', hint: time_format(epochtime - start))
895
+ end
896
+ workspace.rev_write(name, { 'revision' => sha, 'files' => status_digest(*args, **kwargs) }, utc: 'build')
897
+ end
898
+
718
899
  def reset(flag, opts = [], refs: nil, ref: nil, mode: nil, commit: nil)
719
- cmd = git_session 'reset'
900
+ cmd, opts = git_session('reset', opts: opts)
720
901
  case flag
721
902
  when :commit, :index
722
903
  out = option_sanitize(opts, OPT_GIT[:reset] + VAL_GIT[:reset], no: OPT_GIT[:no][:reset]).first
@@ -741,21 +922,19 @@ module Squared
741
922
  return
742
923
  end
743
924
  unless ref == false
744
- append_commit ref
925
+ append_commit(ref, head: true)
745
926
  append_pathspec refs if refs
746
927
  end
747
928
  source
748
929
  end
749
930
 
750
931
  def checkout(flag, opts = [], branch: nil, origin: nil, create: nil, commit: nil, detach: nil)
751
- cmd = git_session 'checkout'
932
+ cmd, opts = git_session('checkout', opts: opts)
752
933
  append_option 'force', 'merge'
753
934
  case flag
754
935
  when :branch
755
936
  cmd << '--detach' if detach == 'd' || option('detach')
756
- if (val = option('track'))
757
- cmd << shell_option('track', val)
758
- end
937
+ append_option('track', equals: true)
759
938
  cmd << if create
760
939
  shell_option(create, branch)
761
940
  else
@@ -787,7 +966,7 @@ module Squared
787
966
  end
788
967
 
789
968
  def tag(flag, opts = [], refs: [], message: nil, commit: nil)
790
- cmd = git_session 'tag'
969
+ cmd, opts = git_session('tag', opts: opts)
791
970
  case flag
792
971
  when :add
793
972
  if option('sign')
@@ -795,8 +974,8 @@ module Squared
795
974
  elsif !session_arg?('s', 'sign', 'u', 'local-user')
796
975
  cmd << '--annotate'
797
976
  end
798
- if !commit && message && (hash = commithash(message))
799
- commit = hash
977
+ if !commit && message && (sha = commithash(message))
978
+ commit = sha
800
979
  else
801
980
  append_message message
802
981
  end
@@ -819,41 +998,27 @@ module Squared
819
998
  source
820
999
  end
821
1000
 
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
1001
+ def logx(flag, opts = [], range: [], index: [])
1002
+ cmd, opts = git_session('log', opts: opts)
1003
+ refs = option_sanitize(opts, collect_hash(OPT_GIT[:log]), no: collect_hash(OPT_GIT[:no][:log])).first
825
1004
  case flag
826
1005
  when :between, :contain
827
1006
  cmd << shell_quote(range.join(flag == :between ? '..' : '...'))
828
1007
  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?
1008
+ cmd.merge(index)
833
1009
  end
834
1010
  append_nocolor
835
- append_pathspec files
1011
+ append_pathspec refs
836
1012
  source(exception: false)
837
1013
  end
838
1014
 
839
- def diff(flag, opts = [], refs: [], branch: nil, range: [])
840
- cmd = git_session 'diff'
1015
+ def diff(flag, opts = [], refs: [], branch: nil, range: [], index: [])
1016
+ cmd, opts = git_session('diff', opts: opts)
841
1017
  files = option_sanitize(opts, collect_hash(OPT_GIT[:diff]) + OPT_GIT[:log][:diff],
842
1018
  no: OPT_GIT[:no][:log][:diff]).first
843
1019
  case flag
844
1020
  when :files, :view, :between, :contain
845
1021
  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
1022
  end
858
1023
  append_nocolor
859
1024
  if flag == :files
@@ -868,15 +1033,14 @@ module Squared
868
1033
  cmd.delete('--merge-base')
869
1034
  cmd << shell_quote(range.join(flag == :between ? '..' : '...'))
870
1035
  else
871
- cmd << '--cached' if flag == :cached
872
1036
  cmd << '--merge-base' if option('merge-base')
873
1037
  cmd << shell_quote(branch) if branch
874
- if sha
1038
+ if !index.empty?
875
1039
  if session_arg?('cached')
876
- raise_error('diff', sha.join(', '), hint: 'one commit') if sha.size > 1
877
- cmd << sha.first
1040
+ raise_error("one commit only: #{index.join(', ')}", hint: '--cached') if index.size > 1
1041
+ cmd << index.first
878
1042
  else
879
- cmd.merge(sha)
1043
+ cmd.merge(index)
880
1044
  end
881
1045
  elsif (n = option('index'))
882
1046
  cmd << "HEAD~#{n}"
@@ -893,12 +1057,12 @@ module Squared
893
1057
  if !message && !amend
894
1058
  return if pass
895
1059
 
896
- raise_error('commit', 'GIT_MESSAGE="description"', hint: 'missing')
1060
+ raise_error('missing message', hint: 'GIT_MESSAGE="description"')
897
1061
  end
898
1062
  pathspec = if flag == :all || (amend && refs.size == 1 && refs.first == '*')
899
1063
  '--all'
900
1064
  elsif (refs = projectmap(refs)).empty?
901
- raise_error('commit', 'pathspec', hint: 'missing')
1065
+ raise_error 'no qualified pathspec'
902
1066
  else
903
1067
  "-- #{refs.join(' ')}"
904
1068
  end
@@ -907,13 +1071,15 @@ module Squared
907
1071
  upstream = nil
908
1072
  source(git_output('fetch --no-tags --quiet'), io: true, banner: false)
909
1073
  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))
1074
+ next unless (r = /^\*\s(\S+)\s+(\h+)(?:\s\[(.+?)(?=\]\s)\])?\s/.match(val))
911
1075
 
912
- branch = data[1]
913
- if !data[3]
1076
+ branch = r[1]
1077
+ if r[3]
1078
+ origin = r[3][%r{^(.+)/#{Regexp.escape(branch)}$}, 1]
1079
+ else
914
1080
  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)}, (.+?)\)$/
1081
+ out = source(git_output('log -n1 --format=%h%d'), io: true, banner: false, stdout: true).first
1082
+ if out =~ /^#{r[2]} \(HEAD -> #{Regexp.escape(branch)}, (.+?)\)$/
917
1083
  split_escape($1).each do |val|
918
1084
  next unless val.end_with?("/#{branch}")
919
1085
 
@@ -923,12 +1089,10 @@ module Squared
923
1089
  end
924
1090
  end
925
1091
  upstream = true if origin
926
- elsif data[3] =~ %r{^(.+)/#{Regexp.escape(branch)}$}
927
- origin = $1
928
1092
  end
929
1093
  break
930
1094
  end
931
- raise_error('commit', 'work tree is not usable') unless origin && branch
1095
+ raise_error 'work tree is not usable' unless origin && branch
932
1096
  cmd = git_session('commit', option('dry-run') && '--dry-run', options: false)
933
1097
  if amend
934
1098
  cmd << '--amend'
@@ -955,8 +1119,24 @@ module Squared
955
1119
  source b
956
1120
  end
957
1121
 
958
- def branch(flag, opts = [], refs: [], ref: nil, target: nil)
959
- cmd = git_session 'branch'
1122
+ def merge(flag, opts = [], command: nil)
1123
+ cmd, opts = git_session('merge', opts: opts)
1124
+ case flag
1125
+ when :commit, :'no-commit'
1126
+ refs = option_sanitize(opts, OPT_GIT[:merge], no: OPT_GIT[:no][:merge]).first
1127
+ raise_error 'no branch/commit' if refs.empty?
1128
+ cmd << "--#{flag}" << '--'
1129
+ append_commit(*refs)
1130
+ else
1131
+ return unless VAL_GIT[:merge][:send].include?(command)
1132
+
1133
+ cmd << "--#{command}"
1134
+ end
1135
+ source
1136
+ end
1137
+
1138
+ def branch(flag = nil, opts = [], refs: [], ref: nil, target: nil)
1139
+ cmd, opts = git_session('branch', opts: opts)
960
1140
  stdout = false
961
1141
  case flag
962
1142
  when :create
@@ -983,7 +1163,7 @@ module Squared
983
1163
  end
984
1164
  ref = nil
985
1165
  when :delete
986
- force, list = refs.partition { |val| val =~ /^[\^~]/ }
1166
+ force, list = refs.partition { |val| val.match?(/^[\^~]/) }
987
1167
  force.each do |val|
988
1168
  dr = val[0, 3]
989
1169
  d = dr.include?('^') ? '-D' : '-d'
@@ -1003,10 +1183,9 @@ module Squared
1003
1183
  cmd << '--edit-description'
1004
1184
  when :current
1005
1185
  cmd << '--show-current'
1006
- else
1007
- opts = option_sanitize(opts, OPT_GIT[:branch], no: OPT_GIT[:no][:branch]).first
1186
+ when :list
1008
1187
  grep = []
1009
- opts.each do |opt|
1188
+ option_sanitize(opts, OPT_GIT[:branch], no: OPT_GIT[:no][:branch]).first.each do |opt|
1010
1189
  if opt =~ /^(v+)$/
1011
1190
  cmd << "-#{$1}"
1012
1191
  else
@@ -1018,42 +1197,59 @@ module Squared
1018
1197
  out, banner, from = source(io: true)
1019
1198
  print_item banner
1020
1199
  ret = write_lines(out, sub: [
1021
- { pat: /^(\*\s+)(\S+)(\s*)$/, styles: :green, index: 2 },
1022
- { pat: %r{^(\s*)(remotes/\S+)(.*)$}, styles: :red, index: 2 }
1200
+ { pat: /^(\*\s+)(\S+)(\s*)$/, styles: color(:green), index: 2 },
1201
+ { pat: %r{^(\s*)(remotes/\S+)(.*)$}, styles: color(:red), index: 2 }
1023
1202
  ])
1024
1203
  list_result(ret, 'branches', from: from)
1025
1204
  return
1205
+ else
1206
+ head = source(git_output('rev-parse --abbrev-ref HEAD'), io: true, banner: false, stdout: true).first.chomp
1207
+ if head.empty?
1208
+ ret = 0
1209
+ else
1210
+ out, banner, from = source(cmd << '-vv --no-abbrev --list', io: true)
1211
+ ret = write_lines(out, grep: /^\*\s+#{Regexp.escape(head)}\s/, banner: banner, first: true) do |line|
1212
+ next line if stdin?
1213
+
1214
+ data = line.sub(/^\*\s+/, '').split(/\s+/)
1215
+ a = sub_style(data[0], styles: theme[:inline])
1216
+ b = sub_style(data[1], styles: theme[:extra])
1217
+ r = /\A(?:\[(.+?)(?=\]\s)\]\s)?(.+)\z/m.match(data[2..-1].join(' '))
1218
+ [" Branch: #{a + (r[1] ? " (#{r[1]})" : '')}", " Commit: #{b}", "Message: #{r[2]}"].compact.join("\n")
1219
+ end
1220
+ on :last, from
1221
+ end
1222
+ if ret == 0
1223
+ warn log_message(Logger::WARN, name, 'no ref found', subject: 'branch', hint: 'head', pass: true)
1224
+ end
1225
+ return
1026
1226
  end
1027
1227
  cmd << shell_escape(target) if target
1028
1228
  cmd << shell_escape(ref) if ref
1029
1229
  source(stdout: stdout)
1030
1230
  end
1031
1231
 
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)
1232
+ def show(flag, opts = [], format: nil, files: [])
1233
+ cmd, opts = git_session('show', opts: opts)
1234
+ case flag
1235
+ when :textconv
1236
+ cmd << '--textconv'
1237
+ files = files.map { |val| Dir[val] }
1238
+ .flatten
1239
+ .select { |val| projectpath?(val) }
1240
+ .map { |val| shell_quote("HEAD:#{val}") }
1241
+ append_value files
1242
+ source(banner: false)
1243
+ return
1244
+ when :oneline
1245
+ format = flag.to_s
1045
1246
  end
1046
- source(sync: false, stderr: true)
1047
- end
1048
-
1049
- def show(format, opts = [])
1050
- cmd = git_session 'show'
1051
1247
  if format
1052
1248
  case (val = format.downcase)
1053
1249
  when 'oneline', 'short', 'medium', 'full', 'fuller', 'reference', 'email', 'raw'
1054
1250
  cmd << basic_option('format', val)
1055
1251
  else
1056
- if format =~ /^t?format:/ || format.include?('%')
1252
+ if format.match?(/^t?format:/) || format.include?('%')
1057
1253
  cmd << quote_option('pretty', format)
1058
1254
  else
1059
1255
  opts << format
@@ -1071,18 +1267,19 @@ module Squared
1071
1267
  end
1072
1268
 
1073
1269
  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
1270
+ cmd, opts = git_session('rev-parse', opts: opts)
1271
+ cmd << if flag == :parseopt
1272
+ '--parseopt'
1273
+ elsif opts.delete('sq-quote')
1274
+ '--sq-quote'
1275
+ end
1079
1276
  case flag
1080
1277
  when :commit
1081
1278
  cmd << ((n = size.to_i) > 0 ? basic_option('short', [n, 5].max) : '--verify')
1082
- append_commit ref
1279
+ append_commit(ref, head: true)
1083
1280
  when :branch
1084
1281
  cmd << '--abbrev-ref'
1085
- append_commit ref
1282
+ append_commit(ref, head: true)
1086
1283
  else
1087
1284
  args = option_sanitize(opts, OPT_GIT[:rev_parse][flag]).first
1088
1285
  append_value(args, escape: session_arg?('sq-quote'))
@@ -1091,7 +1288,7 @@ module Squared
1091
1288
  end
1092
1289
 
1093
1290
  def ls_remote(flag, opts = [], remote: nil)
1094
- cmd = git_session 'ls-remote', '--refs'
1291
+ cmd, opts = git_session('ls-remote', '--refs', opts: opts)
1095
1292
  cmd << "--#{flag}" unless flag == :remote
1096
1293
  grep = option_sanitize(opts, OPT_GIT[:ls_remote]).first
1097
1294
  cmd << shell_quote(remote) if remote
@@ -1102,7 +1299,7 @@ module Squared
1102
1299
  end
1103
1300
 
1104
1301
  def ls_files(flag, opts = [])
1105
- git_session 'ls-files', "--#{flag}"
1302
+ opts = git_session('ls-files', "--#{flag}", opts: opts).last
1106
1303
  grep = option_sanitize(opts, OPT_GIT[:ls_files]).first
1107
1304
  out, banner, from = source(io: true)
1108
1305
  print_item banner
@@ -1110,10 +1307,51 @@ module Squared
1110
1307
  list_result(ret, 'files', from: from, grep: grep)
1111
1308
  end
1112
1309
 
1310
+ def git(flag, opts = [])
1311
+ cmd, opts = git_session(flag, opts: opts)
1312
+ refs = option_sanitize(opts, OPT_GIT[flag], no: OPT_GIT[:no][flag]).first
1313
+ refs = projectmap(refs) unless flag == :revert
1314
+ sync = false
1315
+ stderr = true
1316
+ case flag
1317
+ when :clean
1318
+ unless refs.empty?
1319
+ cmd << '--'
1320
+ cmd.merge(refs)
1321
+ end
1322
+ sync = true
1323
+ stderr = false
1324
+ when :restore
1325
+ if session_arg?('p', 'patch')
1326
+ option_clear refs
1327
+ else
1328
+ append_pathspec(refs, expect: true)
1329
+ end
1330
+ when :revert
1331
+ if VAL_GIT[:rebase][:send].any? { |val| session_arg?(val) }
1332
+ option_clear refs
1333
+ elsif refs.empty?
1334
+ raise_error 'no commit given'
1335
+ else
1336
+ append_commit(*refs)
1337
+ end
1338
+ when :mv
1339
+ raise_error 'no source/destination' unless refs.size > 1
1340
+ cmd.merge(refs)
1341
+ when :rm
1342
+ append_pathspec(refs, expect: true)
1343
+ end
1344
+ source(sync: sync, stderr: stderr)
1345
+ end
1346
+
1113
1347
  def clone?
1114
1348
  ref?(workspace.baseref) && workspace.git_clone?(path, name) ? 1 : false
1115
1349
  end
1116
1350
 
1351
+ def revbuild?
1352
+ build? && !!workspace.revfile
1353
+ end
1354
+
1117
1355
  def enabled?(*, **kwargs)
1118
1356
  super || (kwargs[:base] == false && !!clone?)
1119
1357
  end
@@ -1126,7 +1364,7 @@ module Squared
1126
1364
  if cmd.respond_to?(:done)
1127
1365
  if io && banner == false
1128
1366
  from = nil
1129
- elsif !from && (from = cmd.drop(1).find { |val| val =~ /^[a-z][a-z\-]{2,}$/ })
1367
+ elsif !from && (from = cmd.drop(1).find { |val| val.match?(/^[a-z][a-z\-]{2,}$/) })
1130
1368
  from = :"git:#{from}"
1131
1369
  end
1132
1370
  banner &&= cmd.temp { |val| val.start_with?('--work-tree') || val.start_with?('--git-dir') }
@@ -1172,13 +1410,13 @@ module Squared
1172
1410
  ret = on(:error, from, e)
1173
1411
  raise if exception && ret != true
1174
1412
 
1175
- warn log_message(Logger::WARN, e) if warning?
1413
+ warn log_message(Logger::WARN, e, pass: true) if warning?
1176
1414
  else
1177
1415
  on :last, from
1178
1416
  end
1179
1417
  end
1180
1418
 
1181
- def write_lines(data, banner: nil, loglevel: nil, grep: nil, sub: nil, pass: false)
1419
+ def write_lines(data, banner: nil, loglevel: nil, grep: nil, sub: nil, pass: false, first: false)
1182
1420
  grep = as_a(grep).map do |val|
1183
1421
  next val if val.is_a?(Regexp)
1184
1422
 
@@ -1191,6 +1429,7 @@ module Squared
1191
1429
  data.each do |line|
1192
1430
  next if grep&.none? { |pat| pat.match?(line) }
1193
1431
 
1432
+ line = yield line if block_given?
1194
1433
  if loglevel
1195
1434
  log&.add loglevel, line
1196
1435
  else
@@ -1202,8 +1441,9 @@ module Squared
1202
1441
  end
1203
1442
  end
1204
1443
  ret += 1
1444
+ break if first
1205
1445
  end
1206
- print_item banner, out if banner && (ret > 0 || !pass)
1446
+ print_item banner, out if banner && (ret > 0 || (!pass && !first))
1207
1447
  ret
1208
1448
  end
1209
1449
 
@@ -1226,6 +1466,21 @@ module Squared
1226
1466
  on :last, from
1227
1467
  end
1228
1468
 
1469
+ def status_digest(*args, algorithm: Digest::SHA256, **kwargs)
1470
+ glob = kwargs.fetch(:include, [])
1471
+ pass = kwargs.fetch(:exclude, [])
1472
+ ret = {}
1473
+ out = source(git_output('status -s --porcelain', *args), io: true, banner: false).first
1474
+ out.each do |line|
1475
+ next unless (file = line[/^[A-Z ?!]{3}"?(.+?)"?$/, 1])
1476
+ next if !glob.empty? && glob.none? { |val| File.fnmatch?(val, file, File::FNM_DOTMATCH) }
1477
+ next if !pass.empty? && pass.any? { |val| File.fnmatch?(val, file, File::FNM_DOTMATCH) }
1478
+
1479
+ ret[file] = algorithm.hexdigest(File.read(basepath(file)))
1480
+ end
1481
+ ret
1482
+ end
1483
+
1229
1484
  def append_pull(opts, list, target: @session, no: nil, flag: nil, remote: nil)
1230
1485
  cmd << '--force' if option('force')
1231
1486
  rsm = append_submodules(target: target)
@@ -1265,9 +1520,13 @@ module Squared
1265
1520
  option_clear(out, target: target, subject: flag.to_s) if flag
1266
1521
  end
1267
1522
 
1268
- def append_commit(val, target: @session)
1269
- val = val.to_s.strip
1270
- target << (val.empty? ? append_head(target: target) || 'HEAD' : val)
1523
+ def append_commit(*val, target: @session, head: false)
1524
+ val.compact!
1525
+ if !val.empty?
1526
+ val.each { |ref| target << (commithash(ref) || shell_quote(ref)) }
1527
+ elsif head
1528
+ target << (append_head(target: target) || 'HEAD')
1529
+ end
1271
1530
  end
1272
1531
 
1273
1532
  def append_pathspec(files = [], target: @session, expect: false, parent: false)
@@ -1279,9 +1538,9 @@ module Squared
1279
1538
  end
1280
1539
  files = projectmap(files, parent: parent)
1281
1540
  if !files.empty?
1282
- target << "-- #{files.join(' ')}"
1541
+ target << '--' << files.join(' ')
1283
1542
  elsif expect
1284
- raise_error(parent ? 'pathspec not present' : 'pathspec not within worktree', hint: 'invalid')
1543
+ raise_error(parent ? 'pathspec not present' : 'pathspec not within worktree')
1285
1544
  end
1286
1545
  end
1287
1546
  end
@@ -1316,9 +1575,13 @@ module Squared
1316
1575
  end
1317
1576
  end
1318
1577
 
1319
- def git_session(*cmd, worktree: true, **kwargs)
1578
+ def git_session(*cmd, opts: nil, worktree: true, **kwargs)
1320
1579
  dir = worktree ? ["--work-tree=#{shell_quote(path)}", "--git-dir=#{shell_quote(gitpath)}"] : []
1321
- session('git', *dir, *cmd, **kwargs)
1580
+ ret = session('git', *dir, **kwargs)
1581
+ return ret.merge(cmd) unless opts
1582
+
1583
+ opts = option_sanitize(opts, OPT_GIT[:common]).first
1584
+ [ret.merge(cmd), opts]
1322
1585
  end
1323
1586
 
1324
1587
  def git_output(*cmd, **kwargs)
@@ -1336,11 +1599,11 @@ module Squared
1336
1599
  end
1337
1600
 
1338
1601
  def gitpath
1339
- basepath('.git')
1602
+ basepath '.git'
1340
1603
  end
1341
1604
 
1342
1605
  def commithash(val)
1343
- val =~ /\A#\{(\h{5,40})\}\z/ ? $1 : nil
1606
+ val[/^#\{(\h{5,40})\}$/, 1]
1344
1607
  end
1345
1608
 
1346
1609
  def threadargs