forked from nvie/git-toolbelt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
git-cleanup
executable file
·202 lines (175 loc) · 5.78 KB
/
git-cleanup
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
#!/bin/sh
set -e
usage () {
echo "usage: git cleanup [-nh]" >&2
echo >&2
echo "Deletes all branches that have already been merged into the main branch." >&2
echo "Removes those branches both locally and in the origin remote. Will be " >&2
echo "most conservative with deletions." >&2
echo >&2
echo "Options:" >&2
echo "-n Dry-run" >&2
echo "-s Squashed" >&2
echo "-l Local branches only, don't touch the remotes" >&2
echo "-v Be verbose (show what's skipped)" >&2
echo "-h Show this help" >&2
}
dryrun=0
remotes=1
squashed=0
verbose=0
while getopts nlsvh flag; do
case "$flag" in
n) dryrun=1;;
l) remotes=0;;
s) squashed=1;;
v) verbose=1;;
h) usage; exit 2;;
esac
done
shift $(($OPTIND - 1))
#
# This will clean up any branch (both locally and remotely) that has been
# merged into any of the known "trunks". Trunks are any of:
#
# - main (local) + origin/main
# - master (local) + origin/master
#
safegit () {
if [ "$dryrun" -eq 1 ]; then
echo git "$@"
else
git "$@"
fi
}
#
# The Algorithm[tm]:
# - Find the smallest set of common ancestors for those trunks. (There can
# actually be multiple, although unlikely.)
# - For each local branch, check if any of the common ancestors contains it,
# but not vice-versa (prevents newly-created branches from being deleted)
# - Idem for each remote branch
#
find_common_base () {
if [ $# -eq 1 ]; then
git sha "$1"
else
git merge-base "$1" "$2"
fi
}
find_branch_base () {
branch="$1"
base_point=""
if git local-branch-exists "$branch"; then
base_point=$(find_common_base "$branch" $base_point)
fi
if git remote-branch-exists origin "$branch"; then
base_point=$(find_common_base "origin/$branch" $base_point)
fi
if [ -n "$base_point" ]; then
echo "$base_point"
fi
}
main="$(git main-branch)"
find_bases () {
find_branch_base "$main"
}
bases=$(find_bases)
#
# The Clean Squashed Algorithm[tm]
# - Create a temporary dangling squashed commit with git commit-tree
# - Then use git cherry to check if the squashed commit has already been
# applied to the main branch
# - If it has, then delete the branch
#
clean_squashed () {
branch="$1"
# Find the merge base for this branch
merge_base=$(git merge-base "$main" "$branch")
# Get the tree object of the branch
branch_tree="$(git rev-parse "$branch^{tree}")"
# Create a squashed commit object of the branch tree with parent
# of $base with a commit message of "_"
dangling_squashed_commit="$(git commit-tree "$branch_tree" -p "$merge_base" -m _)"
# Show a summary of what has yet to be applied
cherry_commit="$(git cherry "$main" "$dangling_squashed_commit")"
if [ "$cherry_commit" = "- $dangling_squashed_commit" ]; then
# If "- <commit-sha>", (ex. - "- 851cb44727") this means the
# commit is in main and can be dropped if you rebased
# against main
safegit branch -D "$branch"
elif [ $verbose -eq 1 ]; then
# If "+ <commit-sha>", (ex. - "+ 851cb44727") this means the
# commit still needs to be kept so that it will be applied to
# main
echo "Skipped $branch (no similar squash found)"
fi
}
for branch in $(git local-branches \
| grep -vxF "$main"); do
for base in $bases; do
if git contains "$base" "$branch"; then
if ! git contains "$branch" "$base"; then
# Actually delete
if ! safegit branch -D "$branch"; then
echo "Errors deleting local branch $branch" >&2
fi
break
fi
else
# This is the case where the branches are in fact legit
# local WIP branches or they are squashed merges, and we
# need to check if they have been squashed-merged into
# main. NOTE - this assumes main is up-to-date locally
if [ "$squashed" -eq 1 ]; then
clean_squashed "$branch"
fi
fi
done
done
# Pruning first will remove any remote tracking branches that don't exist in
# the remote anymore anyway.
#XXX: FIXME: This gave trouble, as it tried to remove branches from Heroku remotes... :(
#for remote in $(git remote); do
for remote in origin; do
safegit remote prune "$remote" >/dev/null 2>/dev/null
if [ $remotes -eq 1 ]; then
branches_to_remove=""
for branch in $(git remote-branches "$remote" | grep -vEe "/($main)\$"); do
for base in $bases; do
if git contains "$base" "$branch"; then
if ! git contains "$branch" "$base"; then
branchname=$(echo "$branch" | cut -d/ -f2-)
branches_to_remove="$branches_to_remove $branchname"
break
fi
fi
done
done
if [ -n "$branches_to_remove" ]; then
if ! safegit push "$remote" --delete $branches_to_remove; then
echo "Errors deleting branches $branches_to_remove from remote '$remote'" >&2
fi
fi
fi
done
# Delete any remaining local remote-tracking branches of remotes that are gone
# This is an atypical situation that has occurred to me personally after having
# used the command:
#
# $ hub merge <some-github-url>
#
branches_to_remove=""
for branch in $(git remote-branches); do
for base in $bases; do
if git contains "$base" "$branch"; then
if ! git contains "$branch" "$base"; then
branches_to_remove="$branches_to_remove $branch"
break
fi
fi
done
done
if [ -n "$branches_to_remove" ]; then
safegit branch -dr $branches_to_remove
fi