-
Notifications
You must be signed in to change notification settings - Fork 612
/
ever-green.sh
executable file
·879 lines (771 loc) · 24.9 KB
/
ever-green.sh
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
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
#!/bin/sh
# Update an "ever-green branch", i.e. given a previously rebased branch
# thicket, update it both to include new changes in the original branch as well
# as rebasing the result to the new upstream branch.
#
# Most prominent example: shears/seen, which reflects Git for Windows' patches as
# rebased on top of the now-current "seen" branch.
#
# There are a couple of scenarios to keep in mind:
#
# - The branch could be maintained via merging-rebases, i.e. started with a
# fake merge that merges in previous iterations (without taking any of the
# changes, though).
#
# - The current branch might have advanced since the previous ever-green
# update.
#
# - The upstream branch might have advanced since the previous ever-green
# update.
#
# - The upstream branch might have been force-pushed since the previous
# ever-green update.
#
# If there are new patches to rebase (i.e. if there are changes to the current
# branch that have not made it to the ever-green branch yet), and if the
# upstream branch has changed, we actually need *two* rebase runs. But we will
# pretend that to be a single rebase by appending the second rebase's todo
# script to the first one's.
die () {
echo "$*" >&2
exit 1
}
THIS_SCRIPT="$(realpath "$0")"
make_script () { # <tip-commit-to-rebase> <rebase-i-options>...
# Create throw-away worktree in order to generate todo list
worktree="$(git rev-parse --absolute-git-dir)/temp-rebase.$$"
git worktree add --no-checkout "$worktree" "$1"^0 >&2
shift
cur_git_dir="$(git rev-parse --absolute-git-dir)"
tmp_todo_list="$worktree.todo"
rm -f "$tmp_todo_list" || die "Could not remove $tmp_todo_list"
fake_editor="$(git -C "$worktree" rev-parse --absolute-git-dir)/fake-editor.sh" &&
cat >"$fake_editor" <<-EOF &&
#!/bin/sh
cat "\$1" >"$tmp_todo_list" &&
exit 1
EOF
chmod a+x "$fake_editor" &&
if out="$(unset GIT_DIR; GIT_SEQUENCE_EDITOR="$fake_editor" git -C "$worktree" rebase -i "$@" 2>&1)" ||
test ! -f "$tmp_todo_list"
then
die "Failed to generate todo list for 'rebase $*' in $PWD: $out"
fi
git worktree remove --force "$worktree" &&
result="$(cat "$tmp_todo_list")" &&
rm "$tmp_todo_list" &&
echo "$result"
}
extract_todo_help () {
help_starts_at="$(grep -n '^# Rebase .* ([0-9]* commands)' <"$1")"
help_starts_at="${help_starts_at%%:*}"
case "$help_starts_at" in
'') help=;;
*[^0-9]*) die "BUG: could not get start of help in $1";;
*)
mv -f "$1" "$1.raw" ||
die "Could not rename $1"
help="$(sed "1,$(($help_starts_at-2))d" <"$1.raw")"
sed "$(($help_starts_at-1))q" >"$1" <"$1.raw"
;;
esac
echo "$help"
}
# When stopped with conflicts during a rebase, this function determines whether
# there is an upstream commit (i.e. a commit reachable from HEAD but not from
# the original commit to be picked), and whether it is identical (except for
# Junio's Signed-off-by: line).
#
# Returns 0 if it is essentially identical, 1 if it is not (or if no upstream
# commit could be found).
compare_stopped_to_upstream_commit () {
tip="$(cat "$(git rev-parse --git-path rebase-merge/stopped-sha)")" &&
git rev-parse -q --verify "$tip" || {
echo "Could not get stopped-sha; Not in a rebase?\n" >&2
return 1
}
upstream=HEAD
count=1
upstream_commit="$(git range-diff -s $tip^..$tip $tip.."$upstream" |
sed -n 's/^[^:]*:[^:]*[=!][^:]*: *\([^ ]*\).*/\1/p')"
test -n "$upstream_commit" ||
upstream_commit="$(git range-diff --creation-factor=95 -s $tip^..$tip $tip.."$upstream" |
sed -n 's/^[^:]*:[^:]*[=!][^:]*: *\([^ ]*\).*/\1/p')"
test -n "$upstream_commit" || {
echo "Could not find upstream commit for '$tip'" >&2
return 1
}
if git range-diff -s $tip^..$tip $upstream_commit^..$upstream_commit |
grep -q '^1: [^ ]* *= '
then
echo "$tip is identical to $upstream_commit; skipping" >&2
return 0
fi
! git grep -q '^Signed-off-by: Junio.* <gitster@pobox\.com>$' "$upstream_commit" || {
edited_upstream_commit="$(git cat-file commit "$upstream_commit" |
sed '/^Signed-off-by: Junio.* <gitster@pobox\.com>$/d' |
git hash-object -t commit -w --stdin)"
if git range-diff -s $tip^..$tip $edited_upstream_commit^..$edited_upstream_commit |
grep -q '^1: [^ ]* *= '
then
echo "$tip is essentially identical to $upstream_commit; skipping" >&2
return 0
fi
}
printf "Found upstream commit %s for %s, but they are different:\n\n" "$upstream_commit" "$tip" >&2
git range-diff --creation-factor=95 "$tip~$count..$tip" "$upstream_commit~$count..$upstream_commit"
return 1
}
continue_rebase () {
test -n "$ORIGINAL_GIT_EDITOR" ||
ORIGINAL_GIT_EDITOR="$(git var GIT_EDITOR 2>/dev/null || echo false)"
test -n "$ORIGINAL_GIT_EDITOR" ||
die "Could not determine editor"
export ORIGINAL_GIT_EDITOR
export GIT_EDITOR="\"$THIS_SCRIPT\" fixup-quietly" ||
die "Could not override editor"
while true
do
msgnum="$(cat "$(git rev-parse --git-dir)/rebase-merge/msgnum")" ||
die "Could not determine msgnum"
git rev-parse --verify HEAD >"$(git rev-parse --git-dir)/cur-head" ||
die "Could not record current HEAD"
git diff-files --quiet || {
compare_stopped_to_upstream_commit &&
git add -u &&
git reset --hard
} ||
die "There are unstaged changes; Cannot continue"
git rebase --continue && break
test "exec exit 123 # force re-reading of replacement objects" != \
"$(tail -n 1 "$(git rev-parse --git-path rebase-merge/done)")" ||
continue
test "$msgnum" != "$(cat "$(git rev-parse --git-dir)/rebase-merge/msgnum")" ||
exit 1
test -f "$(git rev-parse --git-dir)/rebase-merge/stopped-sha" ||
exit 1
done
}
case "$1" in
replace-todo-script)
shift
cp -f "$(git rev-parse --git-dir)/replace-todo" "$1" ||
die "Could not replace todo list"
eval "$ORIGINAL_GIT_SEQUENCE_EDITOR \"$1\"" && exit
die "Failed to call '$ORIGINAL_GIT_SEQUENCE_EDITOR'"
;;
nested-rebase)
shift
case "$1" in
--merging=*) merging="${1#*=}"; shift;;
*) merging=;;
esac
todo="$(git rev-parse --git-path rebase-merge/git-rebase-todo)" &&
help="$(extract_todo_help "$todo")" &&
mv -f "$todo" "$todo.save" ||
die "Could not save $todo"
onto="$*"; onto="${onto#*--onto }"
test "a$onto" != "a$*" ||
die "Could not determine --onto from '$*'"
onto="$(git rev-parse "${onto%% *}")" ||
die "Invalid onto: '$*'"
echo "# Now let's rebase the ever-green branch onto $onto" >"$todo" &&
echo "reset $onto" >>"$todo" &&
if test -n "$merging"
then
cat >>"$todo" <<-EOF
exec git merge -s ours -m "\$(cat "\$(git rev-parse --git-dir)/merging-rebase-message")" "$merging"
exec git replace --graft HEAD HEAD^
exec exit 123 # force re-reading of replacement objects
EOF
fi &&
make_script HEAD "$@" >>"$todo" ||
die "Could not retrieve new todo list"
help="$(extract_todo_help "$todo")" &&
cat "$todo.save" >>"$todo" &&
if test -n "$merging"
then
sed -e '/^exec.*fixup.*squash/{
i\
exec git replace --delete "HEAD^{/^Start the merging-rebase}"
:1;N;b1
}' -e '$a\
exec git replace --delete "HEAD^{/^Start the merging-rebase}"' "$todo" >"$todo.new" &&
mv -f "$todo.new" "$todo" ||
die "Could not append exec line to reset the replace ref"
fi &&
echo "$help" >>"$todo" ||
die "Could not append saved todo commands"
if test -z "$ORIGINAL_GIT_SEQUENCE_EDITOR"
then
# If ORIGINAL_GIT_SEQUENCE_EDITOR is no longer set, that means
# that the rebase was interrupted and restarted, i.e. the
# GIT_SEQUENCE_EDITOR is no longer overridden by ever-green.sh.
ORIGINAL_GIT_SEQUENCE_EDITOR="$GIT_SEQUENCE_EDITOR"
test -n "$ORIGINAL_GIT_SEQUENCE_EDITOR" ||
ORIGINAL_GIT_SEQUENCE_EDITOR="$(git config sequence.editor)"
test -n "$ORIGINAL_GIT_SEQUENCE_EDITOR" ||
ORIGINAL_GIT_SEQUENCE_EDITOR="$(git var GIT_EDITOR 2>/dev/null || echo false)"
test -n "$ORIGINAL_GIT_SEQUENCE_EDITOR" ||
die "Could not determine editor"
fi
eval "$ORIGINAL_GIT_SEQUENCE_EDITOR \"$todo\"" ||
die "Could not launch $ORIGINAL_GIT_SEQUENCE_EDITOR"
test -s "$todo" ||
die "Aborted phase 2 of the ever-green rebase"
exit 0
;;
fixup-quietly)
test "$(git rev-parse HEAD)" != "$(cat "$(git rev-parse --git-dir)/cur-head")" ||
exit 0
shift
eval "$ORIGINAL_GIT_EDITOR" "$1" ||
die "Could not execute $ORIGINAL_GIT_EDITOR!"
exit 0
;;
continue-rebase)
shift
test --skip != "$*" || {
shift
git add -u &&
git reset --hard ||
die "Could not --skip"
}
test -z "$*" ||
die "Unhandled arguments: $*"
continue_rebase
exit 0
;;
self-test)
exec </dev/null
tmp_worktree=/tmp/ever-green.self-test
test ! -e "$tmp_worktree" || rm -rf "$tmp_worktree" || die "Could not remove $tmp_worktree"
git init "$tmp_worktree" &&
cd "$tmp_worktree" &&
git config core.autocrlf false ||
die "Could not init $tmp_worktree"
git config user.name "Ever Green" &&
git config user.email "[email protected]" ||
die "Could not configure committer"
# Let's assume that we have a local branch and a remote branch, and we
# want to keep developing the local branch, all the while the remote
# branch is also advancing.
#
# The idea of an ever-green branch is to be a continuously-updated
# branch that reflects what the local branch would look like, after
# rebasing it to the remote branch.
#
# So what does this look like? Assume that we have this local branch:
#
# A - B - M - fixup D - E
# \ /
# D
#
# where A is the revision from the remote branch on which the local
# branch was based. Then, let's assume that the remote branch added a
# new commit, F. The ever-green branch would now look like this:
#
# A - F - B' - M' - E'
# \ /
# D'
#
# Obviously, the fixup for D would have been squashed into D while
# updating the ever-green branch.
#
# To make things realistic, we slip in a change into E' that was not
# there in E. This reflects scenarios where changes are necessary
# during the rebase to make things work again, e.g. when a function
# signature changes in F and E introduces a caller to said function.
#
# Now, let's make things *even* more realistic by adding *quite* a bit
# of local work:
#
# --------- C R - S
# / \ \
# A - B - M - fixup D - E - N --- P - Q - T
# \ / \ / /
# D --------- fixup B - K
#
# And let's also throw in another remote commit: the remote branch now
# looks like this:
#
# A - F - G
#
# At this stage, the tip of the local branch is S, the tip of the
# remote branch is G, and the (outdated) ever-green branch's tip is E'.
#
# Now we want to use ever-green.sh to update the ever-green branch, so
# that it looks like this:
#
# C" R - S
# / \ \
# A - F - G - B" - M" - E" - N" - Q" - T"
# \ / \ /
# D" K" ----
#
# where B" contains the fixup and E" is actually a rebased E' (instead
# of a rebased E that would not have that extra change).
#
# Note that we do *not* want the cousins R and S to be rewritten; they
# should stay the exact same. This reflects the situation where we want
# to merge a branch from git.git's "seen" branch thicket into Git for
# Windows' main branch, and then use the ever-green.sh script to rebase
# on top of a newer "seen" branch thicket.
test_commit () { # <mark> <parent(s)> <commit-message> [<file-name> [<contents>]]
test -n "$2" || echo "reset refs/heads/main"
printf '%s\n' \
'commit refs/heads/main' \
"mark :$1" \
"committer Ever Green <[email protected]> $((1234567890+$1)) +0000" \
'data <<EOM' \
"$3" \
'EOM'
test -z "$2" || {
first=${2% *}
echo "from :$first"
for parent in ${2#$first}
do
echo "merge :$parent"
done
}
test "a$4" = "a-" ||
printf '%s\n' \
"M 100644 inline ${4:-$3}" \
'data <<EOD' \
"${5:-${4:-$3}}" \
'EOD'
printf '%s\n' \
"reset refs/tags/$(echo "$3" |
sed 's/[^a-zA-Z0-9][^a-zA-Z0-9]*/-/g')" \
"from :$1"
}
git fast-import <<-EOF || die "Could not import initial history"
$(test_commit 1 '' A)
$(test_commit 2 1 B)
$(test_commit 3 1 D)
$(test_commit 4 '2 3' M D)
$(test_commit 5 4 C)
$(test_commit 6 4 'fixup! D' D changed)
$(test_commit 7 6 E)
$(test_commit 8 '5 7' N E)
$(test_commit 9 4 'fixup! B' B fixed-up)
$(test_commit 10 '8 9' P B fixed-up)
$(test_commit 11 9 K)
$(test_commit 12 '10 11' Q K)
$(test_commit 20 1 F)
$(test_commit 21 20 G)
$(test_commit 22 '' R)
$(test_commit 23 22 S)
$(test_commit 24 '12 23' T)
EOF
# Start ever-green branch
git checkout -b ever-green E &&
"$THIS_SCRIPT" --initial --onto=F &&
echo "E1" >E &&
git commit --amend -m E1 E ||
die "Could not create previous ever-green"
git tag pre-rebase
git log --graph --format=%s --boundary A..ever-green >actual &&
cat >expect <<-\EOF
* E1
* M
|\
| * D
* | B
|/
* F
o A
EOF
git -P diff --no-index -w expect actual ||
die "Unexpected graph"
"$THIS_SCRIPT" --current-tip=T --previous-tip=E --ever-green-base=F --onto=G ||
die "Could not update ever-green branch"
git log --graph --format=%s --boundary A..ever-green >actual &&
cat >expect <<-\EOF
* T
|\
| * S
| * R
* Q
|\
| * K
* | N
|\ \
| * | E1
| |/
* / C
|/
* M
|\
| * D
* | B
|/
* G
* F
o A
EOF
git -P diff --no-index -w expect actual ||
die "Unexpected graph"
test changed = "$(git show ever-green:D)" ||
die "Lost amendment to D"
test fixed-up = "$(git show ever-green:B)" ||
die "Lost amendment to B"
test E1 = "$(git show ever-green:E)" ||
die "Lost amendment to E"
test 0 = $(git rev-list --count ever-green^2...S --) ||
die "S was rewritten"
# Now, let's do the same for merging-rebases
git checkout -b merging-ever-green E &&
"$THIS_SCRIPT" --initial --merging --onto=F &&
echo "E1" >E &&
git commit --amend -m E1 E ||
die "Could not create previous ever-green"
git tag merging-pre-rebase
git log --graph --format=%s --boundary A..merging-ever-green ^E -- >actual &&
cat >expect <<-\EOF
* E1
* M
|\
| * D
* | B
|/
* Start the merging-rebase to F
|\
* | F
o | A
/
o E
EOF
git -P diff --no-index -w expect actual ||
die "Unexpected graph"
"$THIS_SCRIPT" --current-tip=T --merging --onto=G ||
die "Could not update ever-green branch"
git log --graph --format=%s --boundary A..merging-ever-green ^T -- >actual &&
cat >expect <<-\EOF
* T
|\
* \ Q
|\ \
| * | K
* | | N
|\ \ \
| * | | E1
| |/ /
* / / C
|/ /
* | M
|\ \
| * | D
* | | B
|/ /
* | Start the merging-rebase to G
|\ \
* | | G
* | | F
o | | A
/ /
o / T
|/
o S
EOF
git -P diff --no-index -w expect actual ||
die "Unexpected graph"
git -P diff --exit-code ever-green -- ||
die "Incorrect tree"
test 0 = $(git rev-list --count merging-ever-green^2...S --) ||
die "S was rewritten"
exit 0
;;
esac
# We need the following information:
#
# - the tip of the ever-green branch (HEAD)
#
# - the base commit of the ever-green branch
#
# - tip current tip commit of the original branch
#
# - the latest commit of the original branch that made it into the ever-green branch
#
# - the commit onto which we want to rebase the ever-green branch
usage="$0: <options>
This script expects the ever-green branch to be checked out.
Options:
--ever-green-base=<commit>
Specify the base of the patch thicket of the ever-green branch (i.e. the
target (\"onto\") of the most recent rebase of the ever-green branch)
--current-tip=<commit>
Specify the tip commit of the original branch (possibly containing new
commits to rebase)
--previous-tip=<commit>
Specify the previous tip commit of the original branch, i.e. the commit
on which the current version of the ever-green branch is based
--onto=<commit>
Specify the tip of the upstream branch on which the ever-green branch
is based
--merging-rebase
Perform a merging-rebase; The ever-green branch must already be a
merging rebase
--initial
Start rebasing the ever-green branch, right after creating it from the
tip commit of the original branch
"
ever_green_base=
current_tip=
previous_tip=
onto=
merging=
initial=
while case "$1" in
--ever-green-base=*) ever_green_base="${1#*=}";;
--current=*|--current-tip=*) current_tip="${1#*=}";;
--previous=*|--previous-tip=*) previous_tip="${1#*=}";;
--onto=*) onto="${1#*=}";;
--merging|--merging-rebase) merging=t;;
--merging=*|--merging-rebase=*) merging=t; current_tip="${1#*=}";;
--no-merging-rebase) merging=;;
--initial) initial=t;;
'') break;;
*) die "Unhandled parameter: $1
$usage";;
esac; do shift; done
test -n "$onto" || die "Need onto"
if test -n "$initial"
then
test -z "$current_tip" || die "--initial and --current-tip=<commit> are incompatible"
current_tip="$(git rev-parse --verify HEAD)" ||
die "Could not parse HEAD"
if test -n "$merging" && test -z "$previous_tip"
then
previous_tip="$(git rev-list -1 --grep='^Start the merging-rebase' "$current_tip" --)" ||
die "Failed to look for a new merging-rebase"
fi
test -n "$previous_tip" ||
previous_tip="$(git merge-base -a HEAD "$onto")" ||
die "Could not find merge base between HEAD and $onto"
case "$previous_tip" in
''|*' '*) die "Could not determine unique merge base between HEAD and $onto, please use --previous-tip=<commit> to provide one";;
esac
git reset --hard "$previous_tip" ||
die "Could not rewind to $previous_tip"
test -z "$ever_green_base" || die "--initial and --ever-green-base=<commit> are incompatible"
test -n "$merging" ||
ever_green_base="$previous_tip"
fi
test -n "$current_tip" || die "Need current tip commit of the original branch"
if test -z "$merging"
then
test -n "$ever_green_base" || die "Need base commit of the ever-green branch"
test -n "$previous_tip" || die "Need previous tip commit of the original branch"
current_base=
else
test -z "$ever_green_base" || die "--merging and --ever-green-base=<commit> are incompatible"
if test -z "$initial" && test -n "$previous_tip"
then
die "--merging and --previous-tip=<commit> are incompatible"
fi
# automagically determine previous tip, ever-green base from merging-rebase's start commit
if test -z "$previous_tip"
then
previous_tip="$(git rev-list -1 --grep='^Start the merging-rebase' "..$current_tip" --)" ||
die "Failed to look for a new merging-rebase"
fi
if test -n "$previous_tip"
then
# The original branch was merging-rebased in the meantime, so we ignore any existing ever-green state
current_base="$previous_tip"
git reset --hard "$current_base" ||
die "Cannot roll back to $current_base"
ever_green_base="$(git rev-parse --verify HEAD)" ||
die "Could not determine HEAD"
else
ever_green_base="$(git rev-list -1 --grep='^Start the merging-rebase' "$current_tip.." --)" ||
die "Failed to look for previous merging-rebase"
if test -z "$ever_green_base"
then
die "Ever-green branch was not merging-rebased"
else
previous_tip="$(git rev-parse --verify "$ever_green_base"^2)" ||
die "Could not determine previous tip from $ever_green_base"
current_base="$(git cat-file commit "$ever_green_base" |
sed -n 's/^This commit starts the rebase of \([^ ]*\) to .*/\1/p')"
test -n "$current_base" ||
die "Could not determine the base commit of the original branch thicket from $ever_green_base"
fi
fi
cat >"$(git rev-parse --git-dir)/merging-rebase-message" <<-EOF
Start the merging-rebase to $onto
This commit starts the rebase of $(git rev-parse --short "$current_base") to $(git rev-parse --short "$onto")
EOF
fi
# We do not expect fixup!/squash! commits in the ever-green branch
test -z "$(git log "$ever_green_base.." | sed -n '/^ *$/{N;/\n \(fixup\|squash\)!/p}')" ||
die "Ever-green branches cannot have fixup!/squash! commits"
current_has_new_commits=
test 0 = $(git rev-list --count "$previous_tip..$current_tip" ^HEAD --) ||
current_has_new_commits=t
# Let's fall through if we have to create a merging-rebase
if test -z "$merging" && test -z "$current_has_new_commits"
then
exec git rebase -kir --autosquash --onto "$onto" "$ever_green_base"
die '`git rebase` failed to exec'
fi
not_in_ever_green="|$(git rev-list "${current_base:-HEAD}..$current_tip" -- | tr '\n' '|')"
contained_in_ever_green () {
case "$not_in_ever_green" in
*"|$1"*) return 1;; # no
*) return 0;; # yes
esac
}
is_merge () {
case "$(git show -s --format=%p "$1")" in
*" "*) return 0;; # yes
*) return 1;; # no
esac
}
is_fixup () {
case "$(git show -s --format=%s "$1")" in
"fixup!"*|"squash!"*) return 0;; # yes
*) return 1;; # no
esac
}
string2regex () {
echo "$1" | sed 's/[]\\\/[*?]/\\&/g'
}
find_commit_by_oneline () {
oneline="$(git show -s --format=%s "$1")"
regex="$(string2regex "$oneline")"
result="$(git rev-list --grep="^$regex" "$current_tip..HEAD" -- | tr '\n' ' ' | sed 's/ $//')"
case "$result" in
*' '*|'') return 1;; # multiple results found, or none
*) echo "$result"; return 0;; # found exactly one
esac
}
# range-diff does not include merge commits
if test 0 = "$(git rev-list --count "$ever_green_base.." --)"
then
commit_map=
else
commit_map="$(git range-diff -s "${current_base:-$onto}..$current_tip" "$ever_green_base.." |
sed -n 's/^[^:]*: *\([^ ]*\) [!=][^:]*: *\([^ ]*\).*/|\1=\2:/p')"
fi
map_base_commit () {
while true
do
test -n "$1" || return 0 # dummy
result="${commit_map#*|$1=}"
if test "$commit_map" != "$result"
then
echo "${result%%:*}"
return 0
fi
if contained_in_ever_green "$1"
then
echo "$1"
return 0
elif is_merge "$1"
then
# Try to find the merge commit by name
find_commit_by_oneline "$1" && return 0 ||
set -- "$(git rev-parse "$1"^)"
elif is_fixup "$1"
then
# try parent
set -- "$(git rev-parse "$1"^)"
else
# Fall back to 'onto'
echo 'onto'
return 0
fi
done
}
# This function rebases the new changes on top of the ever-green branch
pick_new_changes_onto_ever_green () {
ever_green_tip="$(git rev-parse --verify HEAD)" ||
die "Could not determine the tip of the ever-green (checked-out) branch"
echo "reset $ever_green_tip"
todo="$(make_script "$current_tip" -ki --autosquash --rebase-merges=no-rebase-cousins --onto "$ever_green_tip" "$previous_tip")" &&
to_map="$(echo "$todo" |
sed -n 's/^reset \([0-9a-f][0-9a-f]*\)\($\| .*\)/\1/p' |
sort | uniq)"
sed_args=
for commit in $to_map
do
mapped=$(map_base_commit $commit)
test -n "$mapped" ||
die "Could not map $(git show --oneline -s $commit) to anything in <ever-green>"
test -z "$mapped" ||
sed_args="$sed_args -e 's/^reset $commit/reset $mapped/'"
done
test -z "$sed_args" ||
todo="$(echo "$todo" |
eval sed "$sed_args")" ||
die "Could not edit todo via sed $sed_args"
echo "$todo"
}
replace_todo="$(git rev-parse --absolute-git-dir)/replace-todo"
if test -z "$current_has_new_commits"
then
if test 0 = $(git rev-list --count "$ever_green_tip".."$onto" --)
then
test -z "$initial" ||
git reset --hard "$current_tip" ||
die "Could not reset to $current_tip"
echo "Nothing needs to be done" >&2
exit 0
fi
# No new changes: let's rebase onto upstream right away!
echo "# Rebase the ever-green branch onto $onto" >"$replace_todo" &&
echo "reset $onto" >>"$replace_todo" &&
if test -n "$merging"
then
cat >>"$replace_todo" <<-EOF
exec git merge -s ours -m "\$(cat "\$(git rev-parse --git-dir)/merging-rebase-message")" "$current_tip"
exec git replace --graft HEAD HEAD^
exec exit 123 # force re-reading of replacement objects
EOF
fi &&
make_script HEAD -ir --autosquash --onto "$onto" "$ever_green_base" >>"$replace_todo" ||
die "Could not generate new todo list"
help="$(extract_todo_help "$replace_todo")" ||
die "Could not extract help text from $replace_todo"
if test -n "$merging"
then
echo "exec git replace --delete 'HEAD^{/^Start the merging-rebase}'" >>"$replace_todo" ||
die "Could not append exec line to reset the replace ref"
fi
else
pick_new_changes_onto_ever_green >"$replace_todo" ||
die "Could not generate todo list for $previous_tip..$current_tip"
help="$(extract_todo_help "$replace_todo")" ||
die "Could not extract todo help from $replace_todo"
if test -n "$merging" || test 0 -lt $(git rev-list --count "$ever_green_tip".."$onto" --)
then
# The second rebase's todo list can only be generated after the first one is done
cat >>"$replace_todo" <<-EOF
# Now perform the rebase onto $onto
exec "$THIS_SCRIPT" nested-rebase ${merging:+--merging="$current_tip"} -kir --autosquash --onto "$onto" "$ever_green_base"
EOF
fi
fi
cat >>"$replace_todo" <<EOF
# error on fixup!/squash! commits in the ever-green branch
exec test -z "\$(git log "$onto.." $(test -z "$merging" || echo ^HEAD^{/^Start.the.merging-rebase}) | sed -n '/^ *$/{N;/\n \(fixup\|squash\)!/p}')" || { echo "Ever-green branches cannot contain fixup!/squash! commits" >&2; exit 1; }
EOF
test -z "$help" ||
echo "$help" >>"$replace_todo" ||
die "Could not append rebase help text to $replace_todo"
# In non-interactive mode, skip editor
test -t 0 ||
export GIT_SEQUENCE_EDITOR=true
export ORIGINAL_GIT_SEQUENCE_EDITOR="$GIT_SEQUENCE_EDITOR"
test -n "$ORIGINAL_GIT_SEQUENCE_EDITOR" || {
ORIGINAL_GIT_SEQUENCE_EDITOR="$(git config sequence.editor)"
test -n "$ORIGINAL_GIT_SEQUENCE_EDITOR" ||
ORIGINAL_GIT_SEQUENCE_EDITOR="$(git var GIT_EDITOR 2>/dev/null || echo false)"
test -n "$ORIGINAL_GIT_SEQUENCE_EDITOR" ||
die "Could not determine editor"
}
export GIT_SEQUENCE_EDITOR="\"$THIS_SCRIPT\" replace-todo-script"
git rebase -kir HEAD ||
continue_rebase