##// END OF EJS Templates
record: add an option to backup all wc modifications...
Idan Kamara -
r14425:e8953450 default
parent child Browse files
Show More
@@ -1,553 +1,557 b''
1 # record.py
1 # record.py
2 #
2 #
3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''commands to interactively select changes for commit/qrefresh'''
8 '''commands to interactively select changes for commit/qrefresh'''
9
9
10 from mercurial.i18n import gettext, _
10 from mercurial.i18n import gettext, _
11 from mercurial import cmdutil, commands, extensions, hg, mdiff, patch
11 from mercurial import cmdutil, commands, extensions, hg, mdiff, patch
12 from mercurial import util
12 from mercurial import util
13 import copy, cStringIO, errno, os, re, shutil, tempfile
13 import copy, cStringIO, errno, os, re, shutil, tempfile
14
14
15 cmdtable = {}
15 cmdtable = {}
16 command = cmdutil.command(cmdtable)
16 command = cmdutil.command(cmdtable)
17
17
18 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
18 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
19
19
20 def scanpatch(fp):
20 def scanpatch(fp):
21 """like patch.iterhunks, but yield different events
21 """like patch.iterhunks, but yield different events
22
22
23 - ('file', [header_lines + fromfile + tofile])
23 - ('file', [header_lines + fromfile + tofile])
24 - ('context', [context_lines])
24 - ('context', [context_lines])
25 - ('hunk', [hunk_lines])
25 - ('hunk', [hunk_lines])
26 - ('range', (-start,len, +start,len, diffp))
26 - ('range', (-start,len, +start,len, diffp))
27 """
27 """
28 lr = patch.linereader(fp)
28 lr = patch.linereader(fp)
29
29
30 def scanwhile(first, p):
30 def scanwhile(first, p):
31 """scan lr while predicate holds"""
31 """scan lr while predicate holds"""
32 lines = [first]
32 lines = [first]
33 while True:
33 while True:
34 line = lr.readline()
34 line = lr.readline()
35 if not line:
35 if not line:
36 break
36 break
37 if p(line):
37 if p(line):
38 lines.append(line)
38 lines.append(line)
39 else:
39 else:
40 lr.push(line)
40 lr.push(line)
41 break
41 break
42 return lines
42 return lines
43
43
44 while True:
44 while True:
45 line = lr.readline()
45 line = lr.readline()
46 if not line:
46 if not line:
47 break
47 break
48 if line.startswith('diff --git a/') or line.startswith('diff -r '):
48 if line.startswith('diff --git a/') or line.startswith('diff -r '):
49 def notheader(line):
49 def notheader(line):
50 s = line.split(None, 1)
50 s = line.split(None, 1)
51 return not s or s[0] not in ('---', 'diff')
51 return not s or s[0] not in ('---', 'diff')
52 header = scanwhile(line, notheader)
52 header = scanwhile(line, notheader)
53 fromfile = lr.readline()
53 fromfile = lr.readline()
54 if fromfile.startswith('---'):
54 if fromfile.startswith('---'):
55 tofile = lr.readline()
55 tofile = lr.readline()
56 header += [fromfile, tofile]
56 header += [fromfile, tofile]
57 else:
57 else:
58 lr.push(fromfile)
58 lr.push(fromfile)
59 yield 'file', header
59 yield 'file', header
60 elif line[0] == ' ':
60 elif line[0] == ' ':
61 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
61 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
62 elif line[0] in '-+':
62 elif line[0] in '-+':
63 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
63 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
64 else:
64 else:
65 m = lines_re.match(line)
65 m = lines_re.match(line)
66 if m:
66 if m:
67 yield 'range', m.groups()
67 yield 'range', m.groups()
68 else:
68 else:
69 raise patch.PatchError('unknown patch content: %r' % line)
69 raise patch.PatchError('unknown patch content: %r' % line)
70
70
71 class header(object):
71 class header(object):
72 """patch header
72 """patch header
73
73
74 XXX shoudn't we move this to mercurial/patch.py ?
74 XXX shoudn't we move this to mercurial/patch.py ?
75 """
75 """
76 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
76 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
77 diff_re = re.compile('diff -r .* (.*)$')
77 diff_re = re.compile('diff -r .* (.*)$')
78 allhunks_re = re.compile('(?:index|new file|deleted file) ')
78 allhunks_re = re.compile('(?:index|new file|deleted file) ')
79 pretty_re = re.compile('(?:new file|deleted file) ')
79 pretty_re = re.compile('(?:new file|deleted file) ')
80 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
80 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
81
81
82 def __init__(self, header):
82 def __init__(self, header):
83 self.header = header
83 self.header = header
84 self.hunks = []
84 self.hunks = []
85
85
86 def binary(self):
86 def binary(self):
87 return util.any(h.startswith('index ') for h in self.header)
87 return util.any(h.startswith('index ') for h in self.header)
88
88
89 def pretty(self, fp):
89 def pretty(self, fp):
90 for h in self.header:
90 for h in self.header:
91 if h.startswith('index '):
91 if h.startswith('index '):
92 fp.write(_('this modifies a binary file (all or nothing)\n'))
92 fp.write(_('this modifies a binary file (all or nothing)\n'))
93 break
93 break
94 if self.pretty_re.match(h):
94 if self.pretty_re.match(h):
95 fp.write(h)
95 fp.write(h)
96 if self.binary():
96 if self.binary():
97 fp.write(_('this is a binary file\n'))
97 fp.write(_('this is a binary file\n'))
98 break
98 break
99 if h.startswith('---'):
99 if h.startswith('---'):
100 fp.write(_('%d hunks, %d lines changed\n') %
100 fp.write(_('%d hunks, %d lines changed\n') %
101 (len(self.hunks),
101 (len(self.hunks),
102 sum([max(h.added, h.removed) for h in self.hunks])))
102 sum([max(h.added, h.removed) for h in self.hunks])))
103 break
103 break
104 fp.write(h)
104 fp.write(h)
105
105
106 def write(self, fp):
106 def write(self, fp):
107 fp.write(''.join(self.header))
107 fp.write(''.join(self.header))
108
108
109 def allhunks(self):
109 def allhunks(self):
110 return util.any(self.allhunks_re.match(h) for h in self.header)
110 return util.any(self.allhunks_re.match(h) for h in self.header)
111
111
112 def files(self):
112 def files(self):
113 match = self.diffgit_re.match(self.header[0])
113 match = self.diffgit_re.match(self.header[0])
114 if match:
114 if match:
115 fromfile, tofile = match.groups()
115 fromfile, tofile = match.groups()
116 if fromfile == tofile:
116 if fromfile == tofile:
117 return [fromfile]
117 return [fromfile]
118 return [fromfile, tofile]
118 return [fromfile, tofile]
119 else:
119 else:
120 return self.diff_re.match(self.header[0]).groups()
120 return self.diff_re.match(self.header[0]).groups()
121
121
122 def filename(self):
122 def filename(self):
123 return self.files()[-1]
123 return self.files()[-1]
124
124
125 def __repr__(self):
125 def __repr__(self):
126 return '<header %s>' % (' '.join(map(repr, self.files())))
126 return '<header %s>' % (' '.join(map(repr, self.files())))
127
127
128 def special(self):
128 def special(self):
129 return util.any(self.special_re.match(h) for h in self.header)
129 return util.any(self.special_re.match(h) for h in self.header)
130
130
131 def countchanges(hunk):
131 def countchanges(hunk):
132 """hunk -> (n+,n-)"""
132 """hunk -> (n+,n-)"""
133 add = len([h for h in hunk if h[0] == '+'])
133 add = len([h for h in hunk if h[0] == '+'])
134 rem = len([h for h in hunk if h[0] == '-'])
134 rem = len([h for h in hunk if h[0] == '-'])
135 return add, rem
135 return add, rem
136
136
137 class hunk(object):
137 class hunk(object):
138 """patch hunk
138 """patch hunk
139
139
140 XXX shouldn't we merge this with patch.hunk ?
140 XXX shouldn't we merge this with patch.hunk ?
141 """
141 """
142 maxcontext = 3
142 maxcontext = 3
143
143
144 def __init__(self, header, fromline, toline, proc, before, hunk, after):
144 def __init__(self, header, fromline, toline, proc, before, hunk, after):
145 def trimcontext(number, lines):
145 def trimcontext(number, lines):
146 delta = len(lines) - self.maxcontext
146 delta = len(lines) - self.maxcontext
147 if False and delta > 0:
147 if False and delta > 0:
148 return number + delta, lines[:self.maxcontext]
148 return number + delta, lines[:self.maxcontext]
149 return number, lines
149 return number, lines
150
150
151 self.header = header
151 self.header = header
152 self.fromline, self.before = trimcontext(fromline, before)
152 self.fromline, self.before = trimcontext(fromline, before)
153 self.toline, self.after = trimcontext(toline, after)
153 self.toline, self.after = trimcontext(toline, after)
154 self.proc = proc
154 self.proc = proc
155 self.hunk = hunk
155 self.hunk = hunk
156 self.added, self.removed = countchanges(self.hunk)
156 self.added, self.removed = countchanges(self.hunk)
157
157
158 def write(self, fp):
158 def write(self, fp):
159 delta = len(self.before) + len(self.after)
159 delta = len(self.before) + len(self.after)
160 if self.after and self.after[-1] == '\\ No newline at end of file\n':
160 if self.after and self.after[-1] == '\\ No newline at end of file\n':
161 delta -= 1
161 delta -= 1
162 fromlen = delta + self.removed
162 fromlen = delta + self.removed
163 tolen = delta + self.added
163 tolen = delta + self.added
164 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
164 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
165 (self.fromline, fromlen, self.toline, tolen,
165 (self.fromline, fromlen, self.toline, tolen,
166 self.proc and (' ' + self.proc)))
166 self.proc and (' ' + self.proc)))
167 fp.write(''.join(self.before + self.hunk + self.after))
167 fp.write(''.join(self.before + self.hunk + self.after))
168
168
169 pretty = write
169 pretty = write
170
170
171 def filename(self):
171 def filename(self):
172 return self.header.filename()
172 return self.header.filename()
173
173
174 def __repr__(self):
174 def __repr__(self):
175 return '<hunk %r@%d>' % (self.filename(), self.fromline)
175 return '<hunk %r@%d>' % (self.filename(), self.fromline)
176
176
177 def parsepatch(fp):
177 def parsepatch(fp):
178 """patch -> [] of headers -> [] of hunks """
178 """patch -> [] of headers -> [] of hunks """
179 class parser(object):
179 class parser(object):
180 """patch parsing state machine"""
180 """patch parsing state machine"""
181 def __init__(self):
181 def __init__(self):
182 self.fromline = 0
182 self.fromline = 0
183 self.toline = 0
183 self.toline = 0
184 self.proc = ''
184 self.proc = ''
185 self.header = None
185 self.header = None
186 self.context = []
186 self.context = []
187 self.before = []
187 self.before = []
188 self.hunk = []
188 self.hunk = []
189 self.headers = []
189 self.headers = []
190
190
191 def addrange(self, limits):
191 def addrange(self, limits):
192 fromstart, fromend, tostart, toend, proc = limits
192 fromstart, fromend, tostart, toend, proc = limits
193 self.fromline = int(fromstart)
193 self.fromline = int(fromstart)
194 self.toline = int(tostart)
194 self.toline = int(tostart)
195 self.proc = proc
195 self.proc = proc
196
196
197 def addcontext(self, context):
197 def addcontext(self, context):
198 if self.hunk:
198 if self.hunk:
199 h = hunk(self.header, self.fromline, self.toline, self.proc,
199 h = hunk(self.header, self.fromline, self.toline, self.proc,
200 self.before, self.hunk, context)
200 self.before, self.hunk, context)
201 self.header.hunks.append(h)
201 self.header.hunks.append(h)
202 self.fromline += len(self.before) + h.removed
202 self.fromline += len(self.before) + h.removed
203 self.toline += len(self.before) + h.added
203 self.toline += len(self.before) + h.added
204 self.before = []
204 self.before = []
205 self.hunk = []
205 self.hunk = []
206 self.proc = ''
206 self.proc = ''
207 self.context = context
207 self.context = context
208
208
209 def addhunk(self, hunk):
209 def addhunk(self, hunk):
210 if self.context:
210 if self.context:
211 self.before = self.context
211 self.before = self.context
212 self.context = []
212 self.context = []
213 self.hunk = hunk
213 self.hunk = hunk
214
214
215 def newfile(self, hdr):
215 def newfile(self, hdr):
216 self.addcontext([])
216 self.addcontext([])
217 h = header(hdr)
217 h = header(hdr)
218 self.headers.append(h)
218 self.headers.append(h)
219 self.header = h
219 self.header = h
220
220
221 def finished(self):
221 def finished(self):
222 self.addcontext([])
222 self.addcontext([])
223 return self.headers
223 return self.headers
224
224
225 transitions = {
225 transitions = {
226 'file': {'context': addcontext,
226 'file': {'context': addcontext,
227 'file': newfile,
227 'file': newfile,
228 'hunk': addhunk,
228 'hunk': addhunk,
229 'range': addrange},
229 'range': addrange},
230 'context': {'file': newfile,
230 'context': {'file': newfile,
231 'hunk': addhunk,
231 'hunk': addhunk,
232 'range': addrange},
232 'range': addrange},
233 'hunk': {'context': addcontext,
233 'hunk': {'context': addcontext,
234 'file': newfile,
234 'file': newfile,
235 'range': addrange},
235 'range': addrange},
236 'range': {'context': addcontext,
236 'range': {'context': addcontext,
237 'hunk': addhunk},
237 'hunk': addhunk},
238 }
238 }
239
239
240 p = parser()
240 p = parser()
241
241
242 state = 'context'
242 state = 'context'
243 for newstate, data in scanpatch(fp):
243 for newstate, data in scanpatch(fp):
244 try:
244 try:
245 p.transitions[state][newstate](p, data)
245 p.transitions[state][newstate](p, data)
246 except KeyError:
246 except KeyError:
247 raise patch.PatchError('unhandled transition: %s -> %s' %
247 raise patch.PatchError('unhandled transition: %s -> %s' %
248 (state, newstate))
248 (state, newstate))
249 state = newstate
249 state = newstate
250 return p.finished()
250 return p.finished()
251
251
252 def filterpatch(ui, headers):
252 def filterpatch(ui, headers):
253 """Interactively filter patch chunks into applied-only chunks"""
253 """Interactively filter patch chunks into applied-only chunks"""
254
254
255 def prompt(skipfile, skipall, query):
255 def prompt(skipfile, skipall, query):
256 """prompt query, and process base inputs
256 """prompt query, and process base inputs
257
257
258 - y/n for the rest of file
258 - y/n for the rest of file
259 - y/n for the rest
259 - y/n for the rest
260 - ? (help)
260 - ? (help)
261 - q (quit)
261 - q (quit)
262
262
263 Return True/False and possibly updated skipfile and skipall.
263 Return True/False and possibly updated skipfile and skipall.
264 """
264 """
265 if skipall is not None:
265 if skipall is not None:
266 return skipall, skipfile, skipall
266 return skipall, skipfile, skipall
267 if skipfile is not None:
267 if skipfile is not None:
268 return skipfile, skipfile, skipall
268 return skipfile, skipfile, skipall
269 while True:
269 while True:
270 resps = _('[Ynsfdaq?]')
270 resps = _('[Ynsfdaq?]')
271 choices = (_('&Yes, record this change'),
271 choices = (_('&Yes, record this change'),
272 _('&No, skip this change'),
272 _('&No, skip this change'),
273 _('&Skip remaining changes to this file'),
273 _('&Skip remaining changes to this file'),
274 _('Record remaining changes to this &file'),
274 _('Record remaining changes to this &file'),
275 _('&Done, skip remaining changes and files'),
275 _('&Done, skip remaining changes and files'),
276 _('Record &all changes to all remaining files'),
276 _('Record &all changes to all remaining files'),
277 _('&Quit, recording no changes'),
277 _('&Quit, recording no changes'),
278 _('&?'))
278 _('&?'))
279 r = ui.promptchoice("%s %s" % (query, resps), choices)
279 r = ui.promptchoice("%s %s" % (query, resps), choices)
280 ui.write("\n")
280 ui.write("\n")
281 if r == 7: # ?
281 if r == 7: # ?
282 doc = gettext(record.__doc__)
282 doc = gettext(record.__doc__)
283 c = doc.find('::') + 2
283 c = doc.find('::') + 2
284 for l in doc[c:].splitlines():
284 for l in doc[c:].splitlines():
285 if l.startswith(' '):
285 if l.startswith(' '):
286 ui.write(l.strip(), '\n')
286 ui.write(l.strip(), '\n')
287 continue
287 continue
288 elif r == 0: # yes
288 elif r == 0: # yes
289 ret = True
289 ret = True
290 elif r == 1: # no
290 elif r == 1: # no
291 ret = False
291 ret = False
292 elif r == 2: # Skip
292 elif r == 2: # Skip
293 ret = skipfile = False
293 ret = skipfile = False
294 elif r == 3: # file (Record remaining)
294 elif r == 3: # file (Record remaining)
295 ret = skipfile = True
295 ret = skipfile = True
296 elif r == 4: # done, skip remaining
296 elif r == 4: # done, skip remaining
297 ret = skipall = False
297 ret = skipall = False
298 elif r == 5: # all
298 elif r == 5: # all
299 ret = skipall = True
299 ret = skipall = True
300 elif r == 6: # quit
300 elif r == 6: # quit
301 raise util.Abort(_('user quit'))
301 raise util.Abort(_('user quit'))
302 return ret, skipfile, skipall
302 return ret, skipfile, skipall
303
303
304 seen = set()
304 seen = set()
305 applied = {} # 'filename' -> [] of chunks
305 applied = {} # 'filename' -> [] of chunks
306 skipfile, skipall = None, None
306 skipfile, skipall = None, None
307 pos, total = 1, sum(len(h.hunks) for h in headers)
307 pos, total = 1, sum(len(h.hunks) for h in headers)
308 for h in headers:
308 for h in headers:
309 pos += len(h.hunks)
309 pos += len(h.hunks)
310 skipfile = None
310 skipfile = None
311 fixoffset = 0
311 fixoffset = 0
312 hdr = ''.join(h.header)
312 hdr = ''.join(h.header)
313 if hdr in seen:
313 if hdr in seen:
314 continue
314 continue
315 seen.add(hdr)
315 seen.add(hdr)
316 if skipall is None:
316 if skipall is None:
317 h.pretty(ui)
317 h.pretty(ui)
318 msg = (_('examine changes to %s?') %
318 msg = (_('examine changes to %s?') %
319 _(' and ').join(map(repr, h.files())))
319 _(' and ').join(map(repr, h.files())))
320 r, skipfile, skipall = prompt(skipfile, skipall, msg)
320 r, skipfile, skipall = prompt(skipfile, skipall, msg)
321 if not r:
321 if not r:
322 continue
322 continue
323 applied[h.filename()] = [h]
323 applied[h.filename()] = [h]
324 if h.allhunks():
324 if h.allhunks():
325 applied[h.filename()] += h.hunks
325 applied[h.filename()] += h.hunks
326 continue
326 continue
327 for i, chunk in enumerate(h.hunks):
327 for i, chunk in enumerate(h.hunks):
328 if skipfile is None and skipall is None:
328 if skipfile is None and skipall is None:
329 chunk.pretty(ui)
329 chunk.pretty(ui)
330 if total == 1:
330 if total == 1:
331 msg = _('record this change to %r?') % chunk.filename()
331 msg = _('record this change to %r?') % chunk.filename()
332 else:
332 else:
333 idx = pos - len(h.hunks) + i
333 idx = pos - len(h.hunks) + i
334 msg = _('record change %d/%d to %r?') % (idx, total,
334 msg = _('record change %d/%d to %r?') % (idx, total,
335 chunk.filename())
335 chunk.filename())
336 r, skipfile, skipall = prompt(skipfile, skipall, msg)
336 r, skipfile, skipall = prompt(skipfile, skipall, msg)
337 if r:
337 if r:
338 if fixoffset:
338 if fixoffset:
339 chunk = copy.copy(chunk)
339 chunk = copy.copy(chunk)
340 chunk.toline += fixoffset
340 chunk.toline += fixoffset
341 applied[chunk.filename()].append(chunk)
341 applied[chunk.filename()].append(chunk)
342 else:
342 else:
343 fixoffset += chunk.removed - chunk.added
343 fixoffset += chunk.removed - chunk.added
344 return sum([h for h in applied.itervalues()
344 return sum([h for h in applied.itervalues()
345 if h[0].special() or len(h) > 1], [])
345 if h[0].special() or len(h) > 1], [])
346
346
347 @command("record",
347 @command("record",
348 commands.table['^commit|ci'][1], # same options as commit
348 commands.table['^commit|ci'][1], # same options as commit
349 _('hg record [OPTION]... [FILE]...'))
349 _('hg record [OPTION]... [FILE]...'))
350 def record(ui, repo, *pats, **opts):
350 def record(ui, repo, *pats, **opts):
351 '''interactively select changes to commit
351 '''interactively select changes to commit
352
352
353 If a list of files is omitted, all changes reported by :hg:`status`
353 If a list of files is omitted, all changes reported by :hg:`status`
354 will be candidates for recording.
354 will be candidates for recording.
355
355
356 See :hg:`help dates` for a list of formats valid for -d/--date.
356 See :hg:`help dates` for a list of formats valid for -d/--date.
357
357
358 You will be prompted for whether to record changes to each
358 You will be prompted for whether to record changes to each
359 modified file, and for files with multiple changes, for each
359 modified file, and for files with multiple changes, for each
360 change to use. For each query, the following responses are
360 change to use. For each query, the following responses are
361 possible::
361 possible::
362
362
363 y - record this change
363 y - record this change
364 n - skip this change
364 n - skip this change
365
365
366 s - skip remaining changes to this file
366 s - skip remaining changes to this file
367 f - record remaining changes to this file
367 f - record remaining changes to this file
368
368
369 d - done, skip remaining changes and files
369 d - done, skip remaining changes and files
370 a - record all changes to all remaining files
370 a - record all changes to all remaining files
371 q - quit, recording no changes
371 q - quit, recording no changes
372
372
373 ? - display help
373 ? - display help
374
374
375 This command is not available when committing a merge.'''
375 This command is not available when committing a merge.'''
376
376
377 dorecord(ui, repo, commands.commit, 'commit', *pats, **opts)
377 dorecord(ui, repo, commands.commit, 'commit', False, *pats, **opts)
378
378
379
379
380 def qrecord(ui, repo, patch, *pats, **opts):
380 def qrecord(ui, repo, patch, *pats, **opts):
381 '''interactively record a new patch
381 '''interactively record a new patch
382
382
383 See :hg:`help qnew` & :hg:`help record` for more information and
383 See :hg:`help qnew` & :hg:`help record` for more information and
384 usage.
384 usage.
385 '''
385 '''
386
386
387 try:
387 try:
388 mq = extensions.find('mq')
388 mq = extensions.find('mq')
389 except KeyError:
389 except KeyError:
390 raise util.Abort(_("'mq' extension not loaded"))
390 raise util.Abort(_("'mq' extension not loaded"))
391
391
392 repo.mq.checkpatchname(patch)
392 repo.mq.checkpatchname(patch)
393
393
394 def committomq(ui, repo, *pats, **opts):
394 def committomq(ui, repo, *pats, **opts):
395 opts['checkname'] = False
395 opts['checkname'] = False
396 mq.new(ui, repo, patch, *pats, **opts)
396 mq.new(ui, repo, patch, *pats, **opts)
397
397
398 dorecord(ui, repo, committomq, 'qnew', *pats, **opts)
398 dorecord(ui, repo, committomq, 'qnew', False, *pats, **opts)
399
399
400
400 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall, *pats, **opts):
401 def dorecord(ui, repo, commitfunc, cmdsuggest, *pats, **opts):
402 if not ui.interactive():
401 if not ui.interactive():
403 raise util.Abort(_('running non-interactively, use %s instead') %
402 raise util.Abort(_('running non-interactively, use %s instead') %
404 cmdsuggest)
403 cmdsuggest)
405
404
406 def recordfunc(ui, repo, message, match, opts):
405 def recordfunc(ui, repo, message, match, opts):
407 """This is generic record driver.
406 """This is generic record driver.
408
407
409 Its job is to interactively filter local changes, and
408 Its job is to interactively filter local changes, and
410 accordingly prepare working directory into a state in which the
409 accordingly prepare working directory into a state in which the
411 job can be delegated to a non-interactive commit command such as
410 job can be delegated to a non-interactive commit command such as
412 'commit' or 'qrefresh'.
411 'commit' or 'qrefresh'.
413
412
414 After the actual job is done by non-interactive command, the
413 After the actual job is done by non-interactive command, the
415 working directory is restored to its original state.
414 working directory is restored to its original state.
416
415
417 In the end we'll record interesting changes, and everything else
416 In the end we'll record interesting changes, and everything else
418 will be left in place, so the user can continue working.
417 will be left in place, so the user can continue working.
419 """
418 """
420
419
421 merge = len(repo[None].parents()) > 1
420 merge = len(repo[None].parents()) > 1
422 if merge:
421 if merge:
423 raise util.Abort(_('cannot partially commit a merge '
422 raise util.Abort(_('cannot partially commit a merge '
424 '(use "hg commit" instead)'))
423 '(use "hg commit" instead)'))
425
424
426 changes = repo.status(match=match)[:3]
425 changes = repo.status(match=match)[:3]
427 diffopts = mdiff.diffopts(git=True, nodates=True)
426 diffopts = mdiff.diffopts(git=True, nodates=True)
428 chunks = patch.diff(repo, changes=changes, opts=diffopts)
427 chunks = patch.diff(repo, changes=changes, opts=diffopts)
429 fp = cStringIO.StringIO()
428 fp = cStringIO.StringIO()
430 fp.write(''.join(chunks))
429 fp.write(''.join(chunks))
431 fp.seek(0)
430 fp.seek(0)
432
431
433 # 1. filter patch, so we have intending-to apply subset of it
432 # 1. filter patch, so we have intending-to apply subset of it
434 chunks = filterpatch(ui, parsepatch(fp))
433 chunks = filterpatch(ui, parsepatch(fp))
435 del fp
434 del fp
436
435
437 contenders = set()
436 contenders = set()
438 for h in chunks:
437 for h in chunks:
439 try:
438 try:
440 contenders.update(set(h.files()))
439 contenders.update(set(h.files()))
441 except AttributeError:
440 except AttributeError:
442 pass
441 pass
443
442
444 changed = changes[0] + changes[1] + changes[2]
443 changed = changes[0] + changes[1] + changes[2]
445 newfiles = [f for f in changed if f in contenders]
444 newfiles = [f for f in changed if f in contenders]
446 if not newfiles:
445 if not newfiles:
447 ui.status(_('no changes to record\n'))
446 ui.status(_('no changes to record\n'))
448 return 0
447 return 0
449
448
450 modified = set(changes[0])
449 modified = set(changes[0])
451
450
452 # 2. backup changed files, so we can restore them in the end
451 # 2. backup changed files, so we can restore them in the end
452 if backupall:
453 tobackup = changed
454 else:
455 tobackup = [f for f in newfiles if f in modified]
456
453 backups = {}
457 backups = {}
454 backupdir = repo.join('record-backups')
458 if tobackup:
455 try:
459 backupdir = repo.join('record-backups')
456 os.mkdir(backupdir)
460 try:
457 except OSError, err:
461 os.mkdir(backupdir)
458 if err.errno != errno.EEXIST:
462 except OSError, err:
459 raise
463 if err.errno != errno.EEXIST:
464 raise
460 try:
465 try:
461 # backup continues
466 # backup continues
462 for f in newfiles:
467 for f in tobackup:
463 if f not in modified:
464 continue
465 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
468 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
466 dir=backupdir)
469 dir=backupdir)
467 os.close(fd)
470 os.close(fd)
468 ui.debug('backup %r as %r\n' % (f, tmpname))
471 ui.debug('backup %r as %r\n' % (f, tmpname))
469 util.copyfile(repo.wjoin(f), tmpname)
472 util.copyfile(repo.wjoin(f), tmpname)
470 shutil.copystat(repo.wjoin(f), tmpname)
473 shutil.copystat(repo.wjoin(f), tmpname)
471 backups[f] = tmpname
474 backups[f] = tmpname
472
475
473 fp = cStringIO.StringIO()
476 fp = cStringIO.StringIO()
474 for c in chunks:
477 for c in chunks:
475 if c.filename() in backups:
478 if c.filename() in backups:
476 c.write(fp)
479 c.write(fp)
477 dopatch = fp.tell()
480 dopatch = fp.tell()
478 fp.seek(0)
481 fp.seek(0)
479
482
480 # 3a. apply filtered patch to clean repo (clean)
483 # 3a. apply filtered patch to clean repo (clean)
481 if backups:
484 if backups:
482 hg.revert(repo, repo.dirstate.p1(),
485 hg.revert(repo, repo.dirstate.p1(),
483 lambda key: key in backups)
486 lambda key: key in backups)
484
487
485 # 3b. (apply)
488 # 3b. (apply)
486 if dopatch:
489 if dopatch:
487 try:
490 try:
488 ui.debug('applying patch\n')
491 ui.debug('applying patch\n')
489 ui.debug(fp.getvalue())
492 ui.debug(fp.getvalue())
490 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
493 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
491 except patch.PatchError, err:
494 except patch.PatchError, err:
492 raise util.Abort(str(err))
495 raise util.Abort(str(err))
493 del fp
496 del fp
494
497
495 # 4. We prepared working directory according to filtered
498 # 4. We prepared working directory according to filtered
496 # patch. Now is the time to delegate the job to
499 # patch. Now is the time to delegate the job to
497 # commit/qrefresh or the like!
500 # commit/qrefresh or the like!
498
501
499 # it is important to first chdir to repo root -- we'll call
502 # it is important to first chdir to repo root -- we'll call
500 # a highlevel command with list of pathnames relative to
503 # a highlevel command with list of pathnames relative to
501 # repo root
504 # repo root
502 cwd = os.getcwd()
505 cwd = os.getcwd()
503 os.chdir(repo.root)
506 os.chdir(repo.root)
504 try:
507 try:
505 commitfunc(ui, repo, *newfiles, **opts)
508 commitfunc(ui, repo, *newfiles, **opts)
506 finally:
509 finally:
507 os.chdir(cwd)
510 os.chdir(cwd)
508
511
509 return 0
512 return 0
510 finally:
513 finally:
511 # 5. finally restore backed-up files
514 # 5. finally restore backed-up files
512 try:
515 try:
513 for realname, tmpname in backups.iteritems():
516 for realname, tmpname in backups.iteritems():
514 ui.debug('restoring %r to %r\n' % (tmpname, realname))
517 ui.debug('restoring %r to %r\n' % (tmpname, realname))
515 util.copyfile(tmpname, repo.wjoin(realname))
518 util.copyfile(tmpname, repo.wjoin(realname))
516 # Our calls to copystat() here and above are a
519 # Our calls to copystat() here and above are a
517 # hack to trick any editors that have f open that
520 # hack to trick any editors that have f open that
518 # we haven't modified them.
521 # we haven't modified them.
519 #
522 #
520 # Also note that this racy as an editor could
523 # Also note that this racy as an editor could
521 # notice the file's mtime before we've finished
524 # notice the file's mtime before we've finished
522 # writing it.
525 # writing it.
523 shutil.copystat(tmpname, repo.wjoin(realname))
526 shutil.copystat(tmpname, repo.wjoin(realname))
524 os.unlink(tmpname)
527 os.unlink(tmpname)
525 os.rmdir(backupdir)
528 if tobackup:
529 os.rmdir(backupdir)
526 except OSError:
530 except OSError:
527 pass
531 pass
528
532
529 # wrap ui.write so diff output can be labeled/colorized
533 # wrap ui.write so diff output can be labeled/colorized
530 def wrapwrite(orig, *args, **kw):
534 def wrapwrite(orig, *args, **kw):
531 label = kw.pop('label', '')
535 label = kw.pop('label', '')
532 for chunk, l in patch.difflabel(lambda: args):
536 for chunk, l in patch.difflabel(lambda: args):
533 orig(chunk, label=label + l)
537 orig(chunk, label=label + l)
534 oldwrite = ui.write
538 oldwrite = ui.write
535 extensions.wrapfunction(ui, 'write', wrapwrite)
539 extensions.wrapfunction(ui, 'write', wrapwrite)
536 try:
540 try:
537 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
541 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
538 finally:
542 finally:
539 ui.write = oldwrite
543 ui.write = oldwrite
540
544
541 cmdtable["qrecord"] = \
545 cmdtable["qrecord"] = \
542 (qrecord, {}, # placeholder until mq is available
546 (qrecord, {}, # placeholder until mq is available
543 _('hg qrecord [OPTION]... PATCH [FILE]...'))
547 _('hg qrecord [OPTION]... PATCH [FILE]...'))
544
548
545 def uisetup(ui):
549 def uisetup(ui):
546 try:
550 try:
547 mq = extensions.find('mq')
551 mq = extensions.find('mq')
548 except KeyError:
552 except KeyError:
549 return
553 return
550
554
551 cmdtable["qrecord"] = \
555 cmdtable["qrecord"] = \
552 (qrecord, mq.cmdtable['^qnew'][1], # same options as qnew
556 (qrecord, mq.cmdtable['^qnew'][1], # same options as qnew
553 _('hg qrecord [OPTION]... PATCH [FILE]...'))
557 _('hg qrecord [OPTION]... PATCH [FILE]...'))
General Comments 0
You need to be logged in to leave comments. Login now