squared 0.5.3 → 0.5.5

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.
@@ -4,7 +4,7 @@ module Squared
4
4
  module Workspace
5
5
  module Git
6
6
  GIT_REPO = Support.hashobj
7
- GIT_PROTO = %r{^(?:https?|ssh|git|file)://}i.freeze
7
+ GIT_PROTO = %r{\A(https?|ssh|git|file)://}i.freeze
8
8
  private_constant :GIT_REPO, :GIT_PROTO
9
9
 
10
10
  attr_reader :revfile
@@ -74,7 +74,7 @@ module Squared
74
74
  @revdoc = JSON.parse(@revfile.read) if @revfile.exist?
75
75
  rescue StandardError => e
76
76
  @revfile = nil
77
- warn log_message(Logger::WARN, e, pass: true) if @warning
77
+ warn log_message(Logger::WARN, e, pass: true)
78
78
  self
79
79
  else
80
80
  @revdoc = {} unless @revdoc.is_a?(Hash)
@@ -247,7 +247,17 @@ module Squared
247
247
  pop: %w[index].freeze,
248
248
  apply: %w[index].freeze
249
249
  }.freeze,
250
- status: %w[u|ignore-submodules=bm? ignored=b? untracked-files=b?],
250
+ status: %w[z u=bm? b|branch long s|short show-stash v|verbose column=b find-renames=i? ignore-submodules=b?
251
+ ignored=b? porcelain=b? untracked-files=b?].freeze,
252
+ submodule: {
253
+ status: %w[cached recursive].freeze,
254
+ update: %w[checkout f|force init merge N|no-fetch no-recommend-shallow no-single-branch recommend-shallow
255
+ rebase recursive remote single-branch depth=i filter=q jobs=i reference=b ref-format=q].freeze,
256
+ branch: %w[b|branch d|default].freeze,
257
+ sync: %w[recursive].freeze
258
+ }.freeze,
259
+ switch: %w[d|detach discard-changes f|force ignore-other-worktrees m|merge q|quiet conflict=b c|create=q
260
+ C|force-create=q orphan=q t|track=b].freeze,
251
261
  tag: %w[n=im cleanup=b create-reflog i|ignore-case omit-empty color=b? column=b contains=b? format=q merged=b?
252
262
  no-contains=b? no-merged=b? points-at=q sort=q trailer=q].freeze,
253
263
  no: {
@@ -273,6 +283,8 @@ module Squared
273
283
  rev_parse: %w[flags].freeze,
274
284
  revert: %w[edit gpg-sign rerere-autoupdate].freeze,
275
285
  show: %w[standard-notes].freeze,
286
+ status: %w[ahead-behind column renames].freeze,
287
+ switch: %w[guess progress recurse-submodules track].freeze,
276
288
  tag: %w[column].freeze
277
289
  }.freeze
278
290
  }.freeze
@@ -292,12 +304,10 @@ module Squared
292
304
  include Rake::DSL
293
305
 
294
306
  def populate(ws, **)
295
- return if ws.series.exclude?(:pull, true)
296
-
297
- namespace(name = ws.task_name('git')) do
298
- all = ws.task_join(name, 'all')
307
+ return if ws.series.exclude?(:pull, true) || ws.size == 1
299
308
 
300
- ws.format_desc(all, 'stash|rebase|autostash?,depend?')
309
+ namespace ws.task_name('git') do |ns|
310
+ ws.format_desc(all = ws.task_join(ns.scope.path, 'all'), 'stash|rebase|autostash?,depend?')
301
311
  task 'all' do |_, args|
302
312
  args = args.to_a
303
313
  cmd = if args.include?('stash')
@@ -314,6 +324,7 @@ module Squared
314
324
  cmd << ws.task_sync('build')
315
325
  Common::Utils.task_invoke(*cmd, **ws.invokeargs)
316
326
  end
327
+
317
328
  ws.series.sync << all
318
329
  ws.series.multiple << all
319
330
  end
@@ -335,25 +346,27 @@ module Squared
335
346
  'checkout' => %i[commit branch track detach path].freeze,
336
347
  'commit' => %i[add all amend amend-orig fixup].freeze,
337
348
  'diff' => %i[head branch files view between contain].freeze,
338
- 'fetch' => %i[origin remote].freeze,
349
+ 'fetch' => %i[origin remote all].freeze,
339
350
  'files' => %i[cached modified deleted others].freeze,
340
- 'git' => %i[add blame clean mv revert rm].freeze,
351
+ 'git' => %i[add blame clean mv revert rm status].freeze,
341
352
  'log' => %i[view between contain].freeze,
342
353
  'merge' => %i[commit no-commit send].freeze,
343
- 'pull' => %i[origin remote].freeze,
354
+ 'pull' => %i[origin remote all].freeze,
344
355
  'rebase' => %i[branch onto send].freeze,
345
356
  'refs' => %i[heads tags remote].freeze,
346
- 'reset' => %i[commit index patch mode].freeze,
357
+ 'reset' => %i[commit index patch mode undo].freeze,
347
358
  'restore' => %i[source staged worktree].freeze,
348
359
  'rev' => %i[commit build output].freeze,
349
360
  'show' => %i[format oneline textconv].freeze,
350
- 'stash' => %i[push pop apply branch drop clear list].freeze,
351
- 'switch' => %i[create detach merge].freeze,
361
+ 'stash' => %i[push pop apply branch drop clear list all].freeze,
362
+ 'submodule' => %i[status update branch url sync].freeze,
363
+ 'switch' => %i[branch create detach].freeze,
352
364
  'tag' => %i[add sign delete list].freeze
353
365
  })
354
366
 
355
367
  def initialize(*, **)
356
368
  super
369
+ @submodule = basepath('.gitmodules').exist?
357
370
  initialize_ref Git.ref if gitpath.exist?
358
371
  end
359
372
 
@@ -363,7 +376,7 @@ module Squared
363
376
 
364
377
  def populate(*, **)
365
378
  super
366
- return unless ref?(Git.ref)
379
+ return unless ref?(Git.ref) || @only
367
380
 
368
381
  namespace name do
369
382
  Git.subtasks do |action, flags|
@@ -385,11 +398,34 @@ module Squared
385
398
  __send__(action, flag, args, remote: remote)
386
399
  end
387
400
  else
388
- format_desc action, flag, 'opts*'
401
+ format_desc(action, flag, 'opts*', after: flag == :all && action == 'pull' ? 'pattern*' : nil)
389
402
  task flag do |_, args|
390
403
  __send__ action, flag, args.to_a
391
404
  end
