##// 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 # 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', False, *pats, **opts)
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 def qrecord(ui, repo, patch, *pats, **opts):
392 def qrecord(ui, repo, patch, *pats, **opts):
381 '''interactively record a new patch
393 '''interactively record a new patch
382
394
383 See :hg:`help qnew` & :hg:`help record` for more information and
395 See :hg:`help qnew` & :hg:`help record` for more information and
384 usage.
396 usage.
385 '''
397 '''
386
398
387 try:
399 try:
388 mq = extensions.find('mq')
400 mq = extensions.find('mq')
389 except KeyError:
401 except KeyError:
390 raise util.Abort(_("'mq' extension not loaded"))
402 raise util.Abort(_("'mq' extension not loaded"))
391
403
392 repo.mq.checkpatchname(patch)
404 repo.mq.checkpatchname(patch)
393
405
394 def committomq(ui, repo, *pats, **opts):
406 def committomq(ui, repo, *pats, **opts):
395 opts['checkname'] = False
407 opts['checkname'] = False
396 mq.new(ui, repo, patch, *pats, **opts)
408 mq.new(ui, repo, patch, *pats, **opts)
397
409
398 dorecord(ui, repo, committomq, 'qnew', False, *pats, **opts)
410 dorecord(ui, repo, committomq, 'qnew', False, *pats, **opts)
399
411
400 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall, *pats, **opts):
412 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall, *pats, **opts):
401 if not ui.interactive():
413 if not ui.interactive():
402 raise util.Abort(_('running non-interactively, use %s instead') %
414 raise util.Abort(_('running non-interactively, use %s instead') %
403 cmdsuggest)
415 cmdsuggest)
404
416
405 def recordfunc(ui, repo, message, match, opts):
417 def recordfunc(ui, repo, message, match, opts):
406 """This is generic record driver.
418 """This is generic record driver.
407
419
408 Its job is to interactively filter local changes, and
420 Its job is to interactively filter local changes, and
409 accordingly prepare working directory into a state in which the
421 accordingly prepare working directory into a state in which the
410 job can be delegated to a non-interactive commit command such as
422 job can be delegated to a non-interactive commit command such as
411 'commit' or 'qrefresh'.
423 'commit' or 'qrefresh'.
412
424
413 After the actual job is done by non-interactive command, the
425 After the actual job is done by non-interactive command, the
414 working directory is restored to its original state.
426 working directory is restored to its original state.
415
427
416 In the end we'll record interesting changes, and everything else
428 In the end we'll record interesting changes, and everything else
417 will be left in place, so the user can continue working.
429 will be left in place, so the user can continue working.
418 """
430 """
419
431
420 merge = len(repo[None].parents()) > 1
432 merge = len(repo[None].parents()) > 1
421 if merge:
433 if merge:
422 raise util.Abort(_('cannot partially commit a merge '
434 raise util.Abort(_('cannot partially commit a merge '
423 '(use "hg commit" instead)'))
435 '(use "hg commit" instead)'))
424
436
425 changes = repo.status(match=match)[:3]
437 changes = repo.status(match=match)[:3]
426 diffopts = mdiff.diffopts(git=True, nodates=True)
438 diffopts = mdiff.diffopts(git=True, nodates=True)
427 chunks = patch.diff(repo, changes=changes, opts=diffopts)
439 chunks = patch.diff(repo, changes=changes, opts=diffopts)
428 fp = cStringIO.StringIO()
440 fp = cStringIO.StringIO()
429 fp.write(''.join(chunks))
441 fp.write(''.join(chunks))
430 fp.seek(0)
442 fp.seek(0)
431
443
432 # 1. filter patch, so we have intending-to apply subset of it
444 # 1. filter patch, so we have intending-to apply subset of it
433 chunks = filterpatch(ui, parsepatch(fp))
445 chunks = filterpatch(ui, parsepatch(fp))
434 del fp
446 del fp
435
447
436 contenders = set()
448 contenders = set()
437 for h in chunks:
449 for h in chunks:
438 try:
450 try:
439 contenders.update(set(h.files()))
451 contenders.update(set(h.files()))
440 except AttributeError:
452 except AttributeError:
441 pass
453 pass
442
454
443 changed = changes[0] + changes[1] + changes[2]
455 changed = changes[0] + changes[1] + changes[2]
444 newfiles = [f for f in changed if f in contenders]
456 newfiles = [f for f in changed if f in contenders]
445 if not newfiles:
457 if not newfiles:
446 ui.status(_('no changes to record\n'))
458 ui.status(_('no changes to record\n'))
447 return 0
459 return 0
448
460
449 modified = set(changes[0])
461 modified = set(changes[0])
450
462
451 # 2. backup changed files, so we can restore them in the end
463 # 2. backup changed files, so we can restore them in the end
452 if backupall:
464 if backupall:
453 tobackup = changed
465 tobackup = changed
454 else:
466 else:
455 tobackup = [f for f in newfiles if f in modified]
467 tobackup = [f for f in newfiles if f in modified]
456
468
457 backups = {}
469 backups = {}
458 if tobackup:
470 if tobackup:
459 backupdir = repo.join('record-backups')
471 backupdir = repo.join('record-backups')
460 try:
472 try:
461 os.mkdir(backupdir)
473 os.mkdir(backupdir)
462 except OSError, err:
474 except OSError, err:
463 if err.errno != errno.EEXIST:
475 if err.errno != errno.EEXIST:
464 raise
476 raise
465 try:
477 try:
466 # backup continues
478 # backup continues
467 for f in tobackup:
479 for f in tobackup:
468 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
480 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
469 dir=backupdir)
481 dir=backupdir)
470 os.close(fd)
482 os.close(fd)
471 ui.debug('backup %r as %r\n' % (f, tmpname))
483 ui.debug('backup %r as %r\n' % (f, tmpname))
472 util.copyfile(repo.wjoin(f), tmpname)
484 util.copyfile(repo.wjoin(f), tmpname)
473 shutil.copystat(repo.wjoin(f), tmpname)
485 shutil.copystat(repo.wjoin(f), tmpname)
474 backups[f] = tmpname
486 backups[f] = tmpname
475
487
476 fp = cStringIO.StringIO()
488 fp = cStringIO.StringIO()
477 for c in chunks:
489 for c in chunks:
478 if c.filename() in backups:
490 if c.filename() in backups:
479 c.write(fp)
491 c.write(fp)
480 dopatch = fp.tell()
492 dopatch = fp.tell()
481 fp.seek(0)
493 fp.seek(0)
482
494
483 # 3a. apply filtered patch to clean repo (clean)
495 # 3a. apply filtered patch to clean repo (clean)
484 if backups:
496 if backups:
485 hg.revert(repo, repo.dirstate.p1(),
497 hg.revert(repo, repo.dirstate.p1(),
486 lambda key: key in backups)
498 lambda key: key in backups)
487
499
488 # 3b. (apply)
500 # 3b. (apply)
489 if dopatch:
501 if dopatch:
490 try:
502 try:
491 ui.debug('applying patch\n')
503 ui.debug('applying patch\n')
492 ui.debug(fp.getvalue())
504 ui.debug(fp.getvalue())
493 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
505 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
494 except patch.PatchError, err:
506 except patch.PatchError, err:
495 raise util.Abort(str(err))
507 raise util.Abort(str(err))
496 del fp
508 del fp
497
509
498 # 4. We prepared working directory according to filtered
510 # 4. We prepared working directory according to filtered
499 # patch. Now is the time to delegate the job to
511 # patch. Now is the time to delegate the job to
500 # commit/qrefresh or the like!
512 # commit/qrefresh or the like!
501
513
502 # it is important to first chdir to repo root -- we'll call
514 # it is important to first chdir to repo root -- we'll call
503 # a highlevel command with list of pathnames relative to
515 # a highlevel command with list of pathnames relative to
504 # repo root
516 # repo root
505 cwd = os.getcwd()
517 cwd = os.getcwd()
506 os.chdir(repo.root)
518 os.chdir(repo.root)
507 try:
519 try:
508 commitfunc(ui, repo, *newfiles, **opts)
520 commitfunc(ui, repo, *newfiles, **opts)
509 finally:
521 finally:
510 os.chdir(cwd)
522 os.chdir(cwd)
511
523
512 return 0
524 return 0
513 finally:
525 finally:
514 # 5. finally restore backed-up files
526 # 5. finally restore backed-up files
515 try:
527 try:
516 for realname, tmpname in backups.iteritems():
528 for realname, tmpname in backups.iteritems():
517 ui.debug('restoring %r to %r\n' % (tmpname, realname))
529 ui.debug('restoring %r to %r\n' % (tmpname, realname))
518 util.copyfile(tmpname, repo.wjoin(realname))
530 util.copyfile(tmpname, repo.wjoin(realname))
519 # Our calls to copystat() here and above are a
531 # Our calls to copystat() here and above are a
520 # hack to trick any editors that have f open that
532 # hack to trick any editors that have f open that
521 # we haven't modified them.
533 # we haven't modified them.
522 #
534 #
523 # Also note that this racy as an editor could
535 # Also note that this racy as an editor could
524 # notice the file's mtime before we've finished
536 # notice the file's mtime before we've finished
525 # writing it.
537 # writing it.
526 shutil.copystat(tmpname, repo.wjoin(realname))
538 shutil.copystat(tmpname, repo.wjoin(realname))
527 os.unlink(tmpname)
539 os.unlink(tmpname)
528 if tobackup:
540 if tobackup:
529 os.rmdir(backupdir)
541 os.rmdir(backupdir)
530 except OSError:
542 except OSError:
531 pass
543 pass
532
544
533 # wrap ui.write so diff output can be labeled/colorized
545 # wrap ui.write so diff output can be labeled/colorized
534 def wrapwrite(orig, *args, **kw):
546 def wrapwrite(orig, *args, **kw):
535 label = kw.pop('label', '')
547 label = kw.pop('label', '')
536 for chunk, l in patch.difflabel(lambda: args):
548 for chunk, l in patch.difflabel(lambda: args):
537 orig(chunk, label=label + l)
549 orig(chunk, label=label + l)
538 oldwrite = ui.write
550 oldwrite = ui.write
539 extensions.wrapfunction(ui, 'write', wrapwrite)
551 extensions.wrapfunction(ui, 'write', wrapwrite)
540 try:
552 try:
541 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
553 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
542 finally:
554 finally:
543 ui.write = oldwrite
555 ui.write = oldwrite
544
556
545 cmdtable["qrecord"] = \
557 cmdtable["qrecord"] = \
546 (qrecord, {}, # placeholder until mq is available
558 (qrecord, {}, # placeholder until mq is available
547 _('hg qrecord [OPTION]... PATCH [FILE]...'))
559 _('hg qrecord [OPTION]... PATCH [FILE]...'))
548
560
549 def uisetup(ui):
561 def uisetup(ui):
550 try:
562 try:
551 mq = extensions.find('mq')
563 mq = extensions.find('mq')
552 except KeyError:
564 except KeyError:
553 return
565 return
554
566
555 cmdtable["qrecord"] = \
567 cmdtable["qrecord"] = \
556 (qrecord, mq.cmdtable['^qnew'][1], # same options as qnew
568 (qrecord, mq.cmdtable['^qnew'][1], # same options as qnew
557 _('hg qrecord [OPTION]... PATCH [FILE]...'))
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