-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathgit-rebase-auto-sink
executable file
·131 lines (99 loc) · 3.72 KB
/
git-rebase-auto-sink
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
#!/usr/bin/env python3
"""
The purpose of this script is to help doing an automatic rebase 'fixup' for a
set of commits, where for each fixup commit, we don't know to which commit
we would like to squash it.
This is similar to the problem that 'git-absorb' [1] is trying to solve.
Algoritm:
* Take two commit refs from the user:
[base] the start of the rebase
[sink_start] the most recent commit that is not part of the fixup group
* Rebase the list commits in `[sink_start]..` in reverse to make sure that
each commit in the list is independent.
* For each commit in `[sink_start]..`, do a binary search using 'git rebase'
over `[base]..[sink_start]` to find the farther in the history in which it
can be laid.
A much more comprehensive work is done in [2].
[1] https://github.com/tummychow/git-absorb
[2] https://github.com/aspiers/git-deps
"""
import sys
import os
import tempfile
import re
from optparse import OptionParser
import itertools
import tempfile
def system(cmd):
return os.system(cmd)
class Abort(Exception): pass
def abort(msg):
print(msg, file=sys.stderr)
sys.exit(-1)
def try_rebase(base, lst):
tf = tempfile.NamedTemporaryFile()
tf.write("\n".join(lst).encode('utf-8'))
editor_cmd = f"{sys.argv[0]} from-rebase {tf.name}"
cmd = f"git -c core.editor='{editor_cmd}' rebase -i {base}"
return system(cmd)
def rebase_abort():
r = system("git rebase --abort")
if r != 0:
raise Abort("git rebase --abort failed")
def from_rebase(lst, rebase_script):
f = open(rebase_script, "w")
for githash in lst:
f.write(f"pick {githash}\n")
f.close()
def main():
if sys.argv[1] == "from-rebase":
lst = open(sys.argv[2]).read().split('\n')
from_rebase(lst, sys.argv[3])
sys.exit(0)
parser = OptionParser()
parser.add_option("-b", "--base", dest="base")
parser.add_option("-s", "--sink-start", dest="sink")
(options, _args) = parser.parse_args()
if not options.base and not options.sink:
abort("missing parameters")
patchset = []
for githash in os.popen(f"git log --pretty='%H' --reverse {options.sink}.."):
patchset.append(githash.strip())
revlist = []
for githash in os.popen(f"git log --pretty='%H' --reverse {options.base}..{options.sink}"):
revlist.append(githash.strip())
if len(patchset) >= 10:
abort(f"too many patches {len(patchset)}")
print("Checking a reversed list of {len(patchset)} patches")
patchset.reverse()
res = try_rebase(options.base, revlist + patchset)
if res != 0:
sys.exit(-1)
for patch_idx, patch in enumerate(patchset):
print(f"Trying to fit {patch} into history [{patch_idx + 1}/{len(patchset)}]")
start = 0
end = len(revlist)
best_save = None
best_save_reflist = None
while start <= end:
middle = int((start + end) / 2)
print(f" At {middle} for [{start}, {end}]")
try_revlist = revlist[:middle] + [patch] + revlist[middle:]
prev_version = os.popen("git rev-parse HEAD").read().strip()
res = try_rebase(options.base, try_revlist)
if res != 0:
start = middle + 1
rebase_abort()
else:
best_save = os.popen("git rev-parse HEAD").read().strip()
best_save_reflist = try_revlist
if end == start:
break
end = middle - 1
os.system(f"git reset --hard {prev_version}")
if best_save is not None:
os.system(f"git reset --hard {best_save}")
revlist = best_save_reflist
print("Done")
if __name__ == "__main__":
main()