392
405
  end
406
+ when 'submodule'
407
+ break unless @submodule
408
+
409
+ case flag
410
+ when :branch
411
+ format_desc action, flag, 'path,opts*'
412
+ task flag, [:path] do |_, args|
413
+ path = param_guard(action, flag, args: args, key: :path)
414
+ submodule(flag, args.extras, path: path)
415
+ end
416
+ when :url
417
+ format_desc action, flag, 'path,url,opts*'
418
+ task flag, [:path, :url] do |_, args|
419
+ path = param_guard(action, flag, args: args, key: :path)
420
+ url = param_guard(action, flag, args: args, key: :url)
421
+ submodule(flag, args.extras, path: path, url: url)
422
+ end
423
+ else
424
+ format_desc action, flag, 'opts*,path*'
425
+ task flag do |_, args|
426
+ submodule flag, args.to_a
427
+ end
428
+ end
393
429
  when 'commit'
394
430
  case flag
395
431
  when :all
@@ -469,7 +505,7 @@ module Squared
469
505
  format_desc(action, flag, 'opts*', after: case flag
470
506
  when :push then 'pathspec*'
471
507
  when :branch then 'name,stash?|:'
472
- when :clear, :list then nil
508
+ when :clear, :list, :all then nil
473
509
  else 'stash?|:' end)
474
510
  task flag do |_, args|
475
511
  stash flag, args.to_a
@@ -590,12 +626,12 @@ module Squared
590
626
  format_desc action, flag, 'ref,opts*'
591
627
  task flag, [:commit] do |_, args|
592
628
  commit = commithead args.commit
593
- if commit
594
- args = args.extras
595
- else
596
- commit, opts = choice_commit(values: ['Options'])
597
- args = OptionPartition.strip(opts)
598
- end
629
+ args = if commit
630
+ args.extras
631
+ else
632
+ commit, opts = choice_commit(values: ['Options'])
633
+ OptionPartition.strip(opts)
634
+ end
599
635
  checkout(flag, args, commit: commit)
600
636
  end
601
637
  when :detach
@@ -629,10 +665,7 @@ module Squared
629
665
  task flag, [:upstream, :name] do |_, args|
630
666
  if (ref = args.upstream)
631
667
  target = args.name
632
- if ref.start_with?('~')
633
- ref = ref[1..-1]
634
- remote = true
635
- end
668
+ remote = true if ref.delete_prefix!('~')
636
669
  else
637
670
  ref, remote, target = choice_refs('Choose a remote', 'remotes', accept: [['Push?', true]],
638
671
  values: ['Enter branch name'])
@@ -685,10 +718,10 @@ module Squared
685
718
  when :create
686
719
  format_desc action, flag, '(^)name,ref?=HEAD|:'
687
720
  task flag, [:name, :commit] do |_, args|
688
- target = param_guard(action, flag, args: args, key: :name)
721
+ branch = param_guard(action, flag, args: args, key: :name)
689
722
  commit = commithead args.commit
690
723
  commit, track = choice_commit(values: ['Track? [Y|n]'], force: false) if commit == ':'
691
- switch(flag, target: target, commit: commit, track: track)
724
+ switch(flag, branch: branch, commit: commit, track: track)
692
725
  end
693
726
  when :detach
694
727
  format_desc action, flag, 'ref?=HEAD'
@@ -696,11 +729,16 @@ module Squared
696
729
  commit = commithead(args.commit) || choice_commit(force: false)
697
730
  switch(flag, commit: commit)
698
731
  end
699
- when :merge
700
- format_desc action, flag, 'branch?'
701
- task flag, [:branch] do |_, args|
702
- commit = args.branch || choice_refs('Choose a branch')
703
- switch(flag, commit: commit)
732
+ when :branch
733
+ format_desc action, flag, 'name|:,opts*'
734
+ task flag, [:name] do |_, args|
735
+ if (branch = args.name)
736
+ args = args.extras
737
+ branch = nil if branch == ':'
738
+ else
739
+ args = []
740
+ end
741
+ switch(flag, args, branch: branch || choice_refs('Choose a branch'))
704
742
  end
705
743
  end
706
744
  when 'reset'
@@ -721,10 +759,10 @@ module Squared
721
759
  end
722
760
  print_success if success?(reset(flag, args, commit: commit))
723
761
  end
724
- when :index
725
- format_desc action, flag, 'opts*,pathspec*'
762
+ when :index, :undo
763
+ format_desc(action, flag, flag == :index ? 'opts*,pathspec*' : nil)
726
764
  task flag do |_, args|
727
- reset flag, args.to_a
765
+ reset(flag, flag == :index ? args.to_a : [])
728
766
  end
729
767
  when :mode
730
768
  format_desc action, flag, 'mode,ref?=HEAD|:'
@@ -766,28 +804,28 @@ module Squared
766
804
  when :branch
767
805
  format_desc action, flag, 'upstream,branch?=HEAD,opts*'
768
806
  task flag, [:upstream] do |_, args|
769
- if (upstream = args.upstream)
770
- args = args.extras
771
- else
772
- upstream, opts = choice_refs('Choose upstream branch', values: ['Options'])
773
- args = OptionPartition.strip(opts)
774
- end
807
+ args = if (upstream = args.upstream)
808
+ args.extras
809
+ else
810
+ upstream, opts = choice_refs('Choose upstream branch', values: ['Options'])
811
+ OptionPartition.strip(opts)
812
+ end
775
813
  rebase(flag, args, upstream: upstream)
776
814
  end
777
815
  when :onto
778
816
  format_desc action, flag, 'ref,upstream,branch?=HEAD'
779
817
  task flag, [:commit, :upstream, :branch] do |_, args|
780
818
  commit = commithead args.commit
781
- if commit
782
- upstream = param_guard(action, flag, args: args, key: :upstream)
783
- branch = args.branch
784
- args = []
785
- else
786
- commit = choice_refs 'Choose "onto" branch'
787
- target, opts = choice_commit(multiple: 2, values: ['Options'], reflog: false)
788
- branch, upstream = target
789
- args = OptionPartition.strip(opts)
790
- end
819
+ args = if commit
820
+ upstream = param_guard(action, flag, args: args, key: :upstream)
821
+ branch = args.branch
822
+ []
823
+ else
824
+ commit = choice_refs 'Choose "onto" branch'
825
+ target, opts = choice_commit(multiple: 2, values: ['Options'], reflog: false)
826
+ branch, upstream = target
827
+ OptionPartition.strip(opts)
828
+ end
791
829
  rebase(flag, args, commit: commit, upstream: upstream, branch: branch)
