##// END OF EJS Templates
Factor tags module out of localrepo (issue548)....
Greg Ward -
r9149:abb7d4d4 default
parent child Browse files
Show More
@@ -0,0 +1,122 b''
1 # tags.py - read tag info from local repository
2 #
3 # Copyright 2009 Matt Mackall <mpm@selenic.com>
4 # Copyright 2009 Greg Ward <greg@gerg.ca>
5 #
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2, incorporated herein by reference.
8
9 # Currently this module only deals with reading tags. Soon it will grow
10 # support for caching tag info. Eventually, it could take care of
11 # updating (adding/removing/moving) tags too.
12
13 from node import bin, hex
14 from i18n import _
15 import encoding
16 import error
17
18 def findglobaltags(ui, repo, alltags, tagtypes):
19 '''Find global tags in repo by reading .hgtags from every head that
20 has a distinct version of it. Updates the dicts alltags, tagtypes
21 in place: alltags maps tag name to (node, hist) pair (see _readtags()
22 below), and tagtypes maps tag name to tag type ('global' in this
23 case).'''
24
25 seen = set()
26 fctx = None
27 ctxs = [] # list of filectx
28 for node in repo.heads():
29 try:
30 fnode = repo[node].filenode('.hgtags')
31 except error.LookupError:
32 continue
33 if fnode not in seen:
34 seen.add(fnode)
35 if not fctx:
36 fctx = repo.filectx('.hgtags', fileid=fnode)
37 else:
38 fctx = fctx.filectx(fnode)
39 ctxs.append(fctx)
40
41 # read the tags file from each head, ending with the tip
42 for fctx in reversed(ctxs):
43 filetags = _readtags(
44 ui, repo, fctx.data().splitlines(), fctx)
45 _updatetags(filetags, "global", alltags, tagtypes)
46
47 def readlocaltags(ui, repo, alltags, tagtypes):
48 '''Read local tags in repo. Update alltags and tagtypes.'''
49 try:
50 data = encoding.fromlocal(repo.opener("localtags").read())
51 # localtags are stored in the local character set
52 # while the internal tag table is stored in UTF-8
53 filetags = _readtags(
54 ui, repo, data.splitlines(), "localtags")
55 _updatetags(filetags, "local", alltags, tagtypes)
56 except IOError:
57 pass
58
59 def _readtags(ui, repo, lines, fn):
60 '''Read tag definitions from a file (or any source of lines).
61 Return a mapping from tag name to (node, hist): node is the node id
62 from the last line read for that name, and hist is the list of node
63 ids previously associated with it (in file order). All node ids are
64 binary, not hex.'''
65
66 filetags = {} # map tag name to (node, hist)
67 count = 0
68
69 def warn(msg):
70 ui.warn(_("%s, line %s: %s\n") % (fn, count, msg))
71
72 for line in lines:
73 count += 1
74 if not line:
75 continue
76 try:
77 (nodehex, name) = line.split(" ", 1)
78 except ValueError:
79 warn(_("cannot parse entry"))
80 continue
81 name = encoding.tolocal(name.strip()) # stored in UTF-8
82 try:
83 nodebin = bin(nodehex)
84 except TypeError:
85 warn(_("node '%s' is not well formed") % nodehex)
86 continue
87 if nodebin not in repo.changelog.nodemap:
88 # silently ignore as pull -r might cause this
89 continue
90
91 # update filetags
92 hist = []
93 if name in filetags:
94 n, hist = filetags[name]
95 hist.append(n)
96 filetags[name] = (nodebin, hist)
97 return filetags
98
99 def _updatetags(filetags, tagtype, alltags, tagtypes):
100 '''Incorporate the tag info read from one file into the two
101 dictionaries, alltags and tagtypes, that contain all tag
102 info (global across all heads plus local).'''
103
104 for name, nodehist in filetags.iteritems():
105 if name not in alltags:
106 alltags[name] = nodehist
107 tagtypes[name] = tagtype
108 continue
109
110 # we prefer alltags[name] if:
111 # it supercedes us OR
112 # mutual supercedes and it has a higher rank
113 # otherwise we win because we're tip-most
114 anode, ahist = nodehist
115 bnode, bhist = alltags[name]
116 if (bnode != anode and anode in bhist and
117 (bnode not in ahist or len(bhist) > len(ahist))):
118 anode = bnode
119 ahist.extend([n for n in bhist if n not in ahist])
120 alltags[name] = anode, ahist
121 tagtypes[name] = tagtype
122
@@ -1,2214 +1,2124 b''
1 1 # localrepo.py - read/write repository class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2, incorporated herein by reference.
7 7
8 8 from node import bin, hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import repo, changegroup, subrepo
11 11 import changelog, dirstate, filelog, manifest, context
12 12 import lock, transaction, store, encoding
13 13 import util, extensions, hook, error
14 14 import match as match_
15 15 import merge as merge_
16 import tags as tags_
16 17 from lock import release
17 18 import weakref, stat, errno, os, time, inspect
18 19 propertycache = util.propertycache
19 20
20 21 class localrepository(repo.repository):
21 22 capabilities = set(('lookup', 'changegroupsubset', 'branchmap'))
22 23 supported = set('revlogv1 store fncache shared'.split())
23 24
24 25 def __init__(self, baseui, path=None, create=0):
25 26 repo.repository.__init__(self)
26 27 self.root = os.path.realpath(path)
27 28 self.path = os.path.join(self.root, ".hg")
28 29 self.origroot = path
29 30 self.opener = util.opener(self.path)
30 31 self.wopener = util.opener(self.root)
31 32 self.baseui = baseui
32 33 self.ui = baseui.copy()
33 34
34 35 try:
35 36 self.ui.readconfig(self.join("hgrc"), self.root)
36 37 extensions.loadall(self.ui)
37 38 except IOError:
38 39 pass
39 40
40 41 if not os.path.isdir(self.path):
41 42 if create:
42 43 if not os.path.exists(path):
43 44 os.mkdir(path)
44 45 os.mkdir(self.path)
45 46 requirements = ["revlogv1"]
46 47 if self.ui.configbool('format', 'usestore', True):
47 48 os.mkdir(os.path.join(self.path, "store"))
48 49 requirements.append("store")
49 50 if self.ui.configbool('format', 'usefncache', True):
50 51 requirements.append("fncache")
51 52 # create an invalid changelog
52 53 self.opener("00changelog.i", "a").write(
53 54 '\0\0\0\2' # represents revlogv2
54 55 ' dummy changelog to prevent using the old repo layout'
55 56 )
56 57 reqfile = self.opener("requires", "w")
57 58 for r in requirements:
58 59 reqfile.write("%s\n" % r)
59 60 reqfile.close()
60 61 else:
61 62 raise error.RepoError(_("repository %s not found") % path)
62 63 elif create:
63 64 raise error.RepoError(_("repository %s already exists") % path)
64 65 else:
65 66 # find requirements
66 67 requirements = set()
67 68 try:
68 69 requirements = set(self.opener("requires").read().splitlines())
69 70 except IOError, inst:
70 71 if inst.errno != errno.ENOENT:
71 72 raise
72 73 for r in requirements - self.supported:
73 74 raise error.RepoError(_("requirement '%s' not supported") % r)
74 75
75 76 self.sharedpath = self.path
76 77 try:
77 78 s = os.path.realpath(self.opener("sharedpath").read())
78 79 if not os.path.exists(s):
79 80 raise error.RepoError(
80 81 _('.hg/sharedpath points to nonexistent directory %s') % s)
81 82 self.sharedpath = s
82 83 except IOError, inst:
83 84 if inst.errno != errno.ENOENT:
84 85 raise
85 86
86 87 self.store = store.store(requirements, self.sharedpath, util.opener)
87 88 self.spath = self.store.path
88 89 self.sopener = self.store.opener
89 90 self.sjoin = self.store.join
90 91 self.opener.createmode = self.store.createmode
91 92
92 93 # These two define the set of tags for this repository. _tags
93 94 # maps tag name to node; _tagtypes maps tag name to 'global' or
94 95 # 'local'. (Global tags are defined by .hgtags across all
95 96 # heads, and local tags are defined in .hg/localtags.) They
96 97 # constitute the in-memory cache of tags.
97 98 self._tags = None
98 99 self._tagtypes = None
99 100
100 101 self.branchcache = None
101 102 self._ubranchcache = None # UTF-8 version of branchcache
102 103 self._branchcachetip = None
103 104 self.nodetagscache = None
104 105 self.filterpats = {}
105 106 self._datafilters = {}
106 107 self._transref = self._lockref = self._wlockref = None
107 108
108 109 @propertycache
109 110 def changelog(self):
110 111 c = changelog.changelog(self.sopener)
111 112 if 'HG_PENDING' in os.environ:
112 113 p = os.environ['HG_PENDING']
113 114 if p.startswith(self.root):
114 115 c.readpending('00changelog.i.a')
115 116 self.sopener.defversion = c.version
116 117 return c
117 118
118 119 @propertycache
119 120 def manifest(self):
120 121 return manifest.manifest(self.sopener)
121 122
122 123 @propertycache
123 124 def dirstate(self):
124 125 return dirstate.dirstate(self.opener, self.ui, self.root)
125 126
126 127 def __getitem__(self, changeid):
127 128 if changeid is None:
128 129 return context.workingctx(self)
129 130 return context.changectx(self, changeid)
130 131
131 132 def __nonzero__(self):
132 133 return True
133 134
134 135 def __len__(self):
135 136 return len(self.changelog)
136 137
137 138 def __iter__(self):
138 139 for i in xrange(len(self)):
139 140 yield i
140 141
141 142 def url(self):
142 143 return 'file:' + self.root
143 144
144 145 def hook(self, name, throw=False, **args):
145 146 return hook.hook(self.ui, self, name, throw, **args)
146 147
147 148 tag_disallowed = ':\r\n'
148 149
149 150 def _tag(self, names, node, message, local, user, date, extra={}):
150 151 if isinstance(names, str):
151 152 allchars = names
152 153 names = (names,)
153 154 else:
154 155 allchars = ''.join(names)
155 156 for c in self.tag_disallowed:
156 157 if c in allchars:
157 158 raise util.Abort(_('%r cannot be used in a tag name') % c)
158 159
159 160 for name in names:
160 161 self.hook('pretag', throw=True, node=hex(node), tag=name,
161 162 local=local)
162 163
163 164 def writetags(fp, names, munge, prevtags):
164 165 fp.seek(0, 2)
165 166 if prevtags and prevtags[-1] != '\n':
166 167 fp.write('\n')
167 168 for name in names:
168 169 m = munge and munge(name) or name
169 170 if self._tagtypes and name in self._tagtypes:
170 171 old = self._tags.get(name, nullid)
171 172 fp.write('%s %s\n' % (hex(old), m))
172 173 fp.write('%s %s\n' % (hex(node), m))
173 174 fp.close()
174 175
175 176 prevtags = ''
176 177 if local:
177 178 try:
178 179 fp = self.opener('localtags', 'r+')
179 180 except IOError:
180 181 fp = self.opener('localtags', 'a')
181 182 else:
182 183 prevtags = fp.read()
183 184
184 185 # local tags are stored in the current charset
185 186 writetags(fp, names, None, prevtags)
186 187 for name in names:
187 188 self.hook('tag', node=hex(node), tag=name, local=local)
188 189 return
189 190
190 191 try:
191 192 fp = self.wfile('.hgtags', 'rb+')
192 193 except IOError:
193 194 fp = self.wfile('.hgtags', 'ab')
194 195 else:
195 196 prevtags = fp.read()
196 197
197 198 # committed tags are stored in UTF-8
198 199 writetags(fp, names, encoding.fromlocal, prevtags)
199 200
200 201 if '.hgtags' not in self.dirstate:
201 202 self.add(['.hgtags'])
202 203
203 204 m = match_.exact(self.root, '', ['.hgtags'])
204 205 tagnode = self.commit(message, user, date, extra=extra, match=m)
205 206
206 207 for name in names:
207 208 self.hook('tag', node=hex(node), tag=name, local=local)
208 209
209 210 return tagnode
210 211
211 212 def tag(self, names, node, message, local, user, date):
212 213 '''tag a revision with one or more symbolic names.
213 214
214 215 names is a list of strings or, when adding a single tag, names may be a
215 216 string.
216 217
217 218 if local is True, the tags are stored in a per-repository file.
218 219 otherwise, they are stored in the .hgtags file, and a new
219 220 changeset is committed with the change.
220 221
221 222 keyword arguments:
222 223
223 224 local: whether to store tags in non-version-controlled file
224 225 (default False)
225 226
226 227 message: commit message to use if committing
227 228
228 229 user: name of user to use if committing
229 230
230 231 date: date tuple to use if committing'''
231 232
232 233 for x in self.status()[:5]:
233 234 if '.hgtags' in x:
234 235 raise util.Abort(_('working copy of .hgtags is changed '
235 236 '(please commit .hgtags manually)'))
236 237
237 238 self.tags() # instantiate the cache
238 239 self._tag(names, node, message, local, user, date)
239 240
240 241 def tags(self):
241 242 '''return a mapping of tag to node'''
242 243 if self._tags is None:
243 244 (self._tags, self._tagtypes) = self._findtags()
244 245
245 246 return self._tags
246 247
247 248 def _findtags(self):
248 249 '''Do the hard work of finding tags. Return a pair of dicts
249 250 (tags, tagtypes) where tags maps tag name to node, and tagtypes
250 251 maps tag name to a string like \'global\' or \'local\'.
251 252 Subclasses or extensions are free to add their own tags, but
252 253 should be aware that the returned dicts will be retained for the
253 254 duration of the localrepo object.'''
254 255
255 256 # XXX what tagtype should subclasses/extensions use? Currently
256 257 # mq and bookmarks add tags, but do not set the tagtype at all.
257 258 # Should each extension invent its own tag type? Should there
258 259 # be one tagtype for all such "virtual" tags? Or is the status
259 260 # quo fine?
260 261
261 def readtags(lines, fn):
262 '''Read tag definitions from a file (or any source of
263 lines). Return a mapping from tag name to (node, hist):
264 node is the node id from the last line read for that name,
265 and hist is the list of node ids previously associated with
266 it (in file order). All node ids are binary, not hex.'''
267
268 filetags = {} # map tag name to (node, hist)
269 count = 0
270
271 def warn(msg):
272 self.ui.warn(_("%s, line %s: %s\n") % (fn, count, msg))
273
274 for line in lines:
275 count += 1
276 if not line:
277 continue
278 try:
279 (nodehex, name) = line.split(" ", 1)
280 except ValueError:
281 warn(_("cannot parse entry"))
282 continue
283 name = encoding.tolocal(name.strip()) # stored in UTF-8
284 try:
285 nodebin = bin(nodehex)
286 except TypeError:
287 warn(_("node '%s' is not well formed") % nodehex)
288 continue
289 if nodebin not in self.changelog.nodemap:
290 # silently ignore as pull -r might cause this
291 continue
292
293 # update filetags
294 hist = []
295 if name in filetags:
296 n, hist = filetags[name]
297 hist.append(n)
298 filetags[name] = (nodebin, hist)
299 return filetags
300
301 262 alltags = {} # map tag name to (node, hist)
302 263 tagtypes = {}
303 264
304 def updatetags(filetags, tagtype):
305 '''Incorporate the tag info read from one file into the two
306 dictionaries, alltags and tagtypes, that contain all tag
307 info (global across all heads plus local).'''
308
309 for name, nodehist in filetags.iteritems():
310 if name not in alltags:
311 alltags[name] = nodehist
312 tagtypes[name] = tagtype
313 continue
314
315 # we prefer alltags[name] if:
316 # it supercedes us OR
317 # mutual supercedes and it has a higher rank
318 # otherwise we win because we're tip-most
319 anode, ahist = nodehist
320 bnode, bhist = alltags[name]
321 if (bnode != anode and anode in bhist and
322 (bnode not in ahist or len(bhist) > len(ahist))):
323 anode = bnode
324 ahist.extend([n for n in bhist if n not in ahist])
325 alltags[name] = anode, ahist
326 tagtypes[name] = tagtype
327
328 seen = set()
329 fctx = None
330 ctxs = [] # list of filectx
331 for node in self.heads():
332 try:
333 fnode = self[node].filenode('.hgtags')
334 except error.LookupError:
335 continue
336 if fnode not in seen:
337 seen.add(fnode)
338 if not fctx:
339 fctx = self.filectx('.hgtags', fileid=fnode)
340 else:
341 fctx = fctx.filectx(fnode)
342 ctxs.append(fctx)
343
344 # read the tags file from each head, ending with the tip
345 for fctx in reversed(ctxs):
346 filetags = readtags(fctx.data().splitlines(), fctx)
347 updatetags(filetags, "global")
348
349 try:
350 data = encoding.fromlocal(self.opener("localtags").read())
351 # localtags are stored in the local character set
352 # while the internal tag table is stored in UTF-8
353 filetags = readtags(data.splitlines(), "localtags")
354 updatetags(filetags, "local")
355 except IOError:
356 pass
265 tags_.findglobaltags(self.ui, self, alltags, tagtypes)
266 tags_.readlocaltags(self.ui, self, alltags, tagtypes)
357 267
358 268 tags = {}
359 269 for (name, (node, hist)) in alltags.iteritems():
360 270 if node != nullid:
361 271 tags[name] = node
362 272 tags['tip'] = self.changelog.tip()
363 273 return (tags, tagtypes)
364 274
365 275 def tagtype(self, tagname):
366 276 '''
367 277 return the type of the given tag. result can be:
368 278
369 279 'local' : a local tag
370 280 'global' : a global tag
371 281 None : tag does not exist
372 282 '''
373 283
374 284 self.tags()
375 285
376 286 return self._tagtypes.get(tagname)
377 287
378 288 def tagslist(self):
379 289 '''return a list of tags ordered by revision'''
380 290 l = []
381 291 for t, n in self.tags().iteritems():
382 292 try:
383 293 r = self.changelog.rev(n)
384 294 except:
385 295 r = -2 # sort to the beginning of the list if unknown
386 296 l.append((r, t, n))
387 297 return [(t, n) for r, t, n in sorted(l)]
388 298
389 299 def nodetags(self, node):
390 300 '''return the tags associated with a node'''
391 301 if not self.nodetagscache:
392 302 self.nodetagscache = {}
393 303 for t, n in self.tags().iteritems():
394 304 self.nodetagscache.setdefault(n, []).append(t)
395 305 return self.nodetagscache.get(node, [])
396 306
397 307 def _branchtags(self, partial, lrev):
398 308 # TODO: rename this function?
399 309 tiprev = len(self) - 1
400 310 if lrev != tiprev:
401 311 self._updatebranchcache(partial, lrev+1, tiprev+1)
402 312 self._writebranchcache(partial, self.changelog.tip(), tiprev)
403 313
404 314 return partial
405 315
406 316 def branchmap(self):
407 317 tip = self.changelog.tip()
408 318 if self.branchcache is not None and self._branchcachetip == tip:
409 319 return self.branchcache
410 320
411 321 oldtip = self._branchcachetip
412 322 self._branchcachetip = tip
413 323 if self.branchcache is None:
414 324 self.branchcache = {} # avoid recursion in changectx
415 325 else:
416 326 self.branchcache.clear() # keep using the same dict
417 327 if oldtip is None or oldtip not in self.changelog.nodemap:
418 328 partial, last, lrev = self._readbranchcache()
419 329 else:
420 330 lrev = self.changelog.rev(oldtip)
421 331 partial = self._ubranchcache
422 332
423 333 self._branchtags(partial, lrev)
424 334 # this private cache holds all heads (not just tips)
425 335 self._ubranchcache = partial
426 336
427 337 # the branch cache is stored on disk as UTF-8, but in the local
428 338 # charset internally
429 339 for k, v in partial.iteritems():
430 340 self.branchcache[encoding.tolocal(k)] = v
431 341 return self.branchcache
432 342
433 343
434 344 def branchtags(self):
435 345 '''return a dict where branch names map to the tipmost head of
436 346 the branch, open heads come before closed'''
437 347 bt = {}
438 348 for bn, heads in self.branchmap().iteritems():
439 349 head = None
440 350 for i in range(len(heads)-1, -1, -1):
441 351 h = heads[i]
442 352 if 'close' not in self.changelog.read(h)[5]:
443 353 head = h
444 354 break
445 355 # no open heads were found
446 356 if head is None:
447 357 head = heads[-1]
448 358 bt[bn] = head
449 359 return bt
450 360
451 361
452 362 def _readbranchcache(self):
453 363 partial = {}
454 364 try:
455 365 f = self.opener("branchheads.cache")
456 366 lines = f.read().split('\n')
457 367 f.close()
458 368 except (IOError, OSError):
459 369 return {}, nullid, nullrev
460 370
461 371 try:
462 372 last, lrev = lines.pop(0).split(" ", 1)
463 373 last, lrev = bin(last), int(lrev)
464 374 if lrev >= len(self) or self[lrev].node() != last:
465 375 # invalidate the cache
466 376 raise ValueError('invalidating branch cache (tip differs)')
467 377 for l in lines:
468 378 if not l: continue
469 379 node, label = l.split(" ", 1)
470 380 partial.setdefault(label.strip(), []).append(bin(node))
471 381 except KeyboardInterrupt:
472 382 raise
473 383 except Exception, inst:
474 384 if self.ui.debugflag:
475 385 self.ui.warn(str(inst), '\n')
476 386 partial, last, lrev = {}, nullid, nullrev
477 387 return partial, last, lrev
478 388
479 389 def _writebranchcache(self, branches, tip, tiprev):
480 390 try:
481 391 f = self.opener("branchheads.cache", "w", atomictemp=True)
482 392 f.write("%s %s\n" % (hex(tip), tiprev))
483 393 for label, nodes in branches.iteritems():
484 394 for node in nodes:
485 395 f.write("%s %s\n" % (hex(node), label))
486 396 f.rename()
487 397 except (IOError, OSError):
488 398 pass
489 399
490 400 def _updatebranchcache(self, partial, start, end):
491 401 # collect new branch entries
492 402 newbranches = {}
493 403 for r in xrange(start, end):
494 404 c = self[r]
495 405 newbranches.setdefault(c.branch(), []).append(c.node())
496 406 # if older branchheads are reachable from new ones, they aren't
497 407 # really branchheads. Note checking parents is insufficient:
498 408 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
499 409 for branch, newnodes in newbranches.iteritems():
500 410 bheads = partial.setdefault(branch, [])
501 411 bheads.extend(newnodes)
502 412 if len(bheads) < 2:
503 413 continue
504 414 newbheads = []
505 415 # starting from tip means fewer passes over reachable
506 416 while newnodes:
507 417 latest = newnodes.pop()
508 418 if latest not in bheads:
509 419 continue
510 420 minbhrev = self[min([self[bh].rev() for bh in bheads])].node()
511 421 reachable = self.changelog.reachable(latest, minbhrev)
512 422 bheads = [b for b in bheads if b not in reachable]
513 423 newbheads.insert(0, latest)
514 424 bheads.extend(newbheads)
515 425 partial[branch] = bheads
516 426
517 427 def lookup(self, key):
518 428 if isinstance(key, int):
519 429 return self.changelog.node(key)
520 430 elif key == '.':
521 431 return self.dirstate.parents()[0]
522 432 elif key == 'null':
523 433 return nullid
524 434 elif key == 'tip':
525 435 return self.changelog.tip()
526 436 n = self.changelog._match(key)
527 437 if n:
528 438 return n
529 439 if key in self.tags():
530 440 return self.tags()[key]
531 441 if key in self.branchtags():
532 442 return self.branchtags()[key]
533 443 n = self.changelog._partialmatch(key)
534 444 if n:
535 445 return n
536 446
537 447 # can't find key, check if it might have come from damaged dirstate
538 448 if key in self.dirstate.parents():
539 449 raise error.Abort(_("working directory has unknown parent '%s'!")
540 450 % short(key))
541 451 try:
542 452 if len(key) == 20:
543 453 key = hex(key)
544 454 except:
545 455 pass
546 456 raise error.RepoError(_("unknown revision '%s'") % key)
547 457
548 458 def local(self):
549 459 return True
550 460
551 461 def join(self, f):
552 462 return os.path.join(self.path, f)
553 463
554 464 def wjoin(self, f):
555 465 return os.path.join(self.root, f)
556 466
557 467 def rjoin(self, f):
558 468 return os.path.join(self.root, util.pconvert(f))
559 469
560 470 def file(self, f):
561 471 if f[0] == '/':
562 472 f = f[1:]
563 473 return filelog.filelog(self.sopener, f)
564 474
565 475 def changectx(self, changeid):
566 476 return self[changeid]
567 477
568 478 def parents(self, changeid=None):
569 479 '''get list of changectxs for parents of changeid'''
570 480 return self[changeid].parents()
571 481
572 482 def filectx(self, path, changeid=None, fileid=None):
573 483 """changeid can be a changeset revision, node, or tag.
574 484 fileid can be a file revision or node."""
575 485 return context.filectx(self, path, changeid, fileid)
576 486
577 487 def getcwd(self):
578 488 return self.dirstate.getcwd()
579 489
580 490 def pathto(self, f, cwd=None):
581 491 return self.dirstate.pathto(f, cwd)
582 492
583 493 def wfile(self, f, mode='r'):
584 494 return self.wopener(f, mode)
585 495
586 496 def _link(self, f):
587 497 return os.path.islink(self.wjoin(f))
588 498
589 499 def _filter(self, filter, filename, data):
590 500 if filter not in self.filterpats:
591 501 l = []
592 502 for pat, cmd in self.ui.configitems(filter):
593 503 if cmd == '!':
594 504 continue
595 505 mf = match_.match(self.root, '', [pat])
596 506 fn = None
597 507 params = cmd
598 508 for name, filterfn in self._datafilters.iteritems():
599 509 if cmd.startswith(name):
600 510 fn = filterfn
601 511 params = cmd[len(name):].lstrip()
602 512 break
603 513 if not fn:
604 514 fn = lambda s, c, **kwargs: util.filter(s, c)
605 515 # Wrap old filters not supporting keyword arguments
606 516 if not inspect.getargspec(fn)[2]:
607 517 oldfn = fn
608 518 fn = lambda s, c, **kwargs: oldfn(s, c)
609 519 l.append((mf, fn, params))
610 520 self.filterpats[filter] = l
611 521
612 522 for mf, fn, cmd in self.filterpats[filter]:
613 523 if mf(filename):
614 524 self.ui.debug(_("filtering %s through %s\n") % (filename, cmd))
615 525 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
616 526 break
617 527
618 528 return data
619 529
620 530 def adddatafilter(self, name, filter):
621 531 self._datafilters[name] = filter
622 532
623 533 def wread(self, filename):
624 534 if self._link(filename):
625 535 data = os.readlink(self.wjoin(filename))
626 536 else:
627 537 data = self.wopener(filename, 'r').read()
628 538 return self._filter("encode", filename, data)
629 539
630 540 def wwrite(self, filename, data, flags):
631 541 data = self._filter("decode", filename, data)
632 542 try:
633 543 os.unlink(self.wjoin(filename))
634 544 except OSError:
635 545 pass
636 546 if 'l' in flags:
637 547 self.wopener.symlink(data, filename)
638 548 else:
639 549 self.wopener(filename, 'w').write(data)
640 550 if 'x' in flags:
641 551 util.set_flags(self.wjoin(filename), False, True)
642 552
643 553 def wwritedata(self, filename, data):
644 554 return self._filter("decode", filename, data)
645 555
646 556 def transaction(self):
647 557 tr = self._transref and self._transref() or None
648 558 if tr and tr.running():
649 559 return tr.nest()
650 560
651 561 # abort here if the journal already exists
652 562 if os.path.exists(self.sjoin("journal")):
653 563 raise error.RepoError(_("journal already exists - run hg recover"))
654 564
655 565 # save dirstate for rollback
656 566 try:
657 567 ds = self.opener("dirstate").read()
658 568 except IOError:
659 569 ds = ""
660 570 self.opener("journal.dirstate", "w").write(ds)
661 571 self.opener("journal.branch", "w").write(self.dirstate.branch())
662 572
663 573 renames = [(self.sjoin("journal"), self.sjoin("undo")),
664 574 (self.join("journal.dirstate"), self.join("undo.dirstate")),
665 575 (self.join("journal.branch"), self.join("undo.branch"))]
666 576 tr = transaction.transaction(self.ui.warn, self.sopener,
667 577 self.sjoin("journal"),
668 578 aftertrans(renames),
669 579 self.store.createmode)
670 580 self._transref = weakref.ref(tr)
671 581 return tr
672 582
673 583 def recover(self):
674 584 lock = self.lock()
675 585 try:
676 586 if os.path.exists(self.sjoin("journal")):
677 587 self.ui.status(_("rolling back interrupted transaction\n"))
678 588 transaction.rollback(self.sopener, self.sjoin("journal"), self.ui.warn)
679 589 self.invalidate()
680 590 return True
681 591 else:
682 592 self.ui.warn(_("no interrupted transaction available\n"))
683 593 return False
684 594 finally:
685 595 lock.release()
686 596
687 597 def rollback(self):
688 598 wlock = lock = None
689 599 try:
690 600 wlock = self.wlock()
691 601 lock = self.lock()
692 602 if os.path.exists(self.sjoin("undo")):
693 603 self.ui.status(_("rolling back last transaction\n"))
694 604 transaction.rollback(self.sopener, self.sjoin("undo"), self.ui.warn)
695 605 util.rename(self.join("undo.dirstate"), self.join("dirstate"))
696 606 try:
697 607 branch = self.opener("undo.branch").read()
698 608 self.dirstate.setbranch(branch)
699 609 except IOError:
700 610 self.ui.warn(_("Named branch could not be reset, "
701 611 "current branch still is: %s\n")
702 612 % encoding.tolocal(self.dirstate.branch()))
703 613 self.invalidate()
704 614 self.dirstate.invalidate()
705 615 else:
706 616 self.ui.warn(_("no rollback information available\n"))
707 617 finally:
708 618 release(lock, wlock)
709 619
710 620 def invalidate(self):
711 621 for a in "changelog manifest".split():
712 622 if a in self.__dict__:
713 623 delattr(self, a)
714 624 self._tags = None
715 625 self._tagtypes = None
716 626 self.nodetagscache = None
717 627 self.branchcache = None
718 628 self._ubranchcache = None
719 629 self._branchcachetip = None
720 630
721 631 def _lock(self, lockname, wait, releasefn, acquirefn, desc):
722 632 try:
723 633 l = lock.lock(lockname, 0, releasefn, desc=desc)
724 634 except error.LockHeld, inst:
725 635 if not wait:
726 636 raise
727 637 self.ui.warn(_("waiting for lock on %s held by %r\n") %
728 638 (desc, inst.locker))
729 639 # default to 600 seconds timeout
730 640 l = lock.lock(lockname, int(self.ui.config("ui", "timeout", "600")),
731 641 releasefn, desc=desc)
732 642 if acquirefn:
733 643 acquirefn()
734 644 return l
735 645
736 646 def lock(self, wait=True):
737 647 l = self._lockref and self._lockref()
738 648 if l is not None and l.held:
739 649 l.lock()
740 650 return l
741 651
742 652 l = self._lock(self.sjoin("lock"), wait, None, self.invalidate,
743 653 _('repository %s') % self.origroot)
744 654 self._lockref = weakref.ref(l)
745 655 return l
746 656
747 657 def wlock(self, wait=True):
748 658 l = self._wlockref and self._wlockref()
749 659 if l is not None and l.held:
750 660 l.lock()
751 661 return l
752 662
753 663 l = self._lock(self.join("wlock"), wait, self.dirstate.write,
754 664 self.dirstate.invalidate, _('working directory of %s') %
755 665 self.origroot)
756 666 self._wlockref = weakref.ref(l)
757 667 return l
758 668
759 669 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist):
760 670 """
761 671 commit an individual file as part of a larger transaction
762 672 """
763 673
764 674 fname = fctx.path()
765 675 text = fctx.data()
766 676 flog = self.file(fname)
767 677 fparent1 = manifest1.get(fname, nullid)
768 678 fparent2 = fparent2o = manifest2.get(fname, nullid)
769 679
770 680 meta = {}
771 681 copy = fctx.renamed()
772 682 if copy and copy[0] != fname:
773 683 # Mark the new revision of this file as a copy of another
774 684 # file. This copy data will effectively act as a parent
775 685 # of this new revision. If this is a merge, the first
776 686 # parent will be the nullid (meaning "look up the copy data")
777 687 # and the second one will be the other parent. For example:
778 688 #
779 689 # 0 --- 1 --- 3 rev1 changes file foo
780 690 # \ / rev2 renames foo to bar and changes it
781 691 # \- 2 -/ rev3 should have bar with all changes and
782 692 # should record that bar descends from
783 693 # bar in rev2 and foo in rev1
784 694 #
785 695 # this allows this merge to succeed:
786 696 #
787 697 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
788 698 # \ / merging rev3 and rev4 should use bar@rev2
789 699 # \- 2 --- 4 as the merge base
790 700 #
791 701
792 702 cfname = copy[0]
793 703 crev = manifest1.get(cfname)
794 704 newfparent = fparent2
795 705
796 706 if manifest2: # branch merge
797 707 if fparent2 == nullid or crev is None: # copied on remote side
798 708 if cfname in manifest2:
799 709 crev = manifest2[cfname]
800 710 newfparent = fparent1
801 711
802 712 # find source in nearest ancestor if we've lost track
803 713 if not crev:
804 714 self.ui.debug(_(" %s: searching for copy revision for %s\n") %
805 715 (fname, cfname))
806 716 for ancestor in self['.'].ancestors():
807 717 if cfname in ancestor:
808 718 crev = ancestor[cfname].filenode()
809 719 break
810 720
811 721 self.ui.debug(_(" %s: copy %s:%s\n") % (fname, cfname, hex(crev)))
812 722 meta["copy"] = cfname
813 723 meta["copyrev"] = hex(crev)
814 724 fparent1, fparent2 = nullid, newfparent
815 725 elif fparent2 != nullid:
816 726 # is one parent an ancestor of the other?
817 727 fparentancestor = flog.ancestor(fparent1, fparent2)
818 728 if fparentancestor == fparent1:
819 729 fparent1, fparent2 = fparent2, nullid
820 730 elif fparentancestor == fparent2:
821 731 fparent2 = nullid
822 732
823 733 # is the file changed?
824 734 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
825 735 changelist.append(fname)
826 736 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
827 737
828 738 # are just the flags changed during merge?
829 739 if fparent1 != fparent2o and manifest1.flags(fname) != fctx.flags():
830 740 changelist.append(fname)
831 741
832 742 return fparent1
833 743
834 744 def commit(self, text="", user=None, date=None, match=None, force=False,
835 745 editor=False, extra={}):
836 746 """Add a new revision to current repository.
837 747
838 748 Revision information is gathered from the working directory,
839 749 match can be used to filter the committed files. If editor is
840 750 supplied, it is called to get a commit message.
841 751 """
842 752
843 753 def fail(f, msg):
844 754 raise util.Abort('%s: %s' % (f, msg))
845 755
846 756 if not match:
847 757 match = match_.always(self.root, '')
848 758
849 759 if not force:
850 760 vdirs = []
851 761 match.dir = vdirs.append
852 762 match.bad = fail
853 763
854 764 wlock = self.wlock()
855 765 try:
856 766 p1, p2 = self.dirstate.parents()
857 767 wctx = self[None]
858 768
859 769 if (not force and p2 != nullid and match and
860 770 (match.files() or match.anypats())):
861 771 raise util.Abort(_('cannot partially commit a merge '
862 772 '(do not specify files or patterns)'))
863 773
864 774 changes = self.status(match=match, clean=force)
865 775 if force:
866 776 changes[0].extend(changes[6]) # mq may commit unchanged files
867 777
868 778 # check subrepos
869 779 subs = []
870 780 for s in wctx.substate:
871 781 if match(s) and wctx.sub(s).dirty():
872 782 subs.append(s)
873 783 if subs and '.hgsubstate' not in changes[0]:
874 784 changes[0].insert(0, '.hgsubstate')
875 785
876 786 # make sure all explicit patterns are matched
877 787 if not force and match.files():
878 788 matched = set(changes[0] + changes[1] + changes[2])
879 789
880 790 for f in match.files():
881 791 if f == '.' or f in matched or f in wctx.substate:
882 792 continue
883 793 if f in changes[3]: # missing
884 794 fail(f, _('file not found!'))
885 795 if f in vdirs: # visited directory
886 796 d = f + '/'
887 797 for mf in matched:
888 798 if mf.startswith(d):
889 799 break
890 800 else:
891 801 fail(f, _("no match under directory!"))
892 802 elif f not in self.dirstate:
893 803 fail(f, _("file not tracked!"))
894 804
895 805 if (not force and not extra.get("close") and p2 == nullid
896 806 and not (changes[0] or changes[1] or changes[2])
897 807 and self[None].branch() == self['.'].branch()):
898 808 return None
899 809
900 810 ms = merge_.mergestate(self)
901 811 for f in changes[0]:
902 812 if f in ms and ms[f] == 'u':
903 813 raise util.Abort(_("unresolved merge conflicts "
904 814 "(see hg resolve)"))
905 815
906 816 cctx = context.workingctx(self, (p1, p2), text, user, date,
907 817 extra, changes)
908 818 if editor:
909 819 cctx._text = editor(self, cctx, subs)
910 820
911 821 # commit subs
912 822 if subs:
913 823 state = wctx.substate.copy()
914 824 for s in subs:
915 825 self.ui.status(_('committing subrepository %s\n') % s)
916 826 sr = wctx.sub(s).commit(cctx._text, user, date)
917 827 state[s] = (state[s][0], sr)
918 828 subrepo.writestate(self, state)
919 829
920 830 ret = self.commitctx(cctx, True)
921 831
922 832 # update dirstate and mergestate
923 833 for f in changes[0] + changes[1]:
924 834 self.dirstate.normal(f)
925 835 for f in changes[2]:
926 836 self.dirstate.forget(f)
927 837 self.dirstate.setparents(ret)
928 838 ms.reset()
929 839
930 840 return ret
931 841
932 842 finally:
933 843 wlock.release()
934 844
935 845 def commitctx(self, ctx, error=False):
936 846 """Add a new revision to current repository.
937 847
938 848 Revision information is passed via the context argument.
939 849 """
940 850
941 851 tr = lock = None
942 852 removed = ctx.removed()
943 853 p1, p2 = ctx.p1(), ctx.p2()
944 854 m1 = p1.manifest().copy()
945 855 m2 = p2.manifest()
946 856 user = ctx.user()
947 857
948 858 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
949 859 self.hook("precommit", throw=True, parent1=xp1, parent2=xp2)
950 860
951 861 lock = self.lock()
952 862 try:
953 863 tr = self.transaction()
954 864 trp = weakref.proxy(tr)
955 865
956 866 # check in files
957 867 new = {}
958 868 changed = []
959 869 linkrev = len(self)
960 870 for f in sorted(ctx.modified() + ctx.added()):
961 871 self.ui.note(f + "\n")
962 872 try:
963 873 fctx = ctx[f]
964 874 new[f] = self._filecommit(fctx, m1, m2, linkrev, trp,
965 875 changed)
966 876 m1.set(f, fctx.flags())
967 877 except (OSError, IOError):
968 878 if error:
969 879 self.ui.warn(_("trouble committing %s!\n") % f)
970 880 raise
971 881 else:
972 882 removed.append(f)
973 883
974 884 # update manifest
975 885 m1.update(new)
976 886 removed = [f for f in sorted(removed) if f in m1 or f in m2]
977 887 drop = [f for f in removed if f in m1]
978 888 for f in drop:
979 889 del m1[f]
980 890 mn = self.manifest.add(m1, trp, linkrev, p1.manifestnode(),
981 891 p2.manifestnode(), (new, drop))
982 892
983 893 # update changelog
984 894 self.changelog.delayupdate()
985 895 n = self.changelog.add(mn, changed + removed, ctx.description(),
986 896 trp, p1.node(), p2.node(),
987 897 user, ctx.date(), ctx.extra().copy())
988 898 p = lambda: self.changelog.writepending() and self.root or ""
989 899 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
990 900 parent2=xp2, pending=p)
991 901 self.changelog.finalize(trp)
992 902 tr.close()
993 903
994 904 if self.branchcache:
995 905 self.branchtags()
996 906
997 907 self.hook("commit", node=hex(n), parent1=xp1, parent2=xp2)
998 908 return n
999 909 finally:
1000 910 del tr
1001 911 lock.release()
1002 912
1003 913 def walk(self, match, node=None):
1004 914 '''
1005 915 walk recursively through the directory tree or a given
1006 916 changeset, finding all files matched by the match
1007 917 function
1008 918 '''
1009 919 return self[node].walk(match)
1010 920
1011 921 def status(self, node1='.', node2=None, match=None,
1012 922 ignored=False, clean=False, unknown=False):
1013 923 """return status of files between two nodes or node and working directory
1014 924
1015 925 If node1 is None, use the first dirstate parent instead.
1016 926 If node2 is None, compare node1 with working directory.
1017 927 """
1018 928
1019 929 def mfmatches(ctx):
1020 930 mf = ctx.manifest().copy()
1021 931 for fn in mf.keys():
1022 932 if not match(fn):
1023 933 del mf[fn]
1024 934 return mf
1025 935
1026 936 if isinstance(node1, context.changectx):
1027 937 ctx1 = node1
1028 938 else:
1029 939 ctx1 = self[node1]
1030 940 if isinstance(node2, context.changectx):
1031 941 ctx2 = node2
1032 942 else:
1033 943 ctx2 = self[node2]
1034 944
1035 945 working = ctx2.rev() is None
1036 946 parentworking = working and ctx1 == self['.']
1037 947 match = match or match_.always(self.root, self.getcwd())
1038 948 listignored, listclean, listunknown = ignored, clean, unknown
1039 949
1040 950 # load earliest manifest first for caching reasons
1041 951 if not working and ctx2.rev() < ctx1.rev():
1042 952 ctx2.manifest()
1043 953
1044 954 if not parentworking:
1045 955 def bad(f, msg):
1046 956 if f not in ctx1:
1047 957 self.ui.warn('%s: %s\n' % (self.dirstate.pathto(f), msg))
1048 958 match.bad = bad
1049 959
1050 960 if working: # we need to scan the working dir
1051 961 s = self.dirstate.status(match, listignored, listclean, listunknown)
1052 962 cmp, modified, added, removed, deleted, unknown, ignored, clean = s
1053 963
1054 964 # check for any possibly clean files
1055 965 if parentworking and cmp:
1056 966 fixup = []
1057 967 # do a full compare of any files that might have changed
1058 968 for f in sorted(cmp):
1059 969 if (f not in ctx1 or ctx2.flags(f) != ctx1.flags(f)
1060 970 or ctx1[f].cmp(ctx2[f].data())):
1061 971 modified.append(f)
1062 972 else:
1063 973 fixup.append(f)
1064 974
1065 975 if listclean:
1066 976 clean += fixup
1067 977
1068 978 # update dirstate for files that are actually clean
1069 979 if fixup:
1070 980 try:
1071 981 # updating the dirstate is optional
1072 982 # so we don't wait on the lock
1073 983 wlock = self.wlock(False)
1074 984 try:
1075 985 for f in fixup:
1076 986 self.dirstate.normal(f)
1077 987 finally:
1078 988 wlock.release()
1079 989 except error.LockError:
1080 990 pass
1081 991
1082 992 if not parentworking:
1083 993 mf1 = mfmatches(ctx1)
1084 994 if working:
1085 995 # we are comparing working dir against non-parent
1086 996 # generate a pseudo-manifest for the working dir
1087 997 mf2 = mfmatches(self['.'])
1088 998 for f in cmp + modified + added:
1089 999 mf2[f] = None
1090 1000 mf2.set(f, ctx2.flags(f))
1091 1001 for f in removed:
1092 1002 if f in mf2:
1093 1003 del mf2[f]
1094 1004 else:
1095 1005 # we are comparing two revisions
1096 1006 deleted, unknown, ignored = [], [], []
1097 1007 mf2 = mfmatches(ctx2)
1098 1008
1099 1009 modified, added, clean = [], [], []
1100 1010 for fn in mf2:
1101 1011 if fn in mf1:
1102 1012 if (mf1.flags(fn) != mf2.flags(fn) or
1103 1013 (mf1[fn] != mf2[fn] and
1104 1014 (mf2[fn] or ctx1[fn].cmp(ctx2[fn].data())))):
1105 1015 modified.append(fn)
1106 1016 elif listclean:
1107 1017 clean.append(fn)
1108 1018 del mf1[fn]
1109 1019 else:
1110 1020 added.append(fn)
1111 1021 removed = mf1.keys()
1112 1022
1113 1023 r = modified, added, removed, deleted, unknown, ignored, clean
1114 1024 [l.sort() for l in r]
1115 1025 return r
1116 1026
1117 1027 def add(self, list):
1118 1028 wlock = self.wlock()
1119 1029 try:
1120 1030 rejected = []
1121 1031 for f in list:
1122 1032 p = self.wjoin(f)
1123 1033 try:
1124 1034 st = os.lstat(p)
1125 1035 except:
1126 1036 self.ui.warn(_("%s does not exist!\n") % f)
1127 1037 rejected.append(f)
1128 1038 continue
1129 1039 if st.st_size > 10000000:
1130 1040 self.ui.warn(_("%s: files over 10MB may cause memory and"
1131 1041 " performance problems\n"
1132 1042 "(use 'hg revert %s' to unadd the file)\n")
1133 1043 % (f, f))
1134 1044 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
1135 1045 self.ui.warn(_("%s not added: only files and symlinks "
1136 1046 "supported currently\n") % f)
1137 1047 rejected.append(p)
1138 1048 elif self.dirstate[f] in 'amn':
1139 1049 self.ui.warn(_("%s already tracked!\n") % f)
1140 1050 elif self.dirstate[f] == 'r':
1141 1051 self.dirstate.normallookup(f)
1142 1052 else:
1143 1053 self.dirstate.add(f)
1144 1054 return rejected
1145 1055 finally:
1146 1056 wlock.release()
1147 1057
1148 1058 def forget(self, list):
1149 1059 wlock = self.wlock()
1150 1060 try:
1151 1061 for f in list:
1152 1062 if self.dirstate[f] != 'a':
1153 1063 self.ui.warn(_("%s not added!\n") % f)
1154 1064 else:
1155 1065 self.dirstate.forget(f)
1156 1066 finally:
1157 1067 wlock.release()
1158 1068
1159 1069 def remove(self, list, unlink=False):
1160 1070 if unlink:
1161 1071 for f in list:
1162 1072 try:
1163 1073 util.unlink(self.wjoin(f))
1164 1074 except OSError, inst:
1165 1075 if inst.errno != errno.ENOENT:
1166 1076 raise
1167 1077 wlock = self.wlock()
1168 1078 try:
1169 1079 for f in list:
1170 1080 if unlink and os.path.exists(self.wjoin(f)):
1171 1081 self.ui.warn(_("%s still exists!\n") % f)
1172 1082 elif self.dirstate[f] == 'a':
1173 1083 self.dirstate.forget(f)
1174 1084 elif f not in self.dirstate:
1175 1085 self.ui.warn(_("%s not tracked!\n") % f)
1176 1086 else:
1177 1087 self.dirstate.remove(f)
1178 1088 finally:
1179 1089 wlock.release()
1180 1090
1181 1091 def undelete(self, list):
1182 1092 manifests = [self.manifest.read(self.changelog.read(p)[0])
1183 1093 for p in self.dirstate.parents() if p != nullid]
1184 1094 wlock = self.wlock()
1185 1095 try:
1186 1096 for f in list:
1187 1097 if self.dirstate[f] != 'r':
1188 1098 self.ui.warn(_("%s not removed!\n") % f)
1189 1099 else:
1190 1100 m = f in manifests[0] and manifests[0] or manifests[1]
1191 1101 t = self.file(f).read(m[f])
1192 1102 self.wwrite(f, t, m.flags(f))
1193 1103 self.dirstate.normal(f)
1194 1104 finally:
1195 1105 wlock.release()
1196 1106
1197 1107 def copy(self, source, dest):
1198 1108 p = self.wjoin(dest)
1199 1109 if not (os.path.exists(p) or os.path.islink(p)):
1200 1110 self.ui.warn(_("%s does not exist!\n") % dest)
1201 1111 elif not (os.path.isfile(p) or os.path.islink(p)):
1202 1112 self.ui.warn(_("copy failed: %s is not a file or a "
1203 1113 "symbolic link\n") % dest)
1204 1114 else:
1205 1115 wlock = self.wlock()
1206 1116 try:
1207 1117 if self.dirstate[dest] in '?r':
1208 1118 self.dirstate.add(dest)
1209 1119 self.dirstate.copy(source, dest)
1210 1120 finally:
1211 1121 wlock.release()
1212 1122
1213 1123 def heads(self, start=None):
1214 1124 heads = self.changelog.heads(start)
1215 1125 # sort the output in rev descending order
1216 1126 heads = [(-self.changelog.rev(h), h) for h in heads]
1217 1127 return [n for (r, n) in sorted(heads)]
1218 1128
1219 1129 def branchheads(self, branch=None, start=None, closed=False):
1220 1130 if branch is None:
1221 1131 branch = self[None].branch()
1222 1132 branches = self.branchmap()
1223 1133 if branch not in branches:
1224 1134 return []
1225 1135 bheads = branches[branch]
1226 1136 # the cache returns heads ordered lowest to highest
1227 1137 bheads.reverse()
1228 1138 if start is not None:
1229 1139 # filter out the heads that cannot be reached from startrev
1230 1140 bheads = self.changelog.nodesbetween([start], bheads)[2]
1231 1141 if not closed:
1232 1142 bheads = [h for h in bheads if
1233 1143 ('close' not in self.changelog.read(h)[5])]
1234 1144 return bheads
1235 1145
1236 1146 def branches(self, nodes):
1237 1147 if not nodes:
1238 1148 nodes = [self.changelog.tip()]
1239 1149 b = []
1240 1150 for n in nodes:
1241 1151 t = n
1242 1152 while 1:
1243 1153 p = self.changelog.parents(n)
1244 1154 if p[1] != nullid or p[0] == nullid:
1245 1155 b.append((t, n, p[0], p[1]))
1246 1156 break
1247 1157 n = p[0]
1248 1158 return b
1249 1159
1250 1160 def between(self, pairs):
1251 1161 r = []
1252 1162
1253 1163 for top, bottom in pairs:
1254 1164 n, l, i = top, [], 0
1255 1165 f = 1
1256 1166
1257 1167 while n != bottom and n != nullid:
1258 1168 p = self.changelog.parents(n)[0]
1259 1169 if i == f:
1260 1170 l.append(n)
1261 1171 f = f * 2
1262 1172 n = p
1263 1173 i += 1
1264 1174
1265 1175 r.append(l)
1266 1176
1267 1177 return r
1268 1178
1269 1179 def findincoming(self, remote, base=None, heads=None, force=False):
1270 1180 """Return list of roots of the subsets of missing nodes from remote
1271 1181
1272 1182 If base dict is specified, assume that these nodes and their parents
1273 1183 exist on the remote side and that no child of a node of base exists
1274 1184 in both remote and self.
1275 1185 Furthermore base will be updated to include the nodes that exists
1276 1186 in self and remote but no children exists in self and remote.
1277 1187 If a list of heads is specified, return only nodes which are heads
1278 1188 or ancestors of these heads.
1279 1189
1280 1190 All the ancestors of base are in self and in remote.
1281 1191 All the descendants of the list returned are missing in self.
1282 1192 (and so we know that the rest of the nodes are missing in remote, see
1283 1193 outgoing)
1284 1194 """
1285 1195 return self.findcommonincoming(remote, base, heads, force)[1]
1286 1196
1287 1197 def findcommonincoming(self, remote, base=None, heads=None, force=False):
1288 1198 """Return a tuple (common, missing roots, heads) used to identify
1289 1199 missing nodes from remote.
1290 1200
1291 1201 If base dict is specified, assume that these nodes and their parents
1292 1202 exist on the remote side and that no child of a node of base exists
1293 1203 in both remote and self.
1294 1204 Furthermore base will be updated to include the nodes that exists
1295 1205 in self and remote but no children exists in self and remote.
1296 1206 If a list of heads is specified, return only nodes which are heads
1297 1207 or ancestors of these heads.
1298 1208
1299 1209 All the ancestors of base are in self and in remote.
1300 1210 """
1301 1211 m = self.changelog.nodemap
1302 1212 search = []
1303 1213 fetch = set()
1304 1214 seen = set()
1305 1215 seenbranch = set()
1306 1216 if base is None:
1307 1217 base = {}
1308 1218
1309 1219 if not heads:
1310 1220 heads = remote.heads()
1311 1221
1312 1222 if self.changelog.tip() == nullid:
1313 1223 base[nullid] = 1
1314 1224 if heads != [nullid]:
1315 1225 return [nullid], [nullid], list(heads)
1316 1226 return [nullid], [], []
1317 1227
1318 1228 # assume we're closer to the tip than the root
1319 1229 # and start by examining the heads
1320 1230 self.ui.status(_("searching for changes\n"))
1321 1231
1322 1232 unknown = []
1323 1233 for h in heads:
1324 1234 if h not in m:
1325 1235 unknown.append(h)
1326 1236 else:
1327 1237 base[h] = 1
1328 1238
1329 1239 heads = unknown
1330 1240 if not unknown:
1331 1241 return base.keys(), [], []
1332 1242
1333 1243 req = set(unknown)
1334 1244 reqcnt = 0
1335 1245
1336 1246 # search through remote branches
1337 1247 # a 'branch' here is a linear segment of history, with four parts:
1338 1248 # head, root, first parent, second parent
1339 1249 # (a branch always has two parents (or none) by definition)
1340 1250 unknown = remote.branches(unknown)
1341 1251 while unknown:
1342 1252 r = []
1343 1253 while unknown:
1344 1254 n = unknown.pop(0)
1345 1255 if n[0] in seen:
1346 1256 continue
1347 1257
1348 1258 self.ui.debug(_("examining %s:%s\n")
1349 1259 % (short(n[0]), short(n[1])))
1350 1260 if n[0] == nullid: # found the end of the branch
1351 1261 pass
1352 1262 elif n in seenbranch:
1353 1263 self.ui.debug(_("branch already found\n"))
1354 1264 continue
1355 1265 elif n[1] and n[1] in m: # do we know the base?
1356 1266 self.ui.debug(_("found incomplete branch %s:%s\n")
1357 1267 % (short(n[0]), short(n[1])))
1358 1268 search.append(n[0:2]) # schedule branch range for scanning
1359 1269 seenbranch.add(n)
1360 1270 else:
1361 1271 if n[1] not in seen and n[1] not in fetch:
1362 1272 if n[2] in m and n[3] in m:
1363 1273 self.ui.debug(_("found new changeset %s\n") %
1364 1274 short(n[1]))
1365 1275 fetch.add(n[1]) # earliest unknown
1366 1276 for p in n[2:4]:
1367 1277 if p in m:
1368 1278 base[p] = 1 # latest known
1369 1279
1370 1280 for p in n[2:4]:
1371 1281 if p not in req and p not in m:
1372 1282 r.append(p)
1373 1283 req.add(p)
1374 1284 seen.add(n[0])
1375 1285
1376 1286 if r:
1377 1287 reqcnt += 1
1378 1288 self.ui.debug(_("request %d: %s\n") %
1379 1289 (reqcnt, " ".join(map(short, r))))
1380 1290 for p in xrange(0, len(r), 10):
1381 1291 for b in remote.branches(r[p:p+10]):
1382 1292 self.ui.debug(_("received %s:%s\n") %
1383 1293 (short(b[0]), short(b[1])))
1384 1294 unknown.append(b)
1385 1295
1386 1296 # do binary search on the branches we found
1387 1297 while search:
1388 1298 newsearch = []
1389 1299 reqcnt += 1
1390 1300 for n, l in zip(search, remote.between(search)):
1391 1301 l.append(n[1])
1392 1302 p = n[0]
1393 1303 f = 1
1394 1304 for i in l:
1395 1305 self.ui.debug(_("narrowing %d:%d %s\n") % (f, len(l), short(i)))
1396 1306 if i in m:
1397 1307 if f <= 2:
1398 1308 self.ui.debug(_("found new branch changeset %s\n") %
1399 1309 short(p))
1400 1310 fetch.add(p)
1401 1311 base[i] = 1
1402 1312 else:
1403 1313 self.ui.debug(_("narrowed branch search to %s:%s\n")
1404 1314 % (short(p), short(i)))
1405 1315 newsearch.append((p, i))
1406 1316 break
1407 1317 p, f = i, f * 2
1408 1318 search = newsearch
1409 1319
1410 1320 # sanity check our fetch list
1411 1321 for f in fetch:
1412 1322 if f in m:
1413 1323 raise error.RepoError(_("already have changeset ")
1414 1324 + short(f[:4]))
1415 1325
1416 1326 if base.keys() == [nullid]:
1417 1327 if force:
1418 1328 self.ui.warn(_("warning: repository is unrelated\n"))
1419 1329 else:
1420 1330 raise util.Abort(_("repository is unrelated"))
1421 1331
1422 1332 self.ui.debug(_("found new changesets starting at ") +
1423 1333 " ".join([short(f) for f in fetch]) + "\n")
1424 1334
1425 1335 self.ui.debug(_("%d total queries\n") % reqcnt)
1426 1336
1427 1337 return base.keys(), list(fetch), heads
1428 1338
1429 1339 def findoutgoing(self, remote, base=None, heads=None, force=False):
1430 1340 """Return list of nodes that are roots of subsets not in remote
1431 1341
1432 1342 If base dict is specified, assume that these nodes and their parents
1433 1343 exist on the remote side.
1434 1344 If a list of heads is specified, return only nodes which are heads
1435 1345 or ancestors of these heads, and return a second element which
1436 1346 contains all remote heads which get new children.
1437 1347 """
1438 1348 if base is None:
1439 1349 base = {}
1440 1350 self.findincoming(remote, base, heads, force=force)
1441 1351
1442 1352 self.ui.debug(_("common changesets up to ")
1443 1353 + " ".join(map(short, base.keys())) + "\n")
1444 1354
1445 1355 remain = set(self.changelog.nodemap)
1446 1356
1447 1357 # prune everything remote has from the tree
1448 1358 remain.remove(nullid)
1449 1359 remove = base.keys()
1450 1360 while remove:
1451 1361 n = remove.pop(0)
1452 1362 if n in remain:
1453 1363 remain.remove(n)
1454 1364 for p in self.changelog.parents(n):
1455 1365 remove.append(p)
1456 1366
1457 1367 # find every node whose parents have been pruned
1458 1368 subset = []
1459 1369 # find every remote head that will get new children
1460 1370 updated_heads = set()
1461 1371 for n in remain:
1462 1372 p1, p2 = self.changelog.parents(n)
1463 1373 if p1 not in remain and p2 not in remain:
1464 1374 subset.append(n)
1465 1375 if heads:
1466 1376 if p1 in heads:
1467 1377 updated_heads.add(p1)
1468 1378 if p2 in heads:
1469 1379 updated_heads.add(p2)
1470 1380
1471 1381 # this is the set of all roots we have to push
1472 1382 if heads:
1473 1383 return subset, list(updated_heads)
1474 1384 else:
1475 1385 return subset
1476 1386
1477 1387 def pull(self, remote, heads=None, force=False):
1478 1388 lock = self.lock()
1479 1389 try:
1480 1390 common, fetch, rheads = self.findcommonincoming(remote, heads=heads,
1481 1391 force=force)
1482 1392 if fetch == [nullid]:
1483 1393 self.ui.status(_("requesting all changes\n"))
1484 1394
1485 1395 if not fetch:
1486 1396 self.ui.status(_("no changes found\n"))
1487 1397 return 0
1488 1398
1489 1399 if heads is None and remote.capable('changegroupsubset'):
1490 1400 heads = rheads
1491 1401
1492 1402 if heads is None:
1493 1403 cg = remote.changegroup(fetch, 'pull')
1494 1404 else:
1495 1405 if not remote.capable('changegroupsubset'):
1496 1406 raise util.Abort(_("Partial pull cannot be done because "
1497 1407 "other repository doesn't support "
1498 1408 "changegroupsubset."))
1499 1409 cg = remote.changegroupsubset(fetch, heads, 'pull')
1500 1410 return self.addchangegroup(cg, 'pull', remote.url())
1501 1411 finally:
1502 1412 lock.release()
1503 1413
1504 1414 def push(self, remote, force=False, revs=None):
1505 1415 # there are two ways to push to remote repo:
1506 1416 #
1507 1417 # addchangegroup assumes local user can lock remote
1508 1418 # repo (local filesystem, old ssh servers).
1509 1419 #
1510 1420 # unbundle assumes local user cannot lock remote repo (new ssh
1511 1421 # servers, http servers).
1512 1422
1513 1423 if remote.capable('unbundle'):
1514 1424 return self.push_unbundle(remote, force, revs)
1515 1425 return self.push_addchangegroup(remote, force, revs)
1516 1426
1517 1427 def prepush(self, remote, force, revs):
1518 1428 common = {}
1519 1429 remote_heads = remote.heads()
1520 1430 inc = self.findincoming(remote, common, remote_heads, force=force)
1521 1431
1522 1432 update, updated_heads = self.findoutgoing(remote, common, remote_heads)
1523 1433 if revs is not None:
1524 1434 msng_cl, bases, heads = self.changelog.nodesbetween(update, revs)
1525 1435 else:
1526 1436 bases, heads = update, self.changelog.heads()
1527 1437
1528 1438 def checkbranch(lheads, rheads, updatelh):
1529 1439 '''
1530 1440 check whether there are more local heads than remote heads on
1531 1441 a specific branch.
1532 1442
1533 1443 lheads: local branch heads
1534 1444 rheads: remote branch heads
1535 1445 updatelh: outgoing local branch heads
1536 1446 '''
1537 1447
1538 1448 warn = 0
1539 1449
1540 1450 if not revs and len(lheads) > len(rheads):
1541 1451 warn = 1
1542 1452 else:
1543 1453 updatelheads = [self.changelog.heads(x, lheads)
1544 1454 for x in updatelh]
1545 1455 newheads = set(sum(updatelheads, [])) & set(lheads)
1546 1456
1547 1457 if not newheads:
1548 1458 return True
1549 1459
1550 1460 for r in rheads:
1551 1461 if r in self.changelog.nodemap:
1552 1462 desc = self.changelog.heads(r, heads)
1553 1463 l = [h for h in heads if h in desc]
1554 1464 if not l:
1555 1465 newheads.add(r)
1556 1466 else:
1557 1467 newheads.add(r)
1558 1468 if len(newheads) > len(rheads):
1559 1469 warn = 1
1560 1470
1561 1471 if warn:
1562 1472 if not rheads: # new branch requires --force
1563 1473 self.ui.warn(_("abort: push creates new"
1564 1474 " remote branch '%s'!\n") %
1565 1475 self[updatelh[0]].branch())
1566 1476 else:
1567 1477 self.ui.warn(_("abort: push creates new remote heads!\n"))
1568 1478
1569 1479 self.ui.status(_("(did you forget to merge?"
1570 1480 " use push -f to force)\n"))
1571 1481 return False
1572 1482 return True
1573 1483
1574 1484 if not bases:
1575 1485 self.ui.status(_("no changes found\n"))
1576 1486 return None, 1
1577 1487 elif not force:
1578 1488 # Check for each named branch if we're creating new remote heads.
1579 1489 # To be a remote head after push, node must be either:
1580 1490 # - unknown locally
1581 1491 # - a local outgoing head descended from update
1582 1492 # - a remote head that's known locally and not
1583 1493 # ancestral to an outgoing head
1584 1494 #
1585 1495 # New named branches cannot be created without --force.
1586 1496
1587 1497 if remote_heads != [nullid]:
1588 1498 if remote.capable('branchmap'):
1589 1499 localhds = {}
1590 1500 if not revs:
1591 1501 localhds = self.branchmap()
1592 1502 else:
1593 1503 for n in heads:
1594 1504 branch = self[n].branch()
1595 1505 if branch in localhds:
1596 1506 localhds[branch].append(n)
1597 1507 else:
1598 1508 localhds[branch] = [n]
1599 1509
1600 1510 remotehds = remote.branchmap()
1601 1511
1602 1512 for lh in localhds:
1603 1513 if lh in remotehds:
1604 1514 rheads = remotehds[lh]
1605 1515 else:
1606 1516 rheads = []
1607 1517 lheads = localhds[lh]
1608 1518 updatelh = [upd for upd in update
1609 1519 if self[upd].branch() == lh]
1610 1520 if not updatelh:
1611 1521 continue
1612 1522 if not checkbranch(lheads, rheads, updatelh):
1613 1523 return None, 0
1614 1524 else:
1615 1525 if not checkbranch(heads, remote_heads, update):
1616 1526 return None, 0
1617 1527
1618 1528 if inc:
1619 1529 self.ui.warn(_("note: unsynced remote changes!\n"))
1620 1530
1621 1531
1622 1532 if revs is None:
1623 1533 # use the fast path, no race possible on push
1624 1534 cg = self._changegroup(common.keys(), 'push')
1625 1535 else:
1626 1536 cg = self.changegroupsubset(update, revs, 'push')
1627 1537 return cg, remote_heads
1628 1538
1629 1539 def push_addchangegroup(self, remote, force, revs):
1630 1540 lock = remote.lock()
1631 1541 try:
1632 1542 ret = self.prepush(remote, force, revs)
1633 1543 if ret[0] is not None:
1634 1544 cg, remote_heads = ret
1635 1545 return remote.addchangegroup(cg, 'push', self.url())
1636 1546 return ret[1]
1637 1547 finally:
1638 1548 lock.release()
1639 1549
1640 1550 def push_unbundle(self, remote, force, revs):
1641 1551 # local repo finds heads on server, finds out what revs it
1642 1552 # must push. once revs transferred, if server finds it has
1643 1553 # different heads (someone else won commit/push race), server
1644 1554 # aborts.
1645 1555
1646 1556 ret = self.prepush(remote, force, revs)
1647 1557 if ret[0] is not None:
1648 1558 cg, remote_heads = ret
1649 1559 if force: remote_heads = ['force']
1650 1560 return remote.unbundle(cg, remote_heads, 'push')
1651 1561 return ret[1]
1652 1562
1653 1563 def changegroupinfo(self, nodes, source):
1654 1564 if self.ui.verbose or source == 'bundle':
1655 1565 self.ui.status(_("%d changesets found\n") % len(nodes))
1656 1566 if self.ui.debugflag:
1657 1567 self.ui.debug(_("list of changesets:\n"))
1658 1568 for node in nodes:
1659 1569 self.ui.debug("%s\n" % hex(node))
1660 1570
1661 1571 def changegroupsubset(self, bases, heads, source, extranodes=None):
1662 1572 """This function generates a changegroup consisting of all the nodes
1663 1573 that are descendents of any of the bases, and ancestors of any of
1664 1574 the heads.
1665 1575
1666 1576 It is fairly complex as determining which filenodes and which
1667 1577 manifest nodes need to be included for the changeset to be complete
1668 1578 is non-trivial.
1669 1579
1670 1580 Another wrinkle is doing the reverse, figuring out which changeset in
1671 1581 the changegroup a particular filenode or manifestnode belongs to.
1672 1582
1673 1583 The caller can specify some nodes that must be included in the
1674 1584 changegroup using the extranodes argument. It should be a dict
1675 1585 where the keys are the filenames (or 1 for the manifest), and the
1676 1586 values are lists of (node, linknode) tuples, where node is a wanted
1677 1587 node and linknode is the changelog node that should be transmitted as
1678 1588 the linkrev.
1679 1589 """
1680 1590
1681 1591 if extranodes is None:
1682 1592 # can we go through the fast path ?
1683 1593 heads.sort()
1684 1594 allheads = self.heads()
1685 1595 allheads.sort()
1686 1596 if heads == allheads:
1687 1597 common = []
1688 1598 # parents of bases are known from both sides
1689 1599 for n in bases:
1690 1600 for p in self.changelog.parents(n):
1691 1601 if p != nullid:
1692 1602 common.append(p)
1693 1603 return self._changegroup(common, source)
1694 1604
1695 1605 self.hook('preoutgoing', throw=True, source=source)
1696 1606
1697 1607 # Set up some initial variables
1698 1608 # Make it easy to refer to self.changelog
1699 1609 cl = self.changelog
1700 1610 # msng is short for missing - compute the list of changesets in this
1701 1611 # changegroup.
1702 1612 msng_cl_lst, bases, heads = cl.nodesbetween(bases, heads)
1703 1613 self.changegroupinfo(msng_cl_lst, source)
1704 1614 # Some bases may turn out to be superfluous, and some heads may be
1705 1615 # too. nodesbetween will return the minimal set of bases and heads
1706 1616 # necessary to re-create the changegroup.
1707 1617
1708 1618 # Known heads are the list of heads that it is assumed the recipient
1709 1619 # of this changegroup will know about.
1710 1620 knownheads = set()
1711 1621 # We assume that all parents of bases are known heads.
1712 1622 for n in bases:
1713 1623 knownheads.update(cl.parents(n))
1714 1624 knownheads.discard(nullid)
1715 1625 knownheads = list(knownheads)
1716 1626 if knownheads:
1717 1627 # Now that we know what heads are known, we can compute which
1718 1628 # changesets are known. The recipient must know about all
1719 1629 # changesets required to reach the known heads from the null
1720 1630 # changeset.
1721 1631 has_cl_set, junk, junk = cl.nodesbetween(None, knownheads)
1722 1632 junk = None
1723 1633 # Transform the list into a set.
1724 1634 has_cl_set = set(has_cl_set)
1725 1635 else:
1726 1636 # If there were no known heads, the recipient cannot be assumed to
1727 1637 # know about any changesets.
1728 1638 has_cl_set = set()
1729 1639
1730 1640 # Make it easy to refer to self.manifest
1731 1641 mnfst = self.manifest
1732 1642 # We don't know which manifests are missing yet
1733 1643 msng_mnfst_set = {}
1734 1644 # Nor do we know which filenodes are missing.
1735 1645 msng_filenode_set = {}
1736 1646
1737 1647 junk = mnfst.index[len(mnfst) - 1] # Get around a bug in lazyindex
1738 1648 junk = None
1739 1649
1740 1650 # A changeset always belongs to itself, so the changenode lookup
1741 1651 # function for a changenode is identity.
1742 1652 def identity(x):
1743 1653 return x
1744 1654
1745 1655 # If we determine that a particular file or manifest node must be a
1746 1656 # node that the recipient of the changegroup will already have, we can
1747 1657 # also assume the recipient will have all the parents. This function
1748 1658 # prunes them from the set of missing nodes.
1749 1659 def prune_parents(revlog, hasset, msngset):
1750 1660 haslst = list(hasset)
1751 1661 haslst.sort(key=revlog.rev)
1752 1662 for node in haslst:
1753 1663 parentlst = [p for p in revlog.parents(node) if p != nullid]
1754 1664 while parentlst:
1755 1665 n = parentlst.pop()
1756 1666 if n not in hasset:
1757 1667 hasset.add(n)
1758 1668 p = [p for p in revlog.parents(n) if p != nullid]
1759 1669 parentlst.extend(p)
1760 1670 for n in hasset:
1761 1671 msngset.pop(n, None)
1762 1672
1763 1673 # This is a function generating function used to set up an environment
1764 1674 # for the inner function to execute in.
1765 1675 def manifest_and_file_collector(changedfileset):
1766 1676 # This is an information gathering function that gathers
1767 1677 # information from each changeset node that goes out as part of
1768 1678 # the changegroup. The information gathered is a list of which
1769 1679 # manifest nodes are potentially required (the recipient may
1770 1680 # already have them) and total list of all files which were
1771 1681 # changed in any changeset in the changegroup.
1772 1682 #
1773 1683 # We also remember the first changenode we saw any manifest
1774 1684 # referenced by so we can later determine which changenode 'owns'
1775 1685 # the manifest.
1776 1686 def collect_manifests_and_files(clnode):
1777 1687 c = cl.read(clnode)
1778 1688 for f in c[3]:
1779 1689 # This is to make sure we only have one instance of each
1780 1690 # filename string for each filename.
1781 1691 changedfileset.setdefault(f, f)
1782 1692 msng_mnfst_set.setdefault(c[0], clnode)
1783 1693 return collect_manifests_and_files
1784 1694
1785 1695 # Figure out which manifest nodes (of the ones we think might be part
1786 1696 # of the changegroup) the recipient must know about and remove them
1787 1697 # from the changegroup.
1788 1698 def prune_manifests():
1789 1699 has_mnfst_set = set()
1790 1700 for n in msng_mnfst_set:
1791 1701 # If a 'missing' manifest thinks it belongs to a changenode
1792 1702 # the recipient is assumed to have, obviously the recipient
1793 1703 # must have that manifest.
1794 1704 linknode = cl.node(mnfst.linkrev(mnfst.rev(n)))
1795 1705 if linknode in has_cl_set:
1796 1706 has_mnfst_set.add(n)
1797 1707 prune_parents(mnfst, has_mnfst_set, msng_mnfst_set)
1798 1708
1799 1709 # Use the information collected in collect_manifests_and_files to say
1800 1710 # which changenode any manifestnode belongs to.
1801 1711 def lookup_manifest_link(mnfstnode):
1802 1712 return msng_mnfst_set[mnfstnode]
1803 1713
1804 1714 # A function generating function that sets up the initial environment
1805 1715 # the inner function.
1806 1716 def filenode_collector(changedfiles):
1807 1717 next_rev = [0]
1808 1718 # This gathers information from each manifestnode included in the
1809 1719 # changegroup about which filenodes the manifest node references
1810 1720 # so we can include those in the changegroup too.
1811 1721 #
1812 1722 # It also remembers which changenode each filenode belongs to. It
1813 1723 # does this by assuming the a filenode belongs to the changenode
1814 1724 # the first manifest that references it belongs to.
1815 1725 def collect_msng_filenodes(mnfstnode):
1816 1726 r = mnfst.rev(mnfstnode)
1817 1727 if r == next_rev[0]:
1818 1728 # If the last rev we looked at was the one just previous,
1819 1729 # we only need to see a diff.
1820 1730 deltamf = mnfst.readdelta(mnfstnode)
1821 1731 # For each line in the delta
1822 1732 for f, fnode in deltamf.iteritems():
1823 1733 f = changedfiles.get(f, None)
1824 1734 # And if the file is in the list of files we care
1825 1735 # about.
1826 1736 if f is not None:
1827 1737 # Get the changenode this manifest belongs to
1828 1738 clnode = msng_mnfst_set[mnfstnode]
1829 1739 # Create the set of filenodes for the file if
1830 1740 # there isn't one already.
1831 1741 ndset = msng_filenode_set.setdefault(f, {})
1832 1742 # And set the filenode's changelog node to the
1833 1743 # manifest's if it hasn't been set already.
1834 1744 ndset.setdefault(fnode, clnode)
1835 1745 else:
1836 1746 # Otherwise we need a full manifest.
1837 1747 m = mnfst.read(mnfstnode)
1838 1748 # For every file in we care about.
1839 1749 for f in changedfiles:
1840 1750 fnode = m.get(f, None)
1841 1751 # If it's in the manifest
1842 1752 if fnode is not None:
1843 1753 # See comments above.
1844 1754 clnode = msng_mnfst_set[mnfstnode]
1845 1755 ndset = msng_filenode_set.setdefault(f, {})
1846 1756 ndset.setdefault(fnode, clnode)
1847 1757 # Remember the revision we hope to see next.
1848 1758 next_rev[0] = r + 1
1849 1759 return collect_msng_filenodes
1850 1760
1851 1761 # We have a list of filenodes we think we need for a file, lets remove
1852 1762 # all those we know the recipient must have.
1853 1763 def prune_filenodes(f, filerevlog):
1854 1764 msngset = msng_filenode_set[f]
1855 1765 hasset = set()
1856 1766 # If a 'missing' filenode thinks it belongs to a changenode we
1857 1767 # assume the recipient must have, then the recipient must have
1858 1768 # that filenode.
1859 1769 for n in msngset:
1860 1770 clnode = cl.node(filerevlog.linkrev(filerevlog.rev(n)))
1861 1771 if clnode in has_cl_set:
1862 1772 hasset.add(n)
1863 1773 prune_parents(filerevlog, hasset, msngset)
1864 1774
1865 1775 # A function generator function that sets up the a context for the
1866 1776 # inner function.
1867 1777 def lookup_filenode_link_func(fname):
1868 1778 msngset = msng_filenode_set[fname]
1869 1779 # Lookup the changenode the filenode belongs to.
1870 1780 def lookup_filenode_link(fnode):
1871 1781 return msngset[fnode]
1872 1782 return lookup_filenode_link
1873 1783
1874 1784 # Add the nodes that were explicitly requested.
1875 1785 def add_extra_nodes(name, nodes):
1876 1786 if not extranodes or name not in extranodes:
1877 1787 return
1878 1788
1879 1789 for node, linknode in extranodes[name]:
1880 1790 if node not in nodes:
1881 1791 nodes[node] = linknode
1882 1792
1883 1793 # Now that we have all theses utility functions to help out and
1884 1794 # logically divide up the task, generate the group.
1885 1795 def gengroup():
1886 1796 # The set of changed files starts empty.
1887 1797 changedfiles = {}
1888 1798 # Create a changenode group generator that will call our functions
1889 1799 # back to lookup the owning changenode and collect information.
1890 1800 group = cl.group(msng_cl_lst, identity,
1891 1801 manifest_and_file_collector(changedfiles))
1892 1802 for chnk in group:
1893 1803 yield chnk
1894 1804
1895 1805 # The list of manifests has been collected by the generator
1896 1806 # calling our functions back.
1897 1807 prune_manifests()
1898 1808 add_extra_nodes(1, msng_mnfst_set)
1899 1809 msng_mnfst_lst = msng_mnfst_set.keys()
1900 1810 # Sort the manifestnodes by revision number.
1901 1811 msng_mnfst_lst.sort(key=mnfst.rev)
1902 1812 # Create a generator for the manifestnodes that calls our lookup
1903 1813 # and data collection functions back.
1904 1814 group = mnfst.group(msng_mnfst_lst, lookup_manifest_link,
1905 1815 filenode_collector(changedfiles))
1906 1816 for chnk in group:
1907 1817 yield chnk
1908 1818
1909 1819 # These are no longer needed, dereference and toss the memory for
1910 1820 # them.
1911 1821 msng_mnfst_lst = None
1912 1822 msng_mnfst_set.clear()
1913 1823
1914 1824 if extranodes:
1915 1825 for fname in extranodes:
1916 1826 if isinstance(fname, int):
1917 1827 continue
1918 1828 msng_filenode_set.setdefault(fname, {})
1919 1829 changedfiles[fname] = 1
1920 1830 # Go through all our files in order sorted by name.
1921 1831 for fname in sorted(changedfiles):
1922 1832 filerevlog = self.file(fname)
1923 1833 if not len(filerevlog):
1924 1834 raise util.Abort(_("empty or missing revlog for %s") % fname)
1925 1835 # Toss out the filenodes that the recipient isn't really
1926 1836 # missing.
1927 1837 if fname in msng_filenode_set:
1928 1838 prune_filenodes(fname, filerevlog)
1929 1839 add_extra_nodes(fname, msng_filenode_set[fname])
1930 1840 msng_filenode_lst = msng_filenode_set[fname].keys()
1931 1841 else:
1932 1842 msng_filenode_lst = []
1933 1843 # If any filenodes are left, generate the group for them,
1934 1844 # otherwise don't bother.
1935 1845 if len(msng_filenode_lst) > 0:
1936 1846 yield changegroup.chunkheader(len(fname))
1937 1847 yield fname
1938 1848 # Sort the filenodes by their revision #
1939 1849 msng_filenode_lst.sort(key=filerevlog.rev)
1940 1850 # Create a group generator and only pass in a changenode
1941 1851 # lookup function as we need to collect no information
1942 1852 # from filenodes.
1943 1853 group = filerevlog.group(msng_filenode_lst,
1944 1854 lookup_filenode_link_func(fname))
1945 1855 for chnk in group:
1946 1856 yield chnk
1947 1857 if fname in msng_filenode_set:
1948 1858 # Don't need this anymore, toss it to free memory.
1949 1859 del msng_filenode_set[fname]
1950 1860 # Signal that no more groups are left.
1951 1861 yield changegroup.closechunk()
1952 1862
1953 1863 if msng_cl_lst:
1954 1864 self.hook('outgoing', node=hex(msng_cl_lst[0]), source=source)
1955 1865
1956 1866 return util.chunkbuffer(gengroup())
1957 1867
1958 1868 def changegroup(self, basenodes, source):
1959 1869 # to avoid a race we use changegroupsubset() (issue1320)
1960 1870 return self.changegroupsubset(basenodes, self.heads(), source)
1961 1871
1962 1872 def _changegroup(self, common, source):
1963 1873 """Generate a changegroup of all nodes that we have that a recipient
1964 1874 doesn't.
1965 1875
1966 1876 This is much easier than the previous function as we can assume that
1967 1877 the recipient has any changenode we aren't sending them.
1968 1878
1969 1879 common is the set of common nodes between remote and self"""
1970 1880
1971 1881 self.hook('preoutgoing', throw=True, source=source)
1972 1882
1973 1883 cl = self.changelog
1974 1884 nodes = cl.findmissing(common)
1975 1885 revset = set([cl.rev(n) for n in nodes])
1976 1886 self.changegroupinfo(nodes, source)
1977 1887
1978 1888 def identity(x):
1979 1889 return x
1980 1890
1981 1891 def gennodelst(log):
1982 1892 for r in log:
1983 1893 if log.linkrev(r) in revset:
1984 1894 yield log.node(r)
1985 1895
1986 1896 def changed_file_collector(changedfileset):
1987 1897 def collect_changed_files(clnode):
1988 1898 c = cl.read(clnode)
1989 1899 changedfileset.update(c[3])
1990 1900 return collect_changed_files
1991 1901
1992 1902 def lookuprevlink_func(revlog):
1993 1903 def lookuprevlink(n):
1994 1904 return cl.node(revlog.linkrev(revlog.rev(n)))
1995 1905 return lookuprevlink
1996 1906
1997 1907 def gengroup():
1998 1908 # construct a list of all changed files
1999 1909 changedfiles = set()
2000 1910
2001 1911 for chnk in cl.group(nodes, identity,
2002 1912 changed_file_collector(changedfiles)):
2003 1913 yield chnk
2004 1914
2005 1915 mnfst = self.manifest
2006 1916 nodeiter = gennodelst(mnfst)
2007 1917 for chnk in mnfst.group(nodeiter, lookuprevlink_func(mnfst)):
2008 1918 yield chnk
2009 1919
2010 1920 for fname in sorted(changedfiles):
2011 1921 filerevlog = self.file(fname)
2012 1922 if not len(filerevlog):
2013 1923 raise util.Abort(_("empty or missing revlog for %s") % fname)
2014 1924 nodeiter = gennodelst(filerevlog)
2015 1925 nodeiter = list(nodeiter)
2016 1926 if nodeiter:
2017 1927 yield changegroup.chunkheader(len(fname))
2018 1928 yield fname
2019 1929 lookup = lookuprevlink_func(filerevlog)
2020 1930 for chnk in filerevlog.group(nodeiter, lookup):
2021 1931 yield chnk
2022 1932
2023 1933 yield changegroup.closechunk()
2024 1934
2025 1935 if nodes:
2026 1936 self.hook('outgoing', node=hex(nodes[0]), source=source)
2027 1937
2028 1938 return util.chunkbuffer(gengroup())
2029 1939
2030 1940 def addchangegroup(self, source, srctype, url, emptyok=False):
2031 1941 """add changegroup to repo.
2032 1942
2033 1943 return values:
2034 1944 - nothing changed or no source: 0
2035 1945 - more heads than before: 1+added heads (2..n)
2036 1946 - less heads than before: -1-removed heads (-2..-n)
2037 1947 - number of heads stays the same: 1
2038 1948 """
2039 1949 def csmap(x):
2040 1950 self.ui.debug(_("add changeset %s\n") % short(x))
2041 1951 return len(cl)
2042 1952
2043 1953 def revmap(x):
2044 1954 return cl.rev(x)
2045 1955
2046 1956 if not source:
2047 1957 return 0
2048 1958
2049 1959 self.hook('prechangegroup', throw=True, source=srctype, url=url)
2050 1960
2051 1961 changesets = files = revisions = 0
2052 1962
2053 1963 # write changelog data to temp files so concurrent readers will not see
2054 1964 # inconsistent view
2055 1965 cl = self.changelog
2056 1966 cl.delayupdate()
2057 1967 oldheads = len(cl.heads())
2058 1968
2059 1969 tr = self.transaction()
2060 1970 try:
2061 1971 trp = weakref.proxy(tr)
2062 1972 # pull off the changeset group
2063 1973 self.ui.status(_("adding changesets\n"))
2064 1974 clstart = len(cl)
2065 1975 chunkiter = changegroup.chunkiter(source)
2066 1976 if cl.addgroup(chunkiter, csmap, trp) is None and not emptyok:
2067 1977 raise util.Abort(_("received changelog group is empty"))
2068 1978 clend = len(cl)
2069 1979 changesets = clend - clstart
2070 1980
2071 1981 # pull off the manifest group
2072 1982 self.ui.status(_("adding manifests\n"))
2073 1983 chunkiter = changegroup.chunkiter(source)
2074 1984 # no need to check for empty manifest group here:
2075 1985 # if the result of the merge of 1 and 2 is the same in 3 and 4,
2076 1986 # no new manifest will be created and the manifest group will
2077 1987 # be empty during the pull
2078 1988 self.manifest.addgroup(chunkiter, revmap, trp)
2079 1989
2080 1990 # process the files
2081 1991 self.ui.status(_("adding file changes\n"))
2082 1992 while 1:
2083 1993 f = changegroup.getchunk(source)
2084 1994 if not f:
2085 1995 break
2086 1996 self.ui.debug(_("adding %s revisions\n") % f)
2087 1997 fl = self.file(f)
2088 1998 o = len(fl)
2089 1999 chunkiter = changegroup.chunkiter(source)
2090 2000 if fl.addgroup(chunkiter, revmap, trp) is None:
2091 2001 raise util.Abort(_("received file revlog group is empty"))
2092 2002 revisions += len(fl) - o
2093 2003 files += 1
2094 2004
2095 2005 newheads = len(cl.heads())
2096 2006 heads = ""
2097 2007 if oldheads and newheads != oldheads:
2098 2008 heads = _(" (%+d heads)") % (newheads - oldheads)
2099 2009
2100 2010 self.ui.status(_("added %d changesets"
2101 2011 " with %d changes to %d files%s\n")
2102 2012 % (changesets, revisions, files, heads))
2103 2013
2104 2014 if changesets > 0:
2105 2015 p = lambda: cl.writepending() and self.root or ""
2106 2016 self.hook('pretxnchangegroup', throw=True,
2107 2017 node=hex(cl.node(clstart)), source=srctype,
2108 2018 url=url, pending=p)
2109 2019
2110 2020 # make changelog see real files again
2111 2021 cl.finalize(trp)
2112 2022
2113 2023 tr.close()
2114 2024 finally:
2115 2025 del tr
2116 2026
2117 2027 if changesets > 0:
2118 2028 # forcefully update the on-disk branch cache
2119 2029 self.ui.debug(_("updating the branch cache\n"))
2120 2030 self.branchtags()
2121 2031 self.hook("changegroup", node=hex(cl.node(clstart)),
2122 2032 source=srctype, url=url)
2123 2033
2124 2034 for i in xrange(clstart, clend):
2125 2035 self.hook("incoming", node=hex(cl.node(i)),
2126 2036 source=srctype, url=url)
2127 2037
2128 2038 # never return 0 here:
2129 2039 if newheads < oldheads:
2130 2040 return newheads - oldheads - 1
2131 2041 else:
2132 2042 return newheads - oldheads + 1
2133 2043
2134 2044
2135 2045 def stream_in(self, remote):
2136 2046 fp = remote.stream_out()
2137 2047 l = fp.readline()
2138 2048 try:
2139 2049 resp = int(l)
2140 2050 except ValueError:
2141 2051 raise error.ResponseError(
2142 2052 _('Unexpected response from remote server:'), l)
2143 2053 if resp == 1:
2144 2054 raise util.Abort(_('operation forbidden by server'))
2145 2055 elif resp == 2:
2146 2056 raise util.Abort(_('locking the remote repository failed'))
2147 2057 elif resp != 0:
2148 2058 raise util.Abort(_('the server sent an unknown error code'))
2149 2059 self.ui.status(_('streaming all changes\n'))
2150 2060 l = fp.readline()
2151 2061 try:
2152 2062 total_files, total_bytes = map(int, l.split(' ', 1))
2153 2063 except (ValueError, TypeError):
2154 2064 raise error.ResponseError(
2155 2065 _('Unexpected response from remote server:'), l)
2156 2066 self.ui.status(_('%d files to transfer, %s of data\n') %
2157 2067 (total_files, util.bytecount(total_bytes)))
2158 2068 start = time.time()
2159 2069 for i in xrange(total_files):
2160 2070 # XXX doesn't support '\n' or '\r' in filenames
2161 2071 l = fp.readline()
2162 2072 try:
2163 2073 name, size = l.split('\0', 1)
2164 2074 size = int(size)
2165 2075 except (ValueError, TypeError):
2166 2076 raise error.ResponseError(
2167 2077 _('Unexpected response from remote server:'), l)
2168 2078 self.ui.debug(_('adding %s (%s)\n') % (name, util.bytecount(size)))
2169 2079 # for backwards compat, name was partially encoded
2170 2080 ofp = self.sopener(store.decodedir(name), 'w')
2171 2081 for chunk in util.filechunkiter(fp, limit=size):
2172 2082 ofp.write(chunk)
2173 2083 ofp.close()
2174 2084 elapsed = time.time() - start
2175 2085 if elapsed <= 0:
2176 2086 elapsed = 0.001
2177 2087 self.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
2178 2088 (util.bytecount(total_bytes), elapsed,
2179 2089 util.bytecount(total_bytes / elapsed)))
2180 2090 self.invalidate()
2181 2091 return len(self.heads()) + 1
2182 2092
2183 2093 def clone(self, remote, heads=[], stream=False):
2184 2094 '''clone remote repository.
2185 2095
2186 2096 keyword arguments:
2187 2097 heads: list of revs to clone (forces use of pull)
2188 2098 stream: use streaming clone if possible'''
2189 2099
2190 2100 # now, all clients that can request uncompressed clones can
2191 2101 # read repo formats supported by all servers that can serve
2192 2102 # them.
2193 2103
2194 2104 # if revlog format changes, client will have to check version
2195 2105 # and format flags on "stream" capability, and use
2196 2106 # uncompressed only if compatible.
2197 2107
2198 2108 if stream and not heads and remote.capable('stream'):
2199 2109 return self.stream_in(remote)
2200 2110 return self.pull(remote, heads)
2201 2111
2202 2112 # used to avoid circular references so destructors work
2203 2113 def aftertrans(files):
2204 2114 renamefiles = [tuple(t) for t in files]
2205 2115 def a():
2206 2116 for src, dest in renamefiles:
2207 2117 util.rename(src, dest)
2208 2118 return a
2209 2119
2210 2120 def instance(ui, path, create):
2211 2121 return localrepository(ui, util.drop_scheme('file', path), create)
2212 2122
2213 2123 def islocal(path):
2214 2124 return True
General Comments 0
You need to be logged in to leave comments. Login now