##// END OF EJS Templates
merge: use file size stored in revlog index...
Matt Mackall -
r2898:db397c38 default
parent child Browse files
Show More
@@ -1,117 +1,127
1 1 # filelog.py - file history class for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from revlog import *
9 9 from demandload import *
10 10 demandload(globals(), "bdiff os")
11 11
12 12 class filelog(revlog):
13 13 def __init__(self, opener, path, defversion=REVLOG_DEFAULT_VERSION):
14 14 revlog.__init__(self, opener,
15 15 os.path.join("data", self.encodedir(path + ".i")),
16 16 os.path.join("data", self.encodedir(path + ".d")),
17 17 defversion)
18 18
19 19 # This avoids a collision between a file named foo and a dir named
20 20 # foo.i or foo.d
21 21 def encodedir(self, path):
22 22 return (path
23 23 .replace(".hg/", ".hg.hg/")
24 24 .replace(".i/", ".i.hg/")
25 25 .replace(".d/", ".d.hg/"))
26 26
27 27 def decodedir(self, path):
28 28 return (path
29 29 .replace(".d.hg/", ".d/")
30 30 .replace(".i.hg/", ".i/")
31 31 .replace(".hg.hg/", ".hg/"))
32 32
33 33 def read(self, node):
34 34 t = self.revision(node)
35 35 if not t.startswith('\1\n'):
36 36 return t
37 37 s = t.index('\1\n', 2)
38 38 return t[s+2:]
39 39
40 40 def readmeta(self, node):
41 41 t = self.revision(node)
42 42 if not t.startswith('\1\n'):
43 43 return {}
44 44 s = t.index('\1\n', 2)
45 45 mt = t[2:s]
46 46 m = {}
47 47 for l in mt.splitlines():
48 48 k, v = l.split(": ", 1)
49 49 m[k] = v
50 50 return m
51 51
52 52 def add(self, text, meta, transaction, link, p1=None, p2=None):
53 53 if meta or text.startswith('\1\n'):
54 54 mt = ""
55 55 if meta:
56 56 mt = [ "%s: %s\n" % (k, v) for k,v in meta.items() ]
57 57 text = "\1\n%s\1\n%s" % ("".join(mt), text)
58 58 return self.addrevision(text, transaction, link, p1, p2)
59 59
60 60 def renamed(self, node):
61 61 if self.parents(node)[0] != nullid:
62 62 return False
63 63 m = self.readmeta(node)
64 64 if m and m.has_key("copy"):
65 65 return (m["copy"], bin(m["copyrev"]))
66 66 return False
67 67
68 def size(self, rev):
69 """return the size of a given revision"""
70
71 # for revisions with renames, we have to go the slow way
72 node = self.node(rev)
73 if self.renamed(node):
74 return len(self.read(node))
75
76 return revlog.size(self, rev)
77
68 78 def cmp(self, node, text):
69 79 """compare text with a given file revision"""
70 80
71 81 # for renames, we have to go the slow way
72 82 if self.renamed(node):
73 83 t2 = self.read(node)
74 84 return t2 != text
75 85
76 86 return revlog.cmp(self, node, text)
77 87
78 88 def annotate(self, node):
79 89
80 90 def decorate(text, rev):
81 91 return ([rev] * len(text.splitlines()), text)
82 92
83 93 def pair(parent, child):
84 94 for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
85 95 child[0][b1:b2] = parent[0][a1:a2]
86 96 return child
87 97
88 98 # find all ancestors
89 99 needed = {node:1}
90 100 visit = [node]
91 101 while visit:
92 102 n = visit.pop(0)
93 103 for p in self.parents(n):
94 104 if p not in needed:
95 105 needed[p] = 1
96 106 visit.append(p)
97 107 else:
98 108 # count how many times we'll use this
99 109 needed[p] += 1
100 110
101 111 # sort by revision which is a topological order
102 112 visit = [ (self.rev(n), n) for n in needed.keys() ]
103 113 visit.sort()
104 114 hist = {}
105 115
106 116 for r,n in visit:
107 117 curr = decorate(self.read(n), self.linkrev(n))
108 118 for p in self.parents(n):
109 119 if p != nullid:
110 120 curr = pair(hist[p], curr)
111 121 # trim the history of unneeded revs
112 122 needed[p] -= 1
113 123 if not needed[p]:
114 124 del hist[p]
115 125 hist[n] = curr
116 126
117 127 return zip(hist[n][0], hist[n][1].splitlines(1))
@@ -1,332 +1,333
1 1 # merge.py - directory-level update/merge handling for Mercurial
2 2 #
3 3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import *
9 9 from i18n import gettext as _
10 10 from demandload import *
11 11 demandload(globals(), "util os tempfile")
12 12
13 13 def fmerge(f, local, other, ancestor):
14 14 """merge executable flags"""
15 15 a, b, c = ancestor.execf(f), local.execf(f), other.execf(f)
16 16 return ((a^b) | (a^c)) ^ a
17 17
18 18 def merge3(repo, fn, my, other, p1, p2):
19 19 """perform a 3-way merge in the working directory"""
20 20
21 21 def temp(prefix, node):
22 22 pre = "%s~%s." % (os.path.basename(fn), prefix)
23 23 (fd, name) = tempfile.mkstemp(prefix=pre)
24 24 f = os.fdopen(fd, "wb")
25 25 repo.wwrite(fn, fl.read(node), f)
26 26 f.close()
27 27 return name
28 28
29 29 fl = repo.file(fn)
30 30 base = fl.ancestor(my, other)
31 31 a = repo.wjoin(fn)
32 32 b = temp("base", base)
33 33 c = temp("other", other)
34 34
35 35 repo.ui.note(_("resolving %s\n") % fn)
36 36 repo.ui.debug(_("file %s: my %s other %s ancestor %s\n") %
37 37 (fn, short(my), short(other), short(base)))
38 38
39 39 cmd = (os.environ.get("HGMERGE") or repo.ui.config("ui", "merge")
40 40 or "hgmerge")
41 41 r = util.system('%s "%s" "%s" "%s"' % (cmd, a, b, c), cwd=repo.root,
42 42 environ={'HG_FILE': fn,
43 43 'HG_MY_NODE': p1,
44 44 'HG_OTHER_NODE': p2,
45 45 'HG_FILE_MY_NODE': hex(my),
46 46 'HG_FILE_OTHER_NODE': hex(other),
47 47 'HG_FILE_BASE_NODE': hex(base)})
48 48 if r:
49 49 repo.ui.warn(_("merging %s failed!\n") % fn)
50 50
51 51 os.unlink(b)
52 52 os.unlink(c)
53 53 return r
54 54
55 55 def update(repo, node, branchmerge=False, force=False, partial=None,
56 56 wlock=None, show_stats=True, remind=True):
57 57
58 58 overwrite = force and not branchmerge
59 59 forcemerge = force and branchmerge
60 60
61 61 if not wlock:
62 62 wlock = repo.wlock()
63 63
64 64 ### check phase
65 65
66 66 pl = repo.dirstate.parents()
67 67 if not overwrite and pl[1] != nullid:
68 68 raise util.Abort(_("outstanding uncommitted merges"))
69 69
70 70 p1, p2 = pl[0], node
71 71 pa = repo.changelog.ancestor(p1, p2)
72 72
73 73 # is there a linear path from p1 to p2?
74 74 linear_path = (pa == p1 or pa == p2)
75 75 if branchmerge and linear_path:
76 76 raise util.Abort(_("there is nothing to merge, just use "
77 77 "'hg update' or look at 'hg heads'"))
78 78
79 79 if not overwrite and not linear_path and not branchmerge:
80 80 raise util.Abort(_("update spans branches, use 'hg merge' "
81 81 "or 'hg update -C' to lose changes"))
82 82
83 83 modified, added, removed, deleted, unknown = repo.status()[:5]
84 84 if branchmerge and not forcemerge:
85 85 if modified or added or removed:
86 86 raise util.Abort(_("outstanding uncommitted changes"))
87 87
88 88 m1n = repo.changelog.read(p1)[0]
89 89 m2n = repo.changelog.read(p2)[0]
90 90 man = repo.manifest.ancestor(m1n, m2n)
91 91 m1 = repo.manifest.read(m1n)
92 92 m2 = repo.manifest.read(m2n).copy()
93 93 ma = repo.manifest.read(man)
94 94
95 95 if not force:
96 96 for f in unknown:
97 97 if f in m2:
98 98 if repo.file(f).cmp(m2[f], repo.wread(f)):
99 99 raise util.Abort(_("'%s' already exists in the working"
100 100 " dir and differs from remote") % f)
101 101
102 102 # resolve the manifest to determine which files
103 103 # we care about merging
104 104 repo.ui.note(_("resolving manifests\n"))
105 105 repo.ui.debug(_(" overwrite %s branchmerge %s partial %s linear %s\n") %
106 106 (overwrite, branchmerge, bool(partial), linear_path))
107 107 repo.ui.debug(_(" ancestor %s local %s remote %s\n") %
108 108 (short(man), short(m1n), short(m2n)))
109 109
110 110 merge = {}
111 111 get = {}
112 112 remove = []
113 113 forget = []
114 114
115 115 # construct a working dir manifest
116 116 mw = m1.copy()
117 117 umap = dict.fromkeys(unknown)
118 118
119 119 for f in added + modified + unknown:
120 120 mw[f] = ""
121 121 # is the wfile new and matches m2?
122 122 if (f not in m1 and f in m2 and
123 123 not repo.file(f).cmp(m2[f], repo.wread(f))):
124 124 mw[f] = m2[f]
125 125
126 126 mw.set(f, util.is_exec(repo.wjoin(f), mw.execf(f)))
127 127
128 128 for f in deleted + removed:
129 129 if f in mw:
130 130 del mw[f]
131 131
132 132 # If we're jumping between revisions (as opposed to merging),
133 133 # and if neither the working directory nor the target rev has
134 134 # the file, then we need to remove it from the dirstate, to
135 135 # prevent the dirstate from listing the file when it is no
136 136 # longer in the manifest.
137 137 if linear_path and f not in m2:
138 138 forget.append(f)
139 139
140 140 # Compare manifests
141 141 for f, n in mw.iteritems():
142 142 if partial and not partial(f):
143 143 continue
144 144 if f in m2:
145 145 s = 0
146 146
147 147 # are files different?
148 148 if n != m2[f]:
149 149 a = ma.get(f, nullid)
150 150 # are both different from the ancestor?
151 151 if n != a and m2[f] != a:
152 152 repo.ui.debug(_(" %s versions differ, resolve\n") % f)
153 153 merge[f] = (fmerge(f, mw, m2, ma), m1.get(f, nullid), m2[f])
154 154 s = 1
155 155 # are we clobbering?
156 156 # is remote's version newer?
157 157 # or are we going back in time?
158 158 elif overwrite or m2[f] != a or (p2 == pa and mw[f] == m1[f]):
159 159 repo.ui.debug(_(" remote %s is newer, get\n") % f)
160 160 get[f] = (m2.execf(f), m2[f])
161 161 s = 1
162 162 elif f in umap or f in added:
163 163 # this unknown file is the same as the checkout
164 164 # we need to reset the dirstate if the file was added
165 165 get[f] = (m2.execf(f), m2[f])
166 166
167 167 if not s and mw.execf(f) != m2.execf(f):
168 168 if overwrite:
169 169 repo.ui.debug(_(" updating permissions for %s\n") % f)
170 170 util.set_exec(repo.wjoin(f), m2.execf(f))
171 171 else:
172 172 if fmerge(f, mw, m2, ma) != mw.execf(f):
173 173 repo.ui.debug(_(" updating permissions for %s\n")
174 174 % f)
175 175 util.set_exec(repo.wjoin(f), mode)
176 176 del m2[f]
177 177 elif f in ma:
178 178 if n != ma[f]:
179 179 r = _("d")
180 180 if not overwrite and (linear_path or branchmerge):
181 181 r = repo.ui.prompt(
182 182 (_(" local changed %s which remote deleted\n") % f) +
183 183 _("(k)eep or (d)elete?"), _("[kd]"), _("k"))
184 184 if r == _("d"):
185 185 remove.append(f)
186 186 else:
187 187 repo.ui.debug(_("other deleted %s\n") % f)
188 188 remove.append(f) # other deleted it
189 189 else:
190 190 # file is created on branch or in working directory
191 191 if overwrite and f not in umap:
192 192 repo.ui.debug(_("remote deleted %s, clobbering\n") % f)
193 193 remove.append(f)
194 194 elif n == m1.get(f, nullid): # same as parent
195 195 if p2 == pa: # going backwards?
196 196 repo.ui.debug(_("remote deleted %s\n") % f)
197 197 remove.append(f)
198 198 else:
199 199 repo.ui.debug(_("local modified %s, keeping\n") % f)
200 200 else:
201 201 repo.ui.debug(_("working dir created %s, keeping\n") % f)
202 202
203 203 for f, n in m2.iteritems():
204 204 if partial and not partial(f):
205 205 continue
206 206 if f[0] == "/":
207 207 continue
208 208 if f in ma and n != ma[f]:
209 209 r = _("k")
210 210 if not overwrite and (linear_path or branchmerge):
211 211 r = repo.ui.prompt(
212 212 (_("remote changed %s which local deleted\n") % f) +
213 213 _("(k)eep or (d)elete?"), _("[kd]"), _("k"))
214 214 if r == _("k"):
215 215 get[f] = (m2.execf(f), n)
216 216 elif f not in ma:
217 217 repo.ui.debug(_("remote created %s\n") % f)
218 218 get[f] = (m2.execf(f), n)
219 219 else:
220 220 if overwrite or p2 == pa: # going backwards?
221 221 repo.ui.debug(_("local deleted %s, recreating\n") % f)
222 222 get[f] = (m2.execf(f), n)
223 223 else:
224 224 repo.ui.debug(_("local deleted %s\n") % f)
225 225
226 226 del mw, m1, m2, ma
227 227
228 228 ### apply phase
229 229
230 230 if overwrite:
231 231 for f in merge:
232 232 get[f] = merge[f][:2]
233 233 merge = {}
234 234
235 235 if linear_path or overwrite:
236 236 # we don't need to do any magic, just jump to the new rev
237 237 p1, p2 = p2, nullid
238 238
239 239 xp1 = hex(p1)
240 240 xp2 = hex(p2)
241 241 if p2 == nullid: xxp2 = ''
242 242 else: xxp2 = xp2
243 243
244 244 repo.hook('preupdate', throw=True, parent1=xp1, parent2=xxp2)
245 245
246 246 # get the files we don't need to change
247 247 files = get.keys()
248 248 files.sort()
249 249 for f in files:
250 250 flag, node = get[f]
251 251 if f[0] == "/":
252 252 continue
253 253 repo.ui.note(_("getting %s\n") % f)
254 254 t = repo.file(f).read(node)
255 255 repo.wwrite(f, t)
256 256 util.set_exec(repo.wjoin(f), flag)
257 257 if not partial:
258 258 if branchmerge:
259 259 repo.dirstate.update([f], 'n', st_mtime=-1)
260 260 else:
261 261 repo.dirstate.update([f], 'n')
262 262
263 263 # merge the tricky bits
264 264 unresolved = []
265 265 files = merge.keys()
266 266 files.sort()
267 267 for f in files:
268 268 repo.ui.status(_("merging %s\n") % f)
269 269 flag, my, other = merge[f]
270 270 ret = merge3(repo, f, my, other, xp1, xp2)
271 271 if ret:
272 272 unresolved.append(f)
273 273 util.set_exec(repo.wjoin(f), flag)
274 274 if not partial:
275 275 if branchmerge:
276 276 # We've done a branch merge, mark this file as merged
277 277 # so that we properly record the merger later
278 278 repo.dirstate.update([f], 'm')
279 279 else:
280 280 # We've update-merged a locally modified file, so
281 281 # we set the dirstate to emulate a normal checkout
282 282 # of that file some time in the past. Thus our
283 283 # merge will appear as a normal local file
284 284 # modification.
285 f_len = len(repo.file(f).read(other))
285 fl = repo.file(f)
286 f_len = fl.size(fl.rev(other))
286 287 repo.dirstate.update([f], 'n', st_size=f_len, st_mtime=-1)
287 288
288 289 remove.sort()
289 290 for f in remove:
290 291 repo.ui.note(_("removing %s\n") % f)
291 292 util.audit_path(f)
292 293 try:
293 294 util.unlink(repo.wjoin(f))
294 295 except OSError, inst:
295 296 if inst.errno != errno.ENOENT:
296 297 repo.ui.warn(_("update failed to remove %s: %s!\n") %
297 298 (f, inst.strerror))
298 299 if not partial:
299 300 if branchmerge:
300 301 repo.dirstate.update(remove, 'r')
301 302 else:
302 303 repo.dirstate.forget(remove)
303 304
304 305 if not partial:
305 306 repo.dirstate.setparents(p1, p2)
306 307 repo.dirstate.forget(forget)
307 308
308 309 if show_stats:
309 310 stats = ((len(get), _("updated")),
310 311 (len(merge) - len(unresolved), _("merged")),
311 312 (len(remove), _("removed")),
312 313 (len(unresolved), _("unresolved")))
313 314 note = ", ".join([_("%d files %s") % s for s in stats])
314 315 repo.ui.status("%s\n" % note)
315 316 if not partial:
316 317 if branchmerge:
317 318 if unresolved:
318 319 repo.ui.status(_("There are unresolved merges,"
319 320 " you can redo the full merge using:\n"
320 321 " hg update -C %s\n"
321 322 " hg merge %s\n"
322 323 % (repo.changelog.rev(p1),
323 324 repo.changelog.rev(p2))))
324 325 elif remind:
325 326 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
326 327 elif unresolved:
327 328 repo.ui.status(_("There are unresolved merges with"
328 329 " locally modified files.\n"))
329 330
330 331 repo.hook('update', parent1=xp1, parent2=xxp2, error=len(unresolved))
331 332 return len(unresolved)
332 333
General Comments 0
You need to be logged in to leave comments. Login now