792
830
  end
793
831
  when :commit, :'no-commit'
@@ -823,7 +861,7 @@ module Squared
823
861
  rev_parse(flag, ref: ref, size: size)
824
862
  end
825
863
  when :build
826
- format_desc action, flag, OPT_GIT[:status]
864
+ format_desc action, flag, 'opts*'
827
865
  task flag do |_, args|
828
866
  revbuild flag, args.to_a
829
867
  end
@@ -840,7 +878,7 @@ module Squared
840
878
  ls_remote(flag, args.extras, remote: args.remote)
841
879
  end
842
880
  else
843
- format_desc action, flag, 'opts*,pattern*'
881
+ format_desc(action, flag, 'opts*,pattern*', after: action == 'files' ? 'pathspec*' : nil)
844
882
  task flag do |_, args|
845
883
  __send__(action == 'refs' ? :ls_remote : :ls_files, flag, args.to_a)
846
884
  end
@@ -851,13 +889,13 @@ module Squared
851
889
  format_desc action, flag, 'ref,opts*,pathspec*'
852
890
  task flag, [:commit] do |_, args|
853
891
  commit = commithead args.commit
854
- if commit
855
- args = args.extras
856
- else
857
- commit, opts, files = choice_commit(values: ['Options', ['Pathspec', true]])
858
- args = OptionPartition.strip(opts)
859
- files = files&.shellsplit
860
- end
892
+ args = if commit
893
+ args.extras
894
+ else
895
+ commit, opts, files = choice_commit(values: ['Options', ['Pathspec', true]])
896
+ files = files&.shellsplit
897
+ OptionPartition.strip(opts)
898
+ end
861
899
  restore(flag, args, commit: commit, files: files)
862
900
  end
863
901
  when :staged, :worktree
@@ -885,10 +923,13 @@ module Squared
885
923
  when :mv then 'source+,destination'
886
924
  when :revert then 'commit+' end
887
925
  format_desc(action, flag, 'opts*', before: before, after: case flag
888
- when :add then 'pathspec*,:pattern:*'
889
- when :clean, :rm then 'pathspec*' end)
926
+ when :add
927
+ 'pathspec*,pattern*'
928
+ when :clean, :rm, :status
929
+ 'pathspec*'
930
+ end)
890
931
  task flag do |_, args|
891
- git flag, args.to_a
932
+ __send__(flag == :status ? :status : :git, flag, args.to_a)
892
933
  end
893
934
  end
894
935
  end
@@ -912,13 +953,12 @@ module Squared
912
953
  super
913
954
  end
914
955
 
915
- def pull(flag = nil, opts = [], sync: invoked_sync?('pull', flag), remote: nil)
956
+ def pull(flag = nil, opts = [], sync: invoked_sync?('pull', flag), remote: nil, hint: nil)
916
957
  cmd, opts = git_session('pull', opts: opts)
958
+ cmd << '--autostash' if option('autostash')
917
959
  if flag == :rebase
918
960
  cmd << '--rebase'
919
- cmd << '--autostash' if option('autostash')
920
961
  else
921
- cmd << '--autostash' if flag == :autostash
922
962
  option('rebase', ignore: false) do |val|
923
963
  cmd << case val
924
964
  when '0', 'false'
@@ -927,15 +967,51 @@ module Squared
927
967
  VAL_GIT[:rebase][:value].include?(val) ? basic_option('rebase', val) : '--rebase'
928
968
  end
929
969
  end
970
+ case flag
971
+ when :all
972
+ unless git_spawn('status -s -z --untracked-files=all').empty?
973
+ if confirm('Stash local changes?', 'Y')
974
+ git_spawn 'stash push --keep-index --quiet'
975
+ elsif !(force = confirm('Force checkout?', 'N'))
976
+ return
977
+ end
978
+ end
979
+ op = OptionPartition.new(opts, OPT_GIT[:pull], cmd, project: self, no: OPT_GIT[:no][:pull])
980
+ reg = if op.empty?
981
+ []
982
+ else
983
+ opts = op.uniq(opts)
984
+ matchmap op
985
+ end
986
+ session_done op.target
987
+ heads = []
988
+ cur = nil
989
+ foreachref('heads', format: '%(if)%(HEAD)%(then)* %(end)%(refname:short)').each do |line|
990
+ line.chomp!
991
+ cur ||= line.delete_prefix!('* ')
992
+ heads << line if matchany?(line, reg)
993
+ end
994
+ raise_error('head not found', hint: 'for-each-ref') unless cur
995
+ opts << 'ff-only' if opts.empty? && !option('ff-only', equals: '0')
996
+ (heads.dup << cur).each_with_index do |branch, index|
997
+ next unless (index < heads.size && cur != branch) || index == heads.size
998
+
999
+ git_spawn 'switch --quiet', force && '--force', shell_quote(branch)
1000
+ pull(nil, opts, sync: false, hint: branch) if heads.include?(branch)
1001
+ end
1002
+ return
1003
+ when :autostash
1004
+ cmd << '--autostash'
1005
+ end
930
1006
  end
931
1007
  append_pull(opts, OPT_GIT[:pull] + OPT_GIT[:fetch][:pull],
932
1008
  no: OPT_GIT[:no][:pull] + OPT_GIT[:no][:fetch][:pull], remote: remote, flag: flag, from: :pull)
933
- source(sync: sync, sub: if verbose
1009
+ source(sync: sync, sub: if stdout?
934
1010
  [
935
1011
  { pat: /^(.+)(\|\s+\d+\s+)([^-]*)(-+)(.*)$/, styles: color(:red), index: 4 },
936
1012
  { pat: /^(.+)(\|\s+\d+\s+)(\++)(.*)$/, styles: color(:green), index: 3 }
937
1013
  ]
938
- end, **threadargs)
1014
+ end, hint: hint, **threadargs)
939
1015
  end
940
1016
 
