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