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