941
1017
  def rebase(flag = nil, opts = [], sync: invoked_sync?('rebase', flag), commit: nil, upstream: nil, branch: nil,
@@ -959,6 +1035,10 @@ module Squared
959
1035
  cmd << upstream
960
1036
  append_head branch
961
1037
  else
1038
+ unless gitpath('REBASE_HEAD').exist?
1039
+ puts log_message(Logger::INFO, name, 'no rebase in progress', hint: command) if stdout?
1040
+ return
1041
+ end
962
1042
  return unless VAL_GIT[:rebase][:send].include?(command)
963
1043
 
964
1044
  cmd << "--#{command}"
@@ -972,6 +1052,7 @@ module Squared
972
1052
 
973
1053
  def fetch(flag = nil, opts = [], sync: invoked_sync?('fetch', flag), remote: nil)
974
1054
  opts = git_session('fetch', opts: opts).last
1055
+ opts << 'all' if flag == :all || option('all')
975
1056
  append_pull(opts, collect_hash(OPT_GIT[:fetch]), no: collect_hash(OPT_GIT[:no][:fetch]),
976
1057
  remote: remote, flag: flag, from: :fetch)
977
1058
  source(sync: sync, **threadargs)
@@ -1011,18 +1092,22 @@ module Squared
1011
1092
 
1012
1093
  def stash(flag = nil, opts = [], sync: invoked_sync?('stash', flag))
1013
1094
  if flag
1095
+ if flag == :all
1096
+ opts << 'include-untracked'
1097
+ flag = :push
1098
+ end
1014
1099
  cmd, opts = git_session('stash', flag, opts: opts)
1015
1100
  list = OPT_GIT[:stash][:common] + OPT_GIT[:stash].fetch(flag, [])
1016
1101
  if flag == :list
1017
- list += collect_hash(OPT_GIT[:log])
1018
- no = collect_hash(OPT_GIT[:no][:log])
1102
+ list += collect_hash OPT_GIT[:log]
1103
+ no = collect_hash OPT_GIT[:no][:log]
1019
1104
  end
1020
1105
  op = OptionPartition.new(opts, list, cmd, project: self, no: no, first: flag == :push ? matchpathspec : nil)
1021
1106
  case flag
1022
1107
  when :push
1023
1108
  append_pathspec op.extras
1024
1109
  when :pop, :apply, :drop, :branch
1025
- if op.extras.delete(':')
1110
+ if op.remove(':')
1026
1111
  if flag == :branch
1027
1112
  if op.empty?
1028
1113
  values = [['Branch name', true]]
@@ -1055,30 +1140,35 @@ module Squared
1055
1140
  end
1056
1141
  else
1057
1142
  git_session('stash', 'push', opts: opts)
1058
- append_option(OPT_GIT[:stash][:push].grep_v(/[=|]/), no: true, ignore: false)
1143
+ append_option(OptionPartition.select(OPT_GIT[:stash][:push], no: false), no: true, ignore: false)
1059
1144
  append_message
1060
1145
  end
1061
1146
  source(banner: !quiet?, sync: sync, **threadargs)
1062
1147
  end
1063
1148
 
1064
- def status(*)
1065
- cmd = git_session 'status'
1066
- cmd << (option('long') ? '--long' : '--short')
1067
- cmd << '--branch' if option('branch')
1068
- option('ignore-submodules', ignore: false) do |val|
1069
- cmd << basic_option('ignore-submodules', case val
1070
- when '0', 'none'
1071
- 'none'
1072
- when '1', 'untracked'
1073
- 'untracked'
1074
- when '2', 'dirty'
1075
- 'dirty'
1076
- else
1077
- 'all'
1078
- end)
1149
+ def status(flag = nil, opts = [])
1150
+ cmd, opts = git_session('status', opts: opts)
1151
+ if flag
1152
+ op = OptionPartition.new(opts, OPT_GIT[:status], cmd, project: self, no: OPT_GIT[:no][:status])
1153
+ append_pathspec op.extras
1154
+ else
1155
+ cmd << (option('long') ? '--long' : '--short')
1156
+ cmd << '--branch' if option('branch')
1157
+ option('ignore-submodules', ignore: false) do |val|
1158
+ cmd << basic_option('ignore-submodules', case val
1159
+ when '0', 'none'
1160
+ 'none'
1161
+ when '1', 'untracked'
1162
+ 'untracked'
1163
+ when '2', 'dirty'
1164
+ 'dirty'
1165
+ else
1166
+ 'all'
1167
+ end)
1168
+ end
1169
+ append_pathspec
1079
1170
  end
1080
- append_pathspec
1081
- if verbose
1171
+ if stdout?
1082
1172
  r = color(:red)
1083
1173
  g = color(:green)
1084
1174
  sub = if session_arg?('short')
@@ -1119,7 +1209,7 @@ module Squared
1119
1209
  kwargs = kwargs.key?(:include) || kwargs.key?(:exclude) ? statusargs.call : @revbuild || {}
1120
1210
  case flag
1121
1211
  when :build
1122
- op = OptionPartition.new(opts, OPT_GIT[:status], project: self)
1212
+ op = OptionPartition.new(opts, %w[ignore-submodules=b? ignored=b? untracked-files=b?], project: self)
1123
1213
  op.clear(append: true)
1124
1214
  args = op.to_a
1125
1215
  else
@@ -1131,7 +1221,7 @@ module Squared
1131
1221
  if (cur = workspace.rev_entry(name)) && cur['revision'] == sha && !env('REVBUILD_FORCE')
1132
1222
  files = status_digest(*args, **kwargs)
1133
1223
  if cur['files'].size == files.size && cur['files'].find { |key, val| files[key] != val }.nil?
1134
- if verbose
1224
+ if stdout?
1135
1225
  if (since = workspace.rev_timesince(name, 'build'))
1136
1226
  puts log_message(Logger::INFO, name, 'no changes', subject: 'revbuild', hint: "#{since} ago")
1137
1227
  else
@@ -1176,6 +1266,9 @@ module Squared
1176
1266
  when :patch
1177
1267
  cmd << '--patch'
1178
1268
  append_pathspec(refs, pass: false)
1269
+ when :undo
1270
+ cmd << '--hard HEAD@{1}'
1271
+ ref = false
1179
1272
  end
1180
1273
  unless ref == false
1181
1274
  append_commit(ref, head: true)
@@ -1186,20 +1279,14 @@ module Squared
1186
1279
 
1187
1280
  def checkout(flag, opts = [], branch: nil, origin: nil, create: nil, commit: nil, detach: nil, merge: false)
1188
1281
  cmd, opts = git_session('checkout', opts: opts)
1189
- append_option 'force', 'merge'
1282
+ append_option 'force', 'f', 'merge'
1190
1283
  case flag
1191
1284
  when :branch
1192
1285
  cmd << '--detach' if detach == 'd' || option('detach')
1193
1286
  append_option('track', equals: true)
1194
1287
  cmd << (create ? quote_option(create, branch) : branch) << commit
1195
1288
  when :track
1196
- if branch
1197
- if branch.start_with?('^')
1198
- opt = 'B'
1199
- branch = branch[1..-1]
1200
- end
1201
- cmd << quote_option(opt || 'b', branch)
1202
- end
1289
+ cmd << quote_option(branch.delete_prefix!('^') ? 'B' : 'b', branch) if branch
1203
1290
  cmd << '--track' << shell_quote(origin)
1204
1291
  when :detach
1205
1292
  cmd << '-m' if merge
@@ -1228,7 +1315,7 @@ module Squared
1228
1315
  elsif !session_arg?('s', 'sign', 'u', 'local-user')
1229
1316
  cmd << '--annotate'
1230
1317
  end
1231
- cmd << '--force' if option('force')
1318
+ cmd << '--force' if option('force', 'f')
1232
1319
  if !commit && message && (sha = commithash(message))
1233
1320
  commit = sha
1234
1321
  message = nil
@@ -1259,7 +1346,7 @@ module Squared
1259
1346
  first: matchpathspec)
