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