]> git.lizzy.rs Git - plan9front.git/blob - sys/src/cmd/hg/mercurial/merge.py
/sys/lib/dist/mkfile: test for .git directory
[plan9front.git] / sys / src / cmd / hg / mercurial / merge.py
1 # merge.py - directory-level update/merge handling for Mercurial
2 #
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 #
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.
7
8 from node import nullid, nullrev, hex, bin
9 from i18n import _
10 import util, filemerge, copies, subrepo
11 import errno, os, shutil
12
13 class mergestate(object):
14     '''track 3-way merge state of individual files'''
15     def __init__(self, repo):
16         self._repo = repo
17         self._read()
18     def reset(self, node=None):
19         self._state = {}
20         if node:
21             self._local = node
22         shutil.rmtree(self._repo.join("merge"), True)
23     def _read(self):
24         self._state = {}
25         try:
26             localnode = None
27             f = self._repo.opener("merge/state")
28             for i, l in enumerate(f):
29                 if i == 0:
30                     localnode = l[:-1]
31                 else:
32                     bits = l[:-1].split("\0")
33                     self._state[bits[0]] = bits[1:]
34             self._local = bin(localnode)
35         except IOError, err:
36             if err.errno != errno.ENOENT:
37                 raise
38     def _write(self):
39         f = self._repo.opener("merge/state", "w")
40         f.write(hex(self._local) + "\n")
41         for d, v in self._state.iteritems():
42             f.write("\0".join([d] + v) + "\n")
43     def add(self, fcl, fco, fca, fd, flags):
44         hash = util.sha1(fcl.path()).hexdigest()
45         self._repo.opener("merge/" + hash, "w").write(fcl.data())
46         self._state[fd] = ['u', hash, fcl.path(), fca.path(),
47                            hex(fca.filenode()), fco.path(), flags]
48         self._write()
49     def __contains__(self, dfile):
50         return dfile in self._state
51     def __getitem__(self, dfile):
52         return self._state[dfile][0]
53     def __iter__(self):
54         l = self._state.keys()
55         l.sort()
56         for f in l:
57             yield f
58     def mark(self, dfile, state):
59         self._state[dfile][0] = state
60         self._write()
61     def resolve(self, dfile, wctx, octx):
62         if self[dfile] == 'r':
63             return 0
64         state, hash, lfile, afile, anode, ofile, flags = self._state[dfile]
65         f = self._repo.opener("merge/" + hash)
66         self._repo.wwrite(dfile, f.read(), flags)
67         fcd = wctx[dfile]
68         fco = octx[ofile]
69         fca = self._repo.filectx(afile, fileid=anode)
70         r = filemerge.filemerge(self._repo, self._local, lfile, fcd, fco, fca)
71         if not r:
72             self.mark(dfile, 'r')
73         return r
74
75 def _checkunknown(wctx, mctx):
76     "check for collisions between unknown files and files in mctx"
77     for f in wctx.unknown():
78         if f in mctx and mctx[f].cmp(wctx[f].data()):
79             raise util.Abort(_("untracked file in working directory differs"
80                                " from file in requested revision: '%s'") % f)
81
82 def _checkcollision(mctx):
83     "check for case folding collisions in the destination context"
84     folded = {}
85     for fn in mctx:
86         fold = fn.lower()
87         if fold in folded:
88             raise util.Abort(_("case-folding collision between %s and %s")
89                              % (fn, folded[fold]))
90         folded[fold] = fn
91
92 def _forgetremoved(wctx, mctx, branchmerge):
93     """
94     Forget removed files
95
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,
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
100     manifest.
101
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
104     as removed.
105     """
106
107     action = []
108     state = branchmerge and 'r' or 'f'
109     for f in wctx.deleted():
110         if f not in mctx:
111             action.append((f, state))
112
113     if not branchmerge:
114         for f in wctx.removed():
115             if f not in mctx:
116                 action.append((f, "f"))
117
118     return action
119
120 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
121     """
122     Merge p1 and p2 with ancestor ma and generate merge action list
123
124     overwrite = whether we clobber working files
125     partial = function to filter file lists
126     """
127
128     def fmerge(f, f2, fa):
129         """merge flags"""
130         a, m, n = ma.flags(fa), m1.flags(f), m2.flags(f2)
131         if m == n: # flags agree
132             return m # unchanged
133         if m and n and not a: # flags set, don't agree, differ from parent
134             r = repo.ui.promptchoice(
135                 _(" conflicting flags for %s\n"
136                   "(n)one, e(x)ec or sym(l)ink?") % f,
137                 (_("&None"), _("E&xec"), _("Sym&link")), 0)
138             if r == 1: return "x" # Exec
139             if r == 2: return "l" # Symlink
140             return ""
141         if m and m != a: # changed from a to m
142             return m
143         if n and n != a: # changed from a to n
144             return n
145         return '' # flag was cleared
146
147     def act(msg, m, f, *args):
148         repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
149         action.append((f, m) + args)
150
151     action, copy = [], {}
152
153     if overwrite:
154         pa = p1
155     elif pa == p2: # backwards
156         pa = p1.p1()
157     elif pa and repo.ui.configbool("merge", "followcopies", True):
158         dirs = repo.ui.configbool("merge", "followdirs", True)
159         copy, diverge = copies.copies(repo, p1, p2, pa, dirs)
160         for of, fl in diverge.iteritems():
161             act("divergent renames", "dr", of, fl)
162
163     repo.ui.note(_("resolving manifests\n"))
164     repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
165     repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
166
167     m1, m2, ma = p1.manifest(), p2.manifest(), pa.manifest()
168     copied = set(copy.values())
169
170     # Compare manifests
171     for f, n in m1.iteritems():
172         if partial and not partial(f):
173             continue
174         if f in m2:
175             rflags = fmerge(f, f, f)
176             a = ma.get(f, nullid)
177             if n == m2[f] or m2[f] == a: # same or local newer
178                 if m1.flags(f) != rflags:
179                     act("update permissions", "e", f, rflags)
180             elif n == a: # remote newer
181                 act("remote is newer", "g", f, rflags)
182             else: # both changed
183                 act("versions differ", "m", f, f, f, rflags, False)
184         elif f in copied: # files we'll deal with on m2 side
185             pass
186         elif f in copy:
187             f2 = copy[f]
188             if f2 not in m2: # directory rename
189                 act("remote renamed directory to " + f2, "d",
190                     f, None, f2, m1.flags(f))
191             else: # case 2 A,B/B/B or case 4,21 A/B/B
192                 act("local copied/moved to " + f2, "m",
193                     f, f2, f, fmerge(f, f2, f2), False)
194         elif f in ma: # clean, a different, no remote
195             if n != ma[f]:
196                 if repo.ui.promptchoice(
197                     _(" local changed %s which remote deleted\n"
198                       "use (c)hanged version or (d)elete?") % f,
199                     (_("&Changed"), _("&Delete")), 0):
200                     act("prompt delete", "r", f)
201                 else:
202                     act("prompt keep", "a", f)
203             elif n[20:] == "a": # added, no remote
204                 act("remote deleted", "f", f)
205             elif n[20:] != "u":
206                 act("other deleted", "r", f)
207
208     for f, n in m2.iteritems():
209         if partial and not partial(f):
210             continue
211         if f in m1 or f in copied: # files already visited
212             continue
213         if f in copy:
214             f2 = copy[f]
215             if f2 not in m1: # directory rename
216                 act("local renamed directory to " + f2, "d",
217                     None, f, f2, m2.flags(f))
218             elif f2 in m2: # rename case 1, A/A,B/A
219                 act("remote copied to " + f, "m",
220                     f2, f, f, fmerge(f2, f, f2), False)
221             else: # case 3,20 A/B/A
222                 act("remote moved to " + f, "m",
223                     f2, f, f, fmerge(f2, f, f2), True)
224         elif f not in ma:
225             act("remote created", "g", f, m2.flags(f))
226         elif n != ma[f]:
227             if repo.ui.promptchoice(
228                 _("remote changed %s which local deleted\n"
229                   "use (c)hanged version or leave (d)eleted?") % f,
230                 (_("&Changed"), _("&Deleted")), 0) == 0:
231                 act("prompt recreating", "g", f, m2.flags(f))
232
233     return action
234
235 def actionkey(a):
236     return a[1] == 'r' and -1 or 0, a
237
238 def applyupdates(repo, action, wctx, mctx):
239     "apply the merge action list to the working directory"
240
241     updated, merged, removed, unresolved = 0, 0, 0, 0
242     ms = mergestate(repo)
243     ms.reset(wctx.parents()[0].node())
244     moves = []
245     action.sort(key=actionkey)
246     substate = wctx.substate # prime
247
248     # prescan for merges
249     for a in action:
250         f, m = a[:2]
251         if m == 'm': # merge
252             f2, fd, flags, move = a[2:]
253             if f == '.hgsubstate': # merged internally
254                 continue
255             repo.ui.debug(_("preserving %s for resolve of %s\n") % (f, fd))
256             fcl = wctx[f]
257             fco = mctx[f2]
258             fca = fcl.ancestor(fco) or repo.filectx(f, fileid=nullrev)
259             ms.add(fcl, fco, fca, fd, flags)
260             if f != fd and move:
261                 moves.append(f)
262
263     # remove renamed files after safely stored
264     for f in moves:
265         if util.lexists(repo.wjoin(f)):
266             repo.ui.debug(_("removing %s\n") % f)
267             os.unlink(repo.wjoin(f))
268
269     audit_path = util.path_auditor(repo.root)
270
271     for a in action:
272         f, m = a[:2]
273         if f and f[0] == "/":
274             continue
275         if m == "r": # remove
276             repo.ui.note(_("removing %s\n") % f)
277             audit_path(f)
278             if f == '.hgsubstate': # subrepo states need updating
279                 subrepo.submerge(repo, wctx, mctx, wctx)
280             try:
281                 util.unlink(repo.wjoin(f))
282             except OSError, inst:
283                 if inst.errno != errno.ENOENT:
284                     repo.ui.warn(_("update failed to remove %s: %s!\n") %
285                                  (f, inst.strerror))
286             removed += 1
287         elif m == "m": # merge
288             if f == '.hgsubstate': # subrepo states need updating
289                 subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx))
290                 continue
291             f2, fd, flags, move = a[2:]
292             r = ms.resolve(fd, wctx, mctx)
293             if r is not None and r > 0:
294                 unresolved += 1
295             else:
296                 if r is None:
297                     updated += 1
298                 else:
299                     merged += 1
300             util.set_flags(repo.wjoin(fd), 'l' in flags, 'x' in flags)
301             if f != fd and move and util.lexists(repo.wjoin(f)):
302                 repo.ui.debug(_("removing %s\n") % f)
303                 os.unlink(repo.wjoin(f))
304         elif m == "g": # get
305             flags = a[2]
306             repo.ui.note(_("getting %s\n") % f)
307             t = mctx.filectx(f).data()
308             repo.wwrite(f, t, flags)
309             updated += 1
310             if f == '.hgsubstate': # subrepo states need updating
311                 subrepo.submerge(repo, wctx, mctx, wctx)
312         elif m == "d": # directory rename
313             f2, fd, flags = a[2:]
314             if f:
315                 repo.ui.note(_("moving %s to %s\n") % (f, fd))
316                 t = wctx.filectx(f).data()
317                 repo.wwrite(fd, t, flags)
318                 util.unlink(repo.wjoin(f))
319             if f2:
320                 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
321                 t = mctx.filectx(f2).data()
322                 repo.wwrite(fd, t, flags)
323             updated += 1
324         elif m == "dr": # divergent renames
325             fl = a[2]
326             repo.ui.warn(_("warning: detected divergent renames of %s to:\n") % f)
327             for nf in fl:
328                 repo.ui.warn(" %s\n" % nf)
329         elif m == "e": # exec
330             flags = a[2]
331             util.set_flags(repo.wjoin(f), 'l' in flags, 'x' in flags)
332
333     return updated, merged, removed, unresolved
334
335 def recordupdates(repo, action, branchmerge):
336     "record merge actions to the dirstate"
337
338     for a in action:
339         f, m = a[:2]
340         if m == "r": # remove
341             if branchmerge:
342                 repo.dirstate.remove(f)
343             else:
344                 repo.dirstate.forget(f)
345         elif m == "a": # re-add
346             if not branchmerge:
347                 repo.dirstate.add(f)
348         elif m == "f": # forget
349             repo.dirstate.forget(f)
350         elif m == "e": # exec change
351             repo.dirstate.normallookup(f)
352         elif m == "g": # get
353             if branchmerge:
354                 repo.dirstate.normaldirty(f)
355             else:
356                 repo.dirstate.normal(f)
357         elif m == "m": # merge
358             f2, fd, flag, move = a[2:]
359             if branchmerge:
360                 # We've done a branch merge, mark this file as merged
361                 # so that we properly record the merger later
362                 repo.dirstate.merge(fd)
363                 if f != f2: # copy/rename
364                     if move:
365                         repo.dirstate.remove(f)
366                     if f != fd:
367                         repo.dirstate.copy(f, fd)
368                     else:
369                         repo.dirstate.copy(f2, fd)
370             else:
371                 # We've update-merged a locally modified file, so
372                 # we set the dirstate to emulate a normal checkout
373                 # of that file some time in the past. Thus our
374                 # merge will appear as a normal local file
375                 # modification.
376                 repo.dirstate.normallookup(fd)
377                 if move:
378                     repo.dirstate.forget(f)
379         elif m == "d": # directory rename
380             f2, fd, flag = a[2:]
381             if not f2 and f not in repo.dirstate:
382                 # untracked file moved
383                 continue
384             if branchmerge:
385                 repo.dirstate.add(fd)
386                 if f:
387                     repo.dirstate.remove(f)
388                     repo.dirstate.copy(f, fd)
389                 if f2:
390                     repo.dirstate.copy(f2, fd)
391             else:
392                 repo.dirstate.normal(fd)
393                 if f:
394                     repo.dirstate.forget(f)
395
396 def update(repo, node, branchmerge, force, partial):
397     """
398     Perform a merge between the working directory and the given node
399
400     branchmerge = whether to merge between branches
401     force = whether to force branch merging or file overwriting
402     partial = a function to filter file lists (dirstate not updated)
403     """
404
405     wlock = repo.wlock()
406     try:
407         wc = repo[None]
408         if node is None:
409             # tip of current branch
410             try:
411                 node = repo.branchtags()[wc.branch()]
412             except KeyError:
413                 if wc.branch() == "default": # no default branch!
414                     node = repo.lookup("tip") # update to tip
415                 else:
416                     raise util.Abort(_("branch %s not found") % wc.branch())
417         overwrite = force and not branchmerge
418         pl = wc.parents()
419         p1, p2 = pl[0], repo[node]
420         pa = p1.ancestor(p2)
421         fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
422         fastforward = False
423
424         ### check phase
425         if not overwrite and len(pl) > 1:
426             raise util.Abort(_("outstanding uncommitted merges"))
427         if branchmerge:
428             if pa == p2:
429                 raise util.Abort(_("can't merge with ancestor"))
430             elif pa == p1:
431                 if p1.branch() != p2.branch():
432                     fastforward = True
433                 else:
434                     raise util.Abort(_("nothing to merge (use 'hg update'"
435                                        " or check 'hg heads')"))
436             if not force and (wc.files() or wc.deleted()):
437                 raise util.Abort(_("outstanding uncommitted changes "
438                                    "(use 'hg status' to list changes)"))
439         elif not overwrite:
440             if pa == p1 or pa == p2: # linear
441                 pass # all good
442             elif p1.branch() == p2.branch():
443                 if wc.files() or wc.deleted():
444                     raise util.Abort(_("crosses branches (use 'hg merge' or "
445                                        "'hg update -C' to discard changes)"))
446                 raise util.Abort(_("crosses branches (use 'hg merge' "
447                                    "or 'hg update -C')"))
448             elif wc.files() or wc.deleted():
449                 raise util.Abort(_("crosses named branches (use "
450                                    "'hg update -C' to discard changes)"))
451             else:
452                 # Allow jumping branches if there are no changes
453                 overwrite = True
454
455         ### calculate phase
456         action = []
457         if not force:
458             _checkunknown(wc, p2)
459         if not util.checkcase(repo.path):
460             _checkcollision(p2)
461         action += _forgetremoved(wc, p2, branchmerge)
462         action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
463
464         ### apply phase
465         if not branchmerge: # just jump to the new rev
466             fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
467         if not partial:
468             repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
469
470         stats = applyupdates(repo, action, wc, p2)
471
472         if not partial:
473             recordupdates(repo, action, branchmerge)
474             repo.dirstate.setparents(fp1, fp2)
475             if not branchmerge and not fastforward:
476                 repo.dirstate.setbranch(p2.branch())
477             repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
478
479         return stats
480     finally:
481         wlock.release()