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