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.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/AGENTS.md +28 -4
- data/README.md +97 -0
- data/lib/hammer/builder.rb +44 -0
- data/lib/hammer/builtins.rb +181 -0
- data/lib/hammer/recipe.rb +92 -0
- data/lib/lux-hammer.rb +157 -9
- data/recipes/git-helper.rb +624 -0
- data/recipes/srt.rb +270 -0
- metadata +6 -2
|
@@ -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
|