lux-hammer 0.3.1 → 0.3.3

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,624 @@
1
+ # desc: work with git (commit, push, pull, rebase, branch, redate, ...)
2
+
3
+ desc <<~TXT
4
+ Git helper. Short aliases over common `git` workflows.
5
+
6
+ Most subcommands operate on the current branch detected at startup.
7
+ Run from inside a git working tree.
8
+ TXT
9
+
10
+ require 'date'
11
+
12
+ unless Dir.exist?('.git') || %w[-h --help help].include?(ARGV.first)
13
+ warn "\e[31mNo .git directory\e[0m"
14
+ exit 1
15
+ end
16
+
17
+ Signal.trap('INT') do
18
+ puts ''
19
+ exit
20
+ end
21
+
22
+ if Dir.exist?('.git')
23
+ BRANCH ||= `git rev-parse --abbrev-ref HEAD 2>/dev/null`.chomp
24
+ detected = `git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null`.chomp.split('/').last
25
+ PARENT ||= detected.to_s.empty? ? 'master' : detected
26
+ else
27
+ BRANCH ||= ''
28
+ PARENT ||= 'master'
29
+ end
30
+
31
+ helpers do
32
+ private
33
+
34
+ def remote_host
35
+ `git remote -v | grep origin`.split(/[:\s]/)[1].to_s
36
+ end
37
+
38
+ def origin_path
39
+ `git remote -v | grep origin`.split(/[:\s]/)[2].to_s.sub('.git', '')
40
+ end
41
+
42
+ def remote_url
43
+ remote_host.include?('gitlab') ? "https://gitlab.com/#{origin_path}" : "https://github.com/#{origin_path}"
44
+ end
45
+
46
+ def remote_page_url
47
+ remote_host.include?('gitlab') ? "#{remote_url}/-/tree/#{BRANCH}" : "#{remote_url}/tree/#{BRANCH}"
48
+ end
49
+
50
+ def remote_pr_url
51
+ if remote_host.include?('gitlab')
52
+ "#{remote_url}/-/merge_requests/new?merge_request[source_branch]=#{BRANCH}"
53
+ else
54
+ "#{remote_url}/pull/new/#{BRANCH}"
55
+ end
56
+ end
57
+
58
+ def remote_compare_url
59
+ remote_host.include?('gitlab') ? "#{remote_url}/-/compare/#{PARENT}...#{BRANCH}" : "#{remote_url}/compare/#{BRANCH}"
60
+ end
61
+
62
+ def changed_files
63
+ `git diff --name-only #{PARENT}..#{BRANCH}`.split($/).select { |f| File.exist?(f) }
64
+ end
65
+
66
+ def local_branches
67
+ `git branch`.split($/).map { |b| b.sub(/^[\s*]+/, '') }.reject(&:empty?)
68
+ end
69
+
70
+ def open_in_browser(url)
71
+ run "open '#{url}'"
72
+ end
73
+
74
+ def bump_version
75
+ return unless BRANCH == 'master' && File.exist?('./.version')
76
+ old = File.read('./.version').gsub(/\s/, '')
77
+ parts = old.split('.')
78
+ parts.push(parts.pop.to_i + 1)
79
+ new = parts.join('.')
80
+ say "Version: #{old} -> #{new.color(:yellow)}"
81
+ File.write('./.version', new)
82
+ run 'git add .version'
83
+ new
84
+ end
85
+
86
+ def run(command, returnable = false)
87
+ say '' if @ran_once
88
+ @ran_once = true
89
+ say command, :gray
90
+ if returnable
91
+ `#{command}`.chomp
92
+ else
93
+ system "#{command} 2>&1"
94
+ end
95
+ end
96
+
97
+ def pick(question, items)
98
+ items = items.chomp.split($/) if items.is_a?(String)
99
+ items = items.map { |i| i.to_s.sub(/^[\s*]+/, '') }.reject(&:empty?).uniq.sort
100
+ return if items.empty?
101
+ idx = choose(question, items)
102
+ idx ? items[idx] : nil
103
+ end
104
+
105
+ def do_commit
106
+ run 'git add .'
107
+
108
+ status_text = `git status`.chomp
109
+ if status_text.include?('nothing to commit')
110
+ say status_text, :yellow
111
+ exit
112
+ end
113
+
114
+ conflicted = `git grep '<<<<<<<'`.chomp.split($/).reject { |f| f.include?('Binary') }
115
+ unless conflicted.empty?
116
+ say 'Resolve merge first in:'
117
+ puts conflicted
118
+ exit
119
+ end
120
+
121
+ rubocop_modified if File.exist?('.rubocop.yml')
122
+
123
+ say 'Modified files:'
124
+ say `git status`.split("\n").drop(4).map { |el| el.sub(/^\t/, '') }.join("\n"), :yellow
125
+ say '---'
126
+ say 'Last 3 commits'
127
+ puts `git log -3 --reverse --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit`
128
+ orig_files = `find ./app -name '*.orig' -o -name '*_LOCAL_*' -o -name '*_BACKUP_*' -o -name '*_BASE_*' -o -name '*_REMOTE_*' 2>/dev/null | grep -v '/tmp/'`
129
+ unless orig_files.strip.empty?
130
+ say '---'
131
+ say 'orig tmp git merge files'
132
+ say orig_files, :red
133
+ end
134
+ say '---'
135
+
136
+ loop do
137
+ print "Message [#{BRANCH.color(:blue)}]: "
138
+ message = $stdin.gets.to_s.chomp
139
+
140
+ if message.empty?
141
+ run 'git reset --mixed'
142
+ exit
143
+ elsif message.length < 5
144
+ say 'Please add better commit message, min length 5 chars', :red
145
+ next
146
+ else
147
+ bump_version
148
+ system('git', 'commit', '-m', message)
149
+ break
150
+ end
151
+ end
152
+ end
153
+
154
+ def rubocop_modified
155
+ files = `git status -s`.chomp.split(/\s+/).select { |it| it.include?('.') }
156
+ files = files.select { |it| it.end_with?('.rb') && File.exist?(it) }
157
+ files -= ['db/schema.rb']
158
+ return if files.empty?
159
+ say 'Rubocop check on:'
160
+ puts files
161
+ system "rubocop #{files.join(' ')}"
162
+ exit unless yes?('Continue?')
163
+ end
164
+
165
+ def do_redate(redate_to, commit = nil)
166
+ head_date = DateTime.parse(`git log -1 --date=format:"%Y-%m-%dT%T" --format="%ad"`.chomp)
167
+
168
+ if redate_to
169
+ if commit
170
+ head_date = DateTime.parse(`git log -1 #{commit} --date=format:"%Y-%m-%dT%T" --format="%ad"`.chomp)
171
+ date = DateTime.parse(redate_to)
172
+ elsif redate_to.include?(':')
173
+ date = DateTime.parse(redate_to)
174
+ elsif redate_to.start_with?('+', '-')
175
+ head_date = DateTime.parse(`git log -2 --date=format:"%Y-%m-%dT%T" --format="%ad"`.chomp.split($/).last)
176
+ direction = redate_to[0]
177
+ hours = redate_to[1..].to_i + rand
178
+ hours = -hours if direction == '-'
179
+ date = head_date + (hours / 24.0)
180
+ else
181
+ error 'Wrong date format'
182
+ end
183
+
184
+ commit ||= `git rev-parse HEAD`.chomp
185
+ run %[GIT_COMMITTER_DATE="#{date}" git commit --amend --date="#{date}" -C #{commit}]
186
+ say "Redated from: #{head_date}"
187
+ say "Redated to : #{date}"
188
+ return
189
+ end
190
+
191
+ say 'Last git date:'
192
+ say " #{head_date.strftime('%A').ljust(10)} - #{head_date}"
193
+ if head_date.wday.positive?
194
+ head_date -= head_date.wday + 1
195
+ say " #{head_date.strftime('%A').ljust(10)} - #{head_date}"
196
+ end
197
+ say '---'
198
+ say "redate #{head_date} <commit> # reset specific commit"
199
+ say "redate #{head_date} # set latest commit to date"
200
+ say 'redate +2 # shift last commit ~2 hours later'
201
+ end
202
+ end
203
+
204
+ # ---- branch / status --------------------------------------------------
205
+
206
+ task :status do
207
+ desc 'git status'
208
+ alt :s
209
+ proc { run 'git status -u' }
210
+ end
211
+
212
+ task :branch do
213
+ desc 'Show current branch'
214
+ alt :b
215
+ proc { print BRANCH }
216
+ end
217
+
218
+ task :parent do
219
+ desc "Show parent branch (#{PARENT})"
220
+ proc { print PARENT }
221
+ end
222
+
223
+ task :head do
224
+ desc 'last commit hash + subject'
225
+ proc { run 'git log -1 --pretty=format:"%H %s"' }
226
+ end
227
+
228
+ # ---- sync / push / pull -----------------------------------------------
229
+
230
+ task :sync do
231
+ desc 'rebase + push + status'
232
+ proc do |_|
233
+ hammer :rebase
234
+ hammer :push
235
+ hammer :status
236
+ end
237
+ end
238
+
239
+ task :pp do
240
+ desc 'Pull & Push'
241
+ proc do |_|
242
+ hammer :pull
243
+ hammer :push
244
+ end
245
+ end
246
+
247
+ task :push do
248
+ desc 'git push origin [current branch]'
249
+ opt :force, type: :boolean, alias: :f, desc: 'use --force-with-lease'
250
+ example 'push'
251
+ example 'push --force'
252
+ example 'push -f'
253
+ proc do |opts|
254
+ flag = opts[:force] ? '--force-with-lease' : ''
255
+ run "git branch -u origin/#{BRANCH}"
256
+ run "git push origin #{BRANCH} #{flag}".rstrip
257
+ end
258
+ end
259
+
260
+ task :pull do
261
+ desc 'git pull origin [current branch] --rebase'
262
+ proc do |_|
263
+ active = `git status --porcelain` =~ /\w/
264
+ run 'git stash push --include-untracked -m "Auto stash before pull" > /dev/null' if active
265
+ run "git pull origin #{BRANCH} --rebase"
266
+ run 'git stash pop > /dev/null' if active
267
+ end
268
+ end
269
+
270
+ task :rebase do
271
+ desc 'fetch + rebase on origin/[current branch]'
272
+ proc do |opts|
273
+ branch = opts[:args].first || BRANCH
274
+ run 'git fetch --all'
275
+ run "git rebase origin/#{branch}"
276
+ end
277
+ end
278
+
279
+ # ---- commit / amend / fixup -------------------------------------------
280
+
281
+ task :commit do
282
+ desc 'add, message, rubocop and other checks'
283
+ alt :c
284
+ proc { do_commit }
285
+ end
286
+
287
+ task :amend do
288
+ desc 'append staged changes to last commit'
289
+ alt :ammend
290
+ proc { run 'git -c core.hooksPath=/dev/null commit --amend --no-edit' }
291
+ end
292
+
293
+ task :fixup do
294
+ desc 'append code to fixup of last commit'
295
+ proc do |_|
296
+ hash, message = `git log -1 --format="%H %s"`.chomp.split(' ', 2)
297
+ next unless yes?("Fixup on: #{message}")
298
+ run 'git add .'
299
+ run "git commit --fixup #{hash}"
300
+ end
301
+ end
302
+
303
+ # ---- diff / file picking ----------------------------------------------
304
+
305
+ task :diff do
306
+ desc 'Show diff for one file ("diff all" for all)'
307
+ proc do |opts|
308
+ arg = opts[:args].first
309
+ if arg
310
+ arg = '' if arg == 'all'
311
+ run "git diff #{PARENT}..#{BRANCH} #{arg}".strip
312
+ else
313
+ file = pick('Select file to show ("diff all" for all)', changed_files)
314
+ run "git diff #{PARENT}..#{BRANCH} #{file}" if file
315
+ end
316
+ end
317
+ end
318
+
319
+ task :fhistory do
320
+ desc 'show file history, via gitk'
321
+ proc do |opts|
322
+ file = opts[:args].first || pick('Select file to show', changed_files)
323
+ next unless file
324
+ run "gitk #{file}"
325
+ end
326
+ end
327
+
328
+ task :restore do
329
+ desc "restore single file to #{PARENT}"
330
+ proc do |opts|
331
+ file = opts[:args].first || pick('Select file to restore', changed_files)
332
+ next unless file
333
+ run "git checkout origin/#{PARENT} #{file}"
334
+ end
335
+ end
336
+
337
+ # ---- stash / branch management ----------------------------------------
338
+
339
+ task :stash do
340
+ desc 'stash tracked and untracked'
341
+ proc { run 'git stash push --include-untracked -m "g stash" > /dev/null' }
342
+ end
343
+
344
+ task :merge do
345
+ desc 'squash-merge current branch into the given branch'
346
+ example 'merge main'
347
+ proc do |opts|
348
+ branch = opts[:args].first or error 'specify branch'
349
+ error 'You must be in a feature branch to squash-merge' if %w[develop main master].include?(BRANCH)
350
+ run "git checkout #{branch}"
351
+ run "git merge --squash #{BRANCH}"
352
+ say.yellow "next (now on #{branch}):"
353
+ say.yellow ' git commit -m "<your_commit_message>"'
354
+ say.yellow " g push --force # pushes #{branch}"
355
+ end
356
+ end
357
+
358
+ task :new do
359
+ desc "create new branch from #{PARENT}"
360
+ example 'new feature-x'
361
+ proc do |opts|
362
+ name = opts[:args].first or error 'specify branch name'
363
+ run "git checkout #{PARENT}"
364
+ run 'git pull'
365
+ run "git checkout -b #{name}"
366
+ end
367
+ end
368
+
369
+ task :ch do
370
+ desc 'change branch (interactive picker or by name part)'
371
+ example 'ch'
372
+ example 'ch feature'
373
+ proc do |opts|
374
+ name_part = opts[:args].first
375
+ branches = `git branch`.chomp.split("\n")
376
+ .map { |b| b.sub(/^[\s*]+/, '') }
377
+ .reject { |b| b.include?('backup') || b.empty? }
378
+ branch = if name_part
379
+ branches.find { |b| b.include?(name_part) } or error "no branch matching #{name_part.inspect}"
380
+ else
381
+ error 'working tree not clean - stash or commit first' unless `git status`.include?('working tree clean')
382
+ pick('Switch to branch: ', branches)
383
+ end
384
+ next unless branch
385
+ run 'git fetch origin'
386
+ run "git checkout #{branch}"
387
+ run "git pull origin #{branch} --rebase"
388
+ end
389
+ end
390
+
391
+ task :swap do
392
+ desc 'swap current branch name with another branch'
393
+ proc do |opts|
394
+ branch = opts[:args].first || pick('Switch branch to swap: ', local_branches - [BRANCH])
395
+ next unless branch
396
+ next unless yes?("Swap #{BRANCH} and #{branch}")
397
+ run "git branch -m #{BRANCH}-tmp"
398
+ run "git branch -m #{branch} #{BRANCH}"
399
+ run "git branch -m #{BRANCH}-tmp #{branch}"
400
+ run "git push origin #{branch} --force-with-lease" if yes?('Push branch?')
401
+ end
402
+ end
403
+
404
+ task :delete do
405
+ desc 'delete local branch (interactive)'
406
+ proc do |_|
407
+ branch = pick('Select a branch to DELETE', local_branches - [BRANCH])
408
+ next unless branch
409
+ next unless yes?("Delete branch #{branch}")
410
+ run "git branch -D #{branch}"
411
+ end
412
+ end
413
+
414
+ task :prune do
415
+ desc 'delete local branches gone on remote'
416
+ proc do |_|
417
+ list = `git branch -vv | grep ': gone]' | awk '{print $1}'`.chomp.split($/)
418
+ list.each do |branch|
419
+ run %[git branch -D "#{branch}"] if yes?("Delete #{branch}?")
420
+ end
421
+ end
422
+ end
423
+
424
+ task :search do
425
+ desc 'search a string in branch / all branches / log'
426
+ example 'search TODO'
427
+ proc do |opts|
428
+ string = opts[:args].first or error 'specify search string'
429
+ choices = [
430
+ ['current branch', %[git grep "#{string}"]],
431
+ ['all branches', %[git grep "#{string}" $(git rev-list --all)]],
432
+ ['commit log', %[git log -p --all -S "#{string}"]]
433
+ ]
434
+ idx = choose('Search in:', choices.map(&:first))
435
+ run choices[idx][1] if idx
436
+ end
437
+ end
438
+
439
+ # ---- open / pr (shortcuts; see also `open:` namespace) ----------------
440
+
441
+ task :open do
442
+ desc 'open project page on GitHub/GitLab'
443
+ proc { open_in_browser(remote_page_url) }
444
+ end
445
+
446
+ task :pr do
447
+ desc 'create / view PR or MR for current branch'
448
+ proc { open_in_browser(remote_pr_url) }
449
+ end
450
+
451
+ # ---- tags / users / stats --------------------------------------------
452
+
453
+ task :tag do
454
+ desc 'tag the repo using ./.version'
455
+ proc do |_|
456
+ error 'no ./.version file' unless File.exist?('.version')
457
+ version = File.read('.version').strip
458
+ error 'empty ./.version' if version.empty?
459
+ run %[git tag -a #{version} -m "$(git show -s --format=%s)"]
460
+ end
461
+ end
462
+
463
+ task :tags do
464
+ desc 'list tags'
465
+ proc { run 'git tag -n' }
466
+ end
467
+
468
+ task :users do
469
+ desc 'all users that have added to this git repo'
470
+ proc { run 'git shortlog --summary --numbered --email' }
471
+ end
472
+
473
+ task :stat do
474
+ desc 'git statistics for last 30 days'
475
+ proc { run 'git-stat -d 30' }
476
+ end
477
+
478
+ task :rm do
479
+ desc 'show how to remove file/dir from git tracking'
480
+ proc do |opts|
481
+ file = opts[:args].first or error 'specify a file'
482
+ say <<~INFO
483
+ # if file is not in git
484
+ Put file in .git/info/exclude
485
+
486
+ # if file is in git
487
+ git update-index --assume-unchanged "#{file}"
488
+ git update-index --no-assume-unchanged "#{file}"
489
+
490
+ # list assume unchanged files
491
+ git ls-files -v | grep "^[[:lower:]]"
492
+ ---
493
+ INFO
494
+ run 'git ls-files -v | grep "^[[:lower:]]"'
495
+ end
496
+ end
497
+
498
+ # ---- undo (top-level + namespace) -------------------------------------
499
+
500
+ task :undo do
501
+ desc 'undo git add: git reset --mixed'
502
+ proc { run 'git reset --mixed' }
503
+ end
504
+
505
+ namespace :undo do
506
+ task :c do
507
+ desc 'undo last commit: git reset --soft HEAD~'
508
+ proc { run 'git reset --soft HEAD~' }
509
+ end
510
+ end
511
+
512
+ # ---- rubocop ----------------------------------------------------------
513
+
514
+ task :rc do
515
+ desc 'rubocop check of modified (unstaged) files'
516
+ alt :rcop
517
+ proc { rubocop_modified }
518
+ end
519
+
520
+ task :rubocop do
521
+ desc 'rubocop check ALL files diffed from parent branch'
522
+ proc do |_|
523
+ files = `git diff #{PARENT}..#{BRANCH} --name-only`
524
+ .split($/)
525
+ .select { |f| %w[rb rake].include?(f.split('.').last) }
526
+ .sort
527
+ files -= ['db/schema.rb']
528
+ next if files.empty?
529
+ puts files
530
+ run "rubocop #{files.join(' ')}"
531
+ end
532
+ end
533
+
534
+ # ---- log (top-level + namespace) --------------------------------------
535
+
536
+ task :log do
537
+ desc 'fancy log with graph'
538
+ proc { run "git log --graph --pretty=format:'%Cred%h%Creset %aI -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit" }
539
+ end
540
+
541
+ namespace :log do
542
+ task :simple do
543
+ desc 'log without graph'
544
+ proc { run "git log --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit" }
545
+ end
546
+
547
+ task :user do
548
+ desc 'log entries for me (or another user)'
549
+ example 'log:user'
550
+ example 'log:user "Alice Smith"'
551
+ proc do |opts|
552
+ user = opts[:args].first || `git config user.name`.chomp
553
+ run %{git log --date=short --pretty="%h %ad %an %s" --author="#{user}"}
554
+ end
555
+ end
556
+ end
557
+
558
+ # ---- open namespace (extras beyond top-level `open`) ------------------
559
+
560
+ namespace :open do
561
+ task :diff do
562
+ desc 'compare branch with parent on GitHub/GitLab'
563
+ proc { open_in_browser(remote_compare_url) }
564
+ end
565
+ end
566
+
567
+ # ---- reset namespace --------------------------------------------------
568
+
569
+ namespace :reset do
570
+ task :hard do
571
+ desc 'HARD reset branch to state on origin'
572
+ proc do |_|
573
+ next unless yes?('HARD RESET BRANCH TO ORIGIN, NO UNDO')
574
+ run 'git fetch origin'
575
+ run "git reset --hard origin/#{BRANCH}"
576
+ end
577
+ end
578
+
579
+ task :local do
580
+ desc 'reset to last local commit + clean -fd'
581
+ proc do |_|
582
+ next unless yes?('Reset branch to last local commit?')
583
+ run 'git reset --hard && git clean -fd'
584
+ end
585
+ end
586
+
587
+ task :head do
588
+ desc 'fetch origin + reset HEAD to origin/HEAD'
589
+ proc do |_|
590
+ run 'git fetch origin'
591
+ run 'git reset --hard origin/HEAD'
592
+ end
593
+ end
594
+ end
595
+
596
+ # ---- date surgery -----------------------------------------------------
597
+
598
+ task :squash do
599
+ desc 'squash last commit into its parent + redate'
600
+ proc do |_|
601
+ unless `git status`.include?('working tree clean')
602
+ run 'git add .'
603
+ run 'git commit -m tmp-squash-message'
604
+ end
605
+ git_date = `git log -2 --date=format:"%Y-%m-%dT%T" --format="%ad"`.chomp.split($/).last
606
+ run %[git reset --soft HEAD~2 && git commit --edit -m"$(git log --format=%B --reverse HEAD..HEAD@{1})"]
607
+ do_redate(git_date)
608
+ end
609
+ end
610
+
611
+ task :redate do
612
+ desc <<~DESC
613
+ fix commit date of latest commit
614
+ redate <iso-date> # set HEAD to date
615
+ redate <iso-date> <commit> # base on <commit>'s date
616
+ redate +2 # shift ~2 hours later
617
+ redate -2 # shift ~2 hours earlier
618
+ DESC
619
+ example 'redate 2020-04-05T21:03:27+00:00'
620
+ example 'redate +2'
621
+ proc do |opts|
622
+ do_redate(opts[:args][0], opts[:args][1])
623
+ end
624
+ end