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