-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgit-sync
More file actions
executable file
·338 lines (292 loc) · 11.1 KB
/
git-sync
File metadata and controls
executable file
·338 lines (292 loc) · 11.1 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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
#!/bin/bash
# git-sync: update your stack after a branch was squash-merged into main
# Work around kcov --bash-handle-sh-invocation (v42) duplicating argv[0] into $1.
# See kcov src/engines/bash-execve-redirector.c — its argv-rebuild loop starts
# at i=0, so the script path arrives twice. Shift it off if we see it.
if [ $# -gt 0 ] && { [ "$1" = "$0" ] || [ "$1" = "${0##*/}" ]; }; then
shift
fi
MAIN_BRANCH=$(git config init.defaultBranch 2>/dev/null || echo "main")
usage() {
echo "Usage: git sync <merged-branch> rebase stack after a squash merge"
echo " git sync --continue continue after resolving a conflict"
echo " git sync --abort cancel and restore original state"
}
ensure_rerere() {
if [ "$(git config rerere.enabled 2>/dev/null)" != "true" ]; then
git config rerere.enabled true
git config rerere.autoupdate true
echo "Enabled rerere so repeated conflicts resolve automatically."
fi
}
find_child() {
local target="$1"
while read -r key value; do
local branch="${key#branch.}"
branch="${branch%.stack-parent}"
if [ "$value" = "$target" ]; then
echo "$branch"
return
fi
done < <(git config --get-regexp '^branch\..*\.stack-parent$' 2>/dev/null)
}
cascade_from() {
local START="$1"
local MERGED STACK_STR OLD_TIPS_STR
MERGED=$(git config sync.merged-branch 2>/dev/null || echo "")
STACK_STR=$(git config sync.cascade-stack 2>/dev/null || echo "")
OLD_TIPS_STR=$(git config sync.cascade-old-tips 2>/dev/null || echo "")
read -ra STACK <<< "$STACK_STR"
read -ra OLD_TIPS <<< "$OLD_TIPS_STR"
for (( i=START; i<${#STACK[@]}; i++ )); do
local BRANCH="${STACK[$i]}"
local NEW_BASE OLD_BASE
if [ "$i" -eq 0 ]; then
NEW_BASE="origin/$MAIN_BRANCH"
OLD_BASE="$MERGED"
else
NEW_BASE="${STACK[$((i-1))]}"
OLD_BASE="${OLD_TIPS[$((i-1))]}"
fi
git config sync.cascade-index "$i"
if ! git rebase --onto "$NEW_BASE" "$OLD_BASE" "$BRANCH"; then
echo ""
echo "Conflict rebasing '$BRANCH' onto '$NEW_BASE'."
echo "Resolve the conflicts, then run: git rebase --continue"
echo "Then run: git sync --continue"
exit 1
fi
done
}
if [ "$1" = "--continue" ]; then
if [ "$(git config sync.in-progress 2>/dev/null)" != "true" ]; then
echo "No sync in progress."
exit 1
fi
ensure_rerere
INDEX=$(git config sync.cascade-index 2>/dev/null || echo "0")
cascade_from $(( INDEX + 1 ))
# cascade done — clean up
MODE=$(git config sync.mode 2>/dev/null || echo "arg")
STACK_STR=$(git config sync.cascade-stack 2>/dev/null || echo "")
REMOTE_TIPS_STR=$(git config sync.cascade-remote-tips 2>/dev/null || echo "")
MERGED=$(git config sync.merged-branch 2>/dev/null || echo "")
RETURN_BRANCH=$(git config sync.return-branch 2>/dev/null || echo "")
read -ra STACK <<< "$STACK_STR"
read -ra REMOTE_TIPS <<< "$REMOTE_TIPS_STR"
# update parent of first branch to origin/main
if [ "${#STACK[@]}" -gt 0 ]; then
git config "branch.${STACK[0]}.stack-parent" "origin/$MAIN_BRANCH"
fi
if [ "$MODE" = "no-arg" ]; then
# no-arg sync: update parents, return to original branch
for (( i=1; i<${#STACK[@]}; i++ )); do
git config "branch.${STACK[$i]}.stack-parent" "${STACK[$((i-1))]}"
done
git config --remove-section sync 2>/dev/null || true
if [ -n "$RETURN_BRANCH" ]; then
git checkout "$RETURN_BRANCH" >/dev/null 2>&1
fi
echo "Stack synced."
echo ""
git stack-tree
else
# arg sync: push, delete merged branch
for (( i=0; i<${#STACK[@]}; i++ )); do
BRANCH="${STACK[$i]}"
REMOTE_TIP="${REMOTE_TIPS[$i]}"
if [ -n "$REMOTE_TIP" ]; then
git push --force-with-lease="$BRANCH:$REMOTE_TIP" origin "$BRANCH" \
|| echo "Note: could not push '$BRANCH'."
else
git push origin "$BRANCH" \
|| echo "Note: could not push '$BRANCH' (not pushed yet?)"
fi
done
git config --remove-section sync 2>/dev/null || true
git config --remove-section "branch.$MERGED" 2>/dev/null || true
git branch -D "$MERGED" 2>/dev/null || echo "Note: could not delete local '$MERGED'."
echo ""
echo "Done. '$MERGED' removed, stack rebased onto origin/$MAIN_BRANCH."
echo ""
git stack-tree
fi
exit 0
fi
if [ "$1" = "--abort" ]; then
if [ "$(git config sync.in-progress 2>/dev/null)" != "true" ]; then
echo "No sync in progress."
exit 1
fi
git rebase --abort 2>/dev/null || true
INDEX=$(git config sync.cascade-index 2>/dev/null || echo "0")
STACK_STR=$(git config sync.cascade-stack 2>/dev/null || echo "")
OLD_TIPS_STR=$(git config sync.cascade-old-tips 2>/dev/null || echo "")
REMOTE_TIPS_STR=$(git config sync.cascade-remote-tips 2>/dev/null || echo "")
read -ra STACK <<< "$STACK_STR"
read -ra OLD_TIPS <<< "$OLD_TIPS_STR"
read -ra REMOTE_TIPS <<< "$REMOTE_TIPS_STR"
for (( i=0; i<INDEX; i++ )); do
BRANCH="${STACK[$i]}"
OLD_TIP="${OLD_TIPS[$i]}"
REMOTE_TIP="${REMOTE_TIPS[$i]}"
git checkout "$BRANCH"
git reset --hard "$OLD_TIP"
echo "Reset '$BRANCH' to its original state."
if [ -n "$REMOTE_TIP" ]; then
if ! git push --force-with-lease="$BRANCH:$REMOTE_TIP" origin "$BRANCH"; then
echo "Warning: could not restore remote '$BRANCH'. Someone may have pushed to it."
echo "Check the branch manually before force pushing."
fi
fi
done
git config --remove-section sync 2>/dev/null || true
CURRENT=$(git branch --show-current)
echo "Sync aborted. You are on '$CURRENT'."
exit 0
fi
if [ -z "$1" ]; then
# no-arg sync: find current branch's stack root and rebase everything
CURRENT=$(git branch --show-current)
# check if we're on a branch that's part of a stack
if [ -z "$CURRENT" ] || [ "$CURRENT" = "$MAIN_BRANCH" ]; then
# not on a stack branch — check if any stack exists
CHILD=$(find_child "origin/$MAIN_BRANCH")
if [ -z "$CHILD" ]; then
CHILD=$(find_child "$MAIN_BRANCH")
fi
if [ -z "$CHILD" ]; then
echo "Nothing to sync — no stacked branches found."
exit 0
fi
fi
ensure_rerere
git fetch origin
ROOT="$CURRENT"
while true; do
PARENT=$(git config "branch.$ROOT.stack-parent" 2>/dev/null || echo "")
if [ -z "$PARENT" ] || [ "$PARENT" = "origin/$MAIN_BRANCH" ]; then
break
fi
ROOT="$PARENT"
done
# check if a branch was squash-merged into main
is_squash_merged() {
local branch="$1"
local unmerged
unmerged=$(git cherry "origin/$MAIN_BRANCH" "$branch" 2>/dev/null | grep -c '^+' || true)
[ "$unmerged" -eq 0 ]
}
# collect stack from root upward
STACK=("$ROOT")
B="$ROOT"
while true; do
NEXT=$(find_child "$B")
if [ -z "$NEXT" ]; then break; fi
STACK+=("$NEXT")
B="$NEXT"
done
# remove squash-merged branches from the bottom of the stack
while [ "${#STACK[@]}" -gt 0 ] && is_squash_merged "${STACK[0]}"; do
MERGED_BRANCH="${STACK[0]}"
STACK=("${STACK[@]:1}")
# reparent the next branch if it exists
if [ "${#STACK[@]}" -gt 0 ]; then
git config "branch.${STACK[0]}.stack-parent" "origin/$MAIN_BRANCH"
fi
git config --remove-section "branch.$MERGED_BRANCH" 2>/dev/null || true
git branch -D "$MERGED_BRANCH" 2>/dev/null || true
done
# save old tips for abort, and record old parent of root for rebase base
OLD_TIPS=()
for B in "${STACK[@]}"; do
OLD_TIPS+=("$(git rev-parse "$B")")
done
# the "merged-branch" for cascade_from is the old base of the first branch
FIRST_PARENT=$(git config "branch.${STACK[0]}.stack-parent" 2>/dev/null || echo "origin/$MAIN_BRANCH")
git config sync.in-progress true
git config sync.mode "no-arg"
git config sync.merged-branch "$FIRST_PARENT"
git config sync.cascade-stack "${STACK[*]}"
git config sync.cascade-old-tips "${OLD_TIPS[*]}"
git config sync.cascade-remote-tips ""
git config sync.cascade-index "0"
git config sync.return-branch "$CURRENT"
# rebase remaining branches using cascade_from
cascade_from 0
# completed without conflict — clean up and return
for (( i=0; i<${#STACK[@]}; i++ )); do
BRANCH="${STACK[$i]}"
if [ "$i" -eq 0 ]; then
git config "branch.$BRANCH.stack-parent" "origin/$MAIN_BRANCH"
else
git config "branch.$BRANCH.stack-parent" "${STACK[$((i-1))]}"
fi
done
git config --remove-section sync 2>/dev/null || true
git checkout "$CURRENT" >/dev/null 2>&1
echo "Stack synced."
echo ""
git stack-tree
exit 0
fi
if [ "$(git config sync.in-progress 2>/dev/null)" = "true" ]; then
echo "A sync is already in progress. Run 'git sync --continue' or 'git sync --abort'."
exit 1
fi
MERGED="$1"
ensure_rerere
git fetch origin
CHILD=$(find_child "$MERGED")
if [ -z "$CHILD" ]; then
echo "No branch found stacked on '$MERGED'. Cleaning up."
git config --remove-section "branch.$MERGED" 2>/dev/null || true
git branch -D "$MERGED" 2>/dev/null || true
exit 0
fi
# collect stack from child upward
STACK=("$CHILD")
B="$CHILD"
while true; do
NEXT=$(find_child "$B")
if [ -z "$NEXT" ]; then
break
fi
STACK+=("$NEXT")
B="$NEXT"
done
# save old local tips and remote tips before any rebase
OLD_TIPS=()
REMOTE_TIPS=()
for B in "${STACK[@]}"; do
OLD_TIPS+=("$(git rev-parse "$B")")
REMOTE_TIPS+=("$(git rev-parse "origin/$B" 2>/dev/null || echo "")")
done
echo "Syncing stack: ${STACK[*]}"
git config sync.in-progress true
git config sync.merged-branch "$MERGED"
git config sync.cascade-stack "${STACK[*]}"
git config sync.cascade-old-tips "${OLD_TIPS[*]}"
git config sync.cascade-remote-tips "${REMOTE_TIPS[*]}"
git config sync.cascade-index "0"
cascade_from 0
# completed without conflict
git config "branch.$CHILD.stack-parent" "origin/$MAIN_BRANCH"
for (( i=0; i<${#STACK[@]}; i++ )); do
BRANCH="${STACK[$i]}"
REMOTE_TIP="${REMOTE_TIPS[$i]}"
if [ -n "$REMOTE_TIP" ]; then
git push --force-with-lease="$BRANCH:$REMOTE_TIP" origin "$BRANCH" \
|| echo "Note: could not push '$BRANCH'."
else
git push origin "$BRANCH" \
|| echo "Note: could not push '$BRANCH' (not pushed yet?)"
fi
done
git config --remove-section sync 2>/dev/null || true
git config --remove-section "branch.$MERGED" 2>/dev/null || true
git branch -D "$MERGED" 2>/dev/null || echo "Note: could not delete local '$MERGED'."
echo ""
echo "Done. '$MERGED' removed, stack rebased onto origin/$MAIN_BRANCH."
echo ""
git stack-tree