1260
1347
  case flag
1261
1348
  when :between, :contain
1262
- op << shell_quote(range.join(flag == :between ? '..' : '...'))
1349
+ op.add_quote(range.join(flag == :between ? '..' : '...'))
1263
1350
  else
1264
1351
  op.merge(index)
1265
1352
  end
@@ -1282,19 +1369,18 @@ module Squared
1282
1369
  op << '--no-index'
1283
1370
  append_pathspec(refs, parent: true)
1284
1371
  else
1372
+ op << '--merge-base' if option('merge-base')
1285
1373
  case flag
1286
1374
  when :view
1287
- op << '--merge-base' if option('merge-base')
1288
1375
  op.merge(range)
1289
1376
  when :between, :contain
1290
1377
  op.delete('--merge-base')
1291
- op << shell_quote(range.join(flag == :between ? '..' : '...'))
1378
+ op.add_quote(range.join(flag == :between ? '..' : '...'))
1292
1379
  else
1293
- op << '--merge-base' if option('merge-base')
1294
- op << shell_quote(branch) if branch
1380
+ op.add_quote(branch) if branch
1295
1381
  if !index.empty?
1296
1382
  if op.arg?('cached')
1297
- raise_error("one commit only: #{index.join(', ')}", hint: '--cached') if index.size > 1
1383
+ raise_error("one commit only: #{index.join(', ')}", hint: 'cached') if index.size > 1
1298
1384
  op << index.first
1299
1385
  else
1300
1386
  op.merge(index)
@@ -1337,7 +1423,6 @@ module Squared
1337
1423
  cmd, opts = git_session('add', opts: opts)
1338
1424
  op = OptionPartition.new(opts, OPT_GIT[:add], cmd, project: self, first: matchpathspec)
1339
1425
  op << '--verbose' if verbose
1340
- dryrun = dryrun?
1341
1426
  format = '%(if)%(HEAD)%(then)%(refname:short)...%(upstream:short)...%(upstream:track)%(end)'
1342
1427
  git_spawn 'fetch --no-tags --quiet'
1343
1428
  foreachref('heads', format: format).each do |line|
@@ -1346,16 +1431,16 @@ module Squared
1346
1431
  branch, origin, hint = line.split('...')
1347
1432
  if hint && !hint.match?(/^\[(\D+0,\D+0)\]$/)
1348
1433
  raise_error('work tree is not usable', hint: hint[1..-2])
1349
- elsif origin.empty?
1434
+ elsif !origin || origin.empty?
1350
1435
  return nil if pass
1351
- break if dryrun
1436
+ break if dryrun?
1352
1437
 
1353
1438
  unless (origin = option('upstream', prefix: 'git', ignore: false))
1354
1439
  if (origin = choice_refs('Choose an upstream', 'remotes', attempts: 1, force: false))
1355
1440
  git_spawn 'branch', quote_option('set-upstream-to', origin)
1356
- else
1357
- origin = readline('Enter an upstream', force: true)
1441
+ break
1358
1442
  end
1443
+ origin = readline('Enter an upstream', force: true)
1359
1444
  end
1360
1445
  raise_error('missing remote name', hint: origin) unless origin.include?('/')
1361
1446
  upstream = true
@@ -1369,7 +1454,7 @@ module Squared
1369
1454
  end
1370
1455
  co = git_session('commit', options: false)
1371
1456
  pu = git_output 'push', upstream && '--set-upstream'
1372
- if dryrun
1457
+ if dryrun?
1373
1458
  op.delete('--dry-run')
1374
1459
  co << '--dry-run'
1375
1460
  pu << '--dry-run'
@@ -1384,8 +1469,14 @@ module Squared
1384
1469
  pu.merge(repotrack(origin, branch))
1385
1470
  puts if pass
1386
1471
  source op
1387
- source co
1388
- source pu
1472
+ if amend || !git_spawn('diff --cached --name-only --no-color').empty? || dryrun?
1473
+ source co
1474
+ source pu
1475
+ elsif banner?
1476
+ puts 'Nothing to commit'
1477
+ elsif stdout?
1478
+ puts log_message(Logger::INFO, name, 'nothing to commit', hint: flag)
1479
+ end
1389
1480
  end
1390
1481
 
1391
1482
  def merge(flag, opts = [], command: nil, branch: nil)
@@ -1402,6 +1493,10 @@ module Squared
1402
1493
  append_commit(*op.extras)
1403
1494
  end
1404
1495
  else
1496
+ unless gitpath('MERGE_HEAD').exist?
1497
+ puts log_message(Logger::INFO, name, 'no merge in progress', hint: command) if stdout?
1498
+ return
1499
+ end
1405
1500
  return unless VAL_GIT[:merge][:send].include?(command)
1406
1501
 
1407
1502
  cmd << "--#{command}"
@@ -1424,13 +1519,13 @@ module Squared
1424
1519
  '--track'
1425
1520
  end
1426
1521
  end
1427
- cmd << '--force' if option('force')
1522
+ cmd << '--force' if option('force', 'f')
1428
1523
  cmd << shell_quote(target)
1429
1524
  cmd << shell_quote(ref) if ref
1430
1525
  when :track
1431
1526
  raise_error('invalid upstream', hint: ref) unless ref.include?('/')
1432
- if ref.start_with?('^')
1433
- cmd << '--unset-upstream' << shell_quote(ref[1..-1])
1527
+ if ref.delete_prefix!('^')
1528
+ cmd << '--unset-upstream' << shell_quote(ref)
1434
1529
  remote = false
1435
1530
  stdout = true
1436
1531
  else
@@ -1439,14 +1534,12 @@ module Squared
1439
1534
  end
1440
1535
  when :delete
1441
1536
  remote&.each do |val|
