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