##// END OF EJS Templates
merge: make test for fast-forward merge stricter (issue619)...
Matt Mackall -
r4748:8808ea7d default
parent child Browse files
Show More
@@ -0,0 +1,20
1 #!/bin/sh
2
3 mkdir t
4 cd t
5 hg init
6 echo a > a
7 hg ci -Ama -d '1000000000 0'
8 echo b > b
9 hg branch b
10 hg ci -Amb -d '1000000000 0'
11 hg co -C 0
12
13 echo fast-forward
14 hg merge b
15 hg ci -Ammerge -d '1000000000 0'
16
17 echo bogus fast-forward should fail
18 hg merge b
19
20 echo done
@@ -0,0 +1,10
1 adding a
2 marked working directory as branch b
3 adding b
4 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
5 fast-forward
6 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
7 (branch merge, don't forget to commit)
8 bogus fast-forward should fail
9 abort: there is nothing to merge, just use 'hg update' or look at 'hg heads'
10 done
@@ -1,519 +1,525
1 # context.py - changeset and file context objects for mercurial
1 # context.py - changeset and file context objects for mercurial
2 #
2 #
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from node import *
8 from node import *
9 from i18n import _
9 from i18n import _
10 import ancestor, bdiff, repo, revlog, util, os, errno
10 import ancestor, bdiff, repo, revlog, util, os, errno
11
11
12 class changectx(object):
12 class changectx(object):
13 """A changecontext object makes access to data related to a particular
13 """A changecontext object makes access to data related to a particular
14 changeset convenient."""
14 changeset convenient."""
15 def __init__(self, repo, changeid=None):
15 def __init__(self, repo, changeid=None):
16 """changeid is a revision number, node, or tag"""
16 """changeid is a revision number, node, or tag"""
17 self._repo = repo
17 self._repo = repo
18
18
19 if not changeid and changeid != 0:
19 if not changeid and changeid != 0:
20 p1, p2 = self._repo.dirstate.parents()
20 p1, p2 = self._repo.dirstate.parents()
21 self._rev = self._repo.changelog.rev(p1)
21 self._rev = self._repo.changelog.rev(p1)
22 if self._rev == -1:
22 if self._rev == -1:
23 changeid = 'tip'
23 changeid = 'tip'
24 else:
24 else:
25 self._node = p1
25 self._node = p1
26 return
26 return
27
27
28 self._node = self._repo.lookup(changeid)
28 self._node = self._repo.lookup(changeid)
29 self._rev = self._repo.changelog.rev(self._node)
29 self._rev = self._repo.changelog.rev(self._node)
30
30
31 def __str__(self):
31 def __str__(self):
32 return short(self.node())
32 return short(self.node())
33
33
34 def __repr__(self):
34 def __repr__(self):
35 return "<changectx %s>" % str(self)
35 return "<changectx %s>" % str(self)
36
36
37 def __eq__(self, other):
37 def __eq__(self, other):
38 try:
38 try:
39 return self._rev == other._rev
39 return self._rev == other._rev
40 except AttributeError:
40 except AttributeError:
41 return False
41 return False
42
42
43 def __ne__(self, other):
44 return not (self == other)
45
43 def __nonzero__(self):
46 def __nonzero__(self):
44 return self._rev != nullrev
47 return self._rev != nullrev
45
48
46 def __getattr__(self, name):
49 def __getattr__(self, name):
47 if name == '_changeset':
50 if name == '_changeset':
48 self._changeset = self._repo.changelog.read(self.node())
51 self._changeset = self._repo.changelog.read(self.node())
49 return self._changeset
52 return self._changeset
50 elif name == '_manifest':
53 elif name == '_manifest':
51 self._manifest = self._repo.manifest.read(self._changeset[0])
54 self._manifest = self._repo.manifest.read(self._changeset[0])
52 return self._manifest
55 return self._manifest
53 elif name == '_manifestdelta':
56 elif name == '_manifestdelta':
54 md = self._repo.manifest.readdelta(self._changeset[0])
57 md = self._repo.manifest.readdelta(self._changeset[0])
55 self._manifestdelta = md
58 self._manifestdelta = md
56 return self._manifestdelta
59 return self._manifestdelta
57 else:
60 else:
58 raise AttributeError, name
61 raise AttributeError, name
59
62
60 def changeset(self): return self._changeset
63 def changeset(self): return self._changeset
61 def manifest(self): return self._manifest
64 def manifest(self): return self._manifest
62
65
63 def rev(self): return self._rev
66 def rev(self): return self._rev
64 def node(self): return self._node
67 def node(self): return self._node
65 def user(self): return self._changeset[1]
68 def user(self): return self._changeset[1]
66 def date(self): return self._changeset[2]
69 def date(self): return self._changeset[2]
67 def files(self): return self._changeset[3]
70 def files(self): return self._changeset[3]
68 def description(self): return self._changeset[4]
71 def description(self): return self._changeset[4]
69 def branch(self): return self._changeset[5].get("branch")
72 def branch(self): return self._changeset[5].get("branch")
70 def tags(self): return self._repo.nodetags(self._node)
73 def tags(self): return self._repo.nodetags(self._node)
71
74
72 def parents(self):
75 def parents(self):
73 """return contexts for each parent changeset"""
76 """return contexts for each parent changeset"""
74 p = self._repo.changelog.parents(self._node)
77 p = self._repo.changelog.parents(self._node)
75 return [changectx(self._repo, x) for x in p]
78 return [changectx(self._repo, x) for x in p]
76
79
77 def children(self):
80 def children(self):
78 """return contexts for each child changeset"""
81 """return contexts for each child changeset"""
79 c = self._repo.changelog.children(self._node)
82 c = self._repo.changelog.children(self._node)
80 return [changectx(self._repo, x) for x in c]
83 return [changectx(self._repo, x) for x in c]
81
84
82 def filenode(self, path):
85 def filenode(self, path):
83 if '_manifest' in self.__dict__:
86 if '_manifest' in self.__dict__:
84 try:
87 try:
85 return self._manifest[path]
88 return self._manifest[path]
86 except KeyError:
89 except KeyError:
87 raise revlog.LookupError(_("'%s' not found in manifest") % path)
90 raise revlog.LookupError(_("'%s' not found in manifest") % path)
88 if '_manifestdelta' in self.__dict__ or path in self.files():
91 if '_manifestdelta' in self.__dict__ or path in self.files():
89 if path in self._manifestdelta:
92 if path in self._manifestdelta:
90 return self._manifestdelta[path]
93 return self._manifestdelta[path]
91 node, flag = self._repo.manifest.find(self._changeset[0], path)
94 node, flag = self._repo.manifest.find(self._changeset[0], path)
92 if not node:
95 if not node:
93 raise revlog.LookupError(_("'%s' not found in manifest") % path)
96 raise revlog.LookupError(_("'%s' not found in manifest") % path)
94
97
95 return node
98 return node
96
99
97 def filectx(self, path, fileid=None, filelog=None):
100 def filectx(self, path, fileid=None, filelog=None):
98 """get a file context from this changeset"""
101 """get a file context from this changeset"""
99 if fileid is None:
102 if fileid is None:
100 fileid = self.filenode(path)
103 fileid = self.filenode(path)
101 return filectx(self._repo, path, fileid=fileid,
104 return filectx(self._repo, path, fileid=fileid,
102 changectx=self, filelog=filelog)
105 changectx=self, filelog=filelog)
103
106
104 def filectxs(self):
107 def filectxs(self):
105 """generate a file context for each file in this changeset's
108 """generate a file context for each file in this changeset's
106 manifest"""
109 manifest"""
107 mf = self.manifest()
110 mf = self.manifest()
108 m = mf.keys()
111 m = mf.keys()
109 m.sort()
112 m.sort()
110 for f in m:
113 for f in m:
111 yield self.filectx(f, fileid=mf[f])
114 yield self.filectx(f, fileid=mf[f])
112
115
113 def ancestor(self, c2):
116 def ancestor(self, c2):
114 """
117 """
115 return the ancestor context of self and c2
118 return the ancestor context of self and c2
116 """
119 """
117 n = self._repo.changelog.ancestor(self._node, c2._node)
120 n = self._repo.changelog.ancestor(self._node, c2._node)
118 return changectx(self._repo, n)
121 return changectx(self._repo, n)
119
122
120 class filectx(object):
123 class filectx(object):
121 """A filecontext object makes access to data related to a particular
124 """A filecontext object makes access to data related to a particular
122 filerevision convenient."""
125 filerevision convenient."""
123 def __init__(self, repo, path, changeid=None, fileid=None,
126 def __init__(self, repo, path, changeid=None, fileid=None,
124 filelog=None, changectx=None):
127 filelog=None, changectx=None):
125 """changeid can be a changeset revision, node, or tag.
128 """changeid can be a changeset revision, node, or tag.
126 fileid can be a file revision or node."""
129 fileid can be a file revision or node."""
127 self._repo = repo
130 self._repo = repo
128 self._path = path
131 self._path = path
129
132
130 assert (changeid is not None
133 assert (changeid is not None
131 or fileid is not None
134 or fileid is not None
132 or changectx is not None)
135 or changectx is not None)
133
136
134 if filelog:
137 if filelog:
135 self._filelog = filelog
138 self._filelog = filelog
136
139
137 if fileid is None:
140 if fileid is None:
138 if changectx is None:
141 if changectx is None:
139 self._changeid = changeid
142 self._changeid = changeid
140 else:
143 else:
141 self._changectx = changectx
144 self._changectx = changectx
142 else:
145 else:
143 self._fileid = fileid
146 self._fileid = fileid
144
147
145 def __getattr__(self, name):
148 def __getattr__(self, name):
146 if name == '_changectx':
149 if name == '_changectx':
147 self._changectx = changectx(self._repo, self._changeid)
150 self._changectx = changectx(self._repo, self._changeid)
148 return self._changectx
151 return self._changectx
149 elif name == '_filelog':
152 elif name == '_filelog':
150 self._filelog = self._repo.file(self._path)
153 self._filelog = self._repo.file(self._path)
151 return self._filelog
154 return self._filelog
152 elif name == '_changeid':
155 elif name == '_changeid':
153 self._changeid = self._filelog.linkrev(self._filenode)
156 self._changeid = self._filelog.linkrev(self._filenode)
154 return self._changeid
157 return self._changeid
155 elif name == '_filenode':
158 elif name == '_filenode':
156 if '_fileid' in self.__dict__:
159 if '_fileid' in self.__dict__:
157 self._filenode = self._filelog.lookup(self._fileid)
160 self._filenode = self._filelog.lookup(self._fileid)
158 else:
161 else:
159 self._filenode = self._changectx.filenode(self._path)
162 self._filenode = self._changectx.filenode(self._path)
160 return self._filenode
163 return self._filenode
161 elif name == '_filerev':
164 elif name == '_filerev':
162 self._filerev = self._filelog.rev(self._filenode)
165 self._filerev = self._filelog.rev(self._filenode)
163 return self._filerev
166 return self._filerev
164 else:
167 else:
165 raise AttributeError, name
168 raise AttributeError, name
166
169
167 def __nonzero__(self):
170 def __nonzero__(self):
168 try:
171 try:
169 n = self._filenode
172 n = self._filenode
170 return True
173 return True
171 except revlog.LookupError:
174 except revlog.LookupError:
172 # file is missing
175 # file is missing
173 return False
176 return False
174
177
175 def __str__(self):
178 def __str__(self):
176 return "%s@%s" % (self.path(), short(self.node()))
179 return "%s@%s" % (self.path(), short(self.node()))
177
180
178 def __repr__(self):
181 def __repr__(self):
179 return "<filectx %s>" % str(self)
182 return "<filectx %s>" % str(self)
180
183
181 def __eq__(self, other):
184 def __eq__(self, other):
182 try:
185 try:
183 return (self._path == other._path
186 return (self._path == other._path
184 and self._changeid == other._changeid)
187 and self._changeid == other._changeid)
185 except AttributeError:
188 except AttributeError:
186 return False
189 return False
187
190
191 def __ne__(self, other):
192 return not (self == other)
193
188 def filectx(self, fileid):
194 def filectx(self, fileid):
189 '''opens an arbitrary revision of the file without
195 '''opens an arbitrary revision of the file without
190 opening a new filelog'''
196 opening a new filelog'''
191 return filectx(self._repo, self._path, fileid=fileid,
197 return filectx(self._repo, self._path, fileid=fileid,
192 filelog=self._filelog)
198 filelog=self._filelog)
193
199
194 def filerev(self): return self._filerev
200 def filerev(self): return self._filerev
195 def filenode(self): return self._filenode
201 def filenode(self): return self._filenode
196 def filelog(self): return self._filelog
202 def filelog(self): return self._filelog
197
203
198 def rev(self):
204 def rev(self):
199 if '_changectx' in self.__dict__:
205 if '_changectx' in self.__dict__:
200 return self._changectx.rev()
206 return self._changectx.rev()
201 return self._filelog.linkrev(self._filenode)
207 return self._filelog.linkrev(self._filenode)
202
208
203 def node(self): return self._changectx.node()
209 def node(self): return self._changectx.node()
204 def user(self): return self._changectx.user()
210 def user(self): return self._changectx.user()
205 def date(self): return self._changectx.date()
211 def date(self): return self._changectx.date()
206 def files(self): return self._changectx.files()
212 def files(self): return self._changectx.files()
207 def description(self): return self._changectx.description()
213 def description(self): return self._changectx.description()
208 def branch(self): return self._changectx.branch()
214 def branch(self): return self._changectx.branch()
209 def manifest(self): return self._changectx.manifest()
215 def manifest(self): return self._changectx.manifest()
210 def changectx(self): return self._changectx
216 def changectx(self): return self._changectx
211
217
212 def data(self): return self._filelog.read(self._filenode)
218 def data(self): return self._filelog.read(self._filenode)
213 def renamed(self): return self._filelog.renamed(self._filenode)
219 def renamed(self): return self._filelog.renamed(self._filenode)
214 def path(self): return self._path
220 def path(self): return self._path
215 def size(self): return self._filelog.size(self._filerev)
221 def size(self): return self._filelog.size(self._filerev)
216
222
217 def cmp(self, text): return self._filelog.cmp(self._filenode, text)
223 def cmp(self, text): return self._filelog.cmp(self._filenode, text)
218
224
219 def parents(self):
225 def parents(self):
220 p = self._path
226 p = self._path
221 fl = self._filelog
227 fl = self._filelog
222 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
228 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
223
229
224 r = self.renamed()
230 r = self.renamed()
225 if r:
231 if r:
226 pl[0] = (r[0], r[1], None)
232 pl[0] = (r[0], r[1], None)
227
233
228 return [filectx(self._repo, p, fileid=n, filelog=l)
234 return [filectx(self._repo, p, fileid=n, filelog=l)
229 for p,n,l in pl if n != nullid]
235 for p,n,l in pl if n != nullid]
230
236
231 def children(self):
237 def children(self):
232 # hard for renames
238 # hard for renames
233 c = self._filelog.children(self._filenode)
239 c = self._filelog.children(self._filenode)
234 return [filectx(self._repo, self._path, fileid=x,
240 return [filectx(self._repo, self._path, fileid=x,
235 filelog=self._filelog) for x in c]
241 filelog=self._filelog) for x in c]
236
242
237 def annotate(self, follow=False):
243 def annotate(self, follow=False):
238 '''returns a list of tuples of (ctx, line) for each line
244 '''returns a list of tuples of (ctx, line) for each line
239 in the file, where ctx is the filectx of the node where
245 in the file, where ctx is the filectx of the node where
240 that line was last changed'''
246 that line was last changed'''
241
247
242 def decorate(text, rev):
248 def decorate(text, rev):
243 return ([rev] * len(text.splitlines()), text)
249 return ([rev] * len(text.splitlines()), text)
244
250
245 def pair(parent, child):
251 def pair(parent, child):
246 for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
252 for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
247 child[0][b1:b2] = parent[0][a1:a2]
253 child[0][b1:b2] = parent[0][a1:a2]
248 return child
254 return child
249
255
250 getlog = util.cachefunc(lambda x: self._repo.file(x))
256 getlog = util.cachefunc(lambda x: self._repo.file(x))
251 def getctx(path, fileid):
257 def getctx(path, fileid):
252 log = path == self._path and self._filelog or getlog(path)
258 log = path == self._path and self._filelog or getlog(path)
253 return filectx(self._repo, path, fileid=fileid, filelog=log)
259 return filectx(self._repo, path, fileid=fileid, filelog=log)
254 getctx = util.cachefunc(getctx)
260 getctx = util.cachefunc(getctx)
255
261
256 def parents(f):
262 def parents(f):
257 # we want to reuse filectx objects as much as possible
263 # we want to reuse filectx objects as much as possible
258 p = f._path
264 p = f._path
259 if f._filerev is None: # working dir
265 if f._filerev is None: # working dir
260 pl = [(n.path(), n.filerev()) for n in f.parents()]
266 pl = [(n.path(), n.filerev()) for n in f.parents()]
261 else:
267 else:
262 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
268 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
263
269
264 if follow:
270 if follow:
265 r = f.renamed()
271 r = f.renamed()
266 if r:
272 if r:
267 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
273 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
268
274
269 return [getctx(p, n) for p, n in pl if n != nullrev]
275 return [getctx(p, n) for p, n in pl if n != nullrev]
270
276
271 # use linkrev to find the first changeset where self appeared
277 # use linkrev to find the first changeset where self appeared
272 if self.rev() != self._filelog.linkrev(self._filenode):
278 if self.rev() != self._filelog.linkrev(self._filenode):
273 base = self.filectx(self.filerev())
279 base = self.filectx(self.filerev())
274 else:
280 else:
275 base = self
281 base = self
276
282
277 # find all ancestors
283 # find all ancestors
278 needed = {base: 1}
284 needed = {base: 1}
279 visit = [base]
285 visit = [base]
280 files = [base._path]
286 files = [base._path]
281 while visit:
287 while visit:
282 f = visit.pop(0)
288 f = visit.pop(0)
283 for p in parents(f):
289 for p in parents(f):
284 if p not in needed:
290 if p not in needed:
285 needed[p] = 1
291 needed[p] = 1
286 visit.append(p)
292 visit.append(p)
287 if p._path not in files:
293 if p._path not in files:
288 files.append(p._path)
294 files.append(p._path)
289 else:
295 else:
290 # count how many times we'll use this
296 # count how many times we'll use this
291 needed[p] += 1
297 needed[p] += 1
292
298
293 # sort by revision (per file) which is a topological order
299 # sort by revision (per file) which is a topological order
294 visit = []
300 visit = []
295 for f in files:
301 for f in files:
296 fn = [(n.rev(), n) for n in needed.keys() if n._path == f]
302 fn = [(n.rev(), n) for n in needed.keys() if n._path == f]
297 visit.extend(fn)
303 visit.extend(fn)
298 visit.sort()
304 visit.sort()
299 hist = {}
305 hist = {}
300
306
301 for r, f in visit:
307 for r, f in visit:
302 curr = decorate(f.data(), f)
308 curr = decorate(f.data(), f)
303 for p in parents(f):
309 for p in parents(f):
304 if p != nullid:
310 if p != nullid:
305 curr = pair(hist[p], curr)
311 curr = pair(hist[p], curr)
306 # trim the history of unneeded revs
312 # trim the history of unneeded revs
307 needed[p] -= 1
313 needed[p] -= 1
308 if not needed[p]:
314 if not needed[p]:
309 del hist[p]
315 del hist[p]
310 hist[f] = curr
316 hist[f] = curr
311
317
312 return zip(hist[f][0], hist[f][1].splitlines(1))
318 return zip(hist[f][0], hist[f][1].splitlines(1))
313
319
314 def ancestor(self, fc2):
320 def ancestor(self, fc2):
315 """
321 """
316 find the common ancestor file context, if any, of self, and fc2
322 find the common ancestor file context, if any, of self, and fc2
317 """
323 """
318
324
319 acache = {}
325 acache = {}
320
326
321 # prime the ancestor cache for the working directory
327 # prime the ancestor cache for the working directory
322 for c in (self, fc2):
328 for c in (self, fc2):
323 if c._filerev == None:
329 if c._filerev == None:
324 pl = [(n.path(), n.filenode()) for n in c.parents()]
330 pl = [(n.path(), n.filenode()) for n in c.parents()]
325 acache[(c._path, None)] = pl
331 acache[(c._path, None)] = pl
326
332
327 flcache = {self._path:self._filelog, fc2._path:fc2._filelog}
333 flcache = {self._path:self._filelog, fc2._path:fc2._filelog}
328 def parents(vertex):
334 def parents(vertex):
329 if vertex in acache:
335 if vertex in acache:
330 return acache[vertex]
336 return acache[vertex]
331 f, n = vertex
337 f, n = vertex
332 if f not in flcache:
338 if f not in flcache:
333 flcache[f] = self._repo.file(f)
339 flcache[f] = self._repo.file(f)
334 fl = flcache[f]
340 fl = flcache[f]
335 pl = [(f, p) for p in fl.parents(n) if p != nullid]
341 pl = [(f, p) for p in fl.parents(n) if p != nullid]
336 re = fl.renamed(n)
342 re = fl.renamed(n)
337 if re:
343 if re:
338 pl.append(re)
344 pl.append(re)
339 acache[vertex] = pl
345 acache[vertex] = pl
340 return pl
346 return pl
341
347
342 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
348 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
343 v = ancestor.ancestor(a, b, parents)
349 v = ancestor.ancestor(a, b, parents)
344 if v:
350 if v:
345 f, n = v
351 f, n = v
346 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
352 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
347
353
348 return None
354 return None
349
355
350 class workingctx(changectx):
356 class workingctx(changectx):
351 """A workingctx object makes access to data related to
357 """A workingctx object makes access to data related to
352 the current working directory convenient."""
358 the current working directory convenient."""
353 def __init__(self, repo):
359 def __init__(self, repo):
354 self._repo = repo
360 self._repo = repo
355 self._rev = None
361 self._rev = None
356 self._node = None
362 self._node = None
357
363
358 def __str__(self):
364 def __str__(self):
359 return str(self._parents[0]) + "+"
365 return str(self._parents[0]) + "+"
360
366
361 def __nonzero__(self):
367 def __nonzero__(self):
362 return True
368 return True
363
369
364 def __getattr__(self, name):
370 def __getattr__(self, name):
365 if name == '_parents':
371 if name == '_parents':
366 self._parents = self._repo.parents()
372 self._parents = self._repo.parents()
367 return self._parents
373 return self._parents
368 if name == '_status':
374 if name == '_status':
369 self._status = self._repo.status()
375 self._status = self._repo.status()
370 return self._status
376 return self._status
371 if name == '_manifest':
377 if name == '_manifest':
372 self._buildmanifest()
378 self._buildmanifest()
373 return self._manifest
379 return self._manifest
374 else:
380 else:
375 raise AttributeError, name
381 raise AttributeError, name
376
382
377 def _buildmanifest(self):
383 def _buildmanifest(self):
378 """generate a manifest corresponding to the working directory"""
384 """generate a manifest corresponding to the working directory"""
379
385
380 man = self._parents[0].manifest().copy()
386 man = self._parents[0].manifest().copy()
381 is_exec = util.execfunc(self._repo.root, man.execf)
387 is_exec = util.execfunc(self._repo.root, man.execf)
382 is_link = util.linkfunc(self._repo.root, man.linkf)
388 is_link = util.linkfunc(self._repo.root, man.linkf)
383 copied = self._repo.dirstate.copies()
389 copied = self._repo.dirstate.copies()
384 modified, added, removed, deleted, unknown = self._status[:5]
390 modified, added, removed, deleted, unknown = self._status[:5]
385 for i, l in (("a", added), ("m", modified), ("u", unknown)):
391 for i, l in (("a", added), ("m", modified), ("u", unknown)):
386 for f in l:
392 for f in l:
387 man[f] = man.get(copied.get(f, f), nullid) + i
393 man[f] = man.get(copied.get(f, f), nullid) + i
388 try:
394 try:
389 man.set(f, is_exec(f), is_link(f))
395 man.set(f, is_exec(f), is_link(f))
390 except OSError:
396 except OSError:
391 pass
397 pass
392
398
393 for f in deleted + removed:
399 for f in deleted + removed:
394 if f in man:
400 if f in man:
395 del man[f]
401 del man[f]
396
402
397 self._manifest = man
403 self._manifest = man
398
404
399 def manifest(self): return self._manifest
405 def manifest(self): return self._manifest
400
406
401 def user(self): return self._repo.ui.username()
407 def user(self): return self._repo.ui.username()
402 def date(self): return util.makedate()
408 def date(self): return util.makedate()
403 def description(self): return ""
409 def description(self): return ""
404 def files(self):
410 def files(self):
405 f = self.modified() + self.added() + self.removed()
411 f = self.modified() + self.added() + self.removed()
406 f.sort()
412 f.sort()
407 return f
413 return f
408
414
409 def modified(self): return self._status[0]
415 def modified(self): return self._status[0]
410 def added(self): return self._status[1]
416 def added(self): return self._status[1]
411 def removed(self): return self._status[2]
417 def removed(self): return self._status[2]
412 def deleted(self): return self._status[3]
418 def deleted(self): return self._status[3]
413 def unknown(self): return self._status[4]
419 def unknown(self): return self._status[4]
414 def clean(self): return self._status[5]
420 def clean(self): return self._status[5]
415 def branch(self): return self._repo.dirstate.branch()
421 def branch(self): return self._repo.dirstate.branch()
416
422
417 def tags(self):
423 def tags(self):
418 t = []
424 t = []
419 [t.extend(p.tags()) for p in self.parents()]
425 [t.extend(p.tags()) for p in self.parents()]
420 return t
426 return t
421
427
422 def parents(self):
428 def parents(self):
423 """return contexts for each parent changeset"""
429 """return contexts for each parent changeset"""
424 return self._parents
430 return self._parents
425
431
426 def children(self):
432 def children(self):
427 return []
433 return []
428
434
429 def filectx(self, path, filelog=None):
435 def filectx(self, path, filelog=None):
430 """get a file context from the working directory"""
436 """get a file context from the working directory"""
431 return workingfilectx(self._repo, path, workingctx=self,
437 return workingfilectx(self._repo, path, workingctx=self,
432 filelog=filelog)
438 filelog=filelog)
433
439
434 def ancestor(self, c2):
440 def ancestor(self, c2):
435 """return the ancestor context of self and c2"""
441 """return the ancestor context of self and c2"""
436 return self._parents[0].ancestor(c2) # punt on two parents for now
442 return self._parents[0].ancestor(c2) # punt on two parents for now
437
443
438 class workingfilectx(filectx):
444 class workingfilectx(filectx):
439 """A workingfilectx object makes access to data related to a particular
445 """A workingfilectx object makes access to data related to a particular
440 file in the working directory convenient."""
446 file in the working directory convenient."""
441 def __init__(self, repo, path, filelog=None, workingctx=None):
447 def __init__(self, repo, path, filelog=None, workingctx=None):
442 """changeid can be a changeset revision, node, or tag.
448 """changeid can be a changeset revision, node, or tag.
443 fileid can be a file revision or node."""
449 fileid can be a file revision or node."""
444 self._repo = repo
450 self._repo = repo
445 self._path = path
451 self._path = path
446 self._changeid = None
452 self._changeid = None
447 self._filerev = self._filenode = None
453 self._filerev = self._filenode = None
448
454
449 if filelog:
455 if filelog:
450 self._filelog = filelog
456 self._filelog = filelog
451 if workingctx:
457 if workingctx:
452 self._changectx = workingctx
458 self._changectx = workingctx
453
459
454 def __getattr__(self, name):
460 def __getattr__(self, name):
455 if name == '_changectx':
461 if name == '_changectx':
456 self._changectx = workingctx(self._repo)
462 self._changectx = workingctx(self._repo)
457 return self._changectx
463 return self._changectx
458 elif name == '_repopath':
464 elif name == '_repopath':
459 self._repopath = (self._repo.dirstate.copied(self._path)
465 self._repopath = (self._repo.dirstate.copied(self._path)
460 or self._path)
466 or self._path)
461 return self._repopath
467 return self._repopath
462 elif name == '_filelog':
468 elif name == '_filelog':
463 self._filelog = self._repo.file(self._repopath)
469 self._filelog = self._repo.file(self._repopath)
464 return self._filelog
470 return self._filelog
465 else:
471 else:
466 raise AttributeError, name
472 raise AttributeError, name
467
473
468 def __nonzero__(self):
474 def __nonzero__(self):
469 return True
475 return True
470
476
471 def __str__(self):
477 def __str__(self):
472 return "%s@%s" % (self.path(), self._changectx)
478 return "%s@%s" % (self.path(), self._changectx)
473
479
474 def filectx(self, fileid):
480 def filectx(self, fileid):
475 '''opens an arbitrary revision of the file without
481 '''opens an arbitrary revision of the file without
476 opening a new filelog'''
482 opening a new filelog'''
477 return filectx(self._repo, self._repopath, fileid=fileid,
483 return filectx(self._repo, self._repopath, fileid=fileid,
478 filelog=self._filelog)
484 filelog=self._filelog)
479
485
480 def rev(self):
486 def rev(self):
481 if '_changectx' in self.__dict__:
487 if '_changectx' in self.__dict__:
482 return self._changectx.rev()
488 return self._changectx.rev()
483 return self._filelog.linkrev(self._filenode)
489 return self._filelog.linkrev(self._filenode)
484
490
485 def data(self): return self._repo.wread(self._path)
491 def data(self): return self._repo.wread(self._path)
486 def renamed(self):
492 def renamed(self):
487 rp = self._repopath
493 rp = self._repopath
488 if rp == self._path:
494 if rp == self._path:
489 return None
495 return None
490 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
496 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
491
497
492 def parents(self):
498 def parents(self):
493 '''return parent filectxs, following copies if necessary'''
499 '''return parent filectxs, following copies if necessary'''
494 p = self._path
500 p = self._path
495 rp = self._repopath
501 rp = self._repopath
496 pcl = self._changectx._parents
502 pcl = self._changectx._parents
497 fl = self._filelog
503 fl = self._filelog
498 pl = [(rp, pcl[0]._manifest.get(rp, nullid), fl)]
504 pl = [(rp, pcl[0]._manifest.get(rp, nullid), fl)]
499 if len(pcl) > 1:
505 if len(pcl) > 1:
500 if rp != p:
506 if rp != p:
501 fl = None
507 fl = None
502 pl.append((p, pcl[1]._manifest.get(p, nullid), fl))
508 pl.append((p, pcl[1]._manifest.get(p, nullid), fl))
503
509
504 return [filectx(self._repo, p, fileid=n, filelog=l)
510 return [filectx(self._repo, p, fileid=n, filelog=l)
505 for p,n,l in pl if n != nullid]
511 for p,n,l in pl if n != nullid]
506
512
507 def children(self):
513 def children(self):
508 return []
514 return []
509
515
510 def size(self): return os.stat(self._repo.wjoin(self._path)).st_size
516 def size(self): return os.stat(self._repo.wjoin(self._path)).st_size
511 def date(self):
517 def date(self):
512 t, tz = self._changectx.date()
518 t, tz = self._changectx.date()
513 try:
519 try:
514 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
520 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
515 except OSError, err:
521 except OSError, err:
516 if err.errno != errno.ENOENT: raise
522 if err.errno != errno.ENOENT: raise
517 return (t, tz)
523 return (t, tz)
518
524
519 def cmp(self, text): return self._repo.wread(self._path) == text
525 def cmp(self, text): return self._repo.wread(self._path) == text
@@ -1,564 +1,564
1 # merge.py - directory-level update/merge handling for Mercurial
1 # merge.py - directory-level update/merge handling for Mercurial
2 #
2 #
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from node import *
8 from node import *
9 from i18n import _
9 from i18n import _
10 import errno, util, os, tempfile, context
10 import errno, util, os, tempfile, context
11
11
12 def filemerge(repo, fw, fo, wctx, mctx):
12 def filemerge(repo, fw, fo, wctx, mctx):
13 """perform a 3-way merge in the working directory
13 """perform a 3-way merge in the working directory
14
14
15 fw = filename in the working directory
15 fw = filename in the working directory
16 fo = filename in other parent
16 fo = filename in other parent
17 wctx, mctx = working and merge changecontexts
17 wctx, mctx = working and merge changecontexts
18 """
18 """
19
19
20 def temp(prefix, ctx):
20 def temp(prefix, ctx):
21 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
21 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
22 (fd, name) = tempfile.mkstemp(prefix=pre)
22 (fd, name) = tempfile.mkstemp(prefix=pre)
23 data = repo.wwritedata(ctx.path(), ctx.data())
23 data = repo.wwritedata(ctx.path(), ctx.data())
24 f = os.fdopen(fd, "wb")
24 f = os.fdopen(fd, "wb")
25 f.write(data)
25 f.write(data)
26 f.close()
26 f.close()
27 return name
27 return name
28
28
29 fcm = wctx.filectx(fw)
29 fcm = wctx.filectx(fw)
30 fco = mctx.filectx(fo)
30 fco = mctx.filectx(fo)
31
31
32 if not fco.cmp(fcm.data()): # files identical?
32 if not fco.cmp(fcm.data()): # files identical?
33 return None
33 return None
34
34
35 fca = fcm.ancestor(fco)
35 fca = fcm.ancestor(fco)
36 if not fca:
36 if not fca:
37 fca = repo.filectx(fw, fileid=nullrev)
37 fca = repo.filectx(fw, fileid=nullrev)
38 a = repo.wjoin(fw)
38 a = repo.wjoin(fw)
39 b = temp("base", fca)
39 b = temp("base", fca)
40 c = temp("other", fco)
40 c = temp("other", fco)
41
41
42 if fw != fo:
42 if fw != fo:
43 repo.ui.status(_("merging %s and %s\n") % (fw, fo))
43 repo.ui.status(_("merging %s and %s\n") % (fw, fo))
44 else:
44 else:
45 repo.ui.status(_("merging %s\n") % fw)
45 repo.ui.status(_("merging %s\n") % fw)
46
46
47 repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcm, fco, fca))
47 repo.ui.debug(_("my %s other %s ancestor %s\n") % (fcm, fco, fca))
48
48
49 cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
49 cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
50 or "hgmerge")
50 or "hgmerge")
51 r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
51 r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
52 environ={'HG_FILE': fw,
52 environ={'HG_FILE': fw,
53 'HG_MY_NODE': str(wctx.parents()[0]),
53 'HG_MY_NODE': str(wctx.parents()[0]),
54 'HG_OTHER_NODE': str(mctx)})
54 'HG_OTHER_NODE': str(mctx)})
55 if r:
55 if r:
56 repo.ui.warn(_("merging %s failed!\n") % fw)
56 repo.ui.warn(_("merging %s failed!\n") % fw)
57
57
58 os.unlink(b)
58 os.unlink(b)
59 os.unlink(c)
59 os.unlink(c)
60 return r
60 return r
61
61
62 def checkunknown(wctx, mctx):
62 def checkunknown(wctx, mctx):
63 "check for collisions between unknown files and files in mctx"
63 "check for collisions between unknown files and files in mctx"
64 man = mctx.manifest()
64 man = mctx.manifest()
65 for f in wctx.unknown():
65 for f in wctx.unknown():
66 if f in man:
66 if f in man:
67 if mctx.filectx(f).cmp(wctx.filectx(f).data()):
67 if mctx.filectx(f).cmp(wctx.filectx(f).data()):
68 raise util.Abort(_("untracked local file '%s' differs"
68 raise util.Abort(_("untracked local file '%s' differs"
69 " from remote version") % f)
69 " from remote version") % f)
70
70
71 def checkcollision(mctx):
71 def checkcollision(mctx):
72 "check for case folding collisions in the destination context"
72 "check for case folding collisions in the destination context"
73 folded = {}
73 folded = {}
74 for fn in mctx.manifest():
74 for fn in mctx.manifest():
75 fold = fn.lower()
75 fold = fn.lower()
76 if fold in folded:
76 if fold in folded:
77 raise util.Abort(_("case-folding collision between %s and %s")
77 raise util.Abort(_("case-folding collision between %s and %s")
78 % (fn, folded[fold]))
78 % (fn, folded[fold]))
79 folded[fold] = fn
79 folded[fold] = fn
80
80
81 def forgetremoved(wctx, mctx):
81 def forgetremoved(wctx, mctx):
82 """
82 """
83 Forget removed files
83 Forget removed files
84
84
85 If we're jumping between revisions (as opposed to merging), and if
85 If we're jumping between revisions (as opposed to merging), and if
86 neither the working directory nor the target rev has the file,
86 neither the working directory nor the target rev has the file,
87 then we need to remove it from the dirstate, to prevent the
87 then we need to remove it from the dirstate, to prevent the
88 dirstate from listing the file when it is no longer in the
88 dirstate from listing the file when it is no longer in the
89 manifest.
89 manifest.
90 """
90 """
91
91
92 action = []
92 action = []
93 man = mctx.manifest()
93 man = mctx.manifest()
94 for f in wctx.deleted() + wctx.removed():
94 for f in wctx.deleted() + wctx.removed():
95 if f not in man:
95 if f not in man:
96 action.append((f, "f"))
96 action.append((f, "f"))
97
97
98 return action
98 return action
99
99
100 def findcopies(repo, m1, m2, ma, limit):
100 def findcopies(repo, m1, m2, ma, limit):
101 """
101 """
102 Find moves and copies between m1 and m2 back to limit linkrev
102 Find moves and copies between m1 and m2 back to limit linkrev
103 """
103 """
104
104
105 def nonoverlap(d1, d2, d3):
105 def nonoverlap(d1, d2, d3):
106 "Return list of elements in d1 not in d2 or d3"
106 "Return list of elements in d1 not in d2 or d3"
107 l = [d for d in d1 if d not in d3 and d not in d2]
107 l = [d for d in d1 if d not in d3 and d not in d2]
108 l.sort()
108 l.sort()
109 return l
109 return l
110
110
111 def dirname(f):
111 def dirname(f):
112 s = f.rfind("/")
112 s = f.rfind("/")
113 if s == -1:
113 if s == -1:
114 return ""
114 return ""
115 return f[:s]
115 return f[:s]
116
116
117 def dirs(files):
117 def dirs(files):
118 d = {}
118 d = {}
119 for f in files:
119 for f in files:
120 f = dirname(f)
120 f = dirname(f)
121 while f not in d:
121 while f not in d:
122 d[f] = True
122 d[f] = True
123 f = dirname(f)
123 f = dirname(f)
124 return d
124 return d
125
125
126 wctx = repo.workingctx()
126 wctx = repo.workingctx()
127
127
128 def makectx(f, n):
128 def makectx(f, n):
129 if len(n) == 20:
129 if len(n) == 20:
130 return repo.filectx(f, fileid=n)
130 return repo.filectx(f, fileid=n)
131 return wctx.filectx(f)
131 return wctx.filectx(f)
132 ctx = util.cachefunc(makectx)
132 ctx = util.cachefunc(makectx)
133
133
134 def findold(fctx):
134 def findold(fctx):
135 "find files that path was copied from, back to linkrev limit"
135 "find files that path was copied from, back to linkrev limit"
136 old = {}
136 old = {}
137 seen = {}
137 seen = {}
138 orig = fctx.path()
138 orig = fctx.path()
139 visit = [fctx]
139 visit = [fctx]
140 while visit:
140 while visit:
141 fc = visit.pop()
141 fc = visit.pop()
142 s = str(fc)
142 s = str(fc)
143 if s in seen:
143 if s in seen:
144 continue
144 continue
145 seen[s] = 1
145 seen[s] = 1
146 if fc.path() != orig and fc.path() not in old:
146 if fc.path() != orig and fc.path() not in old:
147 old[fc.path()] = 1
147 old[fc.path()] = 1
148 if fc.rev() < limit:
148 if fc.rev() < limit:
149 continue
149 continue
150 visit += fc.parents()
150 visit += fc.parents()
151
151
152 old = old.keys()
152 old = old.keys()
153 old.sort()
153 old.sort()
154 return old
154 return old
155
155
156 copy = {}
156 copy = {}
157 fullcopy = {}
157 fullcopy = {}
158 diverge = {}
158 diverge = {}
159
159
160 def checkcopies(c, man):
160 def checkcopies(c, man):
161 '''check possible copies for filectx c'''
161 '''check possible copies for filectx c'''
162 for of in findold(c):
162 for of in findold(c):
163 fullcopy[c.path()] = of # remember for dir rename detection
163 fullcopy[c.path()] = of # remember for dir rename detection
164 if of not in man: # original file not in other manifest?
164 if of not in man: # original file not in other manifest?
165 if of in ma:
165 if of in ma:
166 diverge.setdefault(of, []).append(c.path())
166 diverge.setdefault(of, []).append(c.path())
167 continue
167 continue
168 c2 = ctx(of, man[of])
168 c2 = ctx(of, man[of])
169 ca = c.ancestor(c2)
169 ca = c.ancestor(c2)
170 if not ca: # unrelated?
170 if not ca: # unrelated?
171 continue
171 continue
172 # named changed on only one side?
172 # named changed on only one side?
173 if ca.path() == c.path() or ca.path() == c2.path():
173 if ca.path() == c.path() or ca.path() == c2.path():
174 if c == ca or c2 == ca: # no merge needed, ignore copy
174 if c == ca or c2 == ca: # no merge needed, ignore copy
175 continue
175 continue
176 copy[c.path()] = of
176 copy[c.path()] = of
177
177
178 if not repo.ui.configbool("merge", "followcopies", True):
178 if not repo.ui.configbool("merge", "followcopies", True):
179 return {}, {}
179 return {}, {}
180
180
181 # avoid silly behavior for update from empty dir
181 # avoid silly behavior for update from empty dir
182 if not m1 or not m2 or not ma:
182 if not m1 or not m2 or not ma:
183 return {}, {}
183 return {}, {}
184
184
185 u1 = nonoverlap(m1, m2, ma)
185 u1 = nonoverlap(m1, m2, ma)
186 u2 = nonoverlap(m2, m1, ma)
186 u2 = nonoverlap(m2, m1, ma)
187
187
188 for f in u1:
188 for f in u1:
189 checkcopies(ctx(f, m1[f]), m2)
189 checkcopies(ctx(f, m1[f]), m2)
190
190
191 for f in u2:
191 for f in u2:
192 checkcopies(ctx(f, m2[f]), m1)
192 checkcopies(ctx(f, m2[f]), m1)
193
193
194 d2 = {}
194 d2 = {}
195 for of, fl in diverge.items():
195 for of, fl in diverge.items():
196 for f in fl:
196 for f in fl:
197 fo = list(fl)
197 fo = list(fl)
198 fo.remove(f)
198 fo.remove(f)
199 d2[f] = (of, fo)
199 d2[f] = (of, fo)
200 #diverge = d2
200 #diverge = d2
201
201
202 if not fullcopy or not repo.ui.configbool("merge", "followdirs", True):
202 if not fullcopy or not repo.ui.configbool("merge", "followdirs", True):
203 return copy, diverge
203 return copy, diverge
204
204
205 # generate a directory move map
205 # generate a directory move map
206 d1, d2 = dirs(m1), dirs(m2)
206 d1, d2 = dirs(m1), dirs(m2)
207 invalid = {}
207 invalid = {}
208 dirmove = {}
208 dirmove = {}
209
209
210 # examine each file copy for a potential directory move, which is
210 # examine each file copy for a potential directory move, which is
211 # when all the files in a directory are moved to a new directory
211 # when all the files in a directory are moved to a new directory
212 for dst, src in fullcopy.items():
212 for dst, src in fullcopy.items():
213 dsrc, ddst = dirname(src), dirname(dst)
213 dsrc, ddst = dirname(src), dirname(dst)
214 if dsrc in invalid:
214 if dsrc in invalid:
215 # already seen to be uninteresting
215 # already seen to be uninteresting
216 continue
216 continue
217 elif dsrc in d1 and ddst in d1:
217 elif dsrc in d1 and ddst in d1:
218 # directory wasn't entirely moved locally
218 # directory wasn't entirely moved locally
219 invalid[dsrc] = True
219 invalid[dsrc] = True
220 elif dsrc in d2 and ddst in d2:
220 elif dsrc in d2 and ddst in d2:
221 # directory wasn't entirely moved remotely
221 # directory wasn't entirely moved remotely
222 invalid[dsrc] = True
222 invalid[dsrc] = True
223 elif dsrc in dirmove and dirmove[dsrc] != ddst:
223 elif dsrc in dirmove and dirmove[dsrc] != ddst:
224 # files from the same directory moved to two different places
224 # files from the same directory moved to two different places
225 invalid[dsrc] = True
225 invalid[dsrc] = True
226 else:
226 else:
227 # looks good so far
227 # looks good so far
228 dirmove[dsrc + "/"] = ddst + "/"
228 dirmove[dsrc + "/"] = ddst + "/"
229
229
230 for i in invalid:
230 for i in invalid:
231 if i in dirmove:
231 if i in dirmove:
232 del dirmove[i]
232 del dirmove[i]
233
233
234 del d1, d2, invalid
234 del d1, d2, invalid
235
235
236 if not dirmove:
236 if not dirmove:
237 return copy, diverge
237 return copy, diverge
238
238
239 # check unaccounted nonoverlapping files against directory moves
239 # check unaccounted nonoverlapping files against directory moves
240 for f in u1 + u2:
240 for f in u1 + u2:
241 if f not in fullcopy:
241 if f not in fullcopy:
242 for d in dirmove:
242 for d in dirmove:
243 if f.startswith(d):
243 if f.startswith(d):
244 # new file added in a directory that was moved, move it
244 # new file added in a directory that was moved, move it
245 copy[f] = dirmove[d] + f[len(d):]
245 copy[f] = dirmove[d] + f[len(d):]
246 break
246 break
247
247
248 return copy, diverge
248 return copy, diverge
249
249
250 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
250 def manifestmerge(repo, p1, p2, pa, overwrite, partial):
251 """
251 """
252 Merge p1 and p2 with ancestor ma and generate merge action list
252 Merge p1 and p2 with ancestor ma and generate merge action list
253
253
254 overwrite = whether we clobber working files
254 overwrite = whether we clobber working files
255 partial = function to filter file lists
255 partial = function to filter file lists
256 """
256 """
257
257
258 repo.ui.note(_("resolving manifests\n"))
258 repo.ui.note(_("resolving manifests\n"))
259 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
259 repo.ui.debug(_(" overwrite %s partial %s\n") % (overwrite, bool(partial)))
260 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
260 repo.ui.debug(_(" ancestor %s local %s remote %s\n") % (pa, p1, p2))
261
261
262 m1 = p1.manifest()
262 m1 = p1.manifest()
263 m2 = p2.manifest()
263 m2 = p2.manifest()
264 ma = pa.manifest()
264 ma = pa.manifest()
265 backwards = (pa == p2)
265 backwards = (pa == p2)
266 action = []
266 action = []
267 copy = {}
267 copy = {}
268 diverge = {}
268 diverge = {}
269
269
270 def fmerge(f, f2=None, fa=None):
270 def fmerge(f, f2=None, fa=None):
271 """merge flags"""
271 """merge flags"""
272 if not f2:
272 if not f2:
273 f2 = f
273 f2 = f
274 fa = f
274 fa = f
275 a, b, c = ma.execf(fa), m1.execf(f), m2.execf(f2)
275 a, b, c = ma.execf(fa), m1.execf(f), m2.execf(f2)
276 if ((a^b) | (a^c)) ^ a:
276 if ((a^b) | (a^c)) ^ a:
277 return 'x'
277 return 'x'
278 a, b, c = ma.linkf(fa), m1.linkf(f), m2.linkf(f2)
278 a, b, c = ma.linkf(fa), m1.linkf(f), m2.linkf(f2)
279 if ((a^b) | (a^c)) ^ a:
279 if ((a^b) | (a^c)) ^ a:
280 return 'l'
280 return 'l'
281 return ''
281 return ''
282
282
283 def act(msg, m, f, *args):
283 def act(msg, m, f, *args):
284 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
284 repo.ui.debug(" %s: %s -> %s\n" % (f, msg, m))
285 action.append((f, m) + args)
285 action.append((f, m) + args)
286
286
287 if not (backwards or overwrite):
287 if not (backwards or overwrite):
288 copy, diverge = findcopies(repo, m1, m2, ma, pa.rev())
288 copy, diverge = findcopies(repo, m1, m2, ma, pa.rev())
289
289
290 for of, fl in diverge.items():
290 for of, fl in diverge.items():
291 act("divergent renames", "dr", of, fl)
291 act("divergent renames", "dr", of, fl)
292
292
293 copied = dict.fromkeys(copy.values())
293 copied = dict.fromkeys(copy.values())
294
294
295 # Compare manifests
295 # Compare manifests
296 for f, n in m1.iteritems():
296 for f, n in m1.iteritems():
297 if partial and not partial(f):
297 if partial and not partial(f):
298 continue
298 continue
299 if f in m2:
299 if f in m2:
300 # are files different?
300 # are files different?
301 if n != m2[f]:
301 if n != m2[f]:
302 a = ma.get(f, nullid)
302 a = ma.get(f, nullid)
303 # are both different from the ancestor?
303 # are both different from the ancestor?
304 if not overwrite and n != a and m2[f] != a:
304 if not overwrite and n != a and m2[f] != a:
305 act("versions differ", "m", f, f, f, fmerge(f), False)
305 act("versions differ", "m", f, f, f, fmerge(f), False)
306 # are we clobbering?
306 # are we clobbering?
307 # is remote's version newer?
307 # is remote's version newer?
308 # or are we going back in time and clean?
308 # or are we going back in time and clean?
309 elif overwrite or m2[f] != a or (backwards and not n[20:]):
309 elif overwrite or m2[f] != a or (backwards and not n[20:]):
310 act("remote is newer", "g", f, m2.flags(f))
310 act("remote is newer", "g", f, m2.flags(f))
311 # local is newer, not overwrite, check mode bits
311 # local is newer, not overwrite, check mode bits
312 elif fmerge(f) != m1.flags(f):
312 elif fmerge(f) != m1.flags(f):
313 act("update permissions", "e", f, m2.flags(f))
313 act("update permissions", "e", f, m2.flags(f))
314 # contents same, check mode bits
314 # contents same, check mode bits
315 elif m1.flags(f) != m2.flags(f):
315 elif m1.flags(f) != m2.flags(f):
316 if overwrite or fmerge(f) != m1.flags(f):
316 if overwrite or fmerge(f) != m1.flags(f):
317 act("update permissions", "e", f, m2.flags(f))
317 act("update permissions", "e", f, m2.flags(f))
318 elif f in copied:
318 elif f in copied:
319 continue
319 continue
320 elif f in copy:
320 elif f in copy:
321 f2 = copy[f]
321 f2 = copy[f]
322 if f2 not in m2: # directory rename
322 if f2 not in m2: # directory rename
323 act("remote renamed directory to " + f2, "d",
323 act("remote renamed directory to " + f2, "d",
324 f, None, f2, m1.flags(f))
324 f, None, f2, m1.flags(f))
325 elif f2 in m1: # case 2 A,B/B/B
325 elif f2 in m1: # case 2 A,B/B/B
326 act("local copied to " + f2, "m",
326 act("local copied to " + f2, "m",
327 f, f2, f, fmerge(f, f2, f2), False)
327 f, f2, f, fmerge(f, f2, f2), False)
328 else: # case 4,21 A/B/B
328 else: # case 4,21 A/B/B
329 act("local moved to " + f2, "m",
329 act("local moved to " + f2, "m",
330 f, f2, f, fmerge(f, f2, f2), False)
330 f, f2, f, fmerge(f, f2, f2), False)
331 elif f in ma:
331 elif f in ma:
332 if n != ma[f] and not overwrite:
332 if n != ma[f] and not overwrite:
333 if repo.ui.prompt(
333 if repo.ui.prompt(
334 (_(" local changed %s which remote deleted\n") % f) +
334 (_(" local changed %s which remote deleted\n") % f) +
335 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("d"):
335 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("d"):
336 act("prompt delete", "r", f)
336 act("prompt delete", "r", f)
337 else:
337 else:
338 act("other deleted", "r", f)
338 act("other deleted", "r", f)
339 else:
339 else:
340 # file is created on branch or in working directory
340 # file is created on branch or in working directory
341 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
341 if (overwrite and n[20:] != "u") or (backwards and not n[20:]):
342 act("remote deleted", "r", f)
342 act("remote deleted", "r", f)
343
343
344 for f, n in m2.iteritems():
344 for f, n in m2.iteritems():
345 if partial and not partial(f):
345 if partial and not partial(f):
346 continue
346 continue
347 if f in m1:
347 if f in m1:
348 continue
348 continue
349 if f in copied:
349 if f in copied:
350 continue
350 continue
351 if f in copy:
351 if f in copy:
352 f2 = copy[f]
352 f2 = copy[f]
353 if f2 not in m1: # directory rename
353 if f2 not in m1: # directory rename
354 act("local renamed directory to " + f2, "d",
354 act("local renamed directory to " + f2, "d",
355 None, f, f2, m2.flags(f))
355 None, f, f2, m2.flags(f))
356 elif f2 in m2: # rename case 1, A/A,B/A
356 elif f2 in m2: # rename case 1, A/A,B/A
357 act("remote copied to " + f, "m",
357 act("remote copied to " + f, "m",
358 f2, f, f, fmerge(f2, f, f2), False)
358 f2, f, f, fmerge(f2, f, f2), False)
359 else: # case 3,20 A/B/A
359 else: # case 3,20 A/B/A
360 act("remote moved to " + f, "m",
360 act("remote moved to " + f, "m",
361 f2, f, f, fmerge(f2, f, f2), True)
361 f2, f, f, fmerge(f2, f, f2), True)
362 elif f in ma:
362 elif f in ma:
363 if overwrite or backwards:
363 if overwrite or backwards:
364 act("recreating", "g", f, m2.flags(f))
364 act("recreating", "g", f, m2.flags(f))
365 elif n != ma[f]:
365 elif n != ma[f]:
366 if repo.ui.prompt(
366 if repo.ui.prompt(
367 (_("remote changed %s which local deleted\n") % f) +
367 (_("remote changed %s which local deleted\n") % f) +
368 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("k"):
368 _("(k)eep or (d)elete?"), _("[kd]"), _("k")) == _("k"):
369 act("prompt recreating", "g", f, m2.flags(f))
369 act("prompt recreating", "g", f, m2.flags(f))
370 else:
370 else:
371 act("remote created", "g", f, m2.flags(f))
371 act("remote created", "g", f, m2.flags(f))
372
372
373 return action
373 return action
374
374
375 def applyupdates(repo, action, wctx, mctx):
375 def applyupdates(repo, action, wctx, mctx):
376 "apply the merge action list to the working directory"
376 "apply the merge action list to the working directory"
377
377
378 updated, merged, removed, unresolved = 0, 0, 0, 0
378 updated, merged, removed, unresolved = 0, 0, 0, 0
379 action.sort()
379 action.sort()
380 for a in action:
380 for a in action:
381 f, m = a[:2]
381 f, m = a[:2]
382 if f and f[0] == "/":
382 if f and f[0] == "/":
383 continue
383 continue
384 if m == "r": # remove
384 if m == "r": # remove
385 repo.ui.note(_("removing %s\n") % f)
385 repo.ui.note(_("removing %s\n") % f)
386 util.audit_path(f)
386 util.audit_path(f)
387 try:
387 try:
388 util.unlink(repo.wjoin(f))
388 util.unlink(repo.wjoin(f))
389 except OSError, inst:
389 except OSError, inst:
390 if inst.errno != errno.ENOENT:
390 if inst.errno != errno.ENOENT:
391 repo.ui.warn(_("update failed to remove %s: %s!\n") %
391 repo.ui.warn(_("update failed to remove %s: %s!\n") %
392 (f, inst.strerror))
392 (f, inst.strerror))
393 removed += 1
393 removed += 1
394 elif m == "m": # merge
394 elif m == "m": # merge
395 f2, fd, flags, move = a[2:]
395 f2, fd, flags, move = a[2:]
396 r = filemerge(repo, f, f2, wctx, mctx)
396 r = filemerge(repo, f, f2, wctx, mctx)
397 if r > 0:
397 if r > 0:
398 unresolved += 1
398 unresolved += 1
399 else:
399 else:
400 if r is None:
400 if r is None:
401 updated += 1
401 updated += 1
402 else:
402 else:
403 merged += 1
403 merged += 1
404 if f != fd:
404 if f != fd:
405 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
405 repo.ui.debug(_("copying %s to %s\n") % (f, fd))
406 repo.wwrite(fd, repo.wread(f), flags)
406 repo.wwrite(fd, repo.wread(f), flags)
407 if move:
407 if move:
408 repo.ui.debug(_("removing %s\n") % f)
408 repo.ui.debug(_("removing %s\n") % f)
409 os.unlink(repo.wjoin(f))
409 os.unlink(repo.wjoin(f))
410 util.set_exec(repo.wjoin(fd), "x" in flags)
410 util.set_exec(repo.wjoin(fd), "x" in flags)
411 elif m == "g": # get
411 elif m == "g": # get
412 flags = a[2]
412 flags = a[2]
413 repo.ui.note(_("getting %s\n") % f)
413 repo.ui.note(_("getting %s\n") % f)
414 t = mctx.filectx(f).data()
414 t = mctx.filectx(f).data()
415 repo.wwrite(f, t, flags)
415 repo.wwrite(f, t, flags)
416 updated += 1
416 updated += 1
417 elif m == "d": # directory rename
417 elif m == "d": # directory rename
418 f2, fd, flags = a[2:]
418 f2, fd, flags = a[2:]
419 if f:
419 if f:
420 repo.ui.note(_("moving %s to %s\n") % (f, fd))
420 repo.ui.note(_("moving %s to %s\n") % (f, fd))
421 t = wctx.filectx(f).data()
421 t = wctx.filectx(f).data()
422 repo.wwrite(fd, t, flags)
422 repo.wwrite(fd, t, flags)
423 util.unlink(repo.wjoin(f))
423 util.unlink(repo.wjoin(f))
424 if f2:
424 if f2:
425 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
425 repo.ui.note(_("getting %s to %s\n") % (f2, fd))
426 t = mctx.filectx(f2).data()
426 t = mctx.filectx(f2).data()
427 repo.wwrite(fd, t, flags)
427 repo.wwrite(fd, t, flags)
428 updated += 1
428 updated += 1
429 elif m == "dr": # divergent renames
429 elif m == "dr": # divergent renames
430 fl = a[2]
430 fl = a[2]
431 repo.ui.warn("warning: detected divergent renames of %s to:\n" % f)
431 repo.ui.warn("warning: detected divergent renames of %s to:\n" % f)
432 for nf in fl:
432 for nf in fl:
433 repo.ui.warn(" %s\n" % nf)
433 repo.ui.warn(" %s\n" % nf)
434 elif m == "e": # exec
434 elif m == "e": # exec
435 flags = a[2]
435 flags = a[2]
436 util.set_exec(repo.wjoin(f), flags)
436 util.set_exec(repo.wjoin(f), flags)
437
437
438 return updated, merged, removed, unresolved
438 return updated, merged, removed, unresolved
439
439
440 def recordupdates(repo, action, branchmerge):
440 def recordupdates(repo, action, branchmerge):
441 "record merge actions to the dirstate"
441 "record merge actions to the dirstate"
442
442
443 for a in action:
443 for a in action:
444 f, m = a[:2]
444 f, m = a[:2]
445 if m == "r": # remove
445 if m == "r": # remove
446 if branchmerge:
446 if branchmerge:
447 repo.dirstate.update([f], 'r')
447 repo.dirstate.update([f], 'r')
448 else:
448 else:
449 repo.dirstate.forget([f])
449 repo.dirstate.forget([f])
450 elif m == "f": # forget
450 elif m == "f": # forget
451 repo.dirstate.forget([f])
451 repo.dirstate.forget([f])
452 elif m == "g": # get
452 elif m == "g": # get
453 if branchmerge:
453 if branchmerge:
454 repo.dirstate.update([f], 'n', st_mtime=-1)
454 repo.dirstate.update([f], 'n', st_mtime=-1)
455 else:
455 else:
456 repo.dirstate.update([f], 'n')
456 repo.dirstate.update([f], 'n')
457 elif m == "m": # merge
457 elif m == "m": # merge
458 f2, fd, flag, move = a[2:]
458 f2, fd, flag, move = a[2:]
459 if branchmerge:
459 if branchmerge:
460 # We've done a branch merge, mark this file as merged
460 # We've done a branch merge, mark this file as merged
461 # so that we properly record the merger later
461 # so that we properly record the merger later
462 repo.dirstate.update([fd], 'm')
462 repo.dirstate.update([fd], 'm')
463 if f != f2: # copy/rename
463 if f != f2: # copy/rename
464 if move:
464 if move:
465 repo.dirstate.update([f], 'r')
465 repo.dirstate.update([f], 'r')
466 if f != fd:
466 if f != fd:
467 repo.dirstate.copy(f, fd)
467 repo.dirstate.copy(f, fd)
468 else:
468 else:
469 repo.dirstate.copy(f2, fd)
469 repo.dirstate.copy(f2, fd)
470 else:
470 else:
471 # We've update-merged a locally modified file, so
471 # We've update-merged a locally modified file, so
472 # we set the dirstate to emulate a normal checkout
472 # we set the dirstate to emulate a normal checkout
473 # of that file some time in the past. Thus our
473 # of that file some time in the past. Thus our
474 # merge will appear as a normal local file
474 # merge will appear as a normal local file
475 # modification.
475 # modification.
476 repo.dirstate.update([fd], 'n', st_size=-1, st_mtime=-1)
476 repo.dirstate.update([fd], 'n', st_size=-1, st_mtime=-1)
477 if move:
477 if move:
478 repo.dirstate.forget([f])
478 repo.dirstate.forget([f])
479 elif m == "d": # directory rename
479 elif m == "d": # directory rename
480 f2, fd, flag = a[2:]
480 f2, fd, flag = a[2:]
481 if branchmerge:
481 if branchmerge:
482 repo.dirstate.update([fd], 'a')
482 repo.dirstate.update([fd], 'a')
483 if f:
483 if f:
484 repo.dirstate.update([f], 'r')
484 repo.dirstate.update([f], 'r')
485 repo.dirstate.copy(f, fd)
485 repo.dirstate.copy(f, fd)
486 if f2:
486 if f2:
487 repo.dirstate.copy(f2, fd)
487 repo.dirstate.copy(f2, fd)
488 else:
488 else:
489 repo.dirstate.update([fd], 'n')
489 repo.dirstate.update([fd], 'n')
490 if f:
490 if f:
491 repo.dirstate.forget([f])
491 repo.dirstate.forget([f])
492
492
493 def update(repo, node, branchmerge, force, partial, wlock):
493 def update(repo, node, branchmerge, force, partial, wlock):
494 """
494 """
495 Perform a merge between the working directory and the given node
495 Perform a merge between the working directory and the given node
496
496
497 branchmerge = whether to merge between branches
497 branchmerge = whether to merge between branches
498 force = whether to force branch merging or file overwriting
498 force = whether to force branch merging or file overwriting
499 partial = a function to filter file lists (dirstate not updated)
499 partial = a function to filter file lists (dirstate not updated)
500 wlock = working dir lock, if already held
500 wlock = working dir lock, if already held
501 """
501 """
502
502
503 if not wlock:
503 if not wlock:
504 wlock = repo.wlock()
504 wlock = repo.wlock()
505
505
506 wc = repo.workingctx()
506 wc = repo.workingctx()
507 if node is None:
507 if node is None:
508 # tip of current branch
508 # tip of current branch
509 try:
509 try:
510 node = repo.branchtags()[wc.branch()]
510 node = repo.branchtags()[wc.branch()]
511 except KeyError:
511 except KeyError:
512 raise util.Abort(_("branch %s not found") % wc.branch())
512 raise util.Abort(_("branch %s not found") % wc.branch())
513 overwrite = force and not branchmerge
513 overwrite = force and not branchmerge
514 forcemerge = force and branchmerge
514 forcemerge = force and branchmerge
515 pl = wc.parents()
515 pl = wc.parents()
516 p1, p2 = pl[0], repo.changectx(node)
516 p1, p2 = pl[0], repo.changectx(node)
517 pa = p1.ancestor(p2)
517 pa = p1.ancestor(p2)
518 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
518 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), str(p1), str(p2)
519 fastforward = False
519 fastforward = False
520
520
521 ### check phase
521 ### check phase
522 if not overwrite and len(pl) > 1:
522 if not overwrite and len(pl) > 1:
523 raise util.Abort(_("outstanding uncommitted merges"))
523 raise util.Abort(_("outstanding uncommitted merges"))
524 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
524 if pa == p1 or pa == p2: # is there a linear path from p1 to p2?
525 if branchmerge:
525 if branchmerge:
526 if p1.branch() != p2.branch():
526 if p1.branch() != p2.branch() and pa != p2:
527 fastforward = True
527 fastforward = True
528 else:
528 else:
529 raise util.Abort(_("there is nothing to merge, just use "
529 raise util.Abort(_("there is nothing to merge, just use "
530 "'hg update' or look at 'hg heads'"))
530 "'hg update' or look at 'hg heads'"))
531 elif not (overwrite or branchmerge):
531 elif not (overwrite or branchmerge):
532 raise util.Abort(_("update spans branches, use 'hg merge' "
532 raise util.Abort(_("update spans branches, use 'hg merge' "
533 "or 'hg update -C' to lose changes"))
533 "or 'hg update -C' to lose changes"))
534 if branchmerge and not forcemerge:
534 if branchmerge and not forcemerge:
535 if wc.files():
535 if wc.files():
536 raise util.Abort(_("outstanding uncommitted changes"))
536 raise util.Abort(_("outstanding uncommitted changes"))
537
537
538 ### calculate phase
538 ### calculate phase
539 action = []
539 action = []
540 if not force:
540 if not force:
541 checkunknown(wc, p2)
541 checkunknown(wc, p2)
542 if not util.checkfolding(repo.path):
542 if not util.checkfolding(repo.path):
543 checkcollision(p2)
543 checkcollision(p2)
544 if not branchmerge:
544 if not branchmerge:
545 action += forgetremoved(wc, p2)
545 action += forgetremoved(wc, p2)
546 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
546 action += manifestmerge(repo, wc, p2, pa, overwrite, partial)
547
547
548 ### apply phase
548 ### apply phase
549 if not branchmerge: # just jump to the new rev
549 if not branchmerge: # just jump to the new rev
550 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
550 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, ''
551 if not partial:
551 if not partial:
552 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
552 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xp2)
553
553
554 stats = applyupdates(repo, action, wc, p2)
554 stats = applyupdates(repo, action, wc, p2)
555
555
556 if not partial:
556 if not partial:
557 recordupdates(repo, action, branchmerge)
557 recordupdates(repo, action, branchmerge)
558 repo.dirstate.setparents(fp1, fp2)
558 repo.dirstate.setparents(fp1, fp2)
559 if not branchmerge and not fastforward:
559 if not branchmerge and not fastforward:
560 repo.dirstate.setbranch(p2.branch())
560 repo.dirstate.setbranch(p2.branch())
561 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
561 repo.hook('update', parent1=xp1, parent2=xp2, error=stats[3])
562
562
563 return stats
563 return stats
564
564
General Comments 0
You need to be logged in to leave comments. Login now