forked from jwiegley/git-scripts
-
Notifications
You must be signed in to change notification settings - Fork 1
/
git-record
executable file
·260 lines (224 loc) · 6.73 KB
/
git-record
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
#!/usr/bin/env python
# coding: utf-8
# git-darcs-record, emulate "darcs record" interface on top of a git repository
#
# Usage:
# git-darcs-record first asks for any new file (previously
# untracked) to be added to the index.
# git-darcs-record then asks for each hunk to be recorded in
# the next commit. File deletion and binary blobs are supported
# git-darcs-record finally asks for a small commit message and
# executes the 'git commit' command with the newly created
# changeset in the index
# Copyright (C) 2007 Raphaël Slinckx <[email protected]>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import re, pprint, sys, os
BINARY = re.compile("GIT binary patch")
HEADER = re.compile("diff --git a/(.*) b/(.*)")
class Hunk:
def __init__(self, lines, binary):
self.diff = None
self.lines = lines
self.keep = False
self.binary = binary
def format(self):
output = self.diff.header.modified + "\n"
if self.diff.header.deleted:
output = "Removed file: " + output
if self.binary:
output = "Binary file changed: " + output
if not self.binary:
output += "\n".join(self.lines) + "\n"
return output
class Header:
def __init__(self, lines):
self.lines = lines
self.modified = None
self.deleted = False
# Extract useful info from header from git
for line in lines:
if HEADER.match(line):
match = HEADER.match(line)
self.modified = match.group(1)
if line.startswith("deleted "):
self.deleted = True
# Make sure we know what file we are modifying
assert self.modified
class Diff:
def __init__(self, header, hunks):
self.header = header
self.hunks = hunks
# Put a reference to ourselves in the hunks
for hunk in self.hunks:
hunk.diff = self
self.keep = False
def filter(self):
output = '\n'.join(self.header.lines) + "\n"
for hunk in self.hunks:
if not hunk.keep:
continue
output += '\n'.join(hunk.lines) + "\n"
return output
@classmethod
def filter_diffs(kls, diffs):
output = ""
for diff in diffs:
if not diff.keep:
continue
output += diff.filter()
return output
@classmethod
def parse(kls, lines):
in_header = True
binary = False
header = []
hunks = []
current_hunk = []
for line in lines:
if in_header and (line[0] not in (" ", "@", "\\") or line.startswith("+++ ") or line.startswith("--- ")) and not BINARY.match(line):
header.append(line)
elif BINARY.match(line):
in_header = False
binary = True
header.append(line)
elif len(line) >= 1 and line[0] == "@":
in_header = False
if current_hunk:
hunks.append(Hunk(current_hunk, binary))
current_hunk = []
current_hunk.append(line)
else:
current_hunk.append(line)
if current_hunk:
hunks.append(Hunk(current_hunk, binary))
return Diff(Header(header), hunks)
@classmethod
def split(kls, lines):
diffs = []
current_diff = []
for line in lines:
if line.startswith("diff --git "):
if current_diff:
diffs.append(current_diff)
current_diff = []
current_diff.append(line)
else:
current_diff.append(line)
if current_diff:
diffs.append(current_diff)
return [Diff.parse(lines) for lines in diffs]
def read_answer(question, allowed_responses=["Y", "n", "d", "a"]):
#Make sure there is alway a default selection
assert [r for r in allowed_responses if r.isupper()]
while True:
resp = raw_input("%s [%s] : " % (question, "".join(allowed_responses)))
if resp in [r.lower() for r in allowed_responses]:
break
elif resp == "":
resp = [r for r in allowed_responses if r.isupper()][0].lower()
break
print 'Unexpected answer: %r' % resp
return resp
def setup_git_dir():
global GIT_DIR
GIT_DIR = os.getcwd()
while not os.path.exists(os.path.join(GIT_DIR, ".git")):
GIT_DIR = os.path.dirname(GIT_DIR)
if GIT_DIR == "/":
return False
os.chdir(GIT_DIR)
return True
def git_get_untracked_files():
return [f.strip() for f in os.popen("git ls-files --others --exclude-from='%s' --exclude-per-directory=.gitignore" % (os.path.join(GIT_DIR, ".git", "info", "exclude"))).readlines()]
def git_track_file(f):
os.spawnvp(os.P_WAIT, "git", ["git", "add", f])
def git_diff():
return os.popen("git diff -u --no-color --binary").readlines()
def git_apply(patch):
stdin, stdout = os.popen2(["git", "apply", "--cached", "-"])
stdin.write(patch)
stdin.close()
output = stdout.read()
stdout.close()
os.wait()
return output
def git_status():
os.spawnvp(os.P_WAIT, "git", ["git", "status"])
def git_commit(msg):
os.spawnvp(os.P_WAIT, "git", ["git", "commit", "-m", patch_name])
# Main loop ------------------------
if not setup_git_dir():
print "Must be in a git (sub-)directory! Exiting..."
sys.exit()
# Ask for new files ----------------
git_untracked_files = git_get_untracked_files()
git_track_files = []
all = False
done = False
for i, f in enumerate(git_untracked_files):
if not all:
print "Add file: ", f
resp = read_answer("Shall I add this file? (%d/%d)" % (i+1, len(git_untracked_files)))
else:
resp = "y"
if resp == "y":
git_track_files.append(f)
elif resp == "a":
git_track_files.append(f)
all = True
elif resp == "d":
done = True
break
# Ask for each hunk of the diff
diffs = Diff.split([line[:-1] for line in git_diff()])
total_hunks = sum([len(diff.hunks) for diff in diffs])
n_hunk = 1
for diff in diffs:
if done:
break
for hunk in diff.hunks:
# Check if we are in override mode
if not all:
print
print hunk.format()
resp = read_answer('Shall I record this change? (%d/%d)' % (n_hunk, total_hunks))
# Otherwise say 'y' to all remaining patches
else:
resp = "y"
if resp == "y":
diff.keep = True
hunk.keep = True
elif resp == "a":
diff.keep = True
hunk.keep = True
all = True
elif resp == "d":
done = True
break
n_hunk += 1
# Add new files to track
for f in git_track_files:
git_track_file(f)
# Generate a new patch to be used with git apply
new_patch = Diff.filter_diffs(diffs)
if new_patch:
print git_apply(new_patch)
if new_patch or git_track_files:
git_status()
patch_name = raw_input("What is the patch name? ")
git_commit(patch_name)
else:
print "Ok, if you don't want to record anything, that's fine!"