##// END OF EJS Templates
Benoit Boissinot -
r3592:fffc8a73 default
parent child Browse files
Show More
@@ -1,422 +1,418
1 1 # merge.py - directory-level update/merge handling for Mercurial
2 2 #
3 3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import *
9 9 from i18n import gettext as _
10 10 from demandload import *
11 11 demandload(globals(), "errno util os tempfile")
12 12
13 13 def filemerge(repo, fw, fo, wctx, mctx):
14 14 """perform a 3-way merge in the working directory
15 15
16 16 fw = filename in the working directory
17 17 fo = filename in other parent
18 18 wctx, mctx = working and merge changecontexts
19 19 """
20 20
21 21 def temp(prefix, ctx):
22 22 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
23 23 (fd, name) = tempfile.mkstemp(prefix=pre)
24 24 f = os.fdopen(fd, "wb")
25 25 repo.wwrite(ctx.path(), ctx.data(), f)
26 26 f.close()
27 27 return name
28 28
29 29 fcm = wctx.filectx(fw)
30 30 fco = mctx.filectx(fo)
31 31
32 32 if not fco.cmp(fcm.data()): # files identical?
33 33 return None
34 34
35 35 fca = fcm.ancestor(fco)
36 36 if not fca:
37 37 fca = repo.filectx(fw, fileid=nullrev)
38 38 a = repo.wjoin(fw)
39 39 b = temp("base", fca)
40 40 c = temp("other", fco)
41 41
42 42 if fw != fo:
43 43 repo.ui.status(_("merging %s and %s\n") % (fw, fo))
44 44 else:
45 45 repo.ui.status(_("merging %s\n") % fw)
46 46
47 47 repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcm, fco, fca))
48 48
49 49 cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
50 50 or "hgmerge")
51 51 r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
52 52 environ={'HG_FILE': fw,
53 53 'HG_MY_NODE': str(wctx.parents()[0]),
54 54 'HG_OTHER_NODE': str(mctx)})
55 55 if r:
56 56 repo.ui.warn(_("merging %s failed!\n") % fw)
57 57
58 58 os.unlink(b)
59 59 os.unlink(c)
60 60 return r
61 61
62 62 def checkunknown(wctx, mctx):
63 63 "check for collisions between unknown files and files in mctx"
64 64 man = mctx.manifest()
65 65 for f in wctx.unknown():
66 66 if f in man:
67 67 if mctx.filectx(f).cmp(wctx.filectx(f).data()):
68 68 raise util.Abort(_("'%s' already exists in the working"
69 69 " dir and differs from remote") % f)
70 70
71 71 def forgetremoved(wctx, mctx):
72 72 """
73 73 Forget removed files
74 74
75 75 If we're jumping between revisions (as opposed to merging), and if
76 76 neither the working directory nor the target rev has the file,
77 77 then we need to remove it from the dirstate, to prevent the
78 78 dirstate from listing the file when it is no longer in the
79 79 manifest.
80 80 """
81 81
82 82 action = []
83 83 man = mctx.manifest()
84 84 for f in wctx.deleted() + wctx.removed():
85 85 if f not in man:
86 86 action.append((f, "f"))
87 87
88 88 return action
89 89
90 90 def nonoverlap(d1, d2, d3):
91 91 "Return list of elements in d1 not in d2 or d3"
92 92
93 93 l = []
94 94 for d in d1:
95 95 if d not in d3 and d not in d2:
96 96 l.append(d)
97 97
98 98 l.sort()
99 99 return l
100 100
101 101 def findold(fctx, limit):
102 102 "find files that path was copied from, back to linkrev limit"
103 103
104 104 old = {}
105 105 orig = fctx.path()
106 106 visit = [fctx]
107 107 while visit:
108 108 fc = visit.pop()
109 109 if fc.rev() < limit:
110 110 continue
111 111 if fc.path() != orig and fc.path() not in old:
112 112 old[fc.path()] = 1
113 113 visit += fc.parents()
114 114
115 115 old = old.keys()
116 116 old.sort()
117 117 return old
118 118
119 119 def findcopies(repo, m1, m2, ma, limit):
120 120 """
121 121 Find moves and copies between m1 and m2 back to limit linkrev
122 122 """
123 123
124 124 if not repo.ui.configbool("merge", "followcopies", True):
125 125 return {}
126 126
127 127 # avoid silly behavior for update from empty dir
128 128 if not m1:
129 129 return {}
130 130
131 131 dcopies = repo.dirstate.copies()
132 132 copy = {}
133 133 match = {}
134 134 u1 = nonoverlap(m1, m2, ma)
135 135 u2 = nonoverlap(m2, m1, ma)
136 136 ctx = util.cachefunc(lambda f,n: repo.filectx(f, fileid=n[:20]))
137 137
138 138 def checkpair(c, f2, man):
139 139 ''' check if an apparent pair actually matches '''
140 140 c2 = ctx(f2, man[f2])
141 141 ca = c.ancestor(c2)
142 142 if ca and ca.path() == c.path() or ca.path() == c2.path():
143 143 copy[c.path()] = f2
144 144 copy[f2] = c.path()
145 145
146 146 for f in u1:
147 147 c = ctx(dcopies.get(f, f), m1[f])
148 148 for of in findold(c, limit):
149 149 if of in m2:
150 150 checkpair(c, of, m2)
151 151 else:
152 152 match.setdefault(of, []).append(f)
153 153
154 154 for f in u2:
155 155 c = ctx(f, m2[f])
156 156 for of in findold(c, limit):
157 157 if of in m1:
158 158 checkpair(c, of, m1)
159 159 elif of in match:
160 160 for mf in match[of]:
161 161 checkpair(c, mf, m1)
162 162
163 163 return copy
164 164
165 165 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
166 166 """
167 167 Merge p1 and p2 with ancestor ma and generate merge action list
168 168
169 169 overwrite = whether we clobber working files
170 170 partial = function to filter file lists
171 171 """
172 172
173 173 repo.ui.note(_("resolving manifests\n"))
174 174 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
175 175 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
176 176
177 177 m1 = p1.manifest()
178 178 m2 = p2.manifest()
179 179 ma = pa.manifest()
180 180 backwards = (pa == p2)
181 181 action = []
182 182 copy = {}
183 183
184 184 def fmerge(f, f2=None, fa=None):
185 185 """merge executable flags"""
186 186 if not f2:
187 187 f2 = f
188 188 fa = f
189 189 a, b, c = ma.execf(fa), m1.execf(f), m2.execf(f2)
190 190 return ((a^b) | (a^c)) ^ a
191 191
192 192 def act(msg, m, f, *args):
193 193 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
194 194 action.append((f, m) + args)
195 195
196 196 if not (backwards or overwrite):
197 197 copy = findcopies(repo, m1, m2, ma, pa.rev())
198 198
199 199 # Compare manifests
200 200 for f, n in m1.iteritems():
201 201 if partial and not partial(f):
202 202 continue
203 203 if f in m2:
204 204 # are files different?
205 205 if n != m2[f]:
206 206 a = ma.get(f, nullid)
207 207 # are both different from the ancestor?
208 208 if not overwrite and n != a and m2[f] != a:
209 209 act("versions differ", "m", f, f, f, fmerge(f), False)
210 210 # are we clobbering?
211 211 # is remote's version newer?
212 212 # or are we going back in time and clean?
213 213 elif overwrite or m2[f] != a or (backwards and not n[20:]):
214 214 act("remote is newer", "g", f, m2.execf(f))
215 215 # local is newer, not overwrite, check mode bits
216 216 elif fmerge(f) != m1.execf(f):
217 217 act("update permissions", "e", f, m2.execf(f))
218 218 # contents same, check mode bits
219 219 elif m1.execf(f) != m2.execf(f):
220 220 if overwrite or fmerge(f) != m1.execf(f):
221 221 act("update permissions", "e", f, m2.execf(f))
222 222 elif f in copy:
223 223 f2 = copy[f]
224 224 if f in ma: # case 3,20 A/B/A
225 225 act("remote moved", "m", f, f2, f2, fmerge(f, f2, f), True)
226 226 else:
227 227 if f2 in m1: # case 2 A,B/B/B
228 228 act("local copied", "m",
229 229 f, f2, f, fmerge(f, f2, f2), False)
230 230 else: # case 4,21 A/B/B
231 231 act("local moved", "m",
232 232 f, f2, f, fmerge(f, f2, f2), False)
233 233 elif f in ma:
234 234 if n != ma[f] and not overwrite:
235 235 if repo.ui.prompt(
236 236 (_(" local changed %s which remote deleted\n") % f) +
237 237 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("d"):
238 238 act("prompt delete", "r", f)
239 239 else:
240 240 act("other deleted", "r", f)
241 241 else:
242 242 # file is created on branch or in working directory
243 243 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
244 244 act("remote deleted", "r", f)
245 245
246 246 for f, n in m2.iteritems():
247 247 if partial and not partial(f):
248 248 continue
249 249 if f in m1:
250 250 continue
251 251 if f in copy:
252 252 f2 = copy[f]
253 253 if f2 not in m2: # already seen
254 254 continue
255 255 # rename case 1, A/A,B/A
256 256 act("remote copied", "m", f2, f, f, fmerge(f2, f, f2), False)
257 257 elif f in ma:
258 258 if overwrite or backwards:
259 259 act("recreating", "g", f, m2.execf(f))
260 260 elif n != ma[f]:
261 261 if repo.ui.prompt(
262 262 (_("remote changed %s which local deleted\n") % f) +
263 263 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("k"):
264 264 act("prompt recreating", "g", f, m2.execf(f))
265 265 else:
266 266 act("remote created", "g", f, m2.execf(f))
267 267
268 268 return action
269 269
270 270 def applyupdates(repo, action, wctx, mctx):
271 271 "apply the merge action list to the working directory"
272 272
273 273 updated, merged, removed, unresolved = 0, 0, 0, 0
274 274 action.sort()
275 275 for a in action:
276 276 f, m = a[:2]
277 277 if f[0] == "/":
278 278 continue
279 279 if m == "r": # remove
280 280 repo.ui.note(_("removing %s\n") % f)
281 281 util.audit_path(f)
282 282 try:
283 283 util.unlink(repo.wjoin(f))
284 284 except OSError, inst:
285 285 if inst.errno != errno.ENOENT:
286 286 repo.ui.warn(_("update failed to remove %s: %s!\n") %
287 287 (f, inst.strerror))
288 288 removed +=1
289 289 elif m == "m": # merge
290 290 f2, fd, flag, move = a[2:]
291 291 r = filemerge(repo, f, f2, wctx, mctx)
292 292 if r > 0:
293 293 unresolved += 1
294 294 else:
295 295 if r is None:
296 296 updated += 1
297 297 else:
298 298 merged += 1
299 299 if f != fd:
300 300 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
301 301 repo.wwrite(fd, repo.wread(f))
302 302 if move:
303 303 repo.ui.debug(_("removing %s\n") % f)
304 304 os.unlink(repo.wjoin(f))
305 305 util.set_exec(repo.wjoin(fd), flag)
306 306 elif m == "g": # get
307 307 flag = a[2]
308 308 repo.ui.note(_("getting %s\n") % f)
309 309 t = mctx.filectx(f).data()
310 310 repo.wwrite(f, t)
311 311 util.set_exec(repo.wjoin(f), flag)
312 312 updated += 1
313 313 elif m == "e": # exec
314 314 flag = a[2]
315 315 util.set_exec(repo.wjoin(f), flag)
316 316
317 317 return updated, merged, removed, unresolved
318 318
319 319 def recordupdates(repo, action, branchmerge):
320 320 "record merge actions to the dirstate"
321 321
322 322 for a in action:
323 323 f, m = a[:2]
324 324 if m == "r": # remove
325 325 if branchmerge:
326 326 repo.dirstate.update([f], 'r')
327 327 else:
328 328 repo.dirstate.forget([f])
329 329 elif m == "f": # forget
330 330 repo.dirstate.forget([f])
331 331 elif m == "g": # get
332 332 if branchmerge:
333 333 repo.dirstate.update([f], 'n', st_mtime=-1)
334 334 else:
335 335 repo.dirstate.update([f], 'n')
336 336 elif m == "m": # merge
337 337 f2, fd, flag, move = a[2:]
338 338 if branchmerge:
339 339 # We've done a branch merge, mark this file as merged
340 340 # so that we properly record the merger later
341 341 repo.dirstate.update([fd], 'm')
342 342 if f != f2: # copy/rename
343 343 if move:
344 344 repo.dirstate.update([f], 'r')
345 345 if f != fd:
346 346 repo.dirstate.copy(f, fd)
347 347 else:
348 348 repo.dirstate.copy(f2, fd)
349 349 else:
350 350 # We've update-merged a locally modified file, so
351 351 # we set the dirstate to emulate a normal checkout
352 352 # of that file some time in the past. Thus our
353 353 # merge will appear as a normal local file
354 354 # modification.
355 355 repo.dirstate.update([fd], 'n', st_size=-1, st_mtime=-1)
356 356 if move:
357 357 repo.dirstate.forget([f])
358 358
359 359 def update(repo, node, branchmerge, force, partial, wlock):
360 360 """
361 361 Perform a merge between the working directory and the given node
362 362
363 363 branchmerge = whether to merge between branches
364 364 force = whether to force branch merging or file overwriting
365 365 partial = a function to filter file lists (dirstate not updated)
366 366 wlock = working dir lock, if already held
367 367 """
368 368
369 369 if not wlock:
370 370 wlock = repo.wlock()
371 371
372 372 overwrite = force and not branchmerge
373 373 forcemerge = force and branchmerge
374 374 wc = repo.workingctx()
375 375 pl = wc.parents()
376 376 p1, p2 = pl[0], repo.changectx(node)
377 377 pa = p1.ancestor(p2)
378 378 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
379 379
380 380 ### check phase
381 381 if not overwrite and len(pl) > 1:
382 382 raise util.Abort(_("outstanding uncommitted merges"))
383 383 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
384 384 if branchmerge:
385 385 raise util.Abort(_("there is nothing to merge, just use "
386 386 "'hg update' or look at 'hg heads'"))
387 elif not branchmerge:
388 if not overwrite:
389 if wc.files():
390 raise util.Abort(_("outstanding uncommited changes, use "
391 "'hg update -C' to lose changes"))
392 else:
393 overwrite = True
387 elif not (overwrite or branchmerge):
388 raise util.Abort(_("update spans branches, use 'hg merge' "
389 "or 'hg update -C' to lose changes"))
394 390 if branchmerge and not forcemerge:
395 391 if wc.modified() or wc.added() or wc.removed():
396 392 raise util.Abort(_("outstanding uncommitted changes"))
397 393
398 394 ### calculate phase
399 395 action = []
400 396 if not force:
401 397 checkunknown(wc, p2)
402 398 if not branchmerge:
403 399 action += forgetremoved(wc, p2)
404 400 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
405 401
406 402 ### apply phase
407 403 if not branchmerge: # just jump to the new rev
408 404 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
409 405 if not partial:
410 406 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
411 407
412 408 stats = applyupdates(repo, action, wc, p2)
413 409
414 410 if not partial:
415 411 recordupdates(repo, action, branchmerge)
416 412 repo.dirstate.setparents(fp1, fp2)
417 413 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
418 414 if not branchmerge:
419 415 repo.opener("branch", "w").write(p2.branch() + "\n")
420 416
421 417 return stats
422 418
@@ -1,24 +1,20
1 1 #!/bin/sh
2 2
3 3 mkdir t
4 4 cd t
5 5 hg init
6 6 echo This is file a1 > a
7 7 echo This is file b1 > b
8 8 hg add a b
9 9 hg commit -m "commit #0" -d "1000000 0"
10 10 echo This is file b22 > b
11 11 hg commit -m"comment #1" -d "1000000 0"
12 12 hg update 0
13 13 rm b
14 14 hg commit -A -m"comment #2" -d "1000000 0"
15
16 echo This is file a > a
17 15 # in theory, we shouldn't need the "-y" below, but it prevents
18 16 # this test from hanging when "hg update" erroneously prompts the
19 17 # user for "keep or delete"
20 hg update -y 1 # should fail (unclean repo)
21 hg revert a
22 18 hg update -y 1
23 19
24 20 exit 0
@@ -1,4 +1,3
1 1 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
2 2 removing b
3 abort: outstanding uncommited changes, use 'hg update -C' to lose changes
4 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
3 abort: update spans branches, use 'hg merge' or 'hg update -C' to lose changes
@@ -1,136 +1,136
1 1 adding a
2 2 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
3 3 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
4 4 diff -r 33aaa84a386b a
5 5 --- a/a
6 6 +++ b/a
7 7 @@ -1,1 +1,1 @@ a
8 8 -a
9 9 +abc
10 10 adding b
11 11 M a
12 12 changeset: 0:33aaa84a386b
13 13 user: test
14 14 date: Mon Jan 12 13:46:40 1970 +0000
15 15 summary: 1
16 16
17 17 resolving manifests
18 18 overwrite False partial False
19 19 ancestor 33aaa84a386b local 33aaa84a386b+ remote 802f095af299
20 20 a: versions differ -> m
21 21 b: remote created -> g
22 22 merging a
23 23 my a@33aaa84a386b+ other a@802f095af299 ancestor a@33aaa84a386b
24 24 getting b
25 25 1 files updated, 1 files merged, 0 files removed, 0 files unresolved
26 26 changeset: 1:802f095af299
27 27 tag: tip
28 28 user: test
29 29 date: Mon Jan 12 13:46:40 1970 +0000
30 30 summary: 2
31 31
32 32 resolving manifests
33 33 overwrite False partial False
34 34 ancestor 33aaa84a386b local 802f095af299+ remote 33aaa84a386b
35 35 b: remote deleted -> r
36 36 removing b
37 37 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
38 38 changeset: 0:33aaa84a386b
39 39 user: test
40 40 date: Mon Jan 12 13:46:40 1970 +0000
41 41 summary: 1
42 42
43 43 abort: there is nothing to merge - use "hg update" instead
44 44 failed
45 45 changeset: 0:33aaa84a386b
46 46 user: test
47 47 date: Mon Jan 12 13:46:40 1970 +0000
48 48 summary: 1
49 49
50 50 resolving manifests
51 51 overwrite False partial False
52 52 ancestor 33aaa84a386b local 33aaa84a386b+ remote 802f095af299
53 53 a: versions differ -> m
54 54 b: remote created -> g
55 55 merging a
56 56 my a@33aaa84a386b+ other a@802f095af299 ancestor a@33aaa84a386b
57 57 getting b
58 58 1 files updated, 1 files merged, 0 files removed, 0 files unresolved
59 59 changeset: 1:802f095af299
60 60 tag: tip
61 61 user: test
62 62 date: Mon Jan 12 13:46:40 1970 +0000
63 63 summary: 2
64 64
65 65 changeset: 1:802f095af299
66 66 tag: tip
67 67 user: test
68 68 date: Mon Jan 12 13:46:40 1970 +0000
69 69 files: a b
70 70 description:
71 71 2
72 72
73 73
74 74 changeset: 0:33aaa84a386b
75 75 user: test
76 76 date: Mon Jan 12 13:46:40 1970 +0000
77 77 files: a
78 78 description:
79 79 1
80 80
81 81
82 82 diff -r 802f095af299 a
83 83 --- a/a
84 84 +++ b/a
85 85 @@ -1,1 +1,1 @@ a2
86 86 -a2
87 87 +abc
88 88 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
89 89 adding b
90 90 M a
91 91 changeset: 1:802f095af299
92 92 user: test
93 93 date: Mon Jan 12 13:46:40 1970 +0000
94 94 summary: 2
95 95
96 abort: outstanding uncommited changes, use 'hg update -C' to lose changes
96 abort: update spans branches, use 'hg merge' or 'hg update -C' to lose changes
97 97 failed
98 98 abort: outstanding uncommitted changes
99 99 failed
100 100 resolving manifests
101 101 overwrite False partial False
102 102 ancestor 33aaa84a386b local 802f095af299+ remote 030602aee63d
103 103 a: versions differ -> m
104 104 b: versions differ -> m
105 105 merging a
106 106 my a@802f095af299+ other a@030602aee63d ancestor a@33aaa84a386b
107 107 merging b
108 108 my b@802f095af299+ other b@030602aee63d ancestor b@000000000000
109 109 0 files updated, 2 files merged, 0 files removed, 0 files unresolved
110 110 (branch merge, don't forget to commit)
111 111 changeset: 1:802f095af299
112 112 user: test
113 113 date: Mon Jan 12 13:46:40 1970 +0000
114 114 summary: 2
115 115
116 116 changeset: 2:030602aee63d
117 117 tag: tip
118 118 parent: 0:33aaa84a386b
119 119 user: test
120 120 date: Mon Jan 12 13:46:40 1970 +0000
121 121 summary: 3
122 122
123 123 diff -r 802f095af299 a
124 124 --- a/a
125 125 +++ b/a
126 126 @@ -1,1 +1,1 @@ a2
127 127 -a2
128 128 +abc
129 129 adding a
130 130 pulling from ../a
131 131 requesting all changes
132 132 adding changesets
133 133 adding manifests
134 134 adding file changes
135 135 added 1 changesets with 1 changes to 1 files
136 136 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
General Comments 0
You need to be logged in to leave comments. Login now