Show More
@@ -1,538 +1,546 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, incorporated herein by reference. |
|
6 | # GNU General Public License version 2, incorporated herein by reference. | |
7 |
|
7 | |||
8 | '''interactive change selection during commit or qrefresh''' |
|
8 | '''interactive change selection during commit or 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, operator, os, re, tempfile |
|
13 | import copy, cStringIO, errno, operator, os, re, tempfile | |
14 |
|
14 | |||
15 | lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)') |
|
15 | lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)') | |
16 |
|
16 | |||
17 | def scanpatch(fp): |
|
17 | def scanpatch(fp): | |
18 | """like patch.iterhunks, but yield different events |
|
18 | """like patch.iterhunks, but yield different events | |
19 |
|
19 | |||
20 | - ('file', [header_lines + fromfile + tofile]) |
|
20 | - ('file', [header_lines + fromfile + tofile]) | |
21 | - ('context', [context_lines]) |
|
21 | - ('context', [context_lines]) | |
22 | - ('hunk', [hunk_lines]) |
|
22 | - ('hunk', [hunk_lines]) | |
23 | - ('range', (-start,len, +start,len, diffp)) |
|
23 | - ('range', (-start,len, +start,len, diffp)) | |
24 | """ |
|
24 | """ | |
25 | lr = patch.linereader(fp) |
|
25 | lr = patch.linereader(fp) | |
26 |
|
26 | |||
27 | def scanwhile(first, p): |
|
27 | def scanwhile(first, p): | |
28 | """scan lr while predicate holds""" |
|
28 | """scan lr while predicate holds""" | |
29 | lines = [first] |
|
29 | lines = [first] | |
30 | while True: |
|
30 | while True: | |
31 | line = lr.readline() |
|
31 | line = lr.readline() | |
32 | if not line: |
|
32 | if not line: | |
33 | break |
|
33 | break | |
34 | if p(line): |
|
34 | if p(line): | |
35 | lines.append(line) |
|
35 | lines.append(line) | |
36 | else: |
|
36 | else: | |
37 | lr.push(line) |
|
37 | lr.push(line) | |
38 | break |
|
38 | break | |
39 | return lines |
|
39 | return lines | |
40 |
|
40 | |||
41 | while True: |
|
41 | while True: | |
42 | line = lr.readline() |
|
42 | line = lr.readline() | |
43 | if not line: |
|
43 | if not line: | |
44 | break |
|
44 | break | |
45 | if line.startswith('diff --git a/'): |
|
45 | if line.startswith('diff --git a/'): | |
46 | def notheader(line): |
|
46 | def notheader(line): | |
47 | s = line.split(None, 1) |
|
47 | s = line.split(None, 1) | |
48 | return not s or s[0] not in ('---', 'diff') |
|
48 | return not s or s[0] not in ('---', 'diff') | |
49 | header = scanwhile(line, notheader) |
|
49 | header = scanwhile(line, notheader) | |
50 | fromfile = lr.readline() |
|
50 | fromfile = lr.readline() | |
51 | if fromfile.startswith('---'): |
|
51 | if fromfile.startswith('---'): | |
52 | tofile = lr.readline() |
|
52 | tofile = lr.readline() | |
53 | header += [fromfile, tofile] |
|
53 | header += [fromfile, tofile] | |
54 | else: |
|
54 | else: | |
55 | lr.push(fromfile) |
|
55 | lr.push(fromfile) | |
56 | yield 'file', header |
|
56 | yield 'file', header | |
57 | elif line[0] == ' ': |
|
57 | elif line[0] == ' ': | |
58 | yield 'context', scanwhile(line, lambda l: l[0] in ' \\') |
|
58 | yield 'context', scanwhile(line, lambda l: l[0] in ' \\') | |
59 | elif line[0] in '-+': |
|
59 | elif line[0] in '-+': | |
60 | yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\') |
|
60 | yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\') | |
61 | else: |
|
61 | else: | |
62 | m = lines_re.match(line) |
|
62 | m = lines_re.match(line) | |
63 | if m: |
|
63 | if m: | |
64 | yield 'range', m.groups() |
|
64 | yield 'range', m.groups() | |
65 | else: |
|
65 | else: | |
66 | raise patch.PatchError('unknown patch content: %r' % line) |
|
66 | raise patch.PatchError('unknown patch content: %r' % line) | |
67 |
|
67 | |||
68 | class header(object): |
|
68 | class header(object): | |
69 | """patch header |
|
69 | """patch header | |
70 |
|
70 | |||
71 | XXX shoudn't we move this to mercurial/patch.py ? |
|
71 | XXX shoudn't we move this to mercurial/patch.py ? | |
72 | """ |
|
72 | """ | |
73 | diff_re = re.compile('diff --git a/(.*) b/(.*)$') |
|
73 | diff_re = re.compile('diff --git a/(.*) b/(.*)$') | |
74 | allhunks_re = re.compile('(?:index|new file|deleted file) ') |
|
74 | allhunks_re = re.compile('(?:index|new file|deleted file) ') | |
75 | pretty_re = re.compile('(?:new file|deleted file) ') |
|
75 | pretty_re = re.compile('(?:new file|deleted file) ') | |
76 | special_re = re.compile('(?:index|new|deleted|copy|rename) ') |
|
76 | special_re = re.compile('(?:index|new|deleted|copy|rename) ') | |
77 |
|
77 | |||
78 | def __init__(self, header): |
|
78 | def __init__(self, header): | |
79 | self.header = header |
|
79 | self.header = header | |
80 | self.hunks = [] |
|
80 | self.hunks = [] | |
81 |
|
81 | |||
82 | def binary(self): |
|
82 | def binary(self): | |
83 | for h in self.header: |
|
83 | for h in self.header: | |
84 | if h.startswith('index '): |
|
84 | if h.startswith('index '): | |
85 | return True |
|
85 | return True | |
86 |
|
86 | |||
87 | def pretty(self, fp): |
|
87 | def pretty(self, fp): | |
88 | for h in self.header: |
|
88 | for h in self.header: | |
89 | if h.startswith('index '): |
|
89 | if h.startswith('index '): | |
90 | fp.write(_('this modifies a binary file (all or nothing)\n')) |
|
90 | fp.write(_('this modifies a binary file (all or nothing)\n')) | |
91 | break |
|
91 | break | |
92 | if self.pretty_re.match(h): |
|
92 | if self.pretty_re.match(h): | |
93 | fp.write(h) |
|
93 | fp.write(h) | |
94 | if self.binary(): |
|
94 | if self.binary(): | |
95 | fp.write(_('this is a binary file\n')) |
|
95 | fp.write(_('this is a binary file\n')) | |
96 | break |
|
96 | break | |
97 | if h.startswith('---'): |
|
97 | if h.startswith('---'): | |
98 | fp.write(_('%d hunks, %d lines changed\n') % |
|
98 | fp.write(_('%d hunks, %d lines changed\n') % | |
99 | (len(self.hunks), |
|
99 | (len(self.hunks), | |
100 | sum([h.added + h.removed for h in self.hunks]))) |
|
100 | sum([h.added + h.removed for h in self.hunks]))) | |
101 | break |
|
101 | break | |
102 | fp.write(h) |
|
102 | fp.write(h) | |
103 |
|
103 | |||
104 | def write(self, fp): |
|
104 | def write(self, fp): | |
105 | fp.write(''.join(self.header)) |
|
105 | fp.write(''.join(self.header)) | |
106 |
|
106 | |||
107 | def allhunks(self): |
|
107 | def allhunks(self): | |
108 | for h in self.header: |
|
108 | for h in self.header: | |
109 | if self.allhunks_re.match(h): |
|
109 | if self.allhunks_re.match(h): | |
110 | return True |
|
110 | return True | |
111 |
|
111 | |||
112 | def files(self): |
|
112 | def files(self): | |
113 | fromfile, tofile = self.diff_re.match(self.header[0]).groups() |
|
113 | fromfile, tofile = self.diff_re.match(self.header[0]).groups() | |
114 | if fromfile == tofile: |
|
114 | if fromfile == tofile: | |
115 | return [fromfile] |
|
115 | return [fromfile] | |
116 | return [fromfile, tofile] |
|
116 | return [fromfile, tofile] | |
117 |
|
117 | |||
118 | def filename(self): |
|
118 | def filename(self): | |
119 | return self.files()[-1] |
|
119 | return self.files()[-1] | |
120 |
|
120 | |||
121 | def __repr__(self): |
|
121 | def __repr__(self): | |
122 | return '<header %s>' % (' '.join(map(repr, self.files()))) |
|
122 | return '<header %s>' % (' '.join(map(repr, self.files()))) | |
123 |
|
123 | |||
124 | def special(self): |
|
124 | def special(self): | |
125 | for h in self.header: |
|
125 | for h in self.header: | |
126 | if self.special_re.match(h): |
|
126 | if self.special_re.match(h): | |
127 | return True |
|
127 | return True | |
128 |
|
128 | |||
129 | def countchanges(hunk): |
|
129 | def countchanges(hunk): | |
130 | """hunk -> (n+,n-)""" |
|
130 | """hunk -> (n+,n-)""" | |
131 | add = len([h for h in hunk if h[0] == '+']) |
|
131 | add = len([h for h in hunk if h[0] == '+']) | |
132 | rem = len([h for h in hunk if h[0] == '-']) |
|
132 | rem = len([h for h in hunk if h[0] == '-']) | |
133 | return add, rem |
|
133 | return add, rem | |
134 |
|
134 | |||
135 | class hunk(object): |
|
135 | class hunk(object): | |
136 | """patch hunk |
|
136 | """patch hunk | |
137 |
|
137 | |||
138 | XXX shouldn't we merge this with patch.hunk ? |
|
138 | XXX shouldn't we merge this with patch.hunk ? | |
139 | """ |
|
139 | """ | |
140 | maxcontext = 3 |
|
140 | maxcontext = 3 | |
141 |
|
141 | |||
142 | def __init__(self, header, fromline, toline, proc, before, hunk, after): |
|
142 | def __init__(self, header, fromline, toline, proc, before, hunk, after): | |
143 | def trimcontext(number, lines): |
|
143 | def trimcontext(number, lines): | |
144 | delta = len(lines) - self.maxcontext |
|
144 | delta = len(lines) - self.maxcontext | |
145 | if False and delta > 0: |
|
145 | if False and delta > 0: | |
146 | return number + delta, lines[:self.maxcontext] |
|
146 | return number + delta, lines[:self.maxcontext] | |
147 | return number, lines |
|
147 | return number, lines | |
148 |
|
148 | |||
149 | self.header = header |
|
149 | self.header = header | |
150 | self.fromline, self.before = trimcontext(fromline, before) |
|
150 | self.fromline, self.before = trimcontext(fromline, before) | |
151 | self.toline, self.after = trimcontext(toline, after) |
|
151 | self.toline, self.after = trimcontext(toline, after) | |
152 | self.proc = proc |
|
152 | self.proc = proc | |
153 | self.hunk = hunk |
|
153 | self.hunk = hunk | |
154 | self.added, self.removed = countchanges(self.hunk) |
|
154 | self.added, self.removed = countchanges(self.hunk) | |
155 |
|
155 | |||
156 | def write(self, fp): |
|
156 | def write(self, fp): | |
157 | delta = len(self.before) + len(self.after) |
|
157 | delta = len(self.before) + len(self.after) | |
158 | if self.after and self.after[-1] == '\\ No newline at end of file\n': |
|
158 | if self.after and self.after[-1] == '\\ No newline at end of file\n': | |
159 | delta -= 1 |
|
159 | delta -= 1 | |
160 | fromlen = delta + self.removed |
|
160 | fromlen = delta + self.removed | |
161 | tolen = delta + self.added |
|
161 | tolen = delta + self.added | |
162 | fp.write('@@ -%d,%d +%d,%d @@%s\n' % |
|
162 | fp.write('@@ -%d,%d +%d,%d @@%s\n' % | |
163 | (self.fromline, fromlen, self.toline, tolen, |
|
163 | (self.fromline, fromlen, self.toline, tolen, | |
164 | self.proc and (' ' + self.proc))) |
|
164 | self.proc and (' ' + self.proc))) | |
165 | fp.write(''.join(self.before + self.hunk + self.after)) |
|
165 | fp.write(''.join(self.before + self.hunk + self.after)) | |
166 |
|
166 | |||
167 | pretty = write |
|
167 | pretty = write | |
168 |
|
168 | |||
169 | def filename(self): |
|
169 | def filename(self): | |
170 | return self.header.filename() |
|
170 | return self.header.filename() | |
171 |
|
171 | |||
172 | def __repr__(self): |
|
172 | def __repr__(self): | |
173 | return '<hunk %r@%d>' % (self.filename(), self.fromline) |
|
173 | return '<hunk %r@%d>' % (self.filename(), self.fromline) | |
174 |
|
174 | |||
175 | def parsepatch(fp): |
|
175 | def parsepatch(fp): | |
176 | """patch -> [] of hunks """ |
|
176 | """patch -> [] of hunks """ | |
177 | class parser(object): |
|
177 | class parser(object): | |
178 | """patch parsing state machine""" |
|
178 | """patch parsing state machine""" | |
179 | def __init__(self): |
|
179 | def __init__(self): | |
180 | self.fromline = 0 |
|
180 | self.fromline = 0 | |
181 | self.toline = 0 |
|
181 | self.toline = 0 | |
182 | self.proc = '' |
|
182 | self.proc = '' | |
183 | self.header = None |
|
183 | self.header = None | |
184 | self.context = [] |
|
184 | self.context = [] | |
185 | self.before = [] |
|
185 | self.before = [] | |
186 | self.hunk = [] |
|
186 | self.hunk = [] | |
187 | self.stream = [] |
|
187 | self.stream = [] | |
188 |
|
188 | |||
189 | def addrange(self, (fromstart, fromend, tostart, toend, proc)): |
|
189 | def addrange(self, (fromstart, fromend, tostart, toend, proc)): | |
190 | self.fromline = int(fromstart) |
|
190 | self.fromline = int(fromstart) | |
191 | self.toline = int(tostart) |
|
191 | self.toline = int(tostart) | |
192 | self.proc = proc |
|
192 | self.proc = proc | |
193 |
|
193 | |||
194 | def addcontext(self, context): |
|
194 | def addcontext(self, context): | |
195 | if self.hunk: |
|
195 | if self.hunk: | |
196 | h = hunk(self.header, self.fromline, self.toline, self.proc, |
|
196 | h = hunk(self.header, self.fromline, self.toline, self.proc, | |
197 | self.before, self.hunk, context) |
|
197 | self.before, self.hunk, context) | |
198 | self.header.hunks.append(h) |
|
198 | self.header.hunks.append(h) | |
199 | self.stream.append(h) |
|
199 | self.stream.append(h) | |
200 | self.fromline += len(self.before) + h.removed |
|
200 | self.fromline += len(self.before) + h.removed | |
201 | self.toline += len(self.before) + h.added |
|
201 | self.toline += len(self.before) + h.added | |
202 | self.before = [] |
|
202 | self.before = [] | |
203 | self.hunk = [] |
|
203 | self.hunk = [] | |
204 | self.proc = '' |
|
204 | self.proc = '' | |
205 | self.context = context |
|
205 | self.context = context | |
206 |
|
206 | |||
207 | def addhunk(self, hunk): |
|
207 | def addhunk(self, hunk): | |
208 | if self.context: |
|
208 | if self.context: | |
209 | self.before = self.context |
|
209 | self.before = self.context | |
210 | self.context = [] |
|
210 | self.context = [] | |
211 | self.hunk = hunk |
|
211 | self.hunk = hunk | |
212 |
|
212 | |||
213 | def newfile(self, hdr): |
|
213 | def newfile(self, hdr): | |
214 | self.addcontext([]) |
|
214 | self.addcontext([]) | |
215 | h = header(hdr) |
|
215 | h = header(hdr) | |
216 | self.stream.append(h) |
|
216 | self.stream.append(h) | |
217 | self.header = h |
|
217 | self.header = h | |
218 |
|
218 | |||
219 | def finished(self): |
|
219 | def finished(self): | |
220 | self.addcontext([]) |
|
220 | self.addcontext([]) | |
221 | return self.stream |
|
221 | return self.stream | |
222 |
|
222 | |||
223 | transitions = { |
|
223 | transitions = { | |
224 | 'file': {'context': addcontext, |
|
224 | 'file': {'context': addcontext, | |
225 | 'file': newfile, |
|
225 | 'file': newfile, | |
226 | 'hunk': addhunk, |
|
226 | 'hunk': addhunk, | |
227 | 'range': addrange}, |
|
227 | 'range': addrange}, | |
228 | 'context': {'file': newfile, |
|
228 | 'context': {'file': newfile, | |
229 | 'hunk': addhunk, |
|
229 | 'hunk': addhunk, | |
230 | 'range': addrange}, |
|
230 | 'range': addrange}, | |
231 | 'hunk': {'context': addcontext, |
|
231 | 'hunk': {'context': addcontext, | |
232 | 'file': newfile, |
|
232 | 'file': newfile, | |
233 | 'range': addrange}, |
|
233 | 'range': addrange}, | |
234 | 'range': {'context': addcontext, |
|
234 | 'range': {'context': addcontext, | |
235 | 'hunk': addhunk}, |
|
235 | 'hunk': addhunk}, | |
236 | } |
|
236 | } | |
237 |
|
237 | |||
238 | p = parser() |
|
238 | p = parser() | |
239 |
|
239 | |||
240 | state = 'context' |
|
240 | state = 'context' | |
241 | for newstate, data in scanpatch(fp): |
|
241 | for newstate, data in scanpatch(fp): | |
242 | try: |
|
242 | try: | |
243 | p.transitions[state][newstate](p, data) |
|
243 | p.transitions[state][newstate](p, data) | |
244 | except KeyError: |
|
244 | except KeyError: | |
245 | raise patch.PatchError('unhandled transition: %s -> %s' % |
|
245 | raise patch.PatchError('unhandled transition: %s -> %s' % | |
246 | (state, newstate)) |
|
246 | (state, newstate)) | |
247 | state = newstate |
|
247 | state = newstate | |
248 | return p.finished() |
|
248 | return p.finished() | |
249 |
|
249 | |||
250 | def filterpatch(ui, chunks): |
|
250 | def filterpatch(ui, chunks): | |
251 | """Interactively filter patch chunks into applied-only chunks""" |
|
251 | """Interactively filter patch chunks into applied-only chunks""" | |
252 | chunks = list(chunks) |
|
252 | chunks = list(chunks) | |
253 | chunks.reverse() |
|
253 | chunks.reverse() | |
254 | seen = {} |
|
254 | seen = {} | |
255 | def consumefile(): |
|
255 | def consumefile(): | |
256 | """fetch next portion from chunks until a 'header' is seen |
|
256 | """fetch next portion from chunks until a 'header' is seen | |
257 | NB: header == new-file mark |
|
257 | NB: header == new-file mark | |
258 | """ |
|
258 | """ | |
259 | consumed = [] |
|
259 | consumed = [] | |
260 | while chunks: |
|
260 | while chunks: | |
261 | if isinstance(chunks[-1], header): |
|
261 | if isinstance(chunks[-1], header): | |
262 | break |
|
262 | break | |
263 | else: |
|
263 | else: | |
264 | consumed.append(chunks.pop()) |
|
264 | consumed.append(chunks.pop()) | |
265 | return consumed |
|
265 | return consumed | |
266 |
|
266 | |||
267 | resp_all = [None] # this two are changed from inside prompt, |
|
267 | resp_all = [None] # this two are changed from inside prompt, | |
268 | resp_file = [None] # so can't be usual variables |
|
268 | resp_file = [None] # so can't be usual variables | |
269 | applied = {} # 'filename' -> [] of chunks |
|
269 | applied = {} # 'filename' -> [] of chunks | |
270 | def prompt(query): |
|
270 | def prompt(query): | |
271 | """prompt query, and process base inputs |
|
271 | """prompt query, and process base inputs | |
272 |
|
272 | |||
273 | - y/n for the rest of file |
|
273 | - y/n for the rest of file | |
274 | - y/n for the rest |
|
274 | - y/n for the rest | |
275 | - ? (help) |
|
275 | - ? (help) | |
276 | - q (quit) |
|
276 | - q (quit) | |
277 |
|
277 | |||
278 | else, input is returned to the caller. |
|
278 | else, input is returned to the caller. | |
279 | """ |
|
279 | """ | |
280 | if resp_all[0] is not None: |
|
280 | if resp_all[0] is not None: | |
281 | return resp_all[0] |
|
281 | return resp_all[0] | |
282 | if resp_file[0] is not None: |
|
282 | if resp_file[0] is not None: | |
283 | return resp_file[0] |
|
283 | return resp_file[0] | |
284 | while True: |
|
284 | while True: | |
285 |
|
|
285 | resps = _('[Ynsfdaq?]') | |
286 | r = (ui.prompt("%s %s " % (query, choices), '(?i)%s?$' % choices) |
|
286 | choices = (_('&Yes, record this change'), | |
|
287 | _('&No, skip this change'), | |||
|
288 | _('&Skip remaining changes to this file'), | |||
|
289 | _('Record remaining changes to this &file'), | |||
|
290 | _('&Done, skip remaining changes and files'), | |||
|
291 | _('Record &all changes to all remaining files'), | |||
|
292 | _('&Quit, recording no changes'), | |||
|
293 | _('&?')) | |||
|
294 | r = (ui.prompt("%s %s " % (query, resps), choices) | |||
287 | or _('y')).lower() |
|
295 | or _('y')).lower() | |
288 | if r == _('?'): |
|
296 | if r == _('?'): | |
289 | doc = gettext(record.__doc__) |
|
297 | doc = gettext(record.__doc__) | |
290 | c = doc.find(_('y - record this change')) |
|
298 | c = doc.find(_('y - record this change')) | |
291 | for l in doc[c:].splitlines(): |
|
299 | for l in doc[c:].splitlines(): | |
292 | if l: ui.write(l.strip(), '\n') |
|
300 | if l: ui.write(l.strip(), '\n') | |
293 | continue |
|
301 | continue | |
294 | elif r == _('s'): |
|
302 | elif r == _('s'): | |
295 | r = resp_file[0] = 'n' |
|
303 | r = resp_file[0] = 'n' | |
296 | elif r == _('f'): |
|
304 | elif r == _('f'): | |
297 | r = resp_file[0] = 'y' |
|
305 | r = resp_file[0] = 'y' | |
298 | elif r == _('d'): |
|
306 | elif r == _('d'): | |
299 | r = resp_all[0] = 'n' |
|
307 | r = resp_all[0] = 'n' | |
300 | elif r == _('a'): |
|
308 | elif r == _('a'): | |
301 | r = resp_all[0] = 'y' |
|
309 | r = resp_all[0] = 'y' | |
302 | elif r == _('q'): |
|
310 | elif r == _('q'): | |
303 | raise util.Abort(_('user quit')) |
|
311 | raise util.Abort(_('user quit')) | |
304 | return r |
|
312 | return r | |
305 | pos, total = 0, len(chunks) - 1 |
|
313 | pos, total = 0, len(chunks) - 1 | |
306 | while chunks: |
|
314 | while chunks: | |
307 | chunk = chunks.pop() |
|
315 | chunk = chunks.pop() | |
308 | if isinstance(chunk, header): |
|
316 | if isinstance(chunk, header): | |
309 | # new-file mark |
|
317 | # new-file mark | |
310 | resp_file = [None] |
|
318 | resp_file = [None] | |
311 | fixoffset = 0 |
|
319 | fixoffset = 0 | |
312 | hdr = ''.join(chunk.header) |
|
320 | hdr = ''.join(chunk.header) | |
313 | if hdr in seen: |
|
321 | if hdr in seen: | |
314 | consumefile() |
|
322 | consumefile() | |
315 | continue |
|
323 | continue | |
316 | seen[hdr] = True |
|
324 | seen[hdr] = True | |
317 | if resp_all[0] is None: |
|
325 | if resp_all[0] is None: | |
318 | chunk.pretty(ui) |
|
326 | chunk.pretty(ui) | |
319 | r = prompt(_('examine changes to %s?') % |
|
327 | r = prompt(_('examine changes to %s?') % | |
320 | _(' and ').join(map(repr, chunk.files()))) |
|
328 | _(' and ').join(map(repr, chunk.files()))) | |
321 | if r == _('y'): |
|
329 | if r == _('y'): | |
322 | applied[chunk.filename()] = [chunk] |
|
330 | applied[chunk.filename()] = [chunk] | |
323 | if chunk.allhunks(): |
|
331 | if chunk.allhunks(): | |
324 | applied[chunk.filename()] += consumefile() |
|
332 | applied[chunk.filename()] += consumefile() | |
325 | else: |
|
333 | else: | |
326 | consumefile() |
|
334 | consumefile() | |
327 | else: |
|
335 | else: | |
328 | # new hunk |
|
336 | # new hunk | |
329 | if resp_file[0] is None and resp_all[0] is None: |
|
337 | if resp_file[0] is None and resp_all[0] is None: | |
330 | chunk.pretty(ui) |
|
338 | chunk.pretty(ui) | |
331 | r = total == 1 and prompt(_('record this change to %r?') % |
|
339 | r = total == 1 and prompt(_('record this change to %r?') % | |
332 | chunk.filename()) \ |
|
340 | chunk.filename()) \ | |
333 | or prompt(_('record change %d/%d to %r?') % |
|
341 | or prompt(_('record change %d/%d to %r?') % | |
334 | (pos, total, chunk.filename())) |
|
342 | (pos, total, chunk.filename())) | |
335 | if r == _('y'): |
|
343 | if r == _('y'): | |
336 | if fixoffset: |
|
344 | if fixoffset: | |
337 | chunk = copy.copy(chunk) |
|
345 | chunk = copy.copy(chunk) | |
338 | chunk.toline += fixoffset |
|
346 | chunk.toline += fixoffset | |
339 | applied[chunk.filename()].append(chunk) |
|
347 | applied[chunk.filename()].append(chunk) | |
340 | else: |
|
348 | else: | |
341 | fixoffset += chunk.removed - chunk.added |
|
349 | fixoffset += chunk.removed - chunk.added | |
342 | pos = pos + 1 |
|
350 | pos = pos + 1 | |
343 | return reduce(operator.add, [h for h in applied.itervalues() |
|
351 | return reduce(operator.add, [h for h in applied.itervalues() | |
344 | if h[0].special() or len(h) > 1], []) |
|
352 | if h[0].special() or len(h) > 1], []) | |
345 |
|
353 | |||
346 | def record(ui, repo, *pats, **opts): |
|
354 | def record(ui, repo, *pats, **opts): | |
347 | '''interactively select changes to commit |
|
355 | '''interactively select changes to commit | |
348 |
|
356 | |||
349 | If a list of files is omitted, all changes reported by "hg status" |
|
357 | If a list of files is omitted, all changes reported by "hg status" | |
350 | will be candidates for recording. |
|
358 | will be candidates for recording. | |
351 |
|
359 | |||
352 | See 'hg help dates' for a list of formats valid for -d/--date. |
|
360 | See 'hg help dates' for a list of formats valid for -d/--date. | |
353 |
|
361 | |||
354 | You will be prompted for whether to record changes to each |
|
362 | You will be prompted for whether to record changes to each | |
355 | modified file, and for files with multiple changes, for each |
|
363 | modified file, and for files with multiple changes, for each | |
356 | change to use. For each query, the following responses are |
|
364 | change to use. For each query, the following responses are | |
357 | possible: |
|
365 | possible: | |
358 |
|
366 | |||
359 | y - record this change |
|
367 | y - record this change | |
360 | n - skip this change |
|
368 | n - skip this change | |
361 |
|
369 | |||
362 | s - skip remaining changes to this file |
|
370 | s - skip remaining changes to this file | |
363 | f - record remaining changes to this file |
|
371 | f - record remaining changes to this file | |
364 |
|
372 | |||
365 | d - done, skip remaining changes and files |
|
373 | d - done, skip remaining changes and files | |
366 | a - record all changes to all remaining files |
|
374 | a - record all changes to all remaining files | |
367 | q - quit, recording no changes |
|
375 | q - quit, recording no changes | |
368 |
|
376 | |||
369 | ? - display help''' |
|
377 | ? - display help''' | |
370 |
|
378 | |||
371 | def record_committer(ui, repo, pats, opts): |
|
379 | def record_committer(ui, repo, pats, opts): | |
372 | commands.commit(ui, repo, *pats, **opts) |
|
380 | commands.commit(ui, repo, *pats, **opts) | |
373 |
|
381 | |||
374 | dorecord(ui, repo, record_committer, *pats, **opts) |
|
382 | dorecord(ui, repo, record_committer, *pats, **opts) | |
375 |
|
383 | |||
376 |
|
384 | |||
377 | def qrecord(ui, repo, patch, *pats, **opts): |
|
385 | def qrecord(ui, repo, patch, *pats, **opts): | |
378 | '''interactively record a new patch |
|
386 | '''interactively record a new patch | |
379 |
|
387 | |||
380 | see 'hg help qnew' & 'hg help record' for more information and usage |
|
388 | see 'hg help qnew' & 'hg help record' for more information and usage | |
381 | ''' |
|
389 | ''' | |
382 |
|
390 | |||
383 | try: |
|
391 | try: | |
384 | mq = extensions.find('mq') |
|
392 | mq = extensions.find('mq') | |
385 | except KeyError: |
|
393 | except KeyError: | |
386 | raise util.Abort(_("'mq' extension not loaded")) |
|
394 | raise util.Abort(_("'mq' extension not loaded")) | |
387 |
|
395 | |||
388 | def qrecord_committer(ui, repo, pats, opts): |
|
396 | def qrecord_committer(ui, repo, pats, opts): | |
389 | mq.new(ui, repo, patch, *pats, **opts) |
|
397 | mq.new(ui, repo, patch, *pats, **opts) | |
390 |
|
398 | |||
391 | opts = opts.copy() |
|
399 | opts = opts.copy() | |
392 | opts['force'] = True # always 'qnew -f' |
|
400 | opts['force'] = True # always 'qnew -f' | |
393 | dorecord(ui, repo, qrecord_committer, *pats, **opts) |
|
401 | dorecord(ui, repo, qrecord_committer, *pats, **opts) | |
394 |
|
402 | |||
395 |
|
403 | |||
396 | def dorecord(ui, repo, committer, *pats, **opts): |
|
404 | def dorecord(ui, repo, committer, *pats, **opts): | |
397 | if not ui.interactive(): |
|
405 | if not ui.interactive(): | |
398 | raise util.Abort(_('running non-interactively, use commit instead')) |
|
406 | raise util.Abort(_('running non-interactively, use commit instead')) | |
399 |
|
407 | |||
400 | def recordfunc(ui, repo, message, match, opts): |
|
408 | def recordfunc(ui, repo, message, match, opts): | |
401 | """This is generic record driver. |
|
409 | """This is generic record driver. | |
402 |
|
410 | |||
403 | It's job is to interactively filter local changes, and accordingly |
|
411 | It's job is to interactively filter local changes, and accordingly | |
404 | prepare working dir into a state, where the job can be delegated to |
|
412 | prepare working dir into a state, where the job can be delegated to | |
405 | non-interactive commit command such as 'commit' or 'qrefresh'. |
|
413 | non-interactive commit command such as 'commit' or 'qrefresh'. | |
406 |
|
414 | |||
407 | After the actual job is done by non-interactive command, working dir |
|
415 | After the actual job is done by non-interactive command, working dir | |
408 | state is restored to original. |
|
416 | state is restored to original. | |
409 |
|
417 | |||
410 | In the end we'll record intresting changes, and everything else will be |
|
418 | In the end we'll record intresting changes, and everything else will be | |
411 | left in place, so the user can continue his work. |
|
419 | left in place, so the user can continue his work. | |
412 | """ |
|
420 | """ | |
413 |
|
421 | |||
414 | changes = repo.status(match=match)[:3] |
|
422 | changes = repo.status(match=match)[:3] | |
415 | diffopts = mdiff.diffopts(git=True, nodates=True) |
|
423 | diffopts = mdiff.diffopts(git=True, nodates=True) | |
416 | chunks = patch.diff(repo, changes=changes, opts=diffopts) |
|
424 | chunks = patch.diff(repo, changes=changes, opts=diffopts) | |
417 | fp = cStringIO.StringIO() |
|
425 | fp = cStringIO.StringIO() | |
418 | fp.write(''.join(chunks)) |
|
426 | fp.write(''.join(chunks)) | |
419 | fp.seek(0) |
|
427 | fp.seek(0) | |
420 |
|
428 | |||
421 | # 1. filter patch, so we have intending-to apply subset of it |
|
429 | # 1. filter patch, so we have intending-to apply subset of it | |
422 | chunks = filterpatch(ui, parsepatch(fp)) |
|
430 | chunks = filterpatch(ui, parsepatch(fp)) | |
423 | del fp |
|
431 | del fp | |
424 |
|
432 | |||
425 | contenders = set() |
|
433 | contenders = set() | |
426 | for h in chunks: |
|
434 | for h in chunks: | |
427 | try: contenders.update(set(h.files())) |
|
435 | try: contenders.update(set(h.files())) | |
428 | except AttributeError: pass |
|
436 | except AttributeError: pass | |
429 |
|
437 | |||
430 | changed = changes[0] + changes[1] + changes[2] |
|
438 | changed = changes[0] + changes[1] + changes[2] | |
431 | newfiles = [f for f in changed if f in contenders] |
|
439 | newfiles = [f for f in changed if f in contenders] | |
432 | if not newfiles: |
|
440 | if not newfiles: | |
433 | ui.status(_('no changes to record\n')) |
|
441 | ui.status(_('no changes to record\n')) | |
434 | return 0 |
|
442 | return 0 | |
435 |
|
443 | |||
436 | modified = set(changes[0]) |
|
444 | modified = set(changes[0]) | |
437 |
|
445 | |||
438 | # 2. backup changed files, so we can restore them in the end |
|
446 | # 2. backup changed files, so we can restore them in the end | |
439 | backups = {} |
|
447 | backups = {} | |
440 | backupdir = repo.join('record-backups') |
|
448 | backupdir = repo.join('record-backups') | |
441 | try: |
|
449 | try: | |
442 | os.mkdir(backupdir) |
|
450 | os.mkdir(backupdir) | |
443 | except OSError, err: |
|
451 | except OSError, err: | |
444 | if err.errno != errno.EEXIST: |
|
452 | if err.errno != errno.EEXIST: | |
445 | raise |
|
453 | raise | |
446 | try: |
|
454 | try: | |
447 | # backup continues |
|
455 | # backup continues | |
448 | for f in newfiles: |
|
456 | for f in newfiles: | |
449 | if f not in modified: |
|
457 | if f not in modified: | |
450 | continue |
|
458 | continue | |
451 | fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.', |
|
459 | fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.', | |
452 | dir=backupdir) |
|
460 | dir=backupdir) | |
453 | os.close(fd) |
|
461 | os.close(fd) | |
454 | ui.debug(_('backup %r as %r\n') % (f, tmpname)) |
|
462 | ui.debug(_('backup %r as %r\n') % (f, tmpname)) | |
455 | util.copyfile(repo.wjoin(f), tmpname) |
|
463 | util.copyfile(repo.wjoin(f), tmpname) | |
456 | backups[f] = tmpname |
|
464 | backups[f] = tmpname | |
457 |
|
465 | |||
458 | fp = cStringIO.StringIO() |
|
466 | fp = cStringIO.StringIO() | |
459 | for c in chunks: |
|
467 | for c in chunks: | |
460 | if c.filename() in backups: |
|
468 | if c.filename() in backups: | |
461 | c.write(fp) |
|
469 | c.write(fp) | |
462 | dopatch = fp.tell() |
|
470 | dopatch = fp.tell() | |
463 | fp.seek(0) |
|
471 | fp.seek(0) | |
464 |
|
472 | |||
465 | # 3a. apply filtered patch to clean repo (clean) |
|
473 | # 3a. apply filtered patch to clean repo (clean) | |
466 | if backups: |
|
474 | if backups: | |
467 | hg.revert(repo, repo.dirstate.parents()[0], backups.has_key) |
|
475 | hg.revert(repo, repo.dirstate.parents()[0], backups.has_key) | |
468 |
|
476 | |||
469 | # 3b. (apply) |
|
477 | # 3b. (apply) | |
470 | if dopatch: |
|
478 | if dopatch: | |
471 | try: |
|
479 | try: | |
472 | ui.debug(_('applying patch\n')) |
|
480 | ui.debug(_('applying patch\n')) | |
473 | ui.debug(fp.getvalue()) |
|
481 | ui.debug(fp.getvalue()) | |
474 | pfiles = {} |
|
482 | pfiles = {} | |
475 | patch.internalpatch(fp, ui, 1, repo.root, files=pfiles) |
|
483 | patch.internalpatch(fp, ui, 1, repo.root, files=pfiles) | |
476 | patch.updatedir(ui, repo, pfiles) |
|
484 | patch.updatedir(ui, repo, pfiles) | |
477 | except patch.PatchError, err: |
|
485 | except patch.PatchError, err: | |
478 | s = str(err) |
|
486 | s = str(err) | |
479 | if s: |
|
487 | if s: | |
480 | raise util.Abort(s) |
|
488 | raise util.Abort(s) | |
481 | else: |
|
489 | else: | |
482 | raise util.Abort(_('patch failed to apply')) |
|
490 | raise util.Abort(_('patch failed to apply')) | |
483 | del fp |
|
491 | del fp | |
484 |
|
492 | |||
485 | # 4. We prepared working directory according to filtered patch. |
|
493 | # 4. We prepared working directory according to filtered patch. | |
486 | # Now is the time to delegate the job to commit/qrefresh or the like! |
|
494 | # Now is the time to delegate the job to commit/qrefresh or the like! | |
487 |
|
495 | |||
488 | # it is important to first chdir to repo root -- we'll call a |
|
496 | # it is important to first chdir to repo root -- we'll call a | |
489 | # highlevel command with list of pathnames relative to repo root |
|
497 | # highlevel command with list of pathnames relative to repo root | |
490 | cwd = os.getcwd() |
|
498 | cwd = os.getcwd() | |
491 | os.chdir(repo.root) |
|
499 | os.chdir(repo.root) | |
492 | try: |
|
500 | try: | |
493 | committer(ui, repo, newfiles, opts) |
|
501 | committer(ui, repo, newfiles, opts) | |
494 | finally: |
|
502 | finally: | |
495 | os.chdir(cwd) |
|
503 | os.chdir(cwd) | |
496 |
|
504 | |||
497 | return 0 |
|
505 | return 0 | |
498 | finally: |
|
506 | finally: | |
499 | # 5. finally restore backed-up files |
|
507 | # 5. finally restore backed-up files | |
500 | try: |
|
508 | try: | |
501 | for realname, tmpname in backups.iteritems(): |
|
509 | for realname, tmpname in backups.iteritems(): | |
502 | ui.debug(_('restoring %r to %r\n') % (tmpname, realname)) |
|
510 | ui.debug(_('restoring %r to %r\n') % (tmpname, realname)) | |
503 | util.copyfile(tmpname, repo.wjoin(realname)) |
|
511 | util.copyfile(tmpname, repo.wjoin(realname)) | |
504 | os.unlink(tmpname) |
|
512 | os.unlink(tmpname) | |
505 | os.rmdir(backupdir) |
|
513 | os.rmdir(backupdir) | |
506 | except OSError: |
|
514 | except OSError: | |
507 | pass |
|
515 | pass | |
508 | return cmdutil.commit(ui, repo, recordfunc, pats, opts) |
|
516 | return cmdutil.commit(ui, repo, recordfunc, pats, opts) | |
509 |
|
517 | |||
510 | cmdtable = { |
|
518 | cmdtable = { | |
511 | "record": |
|
519 | "record": | |
512 | (record, |
|
520 | (record, | |
513 |
|
521 | |||
514 | # add commit options |
|
522 | # add commit options | |
515 | commands.table['^commit|ci'][1], |
|
523 | commands.table['^commit|ci'][1], | |
516 |
|
524 | |||
517 | _('hg record [OPTION]... [FILE]...')), |
|
525 | _('hg record [OPTION]... [FILE]...')), | |
518 | } |
|
526 | } | |
519 |
|
527 | |||
520 |
|
528 | |||
521 | def extsetup(): |
|
529 | def extsetup(): | |
522 | try: |
|
530 | try: | |
523 | mq = extensions.find('mq') |
|
531 | mq = extensions.find('mq') | |
524 | except KeyError: |
|
532 | except KeyError: | |
525 | return |
|
533 | return | |
526 |
|
534 | |||
527 | qcmdtable = { |
|
535 | qcmdtable = { | |
528 | "qrecord": |
|
536 | "qrecord": | |
529 | (qrecord, |
|
537 | (qrecord, | |
530 |
|
538 | |||
531 | # add qnew options, except '--force' |
|
539 | # add qnew options, except '--force' | |
532 | [opt for opt in mq.cmdtable['qnew'][1] if opt[1] != 'force'], |
|
540 | [opt for opt in mq.cmdtable['qnew'][1] if opt[1] != 'force'], | |
533 |
|
541 | |||
534 | _('hg qrecord [OPTION]... PATCH [FILE]...')), |
|
542 | _('hg qrecord [OPTION]... PATCH [FILE]...')), | |
535 | } |
|
543 | } | |
536 |
|
544 | |||
537 | cmdtable.update(qcmdtable) |
|
545 | cmdtable.update(qcmdtable) | |
538 |
|
546 |
@@ -1,221 +1,221 b'' | |||||
1 | # filemerge.py - file-level merge handling for Mercurial |
|
1 | # filemerge.py - file-level merge handling for Mercurial | |
2 | # |
|
2 | # | |
3 | # Copyright 2006, 2007, 2008 Matt Mackall <mpm@selenic.com> |
|
3 | # Copyright 2006, 2007, 2008 Matt Mackall <mpm@selenic.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, incorporated herein by reference. |
|
6 | # GNU General Public License version 2, incorporated herein by reference. | |
7 |
|
7 | |||
8 | from node import short |
|
8 | from node import short | |
9 | from i18n import _ |
|
9 | from i18n import _ | |
10 | import util, os, tempfile, simplemerge, re, filecmp |
|
10 | import util, os, tempfile, simplemerge, re, filecmp | |
11 |
|
11 | |||
12 | def _toolstr(ui, tool, part, default=""): |
|
12 | def _toolstr(ui, tool, part, default=""): | |
13 | return ui.config("merge-tools", tool + "." + part, default) |
|
13 | return ui.config("merge-tools", tool + "." + part, default) | |
14 |
|
14 | |||
15 | def _toolbool(ui, tool, part, default=False): |
|
15 | def _toolbool(ui, tool, part, default=False): | |
16 | return ui.configbool("merge-tools", tool + "." + part, default) |
|
16 | return ui.configbool("merge-tools", tool + "." + part, default) | |
17 |
|
17 | |||
18 | def _findtool(ui, tool): |
|
18 | def _findtool(ui, tool): | |
19 | if tool in ("internal:fail", "internal:local", "internal:other"): |
|
19 | if tool in ("internal:fail", "internal:local", "internal:other"): | |
20 | return tool |
|
20 | return tool | |
21 | k = _toolstr(ui, tool, "regkey") |
|
21 | k = _toolstr(ui, tool, "regkey") | |
22 | if k: |
|
22 | if k: | |
23 | p = util.lookup_reg(k, _toolstr(ui, tool, "regname")) |
|
23 | p = util.lookup_reg(k, _toolstr(ui, tool, "regname")) | |
24 | if p: |
|
24 | if p: | |
25 | p = util.find_exe(p + _toolstr(ui, tool, "regappend")) |
|
25 | p = util.find_exe(p + _toolstr(ui, tool, "regappend")) | |
26 | if p: |
|
26 | if p: | |
27 | return p |
|
27 | return p | |
28 | return util.find_exe(_toolstr(ui, tool, "executable", tool)) |
|
28 | return util.find_exe(_toolstr(ui, tool, "executable", tool)) | |
29 |
|
29 | |||
30 | def _picktool(repo, ui, path, binary, symlink): |
|
30 | def _picktool(repo, ui, path, binary, symlink): | |
31 | def check(tool, pat, symlink, binary): |
|
31 | def check(tool, pat, symlink, binary): | |
32 | tmsg = tool |
|
32 | tmsg = tool | |
33 | if pat: |
|
33 | if pat: | |
34 | tmsg += " specified for " + pat |
|
34 | tmsg += " specified for " + pat | |
35 | if not _findtool(ui, tool): |
|
35 | if not _findtool(ui, tool): | |
36 | if pat: # explicitly requested tool deserves a warning |
|
36 | if pat: # explicitly requested tool deserves a warning | |
37 | ui.warn(_("couldn't find merge tool %s\n") % tmsg) |
|
37 | ui.warn(_("couldn't find merge tool %s\n") % tmsg) | |
38 | else: # configured but non-existing tools are more silent |
|
38 | else: # configured but non-existing tools are more silent | |
39 | ui.note(_("couldn't find merge tool %s\n") % tmsg) |
|
39 | ui.note(_("couldn't find merge tool %s\n") % tmsg) | |
40 | elif symlink and not _toolbool(ui, tool, "symlink"): |
|
40 | elif symlink and not _toolbool(ui, tool, "symlink"): | |
41 | ui.warn(_("tool %s can't handle symlinks\n") % tmsg) |
|
41 | ui.warn(_("tool %s can't handle symlinks\n") % tmsg) | |
42 | elif binary and not _toolbool(ui, tool, "binary"): |
|
42 | elif binary and not _toolbool(ui, tool, "binary"): | |
43 | ui.warn(_("tool %s can't handle binary\n") % tmsg) |
|
43 | ui.warn(_("tool %s can't handle binary\n") % tmsg) | |
44 | elif not util.gui() and _toolbool(ui, tool, "gui"): |
|
44 | elif not util.gui() and _toolbool(ui, tool, "gui"): | |
45 | ui.warn(_("tool %s requires a GUI\n") % tmsg) |
|
45 | ui.warn(_("tool %s requires a GUI\n") % tmsg) | |
46 | else: |
|
46 | else: | |
47 | return True |
|
47 | return True | |
48 | return False |
|
48 | return False | |
49 |
|
49 | |||
50 | # HGMERGE takes precedence |
|
50 | # HGMERGE takes precedence | |
51 | hgmerge = os.environ.get("HGMERGE") |
|
51 | hgmerge = os.environ.get("HGMERGE") | |
52 | if hgmerge: |
|
52 | if hgmerge: | |
53 | return (hgmerge, hgmerge) |
|
53 | return (hgmerge, hgmerge) | |
54 |
|
54 | |||
55 | # then patterns |
|
55 | # then patterns | |
56 | for pat, tool in ui.configitems("merge-patterns"): |
|
56 | for pat, tool in ui.configitems("merge-patterns"): | |
57 | mf = util.matcher(repo.root, "", [pat], [], [])[1] |
|
57 | mf = util.matcher(repo.root, "", [pat], [], [])[1] | |
58 | if mf(path) and check(tool, pat, symlink, False): |
|
58 | if mf(path) and check(tool, pat, symlink, False): | |
59 | toolpath = _findtool(ui, tool) |
|
59 | toolpath = _findtool(ui, tool) | |
60 | return (tool, '"' + toolpath + '"') |
|
60 | return (tool, '"' + toolpath + '"') | |
61 |
|
61 | |||
62 | # then merge tools |
|
62 | # then merge tools | |
63 | tools = {} |
|
63 | tools = {} | |
64 | for k,v in ui.configitems("merge-tools"): |
|
64 | for k,v in ui.configitems("merge-tools"): | |
65 | t = k.split('.')[0] |
|
65 | t = k.split('.')[0] | |
66 | if t not in tools: |
|
66 | if t not in tools: | |
67 | tools[t] = int(_toolstr(ui, t, "priority", "0")) |
|
67 | tools[t] = int(_toolstr(ui, t, "priority", "0")) | |
68 | names = tools.keys() |
|
68 | names = tools.keys() | |
69 | tools = sorted([(-p,t) for t,p in tools.items()]) |
|
69 | tools = sorted([(-p,t) for t,p in tools.items()]) | |
70 | uimerge = ui.config("ui", "merge") |
|
70 | uimerge = ui.config("ui", "merge") | |
71 | if uimerge: |
|
71 | if uimerge: | |
72 | if uimerge not in names: |
|
72 | if uimerge not in names: | |
73 | return (uimerge, uimerge) |
|
73 | return (uimerge, uimerge) | |
74 | tools.insert(0, (None, uimerge)) # highest priority |
|
74 | tools.insert(0, (None, uimerge)) # highest priority | |
75 | tools.append((None, "hgmerge")) # the old default, if found |
|
75 | tools.append((None, "hgmerge")) # the old default, if found | |
76 | for p,t in tools: |
|
76 | for p,t in tools: | |
77 | if check(t, None, symlink, binary): |
|
77 | if check(t, None, symlink, binary): | |
78 | toolpath = _findtool(ui, t) |
|
78 | toolpath = _findtool(ui, t) | |
79 | return (t, '"' + toolpath + '"') |
|
79 | return (t, '"' + toolpath + '"') | |
80 | # internal merge as last resort |
|
80 | # internal merge as last resort | |
81 | return (not (symlink or binary) and "internal:merge" or None, None) |
|
81 | return (not (symlink or binary) and "internal:merge" or None, None) | |
82 |
|
82 | |||
83 | def _eoltype(data): |
|
83 | def _eoltype(data): | |
84 | "Guess the EOL type of a file" |
|
84 | "Guess the EOL type of a file" | |
85 | if '\0' in data: # binary |
|
85 | if '\0' in data: # binary | |
86 | return None |
|
86 | return None | |
87 | if '\r\n' in data: # Windows |
|
87 | if '\r\n' in data: # Windows | |
88 | return '\r\n' |
|
88 | return '\r\n' | |
89 | if '\r' in data: # Old Mac |
|
89 | if '\r' in data: # Old Mac | |
90 | return '\r' |
|
90 | return '\r' | |
91 | if '\n' in data: # UNIX |
|
91 | if '\n' in data: # UNIX | |
92 | return '\n' |
|
92 | return '\n' | |
93 | return None # unknown |
|
93 | return None # unknown | |
94 |
|
94 | |||
95 | def _matcheol(file, origfile): |
|
95 | def _matcheol(file, origfile): | |
96 | "Convert EOL markers in a file to match origfile" |
|
96 | "Convert EOL markers in a file to match origfile" | |
97 | tostyle = _eoltype(open(origfile, "rb").read()) |
|
97 | tostyle = _eoltype(open(origfile, "rb").read()) | |
98 | if tostyle: |
|
98 | if tostyle: | |
99 | data = open(file, "rb").read() |
|
99 | data = open(file, "rb").read() | |
100 | style = _eoltype(data) |
|
100 | style = _eoltype(data) | |
101 | if style: |
|
101 | if style: | |
102 | newdata = data.replace(style, tostyle) |
|
102 | newdata = data.replace(style, tostyle) | |
103 | if newdata != data: |
|
103 | if newdata != data: | |
104 | open(file, "wb").write(newdata) |
|
104 | open(file, "wb").write(newdata) | |
105 |
|
105 | |||
106 | def filemerge(repo, mynode, orig, fcd, fco, fca): |
|
106 | def filemerge(repo, mynode, orig, fcd, fco, fca): | |
107 | """perform a 3-way merge in the working directory |
|
107 | """perform a 3-way merge in the working directory | |
108 |
|
108 | |||
109 | mynode = parent node before merge |
|
109 | mynode = parent node before merge | |
110 | orig = original local filename before merge |
|
110 | orig = original local filename before merge | |
111 | fco = other file context |
|
111 | fco = other file context | |
112 | fca = ancestor file context |
|
112 | fca = ancestor file context | |
113 | fcd = local file context for current/destination file |
|
113 | fcd = local file context for current/destination file | |
114 | """ |
|
114 | """ | |
115 |
|
115 | |||
116 | def temp(prefix, ctx): |
|
116 | def temp(prefix, ctx): | |
117 | pre = "%s~%s." % (os.path.basename(ctx.path()), prefix) |
|
117 | pre = "%s~%s." % (os.path.basename(ctx.path()), prefix) | |
118 | (fd, name) = tempfile.mkstemp(prefix=pre) |
|
118 | (fd, name) = tempfile.mkstemp(prefix=pre) | |
119 | data = repo.wwritedata(ctx.path(), ctx.data()) |
|
119 | data = repo.wwritedata(ctx.path(), ctx.data()) | |
120 | f = os.fdopen(fd, "wb") |
|
120 | f = os.fdopen(fd, "wb") | |
121 | f.write(data) |
|
121 | f.write(data) | |
122 | f.close() |
|
122 | f.close() | |
123 | return name |
|
123 | return name | |
124 |
|
124 | |||
125 | def isbin(ctx): |
|
125 | def isbin(ctx): | |
126 | try: |
|
126 | try: | |
127 | return util.binary(ctx.data()) |
|
127 | return util.binary(ctx.data()) | |
128 | except IOError: |
|
128 | except IOError: | |
129 | return False |
|
129 | return False | |
130 |
|
130 | |||
131 | if not fco.cmp(fcd.data()): # files identical? |
|
131 | if not fco.cmp(fcd.data()): # files identical? | |
132 | return None |
|
132 | return None | |
133 |
|
133 | |||
134 | ui = repo.ui |
|
134 | ui = repo.ui | |
135 | fd = fcd.path() |
|
135 | fd = fcd.path() | |
136 | binary = isbin(fcd) or isbin(fco) or isbin(fca) |
|
136 | binary = isbin(fcd) or isbin(fco) or isbin(fca) | |
137 | symlink = 'l' in fcd.flags() + fco.flags() |
|
137 | symlink = 'l' in fcd.flags() + fco.flags() | |
138 | tool, toolpath = _picktool(repo, ui, fd, binary, symlink) |
|
138 | tool, toolpath = _picktool(repo, ui, fd, binary, symlink) | |
139 | ui.debug(_("picked tool '%s' for %s (binary %s symlink %s)\n") % |
|
139 | ui.debug(_("picked tool '%s' for %s (binary %s symlink %s)\n") % | |
140 | (tool, fd, binary, symlink)) |
|
140 | (tool, fd, binary, symlink)) | |
141 |
|
141 | |||
142 | if not tool: |
|
142 | if not tool: | |
143 | tool = "internal:local" |
|
143 | tool = "internal:local" | |
144 | if ui.prompt(_(" no tool found to merge %s\n" |
|
144 | if ui.prompt(_(" no tool found to merge %s\n" | |
145 | "keep (l)ocal or take (o)ther?") % fd, |
|
145 | "keep (l)ocal or take (o)ther?") % fd, | |
146 |
_(" |
|
146 | (_("&Local"), _("&Other")), _("l")) != _("l"): | |
147 | tool = "internal:other" |
|
147 | tool = "internal:other" | |
148 | if tool == "internal:local": |
|
148 | if tool == "internal:local": | |
149 | return 0 |
|
149 | return 0 | |
150 | if tool == "internal:other": |
|
150 | if tool == "internal:other": | |
151 | repo.wwrite(fd, fco.data(), fco.flags()) |
|
151 | repo.wwrite(fd, fco.data(), fco.flags()) | |
152 | return 0 |
|
152 | return 0 | |
153 | if tool == "internal:fail": |
|
153 | if tool == "internal:fail": | |
154 | return 1 |
|
154 | return 1 | |
155 |
|
155 | |||
156 | # do the actual merge |
|
156 | # do the actual merge | |
157 | a = repo.wjoin(fd) |
|
157 | a = repo.wjoin(fd) | |
158 | b = temp("base", fca) |
|
158 | b = temp("base", fca) | |
159 | c = temp("other", fco) |
|
159 | c = temp("other", fco) | |
160 | out = "" |
|
160 | out = "" | |
161 | back = a + ".orig" |
|
161 | back = a + ".orig" | |
162 | util.copyfile(a, back) |
|
162 | util.copyfile(a, back) | |
163 |
|
163 | |||
164 | if orig != fco.path(): |
|
164 | if orig != fco.path(): | |
165 | repo.ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd)) |
|
165 | repo.ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd)) | |
166 | else: |
|
166 | else: | |
167 | repo.ui.status(_("merging %s\n") % fd) |
|
167 | repo.ui.status(_("merging %s\n") % fd) | |
168 |
|
168 | |||
169 | repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcd, fco, fca)) |
|
169 | repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcd, fco, fca)) | |
170 |
|
170 | |||
171 | # do we attempt to simplemerge first? |
|
171 | # do we attempt to simplemerge first? | |
172 | if _toolbool(ui, tool, "premerge", not (binary or symlink)): |
|
172 | if _toolbool(ui, tool, "premerge", not (binary or symlink)): | |
173 | r = simplemerge.simplemerge(a, b, c, quiet=True) |
|
173 | r = simplemerge.simplemerge(a, b, c, quiet=True) | |
174 | if not r: |
|
174 | if not r: | |
175 | ui.debug(_(" premerge successful\n")) |
|
175 | ui.debug(_(" premerge successful\n")) | |
176 | os.unlink(back) |
|
176 | os.unlink(back) | |
177 | os.unlink(b) |
|
177 | os.unlink(b) | |
178 | os.unlink(c) |
|
178 | os.unlink(c) | |
179 | return 0 |
|
179 | return 0 | |
180 | util.copyfile(back, a) # restore from backup and try again |
|
180 | util.copyfile(back, a) # restore from backup and try again | |
181 |
|
181 | |||
182 | env = dict(HG_FILE=fd, |
|
182 | env = dict(HG_FILE=fd, | |
183 | HG_MY_NODE=short(mynode), |
|
183 | HG_MY_NODE=short(mynode), | |
184 | HG_OTHER_NODE=str(fco.changectx()), |
|
184 | HG_OTHER_NODE=str(fco.changectx()), | |
185 | HG_MY_ISLINK='l' in fcd.flags(), |
|
185 | HG_MY_ISLINK='l' in fcd.flags(), | |
186 | HG_OTHER_ISLINK='l' in fco.flags(), |
|
186 | HG_OTHER_ISLINK='l' in fco.flags(), | |
187 | HG_BASE_ISLINK='l' in fca.flags()) |
|
187 | HG_BASE_ISLINK='l' in fca.flags()) | |
188 |
|
188 | |||
189 | if tool == "internal:merge": |
|
189 | if tool == "internal:merge": | |
190 | r = simplemerge.simplemerge(a, b, c, label=['local', 'other']) |
|
190 | r = simplemerge.simplemerge(a, b, c, label=['local', 'other']) | |
191 | else: |
|
191 | else: | |
192 | args = _toolstr(ui, tool, "args", '$local $base $other') |
|
192 | args = _toolstr(ui, tool, "args", '$local $base $other') | |
193 | if "$output" in args: |
|
193 | if "$output" in args: | |
194 | out, a = a, back # read input from backup, write to original |
|
194 | out, a = a, back # read input from backup, write to original | |
195 | replace = dict(local=a, base=b, other=c, output=out) |
|
195 | replace = dict(local=a, base=b, other=c, output=out) | |
196 | args = re.sub("\$(local|base|other|output)", |
|
196 | args = re.sub("\$(local|base|other|output)", | |
197 | lambda x: '"%s"' % replace[x.group()[1:]], args) |
|
197 | lambda x: '"%s"' % replace[x.group()[1:]], args) | |
198 | r = util.system(toolpath + ' ' + args, cwd=repo.root, environ=env) |
|
198 | r = util.system(toolpath + ' ' + args, cwd=repo.root, environ=env) | |
199 |
|
199 | |||
200 | if not r and _toolbool(ui, tool, "checkconflicts"): |
|
200 | if not r and _toolbool(ui, tool, "checkconflicts"): | |
201 | if re.match("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data()): |
|
201 | if re.match("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data()): | |
202 | r = 1 |
|
202 | r = 1 | |
203 |
|
203 | |||
204 | if not r and _toolbool(ui, tool, "checkchanged"): |
|
204 | if not r and _toolbool(ui, tool, "checkchanged"): | |
205 | if filecmp.cmp(repo.wjoin(fd), back): |
|
205 | if filecmp.cmp(repo.wjoin(fd), back): | |
206 | if ui.prompt(_(" output file %s appears unchanged\n" |
|
206 | if ui.prompt(_(" output file %s appears unchanged\n" | |
207 | "was merge successful (yn)?") % fd, |
|
207 | "was merge successful (yn)?") % fd, | |
208 |
_(" |
|
208 | (_("&Yes"), _("&No")), _("n")) != _("y"): | |
209 | r = 1 |
|
209 | r = 1 | |
210 |
|
210 | |||
211 | if _toolbool(ui, tool, "fixeol"): |
|
211 | if _toolbool(ui, tool, "fixeol"): | |
212 | _matcheol(repo.wjoin(fd), back) |
|
212 | _matcheol(repo.wjoin(fd), back) | |
213 |
|
213 | |||
214 | if r: |
|
214 | if r: | |
215 | repo.ui.warn(_("merging %s failed!\n") % fd) |
|
215 | repo.ui.warn(_("merging %s failed!\n") % fd) | |
216 | else: |
|
216 | else: | |
217 | os.unlink(back) |
|
217 | os.unlink(back) | |
218 |
|
218 | |||
219 | os.unlink(b) |
|
219 | os.unlink(b) | |
220 | os.unlink(c) |
|
220 | os.unlink(c) | |
221 | return r |
|
221 | return r |
@@ -1,507 +1,508 b'' | |||||
1 | # merge.py - directory-level update/merge handling for Mercurial |
|
1 | # merge.py - directory-level update/merge handling for Mercurial | |
2 | # |
|
2 | # | |
3 | # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com> |
|
3 | # Copyright 2006, 2007 Matt Mackall <mpm@selenic.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, incorporated herein by reference. |
|
6 | # GNU General Public License version 2, incorporated herein by reference. | |
7 |
|
7 | |||
8 | from node import nullid, nullrev, hex, bin |
|
8 | from node import nullid, nullrev, hex, bin | |
9 | from i18n import _ |
|
9 | from i18n import _ | |
10 | import errno, util, os, filemerge, copies, shutil |
|
10 | import errno, util, os, filemerge, copies, shutil | |
11 |
|
11 | |||
12 | class mergestate(object): |
|
12 | class mergestate(object): | |
13 | '''track 3-way merge state of individual files''' |
|
13 | '''track 3-way merge state of individual files''' | |
14 | def __init__(self, repo): |
|
14 | def __init__(self, repo): | |
15 | self._repo = repo |
|
15 | self._repo = repo | |
16 | self._read() |
|
16 | self._read() | |
17 | def reset(self, node=None): |
|
17 | def reset(self, node=None): | |
18 | self._state = {} |
|
18 | self._state = {} | |
19 | if node: |
|
19 | if node: | |
20 | self._local = node |
|
20 | self._local = node | |
21 | shutil.rmtree(self._repo.join("merge"), True) |
|
21 | shutil.rmtree(self._repo.join("merge"), True) | |
22 | def _read(self): |
|
22 | def _read(self): | |
23 | self._state = {} |
|
23 | self._state = {} | |
24 | try: |
|
24 | try: | |
25 | localnode = None |
|
25 | localnode = None | |
26 | f = self._repo.opener("merge/state") |
|
26 | f = self._repo.opener("merge/state") | |
27 | for i, l in enumerate(f): |
|
27 | for i, l in enumerate(f): | |
28 | if i == 0: |
|
28 | if i == 0: | |
29 | localnode = l[:-1] |
|
29 | localnode = l[:-1] | |
30 | else: |
|
30 | else: | |
31 | bits = l[:-1].split("\0") |
|
31 | bits = l[:-1].split("\0") | |
32 | self._state[bits[0]] = bits[1:] |
|
32 | self._state[bits[0]] = bits[1:] | |
33 | self._local = bin(localnode) |
|
33 | self._local = bin(localnode) | |
34 | except IOError, err: |
|
34 | except IOError, err: | |
35 | if err.errno != errno.ENOENT: |
|
35 | if err.errno != errno.ENOENT: | |
36 | raise |
|
36 | raise | |
37 | def _write(self): |
|
37 | def _write(self): | |
38 | f = self._repo.opener("merge/state", "w") |
|
38 | f = self._repo.opener("merge/state", "w") | |
39 | f.write(hex(self._local) + "\n") |
|
39 | f.write(hex(self._local) + "\n") | |
40 | for d, v in self._state.iteritems(): |
|
40 | for d, v in self._state.iteritems(): | |
41 | f.write("\0".join([d] + v) + "\n") |
|
41 | f.write("\0".join([d] + v) + "\n") | |
42 | def add(self, fcl, fco, fca, fd, flags): |
|
42 | def add(self, fcl, fco, fca, fd, flags): | |
43 | hash = util.sha1(fcl.path()).hexdigest() |
|
43 | hash = util.sha1(fcl.path()).hexdigest() | |
44 | self._repo.opener("merge/" + hash, "w").write(fcl.data()) |
|
44 | self._repo.opener("merge/" + hash, "w").write(fcl.data()) | |
45 | self._state[fd] = ['u', hash, fcl.path(), fca.path(), |
|
45 | self._state[fd] = ['u', hash, fcl.path(), fca.path(), | |
46 | hex(fca.filenode()), fco.path(), flags] |
|
46 | hex(fca.filenode()), fco.path(), flags] | |
47 | self._write() |
|
47 | self._write() | |
48 | def __contains__(self, dfile): |
|
48 | def __contains__(self, dfile): | |
49 | return dfile in self._state |
|
49 | return dfile in self._state | |
50 | def __getitem__(self, dfile): |
|
50 | def __getitem__(self, dfile): | |
51 | return self._state[dfile][0] |
|
51 | return self._state[dfile][0] | |
52 | def __iter__(self): |
|
52 | def __iter__(self): | |
53 | l = self._state.keys() |
|
53 | l = self._state.keys() | |
54 | l.sort() |
|
54 | l.sort() | |
55 | for f in l: |
|
55 | for f in l: | |
56 | yield f |
|
56 | yield f | |
57 | def mark(self, dfile, state): |
|
57 | def mark(self, dfile, state): | |
58 | self._state[dfile][0] = state |
|
58 | self._state[dfile][0] = state | |
59 | self._write() |
|
59 | self._write() | |
60 | def resolve(self, dfile, wctx, octx): |
|
60 | def resolve(self, dfile, wctx, octx): | |
61 | if self[dfile] == 'r': |
|
61 | if self[dfile] == 'r': | |
62 | return 0 |
|
62 | return 0 | |
63 | state, hash, lfile, afile, anode, ofile, flags = self._state[dfile] |
|
63 | state, hash, lfile, afile, anode, ofile, flags = self._state[dfile] | |
64 | f = self._repo.opener("merge/" + hash) |
|
64 | f = self._repo.opener("merge/" + hash) | |
65 | self._repo.wwrite(dfile, f.read(), flags) |
|
65 | self._repo.wwrite(dfile, f.read(), flags) | |
66 | fcd = wctx[dfile] |
|
66 | fcd = wctx[dfile] | |
67 | fco = octx[ofile] |
|
67 | fco = octx[ofile] | |
68 | fca = self._repo.filectx(afile, fileid=anode) |
|
68 | fca = self._repo.filectx(afile, fileid=anode) | |
69 | r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca) |
|
69 | r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca) | |
70 | if not r: |
|
70 | if not r: | |
71 | self.mark(dfile, 'r') |
|
71 | self.mark(dfile, 'r') | |
72 | return r |
|
72 | return r | |
73 |
|
73 | |||
74 | def _checkunknown(wctx, mctx): |
|
74 | def _checkunknown(wctx, mctx): | |
75 | "check for collisions between unknown files and files in mctx" |
|
75 | "check for collisions between unknown files and files in mctx" | |
76 | for f in wctx.unknown(): |
|
76 | for f in wctx.unknown(): | |
77 | if f in mctx and mctx[f].cmp(wctx[f].data()): |
|
77 | if f in mctx and mctx[f].cmp(wctx[f].data()): | |
78 | raise util.Abort(_("untracked file in working directory differs" |
|
78 | raise util.Abort(_("untracked file in working directory differs" | |
79 | " from file in requested revision: '%s'") % f) |
|
79 | " from file in requested revision: '%s'") % f) | |
80 |
|
80 | |||
81 | def _checkcollision(mctx): |
|
81 | def _checkcollision(mctx): | |
82 | "check for case folding collisions in the destination context" |
|
82 | "check for case folding collisions in the destination context" | |
83 | folded = {} |
|
83 | folded = {} | |
84 | for fn in mctx: |
|
84 | for fn in mctx: | |
85 | fold = fn.lower() |
|
85 | fold = fn.lower() | |
86 | if fold in folded: |
|
86 | if fold in folded: | |
87 | raise util.Abort(_("case-folding collision between %s and %s") |
|
87 | raise util.Abort(_("case-folding collision between %s and %s") | |
88 | % (fn, folded[fold])) |
|
88 | % (fn, folded[fold])) | |
89 | folded[fold] = fn |
|
89 | folded[fold] = fn | |
90 |
|
90 | |||
91 | def _forgetremoved(wctx, mctx, branchmerge): |
|
91 | def _forgetremoved(wctx, mctx, branchmerge): | |
92 | """ |
|
92 | """ | |
93 | Forget removed files |
|
93 | Forget removed files | |
94 |
|
94 | |||
95 | If we're jumping between revisions (as opposed to merging), and if |
|
95 | If we're jumping between revisions (as opposed to merging), and if | |
96 | neither the working directory nor the target rev has the file, |
|
96 | neither the working directory nor the target rev has the file, | |
97 | then we need to remove it from the dirstate, to prevent the |
|
97 | then we need to remove it from the dirstate, to prevent the | |
98 | dirstate from listing the file when it is no longer in the |
|
98 | dirstate from listing the file when it is no longer in the | |
99 | manifest. |
|
99 | manifest. | |
100 |
|
100 | |||
101 | If we're merging, and the other revision has removed a file |
|
101 | If we're merging, and the other revision has removed a file | |
102 | that is not present in the working directory, we need to mark it |
|
102 | that is not present in the working directory, we need to mark it | |
103 | as removed. |
|
103 | as removed. | |
104 | """ |
|
104 | """ | |
105 |
|
105 | |||
106 | action = [] |
|
106 | action = [] | |
107 | state = branchmerge and 'r' or 'f' |
|
107 | state = branchmerge and 'r' or 'f' | |
108 | for f in wctx.deleted(): |
|
108 | for f in wctx.deleted(): | |
109 | if f not in mctx: |
|
109 | if f not in mctx: | |
110 | action.append((f, state)) |
|
110 | action.append((f, state)) | |
111 |
|
111 | |||
112 | if not branchmerge: |
|
112 | if not branchmerge: | |
113 | for f in wctx.removed(): |
|
113 | for f in wctx.removed(): | |
114 | if f not in mctx: |
|
114 | if f not in mctx: | |
115 | action.append((f, "f")) |
|
115 | action.append((f, "f")) | |
116 |
|
116 | |||
117 | return action |
|
117 | return action | |
118 |
|
118 | |||
119 | def manifestmerge(repo, p1, p2, pa, overwrite, partial): |
|
119 | def manifestmerge(repo, p1, p2, pa, overwrite, partial): | |
120 | """ |
|
120 | """ | |
121 | Merge p1 and p2 with ancestor ma and generate merge action list |
|
121 | Merge p1 and p2 with ancestor ma and generate merge action list | |
122 |
|
122 | |||
123 | overwrite = whether we clobber working files |
|
123 | overwrite = whether we clobber working files | |
124 | partial = function to filter file lists |
|
124 | partial = function to filter file lists | |
125 | """ |
|
125 | """ | |
126 |
|
126 | |||
127 | repo.ui.note(_("resolving manifests\n")) |
|
127 | repo.ui.note(_("resolving manifests\n")) | |
128 | repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial))) |
|
128 | repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial))) | |
129 | repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2)) |
|
129 | repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2)) | |
130 |
|
130 | |||
131 | m1 = p1.manifest() |
|
131 | m1 = p1.manifest() | |
132 | m2 = p2.manifest() |
|
132 | m2 = p2.manifest() | |
133 | ma = pa.manifest() |
|
133 | ma = pa.manifest() | |
134 | backwards = (pa == p2) |
|
134 | backwards = (pa == p2) | |
135 | action = [] |
|
135 | action = [] | |
136 | copy, copied, diverge = {}, {}, {} |
|
136 | copy, copied, diverge = {}, {}, {} | |
137 |
|
137 | |||
138 | def fmerge(f, f2=None, fa=None): |
|
138 | def fmerge(f, f2=None, fa=None): | |
139 | """merge flags""" |
|
139 | """merge flags""" | |
140 | if not f2: |
|
140 | if not f2: | |
141 | f2 = f |
|
141 | f2 = f | |
142 | fa = f |
|
142 | fa = f | |
143 | a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2) |
|
143 | a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2) | |
144 | if m == n: # flags agree |
|
144 | if m == n: # flags agree | |
145 | return m # unchanged |
|
145 | return m # unchanged | |
146 | if m and n: # flags are set but don't agree |
|
146 | if m and n: # flags are set but don't agree | |
147 | if not a: # both differ from parent |
|
147 | if not a: # both differ from parent | |
148 | r = repo.ui.prompt( |
|
148 | r = repo.ui.prompt( | |
149 | _(" conflicting flags for %s\n" |
|
149 | _(" conflicting flags for %s\n" | |
150 |
"(n)one, e(x)ec or sym(l)ink?") % f, |
|
150 | "(n)one, e(x)ec or sym(l)ink?") % f, | |
151 | return r != "n" and r or '' |
|
151 | (_("&None"), _("E&xec"), _("Sym&link")), _("n")) | |
|
152 | return r != _("n") and r or '' | |||
152 | if m == a: |
|
153 | if m == a: | |
153 | return n # changed from m to n |
|
154 | return n # changed from m to n | |
154 | return m # changed from n to m |
|
155 | return m # changed from n to m | |
155 | if m and m != a: # changed from a to m |
|
156 | if m and m != a: # changed from a to m | |
156 | return m |
|
157 | return m | |
157 | if n and n != a: # changed from a to n |
|
158 | if n and n != a: # changed from a to n | |
158 | return n |
|
159 | return n | |
159 | return '' # flag was cleared |
|
160 | return '' # flag was cleared | |
160 |
|
161 | |||
161 | def act(msg, m, f, *args): |
|
162 | def act(msg, m, f, *args): | |
162 | repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m)) |
|
163 | repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m)) | |
163 | action.append((f, m) + args) |
|
164 | action.append((f, m) + args) | |
164 |
|
165 | |||
165 | if pa and not (backwards or overwrite): |
|
166 | if pa and not (backwards or overwrite): | |
166 | if repo.ui.configbool("merge", "followcopies", True): |
|
167 | if repo.ui.configbool("merge", "followcopies", True): | |
167 | dirs = repo.ui.configbool("merge", "followdirs", True) |
|
168 | dirs = repo.ui.configbool("merge", "followdirs", True) | |
168 | copy, diverge = copies.copies(repo, p1, p2, pa, dirs) |
|
169 | copy, diverge = copies.copies(repo, p1, p2, pa, dirs) | |
169 | copied = set(copy.values()) |
|
170 | copied = set(copy.values()) | |
170 | for of, fl in diverge.iteritems(): |
|
171 | for of, fl in diverge.iteritems(): | |
171 | act("divergent renames", "dr", of, fl) |
|
172 | act("divergent renames", "dr", of, fl) | |
172 |
|
173 | |||
173 | # Compare manifests |
|
174 | # Compare manifests | |
174 | for f, n in m1.iteritems(): |
|
175 | for f, n in m1.iteritems(): | |
175 | if partial and not partial(f): |
|
176 | if partial and not partial(f): | |
176 | continue |
|
177 | continue | |
177 | if f in m2: |
|
178 | if f in m2: | |
178 | if overwrite or backwards: |
|
179 | if overwrite or backwards: | |
179 | rflags = m2.flags(f) |
|
180 | rflags = m2.flags(f) | |
180 | else: |
|
181 | else: | |
181 | rflags = fmerge(f) |
|
182 | rflags = fmerge(f) | |
182 | # are files different? |
|
183 | # are files different? | |
183 | if n != m2[f]: |
|
184 | if n != m2[f]: | |
184 | a = ma.get(f, nullid) |
|
185 | a = ma.get(f, nullid) | |
185 | # are we clobbering? |
|
186 | # are we clobbering? | |
186 | if overwrite: |
|
187 | if overwrite: | |
187 | act("clobbering", "g", f, rflags) |
|
188 | act("clobbering", "g", f, rflags) | |
188 | # or are we going back in time and clean? |
|
189 | # or are we going back in time and clean? | |
189 | elif backwards: |
|
190 | elif backwards: | |
190 | if not n[20:] or not p2[f].cmp(p1[f].data()): |
|
191 | if not n[20:] or not p2[f].cmp(p1[f].data()): | |
191 | act("reverting", "g", f, rflags) |
|
192 | act("reverting", "g", f, rflags) | |
192 | # are both different from the ancestor? |
|
193 | # are both different from the ancestor? | |
193 | elif n != a and m2[f] != a: |
|
194 | elif n != a and m2[f] != a: | |
194 | act("versions differ", "m", f, f, f, rflags, False) |
|
195 | act("versions differ", "m", f, f, f, rflags, False) | |
195 | # is remote's version newer? |
|
196 | # is remote's version newer? | |
196 | elif m2[f] != a: |
|
197 | elif m2[f] != a: | |
197 | act("remote is newer", "g", f, rflags) |
|
198 | act("remote is newer", "g", f, rflags) | |
198 | # local is newer, not overwrite, check mode bits |
|
199 | # local is newer, not overwrite, check mode bits | |
199 | elif m1.flags(f) != rflags: |
|
200 | elif m1.flags(f) != rflags: | |
200 | act("update permissions", "e", f, rflags) |
|
201 | act("update permissions", "e", f, rflags) | |
201 | # contents same, check mode bits |
|
202 | # contents same, check mode bits | |
202 | elif m1.flags(f) != rflags: |
|
203 | elif m1.flags(f) != rflags: | |
203 | act("update permissions", "e", f, rflags) |
|
204 | act("update permissions", "e", f, rflags) | |
204 | elif f in copied: |
|
205 | elif f in copied: | |
205 | continue |
|
206 | continue | |
206 | elif f in copy: |
|
207 | elif f in copy: | |
207 | f2 = copy[f] |
|
208 | f2 = copy[f] | |
208 | if f2 not in m2: # directory rename |
|
209 | if f2 not in m2: # directory rename | |
209 | act("remote renamed directory to " + f2, "d", |
|
210 | act("remote renamed directory to " + f2, "d", | |
210 | f, None, f2, m1.flags(f)) |
|
211 | f, None, f2, m1.flags(f)) | |
211 | elif f2 in m1: # case 2 A,B/B/B |
|
212 | elif f2 in m1: # case 2 A,B/B/B | |
212 | act("local copied to " + f2, "m", |
|
213 | act("local copied to " + f2, "m", | |
213 | f, f2, f, fmerge(f, f2, f2), False) |
|
214 | f, f2, f, fmerge(f, f2, f2), False) | |
214 | else: # case 4,21 A/B/B |
|
215 | else: # case 4,21 A/B/B | |
215 | act("local moved to " + f2, "m", |
|
216 | act("local moved to " + f2, "m", | |
216 | f, f2, f, fmerge(f, f2, f2), False) |
|
217 | f, f2, f, fmerge(f, f2, f2), False) | |
217 | elif f in ma: |
|
218 | elif f in ma: | |
218 | if n != ma[f] and not overwrite: |
|
219 | if n != ma[f] and not overwrite: | |
219 | if repo.ui.prompt( |
|
220 | if repo.ui.prompt( | |
220 | _(" local changed %s which remote deleted\n" |
|
221 | _(" local changed %s which remote deleted\n" | |
221 | "use (c)hanged version or (d)elete?") % f, |
|
222 | "use (c)hanged version or (d)elete?") % f, | |
222 |
_(" |
|
223 | (_("&Changed"), _("&Delete")), _("c")) == _("d"): | |
223 | act("prompt delete", "r", f) |
|
224 | act("prompt delete", "r", f) | |
224 | act("prompt keep", "a", f) |
|
225 | act("prompt keep", "a", f) | |
225 | else: |
|
226 | else: | |
226 | act("other deleted", "r", f) |
|
227 | act("other deleted", "r", f) | |
227 | else: |
|
228 | else: | |
228 | # file is created on branch or in working directory |
|
229 | # file is created on branch or in working directory | |
229 | if (overwrite and n[20:] != "u") or (backwards and not n[20:]): |
|
230 | if (overwrite and n[20:] != "u") or (backwards and not n[20:]): | |
230 | act("remote deleted", "r", f) |
|
231 | act("remote deleted", "r", f) | |
231 |
|
232 | |||
232 | for f, n in m2.iteritems(): |
|
233 | for f, n in m2.iteritems(): | |
233 | if partial and not partial(f): |
|
234 | if partial and not partial(f): | |
234 | continue |
|
235 | continue | |
235 | if f in m1: |
|
236 | if f in m1: | |
236 | continue |
|
237 | continue | |
237 | if f in copied: |
|
238 | if f in copied: | |
238 | continue |
|
239 | continue | |
239 | if f in copy: |
|
240 | if f in copy: | |
240 | f2 = copy[f] |
|
241 | f2 = copy[f] | |
241 | if f2 not in m1: # directory rename |
|
242 | if f2 not in m1: # directory rename | |
242 | act("local renamed directory to " + f2, "d", |
|
243 | act("local renamed directory to " + f2, "d", | |
243 | None, f, f2, m2.flags(f)) |
|
244 | None, f, f2, m2.flags(f)) | |
244 | elif f2 in m2: # rename case 1, A/A,B/A |
|
245 | elif f2 in m2: # rename case 1, A/A,B/A | |
245 | act("remote copied to " + f, "m", |
|
246 | act("remote copied to " + f, "m", | |
246 | f2, f, f, fmerge(f2, f, f2), False) |
|
247 | f2, f, f, fmerge(f2, f, f2), False) | |
247 | else: # case 3,20 A/B/A |
|
248 | else: # case 3,20 A/B/A | |
248 | act("remote moved to " + f, "m", |
|
249 | act("remote moved to " + f, "m", | |
249 | f2, f, f, fmerge(f2, f, f2), True) |
|
250 | f2, f, f, fmerge(f2, f, f2), True) | |
250 | elif f in ma: |
|
251 | elif f in ma: | |
251 | if overwrite or backwards: |
|
252 | if overwrite or backwards: | |
252 | act("recreating", "g", f, m2.flags(f)) |
|
253 | act("recreating", "g", f, m2.flags(f)) | |
253 | elif n != ma[f]: |
|
254 | elif n != ma[f]: | |
254 | if repo.ui.prompt( |
|
255 | if repo.ui.prompt( | |
255 | _("remote changed %s which local deleted\n" |
|
256 | _("remote changed %s which local deleted\n" | |
256 | "use (c)hanged version or leave (d)eleted?") % f, |
|
257 | "use (c)hanged version or leave (d)eleted?") % f, | |
257 |
_(" |
|
258 | (_("&Changed"), _("&Deleted")), _("c")) == _("c"): | |
258 | act("prompt recreating", "g", f, m2.flags(f)) |
|
259 | act("prompt recreating", "g", f, m2.flags(f)) | |
259 | else: |
|
260 | else: | |
260 | act("remote created", "g", f, m2.flags(f)) |
|
261 | act("remote created", "g", f, m2.flags(f)) | |
261 |
|
262 | |||
262 | return action |
|
263 | return action | |
263 |
|
264 | |||
264 | def actioncmp(a1, a2): |
|
265 | def actioncmp(a1, a2): | |
265 | m1 = a1[1] |
|
266 | m1 = a1[1] | |
266 | m2 = a2[1] |
|
267 | m2 = a2[1] | |
267 | if m1 == m2: |
|
268 | if m1 == m2: | |
268 | return cmp(a1, a2) |
|
269 | return cmp(a1, a2) | |
269 | if m1 == 'r': |
|
270 | if m1 == 'r': | |
270 | return -1 |
|
271 | return -1 | |
271 | if m2 == 'r': |
|
272 | if m2 == 'r': | |
272 | return 1 |
|
273 | return 1 | |
273 | return cmp(a1, a2) |
|
274 | return cmp(a1, a2) | |
274 |
|
275 | |||
275 | def applyupdates(repo, action, wctx, mctx): |
|
276 | def applyupdates(repo, action, wctx, mctx): | |
276 | "apply the merge action list to the working directory" |
|
277 | "apply the merge action list to the working directory" | |
277 |
|
278 | |||
278 | updated, merged, removed, unresolved = 0, 0, 0, 0 |
|
279 | updated, merged, removed, unresolved = 0, 0, 0, 0 | |
279 | ms = mergestate(repo) |
|
280 | ms = mergestate(repo) | |
280 | ms.reset(wctx.parents()[0].node()) |
|
281 | ms.reset(wctx.parents()[0].node()) | |
281 | moves = [] |
|
282 | moves = [] | |
282 | action.sort(actioncmp) |
|
283 | action.sort(actioncmp) | |
283 |
|
284 | |||
284 | # prescan for merges |
|
285 | # prescan for merges | |
285 | for a in action: |
|
286 | for a in action: | |
286 | f, m = a[:2] |
|
287 | f, m = a[:2] | |
287 | if m == 'm': # merge |
|
288 | if m == 'm': # merge | |
288 | f2, fd, flags, move = a[2:] |
|
289 | f2, fd, flags, move = a[2:] | |
289 | repo.ui.debug(_("preserving %s for resolve of %s\n") % (f, fd)) |
|
290 | repo.ui.debug(_("preserving %s for resolve of %s\n") % (f, fd)) | |
290 | fcl = wctx[f] |
|
291 | fcl = wctx[f] | |
291 | fco = mctx[f2] |
|
292 | fco = mctx[f2] | |
292 | fca = fcl.ancestor(fco) or repo.filectx(f, fileid=nullrev) |
|
293 | fca = fcl.ancestor(fco) or repo.filectx(f, fileid=nullrev) | |
293 | ms.add(fcl, fco, fca, fd, flags) |
|
294 | ms.add(fcl, fco, fca, fd, flags) | |
294 | if f != fd and move: |
|
295 | if f != fd and move: | |
295 | moves.append(f) |
|
296 | moves.append(f) | |
296 |
|
297 | |||
297 | # remove renamed files after safely stored |
|
298 | # remove renamed files after safely stored | |
298 | for f in moves: |
|
299 | for f in moves: | |
299 | if util.lexists(repo.wjoin(f)): |
|
300 | if util.lexists(repo.wjoin(f)): | |
300 | repo.ui.debug(_("removing %s\n") % f) |
|
301 | repo.ui.debug(_("removing %s\n") % f) | |
301 | os.unlink(repo.wjoin(f)) |
|
302 | os.unlink(repo.wjoin(f)) | |
302 |
|
303 | |||
303 | audit_path = util.path_auditor(repo.root) |
|
304 | audit_path = util.path_auditor(repo.root) | |
304 |
|
305 | |||
305 | for a in action: |
|
306 | for a in action: | |
306 | f, m = a[:2] |
|
307 | f, m = a[:2] | |
307 | if f and f[0] == "/": |
|
308 | if f and f[0] == "/": | |
308 | continue |
|
309 | continue | |
309 | if m == "r": # remove |
|
310 | if m == "r": # remove | |
310 | repo.ui.note(_("removing %s\n") % f) |
|
311 | repo.ui.note(_("removing %s\n") % f) | |
311 | audit_path(f) |
|
312 | audit_path(f) | |
312 | try: |
|
313 | try: | |
313 | util.unlink(repo.wjoin(f)) |
|
314 | util.unlink(repo.wjoin(f)) | |
314 | except OSError, inst: |
|
315 | except OSError, inst: | |
315 | if inst.errno != errno.ENOENT: |
|
316 | if inst.errno != errno.ENOENT: | |
316 | repo.ui.warn(_("update failed to remove %s: %s!\n") % |
|
317 | repo.ui.warn(_("update failed to remove %s: %s!\n") % | |
317 | (f, inst.strerror)) |
|
318 | (f, inst.strerror)) | |
318 | removed += 1 |
|
319 | removed += 1 | |
319 | elif m == "m": # merge |
|
320 | elif m == "m": # merge | |
320 | f2, fd, flags, move = a[2:] |
|
321 | f2, fd, flags, move = a[2:] | |
321 | r = ms.resolve(fd, wctx, mctx) |
|
322 | r = ms.resolve(fd, wctx, mctx) | |
322 | if r > 0: |
|
323 | if r > 0: | |
323 | unresolved += 1 |
|
324 | unresolved += 1 | |
324 | else: |
|
325 | else: | |
325 | if r is None: |
|
326 | if r is None: | |
326 | updated += 1 |
|
327 | updated += 1 | |
327 | else: |
|
328 | else: | |
328 | merged += 1 |
|
329 | merged += 1 | |
329 | util.set_flags(repo.wjoin(fd), 'l' in flags, 'x' in flags) |
|
330 | util.set_flags(repo.wjoin(fd), 'l' in flags, 'x' in flags) | |
330 | if f != fd and move and util.lexists(repo.wjoin(f)): |
|
331 | if f != fd and move and util.lexists(repo.wjoin(f)): | |
331 | repo.ui.debug(_("removing %s\n") % f) |
|
332 | repo.ui.debug(_("removing %s\n") % f) | |
332 | os.unlink(repo.wjoin(f)) |
|
333 | os.unlink(repo.wjoin(f)) | |
333 | elif m == "g": # get |
|
334 | elif m == "g": # get | |
334 | flags = a[2] |
|
335 | flags = a[2] | |
335 | repo.ui.note(_("getting %s\n") % f) |
|
336 | repo.ui.note(_("getting %s\n") % f) | |
336 | t = mctx.filectx(f).data() |
|
337 | t = mctx.filectx(f).data() | |
337 | repo.wwrite(f, t, flags) |
|
338 | repo.wwrite(f, t, flags) | |
338 | updated += 1 |
|
339 | updated += 1 | |
339 | elif m == "d": # directory rename |
|
340 | elif m == "d": # directory rename | |
340 | f2, fd, flags = a[2:] |
|
341 | f2, fd, flags = a[2:] | |
341 | if f: |
|
342 | if f: | |
342 | repo.ui.note(_("moving %s to %s\n") % (f, fd)) |
|
343 | repo.ui.note(_("moving %s to %s\n") % (f, fd)) | |
343 | t = wctx.filectx(f).data() |
|
344 | t = wctx.filectx(f).data() | |
344 | repo.wwrite(fd, t, flags) |
|
345 | repo.wwrite(fd, t, flags) | |
345 | util.unlink(repo.wjoin(f)) |
|
346 | util.unlink(repo.wjoin(f)) | |
346 | if f2: |
|
347 | if f2: | |
347 | repo.ui.note(_("getting %s to %s\n") % (f2, fd)) |
|
348 | repo.ui.note(_("getting %s to %s\n") % (f2, fd)) | |
348 | t = mctx.filectx(f2).data() |
|
349 | t = mctx.filectx(f2).data() | |
349 | repo.wwrite(fd, t, flags) |
|
350 | repo.wwrite(fd, t, flags) | |
350 | updated += 1 |
|
351 | updated += 1 | |
351 | elif m == "dr": # divergent renames |
|
352 | elif m == "dr": # divergent renames | |
352 | fl = a[2] |
|
353 | fl = a[2] | |
353 | repo.ui.warn(_("warning: detected divergent renames of %s to:\n") % f) |
|
354 | repo.ui.warn(_("warning: detected divergent renames of %s to:\n") % f) | |
354 | for nf in fl: |
|
355 | for nf in fl: | |
355 | repo.ui.warn(" %s\n" % nf) |
|
356 | repo.ui.warn(" %s\n" % nf) | |
356 | elif m == "e": # exec |
|
357 | elif m == "e": # exec | |
357 | flags = a[2] |
|
358 | flags = a[2] | |
358 | util.set_flags(repo.wjoin(f), 'l' in flags, 'x' in flags) |
|
359 | util.set_flags(repo.wjoin(f), 'l' in flags, 'x' in flags) | |
359 |
|
360 | |||
360 | return updated, merged, removed, unresolved |
|
361 | return updated, merged, removed, unresolved | |
361 |
|
362 | |||
362 | def recordupdates(repo, action, branchmerge): |
|
363 | def recordupdates(repo, action, branchmerge): | |
363 | "record merge actions to the dirstate" |
|
364 | "record merge actions to the dirstate" | |
364 |
|
365 | |||
365 | for a in action: |
|
366 | for a in action: | |
366 | f, m = a[:2] |
|
367 | f, m = a[:2] | |
367 | if m == "r": # remove |
|
368 | if m == "r": # remove | |
368 | if branchmerge: |
|
369 | if branchmerge: | |
369 | repo.dirstate.remove(f) |
|
370 | repo.dirstate.remove(f) | |
370 | else: |
|
371 | else: | |
371 | repo.dirstate.forget(f) |
|
372 | repo.dirstate.forget(f) | |
372 | elif m == "a": # re-add |
|
373 | elif m == "a": # re-add | |
373 | if not branchmerge: |
|
374 | if not branchmerge: | |
374 | repo.dirstate.add(f) |
|
375 | repo.dirstate.add(f) | |
375 | elif m == "f": # forget |
|
376 | elif m == "f": # forget | |
376 | repo.dirstate.forget(f) |
|
377 | repo.dirstate.forget(f) | |
377 | elif m == "e": # exec change |
|
378 | elif m == "e": # exec change | |
378 | repo.dirstate.normallookup(f) |
|
379 | repo.dirstate.normallookup(f) | |
379 | elif m == "g": # get |
|
380 | elif m == "g": # get | |
380 | if branchmerge: |
|
381 | if branchmerge: | |
381 | repo.dirstate.normaldirty(f) |
|
382 | repo.dirstate.normaldirty(f) | |
382 | else: |
|
383 | else: | |
383 | repo.dirstate.normal(f) |
|
384 | repo.dirstate.normal(f) | |
384 | elif m == "m": # merge |
|
385 | elif m == "m": # merge | |
385 | f2, fd, flag, move = a[2:] |
|
386 | f2, fd, flag, move = a[2:] | |
386 | if branchmerge: |
|
387 | if branchmerge: | |
387 | # We've done a branch merge, mark this file as merged |
|
388 | # We've done a branch merge, mark this file as merged | |
388 | # so that we properly record the merger later |
|
389 | # so that we properly record the merger later | |
389 | repo.dirstate.merge(fd) |
|
390 | repo.dirstate.merge(fd) | |
390 | if f != f2: # copy/rename |
|
391 | if f != f2: # copy/rename | |
391 | if move: |
|
392 | if move: | |
392 | repo.dirstate.remove(f) |
|
393 | repo.dirstate.remove(f) | |
393 | if f != fd: |
|
394 | if f != fd: | |
394 | repo.dirstate.copy(f, fd) |
|
395 | repo.dirstate.copy(f, fd) | |
395 | else: |
|
396 | else: | |
396 | repo.dirstate.copy(f2, fd) |
|
397 | repo.dirstate.copy(f2, fd) | |
397 | else: |
|
398 | else: | |
398 | # We've update-merged a locally modified file, so |
|
399 | # We've update-merged a locally modified file, so | |
399 | # we set the dirstate to emulate a normal checkout |
|
400 | # we set the dirstate to emulate a normal checkout | |
400 | # of that file some time in the past. Thus our |
|
401 | # of that file some time in the past. Thus our | |
401 | # merge will appear as a normal local file |
|
402 | # merge will appear as a normal local file | |
402 | # modification. |
|
403 | # modification. | |
403 | repo.dirstate.normallookup(fd) |
|
404 | repo.dirstate.normallookup(fd) | |
404 | if move: |
|
405 | if move: | |
405 | repo.dirstate.forget(f) |
|
406 | repo.dirstate.forget(f) | |
406 | elif m == "d": # directory rename |
|
407 | elif m == "d": # directory rename | |
407 | f2, fd, flag = a[2:] |
|
408 | f2, fd, flag = a[2:] | |
408 | if not f2 and f not in repo.dirstate: |
|
409 | if not f2 and f not in repo.dirstate: | |
409 | # untracked file moved |
|
410 | # untracked file moved | |
410 | continue |
|
411 | continue | |
411 | if branchmerge: |
|
412 | if branchmerge: | |
412 | repo.dirstate.add(fd) |
|
413 | repo.dirstate.add(fd) | |
413 | if f: |
|
414 | if f: | |
414 | repo.dirstate.remove(f) |
|
415 | repo.dirstate.remove(f) | |
415 | repo.dirstate.copy(f, fd) |
|
416 | repo.dirstate.copy(f, fd) | |
416 | if f2: |
|
417 | if f2: | |
417 | repo.dirstate.copy(f2, fd) |
|
418 | repo.dirstate.copy(f2, fd) | |
418 | else: |
|
419 | else: | |
419 | repo.dirstate.normal(fd) |
|
420 | repo.dirstate.normal(fd) | |
420 | if f: |
|
421 | if f: | |
421 | repo.dirstate.forget(f) |
|
422 | repo.dirstate.forget(f) | |
422 |
|
423 | |||
423 | def update(repo, node, branchmerge, force, partial): |
|
424 | def update(repo, node, branchmerge, force, partial): | |
424 | """ |
|
425 | """ | |
425 | Perform a merge between the working directory and the given node |
|
426 | Perform a merge between the working directory and the given node | |
426 |
|
427 | |||
427 | branchmerge = whether to merge between branches |
|
428 | branchmerge = whether to merge between branches | |
428 | force = whether to force branch merging or file overwriting |
|
429 | force = whether to force branch merging or file overwriting | |
429 | partial = a function to filter file lists (dirstate not updated) |
|
430 | partial = a function to filter file lists (dirstate not updated) | |
430 | """ |
|
431 | """ | |
431 |
|
432 | |||
432 | wlock = repo.wlock() |
|
433 | wlock = repo.wlock() | |
433 | try: |
|
434 | try: | |
434 | wc = repo[None] |
|
435 | wc = repo[None] | |
435 | if node is None: |
|
436 | if node is None: | |
436 | # tip of current branch |
|
437 | # tip of current branch | |
437 | try: |
|
438 | try: | |
438 | node = repo.branchtags()[wc.branch()] |
|
439 | node = repo.branchtags()[wc.branch()] | |
439 | except KeyError: |
|
440 | except KeyError: | |
440 | if wc.branch() == "default": # no default branch! |
|
441 | if wc.branch() == "default": # no default branch! | |
441 | node = repo.lookup("tip") # update to tip |
|
442 | node = repo.lookup("tip") # update to tip | |
442 | else: |
|
443 | else: | |
443 | raise util.Abort(_("branch %s not found") % wc.branch()) |
|
444 | raise util.Abort(_("branch %s not found") % wc.branch()) | |
444 | overwrite = force and not branchmerge |
|
445 | overwrite = force and not branchmerge | |
445 | pl = wc.parents() |
|
446 | pl = wc.parents() | |
446 | p1, p2 = pl[0], repo[node] |
|
447 | p1, p2 = pl[0], repo[node] | |
447 | pa = p1.ancestor(p2) |
|
448 | pa = p1.ancestor(p2) | |
448 | fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2) |
|
449 | fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2) | |
449 | fastforward = False |
|
450 | fastforward = False | |
450 |
|
451 | |||
451 | ### check phase |
|
452 | ### check phase | |
452 | if not overwrite and len(pl) > 1: |
|
453 | if not overwrite and len(pl) > 1: | |
453 | raise util.Abort(_("outstanding uncommitted merges")) |
|
454 | raise util.Abort(_("outstanding uncommitted merges")) | |
454 | if branchmerge: |
|
455 | if branchmerge: | |
455 | if pa == p2: |
|
456 | if pa == p2: | |
456 | raise util.Abort(_("can't merge with ancestor")) |
|
457 | raise util.Abort(_("can't merge with ancestor")) | |
457 | elif pa == p1: |
|
458 | elif pa == p1: | |
458 | if p1.branch() != p2.branch(): |
|
459 | if p1.branch() != p2.branch(): | |
459 | fastforward = True |
|
460 | fastforward = True | |
460 | else: |
|
461 | else: | |
461 | raise util.Abort(_("nothing to merge (use 'hg update'" |
|
462 | raise util.Abort(_("nothing to merge (use 'hg update'" | |
462 | " or check 'hg heads')")) |
|
463 | " or check 'hg heads')")) | |
463 | if not force and (wc.files() or wc.deleted()): |
|
464 | if not force and (wc.files() or wc.deleted()): | |
464 | raise util.Abort(_("outstanding uncommitted changes")) |
|
465 | raise util.Abort(_("outstanding uncommitted changes")) | |
465 | elif not overwrite: |
|
466 | elif not overwrite: | |
466 | if pa == p1 or pa == p2: # linear |
|
467 | if pa == p1 or pa == p2: # linear | |
467 | pass # all good |
|
468 | pass # all good | |
468 | elif p1.branch() == p2.branch(): |
|
469 | elif p1.branch() == p2.branch(): | |
469 | if wc.files() or wc.deleted(): |
|
470 | if wc.files() or wc.deleted(): | |
470 | raise util.Abort(_("crosses branches (use 'hg merge' or " |
|
471 | raise util.Abort(_("crosses branches (use 'hg merge' or " | |
471 | "'hg update -C' to discard changes)")) |
|
472 | "'hg update -C' to discard changes)")) | |
472 | raise util.Abort(_("crosses branches (use 'hg merge' " |
|
473 | raise util.Abort(_("crosses branches (use 'hg merge' " | |
473 | "or 'hg update -C')")) |
|
474 | "or 'hg update -C')")) | |
474 | elif wc.files() or wc.deleted(): |
|
475 | elif wc.files() or wc.deleted(): | |
475 | raise util.Abort(_("crosses named branches (use " |
|
476 | raise util.Abort(_("crosses named branches (use " | |
476 | "'hg update -C' to discard changes)")) |
|
477 | "'hg update -C' to discard changes)")) | |
477 | else: |
|
478 | else: | |
478 | # Allow jumping branches if there are no changes |
|
479 | # Allow jumping branches if there are no changes | |
479 | overwrite = True |
|
480 | overwrite = True | |
480 |
|
481 | |||
481 | ### calculate phase |
|
482 | ### calculate phase | |
482 | action = [] |
|
483 | action = [] | |
483 | if not force: |
|
484 | if not force: | |
484 | _checkunknown(wc, p2) |
|
485 | _checkunknown(wc, p2) | |
485 | if not util.checkcase(repo.path): |
|
486 | if not util.checkcase(repo.path): | |
486 | _checkcollision(p2) |
|
487 | _checkcollision(p2) | |
487 | action += _forgetremoved(wc, p2, branchmerge) |
|
488 | action += _forgetremoved(wc, p2, branchmerge) | |
488 | action += manifestmerge(repo, wc, p2, pa, overwrite, partial) |
|
489 | action += manifestmerge(repo, wc, p2, pa, overwrite, partial) | |
489 |
|
490 | |||
490 | ### apply phase |
|
491 | ### apply phase | |
491 | if not branchmerge: # just jump to the new rev |
|
492 | if not branchmerge: # just jump to the new rev | |
492 | fp1, fp2, xp1, xp2 = fp2, nullid, xp2, '' |
|
493 | fp1, fp2, xp1, xp2 = fp2, nullid, xp2, '' | |
493 | if not partial: |
|
494 | if not partial: | |
494 | repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2) |
|
495 | repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2) | |
495 |
|
496 | |||
496 | stats = applyupdates(repo, action, wc, p2) |
|
497 | stats = applyupdates(repo, action, wc, p2) | |
497 |
|
498 | |||
498 | if not partial: |
|
499 | if not partial: | |
499 | recordupdates(repo, action, branchmerge) |
|
500 | recordupdates(repo, action, branchmerge) | |
500 | repo.dirstate.setparents(fp1, fp2) |
|
501 | repo.dirstate.setparents(fp1, fp2) | |
501 | if not branchmerge and not fastforward: |
|
502 | if not branchmerge and not fastforward: | |
502 | repo.dirstate.setbranch(p2.branch()) |
|
503 | repo.dirstate.setbranch(p2.branch()) | |
503 | repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3]) |
|
504 | repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3]) | |
504 |
|
505 | |||
505 | return stats |
|
506 | return stats | |
506 | finally: |
|
507 | finally: | |
507 | wlock.release() |
|
508 | wlock.release() |
@@ -1,341 +1,346 b'' | |||||
1 | # ui.py - user interface bits for mercurial |
|
1 | # ui.py - user interface bits for mercurial | |
2 | # |
|
2 | # | |
3 | # Copyright 2005-2007 Matt Mackall <mpm@selenic.com> |
|
3 | # Copyright 2005-2007 Matt Mackall <mpm@selenic.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, incorporated herein by reference. |
|
6 | # GNU General Public License version 2, incorporated herein by reference. | |
7 |
|
7 | |||
8 | from i18n import _ |
|
8 | from i18n import _ | |
9 | import errno, getpass, os, re, socket, sys, tempfile |
|
9 | import errno, getpass, os, re, socket, sys, tempfile | |
10 | import config, traceback, util, error |
|
10 | import config, traceback, util, error | |
11 |
|
11 | |||
12 | _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, |
|
12 | _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, | |
13 | '0': False, 'no': False, 'false': False, 'off': False} |
|
13 | '0': False, 'no': False, 'false': False, 'off': False} | |
14 |
|
14 | |||
15 | class ui(object): |
|
15 | class ui(object): | |
16 | def __init__(self, src=None): |
|
16 | def __init__(self, src=None): | |
17 | self._buffers = [] |
|
17 | self._buffers = [] | |
18 | self.quiet = self.verbose = self.debugflag = self._traceback = False |
|
18 | self.quiet = self.verbose = self.debugflag = self._traceback = False | |
19 | self._reportuntrusted = True |
|
19 | self._reportuntrusted = True | |
20 | self._ocfg = config.config() # overlay |
|
20 | self._ocfg = config.config() # overlay | |
21 | self._tcfg = config.config() # trusted |
|
21 | self._tcfg = config.config() # trusted | |
22 | self._ucfg = config.config() # untrusted |
|
22 | self._ucfg = config.config() # untrusted | |
23 | self._trustusers = {} |
|
23 | self._trustusers = {} | |
24 | self._trustgroups = {} |
|
24 | self._trustgroups = {} | |
25 |
|
25 | |||
26 | if src: |
|
26 | if src: | |
27 | self._tcfg = src._tcfg.copy() |
|
27 | self._tcfg = src._tcfg.copy() | |
28 | self._ucfg = src._ucfg.copy() |
|
28 | self._ucfg = src._ucfg.copy() | |
29 | self._ocfg = src._ocfg.copy() |
|
29 | self._ocfg = src._ocfg.copy() | |
30 | self._trustusers = src._trustusers.copy() |
|
30 | self._trustusers = src._trustusers.copy() | |
31 | self._trustgroups = src._trustgroups.copy() |
|
31 | self._trustgroups = src._trustgroups.copy() | |
32 | self.fixconfig() |
|
32 | self.fixconfig() | |
33 | else: |
|
33 | else: | |
34 | # we always trust global config files |
|
34 | # we always trust global config files | |
35 | for f in util.rcpath(): |
|
35 | for f in util.rcpath(): | |
36 | self.readconfig(f, trust=True) |
|
36 | self.readconfig(f, trust=True) | |
37 |
|
37 | |||
38 | def copy(self): |
|
38 | def copy(self): | |
39 | return self.__class__(self) |
|
39 | return self.__class__(self) | |
40 |
|
40 | |||
41 | def _is_trusted(self, fp, f): |
|
41 | def _is_trusted(self, fp, f): | |
42 | st = util.fstat(fp) |
|
42 | st = util.fstat(fp) | |
43 | if util.isowner(fp, st): |
|
43 | if util.isowner(fp, st): | |
44 | return True |
|
44 | return True | |
45 |
|
45 | |||
46 | tusers, tgroups = self._trustusers, self._trustgroups |
|
46 | tusers, tgroups = self._trustusers, self._trustgroups | |
47 | if '*' in tusers or '*' in tgroups: |
|
47 | if '*' in tusers or '*' in tgroups: | |
48 | return True |
|
48 | return True | |
49 |
|
49 | |||
50 | user = util.username(st.st_uid) |
|
50 | user = util.username(st.st_uid) | |
51 | group = util.groupname(st.st_gid) |
|
51 | group = util.groupname(st.st_gid) | |
52 | if user in tusers or group in tgroups or user == util.username(): |
|
52 | if user in tusers or group in tgroups or user == util.username(): | |
53 | return True |
|
53 | return True | |
54 |
|
54 | |||
55 | if self._reportuntrusted: |
|
55 | if self._reportuntrusted: | |
56 | self.warn(_('Not trusting file %s from untrusted ' |
|
56 | self.warn(_('Not trusting file %s from untrusted ' | |
57 | 'user %s, group %s\n') % (f, user, group)) |
|
57 | 'user %s, group %s\n') % (f, user, group)) | |
58 | return False |
|
58 | return False | |
59 |
|
59 | |||
60 | def readconfig(self, filename, root=None, trust=False, |
|
60 | def readconfig(self, filename, root=None, trust=False, | |
61 | sections=None): |
|
61 | sections=None): | |
62 | try: |
|
62 | try: | |
63 | fp = open(filename) |
|
63 | fp = open(filename) | |
64 | except IOError: |
|
64 | except IOError: | |
65 | if not sections: # ignore unless we were looking for something |
|
65 | if not sections: # ignore unless we were looking for something | |
66 | return |
|
66 | return | |
67 | raise |
|
67 | raise | |
68 |
|
68 | |||
69 | cfg = config.config() |
|
69 | cfg = config.config() | |
70 | trusted = sections or trust or self._is_trusted(fp, filename) |
|
70 | trusted = sections or trust or self._is_trusted(fp, filename) | |
71 |
|
71 | |||
72 | try: |
|
72 | try: | |
73 | cfg.read(filename, fp, sections=sections) |
|
73 | cfg.read(filename, fp, sections=sections) | |
74 | except error.ConfigError, inst: |
|
74 | except error.ConfigError, inst: | |
75 | if trusted: |
|
75 | if trusted: | |
76 | raise |
|
76 | raise | |
77 | self.warn(_("Ignored: %s\n") % str(inst)) |
|
77 | self.warn(_("Ignored: %s\n") % str(inst)) | |
78 |
|
78 | |||
79 | if trusted: |
|
79 | if trusted: | |
80 | self._tcfg.update(cfg) |
|
80 | self._tcfg.update(cfg) | |
81 | self._tcfg.update(self._ocfg) |
|
81 | self._tcfg.update(self._ocfg) | |
82 | self._ucfg.update(cfg) |
|
82 | self._ucfg.update(cfg) | |
83 | self._ucfg.update(self._ocfg) |
|
83 | self._ucfg.update(self._ocfg) | |
84 |
|
84 | |||
85 | if root is None: |
|
85 | if root is None: | |
86 | root = os.path.expanduser('~') |
|
86 | root = os.path.expanduser('~') | |
87 | self.fixconfig(root=root) |
|
87 | self.fixconfig(root=root) | |
88 |
|
88 | |||
89 | def fixconfig(self, root=None): |
|
89 | def fixconfig(self, root=None): | |
90 | # translate paths relative to root (or home) into absolute paths |
|
90 | # translate paths relative to root (or home) into absolute paths | |
91 | root = root or os.getcwd() |
|
91 | root = root or os.getcwd() | |
92 | for c in self._tcfg, self._ucfg, self._ocfg: |
|
92 | for c in self._tcfg, self._ucfg, self._ocfg: | |
93 | for n, p in c.items('paths'): |
|
93 | for n, p in c.items('paths'): | |
94 | if p and "://" not in p and not os.path.isabs(p): |
|
94 | if p and "://" not in p and not os.path.isabs(p): | |
95 | c.set("paths", n, os.path.normpath(os.path.join(root, p))) |
|
95 | c.set("paths", n, os.path.normpath(os.path.join(root, p))) | |
96 |
|
96 | |||
97 | # update ui options |
|
97 | # update ui options | |
98 | self.debugflag = self.configbool('ui', 'debug') |
|
98 | self.debugflag = self.configbool('ui', 'debug') | |
99 | self.verbose = self.debugflag or self.configbool('ui', 'verbose') |
|
99 | self.verbose = self.debugflag or self.configbool('ui', 'verbose') | |
100 | self.quiet = not self.debugflag and self.configbool('ui', 'quiet') |
|
100 | self.quiet = not self.debugflag and self.configbool('ui', 'quiet') | |
101 | if self.verbose and self.quiet: |
|
101 | if self.verbose and self.quiet: | |
102 | self.quiet = self.verbose = False |
|
102 | self.quiet = self.verbose = False | |
103 | self._reportuntrusted = self.configbool("ui", "report_untrusted", True) |
|
103 | self._reportuntrusted = self.configbool("ui", "report_untrusted", True) | |
104 | self._traceback = self.configbool('ui', 'traceback', False) |
|
104 | self._traceback = self.configbool('ui', 'traceback', False) | |
105 |
|
105 | |||
106 | # update trust information |
|
106 | # update trust information | |
107 | for user in self.configlist('trusted', 'users'): |
|
107 | for user in self.configlist('trusted', 'users'): | |
108 | self._trustusers[user] = 1 |
|
108 | self._trustusers[user] = 1 | |
109 | for group in self.configlist('trusted', 'groups'): |
|
109 | for group in self.configlist('trusted', 'groups'): | |
110 | self._trustgroups[group] = 1 |
|
110 | self._trustgroups[group] = 1 | |
111 |
|
111 | |||
112 | def setconfig(self, section, name, value): |
|
112 | def setconfig(self, section, name, value): | |
113 | for cfg in (self._ocfg, self._tcfg, self._ucfg): |
|
113 | for cfg in (self._ocfg, self._tcfg, self._ucfg): | |
114 | cfg.set(section, name, value) |
|
114 | cfg.set(section, name, value) | |
115 | self.fixconfig() |
|
115 | self.fixconfig() | |
116 |
|
116 | |||
117 | def _data(self, untrusted): |
|
117 | def _data(self, untrusted): | |
118 | return untrusted and self._ucfg or self._tcfg |
|
118 | return untrusted and self._ucfg or self._tcfg | |
119 |
|
119 | |||
120 | def configsource(self, section, name, untrusted=False): |
|
120 | def configsource(self, section, name, untrusted=False): | |
121 | return self._data(untrusted).source(section, name) or 'none' |
|
121 | return self._data(untrusted).source(section, name) or 'none' | |
122 |
|
122 | |||
123 | def config(self, section, name, default=None, untrusted=False): |
|
123 | def config(self, section, name, default=None, untrusted=False): | |
124 | value = self._data(untrusted).get(section, name, default) |
|
124 | value = self._data(untrusted).get(section, name, default) | |
125 | if self.debugflag and not untrusted and self._reportuntrusted: |
|
125 | if self.debugflag and not untrusted and self._reportuntrusted: | |
126 | uvalue = self._ucfg.get(section, name) |
|
126 | uvalue = self._ucfg.get(section, name) | |
127 | if uvalue is not None and uvalue != value: |
|
127 | if uvalue is not None and uvalue != value: | |
128 | self.debug(_("ignoring untrusted configuration option " |
|
128 | self.debug(_("ignoring untrusted configuration option " | |
129 | "%s.%s = %s\n") % (section, name, uvalue)) |
|
129 | "%s.%s = %s\n") % (section, name, uvalue)) | |
130 | return value |
|
130 | return value | |
131 |
|
131 | |||
132 | def configbool(self, section, name, default=False, untrusted=False): |
|
132 | def configbool(self, section, name, default=False, untrusted=False): | |
133 | v = self.config(section, name, None, untrusted) |
|
133 | v = self.config(section, name, None, untrusted) | |
134 | if v == None: |
|
134 | if v == None: | |
135 | return default |
|
135 | return default | |
136 | if v.lower() not in _booleans: |
|
136 | if v.lower() not in _booleans: | |
137 | raise error.ConfigError(_("%s.%s not a boolean ('%s')") |
|
137 | raise error.ConfigError(_("%s.%s not a boolean ('%s')") | |
138 | % (section, name, v)) |
|
138 | % (section, name, v)) | |
139 | return _booleans[v.lower()] |
|
139 | return _booleans[v.lower()] | |
140 |
|
140 | |||
141 | def configlist(self, section, name, default=None, untrusted=False): |
|
141 | def configlist(self, section, name, default=None, untrusted=False): | |
142 | """Return a list of comma/space separated strings""" |
|
142 | """Return a list of comma/space separated strings""" | |
143 | result = self.config(section, name, untrusted=untrusted) |
|
143 | result = self.config(section, name, untrusted=untrusted) | |
144 | if result is None: |
|
144 | if result is None: | |
145 | result = default or [] |
|
145 | result = default or [] | |
146 | if isinstance(result, basestring): |
|
146 | if isinstance(result, basestring): | |
147 | result = result.replace(",", " ").split() |
|
147 | result = result.replace(",", " ").split() | |
148 | return result |
|
148 | return result | |
149 |
|
149 | |||
150 | def has_section(self, section, untrusted=False): |
|
150 | def has_section(self, section, untrusted=False): | |
151 | '''tell whether section exists in config.''' |
|
151 | '''tell whether section exists in config.''' | |
152 | return section in self._data(untrusted) |
|
152 | return section in self._data(untrusted) | |
153 |
|
153 | |||
154 | def configitems(self, section, untrusted=False): |
|
154 | def configitems(self, section, untrusted=False): | |
155 | items = self._data(untrusted).items(section) |
|
155 | items = self._data(untrusted).items(section) | |
156 | if self.debugflag and not untrusted and self._reportuntrusted: |
|
156 | if self.debugflag and not untrusted and self._reportuntrusted: | |
157 | for k, v in self._ucfg.items(section): |
|
157 | for k, v in self._ucfg.items(section): | |
158 | if self._tcfg.get(section, k) != v: |
|
158 | if self._tcfg.get(section, k) != v: | |
159 | self.debug(_("ignoring untrusted configuration option " |
|
159 | self.debug(_("ignoring untrusted configuration option " | |
160 | "%s.%s = %s\n") % (section, k, v)) |
|
160 | "%s.%s = %s\n") % (section, k, v)) | |
161 | return items |
|
161 | return items | |
162 |
|
162 | |||
163 | def walkconfig(self, untrusted=False): |
|
163 | def walkconfig(self, untrusted=False): | |
164 | cfg = self._data(untrusted) |
|
164 | cfg = self._data(untrusted) | |
165 | for section in cfg.sections(): |
|
165 | for section in cfg.sections(): | |
166 | for name, value in self.configitems(section, untrusted): |
|
166 | for name, value in self.configitems(section, untrusted): | |
167 | yield section, name, str(value).replace('\n', '\\n') |
|
167 | yield section, name, str(value).replace('\n', '\\n') | |
168 |
|
168 | |||
169 | def username(self): |
|
169 | def username(self): | |
170 | """Return default username to be used in commits. |
|
170 | """Return default username to be used in commits. | |
171 |
|
171 | |||
172 | Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL |
|
172 | Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL | |
173 | and stop searching if one of these is set. |
|
173 | and stop searching if one of these is set. | |
174 | If not found and ui.askusername is True, ask the user, else use |
|
174 | If not found and ui.askusername is True, ask the user, else use | |
175 | ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname". |
|
175 | ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname". | |
176 | """ |
|
176 | """ | |
177 | user = os.environ.get("HGUSER") |
|
177 | user = os.environ.get("HGUSER") | |
178 | if user is None: |
|
178 | if user is None: | |
179 | user = self.config("ui", "username") |
|
179 | user = self.config("ui", "username") | |
180 | if user is None: |
|
180 | if user is None: | |
181 | user = os.environ.get("EMAIL") |
|
181 | user = os.environ.get("EMAIL") | |
182 | if user is None and self.configbool("ui", "askusername"): |
|
182 | if user is None and self.configbool("ui", "askusername"): | |
183 | user = self.prompt(_("enter a commit username:"), default=None) |
|
183 | user = self.prompt(_("enter a commit username:"), default=None) | |
184 | if user is None: |
|
184 | if user is None: | |
185 | try: |
|
185 | try: | |
186 | user = '%s@%s' % (util.getuser(), socket.getfqdn()) |
|
186 | user = '%s@%s' % (util.getuser(), socket.getfqdn()) | |
187 | self.warn(_("No username found, using '%s' instead\n") % user) |
|
187 | self.warn(_("No username found, using '%s' instead\n") % user) | |
188 | except KeyError: |
|
188 | except KeyError: | |
189 | pass |
|
189 | pass | |
190 | if not user: |
|
190 | if not user: | |
191 | raise util.Abort(_("Please specify a username.")) |
|
191 | raise util.Abort(_("Please specify a username.")) | |
192 | if "\n" in user: |
|
192 | if "\n" in user: | |
193 | raise util.Abort(_("username %s contains a newline\n") % repr(user)) |
|
193 | raise util.Abort(_("username %s contains a newline\n") % repr(user)) | |
194 | return user |
|
194 | return user | |
195 |
|
195 | |||
196 | def shortuser(self, user): |
|
196 | def shortuser(self, user): | |
197 | """Return a short representation of a user name or email address.""" |
|
197 | """Return a short representation of a user name or email address.""" | |
198 | if not self.verbose: user = util.shortuser(user) |
|
198 | if not self.verbose: user = util.shortuser(user) | |
199 | return user |
|
199 | return user | |
200 |
|
200 | |||
201 | def _path(self, loc): |
|
201 | def _path(self, loc): | |
202 | p = self.config('paths', loc) |
|
202 | p = self.config('paths', loc) | |
203 | if p and '%%' in p: |
|
203 | if p and '%%' in p: | |
204 | ui.warn('(deprecated \'\%\%\' in path %s=%s from %s)\n' % |
|
204 | ui.warn('(deprecated \'\%\%\' in path %s=%s from %s)\n' % | |
205 | (loc, p, self.configsource('paths', loc))) |
|
205 | (loc, p, self.configsource('paths', loc))) | |
206 | p = p.replace('%%', '%') |
|
206 | p = p.replace('%%', '%') | |
207 | return p |
|
207 | return p | |
208 |
|
208 | |||
209 | def expandpath(self, loc, default=None): |
|
209 | def expandpath(self, loc, default=None): | |
210 | """Return repository location relative to cwd or from [paths]""" |
|
210 | """Return repository location relative to cwd or from [paths]""" | |
211 | if "://" in loc or os.path.isdir(os.path.join(loc, '.hg')): |
|
211 | if "://" in loc or os.path.isdir(os.path.join(loc, '.hg')): | |
212 | return loc |
|
212 | return loc | |
213 |
|
213 | |||
214 | path = self._path(loc) |
|
214 | path = self._path(loc) | |
215 | if not path and default is not None: |
|
215 | if not path and default is not None: | |
216 | path = self._path(default) |
|
216 | path = self._path(default) | |
217 | return path or loc |
|
217 | return path or loc | |
218 |
|
218 | |||
219 | def pushbuffer(self): |
|
219 | def pushbuffer(self): | |
220 | self._buffers.append([]) |
|
220 | self._buffers.append([]) | |
221 |
|
221 | |||
222 | def popbuffer(self): |
|
222 | def popbuffer(self): | |
223 | return "".join(self._buffers.pop()) |
|
223 | return "".join(self._buffers.pop()) | |
224 |
|
224 | |||
225 | def write(self, *args): |
|
225 | def write(self, *args): | |
226 | if self._buffers: |
|
226 | if self._buffers: | |
227 | self._buffers[-1].extend([str(a) for a in args]) |
|
227 | self._buffers[-1].extend([str(a) for a in args]) | |
228 | else: |
|
228 | else: | |
229 | for a in args: |
|
229 | for a in args: | |
230 | sys.stdout.write(str(a)) |
|
230 | sys.stdout.write(str(a)) | |
231 |
|
231 | |||
232 | def write_err(self, *args): |
|
232 | def write_err(self, *args): | |
233 | try: |
|
233 | try: | |
234 | if not sys.stdout.closed: sys.stdout.flush() |
|
234 | if not sys.stdout.closed: sys.stdout.flush() | |
235 | for a in args: |
|
235 | for a in args: | |
236 | sys.stderr.write(str(a)) |
|
236 | sys.stderr.write(str(a)) | |
237 | # stderr may be buffered under win32 when redirected to files, |
|
237 | # stderr may be buffered under win32 when redirected to files, | |
238 | # including stdout. |
|
238 | # including stdout. | |
239 | if not sys.stderr.closed: sys.stderr.flush() |
|
239 | if not sys.stderr.closed: sys.stderr.flush() | |
240 | except IOError, inst: |
|
240 | except IOError, inst: | |
241 | if inst.errno != errno.EPIPE: |
|
241 | if inst.errno != errno.EPIPE: | |
242 | raise |
|
242 | raise | |
243 |
|
243 | |||
244 | def flush(self): |
|
244 | def flush(self): | |
245 | try: sys.stdout.flush() |
|
245 | try: sys.stdout.flush() | |
246 | except: pass |
|
246 | except: pass | |
247 | try: sys.stderr.flush() |
|
247 | try: sys.stderr.flush() | |
248 | except: pass |
|
248 | except: pass | |
249 |
|
249 | |||
250 | def interactive(self): |
|
250 | def interactive(self): | |
251 | return self.configbool("ui", "interactive") or sys.stdin.isatty() |
|
251 | return self.configbool("ui", "interactive") or sys.stdin.isatty() | |
252 |
|
252 | |||
253 | def _readline(self, prompt=''): |
|
253 | def _readline(self, prompt=''): | |
254 | if sys.stdin.isatty(): |
|
254 | if sys.stdin.isatty(): | |
255 | try: |
|
255 | try: | |
256 | # magically add command line editing support, where |
|
256 | # magically add command line editing support, where | |
257 | # available |
|
257 | # available | |
258 | import readline |
|
258 | import readline | |
259 | # force demandimport to really load the module |
|
259 | # force demandimport to really load the module | |
260 | readline.read_history_file |
|
260 | readline.read_history_file | |
261 | # windows sometimes raises something other than ImportError |
|
261 | # windows sometimes raises something other than ImportError | |
262 | except Exception: |
|
262 | except Exception: | |
263 | pass |
|
263 | pass | |
264 | line = raw_input(prompt) |
|
264 | line = raw_input(prompt) | |
265 | # When stdin is in binary mode on Windows, it can cause |
|
265 | # When stdin is in binary mode on Windows, it can cause | |
266 | # raw_input() to emit an extra trailing carriage return |
|
266 | # raw_input() to emit an extra trailing carriage return | |
267 | if os.linesep == '\r\n' and line and line[-1] == '\r': |
|
267 | if os.linesep == '\r\n' and line and line[-1] == '\r': | |
268 | line = line[:-1] |
|
268 | line = line[:-1] | |
269 | return line |
|
269 | return line | |
270 |
|
270 | |||
271 |
def prompt(self, msg, |
|
271 | def prompt(self, msg, choices=None, default="y"): | |
272 |
"""Prompt user with msg, read response, and ensure it matches |
|
272 | """Prompt user with msg, read response, and ensure it matches | |
273 |
|
273 | one of the provided choices. choices is a sequence of acceptable | ||
274 | If not interactive -- the default is returned |
|
274 | responses with the format: ('&None', 'E&xec', 'Sym&link') | |
|
275 | No sequence implies no response checking. Responses are case | |||
|
276 | insensitive. If ui is not interactive, the default is returned. | |||
275 | """ |
|
277 | """ | |
276 | if not self.interactive(): |
|
278 | if not self.interactive(): | |
277 | self.note(msg, ' ', default, "\n") |
|
279 | self.note(msg, ' ', default, "\n") | |
278 | return default |
|
280 | return default | |
279 | while True: |
|
281 | while True: | |
280 | try: |
|
282 | try: | |
281 | r = self._readline(msg + ' ') |
|
283 | r = self._readline(msg + ' ') | |
282 | if not r: |
|
284 | if not r: | |
283 | return default |
|
285 | return default | |
284 |
if not |
|
286 | if not choices: | |
285 | return r |
|
287 | return r | |
|
288 | resps = [s[s.index('&')+1].lower() for s in choices] | |||
|
289 | if r.lower() in resps: | |||
|
290 | return r.lower() | |||
286 | else: |
|
291 | else: | |
287 | self.write(_("unrecognized response\n")) |
|
292 | self.write(_("unrecognized response\n")) | |
288 | except EOFError: |
|
293 | except EOFError: | |
289 | raise util.Abort(_('response expected')) |
|
294 | raise util.Abort(_('response expected')) | |
290 |
|
295 | |||
291 | def getpass(self, prompt=None, default=None): |
|
296 | def getpass(self, prompt=None, default=None): | |
292 | if not self.interactive(): return default |
|
297 | if not self.interactive(): return default | |
293 | try: |
|
298 | try: | |
294 | return getpass.getpass(prompt or _('password: ')) |
|
299 | return getpass.getpass(prompt or _('password: ')) | |
295 | except EOFError: |
|
300 | except EOFError: | |
296 | raise util.Abort(_('response expected')) |
|
301 | raise util.Abort(_('response expected')) | |
297 | def status(self, *msg): |
|
302 | def status(self, *msg): | |
298 | if not self.quiet: self.write(*msg) |
|
303 | if not self.quiet: self.write(*msg) | |
299 | def warn(self, *msg): |
|
304 | def warn(self, *msg): | |
300 | self.write_err(*msg) |
|
305 | self.write_err(*msg) | |
301 | def note(self, *msg): |
|
306 | def note(self, *msg): | |
302 | if self.verbose: self.write(*msg) |
|
307 | if self.verbose: self.write(*msg) | |
303 | def debug(self, *msg): |
|
308 | def debug(self, *msg): | |
304 | if self.debugflag: self.write(*msg) |
|
309 | if self.debugflag: self.write(*msg) | |
305 | def edit(self, text, user): |
|
310 | def edit(self, text, user): | |
306 | (fd, name) = tempfile.mkstemp(prefix="hg-editor-", suffix=".txt", |
|
311 | (fd, name) = tempfile.mkstemp(prefix="hg-editor-", suffix=".txt", | |
307 | text=True) |
|
312 | text=True) | |
308 | try: |
|
313 | try: | |
309 | f = os.fdopen(fd, "w") |
|
314 | f = os.fdopen(fd, "w") | |
310 | f.write(text) |
|
315 | f.write(text) | |
311 | f.close() |
|
316 | f.close() | |
312 |
|
317 | |||
313 | editor = self.geteditor() |
|
318 | editor = self.geteditor() | |
314 |
|
319 | |||
315 | util.system("%s \"%s\"" % (editor, name), |
|
320 | util.system("%s \"%s\"" % (editor, name), | |
316 | environ={'HGUSER': user}, |
|
321 | environ={'HGUSER': user}, | |
317 | onerr=util.Abort, errprefix=_("edit failed")) |
|
322 | onerr=util.Abort, errprefix=_("edit failed")) | |
318 |
|
323 | |||
319 | f = open(name) |
|
324 | f = open(name) | |
320 | t = f.read() |
|
325 | t = f.read() | |
321 | f.close() |
|
326 | f.close() | |
322 | t = re.sub("(?m)^HG:.*\n", "", t) |
|
327 | t = re.sub("(?m)^HG:.*\n", "", t) | |
323 | finally: |
|
328 | finally: | |
324 | os.unlink(name) |
|
329 | os.unlink(name) | |
325 |
|
330 | |||
326 | return t |
|
331 | return t | |
327 |
|
332 | |||
328 | def traceback(self): |
|
333 | def traceback(self): | |
329 | '''print exception traceback if traceback printing enabled. |
|
334 | '''print exception traceback if traceback printing enabled. | |
330 | only to call in exception handler. returns true if traceback |
|
335 | only to call in exception handler. returns true if traceback | |
331 | printed.''' |
|
336 | printed.''' | |
332 | if self._traceback: |
|
337 | if self._traceback: | |
333 | traceback.print_exc() |
|
338 | traceback.print_exc() | |
334 | return self._traceback |
|
339 | return self._traceback | |
335 |
|
340 | |||
336 | def geteditor(self): |
|
341 | def geteditor(self): | |
337 | '''return editor to use''' |
|
342 | '''return editor to use''' | |
338 | return (os.environ.get("HGEDITOR") or |
|
343 | return (os.environ.get("HGEDITOR") or | |
339 | self.config("ui", "editor") or |
|
344 | self.config("ui", "editor") or | |
340 | os.environ.get("VISUAL") or |
|
345 | os.environ.get("VISUAL") or | |
341 | os.environ.get("EDITOR", "vi")) |
|
346 | os.environ.get("EDITOR", "vi")) |
General Comments 0
You need to be logged in to leave comments.
Login now