##// END OF EJS Templates
record: change wording of initial per-file prompt
Bryan O'Sullivan -
r5285:3ef19023 default
parent child Browse files
Show More
@@ -1,415 +1,415 b''
1 1 # record.py
2 2 #
3 3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of
6 6 # the GNU General Public License, incorporated herein by reference.
7 7
8 8 '''interactive change selection during commit'''
9 9
10 10 from mercurial.i18n import _
11 11 from mercurial import cmdutil, commands, cmdutil, hg, mdiff, patch, revlog
12 12 from mercurial import util
13 13 import copy, cStringIO, errno, operator, os, re, shutil, tempfile
14 14
15 15 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
16 16
17 17 def scanpatch(fp):
18 18 lr = patch.linereader(fp)
19 19
20 20 def scanwhile(first, p):
21 21 lines = [first]
22 22 while True:
23 23 line = lr.readline()
24 24 if not line:
25 25 break
26 26 if p(line):
27 27 lines.append(line)
28 28 else:
29 29 lr.push(line)
30 30 break
31 31 return lines
32 32
33 33 while True:
34 34 line = lr.readline()
35 35 if not line:
36 36 break
37 37 if line.startswith('diff --git a/'):
38 38 def notheader(line):
39 39 s = line.split(None, 1)
40 40 return not s or s[0] not in ('---', 'diff')
41 41 header = scanwhile(line, notheader)
42 42 fromfile = lr.readline()
43 43 if fromfile.startswith('---'):
44 44 tofile = lr.readline()
45 45 header += [fromfile, tofile]
46 46 else:
47 47 lr.push(fromfile)
48 48 yield 'file', header
49 49 elif line[0] == ' ':
50 50 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
51 51 elif line[0] in '-+':
52 52 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
53 53 else:
54 54 m = lines_re.match(line)
55 55 if m:
56 56 yield 'range', m.groups()
57 57 else:
58 58 raise patch.PatchError('unknown patch content: %r' % line)
59 59
60 60 class header(object):
61 61 diff_re = re.compile('diff --git a/(.*) b/(.*)$')
62 62 allhunks_re = re.compile('(?:index|new file|deleted file) ')
63 63 pretty_re = re.compile('(?:new file|deleted file) ')
64 64 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
65 65
66 66 def __init__(self, header):
67 67 self.header = header
68 68 self.hunks = []
69 69
70 70 def binary(self):
71 71 for h in self.header:
72 72 if h.startswith('index '):
73 73 return True
74 74
75 75 def pretty(self, fp):
76 76 for h in self.header:
77 77 if h.startswith('index '):
78 78 fp.write(_('this modifies a binary file (all or nothing)\n'))
79 79 break
80 80 if self.pretty_re.match(h):
81 81 fp.write(h)
82 82 if self.binary():
83 83 fp.write(_('this is a binary file\n'))
84 84 break
85 85 if h.startswith('---'):
86 86 fp.write(_('%d hunks, %d lines changed\n') %
87 87 (len(self.hunks),
88 88 sum([h.added + h.removed for h in self.hunks])))
89 89 break
90 90 fp.write(h)
91 91
92 92 def write(self, fp):
93 93 fp.write(''.join(self.header))
94 94
95 95 def allhunks(self):
96 96 for h in self.header:
97 97 if self.allhunks_re.match(h):
98 98 return True
99 99
100 100 def files(self):
101 101 fromfile, tofile = self.diff_re.match(self.header[0]).groups()
102 102 if fromfile == tofile:
103 103 return [fromfile]
104 104 return [fromfile, tofile]
105 105
106 106 def filename(self):
107 107 return self.files()[-1]
108 108
109 109 def __repr__(self):
110 110 return '<header %s>' % (' '.join(map(repr, self.files())))
111 111
112 112 def special(self):
113 113 for h in self.header:
114 114 if self.special_re.match(h):
115 115 return True
116 116
117 117 def countchanges(hunk):
118 118 add = len([h for h in hunk if h[0] == '+'])
119 119 rem = len([h for h in hunk if h[0] == '-'])
120 120 return add, rem
121 121
122 122 class hunk(object):
123 123 maxcontext = 3
124 124
125 125 def __init__(self, header, fromline, toline, proc, before, hunk, after):
126 126 def trimcontext(number, lines):
127 127 delta = len(lines) - self.maxcontext
128 128 if False and delta > 0:
129 129 return number + delta, lines[:self.maxcontext]
130 130 return number, lines
131 131
132 132 self.header = header
133 133 self.fromline, self.before = trimcontext(fromline, before)
134 134 self.toline, self.after = trimcontext(toline, after)
135 135 self.proc = proc
136 136 self.hunk = hunk
137 137 self.added, self.removed = countchanges(self.hunk)
138 138
139 139 def write(self, fp):
140 140 delta = len(self.before) + len(self.after)
141 141 fromlen = delta + self.removed
142 142 tolen = delta + self.added
143 143 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
144 144 (self.fromline, fromlen, self.toline, tolen,
145 145 self.proc and (' ' + self.proc)))
146 146 fp.write(''.join(self.before + self.hunk + self.after))
147 147
148 148 pretty = write
149 149
150 150 def filename(self):
151 151 return self.header.filename()
152 152
153 153 def __repr__(self):
154 154 return '<hunk %r@%d>' % (self.filename(), self.fromline)
155 155
156 156 def parsepatch(fp):
157 157 class parser(object):
158 158 def __init__(self):
159 159 self.fromline = 0
160 160 self.toline = 0
161 161 self.proc = ''
162 162 self.header = None
163 163 self.context = []
164 164 self.before = []
165 165 self.hunk = []
166 166 self.stream = []
167 167
168 168 def addrange(self, (fromstart, fromend, tostart, toend, proc)):
169 169 self.fromline = int(fromstart)
170 170 self.toline = int(tostart)
171 171 self.proc = proc
172 172
173 173 def addcontext(self, context):
174 174 if self.hunk:
175 175 h = hunk(self.header, self.fromline, self.toline, self.proc,
176 176 self.before, self.hunk, context)
177 177 self.header.hunks.append(h)
178 178 self.stream.append(h)
179 179 self.fromline += len(self.before) + h.removed
180 180 self.toline += len(self.before) + h.added
181 181 self.before = []
182 182 self.hunk = []
183 183 self.proc = ''
184 184 self.context = context
185 185
186 186 def addhunk(self, hunk):
187 187 if self.context:
188 188 self.before = self.context
189 189 self.context = []
190 190 self.hunk = data
191 191
192 192 def newfile(self, hdr):
193 193 self.addcontext([])
194 194 h = header(hdr)
195 195 self.stream.append(h)
196 196 self.header = h
197 197
198 198 def finished(self):
199 199 self.addcontext([])
200 200 return self.stream
201 201
202 202 transitions = {
203 203 'file': {'context': addcontext,
204 204 'file': newfile,
205 205 'hunk': addhunk,
206 206 'range': addrange},
207 207 'context': {'file': newfile,
208 208 'hunk': addhunk,
209 209 'range': addrange},
210 210 'hunk': {'context': addcontext,
211 211 'file': newfile,
212 212 'range': addrange},
213 213 'range': {'context': addcontext,
214 214 'hunk': addhunk},
215 215 }
216 216
217 217 p = parser()
218 218
219 219 state = 'context'
220 220 for newstate, data in scanpatch(fp):
221 221 try:
222 222 p.transitions[state][newstate](p, data)
223 223 except KeyError:
224 224 raise patch.PatchError('unhandled transition: %s -> %s' %
225 225 (state, newstate))
226 226 state = newstate
227 227 return p.finished()
228 228
229 229 def filterpatch(ui, chunks):
230 230 chunks = list(chunks)
231 231 chunks.reverse()
232 232 seen = {}
233 233 def consumefile():
234 234 consumed = []
235 235 while chunks:
236 236 if isinstance(chunks[-1], header):
237 237 break
238 238 else:
239 239 consumed.append(chunks.pop())
240 240 return consumed
241 241 resp_all = [None]
242 242 resp_file = [None]
243 243 applied = {}
244 244 def prompt(query):
245 245 if resp_all[0] is not None:
246 246 return resp_all[0]
247 247 if resp_file[0] is not None:
248 248 return resp_file[0]
249 249 while True:
250 250 r = (ui.prompt(query + _(' [Ynsfdaq?] '), '[Ynsfdaq?]?$',
251 251 matchflags=re.I) or 'y').lower()
252 252 if r == '?':
253 253 c = record.__doc__.find('y - record this change')
254 254 for l in record.__doc__[c:].splitlines():
255 255 if l: ui.write(_(l.strip()), '\n')
256 256 continue
257 257 elif r == 's':
258 258 r = resp_file[0] = 'n'
259 259 elif r == 'f':
260 260 r = resp_file[0] = 'y'
261 261 elif r == 'd':
262 262 r = resp_all[0] = 'n'
263 263 elif r == 'a':
264 264 r = resp_all[0] = 'y'
265 265 elif r == 'q':
266 266 raise util.Abort(_('user quit'))
267 267 return r
268 268 while chunks:
269 269 chunk = chunks.pop()
270 270 if isinstance(chunk, header):
271 271 resp_file = [None]
272 272 fixoffset = 0
273 273 hdr = ''.join(chunk.header)
274 274 if hdr in seen:
275 275 consumefile()
276 276 continue
277 277 seen[hdr] = True
278 278 if resp_all[0] is None:
279 279 chunk.pretty(ui)
280 r = prompt(_('record changes to %s?') %
280 r = prompt(_('examine changes to %s?') %
281 281 _(' and ').join(map(repr, chunk.files())))
282 282 if r == 'y':
283 283 applied[chunk.filename()] = [chunk]
284 284 if chunk.allhunks():
285 285 applied[chunk.filename()] += consumefile()
286 286 else:
287 287 consumefile()
288 288 else:
289 289 if resp_file[0] is None and resp_all[0] is None:
290 290 chunk.pretty(ui)
291 291 r = prompt(_('record this change to %r?') %
292 292 chunk.filename())
293 293 if r == 'y':
294 294 if fixoffset:
295 295 chunk = copy.copy(chunk)
296 296 chunk.toline += fixoffset
297 297 applied[chunk.filename()].append(chunk)
298 298 else:
299 299 fixoffset += chunk.removed - chunk.added
300 300 return reduce(operator.add, [h for h in applied.itervalues()
301 301 if h[0].special() or len(h) > 1], [])
302 302
303 303 def record(ui, repo, *pats, **opts):
304 304 '''interactively select changes to commit
305 305
306 306 If a list of files is omitted, all changes reported by "hg status"
307 307 will be candidates for recording.
308 308
309 309 You will be prompted for whether to record changes to each
310 310 modified file, and for files with multiple changes, for each
311 311 change to use. For each query, the following responses are
312 312 possible:
313 313
314 314 y - record this change
315 315 n - skip this change
316 316
317 317 s - skip remaining changes to this file
318 318 f - record remaining changes to this file
319 319
320 320 d - done, skip remaining changes and files
321 321 a - record all changes to all remaining files
322 322 q - quit, recording no changes
323 323
324 324 ? - display help'''
325 325
326 326 if not ui.interactive:
327 327 raise util.Abort(_('running non-interactively, use commit instead'))
328 328
329 329 def recordfunc(ui, repo, files, message, match, opts):
330 330 if files:
331 331 changes = None
332 332 else:
333 333 changes = repo.status(files=files, match=match)[:5]
334 334 modified, added, removed = changes[:3]
335 335 files = modified + added + removed
336 336 diffopts = mdiff.diffopts(git=True, nodates=True)
337 337 fp = cStringIO.StringIO()
338 338 patch.diff(repo, repo.dirstate.parents()[0], files=files,
339 339 match=match, changes=changes, opts=diffopts, fp=fp)
340 340 fp.seek(0)
341 341
342 342 chunks = filterpatch(ui, parsepatch(fp))
343 343 del fp
344 344
345 345 contenders = {}
346 346 for h in chunks:
347 347 try: contenders.update(dict.fromkeys(h.files()))
348 348 except AttributeError: pass
349 349
350 350 newfiles = [f for f in files if f in contenders]
351 351
352 352 if not newfiles:
353 353 ui.status(_('no changes to record\n'))
354 354 return 0
355 355
356 356 if changes is None:
357 357 changes = repo.status(files=newfiles, match=match)[:5]
358 358 modified = dict.fromkeys(changes[0])
359 359
360 360 backups = {}
361 361 backupdir = repo.join('record-backups')
362 362 try:
363 363 os.mkdir(backupdir)
364 364 except OSError, err:
365 365 if err.errno != errno.EEXIST:
366 366 raise
367 367 try:
368 368 for f in newfiles:
369 369 if f not in modified:
370 370 continue
371 371 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
372 372 dir=backupdir)
373 373 os.close(fd)
374 374 ui.debug('backup %r as %r\n' % (f, tmpname))
375 375 util.copyfile(repo.wjoin(f), tmpname)
376 376 backups[f] = tmpname
377 377
378 378 fp = cStringIO.StringIO()
379 379 for c in chunks:
380 380 if c.filename() in backups:
381 381 c.write(fp)
382 382 dopatch = fp.tell()
383 383 fp.seek(0)
384 384
385 385 if backups:
386 386 hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
387 387
388 388 if dopatch:
389 389 ui.debug('applying patch\n')
390 390 ui.debug(fp.getvalue())
391 391 patch.internalpatch(fp, ui, 1, repo.root)
392 392 del fp
393 393
394 394 repo.commit(newfiles, message, opts['user'], opts['date'], match,
395 395 force_editor=opts.get('force_editor'))
396 396 return 0
397 397 finally:
398 398 try:
399 399 for realname, tmpname in backups.iteritems():
400 400 ui.debug('restoring %r to %r\n' % (tmpname, realname))
401 401 util.copyfile(tmpname, repo.wjoin(realname))
402 402 os.unlink(tmpname)
403 403 os.rmdir(backupdir)
404 404 except OSError:
405 405 pass
406 406 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
407 407
408 408 cmdtable = {
409 409 "record":
410 410 (record,
411 411 [('A', 'addremove', None,
412 412 _('mark new/missing files as added/removed before committing')),
413 413 ] + commands.walkopts + commands.commitopts + commands.commitopts2,
414 414 _('hg record [OPTION]... [FILE]...')),
415 415 }
General Comments 0
You need to be logged in to leave comments. Login now