##// END OF EJS Templates
pycompat: switch to util.urlreq/util.urlerr for py3 compat
timeless -
r28883:032c4c2f default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,329 +1,330 b''
1 1 # acl.py - changeset access control for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''hooks for controlling repository access
9 9
10 10 This hook makes it possible to allow or deny write access to given
11 11 branches and paths of a repository when receiving incoming changesets
12 12 via pretxnchangegroup and pretxncommit.
13 13
14 14 The authorization is matched based on the local user name on the
15 15 system where the hook runs, and not the committer of the original
16 16 changeset (since the latter is merely informative).
17 17
18 18 The acl hook is best used along with a restricted shell like hgsh,
19 19 preventing authenticating users from doing anything other than pushing
20 20 or pulling. The hook is not safe to use if users have interactive
21 21 shell access, as they can then disable the hook. Nor is it safe if
22 22 remote users share an account, because then there is no way to
23 23 distinguish them.
24 24
25 25 The order in which access checks are performed is:
26 26
27 27 1) Deny list for branches (section ``acl.deny.branches``)
28 28 2) Allow list for branches (section ``acl.allow.branches``)
29 29 3) Deny list for paths (section ``acl.deny``)
30 30 4) Allow list for paths (section ``acl.allow``)
31 31
32 32 The allow and deny sections take key-value pairs.
33 33
34 34 Branch-based Access Control
35 35 ---------------------------
36 36
37 37 Use the ``acl.deny.branches`` and ``acl.allow.branches`` sections to
38 38 have branch-based access control. Keys in these sections can be
39 39 either:
40 40
41 41 - a branch name, or
42 42 - an asterisk, to match any branch;
43 43
44 44 The corresponding values can be either:
45 45
46 46 - a comma-separated list containing users and groups, or
47 47 - an asterisk, to match anyone;
48 48
49 49 You can add the "!" prefix to a user or group name to invert the sense
50 50 of the match.
51 51
52 52 Path-based Access Control
53 53 -------------------------
54 54
55 55 Use the ``acl.deny`` and ``acl.allow`` sections to have path-based
56 56 access control. Keys in these sections accept a subtree pattern (with
57 57 a glob syntax by default). The corresponding values follow the same
58 58 syntax as the other sections above.
59 59
60 60 Groups
61 61 ------
62 62
63 63 Group names must be prefixed with an ``@`` symbol. Specifying a group
64 64 name has the same effect as specifying all the users in that group.
65 65
66 66 You can define group members in the ``acl.groups`` section.
67 67 If a group name is not defined there, and Mercurial is running under
68 68 a Unix-like system, the list of users will be taken from the OS.
69 69 Otherwise, an exception will be raised.
70 70
71 71 Example Configuration
72 72 ---------------------
73 73
74 74 ::
75 75
76 76 [hooks]
77 77
78 78 # Use this if you want to check access restrictions at commit time
79 79 pretxncommit.acl = python:hgext.acl.hook
80 80
81 81 # Use this if you want to check access restrictions for pull, push,
82 82 # bundle and serve.
83 83 pretxnchangegroup.acl = python:hgext.acl.hook
84 84
85 85 [acl]
86 86 # Allow or deny access for incoming changes only if their source is
87 87 # listed here, let them pass otherwise. Source is "serve" for all
88 88 # remote access (http or ssh), "push", "pull" or "bundle" when the
89 89 # related commands are run locally.
90 90 # Default: serve
91 91 sources = serve
92 92
93 93 [acl.deny.branches]
94 94
95 95 # Everyone is denied to the frozen branch:
96 96 frozen-branch = *
97 97
98 98 # A bad user is denied on all branches:
99 99 * = bad-user
100 100
101 101 [acl.allow.branches]
102 102
103 103 # A few users are allowed on branch-a:
104 104 branch-a = user-1, user-2, user-3
105 105
106 106 # Only one user is allowed on branch-b:
107 107 branch-b = user-1
108 108
109 109 # The super user is allowed on any branch:
110 110 * = super-user
111 111
112 112 # Everyone is allowed on branch-for-tests:
113 113 branch-for-tests = *
114 114
115 115 [acl.deny]
116 116 # This list is checked first. If a match is found, acl.allow is not
117 117 # checked. All users are granted access if acl.deny is not present.
118 118 # Format for both lists: glob pattern = user, ..., @group, ...
119 119
120 120 # To match everyone, use an asterisk for the user:
121 121 # my/glob/pattern = *
122 122
123 123 # user6 will not have write access to any file:
124 124 ** = user6
125 125
126 126 # Group "hg-denied" will not have write access to any file:
127 127 ** = @hg-denied
128 128
129 129 # Nobody will be able to change "DONT-TOUCH-THIS.txt", despite
130 130 # everyone being able to change all other files. See below.
131 131 src/main/resources/DONT-TOUCH-THIS.txt = *
132 132
133 133 [acl.allow]
134 134 # if acl.allow is not present, all users are allowed by default
135 135 # empty acl.allow = no users allowed
136 136
137 137 # User "doc_writer" has write access to any file under the "docs"
138 138 # folder:
139 139 docs/** = doc_writer
140 140
141 141 # User "jack" and group "designers" have write access to any file
142 142 # under the "images" folder:
143 143 images/** = jack, @designers
144 144
145 145 # Everyone (except for "user6" and "@hg-denied" - see acl.deny above)
146 146 # will have write access to any file under the "resources" folder
147 147 # (except for 1 file. See acl.deny):
148 148 src/main/resources/** = *
149 149
150 150 .hgtags = release_engineer
151 151
152 152 Examples using the "!" prefix
153 153 .............................
154 154
155 155 Suppose there's a branch that only a given user (or group) should be able to
156 156 push to, and you don't want to restrict access to any other branch that may
157 157 be created.
158 158
159 159 The "!" prefix allows you to prevent anyone except a given user or group to
160 160 push changesets in a given branch or path.
161 161
162 162 In the examples below, we will:
163 163 1) Deny access to branch "ring" to anyone but user "gollum"
164 164 2) Deny access to branch "lake" to anyone but members of the group "hobbit"
165 165 3) Deny access to a file to anyone but user "gollum"
166 166
167 167 ::
168 168
169 169 [acl.allow.branches]
170 170 # Empty
171 171
172 172 [acl.deny.branches]
173 173
174 174 # 1) only 'gollum' can commit to branch 'ring';
175 175 # 'gollum' and anyone else can still commit to any other branch.
176 176 ring = !gollum
177 177
178 178 # 2) only members of the group 'hobbit' can commit to branch 'lake';
179 179 # 'hobbit' members and anyone else can still commit to any other branch.
180 180 lake = !@hobbit
181 181
182 182 # You can also deny access based on file paths:
183 183
184 184 [acl.allow]
185 185 # Empty
186 186
187 187 [acl.deny]
188 188 # 3) only 'gollum' can change the file below;
189 189 # 'gollum' and anyone else can still change any other file.
190 190 /misty/mountains/cave/ring = !gollum
191 191
192 192 '''
193 193
194 194 from __future__ import absolute_import
195 195
196 196 import getpass
197 import urllib
198 197
199 198 from mercurial.i18n import _
200 199 from mercurial import (
201 200 error,
202 201 match,
203 202 util,
204 203 )
205 204
205 urlreq = util.urlreq
206
206 207 # Note for extension authors: ONLY specify testedwith = 'internal' for
207 208 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
208 209 # be specifying the version(s) of Mercurial they are tested with, or
209 210 # leave the attribute unspecified.
210 211 testedwith = 'internal'
211 212
212 213 def _getusers(ui, group):
213 214
214 215 # First, try to use group definition from section [acl.groups]
215 216 hgrcusers = ui.configlist('acl.groups', group)
216 217 if hgrcusers:
217 218 return hgrcusers
218 219
219 220 ui.debug('acl: "%s" not defined in [acl.groups]\n' % group)
220 221 # If no users found in group definition, get users from OS-level group
221 222 try:
222 223 return util.groupmembers(group)
223 224 except KeyError:
224 225 raise error.Abort(_("group '%s' is undefined") % group)
225 226
226 227 def _usermatch(ui, user, usersorgroups):
227 228
228 229 if usersorgroups == '*':
229 230 return True
230 231
231 232 for ug in usersorgroups.replace(',', ' ').split():
232 233
233 234 if ug.startswith('!'):
234 235 # Test for excluded user or group. Format:
235 236 # if ug is a user name: !username
236 237 # if ug is a group name: !@groupname
237 238 ug = ug[1:]
238 239 if not ug.startswith('@') and user != ug \
239 240 or ug.startswith('@') and user not in _getusers(ui, ug[1:]):
240 241 return True
241 242
242 243 # Test for user or group. Format:
243 244 # if ug is a user name: username
244 245 # if ug is a group name: @groupname
245 246 elif user == ug \
246 247 or ug.startswith('@') and user in _getusers(ui, ug[1:]):
247 248 return True
248 249
249 250 return False
250 251
251 252 def buildmatch(ui, repo, user, key):
252 253 '''return tuple of (match function, list enabled).'''
253 254 if not ui.has_section(key):
254 255 ui.debug('acl: %s not enabled\n' % key)
255 256 return None
256 257
257 258 pats = [pat for pat, users in ui.configitems(key)
258 259 if _usermatch(ui, user, users)]
259 260 ui.debug('acl: %s enabled, %d entries for user %s\n' %
260 261 (key, len(pats), user))
261 262
262 263 # Branch-based ACL
263 264 if not repo:
264 265 if pats:
265 266 # If there's an asterisk (meaning "any branch"), always return True;
266 267 # Otherwise, test if b is in pats
267 268 if '*' in pats:
268 269 return util.always
269 270 return lambda b: b in pats
270 271 return util.never
271 272
272 273 # Path-based ACL
273 274 if pats:
274 275 return match.match(repo.root, '', pats)
275 276 return util.never
276 277
277 278 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
278 279 if hooktype not in ['pretxnchangegroup', 'pretxncommit']:
279 280 raise error.Abort(_('config error - hook type "%s" cannot stop '
280 281 'incoming changesets nor commits') % hooktype)
281 282 if (hooktype == 'pretxnchangegroup' and
282 283 source not in ui.config('acl', 'sources', 'serve').split()):
283 284 ui.debug('acl: changes have source "%s" - skipping\n' % source)
284 285 return
285 286
286 287 user = None
287 288 if source == 'serve' and 'url' in kwargs:
288 289 url = kwargs['url'].split(':')
289 290 if url[0] == 'remote' and url[1].startswith('http'):
290 user = urllib.unquote(url[3])
291 user = urlreq.unquote(url[3])
291 292
292 293 if user is None:
293 294 user = getpass.getuser()
294 295
295 296 ui.debug('acl: checking access for user "%s"\n' % user)
296 297
297 298 # deprecated config: acl.config
298 299 cfg = ui.config('acl', 'config')
299 300 if cfg:
300 301 ui.readconfig(cfg, sections=['acl.groups', 'acl.allow.branches',
301 302 'acl.deny.branches', 'acl.allow', 'acl.deny'])
302 303
303 304 allowbranches = buildmatch(ui, None, user, 'acl.allow.branches')
304 305 denybranches = buildmatch(ui, None, user, 'acl.deny.branches')
305 306 allow = buildmatch(ui, repo, user, 'acl.allow')
306 307 deny = buildmatch(ui, repo, user, 'acl.deny')
307 308
308 309 for rev in xrange(repo[node], len(repo)):
309 310 ctx = repo[rev]
310 311 branch = ctx.branch()
311 312 if denybranches and denybranches(branch):
312 313 raise error.Abort(_('acl: user "%s" denied on branch "%s"'
313 314 ' (changeset "%s")')
314 315 % (user, branch, ctx))
315 316 if allowbranches and not allowbranches(branch):
316 317 raise error.Abort(_('acl: user "%s" not allowed on branch "%s"'
317 318 ' (changeset "%s")')
318 319 % (user, branch, ctx))
319 320 ui.debug('acl: branch access granted: "%s" on branch "%s"\n'
320 321 % (ctx, branch))
321 322
322 323 for f in ctx.files():
323 324 if deny and deny(f):
324 325 raise error.Abort(_('acl: user "%s" denied on "%s"'
325 326 ' (changeset "%s")') % (user, f, ctx))
326 327 if allow and not allow(f):
327 328 raise error.Abort(_('acl: user "%s" not allowed on "%s"'
328 329 ' (changeset "%s")') % (user, f, ctx))
329 330 ui.debug('acl: path access granted: "%s"\n' % ctx)
@@ -1,1354 +1,1354 b''
1 1 # Subversion 1.4/1.5 Python API backend
2 2 #
3 3 # Copyright(C) 2007 Daniel Holth et al
4 4 from __future__ import absolute_import
5 5
6 6 import cPickle as pickle
7 7 import os
8 8 import re
9 9 import sys
10 10 import tempfile
11 import urllib
12 import urllib2
13 11 import xml.dom.minidom
14 12
15 13 from mercurial import (
16 14 encoding,
17 15 error,
18 16 scmutil,
19 17 strutil,
20 18 util,
21 19 )
22 20 from mercurial.i18n import _
23 21
24 22 from . import common
25 23
26 24 stringio = util.stringio
27 25 propertycache = util.propertycache
26 urlerr = util.urlerr
27 urlreq = util.urlreq
28 28
29 29 commandline = common.commandline
30 30 commit = common.commit
31 31 converter_sink = common.converter_sink
32 32 converter_source = common.converter_source
33 33 decodeargs = common.decodeargs
34 34 encodeargs = common.encodeargs
35 35 makedatetimestamp = common.makedatetimestamp
36 36 mapfile = common.mapfile
37 37 MissingTool = common.MissingTool
38 38 NoRepo = common.NoRepo
39 39
40 40 # Subversion stuff. Works best with very recent Python SVN bindings
41 41 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
42 42 # these bindings.
43 43
44 44 try:
45 45 import svn
46 46 import svn.client
47 47 import svn.core
48 48 import svn.ra
49 49 import svn.delta
50 50 from . import transport
51 51 import warnings
52 52 warnings.filterwarnings('ignore',
53 53 module='svn.core',
54 54 category=DeprecationWarning)
55 55 svn.core.SubversionException # trigger import to catch error
56 56
57 57 except ImportError:
58 58 svn = None
59 59
60 60 class SvnPathNotFound(Exception):
61 61 pass
62 62
63 63 def revsplit(rev):
64 64 """Parse a revision string and return (uuid, path, revnum).
65 65 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
66 66 ... '/proj%20B/mytrunk/mytrunk@1')
67 67 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
68 68 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
69 69 ('', '', 1)
70 70 >>> revsplit('@7')
71 71 ('', '', 7)
72 72 >>> revsplit('7')
73 73 ('', '', 0)
74 74 >>> revsplit('bad')
75 75 ('', '', 0)
76 76 """
77 77 parts = rev.rsplit('@', 1)
78 78 revnum = 0
79 79 if len(parts) > 1:
80 80 revnum = int(parts[1])
81 81 parts = parts[0].split('/', 1)
82 82 uuid = ''
83 83 mod = ''
84 84 if len(parts) > 1 and parts[0].startswith('svn:'):
85 85 uuid = parts[0][4:]
86 86 mod = '/' + parts[1]
87 87 return uuid, mod, revnum
88 88
89 89 def quote(s):
90 90 # As of svn 1.7, many svn calls expect "canonical" paths. In
91 91 # theory, we should call svn.core.*canonicalize() on all paths
92 92 # before passing them to the API. Instead, we assume the base url
93 93 # is canonical and copy the behaviour of svn URL encoding function
94 94 # so we can extend it safely with new components. The "safe"
95 95 # characters were taken from the "svn_uri__char_validity" table in
96 96 # libsvn_subr/path.c.
97 return urllib.quote(s, "!$&'()*+,-./:=@_~")
97 return urlreq.quote(s, "!$&'()*+,-./:=@_~")
98 98
99 99 def geturl(path):
100 100 try:
101 101 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
102 102 except svn.core.SubversionException:
103 103 # svn.client.url_from_path() fails with local repositories
104 104 pass
105 105 if os.path.isdir(path):
106 106 path = os.path.normpath(os.path.abspath(path))
107 107 if os.name == 'nt':
108 108 path = '/' + util.normpath(path)
109 109 # Module URL is later compared with the repository URL returned
110 110 # by svn API, which is UTF-8.
111 111 path = encoding.tolocal(path)
112 112 path = 'file://%s' % quote(path)
113 113 return svn.core.svn_path_canonicalize(path)
114 114
115 115 def optrev(number):
116 116 optrev = svn.core.svn_opt_revision_t()
117 117 optrev.kind = svn.core.svn_opt_revision_number
118 118 optrev.value.number = number
119 119 return optrev
120 120
121 121 class changedpath(object):
122 122 def __init__(self, p):
123 123 self.copyfrom_path = p.copyfrom_path
124 124 self.copyfrom_rev = p.copyfrom_rev
125 125 self.action = p.action
126 126
127 127 def get_log_child(fp, url, paths, start, end, limit=0,
128 128 discover_changed_paths=True, strict_node_history=False):
129 129 protocol = -1
130 130 def receiver(orig_paths, revnum, author, date, message, pool):
131 131 paths = {}
132 132 if orig_paths is not None:
133 133 for k, v in orig_paths.iteritems():
134 134 paths[k] = changedpath(v)
135 135 pickle.dump((paths, revnum, author, date, message),
136 136 fp, protocol)
137 137
138 138 try:
139 139 # Use an ra of our own so that our parent can consume
140 140 # our results without confusing the server.
141 141 t = transport.SvnRaTransport(url=url)
142 142 svn.ra.get_log(t.ra, paths, start, end, limit,
143 143 discover_changed_paths,
144 144 strict_node_history,
145 145 receiver)
146 146 except IOError:
147 147 # Caller may interrupt the iteration
148 148 pickle.dump(None, fp, protocol)
149 149 except Exception as inst:
150 150 pickle.dump(str(inst), fp, protocol)
151 151 else:
152 152 pickle.dump(None, fp, protocol)
153 153 fp.close()
154 154 # With large history, cleanup process goes crazy and suddenly
155 155 # consumes *huge* amount of memory. The output file being closed,
156 156 # there is no need for clean termination.
157 157 os._exit(0)
158 158
159 159 def debugsvnlog(ui, **opts):
160 160 """Fetch SVN log in a subprocess and channel them back to parent to
161 161 avoid memory collection issues.
162 162 """
163 163 if svn is None:
164 164 raise error.Abort(_('debugsvnlog could not load Subversion python '
165 165 'bindings'))
166 166
167 167 util.setbinary(sys.stdin)
168 168 util.setbinary(sys.stdout)
169 169 args = decodeargs(sys.stdin.read())
170 170 get_log_child(sys.stdout, *args)
171 171
172 172 class logstream(object):
173 173 """Interruptible revision log iterator."""
174 174 def __init__(self, stdout):
175 175 self._stdout = stdout
176 176
177 177 def __iter__(self):
178 178 while True:
179 179 try:
180 180 entry = pickle.load(self._stdout)
181 181 except EOFError:
182 182 raise error.Abort(_('Mercurial failed to run itself, check'
183 183 ' hg executable is in PATH'))
184 184 try:
185 185 orig_paths, revnum, author, date, message = entry
186 186 except (TypeError, ValueError):
187 187 if entry is None:
188 188 break
189 189 raise error.Abort(_("log stream exception '%s'") % entry)
190 190 yield entry
191 191
192 192 def close(self):
193 193 if self._stdout:
194 194 self._stdout.close()
195 195 self._stdout = None
196 196
197 197 class directlogstream(list):
198 198 """Direct revision log iterator.
199 199 This can be used for debugging and development but it will probably leak
200 200 memory and is not suitable for real conversions."""
201 201 def __init__(self, url, paths, start, end, limit=0,
202 202 discover_changed_paths=True, strict_node_history=False):
203 203
204 204 def receiver(orig_paths, revnum, author, date, message, pool):
205 205 paths = {}
206 206 if orig_paths is not None:
207 207 for k, v in orig_paths.iteritems():
208 208 paths[k] = changedpath(v)
209 209 self.append((paths, revnum, author, date, message))
210 210
211 211 # Use an ra of our own so that our parent can consume
212 212 # our results without confusing the server.
213 213 t = transport.SvnRaTransport(url=url)
214 214 svn.ra.get_log(t.ra, paths, start, end, limit,
215 215 discover_changed_paths,
216 216 strict_node_history,
217 217 receiver)
218 218
219 219 def close(self):
220 220 pass
221 221
222 222 # Check to see if the given path is a local Subversion repo. Verify this by
223 223 # looking for several svn-specific files and directories in the given
224 224 # directory.
225 225 def filecheck(ui, path, proto):
226 226 for x in ('locks', 'hooks', 'format', 'db'):
227 227 if not os.path.exists(os.path.join(path, x)):
228 228 return False
229 229 return True
230 230
231 231 # Check to see if a given path is the root of an svn repo over http. We verify
232 232 # this by requesting a version-controlled URL we know can't exist and looking
233 233 # for the svn-specific "not found" XML.
234 234 def httpcheck(ui, path, proto):
235 235 try:
236 opener = urllib2.build_opener()
236 opener = urlreq.buildopener()
237 237 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
238 238 data = rsp.read()
239 except urllib2.HTTPError as inst:
239 except urlerr.httperror as inst:
240 240 if inst.code != 404:
241 241 # Except for 404 we cannot know for sure this is not an svn repo
242 242 ui.warn(_('svn: cannot probe remote repository, assume it could '
243 243 'be a subversion repository. Use --source-type if you '
244 244 'know better.\n'))
245 245 return True
246 246 data = inst.fp.read()
247 247 except Exception:
248 # Could be urllib2.URLError if the URL is invalid or anything else.
248 # Could be urlerr.urlerror if the URL is invalid or anything else.
249 249 return False
250 250 return '<m:human-readable errcode="160013">' in data
251 251
252 252 protomap = {'http': httpcheck,
253 253 'https': httpcheck,
254 254 'file': filecheck,
255 255 }
256 256 def issvnurl(ui, url):
257 257 try:
258 258 proto, path = url.split('://', 1)
259 259 if proto == 'file':
260 260 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
261 261 and path[2:6].lower() == '%3a/'):
262 262 path = path[:2] + ':/' + path[6:]
263 path = urllib.url2pathname(path)
263 path = urlreq.url2pathname(path)
264 264 except ValueError:
265 265 proto = 'file'
266 266 path = os.path.abspath(url)
267 267 if proto == 'file':
268 268 path = util.pconvert(path)
269 269 check = protomap.get(proto, lambda *args: False)
270 270 while '/' in path:
271 271 if check(ui, path, proto):
272 272 return True
273 273 path = path.rsplit('/', 1)[0]
274 274 return False
275 275
276 276 # SVN conversion code stolen from bzr-svn and tailor
277 277 #
278 278 # Subversion looks like a versioned filesystem, branches structures
279 279 # are defined by conventions and not enforced by the tool. First,
280 280 # we define the potential branches (modules) as "trunk" and "branches"
281 281 # children directories. Revisions are then identified by their
282 282 # module and revision number (and a repository identifier).
283 283 #
284 284 # The revision graph is really a tree (or a forest). By default, a
285 285 # revision parent is the previous revision in the same module. If the
286 286 # module directory is copied/moved from another module then the
287 287 # revision is the module root and its parent the source revision in
288 288 # the parent module. A revision has at most one parent.
289 289 #
290 290 class svn_source(converter_source):
291 291 def __init__(self, ui, url, revs=None):
292 292 super(svn_source, self).__init__(ui, url, revs=revs)
293 293
294 294 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
295 295 (os.path.exists(url) and
296 296 os.path.exists(os.path.join(url, '.svn'))) or
297 297 issvnurl(ui, url)):
298 298 raise NoRepo(_("%s does not look like a Subversion repository")
299 299 % url)
300 300 if svn is None:
301 301 raise MissingTool(_('could not load Subversion python bindings'))
302 302
303 303 try:
304 304 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
305 305 if version < (1, 4):
306 306 raise MissingTool(_('Subversion python bindings %d.%d found, '
307 307 '1.4 or later required') % version)
308 308 except AttributeError:
309 309 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
310 310 'or later required'))
311 311
312 312 self.lastrevs = {}
313 313
314 314 latest = None
315 315 try:
316 316 # Support file://path@rev syntax. Useful e.g. to convert
317 317 # deleted branches.
318 318 at = url.rfind('@')
319 319 if at >= 0:
320 320 latest = int(url[at + 1:])
321 321 url = url[:at]
322 322 except ValueError:
323 323 pass
324 324 self.url = geturl(url)
325 325 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
326 326 try:
327 327 self.transport = transport.SvnRaTransport(url=self.url)
328 328 self.ra = self.transport.ra
329 329 self.ctx = self.transport.client
330 330 self.baseurl = svn.ra.get_repos_root(self.ra)
331 331 # Module is either empty or a repository path starting with
332 332 # a slash and not ending with a slash.
333 self.module = urllib.unquote(self.url[len(self.baseurl):])
333 self.module = urlreq.unquote(self.url[len(self.baseurl):])
334 334 self.prevmodule = None
335 335 self.rootmodule = self.module
336 336 self.commits = {}
337 337 self.paths = {}
338 338 self.uuid = svn.ra.get_uuid(self.ra)
339 339 except svn.core.SubversionException:
340 340 ui.traceback()
341 341 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
342 342 svn.core.SVN_VER_MINOR,
343 343 svn.core.SVN_VER_MICRO)
344 344 raise NoRepo(_("%s does not look like a Subversion repository "
345 345 "to libsvn version %s")
346 346 % (self.url, svnversion))
347 347
348 348 if revs:
349 349 if len(revs) > 1:
350 350 raise error.Abort(_('subversion source does not support '
351 351 'specifying multiple revisions'))
352 352 try:
353 353 latest = int(revs[0])
354 354 except ValueError:
355 355 raise error.Abort(_('svn: revision %s is not an integer') %
356 356 revs[0])
357 357
358 358 self.trunkname = self.ui.config('convert', 'svn.trunk',
359 359 'trunk').strip('/')
360 360 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
361 361 try:
362 362 self.startrev = int(self.startrev)
363 363 if self.startrev < 0:
364 364 self.startrev = 0
365 365 except ValueError:
366 366 raise error.Abort(_('svn: start revision %s is not an integer')
367 367 % self.startrev)
368 368
369 369 try:
370 370 self.head = self.latest(self.module, latest)
371 371 except SvnPathNotFound:
372 372 self.head = None
373 373 if not self.head:
374 374 raise error.Abort(_('no revision found in module %s')
375 375 % self.module)
376 376 self.last_changed = self.revnum(self.head)
377 377
378 378 self._changescache = (None, None)
379 379
380 380 if os.path.exists(os.path.join(url, '.svn/entries')):
381 381 self.wc = url
382 382 else:
383 383 self.wc = None
384 384 self.convertfp = None
385 385
386 386 def setrevmap(self, revmap):
387 387 lastrevs = {}
388 388 for revid in revmap.iterkeys():
389 389 uuid, module, revnum = revsplit(revid)
390 390 lastrevnum = lastrevs.setdefault(module, revnum)
391 391 if revnum > lastrevnum:
392 392 lastrevs[module] = revnum
393 393 self.lastrevs = lastrevs
394 394
395 395 def exists(self, path, optrev):
396 396 try:
397 397 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
398 398 optrev, False, self.ctx)
399 399 return True
400 400 except svn.core.SubversionException:
401 401 return False
402 402
403 403 def getheads(self):
404 404
405 405 def isdir(path, revnum):
406 406 kind = self._checkpath(path, revnum)
407 407 return kind == svn.core.svn_node_dir
408 408
409 409 def getcfgpath(name, rev):
410 410 cfgpath = self.ui.config('convert', 'svn.' + name)
411 411 if cfgpath is not None and cfgpath.strip() == '':
412 412 return None
413 413 path = (cfgpath or name).strip('/')
414 414 if not self.exists(path, rev):
415 415 if self.module.endswith(path) and name == 'trunk':
416 416 # we are converting from inside this directory
417 417 return None
418 418 if cfgpath:
419 419 raise error.Abort(_('expected %s to be at %r, but not found'
420 420 ) % (name, path))
421 421 return None
422 422 self.ui.note(_('found %s at %r\n') % (name, path))
423 423 return path
424 424
425 425 rev = optrev(self.last_changed)
426 426 oldmodule = ''
427 427 trunk = getcfgpath('trunk', rev)
428 428 self.tags = getcfgpath('tags', rev)
429 429 branches = getcfgpath('branches', rev)
430 430
431 431 # If the project has a trunk or branches, we will extract heads
432 432 # from them. We keep the project root otherwise.
433 433 if trunk:
434 434 oldmodule = self.module or ''
435 435 self.module += '/' + trunk
436 436 self.head = self.latest(self.module, self.last_changed)
437 437 if not self.head:
438 438 raise error.Abort(_('no revision found in module %s')
439 439 % self.module)
440 440
441 441 # First head in the list is the module's head
442 442 self.heads = [self.head]
443 443 if self.tags is not None:
444 444 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
445 445
446 446 # Check if branches bring a few more heads to the list
447 447 if branches:
448 448 rpath = self.url.strip('/')
449 449 branchnames = svn.client.ls(rpath + '/' + quote(branches),
450 450 rev, False, self.ctx)
451 451 for branch in sorted(branchnames):
452 452 module = '%s/%s/%s' % (oldmodule, branches, branch)
453 453 if not isdir(module, self.last_changed):
454 454 continue
455 455 brevid = self.latest(module, self.last_changed)
456 456 if not brevid:
457 457 self.ui.note(_('ignoring empty branch %s\n') % branch)
458 458 continue
459 459 self.ui.note(_('found branch %s at %d\n') %
460 460 (branch, self.revnum(brevid)))
461 461 self.heads.append(brevid)
462 462
463 463 if self.startrev and self.heads:
464 464 if len(self.heads) > 1:
465 465 raise error.Abort(_('svn: start revision is not supported '
466 466 'with more than one branch'))
467 467 revnum = self.revnum(self.heads[0])
468 468 if revnum < self.startrev:
469 469 raise error.Abort(
470 470 _('svn: no revision found after start revision %d')
471 471 % self.startrev)
472 472
473 473 return self.heads
474 474
475 475 def _getchanges(self, rev, full):
476 476 (paths, parents) = self.paths[rev]
477 477 copies = {}
478 478 if parents:
479 479 files, self.removed, copies = self.expandpaths(rev, paths, parents)
480 480 if full or not parents:
481 481 # Perform a full checkout on roots
482 482 uuid, module, revnum = revsplit(rev)
483 483 entries = svn.client.ls(self.baseurl + quote(module),
484 484 optrev(revnum), True, self.ctx)
485 485 files = [n for n, e in entries.iteritems()
486 486 if e.kind == svn.core.svn_node_file]
487 487 self.removed = set()
488 488
489 489 files.sort()
490 490 files = zip(files, [rev] * len(files))
491 491 return (files, copies)
492 492
493 493 def getchanges(self, rev, full):
494 494 # reuse cache from getchangedfiles
495 495 if self._changescache[0] == rev and not full:
496 496 (files, copies) = self._changescache[1]
497 497 else:
498 498 (files, copies) = self._getchanges(rev, full)
499 499 # caller caches the result, so free it here to release memory
500 500 del self.paths[rev]
501 501 return (files, copies, set())
502 502
503 503 def getchangedfiles(self, rev, i):
504 504 # called from filemap - cache computed values for reuse in getchanges
505 505 (files, copies) = self._getchanges(rev, False)
506 506 self._changescache = (rev, (files, copies))
507 507 return [f[0] for f in files]
508 508
509 509 def getcommit(self, rev):
510 510 if rev not in self.commits:
511 511 uuid, module, revnum = revsplit(rev)
512 512 self.module = module
513 513 self.reparent(module)
514 514 # We assume that:
515 515 # - requests for revisions after "stop" come from the
516 516 # revision graph backward traversal. Cache all of them
517 517 # down to stop, they will be used eventually.
518 518 # - requests for revisions before "stop" come to get
519 519 # isolated branches parents. Just fetch what is needed.
520 520 stop = self.lastrevs.get(module, 0)
521 521 if revnum < stop:
522 522 stop = revnum + 1
523 523 self._fetch_revisions(revnum, stop)
524 524 if rev not in self.commits:
525 525 raise error.Abort(_('svn: revision %s not found') % revnum)
526 526 revcommit = self.commits[rev]
527 527 # caller caches the result, so free it here to release memory
528 528 del self.commits[rev]
529 529 return revcommit
530 530
531 531 def checkrevformat(self, revstr, mapname='splicemap'):
532 532 """ fails if revision format does not match the correct format"""
533 533 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
534 534 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
535 535 '{12,12}(.*)\@[0-9]+$',revstr):
536 536 raise error.Abort(_('%s entry %s is not a valid revision'
537 537 ' identifier') % (mapname, revstr))
538 538
539 539 def numcommits(self):
540 540 return int(self.head.rsplit('@', 1)[1]) - self.startrev
541 541
542 542 def gettags(self):
543 543 tags = {}
544 544 if self.tags is None:
545 545 return tags
546 546
547 547 # svn tags are just a convention, project branches left in a
548 548 # 'tags' directory. There is no other relationship than
549 549 # ancestry, which is expensive to discover and makes them hard
550 550 # to update incrementally. Worse, past revisions may be
551 551 # referenced by tags far away in the future, requiring a deep
552 552 # history traversal on every calculation. Current code
553 553 # performs a single backward traversal, tracking moves within
554 554 # the tags directory (tag renaming) and recording a new tag
555 555 # everytime a project is copied from outside the tags
556 556 # directory. It also lists deleted tags, this behaviour may
557 557 # change in the future.
558 558 pendings = []
559 559 tagspath = self.tags
560 560 start = svn.ra.get_latest_revnum(self.ra)
561 561 stream = self._getlog([self.tags], start, self.startrev)
562 562 try:
563 563 for entry in stream:
564 564 origpaths, revnum, author, date, message = entry
565 565 if not origpaths:
566 566 origpaths = []
567 567 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
568 568 in origpaths.iteritems() if e.copyfrom_path]
569 569 # Apply moves/copies from more specific to general
570 570 copies.sort(reverse=True)
571 571
572 572 srctagspath = tagspath
573 573 if copies and copies[-1][2] == tagspath:
574 574 # Track tags directory moves
575 575 srctagspath = copies.pop()[0]
576 576
577 577 for source, sourcerev, dest in copies:
578 578 if not dest.startswith(tagspath + '/'):
579 579 continue
580 580 for tag in pendings:
581 581 if tag[0].startswith(dest):
582 582 tagpath = source + tag[0][len(dest):]
583 583 tag[:2] = [tagpath, sourcerev]
584 584 break
585 585 else:
586 586 pendings.append([source, sourcerev, dest])
587 587
588 588 # Filter out tags with children coming from different
589 589 # parts of the repository like:
590 590 # /tags/tag.1 (from /trunk:10)
591 591 # /tags/tag.1/foo (from /branches/foo:12)
592 592 # Here/tags/tag.1 discarded as well as its children.
593 593 # It happens with tools like cvs2svn. Such tags cannot
594 594 # be represented in mercurial.
595 595 addeds = dict((p, e.copyfrom_path) for p, e
596 596 in origpaths.iteritems()
597 597 if e.action == 'A' and e.copyfrom_path)
598 598 badroots = set()
599 599 for destroot in addeds:
600 600 for source, sourcerev, dest in pendings:
601 601 if (not dest.startswith(destroot + '/')
602 602 or source.startswith(addeds[destroot] + '/')):
603 603 continue
604 604 badroots.add(destroot)
605 605 break
606 606
607 607 for badroot in badroots:
608 608 pendings = [p for p in pendings if p[2] != badroot
609 609 and not p[2].startswith(badroot + '/')]
610 610
611 611 # Tell tag renamings from tag creations
612 612 renamings = []
613 613 for source, sourcerev, dest in pendings:
614 614 tagname = dest.split('/')[-1]
615 615 if source.startswith(srctagspath):
616 616 renamings.append([source, sourcerev, tagname])
617 617 continue
618 618 if tagname in tags:
619 619 # Keep the latest tag value
620 620 continue
621 621 # From revision may be fake, get one with changes
622 622 try:
623 623 tagid = self.latest(source, sourcerev)
624 624 if tagid and tagname not in tags:
625 625 tags[tagname] = tagid
626 626 except SvnPathNotFound:
627 627 # It happens when we are following directories
628 628 # we assumed were copied with their parents
629 629 # but were really created in the tag
630 630 # directory.
631 631 pass
632 632 pendings = renamings
633 633 tagspath = srctagspath
634 634 finally:
635 635 stream.close()
636 636 return tags
637 637
638 638 def converted(self, rev, destrev):
639 639 if not self.wc:
640 640 return
641 641 if self.convertfp is None:
642 642 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
643 643 'a')
644 644 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
645 645 self.convertfp.flush()
646 646
647 647 def revid(self, revnum, module=None):
648 648 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
649 649
650 650 def revnum(self, rev):
651 651 return int(rev.split('@')[-1])
652 652
653 653 def latest(self, path, stop=None):
654 654 """Find the latest revid affecting path, up to stop revision
655 655 number. If stop is None, default to repository latest
656 656 revision. It may return a revision in a different module,
657 657 since a branch may be moved without a change being
658 658 reported. Return None if computed module does not belong to
659 659 rootmodule subtree.
660 660 """
661 661 def findchanges(path, start, stop=None):
662 662 stream = self._getlog([path], start, stop or 1)
663 663 try:
664 664 for entry in stream:
665 665 paths, revnum, author, date, message = entry
666 666 if stop is None and paths:
667 667 # We do not know the latest changed revision,
668 668 # keep the first one with changed paths.
669 669 break
670 670 if revnum <= stop:
671 671 break
672 672
673 673 for p in paths:
674 674 if (not path.startswith(p) or
675 675 not paths[p].copyfrom_path):
676 676 continue
677 677 newpath = paths[p].copyfrom_path + path[len(p):]
678 678 self.ui.debug("branch renamed from %s to %s at %d\n" %
679 679 (path, newpath, revnum))
680 680 path = newpath
681 681 break
682 682 if not paths:
683 683 revnum = None
684 684 return revnum, path
685 685 finally:
686 686 stream.close()
687 687
688 688 if not path.startswith(self.rootmodule):
689 689 # Requests on foreign branches may be forbidden at server level
690 690 self.ui.debug('ignoring foreign branch %r\n' % path)
691 691 return None
692 692
693 693 if stop is None:
694 694 stop = svn.ra.get_latest_revnum(self.ra)
695 695 try:
696 696 prevmodule = self.reparent('')
697 697 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
698 698 self.reparent(prevmodule)
699 699 except svn.core.SubversionException:
700 700 dirent = None
701 701 if not dirent:
702 702 raise SvnPathNotFound(_('%s not found up to revision %d')
703 703 % (path, stop))
704 704
705 705 # stat() gives us the previous revision on this line of
706 706 # development, but it might be in *another module*. Fetch the
707 707 # log and detect renames down to the latest revision.
708 708 revnum, realpath = findchanges(path, stop, dirent.created_rev)
709 709 if revnum is None:
710 710 # Tools like svnsync can create empty revision, when
711 711 # synchronizing only a subtree for instance. These empty
712 712 # revisions created_rev still have their original values
713 713 # despite all changes having disappeared and can be
714 714 # returned by ra.stat(), at least when stating the root
715 715 # module. In that case, do not trust created_rev and scan
716 716 # the whole history.
717 717 revnum, realpath = findchanges(path, stop)
718 718 if revnum is None:
719 719 self.ui.debug('ignoring empty branch %r\n' % realpath)
720 720 return None
721 721
722 722 if not realpath.startswith(self.rootmodule):
723 723 self.ui.debug('ignoring foreign branch %r\n' % realpath)
724 724 return None
725 725 return self.revid(revnum, realpath)
726 726
727 727 def reparent(self, module):
728 728 """Reparent the svn transport and return the previous parent."""
729 729 if self.prevmodule == module:
730 730 return module
731 731 svnurl = self.baseurl + quote(module)
732 732 prevmodule = self.prevmodule
733 733 if prevmodule is None:
734 734 prevmodule = ''
735 735 self.ui.debug("reparent to %s\n" % svnurl)
736 736 svn.ra.reparent(self.ra, svnurl)
737 737 self.prevmodule = module
738 738 return prevmodule
739 739
740 740 def expandpaths(self, rev, paths, parents):
741 741 changed, removed = set(), set()
742 742 copies = {}
743 743
744 744 new_module, revnum = revsplit(rev)[1:]
745 745 if new_module != self.module:
746 746 self.module = new_module
747 747 self.reparent(self.module)
748 748
749 749 for i, (path, ent) in enumerate(paths):
750 750 self.ui.progress(_('scanning paths'), i, item=path,
751 751 total=len(paths), unit=_('paths'))
752 752 entrypath = self.getrelpath(path)
753 753
754 754 kind = self._checkpath(entrypath, revnum)
755 755 if kind == svn.core.svn_node_file:
756 756 changed.add(self.recode(entrypath))
757 757 if not ent.copyfrom_path or not parents:
758 758 continue
759 759 # Copy sources not in parent revisions cannot be
760 760 # represented, ignore their origin for now
761 761 pmodule, prevnum = revsplit(parents[0])[1:]
762 762 if ent.copyfrom_rev < prevnum:
763 763 continue
764 764 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
765 765 if not copyfrom_path:
766 766 continue
767 767 self.ui.debug("copied to %s from %s@%s\n" %
768 768 (entrypath, copyfrom_path, ent.copyfrom_rev))
769 769 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
770 770 elif kind == 0: # gone, but had better be a deleted *file*
771 771 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
772 772 pmodule, prevnum = revsplit(parents[0])[1:]
773 773 parentpath = pmodule + "/" + entrypath
774 774 fromkind = self._checkpath(entrypath, prevnum, pmodule)
775 775
776 776 if fromkind == svn.core.svn_node_file:
777 777 removed.add(self.recode(entrypath))
778 778 elif fromkind == svn.core.svn_node_dir:
779 779 oroot = parentpath.strip('/')
780 780 nroot = path.strip('/')
781 781 children = self._iterfiles(oroot, prevnum)
782 782 for childpath in children:
783 783 childpath = childpath.replace(oroot, nroot)
784 784 childpath = self.getrelpath("/" + childpath, pmodule)
785 785 if childpath:
786 786 removed.add(self.recode(childpath))
787 787 else:
788 788 self.ui.debug('unknown path in revision %d: %s\n' % \
789 789 (revnum, path))
790 790 elif kind == svn.core.svn_node_dir:
791 791 if ent.action == 'M':
792 792 # If the directory just had a prop change,
793 793 # then we shouldn't need to look for its children.
794 794 continue
795 795 if ent.action == 'R' and parents:
796 796 # If a directory is replacing a file, mark the previous
797 797 # file as deleted
798 798 pmodule, prevnum = revsplit(parents[0])[1:]
799 799 pkind = self._checkpath(entrypath, prevnum, pmodule)
800 800 if pkind == svn.core.svn_node_file:
801 801 removed.add(self.recode(entrypath))
802 802 elif pkind == svn.core.svn_node_dir:
803 803 # We do not know what files were kept or removed,
804 804 # mark them all as changed.
805 805 for childpath in self._iterfiles(pmodule, prevnum):
806 806 childpath = self.getrelpath("/" + childpath)
807 807 if childpath:
808 808 changed.add(self.recode(childpath))
809 809
810 810 for childpath in self._iterfiles(path, revnum):
811 811 childpath = self.getrelpath("/" + childpath)
812 812 if childpath:
813 813 changed.add(self.recode(childpath))
814 814
815 815 # Handle directory copies
816 816 if not ent.copyfrom_path or not parents:
817 817 continue
818 818 # Copy sources not in parent revisions cannot be
819 819 # represented, ignore their origin for now
820 820 pmodule, prevnum = revsplit(parents[0])[1:]
821 821 if ent.copyfrom_rev < prevnum:
822 822 continue
823 823 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
824 824 if not copyfrompath:
825 825 continue
826 826 self.ui.debug("mark %s came from %s:%d\n"
827 827 % (path, copyfrompath, ent.copyfrom_rev))
828 828 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
829 829 for childpath in children:
830 830 childpath = self.getrelpath("/" + childpath, pmodule)
831 831 if not childpath:
832 832 continue
833 833 copytopath = path + childpath[len(copyfrompath):]
834 834 copytopath = self.getrelpath(copytopath)
835 835 copies[self.recode(copytopath)] = self.recode(childpath)
836 836
837 837 self.ui.progress(_('scanning paths'), None)
838 838 changed.update(removed)
839 839 return (list(changed), removed, copies)
840 840
841 841 def _fetch_revisions(self, from_revnum, to_revnum):
842 842 if from_revnum < to_revnum:
843 843 from_revnum, to_revnum = to_revnum, from_revnum
844 844
845 845 self.child_cset = None
846 846
847 847 def parselogentry(orig_paths, revnum, author, date, message):
848 848 """Return the parsed commit object or None, and True if
849 849 the revision is a branch root.
850 850 """
851 851 self.ui.debug("parsing revision %d (%d changes)\n" %
852 852 (revnum, len(orig_paths)))
853 853
854 854 branched = False
855 855 rev = self.revid(revnum)
856 856 # branch log might return entries for a parent we already have
857 857
858 858 if rev in self.commits or revnum < to_revnum:
859 859 return None, branched
860 860
861 861 parents = []
862 862 # check whether this revision is the start of a branch or part
863 863 # of a branch renaming
864 864 orig_paths = sorted(orig_paths.iteritems())
865 865 root_paths = [(p, e) for p, e in orig_paths
866 866 if self.module.startswith(p)]
867 867 if root_paths:
868 868 path, ent = root_paths[-1]
869 869 if ent.copyfrom_path:
870 870 branched = True
871 871 newpath = ent.copyfrom_path + self.module[len(path):]
872 872 # ent.copyfrom_rev may not be the actual last revision
873 873 previd = self.latest(newpath, ent.copyfrom_rev)
874 874 if previd is not None:
875 875 prevmodule, prevnum = revsplit(previd)[1:]
876 876 if prevnum >= self.startrev:
877 877 parents = [previd]
878 878 self.ui.note(
879 879 _('found parent of branch %s at %d: %s\n') %
880 880 (self.module, prevnum, prevmodule))
881 881 else:
882 882 self.ui.debug("no copyfrom path, don't know what to do.\n")
883 883
884 884 paths = []
885 885 # filter out unrelated paths
886 886 for path, ent in orig_paths:
887 887 if self.getrelpath(path) is None:
888 888 continue
889 889 paths.append((path, ent))
890 890
891 891 # Example SVN datetime. Includes microseconds.
892 892 # ISO-8601 conformant
893 893 # '2007-01-04T17:35:00.902377Z'
894 894 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
895 895 if self.ui.configbool('convert', 'localtimezone'):
896 896 date = makedatetimestamp(date[0])
897 897
898 898 if message:
899 899 log = self.recode(message)
900 900 else:
901 901 log = ''
902 902
903 903 if author:
904 904 author = self.recode(author)
905 905 else:
906 906 author = ''
907 907
908 908 try:
909 909 branch = self.module.split("/")[-1]
910 910 if branch == self.trunkname:
911 911 branch = None
912 912 except IndexError:
913 913 branch = None
914 914
915 915 cset = commit(author=author,
916 916 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
917 917 desc=log,
918 918 parents=parents,
919 919 branch=branch,
920 920 rev=rev)
921 921
922 922 self.commits[rev] = cset
923 923 # The parents list is *shared* among self.paths and the
924 924 # commit object. Both will be updated below.
925 925 self.paths[rev] = (paths, cset.parents)
926 926 if self.child_cset and not self.child_cset.parents:
927 927 self.child_cset.parents[:] = [rev]
928 928 self.child_cset = cset
929 929 return cset, branched
930 930
931 931 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
932 932 (self.module, from_revnum, to_revnum))
933 933
934 934 try:
935 935 firstcset = None
936 936 lastonbranch = False
937 937 stream = self._getlog([self.module], from_revnum, to_revnum)
938 938 try:
939 939 for entry in stream:
940 940 paths, revnum, author, date, message = entry
941 941 if revnum < self.startrev:
942 942 lastonbranch = True
943 943 break
944 944 if not paths:
945 945 self.ui.debug('revision %d has no entries\n' % revnum)
946 946 # If we ever leave the loop on an empty
947 947 # revision, do not try to get a parent branch
948 948 lastonbranch = lastonbranch or revnum == 0
949 949 continue
950 950 cset, lastonbranch = parselogentry(paths, revnum, author,
951 951 date, message)
952 952 if cset:
953 953 firstcset = cset
954 954 if lastonbranch:
955 955 break
956 956 finally:
957 957 stream.close()
958 958
959 959 if not lastonbranch and firstcset and not firstcset.parents:
960 960 # The first revision of the sequence (the last fetched one)
961 961 # has invalid parents if not a branch root. Find the parent
962 962 # revision now, if any.
963 963 try:
964 964 firstrevnum = self.revnum(firstcset.rev)
965 965 if firstrevnum > 1:
966 966 latest = self.latest(self.module, firstrevnum - 1)
967 967 if latest:
968 968 firstcset.parents.append(latest)
969 969 except SvnPathNotFound:
970 970 pass
971 971 except svn.core.SubversionException as xxx_todo_changeme:
972 972 (inst, num) = xxx_todo_changeme.args
973 973 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
974 974 raise error.Abort(_('svn: branch has no revision %s')
975 975 % to_revnum)
976 976 raise
977 977
978 978 def getfile(self, file, rev):
979 979 # TODO: ra.get_file transmits the whole file instead of diffs.
980 980 if file in self.removed:
981 981 return None, None
982 982 mode = ''
983 983 try:
984 984 new_module, revnum = revsplit(rev)[1:]
985 985 if self.module != new_module:
986 986 self.module = new_module
987 987 self.reparent(self.module)
988 988 io = stringio()
989 989 info = svn.ra.get_file(self.ra, file, revnum, io)
990 990 data = io.getvalue()
991 991 # ra.get_file() seems to keep a reference on the input buffer
992 992 # preventing collection. Release it explicitly.
993 993 io.close()
994 994 if isinstance(info, list):
995 995 info = info[-1]
996 996 mode = ("svn:executable" in info) and 'x' or ''
997 997 mode = ("svn:special" in info) and 'l' or mode
998 998 except svn.core.SubversionException as e:
999 999 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
1000 1000 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
1001 1001 if e.apr_err in notfound: # File not found
1002 1002 return None, None
1003 1003 raise
1004 1004 if mode == 'l':
1005 1005 link_prefix = "link "
1006 1006 if data.startswith(link_prefix):
1007 1007 data = data[len(link_prefix):]
1008 1008 return data, mode
1009 1009
1010 1010 def _iterfiles(self, path, revnum):
1011 1011 """Enumerate all files in path at revnum, recursively."""
1012 1012 path = path.strip('/')
1013 1013 pool = svn.core.Pool()
1014 1014 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
1015 1015 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1016 1016 if path:
1017 1017 path += '/'
1018 1018 return ((path + p) for p, e in entries.iteritems()
1019 1019 if e.kind == svn.core.svn_node_file)
1020 1020
1021 1021 def getrelpath(self, path, module=None):
1022 1022 if module is None:
1023 1023 module = self.module
1024 1024 # Given the repository url of this wc, say
1025 1025 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1026 1026 # extract the "entry" portion (a relative path) from what
1027 1027 # svn log --xml says, i.e.
1028 1028 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1029 1029 # that is to say "tests/PloneTestCase.py"
1030 1030 if path.startswith(module):
1031 1031 relative = path.rstrip('/')[len(module):]
1032 1032 if relative.startswith('/'):
1033 1033 return relative[1:]
1034 1034 elif relative == '':
1035 1035 return relative
1036 1036
1037 1037 # The path is outside our tracked tree...
1038 1038 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1039 1039 return None
1040 1040
1041 1041 def _checkpath(self, path, revnum, module=None):
1042 1042 if module is not None:
1043 1043 prevmodule = self.reparent('')
1044 1044 path = module + '/' + path
1045 1045 try:
1046 1046 # ra.check_path does not like leading slashes very much, it leads
1047 1047 # to PROPFIND subversion errors
1048 1048 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1049 1049 finally:
1050 1050 if module is not None:
1051 1051 self.reparent(prevmodule)
1052 1052
1053 1053 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1054 1054 strict_node_history=False):
1055 1055 # Normalize path names, svn >= 1.5 only wants paths relative to
1056 1056 # supplied URL
1057 1057 relpaths = []
1058 1058 for p in paths:
1059 1059 if not p.startswith('/'):
1060 1060 p = self.module + '/' + p
1061 1061 relpaths.append(p.strip('/'))
1062 1062 args = [self.baseurl, relpaths, start, end, limit,
1063 1063 discover_changed_paths, strict_node_history]
1064 1064 # developer config: convert.svn.debugsvnlog
1065 1065 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1066 1066 return directlogstream(*args)
1067 1067 arg = encodeargs(args)
1068 1068 hgexe = util.hgexecutable()
1069 1069 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1070 1070 stdin, stdout = util.popen2(util.quotecommand(cmd))
1071 1071 stdin.write(arg)
1072 1072 try:
1073 1073 stdin.close()
1074 1074 except IOError:
1075 1075 raise error.Abort(_('Mercurial failed to run itself, check'
1076 1076 ' hg executable is in PATH'))
1077 1077 return logstream(stdout)
1078 1078
1079 1079 pre_revprop_change = '''#!/bin/sh
1080 1080
1081 1081 REPOS="$1"
1082 1082 REV="$2"
1083 1083 USER="$3"
1084 1084 PROPNAME="$4"
1085 1085 ACTION="$5"
1086 1086
1087 1087 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1088 1088 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1089 1089 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1090 1090
1091 1091 echo "Changing prohibited revision property" >&2
1092 1092 exit 1
1093 1093 '''
1094 1094
1095 1095 class svn_sink(converter_sink, commandline):
1096 1096 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1097 1097 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1098 1098
1099 1099 def prerun(self):
1100 1100 if self.wc:
1101 1101 os.chdir(self.wc)
1102 1102
1103 1103 def postrun(self):
1104 1104 if self.wc:
1105 1105 os.chdir(self.cwd)
1106 1106
1107 1107 def join(self, name):
1108 1108 return os.path.join(self.wc, '.svn', name)
1109 1109
1110 1110 def revmapfile(self):
1111 1111 return self.join('hg-shamap')
1112 1112
1113 1113 def authorfile(self):
1114 1114 return self.join('hg-authormap')
1115 1115
1116 1116 def __init__(self, ui, path):
1117 1117
1118 1118 converter_sink.__init__(self, ui, path)
1119 1119 commandline.__init__(self, ui, 'svn')
1120 1120 self.delete = []
1121 1121 self.setexec = []
1122 1122 self.delexec = []
1123 1123 self.copies = []
1124 1124 self.wc = None
1125 1125 self.cwd = os.getcwd()
1126 1126
1127 1127 created = False
1128 1128 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1129 1129 self.wc = os.path.realpath(path)
1130 1130 self.run0('update')
1131 1131 else:
1132 1132 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1133 1133 path = os.path.realpath(path)
1134 1134 if os.path.isdir(os.path.dirname(path)):
1135 1135 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1136 1136 ui.status(_('initializing svn repository %r\n') %
1137 1137 os.path.basename(path))
1138 1138 commandline(ui, 'svnadmin').run0('create', path)
1139 1139 created = path
1140 1140 path = util.normpath(path)
1141 1141 if not path.startswith('/'):
1142 1142 path = '/' + path
1143 1143 path = 'file://' + path
1144 1144
1145 1145 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1146 1146 ui.status(_('initializing svn working copy %r\n')
1147 1147 % os.path.basename(wcpath))
1148 1148 self.run0('checkout', path, wcpath)
1149 1149
1150 1150 self.wc = wcpath
1151 1151 self.opener = scmutil.opener(self.wc)
1152 1152 self.wopener = scmutil.opener(self.wc)
1153 1153 self.childmap = mapfile(ui, self.join('hg-childmap'))
1154 1154 if util.checkexec(self.wc):
1155 1155 self.is_exec = util.isexec
1156 1156 else:
1157 1157 self.is_exec = None
1158 1158
1159 1159 if created:
1160 1160 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1161 1161 fp = open(hook, 'w')
1162 1162 fp.write(pre_revprop_change)
1163 1163 fp.close()
1164 1164 util.setflags(hook, False, True)
1165 1165
1166 1166 output = self.run0('info')
1167 1167 self.uuid = self.uuid_re.search(output).group(1).strip()
1168 1168
1169 1169 def wjoin(self, *names):
1170 1170 return os.path.join(self.wc, *names)
1171 1171
1172 1172 @propertycache
1173 1173 def manifest(self):
1174 1174 # As of svn 1.7, the "add" command fails when receiving
1175 1175 # already tracked entries, so we have to track and filter them
1176 1176 # ourselves.
1177 1177 m = set()
1178 1178 output = self.run0('ls', recursive=True, xml=True)
1179 1179 doc = xml.dom.minidom.parseString(output)
1180 1180 for e in doc.getElementsByTagName('entry'):
1181 1181 for n in e.childNodes:
1182 1182 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1183 1183 continue
1184 1184 name = ''.join(c.data for c in n.childNodes
1185 1185 if c.nodeType == c.TEXT_NODE)
1186 1186 # Entries are compared with names coming from
1187 1187 # mercurial, so bytes with undefined encoding. Our
1188 1188 # best bet is to assume they are in local
1189 1189 # encoding. They will be passed to command line calls
1190 1190 # later anyway, so they better be.
1191 1191 m.add(encoding.tolocal(name.encode('utf-8')))
1192 1192 break
1193 1193 return m
1194 1194
1195 1195 def putfile(self, filename, flags, data):
1196 1196 if 'l' in flags:
1197 1197 self.wopener.symlink(data, filename)
1198 1198 else:
1199 1199 try:
1200 1200 if os.path.islink(self.wjoin(filename)):
1201 1201 os.unlink(filename)
1202 1202 except OSError:
1203 1203 pass
1204 1204 self.wopener.write(filename, data)
1205 1205
1206 1206 if self.is_exec:
1207 1207 if self.is_exec(self.wjoin(filename)):
1208 1208 if 'x' not in flags:
1209 1209 self.delexec.append(filename)
1210 1210 else:
1211 1211 if 'x' in flags:
1212 1212 self.setexec.append(filename)
1213 1213 util.setflags(self.wjoin(filename), False, 'x' in flags)
1214 1214
1215 1215 def _copyfile(self, source, dest):
1216 1216 # SVN's copy command pukes if the destination file exists, but
1217 1217 # our copyfile method expects to record a copy that has
1218 1218 # already occurred. Cross the semantic gap.
1219 1219 wdest = self.wjoin(dest)
1220 1220 exists = os.path.lexists(wdest)
1221 1221 if exists:
1222 1222 fd, tempname = tempfile.mkstemp(
1223 1223 prefix='hg-copy-', dir=os.path.dirname(wdest))
1224 1224 os.close(fd)
1225 1225 os.unlink(tempname)
1226 1226 os.rename(wdest, tempname)
1227 1227 try:
1228 1228 self.run0('copy', source, dest)
1229 1229 finally:
1230 1230 self.manifest.add(dest)
1231 1231 if exists:
1232 1232 try:
1233 1233 os.unlink(wdest)
1234 1234 except OSError:
1235 1235 pass
1236 1236 os.rename(tempname, wdest)
1237 1237
1238 1238 def dirs_of(self, files):
1239 1239 dirs = set()
1240 1240 for f in files:
1241 1241 if os.path.isdir(self.wjoin(f)):
1242 1242 dirs.add(f)
1243 1243 for i in strutil.rfindall(f, '/'):
1244 1244 dirs.add(f[:i])
1245 1245 return dirs
1246 1246
1247 1247 def add_dirs(self, files):
1248 1248 add_dirs = [d for d in sorted(self.dirs_of(files))
1249 1249 if d not in self.manifest]
1250 1250 if add_dirs:
1251 1251 self.manifest.update(add_dirs)
1252 1252 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1253 1253 return add_dirs
1254 1254
1255 1255 def add_files(self, files):
1256 1256 files = [f for f in files if f not in self.manifest]
1257 1257 if files:
1258 1258 self.manifest.update(files)
1259 1259 self.xargs(files, 'add', quiet=True)
1260 1260 return files
1261 1261
1262 1262 def addchild(self, parent, child):
1263 1263 self.childmap[parent] = child
1264 1264
1265 1265 def revid(self, rev):
1266 1266 return u"svn:%s@%s" % (self.uuid, rev)
1267 1267
1268 1268 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1269 1269 cleanp2):
1270 1270 for parent in parents:
1271 1271 try:
1272 1272 return self.revid(self.childmap[parent])
1273 1273 except KeyError:
1274 1274 pass
1275 1275
1276 1276 # Apply changes to working copy
1277 1277 for f, v in files:
1278 1278 data, mode = source.getfile(f, v)
1279 1279 if data is None:
1280 1280 self.delete.append(f)
1281 1281 else:
1282 1282 self.putfile(f, mode, data)
1283 1283 if f in copies:
1284 1284 self.copies.append([copies[f], f])
1285 1285 if full:
1286 1286 self.delete.extend(sorted(self.manifest.difference(files)))
1287 1287 files = [f[0] for f in files]
1288 1288
1289 1289 entries = set(self.delete)
1290 1290 files = frozenset(files)
1291 1291 entries.update(self.add_dirs(files.difference(entries)))
1292 1292 if self.copies:
1293 1293 for s, d in self.copies:
1294 1294 self._copyfile(s, d)
1295 1295 self.copies = []
1296 1296 if self.delete:
1297 1297 self.xargs(self.delete, 'delete')
1298 1298 for f in self.delete:
1299 1299 self.manifest.remove(f)
1300 1300 self.delete = []
1301 1301 entries.update(self.add_files(files.difference(entries)))
1302 1302 if self.delexec:
1303 1303 self.xargs(self.delexec, 'propdel', 'svn:executable')
1304 1304 self.delexec = []
1305 1305 if self.setexec:
1306 1306 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1307 1307 self.setexec = []
1308 1308
1309 1309 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1310 1310 fp = os.fdopen(fd, 'w')
1311 1311 fp.write(commit.desc)
1312 1312 fp.close()
1313 1313 try:
1314 1314 output = self.run0('commit',
1315 1315 username=util.shortuser(commit.author),
1316 1316 file=messagefile,
1317 1317 encoding='utf-8')
1318 1318 try:
1319 1319 rev = self.commit_re.search(output).group(1)
1320 1320 except AttributeError:
1321 1321 if parents and not files:
1322 1322 return parents[0]
1323 1323 self.ui.warn(_('unexpected svn output:\n'))
1324 1324 self.ui.warn(output)
1325 1325 raise error.Abort(_('unable to cope with svn output'))
1326 1326 if commit.rev:
1327 1327 self.run('propset', 'hg:convert-rev', commit.rev,
1328 1328 revprop=True, revision=rev)
1329 1329 if commit.branch and commit.branch != 'default':
1330 1330 self.run('propset', 'hg:convert-branch', commit.branch,
1331 1331 revprop=True, revision=rev)
1332 1332 for parent in parents:
1333 1333 self.addchild(parent, rev)
1334 1334 return self.revid(rev)
1335 1335 finally:
1336 1336 os.unlink(messagefile)
1337 1337
1338 1338 def puttags(self, tags):
1339 1339 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1340 1340 return None, None
1341 1341
1342 1342 def hascommitfrommap(self, rev):
1343 1343 # We trust that revisions referenced in a map still is present
1344 1344 # TODO: implement something better if necessary and feasible
1345 1345 return True
1346 1346
1347 1347 def hascommitforsplicemap(self, rev):
1348 1348 # This is not correct as one can convert to an existing subversion
1349 1349 # repository and childmap would not list all revisions. Too bad.
1350 1350 if rev in self.childmap:
1351 1351 return True
1352 1352 raise error.Abort(_('splice map revision %s not found in subversion '
1353 1353 'child map (revision lookups are not implemented)')
1354 1354 % rev)
@@ -1,178 +1,180 b''
1 1 # Copyright 2011 Fog Creek Software
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 import os
7 import urllib2
8 7 import re
9 8
10 9 from mercurial import error, httppeer, util, wireproto
11 10 from mercurial.i18n import _
12 11
12 urlerr = util.urlerr
13 urlreq = util.urlreq
14
13 15 import lfutil
14 16
15 17 LARGEFILES_REQUIRED_MSG = ('\nThis repository uses the largefiles extension.'
16 18 '\n\nPlease enable it in your Mercurial config '
17 19 'file.\n')
18 20
19 21 # these will all be replaced by largefiles.uisetup
20 22 capabilitiesorig = None
21 23 ssholdcallstream = None
22 24 httpoldcallstream = None
23 25
24 26 def putlfile(repo, proto, sha):
25 27 '''Server command for putting a largefile into a repository's local store
26 28 and into the user cache.'''
27 29 proto.redirect()
28 30
29 31 path = lfutil.storepath(repo, sha)
30 32 util.makedirs(os.path.dirname(path))
31 33 tmpfp = util.atomictempfile(path, createmode=repo.store.createmode)
32 34
33 35 try:
34 36 proto.getfile(tmpfp)
35 37 tmpfp._fp.seek(0)
36 38 if sha != lfutil.hexsha1(tmpfp._fp):
37 39 raise IOError(0, _('largefile contents do not match hash'))
38 40 tmpfp.close()
39 41 lfutil.linktousercache(repo, sha)
40 42 except IOError as e:
41 43 repo.ui.warn(_('largefiles: failed to put %s into store: %s\n') %
42 44 (sha, e.strerror))
43 45 return wireproto.pushres(1)
44 46 finally:
45 47 tmpfp.discard()
46 48
47 49 return wireproto.pushres(0)
48 50
49 51 def getlfile(repo, proto, sha):
50 52 '''Server command for retrieving a largefile from the repository-local
51 53 cache or user cache.'''
52 54 filename = lfutil.findfile(repo, sha)
53 55 if not filename:
54 56 raise error.Abort(_('requested largefile %s not present in cache')
55 57 % sha)
56 58 f = open(filename, 'rb')
57 59 length = os.fstat(f.fileno())[6]
58 60
59 61 # Since we can't set an HTTP content-length header here, and
60 62 # Mercurial core provides no way to give the length of a streamres
61 63 # (and reading the entire file into RAM would be ill-advised), we
62 64 # just send the length on the first line of the response, like the
63 65 # ssh proto does for string responses.
64 66 def generator():
65 67 yield '%d\n' % length
66 68 for chunk in util.filechunkiter(f):
67 69 yield chunk
68 70 return wireproto.streamres(generator())
69 71
70 72 def statlfile(repo, proto, sha):
71 73 '''Server command for checking if a largefile is present - returns '2\n' if
72 74 the largefile is missing, '0\n' if it seems to be in good condition.
73 75
74 76 The value 1 is reserved for mismatched checksum, but that is too expensive
75 77 to be verified on every stat and must be caught be running 'hg verify'
76 78 server side.'''
77 79 filename = lfutil.findfile(repo, sha)
78 80 if not filename:
79 81 return '2\n'
80 82 return '0\n'
81 83
82 84 def wirereposetup(ui, repo):
83 85 class lfileswirerepository(repo.__class__):
84 86 def putlfile(self, sha, fd):
85 87 # unfortunately, httprepository._callpush tries to convert its
86 88 # input file-like into a bundle before sending it, so we can't use
87 89 # it ...
88 90 if issubclass(self.__class__, httppeer.httppeer):
89 91 res = self._call('putlfile', data=fd, sha=sha,
90 92 headers={'content-type':'application/mercurial-0.1'})
91 93 try:
92 94 d, output = res.split('\n', 1)
93 95 for l in output.splitlines(True):
94 96 self.ui.warn(_('remote: '), l) # assume l ends with \n
95 97 return int(d)
96 98 except ValueError:
97 99 self.ui.warn(_('unexpected putlfile response: %r\n') % res)
98 100 return 1
99 101 # ... but we can't use sshrepository._call because the data=
100 102 # argument won't get sent, and _callpush does exactly what we want
101 103 # in this case: send the data straight through
102 104 else:
103 105 try:
104 106 ret, output = self._callpush("putlfile", fd, sha=sha)
105 107 if ret == "":
106 108 raise error.ResponseError(_('putlfile failed:'),
107 109 output)
108 110 return int(ret)
109 111 except IOError:
110 112 return 1
111 113 except ValueError:
112 114 raise error.ResponseError(
113 115 _('putlfile failed (unexpected response):'), ret)
114 116
115 117 def getlfile(self, sha):
116 118 """returns an iterable with the chunks of the file with sha sha"""
117 119 stream = self._callstream("getlfile", sha=sha)
118 120 length = stream.readline()
119 121 try:
120 122 length = int(length)
121 123 except ValueError:
122 124 self._abort(error.ResponseError(_("unexpected response:"),
123 125 length))
124 126
125 127 # SSH streams will block if reading more than length
126 128 for chunk in util.filechunkiter(stream, 128 * 1024, length):
127 129 yield chunk
128 130 # HTTP streams must hit the end to process the last empty
129 131 # chunk of Chunked-Encoding so the connection can be reused.
130 132 if issubclass(self.__class__, httppeer.httppeer):
131 133 chunk = stream.read(1)
132 134 if chunk:
133 135 self._abort(error.ResponseError(_("unexpected response:"),
134 136 chunk))
135 137
136 138 @wireproto.batchable
137 139 def statlfile(self, sha):
138 140 f = wireproto.future()
139 141 result = {'sha': sha}
140 142 yield result, f
141 143 try:
142 144 yield int(f.value)
143 except (ValueError, urllib2.HTTPError):
145 except (ValueError, urlerr.httperror):
144 146 # If the server returns anything but an integer followed by a
145 147 # newline, newline, it's not speaking our language; if we get
146 148 # an HTTP error, we can't be sure the largefile is present;
147 149 # either way, consider it missing.
148 150 yield 2
149 151
150 152 repo.__class__ = lfileswirerepository
151 153
152 154 # advertise the largefiles=serve capability
153 155 def capabilities(repo, proto):
154 156 '''Wrap server command to announce largefile server capability'''
155 157 return capabilitiesorig(repo, proto) + ' largefiles=serve'
156 158
157 159 def heads(repo, proto):
158 160 '''Wrap server command - largefile capable clients will know to call
159 161 lheads instead'''
160 162 if lfutil.islfilesrepo(repo):
161 163 return wireproto.ooberror(LARGEFILES_REQUIRED_MSG)
162 164 return wireproto.heads(repo, proto)
163 165
164 166 def sshrepocallstream(self, cmd, **args):
165 167 if cmd == 'heads' and self.capable('largefiles'):
166 168 cmd = 'lheads'
167 169 if cmd == 'batch' and self.capable('largefiles'):
168 170 args['cmds'] = args['cmds'].replace('heads ', 'lheads ')
169 171 return ssholdcallstream(self, cmd, **args)
170 172
171 173 headsre = re.compile(r'(^|;)heads\b')
172 174
173 175 def httprepocallstream(self, cmd, **args):
174 176 if cmd == 'heads' and self.capable('largefiles'):
175 177 cmd = 'lheads'
176 178 if cmd == 'batch' and self.capable('largefiles'):
177 179 args['cmds'] = headsre.sub('lheads', args['cmds'])
178 180 return httpoldcallstream(self, cmd, **args)
@@ -1,113 +1,114 b''
1 1 # Copyright 2010-2011 Fog Creek Software
2 2 # Copyright 2010-2011 Unity Technologies
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 '''remote largefile store; the base class for wirestore'''
8 8
9 import urllib2
10
11 9 from mercurial import util, wireproto, error
12 10 from mercurial.i18n import _
13 11
12 urlerr = util.urlerr
13 urlreq = util.urlreq
14
14 15 import lfutil
15 16 import basestore
16 17
17 18 class remotestore(basestore.basestore):
18 19 '''a largefile store accessed over a network'''
19 20 def __init__(self, ui, repo, url):
20 21 super(remotestore, self).__init__(ui, repo, url)
21 22
22 23 def put(self, source, hash):
23 24 if self.sendfile(source, hash):
24 25 raise error.Abort(
25 26 _('remotestore: could not put %s to remote store %s')
26 27 % (source, util.hidepassword(self.url)))
27 28 self.ui.debug(
28 29 _('remotestore: put %s to remote store %s\n')
29 30 % (source, util.hidepassword(self.url)))
30 31
31 32 def exists(self, hashes):
32 33 return dict((h, s == 0) for (h, s) in # dict-from-generator
33 34 self._stat(hashes).iteritems())
34 35
35 36 def sendfile(self, filename, hash):
36 37 self.ui.debug('remotestore: sendfile(%s, %s)\n' % (filename, hash))
37 38 fd = None
38 39 try:
39 40 fd = lfutil.httpsendfile(self.ui, filename)
40 41 return self._put(hash, fd)
41 42 except IOError as e:
42 43 raise error.Abort(
43 44 _('remotestore: could not open file %s: %s')
44 45 % (filename, str(e)))
45 46 finally:
46 47 if fd:
47 48 fd.close()
48 49
49 50 def _getfile(self, tmpfile, filename, hash):
50 51 try:
51 52 chunks = self._get(hash)
52 except urllib2.HTTPError as e:
53 except urlerr.httperror as e:
53 54 # 401s get converted to error.Aborts; everything else is fine being
54 55 # turned into a StoreError
55 56 raise basestore.StoreError(filename, hash, self.url, str(e))
56 except urllib2.URLError as e:
57 except urlerr.urlerror as e:
57 58 # This usually indicates a connection problem, so don't
58 59 # keep trying with the other files... they will probably
59 60 # all fail too.
60 61 raise error.Abort('%s: %s' %
61 62 (util.hidepassword(self.url), e.reason))
62 63 except IOError as e:
63 64 raise basestore.StoreError(filename, hash, self.url, str(e))
64 65
65 66 return lfutil.copyandhash(chunks, tmpfile)
66 67
67 68 def _verifyfile(self, cctx, cset, contents, standin, verified):
68 69 filename = lfutil.splitstandin(standin)
69 70 if not filename:
70 71 return False
71 72 fctx = cctx[standin]
72 73 key = (filename, fctx.filenode())
73 74 if key in verified:
74 75 return False
75 76
76 77 verified.add(key)
77 78
78 79 expecthash = fctx.data()[0:40]
79 80 stat = self._stat([expecthash])[expecthash]
80 81 if not stat:
81 82 return False
82 83 elif stat == 1:
83 84 self.ui.warn(
84 85 _('changeset %s: %s: contents differ\n')
85 86 % (cset, filename))
86 87 return True # failed
87 88 elif stat == 2:
88 89 self.ui.warn(
89 90 _('changeset %s: %s missing\n')
90 91 % (cset, filename))
91 92 return True # failed
92 93 else:
93 94 raise RuntimeError('verify failed: unexpected response from '
94 95 'statlfile (%r)' % stat)
95 96
96 97 def batch(self):
97 98 '''Support for remote batching.'''
98 99 return wireproto.remotebatch(self)
99 100
100 101 def _put(self, hash, fd):
101 102 '''Put file with the given hash in the remote store.'''
102 103 raise NotImplementedError('abstract method')
103 104
104 105 def _get(self, hash):
105 106 '''Get file with the given hash from the remote store.'''
106 107 raise NotImplementedError('abstract method')
107 108
108 109 def _stat(self, hashes):
109 110 '''Get information about availability of files specified by
110 111 hashes in the remote store. Return dictionary mapping hashes
111 112 to return code where 0 means that file is available, other
112 113 values if not.'''
113 114 raise NotImplementedError('abstract method')
@@ -1,1606 +1,1608 b''
1 1 # bundle2.py - generic container format to transmit arbitrary data.
2 2 #
3 3 # Copyright 2013 Facebook, Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """Handling of the new bundle2 format
8 8
9 9 The goal of bundle2 is to act as an atomically packet to transmit a set of
10 10 payloads in an application agnostic way. It consist in a sequence of "parts"
11 11 that will be handed to and processed by the application layer.
12 12
13 13
14 14 General format architecture
15 15 ===========================
16 16
17 17 The format is architectured as follow
18 18
19 19 - magic string
20 20 - stream level parameters
21 21 - payload parts (any number)
22 22 - end of stream marker.
23 23
24 24 the Binary format
25 25 ============================
26 26
27 27 All numbers are unsigned and big-endian.
28 28
29 29 stream level parameters
30 30 ------------------------
31 31
32 32 Binary format is as follow
33 33
34 34 :params size: int32
35 35
36 36 The total number of Bytes used by the parameters
37 37
38 38 :params value: arbitrary number of Bytes
39 39
40 40 A blob of `params size` containing the serialized version of all stream level
41 41 parameters.
42 42
43 43 The blob contains a space separated list of parameters. Parameters with value
44 44 are stored in the form `<name>=<value>`. Both name and value are urlquoted.
45 45
46 46 Empty name are obviously forbidden.
47 47
48 48 Name MUST start with a letter. If this first letter is lower case, the
49 49 parameter is advisory and can be safely ignored. However when the first
50 50 letter is capital, the parameter is mandatory and the bundling process MUST
51 51 stop if he is not able to proceed it.
52 52
53 53 Stream parameters use a simple textual format for two main reasons:
54 54
55 55 - Stream level parameters should remain simple and we want to discourage any
56 56 crazy usage.
57 57 - Textual data allow easy human inspection of a bundle2 header in case of
58 58 troubles.
59 59
60 60 Any Applicative level options MUST go into a bundle2 part instead.
61 61
62 62 Payload part
63 63 ------------------------
64 64
65 65 Binary format is as follow
66 66
67 67 :header size: int32
68 68
69 69 The total number of Bytes used by the part header. When the header is empty
70 70 (size = 0) this is interpreted as the end of stream marker.
71 71
72 72 :header:
73 73
74 74 The header defines how to interpret the part. It contains two piece of
75 75 data: the part type, and the part parameters.
76 76
77 77 The part type is used to route an application level handler, that can
78 78 interpret payload.
79 79
80 80 Part parameters are passed to the application level handler. They are
81 81 meant to convey information that will help the application level object to
82 82 interpret the part payload.
83 83
84 84 The binary format of the header is has follow
85 85
86 86 :typesize: (one byte)
87 87
88 88 :parttype: alphanumerical part name (restricted to [a-zA-Z0-9_:-]*)
89 89
90 90 :partid: A 32bits integer (unique in the bundle) that can be used to refer
91 91 to this part.
92 92
93 93 :parameters:
94 94
95 95 Part's parameter may have arbitrary content, the binary structure is::
96 96
97 97 <mandatory-count><advisory-count><param-sizes><param-data>
98 98
99 99 :mandatory-count: 1 byte, number of mandatory parameters
100 100
101 101 :advisory-count: 1 byte, number of advisory parameters
102 102
103 103 :param-sizes:
104 104
105 105 N couple of bytes, where N is the total number of parameters. Each
106 106 couple contains (<size-of-key>, <size-of-value) for one parameter.
107 107
108 108 :param-data:
109 109
110 110 A blob of bytes from which each parameter key and value can be
111 111 retrieved using the list of size couples stored in the previous
112 112 field.
113 113
114 114 Mandatory parameters comes first, then the advisory ones.
115 115
116 116 Each parameter's key MUST be unique within the part.
117 117
118 118 :payload:
119 119
120 120 payload is a series of `<chunksize><chunkdata>`.
121 121
122 122 `chunksize` is an int32, `chunkdata` are plain bytes (as much as
123 123 `chunksize` says)` The payload part is concluded by a zero size chunk.
124 124
125 125 The current implementation always produces either zero or one chunk.
126 126 This is an implementation limitation that will ultimately be lifted.
127 127
128 128 `chunksize` can be negative to trigger special case processing. No such
129 129 processing is in place yet.
130 130
131 131 Bundle processing
132 132 ============================
133 133
134 134 Each part is processed in order using a "part handler". Handler are registered
135 135 for a certain part type.
136 136
137 137 The matching of a part to its handler is case insensitive. The case of the
138 138 part type is used to know if a part is mandatory or advisory. If the Part type
139 139 contains any uppercase char it is considered mandatory. When no handler is
140 140 known for a Mandatory part, the process is aborted and an exception is raised.
141 141 If the part is advisory and no handler is known, the part is ignored. When the
142 142 process is aborted, the full bundle is still read from the stream to keep the
143 143 channel usable. But none of the part read from an abort are processed. In the
144 144 future, dropping the stream may become an option for channel we do not care to
145 145 preserve.
146 146 """
147 147
148 148 from __future__ import absolute_import
149 149
150 150 import errno
151 151 import re
152 152 import string
153 153 import struct
154 154 import sys
155 import urllib
156 155
157 156 from .i18n import _
158 157 from . import (
159 158 changegroup,
160 159 error,
161 160 obsolete,
162 161 pushkey,
163 162 tags,
164 163 url,
165 164 util,
166 165 )
167 166
167 urlerr = util.urlerr
168 urlreq = util.urlreq
169
168 170 _pack = struct.pack
169 171 _unpack = struct.unpack
170 172
171 173 _fstreamparamsize = '>i'
172 174 _fpartheadersize = '>i'
173 175 _fparttypesize = '>B'
174 176 _fpartid = '>I'
175 177 _fpayloadsize = '>i'
176 178 _fpartparamcount = '>BB'
177 179
178 180 preferedchunksize = 4096
179 181
180 182 _parttypeforbidden = re.compile('[^a-zA-Z0-9_:-]')
181 183
182 184 def outdebug(ui, message):
183 185 """debug regarding output stream (bundling)"""
184 186 if ui.configbool('devel', 'bundle2.debug', False):
185 187 ui.debug('bundle2-output: %s\n' % message)
186 188
187 189 def indebug(ui, message):
188 190 """debug on input stream (unbundling)"""
189 191 if ui.configbool('devel', 'bundle2.debug', False):
190 192 ui.debug('bundle2-input: %s\n' % message)
191 193
192 194 def validateparttype(parttype):
193 195 """raise ValueError if a parttype contains invalid character"""
194 196 if _parttypeforbidden.search(parttype):
195 197 raise ValueError(parttype)
196 198
197 199 def _makefpartparamsizes(nbparams):
198 200 """return a struct format to read part parameter sizes
199 201
200 202 The number parameters is variable so we need to build that format
201 203 dynamically.
202 204 """
203 205 return '>'+('BB'*nbparams)
204 206
205 207 parthandlermapping = {}
206 208
207 209 def parthandler(parttype, params=()):
208 210 """decorator that register a function as a bundle2 part handler
209 211
210 212 eg::
211 213
212 214 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
213 215 def myparttypehandler(...):
214 216 '''process a part of type "my part".'''
215 217 ...
216 218 """
217 219 validateparttype(parttype)
218 220 def _decorator(func):
219 221 lparttype = parttype.lower() # enforce lower case matching.
220 222 assert lparttype not in parthandlermapping
221 223 parthandlermapping[lparttype] = func
222 224 func.params = frozenset(params)
223 225 return func
224 226 return _decorator
225 227
226 228 class unbundlerecords(object):
227 229 """keep record of what happens during and unbundle
228 230
229 231 New records are added using `records.add('cat', obj)`. Where 'cat' is a
230 232 category of record and obj is an arbitrary object.
231 233
232 234 `records['cat']` will return all entries of this category 'cat'.
233 235
234 236 Iterating on the object itself will yield `('category', obj)` tuples
235 237 for all entries.
236 238
237 239 All iterations happens in chronological order.
238 240 """
239 241
240 242 def __init__(self):
241 243 self._categories = {}
242 244 self._sequences = []
243 245 self._replies = {}
244 246
245 247 def add(self, category, entry, inreplyto=None):
246 248 """add a new record of a given category.
247 249
248 250 The entry can then be retrieved in the list returned by
249 251 self['category']."""
250 252 self._categories.setdefault(category, []).append(entry)
251 253 self._sequences.append((category, entry))
252 254 if inreplyto is not None:
253 255 self.getreplies(inreplyto).add(category, entry)
254 256
255 257 def getreplies(self, partid):
256 258 """get the records that are replies to a specific part"""
257 259 return self._replies.setdefault(partid, unbundlerecords())
258 260
259 261 def __getitem__(self, cat):
260 262 return tuple(self._categories.get(cat, ()))
261 263
262 264 def __iter__(self):
263 265 return iter(self._sequences)
264 266
265 267 def __len__(self):
266 268 return len(self._sequences)
267 269
268 270 def __nonzero__(self):
269 271 return bool(self._sequences)
270 272
271 273 class bundleoperation(object):
272 274 """an object that represents a single bundling process
273 275
274 276 Its purpose is to carry unbundle-related objects and states.
275 277
276 278 A new object should be created at the beginning of each bundle processing.
277 279 The object is to be returned by the processing function.
278 280
279 281 The object has very little content now it will ultimately contain:
280 282 * an access to the repo the bundle is applied to,
281 283 * a ui object,
282 284 * a way to retrieve a transaction to add changes to the repo,
283 285 * a way to record the result of processing each part,
284 286 * a way to construct a bundle response when applicable.
285 287 """
286 288
287 289 def __init__(self, repo, transactiongetter, captureoutput=True):
288 290 self.repo = repo
289 291 self.ui = repo.ui
290 292 self.records = unbundlerecords()
291 293 self.gettransaction = transactiongetter
292 294 self.reply = None
293 295 self.captureoutput = captureoutput
294 296
295 297 class TransactionUnavailable(RuntimeError):
296 298 pass
297 299
298 300 def _notransaction():
299 301 """default method to get a transaction while processing a bundle
300 302
301 303 Raise an exception to highlight the fact that no transaction was expected
302 304 to be created"""
303 305 raise TransactionUnavailable()
304 306
305 307 def applybundle(repo, unbundler, tr, source=None, url=None, op=None):
306 308 # transform me into unbundler.apply() as soon as the freeze is lifted
307 309 tr.hookargs['bundle2'] = '1'
308 310 if source is not None and 'source' not in tr.hookargs:
309 311 tr.hookargs['source'] = source
310 312 if url is not None and 'url' not in tr.hookargs:
311 313 tr.hookargs['url'] = url
312 314 return processbundle(repo, unbundler, lambda: tr, op=op)
313 315
314 316 def processbundle(repo, unbundler, transactiongetter=None, op=None):
315 317 """This function process a bundle, apply effect to/from a repo
316 318
317 319 It iterates over each part then searches for and uses the proper handling
318 320 code to process the part. Parts are processed in order.
319 321
320 322 This is very early version of this function that will be strongly reworked
321 323 before final usage.
322 324
323 325 Unknown Mandatory part will abort the process.
324 326
325 327 It is temporarily possible to provide a prebuilt bundleoperation to the
326 328 function. This is used to ensure output is properly propagated in case of
327 329 an error during the unbundling. This output capturing part will likely be
328 330 reworked and this ability will probably go away in the process.
329 331 """
330 332 if op is None:
331 333 if transactiongetter is None:
332 334 transactiongetter = _notransaction
333 335 op = bundleoperation(repo, transactiongetter)
334 336 # todo:
335 337 # - replace this is a init function soon.
336 338 # - exception catching
337 339 unbundler.params
338 340 if repo.ui.debugflag:
339 341 msg = ['bundle2-input-bundle:']
340 342 if unbundler.params:
341 343 msg.append(' %i params')
342 344 if op.gettransaction is None:
343 345 msg.append(' no-transaction')
344 346 else:
345 347 msg.append(' with-transaction')
346 348 msg.append('\n')
347 349 repo.ui.debug(''.join(msg))
348 350 iterparts = enumerate(unbundler.iterparts())
349 351 part = None
350 352 nbpart = 0
351 353 try:
352 354 for nbpart, part in iterparts:
353 355 _processpart(op, part)
354 356 except BaseException as exc:
355 357 for nbpart, part in iterparts:
356 358 # consume the bundle content
357 359 part.seek(0, 2)
358 360 # Small hack to let caller code distinguish exceptions from bundle2
359 361 # processing from processing the old format. This is mostly
360 362 # needed to handle different return codes to unbundle according to the
361 363 # type of bundle. We should probably clean up or drop this return code
362 364 # craziness in a future version.
363 365 exc.duringunbundle2 = True
364 366 salvaged = []
365 367 replycaps = None
366 368 if op.reply is not None:
367 369 salvaged = op.reply.salvageoutput()
368 370 replycaps = op.reply.capabilities
369 371 exc._replycaps = replycaps
370 372 exc._bundle2salvagedoutput = salvaged
371 373 raise
372 374 finally:
373 375 repo.ui.debug('bundle2-input-bundle: %i parts total\n' % nbpart)
374 376
375 377 return op
376 378
377 379 def _processpart(op, part):
378 380 """process a single part from a bundle
379 381
380 382 The part is guaranteed to have been fully consumed when the function exits
381 383 (even if an exception is raised)."""
382 384 status = 'unknown' # used by debug output
383 385 try:
384 386 try:
385 387 handler = parthandlermapping.get(part.type)
386 388 if handler is None:
387 389 status = 'unsupported-type'
388 390 raise error.BundleUnknownFeatureError(parttype=part.type)
389 391 indebug(op.ui, 'found a handler for part %r' % part.type)
390 392 unknownparams = part.mandatorykeys - handler.params
391 393 if unknownparams:
392 394 unknownparams = list(unknownparams)
393 395 unknownparams.sort()
394 396 status = 'unsupported-params (%s)' % unknownparams
395 397 raise error.BundleUnknownFeatureError(parttype=part.type,
396 398 params=unknownparams)
397 399 status = 'supported'
398 400 except error.BundleUnknownFeatureError as exc:
399 401 if part.mandatory: # mandatory parts
400 402 raise
401 403 indebug(op.ui, 'ignoring unsupported advisory part %s' % exc)
402 404 return # skip to part processing
403 405 finally:
404 406 if op.ui.debugflag:
405 407 msg = ['bundle2-input-part: "%s"' % part.type]
406 408 if not part.mandatory:
407 409 msg.append(' (advisory)')
408 410 nbmp = len(part.mandatorykeys)
409 411 nbap = len(part.params) - nbmp
410 412 if nbmp or nbap:
411 413 msg.append(' (params:')
412 414 if nbmp:
413 415 msg.append(' %i mandatory' % nbmp)
414 416 if nbap:
415 417 msg.append(' %i advisory' % nbmp)
416 418 msg.append(')')
417 419 msg.append(' %s\n' % status)
418 420 op.ui.debug(''.join(msg))
419 421
420 422 # handler is called outside the above try block so that we don't
421 423 # risk catching KeyErrors from anything other than the
422 424 # parthandlermapping lookup (any KeyError raised by handler()
423 425 # itself represents a defect of a different variety).
424 426 output = None
425 427 if op.captureoutput and op.reply is not None:
426 428 op.ui.pushbuffer(error=True, subproc=True)
427 429 output = ''
428 430 try:
429 431 handler(op, part)
430 432 finally:
431 433 if output is not None:
432 434 output = op.ui.popbuffer()
433 435 if output:
434 436 outpart = op.reply.newpart('output', data=output,
435 437 mandatory=False)
436 438 outpart.addparam('in-reply-to', str(part.id), mandatory=False)
437 439 finally:
438 440 # consume the part content to not corrupt the stream.
439 441 part.seek(0, 2)
440 442
441 443
442 444 def decodecaps(blob):
443 445 """decode a bundle2 caps bytes blob into a dictionary
444 446
445 447 The blob is a list of capabilities (one per line)
446 448 Capabilities may have values using a line of the form::
447 449
448 450 capability=value1,value2,value3
449 451
450 452 The values are always a list."""
451 453 caps = {}
452 454 for line in blob.splitlines():
453 455 if not line:
454 456 continue
455 457 if '=' not in line:
456 458 key, vals = line, ()
457 459 else:
458 460 key, vals = line.split('=', 1)
459 461 vals = vals.split(',')
460 key = urllib.unquote(key)
461 vals = [urllib.unquote(v) for v in vals]
462 key = urlreq.unquote(key)
463 vals = [urlreq.unquote(v) for v in vals]
462 464 caps[key] = vals
463 465 return caps
464 466
465 467 def encodecaps(caps):
466 468 """encode a bundle2 caps dictionary into a bytes blob"""
467 469 chunks = []
468 470 for ca in sorted(caps):
469 471 vals = caps[ca]
470 ca = urllib.quote(ca)
471 vals = [urllib.quote(v) for v in vals]
472 ca = urlreq.quote(ca)
473 vals = [urlreq.quote(v) for v in vals]
472 474 if vals:
473 475 ca = "%s=%s" % (ca, ','.join(vals))
474 476 chunks.append(ca)
475 477 return '\n'.join(chunks)
476 478
477 479 bundletypes = {
478 480 "": ("", None), # only when using unbundle on ssh and old http servers
479 481 # since the unification ssh accepts a header but there
480 482 # is no capability signaling it.
481 483 "HG20": (), # special-cased below
482 484 "HG10UN": ("HG10UN", None),
483 485 "HG10BZ": ("HG10", 'BZ'),
484 486 "HG10GZ": ("HG10GZ", 'GZ'),
485 487 }
486 488
487 489 # hgweb uses this list to communicate its preferred type
488 490 bundlepriority = ['HG10GZ', 'HG10BZ', 'HG10UN']
489 491
490 492 class bundle20(object):
491 493 """represent an outgoing bundle2 container
492 494
493 495 Use the `addparam` method to add stream level parameter. and `newpart` to
494 496 populate it. Then call `getchunks` to retrieve all the binary chunks of
495 497 data that compose the bundle2 container."""
496 498
497 499 _magicstring = 'HG20'
498 500
499 501 def __init__(self, ui, capabilities=()):
500 502 self.ui = ui
501 503 self._params = []
502 504 self._parts = []
503 505 self.capabilities = dict(capabilities)
504 506 self._compressor = util.compressors[None]()
505 507
506 508 def setcompression(self, alg):
507 509 """setup core part compression to <alg>"""
508 510 if alg is None:
509 511 return
510 512 assert not any(n.lower() == 'Compression' for n, v in self._params)
511 513 self.addparam('Compression', alg)
512 514 self._compressor = util.compressors[alg]()
513 515
514 516 @property
515 517 def nbparts(self):
516 518 """total number of parts added to the bundler"""
517 519 return len(self._parts)
518 520
519 521 # methods used to defines the bundle2 content
520 522 def addparam(self, name, value=None):
521 523 """add a stream level parameter"""
522 524 if not name:
523 525 raise ValueError('empty parameter name')
524 526 if name[0] not in string.letters:
525 527 raise ValueError('non letter first character: %r' % name)
526 528 self._params.append((name, value))
527 529
528 530 def addpart(self, part):
529 531 """add a new part to the bundle2 container
530 532
531 533 Parts contains the actual applicative payload."""
532 534 assert part.id is None
533 535 part.id = len(self._parts) # very cheap counter
534 536 self._parts.append(part)
535 537
536 538 def newpart(self, typeid, *args, **kwargs):
537 539 """create a new part and add it to the containers
538 540
539 541 As the part is directly added to the containers. For now, this means
540 542 that any failure to properly initialize the part after calling
541 543 ``newpart`` should result in a failure of the whole bundling process.
542 544
543 545 You can still fall back to manually create and add if you need better
544 546 control."""
545 547 part = bundlepart(typeid, *args, **kwargs)
546 548 self.addpart(part)
547 549 return part
548 550
549 551 # methods used to generate the bundle2 stream
550 552 def getchunks(self):
551 553 if self.ui.debugflag:
552 554 msg = ['bundle2-output-bundle: "%s",' % self._magicstring]
553 555 if self._params:
554 556 msg.append(' (%i params)' % len(self._params))
555 557 msg.append(' %i parts total\n' % len(self._parts))
556 558 self.ui.debug(''.join(msg))
557 559 outdebug(self.ui, 'start emission of %s stream' % self._magicstring)
558 560 yield self._magicstring
559 561 param = self._paramchunk()
560 562 outdebug(self.ui, 'bundle parameter: %s' % param)
561 563 yield _pack(_fstreamparamsize, len(param))
562 564 if param:
563 565 yield param
564 566 # starting compression
565 567 for chunk in self._getcorechunk():
566 568 yield self._compressor.compress(chunk)
567 569 yield self._compressor.flush()
568 570
569 571 def _paramchunk(self):
570 572 """return a encoded version of all stream parameters"""
571 573 blocks = []
572 574 for par, value in self._params:
573 par = urllib.quote(par)
575 par = urlreq.quote(par)
574 576 if value is not None:
575 value = urllib.quote(value)
577 value = urlreq.quote(value)
576 578 par = '%s=%s' % (par, value)
577 579 blocks.append(par)
578 580 return ' '.join(blocks)
579 581
580 582 def _getcorechunk(self):
581 583 """yield chunk for the core part of the bundle
582 584
583 585 (all but headers and parameters)"""
584 586 outdebug(self.ui, 'start of parts')
585 587 for part in self._parts:
586 588 outdebug(self.ui, 'bundle part: "%s"' % part.type)
587 589 for chunk in part.getchunks(ui=self.ui):
588 590 yield chunk
589 591 outdebug(self.ui, 'end of bundle')
590 592 yield _pack(_fpartheadersize, 0)
591 593
592 594
593 595 def salvageoutput(self):
594 596 """return a list with a copy of all output parts in the bundle
595 597
596 598 This is meant to be used during error handling to make sure we preserve
597 599 server output"""
598 600 salvaged = []
599 601 for part in self._parts:
600 602 if part.type.startswith('output'):
601 603 salvaged.append(part.copy())
602 604 return salvaged
603 605
604 606
605 607 class unpackermixin(object):
606 608 """A mixin to extract bytes and struct data from a stream"""
607 609
608 610 def __init__(self, fp):
609 611 self._fp = fp
610 612 self._seekable = (util.safehasattr(fp, 'seek') and
611 613 util.safehasattr(fp, 'tell'))
612 614
613 615 def _unpack(self, format):
614 616 """unpack this struct format from the stream"""
615 617 data = self._readexact(struct.calcsize(format))
616 618 return _unpack(format, data)
617 619
618 620 def _readexact(self, size):
619 621 """read exactly <size> bytes from the stream"""
620 622 return changegroup.readexactly(self._fp, size)
621 623
622 624 def seek(self, offset, whence=0):
623 625 """move the underlying file pointer"""
624 626 if self._seekable:
625 627 return self._fp.seek(offset, whence)
626 628 else:
627 629 raise NotImplementedError(_('File pointer is not seekable'))
628 630
629 631 def tell(self):
630 632 """return the file offset, or None if file is not seekable"""
631 633 if self._seekable:
632 634 try:
633 635 return self._fp.tell()
634 636 except IOError as e:
635 637 if e.errno == errno.ESPIPE:
636 638 self._seekable = False
637 639 else:
638 640 raise
639 641 return None
640 642
641 643 def close(self):
642 644 """close underlying file"""
643 645 if util.safehasattr(self._fp, 'close'):
644 646 return self._fp.close()
645 647
646 648 def getunbundler(ui, fp, magicstring=None):
647 649 """return a valid unbundler object for a given magicstring"""
648 650 if magicstring is None:
649 651 magicstring = changegroup.readexactly(fp, 4)
650 652 magic, version = magicstring[0:2], magicstring[2:4]
651 653 if magic != 'HG':
652 654 raise error.Abort(_('not a Mercurial bundle'))
653 655 unbundlerclass = formatmap.get(version)
654 656 if unbundlerclass is None:
655 657 raise error.Abort(_('unknown bundle version %s') % version)
656 658 unbundler = unbundlerclass(ui, fp)
657 659 indebug(ui, 'start processing of %s stream' % magicstring)
658 660 return unbundler
659 661
660 662 class unbundle20(unpackermixin):
661 663 """interpret a bundle2 stream
662 664
663 665 This class is fed with a binary stream and yields parts through its
664 666 `iterparts` methods."""
665 667
666 668 _magicstring = 'HG20'
667 669
668 670 def __init__(self, ui, fp):
669 671 """If header is specified, we do not read it out of the stream."""
670 672 self.ui = ui
671 673 self._decompressor = util.decompressors[None]
672 674 self._compressed = None
673 675 super(unbundle20, self).__init__(fp)
674 676
675 677 @util.propertycache
676 678 def params(self):
677 679 """dictionary of stream level parameters"""
678 680 indebug(self.ui, 'reading bundle2 stream parameters')
679 681 params = {}
680 682 paramssize = self._unpack(_fstreamparamsize)[0]
681 683 if paramssize < 0:
682 684 raise error.BundleValueError('negative bundle param size: %i'
683 685 % paramssize)
684 686 if paramssize:
685 687 params = self._readexact(paramssize)
686 688 params = self._processallparams(params)
687 689 return params
688 690
689 691 def _processallparams(self, paramsblock):
690 692 """"""
691 693 params = {}
692 694 for p in paramsblock.split(' '):
693 695 p = p.split('=', 1)
694 p = [urllib.unquote(i) for i in p]
696 p = [urlreq.unquote(i) for i in p]
695 697 if len(p) < 2:
696 698 p.append(None)
697 699 self._processparam(*p)
698 700 params[p[0]] = p[1]
699 701 return params
700 702
701 703
702 704 def _processparam(self, name, value):
703 705 """process a parameter, applying its effect if needed
704 706
705 707 Parameter starting with a lower case letter are advisory and will be
706 708 ignored when unknown. Those starting with an upper case letter are
707 709 mandatory and will this function will raise a KeyError when unknown.
708 710
709 711 Note: no option are currently supported. Any input will be either
710 712 ignored or failing.
711 713 """
712 714 if not name:
713 715 raise ValueError('empty parameter name')
714 716 if name[0] not in string.letters:
715 717 raise ValueError('non letter first character: %r' % name)
716 718 try:
717 719 handler = b2streamparamsmap[name.lower()]
718 720 except KeyError:
719 721 if name[0].islower():
720 722 indebug(self.ui, "ignoring unknown parameter %r" % name)
721 723 else:
722 724 raise error.BundleUnknownFeatureError(params=(name,))
723 725 else:
724 726 handler(self, name, value)
725 727
726 728 def _forwardchunks(self):
727 729 """utility to transfer a bundle2 as binary
728 730
729 731 This is made necessary by the fact the 'getbundle' command over 'ssh'
730 732 have no way to know then the reply end, relying on the bundle to be
731 733 interpreted to know its end. This is terrible and we are sorry, but we
732 734 needed to move forward to get general delta enabled.
733 735 """
734 736 yield self._magicstring
735 737 assert 'params' not in vars(self)
736 738 paramssize = self._unpack(_fstreamparamsize)[0]
737 739 if paramssize < 0:
738 740 raise error.BundleValueError('negative bundle param size: %i'
739 741 % paramssize)
740 742 yield _pack(_fstreamparamsize, paramssize)
741 743 if paramssize:
742 744 params = self._readexact(paramssize)
743 745 self._processallparams(params)
744 746 yield params
745 747 assert self._decompressor is util.decompressors[None]
746 748 # From there, payload might need to be decompressed
747 749 self._fp = self._decompressor(self._fp)
748 750 emptycount = 0
749 751 while emptycount < 2:
750 752 # so we can brainlessly loop
751 753 assert _fpartheadersize == _fpayloadsize
752 754 size = self._unpack(_fpartheadersize)[0]
753 755 yield _pack(_fpartheadersize, size)
754 756 if size:
755 757 emptycount = 0
756 758 else:
757 759 emptycount += 1
758 760 continue
759 761 if size == flaginterrupt:
760 762 continue
761 763 elif size < 0:
762 764 raise error.BundleValueError('negative chunk size: %i')
763 765 yield self._readexact(size)
764 766
765 767
766 768 def iterparts(self):
767 769 """yield all parts contained in the stream"""
768 770 # make sure param have been loaded
769 771 self.params
770 772 # From there, payload need to be decompressed
771 773 self._fp = self._decompressor(self._fp)
772 774 indebug(self.ui, 'start extraction of bundle2 parts')
773 775 headerblock = self._readpartheader()
774 776 while headerblock is not None:
775 777 part = unbundlepart(self.ui, headerblock, self._fp)
776 778 yield part
777 779 part.seek(0, 2)
778 780 headerblock = self._readpartheader()
779 781 indebug(self.ui, 'end of bundle2 stream')
780 782
781 783 def _readpartheader(self):
782 784 """reads a part header size and return the bytes blob
783 785
784 786 returns None if empty"""
785 787 headersize = self._unpack(_fpartheadersize)[0]
786 788 if headersize < 0:
787 789 raise error.BundleValueError('negative part header size: %i'
788 790 % headersize)
789 791 indebug(self.ui, 'part header size: %i' % headersize)
790 792 if headersize:
791 793 return self._readexact(headersize)
792 794 return None
793 795
794 796 def compressed(self):
795 797 self.params # load params
796 798 return self._compressed
797 799
798 800 formatmap = {'20': unbundle20}
799 801
800 802 b2streamparamsmap = {}
801 803
802 804 def b2streamparamhandler(name):
803 805 """register a handler for a stream level parameter"""
804 806 def decorator(func):
805 807 assert name not in formatmap
806 808 b2streamparamsmap[name] = func
807 809 return func
808 810 return decorator
809 811
810 812 @b2streamparamhandler('compression')
811 813 def processcompression(unbundler, param, value):
812 814 """read compression parameter and install payload decompression"""
813 815 if value not in util.decompressors:
814 816 raise error.BundleUnknownFeatureError(params=(param,),
815 817 values=(value,))
816 818 unbundler._decompressor = util.decompressors[value]
817 819 if value is not None:
818 820 unbundler._compressed = True
819 821
820 822 class bundlepart(object):
821 823 """A bundle2 part contains application level payload
822 824
823 825 The part `type` is used to route the part to the application level
824 826 handler.
825 827
826 828 The part payload is contained in ``part.data``. It could be raw bytes or a
827 829 generator of byte chunks.
828 830
829 831 You can add parameters to the part using the ``addparam`` method.
830 832 Parameters can be either mandatory (default) or advisory. Remote side
831 833 should be able to safely ignore the advisory ones.
832 834
833 835 Both data and parameters cannot be modified after the generation has begun.
834 836 """
835 837
836 838 def __init__(self, parttype, mandatoryparams=(), advisoryparams=(),
837 839 data='', mandatory=True):
838 840 validateparttype(parttype)
839 841 self.id = None
840 842 self.type = parttype
841 843 self._data = data
842 844 self._mandatoryparams = list(mandatoryparams)
843 845 self._advisoryparams = list(advisoryparams)
844 846 # checking for duplicated entries
845 847 self._seenparams = set()
846 848 for pname, __ in self._mandatoryparams + self._advisoryparams:
847 849 if pname in self._seenparams:
848 850 raise RuntimeError('duplicated params: %s' % pname)
849 851 self._seenparams.add(pname)
850 852 # status of the part's generation:
851 853 # - None: not started,
852 854 # - False: currently generated,
853 855 # - True: generation done.
854 856 self._generated = None
855 857 self.mandatory = mandatory
856 858
857 859 def copy(self):
858 860 """return a copy of the part
859 861
860 862 The new part have the very same content but no partid assigned yet.
861 863 Parts with generated data cannot be copied."""
862 864 assert not util.safehasattr(self.data, 'next')
863 865 return self.__class__(self.type, self._mandatoryparams,
864 866 self._advisoryparams, self._data, self.mandatory)
865 867
866 868 # methods used to defines the part content
867 869 @property
868 870 def data(self):
869 871 return self._data
870 872
871 873 @data.setter
872 874 def data(self, data):
873 875 if self._generated is not None:
874 876 raise error.ReadOnlyPartError('part is being generated')
875 877 self._data = data
876 878
877 879 @property
878 880 def mandatoryparams(self):
879 881 # make it an immutable tuple to force people through ``addparam``
880 882 return tuple(self._mandatoryparams)
881 883
882 884 @property
883 885 def advisoryparams(self):
884 886 # make it an immutable tuple to force people through ``addparam``
885 887 return tuple(self._advisoryparams)
886 888
887 889 def addparam(self, name, value='', mandatory=True):
888 890 if self._generated is not None:
889 891 raise error.ReadOnlyPartError('part is being generated')
890 892 if name in self._seenparams:
891 893 raise ValueError('duplicated params: %s' % name)
892 894 self._seenparams.add(name)
893 895 params = self._advisoryparams
894 896 if mandatory:
895 897 params = self._mandatoryparams
896 898 params.append((name, value))
897 899
898 900 # methods used to generates the bundle2 stream
899 901 def getchunks(self, ui):
900 902 if self._generated is not None:
901 903 raise RuntimeError('part can only be consumed once')
902 904 self._generated = False
903 905
904 906 if ui.debugflag:
905 907 msg = ['bundle2-output-part: "%s"' % self.type]
906 908 if not self.mandatory:
907 909 msg.append(' (advisory)')
908 910 nbmp = len(self.mandatoryparams)
909 911 nbap = len(self.advisoryparams)
910 912 if nbmp or nbap:
911 913 msg.append(' (params:')
912 914 if nbmp:
913 915 msg.append(' %i mandatory' % nbmp)
914 916 if nbap:
915 917 msg.append(' %i advisory' % nbmp)
916 918 msg.append(')')
917 919 if not self.data:
918 920 msg.append(' empty payload')
919 921 elif util.safehasattr(self.data, 'next'):
920 922 msg.append(' streamed payload')
921 923 else:
922 924 msg.append(' %i bytes payload' % len(self.data))
923 925 msg.append('\n')
924 926 ui.debug(''.join(msg))
925 927
926 928 #### header
927 929 if self.mandatory:
928 930 parttype = self.type.upper()
929 931 else:
930 932 parttype = self.type.lower()
931 933 outdebug(ui, 'part %s: "%s"' % (self.id, parttype))
932 934 ## parttype
933 935 header = [_pack(_fparttypesize, len(parttype)),
934 936 parttype, _pack(_fpartid, self.id),
935 937 ]
936 938 ## parameters
937 939 # count
938 940 manpar = self.mandatoryparams
939 941 advpar = self.advisoryparams
940 942 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
941 943 # size
942 944 parsizes = []
943 945 for key, value in manpar:
944 946 parsizes.append(len(key))
945 947 parsizes.append(len(value))
946 948 for key, value in advpar:
947 949 parsizes.append(len(key))
948 950 parsizes.append(len(value))
949 951 paramsizes = _pack(_makefpartparamsizes(len(parsizes) / 2), *parsizes)
950 952 header.append(paramsizes)
951 953 # key, value
952 954 for key, value in manpar:
953 955 header.append(key)
954 956 header.append(value)
955 957 for key, value in advpar:
956 958 header.append(key)
957 959 header.append(value)
958 960 ## finalize header
959 961 headerchunk = ''.join(header)
960 962 outdebug(ui, 'header chunk size: %i' % len(headerchunk))
961 963 yield _pack(_fpartheadersize, len(headerchunk))
962 964 yield headerchunk
963 965 ## payload
964 966 try:
965 967 for chunk in self._payloadchunks():
966 968 outdebug(ui, 'payload chunk size: %i' % len(chunk))
967 969 yield _pack(_fpayloadsize, len(chunk))
968 970 yield chunk
969 971 except GeneratorExit:
970 972 # GeneratorExit means that nobody is listening for our
971 973 # results anyway, so just bail quickly rather than trying
972 974 # to produce an error part.
973 975 ui.debug('bundle2-generatorexit\n')
974 976 raise
975 977 except BaseException as exc:
976 978 # backup exception data for later
977 979 ui.debug('bundle2-input-stream-interrupt: encoding exception %s'
978 980 % exc)
979 981 exc_info = sys.exc_info()
980 982 msg = 'unexpected error: %s' % exc
981 983 interpart = bundlepart('error:abort', [('message', msg)],
982 984 mandatory=False)
983 985 interpart.id = 0
984 986 yield _pack(_fpayloadsize, -1)
985 987 for chunk in interpart.getchunks(ui=ui):
986 988 yield chunk
987 989 outdebug(ui, 'closing payload chunk')
988 990 # abort current part payload
989 991 yield _pack(_fpayloadsize, 0)
990 992 raise exc_info[0], exc_info[1], exc_info[2]
991 993 # end of payload
992 994 outdebug(ui, 'closing payload chunk')
993 995 yield _pack(_fpayloadsize, 0)
994 996 self._generated = True
995 997
996 998 def _payloadchunks(self):
997 999 """yield chunks of a the part payload
998 1000
999 1001 Exists to handle the different methods to provide data to a part."""
1000 1002 # we only support fixed size data now.
1001 1003 # This will be improved in the future.
1002 1004 if util.safehasattr(self.data, 'next'):
1003 1005 buff = util.chunkbuffer(self.data)
1004 1006 chunk = buff.read(preferedchunksize)
1005 1007 while chunk:
1006 1008 yield chunk
1007 1009 chunk = buff.read(preferedchunksize)
1008 1010 elif len(self.data):
1009 1011 yield self.data
1010 1012
1011 1013
1012 1014 flaginterrupt = -1
1013 1015
1014 1016 class interrupthandler(unpackermixin):
1015 1017 """read one part and process it with restricted capability
1016 1018
1017 1019 This allows to transmit exception raised on the producer size during part
1018 1020 iteration while the consumer is reading a part.
1019 1021
1020 1022 Part processed in this manner only have access to a ui object,"""
1021 1023
1022 1024 def __init__(self, ui, fp):
1023 1025 super(interrupthandler, self).__init__(fp)
1024 1026 self.ui = ui
1025 1027
1026 1028 def _readpartheader(self):
1027 1029 """reads a part header size and return the bytes blob
1028 1030
1029 1031 returns None if empty"""
1030 1032 headersize = self._unpack(_fpartheadersize)[0]
1031 1033 if headersize < 0:
1032 1034 raise error.BundleValueError('negative part header size: %i'
1033 1035 % headersize)
1034 1036 indebug(self.ui, 'part header size: %i\n' % headersize)
1035 1037 if headersize:
1036 1038 return self._readexact(headersize)
1037 1039 return None
1038 1040
1039 1041 def __call__(self):
1040 1042
1041 1043 self.ui.debug('bundle2-input-stream-interrupt:'
1042 1044 ' opening out of band context\n')
1043 1045 indebug(self.ui, 'bundle2 stream interruption, looking for a part.')
1044 1046 headerblock = self._readpartheader()
1045 1047 if headerblock is None:
1046 1048 indebug(self.ui, 'no part found during interruption.')
1047 1049 return
1048 1050 part = unbundlepart(self.ui, headerblock, self._fp)
1049 1051 op = interruptoperation(self.ui)
1050 1052 _processpart(op, part)
1051 1053 self.ui.debug('bundle2-input-stream-interrupt:'
1052 1054 ' closing out of band context\n')
1053 1055
1054 1056 class interruptoperation(object):
1055 1057 """A limited operation to be use by part handler during interruption
1056 1058
1057 1059 It only have access to an ui object.
1058 1060 """
1059 1061
1060 1062 def __init__(self, ui):
1061 1063 self.ui = ui
1062 1064 self.reply = None
1063 1065 self.captureoutput = False
1064 1066
1065 1067 @property
1066 1068 def repo(self):
1067 1069 raise RuntimeError('no repo access from stream interruption')
1068 1070
1069 1071 def gettransaction(self):
1070 1072 raise TransactionUnavailable('no repo access from stream interruption')
1071 1073
1072 1074 class unbundlepart(unpackermixin):
1073 1075 """a bundle part read from a bundle"""
1074 1076
1075 1077 def __init__(self, ui, header, fp):
1076 1078 super(unbundlepart, self).__init__(fp)
1077 1079 self.ui = ui
1078 1080 # unbundle state attr
1079 1081 self._headerdata = header
1080 1082 self._headeroffset = 0
1081 1083 self._initialized = False
1082 1084 self.consumed = False
1083 1085 # part data
1084 1086 self.id = None
1085 1087 self.type = None
1086 1088 self.mandatoryparams = None
1087 1089 self.advisoryparams = None
1088 1090 self.params = None
1089 1091 self.mandatorykeys = ()
1090 1092 self._payloadstream = None
1091 1093 self._readheader()
1092 1094 self._mandatory = None
1093 1095 self._chunkindex = [] #(payload, file) position tuples for chunk starts
1094 1096 self._pos = 0
1095 1097
1096 1098 def _fromheader(self, size):
1097 1099 """return the next <size> byte from the header"""
1098 1100 offset = self._headeroffset
1099 1101 data = self._headerdata[offset:(offset + size)]
1100 1102 self._headeroffset = offset + size
1101 1103 return data
1102 1104
1103 1105 def _unpackheader(self, format):
1104 1106 """read given format from header
1105 1107
1106 1108 This automatically compute the size of the format to read."""
1107 1109 data = self._fromheader(struct.calcsize(format))
1108 1110 return _unpack(format, data)
1109 1111
1110 1112 def _initparams(self, mandatoryparams, advisoryparams):
1111 1113 """internal function to setup all logic related parameters"""
1112 1114 # make it read only to prevent people touching it by mistake.
1113 1115 self.mandatoryparams = tuple(mandatoryparams)
1114 1116 self.advisoryparams = tuple(advisoryparams)
1115 1117 # user friendly UI
1116 1118 self.params = dict(self.mandatoryparams)
1117 1119 self.params.update(dict(self.advisoryparams))
1118 1120 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
1119 1121
1120 1122 def _payloadchunks(self, chunknum=0):
1121 1123 '''seek to specified chunk and start yielding data'''
1122 1124 if len(self._chunkindex) == 0:
1123 1125 assert chunknum == 0, 'Must start with chunk 0'
1124 1126 self._chunkindex.append((0, super(unbundlepart, self).tell()))
1125 1127 else:
1126 1128 assert chunknum < len(self._chunkindex), \
1127 1129 'Unknown chunk %d' % chunknum
1128 1130 super(unbundlepart, self).seek(self._chunkindex[chunknum][1])
1129 1131
1130 1132 pos = self._chunkindex[chunknum][0]
1131 1133 payloadsize = self._unpack(_fpayloadsize)[0]
1132 1134 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
1133 1135 while payloadsize:
1134 1136 if payloadsize == flaginterrupt:
1135 1137 # interruption detection, the handler will now read a
1136 1138 # single part and process it.
1137 1139 interrupthandler(self.ui, self._fp)()
1138 1140 elif payloadsize < 0:
1139 1141 msg = 'negative payload chunk size: %i' % payloadsize
1140 1142 raise error.BundleValueError(msg)
1141 1143 else:
1142 1144 result = self._readexact(payloadsize)
1143 1145 chunknum += 1
1144 1146 pos += payloadsize
1145 1147 if chunknum == len(self._chunkindex):
1146 1148 self._chunkindex.append((pos,
1147 1149 super(unbundlepart, self).tell()))
1148 1150 yield result
1149 1151 payloadsize = self._unpack(_fpayloadsize)[0]
1150 1152 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
1151 1153
1152 1154 def _findchunk(self, pos):
1153 1155 '''for a given payload position, return a chunk number and offset'''
1154 1156 for chunk, (ppos, fpos) in enumerate(self._chunkindex):
1155 1157 if ppos == pos:
1156 1158 return chunk, 0
1157 1159 elif ppos > pos:
1158 1160 return chunk - 1, pos - self._chunkindex[chunk - 1][0]
1159 1161 raise ValueError('Unknown chunk')
1160 1162
1161 1163 def _readheader(self):
1162 1164 """read the header and setup the object"""
1163 1165 typesize = self._unpackheader(_fparttypesize)[0]
1164 1166 self.type = self._fromheader(typesize)
1165 1167 indebug(self.ui, 'part type: "%s"' % self.type)
1166 1168 self.id = self._unpackheader(_fpartid)[0]
1167 1169 indebug(self.ui, 'part id: "%s"' % self.id)
1168 1170 # extract mandatory bit from type
1169 1171 self.mandatory = (self.type != self.type.lower())
1170 1172 self.type = self.type.lower()
1171 1173 ## reading parameters
1172 1174 # param count
1173 1175 mancount, advcount = self._unpackheader(_fpartparamcount)
1174 1176 indebug(self.ui, 'part parameters: %i' % (mancount + advcount))
1175 1177 # param size
1176 1178 fparamsizes = _makefpartparamsizes(mancount + advcount)
1177 1179 paramsizes = self._unpackheader(fparamsizes)
1178 1180 # make it a list of couple again
1179 1181 paramsizes = zip(paramsizes[::2], paramsizes[1::2])
1180 1182 # split mandatory from advisory
1181 1183 mansizes = paramsizes[:mancount]
1182 1184 advsizes = paramsizes[mancount:]
1183 1185 # retrieve param value
1184 1186 manparams = []
1185 1187 for key, value in mansizes:
1186 1188 manparams.append((self._fromheader(key), self._fromheader(value)))
1187 1189 advparams = []
1188 1190 for key, value in advsizes:
1189 1191 advparams.append((self._fromheader(key), self._fromheader(value)))
1190 1192 self._initparams(manparams, advparams)
1191 1193 ## part payload
1192 1194 self._payloadstream = util.chunkbuffer(self._payloadchunks())
1193 1195 # we read the data, tell it
1194 1196 self._initialized = True
1195 1197
1196 1198 def read(self, size=None):
1197 1199 """read payload data"""
1198 1200 if not self._initialized:
1199 1201 self._readheader()
1200 1202 if size is None:
1201 1203 data = self._payloadstream.read()
1202 1204 else:
1203 1205 data = self._payloadstream.read(size)
1204 1206 self._pos += len(data)
1205 1207 if size is None or len(data) < size:
1206 1208 if not self.consumed and self._pos:
1207 1209 self.ui.debug('bundle2-input-part: total payload size %i\n'
1208 1210 % self._pos)
1209 1211 self.consumed = True
1210 1212 return data
1211 1213
1212 1214 def tell(self):
1213 1215 return self._pos
1214 1216
1215 1217 def seek(self, offset, whence=0):
1216 1218 if whence == 0:
1217 1219 newpos = offset
1218 1220 elif whence == 1:
1219 1221 newpos = self._pos + offset
1220 1222 elif whence == 2:
1221 1223 if not self.consumed:
1222 1224 self.read()
1223 1225 newpos = self._chunkindex[-1][0] - offset
1224 1226 else:
1225 1227 raise ValueError('Unknown whence value: %r' % (whence,))
1226 1228
1227 1229 if newpos > self._chunkindex[-1][0] and not self.consumed:
1228 1230 self.read()
1229 1231 if not 0 <= newpos <= self._chunkindex[-1][0]:
1230 1232 raise ValueError('Offset out of range')
1231 1233
1232 1234 if self._pos != newpos:
1233 1235 chunk, internaloffset = self._findchunk(newpos)
1234 1236 self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk))
1235 1237 adjust = self.read(internaloffset)
1236 1238 if len(adjust) != internaloffset:
1237 1239 raise error.Abort(_('Seek failed\n'))
1238 1240 self._pos = newpos
1239 1241
1240 1242 # These are only the static capabilities.
1241 1243 # Check the 'getrepocaps' function for the rest.
1242 1244 capabilities = {'HG20': (),
1243 1245 'error': ('abort', 'unsupportedcontent', 'pushraced',
1244 1246 'pushkey'),
1245 1247 'listkeys': (),
1246 1248 'pushkey': (),
1247 1249 'digests': tuple(sorted(util.DIGESTS.keys())),
1248 1250 'remote-changegroup': ('http', 'https'),
1249 1251 'hgtagsfnodes': (),
1250 1252 }
1251 1253
1252 1254 def getrepocaps(repo, allowpushback=False):
1253 1255 """return the bundle2 capabilities for a given repo
1254 1256
1255 1257 Exists to allow extensions (like evolution) to mutate the capabilities.
1256 1258 """
1257 1259 caps = capabilities.copy()
1258 1260 caps['changegroup'] = tuple(sorted(
1259 1261 changegroup.supportedincomingversions(repo)))
1260 1262 if obsolete.isenabled(repo, obsolete.exchangeopt):
1261 1263 supportedformat = tuple('V%i' % v for v in obsolete.formats)
1262 1264 caps['obsmarkers'] = supportedformat
1263 1265 if allowpushback:
1264 1266 caps['pushback'] = ()
1265 1267 return caps
1266 1268
1267 1269 def bundle2caps(remote):
1268 1270 """return the bundle capabilities of a peer as dict"""
1269 1271 raw = remote.capable('bundle2')
1270 1272 if not raw and raw != '':
1271 1273 return {}
1272 capsblob = urllib.unquote(remote.capable('bundle2'))
1274 capsblob = urlreq.unquote(remote.capable('bundle2'))
1273 1275 return decodecaps(capsblob)
1274 1276
1275 1277 def obsmarkersversion(caps):
1276 1278 """extract the list of supported obsmarkers versions from a bundle2caps dict
1277 1279 """
1278 1280 obscaps = caps.get('obsmarkers', ())
1279 1281 return [int(c[1:]) for c in obscaps if c.startswith('V')]
1280 1282
1281 1283 def writebundle(ui, cg, filename, bundletype, vfs=None, compression=None):
1282 1284 """Write a bundle file and return its filename.
1283 1285
1284 1286 Existing files will not be overwritten.
1285 1287 If no filename is specified, a temporary file is created.
1286 1288 bz2 compression can be turned off.
1287 1289 The bundle file will be deleted in case of errors.
1288 1290 """
1289 1291
1290 1292 if bundletype == "HG20":
1291 1293 bundle = bundle20(ui)
1292 1294 bundle.setcompression(compression)
1293 1295 part = bundle.newpart('changegroup', data=cg.getchunks())
1294 1296 part.addparam('version', cg.version)
1295 1297 chunkiter = bundle.getchunks()
1296 1298 else:
1297 1299 # compression argument is only for the bundle2 case
1298 1300 assert compression is None
1299 1301 if cg.version != '01':
1300 1302 raise error.Abort(_('old bundle types only supports v1 '
1301 1303 'changegroups'))
1302 1304 header, comp = bundletypes[bundletype]
1303 1305 if comp not in util.compressors:
1304 1306 raise error.Abort(_('unknown stream compression type: %s')
1305 1307 % comp)
1306 1308 z = util.compressors[comp]()
1307 1309 subchunkiter = cg.getchunks()
1308 1310 def chunkiter():
1309 1311 yield header
1310 1312 for chunk in subchunkiter:
1311 1313 yield z.compress(chunk)
1312 1314 yield z.flush()
1313 1315 chunkiter = chunkiter()
1314 1316
1315 1317 # parse the changegroup data, otherwise we will block
1316 1318 # in case of sshrepo because we don't know the end of the stream
1317 1319 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1318 1320
1319 1321 @parthandler('changegroup', ('version', 'nbchanges', 'treemanifest'))
1320 1322 def handlechangegroup(op, inpart):
1321 1323 """apply a changegroup part on the repo
1322 1324
1323 1325 This is a very early implementation that will massive rework before being
1324 1326 inflicted to any end-user.
1325 1327 """
1326 1328 # Make sure we trigger a transaction creation
1327 1329 #
1328 1330 # The addchangegroup function will get a transaction object by itself, but
1329 1331 # we need to make sure we trigger the creation of a transaction object used
1330 1332 # for the whole processing scope.
1331 1333 op.gettransaction()
1332 1334 unpackerversion = inpart.params.get('version', '01')
1333 1335 # We should raise an appropriate exception here
1334 1336 cg = changegroup.getunbundler(unpackerversion, inpart, None)
1335 1337 # the source and url passed here are overwritten by the one contained in
1336 1338 # the transaction.hookargs argument. So 'bundle2' is a placeholder
1337 1339 nbchangesets = None
1338 1340 if 'nbchanges' in inpart.params:
1339 1341 nbchangesets = int(inpart.params.get('nbchanges'))
1340 1342 if ('treemanifest' in inpart.params and
1341 1343 'treemanifest' not in op.repo.requirements):
1342 1344 if len(op.repo.changelog) != 0:
1343 1345 raise error.Abort(_(
1344 1346 "bundle contains tree manifests, but local repo is "
1345 1347 "non-empty and does not use tree manifests"))
1346 1348 op.repo.requirements.add('treemanifest')
1347 1349 op.repo._applyopenerreqs()
1348 1350 op.repo._writerequirements()
1349 1351 ret = cg.apply(op.repo, 'bundle2', 'bundle2', expectedtotal=nbchangesets)
1350 1352 op.records.add('changegroup', {'return': ret})
1351 1353 if op.reply is not None:
1352 1354 # This is definitely not the final form of this
1353 1355 # return. But one need to start somewhere.
1354 1356 part = op.reply.newpart('reply:changegroup', mandatory=False)
1355 1357 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1356 1358 part.addparam('return', '%i' % ret, mandatory=False)
1357 1359 assert not inpart.read()
1358 1360
1359 1361 _remotechangegroupparams = tuple(['url', 'size', 'digests'] +
1360 1362 ['digest:%s' % k for k in util.DIGESTS.keys()])
1361 1363 @parthandler('remote-changegroup', _remotechangegroupparams)
1362 1364 def handleremotechangegroup(op, inpart):
1363 1365 """apply a bundle10 on the repo, given an url and validation information
1364 1366
1365 1367 All the information about the remote bundle to import are given as
1366 1368 parameters. The parameters include:
1367 1369 - url: the url to the bundle10.
1368 1370 - size: the bundle10 file size. It is used to validate what was
1369 1371 retrieved by the client matches the server knowledge about the bundle.
1370 1372 - digests: a space separated list of the digest types provided as
1371 1373 parameters.
1372 1374 - digest:<digest-type>: the hexadecimal representation of the digest with
1373 1375 that name. Like the size, it is used to validate what was retrieved by
1374 1376 the client matches what the server knows about the bundle.
1375 1377
1376 1378 When multiple digest types are given, all of them are checked.
1377 1379 """
1378 1380 try:
1379 1381 raw_url = inpart.params['url']
1380 1382 except KeyError:
1381 1383 raise error.Abort(_('remote-changegroup: missing "%s" param') % 'url')
1382 1384 parsed_url = util.url(raw_url)
1383 1385 if parsed_url.scheme not in capabilities['remote-changegroup']:
1384 1386 raise error.Abort(_('remote-changegroup does not support %s urls') %
1385 1387 parsed_url.scheme)
1386 1388
1387 1389 try:
1388 1390 size = int(inpart.params['size'])
1389 1391 except ValueError:
1390 1392 raise error.Abort(_('remote-changegroup: invalid value for param "%s"')
1391 1393 % 'size')
1392 1394 except KeyError:
1393 1395 raise error.Abort(_('remote-changegroup: missing "%s" param') % 'size')
1394 1396
1395 1397 digests = {}
1396 1398 for typ in inpart.params.get('digests', '').split():
1397 1399 param = 'digest:%s' % typ
1398 1400 try:
1399 1401 value = inpart.params[param]
1400 1402 except KeyError:
1401 1403 raise error.Abort(_('remote-changegroup: missing "%s" param') %
1402 1404 param)
1403 1405 digests[typ] = value
1404 1406
1405 1407 real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests)
1406 1408
1407 1409 # Make sure we trigger a transaction creation
1408 1410 #
1409 1411 # The addchangegroup function will get a transaction object by itself, but
1410 1412 # we need to make sure we trigger the creation of a transaction object used
1411 1413 # for the whole processing scope.
1412 1414 op.gettransaction()
1413 1415 from . import exchange
1414 1416 cg = exchange.readbundle(op.repo.ui, real_part, raw_url)
1415 1417 if not isinstance(cg, changegroup.cg1unpacker):
1416 1418 raise error.Abort(_('%s: not a bundle version 1.0') %
1417 1419 util.hidepassword(raw_url))
1418 1420 ret = cg.apply(op.repo, 'bundle2', 'bundle2')
1419 1421 op.records.add('changegroup', {'return': ret})
1420 1422 if op.reply is not None:
1421 1423 # This is definitely not the final form of this
1422 1424 # return. But one need to start somewhere.
1423 1425 part = op.reply.newpart('reply:changegroup')
1424 1426 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1425 1427 part.addparam('return', '%i' % ret, mandatory=False)
1426 1428 try:
1427 1429 real_part.validate()
1428 1430 except error.Abort as e:
1429 1431 raise error.Abort(_('bundle at %s is corrupted:\n%s') %
1430 1432 (util.hidepassword(raw_url), str(e)))
1431 1433 assert not inpart.read()
1432 1434
1433 1435 @parthandler('reply:changegroup', ('return', 'in-reply-to'))
1434 1436 def handlereplychangegroup(op, inpart):
1435 1437 ret = int(inpart.params['return'])
1436 1438 replyto = int(inpart.params['in-reply-to'])
1437 1439 op.records.add('changegroup', {'return': ret}, replyto)
1438 1440
1439 1441 @parthandler('check:heads')
1440 1442 def handlecheckheads(op, inpart):
1441 1443 """check that head of the repo did not change
1442 1444
1443 1445 This is used to detect a push race when using unbundle.
1444 1446 This replaces the "heads" argument of unbundle."""
1445 1447 h = inpart.read(20)
1446 1448 heads = []
1447 1449 while len(h) == 20:
1448 1450 heads.append(h)
1449 1451 h = inpart.read(20)
1450 1452 assert not h
1451 1453 # Trigger a transaction so that we are guaranteed to have the lock now.
1452 1454 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1453 1455 op.gettransaction()
1454 1456 if heads != op.repo.heads():
1455 1457 raise error.PushRaced('repository changed while pushing - '
1456 1458 'please try again')
1457 1459
1458 1460 @parthandler('output')
1459 1461 def handleoutput(op, inpart):
1460 1462 """forward output captured on the server to the client"""
1461 1463 for line in inpart.read().splitlines():
1462 1464 op.ui.status(('remote: %s\n' % line))
1463 1465
1464 1466 @parthandler('replycaps')
1465 1467 def handlereplycaps(op, inpart):
1466 1468 """Notify that a reply bundle should be created
1467 1469
1468 1470 The payload contains the capabilities information for the reply"""
1469 1471 caps = decodecaps(inpart.read())
1470 1472 if op.reply is None:
1471 1473 op.reply = bundle20(op.ui, caps)
1472 1474
1473 1475 class AbortFromPart(error.Abort):
1474 1476 """Sub-class of Abort that denotes an error from a bundle2 part."""
1475 1477
1476 1478 @parthandler('error:abort', ('message', 'hint'))
1477 1479 def handleerrorabort(op, inpart):
1478 1480 """Used to transmit abort error over the wire"""
1479 1481 raise AbortFromPart(inpart.params['message'],
1480 1482 hint=inpart.params.get('hint'))
1481 1483
1482 1484 @parthandler('error:pushkey', ('namespace', 'key', 'new', 'old', 'ret',
1483 1485 'in-reply-to'))
1484 1486 def handleerrorpushkey(op, inpart):
1485 1487 """Used to transmit failure of a mandatory pushkey over the wire"""
1486 1488 kwargs = {}
1487 1489 for name in ('namespace', 'key', 'new', 'old', 'ret'):
1488 1490 value = inpart.params.get(name)
1489 1491 if value is not None:
1490 1492 kwargs[name] = value
1491 1493 raise error.PushkeyFailed(inpart.params['in-reply-to'], **kwargs)
1492 1494
1493 1495 @parthandler('error:unsupportedcontent', ('parttype', 'params'))
1494 1496 def handleerrorunsupportedcontent(op, inpart):
1495 1497 """Used to transmit unknown content error over the wire"""
1496 1498 kwargs = {}
1497 1499 parttype = inpart.params.get('parttype')
1498 1500 if parttype is not None:
1499 1501 kwargs['parttype'] = parttype
1500 1502 params = inpart.params.get('params')
1501 1503 if params is not None:
1502 1504 kwargs['params'] = params.split('\0')
1503 1505
1504 1506 raise error.BundleUnknownFeatureError(**kwargs)
1505 1507
1506 1508 @parthandler('error:pushraced', ('message',))
1507 1509 def handleerrorpushraced(op, inpart):
1508 1510 """Used to transmit push race error over the wire"""
1509 1511 raise error.ResponseError(_('push failed:'), inpart.params['message'])
1510 1512
1511 1513 @parthandler('listkeys', ('namespace',))
1512 1514 def handlelistkeys(op, inpart):
1513 1515 """retrieve pushkey namespace content stored in a bundle2"""
1514 1516 namespace = inpart.params['namespace']
1515 1517 r = pushkey.decodekeys(inpart.read())
1516 1518 op.records.add('listkeys', (namespace, r))
1517 1519
1518 1520 @parthandler('pushkey', ('namespace', 'key', 'old', 'new'))
1519 1521 def handlepushkey(op, inpart):
1520 1522 """process a pushkey request"""
1521 1523 dec = pushkey.decode
1522 1524 namespace = dec(inpart.params['namespace'])
1523 1525 key = dec(inpart.params['key'])
1524 1526 old = dec(inpart.params['old'])
1525 1527 new = dec(inpart.params['new'])
1526 1528 # Grab the transaction to ensure that we have the lock before performing the
1527 1529 # pushkey.
1528 1530 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1529 1531 op.gettransaction()
1530 1532 ret = op.repo.pushkey(namespace, key, old, new)
1531 1533 record = {'namespace': namespace,
1532 1534 'key': key,
1533 1535 'old': old,
1534 1536 'new': new}
1535 1537 op.records.add('pushkey', record)
1536 1538 if op.reply is not None:
1537 1539 rpart = op.reply.newpart('reply:pushkey')
1538 1540 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1539 1541 rpart.addparam('return', '%i' % ret, mandatory=False)
1540 1542 if inpart.mandatory and not ret:
1541 1543 kwargs = {}
1542 1544 for key in ('namespace', 'key', 'new', 'old', 'ret'):
1543 1545 if key in inpart.params:
1544 1546 kwargs[key] = inpart.params[key]
1545 1547 raise error.PushkeyFailed(partid=str(inpart.id), **kwargs)
1546 1548
1547 1549 @parthandler('reply:pushkey', ('return', 'in-reply-to'))
1548 1550 def handlepushkeyreply(op, inpart):
1549 1551 """retrieve the result of a pushkey request"""
1550 1552 ret = int(inpart.params['return'])
1551 1553 partid = int(inpart.params['in-reply-to'])
1552 1554 op.records.add('pushkey', {'return': ret}, partid)
1553 1555
1554 1556 @parthandler('obsmarkers')
1555 1557 def handleobsmarker(op, inpart):
1556 1558 """add a stream of obsmarkers to the repo"""
1557 1559 tr = op.gettransaction()
1558 1560 markerdata = inpart.read()
1559 1561 if op.ui.config('experimental', 'obsmarkers-exchange-debug', False):
1560 1562 op.ui.write(('obsmarker-exchange: %i bytes received\n')
1561 1563 % len(markerdata))
1562 1564 # The mergemarkers call will crash if marker creation is not enabled.
1563 1565 # we want to avoid this if the part is advisory.
1564 1566 if not inpart.mandatory and op.repo.obsstore.readonly:
1565 1567 op.repo.ui.debug('ignoring obsolescence markers, feature not enabled')
1566 1568 return
1567 1569 new = op.repo.obsstore.mergemarkers(tr, markerdata)
1568 1570 if new:
1569 1571 op.repo.ui.status(_('%i new obsolescence markers\n') % new)
1570 1572 op.records.add('obsmarkers', {'new': new})
1571 1573 if op.reply is not None:
1572 1574 rpart = op.reply.newpart('reply:obsmarkers')
1573 1575 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1574 1576 rpart.addparam('new', '%i' % new, mandatory=False)
1575 1577
1576 1578
1577 1579 @parthandler('reply:obsmarkers', ('new', 'in-reply-to'))
1578 1580 def handleobsmarkerreply(op, inpart):
1579 1581 """retrieve the result of a pushkey request"""
1580 1582 ret = int(inpart.params['new'])
1581 1583 partid = int(inpart.params['in-reply-to'])
1582 1584 op.records.add('obsmarkers', {'new': ret}, partid)
1583 1585
1584 1586 @parthandler('hgtagsfnodes')
1585 1587 def handlehgtagsfnodes(op, inpart):
1586 1588 """Applies .hgtags fnodes cache entries to the local repo.
1587 1589
1588 1590 Payload is pairs of 20 byte changeset nodes and filenodes.
1589 1591 """
1590 1592 # Grab the transaction so we ensure that we have the lock at this point.
1591 1593 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1592 1594 op.gettransaction()
1593 1595 cache = tags.hgtagsfnodescache(op.repo.unfiltered())
1594 1596
1595 1597 count = 0
1596 1598 while True:
1597 1599 node = inpart.read(20)
1598 1600 fnode = inpart.read(20)
1599 1601 if len(node) < 20 or len(fnode) < 20:
1600 1602 op.ui.debug('ignoring incomplete received .hgtags fnodes data\n')
1601 1603 break
1602 1604 cache.setfnode(node, fnode)
1603 1605 count += 1
1604 1606
1605 1607 cache.write()
1606 1608 op.ui.debug('applied %i hgtags fnodes cache entries\n' % count)
@@ -1,467 +1,472 b''
1 1 # This library is free software; you can redistribute it and/or
2 2 # modify it under the terms of the GNU Lesser General Public
3 3 # License as published by the Free Software Foundation; either
4 4 # version 2.1 of the License, or (at your option) any later version.
5 5 #
6 6 # This library is distributed in the hope that it will be useful,
7 7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 9 # Lesser General Public License for more details.
10 10 #
11 11 # You should have received a copy of the GNU Lesser General Public
12 12 # License along with this library; if not, see
13 13 # <http://www.gnu.org/licenses/>.
14 14
15 15 # This file is part of urlgrabber, a high-level cross-protocol url-grabber
16 16 # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
17 17
18 18 # $Id: byterange.py,v 1.9 2005/02/14 21:55:07 mstenner Exp $
19 19
20 20 from __future__ import absolute_import
21 21
22 22 import email
23 23 import ftplib
24 24 import mimetypes
25 25 import os
26 26 import re
27 27 import socket
28 28 import stat
29 import urllib
30 import urllib2
29
30 from . import (
31 util,
32 )
33
34 urlerr = util.urlerr
35 urlreq = util.urlreq
31 36
32 addclosehook = urllib.addclosehook
33 addinfourl = urllib.addinfourl
34 splitattr = urllib.splitattr
35 splitpasswd = urllib.splitpasswd
36 splitport = urllib.splitport
37 splituser = urllib.splituser
38 unquote = urllib.unquote
37 addclosehook = urlreq.addclosehook
38 addinfourl = urlreq.addinfourl
39 splitattr = urlreq.splitattr
40 splitpasswd = urlreq.splitpasswd
41 splitport = urlreq.splitport
42 splituser = urlreq.splituser
43 unquote = urlreq.unquote
39 44
40 45 class RangeError(IOError):
41 46 """Error raised when an unsatisfiable range is requested."""
42 47 pass
43 48
44 class HTTPRangeHandler(urllib2.BaseHandler):
49 class HTTPRangeHandler(urlreq.basehandler):
45 50 """Handler that enables HTTP Range headers.
46 51
47 52 This was extremely simple. The Range header is a HTTP feature to
48 53 begin with so all this class does is tell urllib2 that the
49 54 "206 Partial Content" response from the HTTP server is what we
50 55 expected.
51 56
52 57 Example:
53 58 import urllib2
54 59 import byterange
55 60
56 61 range_handler = range.HTTPRangeHandler()
57 opener = urllib2.build_opener(range_handler)
62 opener = urlreq.buildopener(range_handler)
58 63
59 64 # install it
60 urllib2.install_opener(opener)
65 urlreq.installopener(opener)
61 66
62 67 # create Request and set Range header
63 req = urllib2.Request('http://www.python.org/')
68 req = urlreq.request('http://www.python.org/')
64 69 req.header['Range'] = 'bytes=30-50'
65 f = urllib2.urlopen(req)
70 f = urlreq.urlopen(req)
66 71 """
67 72
68 73 def http_error_206(self, req, fp, code, msg, hdrs):
69 74 # 206 Partial Content Response
70 r = urllib.addinfourl(fp, hdrs, req.get_full_url())
75 r = urlreq.addinfourl(fp, hdrs, req.get_full_url())
71 76 r.code = code
72 77 r.msg = msg
73 78 return r
74 79
75 80 def http_error_416(self, req, fp, code, msg, hdrs):
76 81 # HTTP's Range Not Satisfiable error
77 82 raise RangeError('Requested Range Not Satisfiable')
78 83
79 84 class RangeableFileObject(object):
80 85 """File object wrapper to enable raw range handling.
81 86 This was implemented primarily for handling range
82 87 specifications for file:// urls. This object effectively makes
83 88 a file object look like it consists only of a range of bytes in
84 89 the stream.
85 90
86 91 Examples:
87 92 # expose 10 bytes, starting at byte position 20, from
88 93 # /etc/aliases.
89 94 >>> fo = RangeableFileObject(file('/etc/passwd', 'r'), (20,30))
90 95 # seek seeks within the range (to position 23 in this case)
91 96 >>> fo.seek(3)
92 97 # tell tells where your at _within the range_ (position 3 in
93 98 # this case)
94 99 >>> fo.tell()
95 100 # read EOFs if an attempt is made to read past the last
96 101 # byte in the range. the following will return only 7 bytes.
97 102 >>> fo.read(30)
98 103 """
99 104
100 105 def __init__(self, fo, rangetup):
101 106 """Create a RangeableFileObject.
102 107 fo -- a file like object. only the read() method need be
103 108 supported but supporting an optimized seek() is
104 109 preferable.
105 110 rangetup -- a (firstbyte,lastbyte) tuple specifying the range
106 111 to work over.
107 112 The file object provided is assumed to be at byte offset 0.
108 113 """
109 114 self.fo = fo
110 115 (self.firstbyte, self.lastbyte) = range_tuple_normalize(rangetup)
111 116 self.realpos = 0
112 117 self._do_seek(self.firstbyte)
113 118
114 119 def __getattr__(self, name):
115 120 """This effectively allows us to wrap at the instance level.
116 121 Any attribute not found in _this_ object will be searched for
117 122 in self.fo. This includes methods."""
118 123 return getattr(self.fo, name)
119 124
120 125 def tell(self):
121 126 """Return the position within the range.
122 127 This is different from fo.seek in that position 0 is the
123 128 first byte position of the range tuple. For example, if
124 129 this object was created with a range tuple of (500,899),
125 130 tell() will return 0 when at byte position 500 of the file.
126 131 """
127 132 return (self.realpos - self.firstbyte)
128 133
129 134 def seek(self, offset, whence=0):
130 135 """Seek within the byte range.
131 136 Positioning is identical to that described under tell().
132 137 """
133 138 assert whence in (0, 1, 2)
134 139 if whence == 0: # absolute seek
135 140 realoffset = self.firstbyte + offset
136 141 elif whence == 1: # relative seek
137 142 realoffset = self.realpos + offset
138 143 elif whence == 2: # absolute from end of file
139 144 # XXX: are we raising the right Error here?
140 145 raise IOError('seek from end of file not supported.')
141 146
142 147 # do not allow seek past lastbyte in range
143 148 if self.lastbyte and (realoffset >= self.lastbyte):
144 149 realoffset = self.lastbyte
145 150
146 151 self._do_seek(realoffset - self.realpos)
147 152
148 153 def read(self, size=-1):
149 154 """Read within the range.
150 155 This method will limit the size read based on the range.
151 156 """
152 157 size = self._calc_read_size(size)
153 158 rslt = self.fo.read(size)
154 159 self.realpos += len(rslt)
155 160 return rslt
156 161
157 162 def readline(self, size=-1):
158 163 """Read lines within the range.
159 164 This method will limit the size read based on the range.
160 165 """
161 166 size = self._calc_read_size(size)
162 167 rslt = self.fo.readline(size)
163 168 self.realpos += len(rslt)
164 169 return rslt
165 170
166 171 def _calc_read_size(self, size):
167 172 """Handles calculating the amount of data to read based on
168 173 the range.
169 174 """
170 175 if self.lastbyte:
171 176 if size > -1:
172 177 if ((self.realpos + size) >= self.lastbyte):
173 178 size = (self.lastbyte - self.realpos)
174 179 else:
175 180 size = (self.lastbyte - self.realpos)
176 181 return size
177 182
178 183 def _do_seek(self, offset):
179 184 """Seek based on whether wrapped object supports seek().
180 185 offset is relative to the current position (self.realpos).
181 186 """
182 187 assert offset >= 0
183 188 seek = getattr(self.fo, 'seek', self._poor_mans_seek)
184 189 seek(self.realpos + offset)
185 190 self.realpos += offset
186 191
187 192 def _poor_mans_seek(self, offset):
188 193 """Seek by calling the wrapped file objects read() method.
189 194 This is used for file like objects that do not have native
190 195 seek support. The wrapped objects read() method is called
191 196 to manually seek to the desired position.
192 197 offset -- read this number of bytes from the wrapped
193 198 file object.
194 199 raise RangeError if we encounter EOF before reaching the
195 200 specified offset.
196 201 """
197 202 pos = 0
198 203 bufsize = 1024
199 204 while pos < offset:
200 205 if (pos + bufsize) > offset:
201 206 bufsize = offset - pos
202 207 buf = self.fo.read(bufsize)
203 208 if len(buf) != bufsize:
204 209 raise RangeError('Requested Range Not Satisfiable')
205 210 pos += bufsize
206 211
207 class FileRangeHandler(urllib2.FileHandler):
212 class FileRangeHandler(urlreq.filehandler):
208 213 """FileHandler subclass that adds Range support.
209 214 This class handles Range headers exactly like an HTTP
210 215 server would.
211 216 """
212 217 def open_local_file(self, req):
213 218 host = req.get_host()
214 219 file = req.get_selector()
215 localfile = urllib.url2pathname(file)
220 localfile = urlreq.url2pathname(file)
216 221 stats = os.stat(localfile)
217 222 size = stats[stat.ST_SIZE]
218 223 modified = email.Utils.formatdate(stats[stat.ST_MTIME])
219 224 mtype = mimetypes.guess_type(file)[0]
220 225 if host:
221 host, port = urllib.splitport(host)
226 host, port = urlreq.splitport(host)
222 227 if port or socket.gethostbyname(host) not in self.get_names():
223 raise urllib2.URLError('file not on local host')
228 raise urlerr.urlerror('file not on local host')
224 229 fo = open(localfile,'rb')
225 230 brange = req.headers.get('Range', None)
226 231 brange = range_header_to_tuple(brange)
227 232 assert brange != ()
228 233 if brange:
229 234 (fb, lb) = brange
230 235 if lb == '':
231 236 lb = size
232 237 if fb < 0 or fb > size or lb > size:
233 238 raise RangeError('Requested Range Not Satisfiable')
234 239 size = (lb - fb)
235 240 fo = RangeableFileObject(fo, (fb, lb))
236 241 headers = email.message_from_string(
237 242 'Content-Type: %s\nContent-Length: %d\nLast-Modified: %s\n' %
238 243 (mtype or 'text/plain', size, modified))
239 return urllib.addinfourl(fo, headers, 'file:'+file)
244 return urlreq.addinfourl(fo, headers, 'file:'+file)
240 245
241 246
242 247 # FTP Range Support
243 248 # Unfortunately, a large amount of base FTP code had to be copied
244 249 # from urllib and urllib2 in order to insert the FTP REST command.
245 250 # Code modifications for range support have been commented as
246 251 # follows:
247 252 # -- range support modifications start/end here
248 253
249 class FTPRangeHandler(urllib2.FTPHandler):
254 class FTPRangeHandler(urlreq.ftphandler):
250 255 def ftp_open(self, req):
251 256 host = req.get_host()
252 257 if not host:
253 258 raise IOError('ftp error', 'no host given')
254 259 host, port = splitport(host)
255 260 if port is None:
256 261 port = ftplib.FTP_PORT
257 262 else:
258 263 port = int(port)
259 264
260 265 # username/password handling
261 266 user, host = splituser(host)
262 267 if user:
263 268 user, passwd = splitpasswd(user)
264 269 else:
265 270 passwd = None
266 271 host = unquote(host)
267 272 user = unquote(user or '')
268 273 passwd = unquote(passwd or '')
269 274
270 275 try:
271 276 host = socket.gethostbyname(host)
272 277 except socket.error as msg:
273 raise urllib2.URLError(msg)
278 raise urlerr.urlerror(msg)
274 279 path, attrs = splitattr(req.get_selector())
275 280 dirs = path.split('/')
276 281 dirs = map(unquote, dirs)
277 282 dirs, file = dirs[:-1], dirs[-1]
278 283 if dirs and not dirs[0]:
279 284 dirs = dirs[1:]
280 285 try:
281 286 fw = self.connect_ftp(user, passwd, host, port, dirs)
282 287 if file:
283 288 type = 'I'
284 289 else:
285 290 type = 'D'
286 291
287 292 for attr in attrs:
288 293 attr, value = splitattr(attr)
289 294 if attr.lower() == 'type' and \
290 295 value in ('a', 'A', 'i', 'I', 'd', 'D'):
291 296 type = value.upper()
292 297
293 298 # -- range support modifications start here
294 299 rest = None
295 300 range_tup = range_header_to_tuple(req.headers.get('Range', None))
296 301 assert range_tup != ()
297 302 if range_tup:
298 303 (fb, lb) = range_tup
299 304 if fb > 0:
300 305 rest = fb
301 306 # -- range support modifications end here
302 307
303 308 fp, retrlen = fw.retrfile(file, type, rest)
304 309
305 310 # -- range support modifications start here
306 311 if range_tup:
307 312 (fb, lb) = range_tup
308 313 if lb == '':
309 314 if retrlen is None or retrlen == 0:
310 315 raise RangeError('Requested Range Not Satisfiable due'
311 316 ' to unobtainable file length.')
312 317 lb = retrlen
313 318 retrlen = lb - fb
314 319 if retrlen < 0:
315 320 # beginning of range is larger than file
316 321 raise RangeError('Requested Range Not Satisfiable')
317 322 else:
318 323 retrlen = lb - fb
319 324 fp = RangeableFileObject(fp, (0, retrlen))
320 325 # -- range support modifications end here
321 326
322 327 headers = ""
323 328 mtype = mimetypes.guess_type(req.get_full_url())[0]
324 329 if mtype:
325 330 headers += "Content-Type: %s\n" % mtype
326 331 if retrlen is not None and retrlen >= 0:
327 332 headers += "Content-Length: %d\n" % retrlen
328 333 headers = email.message_from_string(headers)
329 334 return addinfourl(fp, headers, req.get_full_url())
330 335 except ftplib.all_errors as msg:
331 336 raise IOError('ftp error', msg)
332 337
333 338 def connect_ftp(self, user, passwd, host, port, dirs):
334 339 fw = ftpwrapper(user, passwd, host, port, dirs)
335 340 return fw
336 341
337 class ftpwrapper(urllib.ftpwrapper):
342 class ftpwrapper(urlreq.ftpwrapper):
338 343 # range support note:
339 344 # this ftpwrapper code is copied directly from
340 345 # urllib. The only enhancement is to add the rest
341 346 # argument and pass it on to ftp.ntransfercmd
342 347 def retrfile(self, file, type, rest=None):
343 348 self.endtransfer()
344 349 if type in ('d', 'D'):
345 350 cmd = 'TYPE A'
346 351 isdir = 1
347 352 else:
348 353 cmd = 'TYPE ' + type
349 354 isdir = 0
350 355 try:
351 356 self.ftp.voidcmd(cmd)
352 357 except ftplib.all_errors:
353 358 self.init()
354 359 self.ftp.voidcmd(cmd)
355 360 conn = None
356 361 if file and not isdir:
357 362 # Use nlst to see if the file exists at all
358 363 try:
359 364 self.ftp.nlst(file)
360 365 except ftplib.error_perm as reason:
361 366 raise IOError('ftp error', reason)
362 367 # Restore the transfer mode!
363 368 self.ftp.voidcmd(cmd)
364 369 # Try to retrieve as a file
365 370 try:
366 371 cmd = 'RETR ' + file
367 372 conn = self.ftp.ntransfercmd(cmd, rest)
368 373 except ftplib.error_perm as reason:
369 374 if str(reason).startswith('501'):
370 375 # workaround for REST not supported error
371 376 fp, retrlen = self.retrfile(file, type)
372 377 fp = RangeableFileObject(fp, (rest,''))
373 378 return (fp, retrlen)
374 379 elif not str(reason).startswith('550'):
375 380 raise IOError('ftp error', reason)
376 381 if not conn:
377 382 # Set transfer mode to ASCII!
378 383 self.ftp.voidcmd('TYPE A')
379 384 # Try a directory listing
380 385 if file:
381 386 cmd = 'LIST ' + file
382 387 else:
383 388 cmd = 'LIST'
384 389 conn = self.ftp.ntransfercmd(cmd)
385 390 self.busy = 1
386 391 # Pass back both a suitably decorated object and a retrieval length
387 392 return (addclosehook(conn[0].makefile('rb'),
388 393 self.endtransfer), conn[1])
389 394
390 395
391 396 ####################################################################
392 397 # Range Tuple Functions
393 398 # XXX: These range tuple functions might go better in a class.
394 399
395 400 _rangere = None
396 401 def range_header_to_tuple(range_header):
397 402 """Get a (firstbyte,lastbyte) tuple from a Range header value.
398 403
399 404 Range headers have the form "bytes=<firstbyte>-<lastbyte>". This
400 405 function pulls the firstbyte and lastbyte values and returns
401 406 a (firstbyte,lastbyte) tuple. If lastbyte is not specified in
402 407 the header value, it is returned as an empty string in the
403 408 tuple.
404 409
405 410 Return None if range_header is None
406 411 Return () if range_header does not conform to the range spec
407 412 pattern.
408 413
409 414 """
410 415 global _rangere
411 416 if range_header is None:
412 417 return None
413 418 if _rangere is None:
414 419 _rangere = re.compile(r'^bytes=(\d{1,})-(\d*)')
415 420 match = _rangere.match(range_header)
416 421 if match:
417 422 tup = range_tuple_normalize(match.group(1, 2))
418 423 if tup and tup[1]:
419 424 tup = (tup[0], tup[1]+1)
420 425 return tup
421 426 return ()
422 427
423 428 def range_tuple_to_header(range_tup):
424 429 """Convert a range tuple to a Range header value.
425 430 Return a string of the form "bytes=<firstbyte>-<lastbyte>" or None
426 431 if no range is needed.
427 432 """
428 433 if range_tup is None:
429 434 return None
430 435 range_tup = range_tuple_normalize(range_tup)
431 436 if range_tup:
432 437 if range_tup[1]:
433 438 range_tup = (range_tup[0], range_tup[1] - 1)
434 439 return 'bytes=%s-%s' % range_tup
435 440
436 441 def range_tuple_normalize(range_tup):
437 442 """Normalize a (first_byte,last_byte) range tuple.
438 443 Return a tuple whose first element is guaranteed to be an int
439 444 and whose second element will be '' (meaning: the last byte) or
440 445 an int. Finally, return None if the normalized tuple == (0,'')
441 446 as that is equivalent to retrieving the entire file.
442 447 """
443 448 if range_tup is None:
444 449 return None
445 450 # handle first byte
446 451 fb = range_tup[0]
447 452 if fb in (None, ''):
448 453 fb = 0
449 454 else:
450 455 fb = int(fb)
451 456 # handle last byte
452 457 try:
453 458 lb = range_tup[1]
454 459 except IndexError:
455 460 lb = ''
456 461 else:
457 462 if lb is None:
458 463 lb = ''
459 464 elif lb != '':
460 465 lb = int(lb)
461 466 # check if range is over the entire file
462 467 if (fb, lb) == (0, ''):
463 468 return None
464 469 # check that the range is valid
465 470 if lb < fb:
466 471 raise RangeError('Invalid byte range: %s-%s' % (fb, lb))
467 472 return (fb, lb)
@@ -1,1929 +1,1930 b''
1 1 # exchange.py - utility to exchange data between repos.
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 import urllib
12 import urllib2
13 11
14 12 from .i18n import _
15 13 from .node import (
16 14 hex,
17 15 nullid,
18 16 )
19 17 from . import (
20 18 base85,
21 19 bookmarks as bookmod,
22 20 bundle2,
23 21 changegroup,
24 22 discovery,
25 23 error,
26 24 lock as lockmod,
27 25 obsolete,
28 26 phases,
29 27 pushkey,
30 28 scmutil,
31 29 sslutil,
32 30 streamclone,
33 31 tags,
34 32 url as urlmod,
35 33 util,
36 34 )
37 35
36 urlerr = util.urlerr
37 urlreq = util.urlreq
38
38 39 # Maps bundle compression human names to internal representation.
39 40 _bundlespeccompressions = {'none': None,
40 41 'bzip2': 'BZ',
41 42 'gzip': 'GZ',
42 43 }
43 44
44 45 # Maps bundle version human names to changegroup versions.
45 46 _bundlespeccgversions = {'v1': '01',
46 47 'v2': '02',
47 48 'packed1': 's1',
48 49 'bundle2': '02', #legacy
49 50 }
50 51
51 52 def parsebundlespec(repo, spec, strict=True, externalnames=False):
52 53 """Parse a bundle string specification into parts.
53 54
54 55 Bundle specifications denote a well-defined bundle/exchange format.
55 56 The content of a given specification should not change over time in
56 57 order to ensure that bundles produced by a newer version of Mercurial are
57 58 readable from an older version.
58 59
59 60 The string currently has the form:
60 61
61 62 <compression>-<type>[;<parameter0>[;<parameter1>]]
62 63
63 64 Where <compression> is one of the supported compression formats
64 65 and <type> is (currently) a version string. A ";" can follow the type and
65 66 all text afterwards is interpretted as URI encoded, ";" delimited key=value
66 67 pairs.
67 68
68 69 If ``strict`` is True (the default) <compression> is required. Otherwise,
69 70 it is optional.
70 71
71 72 If ``externalnames`` is False (the default), the human-centric names will
72 73 be converted to their internal representation.
73 74
74 75 Returns a 3-tuple of (compression, version, parameters). Compression will
75 76 be ``None`` if not in strict mode and a compression isn't defined.
76 77
77 78 An ``InvalidBundleSpecification`` is raised when the specification is
78 79 not syntactically well formed.
79 80
80 81 An ``UnsupportedBundleSpecification`` is raised when the compression or
81 82 bundle type/version is not recognized.
82 83
83 84 Note: this function will likely eventually return a more complex data
84 85 structure, including bundle2 part information.
85 86 """
86 87 def parseparams(s):
87 88 if ';' not in s:
88 89 return s, {}
89 90
90 91 params = {}
91 92 version, paramstr = s.split(';', 1)
92 93
93 94 for p in paramstr.split(';'):
94 95 if '=' not in p:
95 96 raise error.InvalidBundleSpecification(
96 97 _('invalid bundle specification: '
97 98 'missing "=" in parameter: %s') % p)
98 99
99 100 key, value = p.split('=', 1)
100 key = urllib.unquote(key)
101 value = urllib.unquote(value)
101 key = urlreq.unquote(key)
102 value = urlreq.unquote(value)
102 103 params[key] = value
103 104
104 105 return version, params
105 106
106 107
107 108 if strict and '-' not in spec:
108 109 raise error.InvalidBundleSpecification(
109 110 _('invalid bundle specification; '
110 111 'must be prefixed with compression: %s') % spec)
111 112
112 113 if '-' in spec:
113 114 compression, version = spec.split('-', 1)
114 115
115 116 if compression not in _bundlespeccompressions:
116 117 raise error.UnsupportedBundleSpecification(
117 118 _('%s compression is not supported') % compression)
118 119
119 120 version, params = parseparams(version)
120 121
121 122 if version not in _bundlespeccgversions:
122 123 raise error.UnsupportedBundleSpecification(
123 124 _('%s is not a recognized bundle version') % version)
124 125 else:
125 126 # Value could be just the compression or just the version, in which
126 127 # case some defaults are assumed (but only when not in strict mode).
127 128 assert not strict
128 129
129 130 spec, params = parseparams(spec)
130 131
131 132 if spec in _bundlespeccompressions:
132 133 compression = spec
133 134 version = 'v1'
134 135 if 'generaldelta' in repo.requirements:
135 136 version = 'v2'
136 137 elif spec in _bundlespeccgversions:
137 138 if spec == 'packed1':
138 139 compression = 'none'
139 140 else:
140 141 compression = 'bzip2'
141 142 version = spec
142 143 else:
143 144 raise error.UnsupportedBundleSpecification(
144 145 _('%s is not a recognized bundle specification') % spec)
145 146
146 147 # The specification for packed1 can optionally declare the data formats
147 148 # required to apply it. If we see this metadata, compare against what the
148 149 # repo supports and error if the bundle isn't compatible.
149 150 if version == 'packed1' and 'requirements' in params:
150 151 requirements = set(params['requirements'].split(','))
151 152 missingreqs = requirements - repo.supportedformats
152 153 if missingreqs:
153 154 raise error.UnsupportedBundleSpecification(
154 155 _('missing support for repository features: %s') %
155 156 ', '.join(sorted(missingreqs)))
156 157
157 158 if not externalnames:
158 159 compression = _bundlespeccompressions[compression]
159 160 version = _bundlespeccgversions[version]
160 161 return compression, version, params
161 162
162 163 def readbundle(ui, fh, fname, vfs=None):
163 164 header = changegroup.readexactly(fh, 4)
164 165
165 166 alg = None
166 167 if not fname:
167 168 fname = "stream"
168 169 if not header.startswith('HG') and header.startswith('\0'):
169 170 fh = changegroup.headerlessfixup(fh, header)
170 171 header = "HG10"
171 172 alg = 'UN'
172 173 elif vfs:
173 174 fname = vfs.join(fname)
174 175
175 176 magic, version = header[0:2], header[2:4]
176 177
177 178 if magic != 'HG':
178 179 raise error.Abort(_('%s: not a Mercurial bundle') % fname)
179 180 if version == '10':
180 181 if alg is None:
181 182 alg = changegroup.readexactly(fh, 2)
182 183 return changegroup.cg1unpacker(fh, alg)
183 184 elif version.startswith('2'):
184 185 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
185 186 elif version == 'S1':
186 187 return streamclone.streamcloneapplier(fh)
187 188 else:
188 189 raise error.Abort(_('%s: unknown bundle version %s') % (fname, version))
189 190
190 191 def getbundlespec(ui, fh):
191 192 """Infer the bundlespec from a bundle file handle.
192 193
193 194 The input file handle is seeked and the original seek position is not
194 195 restored.
195 196 """
196 197 def speccompression(alg):
197 198 for k, v in _bundlespeccompressions.items():
198 199 if v == alg:
199 200 return k
200 201 return None
201 202
202 203 b = readbundle(ui, fh, None)
203 204 if isinstance(b, changegroup.cg1unpacker):
204 205 alg = b._type
205 206 if alg == '_truncatedBZ':
206 207 alg = 'BZ'
207 208 comp = speccompression(alg)
208 209 if not comp:
209 210 raise error.Abort(_('unknown compression algorithm: %s') % alg)
210 211 return '%s-v1' % comp
211 212 elif isinstance(b, bundle2.unbundle20):
212 213 if 'Compression' in b.params:
213 214 comp = speccompression(b.params['Compression'])
214 215 if not comp:
215 216 raise error.Abort(_('unknown compression algorithm: %s') % comp)
216 217 else:
217 218 comp = 'none'
218 219
219 220 version = None
220 221 for part in b.iterparts():
221 222 if part.type == 'changegroup':
222 223 version = part.params['version']
223 224 if version in ('01', '02'):
224 225 version = 'v2'
225 226 else:
226 227 raise error.Abort(_('changegroup version %s does not have '
227 228 'a known bundlespec') % version,
228 229 hint=_('try upgrading your Mercurial '
229 230 'client'))
230 231
231 232 if not version:
232 233 raise error.Abort(_('could not identify changegroup version in '
233 234 'bundle'))
234 235
235 236 return '%s-%s' % (comp, version)
236 237 elif isinstance(b, streamclone.streamcloneapplier):
237 238 requirements = streamclone.readbundle1header(fh)[2]
238 239 params = 'requirements=%s' % ','.join(sorted(requirements))
239 return 'none-packed1;%s' % urllib.quote(params)
240 return 'none-packed1;%s' % urlreq.quote(params)
240 241 else:
241 242 raise error.Abort(_('unknown bundle type: %s') % b)
242 243
243 244 def buildobsmarkerspart(bundler, markers):
244 245 """add an obsmarker part to the bundler with <markers>
245 246
246 247 No part is created if markers is empty.
247 248 Raises ValueError if the bundler doesn't support any known obsmarker format.
248 249 """
249 250 if markers:
250 251 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
251 252 version = obsolete.commonversion(remoteversions)
252 253 if version is None:
253 254 raise ValueError('bundler does not support common obsmarker format')
254 255 stream = obsolete.encodemarkers(markers, True, version=version)
255 256 return bundler.newpart('obsmarkers', data=stream)
256 257 return None
257 258
258 259 def _canusebundle2(op):
259 260 """return true if a pull/push can use bundle2
260 261
261 262 Feel free to nuke this function when we drop the experimental option"""
262 263 return (op.repo.ui.configbool('experimental', 'bundle2-exp', True)
263 264 and op.remote.capable('bundle2'))
264 265
265 266
266 267 class pushoperation(object):
267 268 """A object that represent a single push operation
268 269
269 270 Its purpose is to carry push related state and very common operations.
270 271
271 272 A new pushoperation should be created at the beginning of each push and
272 273 discarded afterward.
273 274 """
274 275
275 276 def __init__(self, repo, remote, force=False, revs=None, newbranch=False,
276 277 bookmarks=()):
277 278 # repo we push from
278 279 self.repo = repo
279 280 self.ui = repo.ui
280 281 # repo we push to
281 282 self.remote = remote
282 283 # force option provided
283 284 self.force = force
284 285 # revs to be pushed (None is "all")
285 286 self.revs = revs
286 287 # bookmark explicitly pushed
287 288 self.bookmarks = bookmarks
288 289 # allow push of new branch
289 290 self.newbranch = newbranch
290 291 # did a local lock get acquired?
291 292 self.locallocked = None
292 293 # step already performed
293 294 # (used to check what steps have been already performed through bundle2)
294 295 self.stepsdone = set()
295 296 # Integer version of the changegroup push result
296 297 # - None means nothing to push
297 298 # - 0 means HTTP error
298 299 # - 1 means we pushed and remote head count is unchanged *or*
299 300 # we have outgoing changesets but refused to push
300 301 # - other values as described by addchangegroup()
301 302 self.cgresult = None
302 303 # Boolean value for the bookmark push
303 304 self.bkresult = None
304 305 # discover.outgoing object (contains common and outgoing data)
305 306 self.outgoing = None
306 307 # all remote heads before the push
307 308 self.remoteheads = None
308 309 # testable as a boolean indicating if any nodes are missing locally.
309 310 self.incoming = None
310 311 # phases changes that must be pushed along side the changesets
311 312 self.outdatedphases = None
312 313 # phases changes that must be pushed if changeset push fails
313 314 self.fallbackoutdatedphases = None
314 315 # outgoing obsmarkers
315 316 self.outobsmarkers = set()
316 317 # outgoing bookmarks
317 318 self.outbookmarks = []
318 319 # transaction manager
319 320 self.trmanager = None
320 321 # map { pushkey partid -> callback handling failure}
321 322 # used to handle exception from mandatory pushkey part failure
322 323 self.pkfailcb = {}
323 324
324 325 @util.propertycache
325 326 def futureheads(self):
326 327 """future remote heads if the changeset push succeeds"""
327 328 return self.outgoing.missingheads
328 329
329 330 @util.propertycache
330 331 def fallbackheads(self):
331 332 """future remote heads if the changeset push fails"""
332 333 if self.revs is None:
333 334 # not target to push, all common are relevant
334 335 return self.outgoing.commonheads
335 336 unfi = self.repo.unfiltered()
336 337 # I want cheads = heads(::missingheads and ::commonheads)
337 338 # (missingheads is revs with secret changeset filtered out)
338 339 #
339 340 # This can be expressed as:
340 341 # cheads = ( (missingheads and ::commonheads)
341 342 # + (commonheads and ::missingheads))"
342 343 # )
343 344 #
344 345 # while trying to push we already computed the following:
345 346 # common = (::commonheads)
346 347 # missing = ((commonheads::missingheads) - commonheads)
347 348 #
348 349 # We can pick:
349 350 # * missingheads part of common (::commonheads)
350 351 common = self.outgoing.common
351 352 nm = self.repo.changelog.nodemap
352 353 cheads = [node for node in self.revs if nm[node] in common]
353 354 # and
354 355 # * commonheads parents on missing
355 356 revset = unfi.set('%ln and parents(roots(%ln))',
356 357 self.outgoing.commonheads,
357 358 self.outgoing.missing)
358 359 cheads.extend(c.node() for c in revset)
359 360 return cheads
360 361
361 362 @property
362 363 def commonheads(self):
363 364 """set of all common heads after changeset bundle push"""
364 365 if self.cgresult:
365 366 return self.futureheads
366 367 else:
367 368 return self.fallbackheads
368 369
369 370 # mapping of message used when pushing bookmark
370 371 bookmsgmap = {'update': (_("updating bookmark %s\n"),
371 372 _('updating bookmark %s failed!\n')),
372 373 'export': (_("exporting bookmark %s\n"),
373 374 _('exporting bookmark %s failed!\n')),
374 375 'delete': (_("deleting remote bookmark %s\n"),
375 376 _('deleting remote bookmark %s failed!\n')),
376 377 }
377 378
378 379
379 380 def push(repo, remote, force=False, revs=None, newbranch=False, bookmarks=(),
380 381 opargs=None):
381 382 '''Push outgoing changesets (limited by revs) from a local
382 383 repository to remote. Return an integer:
383 384 - None means nothing to push
384 385 - 0 means HTTP error
385 386 - 1 means we pushed and remote head count is unchanged *or*
386 387 we have outgoing changesets but refused to push
387 388 - other values as described by addchangegroup()
388 389 '''
389 390 if opargs is None:
390 391 opargs = {}
391 392 pushop = pushoperation(repo, remote, force, revs, newbranch, bookmarks,
392 393 **opargs)
393 394 if pushop.remote.local():
394 395 missing = (set(pushop.repo.requirements)
395 396 - pushop.remote.local().supported)
396 397 if missing:
397 398 msg = _("required features are not"
398 399 " supported in the destination:"
399 400 " %s") % (', '.join(sorted(missing)))
400 401 raise error.Abort(msg)
401 402
402 403 # there are two ways to push to remote repo:
403 404 #
404 405 # addchangegroup assumes local user can lock remote
405 406 # repo (local filesystem, old ssh servers).
406 407 #
407 408 # unbundle assumes local user cannot lock remote repo (new ssh
408 409 # servers, http servers).
409 410
410 411 if not pushop.remote.canpush():
411 412 raise error.Abort(_("destination does not support push"))
412 413 # get local lock as we might write phase data
413 414 localwlock = locallock = None
414 415 try:
415 416 # bundle2 push may receive a reply bundle touching bookmarks or other
416 417 # things requiring the wlock. Take it now to ensure proper ordering.
417 418 maypushback = pushop.ui.configbool('experimental', 'bundle2.pushback')
418 419 if _canusebundle2(pushop) and maypushback:
419 420 localwlock = pushop.repo.wlock()
420 421 locallock = pushop.repo.lock()
421 422 pushop.locallocked = True
422 423 except IOError as err:
423 424 pushop.locallocked = False
424 425 if err.errno != errno.EACCES:
425 426 raise
426 427 # source repo cannot be locked.
427 428 # We do not abort the push, but just disable the local phase
428 429 # synchronisation.
429 430 msg = 'cannot lock source repository: %s\n' % err
430 431 pushop.ui.debug(msg)
431 432 try:
432 433 if pushop.locallocked:
433 434 pushop.trmanager = transactionmanager(pushop.repo,
434 435 'push-response',
435 436 pushop.remote.url())
436 437 pushop.repo.checkpush(pushop)
437 438 lock = None
438 439 unbundle = pushop.remote.capable('unbundle')
439 440 if not unbundle:
440 441 lock = pushop.remote.lock()
441 442 try:
442 443 _pushdiscovery(pushop)
443 444 if _canusebundle2(pushop):
444 445 _pushbundle2(pushop)
445 446 _pushchangeset(pushop)
446 447 _pushsyncphase(pushop)
447 448 _pushobsolete(pushop)
448 449 _pushbookmark(pushop)
449 450 finally:
450 451 if lock is not None:
451 452 lock.release()
452 453 if pushop.trmanager:
453 454 pushop.trmanager.close()
454 455 finally:
455 456 if pushop.trmanager:
456 457 pushop.trmanager.release()
457 458 if locallock is not None:
458 459 locallock.release()
459 460 if localwlock is not None:
460 461 localwlock.release()
461 462
462 463 return pushop
463 464
464 465 # list of steps to perform discovery before push
465 466 pushdiscoveryorder = []
466 467
467 468 # Mapping between step name and function
468 469 #
469 470 # This exists to help extensions wrap steps if necessary
470 471 pushdiscoverymapping = {}
471 472
472 473 def pushdiscovery(stepname):
473 474 """decorator for function performing discovery before push
474 475
475 476 The function is added to the step -> function mapping and appended to the
476 477 list of steps. Beware that decorated function will be added in order (this
477 478 may matter).
478 479
479 480 You can only use this decorator for a new step, if you want to wrap a step
480 481 from an extension, change the pushdiscovery dictionary directly."""
481 482 def dec(func):
482 483 assert stepname not in pushdiscoverymapping
483 484 pushdiscoverymapping[stepname] = func
484 485 pushdiscoveryorder.append(stepname)
485 486 return func
486 487 return dec
487 488
488 489 def _pushdiscovery(pushop):
489 490 """Run all discovery steps"""
490 491 for stepname in pushdiscoveryorder:
491 492 step = pushdiscoverymapping[stepname]
492 493 step(pushop)
493 494
494 495 @pushdiscovery('changeset')
495 496 def _pushdiscoverychangeset(pushop):
496 497 """discover the changeset that need to be pushed"""
497 498 fci = discovery.findcommonincoming
498 499 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
499 500 common, inc, remoteheads = commoninc
500 501 fco = discovery.findcommonoutgoing
501 502 outgoing = fco(pushop.repo, pushop.remote, onlyheads=pushop.revs,
502 503 commoninc=commoninc, force=pushop.force)
503 504 pushop.outgoing = outgoing
504 505 pushop.remoteheads = remoteheads
505 506 pushop.incoming = inc
506 507
507 508 @pushdiscovery('phase')
508 509 def _pushdiscoveryphase(pushop):
509 510 """discover the phase that needs to be pushed
510 511
511 512 (computed for both success and failure case for changesets push)"""
512 513 outgoing = pushop.outgoing
513 514 unfi = pushop.repo.unfiltered()
514 515 remotephases = pushop.remote.listkeys('phases')
515 516 publishing = remotephases.get('publishing', False)
516 517 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
517 518 and remotephases # server supports phases
518 519 and not pushop.outgoing.missing # no changesets to be pushed
519 520 and publishing):
520 521 # When:
521 522 # - this is a subrepo push
522 523 # - and remote support phase
523 524 # - and no changeset are to be pushed
524 525 # - and remote is publishing
525 526 # We may be in issue 3871 case!
526 527 # We drop the possible phase synchronisation done by
527 528 # courtesy to publish changesets possibly locally draft
528 529 # on the remote.
529 530 remotephases = {'publishing': 'True'}
530 531 ana = phases.analyzeremotephases(pushop.repo,
531 532 pushop.fallbackheads,
532 533 remotephases)
533 534 pheads, droots = ana
534 535 extracond = ''
535 536 if not publishing:
536 537 extracond = ' and public()'
537 538 revset = 'heads((%%ln::%%ln) %s)' % extracond
538 539 # Get the list of all revs draft on remote by public here.
539 540 # XXX Beware that revset break if droots is not strictly
540 541 # XXX root we may want to ensure it is but it is costly
541 542 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
542 543 if not outgoing.missing:
543 544 future = fallback
544 545 else:
545 546 # adds changeset we are going to push as draft
546 547 #
547 548 # should not be necessary for publishing server, but because of an
548 549 # issue fixed in xxxxx we have to do it anyway.
549 550 fdroots = list(unfi.set('roots(%ln + %ln::)',
550 551 outgoing.missing, droots))
551 552 fdroots = [f.node() for f in fdroots]
552 553 future = list(unfi.set(revset, fdroots, pushop.futureheads))
553 554 pushop.outdatedphases = future
554 555 pushop.fallbackoutdatedphases = fallback
555 556
556 557 @pushdiscovery('obsmarker')
557 558 def _pushdiscoveryobsmarkers(pushop):
558 559 if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt)
559 560 and pushop.repo.obsstore
560 561 and 'obsolete' in pushop.remote.listkeys('namespaces')):
561 562 repo = pushop.repo
562 563 # very naive computation, that can be quite expensive on big repo.
563 564 # However: evolution is currently slow on them anyway.
564 565 nodes = (c.node() for c in repo.set('::%ln', pushop.futureheads))
565 566 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
566 567
567 568 @pushdiscovery('bookmarks')
568 569 def _pushdiscoverybookmarks(pushop):
569 570 ui = pushop.ui
570 571 repo = pushop.repo.unfiltered()
571 572 remote = pushop.remote
572 573 ui.debug("checking for updated bookmarks\n")
573 574 ancestors = ()
574 575 if pushop.revs:
575 576 revnums = map(repo.changelog.rev, pushop.revs)
576 577 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
577 578 remotebookmark = remote.listkeys('bookmarks')
578 579
579 580 explicit = set([repo._bookmarks.expandname(bookmark)
580 581 for bookmark in pushop.bookmarks])
581 582
582 583 comp = bookmod.compare(repo, repo._bookmarks, remotebookmark, srchex=hex)
583 584 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
584 585 for b, scid, dcid in advsrc:
585 586 if b in explicit:
586 587 explicit.remove(b)
587 588 if not ancestors or repo[scid].rev() in ancestors:
588 589 pushop.outbookmarks.append((b, dcid, scid))
589 590 # search added bookmark
590 591 for b, scid, dcid in addsrc:
591 592 if b in explicit:
592 593 explicit.remove(b)
593 594 pushop.outbookmarks.append((b, '', scid))
594 595 # search for overwritten bookmark
595 596 for b, scid, dcid in advdst + diverge + differ:
596 597 if b in explicit:
597 598 explicit.remove(b)
598 599 pushop.outbookmarks.append((b, dcid, scid))
599 600 # search for bookmark to delete
600 601 for b, scid, dcid in adddst:
601 602 if b in explicit:
602 603 explicit.remove(b)
603 604 # treat as "deleted locally"
604 605 pushop.outbookmarks.append((b, dcid, ''))
605 606 # identical bookmarks shouldn't get reported
606 607 for b, scid, dcid in same:
607 608 if b in explicit:
608 609 explicit.remove(b)
609 610
610 611 if explicit:
611 612 explicit = sorted(explicit)
612 613 # we should probably list all of them
613 614 ui.warn(_('bookmark %s does not exist on the local '
614 615 'or remote repository!\n') % explicit[0])
615 616 pushop.bkresult = 2
616 617
617 618 pushop.outbookmarks.sort()
618 619
619 620 def _pushcheckoutgoing(pushop):
620 621 outgoing = pushop.outgoing
621 622 unfi = pushop.repo.unfiltered()
622 623 if not outgoing.missing:
623 624 # nothing to push
624 625 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
625 626 return False
626 627 # something to push
627 628 if not pushop.force:
628 629 # if repo.obsstore == False --> no obsolete
629 630 # then, save the iteration
630 631 if unfi.obsstore:
631 632 # this message are here for 80 char limit reason
632 633 mso = _("push includes obsolete changeset: %s!")
633 634 mst = {"unstable": _("push includes unstable changeset: %s!"),
634 635 "bumped": _("push includes bumped changeset: %s!"),
635 636 "divergent": _("push includes divergent changeset: %s!")}
636 637 # If we are to push if there is at least one
637 638 # obsolete or unstable changeset in missing, at
638 639 # least one of the missinghead will be obsolete or
639 640 # unstable. So checking heads only is ok
640 641 for node in outgoing.missingheads:
641 642 ctx = unfi[node]
642 643 if ctx.obsolete():
643 644 raise error.Abort(mso % ctx)
644 645 elif ctx.troubled():
645 646 raise error.Abort(mst[ctx.troubles()[0]] % ctx)
646 647
647 648 discovery.checkheads(pushop)
648 649 return True
649 650
650 651 # List of names of steps to perform for an outgoing bundle2, order matters.
651 652 b2partsgenorder = []
652 653
653 654 # Mapping between step name and function
654 655 #
655 656 # This exists to help extensions wrap steps if necessary
656 657 b2partsgenmapping = {}
657 658
658 659 def b2partsgenerator(stepname, idx=None):
659 660 """decorator for function generating bundle2 part
660 661
661 662 The function is added to the step -> function mapping and appended to the
662 663 list of steps. Beware that decorated functions will be added in order
663 664 (this may matter).
664 665
665 666 You can only use this decorator for new steps, if you want to wrap a step
666 667 from an extension, attack the b2partsgenmapping dictionary directly."""
667 668 def dec(func):
668 669 assert stepname not in b2partsgenmapping
669 670 b2partsgenmapping[stepname] = func
670 671 if idx is None:
671 672 b2partsgenorder.append(stepname)
672 673 else:
673 674 b2partsgenorder.insert(idx, stepname)
674 675 return func
675 676 return dec
676 677
677 678 def _pushb2ctxcheckheads(pushop, bundler):
678 679 """Generate race condition checking parts
679 680
680 681 Exists as an independent function to aid extensions
681 682 """
682 683 if not pushop.force:
683 684 bundler.newpart('check:heads', data=iter(pushop.remoteheads))
684 685
685 686 @b2partsgenerator('changeset')
686 687 def _pushb2ctx(pushop, bundler):
687 688 """handle changegroup push through bundle2
688 689
689 690 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
690 691 """
691 692 if 'changesets' in pushop.stepsdone:
692 693 return
693 694 pushop.stepsdone.add('changesets')
694 695 # Send known heads to the server for race detection.
695 696 if not _pushcheckoutgoing(pushop):
696 697 return
697 698 pushop.repo.prepushoutgoinghooks(pushop)
698 699
699 700 _pushb2ctxcheckheads(pushop, bundler)
700 701
701 702 b2caps = bundle2.bundle2caps(pushop.remote)
702 703 version = '01'
703 704 cgversions = b2caps.get('changegroup')
704 705 if cgversions: # 3.1 and 3.2 ship with an empty value
705 706 cgversions = [v for v in cgversions
706 707 if v in changegroup.supportedoutgoingversions(
707 708 pushop.repo)]
708 709 if not cgversions:
709 710 raise ValueError(_('no common changegroup version'))
710 711 version = max(cgversions)
711 712 cg = changegroup.getlocalchangegroupraw(pushop.repo, 'push',
712 713 pushop.outgoing,
713 714 version=version)
714 715 cgpart = bundler.newpart('changegroup', data=cg)
715 716 if cgversions:
716 717 cgpart.addparam('version', version)
717 718 if 'treemanifest' in pushop.repo.requirements:
718 719 cgpart.addparam('treemanifest', '1')
719 720 def handlereply(op):
720 721 """extract addchangegroup returns from server reply"""
721 722 cgreplies = op.records.getreplies(cgpart.id)
722 723 assert len(cgreplies['changegroup']) == 1
723 724 pushop.cgresult = cgreplies['changegroup'][0]['return']
724 725 return handlereply
725 726
726 727 @b2partsgenerator('phase')
727 728 def _pushb2phases(pushop, bundler):
728 729 """handle phase push through bundle2"""
729 730 if 'phases' in pushop.stepsdone:
730 731 return
731 732 b2caps = bundle2.bundle2caps(pushop.remote)
732 733 if not 'pushkey' in b2caps:
733 734 return
734 735 pushop.stepsdone.add('phases')
735 736 part2node = []
736 737
737 738 def handlefailure(pushop, exc):
738 739 targetid = int(exc.partid)
739 740 for partid, node in part2node:
740 741 if partid == targetid:
741 742 raise error.Abort(_('updating %s to public failed') % node)
742 743
743 744 enc = pushkey.encode
744 745 for newremotehead in pushop.outdatedphases:
745 746 part = bundler.newpart('pushkey')
746 747 part.addparam('namespace', enc('phases'))
747 748 part.addparam('key', enc(newremotehead.hex()))
748 749 part.addparam('old', enc(str(phases.draft)))
749 750 part.addparam('new', enc(str(phases.public)))
750 751 part2node.append((part.id, newremotehead))
751 752 pushop.pkfailcb[part.id] = handlefailure
752 753
753 754 def handlereply(op):
754 755 for partid, node in part2node:
755 756 partrep = op.records.getreplies(partid)
756 757 results = partrep['pushkey']
757 758 assert len(results) <= 1
758 759 msg = None
759 760 if not results:
760 761 msg = _('server ignored update of %s to public!\n') % node
761 762 elif not int(results[0]['return']):
762 763 msg = _('updating %s to public failed!\n') % node
763 764 if msg is not None:
764 765 pushop.ui.warn(msg)
765 766 return handlereply
766 767
767 768 @b2partsgenerator('obsmarkers')
768 769 def _pushb2obsmarkers(pushop, bundler):
769 770 if 'obsmarkers' in pushop.stepsdone:
770 771 return
771 772 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
772 773 if obsolete.commonversion(remoteversions) is None:
773 774 return
774 775 pushop.stepsdone.add('obsmarkers')
775 776 if pushop.outobsmarkers:
776 777 markers = sorted(pushop.outobsmarkers)
777 778 buildobsmarkerspart(bundler, markers)
778 779
779 780 @b2partsgenerator('bookmarks')
780 781 def _pushb2bookmarks(pushop, bundler):
781 782 """handle bookmark push through bundle2"""
782 783 if 'bookmarks' in pushop.stepsdone:
783 784 return
784 785 b2caps = bundle2.bundle2caps(pushop.remote)
785 786 if 'pushkey' not in b2caps:
786 787 return
787 788 pushop.stepsdone.add('bookmarks')
788 789 part2book = []
789 790 enc = pushkey.encode
790 791
791 792 def handlefailure(pushop, exc):
792 793 targetid = int(exc.partid)
793 794 for partid, book, action in part2book:
794 795 if partid == targetid:
795 796 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
796 797 # we should not be called for part we did not generated
797 798 assert False
798 799
799 800 for book, old, new in pushop.outbookmarks:
800 801 part = bundler.newpart('pushkey')
801 802 part.addparam('namespace', enc('bookmarks'))
802 803 part.addparam('key', enc(book))
803 804 part.addparam('old', enc(old))
804 805 part.addparam('new', enc(new))
805 806 action = 'update'
806 807 if not old:
807 808 action = 'export'
808 809 elif not new:
809 810 action = 'delete'
810 811 part2book.append((part.id, book, action))
811 812 pushop.pkfailcb[part.id] = handlefailure
812 813
813 814 def handlereply(op):
814 815 ui = pushop.ui
815 816 for partid, book, action in part2book:
816 817 partrep = op.records.getreplies(partid)
817 818 results = partrep['pushkey']
818 819 assert len(results) <= 1
819 820 if not results:
820 821 pushop.ui.warn(_('server ignored bookmark %s update\n') % book)
821 822 else:
822 823 ret = int(results[0]['return'])
823 824 if ret:
824 825 ui.status(bookmsgmap[action][0] % book)
825 826 else:
826 827 ui.warn(bookmsgmap[action][1] % book)
827 828 if pushop.bkresult is not None:
828 829 pushop.bkresult = 1
829 830 return handlereply
830 831
831 832
832 833 def _pushbundle2(pushop):
833 834 """push data to the remote using bundle2
834 835
835 836 The only currently supported type of data is changegroup but this will
836 837 evolve in the future."""
837 838 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
838 839 pushback = (pushop.trmanager
839 840 and pushop.ui.configbool('experimental', 'bundle2.pushback'))
840 841
841 842 # create reply capability
842 843 capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo,
843 844 allowpushback=pushback))
844 845 bundler.newpart('replycaps', data=capsblob)
845 846 replyhandlers = []
846 847 for partgenname in b2partsgenorder:
847 848 partgen = b2partsgenmapping[partgenname]
848 849 ret = partgen(pushop, bundler)
849 850 if callable(ret):
850 851 replyhandlers.append(ret)
851 852 # do not push if nothing to push
852 853 if bundler.nbparts <= 1:
853 854 return
854 855 stream = util.chunkbuffer(bundler.getchunks())
855 856 try:
856 857 try:
857 858 reply = pushop.remote.unbundle(stream, ['force'], 'push')
858 859 except error.BundleValueError as exc:
859 860 raise error.Abort('missing support for %s' % exc)
860 861 try:
861 862 trgetter = None
862 863 if pushback:
863 864 trgetter = pushop.trmanager.transaction
864 865 op = bundle2.processbundle(pushop.repo, reply, trgetter)
865 866 except error.BundleValueError as exc:
866 867 raise error.Abort('missing support for %s' % exc)
867 868 except bundle2.AbortFromPart as exc:
868 869 pushop.ui.status(_('remote: %s\n') % exc)
869 870 raise error.Abort(_('push failed on remote'), hint=exc.hint)
870 871 except error.PushkeyFailed as exc:
871 872 partid = int(exc.partid)
872 873 if partid not in pushop.pkfailcb:
873 874 raise
874 875 pushop.pkfailcb[partid](pushop, exc)
875 876 for rephand in replyhandlers:
876 877 rephand(op)
877 878
878 879 def _pushchangeset(pushop):
879 880 """Make the actual push of changeset bundle to remote repo"""
880 881 if 'changesets' in pushop.stepsdone:
881 882 return
882 883 pushop.stepsdone.add('changesets')
883 884 if not _pushcheckoutgoing(pushop):
884 885 return
885 886 pushop.repo.prepushoutgoinghooks(pushop)
886 887 outgoing = pushop.outgoing
887 888 unbundle = pushop.remote.capable('unbundle')
888 889 # TODO: get bundlecaps from remote
889 890 bundlecaps = None
890 891 # create a changegroup from local
891 892 if pushop.revs is None and not (outgoing.excluded
892 893 or pushop.repo.changelog.filteredrevs):
893 894 # push everything,
894 895 # use the fast path, no race possible on push
895 896 bundler = changegroup.cg1packer(pushop.repo, bundlecaps)
896 897 cg = changegroup.getsubset(pushop.repo,
897 898 outgoing,
898 899 bundler,
899 900 'push',
900 901 fastpath=True)
901 902 else:
902 903 cg = changegroup.getlocalchangegroup(pushop.repo, 'push', outgoing,
903 904 bundlecaps)
904 905
905 906 # apply changegroup to remote
906 907 if unbundle:
907 908 # local repo finds heads on server, finds out what
908 909 # revs it must push. once revs transferred, if server
909 910 # finds it has different heads (someone else won
910 911 # commit/push race), server aborts.
911 912 if pushop.force:
912 913 remoteheads = ['force']
913 914 else:
914 915 remoteheads = pushop.remoteheads
915 916 # ssh: return remote's addchangegroup()
916 917 # http: return remote's addchangegroup() or 0 for error
917 918 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads,
918 919 pushop.repo.url())
919 920 else:
920 921 # we return an integer indicating remote head count
921 922 # change
922 923 pushop.cgresult = pushop.remote.addchangegroup(cg, 'push',
923 924 pushop.repo.url())
924 925
925 926 def _pushsyncphase(pushop):
926 927 """synchronise phase information locally and remotely"""
927 928 cheads = pushop.commonheads
928 929 # even when we don't push, exchanging phase data is useful
929 930 remotephases = pushop.remote.listkeys('phases')
930 931 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
931 932 and remotephases # server supports phases
932 933 and pushop.cgresult is None # nothing was pushed
933 934 and remotephases.get('publishing', False)):
934 935 # When:
935 936 # - this is a subrepo push
936 937 # - and remote support phase
937 938 # - and no changeset was pushed
938 939 # - and remote is publishing
939 940 # We may be in issue 3871 case!
940 941 # We drop the possible phase synchronisation done by
941 942 # courtesy to publish changesets possibly locally draft
942 943 # on the remote.
943 944 remotephases = {'publishing': 'True'}
944 945 if not remotephases: # old server or public only reply from non-publishing
945 946 _localphasemove(pushop, cheads)
946 947 # don't push any phase data as there is nothing to push
947 948 else:
948 949 ana = phases.analyzeremotephases(pushop.repo, cheads,
949 950 remotephases)
950 951 pheads, droots = ana
951 952 ### Apply remote phase on local
952 953 if remotephases.get('publishing', False):
953 954 _localphasemove(pushop, cheads)
954 955 else: # publish = False
955 956 _localphasemove(pushop, pheads)
956 957 _localphasemove(pushop, cheads, phases.draft)
957 958 ### Apply local phase on remote
958 959
959 960 if pushop.cgresult:
960 961 if 'phases' in pushop.stepsdone:
961 962 # phases already pushed though bundle2
962 963 return
963 964 outdated = pushop.outdatedphases
964 965 else:
965 966 outdated = pushop.fallbackoutdatedphases
966 967
967 968 pushop.stepsdone.add('phases')
968 969
969 970 # filter heads already turned public by the push
970 971 outdated = [c for c in outdated if c.node() not in pheads]
971 972 # fallback to independent pushkey command
972 973 for newremotehead in outdated:
973 974 r = pushop.remote.pushkey('phases',
974 975 newremotehead.hex(),
975 976 str(phases.draft),
976 977 str(phases.public))
977 978 if not r:
978 979 pushop.ui.warn(_('updating %s to public failed!\n')
979 980 % newremotehead)
980 981
981 982 def _localphasemove(pushop, nodes, phase=phases.public):
982 983 """move <nodes> to <phase> in the local source repo"""
983 984 if pushop.trmanager:
984 985 phases.advanceboundary(pushop.repo,
985 986 pushop.trmanager.transaction(),
986 987 phase,
987 988 nodes)
988 989 else:
989 990 # repo is not locked, do not change any phases!
990 991 # Informs the user that phases should have been moved when
991 992 # applicable.
992 993 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
993 994 phasestr = phases.phasenames[phase]
994 995 if actualmoves:
995 996 pushop.ui.status(_('cannot lock source repo, skipping '
996 997 'local %s phase update\n') % phasestr)
997 998
998 999 def _pushobsolete(pushop):
999 1000 """utility function to push obsolete markers to a remote"""
1000 1001 if 'obsmarkers' in pushop.stepsdone:
1001 1002 return
1002 1003 repo = pushop.repo
1003 1004 remote = pushop.remote
1004 1005 pushop.stepsdone.add('obsmarkers')
1005 1006 if pushop.outobsmarkers:
1006 1007 pushop.ui.debug('try to push obsolete markers to remote\n')
1007 1008 rslts = []
1008 1009 remotedata = obsolete._pushkeyescape(sorted(pushop.outobsmarkers))
1009 1010 for key in sorted(remotedata, reverse=True):
1010 1011 # reverse sort to ensure we end with dump0
1011 1012 data = remotedata[key]
1012 1013 rslts.append(remote.pushkey('obsolete', key, '', data))
1013 1014 if [r for r in rslts if not r]:
1014 1015 msg = _('failed to push some obsolete markers!\n')
1015 1016 repo.ui.warn(msg)
1016 1017
1017 1018 def _pushbookmark(pushop):
1018 1019 """Update bookmark position on remote"""
1019 1020 if pushop.cgresult == 0 or 'bookmarks' in pushop.stepsdone:
1020 1021 return
1021 1022 pushop.stepsdone.add('bookmarks')
1022 1023 ui = pushop.ui
1023 1024 remote = pushop.remote
1024 1025
1025 1026 for b, old, new in pushop.outbookmarks:
1026 1027 action = 'update'
1027 1028 if not old:
1028 1029 action = 'export'
1029 1030 elif not new:
1030 1031 action = 'delete'
1031 1032 if remote.pushkey('bookmarks', b, old, new):
1032 1033 ui.status(bookmsgmap[action][0] % b)
1033 1034 else:
1034 1035 ui.warn(bookmsgmap[action][1] % b)
1035 1036 # discovery can have set the value form invalid entry
1036 1037 if pushop.bkresult is not None:
1037 1038 pushop.bkresult = 1
1038 1039
1039 1040 class pulloperation(object):
1040 1041 """A object that represent a single pull operation
1041 1042
1042 1043 It purpose is to carry pull related state and very common operation.
1043 1044
1044 1045 A new should be created at the beginning of each pull and discarded
1045 1046 afterward.
1046 1047 """
1047 1048
1048 1049 def __init__(self, repo, remote, heads=None, force=False, bookmarks=(),
1049 1050 remotebookmarks=None, streamclonerequested=None):
1050 1051 # repo we pull into
1051 1052 self.repo = repo
1052 1053 # repo we pull from
1053 1054 self.remote = remote
1054 1055 # revision we try to pull (None is "all")
1055 1056 self.heads = heads
1056 1057 # bookmark pulled explicitly
1057 1058 self.explicitbookmarks = bookmarks
1058 1059 # do we force pull?
1059 1060 self.force = force
1060 1061 # whether a streaming clone was requested
1061 1062 self.streamclonerequested = streamclonerequested
1062 1063 # transaction manager
1063 1064 self.trmanager = None
1064 1065 # set of common changeset between local and remote before pull
1065 1066 self.common = None
1066 1067 # set of pulled head
1067 1068 self.rheads = None
1068 1069 # list of missing changeset to fetch remotely
1069 1070 self.fetch = None
1070 1071 # remote bookmarks data
1071 1072 self.remotebookmarks = remotebookmarks
1072 1073 # result of changegroup pulling (used as return code by pull)
1073 1074 self.cgresult = None
1074 1075 # list of step already done
1075 1076 self.stepsdone = set()
1076 1077 # Whether we attempted a clone from pre-generated bundles.
1077 1078 self.clonebundleattempted = False
1078 1079
1079 1080 @util.propertycache
1080 1081 def pulledsubset(self):
1081 1082 """heads of the set of changeset target by the pull"""
1082 1083 # compute target subset
1083 1084 if self.heads is None:
1084 1085 # We pulled every thing possible
1085 1086 # sync on everything common
1086 1087 c = set(self.common)
1087 1088 ret = list(self.common)
1088 1089 for n in self.rheads:
1089 1090 if n not in c:
1090 1091 ret.append(n)
1091 1092 return ret
1092 1093 else:
1093 1094 # We pulled a specific subset
1094 1095 # sync on this subset
1095 1096 return self.heads
1096 1097
1097 1098 @util.propertycache
1098 1099 def canusebundle2(self):
1099 1100 return _canusebundle2(self)
1100 1101
1101 1102 @util.propertycache
1102 1103 def remotebundle2caps(self):
1103 1104 return bundle2.bundle2caps(self.remote)
1104 1105
1105 1106 def gettransaction(self):
1106 1107 # deprecated; talk to trmanager directly
1107 1108 return self.trmanager.transaction()
1108 1109
1109 1110 class transactionmanager(object):
1110 1111 """An object to manage the life cycle of a transaction
1111 1112
1112 1113 It creates the transaction on demand and calls the appropriate hooks when
1113 1114 closing the transaction."""
1114 1115 def __init__(self, repo, source, url):
1115 1116 self.repo = repo
1116 1117 self.source = source
1117 1118 self.url = url
1118 1119 self._tr = None
1119 1120
1120 1121 def transaction(self):
1121 1122 """Return an open transaction object, constructing if necessary"""
1122 1123 if not self._tr:
1123 1124 trname = '%s\n%s' % (self.source, util.hidepassword(self.url))
1124 1125 self._tr = self.repo.transaction(trname)
1125 1126 self._tr.hookargs['source'] = self.source
1126 1127 self._tr.hookargs['url'] = self.url
1127 1128 return self._tr
1128 1129
1129 1130 def close(self):
1130 1131 """close transaction if created"""
1131 1132 if self._tr is not None:
1132 1133 self._tr.close()
1133 1134
1134 1135 def release(self):
1135 1136 """release transaction if created"""
1136 1137 if self._tr is not None:
1137 1138 self._tr.release()
1138 1139
1139 1140 def pull(repo, remote, heads=None, force=False, bookmarks=(), opargs=None,
1140 1141 streamclonerequested=None):
1141 1142 """Fetch repository data from a remote.
1142 1143
1143 1144 This is the main function used to retrieve data from a remote repository.
1144 1145
1145 1146 ``repo`` is the local repository to clone into.
1146 1147 ``remote`` is a peer instance.
1147 1148 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1148 1149 default) means to pull everything from the remote.
1149 1150 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1150 1151 default, all remote bookmarks are pulled.
1151 1152 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1152 1153 initialization.
1153 1154 ``streamclonerequested`` is a boolean indicating whether a "streaming
1154 1155 clone" is requested. A "streaming clone" is essentially a raw file copy
1155 1156 of revlogs from the server. This only works when the local repository is
1156 1157 empty. The default value of ``None`` means to respect the server
1157 1158 configuration for preferring stream clones.
1158 1159
1159 1160 Returns the ``pulloperation`` created for this pull.
1160 1161 """
1161 1162 if opargs is None:
1162 1163 opargs = {}
1163 1164 pullop = pulloperation(repo, remote, heads, force, bookmarks=bookmarks,
1164 1165 streamclonerequested=streamclonerequested, **opargs)
1165 1166 if pullop.remote.local():
1166 1167 missing = set(pullop.remote.requirements) - pullop.repo.supported
1167 1168 if missing:
1168 1169 msg = _("required features are not"
1169 1170 " supported in the destination:"
1170 1171 " %s") % (', '.join(sorted(missing)))
1171 1172 raise error.Abort(msg)
1172 1173
1173 1174 lock = pullop.repo.lock()
1174 1175 try:
1175 1176 pullop.trmanager = transactionmanager(repo, 'pull', remote.url())
1176 1177 streamclone.maybeperformlegacystreamclone(pullop)
1177 1178 # This should ideally be in _pullbundle2(). However, it needs to run
1178 1179 # before discovery to avoid extra work.
1179 1180 _maybeapplyclonebundle(pullop)
1180 1181 _pulldiscovery(pullop)
1181 1182 if pullop.canusebundle2:
1182 1183 _pullbundle2(pullop)
1183 1184 _pullchangeset(pullop)
1184 1185 _pullphase(pullop)
1185 1186 _pullbookmarks(pullop)
1186 1187 _pullobsolete(pullop)
1187 1188 pullop.trmanager.close()
1188 1189 finally:
1189 1190 pullop.trmanager.release()
1190 1191 lock.release()
1191 1192
1192 1193 return pullop
1193 1194
1194 1195 # list of steps to perform discovery before pull
1195 1196 pulldiscoveryorder = []
1196 1197
1197 1198 # Mapping between step name and function
1198 1199 #
1199 1200 # This exists to help extensions wrap steps if necessary
1200 1201 pulldiscoverymapping = {}
1201 1202
1202 1203 def pulldiscovery(stepname):
1203 1204 """decorator for function performing discovery before pull
1204 1205
1205 1206 The function is added to the step -> function mapping and appended to the
1206 1207 list of steps. Beware that decorated function will be added in order (this
1207 1208 may matter).
1208 1209
1209 1210 You can only use this decorator for a new step, if you want to wrap a step
1210 1211 from an extension, change the pulldiscovery dictionary directly."""
1211 1212 def dec(func):
1212 1213 assert stepname not in pulldiscoverymapping
1213 1214 pulldiscoverymapping[stepname] = func
1214 1215 pulldiscoveryorder.append(stepname)
1215 1216 return func
1216 1217 return dec
1217 1218
1218 1219 def _pulldiscovery(pullop):
1219 1220 """Run all discovery steps"""
1220 1221 for stepname in pulldiscoveryorder:
1221 1222 step = pulldiscoverymapping[stepname]
1222 1223 step(pullop)
1223 1224
1224 1225 @pulldiscovery('b1:bookmarks')
1225 1226 def _pullbookmarkbundle1(pullop):
1226 1227 """fetch bookmark data in bundle1 case
1227 1228
1228 1229 If not using bundle2, we have to fetch bookmarks before changeset
1229 1230 discovery to reduce the chance and impact of race conditions."""
1230 1231 if pullop.remotebookmarks is not None:
1231 1232 return
1232 1233 if pullop.canusebundle2 and 'listkeys' in pullop.remotebundle2caps:
1233 1234 # all known bundle2 servers now support listkeys, but lets be nice with
1234 1235 # new implementation.
1235 1236 return
1236 1237 pullop.remotebookmarks = pullop.remote.listkeys('bookmarks')
1237 1238
1238 1239
1239 1240 @pulldiscovery('changegroup')
1240 1241 def _pulldiscoverychangegroup(pullop):
1241 1242 """discovery phase for the pull
1242 1243
1243 1244 Current handle changeset discovery only, will change handle all discovery
1244 1245 at some point."""
1245 1246 tmp = discovery.findcommonincoming(pullop.repo,
1246 1247 pullop.remote,
1247 1248 heads=pullop.heads,
1248 1249 force=pullop.force)
1249 1250 common, fetch, rheads = tmp
1250 1251 nm = pullop.repo.unfiltered().changelog.nodemap
1251 1252 if fetch and rheads:
1252 1253 # If a remote heads in filtered locally, lets drop it from the unknown
1253 1254 # remote heads and put in back in common.
1254 1255 #
1255 1256 # This is a hackish solution to catch most of "common but locally
1256 1257 # hidden situation". We do not performs discovery on unfiltered
1257 1258 # repository because it end up doing a pathological amount of round
1258 1259 # trip for w huge amount of changeset we do not care about.
1259 1260 #
1260 1261 # If a set of such "common but filtered" changeset exist on the server
1261 1262 # but are not including a remote heads, we'll not be able to detect it,
1262 1263 scommon = set(common)
1263 1264 filteredrheads = []
1264 1265 for n in rheads:
1265 1266 if n in nm:
1266 1267 if n not in scommon:
1267 1268 common.append(n)
1268 1269 else:
1269 1270 filteredrheads.append(n)
1270 1271 if not filteredrheads:
1271 1272 fetch = []
1272 1273 rheads = filteredrheads
1273 1274 pullop.common = common
1274 1275 pullop.fetch = fetch
1275 1276 pullop.rheads = rheads
1276 1277
1277 1278 def _pullbundle2(pullop):
1278 1279 """pull data using bundle2
1279 1280
1280 1281 For now, the only supported data are changegroup."""
1281 1282 kwargs = {'bundlecaps': caps20to10(pullop.repo)}
1282 1283
1283 1284 streaming, streamreqs = streamclone.canperformstreamclone(pullop)
1284 1285
1285 1286 # pulling changegroup
1286 1287 pullop.stepsdone.add('changegroup')
1287 1288
1288 1289 kwargs['common'] = pullop.common
1289 1290 kwargs['heads'] = pullop.heads or pullop.rheads
1290 1291 kwargs['cg'] = pullop.fetch
1291 1292 if 'listkeys' in pullop.remotebundle2caps:
1292 1293 kwargs['listkeys'] = ['phase']
1293 1294 if pullop.remotebookmarks is None:
1294 1295 # make sure to always includes bookmark data when migrating
1295 1296 # `hg incoming --bundle` to using this function.
1296 1297 kwargs['listkeys'].append('bookmarks')
1297 1298
1298 1299 # If this is a full pull / clone and the server supports the clone bundles
1299 1300 # feature, tell the server whether we attempted a clone bundle. The
1300 1301 # presence of this flag indicates the client supports clone bundles. This
1301 1302 # will enable the server to treat clients that support clone bundles
1302 1303 # differently from those that don't.
1303 1304 if (pullop.remote.capable('clonebundles')
1304 1305 and pullop.heads is None and list(pullop.common) == [nullid]):
1305 1306 kwargs['cbattempted'] = pullop.clonebundleattempted
1306 1307
1307 1308 if streaming:
1308 1309 pullop.repo.ui.status(_('streaming all changes\n'))
1309 1310 elif not pullop.fetch:
1310 1311 pullop.repo.ui.status(_("no changes found\n"))
1311 1312 pullop.cgresult = 0
1312 1313 else:
1313 1314 if pullop.heads is None and list(pullop.common) == [nullid]:
1314 1315 pullop.repo.ui.status(_("requesting all changes\n"))
1315 1316 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1316 1317 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1317 1318 if obsolete.commonversion(remoteversions) is not None:
1318 1319 kwargs['obsmarkers'] = True
1319 1320 pullop.stepsdone.add('obsmarkers')
1320 1321 _pullbundle2extraprepare(pullop, kwargs)
1321 1322 bundle = pullop.remote.getbundle('pull', **kwargs)
1322 1323 try:
1323 1324 op = bundle2.processbundle(pullop.repo, bundle, pullop.gettransaction)
1324 1325 except error.BundleValueError as exc:
1325 1326 raise error.Abort('missing support for %s' % exc)
1326 1327
1327 1328 if pullop.fetch:
1328 1329 results = [cg['return'] for cg in op.records['changegroup']]
1329 1330 pullop.cgresult = changegroup.combineresults(results)
1330 1331
1331 1332 # processing phases change
1332 1333 for namespace, value in op.records['listkeys']:
1333 1334 if namespace == 'phases':
1334 1335 _pullapplyphases(pullop, value)
1335 1336
1336 1337 # processing bookmark update
1337 1338 for namespace, value in op.records['listkeys']:
1338 1339 if namespace == 'bookmarks':
1339 1340 pullop.remotebookmarks = value
1340 1341
1341 1342 # bookmark data were either already there or pulled in the bundle
1342 1343 if pullop.remotebookmarks is not None:
1343 1344 _pullbookmarks(pullop)
1344 1345
1345 1346 def _pullbundle2extraprepare(pullop, kwargs):
1346 1347 """hook function so that extensions can extend the getbundle call"""
1347 1348 pass
1348 1349
1349 1350 def _pullchangeset(pullop):
1350 1351 """pull changeset from unbundle into the local repo"""
1351 1352 # We delay the open of the transaction as late as possible so we
1352 1353 # don't open transaction for nothing or you break future useful
1353 1354 # rollback call
1354 1355 if 'changegroup' in pullop.stepsdone:
1355 1356 return
1356 1357 pullop.stepsdone.add('changegroup')
1357 1358 if not pullop.fetch:
1358 1359 pullop.repo.ui.status(_("no changes found\n"))
1359 1360 pullop.cgresult = 0
1360 1361 return
1361 1362 pullop.gettransaction()
1362 1363 if pullop.heads is None and list(pullop.common) == [nullid]:
1363 1364 pullop.repo.ui.status(_("requesting all changes\n"))
1364 1365 elif pullop.heads is None and pullop.remote.capable('changegroupsubset'):
1365 1366 # issue1320, avoid a race if remote changed after discovery
1366 1367 pullop.heads = pullop.rheads
1367 1368
1368 1369 if pullop.remote.capable('getbundle'):
1369 1370 # TODO: get bundlecaps from remote
1370 1371 cg = pullop.remote.getbundle('pull', common=pullop.common,
1371 1372 heads=pullop.heads or pullop.rheads)
1372 1373 elif pullop.heads is None:
1373 1374 cg = pullop.remote.changegroup(pullop.fetch, 'pull')
1374 1375 elif not pullop.remote.capable('changegroupsubset'):
1375 1376 raise error.Abort(_("partial pull cannot be done because "
1376 1377 "other repository doesn't support "
1377 1378 "changegroupsubset."))
1378 1379 else:
1379 1380 cg = pullop.remote.changegroupsubset(pullop.fetch, pullop.heads, 'pull')
1380 1381 pullop.cgresult = cg.apply(pullop.repo, 'pull', pullop.remote.url())
1381 1382
1382 1383 def _pullphase(pullop):
1383 1384 # Get remote phases data from remote
1384 1385 if 'phases' in pullop.stepsdone:
1385 1386 return
1386 1387 remotephases = pullop.remote.listkeys('phases')
1387 1388 _pullapplyphases(pullop, remotephases)
1388 1389
1389 1390 def _pullapplyphases(pullop, remotephases):
1390 1391 """apply phase movement from observed remote state"""
1391 1392 if 'phases' in pullop.stepsdone:
1392 1393 return
1393 1394 pullop.stepsdone.add('phases')
1394 1395 publishing = bool(remotephases.get('publishing', False))
1395 1396 if remotephases and not publishing:
1396 1397 # remote is new and unpublishing
1397 1398 pheads, _dr = phases.analyzeremotephases(pullop.repo,
1398 1399 pullop.pulledsubset,
1399 1400 remotephases)
1400 1401 dheads = pullop.pulledsubset
1401 1402 else:
1402 1403 # Remote is old or publishing all common changesets
1403 1404 # should be seen as public
1404 1405 pheads = pullop.pulledsubset
1405 1406 dheads = []
1406 1407 unfi = pullop.repo.unfiltered()
1407 1408 phase = unfi._phasecache.phase
1408 1409 rev = unfi.changelog.nodemap.get
1409 1410 public = phases.public
1410 1411 draft = phases.draft
1411 1412
1412 1413 # exclude changesets already public locally and update the others
1413 1414 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
1414 1415 if pheads:
1415 1416 tr = pullop.gettransaction()
1416 1417 phases.advanceboundary(pullop.repo, tr, public, pheads)
1417 1418
1418 1419 # exclude changesets already draft locally and update the others
1419 1420 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
1420 1421 if dheads:
1421 1422 tr = pullop.gettransaction()
1422 1423 phases.advanceboundary(pullop.repo, tr, draft, dheads)
1423 1424
1424 1425 def _pullbookmarks(pullop):
1425 1426 """process the remote bookmark information to update the local one"""
1426 1427 if 'bookmarks' in pullop.stepsdone:
1427 1428 return
1428 1429 pullop.stepsdone.add('bookmarks')
1429 1430 repo = pullop.repo
1430 1431 remotebookmarks = pullop.remotebookmarks
1431 1432 bookmod.updatefromremote(repo.ui, repo, remotebookmarks,
1432 1433 pullop.remote.url(),
1433 1434 pullop.gettransaction,
1434 1435 explicit=pullop.explicitbookmarks)
1435 1436
1436 1437 def _pullobsolete(pullop):
1437 1438 """utility function to pull obsolete markers from a remote
1438 1439
1439 1440 The `gettransaction` is function that return the pull transaction, creating
1440 1441 one if necessary. We return the transaction to inform the calling code that
1441 1442 a new transaction have been created (when applicable).
1442 1443
1443 1444 Exists mostly to allow overriding for experimentation purpose"""
1444 1445 if 'obsmarkers' in pullop.stepsdone:
1445 1446 return
1446 1447 pullop.stepsdone.add('obsmarkers')
1447 1448 tr = None
1448 1449 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1449 1450 pullop.repo.ui.debug('fetching remote obsolete markers\n')
1450 1451 remoteobs = pullop.remote.listkeys('obsolete')
1451 1452 if 'dump0' in remoteobs:
1452 1453 tr = pullop.gettransaction()
1453 1454 markers = []
1454 1455 for key in sorted(remoteobs, reverse=True):
1455 1456 if key.startswith('dump'):
1456 1457 data = base85.b85decode(remoteobs[key])
1457 1458 version, newmarks = obsolete._readmarkers(data)
1458 1459 markers += newmarks
1459 1460 if markers:
1460 1461 pullop.repo.obsstore.add(tr, markers)
1461 1462 pullop.repo.invalidatevolatilesets()
1462 1463 return tr
1463 1464
1464 1465 def caps20to10(repo):
1465 1466 """return a set with appropriate options to use bundle20 during getbundle"""
1466 1467 caps = set(['HG20'])
1467 1468 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo))
1468 caps.add('bundle2=' + urllib.quote(capsblob))
1469 caps.add('bundle2=' + urlreq.quote(capsblob))
1469 1470 return caps
1470 1471
1471 1472 # List of names of steps to perform for a bundle2 for getbundle, order matters.
1472 1473 getbundle2partsorder = []
1473 1474
1474 1475 # Mapping between step name and function
1475 1476 #
1476 1477 # This exists to help extensions wrap steps if necessary
1477 1478 getbundle2partsmapping = {}
1478 1479
1479 1480 def getbundle2partsgenerator(stepname, idx=None):
1480 1481 """decorator for function generating bundle2 part for getbundle
1481 1482
1482 1483 The function is added to the step -> function mapping and appended to the
1483 1484 list of steps. Beware that decorated functions will be added in order
1484 1485 (this may matter).
1485 1486
1486 1487 You can only use this decorator for new steps, if you want to wrap a step
1487 1488 from an extension, attack the getbundle2partsmapping dictionary directly."""
1488 1489 def dec(func):
1489 1490 assert stepname not in getbundle2partsmapping
1490 1491 getbundle2partsmapping[stepname] = func
1491 1492 if idx is None:
1492 1493 getbundle2partsorder.append(stepname)
1493 1494 else:
1494 1495 getbundle2partsorder.insert(idx, stepname)
1495 1496 return func
1496 1497 return dec
1497 1498
1498 1499 def bundle2requested(bundlecaps):
1499 1500 if bundlecaps is not None:
1500 1501 return any(cap.startswith('HG2') for cap in bundlecaps)
1501 1502 return False
1502 1503
1503 1504 def getbundle(repo, source, heads=None, common=None, bundlecaps=None,
1504 1505 **kwargs):
1505 1506 """return a full bundle (with potentially multiple kind of parts)
1506 1507
1507 1508 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
1508 1509 passed. For now, the bundle can contain only changegroup, but this will
1509 1510 changes when more part type will be available for bundle2.
1510 1511
1511 1512 This is different from changegroup.getchangegroup that only returns an HG10
1512 1513 changegroup bundle. They may eventually get reunited in the future when we
1513 1514 have a clearer idea of the API we what to query different data.
1514 1515
1515 1516 The implementation is at a very early stage and will get massive rework
1516 1517 when the API of bundle is refined.
1517 1518 """
1518 1519 usebundle2 = bundle2requested(bundlecaps)
1519 1520 # bundle10 case
1520 1521 if not usebundle2:
1521 1522 if bundlecaps and not kwargs.get('cg', True):
1522 1523 raise ValueError(_('request for bundle10 must include changegroup'))
1523 1524
1524 1525 if kwargs:
1525 1526 raise ValueError(_('unsupported getbundle arguments: %s')
1526 1527 % ', '.join(sorted(kwargs.keys())))
1527 1528 return changegroup.getchangegroup(repo, source, heads=heads,
1528 1529 common=common, bundlecaps=bundlecaps)
1529 1530
1530 1531 # bundle20 case
1531 1532 b2caps = {}
1532 1533 for bcaps in bundlecaps:
1533 1534 if bcaps.startswith('bundle2='):
1534 blob = urllib.unquote(bcaps[len('bundle2='):])
1535 blob = urlreq.unquote(bcaps[len('bundle2='):])
1535 1536 b2caps.update(bundle2.decodecaps(blob))
1536 1537 bundler = bundle2.bundle20(repo.ui, b2caps)
1537 1538
1538 1539 kwargs['heads'] = heads
1539 1540 kwargs['common'] = common
1540 1541
1541 1542 for name in getbundle2partsorder:
1542 1543 func = getbundle2partsmapping[name]
1543 1544 func(bundler, repo, source, bundlecaps=bundlecaps, b2caps=b2caps,
1544 1545 **kwargs)
1545 1546
1546 1547 return util.chunkbuffer(bundler.getchunks())
1547 1548
1548 1549 @getbundle2partsgenerator('changegroup')
1549 1550 def _getbundlechangegrouppart(bundler, repo, source, bundlecaps=None,
1550 1551 b2caps=None, heads=None, common=None, **kwargs):
1551 1552 """add a changegroup part to the requested bundle"""
1552 1553 cg = None
1553 1554 if kwargs.get('cg', True):
1554 1555 # build changegroup bundle here.
1555 1556 version = '01'
1556 1557 cgversions = b2caps.get('changegroup')
1557 1558 if cgversions: # 3.1 and 3.2 ship with an empty value
1558 1559 cgversions = [v for v in cgversions
1559 1560 if v in changegroup.supportedoutgoingversions(repo)]
1560 1561 if not cgversions:
1561 1562 raise ValueError(_('no common changegroup version'))
1562 1563 version = max(cgversions)
1563 1564 outgoing = changegroup.computeoutgoing(repo, heads, common)
1564 1565 cg = changegroup.getlocalchangegroupraw(repo, source, outgoing,
1565 1566 bundlecaps=bundlecaps,
1566 1567 version=version)
1567 1568
1568 1569 if cg:
1569 1570 part = bundler.newpart('changegroup', data=cg)
1570 1571 if cgversions:
1571 1572 part.addparam('version', version)
1572 1573 part.addparam('nbchanges', str(len(outgoing.missing)), mandatory=False)
1573 1574 if 'treemanifest' in repo.requirements:
1574 1575 part.addparam('treemanifest', '1')
1575 1576
1576 1577 @getbundle2partsgenerator('listkeys')
1577 1578 def _getbundlelistkeysparts(bundler, repo, source, bundlecaps=None,
1578 1579 b2caps=None, **kwargs):
1579 1580 """add parts containing listkeys namespaces to the requested bundle"""
1580 1581 listkeys = kwargs.get('listkeys', ())
1581 1582 for namespace in listkeys:
1582 1583 part = bundler.newpart('listkeys')
1583 1584 part.addparam('namespace', namespace)
1584 1585 keys = repo.listkeys(namespace).items()
1585 1586 part.data = pushkey.encodekeys(keys)
1586 1587
1587 1588 @getbundle2partsgenerator('obsmarkers')
1588 1589 def _getbundleobsmarkerpart(bundler, repo, source, bundlecaps=None,
1589 1590 b2caps=None, heads=None, **kwargs):
1590 1591 """add an obsolescence markers part to the requested bundle"""
1591 1592 if kwargs.get('obsmarkers', False):
1592 1593 if heads is None:
1593 1594 heads = repo.heads()
1594 1595 subset = [c.node() for c in repo.set('::%ln', heads)]
1595 1596 markers = repo.obsstore.relevantmarkers(subset)
1596 1597 markers = sorted(markers)
1597 1598 buildobsmarkerspart(bundler, markers)
1598 1599
1599 1600 @getbundle2partsgenerator('hgtagsfnodes')
1600 1601 def _getbundletagsfnodes(bundler, repo, source, bundlecaps=None,
1601 1602 b2caps=None, heads=None, common=None,
1602 1603 **kwargs):
1603 1604 """Transfer the .hgtags filenodes mapping.
1604 1605
1605 1606 Only values for heads in this bundle will be transferred.
1606 1607
1607 1608 The part data consists of pairs of 20 byte changeset node and .hgtags
1608 1609 filenodes raw values.
1609 1610 """
1610 1611 # Don't send unless:
1611 1612 # - changeset are being exchanged,
1612 1613 # - the client supports it.
1613 1614 if not (kwargs.get('cg', True) and 'hgtagsfnodes' in b2caps):
1614 1615 return
1615 1616
1616 1617 outgoing = changegroup.computeoutgoing(repo, heads, common)
1617 1618
1618 1619 if not outgoing.missingheads:
1619 1620 return
1620 1621
1621 1622 cache = tags.hgtagsfnodescache(repo.unfiltered())
1622 1623 chunks = []
1623 1624
1624 1625 # .hgtags fnodes are only relevant for head changesets. While we could
1625 1626 # transfer values for all known nodes, there will likely be little to
1626 1627 # no benefit.
1627 1628 #
1628 1629 # We don't bother using a generator to produce output data because
1629 1630 # a) we only have 40 bytes per head and even esoteric numbers of heads
1630 1631 # consume little memory (1M heads is 40MB) b) we don't want to send the
1631 1632 # part if we don't have entries and knowing if we have entries requires
1632 1633 # cache lookups.
1633 1634 for node in outgoing.missingheads:
1634 1635 # Don't compute missing, as this may slow down serving.
1635 1636 fnode = cache.getfnode(node, computemissing=False)
1636 1637 if fnode is not None:
1637 1638 chunks.extend([node, fnode])
1638 1639
1639 1640 if chunks:
1640 1641 bundler.newpart('hgtagsfnodes', data=''.join(chunks))
1641 1642
1642 1643 def check_heads(repo, their_heads, context):
1643 1644 """check if the heads of a repo have been modified
1644 1645
1645 1646 Used by peer for unbundling.
1646 1647 """
1647 1648 heads = repo.heads()
1648 1649 heads_hash = util.sha1(''.join(sorted(heads))).digest()
1649 1650 if not (their_heads == ['force'] or their_heads == heads or
1650 1651 their_heads == ['hashed', heads_hash]):
1651 1652 # someone else committed/pushed/unbundled while we
1652 1653 # were transferring data
1653 1654 raise error.PushRaced('repository changed while %s - '
1654 1655 'please try again' % context)
1655 1656
1656 1657 def unbundle(repo, cg, heads, source, url):
1657 1658 """Apply a bundle to a repo.
1658 1659
1659 1660 this function makes sure the repo is locked during the application and have
1660 1661 mechanism to check that no push race occurred between the creation of the
1661 1662 bundle and its application.
1662 1663
1663 1664 If the push was raced as PushRaced exception is raised."""
1664 1665 r = 0
1665 1666 # need a transaction when processing a bundle2 stream
1666 1667 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
1667 1668 lockandtr = [None, None, None]
1668 1669 recordout = None
1669 1670 # quick fix for output mismatch with bundle2 in 3.4
1670 1671 captureoutput = repo.ui.configbool('experimental', 'bundle2-output-capture',
1671 1672 False)
1672 1673 if url.startswith('remote:http:') or url.startswith('remote:https:'):
1673 1674 captureoutput = True
1674 1675 try:
1675 1676 check_heads(repo, heads, 'uploading changes')
1676 1677 # push can proceed
1677 1678 if util.safehasattr(cg, 'params'):
1678 1679 r = None
1679 1680 try:
1680 1681 def gettransaction():
1681 1682 if not lockandtr[2]:
1682 1683 lockandtr[0] = repo.wlock()
1683 1684 lockandtr[1] = repo.lock()
1684 1685 lockandtr[2] = repo.transaction(source)
1685 1686 lockandtr[2].hookargs['source'] = source
1686 1687 lockandtr[2].hookargs['url'] = url
1687 1688 lockandtr[2].hookargs['bundle2'] = '1'
1688 1689 return lockandtr[2]
1689 1690
1690 1691 # Do greedy locking by default until we're satisfied with lazy
1691 1692 # locking.
1692 1693 if not repo.ui.configbool('experimental', 'bundle2lazylocking'):
1693 1694 gettransaction()
1694 1695
1695 1696 op = bundle2.bundleoperation(repo, gettransaction,
1696 1697 captureoutput=captureoutput)
1697 1698 try:
1698 1699 op = bundle2.processbundle(repo, cg, op=op)
1699 1700 finally:
1700 1701 r = op.reply
1701 1702 if captureoutput and r is not None:
1702 1703 repo.ui.pushbuffer(error=True, subproc=True)
1703 1704 def recordout(output):
1704 1705 r.newpart('output', data=output, mandatory=False)
1705 1706 if lockandtr[2] is not None:
1706 1707 lockandtr[2].close()
1707 1708 except BaseException as exc:
1708 1709 exc.duringunbundle2 = True
1709 1710 if captureoutput and r is not None:
1710 1711 parts = exc._bundle2salvagedoutput = r.salvageoutput()
1711 1712 def recordout(output):
1712 1713 part = bundle2.bundlepart('output', data=output,
1713 1714 mandatory=False)
1714 1715 parts.append(part)
1715 1716 raise
1716 1717 else:
1717 1718 lockandtr[1] = repo.lock()
1718 1719 r = cg.apply(repo, source, url)
1719 1720 finally:
1720 1721 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
1721 1722 if recordout is not None:
1722 1723 recordout(repo.ui.popbuffer())
1723 1724 return r
1724 1725
1725 1726 def _maybeapplyclonebundle(pullop):
1726 1727 """Apply a clone bundle from a remote, if possible."""
1727 1728
1728 1729 repo = pullop.repo
1729 1730 remote = pullop.remote
1730 1731
1731 1732 if not repo.ui.configbool('ui', 'clonebundles', True):
1732 1733 return
1733 1734
1734 1735 # Only run if local repo is empty.
1735 1736 if len(repo):
1736 1737 return
1737 1738
1738 1739 if pullop.heads:
1739 1740 return
1740 1741
1741 1742 if not remote.capable('clonebundles'):
1742 1743 return
1743 1744
1744 1745 res = remote._call('clonebundles')
1745 1746
1746 1747 # If we call the wire protocol command, that's good enough to record the
1747 1748 # attempt.
1748 1749 pullop.clonebundleattempted = True
1749 1750
1750 1751 entries = parseclonebundlesmanifest(repo, res)
1751 1752 if not entries:
1752 1753 repo.ui.note(_('no clone bundles available on remote; '
1753 1754 'falling back to regular clone\n'))
1754 1755 return
1755 1756
1756 1757 entries = filterclonebundleentries(repo, entries)
1757 1758 if not entries:
1758 1759 # There is a thundering herd concern here. However, if a server
1759 1760 # operator doesn't advertise bundles appropriate for its clients,
1760 1761 # they deserve what's coming. Furthermore, from a client's
1761 1762 # perspective, no automatic fallback would mean not being able to
1762 1763 # clone!
1763 1764 repo.ui.warn(_('no compatible clone bundles available on server; '
1764 1765 'falling back to regular clone\n'))
1765 1766 repo.ui.warn(_('(you may want to report this to the server '
1766 1767 'operator)\n'))
1767 1768 return
1768 1769
1769 1770 entries = sortclonebundleentries(repo.ui, entries)
1770 1771
1771 1772 url = entries[0]['URL']
1772 1773 repo.ui.status(_('applying clone bundle from %s\n') % url)
1773 1774 if trypullbundlefromurl(repo.ui, repo, url):
1774 1775 repo.ui.status(_('finished applying clone bundle\n'))
1775 1776 # Bundle failed.
1776 1777 #
1777 1778 # We abort by default to avoid the thundering herd of
1778 1779 # clients flooding a server that was expecting expensive
1779 1780 # clone load to be offloaded.
1780 1781 elif repo.ui.configbool('ui', 'clonebundlefallback', False):
1781 1782 repo.ui.warn(_('falling back to normal clone\n'))
1782 1783 else:
1783 1784 raise error.Abort(_('error applying bundle'),
1784 1785 hint=_('if this error persists, consider contacting '
1785 1786 'the server operator or disable clone '
1786 1787 'bundles via '
1787 1788 '"--config ui.clonebundles=false"'))
1788 1789
1789 1790 def parseclonebundlesmanifest(repo, s):
1790 1791 """Parses the raw text of a clone bundles manifest.
1791 1792
1792 1793 Returns a list of dicts. The dicts have a ``URL`` key corresponding
1793 1794 to the URL and other keys are the attributes for the entry.
1794 1795 """
1795 1796 m = []
1796 1797 for line in s.splitlines():
1797 1798 fields = line.split()
1798 1799 if not fields:
1799 1800 continue
1800 1801 attrs = {'URL': fields[0]}
1801 1802 for rawattr in fields[1:]:
1802 1803 key, value = rawattr.split('=', 1)
1803 key = urllib.unquote(key)
1804 value = urllib.unquote(value)
1804 key = urlreq.unquote(key)
1805 value = urlreq.unquote(value)
1805 1806 attrs[key] = value
1806 1807
1807 1808 # Parse BUNDLESPEC into components. This makes client-side
1808 1809 # preferences easier to specify since you can prefer a single
1809 1810 # component of the BUNDLESPEC.
1810 1811 if key == 'BUNDLESPEC':
1811 1812 try:
1812 1813 comp, version, params = parsebundlespec(repo, value,
1813 1814 externalnames=True)
1814 1815 attrs['COMPRESSION'] = comp
1815 1816 attrs['VERSION'] = version
1816 1817 except error.InvalidBundleSpecification:
1817 1818 pass
1818 1819 except error.UnsupportedBundleSpecification:
1819 1820 pass
1820 1821
1821 1822 m.append(attrs)
1822 1823
1823 1824 return m
1824 1825
1825 1826 def filterclonebundleentries(repo, entries):
1826 1827 """Remove incompatible clone bundle manifest entries.
1827 1828
1828 1829 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
1829 1830 and returns a new list consisting of only the entries that this client
1830 1831 should be able to apply.
1831 1832
1832 1833 There is no guarantee we'll be able to apply all returned entries because
1833 1834 the metadata we use to filter on may be missing or wrong.
1834 1835 """
1835 1836 newentries = []
1836 1837 for entry in entries:
1837 1838 spec = entry.get('BUNDLESPEC')
1838 1839 if spec:
1839 1840 try:
1840 1841 parsebundlespec(repo, spec, strict=True)
1841 1842 except error.InvalidBundleSpecification as e:
1842 1843 repo.ui.debug(str(e) + '\n')
1843 1844 continue
1844 1845 except error.UnsupportedBundleSpecification as e:
1845 1846 repo.ui.debug('filtering %s because unsupported bundle '
1846 1847 'spec: %s\n' % (entry['URL'], str(e)))
1847 1848 continue
1848 1849
1849 1850 if 'REQUIRESNI' in entry and not sslutil.hassni:
1850 1851 repo.ui.debug('filtering %s because SNI not supported\n' %
1851 1852 entry['URL'])
1852 1853 continue
1853 1854
1854 1855 newentries.append(entry)
1855 1856
1856 1857 return newentries
1857 1858
1858 1859 def sortclonebundleentries(ui, entries):
1859 1860 prefers = ui.configlist('ui', 'clonebundleprefers', default=[])
1860 1861 if not prefers:
1861 1862 return list(entries)
1862 1863
1863 1864 prefers = [p.split('=', 1) for p in prefers]
1864 1865
1865 1866 # Our sort function.
1866 1867 def compareentry(a, b):
1867 1868 for prefkey, prefvalue in prefers:
1868 1869 avalue = a.get(prefkey)
1869 1870 bvalue = b.get(prefkey)
1870 1871
1871 1872 # Special case for b missing attribute and a matches exactly.
1872 1873 if avalue is not None and bvalue is None and avalue == prefvalue:
1873 1874 return -1
1874 1875
1875 1876 # Special case for a missing attribute and b matches exactly.
1876 1877 if bvalue is not None and avalue is None and bvalue == prefvalue:
1877 1878 return 1
1878 1879
1879 1880 # We can't compare unless attribute present on both.
1880 1881 if avalue is None or bvalue is None:
1881 1882 continue
1882 1883
1883 1884 # Same values should fall back to next attribute.
1884 1885 if avalue == bvalue:
1885 1886 continue
1886 1887
1887 1888 # Exact matches come first.
1888 1889 if avalue == prefvalue:
1889 1890 return -1
1890 1891 if bvalue == prefvalue:
1891 1892 return 1
1892 1893
1893 1894 # Fall back to next attribute.
1894 1895 continue
1895 1896
1896 1897 # If we got here we couldn't sort by attributes and prefers. Fall
1897 1898 # back to index order.
1898 1899 return 0
1899 1900
1900 1901 return sorted(entries, cmp=compareentry)
1901 1902
1902 1903 def trypullbundlefromurl(ui, repo, url):
1903 1904 """Attempt to apply a bundle from a URL."""
1904 1905 lock = repo.lock()
1905 1906 try:
1906 1907 tr = repo.transaction('bundleurl')
1907 1908 try:
1908 1909 try:
1909 1910 fh = urlmod.open(ui, url)
1910 1911 cg = readbundle(ui, fh, 'stream')
1911 1912
1912 1913 if isinstance(cg, bundle2.unbundle20):
1913 1914 bundle2.processbundle(repo, cg, lambda: tr)
1914 1915 elif isinstance(cg, streamclone.streamcloneapplier):
1915 1916 cg.apply(repo)
1916 1917 else:
1917 1918 cg.apply(repo, 'clonebundles', url)
1918 1919 tr.close()
1919 1920 return True
1920 except urllib2.HTTPError as e:
1921 except urlerr.httperror as e:
1921 1922 ui.warn(_('HTTP error fetching bundle: %s\n') % str(e))
1922 except urllib2.URLError as e:
1923 except urlerr.urlerror as e:
1923 1924 ui.warn(_('error fetching bundle: %s\n') % e.reason[1])
1924 1925
1925 1926 return False
1926 1927 finally:
1927 1928 tr.release()
1928 1929 finally:
1929 1930 lock.release()
@@ -1,115 +1,117 b''
1 1 #
2 2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import cgi
11 import urllib
12 11 import zlib
13 12
14 13 from .common import (
15 14 HTTP_OK,
16 15 )
17 16
18 17 from .. import (
19 18 util,
20 19 wireproto,
21 20 )
22 21 stringio = util.stringio
23 22
23 urlerr = util.urlerr
24 urlreq = util.urlreq
25
24 26 HGTYPE = 'application/mercurial-0.1'
25 27 HGERRTYPE = 'application/hg-error'
26 28
27 29 class webproto(wireproto.abstractserverproto):
28 30 def __init__(self, req, ui):
29 31 self.req = req
30 32 self.response = ''
31 33 self.ui = ui
32 34 def getargs(self, args):
33 35 knownargs = self._args()
34 36 data = {}
35 37 keys = args.split()
36 38 for k in keys:
37 39 if k == '*':
38 40 star = {}
39 41 for key in knownargs.keys():
40 42 if key != 'cmd' and key not in keys:
41 43 star[key] = knownargs[key][0]
42 44 data['*'] = star
43 45 else:
44 46 data[k] = knownargs[k][0]
45 47 return [data[k] for k in keys]
46 48 def _args(self):
47 49 args = self.req.form.copy()
48 50 postlen = int(self.req.env.get('HTTP_X_HGARGS_POST', 0))
49 51 if postlen:
50 52 args.update(cgi.parse_qs(
51 53 self.req.read(postlen), keep_blank_values=True))
52 54 return args
53 55 chunks = []
54 56 i = 1
55 57 while True:
56 58 h = self.req.env.get('HTTP_X_HGARG_' + str(i))
57 59 if h is None:
58 60 break
59 61 chunks += [h]
60 62 i += 1
61 63 args.update(cgi.parse_qs(''.join(chunks), keep_blank_values=True))
62 64 return args
63 65 def getfile(self, fp):
64 66 length = int(self.req.env['CONTENT_LENGTH'])
65 67 for s in util.filechunkiter(self.req, limit=length):
66 68 fp.write(s)
67 69 def redirect(self):
68 70 self.oldio = self.ui.fout, self.ui.ferr
69 71 self.ui.ferr = self.ui.fout = stringio()
70 72 def restore(self):
71 73 val = self.ui.fout.getvalue()
72 74 self.ui.ferr, self.ui.fout = self.oldio
73 75 return val
74 76 def groupchunks(self, cg):
75 77 z = zlib.compressobj()
76 78 while True:
77 79 chunk = cg.read(4096)
78 80 if not chunk:
79 81 break
80 82 yield z.compress(chunk)
81 83 yield z.flush()
82 84 def _client(self):
83 85 return 'remote:%s:%s:%s' % (
84 86 self.req.env.get('wsgi.url_scheme') or 'http',
85 urllib.quote(self.req.env.get('REMOTE_HOST', '')),
86 urllib.quote(self.req.env.get('REMOTE_USER', '')))
87 urlreq.quote(self.req.env.get('REMOTE_HOST', '')),
88 urlreq.quote(self.req.env.get('REMOTE_USER', '')))
87 89
88 90 def iscmd(cmd):
89 91 return cmd in wireproto.commands
90 92
91 93 def call(repo, req, cmd):
92 94 p = webproto(req, repo.ui)
93 95 rsp = wireproto.dispatch(repo, p, cmd)
94 96 if isinstance(rsp, str):
95 97 req.respond(HTTP_OK, HGTYPE, body=rsp)
96 98 return []
97 99 elif isinstance(rsp, wireproto.streamres):
98 100 req.respond(HTTP_OK, HGTYPE)
99 101 return rsp.gen
100 102 elif isinstance(rsp, wireproto.pushres):
101 103 val = p.restore()
102 104 rsp = '%d\n%s' % (rsp.res, val)
103 105 req.respond(HTTP_OK, HGTYPE, body=rsp)
104 106 return []
105 107 elif isinstance(rsp, wireproto.pusherr):
106 108 # drain the incoming bundle
107 109 req.drain()
108 110 p.restore()
109 111 rsp = '0\n%s\n' % rsp.res
110 112 req.respond(HTTP_OK, HGTYPE, body=rsp)
111 113 return []
112 114 elif isinstance(rsp, wireproto.ooberror):
113 115 rsp = rsp.message
114 116 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
115 117 return []
@@ -1,322 +1,324 b''
1 1 # hgweb/server.py - The standalone hg web server.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import BaseHTTPServer
12 12 import SocketServer
13 13 import errno
14 14 import os
15 15 import socket
16 16 import sys
17 17 import traceback
18 import urllib
19 18
20 19 from ..i18n import _
21 20
22 21 from .. import (
23 22 error,
24 23 util,
25 24 )
26 25
26 urlerr = util.urlerr
27 urlreq = util.urlreq
28
27 29 from . import (
28 30 common,
29 31 )
30 32
31 33 def _splitURI(uri):
32 34 """Return path and query that has been split from uri
33 35
34 36 Just like CGI environment, the path is unquoted, the query is
35 37 not.
36 38 """
37 39 if '?' in uri:
38 40 path, query = uri.split('?', 1)
39 41 else:
40 42 path, query = uri, ''
41 return urllib.unquote(path), query
43 return urlreq.unquote(path), query
42 44
43 45 class _error_logger(object):
44 46 def __init__(self, handler):
45 47 self.handler = handler
46 48 def flush(self):
47 49 pass
48 50 def write(self, str):
49 51 self.writelines(str.split('\n'))
50 52 def writelines(self, seq):
51 53 for msg in seq:
52 54 self.handler.log_error("HG error: %s", msg)
53 55
54 56 class _httprequesthandler(BaseHTTPServer.BaseHTTPRequestHandler):
55 57
56 58 url_scheme = 'http'
57 59
58 60 @staticmethod
59 61 def preparehttpserver(httpserver, ssl_cert):
60 62 """Prepare .socket of new HTTPServer instance"""
61 63 pass
62 64
63 65 def __init__(self, *args, **kargs):
64 66 self.protocol_version = 'HTTP/1.1'
65 67 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
66 68
67 69 def _log_any(self, fp, format, *args):
68 70 fp.write("%s - - [%s] %s\n" % (self.client_address[0],
69 71 self.log_date_time_string(),
70 72 format % args))
71 73 fp.flush()
72 74
73 75 def log_error(self, format, *args):
74 76 self._log_any(self.server.errorlog, format, *args)
75 77
76 78 def log_message(self, format, *args):
77 79 self._log_any(self.server.accesslog, format, *args)
78 80
79 81 def log_request(self, code='-', size='-'):
80 82 xheaders = []
81 83 if util.safehasattr(self, 'headers'):
82 84 xheaders = [h for h in self.headers.items()
83 85 if h[0].startswith('x-')]
84 86 self.log_message('"%s" %s %s%s',
85 87 self.requestline, str(code), str(size),
86 88 ''.join([' %s:%s' % h for h in sorted(xheaders)]))
87 89
88 90 def do_write(self):
89 91 try:
90 92 self.do_hgweb()
91 93 except socket.error as inst:
92 94 if inst[0] != errno.EPIPE:
93 95 raise
94 96
95 97 def do_POST(self):
96 98 try:
97 99 self.do_write()
98 100 except Exception:
99 101 self._start_response("500 Internal Server Error", [])
100 102 self._write("Internal Server Error")
101 103 self._done()
102 104 tb = "".join(traceback.format_exception(*sys.exc_info()))
103 105 self.log_error("Exception happened during processing "
104 106 "request '%s':\n%s", self.path, tb)
105 107
106 108 def do_GET(self):
107 109 self.do_POST()
108 110
109 111 def do_hgweb(self):
110 112 path, query = _splitURI(self.path)
111 113
112 114 env = {}
113 115 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
114 116 env['REQUEST_METHOD'] = self.command
115 117 env['SERVER_NAME'] = self.server.server_name
116 118 env['SERVER_PORT'] = str(self.server.server_port)
117 119 env['REQUEST_URI'] = self.path
118 120 env['SCRIPT_NAME'] = self.server.prefix
119 121 env['PATH_INFO'] = path[len(self.server.prefix):]
120 122 env['REMOTE_HOST'] = self.client_address[0]
121 123 env['REMOTE_ADDR'] = self.client_address[0]
122 124 if query:
123 125 env['QUERY_STRING'] = query
124 126
125 127 if self.headers.typeheader is None:
126 128 env['CONTENT_TYPE'] = self.headers.type
127 129 else:
128 130 env['CONTENT_TYPE'] = self.headers.typeheader
129 131 length = self.headers.getheader('content-length')
130 132 if length:
131 133 env['CONTENT_LENGTH'] = length
132 134 for header in [h for h in self.headers.keys()
133 135 if h not in ('content-type', 'content-length')]:
134 136 hkey = 'HTTP_' + header.replace('-', '_').upper()
135 137 hval = self.headers.getheader(header)
136 138 hval = hval.replace('\n', '').strip()
137 139 if hval:
138 140 env[hkey] = hval
139 141 env['SERVER_PROTOCOL'] = self.request_version
140 142 env['wsgi.version'] = (1, 0)
141 143 env['wsgi.url_scheme'] = self.url_scheme
142 144 if env.get('HTTP_EXPECT', '').lower() == '100-continue':
143 145 self.rfile = common.continuereader(self.rfile, self.wfile.write)
144 146
145 147 env['wsgi.input'] = self.rfile
146 148 env['wsgi.errors'] = _error_logger(self)
147 149 env['wsgi.multithread'] = isinstance(self.server,
148 150 SocketServer.ThreadingMixIn)
149 151 env['wsgi.multiprocess'] = isinstance(self.server,
150 152 SocketServer.ForkingMixIn)
151 153 env['wsgi.run_once'] = 0
152 154
153 155 self.saved_status = None
154 156 self.saved_headers = []
155 157 self.sent_headers = False
156 158 self.length = None
157 159 self._chunked = None
158 160 for chunk in self.server.application(env, self._start_response):
159 161 self._write(chunk)
160 162 if not self.sent_headers:
161 163 self.send_headers()
162 164 self._done()
163 165
164 166 def send_headers(self):
165 167 if not self.saved_status:
166 168 raise AssertionError("Sending headers before "
167 169 "start_response() called")
168 170 saved_status = self.saved_status.split(None, 1)
169 171 saved_status[0] = int(saved_status[0])
170 172 self.send_response(*saved_status)
171 173 self.length = None
172 174 self._chunked = False
173 175 for h in self.saved_headers:
174 176 self.send_header(*h)
175 177 if h[0].lower() == 'content-length':
176 178 self.length = int(h[1])
177 179 if (self.length is None and
178 180 saved_status[0] != common.HTTP_NOT_MODIFIED):
179 181 self._chunked = (not self.close_connection and
180 182 self.request_version == "HTTP/1.1")
181 183 if self._chunked:
182 184 self.send_header('Transfer-Encoding', 'chunked')
183 185 else:
184 186 self.send_header('Connection', 'close')
185 187 self.end_headers()
186 188 self.sent_headers = True
187 189
188 190 def _start_response(self, http_status, headers, exc_info=None):
189 191 code, msg = http_status.split(None, 1)
190 192 code = int(code)
191 193 self.saved_status = http_status
192 194 bad_headers = ('connection', 'transfer-encoding')
193 195 self.saved_headers = [h for h in headers
194 196 if h[0].lower() not in bad_headers]
195 197 return self._write
196 198
197 199 def _write(self, data):
198 200 if not self.saved_status:
199 201 raise AssertionError("data written before start_response() called")
200 202 elif not self.sent_headers:
201 203 self.send_headers()
202 204 if self.length is not None:
203 205 if len(data) > self.length:
204 206 raise AssertionError("Content-length header sent, but more "
205 207 "bytes than specified are being written.")
206 208 self.length = self.length - len(data)
207 209 elif self._chunked and data:
208 210 data = '%x\r\n%s\r\n' % (len(data), data)
209 211 self.wfile.write(data)
210 212 self.wfile.flush()
211 213
212 214 def _done(self):
213 215 if self._chunked:
214 216 self.wfile.write('0\r\n\r\n')
215 217 self.wfile.flush()
216 218
217 219 class _httprequesthandlerssl(_httprequesthandler):
218 220 """HTTPS handler based on Python's ssl module"""
219 221
220 222 url_scheme = 'https'
221 223
222 224 @staticmethod
223 225 def preparehttpserver(httpserver, ssl_cert):
224 226 try:
225 227 import ssl
226 228 ssl.wrap_socket
227 229 except ImportError:
228 230 raise error.Abort(_("SSL support is unavailable"))
229 231 httpserver.socket = ssl.wrap_socket(
230 232 httpserver.socket, server_side=True,
231 233 certfile=ssl_cert, ssl_version=ssl.PROTOCOL_TLSv1)
232 234
233 235 def setup(self):
234 236 self.connection = self.request
235 237 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
236 238 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
237 239
238 240 try:
239 241 import threading
240 242 threading.activeCount() # silence pyflakes and bypass demandimport
241 243 _mixin = SocketServer.ThreadingMixIn
242 244 except ImportError:
243 245 if util.safehasattr(os, "fork"):
244 246 _mixin = SocketServer.ForkingMixIn
245 247 else:
246 248 class _mixin(object):
247 249 pass
248 250
249 251 def openlog(opt, default):
250 252 if opt and opt != '-':
251 253 return open(opt, 'a')
252 254 return default
253 255
254 256 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
255 257
256 258 # SO_REUSEADDR has broken semantics on windows
257 259 if os.name == 'nt':
258 260 allow_reuse_address = 0
259 261
260 262 def __init__(self, ui, app, addr, handler, **kwargs):
261 263 BaseHTTPServer.HTTPServer.__init__(self, addr, handler, **kwargs)
262 264 self.daemon_threads = True
263 265 self.application = app
264 266
265 267 handler.preparehttpserver(self, ui.config('web', 'certificate'))
266 268
267 269 prefix = ui.config('web', 'prefix', '')
268 270 if prefix:
269 271 prefix = '/' + prefix.strip('/')
270 272 self.prefix = prefix
271 273
272 274 alog = openlog(ui.config('web', 'accesslog', '-'), sys.stdout)
273 275 elog = openlog(ui.config('web', 'errorlog', '-'), sys.stderr)
274 276 self.accesslog = alog
275 277 self.errorlog = elog
276 278
277 279 self.addr, self.port = self.socket.getsockname()[0:2]
278 280 self.fqaddr = socket.getfqdn(addr[0])
279 281
280 282 class IPv6HTTPServer(MercurialHTTPServer):
281 283 address_family = getattr(socket, 'AF_INET6', None)
282 284 def __init__(self, *args, **kwargs):
283 285 if self.address_family is None:
284 286 raise error.RepoError(_('IPv6 is not available on this system'))
285 287 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
286 288
287 289 def create_server(ui, app):
288 290
289 291 if ui.config('web', 'certificate'):
290 292 handler = _httprequesthandlerssl
291 293 else:
292 294 handler = _httprequesthandler
293 295
294 296 if ui.configbool('web', 'ipv6'):
295 297 cls = IPv6HTTPServer
296 298 else:
297 299 cls = MercurialHTTPServer
298 300
299 301 # ugly hack due to python issue5853 (for threaded use)
300 302 try:
301 303 import mimetypes
302 304 mimetypes.init()
303 305 except UnicodeDecodeError:
304 306 # Python 2.x's mimetypes module attempts to decode strings
305 307 # from Windows' ANSI APIs as ascii (fail), then re-encode them
306 308 # as ascii (clown fail), because the default Python Unicode
307 309 # codec is hardcoded as ascii.
308 310
309 311 sys.argv # unwrap demand-loader so that reload() works
310 312 reload(sys) # resurrect sys.setdefaultencoding()
311 313 oldenc = sys.getdefaultencoding()
312 314 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
313 315 mimetypes.init()
314 316 sys.setdefaultencoding(oldenc)
315 317
316 318 address = ui.config('web', 'address', '')
317 319 port = util.getport(ui.config('web', 'port', 8000))
318 320 try:
319 321 return cls(ui, app, (address, port), handler)
320 322 except socket.error as inst:
321 323 raise error.Abort(_("cannot start server at '%s:%d': %s")
322 324 % (address, port, inst.args[1]))
@@ -1,288 +1,289 b''
1 1 # httpconnection.py - urllib2 handler for new http support
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 # Copyright 2011 Google, Inc.
7 7 #
8 8 # This software may be used and distributed according to the terms of the
9 9 # GNU General Public License version 2 or any later version.
10 10
11 11 from __future__ import absolute_import
12 12
13 13 import logging
14 14 import os
15 15 import socket
16 import urllib
17 import urllib2
18 16
19 17 from .i18n import _
20 18 from . import (
21 19 httpclient,
22 20 sslutil,
23 21 util,
24 22 )
25 23
24 urlerr = util.urlerr
25 urlreq = util.urlreq
26
26 27 # moved here from url.py to avoid a cycle
27 28 class httpsendfile(object):
28 29 """This is a wrapper around the objects returned by python's "open".
29 30
30 31 Its purpose is to send file-like objects via HTTP.
31 32 It do however not define a __len__ attribute because the length
32 33 might be more than Py_ssize_t can handle.
33 34 """
34 35
35 36 def __init__(self, ui, *args, **kwargs):
36 37 self.ui = ui
37 38 self._data = open(*args, **kwargs)
38 39 self.seek = self._data.seek
39 40 self.close = self._data.close
40 41 self.write = self._data.write
41 42 self.length = os.fstat(self._data.fileno()).st_size
42 43 self._pos = 0
43 44 self._total = self.length // 1024 * 2
44 45
45 46 def read(self, *args, **kwargs):
46 47 try:
47 48 ret = self._data.read(*args, **kwargs)
48 49 except EOFError:
49 50 self.ui.progress(_('sending'), None)
50 51 self._pos += len(ret)
51 52 # We pass double the max for total because we currently have
52 53 # to send the bundle twice in the case of a server that
53 54 # requires authentication. Since we can't know until we try
54 55 # once whether authentication will be required, just lie to
55 56 # the user and maybe the push succeeds suddenly at 50%.
56 57 self.ui.progress(_('sending'), self._pos // 1024,
57 58 unit=_('kb'), total=self._total)
58 59 return ret
59 60
60 61 # moved here from url.py to avoid a cycle
61 62 def readauthforuri(ui, uri, user):
62 63 # Read configuration
63 64 config = dict()
64 65 for key, val in ui.configitems('auth'):
65 66 if '.' not in key:
66 67 ui.warn(_("ignoring invalid [auth] key '%s'\n") % key)
67 68 continue
68 69 group, setting = key.rsplit('.', 1)
69 70 gdict = config.setdefault(group, dict())
70 71 if setting in ('username', 'cert', 'key'):
71 72 val = util.expandpath(val)
72 73 gdict[setting] = val
73 74
74 75 # Find the best match
75 76 scheme, hostpath = uri.split('://', 1)
76 77 bestuser = None
77 78 bestlen = 0
78 79 bestauth = None
79 80 for group, auth in config.iteritems():
80 81 if user and user != auth.get('username', user):
81 82 # If a username was set in the URI, the entry username
82 83 # must either match it or be unset
83 84 continue
84 85 prefix = auth.get('prefix')
85 86 if not prefix:
86 87 continue
87 88 p = prefix.split('://', 1)
88 89 if len(p) > 1:
89 90 schemes, prefix = [p[0]], p[1]
90 91 else:
91 92 schemes = (auth.get('schemes') or 'https').split()
92 93 if (prefix == '*' or hostpath.startswith(prefix)) and \
93 94 (len(prefix) > bestlen or (len(prefix) == bestlen and \
94 95 not bestuser and 'username' in auth)) \
95 96 and scheme in schemes:
96 97 bestlen = len(prefix)
97 98 bestauth = group, auth
98 99 bestuser = auth.get('username')
99 100 if user and not bestuser:
100 101 auth['username'] = user
101 102 return bestauth
102 103
103 104 # Mercurial (at least until we can remove the old codepath) requires
104 105 # that the http response object be sufficiently file-like, so we
105 106 # provide a close() method here.
106 107 class HTTPResponse(httpclient.HTTPResponse):
107 108 def close(self):
108 109 pass
109 110
110 111 class HTTPConnection(httpclient.HTTPConnection):
111 112 response_class = HTTPResponse
112 113 def request(self, method, uri, body=None, headers=None):
113 114 if headers is None:
114 115 headers = {}
115 116 if isinstance(body, httpsendfile):
116 117 body.seek(0)
117 118 httpclient.HTTPConnection.request(self, method, uri, body=body,
118 119 headers=headers)
119 120
120 121
121 122 _configuredlogging = False
122 123 LOGFMT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'
123 124 # Subclass BOTH of these because otherwise urllib2 "helpfully"
124 125 # reinserts them since it notices we don't include any subclasses of
125 126 # them.
126 class http2handler(urllib2.HTTPHandler, urllib2.HTTPSHandler):
127 class http2handler(urlreq.httphandler, urlreq.httpshandler):
127 128 def __init__(self, ui, pwmgr):
128 129 global _configuredlogging
129 urllib2.AbstractHTTPHandler.__init__(self)
130 urlreq.abstracthttphandler.__init__(self)
130 131 self.ui = ui
131 132 self.pwmgr = pwmgr
132 133 self._connections = {}
133 134 # developer config: ui.http2debuglevel
134 135 loglevel = ui.config('ui', 'http2debuglevel', default=None)
135 136 if loglevel and not _configuredlogging:
136 137 _configuredlogging = True
137 138 logger = logging.getLogger('mercurial.httpclient')
138 139 logger.setLevel(getattr(logging, loglevel.upper()))
139 140 handler = logging.StreamHandler()
140 141 handler.setFormatter(logging.Formatter(LOGFMT))
141 142 logger.addHandler(handler)
142 143
143 144 def close_all(self):
144 145 """Close and remove all connection objects being kept for reuse."""
145 146 for openconns in self._connections.values():
146 147 for conn in openconns:
147 148 conn.close()
148 149 self._connections = {}
149 150
150 151 # shamelessly borrowed from urllib2.AbstractHTTPHandler
151 152 def do_open(self, http_class, req, use_ssl):
152 153 """Return an addinfourl object for the request, using http_class.
153 154
154 155 http_class must implement the HTTPConnection API from httplib.
155 156 The addinfourl return value is a file-like object. It also
156 157 has methods and attributes including:
157 158 - info(): return a mimetools.Message object for the headers
158 159 - geturl(): return the original request URL
159 160 - code: HTTP status code
160 161 """
161 162 # If using a proxy, the host returned by get_host() is
162 163 # actually the proxy. On Python 2.6.1, the real destination
163 164 # hostname is encoded in the URI in the urllib2 request
164 165 # object. On Python 2.6.5, it's stored in the _tunnel_host
165 166 # attribute which has no accessor.
166 167 tunhost = getattr(req, '_tunnel_host', None)
167 168 host = req.get_host()
168 169 if tunhost:
169 170 proxyhost = host
170 171 host = tunhost
171 172 elif req.has_proxy():
172 173 proxyhost = req.get_host()
173 174 host = req.get_selector().split('://', 1)[1].split('/', 1)[0]
174 175 else:
175 176 proxyhost = None
176 177
177 178 if proxyhost:
178 179 if ':' in proxyhost:
179 180 # Note: this means we'll explode if we try and use an
180 181 # IPv6 http proxy. This isn't a regression, so we
181 182 # won't worry about it for now.
182 183 proxyhost, proxyport = proxyhost.rsplit(':', 1)
183 184 else:
184 185 proxyport = 3128 # squid default
185 186 proxy = (proxyhost, proxyport)
186 187 else:
187 188 proxy = None
188 189
189 190 if not host:
190 raise urllib2.URLError('no host given')
191 raise urlerr.urlerror('no host given')
191 192
192 193 connkey = use_ssl, host, proxy
193 194 allconns = self._connections.get(connkey, [])
194 195 conns = [c for c in allconns if not c.busy()]
195 196 if conns:
196 197 h = conns[0]
197 198 else:
198 199 if allconns:
199 200 self.ui.debug('all connections for %s busy, making a new '
200 201 'one\n' % host)
201 202 timeout = None
202 203 if req.timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
203 204 timeout = req.timeout
204 205 h = http_class(host, timeout=timeout, proxy_hostport=proxy)
205 206 self._connections.setdefault(connkey, []).append(h)
206 207
207 208 headers = dict(req.headers)
208 209 headers.update(req.unredirected_hdrs)
209 210 headers = dict(
210 211 (name.title(), val) for name, val in headers.items())
211 212 try:
212 213 path = req.get_selector()
213 214 if '://' in path:
214 215 path = path.split('://', 1)[1].split('/', 1)[1]
215 216 if path[0] != '/':
216 217 path = '/' + path
217 218 h.request(req.get_method(), path, req.data, headers)
218 219 r = h.getresponse()
219 220 except socket.error as err: # XXX what error?
220 raise urllib2.URLError(err)
221 raise urlerr.urlerror(err)
221 222
222 223 # Pick apart the HTTPResponse object to get the addinfourl
223 224 # object initialized properly.
224 225 r.recv = r.read
225 226
226 resp = urllib.addinfourl(r, r.headers, req.get_full_url())
227 resp = urlreq.addinfourl(r, r.headers, req.get_full_url())
227 228 resp.code = r.status
228 229 resp.msg = r.reason
229 230 return resp
230 231
231 232 # httplib always uses the given host/port as the socket connect
232 233 # target, and then allows full URIs in the request path, which it
233 234 # then observes and treats as a signal to do proxying instead.
234 235 def http_open(self, req):
235 236 if req.get_full_url().startswith('https'):
236 237 return self.https_open(req)
237 238 def makehttpcon(*args, **kwargs):
238 239 k2 = dict(kwargs)
239 240 k2['use_ssl'] = False
240 241 return HTTPConnection(*args, **k2)
241 242 return self.do_open(makehttpcon, req, False)
242 243
243 244 def https_open(self, req):
244 245 # req.get_full_url() does not contain credentials and we may
245 246 # need them to match the certificates.
246 247 url = req.get_full_url()
247 248 user, password = self.pwmgr.find_stored_password(url)
248 249 res = readauthforuri(self.ui, url, user)
249 250 if res:
250 251 group, auth = res
251 252 self.auth = auth
252 253 self.ui.debug("using auth.%s.* for authentication\n" % group)
253 254 else:
254 255 self.auth = None
255 256 return self.do_open(self._makesslconnection, req, True)
256 257
257 258 def _makesslconnection(self, host, port=443, *args, **kwargs):
258 259 keyfile = None
259 260 certfile = None
260 261
261 262 if args: # key_file
262 263 keyfile = args.pop(0)
263 264 if args: # cert_file
264 265 certfile = args.pop(0)
265 266
266 267 # if the user has specified different key/cert files in
267 268 # hgrc, we prefer these
268 269 if self.auth and 'key' in self.auth and 'cert' in self.auth:
269 270 keyfile = self.auth['key']
270 271 certfile = self.auth['cert']
271 272
272 273 # let host port take precedence
273 274 if ':' in host and '[' not in host or ']:' in host:
274 275 host, port = host.rsplit(':', 1)
275 276 port = int(port)
276 277 if '[' in host:
277 278 host = host[1:-1]
278 279
279 280 kwargs['keyfile'] = keyfile
280 281 kwargs['certfile'] = certfile
281 282
282 283 kwargs.update(sslutil.sslkwargs(self.ui, host))
283 284
284 285 con = HTTPConnection(host, port, use_ssl=True,
285 286 ssl_wrap_socket=sslutil.wrapsocket,
286 287 ssl_validator=sslutil.validator(self.ui, host),
287 288 **kwargs)
288 289 return con
@@ -1,307 +1,308 b''
1 1 # httppeer.py - HTTP repository proxy classes for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12 import httplib
13 13 import os
14 14 import socket
15 15 import tempfile
16 import urllib
17 import urllib2
18 16 import zlib
19 17
20 18 from .i18n import _
21 19 from .node import nullid
22 20 from . import (
23 21 bundle2,
24 22 error,
25 23 httpconnection,
26 24 statichttprepo,
27 25 url,
28 26 util,
29 27 wireproto,
30 28 )
31 29
30 urlerr = util.urlerr
31 urlreq = util.urlreq
32
32 33 def zgenerator(f):
33 34 zd = zlib.decompressobj()
34 35 try:
35 36 for chunk in util.filechunkiter(f):
36 37 while chunk:
37 38 yield zd.decompress(chunk, 2**18)
38 39 chunk = zd.unconsumed_tail
39 40 except httplib.HTTPException:
40 41 raise IOError(None, _('connection ended unexpectedly'))
41 42 yield zd.flush()
42 43
43 44 class httppeer(wireproto.wirepeer):
44 45 def __init__(self, ui, path):
45 46 self.path = path
46 47 self.caps = None
47 48 self.handler = None
48 49 self.urlopener = None
49 50 self.requestbuilder = None
50 51 u = util.url(path)
51 52 if u.query or u.fragment:
52 53 raise error.Abort(_('unsupported URL component: "%s"') %
53 54 (u.query or u.fragment))
54 55
55 56 # urllib cannot handle URLs with embedded user or passwd
56 57 self._url, authinfo = u.authinfo()
57 58
58 59 self.ui = ui
59 60 self.ui.debug('using %s\n' % self._url)
60 61
61 62 self.urlopener = url.opener(ui, authinfo)
62 self.requestbuilder = urllib2.Request
63 self.requestbuilder = urlreq.request
63 64
64 65 def __del__(self):
65 66 if self.urlopener:
66 67 for h in self.urlopener.handlers:
67 68 h.close()
68 69 getattr(h, "close_all", lambda : None)()
69 70
70 71 def url(self):
71 72 return self.path
72 73
73 74 # look up capabilities only when needed
74 75
75 76 def _fetchcaps(self):
76 77 self.caps = set(self._call('capabilities').split())
77 78
78 79 def _capabilities(self):
79 80 if self.caps is None:
80 81 try:
81 82 self._fetchcaps()
82 83 except error.RepoError:
83 84 self.caps = set()
84 85 self.ui.debug('capabilities: %s\n' %
85 86 (' '.join(self.caps or ['none'])))
86 87 return self.caps
87 88
88 89 def lock(self):
89 90 raise error.Abort(_('operation not supported over http'))
90 91
91 92 def _callstream(self, cmd, **args):
92 93 if cmd == 'pushkey':
93 94 args['data'] = ''
94 95 data = args.pop('data', None)
95 96 headers = args.pop('headers', {})
96 97
97 98 self.ui.debug("sending %s command\n" % cmd)
98 99 q = [('cmd', cmd)]
99 100 headersize = 0
100 101 # Important: don't use self.capable() here or else you end up
101 102 # with infinite recursion when trying to look up capabilities
102 103 # for the first time.
103 104 postargsok = self.caps is not None and 'httppostargs' in self.caps
104 105 # TODO: support for httppostargs when data is a file-like
105 106 # object rather than a basestring
106 107 canmungedata = not data or isinstance(data, basestring)
107 108 if postargsok and canmungedata:
108 strargs = urllib.urlencode(sorted(args.items()))
109 strargs = urlreq.urlencode(sorted(args.items()))
109 110 if strargs:
110 111 if not data:
111 112 data = strargs
112 113 elif isinstance(data, basestring):
113 114 data = strargs + data
114 115 headers['X-HgArgs-Post'] = len(strargs)
115 116 else:
116 117 if len(args) > 0:
117 118 httpheader = self.capable('httpheader')
118 119 if httpheader:
119 120 headersize = int(httpheader.split(',', 1)[0])
120 121 if headersize > 0:
121 122 # The headers can typically carry more data than the URL.
122 encargs = urllib.urlencode(sorted(args.items()))
123 encargs = urlreq.urlencode(sorted(args.items()))
123 124 headerfmt = 'X-HgArg-%s'
124 125 contentlen = headersize - len(headerfmt % '000' + ': \r\n')
125 126 headernum = 0
126 127 varyheaders = []
127 128 for i in xrange(0, len(encargs), contentlen):
128 129 headernum += 1
129 130 header = headerfmt % str(headernum)
130 131 headers[header] = encargs[i:i + contentlen]
131 132 varyheaders.append(header)
132 133 headers['Vary'] = ','.join(varyheaders)
133 134 else:
134 135 q += sorted(args.items())
135 qs = '?%s' % urllib.urlencode(q)
136 qs = '?%s' % urlreq.urlencode(q)
136 137 cu = "%s%s" % (self._url, qs)
137 138 size = 0
138 139 if util.safehasattr(data, 'length'):
139 140 size = data.length
140 141 elif data is not None:
141 142 size = len(data)
142 143 if size and self.ui.configbool('ui', 'usehttp2', False):
143 144 headers['Expect'] = '100-Continue'
144 145 headers['X-HgHttp2'] = '1'
145 146 if data is not None and 'Content-Type' not in headers:
146 147 headers['Content-Type'] = 'application/mercurial-0.1'
147 148 req = self.requestbuilder(cu, data, headers)
148 149 if data is not None:
149 150 self.ui.debug("sending %s bytes\n" % size)
150 151 req.add_unredirected_header('Content-Length', '%d' % size)
151 152 try:
152 153 resp = self.urlopener.open(req)
153 except urllib2.HTTPError as inst:
154 except urlerr.httperror as inst:
154 155 if inst.code == 401:
155 156 raise error.Abort(_('authorization failed'))
156 157 raise
157 158 except httplib.HTTPException as inst:
158 159 self.ui.debug('http error while sending %s command\n' % cmd)
159 160 self.ui.traceback()
160 161 raise IOError(None, inst)
161 162 except IndexError:
162 163 # this only happens with Python 2.3, later versions raise URLError
163 164 raise error.Abort(_('http error, possibly caused by proxy setting'))
164 165 # record the url we got redirected to
165 166 resp_url = resp.geturl()
166 167 if resp_url.endswith(qs):
167 168 resp_url = resp_url[:-len(qs)]
168 169 if self._url.rstrip('/') != resp_url.rstrip('/'):
169 170 if not self.ui.quiet:
170 171 self.ui.warn(_('real URL is %s\n') % resp_url)
171 172 self._url = resp_url
172 173 try:
173 174 proto = resp.getheader('content-type')
174 175 except AttributeError:
175 176 proto = resp.headers.get('content-type', '')
176 177
177 178 safeurl = util.hidepassword(self._url)
178 179 if proto.startswith('application/hg-error'):
179 180 raise error.OutOfBandError(resp.read())
180 181 # accept old "text/plain" and "application/hg-changegroup" for now
181 182 if not (proto.startswith('application/mercurial-') or
182 183 (proto.startswith('text/plain')
183 184 and not resp.headers.get('content-length')) or
184 185 proto.startswith('application/hg-changegroup')):
185 186 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
186 187 raise error.RepoError(
187 188 _("'%s' does not appear to be an hg repository:\n"
188 189 "---%%<--- (%s)\n%s\n---%%<---\n")
189 190 % (safeurl, proto or 'no content-type', resp.read(1024)))
190 191
191 192 if proto.startswith('application/mercurial-'):
192 193 try:
193 194 version = proto.split('-', 1)[1]
194 195 version_info = tuple([int(n) for n in version.split('.')])
195 196 except ValueError:
196 197 raise error.RepoError(_("'%s' sent a broken Content-Type "
197 198 "header (%s)") % (safeurl, proto))
198 199 if version_info > (0, 1):
199 200 raise error.RepoError(_("'%s' uses newer protocol %s") %
200 201 (safeurl, version))
201 202
202 203 return resp
203 204
204 205 def _call(self, cmd, **args):
205 206 fp = self._callstream(cmd, **args)
206 207 try:
207 208 return fp.read()
208 209 finally:
209 210 # if using keepalive, allow connection to be reused
210 211 fp.close()
211 212
212 213 def _callpush(self, cmd, cg, **args):
213 214 # have to stream bundle to a temp file because we do not have
214 215 # http 1.1 chunked transfer.
215 216
216 217 types = self.capable('unbundle')
217 218 try:
218 219 types = types.split(',')
219 220 except AttributeError:
220 221 # servers older than d1b16a746db6 will send 'unbundle' as a
221 222 # boolean capability. They only support headerless/uncompressed
222 223 # bundles.
223 224 types = [""]
224 225 for x in types:
225 226 if x in bundle2.bundletypes:
226 227 type = x
227 228 break
228 229
229 230 tempname = bundle2.writebundle(self.ui, cg, None, type)
230 231 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
231 232 headers = {'Content-Type': 'application/mercurial-0.1'}
232 233
233 234 try:
234 235 r = self._call(cmd, data=fp, headers=headers, **args)
235 236 vals = r.split('\n', 1)
236 237 if len(vals) < 2:
237 238 raise error.ResponseError(_("unexpected response:"), r)
238 239 return vals
239 240 except socket.error as err:
240 241 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
241 242 raise error.Abort(_('push failed: %s') % err.args[1])
242 243 raise error.Abort(err.args[1])
243 244 finally:
244 245 fp.close()
245 246 os.unlink(tempname)
246 247
247 248 def _calltwowaystream(self, cmd, fp, **args):
248 249 fh = None
249 250 fp_ = None
250 251 filename = None
251 252 try:
252 253 # dump bundle to disk
253 254 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
254 255 fh = os.fdopen(fd, "wb")
255 256 d = fp.read(4096)
256 257 while d:
257 258 fh.write(d)
258 259 d = fp.read(4096)
259 260 fh.close()
260 261 # start http push
261 262 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
262 263 headers = {'Content-Type': 'application/mercurial-0.1'}
263 264 return self._callstream(cmd, data=fp_, headers=headers, **args)
264 265 finally:
265 266 if fp_ is not None:
266 267 fp_.close()
267 268 if fh is not None:
268 269 fh.close()
269 270 os.unlink(filename)
270 271
271 272 def _callcompressable(self, cmd, **args):
272 273 stream = self._callstream(cmd, **args)
273 274 return util.chunkbuffer(zgenerator(stream))
274 275
275 276 def _abort(self, exception):
276 277 raise exception
277 278
278 279 class httpspeer(httppeer):
279 280 def __init__(self, ui, path):
280 281 if not url.has_https:
281 282 raise error.Abort(_('Python support for SSL and HTTPS '
282 283 'is not installed'))
283 284 httppeer.__init__(self, ui, path)
284 285
285 286 def instance(ui, path, create):
286 287 if create:
287 288 raise error.Abort(_('cannot create new http repository'))
288 289 try:
289 290 if path.startswith('https:'):
290 291 inst = httpspeer(ui, path)
291 292 else:
292 293 inst = httppeer(ui, path)
293 294 try:
294 295 # Try to do useful work when checking compatibility.
295 296 # Usually saves a roundtrip since we want the caps anyway.
296 297 inst._fetchcaps()
297 298 except error.RepoError:
298 299 # No luck, try older compatibility check.
299 300 inst.between([(nullid, nullid)])
300 301 return inst
301 302 except error.RepoError as httpexception:
302 303 try:
303 304 r = statichttprepo.instance(ui, "static-" + path, create)
304 305 ui.note('(falling back to static-http)\n')
305 306 return r
306 307 except error.RepoError:
307 308 raise httpexception # use the original http RepoError instead
@@ -1,753 +1,759 b''
1 1 # This library is free software; you can redistribute it and/or
2 2 # modify it under the terms of the GNU Lesser General Public
3 3 # License as published by the Free Software Foundation; either
4 4 # version 2.1 of the License, or (at your option) any later version.
5 5 #
6 6 # This library is distributed in the hope that it will be useful,
7 7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 9 # Lesser General Public License for more details.
10 10 #
11 11 # You should have received a copy of the GNU Lesser General Public
12 12 # License along with this library; if not, see
13 13 # <http://www.gnu.org/licenses/>.
14 14
15 15 # This file is part of urlgrabber, a high-level cross-protocol url-grabber
16 16 # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
17 17
18 18 # Modified by Benoit Boissinot:
19 19 # - fix for digest auth (inspired from urllib2.py @ Python v2.4)
20 20 # Modified by Dirkjan Ochtman:
21 21 # - import md5 function from a local util module
22 22 # Modified by Augie Fackler:
23 23 # - add safesend method and use it to prevent broken pipe errors
24 24 # on large POST requests
25 25
26 26 """An HTTP handler for urllib2 that supports HTTP 1.1 and keepalive.
27 27
28 28 >>> import urllib2
29 29 >>> from keepalive import HTTPHandler
30 30 >>> keepalive_handler = HTTPHandler()
31 >>> opener = urllib2.build_opener(keepalive_handler)
32 >>> urllib2.install_opener(opener)
31 >>> opener = urlreq.buildopener(keepalive_handler)
32 >>> urlreq.installopener(opener)
33 33 >>>
34 >>> fo = urllib2.urlopen('http://www.python.org')
34 >>> fo = urlreq.urlopen('http://www.python.org')
35 35
36 36 If a connection to a given host is requested, and all of the existing
37 37 connections are still in use, another connection will be opened. If
38 38 the handler tries to use an existing connection but it fails in some
39 39 way, it will be closed and removed from the pool.
40 40
41 41 To remove the handler, simply re-run build_opener with no arguments, and
42 42 install that opener.
43 43
44 44 You can explicitly close connections by using the close_connection()
45 45 method of the returned file-like object (described below) or you can
46 46 use the handler methods:
47 47
48 48 close_connection(host)
49 49 close_all()
50 50 open_connections()
51 51
52 52 NOTE: using the close_connection and close_all methods of the handler
53 53 should be done with care when using multiple threads.
54 54 * there is nothing that prevents another thread from creating new
55 55 connections immediately after connections are closed
56 56 * no checks are done to prevent in-use connections from being closed
57 57
58 58 >>> keepalive_handler.close_all()
59 59
60 60 EXTRA ATTRIBUTES AND METHODS
61 61
62 62 Upon a status of 200, the object returned has a few additional
63 63 attributes and methods, which should not be used if you want to
64 64 remain consistent with the normal urllib2-returned objects:
65 65
66 66 close_connection() - close the connection to the host
67 67 readlines() - you know, readlines()
68 68 status - the return status (i.e. 404)
69 69 reason - english translation of status (i.e. 'File not found')
70 70
71 71 If you want the best of both worlds, use this inside an
72 72 AttributeError-catching try:
73 73
74 74 >>> try: status = fo.status
75 75 >>> except AttributeError: status = None
76 76
77 77 Unfortunately, these are ONLY there if status == 200, so it's not
78 78 easy to distinguish between non-200 responses. The reason is that
79 79 urllib2 tries to do clever things with error codes 301, 302, 401,
80 80 and 407, and it wraps the object upon return.
81 81
82 82 For python versions earlier than 2.4, you can avoid this fancy error
83 83 handling by setting the module-level global HANDLE_ERRORS to zero.
84 84 You see, prior to 2.4, it's the HTTP Handler's job to determine what
85 85 to handle specially, and what to just pass up. HANDLE_ERRORS == 0
86 86 means "pass everything up". In python 2.4, however, this job no
87 87 longer belongs to the HTTP Handler and is now done by a NEW handler,
88 88 HTTPErrorProcessor. Here's the bottom line:
89 89
90 90 python version < 2.4
91 91 HANDLE_ERRORS == 1 (default) pass up 200, treat the rest as
92 92 errors
93 93 HANDLE_ERRORS == 0 pass everything up, error processing is
94 94 left to the calling code
95 95 python version >= 2.4
96 96 HANDLE_ERRORS == 1 pass up 200, treat the rest as errors
97 97 HANDLE_ERRORS == 0 (default) pass everything up, let the
98 98 other handlers (specifically,
99 99 HTTPErrorProcessor) decide what to do
100 100
101 101 In practice, setting the variable either way makes little difference
102 102 in python 2.4, so for the most consistent behavior across versions,
103 103 you probably just want to use the defaults, which will give you
104 104 exceptions on errors.
105 105
106 106 """
107 107
108 108 # $Id: keepalive.py,v 1.14 2006/04/04 21:00:32 mstenner Exp $
109 109
110 110 from __future__ import absolute_import, print_function
111 111
112 112 import errno
113 113 import httplib
114 114 import socket
115 115 import sys
116 116 import thread
117 import urllib2
117
118 from . import (
119 util,
120 )
121
122 urlerr = util.urlerr
123 urlreq = util.urlreq
118 124
119 125 DEBUG = None
120 126
121 127 if sys.version_info < (2, 4):
122 128 HANDLE_ERRORS = 1
123 129 else: HANDLE_ERRORS = 0
124 130
125 131 class ConnectionManager(object):
126 132 """
127 133 The connection manager must be able to:
128 134 * keep track of all existing
129 135 """
130 136 def __init__(self):
131 137 self._lock = thread.allocate_lock()
132 138 self._hostmap = {} # map hosts to a list of connections
133 139 self._connmap = {} # map connections to host
134 140 self._readymap = {} # map connection to ready state
135 141
136 142 def add(self, host, connection, ready):
137 143 self._lock.acquire()
138 144 try:
139 145 if host not in self._hostmap:
140 146 self._hostmap[host] = []
141 147 self._hostmap[host].append(connection)
142 148 self._connmap[connection] = host
143 149 self._readymap[connection] = ready
144 150 finally:
145 151 self._lock.release()
146 152
147 153 def remove(self, connection):
148 154 self._lock.acquire()
149 155 try:
150 156 try:
151 157 host = self._connmap[connection]
152 158 except KeyError:
153 159 pass
154 160 else:
155 161 del self._connmap[connection]
156 162 del self._readymap[connection]
157 163 self._hostmap[host].remove(connection)
158 164 if not self._hostmap[host]: del self._hostmap[host]
159 165 finally:
160 166 self._lock.release()
161 167
162 168 def set_ready(self, connection, ready):
163 169 try:
164 170 self._readymap[connection] = ready
165 171 except KeyError:
166 172 pass
167 173
168 174 def get_ready_conn(self, host):
169 175 conn = None
170 176 self._lock.acquire()
171 177 try:
172 178 if host in self._hostmap:
173 179 for c in self._hostmap[host]:
174 180 if self._readymap[c]:
175 181 self._readymap[c] = 0
176 182 conn = c
177 183 break
178 184 finally:
179 185 self._lock.release()
180 186 return conn
181 187
182 188 def get_all(self, host=None):
183 189 if host:
184 190 return list(self._hostmap.get(host, []))
185 191 else:
186 192 return dict(self._hostmap)
187 193
188 194 class KeepAliveHandler(object):
189 195 def __init__(self):
190 196 self._cm = ConnectionManager()
191 197
192 198 #### Connection Management
193 199 def open_connections(self):
194 200 """return a list of connected hosts and the number of connections
195 201 to each. [('foo.com:80', 2), ('bar.org', 1)]"""
196 202 return [(host, len(li)) for (host, li) in self._cm.get_all().items()]
197 203
198 204 def close_connection(self, host):
199 205 """close connection(s) to <host>
200 206 host is the host:port spec, as in 'www.cnn.com:8080' as passed in.
201 207 no error occurs if there is no connection to that host."""
202 208 for h in self._cm.get_all(host):
203 209 self._cm.remove(h)
204 210 h.close()
205 211
206 212 def close_all(self):
207 213 """close all open connections"""
208 214 for host, conns in self._cm.get_all().iteritems():
209 215 for h in conns:
210 216 self._cm.remove(h)
211 217 h.close()
212 218
213 219 def _request_closed(self, request, host, connection):
214 220 """tells us that this request is now closed and that the
215 221 connection is ready for another request"""
216 222 self._cm.set_ready(connection, 1)
217 223
218 224 def _remove_connection(self, host, connection, close=0):
219 225 if close:
220 226 connection.close()
221 227 self._cm.remove(connection)
222 228
223 229 #### Transaction Execution
224 230 def http_open(self, req):
225 231 return self.do_open(HTTPConnection, req)
226 232
227 233 def do_open(self, http_class, req):
228 234 host = req.get_host()
229 235 if not host:
230 raise urllib2.URLError('no host given')
236 raise urlerr.urlerror('no host given')
231 237
232 238 try:
233 239 h = self._cm.get_ready_conn(host)
234 240 while h:
235 241 r = self._reuse_connection(h, req, host)
236 242
237 243 # if this response is non-None, then it worked and we're
238 244 # done. Break out, skipping the else block.
239 245 if r:
240 246 break
241 247
242 248 # connection is bad - possibly closed by server
243 249 # discard it and ask for the next free connection
244 250 h.close()
245 251 self._cm.remove(h)
246 252 h = self._cm.get_ready_conn(host)
247 253 else:
248 254 # no (working) free connections were found. Create a new one.
249 255 h = http_class(host)
250 256 if DEBUG:
251 257 DEBUG.info("creating new connection to %s (%d)",
252 258 host, id(h))
253 259 self._cm.add(host, h, 0)
254 260 self._start_transaction(h, req)
255 261 r = h.getresponse()
256 262 except (socket.error, httplib.HTTPException) as err:
257 raise urllib2.URLError(err)
263 raise urlerr.urlerror(err)
258 264
259 265 # if not a persistent connection, don't try to reuse it
260 266 if r.will_close:
261 267 self._cm.remove(h)
262 268
263 269 if DEBUG:
264 270 DEBUG.info("STATUS: %s, %s", r.status, r.reason)
265 271 r._handler = self
266 272 r._host = host
267 273 r._url = req.get_full_url()
268 274 r._connection = h
269 275 r.code = r.status
270 276 r.headers = r.msg
271 277 r.msg = r.reason
272 278
273 279 if r.status == 200 or not HANDLE_ERRORS:
274 280 return r
275 281 else:
276 282 return self.parent.error('http', req, r,
277 283 r.status, r.msg, r.headers)
278 284
279 285 def _reuse_connection(self, h, req, host):
280 286 """start the transaction with a re-used connection
281 287 return a response object (r) upon success or None on failure.
282 288 This DOES not close or remove bad connections in cases where
283 289 it returns. However, if an unexpected exception occurs, it
284 290 will close and remove the connection before re-raising.
285 291 """
286 292 try:
287 293 self._start_transaction(h, req)
288 294 r = h.getresponse()
289 295 # note: just because we got something back doesn't mean it
290 296 # worked. We'll check the version below, too.
291 297 except (socket.error, httplib.HTTPException):
292 298 r = None
293 299 except: # re-raises
294 300 # adding this block just in case we've missed
295 301 # something we will still raise the exception, but
296 302 # lets try and close the connection and remove it
297 303 # first. We previously got into a nasty loop
298 304 # where an exception was uncaught, and so the
299 305 # connection stayed open. On the next try, the
300 306 # same exception was raised, etc. The trade-off is
301 307 # that it's now possible this call will raise
302 308 # a DIFFERENT exception
303 309 if DEBUG:
304 310 DEBUG.error("unexpected exception - closing "
305 311 "connection to %s (%d)", host, id(h))
306 312 self._cm.remove(h)
307 313 h.close()
308 314 raise
309 315
310 316 if r is None or r.version == 9:
311 317 # httplib falls back to assuming HTTP 0.9 if it gets a
312 318 # bad header back. This is most likely to happen if
313 319 # the socket has been closed by the server since we
314 320 # last used the connection.
315 321 if DEBUG:
316 322 DEBUG.info("failed to re-use connection to %s (%d)",
317 323 host, id(h))
318 324 r = None
319 325 else:
320 326 if DEBUG:
321 327 DEBUG.info("re-using connection to %s (%d)", host, id(h))
322 328
323 329 return r
324 330
325 331 def _start_transaction(self, h, req):
326 332 # What follows mostly reimplements HTTPConnection.request()
327 333 # except it adds self.parent.addheaders in the mix.
328 334 headers = req.headers.copy()
329 335 if sys.version_info >= (2, 4):
330 336 headers.update(req.unredirected_hdrs)
331 337 headers.update(self.parent.addheaders)
332 338 headers = dict((n.lower(), v) for n, v in headers.items())
333 339 skipheaders = {}
334 340 for n in ('host', 'accept-encoding'):
335 341 if n in headers:
336 342 skipheaders['skip_' + n.replace('-', '_')] = 1
337 343 try:
338 344 if req.has_data():
339 345 data = req.get_data()
340 346 h.putrequest('POST', req.get_selector(), **skipheaders)
341 347 if 'content-type' not in headers:
342 348 h.putheader('Content-type',
343 349 'application/x-www-form-urlencoded')
344 350 if 'content-length' not in headers:
345 351 h.putheader('Content-length', '%d' % len(data))
346 352 else:
347 353 h.putrequest('GET', req.get_selector(), **skipheaders)
348 354 except socket.error as err:
349 raise urllib2.URLError(err)
355 raise urlerr.urlerror(err)
350 356 for k, v in headers.items():
351 357 h.putheader(k, v)
352 358 h.endheaders()
353 359 if req.has_data():
354 360 h.send(data)
355 361
356 class HTTPHandler(KeepAliveHandler, urllib2.HTTPHandler):
362 class HTTPHandler(KeepAliveHandler, urlreq.httphandler):
357 363 pass
358 364
359 365 class HTTPResponse(httplib.HTTPResponse):
360 366 # we need to subclass HTTPResponse in order to
361 367 # 1) add readline() and readlines() methods
362 368 # 2) add close_connection() methods
363 369 # 3) add info() and geturl() methods
364 370
365 371 # in order to add readline(), read must be modified to deal with a
366 372 # buffer. example: readline must read a buffer and then spit back
367 373 # one line at a time. The only real alternative is to read one
368 374 # BYTE at a time (ick). Once something has been read, it can't be
369 375 # put back (ok, maybe it can, but that's even uglier than this),
370 376 # so if you THEN do a normal read, you must first take stuff from
371 377 # the buffer.
372 378
373 379 # the read method wraps the original to accommodate buffering,
374 380 # although read() never adds to the buffer.
375 381 # Both readline and readlines have been stolen with almost no
376 382 # modification from socket.py
377 383
378 384
379 385 def __init__(self, sock, debuglevel=0, strict=0, method=None):
380 386 httplib.HTTPResponse.__init__(self, sock, debuglevel, method)
381 387 self.fileno = sock.fileno
382 388 self.code = None
383 389 self._rbuf = ''
384 390 self._rbufsize = 8096
385 391 self._handler = None # inserted by the handler later
386 392 self._host = None # (same)
387 393 self._url = None # (same)
388 394 self._connection = None # (same)
389 395
390 396 _raw_read = httplib.HTTPResponse.read
391 397
392 398 def close(self):
393 399 if self.fp:
394 400 self.fp.close()
395 401 self.fp = None
396 402 if self._handler:
397 403 self._handler._request_closed(self, self._host,
398 404 self._connection)
399 405
400 406 def close_connection(self):
401 407 self._handler._remove_connection(self._host, self._connection, close=1)
402 408 self.close()
403 409
404 410 def info(self):
405 411 return self.headers
406 412
407 413 def geturl(self):
408 414 return self._url
409 415
410 416 def read(self, amt=None):
411 417 # the _rbuf test is only in this first if for speed. It's not
412 418 # logically necessary
413 419 if self._rbuf and not amt is None:
414 420 L = len(self._rbuf)
415 421 if amt > L:
416 422 amt -= L
417 423 else:
418 424 s = self._rbuf[:amt]
419 425 self._rbuf = self._rbuf[amt:]
420 426 return s
421 427
422 428 s = self._rbuf + self._raw_read(amt)
423 429 self._rbuf = ''
424 430 return s
425 431
426 432 # stolen from Python SVN #68532 to fix issue1088
427 433 def _read_chunked(self, amt):
428 434 chunk_left = self.chunk_left
429 435 value = ''
430 436
431 437 # XXX This accumulates chunks by repeated string concatenation,
432 438 # which is not efficient as the number or size of chunks gets big.
433 439 while True:
434 440 if chunk_left is None:
435 441 line = self.fp.readline()
436 442 i = line.find(';')
437 443 if i >= 0:
438 444 line = line[:i] # strip chunk-extensions
439 445 try:
440 446 chunk_left = int(line, 16)
441 447 except ValueError:
442 448 # close the connection as protocol synchronization is
443 449 # probably lost
444 450 self.close()
445 451 raise httplib.IncompleteRead(value)
446 452 if chunk_left == 0:
447 453 break
448 454 if amt is None:
449 455 value += self._safe_read(chunk_left)
450 456 elif amt < chunk_left:
451 457 value += self._safe_read(amt)
452 458 self.chunk_left = chunk_left - amt
453 459 return value
454 460 elif amt == chunk_left:
455 461 value += self._safe_read(amt)
456 462 self._safe_read(2) # toss the CRLF at the end of the chunk
457 463 self.chunk_left = None
458 464 return value
459 465 else:
460 466 value += self._safe_read(chunk_left)
461 467 amt -= chunk_left
462 468
463 469 # we read the whole chunk, get another
464 470 self._safe_read(2) # toss the CRLF at the end of the chunk
465 471 chunk_left = None
466 472
467 473 # read and discard trailer up to the CRLF terminator
468 474 ### note: we shouldn't have any trailers!
469 475 while True:
470 476 line = self.fp.readline()
471 477 if not line:
472 478 # a vanishingly small number of sites EOF without
473 479 # sending the trailer
474 480 break
475 481 if line == '\r\n':
476 482 break
477 483
478 484 # we read everything; close the "file"
479 485 self.close()
480 486
481 487 return value
482 488
483 489 def readline(self, limit=-1):
484 490 i = self._rbuf.find('\n')
485 491 while i < 0 and not (0 < limit <= len(self._rbuf)):
486 492 new = self._raw_read(self._rbufsize)
487 493 if not new:
488 494 break
489 495 i = new.find('\n')
490 496 if i >= 0:
491 497 i = i + len(self._rbuf)
492 498 self._rbuf = self._rbuf + new
493 499 if i < 0:
494 500 i = len(self._rbuf)
495 501 else:
496 502 i = i + 1
497 503 if 0 <= limit < len(self._rbuf):
498 504 i = limit
499 505 data, self._rbuf = self._rbuf[:i], self._rbuf[i:]
500 506 return data
501 507
502 508 def readlines(self, sizehint=0):
503 509 total = 0
504 510 list = []
505 511 while True:
506 512 line = self.readline()
507 513 if not line:
508 514 break
509 515 list.append(line)
510 516 total += len(line)
511 517 if sizehint and total >= sizehint:
512 518 break
513 519 return list
514 520
515 521 def safesend(self, str):
516 522 """Send `str' to the server.
517 523
518 524 Shamelessly ripped off from httplib to patch a bad behavior.
519 525 """
520 526 # _broken_pipe_resp is an attribute we set in this function
521 527 # if the socket is closed while we're sending data but
522 528 # the server sent us a response before hanging up.
523 529 # In that case, we want to pretend to send the rest of the
524 530 # outgoing data, and then let the user use getresponse()
525 531 # (which we wrap) to get this last response before
526 532 # opening a new socket.
527 533 if getattr(self, '_broken_pipe_resp', None) is not None:
528 534 return
529 535
530 536 if self.sock is None:
531 537 if self.auto_open:
532 538 self.connect()
533 539 else:
534 540 raise httplib.NotConnected
535 541
536 542 # send the data to the server. if we get a broken pipe, then close
537 543 # the socket. we want to reconnect when somebody tries to send again.
538 544 #
539 545 # NOTE: we DO propagate the error, though, because we cannot simply
540 546 # ignore the error... the caller will know if they can retry.
541 547 if self.debuglevel > 0:
542 548 print("send:", repr(str))
543 549 try:
544 550 blocksize = 8192
545 551 read = getattr(str, 'read', None)
546 552 if read is not None:
547 553 if self.debuglevel > 0:
548 554 print("sending a read()able")
549 555 data = read(blocksize)
550 556 while data:
551 557 self.sock.sendall(data)
552 558 data = read(blocksize)
553 559 else:
554 560 self.sock.sendall(str)
555 561 except socket.error as v:
556 562 reraise = True
557 563 if v[0] == errno.EPIPE: # Broken pipe
558 564 if self._HTTPConnection__state == httplib._CS_REQ_SENT:
559 565 self._broken_pipe_resp = None
560 566 self._broken_pipe_resp = self.getresponse()
561 567 reraise = False
562 568 self.close()
563 569 if reraise:
564 570 raise
565 571
566 572 def wrapgetresponse(cls):
567 573 """Wraps getresponse in cls with a broken-pipe sane version.
568 574 """
569 575 def safegetresponse(self):
570 576 # In safesend() we might set the _broken_pipe_resp
571 577 # attribute, in which case the socket has already
572 578 # been closed and we just need to give them the response
573 579 # back. Otherwise, we use the normal response path.
574 580 r = getattr(self, '_broken_pipe_resp', None)
575 581 if r is not None:
576 582 return r
577 583 return cls.getresponse(self)
578 584 safegetresponse.__doc__ = cls.getresponse.__doc__
579 585 return safegetresponse
580 586
581 587 class HTTPConnection(httplib.HTTPConnection):
582 588 # use the modified response class
583 589 response_class = HTTPResponse
584 590 send = safesend
585 591 getresponse = wrapgetresponse(httplib.HTTPConnection)
586 592
587 593
588 594 #########################################################################
589 595 ##### TEST FUNCTIONS
590 596 #########################################################################
591 597
592 598 def error_handler(url):
593 599 global HANDLE_ERRORS
594 600 orig = HANDLE_ERRORS
595 601 keepalive_handler = HTTPHandler()
596 opener = urllib2.build_opener(keepalive_handler)
597 urllib2.install_opener(opener)
602 opener = urlreq.buildopener(keepalive_handler)
603 urlreq.installopener(opener)
598 604 pos = {0: 'off', 1: 'on'}
599 605 for i in (0, 1):
600 606 print(" fancy error handling %s (HANDLE_ERRORS = %i)" % (pos[i], i))
601 607 HANDLE_ERRORS = i
602 608 try:
603 fo = urllib2.urlopen(url)
609 fo = urlreq.urlopen(url)
604 610 fo.read()
605 611 fo.close()
606 612 try:
607 613 status, reason = fo.status, fo.reason
608 614 except AttributeError:
609 615 status, reason = None, None
610 616 except IOError as e:
611 617 print(" EXCEPTION: %s" % e)
612 618 raise
613 619 else:
614 620 print(" status = %s, reason = %s" % (status, reason))
615 621 HANDLE_ERRORS = orig
616 622 hosts = keepalive_handler.open_connections()
617 623 print("open connections:", hosts)
618 624 keepalive_handler.close_all()
619 625
620 626 def continuity(url):
621 627 from . import util
622 628 md5 = util.md5
623 629 format = '%25s: %s'
624 630
625 631 # first fetch the file with the normal http handler
626 opener = urllib2.build_opener()
627 urllib2.install_opener(opener)
628 fo = urllib2.urlopen(url)
632 opener = urlreq.buildopener()
633 urlreq.installopener(opener)
634 fo = urlreq.urlopen(url)
629 635 foo = fo.read()
630 636 fo.close()
631 637 m = md5(foo)
632 638 print(format % ('normal urllib', m.hexdigest()))
633 639
634 640 # now install the keepalive handler and try again
635 opener = urllib2.build_opener(HTTPHandler())
636 urllib2.install_opener(opener)
641 opener = urlreq.buildopener(HTTPHandler())
642 urlreq.installopener(opener)
637 643
638 fo = urllib2.urlopen(url)
644 fo = urlreq.urlopen(url)
639 645 foo = fo.read()
640 646 fo.close()
641 647 m = md5(foo)
642 648 print(format % ('keepalive read', m.hexdigest()))
643 649
644 fo = urllib2.urlopen(url)
650 fo = urlreq.urlopen(url)
645 651 foo = ''
646 652 while True:
647 653 f = fo.readline()
648 654 if f:
649 655 foo = foo + f
650 656 else: break
651 657 fo.close()
652 658 m = md5(foo)
653 659 print(format % ('keepalive readline', m.hexdigest()))
654 660
655 661 def comp(N, url):
656 662 print(' making %i connections to:\n %s' % (N, url))
657 663
658 664 sys.stdout.write(' first using the normal urllib handlers')
659 665 # first use normal opener
660 opener = urllib2.build_opener()
661 urllib2.install_opener(opener)
666 opener = urlreq.buildopener()
667 urlreq.installopener(opener)
662 668 t1 = fetch(N, url)
663 669 print(' TIME: %.3f s' % t1)
664 670
665 671 sys.stdout.write(' now using the keepalive handler ')
666 672 # now install the keepalive handler and try again
667 opener = urllib2.build_opener(HTTPHandler())
668 urllib2.install_opener(opener)
673 opener = urlreq.buildopener(HTTPHandler())
674 urlreq.installopener(opener)
669 675 t2 = fetch(N, url)
670 676 print(' TIME: %.3f s' % t2)
671 677 print(' improvement factor: %.2f' % (t1 / t2))
672 678
673 679 def fetch(N, url, delay=0):
674 680 import time
675 681 lens = []
676 682 starttime = time.time()
677 683 for i in range(N):
678 684 if delay and i > 0:
679 685 time.sleep(delay)
680 fo = urllib2.urlopen(url)
686 fo = urlreq.urlopen(url)
681 687 foo = fo.read()
682 688 fo.close()
683 689 lens.append(len(foo))
684 690 diff = time.time() - starttime
685 691
686 692 j = 0
687 693 for i in lens[1:]:
688 694 j = j + 1
689 695 if not i == lens[0]:
690 696 print("WARNING: inconsistent length on read %i: %i" % (j, i))
691 697
692 698 return diff
693 699
694 700 def test_timeout(url):
695 701 global DEBUG
696 702 dbbackup = DEBUG
697 703 class FakeLogger(object):
698 704 def debug(self, msg, *args):
699 705 print(msg % args)
700 706 info = warning = error = debug
701 707 DEBUG = FakeLogger()
702 708 print(" fetching the file to establish a connection")
703 fo = urllib2.urlopen(url)
709 fo = urlreq.urlopen(url)
704 710 data1 = fo.read()
705 711 fo.close()
706 712
707 713 i = 20
708 714 print(" waiting %i seconds for the server to close the connection" % i)
709 715 while i > 0:
710 716 sys.stdout.write('\r %2i' % i)
711 717 sys.stdout.flush()
712 718 time.sleep(1)
713 719 i -= 1
714 720 sys.stderr.write('\r')
715 721
716 722 print(" fetching the file a second time")
717 fo = urllib2.urlopen(url)
723 fo = urlreq.urlopen(url)
718 724 data2 = fo.read()
719 725 fo.close()
720 726
721 727 if data1 == data2:
722 728 print(' data are identical')
723 729 else:
724 730 print(' ERROR: DATA DIFFER')
725 731
726 732 DEBUG = dbbackup
727 733
728 734
729 735 def test(url, N=10):
730 736 print("checking error handler (do this on a non-200)")
731 737 try: error_handler(url)
732 738 except IOError:
733 739 print("exiting - exception will prevent further tests")
734 740 sys.exit()
735 741 print('')
736 742 print("performing continuity test (making sure stuff isn't corrupted)")
737 743 continuity(url)
738 744 print('')
739 745 print("performing speed comparison")
740 746 comp(N, url)
741 747 print('')
742 748 print("performing dropped-connection check")
743 749 test_timeout(url)
744 750
745 751 if __name__ == '__main__':
746 752 import time
747 753 try:
748 754 N = int(sys.argv[1])
749 755 url = sys.argv[2]
750 756 except (IndexError, ValueError):
751 757 print("%s <integer> <url>" % sys.argv[0])
752 758 else:
753 759 test(url, N)
@@ -1,1982 +1,1983 b''
1 1 # localrepo.py - read/write repository class for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import inspect
12 12 import os
13 13 import random
14 14 import time
15 import urllib
16 15 import weakref
17 16
18 17 from .i18n import _
19 18 from .node import (
20 19 hex,
21 20 nullid,
22 21 short,
23 22 wdirrev,
24 23 )
25 24 from . import (
26 25 bookmarks,
27 26 branchmap,
28 27 bundle2,
29 28 changegroup,
30 29 changelog,
31 30 cmdutil,
32 31 context,
33 32 dirstate,
34 33 encoding,
35 34 error,
36 35 exchange,
37 36 extensions,
38 37 filelog,
39 38 hook,
40 39 lock as lockmod,
41 40 manifest,
42 41 match as matchmod,
43 42 merge as mergemod,
44 43 namespaces,
45 44 obsolete,
46 45 pathutil,
47 46 peer,
48 47 phases,
49 48 pushkey,
50 49 repoview,
51 50 revset,
52 51 scmutil,
53 52 store,
54 53 subrepo,
55 54 tags as tagsmod,
56 55 transaction,
57 56 util,
58 57 )
59 58
60 59 release = lockmod.release
61 60 propertycache = util.propertycache
61 urlerr = util.urlerr
62 urlreq = util.urlreq
62 63 filecache = scmutil.filecache
63 64
64 65 class repofilecache(filecache):
65 66 """All filecache usage on repo are done for logic that should be unfiltered
66 67 """
67 68
68 69 def __get__(self, repo, type=None):
69 70 return super(repofilecache, self).__get__(repo.unfiltered(), type)
70 71 def __set__(self, repo, value):
71 72 return super(repofilecache, self).__set__(repo.unfiltered(), value)
72 73 def __delete__(self, repo):
73 74 return super(repofilecache, self).__delete__(repo.unfiltered())
74 75
75 76 class storecache(repofilecache):
76 77 """filecache for files in the store"""
77 78 def join(self, obj, fname):
78 79 return obj.sjoin(fname)
79 80
80 81 class unfilteredpropertycache(propertycache):
81 82 """propertycache that apply to unfiltered repo only"""
82 83
83 84 def __get__(self, repo, type=None):
84 85 unfi = repo.unfiltered()
85 86 if unfi is repo:
86 87 return super(unfilteredpropertycache, self).__get__(unfi)
87 88 return getattr(unfi, self.name)
88 89
89 90 class filteredpropertycache(propertycache):
90 91 """propertycache that must take filtering in account"""
91 92
92 93 def cachevalue(self, obj, value):
93 94 object.__setattr__(obj, self.name, value)
94 95
95 96
96 97 def hasunfilteredcache(repo, name):
97 98 """check if a repo has an unfilteredpropertycache value for <name>"""
98 99 return name in vars(repo.unfiltered())
99 100
100 101 def unfilteredmethod(orig):
101 102 """decorate method that always need to be run on unfiltered version"""
102 103 def wrapper(repo, *args, **kwargs):
103 104 return orig(repo.unfiltered(), *args, **kwargs)
104 105 return wrapper
105 106
106 107 moderncaps = set(('lookup', 'branchmap', 'pushkey', 'known', 'getbundle',
107 108 'unbundle'))
108 109 legacycaps = moderncaps.union(set(['changegroupsubset']))
109 110
110 111 class localpeer(peer.peerrepository):
111 112 '''peer for a local repo; reflects only the most recent API'''
112 113
113 114 def __init__(self, repo, caps=moderncaps):
114 115 peer.peerrepository.__init__(self)
115 116 self._repo = repo.filtered('served')
116 117 self.ui = repo.ui
117 118 self._caps = repo._restrictcapabilities(caps)
118 119 self.requirements = repo.requirements
119 120 self.supportedformats = repo.supportedformats
120 121
121 122 def close(self):
122 123 self._repo.close()
123 124
124 125 def _capabilities(self):
125 126 return self._caps
126 127
127 128 def local(self):
128 129 return self._repo
129 130
130 131 def canpush(self):
131 132 return True
132 133
133 134 def url(self):
134 135 return self._repo.url()
135 136
136 137 def lookup(self, key):
137 138 return self._repo.lookup(key)
138 139
139 140 def branchmap(self):
140 141 return self._repo.branchmap()
141 142
142 143 def heads(self):
143 144 return self._repo.heads()
144 145
145 146 def known(self, nodes):
146 147 return self._repo.known(nodes)
147 148
148 149 def getbundle(self, source, heads=None, common=None, bundlecaps=None,
149 150 **kwargs):
150 151 cg = exchange.getbundle(self._repo, source, heads=heads,
151 152 common=common, bundlecaps=bundlecaps, **kwargs)
152 153 if bundlecaps is not None and 'HG20' in bundlecaps:
153 154 # When requesting a bundle2, getbundle returns a stream to make the
154 155 # wire level function happier. We need to build a proper object
155 156 # from it in local peer.
156 157 cg = bundle2.getunbundler(self.ui, cg)
157 158 return cg
158 159
159 160 # TODO We might want to move the next two calls into legacypeer and add
160 161 # unbundle instead.
161 162
162 163 def unbundle(self, cg, heads, url):
163 164 """apply a bundle on a repo
164 165
165 166 This function handles the repo locking itself."""
166 167 try:
167 168 try:
168 169 cg = exchange.readbundle(self.ui, cg, None)
169 170 ret = exchange.unbundle(self._repo, cg, heads, 'push', url)
170 171 if util.safehasattr(ret, 'getchunks'):
171 172 # This is a bundle20 object, turn it into an unbundler.
172 173 # This little dance should be dropped eventually when the
173 174 # API is finally improved.
174 175 stream = util.chunkbuffer(ret.getchunks())
175 176 ret = bundle2.getunbundler(self.ui, stream)
176 177 return ret
177 178 except Exception as exc:
178 179 # If the exception contains output salvaged from a bundle2
179 180 # reply, we need to make sure it is printed before continuing
180 181 # to fail. So we build a bundle2 with such output and consume
181 182 # it directly.
182 183 #
183 184 # This is not very elegant but allows a "simple" solution for
184 185 # issue4594
185 186 output = getattr(exc, '_bundle2salvagedoutput', ())
186 187 if output:
187 188 bundler = bundle2.bundle20(self._repo.ui)
188 189 for out in output:
189 190 bundler.addpart(out)
190 191 stream = util.chunkbuffer(bundler.getchunks())
191 192 b = bundle2.getunbundler(self.ui, stream)
192 193 bundle2.processbundle(self._repo, b)
193 194 raise
194 195 except error.PushRaced as exc:
195 196 raise error.ResponseError(_('push failed:'), str(exc))
196 197
197 198 def lock(self):
198 199 return self._repo.lock()
199 200
200 201 def addchangegroup(self, cg, source, url):
201 202 return cg.apply(self._repo, source, url)
202 203
203 204 def pushkey(self, namespace, key, old, new):
204 205 return self._repo.pushkey(namespace, key, old, new)
205 206
206 207 def listkeys(self, namespace):
207 208 return self._repo.listkeys(namespace)
208 209
209 210 def debugwireargs(self, one, two, three=None, four=None, five=None):
210 211 '''used to test argument passing over the wire'''
211 212 return "%s %s %s %s %s" % (one, two, three, four, five)
212 213
213 214 class locallegacypeer(localpeer):
214 215 '''peer extension which implements legacy methods too; used for tests with
215 216 restricted capabilities'''
216 217
217 218 def __init__(self, repo):
218 219 localpeer.__init__(self, repo, caps=legacycaps)
219 220
220 221 def branches(self, nodes):
221 222 return self._repo.branches(nodes)
222 223
223 224 def between(self, pairs):
224 225 return self._repo.between(pairs)
225 226
226 227 def changegroup(self, basenodes, source):
227 228 return changegroup.changegroup(self._repo, basenodes, source)
228 229
229 230 def changegroupsubset(self, bases, heads, source):
230 231 return changegroup.changegroupsubset(self._repo, bases, heads, source)
231 232
232 233 class localrepository(object):
233 234
234 235 supportedformats = set(('revlogv1', 'generaldelta', 'treemanifest',
235 236 'manifestv2'))
236 237 _basesupported = supportedformats | set(('store', 'fncache', 'shared',
237 238 'dotencode'))
238 239 openerreqs = set(('revlogv1', 'generaldelta', 'treemanifest', 'manifestv2'))
239 240 filtername = None
240 241
241 242 # a list of (ui, featureset) functions.
242 243 # only functions defined in module of enabled extensions are invoked
243 244 featuresetupfuncs = set()
244 245
245 246 def __init__(self, baseui, path=None, create=False):
246 247 self.requirements = set()
247 248 self.wvfs = scmutil.vfs(path, expandpath=True, realpath=True)
248 249 self.wopener = self.wvfs
249 250 self.root = self.wvfs.base
250 251 self.path = self.wvfs.join(".hg")
251 252 self.origroot = path
252 253 self.auditor = pathutil.pathauditor(self.root, self._checknested)
253 254 self.nofsauditor = pathutil.pathauditor(self.root, self._checknested,
254 255 realfs=False)
255 256 self.vfs = scmutil.vfs(self.path)
256 257 self.opener = self.vfs
257 258 self.baseui = baseui
258 259 self.ui = baseui.copy()
259 260 self.ui.copy = baseui.copy # prevent copying repo configuration
260 261 # A list of callback to shape the phase if no data were found.
261 262 # Callback are in the form: func(repo, roots) --> processed root.
262 263 # This list it to be filled by extension during repo setup
263 264 self._phasedefaults = []
264 265 try:
265 266 self.ui.readconfig(self.join("hgrc"), self.root)
266 267 extensions.loadall(self.ui)
267 268 except IOError:
268 269 pass
269 270
270 271 if self.featuresetupfuncs:
271 272 self.supported = set(self._basesupported) # use private copy
272 273 extmods = set(m.__name__ for n, m
273 274 in extensions.extensions(self.ui))
274 275 for setupfunc in self.featuresetupfuncs:
275 276 if setupfunc.__module__ in extmods:
276 277 setupfunc(self.ui, self.supported)
277 278 else:
278 279 self.supported = self._basesupported
279 280
280 281 if not self.vfs.isdir():
281 282 if create:
282 283 self.requirements = newreporequirements(self)
283 284
284 285 if not self.wvfs.exists():
285 286 self.wvfs.makedirs()
286 287 self.vfs.makedir(notindexed=True)
287 288
288 289 if 'store' in self.requirements:
289 290 self.vfs.mkdir("store")
290 291
291 292 # create an invalid changelog
292 293 self.vfs.append(
293 294 "00changelog.i",
294 295 '\0\0\0\2' # represents revlogv2
295 296 ' dummy changelog to prevent using the old repo layout'
296 297 )
297 298 else:
298 299 raise error.RepoError(_("repository %s not found") % path)
299 300 elif create:
300 301 raise error.RepoError(_("repository %s already exists") % path)
301 302 else:
302 303 try:
303 304 self.requirements = scmutil.readrequires(
304 305 self.vfs, self.supported)
305 306 except IOError as inst:
306 307 if inst.errno != errno.ENOENT:
307 308 raise
308 309
309 310 self.sharedpath = self.path
310 311 try:
311 312 vfs = scmutil.vfs(self.vfs.read("sharedpath").rstrip('\n'),
312 313 realpath=True)
313 314 s = vfs.base
314 315 if not vfs.exists():
315 316 raise error.RepoError(
316 317 _('.hg/sharedpath points to nonexistent directory %s') % s)
317 318 self.sharedpath = s
318 319 except IOError as inst:
319 320 if inst.errno != errno.ENOENT:
320 321 raise
321 322
322 323 self.store = store.store(
323 324 self.requirements, self.sharedpath, scmutil.vfs)
324 325 self.spath = self.store.path
325 326 self.svfs = self.store.vfs
326 327 self.sjoin = self.store.join
327 328 self.vfs.createmode = self.store.createmode
328 329 self._applyopenerreqs()
329 330 if create:
330 331 self._writerequirements()
331 332
332 333 self._dirstatevalidatewarned = False
333 334
334 335 self._branchcaches = {}
335 336 self._revbranchcache = None
336 337 self.filterpats = {}
337 338 self._datafilters = {}
338 339 self._transref = self._lockref = self._wlockref = None
339 340
340 341 # A cache for various files under .hg/ that tracks file changes,
341 342 # (used by the filecache decorator)
342 343 #
343 344 # Maps a property name to its util.filecacheentry
344 345 self._filecache = {}
345 346
346 347 # hold sets of revision to be filtered
347 348 # should be cleared when something might have changed the filter value:
348 349 # - new changesets,
349 350 # - phase change,
350 351 # - new obsolescence marker,
351 352 # - working directory parent change,
352 353 # - bookmark changes
353 354 self.filteredrevcache = {}
354 355
355 356 # generic mapping between names and nodes
356 357 self.names = namespaces.namespaces()
357 358
358 359 def close(self):
359 360 self._writecaches()
360 361
361 362 def _writecaches(self):
362 363 if self._revbranchcache:
363 364 self._revbranchcache.write()
364 365
365 366 def _restrictcapabilities(self, caps):
366 367 if self.ui.configbool('experimental', 'bundle2-advertise', True):
367 368 caps = set(caps)
368 369 capsblob = bundle2.encodecaps(bundle2.getrepocaps(self))
369 caps.add('bundle2=' + urllib.quote(capsblob))
370 caps.add('bundle2=' + urlreq.quote(capsblob))
370 371 return caps
371 372
372 373 def _applyopenerreqs(self):
373 374 self.svfs.options = dict((r, 1) for r in self.requirements
374 375 if r in self.openerreqs)
375 376 # experimental config: format.chunkcachesize
376 377 chunkcachesize = self.ui.configint('format', 'chunkcachesize')
377 378 if chunkcachesize is not None:
378 379 self.svfs.options['chunkcachesize'] = chunkcachesize
379 380 # experimental config: format.maxchainlen
380 381 maxchainlen = self.ui.configint('format', 'maxchainlen')
381 382 if maxchainlen is not None:
382 383 self.svfs.options['maxchainlen'] = maxchainlen
383 384 # experimental config: format.manifestcachesize
384 385 manifestcachesize = self.ui.configint('format', 'manifestcachesize')
385 386 if manifestcachesize is not None:
386 387 self.svfs.options['manifestcachesize'] = manifestcachesize
387 388 # experimental config: format.aggressivemergedeltas
388 389 aggressivemergedeltas = self.ui.configbool('format',
389 390 'aggressivemergedeltas', False)
390 391 self.svfs.options['aggressivemergedeltas'] = aggressivemergedeltas
391 392 self.svfs.options['lazydeltabase'] = not scmutil.gddeltaconfig(self.ui)
392 393
393 394 def _writerequirements(self):
394 395 scmutil.writerequires(self.vfs, self.requirements)
395 396
396 397 def _checknested(self, path):
397 398 """Determine if path is a legal nested repository."""
398 399 if not path.startswith(self.root):
399 400 return False
400 401 subpath = path[len(self.root) + 1:]
401 402 normsubpath = util.pconvert(subpath)
402 403
403 404 # XXX: Checking against the current working copy is wrong in
404 405 # the sense that it can reject things like
405 406 #
406 407 # $ hg cat -r 10 sub/x.txt
407 408 #
408 409 # if sub/ is no longer a subrepository in the working copy
409 410 # parent revision.
410 411 #
411 412 # However, it can of course also allow things that would have
412 413 # been rejected before, such as the above cat command if sub/
413 414 # is a subrepository now, but was a normal directory before.
414 415 # The old path auditor would have rejected by mistake since it
415 416 # panics when it sees sub/.hg/.
416 417 #
417 418 # All in all, checking against the working copy seems sensible
418 419 # since we want to prevent access to nested repositories on
419 420 # the filesystem *now*.
420 421 ctx = self[None]
421 422 parts = util.splitpath(subpath)
422 423 while parts:
423 424 prefix = '/'.join(parts)
424 425 if prefix in ctx.substate:
425 426 if prefix == normsubpath:
426 427 return True
427 428 else:
428 429 sub = ctx.sub(prefix)
429 430 return sub.checknested(subpath[len(prefix) + 1:])
430 431 else:
431 432 parts.pop()
432 433 return False
433 434
434 435 def peer(self):
435 436 return localpeer(self) # not cached to avoid reference cycle
436 437
437 438 def unfiltered(self):
438 439 """Return unfiltered version of the repository
439 440
440 441 Intended to be overwritten by filtered repo."""
441 442 return self
442 443
443 444 def filtered(self, name):
444 445 """Return a filtered version of a repository"""
445 446 # build a new class with the mixin and the current class
446 447 # (possibly subclass of the repo)
447 448 class proxycls(repoview.repoview, self.unfiltered().__class__):
448 449 pass
449 450 return proxycls(self, name)
450 451
451 452 @repofilecache('bookmarks', 'bookmarks.current')
452 453 def _bookmarks(self):
453 454 return bookmarks.bmstore(self)
454 455
455 456 @property
456 457 def _activebookmark(self):
457 458 return self._bookmarks.active
458 459
459 460 def bookmarkheads(self, bookmark):
460 461 name = bookmark.split('@', 1)[0]
461 462 heads = []
462 463 for mark, n in self._bookmarks.iteritems():
463 464 if mark.split('@', 1)[0] == name:
464 465 heads.append(n)
465 466 return heads
466 467
467 468 # _phaserevs and _phasesets depend on changelog. what we need is to
468 469 # call _phasecache.invalidate() if '00changelog.i' was changed, but it
469 470 # can't be easily expressed in filecache mechanism.
470 471 @storecache('phaseroots', '00changelog.i')
471 472 def _phasecache(self):
472 473 return phases.phasecache(self, self._phasedefaults)
473 474
474 475 @storecache('obsstore')
475 476 def obsstore(self):
476 477 # read default format for new obsstore.
477 478 # developer config: format.obsstore-version
478 479 defaultformat = self.ui.configint('format', 'obsstore-version', None)
479 480 # rely on obsstore class default when possible.
480 481 kwargs = {}
481 482 if defaultformat is not None:
482 483 kwargs['defaultformat'] = defaultformat
483 484 readonly = not obsolete.isenabled(self, obsolete.createmarkersopt)
484 485 store = obsolete.obsstore(self.svfs, readonly=readonly,
485 486 **kwargs)
486 487 if store and readonly:
487 488 self.ui.warn(
488 489 _('obsolete feature not enabled but %i markers found!\n')
489 490 % len(list(store)))
490 491 return store
491 492
492 493 @storecache('00changelog.i')
493 494 def changelog(self):
494 495 c = changelog.changelog(self.svfs)
495 496 if 'HG_PENDING' in os.environ:
496 497 p = os.environ['HG_PENDING']
497 498 if p.startswith(self.root):
498 499 c.readpending('00changelog.i.a')
499 500 return c
500 501
501 502 @storecache('00manifest.i')
502 503 def manifest(self):
503 504 return manifest.manifest(self.svfs)
504 505
505 506 def dirlog(self, dir):
506 507 return self.manifest.dirlog(dir)
507 508
508 509 @repofilecache('dirstate')
509 510 def dirstate(self):
510 511 return dirstate.dirstate(self.vfs, self.ui, self.root,
511 512 self._dirstatevalidate)
512 513
513 514 def _dirstatevalidate(self, node):
514 515 try:
515 516 self.changelog.rev(node)
516 517 return node
517 518 except error.LookupError:
518 519 if not self._dirstatevalidatewarned:
519 520 self._dirstatevalidatewarned = True
520 521 self.ui.warn(_("warning: ignoring unknown"
521 522 " working parent %s!\n") % short(node))
522 523 return nullid
523 524
524 525 def __getitem__(self, changeid):
525 526 if changeid is None or changeid == wdirrev:
526 527 return context.workingctx(self)
527 528 if isinstance(changeid, slice):
528 529 return [context.changectx(self, i)
529 530 for i in xrange(*changeid.indices(len(self)))
530 531 if i not in self.changelog.filteredrevs]
531 532 return context.changectx(self, changeid)
532 533
533 534 def __contains__(self, changeid):
534 535 try:
535 536 self[changeid]
536 537 return True
537 538 except error.RepoLookupError:
538 539 return False
539 540
540 541 def __nonzero__(self):
541 542 return True
542 543
543 544 def __len__(self):
544 545 return len(self.changelog)
545 546
546 547 def __iter__(self):
547 548 return iter(self.changelog)
548 549
549 550 def revs(self, expr, *args):
550 551 '''Find revisions matching a revset.
551 552
552 553 The revset is specified as a string ``expr`` that may contain
553 554 %-formatting to escape certain types. See ``revset.formatspec``.
554 555
555 556 Return a revset.abstractsmartset, which is a list-like interface
556 557 that contains integer revisions.
557 558 '''
558 559 expr = revset.formatspec(expr, *args)
559 560 m = revset.match(None, expr)
560 561 return m(self)
561 562
562 563 def set(self, expr, *args):
563 564 '''Find revisions matching a revset and emit changectx instances.
564 565
565 566 This is a convenience wrapper around ``revs()`` that iterates the
566 567 result and is a generator of changectx instances.
567 568 '''
568 569 for r in self.revs(expr, *args):
569 570 yield self[r]
570 571
571 572 def url(self):
572 573 return 'file:' + self.root
573 574
574 575 def hook(self, name, throw=False, **args):
575 576 """Call a hook, passing this repo instance.
576 577
577 578 This a convenience method to aid invoking hooks. Extensions likely
578 579 won't call this unless they have registered a custom hook or are
579 580 replacing code that is expected to call a hook.
580 581 """
581 582 return hook.hook(self.ui, self, name, throw, **args)
582 583
583 584 @unfilteredmethod
584 585 def _tag(self, names, node, message, local, user, date, extra=None,
585 586 editor=False):
586 587 if isinstance(names, str):
587 588 names = (names,)
588 589
589 590 branches = self.branchmap()
590 591 for name in names:
591 592 self.hook('pretag', throw=True, node=hex(node), tag=name,
592 593 local=local)
593 594 if name in branches:
594 595 self.ui.warn(_("warning: tag %s conflicts with existing"
595 596 " branch name\n") % name)
596 597
597 598 def writetags(fp, names, munge, prevtags):
598 599 fp.seek(0, 2)
599 600 if prevtags and prevtags[-1] != '\n':
600 601 fp.write('\n')
601 602 for name in names:
602 603 if munge:
603 604 m = munge(name)
604 605 else:
605 606 m = name
606 607
607 608 if (self._tagscache.tagtypes and
608 609 name in self._tagscache.tagtypes):
609 610 old = self.tags().get(name, nullid)
610 611 fp.write('%s %s\n' % (hex(old), m))
611 612 fp.write('%s %s\n' % (hex(node), m))
612 613 fp.close()
613 614
614 615 prevtags = ''
615 616 if local:
616 617 try:
617 618 fp = self.vfs('localtags', 'r+')
618 619 except IOError:
619 620 fp = self.vfs('localtags', 'a')
620 621 else:
621 622 prevtags = fp.read()
622 623
623 624 # local tags are stored in the current charset
624 625 writetags(fp, names, None, prevtags)
625 626 for name in names:
626 627 self.hook('tag', node=hex(node), tag=name, local=local)
627 628 return
628 629
629 630 try:
630 631 fp = self.wfile('.hgtags', 'rb+')
631 632 except IOError as e:
632 633 if e.errno != errno.ENOENT:
633 634 raise
634 635 fp = self.wfile('.hgtags', 'ab')
635 636 else:
636 637 prevtags = fp.read()
637 638
638 639 # committed tags are stored in UTF-8
639 640 writetags(fp, names, encoding.fromlocal, prevtags)
640 641
641 642 fp.close()
642 643
643 644 self.invalidatecaches()
644 645
645 646 if '.hgtags' not in self.dirstate:
646 647 self[None].add(['.hgtags'])
647 648
648 649 m = matchmod.exact(self.root, '', ['.hgtags'])
649 650 tagnode = self.commit(message, user, date, extra=extra, match=m,
650 651 editor=editor)
651 652
652 653 for name in names:
653 654 self.hook('tag', node=hex(node), tag=name, local=local)
654 655
655 656 return tagnode
656 657
657 658 def tag(self, names, node, message, local, user, date, editor=False):
658 659 '''tag a revision with one or more symbolic names.
659 660
660 661 names is a list of strings or, when adding a single tag, names may be a
661 662 string.
662 663
663 664 if local is True, the tags are stored in a per-repository file.
664 665 otherwise, they are stored in the .hgtags file, and a new
665 666 changeset is committed with the change.
666 667
667 668 keyword arguments:
668 669
669 670 local: whether to store tags in non-version-controlled file
670 671 (default False)
671 672
672 673 message: commit message to use if committing
673 674
674 675 user: name of user to use if committing
675 676
676 677 date: date tuple to use if committing'''
677 678
678 679 if not local:
679 680 m = matchmod.exact(self.root, '', ['.hgtags'])
680 681 if any(self.status(match=m, unknown=True, ignored=True)):
681 682 raise error.Abort(_('working copy of .hgtags is changed'),
682 683 hint=_('please commit .hgtags manually'))
683 684
684 685 self.tags() # instantiate the cache
685 686 self._tag(names, node, message, local, user, date, editor=editor)
686 687
687 688 @filteredpropertycache
688 689 def _tagscache(self):
689 690 '''Returns a tagscache object that contains various tags related
690 691 caches.'''
691 692
692 693 # This simplifies its cache management by having one decorated
693 694 # function (this one) and the rest simply fetch things from it.
694 695 class tagscache(object):
695 696 def __init__(self):
696 697 # These two define the set of tags for this repository. tags
697 698 # maps tag name to node; tagtypes maps tag name to 'global' or
698 699 # 'local'. (Global tags are defined by .hgtags across all
699 700 # heads, and local tags are defined in .hg/localtags.)
700 701 # They constitute the in-memory cache of tags.
701 702 self.tags = self.tagtypes = None
702 703
703 704 self.nodetagscache = self.tagslist = None
704 705
705 706 cache = tagscache()
706 707 cache.tags, cache.tagtypes = self._findtags()
707 708
708 709 return cache
709 710
710 711 def tags(self):
711 712 '''return a mapping of tag to node'''
712 713 t = {}
713 714 if self.changelog.filteredrevs:
714 715 tags, tt = self._findtags()
715 716 else:
716 717 tags = self._tagscache.tags
717 718 for k, v in tags.iteritems():
718 719 try:
719 720 # ignore tags to unknown nodes
720 721 self.changelog.rev(v)
721 722 t[k] = v
722 723 except (error.LookupError, ValueError):
723 724 pass
724 725 return t
725 726
726 727 def _findtags(self):
727 728 '''Do the hard work of finding tags. Return a pair of dicts
728 729 (tags, tagtypes) where tags maps tag name to node, and tagtypes
729 730 maps tag name to a string like \'global\' or \'local\'.
730 731 Subclasses or extensions are free to add their own tags, but
731 732 should be aware that the returned dicts will be retained for the
732 733 duration of the localrepo object.'''
733 734
734 735 # XXX what tagtype should subclasses/extensions use? Currently
735 736 # mq and bookmarks add tags, but do not set the tagtype at all.
736 737 # Should each extension invent its own tag type? Should there
737 738 # be one tagtype for all such "virtual" tags? Or is the status
738 739 # quo fine?
739 740
740 741 alltags = {} # map tag name to (node, hist)
741 742 tagtypes = {}
742 743
743 744 tagsmod.findglobaltags(self.ui, self, alltags, tagtypes)
744 745 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
745 746
746 747 # Build the return dicts. Have to re-encode tag names because
747 748 # the tags module always uses UTF-8 (in order not to lose info
748 749 # writing to the cache), but the rest of Mercurial wants them in
749 750 # local encoding.
750 751 tags = {}
751 752 for (name, (node, hist)) in alltags.iteritems():
752 753 if node != nullid:
753 754 tags[encoding.tolocal(name)] = node
754 755 tags['tip'] = self.changelog.tip()
755 756 tagtypes = dict([(encoding.tolocal(name), value)
756 757 for (name, value) in tagtypes.iteritems()])
757 758 return (tags, tagtypes)
758 759
759 760 def tagtype(self, tagname):
760 761 '''
761 762 return the type of the given tag. result can be:
762 763
763 764 'local' : a local tag
764 765 'global' : a global tag
765 766 None : tag does not exist
766 767 '''
767 768
768 769 return self._tagscache.tagtypes.get(tagname)
769 770
770 771 def tagslist(self):
771 772 '''return a list of tags ordered by revision'''
772 773 if not self._tagscache.tagslist:
773 774 l = []
774 775 for t, n in self.tags().iteritems():
775 776 l.append((self.changelog.rev(n), t, n))
776 777 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
777 778
778 779 return self._tagscache.tagslist
779 780
780 781 def nodetags(self, node):
781 782 '''return the tags associated with a node'''
782 783 if not self._tagscache.nodetagscache:
783 784 nodetagscache = {}
784 785 for t, n in self._tagscache.tags.iteritems():
785 786 nodetagscache.setdefault(n, []).append(t)
786 787 for tags in nodetagscache.itervalues():
787 788 tags.sort()
788 789 self._tagscache.nodetagscache = nodetagscache
789 790 return self._tagscache.nodetagscache.get(node, [])
790 791
791 792 def nodebookmarks(self, node):
792 793 """return the list of bookmarks pointing to the specified node"""
793 794 marks = []
794 795 for bookmark, n in self._bookmarks.iteritems():
795 796 if n == node:
796 797 marks.append(bookmark)
797 798 return sorted(marks)
798 799
799 800 def branchmap(self):
800 801 '''returns a dictionary {branch: [branchheads]} with branchheads
801 802 ordered by increasing revision number'''
802 803 branchmap.updatecache(self)
803 804 return self._branchcaches[self.filtername]
804 805
805 806 @unfilteredmethod
806 807 def revbranchcache(self):
807 808 if not self._revbranchcache:
808 809 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
809 810 return self._revbranchcache
810 811
811 812 def branchtip(self, branch, ignoremissing=False):
812 813 '''return the tip node for a given branch
813 814
814 815 If ignoremissing is True, then this method will not raise an error.
815 816 This is helpful for callers that only expect None for a missing branch
816 817 (e.g. namespace).
817 818
818 819 '''
819 820 try:
820 821 return self.branchmap().branchtip(branch)
821 822 except KeyError:
822 823 if not ignoremissing:
823 824 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
824 825 else:
825 826 pass
826 827
827 828 def lookup(self, key):
828 829 return self[key].node()
829 830
830 831 def lookupbranch(self, key, remote=None):
831 832 repo = remote or self
832 833 if key in repo.branchmap():
833 834 return key
834 835
835 836 repo = (remote and remote.local()) and remote or self
836 837 return repo[key].branch()
837 838
838 839 def known(self, nodes):
839 840 cl = self.changelog
840 841 nm = cl.nodemap
841 842 filtered = cl.filteredrevs
842 843 result = []
843 844 for n in nodes:
844 845 r = nm.get(n)
845 846 resp = not (r is None or r in filtered)
846 847 result.append(resp)
847 848 return result
848 849
849 850 def local(self):
850 851 return self
851 852
852 853 def publishing(self):
853 854 # it's safe (and desirable) to trust the publish flag unconditionally
854 855 # so that we don't finalize changes shared between users via ssh or nfs
855 856 return self.ui.configbool('phases', 'publish', True, untrusted=True)
856 857
857 858 def cancopy(self):
858 859 # so statichttprepo's override of local() works
859 860 if not self.local():
860 861 return False
861 862 if not self.publishing():
862 863 return True
863 864 # if publishing we can't copy if there is filtered content
864 865 return not self.filtered('visible').changelog.filteredrevs
865 866
866 867 def shared(self):
867 868 '''the type of shared repository (None if not shared)'''
868 869 if self.sharedpath != self.path:
869 870 return 'store'
870 871 return None
871 872
872 873 def join(self, f, *insidef):
873 874 return self.vfs.join(os.path.join(f, *insidef))
874 875
875 876 def wjoin(self, f, *insidef):
876 877 return self.vfs.reljoin(self.root, f, *insidef)
877 878
878 879 def file(self, f):
879 880 if f[0] == '/':
880 881 f = f[1:]
881 882 return filelog.filelog(self.svfs, f)
882 883
883 884 def parents(self, changeid=None):
884 885 '''get list of changectxs for parents of changeid'''
885 886 msg = 'repo.parents() is deprecated, use repo[%r].parents()' % changeid
886 887 self.ui.deprecwarn(msg, '3.7')
887 888 return self[changeid].parents()
888 889
889 890 def changectx(self, changeid):
890 891 return self[changeid]
891 892
892 893 def setparents(self, p1, p2=nullid):
893 894 self.dirstate.beginparentchange()
894 895 copies = self.dirstate.setparents(p1, p2)
895 896 pctx = self[p1]
896 897 if copies:
897 898 # Adjust copy records, the dirstate cannot do it, it
898 899 # requires access to parents manifests. Preserve them
899 900 # only for entries added to first parent.
900 901 for f in copies:
901 902 if f not in pctx and copies[f] in pctx:
902 903 self.dirstate.copy(copies[f], f)
903 904 if p2 == nullid:
904 905 for f, s in sorted(self.dirstate.copies().items()):
905 906 if f not in pctx and s not in pctx:
906 907 self.dirstate.copy(None, f)
907 908 self.dirstate.endparentchange()
908 909
909 910 def filectx(self, path, changeid=None, fileid=None):
910 911 """changeid can be a changeset revision, node, or tag.
911 912 fileid can be a file revision or node."""
912 913 return context.filectx(self, path, changeid, fileid)
913 914
914 915 def getcwd(self):
915 916 return self.dirstate.getcwd()
916 917
917 918 def pathto(self, f, cwd=None):
918 919 return self.dirstate.pathto(f, cwd)
919 920
920 921 def wfile(self, f, mode='r'):
921 922 return self.wvfs(f, mode)
922 923
923 924 def _link(self, f):
924 925 return self.wvfs.islink(f)
925 926
926 927 def _loadfilter(self, filter):
927 928 if filter not in self.filterpats:
928 929 l = []
929 930 for pat, cmd in self.ui.configitems(filter):
930 931 if cmd == '!':
931 932 continue
932 933 mf = matchmod.match(self.root, '', [pat])
933 934 fn = None
934 935 params = cmd
935 936 for name, filterfn in self._datafilters.iteritems():
936 937 if cmd.startswith(name):
937 938 fn = filterfn
938 939 params = cmd[len(name):].lstrip()
939 940 break
940 941 if not fn:
941 942 fn = lambda s, c, **kwargs: util.filter(s, c)
942 943 # Wrap old filters not supporting keyword arguments
943 944 if not inspect.getargspec(fn)[2]:
944 945 oldfn = fn
945 946 fn = lambda s, c, **kwargs: oldfn(s, c)
946 947 l.append((mf, fn, params))
947 948 self.filterpats[filter] = l
948 949 return self.filterpats[filter]
949 950
950 951 def _filter(self, filterpats, filename, data):
951 952 for mf, fn, cmd in filterpats:
952 953 if mf(filename):
953 954 self.ui.debug("filtering %s through %s\n" % (filename, cmd))
954 955 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
955 956 break
956 957
957 958 return data
958 959
959 960 @unfilteredpropertycache
960 961 def _encodefilterpats(self):
961 962 return self._loadfilter('encode')
962 963
963 964 @unfilteredpropertycache
964 965 def _decodefilterpats(self):
965 966 return self._loadfilter('decode')
966 967
967 968 def adddatafilter(self, name, filter):
968 969 self._datafilters[name] = filter
969 970
970 971 def wread(self, filename):
971 972 if self._link(filename):
972 973 data = self.wvfs.readlink(filename)
973 974 else:
974 975 data = self.wvfs.read(filename)
975 976 return self._filter(self._encodefilterpats, filename, data)
976 977
977 978 def wwrite(self, filename, data, flags, backgroundclose=False):
978 979 """write ``data`` into ``filename`` in the working directory
979 980
980 981 This returns length of written (maybe decoded) data.
981 982 """
982 983 data = self._filter(self._decodefilterpats, filename, data)
983 984 if 'l' in flags:
984 985 self.wvfs.symlink(data, filename)
985 986 else:
986 987 self.wvfs.write(filename, data, backgroundclose=backgroundclose)
987 988 if 'x' in flags:
988 989 self.wvfs.setflags(filename, False, True)
989 990 return len(data)
990 991
991 992 def wwritedata(self, filename, data):
992 993 return self._filter(self._decodefilterpats, filename, data)
993 994
994 995 def currenttransaction(self):
995 996 """return the current transaction or None if non exists"""
996 997 if self._transref:
997 998 tr = self._transref()
998 999 else:
999 1000 tr = None
1000 1001
1001 1002 if tr and tr.running():
1002 1003 return tr
1003 1004 return None
1004 1005
1005 1006 def transaction(self, desc, report=None):
1006 1007 if (self.ui.configbool('devel', 'all-warnings')
1007 1008 or self.ui.configbool('devel', 'check-locks')):
1008 1009 l = self._lockref and self._lockref()
1009 1010 if l is None or not l.held:
1010 1011 self.ui.develwarn('transaction with no lock')
1011 1012 tr = self.currenttransaction()
1012 1013 if tr is not None:
1013 1014 return tr.nest()
1014 1015
1015 1016 # abort here if the journal already exists
1016 1017 if self.svfs.exists("journal"):
1017 1018 raise error.RepoError(
1018 1019 _("abandoned transaction found"),
1019 1020 hint=_("run 'hg recover' to clean up transaction"))
1020 1021
1021 1022 # make journal.dirstate contain in-memory changes at this point
1022 1023 self.dirstate.write(None)
1023 1024
1024 1025 idbase = "%.40f#%f" % (random.random(), time.time())
1025 1026 txnid = 'TXN:' + util.sha1(idbase).hexdigest()
1026 1027 self.hook('pretxnopen', throw=True, txnname=desc, txnid=txnid)
1027 1028
1028 1029 self._writejournal(desc)
1029 1030 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
1030 1031 if report:
1031 1032 rp = report
1032 1033 else:
1033 1034 rp = self.ui.warn
1034 1035 vfsmap = {'plain': self.vfs} # root of .hg/
1035 1036 # we must avoid cyclic reference between repo and transaction.
1036 1037 reporef = weakref.ref(self)
1037 1038 def validate(tr):
1038 1039 """will run pre-closing hooks"""
1039 1040 reporef().hook('pretxnclose', throw=True,
1040 1041 txnname=desc, **tr.hookargs)
1041 1042 def releasefn(tr, success):
1042 1043 repo = reporef()
1043 1044 if success:
1044 1045 # this should be explicitly invoked here, because
1045 1046 # in-memory changes aren't written out at closing
1046 1047 # transaction, if tr.addfilegenerator (via
1047 1048 # dirstate.write or so) isn't invoked while
1048 1049 # transaction running
1049 1050 repo.dirstate.write(None)
1050 1051 else:
1051 1052 # prevent in-memory changes from being written out at
1052 1053 # the end of outer wlock scope or so
1053 1054 repo.dirstate.invalidate()
1054 1055
1055 1056 # discard all changes (including ones already written
1056 1057 # out) in this transaction
1057 1058 repo.vfs.rename('journal.dirstate', 'dirstate')
1058 1059
1059 1060 repo.invalidate(clearfilecache=True)
1060 1061
1061 1062 tr = transaction.transaction(rp, self.svfs, vfsmap,
1062 1063 "journal",
1063 1064 "undo",
1064 1065 aftertrans(renames),
1065 1066 self.store.createmode,
1066 1067 validator=validate,
1067 1068 releasefn=releasefn)
1068 1069
1069 1070 tr.hookargs['txnid'] = txnid
1070 1071 # note: writing the fncache only during finalize mean that the file is
1071 1072 # outdated when running hooks. As fncache is used for streaming clone,
1072 1073 # this is not expected to break anything that happen during the hooks.
1073 1074 tr.addfinalize('flush-fncache', self.store.write)
1074 1075 def txnclosehook(tr2):
1075 1076 """To be run if transaction is successful, will schedule a hook run
1076 1077 """
1077 1078 # Don't reference tr2 in hook() so we don't hold a reference.
1078 1079 # This reduces memory consumption when there are multiple
1079 1080 # transactions per lock. This can likely go away if issue5045
1080 1081 # fixes the function accumulation.
1081 1082 hookargs = tr2.hookargs
1082 1083
1083 1084 def hook():
1084 1085 reporef().hook('txnclose', throw=False, txnname=desc,
1085 1086 **hookargs)
1086 1087 reporef()._afterlock(hook)
1087 1088 tr.addfinalize('txnclose-hook', txnclosehook)
1088 1089 def txnaborthook(tr2):
1089 1090 """To be run if transaction is aborted
1090 1091 """
1091 1092 reporef().hook('txnabort', throw=False, txnname=desc,
1092 1093 **tr2.hookargs)
1093 1094 tr.addabort('txnabort-hook', txnaborthook)
1094 1095 # avoid eager cache invalidation. in-memory data should be identical
1095 1096 # to stored data if transaction has no error.
1096 1097 tr.addpostclose('refresh-filecachestats', self._refreshfilecachestats)
1097 1098 self._transref = weakref.ref(tr)
1098 1099 return tr
1099 1100
1100 1101 def _journalfiles(self):
1101 1102 return ((self.svfs, 'journal'),
1102 1103 (self.vfs, 'journal.dirstate'),
1103 1104 (self.vfs, 'journal.branch'),
1104 1105 (self.vfs, 'journal.desc'),
1105 1106 (self.vfs, 'journal.bookmarks'),
1106 1107 (self.svfs, 'journal.phaseroots'))
1107 1108
1108 1109 def undofiles(self):
1109 1110 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
1110 1111
1111 1112 def _writejournal(self, desc):
1112 1113 self.vfs.write("journal.dirstate",
1113 1114 self.vfs.tryread("dirstate"))
1114 1115 self.vfs.write("journal.branch",
1115 1116 encoding.fromlocal(self.dirstate.branch()))
1116 1117 self.vfs.write("journal.desc",
1117 1118 "%d\n%s\n" % (len(self), desc))
1118 1119 self.vfs.write("journal.bookmarks",
1119 1120 self.vfs.tryread("bookmarks"))
1120 1121 self.svfs.write("journal.phaseroots",
1121 1122 self.svfs.tryread("phaseroots"))
1122 1123
1123 1124 def recover(self):
1124 1125 with self.lock():
1125 1126 if self.svfs.exists("journal"):
1126 1127 self.ui.status(_("rolling back interrupted transaction\n"))
1127 1128 vfsmap = {'': self.svfs,
1128 1129 'plain': self.vfs,}
1129 1130 transaction.rollback(self.svfs, vfsmap, "journal",
1130 1131 self.ui.warn)
1131 1132 self.invalidate()
1132 1133 return True
1133 1134 else:
1134 1135 self.ui.warn(_("no interrupted transaction available\n"))
1135 1136 return False
1136 1137
1137 1138 def rollback(self, dryrun=False, force=False):
1138 1139 wlock = lock = dsguard = None
1139 1140 try:
1140 1141 wlock = self.wlock()
1141 1142 lock = self.lock()
1142 1143 if self.svfs.exists("undo"):
1143 1144 dsguard = cmdutil.dirstateguard(self, 'rollback')
1144 1145
1145 1146 return self._rollback(dryrun, force, dsguard)
1146 1147 else:
1147 1148 self.ui.warn(_("no rollback information available\n"))
1148 1149 return 1
1149 1150 finally:
1150 1151 release(dsguard, lock, wlock)
1151 1152
1152 1153 @unfilteredmethod # Until we get smarter cache management
1153 1154 def _rollback(self, dryrun, force, dsguard):
1154 1155 ui = self.ui
1155 1156 try:
1156 1157 args = self.vfs.read('undo.desc').splitlines()
1157 1158 (oldlen, desc, detail) = (int(args[0]), args[1], None)
1158 1159 if len(args) >= 3:
1159 1160 detail = args[2]
1160 1161 oldtip = oldlen - 1
1161 1162
1162 1163 if detail and ui.verbose:
1163 1164 msg = (_('repository tip rolled back to revision %s'
1164 1165 ' (undo %s: %s)\n')
1165 1166 % (oldtip, desc, detail))
1166 1167 else:
1167 1168 msg = (_('repository tip rolled back to revision %s'
1168 1169 ' (undo %s)\n')
1169 1170 % (oldtip, desc))
1170 1171 except IOError:
1171 1172 msg = _('rolling back unknown transaction\n')
1172 1173 desc = None
1173 1174
1174 1175 if not force and self['.'] != self['tip'] and desc == 'commit':
1175 1176 raise error.Abort(
1176 1177 _('rollback of last commit while not checked out '
1177 1178 'may lose data'), hint=_('use -f to force'))
1178 1179
1179 1180 ui.status(msg)
1180 1181 if dryrun:
1181 1182 return 0
1182 1183
1183 1184 parents = self.dirstate.parents()
1184 1185 self.destroying()
1185 1186 vfsmap = {'plain': self.vfs, '': self.svfs}
1186 1187 transaction.rollback(self.svfs, vfsmap, 'undo', ui.warn)
1187 1188 if self.vfs.exists('undo.bookmarks'):
1188 1189 self.vfs.rename('undo.bookmarks', 'bookmarks')
1189 1190 if self.svfs.exists('undo.phaseroots'):
1190 1191 self.svfs.rename('undo.phaseroots', 'phaseroots')
1191 1192 self.invalidate()
1192 1193
1193 1194 parentgone = (parents[0] not in self.changelog.nodemap or
1194 1195 parents[1] not in self.changelog.nodemap)
1195 1196 if parentgone:
1196 1197 # prevent dirstateguard from overwriting already restored one
1197 1198 dsguard.close()
1198 1199
1199 1200 self.vfs.rename('undo.dirstate', 'dirstate')
1200 1201 try:
1201 1202 branch = self.vfs.read('undo.branch')
1202 1203 self.dirstate.setbranch(encoding.tolocal(branch))
1203 1204 except IOError:
1204 1205 ui.warn(_('named branch could not be reset: '
1205 1206 'current branch is still \'%s\'\n')
1206 1207 % self.dirstate.branch())
1207 1208
1208 1209 self.dirstate.invalidate()
1209 1210 parents = tuple([p.rev() for p in self[None].parents()])
1210 1211 if len(parents) > 1:
1211 1212 ui.status(_('working directory now based on '
1212 1213 'revisions %d and %d\n') % parents)
1213 1214 else:
1214 1215 ui.status(_('working directory now based on '
1215 1216 'revision %d\n') % parents)
1216 1217 mergemod.mergestate.clean(self, self['.'].node())
1217 1218
1218 1219 # TODO: if we know which new heads may result from this rollback, pass
1219 1220 # them to destroy(), which will prevent the branchhead cache from being
1220 1221 # invalidated.
1221 1222 self.destroyed()
1222 1223 return 0
1223 1224
1224 1225 def invalidatecaches(self):
1225 1226
1226 1227 if '_tagscache' in vars(self):
1227 1228 # can't use delattr on proxy
1228 1229 del self.__dict__['_tagscache']
1229 1230
1230 1231 self.unfiltered()._branchcaches.clear()
1231 1232 self.invalidatevolatilesets()
1232 1233
1233 1234 def invalidatevolatilesets(self):
1234 1235 self.filteredrevcache.clear()
1235 1236 obsolete.clearobscaches(self)
1236 1237
1237 1238 def invalidatedirstate(self):
1238 1239 '''Invalidates the dirstate, causing the next call to dirstate
1239 1240 to check if it was modified since the last time it was read,
1240 1241 rereading it if it has.
1241 1242
1242 1243 This is different to dirstate.invalidate() that it doesn't always
1243 1244 rereads the dirstate. Use dirstate.invalidate() if you want to
1244 1245 explicitly read the dirstate again (i.e. restoring it to a previous
1245 1246 known good state).'''
1246 1247 if hasunfilteredcache(self, 'dirstate'):
1247 1248 for k in self.dirstate._filecache:
1248 1249 try:
1249 1250 delattr(self.dirstate, k)
1250 1251 except AttributeError:
1251 1252 pass
1252 1253 delattr(self.unfiltered(), 'dirstate')
1253 1254
1254 1255 def invalidate(self, clearfilecache=False):
1255 1256 unfiltered = self.unfiltered() # all file caches are stored unfiltered
1256 1257 for k in self._filecache.keys():
1257 1258 # dirstate is invalidated separately in invalidatedirstate()
1258 1259 if k == 'dirstate':
1259 1260 continue
1260 1261
1261 1262 if clearfilecache:
1262 1263 del self._filecache[k]
1263 1264 try:
1264 1265 delattr(unfiltered, k)
1265 1266 except AttributeError:
1266 1267 pass
1267 1268 self.invalidatecaches()
1268 1269 self.store.invalidatecaches()
1269 1270
1270 1271 def invalidateall(self):
1271 1272 '''Fully invalidates both store and non-store parts, causing the
1272 1273 subsequent operation to reread any outside changes.'''
1273 1274 # extension should hook this to invalidate its caches
1274 1275 self.invalidate()
1275 1276 self.invalidatedirstate()
1276 1277
1277 1278 def _refreshfilecachestats(self, tr):
1278 1279 """Reload stats of cached files so that they are flagged as valid"""
1279 1280 for k, ce in self._filecache.items():
1280 1281 if k == 'dirstate' or k not in self.__dict__:
1281 1282 continue
1282 1283 ce.refresh()
1283 1284
1284 1285 def _lock(self, vfs, lockname, wait, releasefn, acquirefn, desc,
1285 1286 inheritchecker=None, parentenvvar=None):
1286 1287 parentlock = None
1287 1288 # the contents of parentenvvar are used by the underlying lock to
1288 1289 # determine whether it can be inherited
1289 1290 if parentenvvar is not None:
1290 1291 parentlock = os.environ.get(parentenvvar)
1291 1292 try:
1292 1293 l = lockmod.lock(vfs, lockname, 0, releasefn=releasefn,
1293 1294 acquirefn=acquirefn, desc=desc,
1294 1295 inheritchecker=inheritchecker,
1295 1296 parentlock=parentlock)
1296 1297 except error.LockHeld as inst:
1297 1298 if not wait:
1298 1299 raise
1299 1300 self.ui.warn(_("waiting for lock on %s held by %r\n") %
1300 1301 (desc, inst.locker))
1301 1302 # default to 600 seconds timeout
1302 1303 l = lockmod.lock(vfs, lockname,
1303 1304 int(self.ui.config("ui", "timeout", "600")),
1304 1305 releasefn=releasefn, acquirefn=acquirefn,
1305 1306 desc=desc)
1306 1307 self.ui.warn(_("got lock after %s seconds\n") % l.delay)
1307 1308 return l
1308 1309
1309 1310 def _afterlock(self, callback):
1310 1311 """add a callback to be run when the repository is fully unlocked
1311 1312
1312 1313 The callback will be executed when the outermost lock is released
1313 1314 (with wlock being higher level than 'lock')."""
1314 1315 for ref in (self._wlockref, self._lockref):
1315 1316 l = ref and ref()
1316 1317 if l and l.held:
1317 1318 l.postrelease.append(callback)
1318 1319 break
1319 1320 else: # no lock have been found.
1320 1321 callback()
1321 1322
1322 1323 def lock(self, wait=True):
1323 1324 '''Lock the repository store (.hg/store) and return a weak reference
1324 1325 to the lock. Use this before modifying the store (e.g. committing or
1325 1326 stripping). If you are opening a transaction, get a lock as well.)
1326 1327
1327 1328 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1328 1329 'wlock' first to avoid a dead-lock hazard.'''
1329 1330 l = self._lockref and self._lockref()
1330 1331 if l is not None and l.held:
1331 1332 l.lock()
1332 1333 return l
1333 1334
1334 1335 l = self._lock(self.svfs, "lock", wait, None,
1335 1336 self.invalidate, _('repository %s') % self.origroot)
1336 1337 self._lockref = weakref.ref(l)
1337 1338 return l
1338 1339
1339 1340 def _wlockchecktransaction(self):
1340 1341 if self.currenttransaction() is not None:
1341 1342 raise error.LockInheritanceContractViolation(
1342 1343 'wlock cannot be inherited in the middle of a transaction')
1343 1344
1344 1345 def wlock(self, wait=True):
1345 1346 '''Lock the non-store parts of the repository (everything under
1346 1347 .hg except .hg/store) and return a weak reference to the lock.
1347 1348
1348 1349 Use this before modifying files in .hg.
1349 1350
1350 1351 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1351 1352 'wlock' first to avoid a dead-lock hazard.'''
1352 1353 l = self._wlockref and self._wlockref()
1353 1354 if l is not None and l.held:
1354 1355 l.lock()
1355 1356 return l
1356 1357
1357 1358 # We do not need to check for non-waiting lock acquisition. Such
1358 1359 # acquisition would not cause dead-lock as they would just fail.
1359 1360 if wait and (self.ui.configbool('devel', 'all-warnings')
1360 1361 or self.ui.configbool('devel', 'check-locks')):
1361 1362 l = self._lockref and self._lockref()
1362 1363 if l is not None and l.held:
1363 1364 self.ui.develwarn('"wlock" acquired after "lock"')
1364 1365
1365 1366 def unlock():
1366 1367 if self.dirstate.pendingparentchange():
1367 1368 self.dirstate.invalidate()
1368 1369 else:
1369 1370 self.dirstate.write(None)
1370 1371
1371 1372 self._filecache['dirstate'].refresh()
1372 1373
1373 1374 l = self._lock(self.vfs, "wlock", wait, unlock,
1374 1375 self.invalidatedirstate, _('working directory of %s') %
1375 1376 self.origroot,
1376 1377 inheritchecker=self._wlockchecktransaction,
1377 1378 parentenvvar='HG_WLOCK_LOCKER')
1378 1379 self._wlockref = weakref.ref(l)
1379 1380 return l
1380 1381
1381 1382 def _currentlock(self, lockref):
1382 1383 """Returns the lock if it's held, or None if it's not."""
1383 1384 if lockref is None:
1384 1385 return None
1385 1386 l = lockref()
1386 1387 if l is None or not l.held:
1387 1388 return None
1388 1389 return l
1389 1390
1390 1391 def currentwlock(self):
1391 1392 """Returns the wlock if it's held, or None if it's not."""
1392 1393 return self._currentlock(self._wlockref)
1393 1394
1394 1395 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist):
1395 1396 """
1396 1397 commit an individual file as part of a larger transaction
1397 1398 """
1398 1399
1399 1400 fname = fctx.path()
1400 1401 fparent1 = manifest1.get(fname, nullid)
1401 1402 fparent2 = manifest2.get(fname, nullid)
1402 1403 if isinstance(fctx, context.filectx):
1403 1404 node = fctx.filenode()
1404 1405 if node in [fparent1, fparent2]:
1405 1406 self.ui.debug('reusing %s filelog entry\n' % fname)
1406 1407 return node
1407 1408
1408 1409 flog = self.file(fname)
1409 1410 meta = {}
1410 1411 copy = fctx.renamed()
1411 1412 if copy and copy[0] != fname:
1412 1413 # Mark the new revision of this file as a copy of another
1413 1414 # file. This copy data will effectively act as a parent
1414 1415 # of this new revision. If this is a merge, the first
1415 1416 # parent will be the nullid (meaning "look up the copy data")
1416 1417 # and the second one will be the other parent. For example:
1417 1418 #
1418 1419 # 0 --- 1 --- 3 rev1 changes file foo
1419 1420 # \ / rev2 renames foo to bar and changes it
1420 1421 # \- 2 -/ rev3 should have bar with all changes and
1421 1422 # should record that bar descends from
1422 1423 # bar in rev2 and foo in rev1
1423 1424 #
1424 1425 # this allows this merge to succeed:
1425 1426 #
1426 1427 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
1427 1428 # \ / merging rev3 and rev4 should use bar@rev2
1428 1429 # \- 2 --- 4 as the merge base
1429 1430 #
1430 1431
1431 1432 cfname = copy[0]
1432 1433 crev = manifest1.get(cfname)
1433 1434 newfparent = fparent2
1434 1435
1435 1436 if manifest2: # branch merge
1436 1437 if fparent2 == nullid or crev is None: # copied on remote side
1437 1438 if cfname in manifest2:
1438 1439 crev = manifest2[cfname]
1439 1440 newfparent = fparent1
1440 1441
1441 1442 # Here, we used to search backwards through history to try to find
1442 1443 # where the file copy came from if the source of a copy was not in
1443 1444 # the parent directory. However, this doesn't actually make sense to
1444 1445 # do (what does a copy from something not in your working copy even
1445 1446 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
1446 1447 # the user that copy information was dropped, so if they didn't
1447 1448 # expect this outcome it can be fixed, but this is the correct
1448 1449 # behavior in this circumstance.
1449 1450
1450 1451 if crev:
1451 1452 self.ui.debug(" %s: copy %s:%s\n" % (fname, cfname, hex(crev)))
1452 1453 meta["copy"] = cfname
1453 1454 meta["copyrev"] = hex(crev)
1454 1455 fparent1, fparent2 = nullid, newfparent
1455 1456 else:
1456 1457 self.ui.warn(_("warning: can't find ancestor for '%s' "
1457 1458 "copied from '%s'!\n") % (fname, cfname))
1458 1459
1459 1460 elif fparent1 == nullid:
1460 1461 fparent1, fparent2 = fparent2, nullid
1461 1462 elif fparent2 != nullid:
1462 1463 # is one parent an ancestor of the other?
1463 1464 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
1464 1465 if fparent1 in fparentancestors:
1465 1466 fparent1, fparent2 = fparent2, nullid
1466 1467 elif fparent2 in fparentancestors:
1467 1468 fparent2 = nullid
1468 1469
1469 1470 # is the file changed?
1470 1471 text = fctx.data()
1471 1472 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
1472 1473 changelist.append(fname)
1473 1474 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
1474 1475 # are just the flags changed during merge?
1475 1476 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
1476 1477 changelist.append(fname)
1477 1478
1478 1479 return fparent1
1479 1480
1480 1481 def checkcommitpatterns(self, wctx, vdirs, match, status, fail):
1481 1482 """check for commit arguments that aren't commitable"""
1482 1483 if match.isexact() or match.prefix():
1483 1484 matched = set(status.modified + status.added + status.removed)
1484 1485
1485 1486 for f in match.files():
1486 1487 f = self.dirstate.normalize(f)
1487 1488 if f == '.' or f in matched or f in wctx.substate:
1488 1489 continue
1489 1490 if f in status.deleted:
1490 1491 fail(f, _('file not found!'))
1491 1492 if f in vdirs: # visited directory
1492 1493 d = f + '/'
1493 1494 for mf in matched:
1494 1495 if mf.startswith(d):
1495 1496 break
1496 1497 else:
1497 1498 fail(f, _("no match under directory!"))
1498 1499 elif f not in self.dirstate:
1499 1500 fail(f, _("file not tracked!"))
1500 1501
1501 1502 @unfilteredmethod
1502 1503 def commit(self, text="", user=None, date=None, match=None, force=False,
1503 1504 editor=False, extra=None):
1504 1505 """Add a new revision to current repository.
1505 1506
1506 1507 Revision information is gathered from the working directory,
1507 1508 match can be used to filter the committed files. If editor is
1508 1509 supplied, it is called to get a commit message.
1509 1510 """
1510 1511 if extra is None:
1511 1512 extra = {}
1512 1513
1513 1514 def fail(f, msg):
1514 1515 raise error.Abort('%s: %s' % (f, msg))
1515 1516
1516 1517 if not match:
1517 1518 match = matchmod.always(self.root, '')
1518 1519
1519 1520 if not force:
1520 1521 vdirs = []
1521 1522 match.explicitdir = vdirs.append
1522 1523 match.bad = fail
1523 1524
1524 1525 wlock = lock = tr = None
1525 1526 try:
1526 1527 wlock = self.wlock()
1527 1528 lock = self.lock() # for recent changelog (see issue4368)
1528 1529
1529 1530 wctx = self[None]
1530 1531 merge = len(wctx.parents()) > 1
1531 1532
1532 1533 if not force and merge and match.ispartial():
1533 1534 raise error.Abort(_('cannot partially commit a merge '
1534 1535 '(do not specify files or patterns)'))
1535 1536
1536 1537 status = self.status(match=match, clean=force)
1537 1538 if force:
1538 1539 status.modified.extend(status.clean) # mq may commit clean files
1539 1540
1540 1541 # check subrepos
1541 1542 subs = []
1542 1543 commitsubs = set()
1543 1544 newstate = wctx.substate.copy()
1544 1545 # only manage subrepos and .hgsubstate if .hgsub is present
1545 1546 if '.hgsub' in wctx:
1546 1547 # we'll decide whether to track this ourselves, thanks
1547 1548 for c in status.modified, status.added, status.removed:
1548 1549 if '.hgsubstate' in c:
1549 1550 c.remove('.hgsubstate')
1550 1551
1551 1552 # compare current state to last committed state
1552 1553 # build new substate based on last committed state
1553 1554 oldstate = wctx.p1().substate
1554 1555 for s in sorted(newstate.keys()):
1555 1556 if not match(s):
1556 1557 # ignore working copy, use old state if present
1557 1558 if s in oldstate:
1558 1559 newstate[s] = oldstate[s]
1559 1560 continue
1560 1561 if not force:
1561 1562 raise error.Abort(
1562 1563 _("commit with new subrepo %s excluded") % s)
1563 1564 dirtyreason = wctx.sub(s).dirtyreason(True)
1564 1565 if dirtyreason:
1565 1566 if not self.ui.configbool('ui', 'commitsubrepos'):
1566 1567 raise error.Abort(dirtyreason,
1567 1568 hint=_("use --subrepos for recursive commit"))
1568 1569 subs.append(s)
1569 1570 commitsubs.add(s)
1570 1571 else:
1571 1572 bs = wctx.sub(s).basestate()
1572 1573 newstate[s] = (newstate[s][0], bs, newstate[s][2])
1573 1574 if oldstate.get(s, (None, None, None))[1] != bs:
1574 1575 subs.append(s)
1575 1576
1576 1577 # check for removed subrepos
1577 1578 for p in wctx.parents():
1578 1579 r = [s for s in p.substate if s not in newstate]
1579 1580 subs += [s for s in r if match(s)]
1580 1581 if subs:
1581 1582 if (not match('.hgsub') and
1582 1583 '.hgsub' in (wctx.modified() + wctx.added())):
1583 1584 raise error.Abort(
1584 1585 _("can't commit subrepos without .hgsub"))
1585 1586 status.modified.insert(0, '.hgsubstate')
1586 1587
1587 1588 elif '.hgsub' in status.removed:
1588 1589 # clean up .hgsubstate when .hgsub is removed
1589 1590 if ('.hgsubstate' in wctx and
1590 1591 '.hgsubstate' not in (status.modified + status.added +
1591 1592 status.removed)):
1592 1593 status.removed.insert(0, '.hgsubstate')
1593 1594
1594 1595 # make sure all explicit patterns are matched
1595 1596 if not force:
1596 1597 self.checkcommitpatterns(wctx, vdirs, match, status, fail)
1597 1598
1598 1599 cctx = context.workingcommitctx(self, status,
1599 1600 text, user, date, extra)
1600 1601
1601 1602 # internal config: ui.allowemptycommit
1602 1603 allowemptycommit = (wctx.branch() != wctx.p1().branch()
1603 1604 or extra.get('close') or merge or cctx.files()
1604 1605 or self.ui.configbool('ui', 'allowemptycommit'))
1605 1606 if not allowemptycommit:
1606 1607 return None
1607 1608
1608 1609 if merge and cctx.deleted():
1609 1610 raise error.Abort(_("cannot commit merge with missing files"))
1610 1611
1611 1612 ms = mergemod.mergestate.read(self)
1612 1613
1613 1614 if list(ms.unresolved()):
1614 1615 raise error.Abort(_('unresolved merge conflicts '
1615 1616 '(see "hg help resolve")'))
1616 1617 if ms.mdstate() != 's' or list(ms.driverresolved()):
1617 1618 raise error.Abort(_('driver-resolved merge conflicts'),
1618 1619 hint=_('run "hg resolve --all" to resolve'))
1619 1620
1620 1621 if editor:
1621 1622 cctx._text = editor(self, cctx, subs)
1622 1623 edited = (text != cctx._text)
1623 1624
1624 1625 # Save commit message in case this transaction gets rolled back
1625 1626 # (e.g. by a pretxncommit hook). Leave the content alone on
1626 1627 # the assumption that the user will use the same editor again.
1627 1628 msgfn = self.savecommitmessage(cctx._text)
1628 1629
1629 1630 # commit subs and write new state
1630 1631 if subs:
1631 1632 for s in sorted(commitsubs):
1632 1633 sub = wctx.sub(s)
1633 1634 self.ui.status(_('committing subrepository %s\n') %
1634 1635 subrepo.subrelpath(sub))
1635 1636 sr = sub.commit(cctx._text, user, date)
1636 1637 newstate[s] = (newstate[s][0], sr)
1637 1638 subrepo.writestate(self, newstate)
1638 1639
1639 1640 p1, p2 = self.dirstate.parents()
1640 1641 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
1641 1642 try:
1642 1643 self.hook("precommit", throw=True, parent1=hookp1,
1643 1644 parent2=hookp2)
1644 1645 tr = self.transaction('commit')
1645 1646 ret = self.commitctx(cctx, True)
1646 1647 except: # re-raises
1647 1648 if edited:
1648 1649 self.ui.write(
1649 1650 _('note: commit message saved in %s\n') % msgfn)
1650 1651 raise
1651 1652 # update bookmarks, dirstate and mergestate
1652 1653 bookmarks.update(self, [p1, p2], ret)
1653 1654 cctx.markcommitted(ret)
1654 1655 ms.reset()
1655 1656 tr.close()
1656 1657
1657 1658 finally:
1658 1659 lockmod.release(tr, lock, wlock)
1659 1660
1660 1661 def commithook(node=hex(ret), parent1=hookp1, parent2=hookp2):
1661 1662 # hack for command that use a temporary commit (eg: histedit)
1662 1663 # temporary commit got stripped before hook release
1663 1664 if self.changelog.hasnode(ret):
1664 1665 self.hook("commit", node=node, parent1=parent1,
1665 1666 parent2=parent2)
1666 1667 self._afterlock(commithook)
1667 1668 return ret
1668 1669
1669 1670 @unfilteredmethod
1670 1671 def commitctx(self, ctx, error=False):
1671 1672 """Add a new revision to current repository.
1672 1673 Revision information is passed via the context argument.
1673 1674 """
1674 1675
1675 1676 tr = None
1676 1677 p1, p2 = ctx.p1(), ctx.p2()
1677 1678 user = ctx.user()
1678 1679
1679 1680 lock = self.lock()
1680 1681 try:
1681 1682 tr = self.transaction("commit")
1682 1683 trp = weakref.proxy(tr)
1683 1684
1684 1685 if ctx.files():
1685 1686 m1 = p1.manifest()
1686 1687 m2 = p2.manifest()
1687 1688 m = m1.copy()
1688 1689
1689 1690 # check in files
1690 1691 added = []
1691 1692 changed = []
1692 1693 removed = list(ctx.removed())
1693 1694 linkrev = len(self)
1694 1695 self.ui.note(_("committing files:\n"))
1695 1696 for f in sorted(ctx.modified() + ctx.added()):
1696 1697 self.ui.note(f + "\n")
1697 1698 try:
1698 1699 fctx = ctx[f]
1699 1700 if fctx is None:
1700 1701 removed.append(f)
1701 1702 else:
1702 1703 added.append(f)
1703 1704 m[f] = self._filecommit(fctx, m1, m2, linkrev,
1704 1705 trp, changed)
1705 1706 m.setflag(f, fctx.flags())
1706 1707 except OSError as inst:
1707 1708 self.ui.warn(_("trouble committing %s!\n") % f)
1708 1709 raise
1709 1710 except IOError as inst:
1710 1711 errcode = getattr(inst, 'errno', errno.ENOENT)
1711 1712 if error or errcode and errcode != errno.ENOENT:
1712 1713 self.ui.warn(_("trouble committing %s!\n") % f)
1713 1714 raise
1714 1715
1715 1716 # update manifest
1716 1717 self.ui.note(_("committing manifest\n"))
1717 1718 removed = [f for f in sorted(removed) if f in m1 or f in m2]
1718 1719 drop = [f for f in removed if f in m]
1719 1720 for f in drop:
1720 1721 del m[f]
1721 1722 mn = self.manifest.add(m, trp, linkrev,
1722 1723 p1.manifestnode(), p2.manifestnode(),
1723 1724 added, drop)
1724 1725 files = changed + removed
1725 1726 else:
1726 1727 mn = p1.manifestnode()
1727 1728 files = []
1728 1729
1729 1730 # update changelog
1730 1731 self.ui.note(_("committing changelog\n"))
1731 1732 self.changelog.delayupdate(tr)
1732 1733 n = self.changelog.add(mn, files, ctx.description(),
1733 1734 trp, p1.node(), p2.node(),
1734 1735 user, ctx.date(), ctx.extra().copy())
1735 1736 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
1736 1737 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
1737 1738 parent2=xp2)
1738 1739 # set the new commit is proper phase
1739 1740 targetphase = subrepo.newcommitphase(self.ui, ctx)
1740 1741 if targetphase:
1741 1742 # retract boundary do not alter parent changeset.
1742 1743 # if a parent have higher the resulting phase will
1743 1744 # be compliant anyway
1744 1745 #
1745 1746 # if minimal phase was 0 we don't need to retract anything
1746 1747 phases.retractboundary(self, tr, targetphase, [n])
1747 1748 tr.close()
1748 1749 branchmap.updatecache(self.filtered('served'))
1749 1750 return n
1750 1751 finally:
1751 1752 if tr:
1752 1753 tr.release()
1753 1754 lock.release()
1754 1755
1755 1756 @unfilteredmethod
1756 1757 def destroying(self):
1757 1758 '''Inform the repository that nodes are about to be destroyed.
1758 1759 Intended for use by strip and rollback, so there's a common
1759 1760 place for anything that has to be done before destroying history.
1760 1761
1761 1762 This is mostly useful for saving state that is in memory and waiting
1762 1763 to be flushed when the current lock is released. Because a call to
1763 1764 destroyed is imminent, the repo will be invalidated causing those
1764 1765 changes to stay in memory (waiting for the next unlock), or vanish
1765 1766 completely.
1766 1767 '''
1767 1768 # When using the same lock to commit and strip, the phasecache is left
1768 1769 # dirty after committing. Then when we strip, the repo is invalidated,
1769 1770 # causing those changes to disappear.
1770 1771 if '_phasecache' in vars(self):
1771 1772 self._phasecache.write()
1772 1773
1773 1774 @unfilteredmethod
1774 1775 def destroyed(self):
1775 1776 '''Inform the repository that nodes have been destroyed.
1776 1777 Intended for use by strip and rollback, so there's a common
1777 1778 place for anything that has to be done after destroying history.
1778 1779 '''
1779 1780 # When one tries to:
1780 1781 # 1) destroy nodes thus calling this method (e.g. strip)
1781 1782 # 2) use phasecache somewhere (e.g. commit)
1782 1783 #
1783 1784 # then 2) will fail because the phasecache contains nodes that were
1784 1785 # removed. We can either remove phasecache from the filecache,
1785 1786 # causing it to reload next time it is accessed, or simply filter
1786 1787 # the removed nodes now and write the updated cache.
1787 1788 self._phasecache.filterunknown(self)
1788 1789 self._phasecache.write()
1789 1790
1790 1791 # update the 'served' branch cache to help read only server process
1791 1792 # Thanks to branchcache collaboration this is done from the nearest
1792 1793 # filtered subset and it is expected to be fast.
1793 1794 branchmap.updatecache(self.filtered('served'))
1794 1795
1795 1796 # Ensure the persistent tag cache is updated. Doing it now
1796 1797 # means that the tag cache only has to worry about destroyed
1797 1798 # heads immediately after a strip/rollback. That in turn
1798 1799 # guarantees that "cachetip == currenttip" (comparing both rev
1799 1800 # and node) always means no nodes have been added or destroyed.
1800 1801
1801 1802 # XXX this is suboptimal when qrefresh'ing: we strip the current
1802 1803 # head, refresh the tag cache, then immediately add a new head.
1803 1804 # But I think doing it this way is necessary for the "instant
1804 1805 # tag cache retrieval" case to work.
1805 1806 self.invalidate()
1806 1807
1807 1808 def walk(self, match, node=None):
1808 1809 '''
1809 1810 walk recursively through the directory tree or a given
1810 1811 changeset, finding all files matched by the match
1811 1812 function
1812 1813 '''
1813 1814 return self[node].walk(match)
1814 1815
1815 1816 def status(self, node1='.', node2=None, match=None,
1816 1817 ignored=False, clean=False, unknown=False,
1817 1818 listsubrepos=False):
1818 1819 '''a convenience method that calls node1.status(node2)'''
1819 1820 return self[node1].status(node2, match, ignored, clean, unknown,
1820 1821 listsubrepos)
1821 1822
1822 1823 def heads(self, start=None):
1823 1824 heads = self.changelog.heads(start)
1824 1825 # sort the output in rev descending order
1825 1826 return sorted(heads, key=self.changelog.rev, reverse=True)
1826 1827
1827 1828 def branchheads(self, branch=None, start=None, closed=False):
1828 1829 '''return a (possibly filtered) list of heads for the given branch
1829 1830
1830 1831 Heads are returned in topological order, from newest to oldest.
1831 1832 If branch is None, use the dirstate branch.
1832 1833 If start is not None, return only heads reachable from start.
1833 1834 If closed is True, return heads that are marked as closed as well.
1834 1835 '''
1835 1836 if branch is None:
1836 1837 branch = self[None].branch()
1837 1838 branches = self.branchmap()
1838 1839 if branch not in branches:
1839 1840 return []
1840 1841 # the cache returns heads ordered lowest to highest
1841 1842 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
1842 1843 if start is not None:
1843 1844 # filter out the heads that cannot be reached from startrev
1844 1845 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
1845 1846 bheads = [h for h in bheads if h in fbheads]
1846 1847 return bheads
1847 1848
1848 1849 def branches(self, nodes):
1849 1850 if not nodes:
1850 1851 nodes = [self.changelog.tip()]
1851 1852 b = []
1852 1853 for n in nodes:
1853 1854 t = n
1854 1855 while True:
1855 1856 p = self.changelog.parents(n)
1856 1857 if p[1] != nullid or p[0] == nullid:
1857 1858 b.append((t, n, p[0], p[1]))
1858 1859 break
1859 1860 n = p[0]
1860 1861 return b
1861 1862
1862 1863 def between(self, pairs):
1863 1864 r = []
1864 1865
1865 1866 for top, bottom in pairs:
1866 1867 n, l, i = top, [], 0
1867 1868 f = 1
1868 1869
1869 1870 while n != bottom and n != nullid:
1870 1871 p = self.changelog.parents(n)[0]
1871 1872 if i == f:
1872 1873 l.append(n)
1873 1874 f = f * 2
1874 1875 n = p
1875 1876 i += 1
1876 1877
1877 1878 r.append(l)
1878 1879
1879 1880 return r
1880 1881
1881 1882 def checkpush(self, pushop):
1882 1883 """Extensions can override this function if additional checks have
1883 1884 to be performed before pushing, or call it if they override push
1884 1885 command.
1885 1886 """
1886 1887 pass
1887 1888
1888 1889 @unfilteredpropertycache
1889 1890 def prepushoutgoinghooks(self):
1890 1891 """Return util.hooks consists of a pushop with repo, remote, outgoing
1891 1892 methods, which are called before pushing changesets.
1892 1893 """
1893 1894 return util.hooks()
1894 1895
1895 1896 def pushkey(self, namespace, key, old, new):
1896 1897 try:
1897 1898 tr = self.currenttransaction()
1898 1899 hookargs = {}
1899 1900 if tr is not None:
1900 1901 hookargs.update(tr.hookargs)
1901 1902 hookargs['namespace'] = namespace
1902 1903 hookargs['key'] = key
1903 1904 hookargs['old'] = old
1904 1905 hookargs['new'] = new
1905 1906 self.hook('prepushkey', throw=True, **hookargs)
1906 1907 except error.HookAbort as exc:
1907 1908 self.ui.write_err(_("pushkey-abort: %s\n") % exc)
1908 1909 if exc.hint:
1909 1910 self.ui.write_err(_("(%s)\n") % exc.hint)
1910 1911 return False
1911 1912 self.ui.debug('pushing key for "%s:%s"\n' % (namespace, key))
1912 1913 ret = pushkey.push(self, namespace, key, old, new)
1913 1914 def runhook():
1914 1915 self.hook('pushkey', namespace=namespace, key=key, old=old, new=new,
1915 1916 ret=ret)
1916 1917 self._afterlock(runhook)
1917 1918 return ret
1918 1919
1919 1920 def listkeys(self, namespace):
1920 1921 self.hook('prelistkeys', throw=True, namespace=namespace)
1921 1922 self.ui.debug('listing keys for "%s"\n' % namespace)
1922 1923 values = pushkey.list(self, namespace)
1923 1924 self.hook('listkeys', namespace=namespace, values=values)
1924 1925 return values
1925 1926
1926 1927 def debugwireargs(self, one, two, three=None, four=None, five=None):
1927 1928 '''used to test argument passing over the wire'''
1928 1929 return "%s %s %s %s %s" % (one, two, three, four, five)
1929 1930
1930 1931 def savecommitmessage(self, text):
1931 1932 fp = self.vfs('last-message.txt', 'wb')
1932 1933 try:
1933 1934 fp.write(text)
1934 1935 finally:
1935 1936 fp.close()
1936 1937 return self.pathto(fp.name[len(self.root) + 1:])
1937 1938
1938 1939 # used to avoid circular references so destructors work
1939 1940 def aftertrans(files):
1940 1941 renamefiles = [tuple(t) for t in files]
1941 1942 def a():
1942 1943 for vfs, src, dest in renamefiles:
1943 1944 try:
1944 1945 vfs.rename(src, dest)
1945 1946 except OSError: # journal file does not yet exist
1946 1947 pass
1947 1948 return a
1948 1949
1949 1950 def undoname(fn):
1950 1951 base, name = os.path.split(fn)
1951 1952 assert name.startswith('journal')
1952 1953 return os.path.join(base, name.replace('journal', 'undo', 1))
1953 1954
1954 1955 def instance(ui, path, create):
1955 1956 return localrepository(ui, util.urllocalpath(path), create)
1956 1957
1957 1958 def islocal(path):
1958 1959 return True
1959 1960
1960 1961 def newreporequirements(repo):
1961 1962 """Determine the set of requirements for a new local repository.
1962 1963
1963 1964 Extensions can wrap this function to specify custom requirements for
1964 1965 new repositories.
1965 1966 """
1966 1967 ui = repo.ui
1967 1968 requirements = set(['revlogv1'])
1968 1969 if ui.configbool('format', 'usestore', True):
1969 1970 requirements.add('store')
1970 1971 if ui.configbool('format', 'usefncache', True):
1971 1972 requirements.add('fncache')
1972 1973 if ui.configbool('format', 'dotencode', True):
1973 1974 requirements.add('dotencode')
1974 1975
1975 1976 if scmutil.gdinitconfig(ui):
1976 1977 requirements.add('generaldelta')
1977 1978 if ui.configbool('experimental', 'treemanifest', False):
1978 1979 requirements.add('treemanifest')
1979 1980 if ui.configbool('experimental', 'manifestv2', False):
1980 1981 requirements.add('manifestv2')
1981 1982
1982 1983 return requirements
@@ -1,186 +1,187 b''
1 1 # statichttprepo.py - simple http repository class for mercurial
2 2 #
3 3 # This provides read-only repo access to repositories exported via static http
4 4 #
5 5 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import
11 11
12 12 import errno
13 13 import os
14 import urllib
15 import urllib2
16 14
17 15 from .i18n import _
18 16 from . import (
19 17 byterange,
20 18 changelog,
21 19 error,
22 20 localrepo,
23 21 manifest,
24 22 namespaces,
25 23 scmutil,
26 24 store,
27 25 url,
28 26 util,
29 27 )
30 28
29 urlerr = util.urlerr
30 urlreq = util.urlreq
31
31 32 class httprangereader(object):
32 33 def __init__(self, url, opener):
33 34 # we assume opener has HTTPRangeHandler
34 35 self.url = url
35 36 self.pos = 0
36 37 self.opener = opener
37 38 self.name = url
38 39
39 40 def __enter__(self):
40 41 return self
41 42
42 43 def __exit__(self, exc_type, exc_value, traceback):
43 44 self.close()
44 45
45 46 def seek(self, pos):
46 47 self.pos = pos
47 48 def read(self, bytes=None):
48 req = urllib2.Request(self.url)
49 req = urlreq.request(self.url)
49 50 end = ''
50 51 if bytes:
51 52 end = self.pos + bytes - 1
52 53 if self.pos or end:
53 54 req.add_header('Range', 'bytes=%d-%s' % (self.pos, end))
54 55
55 56 try:
56 57 f = self.opener.open(req)
57 58 data = f.read()
58 59 code = f.code
59 except urllib2.HTTPError as inst:
60 except urlerr.httperror as inst:
60 61 num = inst.code == 404 and errno.ENOENT or None
61 62 raise IOError(num, inst)
62 except urllib2.URLError as inst:
63 except urlerr.urlerror as inst:
63 64 raise IOError(None, inst.reason[1])
64 65
65 66 if code == 200:
66 67 # HTTPRangeHandler does nothing if remote does not support
67 68 # Range headers and returns the full entity. Let's slice it.
68 69 if bytes:
69 70 data = data[self.pos:self.pos + bytes]
70 71 else:
71 72 data = data[self.pos:]
72 73 elif bytes:
73 74 data = data[:bytes]
74 75 self.pos += len(data)
75 76 return data
76 77 def readlines(self):
77 78 return self.read().splitlines(True)
78 79 def __iter__(self):
79 80 return iter(self.readlines())
80 81 def close(self):
81 82 pass
82 83
83 84 def build_opener(ui, authinfo):
84 85 # urllib cannot handle URLs with embedded user or passwd
85 86 urlopener = url.opener(ui, authinfo)
86 87 urlopener.add_handler(byterange.HTTPRangeHandler())
87 88
88 89 class statichttpvfs(scmutil.abstractvfs):
89 90 def __init__(self, base):
90 91 self.base = base
91 92
92 93 def __call__(self, path, mode='r', *args, **kw):
93 94 if mode not in ('r', 'rb'):
94 95 raise IOError('Permission denied')
95 f = "/".join((self.base, urllib.quote(path)))
96 f = "/".join((self.base, urlreq.quote(path)))
96 97 return httprangereader(f, urlopener)
97 98
98 99 def join(self, path):
99 100 if path:
100 101 return os.path.join(self.base, path)
101 102 else:
102 103 return self.base
103 104
104 105 return statichttpvfs
105 106
106 107 class statichttppeer(localrepo.localpeer):
107 108 def local(self):
108 109 return None
109 110 def canpush(self):
110 111 return False
111 112
112 113 class statichttprepository(localrepo.localrepository):
113 114 supported = localrepo.localrepository._basesupported
114 115
115 116 def __init__(self, ui, path):
116 117 self._url = path
117 118 self.ui = ui
118 119
119 120 self.root = path
120 121 u = util.url(path.rstrip('/') + "/.hg")
121 122 self.path, authinfo = u.authinfo()
122 123
123 124 opener = build_opener(ui, authinfo)
124 125 self.opener = opener(self.path)
125 126 self.vfs = self.opener
126 127 self._phasedefaults = []
127 128
128 129 self.names = namespaces.namespaces()
129 130
130 131 try:
131 132 requirements = scmutil.readrequires(self.vfs, self.supported)
132 133 except IOError as inst:
133 134 if inst.errno != errno.ENOENT:
134 135 raise
135 136 requirements = set()
136 137
137 138 # check if it is a non-empty old-style repository
138 139 try:
139 140 fp = self.vfs("00changelog.i")
140 141 fp.read(1)
141 142 fp.close()
142 143 except IOError as inst:
143 144 if inst.errno != errno.ENOENT:
144 145 raise
145 146 # we do not care about empty old-style repositories here
146 147 msg = _("'%s' does not appear to be an hg repository") % path
147 148 raise error.RepoError(msg)
148 149
149 150 # setup store
150 151 self.store = store.store(requirements, self.path, opener)
151 152 self.spath = self.store.path
152 153 self.svfs = self.store.opener
153 154 self.sjoin = self.store.join
154 155 self._filecache = {}
155 156 self.requirements = requirements
156 157
157 158 self.manifest = manifest.manifest(self.svfs)
158 159 self.changelog = changelog.changelog(self.svfs)
159 160 self._tags = None
160 161 self.nodetagscache = None
161 162 self._branchcaches = {}
162 163 self._revbranchcache = None
163 164 self.encodepats = None
164 165 self.decodepats = None
165 166 self._transref = None
166 167
167 168 def _restrictcapabilities(self, caps):
168 169 caps = super(statichttprepository, self)._restrictcapabilities(caps)
169 170 return caps.difference(["pushkey"])
170 171
171 172 def url(self):
172 173 return self._url
173 174
174 175 def local(self):
175 176 return False
176 177
177 178 def peer(self):
178 179 return statichttppeer(self)
179 180
180 181 def lock(self, wait=True):
181 182 raise error.Abort(_('cannot lock static-http repository'))
182 183
183 184 def instance(ui, path, create):
184 185 if create:
185 186 raise error.Abort(_('cannot create new static-http repository'))
186 187 return statichttprepository(ui, path[7:])
@@ -1,431 +1,433 b''
1 1 # template-filters.py - common template expansion filters
2 2 #
3 3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import cgi
11 11 import os
12 12 import re
13 13 import time
14 import urllib
15 14
16 15 from . import (
17 16 encoding,
18 17 hbisect,
19 18 node,
20 19 registrar,
21 20 templatekw,
22 21 util,
23 22 )
24 23
24 urlerr = util.urlerr
25 urlreq = util.urlreq
26
25 27 # filters are callables like:
26 28 # fn(obj)
27 29 # with:
28 30 # obj - object to be filtered (text, date, list and so on)
29 31 filters = {}
30 32
31 33 templatefilter = registrar.templatefilter(filters)
32 34
33 35 @templatefilter('addbreaks')
34 36 def addbreaks(text):
35 37 """Any text. Add an XHTML "<br />" tag before the end of
36 38 every line except the last.
37 39 """
38 40 return text.replace('\n', '<br/>\n')
39 41
40 42 agescales = [("year", 3600 * 24 * 365, 'Y'),
41 43 ("month", 3600 * 24 * 30, 'M'),
42 44 ("week", 3600 * 24 * 7, 'W'),
43 45 ("day", 3600 * 24, 'd'),
44 46 ("hour", 3600, 'h'),
45 47 ("minute", 60, 'm'),
46 48 ("second", 1, 's')]
47 49
48 50 @templatefilter('age')
49 51 def age(date, abbrev=False):
50 52 """Date. Returns a human-readable date/time difference between the
51 53 given date/time and the current date/time.
52 54 """
53 55
54 56 def plural(t, c):
55 57 if c == 1:
56 58 return t
57 59 return t + "s"
58 60 def fmt(t, c, a):
59 61 if abbrev:
60 62 return "%d%s" % (c, a)
61 63 return "%d %s" % (c, plural(t, c))
62 64
63 65 now = time.time()
64 66 then = date[0]
65 67 future = False
66 68 if then > now:
67 69 future = True
68 70 delta = max(1, int(then - now))
69 71 if delta > agescales[0][1] * 30:
70 72 return 'in the distant future'
71 73 else:
72 74 delta = max(1, int(now - then))
73 75 if delta > agescales[0][1] * 2:
74 76 return util.shortdate(date)
75 77
76 78 for t, s, a in agescales:
77 79 n = delta // s
78 80 if n >= 2 or s == 1:
79 81 if future:
80 82 return '%s from now' % fmt(t, n, a)
81 83 return '%s ago' % fmt(t, n, a)
82 84
83 85 @templatefilter('basename')
84 86 def basename(path):
85 87 """Any text. Treats the text as a path, and returns the last
86 88 component of the path after splitting by the path separator
87 89 (ignoring trailing separators). For example, "foo/bar/baz" becomes
88 90 "baz" and "foo/bar//" becomes "bar".
89 91 """
90 92 return os.path.basename(path)
91 93
92 94 @templatefilter('count')
93 95 def count(i):
94 96 """List or text. Returns the length as an integer."""
95 97 return len(i)
96 98
97 99 @templatefilter('domain')
98 100 def domain(author):
99 101 """Any text. Finds the first string that looks like an email
100 102 address, and extracts just the domain component. Example: ``User
101 103 <user@example.com>`` becomes ``example.com``.
102 104 """
103 105 f = author.find('@')
104 106 if f == -1:
105 107 return ''
106 108 author = author[f + 1:]
107 109 f = author.find('>')
108 110 if f >= 0:
109 111 author = author[:f]
110 112 return author
111 113
112 114 @templatefilter('email')
113 115 def email(text):
114 116 """Any text. Extracts the first string that looks like an email
115 117 address. Example: ``User <user@example.com>`` becomes
116 118 ``user@example.com``.
117 119 """
118 120 return util.email(text)
119 121
120 122 @templatefilter('escape')
121 123 def escape(text):
122 124 """Any text. Replaces the special XML/XHTML characters "&", "<"
123 125 and ">" with XML entities, and filters out NUL characters.
124 126 """
125 127 return cgi.escape(text.replace('\0', ''), True)
126 128
127 129 para_re = None
128 130 space_re = None
129 131
130 132 def fill(text, width, initindent='', hangindent=''):
131 133 '''fill many paragraphs with optional indentation.'''
132 134 global para_re, space_re
133 135 if para_re is None:
134 136 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
135 137 space_re = re.compile(r' +')
136 138
137 139 def findparas():
138 140 start = 0
139 141 while True:
140 142 m = para_re.search(text, start)
141 143 if not m:
142 144 uctext = unicode(text[start:], encoding.encoding)
143 145 w = len(uctext)
144 146 while 0 < w and uctext[w - 1].isspace():
145 147 w -= 1
146 148 yield (uctext[:w].encode(encoding.encoding),
147 149 uctext[w:].encode(encoding.encoding))
148 150 break
149 151 yield text[start:m.start(0)], m.group(1)
150 152 start = m.end(1)
151 153
152 154 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
153 155 width, initindent, hangindent) + rest
154 156 for para, rest in findparas()])
155 157
156 158 @templatefilter('fill68')
157 159 def fill68(text):
158 160 """Any text. Wraps the text to fit in 68 columns."""
159 161 return fill(text, 68)
160 162
161 163 @templatefilter('fill76')
162 164 def fill76(text):
163 165 """Any text. Wraps the text to fit in 76 columns."""
164 166 return fill(text, 76)
165 167
166 168 @templatefilter('firstline')
167 169 def firstline(text):
168 170 """Any text. Returns the first line of text."""
169 171 try:
170 172 return text.splitlines(True)[0].rstrip('\r\n')
171 173 except IndexError:
172 174 return ''
173 175
174 176 @templatefilter('hex')
175 177 def hexfilter(text):
176 178 """Any text. Convert a binary Mercurial node identifier into
177 179 its long hexadecimal representation.
178 180 """
179 181 return node.hex(text)
180 182
181 183 @templatefilter('hgdate')
182 184 def hgdate(text):
183 185 """Date. Returns the date as a pair of numbers: "1157407993
184 186 25200" (Unix timestamp, timezone offset).
185 187 """
186 188 return "%d %d" % text
187 189
188 190 @templatefilter('isodate')
189 191 def isodate(text):
190 192 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
191 193 +0200".
192 194 """
193 195 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
194 196
195 197 @templatefilter('isodatesec')
196 198 def isodatesec(text):
197 199 """Date. Returns the date in ISO 8601 format, including
198 200 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
199 201 filter.
200 202 """
201 203 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
202 204
203 205 def indent(text, prefix):
204 206 '''indent each non-empty line of text after first with prefix.'''
205 207 lines = text.splitlines()
206 208 num_lines = len(lines)
207 209 endswithnewline = text[-1:] == '\n'
208 210 def indenter():
209 211 for i in xrange(num_lines):
210 212 l = lines[i]
211 213 if i and l.strip():
212 214 yield prefix
213 215 yield l
214 216 if i < num_lines - 1 or endswithnewline:
215 217 yield '\n'
216 218 return "".join(indenter())
217 219
218 220 @templatefilter('json')
219 221 def json(obj):
220 222 if obj is None or obj is False or obj is True:
221 223 return {None: 'null', False: 'false', True: 'true'}[obj]
222 224 elif isinstance(obj, int) or isinstance(obj, float):
223 225 return str(obj)
224 226 elif isinstance(obj, str):
225 227 return '"%s"' % encoding.jsonescape(obj, paranoid=True)
226 228 elif util.safehasattr(obj, 'keys'):
227 229 out = []
228 230 for k, v in sorted(obj.iteritems()):
229 231 s = '%s: %s' % (json(k), json(v))
230 232 out.append(s)
231 233 return '{' + ', '.join(out) + '}'
232 234 elif util.safehasattr(obj, '__iter__'):
233 235 out = []
234 236 for i in obj:
235 237 out.append(json(i))
236 238 return '[' + ', '.join(out) + ']'
237 239 elif util.safehasattr(obj, '__call__'):
238 240 return json(obj())
239 241 else:
240 242 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
241 243
242 244 @templatefilter('lower')
243 245 def lower(text):
244 246 """Any text. Converts the text to lowercase."""
245 247 return encoding.lower(text)
246 248
247 249 @templatefilter('nonempty')
248 250 def nonempty(str):
249 251 """Any text. Returns '(none)' if the string is empty."""
250 252 return str or "(none)"
251 253
252 254 @templatefilter('obfuscate')
253 255 def obfuscate(text):
254 256 """Any text. Returns the input text rendered as a sequence of
255 257 XML entities.
256 258 """
257 259 text = unicode(text, encoding.encoding, 'replace')
258 260 return ''.join(['&#%d;' % ord(c) for c in text])
259 261
260 262 @templatefilter('permissions')
261 263 def permissions(flags):
262 264 if "l" in flags:
263 265 return "lrwxrwxrwx"
264 266 if "x" in flags:
265 267 return "-rwxr-xr-x"
266 268 return "-rw-r--r--"
267 269
268 270 @templatefilter('person')
269 271 def person(author):
270 272 """Any text. Returns the name before an email address,
271 273 interpreting it as per RFC 5322.
272 274
273 275 >>> person('foo@bar')
274 276 'foo'
275 277 >>> person('Foo Bar <foo@bar>')
276 278 'Foo Bar'
277 279 >>> person('"Foo Bar" <foo@bar>')
278 280 'Foo Bar'
279 281 >>> person('"Foo \"buz\" Bar" <foo@bar>')
280 282 'Foo "buz" Bar'
281 283 >>> # The following are invalid, but do exist in real-life
282 284 ...
283 285 >>> person('Foo "buz" Bar <foo@bar>')
284 286 'Foo "buz" Bar'
285 287 >>> person('"Foo Bar <foo@bar>')
286 288 'Foo Bar'
287 289 """
288 290 if '@' not in author:
289 291 return author
290 292 f = author.find('<')
291 293 if f != -1:
292 294 return author[:f].strip(' "').replace('\\"', '"')
293 295 f = author.find('@')
294 296 return author[:f].replace('.', ' ')
295 297
296 298 @templatefilter('revescape')
297 299 def revescape(text):
298 300 """Any text. Escapes all "special" characters, except @.
299 301 Forward slashes are escaped twice to prevent web servers from prematurely
300 302 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
301 303 """
302 return urllib.quote(text, safe='/@').replace('/', '%252F')
304 return urlreq.quote(text, safe='/@').replace('/', '%252F')
303 305
304 306 @templatefilter('rfc3339date')
305 307 def rfc3339date(text):
306 308 """Date. Returns a date using the Internet date format
307 309 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
308 310 """
309 311 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
310 312
311 313 @templatefilter('rfc822date')
312 314 def rfc822date(text):
313 315 """Date. Returns a date using the same format used in email
314 316 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
315 317 """
316 318 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
317 319
318 320 @templatefilter('short')
319 321 def short(text):
320 322 """Changeset hash. Returns the short form of a changeset hash,
321 323 i.e. a 12 hexadecimal digit string.
322 324 """
323 325 return text[:12]
324 326
325 327 @templatefilter('shortbisect')
326 328 def shortbisect(text):
327 329 """Any text. Treats `text` as a bisection status, and
328 330 returns a single-character representing the status (G: good, B: bad,
329 331 S: skipped, U: untested, I: ignored). Returns single space if `text`
330 332 is not a valid bisection status.
331 333 """
332 334 return hbisect.shortlabel(text) or ' '
333 335
334 336 @templatefilter('shortdate')
335 337 def shortdate(text):
336 338 """Date. Returns a date like "2006-09-18"."""
337 339 return util.shortdate(text)
338 340
339 341 @templatefilter('splitlines')
340 342 def splitlines(text):
341 343 """Any text. Split text into a list of lines."""
342 344 return templatekw.showlist('line', text.splitlines(), 'lines')
343 345
344 346 @templatefilter('stringescape')
345 347 def stringescape(text):
346 348 return text.encode('string_escape')
347 349
348 350 @templatefilter('stringify')
349 351 def stringify(thing):
350 352 """Any type. Turns the value into text by converting values into
351 353 text and concatenating them.
352 354 """
353 355 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
354 356 return "".join([stringify(t) for t in thing if t is not None])
355 357 if thing is None:
356 358 return ""
357 359 return str(thing)
358 360
359 361 @templatefilter('stripdir')
360 362 def stripdir(text):
361 363 """Treat the text as path and strip a directory level, if
362 364 possible. For example, "foo" and "foo/bar" becomes "foo".
363 365 """
364 366 dir = os.path.dirname(text)
365 367 if dir == "":
366 368 return os.path.basename(text)
367 369 else:
368 370 return dir
369 371
370 372 @templatefilter('tabindent')
371 373 def tabindent(text):
372 374 """Any text. Returns the text, with every non-empty line
373 375 except the first starting with a tab character.
374 376 """
375 377 return indent(text, '\t')
376 378
377 379 @templatefilter('upper')
378 380 def upper(text):
379 381 """Any text. Converts the text to uppercase."""
380 382 return encoding.upper(text)
381 383
382 384 @templatefilter('urlescape')
383 385 def urlescape(text):
384 386 """Any text. Escapes all "special" characters. For example,
385 387 "foo bar" becomes "foo%20bar".
386 388 """
387 return urllib.quote(text)
389 return urlreq.quote(text)
388 390
389 391 @templatefilter('user')
390 392 def userfilter(text):
391 393 """Any text. Returns a short representation of a user name or email
392 394 address."""
393 395 return util.shortuser(text)
394 396
395 397 @templatefilter('emailuser')
396 398 def emailuser(text):
397 399 """Any text. Returns the user portion of an email address."""
398 400 return util.emailuser(text)
399 401
400 402 @templatefilter('utf8')
401 403 def utf8(text):
402 404 """Any text. Converts from the local character encoding to UTF-8."""
403 405 return encoding.fromlocal(text)
404 406
405 407 @templatefilter('xmlescape')
406 408 def xmlescape(text):
407 409 text = (text
408 410 .replace('&', '&amp;')
409 411 .replace('<', '&lt;')
410 412 .replace('>', '&gt;')
411 413 .replace('"', '&quot;')
412 414 .replace("'", '&#39;')) # &apos; invalid in HTML
413 415 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
414 416
415 417 def websub(text, websubtable):
416 418 """:websub: Any text. Only applies to hgweb. Applies the regular
417 419 expression replacements defined in the websub section.
418 420 """
419 421 if websubtable:
420 422 for regexp, format in websubtable:
421 423 text = regexp.sub(format, text)
422 424 return text
423 425
424 426 def loadfilter(ui, extname, registrarobj):
425 427 """Load template filter from specified registrarobj
426 428 """
427 429 for name, func in registrarobj._table.iteritems():
428 430 filters[name] = func
429 431
430 432 # tell hggettext to extract docstrings from these functions:
431 433 i18nfunctions = filters.values()
@@ -1,513 +1,514 b''
1 1 # url.py - HTTP handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import
11 11
12 12 import base64
13 13 import httplib
14 14 import os
15 15 import socket
16 import urllib
17 import urllib2
18 16
19 17 from .i18n import _
20 18 from . import (
21 19 error,
22 20 httpconnection as httpconnectionmod,
23 21 keepalive,
24 22 sslutil,
25 23 util,
26 24 )
27 25 stringio = util.stringio
28 26
29 class passwordmgr(urllib2.HTTPPasswordMgrWithDefaultRealm):
27 urlerr = util.urlerr
28 urlreq = util.urlreq
29
30 class passwordmgr(urlreq.httppasswordmgrwithdefaultrealm):
30 31 def __init__(self, ui):
31 urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
32 urlreq.httppasswordmgrwithdefaultrealm.__init__(self)
32 33 self.ui = ui
33 34
34 35 def find_user_password(self, realm, authuri):
35 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
36 authinfo = urlreq.httppasswordmgrwithdefaultrealm.find_user_password(
36 37 self, realm, authuri)
37 38 user, passwd = authinfo
38 39 if user and passwd:
39 40 self._writedebug(user, passwd)
40 41 return (user, passwd)
41 42
42 43 if not user or not passwd:
43 44 res = httpconnectionmod.readauthforuri(self.ui, authuri, user)
44 45 if res:
45 46 group, auth = res
46 47 user, passwd = auth.get('username'), auth.get('password')
47 48 self.ui.debug("using auth.%s.* for authentication\n" % group)
48 49 if not user or not passwd:
49 50 u = util.url(authuri)
50 51 u.query = None
51 52 if not self.ui.interactive():
52 53 raise error.Abort(_('http authorization required for %s') %
53 54 util.hidepassword(str(u)))
54 55
55 56 self.ui.write(_("http authorization required for %s\n") %
56 57 util.hidepassword(str(u)))
57 58 self.ui.write(_("realm: %s\n") % realm)
58 59 if user:
59 60 self.ui.write(_("user: %s\n") % user)
60 61 else:
61 62 user = self.ui.prompt(_("user:"), default=None)
62 63
63 64 if not passwd:
64 65 passwd = self.ui.getpass()
65 66
66 67 self.add_password(realm, authuri, user, passwd)
67 68 self._writedebug(user, passwd)
68 69 return (user, passwd)
69 70
70 71 def _writedebug(self, user, passwd):
71 72 msg = _('http auth: user %s, password %s\n')
72 73 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
73 74
74 75 def find_stored_password(self, authuri):
75 return urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
76 return urlreq.httppasswordmgrwithdefaultrealm.find_user_password(
76 77 self, None, authuri)
77 78
78 class proxyhandler(urllib2.ProxyHandler):
79 class proxyhandler(urlreq.proxyhandler):
79 80 def __init__(self, ui):
80 81 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
81 82 # XXX proxyauthinfo = None
82 83
83 84 if proxyurl:
84 85 # proxy can be proper url or host[:port]
85 86 if not (proxyurl.startswith('http:') or
86 87 proxyurl.startswith('https:')):
87 88 proxyurl = 'http://' + proxyurl + '/'
88 89 proxy = util.url(proxyurl)
89 90 if not proxy.user:
90 91 proxy.user = ui.config("http_proxy", "user")
91 92 proxy.passwd = ui.config("http_proxy", "passwd")
92 93
93 94 # see if we should use a proxy for this url
94 95 no_list = ["localhost", "127.0.0.1"]
95 96 no_list.extend([p.lower() for
96 97 p in ui.configlist("http_proxy", "no")])
97 98 no_list.extend([p.strip().lower() for
98 99 p in os.getenv("no_proxy", '').split(',')
99 100 if p.strip()])
100 101 # "http_proxy.always" config is for running tests on localhost
101 102 if ui.configbool("http_proxy", "always"):
102 103 self.no_list = []
103 104 else:
104 105 self.no_list = no_list
105 106
106 107 proxyurl = str(proxy)
107 108 proxies = {'http': proxyurl, 'https': proxyurl}
108 109 ui.debug('proxying through http://%s:%s\n' %
109 110 (proxy.host, proxy.port))
110 111 else:
111 112 proxies = {}
112 113
113 114 # urllib2 takes proxy values from the environment and those
114 115 # will take precedence if found. So, if there's a config entry
115 116 # defining a proxy, drop the environment ones
116 117 if ui.config("http_proxy", "host"):
117 118 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
118 119 try:
119 120 if env in os.environ:
120 121 del os.environ[env]
121 122 except OSError:
122 123 pass
123 124
124 urllib2.ProxyHandler.__init__(self, proxies)
125 urlreq.proxyhandler.__init__(self, proxies)
125 126 self.ui = ui
126 127
127 128 def proxy_open(self, req, proxy, type_):
128 129 host = req.get_host().split(':')[0]
129 130 for e in self.no_list:
130 131 if host == e:
131 132 return None
132 133 if e.startswith('*.') and host.endswith(e[2:]):
133 134 return None
134 135 if e.startswith('.') and host.endswith(e[1:]):
135 136 return None
136 137
137 return urllib2.ProxyHandler.proxy_open(self, req, proxy, type_)
138 return urlreq.proxyhandler.proxy_open(self, req, proxy, type_)
138 139
139 140 def _gen_sendfile(orgsend):
140 141 def _sendfile(self, data):
141 142 # send a file
142 143 if isinstance(data, httpconnectionmod.httpsendfile):
143 144 # if auth required, some data sent twice, so rewind here
144 145 data.seek(0)
145 146 for chunk in util.filechunkiter(data):
146 147 orgsend(self, chunk)
147 148 else:
148 149 orgsend(self, data)
149 150 return _sendfile
150 151
151 has_https = util.safehasattr(urllib2, 'HTTPSHandler')
152 has_https = util.safehasattr(urlreq, 'httpshandler')
152 153 if has_https:
153 154 try:
154 155 _create_connection = socket.create_connection
155 156 except AttributeError:
156 157 _GLOBAL_DEFAULT_TIMEOUT = object()
157 158
158 159 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
159 160 source_address=None):
160 161 # lifted from Python 2.6
161 162
162 163 msg = "getaddrinfo returns an empty list"
163 164 host, port = address
164 165 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
165 166 af, socktype, proto, canonname, sa = res
166 167 sock = None
167 168 try:
168 169 sock = socket.socket(af, socktype, proto)
169 170 if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
170 171 sock.settimeout(timeout)
171 172 if source_address:
172 173 sock.bind(source_address)
173 174 sock.connect(sa)
174 175 return sock
175 176
176 177 except socket.error as msg:
177 178 if sock is not None:
178 179 sock.close()
179 180
180 181 raise socket.error(msg)
181 182
182 183 class httpconnection(keepalive.HTTPConnection):
183 184 # must be able to send big bundle as stream.
184 185 send = _gen_sendfile(keepalive.HTTPConnection.send)
185 186
186 187 def connect(self):
187 188 if has_https and self.realhostport: # use CONNECT proxy
188 189 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
189 190 self.sock.connect((self.host, self.port))
190 191 if _generic_proxytunnel(self):
191 192 # we do not support client X.509 certificates
192 193 self.sock = sslutil.wrapsocket(self.sock, None, None, None,
193 194 serverhostname=self.host)
194 195 else:
195 196 keepalive.HTTPConnection.connect(self)
196 197
197 198 def getresponse(self):
198 199 proxyres = getattr(self, 'proxyres', None)
199 200 if proxyres:
200 201 if proxyres.will_close:
201 202 self.close()
202 203 self.proxyres = None
203 204 return proxyres
204 205 return keepalive.HTTPConnection.getresponse(self)
205 206
206 207 # general transaction handler to support different ways to handle
207 208 # HTTPS proxying before and after Python 2.6.3.
208 209 def _generic_start_transaction(handler, h, req):
209 210 tunnel_host = getattr(req, '_tunnel_host', None)
210 211 if tunnel_host:
211 212 if tunnel_host[:7] not in ['http://', 'https:/']:
212 213 tunnel_host = 'https://' + tunnel_host
213 214 new_tunnel = True
214 215 else:
215 216 tunnel_host = req.get_selector()
216 217 new_tunnel = False
217 218
218 219 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
219 220 u = util.url(tunnel_host)
220 221 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
221 222 h.realhostport = ':'.join([u.host, (u.port or '443')])
222 223 h.headers = req.headers.copy()
223 224 h.headers.update(handler.parent.addheaders)
224 225 return
225 226
226 227 h.realhostport = None
227 228 h.headers = None
228 229
229 230 def _generic_proxytunnel(self):
230 231 proxyheaders = dict(
231 232 [(x, self.headers[x]) for x in self.headers
232 233 if x.lower().startswith('proxy-')])
233 234 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
234 235 for header in proxyheaders.iteritems():
235 236 self.send('%s: %s\r\n' % header)
236 237 self.send('\r\n')
237 238
238 239 # majority of the following code is duplicated from
239 240 # httplib.HTTPConnection as there are no adequate places to
240 241 # override functions to provide the needed functionality
241 242 res = self.response_class(self.sock,
242 243 strict=self.strict,
243 244 method=self._method)
244 245
245 246 while True:
246 247 version, status, reason = res._read_status()
247 248 if status != httplib.CONTINUE:
248 249 break
249 250 while True:
250 251 skip = res.fp.readline().strip()
251 252 if not skip:
252 253 break
253 254 res.status = status
254 255 res.reason = reason.strip()
255 256
256 257 if res.status == 200:
257 258 while True:
258 259 line = res.fp.readline()
259 260 if line == '\r\n':
260 261 break
261 262 return True
262 263
263 264 if version == 'HTTP/1.0':
264 265 res.version = 10
265 266 elif version.startswith('HTTP/1.'):
266 267 res.version = 11
267 268 elif version == 'HTTP/0.9':
268 269 res.version = 9
269 270 else:
270 271 raise httplib.UnknownProtocol(version)
271 272
272 273 if res.version == 9:
273 274 res.length = None
274 275 res.chunked = 0
275 276 res.will_close = 1
276 277 res.msg = httplib.HTTPMessage(stringio())
277 278 return False
278 279
279 280 res.msg = httplib.HTTPMessage(res.fp)
280 281 res.msg.fp = None
281 282
282 283 # are we using the chunked-style of transfer encoding?
283 284 trenc = res.msg.getheader('transfer-encoding')
284 285 if trenc and trenc.lower() == "chunked":
285 286 res.chunked = 1
286 287 res.chunk_left = None
287 288 else:
288 289 res.chunked = 0
289 290
290 291 # will the connection close at the end of the response?
291 292 res.will_close = res._check_close()
292 293
293 294 # do we have a Content-Length?
294 295 # NOTE: RFC 2616, section 4.4, #3 says we ignore this if
295 296 # transfer-encoding is "chunked"
296 297 length = res.msg.getheader('content-length')
297 298 if length and not res.chunked:
298 299 try:
299 300 res.length = int(length)
300 301 except ValueError:
301 302 res.length = None
302 303 else:
303 304 if res.length < 0: # ignore nonsensical negative lengths
304 305 res.length = None
305 306 else:
306 307 res.length = None
307 308
308 309 # does the body have a fixed length? (of zero)
309 310 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
310 311 100 <= status < 200 or # 1xx codes
311 312 res._method == 'HEAD'):
312 313 res.length = 0
313 314
314 315 # if the connection remains open, and we aren't using chunked, and
315 316 # a content-length was not provided, then assume that the connection
316 317 # WILL close.
317 318 if (not res.will_close and
318 319 not res.chunked and
319 320 res.length is None):
320 321 res.will_close = 1
321 322
322 323 self.proxyres = res
323 324
324 325 return False
325 326
326 327 class httphandler(keepalive.HTTPHandler):
327 328 def http_open(self, req):
328 329 return self.do_open(httpconnection, req)
329 330
330 331 def _start_transaction(self, h, req):
331 332 _generic_start_transaction(self, h, req)
332 333 return keepalive.HTTPHandler._start_transaction(self, h, req)
333 334
334 335 if has_https:
335 336 class httpsconnection(httplib.HTTPConnection):
336 337 response_class = keepalive.HTTPResponse
337 338 default_port = httplib.HTTPS_PORT
338 339 # must be able to send big bundle as stream.
339 340 send = _gen_sendfile(keepalive.safesend)
340 341 getresponse = keepalive.wrapgetresponse(httplib.HTTPConnection)
341 342
342 343 def __init__(self, host, port=None, key_file=None, cert_file=None,
343 344 *args, **kwargs):
344 345 httplib.HTTPConnection.__init__(self, host, port, *args, **kwargs)
345 346 self.key_file = key_file
346 347 self.cert_file = cert_file
347 348
348 349 def connect(self):
349 350 self.sock = _create_connection((self.host, self.port))
350 351
351 352 host = self.host
352 353 if self.realhostport: # use CONNECT proxy
353 354 _generic_proxytunnel(self)
354 355 host = self.realhostport.rsplit(':', 1)[0]
355 356 self.sock = sslutil.wrapsocket(
356 357 self.sock, self.key_file, self.cert_file, serverhostname=host,
357 358 **sslutil.sslkwargs(self.ui, host))
358 359 sslutil.validator(self.ui, host)(self.sock)
359 360
360 class httpshandler(keepalive.KeepAliveHandler, urllib2.HTTPSHandler):
361 class httpshandler(keepalive.KeepAliveHandler, urlreq.httpshandler):
361 362 def __init__(self, ui):
362 363 keepalive.KeepAliveHandler.__init__(self)
363 urllib2.HTTPSHandler.__init__(self)
364 urlreq.httpshandler.__init__(self)
364 365 self.ui = ui
365 366 self.pwmgr = passwordmgr(self.ui)
366 367
367 368 def _start_transaction(self, h, req):
368 369 _generic_start_transaction(self, h, req)
369 370 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
370 371
371 372 def https_open(self, req):
372 373 # req.get_full_url() does not contain credentials and we may
373 374 # need them to match the certificates.
374 375 url = req.get_full_url()
375 376 user, password = self.pwmgr.find_stored_password(url)
376 377 res = httpconnectionmod.readauthforuri(self.ui, url, user)
377 378 if res:
378 379 group, auth = res
379 380 self.auth = auth
380 381 self.ui.debug("using auth.%s.* for authentication\n" % group)
381 382 else:
382 383 self.auth = None
383 384 return self.do_open(self._makeconnection, req)
384 385
385 386 def _makeconnection(self, host, port=None, *args, **kwargs):
386 387 keyfile = None
387 388 certfile = None
388 389
389 390 if len(args) >= 1: # key_file
390 391 keyfile = args[0]
391 392 if len(args) >= 2: # cert_file
392 393 certfile = args[1]
393 394 args = args[2:]
394 395
395 396 # if the user has specified different key/cert files in
396 397 # hgrc, we prefer these
397 398 if self.auth and 'key' in self.auth and 'cert' in self.auth:
398 399 keyfile = self.auth['key']
399 400 certfile = self.auth['cert']
400 401
401 402 conn = httpsconnection(host, port, keyfile, certfile, *args,
402 403 **kwargs)
403 404 conn.ui = self.ui
404 405 return conn
405 406
406 class httpdigestauthhandler(urllib2.HTTPDigestAuthHandler):
407 class httpdigestauthhandler(urlreq.httpdigestauthhandler):
407 408 def __init__(self, *args, **kwargs):
408 urllib2.HTTPDigestAuthHandler.__init__(self, *args, **kwargs)
409 urlreq.httpdigestauthhandler.__init__(self, *args, **kwargs)
409 410 self.retried_req = None
410 411
411 412 def reset_retry_count(self):
412 413 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
413 414 # forever. We disable reset_retry_count completely and reset in
414 415 # http_error_auth_reqed instead.
415 416 pass
416 417
417 418 def http_error_auth_reqed(self, auth_header, host, req, headers):
418 419 # Reset the retry counter once for each request.
419 420 if req is not self.retried_req:
420 421 self.retried_req = req
421 422 self.retried = 0
422 return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed(
423 return urlreq.httpdigestauthhandler.http_error_auth_reqed(
423 424 self, auth_header, host, req, headers)
424 425
425 class httpbasicauthhandler(urllib2.HTTPBasicAuthHandler):
426 class httpbasicauthhandler(urlreq.httpbasicauthhandler):
426 427 def __init__(self, *args, **kwargs):
427 428 self.auth = None
428 urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs)
429 urlreq.httpbasicauthhandler.__init__(self, *args, **kwargs)
429 430 self.retried_req = None
430 431
431 432 def http_request(self, request):
432 433 if self.auth:
433 434 request.add_unredirected_header(self.auth_header, self.auth)
434 435
435 436 return request
436 437
437 438 def https_request(self, request):
438 439 if self.auth:
439 440 request.add_unredirected_header(self.auth_header, self.auth)
440 441
441 442 return request
442 443
443 444 def reset_retry_count(self):
444 445 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
445 446 # forever. We disable reset_retry_count completely and reset in
446 447 # http_error_auth_reqed instead.
447 448 pass
448 449
449 450 def http_error_auth_reqed(self, auth_header, host, req, headers):
450 451 # Reset the retry counter once for each request.
451 452 if req is not self.retried_req:
452 453 self.retried_req = req
453 454 self.retried = 0
454 return urllib2.HTTPBasicAuthHandler.http_error_auth_reqed(
455 return urlreq.httpbasicauthhandler.http_error_auth_reqed(
455 456 self, auth_header, host, req, headers)
456 457
457 458 def retry_http_basic_auth(self, host, req, realm):
458 459 user, pw = self.passwd.find_user_password(realm, req.get_full_url())
459 460 if pw is not None:
460 461 raw = "%s:%s" % (user, pw)
461 462 auth = 'Basic %s' % base64.b64encode(raw).strip()
462 463 if req.headers.get(self.auth_header, None) == auth:
463 464 return None
464 465 self.auth = auth
465 466 req.add_unredirected_header(self.auth_header, auth)
466 467 return self.parent.open(req)
467 468 else:
468 469 return None
469 470
470 471 handlerfuncs = []
471 472
472 473 def opener(ui, authinfo=None):
473 474 '''
474 475 construct an opener suitable for urllib2
475 476 authinfo will be added to the password manager
476 477 '''
477 478 # experimental config: ui.usehttp2
478 479 if ui.configbool('ui', 'usehttp2', False):
479 480 handlers = [httpconnectionmod.http2handler(ui, passwordmgr(ui))]
480 481 else:
481 482 handlers = [httphandler()]
482 483 if has_https:
483 484 handlers.append(httpshandler(ui))
484 485
485 486 handlers.append(proxyhandler(ui))
486 487
487 488 passmgr = passwordmgr(ui)
488 489 if authinfo is not None:
489 490 passmgr.add_password(*authinfo)
490 491 user, passwd = authinfo[2:4]
491 492 ui.debug('http auth: user %s, password %s\n' %
492 493 (user, passwd and '*' * len(passwd) or 'not set'))
493 494
494 495 handlers.extend((httpbasicauthhandler(passmgr),
495 496 httpdigestauthhandler(passmgr)))
496 497 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
497 opener = urllib2.build_opener(*handlers)
498 opener = urlreq.buildopener(*handlers)
498 499
499 500 # 1.0 here is the _protocol_ version
500 501 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
501 502 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
502 503 return opener
503 504
504 505 def open(ui, url_, data=None):
505 506 u = util.url(url_)
506 507 if u.scheme:
507 508 u.scheme = u.scheme.lower()
508 509 url_, authinfo = u.authinfo()
509 510 else:
510 511 path = util.normpath(os.path.abspath(url_))
511 url_ = 'file://' + urllib.pathname2url(path)
512 url_ = 'file://' + urlreq.pathname2url(path)
512 513 authinfo = None
513 514 return opener(ui, authinfo).open(url_, data)
@@ -1,2755 +1,2758 b''
1 1 # util.py - Mercurial utility functions and platform specific implementations
2 2 #
3 3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 """Mercurial utility functions and platform specific implementations.
11 11
12 12 This contains helper routines that are independent of the SCM core and
13 13 hide platform-specific details from the core.
14 14 """
15 15
16 16 from __future__ import absolute_import
17 17
18 18 import bz2
19 19 import calendar
20 20 import collections
21 21 import datetime
22 22 import errno
23 23 import gc
24 24 import hashlib
25 25 import imp
26 26 import os
27 27 import re as remod
28 28 import shutil
29 29 import signal
30 30 import socket
31 31 import subprocess
32 32 import sys
33 33 import tempfile
34 34 import textwrap
35 35 import time
36 36 import traceback
37 import urllib
38 37 import zlib
39 38
40 39 from . import (
41 40 encoding,
42 41 error,
43 42 i18n,
44 43 osutil,
45 44 parsers,
46 45 pycompat,
47 46 )
48 47
49 48 for attr in (
50 49 'empty',
51 50 'queue',
52 51 'urlerr',
53 'urlreq',
52 # we do import urlreq, but we do it outside the loop
53 #'urlreq',
54 54 'stringio',
55 55 ):
56 56 globals()[attr] = getattr(pycompat, attr)
57 57
58 # This line is to make pyflakes happy:
59 urlreq = pycompat.urlreq
60
58 61 if os.name == 'nt':
59 62 from . import windows as platform
60 63 else:
61 64 from . import posix as platform
62 65
63 66 md5 = hashlib.md5
64 67 sha1 = hashlib.sha1
65 68 sha512 = hashlib.sha512
66 69 _ = i18n._
67 70
68 71 cachestat = platform.cachestat
69 72 checkexec = platform.checkexec
70 73 checklink = platform.checklink
71 74 copymode = platform.copymode
72 75 executablepath = platform.executablepath
73 76 expandglobs = platform.expandglobs
74 77 explainexit = platform.explainexit
75 78 findexe = platform.findexe
76 79 gethgcmd = platform.gethgcmd
77 80 getuser = platform.getuser
78 81 getpid = os.getpid
79 82 groupmembers = platform.groupmembers
80 83 groupname = platform.groupname
81 84 hidewindow = platform.hidewindow
82 85 isexec = platform.isexec
83 86 isowner = platform.isowner
84 87 localpath = platform.localpath
85 88 lookupreg = platform.lookupreg
86 89 makedir = platform.makedir
87 90 nlinks = platform.nlinks
88 91 normpath = platform.normpath
89 92 normcase = platform.normcase
90 93 normcasespec = platform.normcasespec
91 94 normcasefallback = platform.normcasefallback
92 95 openhardlinks = platform.openhardlinks
93 96 oslink = platform.oslink
94 97 parsepatchoutput = platform.parsepatchoutput
95 98 pconvert = platform.pconvert
96 99 poll = platform.poll
97 100 popen = platform.popen
98 101 posixfile = platform.posixfile
99 102 quotecommand = platform.quotecommand
100 103 readpipe = platform.readpipe
101 104 rename = platform.rename
102 105 removedirs = platform.removedirs
103 106 samedevice = platform.samedevice
104 107 samefile = platform.samefile
105 108 samestat = platform.samestat
106 109 setbinary = platform.setbinary
107 110 setflags = platform.setflags
108 111 setsignalhandler = platform.setsignalhandler
109 112 shellquote = platform.shellquote
110 113 spawndetached = platform.spawndetached
111 114 split = platform.split
112 115 sshargs = platform.sshargs
113 116 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
114 117 statisexec = platform.statisexec
115 118 statislink = platform.statislink
116 119 termwidth = platform.termwidth
117 120 testpid = platform.testpid
118 121 umask = platform.umask
119 122 unlink = platform.unlink
120 123 unlinkpath = platform.unlinkpath
121 124 username = platform.username
122 125
123 126 # Python compatibility
124 127
125 128 _notset = object()
126 129
127 130 # disable Python's problematic floating point timestamps (issue4836)
128 131 # (Python hypocritically says you shouldn't change this behavior in
129 132 # libraries, and sure enough Mercurial is not a library.)
130 133 os.stat_float_times(False)
131 134
132 135 def safehasattr(thing, attr):
133 136 return getattr(thing, attr, _notset) is not _notset
134 137
135 138 DIGESTS = {
136 139 'md5': md5,
137 140 'sha1': sha1,
138 141 'sha512': sha512,
139 142 }
140 143 # List of digest types from strongest to weakest
141 144 DIGESTS_BY_STRENGTH = ['sha512', 'sha1', 'md5']
142 145
143 146 for k in DIGESTS_BY_STRENGTH:
144 147 assert k in DIGESTS
145 148
146 149 class digester(object):
147 150 """helper to compute digests.
148 151
149 152 This helper can be used to compute one or more digests given their name.
150 153
151 154 >>> d = digester(['md5', 'sha1'])
152 155 >>> d.update('foo')
153 156 >>> [k for k in sorted(d)]
154 157 ['md5', 'sha1']
155 158 >>> d['md5']
156 159 'acbd18db4cc2f85cedef654fccc4a4d8'
157 160 >>> d['sha1']
158 161 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
159 162 >>> digester.preferred(['md5', 'sha1'])
160 163 'sha1'
161 164 """
162 165
163 166 def __init__(self, digests, s=''):
164 167 self._hashes = {}
165 168 for k in digests:
166 169 if k not in DIGESTS:
167 170 raise Abort(_('unknown digest type: %s') % k)
168 171 self._hashes[k] = DIGESTS[k]()
169 172 if s:
170 173 self.update(s)
171 174
172 175 def update(self, data):
173 176 for h in self._hashes.values():
174 177 h.update(data)
175 178
176 179 def __getitem__(self, key):
177 180 if key not in DIGESTS:
178 181 raise Abort(_('unknown digest type: %s') % k)
179 182 return self._hashes[key].hexdigest()
180 183
181 184 def __iter__(self):
182 185 return iter(self._hashes)
183 186
184 187 @staticmethod
185 188 def preferred(supported):
186 189 """returns the strongest digest type in both supported and DIGESTS."""
187 190
188 191 for k in DIGESTS_BY_STRENGTH:
189 192 if k in supported:
190 193 return k
191 194 return None
192 195
193 196 class digestchecker(object):
194 197 """file handle wrapper that additionally checks content against a given
195 198 size and digests.
196 199
197 200 d = digestchecker(fh, size, {'md5': '...'})
198 201
199 202 When multiple digests are given, all of them are validated.
200 203 """
201 204
202 205 def __init__(self, fh, size, digests):
203 206 self._fh = fh
204 207 self._size = size
205 208 self._got = 0
206 209 self._digests = dict(digests)
207 210 self._digester = digester(self._digests.keys())
208 211
209 212 def read(self, length=-1):
210 213 content = self._fh.read(length)
211 214 self._digester.update(content)
212 215 self._got += len(content)
213 216 return content
214 217
215 218 def validate(self):
216 219 if self._size != self._got:
217 220 raise Abort(_('size mismatch: expected %d, got %d') %
218 221 (self._size, self._got))
219 222 for k, v in self._digests.items():
220 223 if v != self._digester[k]:
221 224 # i18n: first parameter is a digest name
222 225 raise Abort(_('%s mismatch: expected %s, got %s') %
223 226 (k, v, self._digester[k]))
224 227
225 228 try:
226 229 buffer = buffer
227 230 except NameError:
228 231 if sys.version_info[0] < 3:
229 232 def buffer(sliceable, offset=0):
230 233 return sliceable[offset:]
231 234 else:
232 235 def buffer(sliceable, offset=0):
233 236 return memoryview(sliceable)[offset:]
234 237
235 238 closefds = os.name == 'posix'
236 239
237 240 _chunksize = 4096
238 241
239 242 class bufferedinputpipe(object):
240 243 """a manually buffered input pipe
241 244
242 245 Python will not let us use buffered IO and lazy reading with 'polling' at
243 246 the same time. We cannot probe the buffer state and select will not detect
244 247 that data are ready to read if they are already buffered.
245 248
246 249 This class let us work around that by implementing its own buffering
247 250 (allowing efficient readline) while offering a way to know if the buffer is
248 251 empty from the output (allowing collaboration of the buffer with polling).
249 252
250 253 This class lives in the 'util' module because it makes use of the 'os'
251 254 module from the python stdlib.
252 255 """
253 256
254 257 def __init__(self, input):
255 258 self._input = input
256 259 self._buffer = []
257 260 self._eof = False
258 261 self._lenbuf = 0
259 262
260 263 @property
261 264 def hasbuffer(self):
262 265 """True is any data is currently buffered
263 266
264 267 This will be used externally a pre-step for polling IO. If there is
265 268 already data then no polling should be set in place."""
266 269 return bool(self._buffer)
267 270
268 271 @property
269 272 def closed(self):
270 273 return self._input.closed
271 274
272 275 def fileno(self):
273 276 return self._input.fileno()
274 277
275 278 def close(self):
276 279 return self._input.close()
277 280
278 281 def read(self, size):
279 282 while (not self._eof) and (self._lenbuf < size):
280 283 self._fillbuffer()
281 284 return self._frombuffer(size)
282 285
283 286 def readline(self, *args, **kwargs):
284 287 if 1 < len(self._buffer):
285 288 # this should not happen because both read and readline end with a
286 289 # _frombuffer call that collapse it.
287 290 self._buffer = [''.join(self._buffer)]
288 291 self._lenbuf = len(self._buffer[0])
289 292 lfi = -1
290 293 if self._buffer:
291 294 lfi = self._buffer[-1].find('\n')
292 295 while (not self._eof) and lfi < 0:
293 296 self._fillbuffer()
294 297 if self._buffer:
295 298 lfi = self._buffer[-1].find('\n')
296 299 size = lfi + 1
297 300 if lfi < 0: # end of file
298 301 size = self._lenbuf
299 302 elif 1 < len(self._buffer):
300 303 # we need to take previous chunks into account
301 304 size += self._lenbuf - len(self._buffer[-1])
302 305 return self._frombuffer(size)
303 306
304 307 def _frombuffer(self, size):
305 308 """return at most 'size' data from the buffer
306 309
307 310 The data are removed from the buffer."""
308 311 if size == 0 or not self._buffer:
309 312 return ''
310 313 buf = self._buffer[0]
311 314 if 1 < len(self._buffer):
312 315 buf = ''.join(self._buffer)
313 316
314 317 data = buf[:size]
315 318 buf = buf[len(data):]
316 319 if buf:
317 320 self._buffer = [buf]
318 321 self._lenbuf = len(buf)
319 322 else:
320 323 self._buffer = []
321 324 self._lenbuf = 0
322 325 return data
323 326
324 327 def _fillbuffer(self):
325 328 """read data to the buffer"""
326 329 data = os.read(self._input.fileno(), _chunksize)
327 330 if not data:
328 331 self._eof = True
329 332 else:
330 333 self._lenbuf += len(data)
331 334 self._buffer.append(data)
332 335
333 336 def popen2(cmd, env=None, newlines=False):
334 337 # Setting bufsize to -1 lets the system decide the buffer size.
335 338 # The default for bufsize is 0, meaning unbuffered. This leads to
336 339 # poor performance on Mac OS X: http://bugs.python.org/issue4194
337 340 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
338 341 close_fds=closefds,
339 342 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
340 343 universal_newlines=newlines,
341 344 env=env)
342 345 return p.stdin, p.stdout
343 346
344 347 def popen3(cmd, env=None, newlines=False):
345 348 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
346 349 return stdin, stdout, stderr
347 350
348 351 def popen4(cmd, env=None, newlines=False, bufsize=-1):
349 352 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
350 353 close_fds=closefds,
351 354 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
352 355 stderr=subprocess.PIPE,
353 356 universal_newlines=newlines,
354 357 env=env)
355 358 return p.stdin, p.stdout, p.stderr, p
356 359
357 360 def version():
358 361 """Return version information if available."""
359 362 try:
360 363 from . import __version__
361 364 return __version__.version
362 365 except ImportError:
363 366 return 'unknown'
364 367
365 368 def versiontuple(v=None, n=4):
366 369 """Parses a Mercurial version string into an N-tuple.
367 370
368 371 The version string to be parsed is specified with the ``v`` argument.
369 372 If it isn't defined, the current Mercurial version string will be parsed.
370 373
371 374 ``n`` can be 2, 3, or 4. Here is how some version strings map to
372 375 returned values:
373 376
374 377 >>> v = '3.6.1+190-df9b73d2d444'
375 378 >>> versiontuple(v, 2)
376 379 (3, 6)
377 380 >>> versiontuple(v, 3)
378 381 (3, 6, 1)
379 382 >>> versiontuple(v, 4)
380 383 (3, 6, 1, '190-df9b73d2d444')
381 384
382 385 >>> versiontuple('3.6.1+190-df9b73d2d444+20151118')
383 386 (3, 6, 1, '190-df9b73d2d444+20151118')
384 387
385 388 >>> v = '3.6'
386 389 >>> versiontuple(v, 2)
387 390 (3, 6)
388 391 >>> versiontuple(v, 3)
389 392 (3, 6, None)
390 393 >>> versiontuple(v, 4)
391 394 (3, 6, None, None)
392 395 """
393 396 if not v:
394 397 v = version()
395 398 parts = v.split('+', 1)
396 399 if len(parts) == 1:
397 400 vparts, extra = parts[0], None
398 401 else:
399 402 vparts, extra = parts
400 403
401 404 vints = []
402 405 for i in vparts.split('.'):
403 406 try:
404 407 vints.append(int(i))
405 408 except ValueError:
406 409 break
407 410 # (3, 6) -> (3, 6, None)
408 411 while len(vints) < 3:
409 412 vints.append(None)
410 413
411 414 if n == 2:
412 415 return (vints[0], vints[1])
413 416 if n == 3:
414 417 return (vints[0], vints[1], vints[2])
415 418 if n == 4:
416 419 return (vints[0], vints[1], vints[2], extra)
417 420
418 421 # used by parsedate
419 422 defaultdateformats = (
420 423 '%Y-%m-%d %H:%M:%S',
421 424 '%Y-%m-%d %I:%M:%S%p',
422 425 '%Y-%m-%d %H:%M',
423 426 '%Y-%m-%d %I:%M%p',
424 427 '%Y-%m-%d',
425 428 '%m-%d',
426 429 '%m/%d',
427 430 '%m/%d/%y',
428 431 '%m/%d/%Y',
429 432 '%a %b %d %H:%M:%S %Y',
430 433 '%a %b %d %I:%M:%S%p %Y',
431 434 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
432 435 '%b %d %H:%M:%S %Y',
433 436 '%b %d %I:%M:%S%p %Y',
434 437 '%b %d %H:%M:%S',
435 438 '%b %d %I:%M:%S%p',
436 439 '%b %d %H:%M',
437 440 '%b %d %I:%M%p',
438 441 '%b %d %Y',
439 442 '%b %d',
440 443 '%H:%M:%S',
441 444 '%I:%M:%S%p',
442 445 '%H:%M',
443 446 '%I:%M%p',
444 447 )
445 448
446 449 extendeddateformats = defaultdateformats + (
447 450 "%Y",
448 451 "%Y-%m",
449 452 "%b",
450 453 "%b %Y",
451 454 )
452 455
453 456 def cachefunc(func):
454 457 '''cache the result of function calls'''
455 458 # XXX doesn't handle keywords args
456 459 if func.__code__.co_argcount == 0:
457 460 cache = []
458 461 def f():
459 462 if len(cache) == 0:
460 463 cache.append(func())
461 464 return cache[0]
462 465 return f
463 466 cache = {}
464 467 if func.__code__.co_argcount == 1:
465 468 # we gain a small amount of time because
466 469 # we don't need to pack/unpack the list
467 470 def f(arg):
468 471 if arg not in cache:
469 472 cache[arg] = func(arg)
470 473 return cache[arg]
471 474 else:
472 475 def f(*args):
473 476 if args not in cache:
474 477 cache[args] = func(*args)
475 478 return cache[args]
476 479
477 480 return f
478 481
479 482 class sortdict(dict):
480 483 '''a simple sorted dictionary'''
481 484 def __init__(self, data=None):
482 485 self._list = []
483 486 if data:
484 487 self.update(data)
485 488 def copy(self):
486 489 return sortdict(self)
487 490 def __setitem__(self, key, val):
488 491 if key in self:
489 492 self._list.remove(key)
490 493 self._list.append(key)
491 494 dict.__setitem__(self, key, val)
492 495 def __iter__(self):
493 496 return self._list.__iter__()
494 497 def update(self, src):
495 498 if isinstance(src, dict):
496 499 src = src.iteritems()
497 500 for k, v in src:
498 501 self[k] = v
499 502 def clear(self):
500 503 dict.clear(self)
501 504 self._list = []
502 505 def items(self):
503 506 return [(k, self[k]) for k in self._list]
504 507 def __delitem__(self, key):
505 508 dict.__delitem__(self, key)
506 509 self._list.remove(key)
507 510 def pop(self, key, *args, **kwargs):
508 511 dict.pop(self, key, *args, **kwargs)
509 512 try:
510 513 self._list.remove(key)
511 514 except ValueError:
512 515 pass
513 516 def keys(self):
514 517 return self._list
515 518 def iterkeys(self):
516 519 return self._list.__iter__()
517 520 def iteritems(self):
518 521 for k in self._list:
519 522 yield k, self[k]
520 523 def insert(self, index, key, val):
521 524 self._list.insert(index, key)
522 525 dict.__setitem__(self, key, val)
523 526
524 527 class _lrucachenode(object):
525 528 """A node in a doubly linked list.
526 529
527 530 Holds a reference to nodes on either side as well as a key-value
528 531 pair for the dictionary entry.
529 532 """
530 533 __slots__ = ('next', 'prev', 'key', 'value')
531 534
532 535 def __init__(self):
533 536 self.next = None
534 537 self.prev = None
535 538
536 539 self.key = _notset
537 540 self.value = None
538 541
539 542 def markempty(self):
540 543 """Mark the node as emptied."""
541 544 self.key = _notset
542 545
543 546 class lrucachedict(object):
544 547 """Dict that caches most recent accesses and sets.
545 548
546 549 The dict consists of an actual backing dict - indexed by original
547 550 key - and a doubly linked circular list defining the order of entries in
548 551 the cache.
549 552
550 553 The head node is the newest entry in the cache. If the cache is full,
551 554 we recycle head.prev and make it the new head. Cache accesses result in
552 555 the node being moved to before the existing head and being marked as the
553 556 new head node.
554 557 """
555 558 def __init__(self, max):
556 559 self._cache = {}
557 560
558 561 self._head = head = _lrucachenode()
559 562 head.prev = head
560 563 head.next = head
561 564 self._size = 1
562 565 self._capacity = max
563 566
564 567 def __len__(self):
565 568 return len(self._cache)
566 569
567 570 def __contains__(self, k):
568 571 return k in self._cache
569 572
570 573 def __iter__(self):
571 574 # We don't have to iterate in cache order, but why not.
572 575 n = self._head
573 576 for i in range(len(self._cache)):
574 577 yield n.key
575 578 n = n.next
576 579
577 580 def __getitem__(self, k):
578 581 node = self._cache[k]
579 582 self._movetohead(node)
580 583 return node.value
581 584
582 585 def __setitem__(self, k, v):
583 586 node = self._cache.get(k)
584 587 # Replace existing value and mark as newest.
585 588 if node is not None:
586 589 node.value = v
587 590 self._movetohead(node)
588 591 return
589 592
590 593 if self._size < self._capacity:
591 594 node = self._addcapacity()
592 595 else:
593 596 # Grab the last/oldest item.
594 597 node = self._head.prev
595 598
596 599 # At capacity. Kill the old entry.
597 600 if node.key is not _notset:
598 601 del self._cache[node.key]
599 602
600 603 node.key = k
601 604 node.value = v
602 605 self._cache[k] = node
603 606 # And mark it as newest entry. No need to adjust order since it
604 607 # is already self._head.prev.
605 608 self._head = node
606 609
607 610 def __delitem__(self, k):
608 611 node = self._cache.pop(k)
609 612 node.markempty()
610 613
611 614 # Temporarily mark as newest item before re-adjusting head to make
612 615 # this node the oldest item.
613 616 self._movetohead(node)
614 617 self._head = node.next
615 618
616 619 # Additional dict methods.
617 620
618 621 def get(self, k, default=None):
619 622 try:
620 623 return self._cache[k]
621 624 except KeyError:
622 625 return default
623 626
624 627 def clear(self):
625 628 n = self._head
626 629 while n.key is not _notset:
627 630 n.markempty()
628 631 n = n.next
629 632
630 633 self._cache.clear()
631 634
632 635 def copy(self):
633 636 result = lrucachedict(self._capacity)
634 637 n = self._head.prev
635 638 # Iterate in oldest-to-newest order, so the copy has the right ordering
636 639 for i in range(len(self._cache)):
637 640 result[n.key] = n.value
638 641 n = n.prev
639 642 return result
640 643
641 644 def _movetohead(self, node):
642 645 """Mark a node as the newest, making it the new head.
643 646
644 647 When a node is accessed, it becomes the freshest entry in the LRU
645 648 list, which is denoted by self._head.
646 649
647 650 Visually, let's make ``N`` the new head node (* denotes head):
648 651
649 652 previous/oldest <-> head <-> next/next newest
650 653
651 654 ----<->--- A* ---<->-----
652 655 | |
653 656 E <-> D <-> N <-> C <-> B
654 657
655 658 To:
656 659
657 660 ----<->--- N* ---<->-----
658 661 | |
659 662 E <-> D <-> C <-> B <-> A
660 663
661 664 This requires the following moves:
662 665
663 666 C.next = D (node.prev.next = node.next)
664 667 D.prev = C (node.next.prev = node.prev)
665 668 E.next = N (head.prev.next = node)
666 669 N.prev = E (node.prev = head.prev)
667 670 N.next = A (node.next = head)
668 671 A.prev = N (head.prev = node)
669 672 """
670 673 head = self._head
671 674 # C.next = D
672 675 node.prev.next = node.next
673 676 # D.prev = C
674 677 node.next.prev = node.prev
675 678 # N.prev = E
676 679 node.prev = head.prev
677 680 # N.next = A
678 681 # It is tempting to do just "head" here, however if node is
679 682 # adjacent to head, this will do bad things.
680 683 node.next = head.prev.next
681 684 # E.next = N
682 685 node.next.prev = node
683 686 # A.prev = N
684 687 node.prev.next = node
685 688
686 689 self._head = node
687 690
688 691 def _addcapacity(self):
689 692 """Add a node to the circular linked list.
690 693
691 694 The new node is inserted before the head node.
692 695 """
693 696 head = self._head
694 697 node = _lrucachenode()
695 698 head.prev.next = node
696 699 node.prev = head.prev
697 700 node.next = head
698 701 head.prev = node
699 702 self._size += 1
700 703 return node
701 704
702 705 def lrucachefunc(func):
703 706 '''cache most recent results of function calls'''
704 707 cache = {}
705 708 order = collections.deque()
706 709 if func.__code__.co_argcount == 1:
707 710 def f(arg):
708 711 if arg not in cache:
709 712 if len(cache) > 20:
710 713 del cache[order.popleft()]
711 714 cache[arg] = func(arg)
712 715 else:
713 716 order.remove(arg)
714 717 order.append(arg)
715 718 return cache[arg]
716 719 else:
717 720 def f(*args):
718 721 if args not in cache:
719 722 if len(cache) > 20:
720 723 del cache[order.popleft()]
721 724 cache[args] = func(*args)
722 725 else:
723 726 order.remove(args)
724 727 order.append(args)
725 728 return cache[args]
726 729
727 730 return f
728 731
729 732 class propertycache(object):
730 733 def __init__(self, func):
731 734 self.func = func
732 735 self.name = func.__name__
733 736 def __get__(self, obj, type=None):
734 737 result = self.func(obj)
735 738 self.cachevalue(obj, result)
736 739 return result
737 740
738 741 def cachevalue(self, obj, value):
739 742 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
740 743 obj.__dict__[self.name] = value
741 744
742 745 def pipefilter(s, cmd):
743 746 '''filter string S through command CMD, returning its output'''
744 747 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
745 748 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
746 749 pout, perr = p.communicate(s)
747 750 return pout
748 751
749 752 def tempfilter(s, cmd):
750 753 '''filter string S through a pair of temporary files with CMD.
751 754 CMD is used as a template to create the real command to be run,
752 755 with the strings INFILE and OUTFILE replaced by the real names of
753 756 the temporary files generated.'''
754 757 inname, outname = None, None
755 758 try:
756 759 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
757 760 fp = os.fdopen(infd, 'wb')
758 761 fp.write(s)
759 762 fp.close()
760 763 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
761 764 os.close(outfd)
762 765 cmd = cmd.replace('INFILE', inname)
763 766 cmd = cmd.replace('OUTFILE', outname)
764 767 code = os.system(cmd)
765 768 if sys.platform == 'OpenVMS' and code & 1:
766 769 code = 0
767 770 if code:
768 771 raise Abort(_("command '%s' failed: %s") %
769 772 (cmd, explainexit(code)))
770 773 return readfile(outname)
771 774 finally:
772 775 try:
773 776 if inname:
774 777 os.unlink(inname)
775 778 except OSError:
776 779 pass
777 780 try:
778 781 if outname:
779 782 os.unlink(outname)
780 783 except OSError:
781 784 pass
782 785
783 786 filtertable = {
784 787 'tempfile:': tempfilter,
785 788 'pipe:': pipefilter,
786 789 }
787 790
788 791 def filter(s, cmd):
789 792 "filter a string through a command that transforms its input to its output"
790 793 for name, fn in filtertable.iteritems():
791 794 if cmd.startswith(name):
792 795 return fn(s, cmd[len(name):].lstrip())
793 796 return pipefilter(s, cmd)
794 797
795 798 def binary(s):
796 799 """return true if a string is binary data"""
797 800 return bool(s and '\0' in s)
798 801
799 802 def increasingchunks(source, min=1024, max=65536):
800 803 '''return no less than min bytes per chunk while data remains,
801 804 doubling min after each chunk until it reaches max'''
802 805 def log2(x):
803 806 if not x:
804 807 return 0
805 808 i = 0
806 809 while x:
807 810 x >>= 1
808 811 i += 1
809 812 return i - 1
810 813
811 814 buf = []
812 815 blen = 0
813 816 for chunk in source:
814 817 buf.append(chunk)
815 818 blen += len(chunk)
816 819 if blen >= min:
817 820 if min < max:
818 821 min = min << 1
819 822 nmin = 1 << log2(blen)
820 823 if nmin > min:
821 824 min = nmin
822 825 if min > max:
823 826 min = max
824 827 yield ''.join(buf)
825 828 blen = 0
826 829 buf = []
827 830 if buf:
828 831 yield ''.join(buf)
829 832
830 833 Abort = error.Abort
831 834
832 835 def always(fn):
833 836 return True
834 837
835 838 def never(fn):
836 839 return False
837 840
838 841 def nogc(func):
839 842 """disable garbage collector
840 843
841 844 Python's garbage collector triggers a GC each time a certain number of
842 845 container objects (the number being defined by gc.get_threshold()) are
843 846 allocated even when marked not to be tracked by the collector. Tracking has
844 847 no effect on when GCs are triggered, only on what objects the GC looks
845 848 into. As a workaround, disable GC while building complex (huge)
846 849 containers.
847 850
848 851 This garbage collector issue have been fixed in 2.7.
849 852 """
850 853 def wrapper(*args, **kwargs):
851 854 gcenabled = gc.isenabled()
852 855 gc.disable()
853 856 try:
854 857 return func(*args, **kwargs)
855 858 finally:
856 859 if gcenabled:
857 860 gc.enable()
858 861 return wrapper
859 862
860 863 def pathto(root, n1, n2):
861 864 '''return the relative path from one place to another.
862 865 root should use os.sep to separate directories
863 866 n1 should use os.sep to separate directories
864 867 n2 should use "/" to separate directories
865 868 returns an os.sep-separated path.
866 869
867 870 If n1 is a relative path, it's assumed it's
868 871 relative to root.
869 872 n2 should always be relative to root.
870 873 '''
871 874 if not n1:
872 875 return localpath(n2)
873 876 if os.path.isabs(n1):
874 877 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
875 878 return os.path.join(root, localpath(n2))
876 879 n2 = '/'.join((pconvert(root), n2))
877 880 a, b = splitpath(n1), n2.split('/')
878 881 a.reverse()
879 882 b.reverse()
880 883 while a and b and a[-1] == b[-1]:
881 884 a.pop()
882 885 b.pop()
883 886 b.reverse()
884 887 return os.sep.join((['..'] * len(a)) + b) or '.'
885 888
886 889 def mainfrozen():
887 890 """return True if we are a frozen executable.
888 891
889 892 The code supports py2exe (most common, Windows only) and tools/freeze
890 893 (portable, not much used).
891 894 """
892 895 return (safehasattr(sys, "frozen") or # new py2exe
893 896 safehasattr(sys, "importers") or # old py2exe
894 897 imp.is_frozen("__main__")) # tools/freeze
895 898
896 899 # the location of data files matching the source code
897 900 if mainfrozen() and getattr(sys, 'frozen', None) != 'macosx_app':
898 901 # executable version (py2exe) doesn't support __file__
899 902 datapath = os.path.dirname(sys.executable)
900 903 else:
901 904 datapath = os.path.dirname(__file__)
902 905
903 906 i18n.setdatapath(datapath)
904 907
905 908 _hgexecutable = None
906 909
907 910 def hgexecutable():
908 911 """return location of the 'hg' executable.
909 912
910 913 Defaults to $HG or 'hg' in the search path.
911 914 """
912 915 if _hgexecutable is None:
913 916 hg = os.environ.get('HG')
914 917 mainmod = sys.modules['__main__']
915 918 if hg:
916 919 _sethgexecutable(hg)
917 920 elif mainfrozen():
918 921 if getattr(sys, 'frozen', None) == 'macosx_app':
919 922 # Env variable set by py2app
920 923 _sethgexecutable(os.environ['EXECUTABLEPATH'])
921 924 else:
922 925 _sethgexecutable(sys.executable)
923 926 elif os.path.basename(getattr(mainmod, '__file__', '')) == 'hg':
924 927 _sethgexecutable(mainmod.__file__)
925 928 else:
926 929 exe = findexe('hg') or os.path.basename(sys.argv[0])
927 930 _sethgexecutable(exe)
928 931 return _hgexecutable
929 932
930 933 def _sethgexecutable(path):
931 934 """set location of the 'hg' executable"""
932 935 global _hgexecutable
933 936 _hgexecutable = path
934 937
935 938 def _isstdout(f):
936 939 fileno = getattr(f, 'fileno', None)
937 940 return fileno and fileno() == sys.__stdout__.fileno()
938 941
939 942 def system(cmd, environ=None, cwd=None, onerr=None, errprefix=None, out=None):
940 943 '''enhanced shell command execution.
941 944 run with environment maybe modified, maybe in different dir.
942 945
943 946 if command fails and onerr is None, return status, else raise onerr
944 947 object as exception.
945 948
946 949 if out is specified, it is assumed to be a file-like object that has a
947 950 write() method. stdout and stderr will be redirected to out.'''
948 951 if environ is None:
949 952 environ = {}
950 953 try:
951 954 sys.stdout.flush()
952 955 except Exception:
953 956 pass
954 957 def py2shell(val):
955 958 'convert python object into string that is useful to shell'
956 959 if val is None or val is False:
957 960 return '0'
958 961 if val is True:
959 962 return '1'
960 963 return str(val)
961 964 origcmd = cmd
962 965 cmd = quotecommand(cmd)
963 966 if sys.platform == 'plan9' and (sys.version_info[0] == 2
964 967 and sys.version_info[1] < 7):
965 968 # subprocess kludge to work around issues in half-baked Python
966 969 # ports, notably bichued/python:
967 970 if not cwd is None:
968 971 os.chdir(cwd)
969 972 rc = os.system(cmd)
970 973 else:
971 974 env = dict(os.environ)
972 975 env.update((k, py2shell(v)) for k, v in environ.iteritems())
973 976 env['HG'] = hgexecutable()
974 977 if out is None or _isstdout(out):
975 978 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
976 979 env=env, cwd=cwd)
977 980 else:
978 981 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
979 982 env=env, cwd=cwd, stdout=subprocess.PIPE,
980 983 stderr=subprocess.STDOUT)
981 984 while True:
982 985 line = proc.stdout.readline()
983 986 if not line:
984 987 break
985 988 out.write(line)
986 989 proc.wait()
987 990 rc = proc.returncode
988 991 if sys.platform == 'OpenVMS' and rc & 1:
989 992 rc = 0
990 993 if rc and onerr:
991 994 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
992 995 explainexit(rc)[0])
993 996 if errprefix:
994 997 errmsg = '%s: %s' % (errprefix, errmsg)
995 998 raise onerr(errmsg)
996 999 return rc
997 1000
998 1001 def checksignature(func):
999 1002 '''wrap a function with code to check for calling errors'''
1000 1003 def check(*args, **kwargs):
1001 1004 try:
1002 1005 return func(*args, **kwargs)
1003 1006 except TypeError:
1004 1007 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
1005 1008 raise error.SignatureError
1006 1009 raise
1007 1010
1008 1011 return check
1009 1012
1010 1013 def copyfile(src, dest, hardlink=False, copystat=False):
1011 1014 '''copy a file, preserving mode and optionally other stat info like
1012 1015 atime/mtime'''
1013 1016 if os.path.lexists(dest):
1014 1017 unlink(dest)
1015 1018 # hardlinks are problematic on CIFS, quietly ignore this flag
1016 1019 # until we find a way to work around it cleanly (issue4546)
1017 1020 if False and hardlink:
1018 1021 try:
1019 1022 oslink(src, dest)
1020 1023 return
1021 1024 except (IOError, OSError):
1022 1025 pass # fall back to normal copy
1023 1026 if os.path.islink(src):
1024 1027 os.symlink(os.readlink(src), dest)
1025 1028 # copytime is ignored for symlinks, but in general copytime isn't needed
1026 1029 # for them anyway
1027 1030 else:
1028 1031 try:
1029 1032 shutil.copyfile(src, dest)
1030 1033 if copystat:
1031 1034 # copystat also copies mode
1032 1035 shutil.copystat(src, dest)
1033 1036 else:
1034 1037 shutil.copymode(src, dest)
1035 1038 except shutil.Error as inst:
1036 1039 raise Abort(str(inst))
1037 1040
1038 1041 def copyfiles(src, dst, hardlink=None, progress=lambda t, pos: None):
1039 1042 """Copy a directory tree using hardlinks if possible."""
1040 1043 num = 0
1041 1044
1042 1045 if hardlink is None:
1043 1046 hardlink = (os.stat(src).st_dev ==
1044 1047 os.stat(os.path.dirname(dst)).st_dev)
1045 1048 if hardlink:
1046 1049 topic = _('linking')
1047 1050 else:
1048 1051 topic = _('copying')
1049 1052
1050 1053 if os.path.isdir(src):
1051 1054 os.mkdir(dst)
1052 1055 for name, kind in osutil.listdir(src):
1053 1056 srcname = os.path.join(src, name)
1054 1057 dstname = os.path.join(dst, name)
1055 1058 def nprog(t, pos):
1056 1059 if pos is not None:
1057 1060 return progress(t, pos + num)
1058 1061 hardlink, n = copyfiles(srcname, dstname, hardlink, progress=nprog)
1059 1062 num += n
1060 1063 else:
1061 1064 if hardlink:
1062 1065 try:
1063 1066 oslink(src, dst)
1064 1067 except (IOError, OSError):
1065 1068 hardlink = False
1066 1069 shutil.copy(src, dst)
1067 1070 else:
1068 1071 shutil.copy(src, dst)
1069 1072 num += 1
1070 1073 progress(topic, num)
1071 1074 progress(topic, None)
1072 1075
1073 1076 return hardlink, num
1074 1077
1075 1078 _winreservednames = '''con prn aux nul
1076 1079 com1 com2 com3 com4 com5 com6 com7 com8 com9
1077 1080 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
1078 1081 _winreservedchars = ':*?"<>|'
1079 1082 def checkwinfilename(path):
1080 1083 r'''Check that the base-relative path is a valid filename on Windows.
1081 1084 Returns None if the path is ok, or a UI string describing the problem.
1082 1085
1083 1086 >>> checkwinfilename("just/a/normal/path")
1084 1087 >>> checkwinfilename("foo/bar/con.xml")
1085 1088 "filename contains 'con', which is reserved on Windows"
1086 1089 >>> checkwinfilename("foo/con.xml/bar")
1087 1090 "filename contains 'con', which is reserved on Windows"
1088 1091 >>> checkwinfilename("foo/bar/xml.con")
1089 1092 >>> checkwinfilename("foo/bar/AUX/bla.txt")
1090 1093 "filename contains 'AUX', which is reserved on Windows"
1091 1094 >>> checkwinfilename("foo/bar/bla:.txt")
1092 1095 "filename contains ':', which is reserved on Windows"
1093 1096 >>> checkwinfilename("foo/bar/b\07la.txt")
1094 1097 "filename contains '\\x07', which is invalid on Windows"
1095 1098 >>> checkwinfilename("foo/bar/bla ")
1096 1099 "filename ends with ' ', which is not allowed on Windows"
1097 1100 >>> checkwinfilename("../bar")
1098 1101 >>> checkwinfilename("foo\\")
1099 1102 "filename ends with '\\', which is invalid on Windows"
1100 1103 >>> checkwinfilename("foo\\/bar")
1101 1104 "directory name ends with '\\', which is invalid on Windows"
1102 1105 '''
1103 1106 if path.endswith('\\'):
1104 1107 return _("filename ends with '\\', which is invalid on Windows")
1105 1108 if '\\/' in path:
1106 1109 return _("directory name ends with '\\', which is invalid on Windows")
1107 1110 for n in path.replace('\\', '/').split('/'):
1108 1111 if not n:
1109 1112 continue
1110 1113 for c in n:
1111 1114 if c in _winreservedchars:
1112 1115 return _("filename contains '%s', which is reserved "
1113 1116 "on Windows") % c
1114 1117 if ord(c) <= 31:
1115 1118 return _("filename contains %r, which is invalid "
1116 1119 "on Windows") % c
1117 1120 base = n.split('.')[0]
1118 1121 if base and base.lower() in _winreservednames:
1119 1122 return _("filename contains '%s', which is reserved "
1120 1123 "on Windows") % base
1121 1124 t = n[-1]
1122 1125 if t in '. ' and n not in '..':
1123 1126 return _("filename ends with '%s', which is not allowed "
1124 1127 "on Windows") % t
1125 1128
1126 1129 if os.name == 'nt':
1127 1130 checkosfilename = checkwinfilename
1128 1131 else:
1129 1132 checkosfilename = platform.checkosfilename
1130 1133
1131 1134 def makelock(info, pathname):
1132 1135 try:
1133 1136 return os.symlink(info, pathname)
1134 1137 except OSError as why:
1135 1138 if why.errno == errno.EEXIST:
1136 1139 raise
1137 1140 except AttributeError: # no symlink in os
1138 1141 pass
1139 1142
1140 1143 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
1141 1144 os.write(ld, info)
1142 1145 os.close(ld)
1143 1146
1144 1147 def readlock(pathname):
1145 1148 try:
1146 1149 return os.readlink(pathname)
1147 1150 except OSError as why:
1148 1151 if why.errno not in (errno.EINVAL, errno.ENOSYS):
1149 1152 raise
1150 1153 except AttributeError: # no symlink in os
1151 1154 pass
1152 1155 fp = posixfile(pathname)
1153 1156 r = fp.read()
1154 1157 fp.close()
1155 1158 return r
1156 1159
1157 1160 def fstat(fp):
1158 1161 '''stat file object that may not have fileno method.'''
1159 1162 try:
1160 1163 return os.fstat(fp.fileno())
1161 1164 except AttributeError:
1162 1165 return os.stat(fp.name)
1163 1166
1164 1167 # File system features
1165 1168
1166 1169 def checkcase(path):
1167 1170 """
1168 1171 Return true if the given path is on a case-sensitive filesystem
1169 1172
1170 1173 Requires a path (like /foo/.hg) ending with a foldable final
1171 1174 directory component.
1172 1175 """
1173 1176 s1 = os.lstat(path)
1174 1177 d, b = os.path.split(path)
1175 1178 b2 = b.upper()
1176 1179 if b == b2:
1177 1180 b2 = b.lower()
1178 1181 if b == b2:
1179 1182 return True # no evidence against case sensitivity
1180 1183 p2 = os.path.join(d, b2)
1181 1184 try:
1182 1185 s2 = os.lstat(p2)
1183 1186 if s2 == s1:
1184 1187 return False
1185 1188 return True
1186 1189 except OSError:
1187 1190 return True
1188 1191
1189 1192 try:
1190 1193 import re2
1191 1194 _re2 = None
1192 1195 except ImportError:
1193 1196 _re2 = False
1194 1197
1195 1198 class _re(object):
1196 1199 def _checkre2(self):
1197 1200 global _re2
1198 1201 try:
1199 1202 # check if match works, see issue3964
1200 1203 _re2 = bool(re2.match(r'\[([^\[]+)\]', '[ui]'))
1201 1204 except ImportError:
1202 1205 _re2 = False
1203 1206
1204 1207 def compile(self, pat, flags=0):
1205 1208 '''Compile a regular expression, using re2 if possible
1206 1209
1207 1210 For best performance, use only re2-compatible regexp features. The
1208 1211 only flags from the re module that are re2-compatible are
1209 1212 IGNORECASE and MULTILINE.'''
1210 1213 if _re2 is None:
1211 1214 self._checkre2()
1212 1215 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
1213 1216 if flags & remod.IGNORECASE:
1214 1217 pat = '(?i)' + pat
1215 1218 if flags & remod.MULTILINE:
1216 1219 pat = '(?m)' + pat
1217 1220 try:
1218 1221 return re2.compile(pat)
1219 1222 except re2.error:
1220 1223 pass
1221 1224 return remod.compile(pat, flags)
1222 1225
1223 1226 @propertycache
1224 1227 def escape(self):
1225 1228 '''Return the version of escape corresponding to self.compile.
1226 1229
1227 1230 This is imperfect because whether re2 or re is used for a particular
1228 1231 function depends on the flags, etc, but it's the best we can do.
1229 1232 '''
1230 1233 global _re2
1231 1234 if _re2 is None:
1232 1235 self._checkre2()
1233 1236 if _re2:
1234 1237 return re2.escape
1235 1238 else:
1236 1239 return remod.escape
1237 1240
1238 1241 re = _re()
1239 1242
1240 1243 _fspathcache = {}
1241 1244 def fspath(name, root):
1242 1245 '''Get name in the case stored in the filesystem
1243 1246
1244 1247 The name should be relative to root, and be normcase-ed for efficiency.
1245 1248
1246 1249 Note that this function is unnecessary, and should not be
1247 1250 called, for case-sensitive filesystems (simply because it's expensive).
1248 1251
1249 1252 The root should be normcase-ed, too.
1250 1253 '''
1251 1254 def _makefspathcacheentry(dir):
1252 1255 return dict((normcase(n), n) for n in os.listdir(dir))
1253 1256
1254 1257 seps = os.sep
1255 1258 if os.altsep:
1256 1259 seps = seps + os.altsep
1257 1260 # Protect backslashes. This gets silly very quickly.
1258 1261 seps.replace('\\','\\\\')
1259 1262 pattern = remod.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
1260 1263 dir = os.path.normpath(root)
1261 1264 result = []
1262 1265 for part, sep in pattern.findall(name):
1263 1266 if sep:
1264 1267 result.append(sep)
1265 1268 continue
1266 1269
1267 1270 if dir not in _fspathcache:
1268 1271 _fspathcache[dir] = _makefspathcacheentry(dir)
1269 1272 contents = _fspathcache[dir]
1270 1273
1271 1274 found = contents.get(part)
1272 1275 if not found:
1273 1276 # retry "once per directory" per "dirstate.walk" which
1274 1277 # may take place for each patches of "hg qpush", for example
1275 1278 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
1276 1279 found = contents.get(part)
1277 1280
1278 1281 result.append(found or part)
1279 1282 dir = os.path.join(dir, part)
1280 1283
1281 1284 return ''.join(result)
1282 1285
1283 1286 def checknlink(testfile):
1284 1287 '''check whether hardlink count reporting works properly'''
1285 1288
1286 1289 # testfile may be open, so we need a separate file for checking to
1287 1290 # work around issue2543 (or testfile may get lost on Samba shares)
1288 1291 f1 = testfile + ".hgtmp1"
1289 1292 if os.path.lexists(f1):
1290 1293 return False
1291 1294 try:
1292 1295 posixfile(f1, 'w').close()
1293 1296 except IOError:
1294 1297 return False
1295 1298
1296 1299 f2 = testfile + ".hgtmp2"
1297 1300 fd = None
1298 1301 try:
1299 1302 oslink(f1, f2)
1300 1303 # nlinks() may behave differently for files on Windows shares if
1301 1304 # the file is open.
1302 1305 fd = posixfile(f2)
1303 1306 return nlinks(f2) > 1
1304 1307 except OSError:
1305 1308 return False
1306 1309 finally:
1307 1310 if fd is not None:
1308 1311 fd.close()
1309 1312 for f in (f1, f2):
1310 1313 try:
1311 1314 os.unlink(f)
1312 1315 except OSError:
1313 1316 pass
1314 1317
1315 1318 def endswithsep(path):
1316 1319 '''Check path ends with os.sep or os.altsep.'''
1317 1320 return path.endswith(os.sep) or os.altsep and path.endswith(os.altsep)
1318 1321
1319 1322 def splitpath(path):
1320 1323 '''Split path by os.sep.
1321 1324 Note that this function does not use os.altsep because this is
1322 1325 an alternative of simple "xxx.split(os.sep)".
1323 1326 It is recommended to use os.path.normpath() before using this
1324 1327 function if need.'''
1325 1328 return path.split(os.sep)
1326 1329
1327 1330 def gui():
1328 1331 '''Are we running in a GUI?'''
1329 1332 if sys.platform == 'darwin':
1330 1333 if 'SSH_CONNECTION' in os.environ:
1331 1334 # handle SSH access to a box where the user is logged in
1332 1335 return False
1333 1336 elif getattr(osutil, 'isgui', None):
1334 1337 # check if a CoreGraphics session is available
1335 1338 return osutil.isgui()
1336 1339 else:
1337 1340 # pure build; use a safe default
1338 1341 return True
1339 1342 else:
1340 1343 return os.name == "nt" or os.environ.get("DISPLAY")
1341 1344
1342 1345 def mktempcopy(name, emptyok=False, createmode=None):
1343 1346 """Create a temporary file with the same contents from name
1344 1347
1345 1348 The permission bits are copied from the original file.
1346 1349
1347 1350 If the temporary file is going to be truncated immediately, you
1348 1351 can use emptyok=True as an optimization.
1349 1352
1350 1353 Returns the name of the temporary file.
1351 1354 """
1352 1355 d, fn = os.path.split(name)
1353 1356 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1354 1357 os.close(fd)
1355 1358 # Temporary files are created with mode 0600, which is usually not
1356 1359 # what we want. If the original file already exists, just copy
1357 1360 # its mode. Otherwise, manually obey umask.
1358 1361 copymode(name, temp, createmode)
1359 1362 if emptyok:
1360 1363 return temp
1361 1364 try:
1362 1365 try:
1363 1366 ifp = posixfile(name, "rb")
1364 1367 except IOError as inst:
1365 1368 if inst.errno == errno.ENOENT:
1366 1369 return temp
1367 1370 if not getattr(inst, 'filename', None):
1368 1371 inst.filename = name
1369 1372 raise
1370 1373 ofp = posixfile(temp, "wb")
1371 1374 for chunk in filechunkiter(ifp):
1372 1375 ofp.write(chunk)
1373 1376 ifp.close()
1374 1377 ofp.close()
1375 1378 except: # re-raises
1376 1379 try: os.unlink(temp)
1377 1380 except OSError: pass
1378 1381 raise
1379 1382 return temp
1380 1383
1381 1384 class atomictempfile(object):
1382 1385 '''writable file object that atomically updates a file
1383 1386
1384 1387 All writes will go to a temporary copy of the original file. Call
1385 1388 close() when you are done writing, and atomictempfile will rename
1386 1389 the temporary copy to the original name, making the changes
1387 1390 visible. If the object is destroyed without being closed, all your
1388 1391 writes are discarded.
1389 1392 '''
1390 1393 def __init__(self, name, mode='w+b', createmode=None):
1391 1394 self.__name = name # permanent name
1392 1395 self._tempname = mktempcopy(name, emptyok=('w' in mode),
1393 1396 createmode=createmode)
1394 1397 self._fp = posixfile(self._tempname, mode)
1395 1398
1396 1399 # delegated methods
1397 1400 self.write = self._fp.write
1398 1401 self.seek = self._fp.seek
1399 1402 self.tell = self._fp.tell
1400 1403 self.fileno = self._fp.fileno
1401 1404
1402 1405 def close(self):
1403 1406 if not self._fp.closed:
1404 1407 self._fp.close()
1405 1408 rename(self._tempname, localpath(self.__name))
1406 1409
1407 1410 def discard(self):
1408 1411 if not self._fp.closed:
1409 1412 try:
1410 1413 os.unlink(self._tempname)
1411 1414 except OSError:
1412 1415 pass
1413 1416 self._fp.close()
1414 1417
1415 1418 def __del__(self):
1416 1419 if safehasattr(self, '_fp'): # constructor actually did something
1417 1420 self.discard()
1418 1421
1419 1422 def makedirs(name, mode=None, notindexed=False):
1420 1423 """recursive directory creation with parent mode inheritance"""
1421 1424 try:
1422 1425 makedir(name, notindexed)
1423 1426 except OSError as err:
1424 1427 if err.errno == errno.EEXIST:
1425 1428 return
1426 1429 if err.errno != errno.ENOENT or not name:
1427 1430 raise
1428 1431 parent = os.path.dirname(os.path.abspath(name))
1429 1432 if parent == name:
1430 1433 raise
1431 1434 makedirs(parent, mode, notindexed)
1432 1435 makedir(name, notindexed)
1433 1436 if mode is not None:
1434 1437 os.chmod(name, mode)
1435 1438
1436 1439 def ensuredirs(name, mode=None, notindexed=False):
1437 1440 """race-safe recursive directory creation
1438 1441
1439 1442 Newly created directories are marked as "not to be indexed by
1440 1443 the content indexing service", if ``notindexed`` is specified
1441 1444 for "write" mode access.
1442 1445 """
1443 1446 if os.path.isdir(name):
1444 1447 return
1445 1448 parent = os.path.dirname(os.path.abspath(name))
1446 1449 if parent != name:
1447 1450 ensuredirs(parent, mode, notindexed)
1448 1451 try:
1449 1452 makedir(name, notindexed)
1450 1453 except OSError as err:
1451 1454 if err.errno == errno.EEXIST and os.path.isdir(name):
1452 1455 # someone else seems to have won a directory creation race
1453 1456 return
1454 1457 raise
1455 1458 if mode is not None:
1456 1459 os.chmod(name, mode)
1457 1460
1458 1461 def readfile(path):
1459 1462 with open(path, 'rb') as fp:
1460 1463 return fp.read()
1461 1464
1462 1465 def writefile(path, text):
1463 1466 with open(path, 'wb') as fp:
1464 1467 fp.write(text)
1465 1468
1466 1469 def appendfile(path, text):
1467 1470 with open(path, 'ab') as fp:
1468 1471 fp.write(text)
1469 1472
1470 1473 class chunkbuffer(object):
1471 1474 """Allow arbitrary sized chunks of data to be efficiently read from an
1472 1475 iterator over chunks of arbitrary size."""
1473 1476
1474 1477 def __init__(self, in_iter):
1475 1478 """in_iter is the iterator that's iterating over the input chunks.
1476 1479 targetsize is how big a buffer to try to maintain."""
1477 1480 def splitbig(chunks):
1478 1481 for chunk in chunks:
1479 1482 if len(chunk) > 2**20:
1480 1483 pos = 0
1481 1484 while pos < len(chunk):
1482 1485 end = pos + 2 ** 18
1483 1486 yield chunk[pos:end]
1484 1487 pos = end
1485 1488 else:
1486 1489 yield chunk
1487 1490 self.iter = splitbig(in_iter)
1488 1491 self._queue = collections.deque()
1489 1492 self._chunkoffset = 0
1490 1493
1491 1494 def read(self, l=None):
1492 1495 """Read L bytes of data from the iterator of chunks of data.
1493 1496 Returns less than L bytes if the iterator runs dry.
1494 1497
1495 1498 If size parameter is omitted, read everything"""
1496 1499 if l is None:
1497 1500 return ''.join(self.iter)
1498 1501
1499 1502 left = l
1500 1503 buf = []
1501 1504 queue = self._queue
1502 1505 while left > 0:
1503 1506 # refill the queue
1504 1507 if not queue:
1505 1508 target = 2**18
1506 1509 for chunk in self.iter:
1507 1510 queue.append(chunk)
1508 1511 target -= len(chunk)
1509 1512 if target <= 0:
1510 1513 break
1511 1514 if not queue:
1512 1515 break
1513 1516
1514 1517 # The easy way to do this would be to queue.popleft(), modify the
1515 1518 # chunk (if necessary), then queue.appendleft(). However, for cases
1516 1519 # where we read partial chunk content, this incurs 2 dequeue
1517 1520 # mutations and creates a new str for the remaining chunk in the
1518 1521 # queue. Our code below avoids this overhead.
1519 1522
1520 1523 chunk = queue[0]
1521 1524 chunkl = len(chunk)
1522 1525 offset = self._chunkoffset
1523 1526
1524 1527 # Use full chunk.
1525 1528 if offset == 0 and left >= chunkl:
1526 1529 left -= chunkl
1527 1530 queue.popleft()
1528 1531 buf.append(chunk)
1529 1532 # self._chunkoffset remains at 0.
1530 1533 continue
1531 1534
1532 1535 chunkremaining = chunkl - offset
1533 1536
1534 1537 # Use all of unconsumed part of chunk.
1535 1538 if left >= chunkremaining:
1536 1539 left -= chunkremaining
1537 1540 queue.popleft()
1538 1541 # offset == 0 is enabled by block above, so this won't merely
1539 1542 # copy via ``chunk[0:]``.
1540 1543 buf.append(chunk[offset:])
1541 1544 self._chunkoffset = 0
1542 1545
1543 1546 # Partial chunk needed.
1544 1547 else:
1545 1548 buf.append(chunk[offset:offset + left])
1546 1549 self._chunkoffset += left
1547 1550 left -= chunkremaining
1548 1551
1549 1552 return ''.join(buf)
1550 1553
1551 1554 def filechunkiter(f, size=65536, limit=None):
1552 1555 """Create a generator that produces the data in the file size
1553 1556 (default 65536) bytes at a time, up to optional limit (default is
1554 1557 to read all data). Chunks may be less than size bytes if the
1555 1558 chunk is the last chunk in the file, or the file is a socket or
1556 1559 some other type of file that sometimes reads less data than is
1557 1560 requested."""
1558 1561 assert size >= 0
1559 1562 assert limit is None or limit >= 0
1560 1563 while True:
1561 1564 if limit is None:
1562 1565 nbytes = size
1563 1566 else:
1564 1567 nbytes = min(limit, size)
1565 1568 s = nbytes and f.read(nbytes)
1566 1569 if not s:
1567 1570 break
1568 1571 if limit:
1569 1572 limit -= len(s)
1570 1573 yield s
1571 1574
1572 1575 def makedate(timestamp=None):
1573 1576 '''Return a unix timestamp (or the current time) as a (unixtime,
1574 1577 offset) tuple based off the local timezone.'''
1575 1578 if timestamp is None:
1576 1579 timestamp = time.time()
1577 1580 if timestamp < 0:
1578 1581 hint = _("check your clock")
1579 1582 raise Abort(_("negative timestamp: %d") % timestamp, hint=hint)
1580 1583 delta = (datetime.datetime.utcfromtimestamp(timestamp) -
1581 1584 datetime.datetime.fromtimestamp(timestamp))
1582 1585 tz = delta.days * 86400 + delta.seconds
1583 1586 return timestamp, tz
1584 1587
1585 1588 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1586 1589 """represent a (unixtime, offset) tuple as a localized time.
1587 1590 unixtime is seconds since the epoch, and offset is the time zone's
1588 1591 number of seconds away from UTC.
1589 1592
1590 1593 >>> datestr((0, 0))
1591 1594 'Thu Jan 01 00:00:00 1970 +0000'
1592 1595 >>> datestr((42, 0))
1593 1596 'Thu Jan 01 00:00:42 1970 +0000'
1594 1597 >>> datestr((-42, 0))
1595 1598 'Wed Dec 31 23:59:18 1969 +0000'
1596 1599 >>> datestr((0x7fffffff, 0))
1597 1600 'Tue Jan 19 03:14:07 2038 +0000'
1598 1601 >>> datestr((-0x80000000, 0))
1599 1602 'Fri Dec 13 20:45:52 1901 +0000'
1600 1603 """
1601 1604 t, tz = date or makedate()
1602 1605 if "%1" in format or "%2" in format or "%z" in format:
1603 1606 sign = (tz > 0) and "-" or "+"
1604 1607 minutes = abs(tz) // 60
1605 1608 q, r = divmod(minutes, 60)
1606 1609 format = format.replace("%z", "%1%2")
1607 1610 format = format.replace("%1", "%c%02d" % (sign, q))
1608 1611 format = format.replace("%2", "%02d" % r)
1609 1612 d = t - tz
1610 1613 if d > 0x7fffffff:
1611 1614 d = 0x7fffffff
1612 1615 elif d < -0x80000000:
1613 1616 d = -0x80000000
1614 1617 # Never use time.gmtime() and datetime.datetime.fromtimestamp()
1615 1618 # because they use the gmtime() system call which is buggy on Windows
1616 1619 # for negative values.
1617 1620 t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d)
1618 1621 s = t.strftime(format)
1619 1622 return s
1620 1623
1621 1624 def shortdate(date=None):
1622 1625 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1623 1626 return datestr(date, format='%Y-%m-%d')
1624 1627
1625 1628 def parsetimezone(tz):
1626 1629 """parse a timezone string and return an offset integer"""
1627 1630 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1628 1631 sign = (tz[0] == "+") and 1 or -1
1629 1632 hours = int(tz[1:3])
1630 1633 minutes = int(tz[3:5])
1631 1634 return -sign * (hours * 60 + minutes) * 60
1632 1635 if tz == "GMT" or tz == "UTC":
1633 1636 return 0
1634 1637 return None
1635 1638
1636 1639 def strdate(string, format, defaults=[]):
1637 1640 """parse a localized time string and return a (unixtime, offset) tuple.
1638 1641 if the string cannot be parsed, ValueError is raised."""
1639 1642 # NOTE: unixtime = localunixtime + offset
1640 1643 offset, date = parsetimezone(string.split()[-1]), string
1641 1644 if offset is not None:
1642 1645 date = " ".join(string.split()[:-1])
1643 1646
1644 1647 # add missing elements from defaults
1645 1648 usenow = False # default to using biased defaults
1646 1649 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
1647 1650 found = [True for p in part if ("%"+p) in format]
1648 1651 if not found:
1649 1652 date += "@" + defaults[part][usenow]
1650 1653 format += "@%" + part[0]
1651 1654 else:
1652 1655 # We've found a specific time element, less specific time
1653 1656 # elements are relative to today
1654 1657 usenow = True
1655 1658
1656 1659 timetuple = time.strptime(date, format)
1657 1660 localunixtime = int(calendar.timegm(timetuple))
1658 1661 if offset is None:
1659 1662 # local timezone
1660 1663 unixtime = int(time.mktime(timetuple))
1661 1664 offset = unixtime - localunixtime
1662 1665 else:
1663 1666 unixtime = localunixtime + offset
1664 1667 return unixtime, offset
1665 1668
1666 1669 def parsedate(date, formats=None, bias=None):
1667 1670 """parse a localized date/time and return a (unixtime, offset) tuple.
1668 1671
1669 1672 The date may be a "unixtime offset" string or in one of the specified
1670 1673 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1671 1674
1672 1675 >>> parsedate(' today ') == parsedate(\
1673 1676 datetime.date.today().strftime('%b %d'))
1674 1677 True
1675 1678 >>> parsedate( 'yesterday ') == parsedate((datetime.date.today() -\
1676 1679 datetime.timedelta(days=1)\
1677 1680 ).strftime('%b %d'))
1678 1681 True
1679 1682 >>> now, tz = makedate()
1680 1683 >>> strnow, strtz = parsedate('now')
1681 1684 >>> (strnow - now) < 1
1682 1685 True
1683 1686 >>> tz == strtz
1684 1687 True
1685 1688 """
1686 1689 if bias is None:
1687 1690 bias = {}
1688 1691 if not date:
1689 1692 return 0, 0
1690 1693 if isinstance(date, tuple) and len(date) == 2:
1691 1694 return date
1692 1695 if not formats:
1693 1696 formats = defaultdateformats
1694 1697 date = date.strip()
1695 1698
1696 1699 if date == 'now' or date == _('now'):
1697 1700 return makedate()
1698 1701 if date == 'today' or date == _('today'):
1699 1702 date = datetime.date.today().strftime('%b %d')
1700 1703 elif date == 'yesterday' or date == _('yesterday'):
1701 1704 date = (datetime.date.today() -
1702 1705 datetime.timedelta(days=1)).strftime('%b %d')
1703 1706
1704 1707 try:
1705 1708 when, offset = map(int, date.split(' '))
1706 1709 except ValueError:
1707 1710 # fill out defaults
1708 1711 now = makedate()
1709 1712 defaults = {}
1710 1713 for part in ("d", "mb", "yY", "HI", "M", "S"):
1711 1714 # this piece is for rounding the specific end of unknowns
1712 1715 b = bias.get(part)
1713 1716 if b is None:
1714 1717 if part[0] in "HMS":
1715 1718 b = "00"
1716 1719 else:
1717 1720 b = "0"
1718 1721
1719 1722 # this piece is for matching the generic end to today's date
1720 1723 n = datestr(now, "%" + part[0])
1721 1724
1722 1725 defaults[part] = (b, n)
1723 1726
1724 1727 for format in formats:
1725 1728 try:
1726 1729 when, offset = strdate(date, format, defaults)
1727 1730 except (ValueError, OverflowError):
1728 1731 pass
1729 1732 else:
1730 1733 break
1731 1734 else:
1732 1735 raise Abort(_('invalid date: %r') % date)
1733 1736 # validate explicit (probably user-specified) date and
1734 1737 # time zone offset. values must fit in signed 32 bits for
1735 1738 # current 32-bit linux runtimes. timezones go from UTC-12
1736 1739 # to UTC+14
1737 1740 if when < -0x80000000 or when > 0x7fffffff:
1738 1741 raise Abort(_('date exceeds 32 bits: %d') % when)
1739 1742 if offset < -50400 or offset > 43200:
1740 1743 raise Abort(_('impossible time zone offset: %d') % offset)
1741 1744 return when, offset
1742 1745
1743 1746 def matchdate(date):
1744 1747 """Return a function that matches a given date match specifier
1745 1748
1746 1749 Formats include:
1747 1750
1748 1751 '{date}' match a given date to the accuracy provided
1749 1752
1750 1753 '<{date}' on or before a given date
1751 1754
1752 1755 '>{date}' on or after a given date
1753 1756
1754 1757 >>> p1 = parsedate("10:29:59")
1755 1758 >>> p2 = parsedate("10:30:00")
1756 1759 >>> p3 = parsedate("10:30:59")
1757 1760 >>> p4 = parsedate("10:31:00")
1758 1761 >>> p5 = parsedate("Sep 15 10:30:00 1999")
1759 1762 >>> f = matchdate("10:30")
1760 1763 >>> f(p1[0])
1761 1764 False
1762 1765 >>> f(p2[0])
1763 1766 True
1764 1767 >>> f(p3[0])
1765 1768 True
1766 1769 >>> f(p4[0])
1767 1770 False
1768 1771 >>> f(p5[0])
1769 1772 False
1770 1773 """
1771 1774
1772 1775 def lower(date):
1773 1776 d = {'mb': "1", 'd': "1"}
1774 1777 return parsedate(date, extendeddateformats, d)[0]
1775 1778
1776 1779 def upper(date):
1777 1780 d = {'mb': "12", 'HI': "23", 'M': "59", 'S': "59"}
1778 1781 for days in ("31", "30", "29"):
1779 1782 try:
1780 1783 d["d"] = days
1781 1784 return parsedate(date, extendeddateformats, d)[0]
1782 1785 except Abort:
1783 1786 pass
1784 1787 d["d"] = "28"
1785 1788 return parsedate(date, extendeddateformats, d)[0]
1786 1789
1787 1790 date = date.strip()
1788 1791
1789 1792 if not date:
1790 1793 raise Abort(_("dates cannot consist entirely of whitespace"))
1791 1794 elif date[0] == "<":
1792 1795 if not date[1:]:
1793 1796 raise Abort(_("invalid day spec, use '<DATE'"))
1794 1797 when = upper(date[1:])
1795 1798 return lambda x: x <= when
1796 1799 elif date[0] == ">":
1797 1800 if not date[1:]:
1798 1801 raise Abort(_("invalid day spec, use '>DATE'"))
1799 1802 when = lower(date[1:])
1800 1803 return lambda x: x >= when
1801 1804 elif date[0] == "-":
1802 1805 try:
1803 1806 days = int(date[1:])
1804 1807 except ValueError:
1805 1808 raise Abort(_("invalid day spec: %s") % date[1:])
1806 1809 if days < 0:
1807 1810 raise Abort(_('%s must be nonnegative (see "hg help dates")')
1808 1811 % date[1:])
1809 1812 when = makedate()[0] - days * 3600 * 24
1810 1813 return lambda x: x >= when
1811 1814 elif " to " in date:
1812 1815 a, b = date.split(" to ")
1813 1816 start, stop = lower(a), upper(b)
1814 1817 return lambda x: x >= start and x <= stop
1815 1818 else:
1816 1819 start, stop = lower(date), upper(date)
1817 1820 return lambda x: x >= start and x <= stop
1818 1821
1819 1822 def stringmatcher(pattern):
1820 1823 """
1821 1824 accepts a string, possibly starting with 're:' or 'literal:' prefix.
1822 1825 returns the matcher name, pattern, and matcher function.
1823 1826 missing or unknown prefixes are treated as literal matches.
1824 1827
1825 1828 helper for tests:
1826 1829 >>> def test(pattern, *tests):
1827 1830 ... kind, pattern, matcher = stringmatcher(pattern)
1828 1831 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
1829 1832
1830 1833 exact matching (no prefix):
1831 1834 >>> test('abcdefg', 'abc', 'def', 'abcdefg')
1832 1835 ('literal', 'abcdefg', [False, False, True])
1833 1836
1834 1837 regex matching ('re:' prefix)
1835 1838 >>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar')
1836 1839 ('re', 'a.+b', [False, False, True])
1837 1840
1838 1841 force exact matches ('literal:' prefix)
1839 1842 >>> test('literal:re:foobar', 'foobar', 're:foobar')
1840 1843 ('literal', 're:foobar', [False, True])
1841 1844
1842 1845 unknown prefixes are ignored and treated as literals
1843 1846 >>> test('foo:bar', 'foo', 'bar', 'foo:bar')
1844 1847 ('literal', 'foo:bar', [False, False, True])
1845 1848 """
1846 1849 if pattern.startswith('re:'):
1847 1850 pattern = pattern[3:]
1848 1851 try:
1849 1852 regex = remod.compile(pattern)
1850 1853 except remod.error as e:
1851 1854 raise error.ParseError(_('invalid regular expression: %s')
1852 1855 % e)
1853 1856 return 're', pattern, regex.search
1854 1857 elif pattern.startswith('literal:'):
1855 1858 pattern = pattern[8:]
1856 1859 return 'literal', pattern, pattern.__eq__
1857 1860
1858 1861 def shortuser(user):
1859 1862 """Return a short representation of a user name or email address."""
1860 1863 f = user.find('@')
1861 1864 if f >= 0:
1862 1865 user = user[:f]
1863 1866 f = user.find('<')
1864 1867 if f >= 0:
1865 1868 user = user[f + 1:]
1866 1869 f = user.find(' ')
1867 1870 if f >= 0:
1868 1871 user = user[:f]
1869 1872 f = user.find('.')
1870 1873 if f >= 0:
1871 1874 user = user[:f]
1872 1875 return user
1873 1876
1874 1877 def emailuser(user):
1875 1878 """Return the user portion of an email address."""
1876 1879 f = user.find('@')
1877 1880 if f >= 0:
1878 1881 user = user[:f]
1879 1882 f = user.find('<')
1880 1883 if f >= 0:
1881 1884 user = user[f + 1:]
1882 1885 return user
1883 1886
1884 1887 def email(author):
1885 1888 '''get email of author.'''
1886 1889 r = author.find('>')
1887 1890 if r == -1:
1888 1891 r = None
1889 1892 return author[author.find('<') + 1:r]
1890 1893
1891 1894 def ellipsis(text, maxlength=400):
1892 1895 """Trim string to at most maxlength (default: 400) columns in display."""
1893 1896 return encoding.trim(text, maxlength, ellipsis='...')
1894 1897
1895 1898 def unitcountfn(*unittable):
1896 1899 '''return a function that renders a readable count of some quantity'''
1897 1900
1898 1901 def go(count):
1899 1902 for multiplier, divisor, format in unittable:
1900 1903 if count >= divisor * multiplier:
1901 1904 return format % (count / float(divisor))
1902 1905 return unittable[-1][2] % count
1903 1906
1904 1907 return go
1905 1908
1906 1909 bytecount = unitcountfn(
1907 1910 (100, 1 << 30, _('%.0f GB')),
1908 1911 (10, 1 << 30, _('%.1f GB')),
1909 1912 (1, 1 << 30, _('%.2f GB')),
1910 1913 (100, 1 << 20, _('%.0f MB')),
1911 1914 (10, 1 << 20, _('%.1f MB')),
1912 1915 (1, 1 << 20, _('%.2f MB')),
1913 1916 (100, 1 << 10, _('%.0f KB')),
1914 1917 (10, 1 << 10, _('%.1f KB')),
1915 1918 (1, 1 << 10, _('%.2f KB')),
1916 1919 (1, 1, _('%.0f bytes')),
1917 1920 )
1918 1921
1919 1922 def uirepr(s):
1920 1923 # Avoid double backslash in Windows path repr()
1921 1924 return repr(s).replace('\\\\', '\\')
1922 1925
1923 1926 # delay import of textwrap
1924 1927 def MBTextWrapper(**kwargs):
1925 1928 class tw(textwrap.TextWrapper):
1926 1929 """
1927 1930 Extend TextWrapper for width-awareness.
1928 1931
1929 1932 Neither number of 'bytes' in any encoding nor 'characters' is
1930 1933 appropriate to calculate terminal columns for specified string.
1931 1934
1932 1935 Original TextWrapper implementation uses built-in 'len()' directly,
1933 1936 so overriding is needed to use width information of each characters.
1934 1937
1935 1938 In addition, characters classified into 'ambiguous' width are
1936 1939 treated as wide in East Asian area, but as narrow in other.
1937 1940
1938 1941 This requires use decision to determine width of such characters.
1939 1942 """
1940 1943 def _cutdown(self, ucstr, space_left):
1941 1944 l = 0
1942 1945 colwidth = encoding.ucolwidth
1943 1946 for i in xrange(len(ucstr)):
1944 1947 l += colwidth(ucstr[i])
1945 1948 if space_left < l:
1946 1949 return (ucstr[:i], ucstr[i:])
1947 1950 return ucstr, ''
1948 1951
1949 1952 # overriding of base class
1950 1953 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
1951 1954 space_left = max(width - cur_len, 1)
1952 1955
1953 1956 if self.break_long_words:
1954 1957 cut, res = self._cutdown(reversed_chunks[-1], space_left)
1955 1958 cur_line.append(cut)
1956 1959 reversed_chunks[-1] = res
1957 1960 elif not cur_line:
1958 1961 cur_line.append(reversed_chunks.pop())
1959 1962
1960 1963 # this overriding code is imported from TextWrapper of Python 2.6
1961 1964 # to calculate columns of string by 'encoding.ucolwidth()'
1962 1965 def _wrap_chunks(self, chunks):
1963 1966 colwidth = encoding.ucolwidth
1964 1967
1965 1968 lines = []
1966 1969 if self.width <= 0:
1967 1970 raise ValueError("invalid width %r (must be > 0)" % self.width)
1968 1971
1969 1972 # Arrange in reverse order so items can be efficiently popped
1970 1973 # from a stack of chucks.
1971 1974 chunks.reverse()
1972 1975
1973 1976 while chunks:
1974 1977
1975 1978 # Start the list of chunks that will make up the current line.
1976 1979 # cur_len is just the length of all the chunks in cur_line.
1977 1980 cur_line = []
1978 1981 cur_len = 0
1979 1982
1980 1983 # Figure out which static string will prefix this line.
1981 1984 if lines:
1982 1985 indent = self.subsequent_indent
1983 1986 else:
1984 1987 indent = self.initial_indent
1985 1988
1986 1989 # Maximum width for this line.
1987 1990 width = self.width - len(indent)
1988 1991
1989 1992 # First chunk on line is whitespace -- drop it, unless this
1990 1993 # is the very beginning of the text (i.e. no lines started yet).
1991 1994 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
1992 1995 del chunks[-1]
1993 1996
1994 1997 while chunks:
1995 1998 l = colwidth(chunks[-1])
1996 1999
1997 2000 # Can at least squeeze this chunk onto the current line.
1998 2001 if cur_len + l <= width:
1999 2002 cur_line.append(chunks.pop())
2000 2003 cur_len += l
2001 2004
2002 2005 # Nope, this line is full.
2003 2006 else:
2004 2007 break
2005 2008
2006 2009 # The current line is full, and the next chunk is too big to
2007 2010 # fit on *any* line (not just this one).
2008 2011 if chunks and colwidth(chunks[-1]) > width:
2009 2012 self._handle_long_word(chunks, cur_line, cur_len, width)
2010 2013
2011 2014 # If the last chunk on this line is all whitespace, drop it.
2012 2015 if (self.drop_whitespace and
2013 2016 cur_line and cur_line[-1].strip() == ''):
2014 2017 del cur_line[-1]
2015 2018
2016 2019 # Convert current line back to a string and store it in list
2017 2020 # of all lines (return value).
2018 2021 if cur_line:
2019 2022 lines.append(indent + ''.join(cur_line))
2020 2023
2021 2024 return lines
2022 2025
2023 2026 global MBTextWrapper
2024 2027 MBTextWrapper = tw
2025 2028 return tw(**kwargs)
2026 2029
2027 2030 def wrap(line, width, initindent='', hangindent=''):
2028 2031 maxindent = max(len(hangindent), len(initindent))
2029 2032 if width <= maxindent:
2030 2033 # adjust for weird terminal size
2031 2034 width = max(78, maxindent + 1)
2032 2035 line = line.decode(encoding.encoding, encoding.encodingmode)
2033 2036 initindent = initindent.decode(encoding.encoding, encoding.encodingmode)
2034 2037 hangindent = hangindent.decode(encoding.encoding, encoding.encodingmode)
2035 2038 wrapper = MBTextWrapper(width=width,
2036 2039 initial_indent=initindent,
2037 2040 subsequent_indent=hangindent)
2038 2041 return wrapper.fill(line).encode(encoding.encoding)
2039 2042
2040 2043 def iterlines(iterator):
2041 2044 for chunk in iterator:
2042 2045 for line in chunk.splitlines():
2043 2046 yield line
2044 2047
2045 2048 def expandpath(path):
2046 2049 return os.path.expanduser(os.path.expandvars(path))
2047 2050
2048 2051 def hgcmd():
2049 2052 """Return the command used to execute current hg
2050 2053
2051 2054 This is different from hgexecutable() because on Windows we want
2052 2055 to avoid things opening new shell windows like batch files, so we
2053 2056 get either the python call or current executable.
2054 2057 """
2055 2058 if mainfrozen():
2056 2059 if getattr(sys, 'frozen', None) == 'macosx_app':
2057 2060 # Env variable set by py2app
2058 2061 return [os.environ['EXECUTABLEPATH']]
2059 2062 else:
2060 2063 return [sys.executable]
2061 2064 return gethgcmd()
2062 2065
2063 2066 def rundetached(args, condfn):
2064 2067 """Execute the argument list in a detached process.
2065 2068
2066 2069 condfn is a callable which is called repeatedly and should return
2067 2070 True once the child process is known to have started successfully.
2068 2071 At this point, the child process PID is returned. If the child
2069 2072 process fails to start or finishes before condfn() evaluates to
2070 2073 True, return -1.
2071 2074 """
2072 2075 # Windows case is easier because the child process is either
2073 2076 # successfully starting and validating the condition or exiting
2074 2077 # on failure. We just poll on its PID. On Unix, if the child
2075 2078 # process fails to start, it will be left in a zombie state until
2076 2079 # the parent wait on it, which we cannot do since we expect a long
2077 2080 # running process on success. Instead we listen for SIGCHLD telling
2078 2081 # us our child process terminated.
2079 2082 terminated = set()
2080 2083 def handler(signum, frame):
2081 2084 terminated.add(os.wait())
2082 2085 prevhandler = None
2083 2086 SIGCHLD = getattr(signal, 'SIGCHLD', None)
2084 2087 if SIGCHLD is not None:
2085 2088 prevhandler = signal.signal(SIGCHLD, handler)
2086 2089 try:
2087 2090 pid = spawndetached(args)
2088 2091 while not condfn():
2089 2092 if ((pid in terminated or not testpid(pid))
2090 2093 and not condfn()):
2091 2094 return -1
2092 2095 time.sleep(0.1)
2093 2096 return pid
2094 2097 finally:
2095 2098 if prevhandler is not None:
2096 2099 signal.signal(signal.SIGCHLD, prevhandler)
2097 2100
2098 2101 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2099 2102 """Return the result of interpolating items in the mapping into string s.
2100 2103
2101 2104 prefix is a single character string, or a two character string with
2102 2105 a backslash as the first character if the prefix needs to be escaped in
2103 2106 a regular expression.
2104 2107
2105 2108 fn is an optional function that will be applied to the replacement text
2106 2109 just before replacement.
2107 2110
2108 2111 escape_prefix is an optional flag that allows using doubled prefix for
2109 2112 its escaping.
2110 2113 """
2111 2114 fn = fn or (lambda s: s)
2112 2115 patterns = '|'.join(mapping.keys())
2113 2116 if escape_prefix:
2114 2117 patterns += '|' + prefix
2115 2118 if len(prefix) > 1:
2116 2119 prefix_char = prefix[1:]
2117 2120 else:
2118 2121 prefix_char = prefix
2119 2122 mapping[prefix_char] = prefix_char
2120 2123 r = remod.compile(r'%s(%s)' % (prefix, patterns))
2121 2124 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2122 2125
2123 2126 def getport(port):
2124 2127 """Return the port for a given network service.
2125 2128
2126 2129 If port is an integer, it's returned as is. If it's a string, it's
2127 2130 looked up using socket.getservbyname(). If there's no matching
2128 2131 service, error.Abort is raised.
2129 2132 """
2130 2133 try:
2131 2134 return int(port)
2132 2135 except ValueError:
2133 2136 pass
2134 2137
2135 2138 try:
2136 2139 return socket.getservbyname(port)
2137 2140 except socket.error:
2138 2141 raise Abort(_("no port number associated with service '%s'") % port)
2139 2142
2140 2143 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
2141 2144 '0': False, 'no': False, 'false': False, 'off': False,
2142 2145 'never': False}
2143 2146
2144 2147 def parsebool(s):
2145 2148 """Parse s into a boolean.
2146 2149
2147 2150 If s is not a valid boolean, returns None.
2148 2151 """
2149 2152 return _booleans.get(s.lower(), None)
2150 2153
2151 2154 _hexdig = '0123456789ABCDEFabcdef'
2152 2155 _hextochr = dict((a + b, chr(int(a + b, 16)))
2153 2156 for a in _hexdig for b in _hexdig)
2154 2157
2155 2158 def _urlunquote(s):
2156 2159 """Decode HTTP/HTML % encoding.
2157 2160
2158 2161 >>> _urlunquote('abc%20def')
2159 2162 'abc def'
2160 2163 """
2161 2164 res = s.split('%')
2162 2165 # fastpath
2163 2166 if len(res) == 1:
2164 2167 return s
2165 2168 s = res[0]
2166 2169 for item in res[1:]:
2167 2170 try:
2168 2171 s += _hextochr[item[:2]] + item[2:]
2169 2172 except KeyError:
2170 2173 s += '%' + item
2171 2174 except UnicodeDecodeError:
2172 2175 s += unichr(int(item[:2], 16)) + item[2:]
2173 2176 return s
2174 2177
2175 2178 class url(object):
2176 2179 r"""Reliable URL parser.
2177 2180
2178 2181 This parses URLs and provides attributes for the following
2179 2182 components:
2180 2183
2181 2184 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2182 2185
2183 2186 Missing components are set to None. The only exception is
2184 2187 fragment, which is set to '' if present but empty.
2185 2188
2186 2189 If parsefragment is False, fragment is included in query. If
2187 2190 parsequery is False, query is included in path. If both are
2188 2191 False, both fragment and query are included in path.
2189 2192
2190 2193 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2191 2194
2192 2195 Note that for backward compatibility reasons, bundle URLs do not
2193 2196 take host names. That means 'bundle://../' has a path of '../'.
2194 2197
2195 2198 Examples:
2196 2199
2197 2200 >>> url('http://www.ietf.org/rfc/rfc2396.txt')
2198 2201 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2199 2202 >>> url('ssh://[::1]:2200//home/joe/repo')
2200 2203 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2201 2204 >>> url('file:///home/joe/repo')
2202 2205 <url scheme: 'file', path: '/home/joe/repo'>
2203 2206 >>> url('file:///c:/temp/foo/')
2204 2207 <url scheme: 'file', path: 'c:/temp/foo/'>
2205 2208 >>> url('bundle:foo')
2206 2209 <url scheme: 'bundle', path: 'foo'>
2207 2210 >>> url('bundle://../foo')
2208 2211 <url scheme: 'bundle', path: '../foo'>
2209 2212 >>> url(r'c:\foo\bar')
2210 2213 <url path: 'c:\\foo\\bar'>
2211 2214 >>> url(r'\\blah\blah\blah')
2212 2215 <url path: '\\\\blah\\blah\\blah'>
2213 2216 >>> url(r'\\blah\blah\blah#baz')
2214 2217 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2215 2218 >>> url(r'file:///C:\users\me')
2216 2219 <url scheme: 'file', path: 'C:\\users\\me'>
2217 2220
2218 2221 Authentication credentials:
2219 2222
2220 2223 >>> url('ssh://joe:xyz@x/repo')
2221 2224 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
2222 2225 >>> url('ssh://joe@x/repo')
2223 2226 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
2224 2227
2225 2228 Query strings and fragments:
2226 2229
2227 2230 >>> url('http://host/a?b#c')
2228 2231 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
2229 2232 >>> url('http://host/a?b#c', parsequery=False, parsefragment=False)
2230 2233 <url scheme: 'http', host: 'host', path: 'a?b#c'>
2231 2234 """
2232 2235
2233 2236 _safechars = "!~*'()+"
2234 2237 _safepchars = "/!~*'()+:\\"
2235 2238 _matchscheme = remod.compile(r'^[a-zA-Z0-9+.\-]+:').match
2236 2239
2237 2240 def __init__(self, path, parsequery=True, parsefragment=True):
2238 2241 # We slowly chomp away at path until we have only the path left
2239 2242 self.scheme = self.user = self.passwd = self.host = None
2240 2243 self.port = self.path = self.query = self.fragment = None
2241 2244 self._localpath = True
2242 2245 self._hostport = ''
2243 2246 self._origpath = path
2244 2247
2245 2248 if parsefragment and '#' in path:
2246 2249 path, self.fragment = path.split('#', 1)
2247 2250 if not path:
2248 2251 path = None
2249 2252
2250 2253 # special case for Windows drive letters and UNC paths
2251 2254 if hasdriveletter(path) or path.startswith(r'\\'):
2252 2255 self.path = path
2253 2256 return
2254 2257
2255 2258 # For compatibility reasons, we can't handle bundle paths as
2256 2259 # normal URLS
2257 2260 if path.startswith('bundle:'):
2258 2261 self.scheme = 'bundle'
2259 2262 path = path[7:]
2260 2263 if path.startswith('//'):
2261 2264 path = path[2:]
2262 2265 self.path = path
2263 2266 return
2264 2267
2265 2268 if self._matchscheme(path):
2266 2269 parts = path.split(':', 1)
2267 2270 if parts[0]:
2268 2271 self.scheme, path = parts
2269 2272 self._localpath = False
2270 2273
2271 2274 if not path:
2272 2275 path = None
2273 2276 if self._localpath:
2274 2277 self.path = ''
2275 2278 return
2276 2279 else:
2277 2280 if self._localpath:
2278 2281 self.path = path
2279 2282 return
2280 2283
2281 2284 if parsequery and '?' in path:
2282 2285 path, self.query = path.split('?', 1)
2283 2286 if not path:
2284 2287 path = None
2285 2288 if not self.query:
2286 2289 self.query = None
2287 2290
2288 2291 # // is required to specify a host/authority
2289 2292 if path and path.startswith('//'):
2290 2293 parts = path[2:].split('/', 1)
2291 2294 if len(parts) > 1:
2292 2295 self.host, path = parts
2293 2296 else:
2294 2297 self.host = parts[0]
2295 2298 path = None
2296 2299 if not self.host:
2297 2300 self.host = None
2298 2301 # path of file:///d is /d
2299 2302 # path of file:///d:/ is d:/, not /d:/
2300 2303 if path and not hasdriveletter(path):
2301 2304 path = '/' + path
2302 2305
2303 2306 if self.host and '@' in self.host:
2304 2307 self.user, self.host = self.host.rsplit('@', 1)
2305 2308 if ':' in self.user:
2306 2309 self.user, self.passwd = self.user.split(':', 1)
2307 2310 if not self.host:
2308 2311 self.host = None
2309 2312
2310 2313 # Don't split on colons in IPv6 addresses without ports
2311 2314 if (self.host and ':' in self.host and
2312 2315 not (self.host.startswith('[') and self.host.endswith(']'))):
2313 2316 self._hostport = self.host
2314 2317 self.host, self.port = self.host.rsplit(':', 1)
2315 2318 if not self.host:
2316 2319 self.host = None
2317 2320
2318 2321 if (self.host and self.scheme == 'file' and
2319 2322 self.host not in ('localhost', '127.0.0.1', '[::1]')):
2320 2323 raise Abort(_('file:// URLs can only refer to localhost'))
2321 2324
2322 2325 self.path = path
2323 2326
2324 2327 # leave the query string escaped
2325 2328 for a in ('user', 'passwd', 'host', 'port',
2326 2329 'path', 'fragment'):
2327 2330 v = getattr(self, a)
2328 2331 if v is not None:
2329 2332 setattr(self, a, _urlunquote(v))
2330 2333
2331 2334 def __repr__(self):
2332 2335 attrs = []
2333 2336 for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path',
2334 2337 'query', 'fragment'):
2335 2338 v = getattr(self, a)
2336 2339 if v is not None:
2337 2340 attrs.append('%s: %r' % (a, v))
2338 2341 return '<url %s>' % ', '.join(attrs)
2339 2342
2340 2343 def __str__(self):
2341 2344 r"""Join the URL's components back into a URL string.
2342 2345
2343 2346 Examples:
2344 2347
2345 2348 >>> str(url('http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
2346 2349 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
2347 2350 >>> str(url('http://user:pw@host:80/?foo=bar&baz=42'))
2348 2351 'http://user:pw@host:80/?foo=bar&baz=42'
2349 2352 >>> str(url('http://user:pw@host:80/?foo=bar%3dbaz'))
2350 2353 'http://user:pw@host:80/?foo=bar%3dbaz'
2351 2354 >>> str(url('ssh://user:pw@[::1]:2200//home/joe#'))
2352 2355 'ssh://user:pw@[::1]:2200//home/joe#'
2353 2356 >>> str(url('http://localhost:80//'))
2354 2357 'http://localhost:80//'
2355 2358 >>> str(url('http://localhost:80/'))
2356 2359 'http://localhost:80/'
2357 2360 >>> str(url('http://localhost:80'))
2358 2361 'http://localhost:80/'
2359 2362 >>> str(url('bundle:foo'))
2360 2363 'bundle:foo'
2361 2364 >>> str(url('bundle://../foo'))
2362 2365 'bundle:../foo'
2363 2366 >>> str(url('path'))
2364 2367 'path'
2365 2368 >>> str(url('file:///tmp/foo/bar'))
2366 2369 'file:///tmp/foo/bar'
2367 2370 >>> str(url('file:///c:/tmp/foo/bar'))
2368 2371 'file:///c:/tmp/foo/bar'
2369 2372 >>> print url(r'bundle:foo\bar')
2370 2373 bundle:foo\bar
2371 2374 >>> print url(r'file:///D:\data\hg')
2372 2375 file:///D:\data\hg
2373 2376 """
2374 2377 if self._localpath:
2375 2378 s = self.path
2376 2379 if self.scheme == 'bundle':
2377 2380 s = 'bundle:' + s
2378 2381 if self.fragment:
2379 2382 s += '#' + self.fragment
2380 2383 return s
2381 2384
2382 2385 s = self.scheme + ':'
2383 2386 if self.user or self.passwd or self.host:
2384 2387 s += '//'
2385 2388 elif self.scheme and (not self.path or self.path.startswith('/')
2386 2389 or hasdriveletter(self.path)):
2387 2390 s += '//'
2388 2391 if hasdriveletter(self.path):
2389 2392 s += '/'
2390 2393 if self.user:
2391 s += urllib.quote(self.user, safe=self._safechars)
2394 s += urlreq.quote(self.user, safe=self._safechars)
2392 2395 if self.passwd:
2393 s += ':' + urllib.quote(self.passwd, safe=self._safechars)
2396 s += ':' + urlreq.quote(self.passwd, safe=self._safechars)
2394 2397 if self.user or self.passwd:
2395 2398 s += '@'
2396 2399 if self.host:
2397 2400 if not (self.host.startswith('[') and self.host.endswith(']')):
2398 s += urllib.quote(self.host)
2401 s += urlreq.quote(self.host)
2399 2402 else:
2400 2403 s += self.host
2401 2404 if self.port:
2402 s += ':' + urllib.quote(self.port)
2405 s += ':' + urlreq.quote(self.port)
2403 2406 if self.host:
2404 2407 s += '/'
2405 2408 if self.path:
2406 2409 # TODO: similar to the query string, we should not unescape the
2407 2410 # path when we store it, the path might contain '%2f' = '/',
2408 2411 # which we should *not* escape.
2409 s += urllib.quote(self.path, safe=self._safepchars)
2412 s += urlreq.quote(self.path, safe=self._safepchars)
2410 2413 if self.query:
2411 2414 # we store the query in escaped form.
2412 2415 s += '?' + self.query
2413 2416 if self.fragment is not None:
2414 s += '#' + urllib.quote(self.fragment, safe=self._safepchars)
2417 s += '#' + urlreq.quote(self.fragment, safe=self._safepchars)
2415 2418 return s
2416 2419
2417 2420 def authinfo(self):
2418 2421 user, passwd = self.user, self.passwd
2419 2422 try:
2420 2423 self.user, self.passwd = None, None
2421 2424 s = str(self)
2422 2425 finally:
2423 2426 self.user, self.passwd = user, passwd
2424 2427 if not self.user:
2425 2428 return (s, None)
2426 2429 # authinfo[1] is passed to urllib2 password manager, and its
2427 2430 # URIs must not contain credentials. The host is passed in the
2428 2431 # URIs list because Python < 2.4.3 uses only that to search for
2429 2432 # a password.
2430 2433 return (s, (None, (s, self.host),
2431 2434 self.user, self.passwd or ''))
2432 2435
2433 2436 def isabs(self):
2434 2437 if self.scheme and self.scheme != 'file':
2435 2438 return True # remote URL
2436 2439 if hasdriveletter(self.path):
2437 2440 return True # absolute for our purposes - can't be joined()
2438 2441 if self.path.startswith(r'\\'):
2439 2442 return True # Windows UNC path
2440 2443 if self.path.startswith('/'):
2441 2444 return True # POSIX-style
2442 2445 return False
2443 2446
2444 2447 def localpath(self):
2445 2448 if self.scheme == 'file' or self.scheme == 'bundle':
2446 2449 path = self.path or '/'
2447 2450 # For Windows, we need to promote hosts containing drive
2448 2451 # letters to paths with drive letters.
2449 2452 if hasdriveletter(self._hostport):
2450 2453 path = self._hostport + '/' + self.path
2451 2454 elif (self.host is not None and self.path
2452 2455 and not hasdriveletter(path)):
2453 2456 path = '/' + path
2454 2457 return path
2455 2458 return self._origpath
2456 2459
2457 2460 def islocal(self):
2458 2461 '''whether localpath will return something that posixfile can open'''
2459 2462 return (not self.scheme or self.scheme == 'file'
2460 2463 or self.scheme == 'bundle')
2461 2464
2462 2465 def hasscheme(path):
2463 2466 return bool(url(path).scheme)
2464 2467
2465 2468 def hasdriveletter(path):
2466 2469 return path and path[1:2] == ':' and path[0:1].isalpha()
2467 2470
2468 2471 def urllocalpath(path):
2469 2472 return url(path, parsequery=False, parsefragment=False).localpath()
2470 2473
2471 2474 def hidepassword(u):
2472 2475 '''hide user credential in a url string'''
2473 2476 u = url(u)
2474 2477 if u.passwd:
2475 2478 u.passwd = '***'
2476 2479 return str(u)
2477 2480
2478 2481 def removeauth(u):
2479 2482 '''remove all authentication information from a url string'''
2480 2483 u = url(u)
2481 2484 u.user = u.passwd = None
2482 2485 return str(u)
2483 2486
2484 2487 def isatty(fp):
2485 2488 try:
2486 2489 return fp.isatty()
2487 2490 except AttributeError:
2488 2491 return False
2489 2492
2490 2493 timecount = unitcountfn(
2491 2494 (1, 1e3, _('%.0f s')),
2492 2495 (100, 1, _('%.1f s')),
2493 2496 (10, 1, _('%.2f s')),
2494 2497 (1, 1, _('%.3f s')),
2495 2498 (100, 0.001, _('%.1f ms')),
2496 2499 (10, 0.001, _('%.2f ms')),
2497 2500 (1, 0.001, _('%.3f ms')),
2498 2501 (100, 0.000001, _('%.1f us')),
2499 2502 (10, 0.000001, _('%.2f us')),
2500 2503 (1, 0.000001, _('%.3f us')),
2501 2504 (100, 0.000000001, _('%.1f ns')),
2502 2505 (10, 0.000000001, _('%.2f ns')),
2503 2506 (1, 0.000000001, _('%.3f ns')),
2504 2507 )
2505 2508
2506 2509 _timenesting = [0]
2507 2510
2508 2511 def timed(func):
2509 2512 '''Report the execution time of a function call to stderr.
2510 2513
2511 2514 During development, use as a decorator when you need to measure
2512 2515 the cost of a function, e.g. as follows:
2513 2516
2514 2517 @util.timed
2515 2518 def foo(a, b, c):
2516 2519 pass
2517 2520 '''
2518 2521
2519 2522 def wrapper(*args, **kwargs):
2520 2523 start = time.time()
2521 2524 indent = 2
2522 2525 _timenesting[0] += indent
2523 2526 try:
2524 2527 return func(*args, **kwargs)
2525 2528 finally:
2526 2529 elapsed = time.time() - start
2527 2530 _timenesting[0] -= indent
2528 2531 sys.stderr.write('%s%s: %s\n' %
2529 2532 (' ' * _timenesting[0], func.__name__,
2530 2533 timecount(elapsed)))
2531 2534 return wrapper
2532 2535
2533 2536 _sizeunits = (('m', 2**20), ('k', 2**10), ('g', 2**30),
2534 2537 ('kb', 2**10), ('mb', 2**20), ('gb', 2**30), ('b', 1))
2535 2538
2536 2539 def sizetoint(s):
2537 2540 '''Convert a space specifier to a byte count.
2538 2541
2539 2542 >>> sizetoint('30')
2540 2543 30
2541 2544 >>> sizetoint('2.2kb')
2542 2545 2252
2543 2546 >>> sizetoint('6M')
2544 2547 6291456
2545 2548 '''
2546 2549 t = s.strip().lower()
2547 2550 try:
2548 2551 for k, u in _sizeunits:
2549 2552 if t.endswith(k):
2550 2553 return int(float(t[:-len(k)]) * u)
2551 2554 return int(t)
2552 2555 except ValueError:
2553 2556 raise error.ParseError(_("couldn't parse size: %s") % s)
2554 2557
2555 2558 class hooks(object):
2556 2559 '''A collection of hook functions that can be used to extend a
2557 2560 function's behavior. Hooks are called in lexicographic order,
2558 2561 based on the names of their sources.'''
2559 2562
2560 2563 def __init__(self):
2561 2564 self._hooks = []
2562 2565
2563 2566 def add(self, source, hook):
2564 2567 self._hooks.append((source, hook))
2565 2568
2566 2569 def __call__(self, *args):
2567 2570 self._hooks.sort(key=lambda x: x[0])
2568 2571 results = []
2569 2572 for source, hook in self._hooks:
2570 2573 results.append(hook(*args))
2571 2574 return results
2572 2575
2573 2576 def getstackframes(skip=0, line=' %-*s in %s\n', fileline='%s:%s'):
2574 2577 '''Yields lines for a nicely formatted stacktrace.
2575 2578 Skips the 'skip' last entries.
2576 2579 Each file+linenumber is formatted according to fileline.
2577 2580 Each line is formatted according to line.
2578 2581 If line is None, it yields:
2579 2582 length of longest filepath+line number,
2580 2583 filepath+linenumber,
2581 2584 function
2582 2585
2583 2586 Not be used in production code but very convenient while developing.
2584 2587 '''
2585 2588 entries = [(fileline % (fn, ln), func)
2586 2589 for fn, ln, func, _text in traceback.extract_stack()[:-skip - 1]]
2587 2590 if entries:
2588 2591 fnmax = max(len(entry[0]) for entry in entries)
2589 2592 for fnln, func in entries:
2590 2593 if line is None:
2591 2594 yield (fnmax, fnln, func)
2592 2595 else:
2593 2596 yield line % (fnmax, fnln, func)
2594 2597
2595 2598 def debugstacktrace(msg='stacktrace', skip=0, f=sys.stderr, otherf=sys.stdout):
2596 2599 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
2597 2600 Skips the 'skip' last entries. By default it will flush stdout first.
2598 2601 It can be used everywhere and intentionally does not require an ui object.
2599 2602 Not be used in production code but very convenient while developing.
2600 2603 '''
2601 2604 if otherf:
2602 2605 otherf.flush()
2603 2606 f.write('%s at:\n' % msg)
2604 2607 for line in getstackframes(skip + 1):
2605 2608 f.write(line)
2606 2609 f.flush()
2607 2610
2608 2611 class dirs(object):
2609 2612 '''a multiset of directory names from a dirstate or manifest'''
2610 2613
2611 2614 def __init__(self, map, skip=None):
2612 2615 self._dirs = {}
2613 2616 addpath = self.addpath
2614 2617 if safehasattr(map, 'iteritems') and skip is not None:
2615 2618 for f, s in map.iteritems():
2616 2619 if s[0] != skip:
2617 2620 addpath(f)
2618 2621 else:
2619 2622 for f in map:
2620 2623 addpath(f)
2621 2624
2622 2625 def addpath(self, path):
2623 2626 dirs = self._dirs
2624 2627 for base in finddirs(path):
2625 2628 if base in dirs:
2626 2629 dirs[base] += 1
2627 2630 return
2628 2631 dirs[base] = 1
2629 2632
2630 2633 def delpath(self, path):
2631 2634 dirs = self._dirs
2632 2635 for base in finddirs(path):
2633 2636 if dirs[base] > 1:
2634 2637 dirs[base] -= 1
2635 2638 return
2636 2639 del dirs[base]
2637 2640
2638 2641 def __iter__(self):
2639 2642 return self._dirs.iterkeys()
2640 2643
2641 2644 def __contains__(self, d):
2642 2645 return d in self._dirs
2643 2646
2644 2647 if safehasattr(parsers, 'dirs'):
2645 2648 dirs = parsers.dirs
2646 2649
2647 2650 def finddirs(path):
2648 2651 pos = path.rfind('/')
2649 2652 while pos != -1:
2650 2653 yield path[:pos]
2651 2654 pos = path.rfind('/', 0, pos)
2652 2655
2653 2656 # compression utility
2654 2657
2655 2658 class nocompress(object):
2656 2659 def compress(self, x):
2657 2660 return x
2658 2661 def flush(self):
2659 2662 return ""
2660 2663
2661 2664 compressors = {
2662 2665 None: nocompress,
2663 2666 # lambda to prevent early import
2664 2667 'BZ': lambda: bz2.BZ2Compressor(),
2665 2668 'GZ': lambda: zlib.compressobj(),
2666 2669 }
2667 2670 # also support the old form by courtesies
2668 2671 compressors['UN'] = compressors[None]
2669 2672
2670 2673 def _makedecompressor(decompcls):
2671 2674 def generator(f):
2672 2675 d = decompcls()
2673 2676 for chunk in filechunkiter(f):
2674 2677 yield d.decompress(chunk)
2675 2678 def func(fh):
2676 2679 return chunkbuffer(generator(fh))
2677 2680 return func
2678 2681
2679 2682 class ctxmanager(object):
2680 2683 '''A context manager for use in 'with' blocks to allow multiple
2681 2684 contexts to be entered at once. This is both safer and more
2682 2685 flexible than contextlib.nested.
2683 2686
2684 2687 Once Mercurial supports Python 2.7+, this will become mostly
2685 2688 unnecessary.
2686 2689 '''
2687 2690
2688 2691 def __init__(self, *args):
2689 2692 '''Accepts a list of no-argument functions that return context
2690 2693 managers. These will be invoked at __call__ time.'''
2691 2694 self._pending = args
2692 2695 self._atexit = []
2693 2696
2694 2697 def __enter__(self):
2695 2698 return self
2696 2699
2697 2700 def enter(self):
2698 2701 '''Create and enter context managers in the order in which they were
2699 2702 passed to the constructor.'''
2700 2703 values = []
2701 2704 for func in self._pending:
2702 2705 obj = func()
2703 2706 values.append(obj.__enter__())
2704 2707 self._atexit.append(obj.__exit__)
2705 2708 del self._pending
2706 2709 return values
2707 2710
2708 2711 def atexit(self, func, *args, **kwargs):
2709 2712 '''Add a function to call when this context manager exits. The
2710 2713 ordering of multiple atexit calls is unspecified, save that
2711 2714 they will happen before any __exit__ functions.'''
2712 2715 def wrapper(exc_type, exc_val, exc_tb):
2713 2716 func(*args, **kwargs)
2714 2717 self._atexit.append(wrapper)
2715 2718 return func
2716 2719
2717 2720 def __exit__(self, exc_type, exc_val, exc_tb):
2718 2721 '''Context managers are exited in the reverse order from which
2719 2722 they were created.'''
2720 2723 received = exc_type is not None
2721 2724 suppressed = False
2722 2725 pending = None
2723 2726 self._atexit.reverse()
2724 2727 for exitfunc in self._atexit:
2725 2728 try:
2726 2729 if exitfunc(exc_type, exc_val, exc_tb):
2727 2730 suppressed = True
2728 2731 exc_type = None
2729 2732 exc_val = None
2730 2733 exc_tb = None
2731 2734 except BaseException:
2732 2735 pending = sys.exc_info()
2733 2736 exc_type, exc_val, exc_tb = pending = sys.exc_info()
2734 2737 del self._atexit
2735 2738 if pending:
2736 2739 raise exc_val
2737 2740 return received and suppressed
2738 2741
2739 2742 def _bz2():
2740 2743 d = bz2.BZ2Decompressor()
2741 2744 # Bzip2 stream start with BZ, but we stripped it.
2742 2745 # we put it back for good measure.
2743 2746 d.decompress('BZ')
2744 2747 return d
2745 2748
2746 2749 decompressors = {None: lambda fh: fh,
2747 2750 '_truncatedBZ': _makedecompressor(_bz2),
2748 2751 'BZ': _makedecompressor(lambda: bz2.BZ2Decompressor()),
2749 2752 'GZ': _makedecompressor(lambda: zlib.decompressobj()),
2750 2753 }
2751 2754 # also support the old form by courtesies
2752 2755 decompressors['UN'] = decompressors[None]
2753 2756
2754 2757 # convenient shortcut
2755 2758 dst = debugstacktrace
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now