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