7 |
7 |
8 from node import nullid, nullrev |
8 from node import nullid, nullrev |
9 from i18n import _ |
9 from i18n import _ |
10 import errno, util, os, heapq, filemerge |
10 import errno, util, os, heapq, filemerge |
11 |
11 |
12 def checkunknown(wctx, mctx): |
12 def _checkunknown(wctx, mctx): |
13 "check for collisions between unknown files and files in mctx" |
13 "check for collisions between unknown files and files in mctx" |
14 man = mctx.manifest() |
14 man = mctx.manifest() |
15 for f in wctx.unknown(): |
15 for f in wctx.unknown(): |
16 if f in man: |
16 if f in man: |
17 if mctx.filectx(f).cmp(wctx.filectx(f).data()): |
17 if mctx.filectx(f).cmp(wctx.filectx(f).data()): |
18 raise util.Abort(_("untracked file in working directory differs" |
18 raise util.Abort(_("untracked file in working directory differs" |
19 " from file in requested revision: '%s'") |
19 " from file in requested revision: '%s'") |
20 % f) |
20 % f) |
21 |
21 |
22 def checkcollision(mctx): |
22 def _checkcollision(mctx): |
23 "check for case folding collisions in the destination context" |
23 "check for case folding collisions in the destination context" |
24 folded = {} |
24 folded = {} |
25 for fn in mctx.manifest(): |
25 for fn in mctx.manifest(): |
26 fold = fn.lower() |
26 fold = fn.lower() |
27 if fold in folded: |
27 if fold in folded: |
28 raise util.Abort(_("case-folding collision between %s and %s") |
28 raise util.Abort(_("case-folding collision between %s and %s") |
29 % (fn, folded[fold])) |
29 % (fn, folded[fold])) |
30 folded[fold] = fn |
30 folded[fold] = fn |
31 |
31 |
32 def forgetremoved(wctx, mctx, branchmerge): |
32 def _forgetremoved(wctx, mctx, branchmerge): |
33 """ |
33 """ |
34 Forget removed files |
34 Forget removed files |
35 |
35 |
36 If we're jumping between revisions (as opposed to merging), and if |
36 If we're jumping between revisions (as opposed to merging), and if |
37 neither the working directory nor the target rev has the file, |
37 neither the working directory nor the target rev has the file, |
56 if f not in man: |
56 if f not in man: |
57 action.append((f, "f")) |
57 action.append((f, "f")) |
58 |
58 |
59 return action |
59 return action |
60 |
60 |
|
61 def _nonoverlap(d1, d2, d3): |
|
62 "Return list of elements in d1 not in d2 or d3" |
|
63 l = [d for d in d1 if d not in d3 and d not in d2] |
|
64 l.sort() |
|
65 return l |
|
66 |
|
67 def _dirname(f): |
|
68 s = f.rfind("/") |
|
69 if s == -1: |
|
70 return "" |
|
71 return f[:s] |
|
72 |
|
73 def _dirs(files): |
|
74 d = {} |
|
75 for f in files: |
|
76 f = _dirname(f) |
|
77 while f not in d: |
|
78 d[f] = True |
|
79 f = _dirname(f) |
|
80 return d |
|
81 |
|
82 def _findoldnames(fctx, limit): |
|
83 "find files that path was copied from, back to linkrev limit" |
|
84 old = {} |
|
85 seen = {} |
|
86 orig = fctx.path() |
|
87 visit = [fctx] |
|
88 while visit: |
|
89 fc = visit.pop() |
|
90 s = str(fc) |
|
91 if s in seen: |
|
92 continue |
|
93 seen[s] = 1 |
|
94 if fc.path() != orig and fc.path() not in old: |
|
95 old[fc.path()] = 1 |
|
96 if fc.rev() < limit: |
|
97 continue |
|
98 visit += fc.parents() |
|
99 |
|
100 old = old.keys() |
|
101 old.sort() |
|
102 return old |
|
103 |
61 def findcopies(repo, m1, m2, ma, limit): |
104 def findcopies(repo, m1, m2, ma, limit): |
62 """ |
105 """ |
63 Find moves and copies between m1 and m2 back to limit linkrev |
106 Find moves and copies between m1 and m2 back to limit linkrev |
64 """ |
107 """ |
65 |
|
66 def nonoverlap(d1, d2, d3): |
|
67 "Return list of elements in d1 not in d2 or d3" |
|
68 l = [d for d in d1 if d not in d3 and d not in d2] |
|
69 l.sort() |
|
70 return l |
|
71 |
|
72 def dirname(f): |
|
73 s = f.rfind("/") |
|
74 if s == -1: |
|
75 return "" |
|
76 return f[:s] |
|
77 |
|
78 def dirs(files): |
|
79 d = {} |
|
80 for f in files: |
|
81 f = dirname(f) |
|
82 while f not in d: |
|
83 d[f] = True |
|
84 f = dirname(f) |
|
85 return d |
|
86 |
108 |
87 wctx = repo.workingctx() |
109 wctx = repo.workingctx() |
88 |
110 |
89 def makectx(f, n): |
111 def makectx(f, n): |
90 if len(n) == 20: |
112 if len(n) == 20: |
91 return repo.filectx(f, fileid=n) |
113 return repo.filectx(f, fileid=n) |
92 return wctx.filectx(f) |
114 return wctx.filectx(f) |
93 ctx = util.cachefunc(makectx) |
115 ctx = util.cachefunc(makectx) |
94 |
116 |
95 def findold(fctx): |
|
96 "find files that path was copied from, back to linkrev limit" |
|
97 old = {} |
|
98 seen = {} |
|
99 orig = fctx.path() |
|
100 visit = [fctx] |
|
101 while visit: |
|
102 fc = visit.pop() |
|
103 s = str(fc) |
|
104 if s in seen: |
|
105 continue |
|
106 seen[s] = 1 |
|
107 if fc.path() != orig and fc.path() not in old: |
|
108 old[fc.path()] = 1 |
|
109 if fc.rev() < limit and fc.rev() is not None: |
|
110 continue |
|
111 visit += fc.parents() |
|
112 |
|
113 old = old.keys() |
|
114 old.sort() |
|
115 return old |
|
116 |
|
117 copy = {} |
117 copy = {} |
118 fullcopy = {} |
118 fullcopy = {} |
119 diverge = {} |
119 diverge = {} |
120 |
120 |
121 def checkcopies(c, man, aman): |
121 def checkcopies(c, man, aman): |
122 '''check possible copies for filectx c''' |
122 '''check possible copies for filectx c''' |
123 for of in findold(c): |
123 for of in _findoldnames(c, limit): |
124 fullcopy[c.path()] = of # remember for dir rename detection |
124 fullcopy[c.path()] = of # remember for dir rename detection |
125 if of not in man: # original file not in other manifest? |
125 if of not in man: # original file not in other manifest? |
126 if of in ma: |
126 if of in ma: |
127 diverge.setdefault(of, []).append(c.path()) |
127 diverge.setdefault(of, []).append(c.path()) |
128 continue |
128 continue |
147 if not m1 or not m2 or not ma: |
147 if not m1 or not m2 or not ma: |
148 return {}, {} |
148 return {}, {} |
149 |
149 |
150 repo.ui.debug(_(" searching for copies back to rev %d\n") % limit) |
150 repo.ui.debug(_(" searching for copies back to rev %d\n") % limit) |
151 |
151 |
152 u1 = nonoverlap(m1, m2, ma) |
152 u1 = _nonoverlap(m1, m2, ma) |
153 u2 = nonoverlap(m2, m1, ma) |
153 u2 = _nonoverlap(m2, m1, ma) |
154 |
154 |
155 if u1: |
155 if u1: |
156 repo.ui.debug(_(" unmatched files in local:\n %s\n") |
156 repo.ui.debug(_(" unmatched files in local:\n %s\n") |
157 % "\n ".join(u1)) |
157 % "\n ".join(u1)) |
158 if u2: |
158 if u2: |
186 return copy, diverge |
186 return copy, diverge |
187 |
187 |
188 repo.ui.debug(_(" checking for directory renames\n")) |
188 repo.ui.debug(_(" checking for directory renames\n")) |
189 |
189 |
190 # generate a directory move map |
190 # generate a directory move map |
191 d1, d2 = dirs(m1), dirs(m2) |
191 d1, d2 = _dirs(m1), _dirs(m2) |
192 invalid = {} |
192 invalid = {} |
193 dirmove = {} |
193 dirmove = {} |
194 |
194 |
195 # examine each file copy for a potential directory move, which is |
195 # examine each file copy for a potential directory move, which is |
196 # when all the files in a directory are moved to a new directory |
196 # when all the files in a directory are moved to a new directory |
197 for dst, src in fullcopy.items(): |
197 for dst, src in fullcopy.items(): |
198 dsrc, ddst = dirname(src), dirname(dst) |
198 dsrc, ddst = _dirname(src), _dirname(dst) |
199 if dsrc in invalid: |
199 if dsrc in invalid: |
200 # already seen to be uninteresting |
200 # already seen to be uninteresting |
201 continue |
201 continue |
202 elif dsrc in d1 and ddst in d1: |
202 elif dsrc in d1 and ddst in d1: |
203 # directory wasn't entirely moved locally |
203 # directory wasn't entirely moved locally |
234 repo.ui.debug(_(" file %s -> %s\n") % (f, copy[f])) |
234 repo.ui.debug(_(" file %s -> %s\n") % (f, copy[f])) |
235 break |
235 break |
236 |
236 |
237 return copy, diverge |
237 return copy, diverge |
238 |
238 |
239 def symmetricdifference(repo, rev1, rev2): |
239 def _symmetricdifference(repo, rev1, rev2): |
240 """symmetric difference of the sets of ancestors of rev1 and rev2 |
240 """symmetric difference of the sets of ancestors of rev1 and rev2 |
241 |
241 |
242 I.e. revisions that are ancestors of rev1 or rev2, but not both. |
242 I.e. revisions that are ancestors of rev1 or rev2, but not both. |
243 """ |
243 """ |
244 # basic idea: |
244 # basic idea: |
338 if not (backwards or overwrite): |
338 if not (backwards or overwrite): |
339 rev1 = p1.rev() |
339 rev1 = p1.rev() |
340 if rev1 is None: |
340 if rev1 is None: |
341 # p1 is a workingctx |
341 # p1 is a workingctx |
342 rev1 = p1.parents()[0].rev() |
342 rev1 = p1.parents()[0].rev() |
343 limit = min(symmetricdifference(repo, rev1, p2.rev())) |
343 limit = min(_symmetricdifference(repo, rev1, p2.rev())) |
344 copy, diverge = findcopies(repo, m1, m2, ma, limit) |
344 copy, diverge = findcopies(repo, m1, m2, ma, limit) |
345 |
345 |
346 for of, fl in diverge.items(): |
346 for of, fl in diverge.items(): |
347 act("divergent renames", "dr", of, fl) |
347 act("divergent renames", "dr", of, fl) |
348 |
348 |
613 raise util.Abort(_("outstanding uncommitted changes")) |
613 raise util.Abort(_("outstanding uncommitted changes")) |
614 |
614 |
615 ### calculate phase |
615 ### calculate phase |
616 action = [] |
616 action = [] |
617 if not force: |
617 if not force: |
618 checkunknown(wc, p2) |
618 _checkunknown(wc, p2) |
619 if not util.checkfolding(repo.path): |
619 if not util.checkfolding(repo.path): |
620 checkcollision(p2) |
620 _checkcollision(p2) |
621 action += forgetremoved(wc, p2, branchmerge) |
621 action += _forgetremoved(wc, p2, branchmerge) |
622 action += manifestmerge(repo, wc, p2, pa, overwrite, partial) |
622 action += manifestmerge(repo, wc, p2, pa, overwrite, partial) |
623 |
623 |
624 ### apply phase |
624 ### apply phase |
625 if not branchmerge: # just jump to the new rev |
625 if not branchmerge: # just jump to the new rev |
626 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, '' |
626 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, '' |