/
fmc
executable file
·91 lines (77 loc) · 3.1 KB
/
fmc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
#!/usr/bin/env zsh
set -euo pipefail
# fmc: https://github.com/0ax1/fmc
# Echo the revision range to inspect. In case the current branch is not main,
# the range is given by the common ancestor with main and the current branch's
# HEAD. Otherwise, all commits of main are taken into account.
rev_range() {
current_branch=`git rev-parse --abbrev-ref HEAD`
# Exit in case no additional commits on top of the reference branch have been made.
check_commits_on_top() {
if [[ $1 == `git rev-parse HEAD` ]]; then
exit 1
fi
}
if [[ ! -z `git branch --list main` && $current_branch != "main" ]]; then
merge_base=`git merge-base HEAD main`
check_commits_on_top $merge_base "main"
echo "$merge_base..HEAD"
elif [[ ! -z `git branch --list master` && $current_branch != "master" ]]; then
merge_base=`git merge-base HEAD master`
check_commits_on_top $merge_base "master"
echo "$merge_base..HEAD"
else
echo "HEAD"
fi
}
# List line matching commits prefixed with 'L '.
line_candidates() {
range=`rev_range`
# Iterate files which have been modified.
(for file in `git diff --name-only --staged --diff-filter=M`; do
local readonly last_line=`git show HEAD:$file | wc -l`
# Hunk format: @@ -del[,line_count] +add[,line_count] @@
(git diff --staged -U0 $file | grep -oE "@@ [+,-][0-9]+" | sed 's/[^0-9]*//g') | while read line; do
# Check for appended lines which cannot be blamed.
if [[ $line -le $last_line ]]; then
git blame -s -b --root --no-progress -L "$line,$line" $range -- $file
else
git blame -s -b --root --no-progress -L "$last_line,$last_line" $range -- $file
fi
done
# Get the commit hash and prefix the line with 'L '.
done) | cut -d " " -f1 | sed 's/^/L /' | grep -oE 'L [0-9a-f]+'
}
# List file matching commits prefixed with 'F '.
file_candidates() {
range=`rev_range`
# Iterate files which have been modified.
(for file in `git diff --name-only --staged --diff-filter=M`; do
git rev-list -n 4 -E $range -- $file
done) | sed 's/^/F /'
}
# Output a formatted log of the match type and commit info.
print_candidate() {
# Abbreviate the commit hash to its minimal required length.
local readonly len=`git rev-parse --short HEAD | awk '{print length}'`
git --no-pager log --format="%h |$1| %s - %an" --abbrev=$len -n 1 $2
}
# List the most recent unique candidate commits (limited to 4).
list_all_candidates() {
if git diff --cached --quiet; then
echo 'No staged changes.' >&2
exit 1
fi
# Cd to the repo's top-level directory.
cd "`git rev-parse --show-toplevel`"
# Only take file candidates into account if no line candidates are found.
local readonly l_candidates=`line_candidates`
((if [[ ! -z $l_candidates ]]; then
echo $l_candidates
else
file_candidates
fi) | while read match_type commit_sha; do
print_candidate $match_type $commit_sha
done) | awk '!seen[$1]++' | head -4
}
list_all_candidates