1442
- source git_output('push', '--delete', *val.split('/', 2).map { |s| shell_quote(s) })
1537
+ source git_output('push --delete', *val.split('/', 2).map { |s| shell_quote(s) })
1443
1538
  end
1444
1539
  force, list = refs.partition { |val| val.start_with?(/[~^]/) }
1445
1540
  force.each do |val|
1446
- dr = val[0, 2]
1447
- d = dr.include?('^') ? '-D' : '-d'
1448
- r = '-r' if dr.include?('~')
1449
- source git_output('branch', d, r, shell_quote(val.sub(/^[~^]+/, '')))
1541
+ r = '-r' if val.delete!('~')
1542
+ source git_output('branch', val.delete!('^') ? '-D' : '-d', r, shell_quote(val))
1450
1543
  end
1451
1544
  return if list.empty?
1452
1545
 
@@ -1454,8 +1547,9 @@ module Squared
1454
1547
  append_value list
1455
1548
  remote = nil
1456
1549
  when :move, :copy
1457
- flag = "-#{flag.to_s[0]}"
1458
- cmd << (option('force') ? flag.upcase : flag)
1550
+ s = "-#{flag.to_s[0]}"
1551
+ s.upcase! if option('force', 'f')
1552
+ cmd << s
1459
1553
  refs.compact.each { |val| cmd << shell_quote(val) }
1460
1554
  stdout = true
1461
1555
  when :current
@@ -1465,7 +1559,7 @@ module Squared
1465
1559
  when :list
1466
1560
  op = OptionPartition.new(opts, OPT_GIT[:branch], cmd << '--list',
1467
1561
  project: self, no: OPT_GIT[:no][:branch], single: /\Av+\z/)
1468
- op.each { |val| op << shell_quote(val) }
1562
+ op.each { |val| op.add_quote(val) }
1469
1563
  out, banner, from = source(io: true)
1470
1564
  print_item banner
