squared 0.0.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.
@@ -0,0 +1,521 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Squared
4
+ module Repo
5
+ module Project
6
+ class Git < Base
7
+ include Common::Format
8
+
9
+ class << self
10
+ def tasks
11
+ %i[pull rebase fetch stash status].freeze
12
+ end
13
+
14
+ def to_sym
15
+ :git
16
+ end
17
+
18
+ def is_a?(path)
19
+ if path.is_a?(::String) || path.is_a?(::Pathname)
20
+ Pathname.new(path).join('.git').directory?
21
+ else
22
+ super
23
+ end
24
+ end
25
+ end
26
+
27
+ @@tasks[:git] = {
28
+ checkout: %i[branch detach force merge],
29
+ commit: %i[add amend amend-orig all no-all],
30
+ diff: %i[head cached branch],
31
+ fetch: %i[all submodules unshallow],
32
+ files: %i[cached modified deleted others],
33
+ pull: %i[rebase no-rebase commit no-commit submodules],
34
+ stash: %i[push pop apply list clear],
35
+ refs: %i[heads tags],
36
+ reset: %i[hard mixed soft],
37
+ rev: %i[commit branch]
38
+ }.freeze
39
+
40
+ def populate(*)
41
+ super
42
+ return unless vardir.exist?
43
+
44
+ namespace @name do
45
+ @@tasks[:git].each do |action, flags|
46
+ namespace action do
47
+ flags.each do |flag|
48
+ case action
49
+ when :pull
50
+ desc format_desc(action, flag, %w[all tags prune ff-only autostash dry-run])
51
+ task flag, [:opts] do |_, args|
52
+ opts = collect_args(args, :opts)
53
+ pull(flag, opts: opts)
54
+ end
55
+ when :fetch
56
+ desc format_desc(action, flag, %w[tags prune prune-tags depth=n dry-run])
57
+ task flag, [:opts] do |_, args|
58
+ opts = collect_args(args, :opts)
59
+ fetch(flag, opts: opts)
60
+ end
61
+ when :commit
62
+ if flag == :all
63
+ desc format_desc(action, flag, 'message?')
64
+ task flag, [:message] do |_, args|
65
+ commit(:all, message: args.fetch(:message, nil))
66
+ end
67
+ else
68
+ desc format_desc(action, flag, 'pathspec+')
69
+ task flag, [:pathspec] do |_, args|
70
+ guard_params(action, flag, args: args, key: :pathspec)
71
+ commit(flag, collect_args(args, :pathspec))
72
+ end
73
+ end
74
+ when :stash
75
+ if flag == :push
76
+ desc format_desc(action, flag, 'pathspec*')
77
+ task :push, [:pathspec] do |_, args|
78
+ stash(:push, collect_args(args, :pathspec))
79
+ end
80
+ else
81
+ desc format_desc(action, flag, 'commit?')
82
+ task flag, [:commit] do |_, args|
83
+ stash(flag, commit: args.commit)
84
+ end
85
+ end
86
+ when :rev
87
+ desc format_desc(action, flag, "ref?=HEAD#{flag == :commit ? ',size?' : ''}")
88
+ task flag, [:ref, :size] do |_, args|
89
+ rev(flag, ref: args.ref, size: args.size)
90
+ end
91
+ when :refs, :files
92
+ desc format_desc(action, flag, 'grep?=pattern')
93
+ task flag, [:grep] do |_, args|
94
+ send(action, flag, grep: args.fetch(:grep, nil))
95
+ end
96
+ when :diff
97
+ case flag
98
+ when :head
99
+ desc format_desc(action, flag, 'index?=0,pathspec*')
100
+ task flag, [:pathspec] do |_, args|
101
+ files = collect_args(args, :pathspec)
102
+ index = /^\d+$/.match?(files.first) && !option('index') ? files.shift.to_i : 0
103
+ diff(:head, files, index: index)
104
+ end
105
+ when :cached
106
+ desc format_desc(action, flag, 'pathspec*')
107
+ task flag, [:pathspec] do |_, args|
108
+ diff(:cached, collect_args(args, :pathspec))
109
+ end
110
+ when :branch
111
+ desc format_desc(action, flag, 'name,pathspec*')
112
+ task flag, [:name, :pathspec] do |_, args|
113
+ guard_params(action, flag, args: args, key: :name)
114
+ diff(:branch, collect_args(args, :pathspec), branch: args.name)
115
+ end
116
+ end
117
+ when :checkout
118
+ case flag
119
+ when :branch
120
+ desc format_desc(action, flag, 'name,create?=b|B,commit?,detach?=d')
121
+ task flag, [:name, :create, :commit, :detach] do |_, args|
122
+ guard_params(action, flag, args: args, key: :name)
123
+ create = args.create
124
+ if args.commit == 'd'
125
+ detach = 'd'
126
+ commit = nil
127
+ elsif create == 'd'
128
+ create = nil
129
+ commit = nil
130
+ detach = 'd'
131
+ elsif create && create.size > 1
132
+ commit = create
133
+ create = nil
134
+ detach = args.commit
135
+ else
136
+ detach = args.detach
137
+ commit = args.commit
138
+ end
139
+ guard_params('checkout', :branch, args: args, key: :create, pat: /^[Bb]$/) if create
140
+ checkout(:branch, branch: args.name, create: create, commit: commit, detach: detach)
141
+ end
142
+ when :detach
143
+ desc format_desc(action, flag, 'branch/commit?')
144
+ task flag, [:commit] do |_, args|
145
+ checkout(:detach, commit: args.commit)
146
+ end
147
+ else
148
+ desc format_desc(action, flag, 'pathspec*')
149
+ task flag, [:pathspec] do |_, args|
150
+ checkout(flag, collect_args(args, :pathspec))
151
+ end
152
+ end
153
+ when :reset
154
+ desc format_desc(action, flag, 'commit')
155
+ task flag, [:commit] do |_, args|
156
+ guard_params(action, flag, args: args, key: :commit)
157
+ reset(flag, commit: args.commit)
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ def pull(flag = nil, sync: invoked_sync?('pull', flag), opts: [])
167
+ cmd = git_session 'pull'
168
+ if flag == :'no-rebase'
169
+ cmd << '--no-rebase'
170
+ elsif flag == :rebase || option('rebase')
171
+ cmd << '--rebase'
172
+ end
173
+ if flag == :'no-commit'
174
+ cmd << '--no-commit'
175
+ elsif flag == :commit || option('commit')
176
+ cmd << '--commit'
177
+ end
178
+ cmd << '--recurse-submodules' if flag == :submodules || option('recurse-submodules')
179
+ opts.each do |val|
180
+ case val
181
+ when 'all', 'tags', 'prune', 'ff-only', 'autostash', 'dry-run'
182
+ cmd << "--#{val}"
183
+ end
184
+ end
185
+ source(sync: sync, stderr: true, exception: !@workspace.multitask?)
186
+ end
187
+
188
+ def rebase
189
+ pull(:rebase, sync: invoked_sync?('rebase'))
190
+ end
191
+
192
+ def fetch(flag = nil, opts: [])
193
+ cmd = git_session 'fetch'
194
+ cmd << '--all' if flag == :all || option('all')
195
+ cmd << '--recurse-submodules' if flag == :submodules || option('recurse-submodules')
196
+ opts.each do |val|
197
+ case val
198
+ when 'tags', 'prune', 'prune-tags', 'dry-run'
199
+ cmd << "--#{val}"
200
+ else
201
+ cmd << "--depth=#{$1}" if /^depth=(\d+)$/.match(val)
202
+ end
203
+ end
204
+ source(sync: invoked_sync?('fetch', flag), stderr: true, exception: !@workspace.multitask?)
205
+ end
206
+
207
+ def stash(flag = :push, files = [], commit: nil)
208
+ cmd = git_session 'stash', flag.to_s
209
+ case flag
210
+ when :apply, :pop
211
+ cmd << '--index' if option('index')
212
+ cmd << commit if commit
213
+ else
214
+ %w[all staged include-untracked].each { |val| cmd << "--#{val}" if option(val) }
215
+ append_message option('message')
216
+ append_pathspec files
217
+ end
218
+ source(sync: invoked_sync?('stash', flag), exception: @workspace.exception)
219
+ end
220
+
221
+ def status
222
+ cmd = git_session 'status', option('long') ? '--long' : '--short'
223
+ if (val = option('ignore-submodules'))
224
+ cmd << "--ignore-submodules=#{case val
225
+ when '0', 'none'
226
+ 'none'
227
+ when '1', 'untracked'
228
+ 'untracked'
229
+ when '2', 'dirty'
230
+ 'dirty'
231
+ else
232
+ 'all'
233
+ end}"
234
+ end
235
+ if (val = option('pathspec'))
236
+ append_pathspec split_escape(val)
237
+ end
238
+ out, banner = source(io: true)
239
+ if banner && invoked_sync?('status')
240
+ print_item banner
241
+ banner = nil
242
+ end
243
+ ret = write_lines(out, banner: banner, sub: [
244
+ { pat: /^(.)([A-Z])(.+)$/, styles: :red, index: 2 },
245
+ { pat: /^([A-Z])(.+)$/, styles: :green },
246
+ { pat: /^(\?\?)(.+)$/, styles: :red },
247
+ { pat: /^(## )(.+)(\.{3})(.+)$/, styles: [nil, :green, nil, :red], index: -1 }
248
+ ])
249
+ list_result(ret, 'files', action: 'modified')
250
+ end
251
+
252
+ def reset(flag, commit: nil)
253
+ source "git reset --#{flag} #{commit}"
254
+ end
255
+
256
+ def checkout(flag, files = [], branch: nil, create: nil, commit: nil, detach: nil)
257
+ cmd = git_session 'checkout'
258
+ case flag
259
+ when :branch
260
+ cmd << '--detach' if detach == 'd' || option('detach')
261
+ if create
262
+ cmd << "-#{create} #{branch}"
263
+ if (val = option('start-point'))
264
+ cmd << val
265
+ end
266
+ else
267
+ cmd << branch
268
+ end
269
+ cmd << commit if commit
270
+ when :detach
271
+ cmd << "--#{flag}"
272
+ cmd << commit if commit
273
+ else
274
+ cmd << "--#{flag}"
275
+ if option('ours')
276
+ cmd << '--ours'
277
+ elsif option('theirs')
278
+ cmd << '--theirs'
279
+ end
280
+ if (val = option('tree-ish'))
281
+ cmd << val
282
+ end
283
+ append_pathspec files
284
+ end
285
+ source
286
+ end
287
+
288
+ def rev(flag, ref: nil, size: nil)
289
+ opt = if flag == :branch
290
+ '--abbrev-ref'
291
+ else
292
+ (n = size.to_i) > 0 ? "--short=#{[n, 5].max}" : '--verify'
293
+ end
294
+ git_session 'rev-parse', opt, ref || 'HEAD'
295
+ source
296
+ end
297
+
298
+ def refs(flag, grep: nil)
299
+ git_session 'ls-remote', " --#{flag}"
300
+ out, banner = source(io: true)
301
+ print_item banner
302
+ ret = write_lines(out, grep: grep)
303
+ list_result(ret, flag.to_s, grep: grep)
304
+ end
305
+
306
+ def files(flag, grep: nil)
307
+ git_session 'ls-files', " --#{flag}"
308
+ out, banner = source(io: true)
309
+ print_item banner
310
+ ret = write_lines(out, grep: grep)
311
+ list_result(ret, 'files', grep: grep)
312
+ end
313
+
314
+ def diff(flag, files = [], branch: nil, index: 0)
315
+ cmd = git_session 'diff'
316
+ if /^#[0-9a-f]{5,40}\#$/.match?(sha = files.first)
317
+ files.shift
318
+ else
319
+ sha = nil
320
+ end
321
+ if (val = option('unified')).to_i > 0
322
+ cmd << "--unified=#{val}"
323
+ end
324
+ append_nocolor
325
+ case flag
326
+ when :cached
327
+ cmd << '--cached'
328
+ cmd << '--merge-base' if option('merge-base')
329
+ when :branch
330
+ cmd << branch
331
+ else
332
+ if (val = option('index')) || index > 0
333
+ cmd << "HEAD~#{val || index}"
334
+ elsif sha && option('merge-base')
335
+ cmd << '--merge-base'
336
+ end
337
+ end
338
+ cmd << sha if sha
339
+ append_pathspec files
340
+ source
341
+ end
342
+
343
+ def commit(flag, files = [], message: nil, pass: false)
344
+ message = option('message') if message.nil?
345
+ amend = flag.to_s.start_with?('amend')
346
+ if !message && !amend
347
+ return if pass
348
+
349
+ raise ArgumentError, message('commit', 'GIT_MESSAGE="description"', hint: 'missing')
350
+ end
351
+ pathspec = if flag == :all || (amend && files.size == 1 && files.first == '*')
352
+ '--all'
353
+ else
354
+ files = source_path(as_a(files))
355
+ raise ArgumentError, message('commit', 'pathspec', hint: 'missing') if files.empty?
356
+
357
+ "-- #{files.join(' ')}"
358
+ end
359
+ if !push?
360
+ source('git branch -vv --list', io: true, banner: false)[0].each do |val|
361
+ origin = %r{^\* [^\[]+(?<= )\[([\w.-/]+?)/([\w.-]+)\] }.match(val)
362
+ next unless origin
363
+
364
+ @origin = origin[1]
365
+ @branch = origin[2]
366
+ break
367
+ end
368
+ end
369
+ raise ArgumentError, message('commit', 'work tree is not usable') unless push?
370
+
371
+ cmd = git_session 'commit'
372
+ cmd << '--dry-run' if option('dry-run')
373
+ if amend
374
+ cmd << '--amend'
375
+ else
376
+ cmd.delete('--amend')
377
+ end
378
+ if message
379
+ append_message message
380
+ elsif flag == :'amend-orig' || option('no-edit')
381
+ cmd << '--no-edit'
382
+ end
383
+ a = ['git add --verbose']
384
+ b = ['git push']
385
+ if dry_run?
386
+ a << '--dry-run'
387
+ b << '--dry-run'
388
+ end
389
+ a << pathspec
390
+ b << '--force' if amend
391
+ b << @origin << @branch
392
+ puts if pass
393
+ source a.join(' ')
394
+ source
395
+ source b.join(' ')
396
+ end
397
+
398
+ protected
399
+
400
+ def source(cmd = @session, exception: true, banner: true, io: false, sync: true, stdout: false, stderr: false)
401
+ cmd = close_session(cmd)
402
+ info cmd
403
+ begin
404
+ banner = format_banner(cmd.gsub(trailing_slash(@path), ''), banner: banner, multitask: true)
405
+ cmd = cmd.sub(/^git\b/, "git --work-tree #{shell_quote(@path)} --git-dir #{shell_quote(vardir)}")
406
+ if io
407
+ [IO.popen(cmd), banner]
408
+ elsif pipe? ? sync : stdout
409
+ print_item banner
410
+ puts `#{cmd}`
411
+ elsif sync || (!exception && !stderr)
412
+ print_item banner
413
+ shell(cmd, exception: exception)
414
+ else
415
+ require 'open3'
416
+ if stderr
417
+ Open3.popen3(cmd) do |_, out, err|
418
+ ret = write_lines(out, banner: banner, pass: true)
419
+ if ret == 0
420
+ write_lines(err, banner: banner)
421
+ else
422
+ write_lines(err, loglevel: Logger::DEBUG)
423
+ end
424
+ end
425
+ else
426
+ Open3.popen2e(cmd) { |_, out| write_lines(out, banner: banner) }
427
+ end
428
+ end
429
+ rescue StandardError => e
430
+ error e
431
+ raise if exception
432
+
433
+ emphasize e, message('git', cmd)
434
+ end
435
+ end
436
+
437
+ def write_lines(iter, banner: nil, loglevel: nil, grep: nil, sub: nil, pass: false)
438
+ grep = Regexp.new(grep == '*' ? '.+' : grep) if grep.is_a?(::String)
439
+ sub = nil if pipe?
440
+ found = 0
441
+ lines = []
442
+ iter.each do |line|
443
+ next if grep && !grep.match?(line)
444
+
445
+ if loglevel
446
+ log loglevel, line
447
+ else
448
+ sub&.each { |h| line = sub_style(line, **h) }
449
+ if banner
450
+ lines << line
451
+ else
452
+ puts line
453
+ end
454
+ end
455
+ found += 1
456
+ end
457
+ print_item banner, lines if banner && (found > 0 || !pass)
458
+ found
459
+ end
460
+
461
+ def list_result(size, type, action: 'found', grep: nil)
462
+ return unless verbose?
463
+
464
+ if size > 0
465
+ args = styles[:banner]&.reject { |s| s.to_s.end_with?('!') } || []
466
+ args << :bold if args.size <= 1
467
+ puts Project.footer "#{sub_style(size, *args)} #{size == 1 ? type.sub(/s$/, '') : type}"
468
+ else
469
+ puts empty_status("No #{type} were #{action}", 'grep', grep)
470
+ end
471
+ end
472
+
473
+ def source_path(files)
474
+ files.select { |val| source_path?(val) }.map { |val| shell_quote(base_path(val.strip)) }
475
+ end
476
+
477
+ def source_path?(path)
478
+ return path.to_s.start_with?("#{@path}#{::File::SEPARATOR}") if Pathname.new(path).absolute?
479
+
480
+ !%r{^\.\.[/\\]}.match?(path)
481
+ end
482
+
483
+ private
484
+
485
+ def append_pathspec(files)
486
+ files = source_path(files)
487
+ @session << "-- #{files.join(' ')}" unless files.empty?
488
+ end
489
+
490
+ def append_message(val)
491
+ @session << "--message \"#{double_quote(val)}\"" if val
492
+ end
493
+
494
+ def invoked_sync?(action, flag = nil)
495
+ !flag.nil? || super(action)
496
+ end
497
+
498
+ def option(val)
499
+ ret = ENV.fetch("GIT_#{val.gsub(/\W/, '_').upcase}", '')
500
+ ret unless ret.empty? || ret == '0'
501
+ end
502
+
503
+ def git_session(*cmd)
504
+ session('git', *cmd)
505
+ end
506
+
507
+ def vardir
508
+ @vardir ||= base_path('.git')
509
+ end
510
+
511
+ def push?
512
+ !@origin.nil? && !@branch.nil?
513
+ end
514
+
515
+ def dry_run?
516
+ @session.include?('--dry-run')
517
+ end
518
+ end
519
+ end
520
+ end
521
+ end