-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathgit-rm-merged-branches
More file actions
executable file
·218 lines (173 loc) · 4.71 KB
/
git-rm-merged-branches
File metadata and controls
executable file
·218 lines (173 loc) · 4.71 KB
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
#!/bin/bash
set -euo pipefail
usage() {
cat >&2 <<EOM
usage: git rm-merged-branches [OPTIONS]
Look for git branches that have been merged to main, and interactively prompt
for whether to delete them.
This is useful to clean up local branches that are associated with pull
requests that have been merged.
Git can natively tell us which branches were merged (git branch --merged).
This command also tries to handle squash + merge cases. (To do this, it checks
each branch by squashing the branch and cherry picking the result to see if
there is any remaining diff.)
The default branch is assumed to be called 'main' or 'master'.
Exclusions:
- The currently checked out branch is ignored
- Branches env/* or stages/* are ignored
Options:
-h, --help Display help text
--no-prune-squash Don't check for squashed branches
EOM
}
run() {
echo >&2 "+ $*"
"$@"
}
# usage: prompt_yn [PROMPT]
# Prompt the user for a yes/no response.
#
# Exit codes:
# 0: user entered yes
# 2: STDIN is not a TTY
# 10: user entered no
#
prompt_yn() {
local prompt ans
if [ $# -ge 1 ]; then
prompt="$1"
else
prompt="Continue?"
fi
if [ ! -t 0 ]; then
echo >&2 "$prompt [y/n]"
echo >&2 "prompt_yn: error: stdin is not a TTY!"
return 2
fi
while true; do
read -r -p "$prompt [y/n] " ans
case "$ans" in
Y|y|yes|YES|Yes)
return
;;
N|n|no|NO|No)
return 10
;;
esac
done
}
# Determine the main/master branch and echo its name.
git-main-branch() {
local ret
git rev-parse --verify --quiet main >/dev/null && ret=$? || ret=$?
case "$ret" in
0)
echo main
return
;;
1)
# pass
;;
*)
# probably not a git repo / some other error
return "$ret"
;;
esac
if git rev-parse --verify --quiet master >/dev/null; then
echo master
else
echo >&2 "Neither master nor main exists"
return 1
fi
}
git-is-branch-cherry-equivalent() {
local main branch
main="$1"
branch="$2"
local merge_base
merge_base="$(git merge-base "$main" "$branch")"
# get the tree content as of the branch
local tree
tree="$(git rev-parse "$branch^{tree}")"
# make a commit that's a single squashed commit of the tree, with
# the main/master merge base as the parent
local tmp_commit
tmp_commit="$(git commit-tree "$tree" -p "$merge_base" -m "tmp from $0")"
# TODO: is there any way to gc this tmp commit when we're done?
# determine whether cherry-pick would do anything
if [[ $(git cherry "$main" "$tmp_commit") == "-"* ]]; then
# - prefix from git cherry means cherry-pick would be a no-op
return 0
fi
return 1
}
# usage: is-branch-excluded BRANCH CURRENT_BRANCH
is-branch-excluded() {
local branch current_branch
branch="$1"
current_branch="$2"
# branches excluded from pruning
case "$branch" in
master|main) return 0 ;;
stages/*) return 0 ;;
env/*) return 0 ;;
"$current_branch") return 0 ;;
esac
return 1
}
main() {
local branch current_branch
local nothing_to_do
current_branch="$(git branch --show-current)"
nothing_to_do=1
# prune merged branches
for branch in $(git branch --format='%(refname:short)' --merged); do
if is-branch-excluded "$branch" "$current_branch"; then
continue
fi
nothing_to_do=
if prompt_yn "Delete '$branch'?"; then
run git branch -d "$branch"
fi
done
if [ -n "$prune_squash" ]; then
local main
main="$(git-main-branch)"
# prune squashed branches
for branch in $(git for-each-ref refs/heads/ "--format=%(refname:short)")
do
if is-branch-excluded "$branch" "$current_branch"; then
continue
fi
if git-is-branch-cherry-equivalent "$main" "$branch"; then
nothing_to_do=
if prompt_yn "Delete squashed '$branch'?"; then
run git branch -D "$branch"
fi
fi
done
fi
if [ -n "$nothing_to_do" ]; then
echo "Nothing to do"
else
echo "Done"
fi
}
prune_squash=1
while [ $# -ge 1 ] && [[ $1 == -* ]]; do
case "$1" in
-h|--help)
usage
exit
;;
--no-prune-squash)
prune_squash=
;;
*)
echo >&2 "Invalid option: $1"
exit 1
;;
esac
shift
done
main