##// END OF EJS Templates
scmutil: extra utility to display a reasonable amount of nodes...
Boris Feld -
r35185:bc775b8c default
parent child Browse files
Show More
@@ -1,527 +1,525 b''
1 1 # discovery.py - protocol changeset discovery functions
2 2 #
3 3 # Copyright 2010 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 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import functools
11 11
12 12 from .i18n import _
13 13 from .node import (
14 14 hex,
15 15 nullid,
16 16 short,
17 17 )
18 18
19 19 from . import (
20 20 bookmarks,
21 21 branchmap,
22 22 error,
23 23 phases,
24 scmutil,
24 25 setdiscovery,
25 26 treediscovery,
26 27 util,
27 28 )
28 29
29 30 def findcommonincoming(repo, remote, heads=None, force=False):
30 31 """Return a tuple (common, anyincoming, heads) used to identify the common
31 32 subset of nodes between repo and remote.
32 33
33 34 "common" is a list of (at least) the heads of the common subset.
34 35 "anyincoming" is testable as a boolean indicating if any nodes are missing
35 36 locally. If remote does not support getbundle, this actually is a list of
36 37 roots of the nodes that would be incoming, to be supplied to
37 38 changegroupsubset. No code except for pull should be relying on this fact
38 39 any longer.
39 40 "heads" is either the supplied heads, or else the remote's heads.
40 41
41 42 If you pass heads and they are all known locally, the response lists just
42 43 these heads in "common" and in "heads".
43 44
44 45 Please use findcommonoutgoing to compute the set of outgoing nodes to give
45 46 extensions a good hook into outgoing.
46 47 """
47 48
48 49 if not remote.capable('getbundle'):
49 50 return treediscovery.findcommonincoming(repo, remote, heads, force)
50 51
51 52 if heads:
52 53 allknown = True
53 54 knownnode = repo.changelog.hasnode # no nodemap until it is filtered
54 55 for h in heads:
55 56 if not knownnode(h):
56 57 allknown = False
57 58 break
58 59 if allknown:
59 60 return (heads, False, heads)
60 61
61 62 res = setdiscovery.findcommonheads(repo.ui, repo, remote,
62 63 abortwhenunrelated=not force)
63 64 common, anyinc, srvheads = res
64 65 return (list(common), anyinc, heads or list(srvheads))
65 66
66 67 class outgoing(object):
67 68 '''Represents the set of nodes present in a local repo but not in a
68 69 (possibly) remote one.
69 70
70 71 Members:
71 72
72 73 missing is a list of all nodes present in local but not in remote.
73 74 common is a list of all nodes shared between the two repos.
74 75 excluded is the list of missing changeset that shouldn't be sent remotely.
75 76 missingheads is the list of heads of missing.
76 77 commonheads is the list of heads of common.
77 78
78 79 The sets are computed on demand from the heads, unless provided upfront
79 80 by discovery.'''
80 81
81 82 def __init__(self, repo, commonheads=None, missingheads=None,
82 83 missingroots=None):
83 84 # at least one of them must not be set
84 85 assert None in (commonheads, missingroots)
85 86 cl = repo.changelog
86 87 if missingheads is None:
87 88 missingheads = cl.heads()
88 89 if missingroots:
89 90 discbases = []
90 91 for n in missingroots:
91 92 discbases.extend([p for p in cl.parents(n) if p != nullid])
92 93 # TODO remove call to nodesbetween.
93 94 # TODO populate attributes on outgoing instance instead of setting
94 95 # discbases.
95 96 csets, roots, heads = cl.nodesbetween(missingroots, missingheads)
96 97 included = set(csets)
97 98 missingheads = heads
98 99 commonheads = [n for n in discbases if n not in included]
99 100 elif not commonheads:
100 101 commonheads = [nullid]
101 102 self.commonheads = commonheads
102 103 self.missingheads = missingheads
103 104 self._revlog = cl
104 105 self._common = None
105 106 self._missing = None
106 107 self.excluded = []
107 108
108 109 def _computecommonmissing(self):
109 110 sets = self._revlog.findcommonmissing(self.commonheads,
110 111 self.missingheads)
111 112 self._common, self._missing = sets
112 113
113 114 @util.propertycache
114 115 def common(self):
115 116 if self._common is None:
116 117 self._computecommonmissing()
117 118 return self._common
118 119
119 120 @util.propertycache
120 121 def missing(self):
121 122 if self._missing is None:
122 123 self._computecommonmissing()
123 124 return self._missing
124 125
125 126 def findcommonoutgoing(repo, other, onlyheads=None, force=False,
126 127 commoninc=None, portable=False):
127 128 '''Return an outgoing instance to identify the nodes present in repo but
128 129 not in other.
129 130
130 131 If onlyheads is given, only nodes ancestral to nodes in onlyheads
131 132 (inclusive) are included. If you already know the local repo's heads,
132 133 passing them in onlyheads is faster than letting them be recomputed here.
133 134
134 135 If commoninc is given, it must be the result of a prior call to
135 136 findcommonincoming(repo, other, force) to avoid recomputing it here.
136 137
137 138 If portable is given, compute more conservative common and missingheads,
138 139 to make bundles created from the instance more portable.'''
139 140 # declare an empty outgoing object to be filled later
140 141 og = outgoing(repo, None, None)
141 142
142 143 # get common set if not provided
143 144 if commoninc is None:
144 145 commoninc = findcommonincoming(repo, other, force=force)
145 146 og.commonheads, _any, _hds = commoninc
146 147
147 148 # compute outgoing
148 149 mayexclude = (repo._phasecache.phaseroots[phases.secret] or repo.obsstore)
149 150 if not mayexclude:
150 151 og.missingheads = onlyheads or repo.heads()
151 152 elif onlyheads is None:
152 153 # use visible heads as it should be cached
153 154 og.missingheads = repo.filtered("served").heads()
154 155 og.excluded = [ctx.node() for ctx in repo.set('secret() or extinct()')]
155 156 else:
156 157 # compute common, missing and exclude secret stuff
157 158 sets = repo.changelog.findcommonmissing(og.commonheads, onlyheads)
158 159 og._common, allmissing = sets
159 160 og._missing = missing = []
160 161 og.excluded = excluded = []
161 162 for node in allmissing:
162 163 ctx = repo[node]
163 164 if ctx.phase() >= phases.secret or ctx.extinct():
164 165 excluded.append(node)
165 166 else:
166 167 missing.append(node)
167 168 if len(missing) == len(allmissing):
168 169 missingheads = onlyheads
169 170 else: # update missing heads
170 171 missingheads = phases.newheads(repo, onlyheads, excluded)
171 172 og.missingheads = missingheads
172 173 if portable:
173 174 # recompute common and missingheads as if -r<rev> had been given for
174 175 # each head of missing, and --base <rev> for each head of the proper
175 176 # ancestors of missing
176 177 og._computecommonmissing()
177 178 cl = repo.changelog
178 179 missingrevs = set(cl.rev(n) for n in og._missing)
179 180 og._common = set(cl.ancestors(missingrevs)) - missingrevs
180 181 commonheads = set(og.commonheads)
181 182 og.missingheads = [h for h in og.missingheads if h not in commonheads]
182 183
183 184 return og
184 185
185 186 def _headssummary(pushop):
186 187 """compute a summary of branch and heads status before and after push
187 188
188 189 return {'branch': ([remoteheads], [newheads],
189 190 [unsyncedheads], [discardedheads])} mapping
190 191
191 192 - branch: the branch name,
192 193 - remoteheads: the list of remote heads known locally
193 194 None if the branch is new,
194 195 - newheads: the new remote heads (known locally) with outgoing pushed,
195 196 - unsyncedheads: the list of remote heads unknown locally,
196 197 - discardedheads: the list of heads made obsolete by the push.
197 198 """
198 199 repo = pushop.repo.unfiltered()
199 200 remote = pushop.remote
200 201 outgoing = pushop.outgoing
201 202 cl = repo.changelog
202 203 headssum = {}
203 204 # A. Create set of branches involved in the push.
204 205 branches = set(repo[n].branch() for n in outgoing.missing)
205 206 remotemap = remote.branchmap()
206 207 newbranches = branches - set(remotemap)
207 208 branches.difference_update(newbranches)
208 209
209 210 # A. register remote heads
210 211 remotebranches = set()
211 212 for branch, heads in remote.branchmap().iteritems():
212 213 remotebranches.add(branch)
213 214 known = []
214 215 unsynced = []
215 216 knownnode = cl.hasnode # do not use nodemap until it is filtered
216 217 for h in heads:
217 218 if knownnode(h):
218 219 known.append(h)
219 220 else:
220 221 unsynced.append(h)
221 222 headssum[branch] = (known, list(known), unsynced)
222 223 # B. add new branch data
223 224 missingctx = list(repo[n] for n in outgoing.missing)
224 225 touchedbranches = set()
225 226 for ctx in missingctx:
226 227 branch = ctx.branch()
227 228 touchedbranches.add(branch)
228 229 if branch not in headssum:
229 230 headssum[branch] = (None, [], [])
230 231
231 232 # C drop data about untouched branches:
232 233 for branch in remotebranches - touchedbranches:
233 234 del headssum[branch]
234 235
235 236 # D. Update newmap with outgoing changes.
236 237 # This will possibly add new heads and remove existing ones.
237 238 newmap = branchmap.branchcache((branch, heads[1])
238 239 for branch, heads in headssum.iteritems()
239 240 if heads[0] is not None)
240 241 newmap.update(repo, (ctx.rev() for ctx in missingctx))
241 242 for branch, newheads in newmap.iteritems():
242 243 headssum[branch][1][:] = newheads
243 244 for branch, items in headssum.iteritems():
244 245 for l in items:
245 246 if l is not None:
246 247 l.sort()
247 248 headssum[branch] = items + ([],)
248 249
249 250 # If there are no obsstore, no post processing are needed.
250 251 if repo.obsstore:
251 252 torev = repo.changelog.rev
252 253 futureheads = set(torev(h) for h in outgoing.missingheads)
253 254 futureheads |= set(torev(h) for h in outgoing.commonheads)
254 255 allfuturecommon = repo.changelog.ancestors(futureheads, inclusive=True)
255 256 for branch, heads in sorted(headssum.iteritems()):
256 257 remoteheads, newheads, unsyncedheads, placeholder = heads
257 258 result = _postprocessobsolete(pushop, allfuturecommon, newheads)
258 259 headssum[branch] = (remoteheads, sorted(result[0]), unsyncedheads,
259 260 sorted(result[1]))
260 261 return headssum
261 262
262 263 def _oldheadssummary(repo, remoteheads, outgoing, inc=False):
263 264 """Compute branchmapsummary for repo without branchmap support"""
264 265
265 266 # 1-4b. old servers: Check for new topological heads.
266 267 # Construct {old,new}map with branch = None (topological branch).
267 268 # (code based on update)
268 269 knownnode = repo.changelog.hasnode # no nodemap until it is filtered
269 270 oldheads = sorted(h for h in remoteheads if knownnode(h))
270 271 # all nodes in outgoing.missing are children of either:
271 272 # - an element of oldheads
272 273 # - another element of outgoing.missing
273 274 # - nullrev
274 275 # This explains why the new head are very simple to compute.
275 276 r = repo.set('heads(%ln + %ln)', oldheads, outgoing.missing)
276 277 newheads = sorted(c.node() for c in r)
277 278 # set some unsynced head to issue the "unsynced changes" warning
278 279 if inc:
279 280 unsynced = [None]
280 281 else:
281 282 unsynced = []
282 283 return {None: (oldheads, newheads, unsynced, [])}
283 284
284 285 def _nowarnheads(pushop):
285 286 # Compute newly pushed bookmarks. We don't warn about bookmarked heads.
286 287 repo = pushop.repo.unfiltered()
287 288 remote = pushop.remote
288 289 localbookmarks = repo._bookmarks
289 290 remotebookmarks = remote.listkeys('bookmarks')
290 291 bookmarkedheads = set()
291 292
292 293 # internal config: bookmarks.pushing
293 294 newbookmarks = [localbookmarks.expandname(b)
294 295 for b in pushop.ui.configlist('bookmarks', 'pushing')]
295 296
296 297 for bm in localbookmarks:
297 298 rnode = remotebookmarks.get(bm)
298 299 if rnode and rnode in repo:
299 300 lctx, rctx = repo[bm], repo[rnode]
300 301 if bookmarks.validdest(repo, rctx, lctx):
301 302 bookmarkedheads.add(lctx.node())
302 303 else:
303 304 if bm in newbookmarks and bm not in remotebookmarks:
304 305 bookmarkedheads.add(repo[bm].node())
305 306
306 307 return bookmarkedheads
307 308
308 309 def checkheads(pushop):
309 310 """Check that a push won't add any outgoing head
310 311
311 312 raise Abort error and display ui message as needed.
312 313 """
313 314
314 315 repo = pushop.repo.unfiltered()
315 316 remote = pushop.remote
316 317 outgoing = pushop.outgoing
317 318 remoteheads = pushop.remoteheads
318 319 newbranch = pushop.newbranch
319 320 inc = bool(pushop.incoming)
320 321
321 322 # Check for each named branch if we're creating new remote heads.
322 323 # To be a remote head after push, node must be either:
323 324 # - unknown locally
324 325 # - a local outgoing head descended from update
325 326 # - a remote head that's known locally and not
326 327 # ancestral to an outgoing head
327 328 if remoteheads == [nullid]:
328 329 # remote is empty, nothing to check.
329 330 return
330 331
331 332 if remote.capable('branchmap'):
332 333 headssum = _headssummary(pushop)
333 334 else:
334 335 headssum = _oldheadssummary(repo, remoteheads, outgoing, inc)
335 336 pushop.pushbranchmap = headssum
336 337 newbranches = [branch for branch, heads in headssum.iteritems()
337 338 if heads[0] is None]
338 339 # 1. Check for new branches on the remote.
339 340 if newbranches and not newbranch: # new branch requires --new-branch
340 341 branchnames = ', '.join(sorted(newbranches))
341 342 raise error.Abort(_("push creates new remote branches: %s!")
342 343 % branchnames,
343 344 hint=_("use 'hg push --new-branch' to create"
344 345 " new remote branches"))
345 346
346 347 # 2. Find heads that we need not warn about
347 348 nowarnheads = _nowarnheads(pushop)
348 349
349 350 # 3. Check for new heads.
350 351 # If there are more heads after the push than before, a suitable
351 352 # error message, depending on unsynced status, is displayed.
352 353 errormsg = None
353 354 for branch, heads in sorted(headssum.iteritems()):
354 355 remoteheads, newheads, unsyncedheads, discardedheads = heads
355 356 # add unsynced data
356 357 if remoteheads is None:
357 358 oldhs = set()
358 359 else:
359 360 oldhs = set(remoteheads)
360 361 oldhs.update(unsyncedheads)
361 362 dhs = None # delta heads, the new heads on branch
362 363 newhs = set(newheads)
363 364 newhs.update(unsyncedheads)
364 365 if unsyncedheads:
365 366 if None in unsyncedheads:
366 367 # old remote, no heads data
367 368 heads = None
368 elif len(unsyncedheads) <= 4 or repo.ui.verbose:
369 heads = ' '.join(short(h) for h in unsyncedheads)
370 369 else:
371 heads = (' '.join(short(h) for h in unsyncedheads[:4]) +
372 ' ' + _("and %s others") % (len(unsyncedheads) - 4))
370 heads = scmutil.nodesummaries(repo, unsyncedheads)
373 371 if heads is None:
374 372 repo.ui.status(_("remote has heads that are "
375 373 "not known locally\n"))
376 374 elif branch is None:
377 375 repo.ui.status(_("remote has heads that are "
378 376 "not known locally: %s\n") % heads)
379 377 else:
380 378 repo.ui.status(_("remote has heads on branch '%s' that are "
381 379 "not known locally: %s\n") % (branch, heads))
382 380 if remoteheads is None:
383 381 if len(newhs) > 1:
384 382 dhs = list(newhs)
385 383 if errormsg is None:
386 384 errormsg = (_("push creates new branch '%s' "
387 385 "with multiple heads") % (branch))
388 386 hint = _("merge or"
389 387 " see 'hg help push' for details about"
390 388 " pushing new heads")
391 389 elif len(newhs) > len(oldhs):
392 390 # remove bookmarked or existing remote heads from the new heads list
393 391 dhs = sorted(newhs - nowarnheads - oldhs)
394 392 if dhs:
395 393 if errormsg is None:
396 394 if branch not in ('default', None):
397 395 errormsg = _("push creates new remote head %s "
398 396 "on branch '%s'!") % (short(dhs[0]), branch)
399 397 elif repo[dhs[0]].bookmarks():
400 398 errormsg = _("push creates new remote head %s "
401 399 "with bookmark '%s'!") % (
402 400 short(dhs[0]), repo[dhs[0]].bookmarks()[0])
403 401 else:
404 402 errormsg = _("push creates new remote head %s!"
405 403 ) % short(dhs[0])
406 404 if unsyncedheads:
407 405 hint = _("pull and merge or"
408 406 " see 'hg help push' for details about"
409 407 " pushing new heads")
410 408 else:
411 409 hint = _("merge or"
412 410 " see 'hg help push' for details about"
413 411 " pushing new heads")
414 412 if branch is None:
415 413 repo.ui.note(_("new remote heads:\n"))
416 414 else:
417 415 repo.ui.note(_("new remote heads on branch '%s':\n") % branch)
418 416 for h in dhs:
419 417 repo.ui.note((" %s\n") % short(h))
420 418 if errormsg:
421 419 raise error.Abort(errormsg, hint=hint)
422 420
423 421 def _postprocessobsolete(pushop, futurecommon, candidate_newhs):
424 422 """post process the list of new heads with obsolescence information
425 423
426 424 Exists as a sub-function to contain the complexity and allow extensions to
427 425 experiment with smarter logic.
428 426
429 427 Returns (newheads, discarded_heads) tuple
430 428 """
431 429 # known issue
432 430 #
433 431 # * We "silently" skip processing on all changeset unknown locally
434 432 #
435 433 # * if <nh> is public on the remote, it won't be affected by obsolete
436 434 # marker and a new is created
437 435
438 436 # define various utilities and containers
439 437 repo = pushop.repo
440 438 unfi = repo.unfiltered()
441 439 tonode = unfi.changelog.node
442 440 torev = unfi.changelog.nodemap.get
443 441 public = phases.public
444 442 getphase = unfi._phasecache.phase
445 443 ispublic = (lambda r: getphase(unfi, r) == public)
446 444 ispushed = (lambda n: torev(n) in futurecommon)
447 445 hasoutmarker = functools.partial(pushingmarkerfor, unfi.obsstore, ispushed)
448 446 successorsmarkers = unfi.obsstore.successors
449 447 newhs = set() # final set of new heads
450 448 discarded = set() # new head of fully replaced branch
451 449
452 450 localcandidate = set() # candidate heads known locally
453 451 unknownheads = set() # candidate heads unknown locally
454 452 for h in candidate_newhs:
455 453 if h in unfi:
456 454 localcandidate.add(h)
457 455 else:
458 456 if successorsmarkers.get(h) is not None:
459 457 msg = ('checkheads: remote head unknown locally has'
460 458 ' local marker: %s\n')
461 459 repo.ui.debug(msg % hex(h))
462 460 unknownheads.add(h)
463 461
464 462 # fast path the simple case
465 463 if len(localcandidate) == 1:
466 464 return unknownheads | set(candidate_newhs), set()
467 465
468 466 # actually process branch replacement
469 467 while localcandidate:
470 468 nh = localcandidate.pop()
471 469 # run this check early to skip the evaluation of the whole branch
472 470 if (torev(nh) in futurecommon or ispublic(torev(nh))):
473 471 newhs.add(nh)
474 472 continue
475 473
476 474 # Get all revs/nodes on the branch exclusive to this head
477 475 # (already filtered heads are "ignored"))
478 476 branchrevs = unfi.revs('only(%n, (%ln+%ln))',
479 477 nh, localcandidate, newhs)
480 478 branchnodes = [tonode(r) for r in branchrevs]
481 479
482 480 # The branch won't be hidden on the remote if
483 481 # * any part of it is public,
484 482 # * any part of it is considered part of the result by previous logic,
485 483 # * if we have no markers to push to obsolete it.
486 484 if (any(ispublic(r) for r in branchrevs)
487 485 or any(torev(n) in futurecommon for n in branchnodes)
488 486 or any(not hasoutmarker(n) for n in branchnodes)):
489 487 newhs.add(nh)
490 488 else:
491 489 # note: there is a corner case if there is a merge in the branch.
492 490 # we might end up with -more- heads. However, these heads are not
493 491 # "added" by the push, but more by the "removal" on the remote so I
494 492 # think is a okay to ignore them,
495 493 discarded.add(nh)
496 494 newhs |= unknownheads
497 495 return newhs, discarded
498 496
499 497 def pushingmarkerfor(obsstore, ispushed, node):
500 498 """true if some markers are to be pushed for node
501 499
502 500 We cannot just look in to the pushed obsmarkers from the pushop because
503 501 discovery might have filtered relevant markers. In addition listing all
504 502 markers relevant to all changesets in the pushed set would be too expensive
505 503 (O(len(repo)))
506 504
507 505 (note: There are cache opportunity in this function. but it would requires
508 506 a two dimensional stack.)
509 507 """
510 508 successorsmarkers = obsstore.successors
511 509 stack = [node]
512 510 seen = set(stack)
513 511 while stack:
514 512 current = stack.pop()
515 513 if ispushed(current):
516 514 return True
517 515 markers = successorsmarkers.get(current, ())
518 516 # markers fields = ('prec', 'succs', 'flag', 'meta', 'date', 'parents')
519 517 for m in markers:
520 518 nexts = m[1] # successors
521 519 if not nexts: # this is a prune marker
522 520 nexts = m[5] or () # parents
523 521 for n in nexts:
524 522 if n not in seen:
525 523 seen.add(n)
526 524 stack.append(n)
527 525 return False
@@ -1,1287 +1,1293 b''
1 1 # scmutil.py - Mercurial core utility functions
2 2 #
3 3 # Copyright 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 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import glob
12 12 import hashlib
13 13 import os
14 14 import re
15 15 import socket
16 16 import subprocess
17 17 import weakref
18 18
19 19 from .i18n import _
20 20 from .node import (
21 21 hex,
22 22 nullid,
23 23 short,
24 24 wdirid,
25 25 wdirrev,
26 26 )
27 27
28 28 from . import (
29 29 encoding,
30 30 error,
31 31 match as matchmod,
32 32 obsolete,
33 33 obsutil,
34 34 pathutil,
35 35 phases,
36 36 pycompat,
37 37 revsetlang,
38 38 similar,
39 39 url,
40 40 util,
41 41 vfs,
42 42 )
43 43
44 44 if pycompat.iswindows:
45 45 from . import scmwindows as scmplatform
46 46 else:
47 47 from . import scmposix as scmplatform
48 48
49 49 termsize = scmplatform.termsize
50 50
51 51 class status(tuple):
52 52 '''Named tuple with a list of files per status. The 'deleted', 'unknown'
53 53 and 'ignored' properties are only relevant to the working copy.
54 54 '''
55 55
56 56 __slots__ = ()
57 57
58 58 def __new__(cls, modified, added, removed, deleted, unknown, ignored,
59 59 clean):
60 60 return tuple.__new__(cls, (modified, added, removed, deleted, unknown,
61 61 ignored, clean))
62 62
63 63 @property
64 64 def modified(self):
65 65 '''files that have been modified'''
66 66 return self[0]
67 67
68 68 @property
69 69 def added(self):
70 70 '''files that have been added'''
71 71 return self[1]
72 72
73 73 @property
74 74 def removed(self):
75 75 '''files that have been removed'''
76 76 return self[2]
77 77
78 78 @property
79 79 def deleted(self):
80 80 '''files that are in the dirstate, but have been deleted from the
81 81 working copy (aka "missing")
82 82 '''
83 83 return self[3]
84 84
85 85 @property
86 86 def unknown(self):
87 87 '''files not in the dirstate that are not ignored'''
88 88 return self[4]
89 89
90 90 @property
91 91 def ignored(self):
92 92 '''files not in the dirstate that are ignored (by _dirignore())'''
93 93 return self[5]
94 94
95 95 @property
96 96 def clean(self):
97 97 '''files that have not been modified'''
98 98 return self[6]
99 99
100 100 def __repr__(self, *args, **kwargs):
101 101 return (('<status modified=%r, added=%r, removed=%r, deleted=%r, '
102 102 'unknown=%r, ignored=%r, clean=%r>') % self)
103 103
104 104 def itersubrepos(ctx1, ctx2):
105 105 """find subrepos in ctx1 or ctx2"""
106 106 # Create a (subpath, ctx) mapping where we prefer subpaths from
107 107 # ctx1. The subpaths from ctx2 are important when the .hgsub file
108 108 # has been modified (in ctx2) but not yet committed (in ctx1).
109 109 subpaths = dict.fromkeys(ctx2.substate, ctx2)
110 110 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
111 111
112 112 missing = set()
113 113
114 114 for subpath in ctx2.substate:
115 115 if subpath not in ctx1.substate:
116 116 del subpaths[subpath]
117 117 missing.add(subpath)
118 118
119 119 for subpath, ctx in sorted(subpaths.iteritems()):
120 120 yield subpath, ctx.sub(subpath)
121 121
122 122 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
123 123 # status and diff will have an accurate result when it does
124 124 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
125 125 # against itself.
126 126 for subpath in missing:
127 127 yield subpath, ctx2.nullsub(subpath, ctx1)
128 128
129 129 def nochangesfound(ui, repo, excluded=None):
130 130 '''Report no changes for push/pull, excluded is None or a list of
131 131 nodes excluded from the push/pull.
132 132 '''
133 133 secretlist = []
134 134 if excluded:
135 135 for n in excluded:
136 136 ctx = repo[n]
137 137 if ctx.phase() >= phases.secret and not ctx.extinct():
138 138 secretlist.append(n)
139 139
140 140 if secretlist:
141 141 ui.status(_("no changes found (ignored %d secret changesets)\n")
142 142 % len(secretlist))
143 143 else:
144 144 ui.status(_("no changes found\n"))
145 145
146 146 def callcatch(ui, func):
147 147 """call func() with global exception handling
148 148
149 149 return func() if no exception happens. otherwise do some error handling
150 150 and return an exit code accordingly. does not handle all exceptions.
151 151 """
152 152 try:
153 153 try:
154 154 return func()
155 155 except: # re-raises
156 156 ui.traceback()
157 157 raise
158 158 # Global exception handling, alphabetically
159 159 # Mercurial-specific first, followed by built-in and library exceptions
160 160 except error.LockHeld as inst:
161 161 if inst.errno == errno.ETIMEDOUT:
162 162 reason = _('timed out waiting for lock held by %r') % inst.locker
163 163 else:
164 164 reason = _('lock held by %r') % inst.locker
165 165 ui.warn(_("abort: %s: %s\n") % (inst.desc or inst.filename, reason))
166 166 if not inst.locker:
167 167 ui.warn(_("(lock might be very busy)\n"))
168 168 except error.LockUnavailable as inst:
169 169 ui.warn(_("abort: could not lock %s: %s\n") %
170 170 (inst.desc or inst.filename,
171 171 encoding.strtolocal(inst.strerror)))
172 172 except error.OutOfBandError as inst:
173 173 if inst.args:
174 174 msg = _("abort: remote error:\n")
175 175 else:
176 176 msg = _("abort: remote error\n")
177 177 ui.warn(msg)
178 178 if inst.args:
179 179 ui.warn(''.join(inst.args))
180 180 if inst.hint:
181 181 ui.warn('(%s)\n' % inst.hint)
182 182 except error.RepoError as inst:
183 183 ui.warn(_("abort: %s!\n") % inst)
184 184 if inst.hint:
185 185 ui.warn(_("(%s)\n") % inst.hint)
186 186 except error.ResponseError as inst:
187 187 ui.warn(_("abort: %s") % inst.args[0])
188 188 if not isinstance(inst.args[1], basestring):
189 189 ui.warn(" %r\n" % (inst.args[1],))
190 190 elif not inst.args[1]:
191 191 ui.warn(_(" empty string\n"))
192 192 else:
193 193 ui.warn("\n%r\n" % util.ellipsis(inst.args[1]))
194 194 except error.CensoredNodeError as inst:
195 195 ui.warn(_("abort: file censored %s!\n") % inst)
196 196 except error.RevlogError as inst:
197 197 ui.warn(_("abort: %s!\n") % inst)
198 198 except error.InterventionRequired as inst:
199 199 ui.warn("%s\n" % inst)
200 200 if inst.hint:
201 201 ui.warn(_("(%s)\n") % inst.hint)
202 202 return 1
203 203 except error.WdirUnsupported:
204 204 ui.warn(_("abort: working directory revision cannot be specified\n"))
205 205 except error.Abort as inst:
206 206 ui.warn(_("abort: %s\n") % inst)
207 207 if inst.hint:
208 208 ui.warn(_("(%s)\n") % inst.hint)
209 209 except ImportError as inst:
210 210 ui.warn(_("abort: %s!\n") % inst)
211 211 m = str(inst).split()[-1]
212 212 if m in "mpatch bdiff".split():
213 213 ui.warn(_("(did you forget to compile extensions?)\n"))
214 214 elif m in "zlib".split():
215 215 ui.warn(_("(is your Python install correct?)\n"))
216 216 except IOError as inst:
217 217 if util.safehasattr(inst, "code"):
218 218 ui.warn(_("abort: %s\n") % inst)
219 219 elif util.safehasattr(inst, "reason"):
220 220 try: # usually it is in the form (errno, strerror)
221 221 reason = inst.reason.args[1]
222 222 except (AttributeError, IndexError):
223 223 # it might be anything, for example a string
224 224 reason = inst.reason
225 225 if isinstance(reason, unicode):
226 226 # SSLError of Python 2.7.9 contains a unicode
227 227 reason = encoding.unitolocal(reason)
228 228 ui.warn(_("abort: error: %s\n") % reason)
229 229 elif (util.safehasattr(inst, "args")
230 230 and inst.args and inst.args[0] == errno.EPIPE):
231 231 pass
232 232 elif getattr(inst, "strerror", None):
233 233 if getattr(inst, "filename", None):
234 234 ui.warn(_("abort: %s: %s\n") % (
235 235 encoding.strtolocal(inst.strerror), inst.filename))
236 236 else:
237 237 ui.warn(_("abort: %s\n") % encoding.strtolocal(inst.strerror))
238 238 else:
239 239 raise
240 240 except OSError as inst:
241 241 if getattr(inst, "filename", None) is not None:
242 242 ui.warn(_("abort: %s: '%s'\n") % (
243 243 encoding.strtolocal(inst.strerror), inst.filename))
244 244 else:
245 245 ui.warn(_("abort: %s\n") % encoding.strtolocal(inst.strerror))
246 246 except MemoryError:
247 247 ui.warn(_("abort: out of memory\n"))
248 248 except SystemExit as inst:
249 249 # Commands shouldn't sys.exit directly, but give a return code.
250 250 # Just in case catch this and and pass exit code to caller.
251 251 return inst.code
252 252 except socket.error as inst:
253 253 ui.warn(_("abort: %s\n") % inst.args[-1])
254 254
255 255 return -1
256 256
257 257 def checknewlabel(repo, lbl, kind):
258 258 # Do not use the "kind" parameter in ui output.
259 259 # It makes strings difficult to translate.
260 260 if lbl in ['tip', '.', 'null']:
261 261 raise error.Abort(_("the name '%s' is reserved") % lbl)
262 262 for c in (':', '\0', '\n', '\r'):
263 263 if c in lbl:
264 264 raise error.Abort(_("%r cannot be used in a name") % c)
265 265 try:
266 266 int(lbl)
267 267 raise error.Abort(_("cannot use an integer as a name"))
268 268 except ValueError:
269 269 pass
270 270
271 271 def checkfilename(f):
272 272 '''Check that the filename f is an acceptable filename for a tracked file'''
273 273 if '\r' in f or '\n' in f:
274 274 raise error.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
275 275
276 276 def checkportable(ui, f):
277 277 '''Check if filename f is portable and warn or abort depending on config'''
278 278 checkfilename(f)
279 279 abort, warn = checkportabilityalert(ui)
280 280 if abort or warn:
281 281 msg = util.checkwinfilename(f)
282 282 if msg:
283 283 msg = "%s: %s" % (msg, util.shellquote(f))
284 284 if abort:
285 285 raise error.Abort(msg)
286 286 ui.warn(_("warning: %s\n") % msg)
287 287
288 288 def checkportabilityalert(ui):
289 289 '''check if the user's config requests nothing, a warning, or abort for
290 290 non-portable filenames'''
291 291 val = ui.config('ui', 'portablefilenames')
292 292 lval = val.lower()
293 293 bval = util.parsebool(val)
294 294 abort = pycompat.iswindows or lval == 'abort'
295 295 warn = bval or lval == 'warn'
296 296 if bval is None and not (warn or abort or lval == 'ignore'):
297 297 raise error.ConfigError(
298 298 _("ui.portablefilenames value is invalid ('%s')") % val)
299 299 return abort, warn
300 300
301 301 class casecollisionauditor(object):
302 302 def __init__(self, ui, abort, dirstate):
303 303 self._ui = ui
304 304 self._abort = abort
305 305 allfiles = '\0'.join(dirstate._map)
306 306 self._loweredfiles = set(encoding.lower(allfiles).split('\0'))
307 307 self._dirstate = dirstate
308 308 # The purpose of _newfiles is so that we don't complain about
309 309 # case collisions if someone were to call this object with the
310 310 # same filename twice.
311 311 self._newfiles = set()
312 312
313 313 def __call__(self, f):
314 314 if f in self._newfiles:
315 315 return
316 316 fl = encoding.lower(f)
317 317 if fl in self._loweredfiles and f not in self._dirstate:
318 318 msg = _('possible case-folding collision for %s') % f
319 319 if self._abort:
320 320 raise error.Abort(msg)
321 321 self._ui.warn(_("warning: %s\n") % msg)
322 322 self._loweredfiles.add(fl)
323 323 self._newfiles.add(f)
324 324
325 325 def filteredhash(repo, maxrev):
326 326 """build hash of filtered revisions in the current repoview.
327 327
328 328 Multiple caches perform up-to-date validation by checking that the
329 329 tiprev and tipnode stored in the cache file match the current repository.
330 330 However, this is not sufficient for validating repoviews because the set
331 331 of revisions in the view may change without the repository tiprev and
332 332 tipnode changing.
333 333
334 334 This function hashes all the revs filtered from the view and returns
335 335 that SHA-1 digest.
336 336 """
337 337 cl = repo.changelog
338 338 if not cl.filteredrevs:
339 339 return None
340 340 key = None
341 341 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
342 342 if revs:
343 343 s = hashlib.sha1()
344 344 for rev in revs:
345 345 s.update('%d;' % rev)
346 346 key = s.digest()
347 347 return key
348 348
349 349 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
350 350 '''yield every hg repository under path, always recursively.
351 351 The recurse flag will only control recursion into repo working dirs'''
352 352 def errhandler(err):
353 353 if err.filename == path:
354 354 raise err
355 355 samestat = getattr(os.path, 'samestat', None)
356 356 if followsym and samestat is not None:
357 357 def adddir(dirlst, dirname):
358 358 match = False
359 359 dirstat = os.stat(dirname)
360 360 for lstdirstat in dirlst:
361 361 if samestat(dirstat, lstdirstat):
362 362 match = True
363 363 break
364 364 if not match:
365 365 dirlst.append(dirstat)
366 366 return not match
367 367 else:
368 368 followsym = False
369 369
370 370 if (seen_dirs is None) and followsym:
371 371 seen_dirs = []
372 372 adddir(seen_dirs, path)
373 373 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
374 374 dirs.sort()
375 375 if '.hg' in dirs:
376 376 yield root # found a repository
377 377 qroot = os.path.join(root, '.hg', 'patches')
378 378 if os.path.isdir(os.path.join(qroot, '.hg')):
379 379 yield qroot # we have a patch queue repo here
380 380 if recurse:
381 381 # avoid recursing inside the .hg directory
382 382 dirs.remove('.hg')
383 383 else:
384 384 dirs[:] = [] # don't descend further
385 385 elif followsym:
386 386 newdirs = []
387 387 for d in dirs:
388 388 fname = os.path.join(root, d)
389 389 if adddir(seen_dirs, fname):
390 390 if os.path.islink(fname):
391 391 for hgname in walkrepos(fname, True, seen_dirs):
392 392 yield hgname
393 393 else:
394 394 newdirs.append(d)
395 395 dirs[:] = newdirs
396 396
397 397 def binnode(ctx):
398 398 """Return binary node id for a given basectx"""
399 399 node = ctx.node()
400 400 if node is None:
401 401 return wdirid
402 402 return node
403 403
404 404 def intrev(ctx):
405 405 """Return integer for a given basectx that can be used in comparison or
406 406 arithmetic operation"""
407 407 rev = ctx.rev()
408 408 if rev is None:
409 409 return wdirrev
410 410 return rev
411 411
412 412 def formatchangeid(ctx):
413 413 """Format changectx as '{rev}:{node|formatnode}', which is the default
414 414 template provided by cmdutil.changeset_templater"""
415 415 repo = ctx.repo()
416 416 return formatrevnode(repo.ui, intrev(ctx), binnode(ctx))
417 417
418 418 def formatrevnode(ui, rev, node):
419 419 """Format given revision and node depending on the current verbosity"""
420 420 if ui.debugflag:
421 421 hexfunc = hex
422 422 else:
423 423 hexfunc = short
424 424 return '%d:%s' % (rev, hexfunc(node))
425 425
426 426 def revsingle(repo, revspec, default='.', localalias=None):
427 427 if not revspec and revspec != 0:
428 428 return repo[default]
429 429
430 430 l = revrange(repo, [revspec], localalias=localalias)
431 431 if not l:
432 432 raise error.Abort(_('empty revision set'))
433 433 return repo[l.last()]
434 434
435 435 def _pairspec(revspec):
436 436 tree = revsetlang.parse(revspec)
437 437 return tree and tree[0] in ('range', 'rangepre', 'rangepost', 'rangeall')
438 438
439 439 def revpair(repo, revs):
440 440 if not revs:
441 441 return repo.dirstate.p1(), None
442 442
443 443 l = revrange(repo, revs)
444 444
445 445 if not l:
446 446 first = second = None
447 447 elif l.isascending():
448 448 first = l.min()
449 449 second = l.max()
450 450 elif l.isdescending():
451 451 first = l.max()
452 452 second = l.min()
453 453 else:
454 454 first = l.first()
455 455 second = l.last()
456 456
457 457 if first is None:
458 458 raise error.Abort(_('empty revision range'))
459 459 if (first == second and len(revs) >= 2
460 460 and not all(revrange(repo, [r]) for r in revs)):
461 461 raise error.Abort(_('empty revision on one side of range'))
462 462
463 463 # if top-level is range expression, the result must always be a pair
464 464 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
465 465 return repo.lookup(first), None
466 466
467 467 return repo.lookup(first), repo.lookup(second)
468 468
469 469 def revrange(repo, specs, localalias=None):
470 470 """Execute 1 to many revsets and return the union.
471 471
472 472 This is the preferred mechanism for executing revsets using user-specified
473 473 config options, such as revset aliases.
474 474
475 475 The revsets specified by ``specs`` will be executed via a chained ``OR``
476 476 expression. If ``specs`` is empty, an empty result is returned.
477 477
478 478 ``specs`` can contain integers, in which case they are assumed to be
479 479 revision numbers.
480 480
481 481 It is assumed the revsets are already formatted. If you have arguments
482 482 that need to be expanded in the revset, call ``revsetlang.formatspec()``
483 483 and pass the result as an element of ``specs``.
484 484
485 485 Specifying a single revset is allowed.
486 486
487 487 Returns a ``revset.abstractsmartset`` which is a list-like interface over
488 488 integer revisions.
489 489 """
490 490 allspecs = []
491 491 for spec in specs:
492 492 if isinstance(spec, int):
493 493 spec = revsetlang.formatspec('rev(%d)', spec)
494 494 allspecs.append(spec)
495 495 return repo.anyrevs(allspecs, user=True, localalias=localalias)
496 496
497 497 def meaningfulparents(repo, ctx):
498 498 """Return list of meaningful (or all if debug) parentrevs for rev.
499 499
500 500 For merges (two non-nullrev revisions) both parents are meaningful.
501 501 Otherwise the first parent revision is considered meaningful if it
502 502 is not the preceding revision.
503 503 """
504 504 parents = ctx.parents()
505 505 if len(parents) > 1:
506 506 return parents
507 507 if repo.ui.debugflag:
508 508 return [parents[0], repo['null']]
509 509 if parents[0].rev() >= intrev(ctx) - 1:
510 510 return []
511 511 return parents
512 512
513 513 def expandpats(pats):
514 514 '''Expand bare globs when running on windows.
515 515 On posix we assume it already has already been done by sh.'''
516 516 if not util.expandglobs:
517 517 return list(pats)
518 518 ret = []
519 519 for kindpat in pats:
520 520 kind, pat = matchmod._patsplit(kindpat, None)
521 521 if kind is None:
522 522 try:
523 523 globbed = glob.glob(pat)
524 524 except re.error:
525 525 globbed = [pat]
526 526 if globbed:
527 527 ret.extend(globbed)
528 528 continue
529 529 ret.append(kindpat)
530 530 return ret
531 531
532 532 def matchandpats(ctx, pats=(), opts=None, globbed=False, default='relpath',
533 533 badfn=None):
534 534 '''Return a matcher and the patterns that were used.
535 535 The matcher will warn about bad matches, unless an alternate badfn callback
536 536 is provided.'''
537 537 if pats == ("",):
538 538 pats = []
539 539 if opts is None:
540 540 opts = {}
541 541 if not globbed and default == 'relpath':
542 542 pats = expandpats(pats or [])
543 543
544 544 def bad(f, msg):
545 545 ctx.repo().ui.warn("%s: %s\n" % (m.rel(f), msg))
546 546
547 547 if badfn is None:
548 548 badfn = bad
549 549
550 550 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
551 551 default, listsubrepos=opts.get('subrepos'), badfn=badfn)
552 552
553 553 if m.always():
554 554 pats = []
555 555 return m, pats
556 556
557 557 def match(ctx, pats=(), opts=None, globbed=False, default='relpath',
558 558 badfn=None):
559 559 '''Return a matcher that will warn about bad matches.'''
560 560 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
561 561
562 562 def matchall(repo):
563 563 '''Return a matcher that will efficiently match everything.'''
564 564 return matchmod.always(repo.root, repo.getcwd())
565 565
566 566 def matchfiles(repo, files, badfn=None):
567 567 '''Return a matcher that will efficiently match exactly these files.'''
568 568 return matchmod.exact(repo.root, repo.getcwd(), files, badfn=badfn)
569 569
570 570 def parsefollowlinespattern(repo, rev, pat, msg):
571 571 """Return a file name from `pat` pattern suitable for usage in followlines
572 572 logic.
573 573 """
574 574 if not matchmod.patkind(pat):
575 575 return pathutil.canonpath(repo.root, repo.getcwd(), pat)
576 576 else:
577 577 ctx = repo[rev]
578 578 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=ctx)
579 579 files = [f for f in ctx if m(f)]
580 580 if len(files) != 1:
581 581 raise error.ParseError(msg)
582 582 return files[0]
583 583
584 584 def origpath(ui, repo, filepath):
585 585 '''customize where .orig files are created
586 586
587 587 Fetch user defined path from config file: [ui] origbackuppath = <path>
588 588 Fall back to default (filepath with .orig suffix) if not specified
589 589 '''
590 590 origbackuppath = ui.config('ui', 'origbackuppath')
591 591 if not origbackuppath:
592 592 return filepath + ".orig"
593 593
594 594 # Convert filepath from an absolute path into a path inside the repo.
595 595 filepathfromroot = util.normpath(os.path.relpath(filepath,
596 596 start=repo.root))
597 597
598 598 origvfs = vfs.vfs(repo.wjoin(origbackuppath))
599 599 origbackupdir = origvfs.dirname(filepathfromroot)
600 600 if not origvfs.isdir(origbackupdir) or origvfs.islink(origbackupdir):
601 601 ui.note(_('creating directory: %s\n') % origvfs.join(origbackupdir))
602 602
603 603 # Remove any files that conflict with the backup file's path
604 604 for f in reversed(list(util.finddirs(filepathfromroot))):
605 605 if origvfs.isfileorlink(f):
606 606 ui.note(_('removing conflicting file: %s\n')
607 607 % origvfs.join(f))
608 608 origvfs.unlink(f)
609 609 break
610 610
611 611 origvfs.makedirs(origbackupdir)
612 612
613 613 if origvfs.isdir(filepathfromroot) and not origvfs.islink(filepathfromroot):
614 614 ui.note(_('removing conflicting directory: %s\n')
615 615 % origvfs.join(filepathfromroot))
616 616 origvfs.rmtree(filepathfromroot, forcibly=True)
617 617
618 618 return origvfs.join(filepathfromroot)
619 619
620 620 class _containsnode(object):
621 621 """proxy __contains__(node) to container.__contains__ which accepts revs"""
622 622
623 623 def __init__(self, repo, revcontainer):
624 624 self._torev = repo.changelog.rev
625 625 self._revcontains = revcontainer.__contains__
626 626
627 627 def __contains__(self, node):
628 628 return self._revcontains(self._torev(node))
629 629
630 630 def cleanupnodes(repo, replacements, operation, moves=None, metadata=None):
631 631 """do common cleanups when old nodes are replaced by new nodes
632 632
633 633 That includes writing obsmarkers or stripping nodes, and moving bookmarks.
634 634 (we might also want to move working directory parent in the future)
635 635
636 636 By default, bookmark moves are calculated automatically from 'replacements',
637 637 but 'moves' can be used to override that. Also, 'moves' may include
638 638 additional bookmark moves that should not have associated obsmarkers.
639 639
640 640 replacements is {oldnode: [newnode]} or a iterable of nodes if they do not
641 641 have replacements. operation is a string, like "rebase".
642 642
643 643 metadata is dictionary containing metadata to be stored in obsmarker if
644 644 obsolescence is enabled.
645 645 """
646 646 if not replacements and not moves:
647 647 return
648 648
649 649 # translate mapping's other forms
650 650 if not util.safehasattr(replacements, 'items'):
651 651 replacements = {n: () for n in replacements}
652 652
653 653 # Calculate bookmark movements
654 654 if moves is None:
655 655 moves = {}
656 656 # Unfiltered repo is needed since nodes in replacements might be hidden.
657 657 unfi = repo.unfiltered()
658 658 for oldnode, newnodes in replacements.items():
659 659 if oldnode in moves:
660 660 continue
661 661 if len(newnodes) > 1:
662 662 # usually a split, take the one with biggest rev number
663 663 newnode = next(unfi.set('max(%ln)', newnodes)).node()
664 664 elif len(newnodes) == 0:
665 665 # move bookmark backwards
666 666 roots = list(unfi.set('max((::%n) - %ln)', oldnode,
667 667 list(replacements)))
668 668 if roots:
669 669 newnode = roots[0].node()
670 670 else:
671 671 newnode = nullid
672 672 else:
673 673 newnode = newnodes[0]
674 674 moves[oldnode] = newnode
675 675
676 676 with repo.transaction('cleanup') as tr:
677 677 # Move bookmarks
678 678 bmarks = repo._bookmarks
679 679 bmarkchanges = []
680 680 allnewnodes = [n for ns in replacements.values() for n in ns]
681 681 for oldnode, newnode in moves.items():
682 682 oldbmarks = repo.nodebookmarks(oldnode)
683 683 if not oldbmarks:
684 684 continue
685 685 from . import bookmarks # avoid import cycle
686 686 repo.ui.debug('moving bookmarks %r from %s to %s\n' %
687 687 (oldbmarks, hex(oldnode), hex(newnode)))
688 688 # Delete divergent bookmarks being parents of related newnodes
689 689 deleterevs = repo.revs('parents(roots(%ln & (::%n))) - parents(%n)',
690 690 allnewnodes, newnode, oldnode)
691 691 deletenodes = _containsnode(repo, deleterevs)
692 692 for name in oldbmarks:
693 693 bmarkchanges.append((name, newnode))
694 694 for b in bookmarks.divergent2delete(repo, deletenodes, name):
695 695 bmarkchanges.append((b, None))
696 696
697 697 if bmarkchanges:
698 698 bmarks.applychanges(repo, tr, bmarkchanges)
699 699
700 700 # Obsolete or strip nodes
701 701 if obsolete.isenabled(repo, obsolete.createmarkersopt):
702 702 # If a node is already obsoleted, and we want to obsolete it
703 703 # without a successor, skip that obssolete request since it's
704 704 # unnecessary. That's the "if s or not isobs(n)" check below.
705 705 # Also sort the node in topology order, that might be useful for
706 706 # some obsstore logic.
707 707 # NOTE: the filtering and sorting might belong to createmarkers.
708 708 isobs = unfi.obsstore.successors.__contains__
709 709 torev = unfi.changelog.rev
710 710 sortfunc = lambda ns: torev(ns[0])
711 711 rels = [(unfi[n], tuple(unfi[m] for m in s))
712 712 for n, s in sorted(replacements.items(), key=sortfunc)
713 713 if s or not isobs(n)]
714 714 if rels:
715 715 obsolete.createmarkers(repo, rels, operation=operation,
716 716 metadata=metadata)
717 717 else:
718 718 from . import repair # avoid import cycle
719 719 tostrip = list(replacements)
720 720 if tostrip:
721 721 repair.delayedstrip(repo.ui, repo, tostrip, operation)
722 722
723 723 def addremove(repo, matcher, prefix, opts=None, dry_run=None, similarity=None):
724 724 if opts is None:
725 725 opts = {}
726 726 m = matcher
727 727 if dry_run is None:
728 728 dry_run = opts.get('dry_run')
729 729 if similarity is None:
730 730 similarity = float(opts.get('similarity') or 0)
731 731
732 732 ret = 0
733 733 join = lambda f: os.path.join(prefix, f)
734 734
735 735 wctx = repo[None]
736 736 for subpath in sorted(wctx.substate):
737 737 submatch = matchmod.subdirmatcher(subpath, m)
738 738 if opts.get('subrepos') or m.exact(subpath) or any(submatch.files()):
739 739 sub = wctx.sub(subpath)
740 740 try:
741 741 if sub.addremove(submatch, prefix, opts, dry_run, similarity):
742 742 ret = 1
743 743 except error.LookupError:
744 744 repo.ui.status(_("skipping missing subrepository: %s\n")
745 745 % join(subpath))
746 746
747 747 rejected = []
748 748 def badfn(f, msg):
749 749 if f in m.files():
750 750 m.bad(f, msg)
751 751 rejected.append(f)
752 752
753 753 badmatch = matchmod.badmatch(m, badfn)
754 754 added, unknown, deleted, removed, forgotten = _interestingfiles(repo,
755 755 badmatch)
756 756
757 757 unknownset = set(unknown + forgotten)
758 758 toprint = unknownset.copy()
759 759 toprint.update(deleted)
760 760 for abs in sorted(toprint):
761 761 if repo.ui.verbose or not m.exact(abs):
762 762 if abs in unknownset:
763 763 status = _('adding %s\n') % m.uipath(abs)
764 764 else:
765 765 status = _('removing %s\n') % m.uipath(abs)
766 766 repo.ui.status(status)
767 767
768 768 renames = _findrenames(repo, m, added + unknown, removed + deleted,
769 769 similarity)
770 770
771 771 if not dry_run:
772 772 _markchanges(repo, unknown + forgotten, deleted, renames)
773 773
774 774 for f in rejected:
775 775 if f in m.files():
776 776 return 1
777 777 return ret
778 778
779 779 def marktouched(repo, files, similarity=0.0):
780 780 '''Assert that files have somehow been operated upon. files are relative to
781 781 the repo root.'''
782 782 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
783 783 rejected = []
784 784
785 785 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
786 786
787 787 if repo.ui.verbose:
788 788 unknownset = set(unknown + forgotten)
789 789 toprint = unknownset.copy()
790 790 toprint.update(deleted)
791 791 for abs in sorted(toprint):
792 792 if abs in unknownset:
793 793 status = _('adding %s\n') % abs
794 794 else:
795 795 status = _('removing %s\n') % abs
796 796 repo.ui.status(status)
797 797
798 798 renames = _findrenames(repo, m, added + unknown, removed + deleted,
799 799 similarity)
800 800
801 801 _markchanges(repo, unknown + forgotten, deleted, renames)
802 802
803 803 for f in rejected:
804 804 if f in m.files():
805 805 return 1
806 806 return 0
807 807
808 808 def _interestingfiles(repo, matcher):
809 809 '''Walk dirstate with matcher, looking for files that addremove would care
810 810 about.
811 811
812 812 This is different from dirstate.status because it doesn't care about
813 813 whether files are modified or clean.'''
814 814 added, unknown, deleted, removed, forgotten = [], [], [], [], []
815 815 audit_path = pathutil.pathauditor(repo.root, cached=True)
816 816
817 817 ctx = repo[None]
818 818 dirstate = repo.dirstate
819 819 walkresults = dirstate.walk(matcher, subrepos=sorted(ctx.substate),
820 820 unknown=True, ignored=False, full=False)
821 821 for abs, st in walkresults.iteritems():
822 822 dstate = dirstate[abs]
823 823 if dstate == '?' and audit_path.check(abs):
824 824 unknown.append(abs)
825 825 elif dstate != 'r' and not st:
826 826 deleted.append(abs)
827 827 elif dstate == 'r' and st:
828 828 forgotten.append(abs)
829 829 # for finding renames
830 830 elif dstate == 'r' and not st:
831 831 removed.append(abs)
832 832 elif dstate == 'a':
833 833 added.append(abs)
834 834
835 835 return added, unknown, deleted, removed, forgotten
836 836
837 837 def _findrenames(repo, matcher, added, removed, similarity):
838 838 '''Find renames from removed files to added ones.'''
839 839 renames = {}
840 840 if similarity > 0:
841 841 for old, new, score in similar.findrenames(repo, added, removed,
842 842 similarity):
843 843 if (repo.ui.verbose or not matcher.exact(old)
844 844 or not matcher.exact(new)):
845 845 repo.ui.status(_('recording removal of %s as rename to %s '
846 846 '(%d%% similar)\n') %
847 847 (matcher.rel(old), matcher.rel(new),
848 848 score * 100))
849 849 renames[new] = old
850 850 return renames
851 851
852 852 def _markchanges(repo, unknown, deleted, renames):
853 853 '''Marks the files in unknown as added, the files in deleted as removed,
854 854 and the files in renames as copied.'''
855 855 wctx = repo[None]
856 856 with repo.wlock():
857 857 wctx.forget(deleted)
858 858 wctx.add(unknown)
859 859 for new, old in renames.iteritems():
860 860 wctx.copy(old, new)
861 861
862 862 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
863 863 """Update the dirstate to reflect the intent of copying src to dst. For
864 864 different reasons it might not end with dst being marked as copied from src.
865 865 """
866 866 origsrc = repo.dirstate.copied(src) or src
867 867 if dst == origsrc: # copying back a copy?
868 868 if repo.dirstate[dst] not in 'mn' and not dryrun:
869 869 repo.dirstate.normallookup(dst)
870 870 else:
871 871 if repo.dirstate[origsrc] == 'a' and origsrc == src:
872 872 if not ui.quiet:
873 873 ui.warn(_("%s has not been committed yet, so no copy "
874 874 "data will be stored for %s.\n")
875 875 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
876 876 if repo.dirstate[dst] in '?r' and not dryrun:
877 877 wctx.add([dst])
878 878 elif not dryrun:
879 879 wctx.copy(origsrc, dst)
880 880
881 881 def readrequires(opener, supported):
882 882 '''Reads and parses .hg/requires and checks if all entries found
883 883 are in the list of supported features.'''
884 884 requirements = set(opener.read("requires").splitlines())
885 885 missings = []
886 886 for r in requirements:
887 887 if r not in supported:
888 888 if not r or not r[0].isalnum():
889 889 raise error.RequirementError(_(".hg/requires file is corrupt"))
890 890 missings.append(r)
891 891 missings.sort()
892 892 if missings:
893 893 raise error.RequirementError(
894 894 _("repository requires features unknown to this Mercurial: %s")
895 895 % " ".join(missings),
896 896 hint=_("see https://mercurial-scm.org/wiki/MissingRequirement"
897 897 " for more information"))
898 898 return requirements
899 899
900 900 def writerequires(opener, requirements):
901 901 with opener('requires', 'w') as fp:
902 902 for r in sorted(requirements):
903 903 fp.write("%s\n" % r)
904 904
905 905 class filecachesubentry(object):
906 906 def __init__(self, path, stat):
907 907 self.path = path
908 908 self.cachestat = None
909 909 self._cacheable = None
910 910
911 911 if stat:
912 912 self.cachestat = filecachesubentry.stat(self.path)
913 913
914 914 if self.cachestat:
915 915 self._cacheable = self.cachestat.cacheable()
916 916 else:
917 917 # None means we don't know yet
918 918 self._cacheable = None
919 919
920 920 def refresh(self):
921 921 if self.cacheable():
922 922 self.cachestat = filecachesubentry.stat(self.path)
923 923
924 924 def cacheable(self):
925 925 if self._cacheable is not None:
926 926 return self._cacheable
927 927
928 928 # we don't know yet, assume it is for now
929 929 return True
930 930
931 931 def changed(self):
932 932 # no point in going further if we can't cache it
933 933 if not self.cacheable():
934 934 return True
935 935
936 936 newstat = filecachesubentry.stat(self.path)
937 937
938 938 # we may not know if it's cacheable yet, check again now
939 939 if newstat and self._cacheable is None:
940 940 self._cacheable = newstat.cacheable()
941 941
942 942 # check again
943 943 if not self._cacheable:
944 944 return True
945 945
946 946 if self.cachestat != newstat:
947 947 self.cachestat = newstat
948 948 return True
949 949 else:
950 950 return False
951 951
952 952 @staticmethod
953 953 def stat(path):
954 954 try:
955 955 return util.cachestat(path)
956 956 except OSError as e:
957 957 if e.errno != errno.ENOENT:
958 958 raise
959 959
960 960 class filecacheentry(object):
961 961 def __init__(self, paths, stat=True):
962 962 self._entries = []
963 963 for path in paths:
964 964 self._entries.append(filecachesubentry(path, stat))
965 965
966 966 def changed(self):
967 967 '''true if any entry has changed'''
968 968 for entry in self._entries:
969 969 if entry.changed():
970 970 return True
971 971 return False
972 972
973 973 def refresh(self):
974 974 for entry in self._entries:
975 975 entry.refresh()
976 976
977 977 class filecache(object):
978 978 '''A property like decorator that tracks files under .hg/ for updates.
979 979
980 980 Records stat info when called in _filecache.
981 981
982 982 On subsequent calls, compares old stat info with new info, and recreates the
983 983 object when any of the files changes, updating the new stat info in
984 984 _filecache.
985 985
986 986 Mercurial either atomic renames or appends for files under .hg,
987 987 so to ensure the cache is reliable we need the filesystem to be able
988 988 to tell us if a file has been replaced. If it can't, we fallback to
989 989 recreating the object on every call (essentially the same behavior as
990 990 propertycache).
991 991
992 992 '''
993 993 def __init__(self, *paths):
994 994 self.paths = paths
995 995
996 996 def join(self, obj, fname):
997 997 """Used to compute the runtime path of a cached file.
998 998
999 999 Users should subclass filecache and provide their own version of this
1000 1000 function to call the appropriate join function on 'obj' (an instance
1001 1001 of the class that its member function was decorated).
1002 1002 """
1003 1003 raise NotImplementedError
1004 1004
1005 1005 def __call__(self, func):
1006 1006 self.func = func
1007 1007 self.name = func.__name__.encode('ascii')
1008 1008 return self
1009 1009
1010 1010 def __get__(self, obj, type=None):
1011 1011 # if accessed on the class, return the descriptor itself.
1012 1012 if obj is None:
1013 1013 return self
1014 1014 # do we need to check if the file changed?
1015 1015 if self.name in obj.__dict__:
1016 1016 assert self.name in obj._filecache, self.name
1017 1017 return obj.__dict__[self.name]
1018 1018
1019 1019 entry = obj._filecache.get(self.name)
1020 1020
1021 1021 if entry:
1022 1022 if entry.changed():
1023 1023 entry.obj = self.func(obj)
1024 1024 else:
1025 1025 paths = [self.join(obj, path) for path in self.paths]
1026 1026
1027 1027 # We stat -before- creating the object so our cache doesn't lie if
1028 1028 # a writer modified between the time we read and stat
1029 1029 entry = filecacheentry(paths, True)
1030 1030 entry.obj = self.func(obj)
1031 1031
1032 1032 obj._filecache[self.name] = entry
1033 1033
1034 1034 obj.__dict__[self.name] = entry.obj
1035 1035 return entry.obj
1036 1036
1037 1037 def __set__(self, obj, value):
1038 1038 if self.name not in obj._filecache:
1039 1039 # we add an entry for the missing value because X in __dict__
1040 1040 # implies X in _filecache
1041 1041 paths = [self.join(obj, path) for path in self.paths]
1042 1042 ce = filecacheentry(paths, False)
1043 1043 obj._filecache[self.name] = ce
1044 1044 else:
1045 1045 ce = obj._filecache[self.name]
1046 1046
1047 1047 ce.obj = value # update cached copy
1048 1048 obj.__dict__[self.name] = value # update copy returned by obj.x
1049 1049
1050 1050 def __delete__(self, obj):
1051 1051 try:
1052 1052 del obj.__dict__[self.name]
1053 1053 except KeyError:
1054 1054 raise AttributeError(self.name)
1055 1055
1056 1056 def extdatasource(repo, source):
1057 1057 """Gather a map of rev -> value dict from the specified source
1058 1058
1059 1059 A source spec is treated as a URL, with a special case shell: type
1060 1060 for parsing the output from a shell command.
1061 1061
1062 1062 The data is parsed as a series of newline-separated records where
1063 1063 each record is a revision specifier optionally followed by a space
1064 1064 and a freeform string value. If the revision is known locally, it
1065 1065 is converted to a rev, otherwise the record is skipped.
1066 1066
1067 1067 Note that both key and value are treated as UTF-8 and converted to
1068 1068 the local encoding. This allows uniformity between local and
1069 1069 remote data sources.
1070 1070 """
1071 1071
1072 1072 spec = repo.ui.config("extdata", source)
1073 1073 if not spec:
1074 1074 raise error.Abort(_("unknown extdata source '%s'") % source)
1075 1075
1076 1076 data = {}
1077 1077 src = proc = None
1078 1078 try:
1079 1079 if spec.startswith("shell:"):
1080 1080 # external commands should be run relative to the repo root
1081 1081 cmd = spec[6:]
1082 1082 proc = subprocess.Popen(cmd, shell=True, bufsize=-1,
1083 1083 close_fds=util.closefds,
1084 1084 stdout=subprocess.PIPE, cwd=repo.root)
1085 1085 src = proc.stdout
1086 1086 else:
1087 1087 # treat as a URL or file
1088 1088 src = url.open(repo.ui, spec)
1089 1089 for l in src:
1090 1090 if " " in l:
1091 1091 k, v = l.strip().split(" ", 1)
1092 1092 else:
1093 1093 k, v = l.strip(), ""
1094 1094
1095 1095 k = encoding.tolocal(k)
1096 1096 try:
1097 1097 data[repo[k].rev()] = encoding.tolocal(v)
1098 1098 except (error.LookupError, error.RepoLookupError):
1099 1099 pass # we ignore data for nodes that don't exist locally
1100 1100 finally:
1101 1101 if proc:
1102 1102 proc.communicate()
1103 1103 if proc.returncode != 0:
1104 1104 # not an error so 'cmd | grep' can be empty
1105 1105 repo.ui.debug("extdata command '%s' %s\n"
1106 1106 % (cmd, util.explainexit(proc.returncode)[0]))
1107 1107 if src:
1108 1108 src.close()
1109 1109
1110 1110 return data
1111 1111
1112 1112 def _locksub(repo, lock, envvar, cmd, environ=None, *args, **kwargs):
1113 1113 if lock is None:
1114 1114 raise error.LockInheritanceContractViolation(
1115 1115 'lock can only be inherited while held')
1116 1116 if environ is None:
1117 1117 environ = {}
1118 1118 with lock.inherit() as locker:
1119 1119 environ[envvar] = locker
1120 1120 return repo.ui.system(cmd, environ=environ, *args, **kwargs)
1121 1121
1122 1122 def wlocksub(repo, cmd, *args, **kwargs):
1123 1123 """run cmd as a subprocess that allows inheriting repo's wlock
1124 1124
1125 1125 This can only be called while the wlock is held. This takes all the
1126 1126 arguments that ui.system does, and returns the exit code of the
1127 1127 subprocess."""
1128 1128 return _locksub(repo, repo.currentwlock(), 'HG_WLOCK_LOCKER', cmd, *args,
1129 1129 **kwargs)
1130 1130
1131 1131 def gdinitconfig(ui):
1132 1132 """helper function to know if a repo should be created as general delta
1133 1133 """
1134 1134 # experimental config: format.generaldelta
1135 1135 return (ui.configbool('format', 'generaldelta')
1136 1136 or ui.configbool('format', 'usegeneraldelta'))
1137 1137
1138 1138 def gddeltaconfig(ui):
1139 1139 """helper function to know if incoming delta should be optimised
1140 1140 """
1141 1141 # experimental config: format.generaldelta
1142 1142 return ui.configbool('format', 'generaldelta')
1143 1143
1144 1144 class simplekeyvaluefile(object):
1145 1145 """A simple file with key=value lines
1146 1146
1147 1147 Keys must be alphanumerics and start with a letter, values must not
1148 1148 contain '\n' characters"""
1149 1149 firstlinekey = '__firstline'
1150 1150
1151 1151 def __init__(self, vfs, path, keys=None):
1152 1152 self.vfs = vfs
1153 1153 self.path = path
1154 1154
1155 1155 def read(self, firstlinenonkeyval=False):
1156 1156 """Read the contents of a simple key-value file
1157 1157
1158 1158 'firstlinenonkeyval' indicates whether the first line of file should
1159 1159 be treated as a key-value pair or reuturned fully under the
1160 1160 __firstline key."""
1161 1161 lines = self.vfs.readlines(self.path)
1162 1162 d = {}
1163 1163 if firstlinenonkeyval:
1164 1164 if not lines:
1165 1165 e = _("empty simplekeyvalue file")
1166 1166 raise error.CorruptedState(e)
1167 1167 # we don't want to include '\n' in the __firstline
1168 1168 d[self.firstlinekey] = lines[0][:-1]
1169 1169 del lines[0]
1170 1170
1171 1171 try:
1172 1172 # the 'if line.strip()' part prevents us from failing on empty
1173 1173 # lines which only contain '\n' therefore are not skipped
1174 1174 # by 'if line'
1175 1175 updatedict = dict(line[:-1].split('=', 1) for line in lines
1176 1176 if line.strip())
1177 1177 if self.firstlinekey in updatedict:
1178 1178 e = _("%r can't be used as a key")
1179 1179 raise error.CorruptedState(e % self.firstlinekey)
1180 1180 d.update(updatedict)
1181 1181 except ValueError as e:
1182 1182 raise error.CorruptedState(str(e))
1183 1183 return d
1184 1184
1185 1185 def write(self, data, firstline=None):
1186 1186 """Write key=>value mapping to a file
1187 1187 data is a dict. Keys must be alphanumerical and start with a letter.
1188 1188 Values must not contain newline characters.
1189 1189
1190 1190 If 'firstline' is not None, it is written to file before
1191 1191 everything else, as it is, not in a key=value form"""
1192 1192 lines = []
1193 1193 if firstline is not None:
1194 1194 lines.append('%s\n' % firstline)
1195 1195
1196 1196 for k, v in data.items():
1197 1197 if k == self.firstlinekey:
1198 1198 e = "key name '%s' is reserved" % self.firstlinekey
1199 1199 raise error.ProgrammingError(e)
1200 1200 if not k[0].isalpha():
1201 1201 e = "keys must start with a letter in a key-value file"
1202 1202 raise error.ProgrammingError(e)
1203 1203 if not k.isalnum():
1204 1204 e = "invalid key name in a simple key-value file"
1205 1205 raise error.ProgrammingError(e)
1206 1206 if '\n' in v:
1207 1207 e = "invalid value in a simple key-value file"
1208 1208 raise error.ProgrammingError(e)
1209 1209 lines.append("%s=%s\n" % (k, v))
1210 1210 with self.vfs(self.path, mode='wb', atomictemp=True) as fp:
1211 1211 fp.write(''.join(lines))
1212 1212
1213 1213 _reportobsoletedsource = [
1214 1214 'debugobsolete',
1215 1215 'pull',
1216 1216 'push',
1217 1217 'serve',
1218 1218 'unbundle',
1219 1219 ]
1220 1220
1221 1221 _reportnewcssource = [
1222 1222 'pull',
1223 1223 'unbundle',
1224 1224 ]
1225 1225
1226 1226 def registersummarycallback(repo, otr, txnname=''):
1227 1227 """register a callback to issue a summary after the transaction is closed
1228 1228 """
1229 1229 def txmatch(sources):
1230 1230 return any(txnname.startswith(source) for source in sources)
1231 1231
1232 1232 categories = []
1233 1233
1234 1234 def reportsummary(func):
1235 1235 """decorator for report callbacks."""
1236 1236 # The repoview life cycle is shorter than the one of the actual
1237 1237 # underlying repository. So the filtered object can die before the
1238 1238 # weakref is used leading to troubles. We keep a reference to the
1239 1239 # unfiltered object and restore the filtering when retrieving the
1240 1240 # repository through the weakref.
1241 1241 filtername = repo.filtername
1242 1242 reporef = weakref.ref(repo.unfiltered())
1243 1243 def wrapped(tr):
1244 1244 repo = reporef()
1245 1245 if filtername:
1246 1246 repo = repo.filtered(filtername)
1247 1247 func(repo, tr)
1248 1248 newcat = '%2i-txnreport' % len(categories)
1249 1249 otr.addpostclose(newcat, wrapped)
1250 1250 categories.append(newcat)
1251 1251 return wrapped
1252 1252
1253 1253 if txmatch(_reportobsoletedsource):
1254 1254 @reportsummary
1255 1255 def reportobsoleted(repo, tr):
1256 1256 obsoleted = obsutil.getobsoleted(repo, tr)
1257 1257 if obsoleted:
1258 1258 repo.ui.status(_('obsoleted %i changesets\n')
1259 1259 % len(obsoleted))
1260 1260
1261 1261 if txmatch(_reportnewcssource):
1262 1262 @reportsummary
1263 1263 def reportnewcs(repo, tr):
1264 1264 """Report the range of new revisions pulled/unbundled."""
1265 1265 newrevs = list(tr.changes.get('revs', set()))
1266 1266 if not newrevs:
1267 1267 return
1268 1268
1269 1269 # Compute the bounds of new revisions' range, excluding obsoletes.
1270 1270 unfi = repo.unfiltered()
1271 1271 revs = unfi.revs('%ld and not obsolete()', newrevs)
1272 1272 if not revs:
1273 1273 # Got only obsoletes.
1274 1274 return
1275 1275 minrev, maxrev = repo[revs.min()], repo[revs.max()]
1276 1276
1277 1277 if minrev == maxrev:
1278 1278 revrange = minrev
1279 1279 else:
1280 1280 revrange = '%s:%s' % (minrev, maxrev)
1281 1281 repo.ui.status(_('new changesets %s\n') % revrange)
1282 1282
1283 def nodesummaries(repo, nodes, maxnumnodes=4):
1284 if len(nodes) <= maxnumnodes or repo.ui.verbose:
1285 return ' '.join(short(h) for h in nodes)
1286 first = ' '.join(short(h) for h in nodes[:maxnumnodes])
1287 return _("%s and %s others") % (first, len(nodes) - maxnumnodes)
1288
1283 1289 def wrapconvertsink(sink):
1284 1290 """Allow extensions to wrap the sink returned by convcmd.convertsink()
1285 1291 before it is used, whether or not the convert extension was formally loaded.
1286 1292 """
1287 1293 return sink
General Comments 0
You need to be logged in to leave comments. Login now