1471
1565
  ret = write_lines(out, sub: [
@@ -1475,12 +1569,12 @@ module Squared
1475
1569
  list_result(ret, 'branches', from: from)
1476
1570
  return
1477
1571
  else
1478
- head = git_spawn('rev-parse --abbrev-ref HEAD').chomp
1479
- if head.empty?
1572
+ if (head = git_spawn('rev-parse --abbrev-ref HEAD').chomp).empty?
1480
1573
  ret = 0
1481
1574
  else
1482
1575
  git_spawn 'fetch --all --prune --quiet' if option('sync')
1483
- out, banner, from = source(cmd << '-vv --no-abbrev --list', io: true)
1576
+ cmd << '-vv --no-abbrev --list'
1577
+ out, banner, from = source(io: true)
1484
1578
  ret = write_lines(out, grep: [/^\*\s+#{Regexp.escape(head)}\s/], banner: banner, first: true) do |line|
1485
1579
  next line if stdin?
1486
1580
 
@@ -1511,33 +1605,50 @@ module Squared
1511
1605
  if !ref
1512
1606
  print_success if flag == :create
1513
1607
  elsif remote && target
1514
- source git_output('push', '-u', shell_quote(ref.split('/', 2).first), shell_quote(target))
1608
+ source git_output('push -u', shell_quote(ref.split('/', 2).first), shell_quote(target))
1515
1609
  end
1516
1610
  end
1517
1611
 
1518
- def switch(flag, opts = [], target: nil, commit: nil, track: nil)
1519
- cmd = git_session('switch', opts: opts).first
1520
- case flag
1521
- when :create
1522
- c = 'c'
1523
- if target.start_with?('^')
1524
- target = target[1..-1]
1525
- c = c.upcase
1612
+ def switch(flag, opts = [], branch: nil, commit: nil, track: nil)
1613
+ cmd, opts = git_session('switch', opts: opts)
1614
+ cmd << '--force' if option('force', 'f')
1615
+ if flag == :branch
1616
+ op = OptionPartition.new(opts, OPT_GIT[:switch], cmd, project: self, no: OPT_GIT[:no][:switch])
1617
+ op.add_quote(branch)
1618
+ else
1619
+ case flag
1620
+ when :create
1621
+ cmd << quote_option(branch.delete_prefix!('^') ? 'C' : 'c', branch)
1622
+ cmd << case (track ||= option('track', ignore: false))
1623
+ when 'n', 'N', '0', 'false'
1624
+ '--no-track'
1625
+ when 'y', 'Y', '1', 'true'
1626
+ '--track'
1627
+ when 'direct', 'inherit'
1628
+ basic_option 'track', track
1629
+ end
1630
+ when :detach
1631
+ cmd << "--#{flag}"
1526
1632
  end
1527
- cmd << quote_option(c, target)
1528
- cmd << case (track ||= option('track', ignore: false))
1529
- when 'n', 'N', '0', 'false'
1530
- '--no-track'
1531
- when 'y', 'Y', '1', 'true'
1532
- '--track'
1533
- when 'direct', 'inherit'
1534
- basic_option 'track', track
1535
- end
1536
- when :detach, :merge
1537
- cmd << "--#{flag}"
1633
+ append_head commit
1634
+ end
1635
+ source
1636
+ end
1637
+
1638
+ def submodule(flag, opts = [], path: nil, url: nil)
1639
+ cmd, opts = git_session('submodule', opts: opts)
1640
+ op = OptionPartition.new(opts, OPT_GIT[:submodule].fetch(flag, []), cmd, project: self)
1641
+ case flag
1642
+ when :branch, :url
1643
+ op.adjoin("set-#{flag}")
1644
+ op << '--'
1645
+ op.add_path(path) if path
1646
+ op.add_quote(url) if url
1647
+ else
1648
+ op.adjoin(flag)
1649
+ op << '--recursive' if option('recursive', 'r')
1650
+ op.splice(path: true)
1538
1651
  end
1539
- cmd << '--force' if option('force')
1540
- append_head commit
1541
1652
  source
1542
1653
  end
1543
1654
 
@@ -1560,19 +1671,15 @@ module Squared
1560
1671
  source(banner: false)
1561
1672
  return
1562
1673
  when :oneline
1563
- format = 'oneline'
1674
+ format = flag.to_s
1564
1675
  end
1565
- if format
1566
- case (val = format.downcase)
1567
- when 'oneline', 'short', 'medium', 'full', 'fuller', 'reference', 'email', 'raw'
1568
- cmd << basic_option('format', val)
1569
- else
1570
- if format.start_with?(/t?format:/) || format.include?('%')
1571
- cmd << quote_option('pretty', format)
1572
- else
1573
- opts << format
1574
- end
1575
- end
1676
+ case format
1677
+ when 'oneline', 'short', 'medium', 'full', 'fuller', 'reference', 'email', 'raw'
1678
+ cmd << basic_option('format', format)
1679
+ when /(?:^t?format:|%)/
1680
+ cmd << quote_option('pretty', format)
1681
+ else
1682
+ opts << format if format
1576
1683
  end
1577
1684
  op = OptionPartition.new(opts, OPT_GIT[:show] + OPT_GIT[:diff][:show] + OPT_GIT[:log][:diff], cmd,
1578
1685
  project: self,
@@ -1606,7 +1713,7 @@ module Squared
1606
1713
  cmd, opts = git_session('ls-remote', '--refs', opts: opts)
1607
1714
  cmd << "--#{flag}" unless flag == :remote
1608
1715
  op = OptionPartition.new(opts, OPT_GIT[:ls_remote], cmd, project: self)
1609
- op << shell_quote(remote) if remote
1716
+ op.add_quote(remote) if remote
1610
1717
  out, banner, from = source(io: true)
1611
1718
  print_item banner
1612
1719
  ret = write_lines(out, grep: op.extras, prefix: "refs/#{flag}/")
@@ -1616,6 +1723,7 @@ module Squared
1616
1723
  def ls_files(flag, opts = [])
1617
1724
  cmd, opts = git_session('ls-files', "--#{flag}", opts: opts)
1618
1725
  op = OptionPartition.new(opts, OPT_GIT[:ls_files], cmd, project: self)
1726
+ op.splice(path: true, pattern: true)
1619
1727
  out, banner, from = source(io: true)
1620
1728
  print_item banner
1621
1729
  ret = write_lines(out, grep: op.extras)
@@ -1644,15 +1752,16 @@ module Squared
1644
1752
  end
1645
1753
  when :add, :clean
1646
1754
  if flag == :add && !op.arg?('pathspec-from-file')
1647
- grep, list = op.partition { |val| val.start_with?(':') && val.end_with?(':') }
1755
+ grep, list = op.partition { |val| OptionPartition.pattern?(val) }
1648
1756
  unless grep.empty? && !list.empty?
1649
1757
  grep.map! { |val| Regexp.new(val[1..-2]) }
1650
- files = status_data.map! do |a, b|
1651
- next if b.strip.empty? || (!grep.empty? && grep.none? { |pat| pat.match?(a) })
1758
+ files = [].tap do |out|
1759
+ status_data.each do |a, b|
1760
+ next if b.strip.empty? || (!grep.empty? && grep.none? { |pat| pat.match?(a) })
1652
1761
 
1653
- "#{sub_style(b, styles: color(:red))} #{a}"
1762
+ out << "#{sub_style(b, styles: color(:red))} #{a}"
1763
+ end
1654
1764
  end
1655
- .compact
1656
1765
  unless files.empty?
1657
1766
  files = choice_index('Select files', files, multiple: true, force: true, trim: /^\S+\s/,
1658
1767
  accept: [['Add?', false, true]])
@@ -1660,7 +1769,7 @@ module Squared
1660
1769
  op.swap(list + files)
1661
1770
  end
1662
1771
  end
1663
- return source(git_session('status', '-s'), banner: false) unless append_pathspec(op.extras)
1772
+ return source(git_session('status -s'), banner: false) unless append_pathspec(op.extras)
1664
1773
 
1665
1774
  verbose = flag == :add && !op.arg?('verbose')
1666
1775
  print_success if success?(source) && verbose
@@ -1690,7 +1799,7 @@ module Squared
1690
1799
  private
1691
1800
 
1692
1801
  def source(cmd = @session, exception: true, io: false, sync: true, stdout: false, stderr: false, banner: true,
1693
- multiple: false, from: nil, **kwargs)
1802
+ multiple: false, hint: nil, from: nil, send: :system, **kwargs)
1694
1803
  cmd = cmd.target if cmd.is_a?(OptionPartition)
1695
1804
  if io && banner == false
1696
1805
  from = nil
@@ -1701,14 +1810,15 @@ module Squared
1701
1810
  if from.nil? && (from = cmd.drop(1).find { |val| val.match?(/\A[a-z]{1,2}[a-z\-]*\z/) })
1702
1811
  from = :"git:#{from}"
1703
1812
  end
1704
- banner &&= cmd.temp { |val| val.start_with?('--work-tree') || val.start_with?('--git-dir') }
1813
+ banner &&= cmd.temp { |val| val.start_with?(/--(?:work-tree|git-dir)/) }
1705
1814
  end
1706
1815
  from = nil if from == false
1707
1816
  end
1708
1817
  cmd = session_done cmd
1709
1818
  log&.info cmd
1710
1819
  banner = if banner
1711
- format_banner((banner.is_a?(String) ? banner : cmd).gsub(File.join(path, ''), ''), banner: true)
1820
+ banner = (banner.is_a?(String) ? banner : cmd).gsub(File.join(path, ''), '')
1821
+ format_banner(hint ? "#{banner} (#{hint})" : banner, banner: true)
1712
1822
  end
1713
1823
  on :first, from
1714
1824
  begin
@@ -1729,7 +1839,7 @@ module Squared
1729
1839
  end
1730
1840
  elsif !kwargs[:sub] && (sync || (!exception && !stderr))
1731
1841
  print_item banner unless multiple
1732
- ret = shell(cmd, exception: exception)
1842
+ ret = shell(cmd, name: send, exception: exception)
1733
1843
  else
1734
1844
  require 'open3'
1735
1845
  if stderr
@@ -1756,16 +1866,7 @@ module Squared
1756
1866
  end
1757
1867
 
1758
1868
  def write_lines(data, grep: [], prefix: nil, sub: nil, banner: nil, loglevel: nil, pass: false, first: false)
1759
- grep = unless grep.empty?
1760
- grep.map do |val|
1761
- if val.is_a?(Regexp)
1762
- val
1763
- else
1764
- val = ".*#{val}" if prefix && !val.sub!(/\A(\^|\\A)/, '')
1765
- Regexp.new("#{prefix}#{val == '*' ? '.+' : val}")
1766
- end
1767
- end
1768
- end
1869
+ grep = grep.empty? ? nil : matchmap(grep, prefix)
1769
1870
  sub = nil if stdin?
1770
1871
  ret = 0
1771
1872
  out = []
@@ -1796,7 +1897,7 @@ module Squared
1796
1897
  def list_result(size, type, grep: [], action: 'found', from: nil)
1797
1898
  if size == 0
1798
1899
  puts empty_status("No #{type} were #{action}", 'grep', grep.join(', '))
1799
- elsif verbose
1900
+ elsif stdout?
1800
1901
  styles = theme.fetch(:banner, []).reject { |s| s.to_s.end_with?('!') }
1801
1902
  styles << :bold if styles.size <= 1
1802
1903
  puts print_footer("#{size} #{size == 1 ? type.sub(/(?:(?<!l)e)?s\z/, '') : type}",
@@ -1839,8 +1940,7 @@ module Squared
1839
1940
  glob = kwargs.fetch(:include, [])
1840
1941
  pass = kwargs.fetch(:exclude, [])
1841
1942
  ret = {}
1842
- status_data(*args).each do |line|
1843
- file = line.first
1943
+ status_data(*args).each do |file,|
1844
1944
  next if !glob.empty? && glob.none? { |val| File.fnmatch?(val, file, File::FNM_DOTMATCH) }
1845
1945
  next if !pass.empty? && pass.any? { |val| File.fnmatch?(val, file, File::FNM_DOTMATCH) }
1846
1946
 
@@ -1850,17 +1950,17 @@ module Squared
1850
1950
  end
1851
1951
 
1852
1952
  def status_data(*args)
1853
- ret = []
1854
- git_spawn('status -z -uall', *args).split("\x0").each do |line|
1855
- next unless line =~ /^(.)(.) (.+)$/
1953
+ [].tap do |ret|
1954
+ git_spawn('status -z -uall', *args).split("\x0").each do |line|
1955
+ next unless line =~ /^(.)(.) (.+)$/
1856
1956
 
1857
- ret << [$3, $2, $1]
1957
+ ret << [$3, $2, $1]
1958
+ end
1858
1959
  end
1859
- ret
1860
1960
  end
1861
1961
 
1862
1962
  def append_pull(opts, list, target: @session, flag: nil, no: nil, remote: nil, from: nil)
1863
- target << '--force' if option('force', target: target)
1963
+ target << '--force' if option('force', 'f', target: target)
1864
1964
  append_submodules(target: target, from: from)
1865
1965
  return if !remote && opts.empty?
1866
1966
 
@@ -1872,11 +1972,9 @@ module Squared
1872
1972
  when 'rebase'
1873
1973
  op << basic_option($1, $2) if VAL_GIT[:rebase][:value].include?($2)
1874
1974
  when 'shallow-since'
1875
- next unless (val = Date.parse($2))
1876
-
1877
- op << quote_option($1, val.strftime('%F %T'))
1975
+ op.append?($1) { Date.parse($2)&.strftime('%F %T') }
1878
1976
  when 'recurse-submodules'
1879
- op << basic_option($1, $2) unless op.arg?('recurse-submodules')
1977
+ op.append?($1, $2, type: :basic)
1880
1978
  when 'refspec'
1881
1979
  refspec << shell_escape($2, quote: true)
1882
1980
  end
@@ -1886,7 +1984,7 @@ module Squared
1886
1984
  op.errors << opt
1887
1985
  end
1888
1986
  end
1889
- op << '--verbose' if (flag || from == :fetch) && verbose && !op.arg?('quiet')
1987
+ op << '--verbose' if (flag || from == :fetch) && stdout? && !op.arg?('quiet')
1890
1988
  if remote
1891
1989
  op.append(remote, delim: true)
1892
1990
  if (val = option('refspec', target: target, strict: true))
@@ -1899,7 +1997,7 @@ module Squared
1899
1997
  op.swap.merge(op.map! { |opt| shell_escape(opt, quote: true) })
1900
1998
  return
1901
1999
  elsif option('all')
1902
- cmd << '--all'
2000
+ op << '--all'
1903
2001
  end
1904
2002
  op.clear(errors: true, subject: flag.to_s) if flag
1905
2003
  end
@@ -1918,7 +2016,7 @@ module Squared
1918
2016
  option_clear files
1919
2017
  true
1920
2018
  else
1921
- option('pathspec', target: target) { |val| files = split_escape val } if files.empty?
2019
+ option('pathspec', target: target) { |val| files = split_escape(val) } if files.empty?
1922
2020
  files = projectmap(files, parent: parent, pass: pass)
1923
2021
  if !files.empty?
1924
2022
  target << '--' << files.join(' ')
@@ -1976,20 +2074,28 @@ module Squared
1976
2074
  end
1977
2075
 
1978
2076
  def git_session(*cmd, opts: nil, worktree: true, **kwargs)
1979
- dir = worktree ? ["--work-tree=#{shell_quote(path)}", "--git-dir=#{shell_quote(gitpath)}"] : []
2077
+ dir = worktree ? [quote_option('work-tree', path), quote_option('git-dir', gitpath)] : []
1980
2078
  return session('git', *dir, *cmd, **kwargs) unless opts
1981
2079
 
1982
2080
  op = OptionPartition.new(opts, OPT_GIT[:common], dir, project: self)
1983
- ret = session('git', *op.to_a, *cmd, **kwargs)
1984
- [ret, op.extras]
2081
+ [session('git', *op.to_a, *cmd, **kwargs), op.extras]
1985
2082
  end
1986
2083
 
1987
2084
  def git_output(*cmd, **kwargs)
1988
2085
  git_session(*cmd, main: false, options: false, **kwargs)
1989
2086
  end
1990
2087
 
1991
- def git_spawn(*cmd, stdout: true)
1992
- source(cmd.first.is_a?(Set) ? cmd.first : git_output(*cmd), io: true, banner: false, stdout: stdout)
2088
+ def git_spawn(*cmd, exception: true, io: true, sync: true, stdout: true, banner: false, **kwargs)
2089
+ kwargs[:send] = if sync
2090
+ :system
2091
+ else
2092
+ exception = false
2093
+ io = false
2094
+ stdout = false
2095
+ :spawn
2096
+ end
2097
+ source(cmd.first.is_a?(Set) ? cmd.first : git_output(*cmd), exception: exception, io: io, sync: sync,
2098
+ stdout: stdout, banner: banner, **kwargs)
1993
2099
  end
1994
2100
 
1995
2101
  def dryrun?(*, target: @session, **)
@@ -2004,8 +2110,8 @@ module Squared
2004
2110
  target.include?('--quiet') || (target.include?('-q') && stripext(target.first) == 'git')
2005
2111
  end
2006
2112
 
2007
- def gitpath
2008
- path + '.git'
2113
+ def gitpath(*args)
2114
+ path.join('.git', *args)
2009
2115
  end
2010
2116
 
2011
2117
  def repotrack(origin, branch, quote: true)