git-improved 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1626 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+ # frozen_string_literal: true
4
+
5
+ ###
6
+ ### $Release: 0.1.0 $
7
+ ### $Copyright: copyright(c) 2023 kwatch@gmail.com $
8
+ ### $License: MIT License $
9
+ ###
10
+
11
+ require 'benry/cmdapp'
12
+ #require 'benry/unixcommand' # lazy load
13
+
14
+
15
+ if (RUBY_VERSION.split('.').collect(&:to_i) <=> [2, 6]) < 0
16
+ Kernel.module_eval do
17
+ alias __orig_system system
18
+ def system(*args, **kws)
19
+ if kws.delete(:exception)
20
+ __orig_system(*args, **kws) or
21
+ raise "Command failed: #{args.join(' ')}"
22
+ else
23
+ __orig_system(*args, **kws)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+
30
+ module GitImproved
31
+
32
+ VERSION = "$Version: 0.0.0 $".split()[1]
33
+ ENVVAR_INITFILE = "GI_INITFILE"
34
+
35
+
36
+ class GitConfig
37
+
38
+ def initialize()
39
+ @prompt = "[#{File.basename($0)}]$ "
40
+ @default_action = "status:here" # or: "status:info"
41
+ @initial_branch = "main" # != 'master'
42
+ @initial_commit_message = "Initial commit (empty)"
43
+ @gitignore_items = ["*~", "*.DS_Store", "tmp", "*.pyc"]
44
+ @history_graph_format = "%C(auto)%h %ad <%al> | %d %s"
45
+ #@history_graph_format = "\e[32m%h %ad\e[0m <%al> \e[2m|\e[0m\e[33m%d\e[0m %s"
46
+ @history_graph_options = ["--graph", "--date=short", "--decorate"]
47
+ end
48
+
49
+ attr_accessor :prompt
50
+ attr_accessor :default_action
51
+ attr_accessor :initial_branch
52
+ attr_accessor :initial_commit_message
53
+ attr_accessor :gitignore_items
54
+ attr_accessor :history_graph_format
55
+ #attr_accessor :history_graph_format
56
+ attr_accessor :history_graph_options
57
+
58
+ end
59
+
60
+
61
+ GIT_CONFIG = GitConfig.new
62
+
63
+ APP_CONFIG = Benry::CmdApp::Config.new("Git Improved", VERSION).tap do |c|
64
+ c.option_topic = true
65
+ c.option_quiet = true
66
+ c.option_color = true
67
+ c.option_dryrun = true
68
+ c.format_option = " %-19s : %s"
69
+ c.format_action = " %-19s : %s"
70
+ c.backtrace_ignore_rexp = /\/gi(?:t-improved\.rb)?:/
71
+ c.help_postamble = {
72
+ "Example:" => <<END,
73
+ $ mkdir mysample # or: gi repo:clone github:<user>/<repo>
74
+ $ cd mysample
75
+ $ gi repo:init -u yourname -e yourname@gmail.com
76
+ $ vi README.md # create a new file
77
+ $ gi track README.md # track files into the repository
78
+ $ gi cc "add README file" # commit changes
79
+ $ vi README.md # update an existing file
80
+ $ gi stage . # add changes into staging area
81
+ $ gi staged # show changes in staging area
82
+ $ gi cc "update README file" # commit changes
83
+ $ gi repo:remote:origin github:yourname/mysample # set remote repo
84
+ $ gi push # upload local commits to remote repo
85
+ END
86
+ "Document:" => " https://kwatch.github.io/git-improved/",
87
+ }
88
+ end
89
+
90
+
91
+ class GitCommandFailed < Benry::CmdApp::CommandError
92
+
93
+ def initialize(git_command=nil)
94
+ super "Git command failed: #{git_command}"
95
+ @git_command = git_command
96
+ end
97
+
98
+ attr_reader :git_commit
99
+
100
+ end
101
+
102
+
103
+ module ActionHelper
104
+
105
+ def git(*args)
106
+ argstr = args.collect {|s| _qq(s) }.join(" ")
107
+ echoback("git #{argstr}")
108
+ return if $DRYRUN_MODE
109
+ out = $SUBPROCESS_OUTPUT || nil
110
+ if out
111
+ system(["git", "git"], *args, out: out, err: out) or
112
+ raise GitCommandFailed, "git #{argstr}"
113
+ else
114
+ system(["git", "git"], *args) or
115
+ raise GitCommandFailed, "git #{argstr}"
116
+ end
117
+ end
118
+
119
+ def git!(*args)
120
+ git(*args)
121
+ rescue GitCommandFailed
122
+ false
123
+ end
124
+
125
+ def system!(command)
126
+ out = $SUBPROCESS_OUTPUT || nil
127
+ if out
128
+ system command, exception: true, out: out, err: out
129
+ else
130
+ system command, exception: true
131
+ end
132
+ end
133
+
134
+ protected
135
+
136
+ def curr_branch()
137
+ return `git rev-parse --abbrev-ref HEAD`.strip()
138
+ end
139
+
140
+ def prev_branch()
141
+ #s = `git rev-parse --symbolic-full-name @{-1}`.strip()
142
+ #return s.split("/").last
143
+ return `git rev-parse --abbrev-ref @{-1}`.strip()
144
+ end
145
+
146
+ def parent_branch()
147
+ # ref: https://stackoverflow.com/questions/3161204/
148
+ # git show-branch -a \
149
+ # | sed 's/].*//' \
150
+ # | grep '\*' \
151
+ # | grep -v "\\[$(git branch --show-current)\$" \
152
+ # | head -n1 \
153
+ # | sed 's/^.*\[//'
154
+ curr = curr_branch()
155
+ end_str = "[#{curr}\n"
156
+ output = `git show-branch -a`
157
+ output.each_line do |line|
158
+ line = line.sub(/\].*/, '')
159
+ next unless line =~ /\*/
160
+ next if line.end_with?(end_str)
161
+ parent = line.sub(/^.*?\[/, '').strip()
162
+ return parent
163
+ end
164
+ return nil
165
+ end
166
+
167
+ def resolve_branch(branch)
168
+ case branch
169
+ when "CURR" ; return curr_branch()
170
+ when "PREV" ; return prev_branch()
171
+ when "PARENT" ; return parent_branch()
172
+ when "-" ; return prev_branch()
173
+ else ; return branch
174
+ end
175
+ end
176
+
177
+ def resolve_except_prev_branch(branch)
178
+ if branch == nil || branch == "-" || branch == "PREV"
179
+ return "-"
180
+ else
181
+ return resolve_branch(branch)
182
+ end
183
+ end
184
+
185
+ def resolve_repository_url(url)
186
+ case url
187
+ when /^github:/
188
+ url =~ /^github:(?:\/\/)?([^\/]+)\/([^\/]+)$/ or
189
+ raise action_error("Invalid GitHub URL: #{url}")
190
+ user = $1; project = $2
191
+ return "git@github.com:#{user}/#{project}.git"
192
+ when /^gitlab:/
193
+ url =~ /^gitlab:(?:\/\/)?([^\/]+)\/([^\/]+)$/ or
194
+ raise action_error("Invalid GitLub URL: #{url}")
195
+ user = $1; project = $2
196
+ return "git@gitlab.com:#{user}/#{project}.git"
197
+ else
198
+ return url
199
+ end
200
+ end
201
+
202
+ def remote_repo_of_branch(branch)
203
+ branch_ = Regexp.escape(branch)
204
+ output = `git config --get-regexp '^branch\\.#{branch_}\\.remote'`
205
+ arr = output.each_line.grep(/^branch\..*?\.remote (.*)/) { $1 }
206
+ remote = arr.empty? ? nil : arr[0]
207
+ return remote
208
+ end
209
+
210
+ def color_mode?
211
+ return $stdout.tty?
212
+ end
213
+
214
+ def ask_to_user(question)
215
+ print "#{question} "
216
+ $stdout.flush()
217
+ answer = $stdin.readline().strip()
218
+ return answer.empty? ? nil : answer
219
+ end
220
+
221
+ def ask_to_user!(question)
222
+ answer = ""
223
+ while answer.empty?
224
+ print "#{question}: "
225
+ $stdout.flush()
226
+ answer = $stdin.read().strip()
227
+ end
228
+ return answer
229
+ end
230
+
231
+ def confirm(question, default_yes: true)
232
+ if default_yes
233
+ return _confirm(question, "[Y/n]", "Y") {|ans| ans !~ /\A[nN]/ }
234
+ else
235
+ return _confirm(question, "[y/N]", "N") {|ans| ans !~ /\A[yY]/ }
236
+ end
237
+ end
238
+
239
+ private
240
+
241
+ def _confirm(question, prompt, default_answer, &block)
242
+ print "#{question} #{prompt}: "
243
+ $stdout.flush()
244
+ answer = $stdin.readline().strip()
245
+ anser = default_answer if answer.empty?
246
+ return yield(answer)
247
+ end
248
+
249
+ def _qq(str, force: false)
250
+ if force || str =~ /\A[-+\w.,:=%\/^@]+\z/
251
+ return str
252
+ elsif str =~ /\A(-[-\w]+=)/
253
+ return $1 + _qq($')
254
+ else
255
+ #return '"' + str.gsub(/[$!`\\"]/) { "\\#{$&}" } + '"'
256
+ return '"' + str.gsub(/[$!`\\"]/, "\\\\\\&") + '"'
257
+ end
258
+ end
259
+
260
+ def _same_commit_id?(branch1, branch2)
261
+ arr = `git rev-parse #{branch1} #{branch2}`.split()
262
+ return arr[0] == arr[1]
263
+ end
264
+
265
+ end
266
+
267
+
268
+ class GitAction < Benry::CmdApp::Action
269
+ #include Benry::UnixCommand ## include lazily
270
+ include ActionHelper
271
+
272
+ protected
273
+
274
+ def prompt()
275
+ return "[gi]$ "
276
+ end
277
+
278
+ def echoback(command)
279
+ e1, e2 = color_mode?() ? ["\e[2m", "\e[0m"] : ["", ""]
280
+ puts "#{e1}#{prompt()}#{command}#{e2}" unless $QUIET_MODE
281
+ #puts "#{e1}#{super}#{e2}" unless $QUIET_MODE
282
+ end
283
+
284
+ def _lazyload_unixcommand()
285
+ require 'benry/unixcommand'
286
+ GitAction.class_eval {
287
+ include Benry::UnixCommand
288
+ remove_method :mkdir, :cd, :touch
289
+ }
290
+ end
291
+ private :_lazyload_unixcommand
292
+
293
+ def sys(*args)
294
+ if $DRYRUN_MODE
295
+ echoback args.join(' ')
296
+ else
297
+ _lazyload_unixcommand()
298
+ super
299
+ end
300
+ end
301
+
302
+ def sys!(*args)
303
+ if $DRYRUN_MODE
304
+ echoback args.join(' ')
305
+ else
306
+ _lazyload_unixcommand()
307
+ super
308
+ end
309
+ end
310
+
311
+ def mkdir(*args)
312
+ if $DRYRUN_MODE
313
+ echoback "mkdir #{args.join(' ')}"
314
+ else
315
+ _lazyload_unixcommand()
316
+ super
317
+ end
318
+ end
319
+
320
+ def cd(dir, &block)
321
+ if $DRYRUN_MODE
322
+ echoback "cd #{dir}"
323
+ if File.directory?(dir)
324
+ Dir.chdir dir, &block
325
+ else
326
+ yield if block_given?()
327
+ end
328
+ echoback "cd -" if block_given?()
329
+ else
330
+ _lazyload_unixcommand()
331
+ super
332
+ end
333
+ end
334
+
335
+ def touch(*args)
336
+ if $DRYRUN_MODE
337
+ echoback "touch #{args.join(' ')}"
338
+ else
339
+ _lazyload_unixcommand()
340
+ super
341
+ end
342
+ end
343
+
344
+ public
345
+
346
+
347
+ ##
348
+ ## status:
349
+ ##
350
+ category "status:" do
351
+
352
+ @action.("same as 'stats:compact .'", important: true)
353
+ def here()
354
+ git "status", "-sb", "."
355
+ end
356
+
357
+ @action.("show various infomation of current status")
358
+ def info(path=".")
359
+ #command = "git status -sb #{path} | awk '/^\\?\\? /{print $2}' | sed 's!/$!!' | xargs ls -dF --color"
360
+ command = "git status -sb #{path} | sed -n 's!/$!!;/^??/s/^?? //p' | xargs ls -dF --color"
361
+ #command = "git status -sb #{path} | perl -ne 'print if s!^\\?\\? (.*?)/?$!\\1!' | xargs ls -dF --color"
362
+ #command = "git status -sb #{path} | ruby -ne \"puts \\$1 if /^\\?\\? (.*?)\\/?$/\" | xargs ls -dF --color"
363
+ echoback command
364
+ system! command
365
+ git "status", "-sb", "-uno", path
366
+ #run_action "branch:echo", "CURR"
367
+ end
368
+
369
+ status_optset = optionset {
370
+ @option.(:trackedonly, "-U", "ignore untracked files")
371
+ }
372
+
373
+ @action.("show status in compact format")
374
+ @optionset.(status_optset)
375
+ def compact(*path, trackedonly: false)
376
+ opts = trackedonly ? ["-uno"] : []
377
+ git "status", "-sb", *opts, *path
378
+ end
379
+
380
+ @action.("show status in default format")
381
+ @optionset.(status_optset)
382
+ def default(*path, trackedonly: false)
383
+ opts = trackedonly ? ["-uno"] : []
384
+ git "status", *opts, *path
385
+ end
386
+
387
+ end
388
+
389
+ define_alias "status", "status:compact"
390
+
391
+
392
+ ##
393
+ ## branch:
394
+ ##
395
+ category "branch:" do
396
+
397
+ @action.("list branches")
398
+ @option.(:all , "-a, --all" , "list both local and remote branches (default)")
399
+ @option.(:remote, "-r, --remote", "list remote branches")
400
+ @option.(:local , "-l, --local" , "list local branches")
401
+ def list(all: false, remote: false, local: false)
402
+ opt = remote ? "-r" : local ? "-l" : "-a"
403
+ git "branch", opt
404
+ end
405
+
406
+ @action.("switch to previous or other branch", important: true)
407
+ def switch(branch=nil)
408
+ branch = resolve_except_prev_branch(branch)
409
+ git "checkout", branch
410
+ #git "switch", branch
411
+ end
412
+
413
+ @action.("create a new branch, not switch to it")
414
+ @option.(:on, "--on=<commit>", "commit-id on where the new branch will be created")
415
+ @option.(:switch, "-w, --switch", "switch to the new branch after created")
416
+ def create(branch, on: nil, switch: false)
417
+ args = on ? [on] : []
418
+ git "branch", branch, *args
419
+ git "checkout", branch if switch
420
+ end
421
+
422
+ @action.("create a new branch and switch to it", important: true)
423
+ @option.(:on, "--on=<commit>", "commit-id on where the new branch will be created")
424
+ def fork(branch, on: nil)
425
+ args = on ? [on] : []
426
+ git "checkout", "-b", branch, *args
427
+ end
428
+
429
+ mergeopts = optionset {
430
+ @option.(:delete , "-d, --delete", "delete the current branch after merged")
431
+ @option.(:fastforward, " --ff", "use fast-forward merge")
432
+ @option.(:reuse , "-M", "reuse commit message (not invoke text editor for it)")
433
+ }
434
+
435
+ @action.("merge current branch into previous or other branch", important: true)
436
+ @optionset.(mergeopts)
437
+ def join(branch=nil, delete: false, fastforward: false, reuse: false)
438
+ into_branch = resolve_branch(branch || "PREV")
439
+ __merge(curr_branch(), into_branch, true, fastforward, delete, reuse)
440
+ end
441
+
442
+ @action.("merge previous or other branch into current branch")
443
+ @optionset.(mergeopts)
444
+ def merge(branch=nil, delete: false, fastforward: false, reuse: false)
445
+ merge_branch = resolve_branch(branch || "PREV")
446
+ __merge(merge_branch, curr_branch(), false, fastforward, delete, reuse)
447
+
448
+ end
449
+
450
+ def __merge(merge_branch, into_branch, switch, fastforward, delete, reuse)
451
+ b = proc {|s| "'\e[1m#{s}\e[0m'" } # bold font
452
+ #msg = "Merge #{b.(merge_branch)} branch into #{b.(into_branch)}?"
453
+ msg = switch \
454
+ ? "Merge current branch #{b.(merge_branch)} into #{b.(into_branch)}." \
455
+ : "Merge #{b.(merge_branch)} branch into #{b.(into_branch)}."
456
+ if confirm(msg + " OK?")
457
+ _check_fastforward_merge_available(into_branch, merge_branch)
458
+ opts = fastforward ? ["--ff-only"] : ["--no-ff"]
459
+ opts << "--no-edit" if reuse
460
+ git "checkout", into_branch if switch
461
+ git "merge", *opts, (switch ? "-" : merge_branch)
462
+ git "branch", "-d", merge_branch if delete
463
+ else
464
+ puts "** Not joined." if switch
465
+ puts "** Not merged." unless switch
466
+ end
467
+ end
468
+ private :__merge
469
+
470
+ def _check_fastforward_merge_available(parent_branch, child_branch)
471
+ parent, child = parent_branch, child_branch
472
+ cmd = "git merge-base --is-ancestor #{parent} #{child}"
473
+ result_ok = system cmd
474
+ result_ok or
475
+ raise action_error("Cannot merge '#{child}' branch; rebase it onto '#{parent}' in advance.")
476
+ end
477
+ private :_check_fastforward_merge_available
478
+
479
+ @action.("create a new local branch from a remote branch")
480
+ @option.(:remote, "--remote=<remote>", "remote repository name (default: origin)")
481
+ def checkout(branch, remote: "origin")
482
+ local_branch = branch
483
+ remote_branch = "#{remote}/#{branch}"
484
+ git "checkout", "-b", local_branch, remote_branch
485
+ end
486
+
487
+ @action.("rename the current branch to other name")
488
+ @option.(:target, "-t <branch>", "target branch instead of current branch")
489
+ def rename(new_branch, target: nil)
490
+ old_branch = target || curr_branch()
491
+ git "branch", "-m", old_branch, new_branch
492
+ end
493
+
494
+ @action.("delete a branch")
495
+ @option.(:force, "-f, --force", "delete forcedly even if not merged")
496
+ @option.(:remote, "-r, --remote[=origin]", "delete a remote branch")
497
+ def delete(branch, force: false, remote: nil)
498
+ if branch == nil
499
+ branch = curr_branch()
500
+ yes = confirm "Are you sure to delete current branch '#{branch}'?", default_yes: false
501
+ return unless yes
502
+ git "checkout", "-" unless remote
503
+ else
504
+ branch = resolve_branch(branch)
505
+ end
506
+ if remote
507
+ remote = "origin" if remote == true
508
+ opts = force ? ["-f"] : []
509
+ #git "push", *opts, remote, ":#{branch}"
510
+ git "push", "--delete", *opts, remote, branch
511
+ else
512
+ opts = force ? ["-D"] : ["-d"]
513
+ git "branch", *opts, branch
514
+ end
515
+ end
516
+
517
+ @action.("change commit-id of current HEAD")
518
+ @option.(:restore, "--restore", "restore files after reset")
519
+ def reset(commit, restore: false)
520
+ opts = []
521
+ opts << "--hard" if restore
522
+ git "reset", *opts, commit
523
+ end
524
+
525
+ @action.("rebase (move) current branch on top of other branch")
526
+ @option.(:from, "--from=<commit-id>", "commit-id where current branch started")
527
+ def rebase(branch_onto, branch_upstream=nil, from: nil)
528
+ br_onto = resolve_branch(branch_onto)
529
+ if from
530
+ git "rebase", "--onto=#{br_onto}", from+"^"
531
+ elsif branch_upstream
532
+ git "rebase", "--onto=#{br_onto}", resolve_branch(branch_upstream)
533
+ else
534
+ git "rebase", br_onto
535
+ end
536
+ end
537
+
538
+ @action.("git pull && git stash && git rebase && git stash pop")
539
+ @option.(:rebase, "-b, --rebase", "rebase if prev branch updated")
540
+ def update(branch=nil, rebase: false)
541
+ if curr_branch() == GIT_CONFIG.initial_branch
542
+ git "pull"
543
+ return
544
+ end
545
+ #
546
+ branch ||= prev_branch()
547
+ remote = remote_repo_of_branch(branch) or
548
+ raise action_error("Previous branch '#{branch}' has no remote repo. (Hint: run `gi branch:upstream -t #{branch} origin`.)")
549
+ puts "[INFO] previous: #{branch}, remote: #{remote}" unless $QUIET_MODE
550
+ #
551
+ git "fetch"
552
+ file_changed = ! `git diff`.empty?
553
+ remote_updated = ! _same_commit_id?(branch, "#{remote}/#{branch}")
554
+ rebase_required = ! `git log --oneline HEAD..#{branch}`.empty?
555
+ if remote_updated || (rebase && rebase_required)
556
+ git "stash", "push", "-q" if file_changed
557
+ if remote_updated
558
+ git "checkout", "-q", branch
559
+ #git "reset", "--hard", "#{remote}/#{branch}"
560
+ git "pull"
561
+ git "checkout", "-q", "-"
562
+ end
563
+ git "rebase", branch if rebase
564
+ git "stash", "pop", "-q" if file_changed
565
+ end
566
+ end
567
+
568
+ @action.("print upstream repo name of current branch")
569
+ def upstream()
570
+ #git! "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"
571
+ branch = curr_branch()
572
+ echoback "git config --get-regexp '^branch\\.#{branch}\\.remote' | awk '{print $2}'"
573
+ output = `git config --get-regexp '^branch\\.#{branch}\\.remote'`
574
+ output.each_line {|line| puts line.split()[1] }
575
+ end
576
+
577
+ @action.("show current branch name")
578
+ def current()
579
+ git "rev-parse", "--abbrev-ref", "HEAD"
580
+ #git "symbolic-ref", "--short", "HEAD"
581
+ #git "branch", "--show-current"
582
+ end
583
+
584
+ @action.("show previous branch name")
585
+ def previous()
586
+ #git "rev-parse", "--symbolic-full-name", "@{-1}"
587
+ git "rev-parse", "--abbrev-ref", "@{-1}"
588
+ end
589
+
590
+ @action.("show parent branch name (EXPERIMENTAL)")
591
+ def parent()
592
+ # ref: https://stackoverflow.com/questions/3161204/
593
+ command = <<~'END'
594
+ git show-branch -a \
595
+ | sed 's/].*//' \
596
+ | grep '\*' \
597
+ | grep -v "\\[$(git branch --show-current)\$" \
598
+ | head -n1 \
599
+ | sed 's/^.*\[//'
600
+ END
601
+ echoback(command.gsub(/\\\n/, '').strip())
602
+ puts parent_branch()
603
+ end
604
+
605
+ @action.("print CURR/PREV/PARENT branch name")
606
+ def echo(branch)
607
+ case branch
608
+ when "CURR" ; run_action "current"
609
+ when "PREV", "-" ; run_action "previous"
610
+ when "PARENT" ; run_action "parent" # (EXPERIMENTAL)
611
+ else ; git "rev-parse", "--abbrev-ref", branch
612
+ end
613
+ end
614
+
615
+ end
616
+
617
+ define_alias("branches", "branch:list")
618
+ define_alias("branch" , "branch:create")
619
+ define_alias("switch" , "branch:switch")
620
+ define_alias("sw" , "branch:switch")
621
+ define_alias("fork" , "branch:fork")
622
+ define_alias("join" , "branch:join")
623
+ define_alias("merge" , "branch:merge")
624
+ define_alias("update" , "branch:update")
625
+
626
+
627
+ ##
628
+ ## file:
629
+ ##
630
+ category "file:" do
631
+
632
+ @action.("list (un)tracked/ignored/missing files")
633
+ @option.(:filtertype, "-F <filtertype>", "one of:", detail: <<~END)
634
+ - tracked : only tracked files (default)
635
+ - untracked : only not-tracked files
636
+ - ignored : ignored files by '.gitignore'
637
+ - missing : tracked but missing files
638
+ END
639
+ @option.(:full, "--full", "show full list")
640
+ def list(path=".", filtertype: "tracked", full: false)
641
+ method_name = "__file__list__#{filtertype}"
642
+ respond_to?(method_name, true) or (
643
+ s = self.private_methods.grep(/^__file__list__(.*)/) { $1 }.join('/')
644
+ raise option_error("#{filtertype}: Uknown filter type (expected: #{s}})")
645
+ )
646
+ __send__(method_name, path, full)
647
+ end
648
+
649
+ private
650
+
651
+ def __file__list__tracked(path, full)
652
+ paths = path ? [path] : []
653
+ git "ls-files", *paths
654
+ end
655
+
656
+ def __file__list__untracked(path, full)
657
+ opt = full ? " -u" : nil
658
+ echoback "git status -s#{opt} #{path} | grep '^?? '"
659
+ output = `git status -s#{opt} #{path}`
660
+ puts output.each_line().grep(/^\?\? /)
661
+ end
662
+
663
+ def __file__list__ignored(path, full)
664
+ opt = full ? "--ignored=matching" : "--ignored"
665
+ echoback "git status -s #{opt} #{path} | grep '^!! '"
666
+ output = `git status -s #{opt} #{path}`
667
+ puts output.each_line().grep(/^!! /)
668
+ end
669
+
670
+ def __file__list__missing(path, full)
671
+ paths = path ? [path] : []
672
+ git "ls-files", "--deleted", *paths
673
+ end
674
+
675
+ public
676
+
677
+ ## TODO: should move to 'file:' category?
678
+ @action.("register files into the repository", important: true)
679
+ @option.(:force, "-f, --force", "allow to track ignored files")
680
+ @option.(:recursive, "-r, --recursive", "track files under directories")
681
+ #@option.(:allow_empty_dir, "-e, --allow-empty-dir", "create '.gitkeep' to track empty directory")
682
+ def track(file, *file2, force: false, recursive: false)
683
+ files = [file] + file2
684
+ files.each do |x|
685
+ output = `git ls-files -- #{x}`
686
+ output.empty? or
687
+ raise action_error("#{x}: Already tracked.")
688
+ end
689
+ files.each do |x|
690
+ if File.directory?(x)
691
+ recursive or
692
+ raise action_error("#{x}: File expected, but is a directory (specify `-r` or `--recursive` otpion to track files under the directory).")
693
+ end
694
+ end
695
+ opts = force ? ["-f"] : []
696
+ git "add", *opts, *files
697
+ end
698
+
699
+ @action.("show changes of files", important: true)
700
+ def changes(*path)
701
+ git "diff", *path
702
+ end
703
+
704
+ @action.("move files into a directory")
705
+ @option.(:to, "--to=<dir>", "target directory")
706
+ def move(file, *file2, to: nil)
707
+ dir = to
708
+ dir != nil or
709
+ raise option_error("Option `--to=<dir>` required.")
710
+ File.exist?(dir) or
711
+ raise option_error("--to=#{dir}: Directory not exist (create it first).")
712
+ File.directory?(dir) or
713
+ raise option_error("--to=#{dir}: Not a directory (to rename files, use 'file:rename' action instead).")
714
+ files = [file] + file2
715
+ git "mv", *files, dir
716
+ end
717
+
718
+ @action.("rename a file or directory to new name")
719
+ def rename(old_file, new_file)
720
+ ! File.exist?(new_file) or
721
+ raise action_failed("#{new_file}: Already exist.")
722
+ git "mv", old_file, new_file
723
+ end
724
+
725
+ @action.("delete files or directories")
726
+ @option.(:recursive, "-r, --recursive", "delete files recursively.")
727
+ def delete(file, *file2, recursive: false)
728
+ files = [file] + file2
729
+ opts = recursive ? ["-r"] : []
730
+ git "rm", *opts, *files
731
+ end
732
+
733
+ @action.("restore files (= clear changes)", important: true)
734
+ def restore(*path)
735
+ if path.empty?
736
+ git "reset", "--hard"
737
+ #git "checkout", "--", "." # path required
738
+ else
739
+ #git "reset", "--hard", "--", *path #=> fatal: Cannot do hard reset with paths.
740
+ git "checkout", "--", *path
741
+ end
742
+ end
743
+
744
+ @action.("print commit-id, author, and timestap of each line")
745
+ @option.(:range, "-L <N1,N2|:func>", "range (start,end) or function name")
746
+ def blame(path, *path2, range: nil)
747
+ paths = [path] + path2
748
+ opts = []
749
+ opts << "-L" << range if range
750
+ git "blame", *opts, *paths
751
+ end
752
+
753
+ @action.("find by pattern")
754
+ def egrep(pattern, commit=nil)
755
+ args = []
756
+ args << commit if commit
757
+ git "grep", "-E", pattern, *args
758
+ end
759
+
760
+ end
761
+
762
+ define_alias("files" , "file:list")
763
+ #define_alias("ls" , "file:list")
764
+ define_alias("track" , "file:track")
765
+ define_alias("register" , "file:track")
766
+ define_alias("changes" , "file:changes")
767
+ #define_alias("move" , "file:move")
768
+ #define_alias("rename" , "file:rename")
769
+ #define_alias("delete" , "file:delete")
770
+ #define_alias("restore" , "file:restore")
771
+
772
+
773
+ ##
774
+ ## staging:
775
+ ##
776
+ category "staging:" do
777
+
778
+ @action.("add changes of files into staging area", important: true)
779
+ @option.(:pick, "-p, --pick", "pick up changes interactively")
780
+ #@option.(:update, "-u, --update", "add all changes of tracked files")
781
+ def add(path, *path2, pick: false) # , update: false
782
+ paths = [path] + path2
783
+ paths.each do |x|
784
+ next if File.directory?(x)
785
+ output = `git ls-files #{x}`
786
+ ! output.strip.empty? or
787
+ raise action_error("#{x}: Not tracked yet (run 'track' action instead).")
788
+ end
789
+ #
790
+ opts = []
791
+ opts << "-p" if pick
792
+ opts << "-u" unless pick
793
+ git "add", *opts, *paths
794
+ end
795
+
796
+ @action.("show changes in staging area", important: true)
797
+ def show(*path)
798
+ git "diff", "--cached", *path
799
+ end
800
+
801
+ @action.("edit changes in staging area")
802
+ def edit(*path)
803
+ git "add", "--edit", *path
804
+ end
805
+
806
+ @action.("delete all changes in staging area", important: true)
807
+ def clear(*path)
808
+ args = path.empty? ? [] : ["--"] + path
809
+ git "reset", "HEAD", *args
810
+ end
811
+
812
+ end
813
+
814
+ define_alias("stage" , "staging:add")
815
+ define_alias("staged" , "staging:show")
816
+ define_alias("unstage" , "staging:clear")
817
+ define_alias("pick" , ["staging:add", "-p"])
818
+
819
+
820
+ ##
821
+ ## commit:
822
+ ##
823
+ category "commit:" do
824
+
825
+ @action.("create a new commit", important: true)
826
+ #@option.(:message, "-m, --message=<message>", "commit message")
827
+ @option.(:file, "-f, --file=<file>", "commit message file")
828
+ def create(message=nil, *path, file: nil)
829
+ opts = []
830
+ opts << "-m" << message if message && ! message.empty?
831
+ opts << "--file=#{file}" if file
832
+ args = path.empty? ? [] : ["--", *path]
833
+ git "commit", *opts, *args
834
+ end
835
+
836
+ @action.("correct the last commit", important: true)
837
+ @option.(:reuse, "-M", "reuse commit message (not invoke text editor for it)")
838
+ def correct(reuse: false)
839
+ opts = reuse ? ["--no-edit"] : []
840
+ git "commit", "--amend", *opts
841
+ end
842
+
843
+ @action.("correct the previous commit")
844
+ @option.(:histedit, "-e, --histedit", "start 'history:edit' action after fixup commit created")
845
+ def fixup(commit, histedit: nil)
846
+ git "commit", "--fixup=#{commit}"
847
+ if histedit
848
+ run_once "history:edit:start", "#{commit}^"
849
+ end
850
+ end
851
+
852
+ @action.("apply a commit to curr branch (known as 'cherry-pick')")
853
+ def apply(commit, *commit2)
854
+ commits = [commit] + commit2
855
+ git "cherry-pick", *commits
856
+ end
857
+
858
+ @action.("show commits in current branch", important: true)
859
+ @option.(:count, "-n <N>", "show latest N commits", type: Integer)
860
+ @option.(:file, "-f, --file=<path>", "show commits related to file")
861
+ def show(commit=nil, count: nil, file: nil)
862
+ if count && commit
863
+ git "show", "#{commit}~#{count}..#{commit}"
864
+ elsif count
865
+ git "show", "HEAD~#{count}..HEAD"
866
+ elsif commit
867
+ git "show", commit
868
+ elsif file
869
+ git "log", "-p", "--", file
870
+ else
871
+ git "log", "-p"
872
+ end
873
+ end
874
+
875
+ @action.("create a new commit which reverts the target commit")
876
+ @option.(:count, "-n <N>", "show latest N commits", type: Integer)
877
+ @option.(:mainline, "--mainline=<N>", "parent number (necessary to revert merge commit)")
878
+ @option.(:reuse, "-M", "reuse commit message (not invoke text editor for it)")
879
+ def revert(*commit, count: nil, mainline: nil, reuse: false)
880
+ commits = commit
881
+ opts = []
882
+ opts << "--no-edit" if reuse
883
+ opts << "-m" << mainline.to_s if mainline
884
+ if count
885
+ commits.length <= 1 or
886
+ raise action_error("Multiple commits are not allowed when '-n' option specified.")
887
+ commit = commits.empty? ? "HEAD" : commits[0]
888
+ git "revert", *opts, "#{commit}~#{count}..#{commit}"
889
+ elsif ! commits.empty?
890
+ git "revert", *opts, *commits
891
+ else
892
+ raise action_error("`<commit-id>` or `-n <N>` option required.")
893
+ end
894
+ end
895
+
896
+ @action.("cancel recent commits up to the target commit-id", important: true)
897
+ @option.(:count , "-n <N>" , "cancel recent N commits", type: Integer)
898
+ @option.(:restore, "--restore", "restore files after rollback")
899
+ def rollback(commit=nil, count: nil, restore: false)
900
+ opts = restore ? ["--hard"] : []
901
+ if commit && count
902
+ raise action_failed("Commit-id and `-n` option are exclusive.")
903
+ elsif commit
904
+ git "reset", *opts, commit
905
+ elsif count
906
+ git "reset", *opts, "HEAD~#{count}"
907
+ else
908
+ git "reset", *opts, "HEAD^"
909
+ end
910
+ end
911
+
912
+ end
913
+
914
+ define_alias("commit" , "commit:create")
915
+ define_alias("cc" , "commit:create")
916
+ define_alias("correct" , "commit:correct")
917
+ define_alias("fixup" , "commit:fixup")
918
+ define_alias("commits" , "commit:show")
919
+ #define_alias("rollback", "commit:rollback")
920
+
921
+
922
+ ##
923
+ ## history:
924
+ ##
925
+ category "history:", action: "show" do
926
+
927
+ @action.("show commit history in various format", important: true)
928
+ @option.(:all, "-a, --all" , "show history of all branches")
929
+ @option.(:format, "-F, --format=<format>", "default/oneline/fuller/graph")
930
+ @option.(:author, "-u, --author", "show author name before '@' of email address (only for 'graph' format)")
931
+ def show(*path, all: false, format: "default", author: false)
932
+ opts = []
933
+ HISTORY_SHOW_OPTIONS.key?(format) or
934
+ raise option_error("#{format}: Unknown format.")
935
+ val = HISTORY_SHOW_OPTIONS[format]
936
+ case val
937
+ when nil ;
938
+ when String ; opts << val
939
+ when Array ; opts.concat(val)
940
+ when Proc ; opts.concat([val.call(author: author)].flatten)
941
+ else
942
+ raise TypeError.new("HISTORY_SHOW_OPTIONS[#{format.inspect}]: Unexpected type value: #{val.inspect}")
943
+ end
944
+ opts = ["--all"] + opts if all
945
+ ## use 'git!' to ignore pipe error when pager process quitted
946
+ git! "log", *opts, *path
947
+ end
948
+
949
+ HISTORY_SHOW_OPTIONS = {
950
+ "default" => nil,
951
+ "compact" => "--oneline",
952
+ "oneline" => "--oneline",
953
+ "detailed" => "--format=fuller",
954
+ "fuller" => "--format=fuller",
955
+ "graph" => proc {|author: false, **_kws|
956
+ fmt = GIT_CONFIG.history_graph_format
957
+ fmt = fmt.sub(/ ?<?%a[eEnNlL]>? ?/, ' ') unless author
958
+ opts = ["--format=#{fmt}"] + GIT_CONFIG.history_graph_options
959
+ opts
960
+ },
961
+ }
962
+
963
+ @action.("show commits not uploaded yet")
964
+ def notuploaded()
965
+ git "cherry", "-v"
966
+ end
967
+
968
+ ## history:edit
969
+ category "edit:" do
970
+
971
+ @action.("start `git rebase -i` to edit commit history", important: true)
972
+ @option.(:count , "-n, --num=<N>", "edit last N commits")
973
+ #@option.(:stash, "-s, --stash", "store current changes into stash temporarily")
974
+ def start(commit=nil, count: nil)
975
+ if commit && count
976
+ raise action_error("Commit-id and `-n` option are exclusive.")
977
+ elsif commit
978
+ nil
979
+ arg = "#{commit}^"
980
+ elsif count
981
+ arg = "HEAD~#{count}"
982
+ else
983
+ raise action_error("Commit-id or `-n` option required.")
984
+ end
985
+ git "rebase", "-i", "--autosquash", arg
986
+ end
987
+
988
+ @action.("resume (= conitnue) suspended `git rebase -i`")
989
+ def resume()
990
+ git "rebase", "--continue"
991
+ end
992
+
993
+ @action.("skip current commit and resume")
994
+ def skip()
995
+ git "rebase", "--skip"
996
+ end
997
+
998
+ @action.("cancel (or abort) `git rebase -i`")
999
+ def cancel()
1000
+ git "rebase", "--abort"
1001
+ end
1002
+
1003
+ end
1004
+
1005
+ end
1006
+
1007
+ define_alias "hist" , ["history", "-F", "graph"]
1008
+ #define_alias "history" , "history:show"
1009
+ define_alias "histedit" , "history:edit:start"
1010
+ #define_alias "histedit:resume", "history:edit:resume"
1011
+ #define_alias "histedit:skip" , "history:edit:skip"
1012
+ #define_alias "histedit:cancel", "history:edit:cancel"
1013
+
1014
+
1015
+ ##
1016
+ ## repo:
1017
+ ##
1018
+ category "repo:" do
1019
+
1020
+ def _config_user_and_email(user, email)
1021
+ if user == nil && `git config --get user.name`.strip().empty?
1022
+ user = ask_to_user "User name:"
1023
+ end
1024
+ git "config", "user.name" , user if user
1025
+ if email == nil && `git config --get user.email`.strip().empty?
1026
+ email = ask_to_user "Email address:"
1027
+ end
1028
+ git "config", "user.email", email if email
1029
+ end
1030
+ private :_config_user_and_email
1031
+
1032
+ def _generate_gitignore_file(filename)
1033
+ items = GIT_CONFIG.gitignore_items
1034
+ sep = "> "
1035
+ items.each do |x|
1036
+ echoback "echo %-14s %s %s" % ["'#{x}'", sep, filename]
1037
+ sep = ">>"
1038
+ end
1039
+ content = (items + [""]).join("\n")
1040
+ File.write(filename, content, encoding: 'utf-8')
1041
+ end
1042
+ private :_generate_gitignore_file
1043
+
1044
+ initopts = optionset() {
1045
+ @option.(:initial_branch, "-b, --branch=<branch>", "branch name (default: '#{GIT_CONFIG.initial_branch}')")
1046
+ @option.(:user , "-u, --user=<user>", "user name")
1047
+ @option.(:email, "-e, --email=<email>", "email address")
1048
+ @option.(:initial_commit, "-x", "not create an empty initial commit", value: false)
1049
+ }
1050
+
1051
+ @action.("initialize git repository with empty initial commit", important: true)
1052
+ @optionset.(initopts)
1053
+ def init(user: nil, email: nil, initial_branch: nil, initial_commit: true)
1054
+ ! File.exist?(".git") or
1055
+ raise action_error("Directory '.git' already exists.")
1056
+ branch ||= GIT_CONFIG.initial_branch
1057
+ git "init", "--initial-branch=#{branch}"
1058
+ _config_user_and_email(user, email)
1059
+ if initial_commit
1060
+ git "commit", "--allow-empty", "-m", GIT_CONFIG.initial_commit_message
1061
+ end
1062
+ filename = ".gitignore"
1063
+ _generate_gitignore_file(filename) unless File.exist?(filename)
1064
+ end
1065
+
1066
+ @action.("create a new directory and initialize it as a git repo")
1067
+ @optionset.(initopts)
1068
+ def create(name, user: nil, email: nil, initial_branch: nil, initial_commit: true)
1069
+ dir = name
1070
+ mkdir dir
1071
+ cd dir do
1072
+ run_once "init", user: user, email: email, initial_branch: initial_branch, initial_commit: initial_commit
1073
+ end
1074
+ end
1075
+
1076
+ @action.("copy a repository ('github:<user>/<repo>' is available)")
1077
+ @optionset.(initopts.select(:user, :email))
1078
+ def clone(url, dir=nil, user: nil, email: nil)
1079
+ url = resolve_repository_url(url)
1080
+ args = dir ? [dir] : []
1081
+ files = Dir.glob("*")
1082
+ git "clone", url, *args
1083
+ newdir = (Dir.glob("*") - files)[0] || dir
1084
+ cd newdir do
1085
+ _config_user_and_email(user, email)
1086
+ end if newdir
1087
+ end
1088
+
1089
+ ## repo:remote:
1090
+ category "remote:", action: "handle" do
1091
+
1092
+ @action.("list/get/set/delete remote repository", usage: [
1093
+ " # list",
1094
+ "<name> # get",
1095
+ "<name> <url> # set ('github:user/repo' is avaialble)",
1096
+ "<name> \"\" # delete",
1097
+ ], postamble: {
1098
+ "Example:" => <<~'END'.gsub(/^/, " "),
1099
+ $ gi repo:remote # list
1100
+ $ gi repo:remote origin # get
1101
+ $ gi repo:remote origin github:user1/repo1 # set
1102
+ $ gi repo:remote origin "" # delete
1103
+ END
1104
+ })
1105
+ def handle(name=nil, url=nil)
1106
+ url = resolve_repository_url(url) if url
1107
+ if name == nil
1108
+ git "remote", "-v"
1109
+ elsif url == nil
1110
+ git "remote", "get-url", name
1111
+ elsif url == ""
1112
+ git "remote", "remove", name
1113
+ elsif `git remote`.split().include?(name)
1114
+ git "remote", "set-url", name, url
1115
+ else
1116
+ git "remote", "add", name, url
1117
+ end
1118
+ end
1119
+
1120
+ @action.("get/set/delete origin (= default remote repository)", usage: [
1121
+ " # get",
1122
+ "<url> # set ('github:user/repo' is avaialble)",
1123
+ "\"\" # delete",
1124
+ ], postamble: {
1125
+ "Example:" => <<~'END'.gsub(/^/, " "),
1126
+ $ gi repo:remote:origin # get
1127
+ $ gi repo:remote:origin github:user1/repo1 # set
1128
+ $ gi repo:remote:origin "" # delete
1129
+ END
1130
+ })
1131
+ def origin(url=nil)
1132
+ run_action "repo:remote", "origin", url
1133
+ end
1134
+
1135
+ end
1136
+
1137
+ end
1138
+
1139
+
1140
+ ##
1141
+ ## tag:
1142
+ ##
1143
+ category "tag:", action: "handle" do
1144
+
1145
+ @action.("list/show/create/delete tags", important: true, usage: [
1146
+ " # list",
1147
+ "<tag> # show commit-id of the tag",
1148
+ "<tag> <commit> # create a tag on the commit",
1149
+ "<tag> HEAD # create a tag on current commit",
1150
+ "<tag> \"\" # delete a tag",
1151
+ ])
1152
+ @option.(:remote, "-r, --remote[=origin]", "list/delete tags on remote (not for show/create)")
1153
+ def handle(tag=nil, commit=nil, remote: nil)
1154
+ if tag == nil # list
1155
+ if remote
1156
+ #git "show-ref", "--tags"
1157
+ git "ls-remote", "--tags"
1158
+ else
1159
+ git "tag", "-l"
1160
+ end
1161
+ elsif commit == nil # show
1162
+ ! remote or
1163
+ raise option_error("Option '-r' or '--remote' is not available for showing tag.")
1164
+ git "rev-parse", tag
1165
+ elsif commit == "" # delete
1166
+ if remote
1167
+ remote = "origin" if remote == true
1168
+ #git "push", "--delete", remote, tag # may delete same name branch
1169
+ git "push", remote, ":refs/tags/#{tag}"
1170
+ else
1171
+ git "tag", "--delete", tag
1172
+ end
1173
+ else # create
1174
+ ! remote or
1175
+ raise option_error("Option '-r' or '--remote' is not available for creating tag.")
1176
+ git "tag", tag, commit
1177
+ end
1178
+ end
1179
+
1180
+ @action.("list tags")
1181
+ @option.(:remote, "-r, --remote", "list remote tags")
1182
+ def list(remote: false)
1183
+ if remote
1184
+ #git "show-ref", "--tags"
1185
+ git "ls-remote", "--tags"
1186
+ else
1187
+ git "tag", "-l"
1188
+ end
1189
+ end
1190
+
1191
+ @action.("create a new tag", important: true)
1192
+ #@option.(:on, "--on=<commit>", "commit-id where new tag created on")
1193
+ def create(tag, commit=nil)
1194
+ args = commit ? [commit] : []
1195
+ git "tag", tag, *args
1196
+ end
1197
+
1198
+ @action.("delete a tag")
1199
+ @option.(:remote, "-r, --remote[=origin]", "delete from remote repository")
1200
+ def delete(tag, *tag_, remote: nil)
1201
+ tags = [tag] + tag_
1202
+ if remote
1203
+ remote = "origin" if remote == true
1204
+ tags.each do |tag|
1205
+ #git "push", "--delete", remote, tag # may delete same name branch
1206
+ git "push", remote, ":refs/tags/#{tag}" # delete a tag safely
1207
+ end
1208
+ else
1209
+ git "tag", "-d", *tags
1210
+ end
1211
+ end
1212
+
1213
+ @action.("upload tags")
1214
+ def upload()
1215
+ git "push", "--tags"
1216
+ end
1217
+
1218
+ @action.("download tags")
1219
+ def download()
1220
+ git "fetch", "--tags", "--prune-tags"
1221
+ end
1222
+
1223
+ end
1224
+
1225
+ define_alias("tags", "tag:list")
1226
+
1227
+
1228
+ ##
1229
+ ## sync:
1230
+ ##
1231
+ category "sync:" do
1232
+
1233
+ uploadopts = optionset {
1234
+ @option.(:upstream, "-u <remote>" , "set upstream")
1235
+ @option.(:origin , "-U" , "same as '-u origin'")
1236
+ @option.(:force , "-f, --force" , "upload forcedly")
1237
+ }
1238
+
1239
+ @action.("download and upload commits")
1240
+ @optionset.(uploadopts)
1241
+ def both(upstream: nil, origin: false, force: false)
1242
+ run_action "pull"
1243
+ run_action "push", upstream: upstream, origin: origin, force: force
1244
+ end
1245
+
1246
+ @action.("upload commits to remote")
1247
+ @optionset.(uploadopts)
1248
+ def push(upstream: nil, origin: false, force: false)
1249
+ branch = curr_branch()
1250
+ upstream ||= "origin" if origin
1251
+ upstream ||= _ask_remote_repo(branch)
1252
+ #
1253
+ opts = []
1254
+ opts << "-f" if force
1255
+ if upstream
1256
+ git "push", *opts, "-u", upstream, branch # branch name is required
1257
+ else
1258
+ git "push", *opts
1259
+ end
1260
+ end
1261
+
1262
+ def _ask_remote_repo(branch)
1263
+ output = `git config --get-regexp '^branch\.'`
1264
+ has_upstream = output.each_line.any? {|line|
1265
+ line =~ /\Abranch\.(.*)\.remote / && $1 == branch
1266
+ }
1267
+ return nil if has_upstream
1268
+ remote = ask_to_user "Enter the remote repo name (default: \e[1morigin\e[0m) :"
1269
+ return remote && ! remote.empty? ? remote : "origin"
1270
+ end
1271
+ private :_ask_remote_repo
1272
+
1273
+ @action.("download commits from remote and apply them to local")
1274
+ @option.(:apply, "-N, --not-apply", "just download, not apply", value: false)
1275
+ def pull(apply: true)
1276
+ if apply
1277
+ git "pull", "--prune"
1278
+ else
1279
+ git "fetch", "--prune"
1280
+ end
1281
+ end
1282
+
1283
+ end
1284
+
1285
+ define_alias("sync" , "sync:both")
1286
+ define_alias("push" , "sync:push")
1287
+ define_alias("upload" , "sync:push")
1288
+ define_alias("up" , "sync:push")
1289
+ define_alias("pull" , "sync:pull")
1290
+ define_alias("download" , "sync:pull")
1291
+ define_alias("dl" , "sync:pull")
1292
+
1293
+
1294
+ ##
1295
+ ## stash:
1296
+ ##
1297
+ category "stash:" do
1298
+
1299
+ @action.("list stash history")
1300
+ def list()
1301
+ git "stash", "list"
1302
+ end
1303
+
1304
+ @action.("show changes on stash")
1305
+ @option.(:num, "-n <N>", "show N-th changes on stash (1-origin)", type: Integer)
1306
+ #@option.(:index, "-x, --index=<N>", "show N-th changes on stash (0-origin)", type: Integer)
1307
+ def show(num: nil)
1308
+ args = num ? ["stash@{#{num - 1}}"] : []
1309
+ git "stash", "show", "-p", *args
1310
+ end
1311
+
1312
+ @action.("save current changes into stash", important: true)
1313
+ @option.(:message, "-m <message>", "message")
1314
+ @option.(:pick, "-p, --pick" , "pick up changes interactively")
1315
+ def put(*path, message: nil, pick: false)
1316
+ opts = []
1317
+ opts << "-m" << message if message
1318
+ opts << "-p" if pick
1319
+ args = path.empty? ? [] : ["--"] + path
1320
+ git "stash", "push", *opts, *args
1321
+ end
1322
+
1323
+ @action.("restore latest changes from stash", important: true)
1324
+ @option.(:num, "-n <N>", "pop N-th changes on stash (1-origin)", type: Integer)
1325
+ def pop(num: nil)
1326
+ args = num ? ["stash@{#{num - 1}}"] : []
1327
+ git "stash", "pop", *args
1328
+ end
1329
+
1330
+ @action.("delete latest changes from stash")
1331
+ @option.(:num, "-n, --num=<N>", "drop N-th changes on stash (1-origin)", type: Integer)
1332
+ def drop(num: nil)
1333
+ args = num ? ["stash@{#{num - 1}}"] : []
1334
+ git "stash", "drop", *args
1335
+ end
1336
+
1337
+ end
1338
+
1339
+
1340
+ ##
1341
+ ## config:
1342
+ ##
1343
+ category "config:", action: "handle" do
1344
+
1345
+ optset = optionset() {
1346
+ @option.(:global, "-g, --global", "handle global config")
1347
+ @option.(:local , "-l, --local" , "handle repository local config")
1348
+ }
1349
+
1350
+ def _build_config_options(global, local)
1351
+ opts = []
1352
+ opts << "--global" if global
1353
+ opts << "--local" if local
1354
+ return opts
1355
+ end
1356
+
1357
+ @action.("list/get/set/delete config values", usage: [
1358
+ " # list",
1359
+ "<key> # get",
1360
+ "<key> <value> # set",
1361
+ "<key> \"\" # delete",
1362
+ "<prefix> # filter by prefix",
1363
+ ], postamble: {
1364
+ "Example:" => (<<~END).gsub(/^/, " "),
1365
+ $ gi config # list
1366
+ $ gi config core.editor # get
1367
+ $ gi config core.editor vim # set
1368
+ $ gi config core.editor "" # delete
1369
+ $ gi config core. # filter by prefix
1370
+ $ gi config . # list top level prefixes
1371
+ END
1372
+ })
1373
+ @optionset.(optset)
1374
+ def handle(key=nil, value=nil, global: false, local: false)
1375
+ opts = _build_config_options(global, local)
1376
+ if key == nil # list
1377
+ git "config", *opts, "--list"
1378
+ elsif value == nil # get or filter
1379
+ case key
1380
+ when "." # list top level prefixes
1381
+ echoback "gi config | awk -F. 'NR>1{d[$1]++}END{for(k in d){print(k\"\\t(\"d[k]\")\")}}' | sort"
1382
+ d = {}
1383
+ `git config -l #{opts.join(' ')}`.each_line do |line|
1384
+ d[$1] = (d[$1] || 0) + 1 if line =~ /^(\w+\.)/
1385
+ end
1386
+ d.keys.sort.each {|k| puts "#{k}\t(#{d[k]})" }
1387
+ when /\.$/ # list (filter)
1388
+ pat = "^"+key.gsub('.', '\\.')
1389
+ #git "config", *opts, "--get-regexp", pat # different with `config -l`
1390
+ echoback "git config -l #{opts.join(' ')} | grep '#{pat}'"
1391
+ `git config -l #{opts.join(' ')}`.each_line do |line|
1392
+ print line if line.start_with?(key)
1393
+ end
1394
+ else # get
1395
+ #git "config", "--get", *opts, key
1396
+ git "config", *opts, key
1397
+ end
1398
+ elsif value == "" # delete
1399
+ git "config", *opts, "--unset", key
1400
+ else # set
1401
+ git "config", *opts, key, value
1402
+ end
1403
+ end
1404
+
1405
+ @action.("set user name and email", usage: [
1406
+ "<user> <u@email> # set user name and email",
1407
+ "<user@email> # set email (contains '@')",
1408
+ "<user> # set user (not contain '@')",
1409
+ ])
1410
+ @optionset.(optset)
1411
+ def setuser(user, email=nil, global: false, local: false)
1412
+ opts = _build_config_options(global, local)
1413
+ if email == nil && user =~ /@/
1414
+ email = user
1415
+ user = nil
1416
+ end
1417
+ user = nil if user == '-'
1418
+ email = nil if email == '-'
1419
+ git "config", *opts, "user.name" , user if user
1420
+ git "config", *opts, "user.email", email if email
1421
+ end
1422
+
1423
+ @action.("list/get/set/delete aliases of 'git' (not of 'gi')", usage: [
1424
+ " # list",
1425
+ "<name> # get",
1426
+ "<name> <value> # set",
1427
+ "<name> \"\" # delete",
1428
+ ])
1429
+ def alias(name=nil, value=nil)
1430
+ if value == "" # delete
1431
+ git "config", "--global", "--unset", "alias.#{name}"
1432
+ elsif value != nil # set
1433
+ git "config", "--global", "alias.#{name}", value
1434
+ elsif name != nil # get
1435
+ git "config", "--global", "alias.#{name}"
1436
+ else # list
1437
+ command = "git config --get-regexp '^alias\\.' | sed -e 's/^alias\\.//;s/ /\\t= /'"
1438
+ echoback(command)
1439
+ output = `git config --get-regexp '^alias.'`
1440
+ print output.gsub(/^alias\.(\S+) (.*)/) { "%s\t= %s" % [$1, $2] }
1441
+ end
1442
+ end
1443
+
1444
+
1445
+ end
1446
+
1447
+
1448
+ ##
1449
+ ## misc:
1450
+ ##
1451
+ category "misc:" do
1452
+
1453
+ @action.("generate a init file, or print to stdout if no args",
1454
+ usage: [
1455
+ "<filename> # generate a file",
1456
+ " # print to stdout",
1457
+ ])
1458
+ def initfile(filename=nil)
1459
+ str = File.read(__FILE__, encoding: "utf-8")
1460
+ code = str.split(/^__END__\n/, 2)[1]
1461
+ code = code.gsub(/%SCRIPT%/, APP_CONFIG.app_command)
1462
+ code = code.gsub(/%ENVVAR_INITFILE%/, ENVVAR_INITFILE)
1463
+ #
1464
+ if ! filename || filename == "-"
1465
+ print code
1466
+ elsif File.exist?(filename)
1467
+ raise action_error("#{filename}: File already exists (remove it before generating new file).")
1468
+ else
1469
+ File.write(filename, code, encoding: 'utf-8')
1470
+ puts "[OK] #{filename} generated." unless $QUIET_MODE
1471
+ end
1472
+ end
1473
+
1474
+ end
1475
+
1476
+
1477
+ end
1478
+
1479
+
1480
+ Benry::CmdApp.module_eval do
1481
+ define_abbrev("b:" , "branch:")
1482
+ define_abbrev("c:" , "commit:")
1483
+ define_abbrev("C:" , "config:")
1484
+ define_abbrev("g:" , "staging:")
1485
+ define_abbrev("f:" , "file:")
1486
+ define_abbrev("r:" , "repo:")
1487
+ define_abbrev("r:r:", "repo:remote:")
1488
+ define_abbrev("h:" , "history:")
1489
+ define_abbrev("h:e:", "history:edit:")
1490
+ define_abbrev("histedit:", "history:edit:")
1491
+ #define_abbrev("t:" , "tag:")
1492
+ #define_abbrev("s:" , "status:")
1493
+ #define_abbrev("y:" , "sync:")
1494
+ #define_abbrev("T:" , "stash:")
1495
+ end
1496
+
1497
+
1498
+ class AppHelpBuilder < Benry::CmdApp::ApplicationHelpBuilder
1499
+
1500
+ def build_help_message(*args, **kwargs)
1501
+ @_omit_actions_part = true
1502
+ return super
1503
+ ensure
1504
+ @_omit_actions_part = false
1505
+ end
1506
+
1507
+ def section_actions(*args, **kwargs)
1508
+ if @_omit_actions_part
1509
+ text =" (Too long to show. Run `#{@config.app_command} -l` to list all actions.)"
1510
+ return render_section(header(:HEADER_ACTIONS), text)
1511
+ else
1512
+ return super
1513
+ end
1514
+ end
1515
+
1516
+ end
1517
+
1518
+
1519
+ def self.main(argv=ARGV)
1520
+ errmsg = _load_init_file(ENV[ENVVAR_INITFILE])
1521
+ if errmsg
1522
+ $stderr.puts "\e[31m[ERROR]\e[0m #{errmsg}"
1523
+ return 1
1524
+ end
1525
+ #
1526
+ APP_CONFIG.default_action = GIT_CONFIG.default_action
1527
+ app_help_builder = AppHelpBuilder.new(APP_CONFIG)
1528
+ app = Benry::CmdApp::Application.new(APP_CONFIG, nil, app_help_builder)
1529
+ return app.main(argv)
1530
+ end
1531
+
1532
+ def self._load_init_file(filename)
1533
+ return nil if filename == nil || filename.empty?
1534
+ filename = File.expand_path(filename)
1535
+ File.exist?(filename) or
1536
+ return "#{filename}: Init file specified but not exist."
1537
+ require File.absolute_path(filename)
1538
+ return nil
1539
+ end
1540
+ private_class_method :_load_init_file
1541
+
1542
+
1543
+ end
1544
+
1545
+
1546
+ if __FILE__ == $0
1547
+ exit GitImproved.main()
1548
+ end
1549
+
1550
+
1551
+ __END__
1552
+ # coding: utf-8
1553
+ # frozen_string_literal: true
1554
+
1555
+ ##
1556
+ ## @(#) Init file for '%SCRIPT%' command.
1557
+ ##
1558
+ ## This file is loaded by '%SCRIPT%' command only if $%ENVVAR_INITFILE% is set,
1559
+ ## for example:
1560
+ ##
1561
+ ## $ gi hello
1562
+ ## [ERROR] hello: Action not found.
1563
+ ##
1564
+ ## $ export %ENVVAR_INITFILE%="~/.gi_init.rb"
1565
+ ## $ gi hello
1566
+ ## Hello, world!
1567
+ ##
1568
+
1569
+ GitImproved.module_eval do
1570
+
1571
+
1572
+ ##
1573
+ ## Configuration example
1574
+ ##
1575
+ GIT_CONFIG.tap do |c|
1576
+ #c.prompt = "[gi]$ "
1577
+ #c.default_action = "status:here" # or: "status:info"
1578
+ #c.initial_branch = "main" # != 'master'
1579
+ #c.initial_commit_message = "Initial commit (empty)"
1580
+ #c.gitignore_items = ["*~", "*.DS_Store", "tmp/*", "*.pyc"]
1581
+ #c.history_graph_format = "%C(auto)%h %ad <%al> | %d %s"
1582
+ ##c.history_graph_format = "\e[32m%h %ad\e[0m <%al> \e[2m|\e[0m\e[33m%d\e[0m %s"
1583
+ #c.history_graph_options = ["--graph", "--date=short", "--decorate"]
1584
+ end
1585
+
1586
+
1587
+ ##
1588
+ ## Custom alias example
1589
+ ##
1590
+ GitAction.class_eval do
1591
+
1592
+ ## `gi br <branch>` == `gi breanch:create -w <branch>`
1593
+ define_alias "br", ["branch:create", "-w"]
1594
+
1595
+ end
1596
+
1597
+
1598
+ ##
1599
+ ## Custom action example
1600
+ ##
1601
+ GitAction.class_eval do
1602
+
1603
+ #category "example:" do
1604
+
1605
+ langs = ["en", "fr", "it"]
1606
+
1607
+ @action.("print greeting message")
1608
+ @option.(:lang, "-l, --lang=<lang>", "language (en/fr/it)", enum: langs)
1609
+ def hello(name="world", lang: "en")
1610
+ case lang
1611
+ when "en" ; puts "Hello, #{name}!"
1612
+ when "fr" ; puts "Bonjour, #{name}!"
1613
+ when "it" ; puts "Chao, #{name}!"
1614
+ else
1615
+ raise option_error("#{lang}: Unknown language.")
1616
+ end
1617
+ end
1618
+
1619
+ #end
1620
+
1621
+ #define_alias "hello", "example:hello"
1622
+
1623
+ end
1624
+
1625
+
1626
+ end