##// 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 # acl.py - changeset access control for mercurial
1 # acl.py - changeset access control for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''hooks for controlling repository access
8 '''hooks for controlling repository access
9
9
10 This hook makes it possible to allow or deny write access to given
10 This hook makes it possible to allow or deny write access to given
11 branches and paths of a repository when receiving incoming changesets
11 branches and paths of a repository when receiving incoming changesets
12 via pretxnchangegroup and pretxncommit.
12 via pretxnchangegroup and pretxncommit.
13
13
14 The authorization is matched based on the local user name on the
14 The authorization is matched based on the local user name on the
15 system where the hook runs, and not the committer of the original
15 system where the hook runs, and not the committer of the original
16 changeset (since the latter is merely informative).
16 changeset (since the latter is merely informative).
17
17
18 The acl hook is best used along with a restricted shell like hgsh,
18 The acl hook is best used along with a restricted shell like hgsh,
19 preventing authenticating users from doing anything other than pushing
19 preventing authenticating users from doing anything other than pushing
20 or pulling. The hook is not safe to use if users have interactive
20 or pulling. The hook is not safe to use if users have interactive
21 shell access, as they can then disable the hook. Nor is it safe if
21 shell access, as they can then disable the hook. Nor is it safe if
22 remote users share an account, because then there is no way to
22 remote users share an account, because then there is no way to
23 distinguish them.
23 distinguish them.
24
24
25 The order in which access checks are performed is:
25 The order in which access checks are performed is:
26
26
27 1) Deny list for branches (section ``acl.deny.branches``)
27 1) Deny list for branches (section ``acl.deny.branches``)
28 2) Allow list for branches (section ``acl.allow.branches``)
28 2) Allow list for branches (section ``acl.allow.branches``)
29 3) Deny list for paths (section ``acl.deny``)
29 3) Deny list for paths (section ``acl.deny``)
30 4) Allow list for paths (section ``acl.allow``)
30 4) Allow list for paths (section ``acl.allow``)
31
31
32 The allow and deny sections take key-value pairs.
32 The allow and deny sections take key-value pairs.
33
33
34 Branch-based Access Control
34 Branch-based Access Control
35 ---------------------------
35 ---------------------------
36
36
37 Use the ``acl.deny.branches`` and ``acl.allow.branches`` sections to
37 Use the ``acl.deny.branches`` and ``acl.allow.branches`` sections to
38 have branch-based access control. Keys in these sections can be
38 have branch-based access control. Keys in these sections can be
39 either:
39 either:
40
40
41 - a branch name, or
41 - a branch name, or
42 - an asterisk, to match any branch;
42 - an asterisk, to match any branch;
43
43
44 The corresponding values can be either:
44 The corresponding values can be either:
45
45
46 - a comma-separated list containing users and groups, or
46 - a comma-separated list containing users and groups, or
47 - an asterisk, to match anyone;
47 - an asterisk, to match anyone;
48
48
49 You can add the "!" prefix to a user or group name to invert the sense
49 You can add the "!" prefix to a user or group name to invert the sense
50 of the match.
50 of the match.
51
51
52 Path-based Access Control
52 Path-based Access Control
53 -------------------------
53 -------------------------
54
54
55 Use the ``acl.deny`` and ``acl.allow`` sections to have path-based
55 Use the ``acl.deny`` and ``acl.allow`` sections to have path-based
56 access control. Keys in these sections accept a subtree pattern (with
56 access control. Keys in these sections accept a subtree pattern (with
57 a glob syntax by default). The corresponding values follow the same
57 a glob syntax by default). The corresponding values follow the same
58 syntax as the other sections above.
58 syntax as the other sections above.
59
59
60 Groups
60 Groups
61 ------
61 ------
62
62
63 Group names must be prefixed with an ``@`` symbol. Specifying a group
63 Group names must be prefixed with an ``@`` symbol. Specifying a group
64 name has the same effect as specifying all the users in that group.
64 name has the same effect as specifying all the users in that group.
65
65
66 You can define group members in the ``acl.groups`` section.
66 You can define group members in the ``acl.groups`` section.
67 If a group name is not defined there, and Mercurial is running under
67 If a group name is not defined there, and Mercurial is running under
68 a Unix-like system, the list of users will be taken from the OS.
68 a Unix-like system, the list of users will be taken from the OS.
69 Otherwise, an exception will be raised.
69 Otherwise, an exception will be raised.
70
70
71 Example Configuration
71 Example Configuration
72 ---------------------
72 ---------------------
73
73
74 ::
74 ::
75
75
76 [hooks]
76 [hooks]
77
77
78 # Use this if you want to check access restrictions at commit time
78 # Use this if you want to check access restrictions at commit time
79 pretxncommit.acl = python:hgext.acl.hook
79 pretxncommit.acl = python:hgext.acl.hook
80
80
81 # Use this if you want to check access restrictions for pull, push,
81 # Use this if you want to check access restrictions for pull, push,
82 # bundle and serve.
82 # bundle and serve.
83 pretxnchangegroup.acl = python:hgext.acl.hook
83 pretxnchangegroup.acl = python:hgext.acl.hook
84
84
85 [acl]
85 [acl]
86 # Allow or deny access for incoming changes only if their source is
86 # Allow or deny access for incoming changes only if their source is
87 # listed here, let them pass otherwise. Source is "serve" for all
87 # listed here, let them pass otherwise. Source is "serve" for all
88 # remote access (http or ssh), "push", "pull" or "bundle" when the
88 # remote access (http or ssh), "push", "pull" or "bundle" when the
89 # related commands are run locally.
89 # related commands are run locally.
90 # Default: serve
90 # Default: serve
91 sources = serve
91 sources = serve
92
92
93 [acl.deny.branches]
93 [acl.deny.branches]
94
94
95 # Everyone is denied to the frozen branch:
95 # Everyone is denied to the frozen branch:
96 frozen-branch = *
96 frozen-branch = *
97
97
98 # A bad user is denied on all branches:
98 # A bad user is denied on all branches:
99 * = bad-user
99 * = bad-user
100
100
101 [acl.allow.branches]
101 [acl.allow.branches]
102
102
103 # A few users are allowed on branch-a:
103 # A few users are allowed on branch-a:
104 branch-a = user-1, user-2, user-3
104 branch-a = user-1, user-2, user-3
105
105
106 # Only one user is allowed on branch-b:
106 # Only one user is allowed on branch-b:
107 branch-b = user-1
107 branch-b = user-1
108
108
109 # The super user is allowed on any branch:
109 # The super user is allowed on any branch:
110 * = super-user
110 * = super-user
111
111
112 # Everyone is allowed on branch-for-tests:
112 # Everyone is allowed on branch-for-tests:
113 branch-for-tests = *
113 branch-for-tests = *
114
114
115 [acl.deny]
115 [acl.deny]
116 # This list is checked first. If a match is found, acl.allow is not
116 # This list is checked first. If a match is found, acl.allow is not
117 # checked. All users are granted access if acl.deny is not present.
117 # checked. All users are granted access if acl.deny is not present.
118 # Format for both lists: glob pattern = user, ..., @group, ...
118 # Format for both lists: glob pattern = user, ..., @group, ...
119
119
120 # To match everyone, use an asterisk for the user:
120 # To match everyone, use an asterisk for the user:
121 # my/glob/pattern = *
121 # my/glob/pattern = *
122
122
123 # user6 will not have write access to any file:
123 # user6 will not have write access to any file:
124 ** = user6
124 ** = user6
125
125
126 # Group "hg-denied" will not have write access to any file:
126 # Group "hg-denied" will not have write access to any file:
127 ** = @hg-denied
127 ** = @hg-denied
128
128
129 # Nobody will be able to change "DONT-TOUCH-THIS.txt", despite
129 # Nobody will be able to change "DONT-TOUCH-THIS.txt", despite
130 # everyone being able to change all other files. See below.
130 # everyone being able to change all other files. See below.
131 src/main/resources/DONT-TOUCH-THIS.txt = *
131 src/main/resources/DONT-TOUCH-THIS.txt = *
132
132
133 [acl.allow]
133 [acl.allow]
134 # if acl.allow is not present, all users are allowed by default
134 # if acl.allow is not present, all users are allowed by default
135 # empty acl.allow = no users allowed
135 # empty acl.allow = no users allowed
136
136
137 # User "doc_writer" has write access to any file under the "docs"
137 # User "doc_writer" has write access to any file under the "docs"
138 # folder:
138 # folder:
139 docs/** = doc_writer
139 docs/** = doc_writer
140
140
141 # User "jack" and group "designers" have write access to any file
141 # User "jack" and group "designers" have write access to any file
142 # under the "images" folder:
142 # under the "images" folder:
143 images/** = jack, @designers
143 images/** = jack, @designers
144
144
145 # Everyone (except for "user6" and "@hg-denied" - see acl.deny above)
145 # Everyone (except for "user6" and "@hg-denied" - see acl.deny above)
146 # will have write access to any file under the "resources" folder
146 # will have write access to any file under the "resources" folder
147 # (except for 1 file. See acl.deny):
147 # (except for 1 file. See acl.deny):
148 src/main/resources/** = *
148 src/main/resources/** = *
149
149
150 .hgtags = release_engineer
150 .hgtags = release_engineer
151
151
152 Examples using the "!" prefix
152 Examples using the "!" prefix
153 .............................
153 .............................
154
154
155 Suppose there's a branch that only a given user (or group) should be able to
155 Suppose there's a branch that only a given user (or group) should be able to
156 push to, and you don't want to restrict access to any other branch that may
156 push to, and you don't want to restrict access to any other branch that may
157 be created.
157 be created.
158
158
159 The "!" prefix allows you to prevent anyone except a given user or group to
159 The "!" prefix allows you to prevent anyone except a given user or group to
160 push changesets in a given branch or path.
160 push changesets in a given branch or path.
161
161
162 In the examples below, we will:
162 In the examples below, we will:
163 1) Deny access to branch "ring" to anyone but user "gollum"
163 1) Deny access to branch "ring" to anyone but user "gollum"
164 2) Deny access to branch "lake" to anyone but members of the group "hobbit"
164 2) Deny access to branch "lake" to anyone but members of the group "hobbit"
165 3) Deny access to a file to anyone but user "gollum"
165 3) Deny access to a file to anyone but user "gollum"
166
166
167 ::
167 ::
168
168
169 [acl.allow.branches]
169 [acl.allow.branches]
170 # Empty
170 # Empty
171
171
172 [acl.deny.branches]
172 [acl.deny.branches]
173
173
174 # 1) only 'gollum' can commit to branch 'ring';
174 # 1) only 'gollum' can commit to branch 'ring';
175 # 'gollum' and anyone else can still commit to any other branch.
175 # 'gollum' and anyone else can still commit to any other branch.
176 ring = !gollum
176 ring = !gollum
177
177
178 # 2) only members of the group 'hobbit' can commit to branch 'lake';
178 # 2) only members of the group 'hobbit' can commit to branch 'lake';
179 # 'hobbit' members and anyone else can still commit to any other branch.
179 # 'hobbit' members and anyone else can still commit to any other branch.
180 lake = !@hobbit
180 lake = !@hobbit
181
181
182 # You can also deny access based on file paths:
182 # You can also deny access based on file paths:
183
183
184 [acl.allow]
184 [acl.allow]
185 # Empty
185 # Empty
186
186
187 [acl.deny]
187 [acl.deny]
188 # 3) only 'gollum' can change the file below;
188 # 3) only 'gollum' can change the file below;
189 # 'gollum' and anyone else can still change any other file.
189 # 'gollum' and anyone else can still change any other file.
190 /misty/mountains/cave/ring = !gollum
190 /misty/mountains/cave/ring = !gollum
191
191
192 '''
192 '''
193
193
194 from __future__ import absolute_import
194 from __future__ import absolute_import
195
195
196 import getpass
196 import getpass
197 import urllib
198
197
199 from mercurial.i18n import _
198 from mercurial.i18n import _
200 from mercurial import (
199 from mercurial import (
201 error,
200 error,
202 match,
201 match,
203 util,
202 util,
204 )
203 )
205
204
205 urlreq = util.urlreq
206
206 # Note for extension authors: ONLY specify testedwith = 'internal' for
207 # Note for extension authors: ONLY specify testedwith = 'internal' for
207 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
208 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
208 # be specifying the version(s) of Mercurial they are tested with, or
209 # be specifying the version(s) of Mercurial they are tested with, or
209 # leave the attribute unspecified.
210 # leave the attribute unspecified.
210 testedwith = 'internal'
211 testedwith = 'internal'
211
212
212 def _getusers(ui, group):
213 def _getusers(ui, group):
213
214
214 # First, try to use group definition from section [acl.groups]
215 # First, try to use group definition from section [acl.groups]
215 hgrcusers = ui.configlist('acl.groups', group)
216 hgrcusers = ui.configlist('acl.groups', group)
216 if hgrcusers:
217 if hgrcusers:
217 return hgrcusers
218 return hgrcusers
218
219
219 ui.debug('acl: "%s" not defined in [acl.groups]\n' % group)
220 ui.debug('acl: "%s" not defined in [acl.groups]\n' % group)
220 # If no users found in group definition, get users from OS-level group
221 # If no users found in group definition, get users from OS-level group
221 try:
222 try:
222 return util.groupmembers(group)
223 return util.groupmembers(group)
223 except KeyError:
224 except KeyError:
224 raise error.Abort(_("group '%s' is undefined") % group)
225 raise error.Abort(_("group '%s' is undefined") % group)
225
226
226 def _usermatch(ui, user, usersorgroups):
227 def _usermatch(ui, user, usersorgroups):
227
228
228 if usersorgroups == '*':
229 if usersorgroups == '*':
229 return True
230 return True
230
231
231 for ug in usersorgroups.replace(',', ' ').split():
232 for ug in usersorgroups.replace(',', ' ').split():
232
233
233 if ug.startswith('!'):
234 if ug.startswith('!'):
234 # Test for excluded user or group. Format:
235 # Test for excluded user or group. Format:
235 # if ug is a user name: !username
236 # if ug is a user name: !username
236 # if ug is a group name: !@groupname
237 # if ug is a group name: !@groupname
237 ug = ug[1:]
238 ug = ug[1:]
238 if not ug.startswith('@') and user != ug \
239 if not ug.startswith('@') and user != ug \
239 or ug.startswith('@') and user not in _getusers(ui, ug[1:]):
240 or ug.startswith('@') and user not in _getusers(ui, ug[1:]):
240 return True
241 return True
241
242
242 # Test for user or group. Format:
243 # Test for user or group. Format:
243 # if ug is a user name: username
244 # if ug is a user name: username
244 # if ug is a group name: @groupname
245 # if ug is a group name: @groupname
245 elif user == ug \
246 elif user == ug \
246 or ug.startswith('@') and user in _getusers(ui, ug[1:]):
247 or ug.startswith('@') and user in _getusers(ui, ug[1:]):
247 return True
248 return True
248
249
249 return False
250 return False
250
251
251 def buildmatch(ui, repo, user, key):
252 def buildmatch(ui, repo, user, key):
252 '''return tuple of (match function, list enabled).'''
253 '''return tuple of (match function, list enabled).'''
253 if not ui.has_section(key):
254 if not ui.has_section(key):
254 ui.debug('acl: %s not enabled\n' % key)
255 ui.debug('acl: %s not enabled\n' % key)
255 return None
256 return None
256
257
257 pats = [pat for pat, users in ui.configitems(key)
258 pats = [pat for pat, users in ui.configitems(key)
258 if _usermatch(ui, user, users)]
259 if _usermatch(ui, user, users)]
259 ui.debug('acl: %s enabled, %d entries for user %s\n' %
260 ui.debug('acl: %s enabled, %d entries for user %s\n' %
260 (key, len(pats), user))
261 (key, len(pats), user))
261
262
262 # Branch-based ACL
263 # Branch-based ACL
263 if not repo:
264 if not repo:
264 if pats:
265 if pats:
265 # If there's an asterisk (meaning "any branch"), always return True;
266 # If there's an asterisk (meaning "any branch"), always return True;
266 # Otherwise, test if b is in pats
267 # Otherwise, test if b is in pats
267 if '*' in pats:
268 if '*' in pats:
268 return util.always
269 return util.always
269 return lambda b: b in pats
270 return lambda b: b in pats
270 return util.never
271 return util.never
271
272
272 # Path-based ACL
273 # Path-based ACL
273 if pats:
274 if pats:
274 return match.match(repo.root, '', pats)
275 return match.match(repo.root, '', pats)
275 return util.never
276 return util.never
276
277
277 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
278 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
278 if hooktype not in ['pretxnchangegroup', 'pretxncommit']:
279 if hooktype not in ['pretxnchangegroup', 'pretxncommit']:
279 raise error.Abort(_('config error - hook type "%s" cannot stop '
280 raise error.Abort(_('config error - hook type "%s" cannot stop '
280 'incoming changesets nor commits') % hooktype)
281 'incoming changesets nor commits') % hooktype)
281 if (hooktype == 'pretxnchangegroup' and
282 if (hooktype == 'pretxnchangegroup' and
282 source not in ui.config('acl', 'sources', 'serve').split()):
283 source not in ui.config('acl', 'sources', 'serve').split()):
283 ui.debug('acl: changes have source "%s" - skipping\n' % source)
284 ui.debug('acl: changes have source "%s" - skipping\n' % source)
284 return
285 return
285
286
286 user = None
287 user = None
287 if source == 'serve' and 'url' in kwargs:
288 if source == 'serve' and 'url' in kwargs:
288 url = kwargs['url'].split(':')
289 url = kwargs['url'].split(':')
289 if url[0] == 'remote' and url[1].startswith('http'):
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 if user is None:
293 if user is None:
293 user = getpass.getuser()
294 user = getpass.getuser()
294
295
295 ui.debug('acl: checking access for user "%s"\n' % user)
296 ui.debug('acl: checking access for user "%s"\n' % user)
296
297
297 # deprecated config: acl.config
298 # deprecated config: acl.config
298 cfg = ui.config('acl', 'config')
299 cfg = ui.config('acl', 'config')
299 if cfg:
300 if cfg:
300 ui.readconfig(cfg, sections=['acl.groups', 'acl.allow.branches',
301 ui.readconfig(cfg, sections=['acl.groups', 'acl.allow.branches',
301 'acl.deny.branches', 'acl.allow', 'acl.deny'])
302 'acl.deny.branches', 'acl.allow', 'acl.deny'])
302
303
303 allowbranches = buildmatch(ui, None, user, 'acl.allow.branches')
304 allowbranches = buildmatch(ui, None, user, 'acl.allow.branches')
304 denybranches = buildmatch(ui, None, user, 'acl.deny.branches')
305 denybranches = buildmatch(ui, None, user, 'acl.deny.branches')
305 allow = buildmatch(ui, repo, user, 'acl.allow')
306 allow = buildmatch(ui, repo, user, 'acl.allow')
306 deny = buildmatch(ui, repo, user, 'acl.deny')
307 deny = buildmatch(ui, repo, user, 'acl.deny')
307
308
308 for rev in xrange(repo[node], len(repo)):
309 for rev in xrange(repo[node], len(repo)):
309 ctx = repo[rev]
310 ctx = repo[rev]
310 branch = ctx.branch()
311 branch = ctx.branch()
311 if denybranches and denybranches(branch):
312 if denybranches and denybranches(branch):
312 raise error.Abort(_('acl: user "%s" denied on branch "%s"'
313 raise error.Abort(_('acl: user "%s" denied on branch "%s"'
313 ' (changeset "%s")')
314 ' (changeset "%s")')
314 % (user, branch, ctx))
315 % (user, branch, ctx))
315 if allowbranches and not allowbranches(branch):
316 if allowbranches and not allowbranches(branch):
316 raise error.Abort(_('acl: user "%s" not allowed on branch "%s"'
317 raise error.Abort(_('acl: user "%s" not allowed on branch "%s"'
317 ' (changeset "%s")')
318 ' (changeset "%s")')
318 % (user, branch, ctx))
319 % (user, branch, ctx))
319 ui.debug('acl: branch access granted: "%s" on branch "%s"\n'
320 ui.debug('acl: branch access granted: "%s" on branch "%s"\n'
320 % (ctx, branch))
321 % (ctx, branch))
321
322
322 for f in ctx.files():
323 for f in ctx.files():
323 if deny and deny(f):
324 if deny and deny(f):
324 raise error.Abort(_('acl: user "%s" denied on "%s"'
325 raise error.Abort(_('acl: user "%s" denied on "%s"'
325 ' (changeset "%s")') % (user, f, ctx))
326 ' (changeset "%s")') % (user, f, ctx))
326 if allow and not allow(f):
327 if allow and not allow(f):
327 raise error.Abort(_('acl: user "%s" not allowed on "%s"'
328 raise error.Abort(_('acl: user "%s" not allowed on "%s"'
328 ' (changeset "%s")') % (user, f, ctx))
329 ' (changeset "%s")') % (user, f, ctx))
329 ui.debug('acl: path access granted: "%s"\n' % ctx)
330 ui.debug('acl: path access granted: "%s"\n' % ctx)
@@ -1,1354 +1,1354 b''
1 # Subversion 1.4/1.5 Python API backend
1 # Subversion 1.4/1.5 Python API backend
2 #
2 #
3 # Copyright(C) 2007 Daniel Holth et al
3 # Copyright(C) 2007 Daniel Holth et al
4 from __future__ import absolute_import
4 from __future__ import absolute_import
5
5
6 import cPickle as pickle
6 import cPickle as pickle
7 import os
7 import os
8 import re
8 import re
9 import sys
9 import sys
10 import tempfile
10 import tempfile
11 import urllib
12 import urllib2
13 import xml.dom.minidom
11 import xml.dom.minidom
14
12
15 from mercurial import (
13 from mercurial import (
16 encoding,
14 encoding,
17 error,
15 error,
18 scmutil,
16 scmutil,
19 strutil,
17 strutil,
20 util,
18 util,
21 )
19 )
22 from mercurial.i18n import _
20 from mercurial.i18n import _
23
21
24 from . import common
22 from . import common
25
23
26 stringio = util.stringio
24 stringio = util.stringio
27 propertycache = util.propertycache
25 propertycache = util.propertycache
26 urlerr = util.urlerr
27 urlreq = util.urlreq
28
28
29 commandline = common.commandline
29 commandline = common.commandline
30 commit = common.commit
30 commit = common.commit
31 converter_sink = common.converter_sink
31 converter_sink = common.converter_sink
32 converter_source = common.converter_source
32 converter_source = common.converter_source
33 decodeargs = common.decodeargs
33 decodeargs = common.decodeargs
34 encodeargs = common.encodeargs
34 encodeargs = common.encodeargs
35 makedatetimestamp = common.makedatetimestamp
35 makedatetimestamp = common.makedatetimestamp
36 mapfile = common.mapfile
36 mapfile = common.mapfile
37 MissingTool = common.MissingTool
37 MissingTool = common.MissingTool
38 NoRepo = common.NoRepo
38 NoRepo = common.NoRepo
39
39
40 # Subversion stuff. Works best with very recent Python SVN bindings
40 # Subversion stuff. Works best with very recent Python SVN bindings
41 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
41 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
42 # these bindings.
42 # these bindings.
43
43
44 try:
44 try:
45 import svn
45 import svn
46 import svn.client
46 import svn.client
47 import svn.core
47 import svn.core
48 import svn.ra
48 import svn.ra
49 import svn.delta
49 import svn.delta
50 from . import transport
50 from . import transport
51 import warnings
51 import warnings
52 warnings.filterwarnings('ignore',
52 warnings.filterwarnings('ignore',
53 module='svn.core',
53 module='svn.core',
54 category=DeprecationWarning)
54 category=DeprecationWarning)
55 svn.core.SubversionException # trigger import to catch error
55 svn.core.SubversionException # trigger import to catch error
56
56
57 except ImportError:
57 except ImportError:
58 svn = None
58 svn = None
59
59
60 class SvnPathNotFound(Exception):
60 class SvnPathNotFound(Exception):
61 pass
61 pass
62
62
63 def revsplit(rev):
63 def revsplit(rev):
64 """Parse a revision string and return (uuid, path, revnum).
64 """Parse a revision string and return (uuid, path, revnum).
65 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
65 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
66 ... '/proj%20B/mytrunk/mytrunk@1')
66 ... '/proj%20B/mytrunk/mytrunk@1')
67 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
67 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
68 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
68 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
69 ('', '', 1)
69 ('', '', 1)
70 >>> revsplit('@7')
70 >>> revsplit('@7')
71 ('', '', 7)
71 ('', '', 7)
72 >>> revsplit('7')
72 >>> revsplit('7')
73 ('', '', 0)
73 ('', '', 0)
74 >>> revsplit('bad')
74 >>> revsplit('bad')
75 ('', '', 0)
75 ('', '', 0)
76 """
76 """
77 parts = rev.rsplit('@', 1)
77 parts = rev.rsplit('@', 1)
78 revnum = 0
78 revnum = 0
79 if len(parts) > 1:
79 if len(parts) > 1:
80 revnum = int(parts[1])
80 revnum = int(parts[1])
81 parts = parts[0].split('/', 1)
81 parts = parts[0].split('/', 1)
82 uuid = ''
82 uuid = ''
83 mod = ''
83 mod = ''
84 if len(parts) > 1 and parts[0].startswith('svn:'):
84 if len(parts) > 1 and parts[0].startswith('svn:'):
85 uuid = parts[0][4:]
85 uuid = parts[0][4:]
86 mod = '/' + parts[1]
86 mod = '/' + parts[1]
87 return uuid, mod, revnum
87 return uuid, mod, revnum
88
88
89 def quote(s):
89 def quote(s):
90 # As of svn 1.7, many svn calls expect "canonical" paths. In
90 # As of svn 1.7, many svn calls expect "canonical" paths. In
91 # theory, we should call svn.core.*canonicalize() on all paths
91 # theory, we should call svn.core.*canonicalize() on all paths
92 # before passing them to the API. Instead, we assume the base url
92 # before passing them to the API. Instead, we assume the base url
93 # is canonical and copy the behaviour of svn URL encoding function
93 # is canonical and copy the behaviour of svn URL encoding function
94 # so we can extend it safely with new components. The "safe"
94 # so we can extend it safely with new components. The "safe"
95 # characters were taken from the "svn_uri__char_validity" table in
95 # characters were taken from the "svn_uri__char_validity" table in
96 # libsvn_subr/path.c.
96 # libsvn_subr/path.c.
97 return urllib.quote(s, "!$&'()*+,-./:=@_~")
97 return urlreq.quote(s, "!$&'()*+,-./:=@_~")
98
98
99 def geturl(path):
99 def geturl(path):
100 try:
100 try:
101 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
101 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
102 except svn.core.SubversionException:
102 except svn.core.SubversionException:
103 # svn.client.url_from_path() fails with local repositories
103 # svn.client.url_from_path() fails with local repositories
104 pass
104 pass
105 if os.path.isdir(path):
105 if os.path.isdir(path):
106 path = os.path.normpath(os.path.abspath(path))
106 path = os.path.normpath(os.path.abspath(path))
107 if os.name == 'nt':
107 if os.name == 'nt':
108 path = '/' + util.normpath(path)
108 path = '/' + util.normpath(path)
109 # Module URL is later compared with the repository URL returned
109 # Module URL is later compared with the repository URL returned
110 # by svn API, which is UTF-8.
110 # by svn API, which is UTF-8.
111 path = encoding.tolocal(path)
111 path = encoding.tolocal(path)
112 path = 'file://%s' % quote(path)
112 path = 'file://%s' % quote(path)
113 return svn.core.svn_path_canonicalize(path)
113 return svn.core.svn_path_canonicalize(path)
114
114
115 def optrev(number):
115 def optrev(number):
116 optrev = svn.core.svn_opt_revision_t()
116 optrev = svn.core.svn_opt_revision_t()
117 optrev.kind = svn.core.svn_opt_revision_number
117 optrev.kind = svn.core.svn_opt_revision_number
118 optrev.value.number = number
118 optrev.value.number = number
119 return optrev
119 return optrev
120
120
121 class changedpath(object):
121 class changedpath(object):
122 def __init__(self, p):
122 def __init__(self, p):
123 self.copyfrom_path = p.copyfrom_path
123 self.copyfrom_path = p.copyfrom_path
124 self.copyfrom_rev = p.copyfrom_rev
124 self.copyfrom_rev = p.copyfrom_rev
125 self.action = p.action
125 self.action = p.action
126
126
127 def get_log_child(fp, url, paths, start, end, limit=0,
127 def get_log_child(fp, url, paths, start, end, limit=0,
128 discover_changed_paths=True, strict_node_history=False):
128 discover_changed_paths=True, strict_node_history=False):
129 protocol = -1
129 protocol = -1
130 def receiver(orig_paths, revnum, author, date, message, pool):
130 def receiver(orig_paths, revnum, author, date, message, pool):
131 paths = {}
131 paths = {}
132 if orig_paths is not None:
132 if orig_paths is not None:
133 for k, v in orig_paths.iteritems():
133 for k, v in orig_paths.iteritems():
134 paths[k] = changedpath(v)
134 paths[k] = changedpath(v)
135 pickle.dump((paths, revnum, author, date, message),
135 pickle.dump((paths, revnum, author, date, message),
136 fp, protocol)
136 fp, protocol)
137
137
138 try:
138 try:
139 # Use an ra of our own so that our parent can consume
139 # Use an ra of our own so that our parent can consume
140 # our results without confusing the server.
140 # our results without confusing the server.
141 t = transport.SvnRaTransport(url=url)
141 t = transport.SvnRaTransport(url=url)
142 svn.ra.get_log(t.ra, paths, start, end, limit,
142 svn.ra.get_log(t.ra, paths, start, end, limit,
143 discover_changed_paths,
143 discover_changed_paths,
144 strict_node_history,
144 strict_node_history,
145 receiver)
145 receiver)
146 except IOError:
146 except IOError:
147 # Caller may interrupt the iteration
147 # Caller may interrupt the iteration
148 pickle.dump(None, fp, protocol)
148 pickle.dump(None, fp, protocol)
149 except Exception as inst:
149 except Exception as inst:
150 pickle.dump(str(inst), fp, protocol)
150 pickle.dump(str(inst), fp, protocol)
151 else:
151 else:
152 pickle.dump(None, fp, protocol)
152 pickle.dump(None, fp, protocol)
153 fp.close()
153 fp.close()
154 # With large history, cleanup process goes crazy and suddenly
154 # With large history, cleanup process goes crazy and suddenly
155 # consumes *huge* amount of memory. The output file being closed,
155 # consumes *huge* amount of memory. The output file being closed,
156 # there is no need for clean termination.
156 # there is no need for clean termination.
157 os._exit(0)
157 os._exit(0)
158
158
159 def debugsvnlog(ui, **opts):
159 def debugsvnlog(ui, **opts):
160 """Fetch SVN log in a subprocess and channel them back to parent to
160 """Fetch SVN log in a subprocess and channel them back to parent to
161 avoid memory collection issues.
161 avoid memory collection issues.
162 """
162 """
163 if svn is None:
163 if svn is None:
164 raise error.Abort(_('debugsvnlog could not load Subversion python '
164 raise error.Abort(_('debugsvnlog could not load Subversion python '
165 'bindings'))
165 'bindings'))
166
166
167 util.setbinary(sys.stdin)
167 util.setbinary(sys.stdin)
168 util.setbinary(sys.stdout)
168 util.setbinary(sys.stdout)
169 args = decodeargs(sys.stdin.read())
169 args = decodeargs(sys.stdin.read())
170 get_log_child(sys.stdout, *args)
170 get_log_child(sys.stdout, *args)
171
171
172 class logstream(object):
172 class logstream(object):
173 """Interruptible revision log iterator."""
173 """Interruptible revision log iterator."""
174 def __init__(self, stdout):
174 def __init__(self, stdout):
175 self._stdout = stdout
175 self._stdout = stdout
176
176
177 def __iter__(self):
177 def __iter__(self):
178 while True:
178 while True:
179 try:
179 try:
180 entry = pickle.load(self._stdout)
180 entry = pickle.load(self._stdout)
181 except EOFError:
181 except EOFError:
182 raise error.Abort(_('Mercurial failed to run itself, check'
182 raise error.Abort(_('Mercurial failed to run itself, check'
183 ' hg executable is in PATH'))
183 ' hg executable is in PATH'))
184 try:
184 try:
185 orig_paths, revnum, author, date, message = entry
185 orig_paths, revnum, author, date, message = entry
186 except (TypeError, ValueError):
186 except (TypeError, ValueError):
187 if entry is None:
187 if entry is None:
188 break
188 break
189 raise error.Abort(_("log stream exception '%s'") % entry)
189 raise error.Abort(_("log stream exception '%s'") % entry)
190 yield entry
190 yield entry
191
191
192 def close(self):
192 def close(self):
193 if self._stdout:
193 if self._stdout:
194 self._stdout.close()
194 self._stdout.close()
195 self._stdout = None
195 self._stdout = None
196
196
197 class directlogstream(list):
197 class directlogstream(list):
198 """Direct revision log iterator.
198 """Direct revision log iterator.
199 This can be used for debugging and development but it will probably leak
199 This can be used for debugging and development but it will probably leak
200 memory and is not suitable for real conversions."""
200 memory and is not suitable for real conversions."""
201 def __init__(self, url, paths, start, end, limit=0,
201 def __init__(self, url, paths, start, end, limit=0,
202 discover_changed_paths=True, strict_node_history=False):
202 discover_changed_paths=True, strict_node_history=False):
203
203
204 def receiver(orig_paths, revnum, author, date, message, pool):
204 def receiver(orig_paths, revnum, author, date, message, pool):
205 paths = {}
205 paths = {}
206 if orig_paths is not None:
206 if orig_paths is not None:
207 for k, v in orig_paths.iteritems():
207 for k, v in orig_paths.iteritems():
208 paths[k] = changedpath(v)
208 paths[k] = changedpath(v)
209 self.append((paths, revnum, author, date, message))
209 self.append((paths, revnum, author, date, message))
210
210
211 # Use an ra of our own so that our parent can consume
211 # Use an ra of our own so that our parent can consume
212 # our results without confusing the server.
212 # our results without confusing the server.
213 t = transport.SvnRaTransport(url=url)
213 t = transport.SvnRaTransport(url=url)
214 svn.ra.get_log(t.ra, paths, start, end, limit,
214 svn.ra.get_log(t.ra, paths, start, end, limit,
215 discover_changed_paths,
215 discover_changed_paths,
216 strict_node_history,
216 strict_node_history,
217 receiver)
217 receiver)
218
218
219 def close(self):
219 def close(self):
220 pass
220 pass
221
221
222 # Check to see if the given path is a local Subversion repo. Verify this by
222 # Check to see if the given path is a local Subversion repo. Verify this by
223 # looking for several svn-specific files and directories in the given
223 # looking for several svn-specific files and directories in the given
224 # directory.
224 # directory.
225 def filecheck(ui, path, proto):
225 def filecheck(ui, path, proto):
226 for x in ('locks', 'hooks', 'format', 'db'):
226 for x in ('locks', 'hooks', 'format', 'db'):
227 if not os.path.exists(os.path.join(path, x)):
227 if not os.path.exists(os.path.join(path, x)):
228 return False
228 return False
229 return True
229 return True
230
230
231 # Check to see if a given path is the root of an svn repo over http. We verify
231 # Check to see if a given path is the root of an svn repo over http. We verify
232 # this by requesting a version-controlled URL we know can't exist and looking
232 # this by requesting a version-controlled URL we know can't exist and looking
233 # for the svn-specific "not found" XML.
233 # for the svn-specific "not found" XML.
234 def httpcheck(ui, path, proto):
234 def httpcheck(ui, path, proto):
235 try:
235 try:
236 opener = urllib2.build_opener()
236 opener = urlreq.buildopener()
237 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
237 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
238 data = rsp.read()
238 data = rsp.read()
239 except urllib2.HTTPError as inst:
239 except urlerr.httperror as inst:
240 if inst.code != 404:
240 if inst.code != 404:
241 # Except for 404 we cannot know for sure this is not an svn repo
241 # Except for 404 we cannot know for sure this is not an svn repo
242 ui.warn(_('svn: cannot probe remote repository, assume it could '
242 ui.warn(_('svn: cannot probe remote repository, assume it could '
243 'be a subversion repository. Use --source-type if you '
243 'be a subversion repository. Use --source-type if you '
244 'know better.\n'))
244 'know better.\n'))
245 return True
245 return True
246 data = inst.fp.read()
246 data = inst.fp.read()
247 except Exception:
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 return False
249 return False
250 return '<m:human-readable errcode="160013">' in data
250 return '<m:human-readable errcode="160013">' in data
251
251
252 protomap = {'http': httpcheck,
252 protomap = {'http': httpcheck,
253 'https': httpcheck,
253 'https': httpcheck,
254 'file': filecheck,
254 'file': filecheck,
255 }
255 }
256 def issvnurl(ui, url):
256 def issvnurl(ui, url):
257 try:
257 try:
258 proto, path = url.split('://', 1)
258 proto, path = url.split('://', 1)
259 if proto == 'file':
259 if proto == 'file':
260 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
260 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
261 and path[2:6].lower() == '%3a/'):
261 and path[2:6].lower() == '%3a/'):
262 path = path[:2] + ':/' + path[6:]
262 path = path[:2] + ':/' + path[6:]
263 path = urllib.url2pathname(path)
263 path = urlreq.url2pathname(path)
264 except ValueError:
264 except ValueError:
265 proto = 'file'
265 proto = 'file'
266 path = os.path.abspath(url)
266 path = os.path.abspath(url)
267 if proto == 'file':
267 if proto == 'file':
268 path = util.pconvert(path)
268 path = util.pconvert(path)
269 check = protomap.get(proto, lambda *args: False)
269 check = protomap.get(proto, lambda *args: False)
270 while '/' in path:
270 while '/' in path:
271 if check(ui, path, proto):
271 if check(ui, path, proto):
272 return True
272 return True
273 path = path.rsplit('/', 1)[0]
273 path = path.rsplit('/', 1)[0]
274 return False
274 return False
275
275
276 # SVN conversion code stolen from bzr-svn and tailor
276 # SVN conversion code stolen from bzr-svn and tailor
277 #
277 #
278 # Subversion looks like a versioned filesystem, branches structures
278 # Subversion looks like a versioned filesystem, branches structures
279 # are defined by conventions and not enforced by the tool. First,
279 # are defined by conventions and not enforced by the tool. First,
280 # we define the potential branches (modules) as "trunk" and "branches"
280 # we define the potential branches (modules) as "trunk" and "branches"
281 # children directories. Revisions are then identified by their
281 # children directories. Revisions are then identified by their
282 # module and revision number (and a repository identifier).
282 # module and revision number (and a repository identifier).
283 #
283 #
284 # The revision graph is really a tree (or a forest). By default, a
284 # The revision graph is really a tree (or a forest). By default, a
285 # revision parent is the previous revision in the same module. If the
285 # revision parent is the previous revision in the same module. If the
286 # module directory is copied/moved from another module then the
286 # module directory is copied/moved from another module then the
287 # revision is the module root and its parent the source revision in
287 # revision is the module root and its parent the source revision in
288 # the parent module. A revision has at most one parent.
288 # the parent module. A revision has at most one parent.
289 #
289 #
290 class svn_source(converter_source):
290 class svn_source(converter_source):
291 def __init__(self, ui, url, revs=None):
291 def __init__(self, ui, url, revs=None):
292 super(svn_source, self).__init__(ui, url, revs=revs)
292 super(svn_source, self).__init__(ui, url, revs=revs)
293
293
294 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
294 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
295 (os.path.exists(url) and
295 (os.path.exists(url) and
296 os.path.exists(os.path.join(url, '.svn'))) or
296 os.path.exists(os.path.join(url, '.svn'))) or
297 issvnurl(ui, url)):
297 issvnurl(ui, url)):
298 raise NoRepo(_("%s does not look like a Subversion repository")
298 raise NoRepo(_("%s does not look like a Subversion repository")
299 % url)
299 % url)
300 if svn is None:
300 if svn is None:
301 raise MissingTool(_('could not load Subversion python bindings'))
301 raise MissingTool(_('could not load Subversion python bindings'))
302
302
303 try:
303 try:
304 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
304 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
305 if version < (1, 4):
305 if version < (1, 4):
306 raise MissingTool(_('Subversion python bindings %d.%d found, '
306 raise MissingTool(_('Subversion python bindings %d.%d found, '
307 '1.4 or later required') % version)
307 '1.4 or later required') % version)
308 except AttributeError:
308 except AttributeError:
309 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
309 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
310 'or later required'))
310 'or later required'))
311
311
312 self.lastrevs = {}
312 self.lastrevs = {}
313
313
314 latest = None
314 latest = None
315 try:
315 try:
316 # Support file://path@rev syntax. Useful e.g. to convert
316 # Support file://path@rev syntax. Useful e.g. to convert
317 # deleted branches.
317 # deleted branches.
318 at = url.rfind('@')
318 at = url.rfind('@')
319 if at >= 0:
319 if at >= 0:
320 latest = int(url[at + 1:])
320 latest = int(url[at + 1:])
321 url = url[:at]
321 url = url[:at]
322 except ValueError:
322 except ValueError:
323 pass
323 pass
324 self.url = geturl(url)
324 self.url = geturl(url)
325 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
325 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
326 try:
326 try:
327 self.transport = transport.SvnRaTransport(url=self.url)
327 self.transport = transport.SvnRaTransport(url=self.url)
328 self.ra = self.transport.ra
328 self.ra = self.transport.ra
329 self.ctx = self.transport.client
329 self.ctx = self.transport.client
330 self.baseurl = svn.ra.get_repos_root(self.ra)
330 self.baseurl = svn.ra.get_repos_root(self.ra)
331 # Module is either empty or a repository path starting with
331 # Module is either empty or a repository path starting with
332 # a slash and not ending with a slash.
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 self.prevmodule = None
334 self.prevmodule = None
335 self.rootmodule = self.module
335 self.rootmodule = self.module
336 self.commits = {}
336 self.commits = {}
337 self.paths = {}
337 self.paths = {}
338 self.uuid = svn.ra.get_uuid(self.ra)
338 self.uuid = svn.ra.get_uuid(self.ra)
339 except svn.core.SubversionException:
339 except svn.core.SubversionException:
340 ui.traceback()
340 ui.traceback()
341 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
341 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
342 svn.core.SVN_VER_MINOR,
342 svn.core.SVN_VER_MINOR,
343 svn.core.SVN_VER_MICRO)
343 svn.core.SVN_VER_MICRO)
344 raise NoRepo(_("%s does not look like a Subversion repository "
344 raise NoRepo(_("%s does not look like a Subversion repository "
345 "to libsvn version %s")
345 "to libsvn version %s")
346 % (self.url, svnversion))
346 % (self.url, svnversion))
347
347
348 if revs:
348 if revs:
349 if len(revs) > 1:
349 if len(revs) > 1:
350 raise error.Abort(_('subversion source does not support '
350 raise error.Abort(_('subversion source does not support '
351 'specifying multiple revisions'))
351 'specifying multiple revisions'))
352 try:
352 try:
353 latest = int(revs[0])
353 latest = int(revs[0])
354 except ValueError:
354 except ValueError:
355 raise error.Abort(_('svn: revision %s is not an integer') %
355 raise error.Abort(_('svn: revision %s is not an integer') %
356 revs[0])
356 revs[0])
357
357
358 self.trunkname = self.ui.config('convert', 'svn.trunk',
358 self.trunkname = self.ui.config('convert', 'svn.trunk',
359 'trunk').strip('/')
359 'trunk').strip('/')
360 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
360 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
361 try:
361 try:
362 self.startrev = int(self.startrev)
362 self.startrev = int(self.startrev)
363 if self.startrev < 0:
363 if self.startrev < 0:
364 self.startrev = 0
364 self.startrev = 0
365 except ValueError:
365 except ValueError:
366 raise error.Abort(_('svn: start revision %s is not an integer')
366 raise error.Abort(_('svn: start revision %s is not an integer')
367 % self.startrev)
367 % self.startrev)
368
368
369 try:
369 try:
370 self.head = self.latest(self.module, latest)
370 self.head = self.latest(self.module, latest)
371 except SvnPathNotFound:
371 except SvnPathNotFound:
372 self.head = None
372 self.head = None
373 if not self.head:
373 if not self.head:
374 raise error.Abort(_('no revision found in module %s')
374 raise error.Abort(_('no revision found in module %s')
375 % self.module)
375 % self.module)
376 self.last_changed = self.revnum(self.head)
376 self.last_changed = self.revnum(self.head)
377
377
378 self._changescache = (None, None)
378 self._changescache = (None, None)
379
379
380 if os.path.exists(os.path.join(url, '.svn/entries')):
380 if os.path.exists(os.path.join(url, '.svn/entries')):
381 self.wc = url
381 self.wc = url
382 else:
382 else:
383 self.wc = None
383 self.wc = None
384 self.convertfp = None
384 self.convertfp = None
385
385
386 def setrevmap(self, revmap):
386 def setrevmap(self, revmap):
387 lastrevs = {}
387 lastrevs = {}
388 for revid in revmap.iterkeys():
388 for revid in revmap.iterkeys():
389 uuid, module, revnum = revsplit(revid)
389 uuid, module, revnum = revsplit(revid)
390 lastrevnum = lastrevs.setdefault(module, revnum)
390 lastrevnum = lastrevs.setdefault(module, revnum)
391 if revnum > lastrevnum:
391 if revnum > lastrevnum:
392 lastrevs[module] = revnum
392 lastrevs[module] = revnum
393 self.lastrevs = lastrevs
393 self.lastrevs = lastrevs
394
394
395 def exists(self, path, optrev):
395 def exists(self, path, optrev):
396 try:
396 try:
397 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
397 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
398 optrev, False, self.ctx)
398 optrev, False, self.ctx)
399 return True
399 return True
400 except svn.core.SubversionException:
400 except svn.core.SubversionException:
401 return False
401 return False
402
402
403 def getheads(self):
403 def getheads(self):
404
404
405 def isdir(path, revnum):
405 def isdir(path, revnum):
406 kind = self._checkpath(path, revnum)
406 kind = self._checkpath(path, revnum)
407 return kind == svn.core.svn_node_dir
407 return kind == svn.core.svn_node_dir
408
408
409 def getcfgpath(name, rev):
409 def getcfgpath(name, rev):
410 cfgpath = self.ui.config('convert', 'svn.' + name)
410 cfgpath = self.ui.config('convert', 'svn.' + name)
411 if cfgpath is not None and cfgpath.strip() == '':
411 if cfgpath is not None and cfgpath.strip() == '':
412 return None
412 return None
413 path = (cfgpath or name).strip('/')
413 path = (cfgpath or name).strip('/')
414 if not self.exists(path, rev):
414 if not self.exists(path, rev):
415 if self.module.endswith(path) and name == 'trunk':
415 if self.module.endswith(path) and name == 'trunk':
416 # we are converting from inside this directory
416 # we are converting from inside this directory
417 return None
417 return None
418 if cfgpath:
418 if cfgpath:
419 raise error.Abort(_('expected %s to be at %r, but not found'
419 raise error.Abort(_('expected %s to be at %r, but not found'
420 ) % (name, path))
420 ) % (name, path))
421 return None
421 return None
422 self.ui.note(_('found %s at %r\n') % (name, path))
422 self.ui.note(_('found %s at %r\n') % (name, path))
423 return path
423 return path
424
424
425 rev = optrev(self.last_changed)
425 rev = optrev(self.last_changed)
426 oldmodule = ''
426 oldmodule = ''
427 trunk = getcfgpath('trunk', rev)
427 trunk = getcfgpath('trunk', rev)
428 self.tags = getcfgpath('tags', rev)
428 self.tags = getcfgpath('tags', rev)
429 branches = getcfgpath('branches', rev)
429 branches = getcfgpath('branches', rev)
430
430
431 # If the project has a trunk or branches, we will extract heads
431 # If the project has a trunk or branches, we will extract heads
432 # from them. We keep the project root otherwise.
432 # from them. We keep the project root otherwise.
433 if trunk:
433 if trunk:
434 oldmodule = self.module or ''
434 oldmodule = self.module or ''
435 self.module += '/' + trunk
435 self.module += '/' + trunk
436 self.head = self.latest(self.module, self.last_changed)
436 self.head = self.latest(self.module, self.last_changed)
437 if not self.head:
437 if not self.head:
438 raise error.Abort(_('no revision found in module %s')
438 raise error.Abort(_('no revision found in module %s')
439 % self.module)
439 % self.module)
440
440
441 # First head in the list is the module's head
441 # First head in the list is the module's head
442 self.heads = [self.head]
442 self.heads = [self.head]
443 if self.tags is not None:
443 if self.tags is not None:
444 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
444 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
445
445
446 # Check if branches bring a few more heads to the list
446 # Check if branches bring a few more heads to the list
447 if branches:
447 if branches:
448 rpath = self.url.strip('/')
448 rpath = self.url.strip('/')
449 branchnames = svn.client.ls(rpath + '/' + quote(branches),
449 branchnames = svn.client.ls(rpath + '/' + quote(branches),
450 rev, False, self.ctx)
450 rev, False, self.ctx)
451 for branch in sorted(branchnames):
451 for branch in sorted(branchnames):
452 module = '%s/%s/%s' % (oldmodule, branches, branch)
452 module = '%s/%s/%s' % (oldmodule, branches, branch)
453 if not isdir(module, self.last_changed):
453 if not isdir(module, self.last_changed):
454 continue
454 continue
455 brevid = self.latest(module, self.last_changed)
455 brevid = self.latest(module, self.last_changed)
456 if not brevid:
456 if not brevid:
457 self.ui.note(_('ignoring empty branch %s\n') % branch)
457 self.ui.note(_('ignoring empty branch %s\n') % branch)
458 continue
458 continue
459 self.ui.note(_('found branch %s at %d\n') %
459 self.ui.note(_('found branch %s at %d\n') %
460 (branch, self.revnum(brevid)))
460 (branch, self.revnum(brevid)))
461 self.heads.append(brevid)
461 self.heads.append(brevid)
462
462
463 if self.startrev and self.heads:
463 if self.startrev and self.heads:
464 if len(self.heads) > 1:
464 if len(self.heads) > 1:
465 raise error.Abort(_('svn: start revision is not supported '
465 raise error.Abort(_('svn: start revision is not supported '
466 'with more than one branch'))
466 'with more than one branch'))
467 revnum = self.revnum(self.heads[0])
467 revnum = self.revnum(self.heads[0])
468 if revnum < self.startrev:
468 if revnum < self.startrev:
469 raise error.Abort(
469 raise error.Abort(
470 _('svn: no revision found after start revision %d')
470 _('svn: no revision found after start revision %d')
471 % self.startrev)
471 % self.startrev)
472
472
473 return self.heads
473 return self.heads
474
474
475 def _getchanges(self, rev, full):
475 def _getchanges(self, rev, full):
476 (paths, parents) = self.paths[rev]
476 (paths, parents) = self.paths[rev]
477 copies = {}
477 copies = {}
478 if parents:
478 if parents:
479 files, self.removed, copies = self.expandpaths(rev, paths, parents)
479 files, self.removed, copies = self.expandpaths(rev, paths, parents)
480 if full or not parents:
480 if full or not parents:
481 # Perform a full checkout on roots
481 # Perform a full checkout on roots
482 uuid, module, revnum = revsplit(rev)
482 uuid, module, revnum = revsplit(rev)
483 entries = svn.client.ls(self.baseurl + quote(module),
483 entries = svn.client.ls(self.baseurl + quote(module),
484 optrev(revnum), True, self.ctx)
484 optrev(revnum), True, self.ctx)
485 files = [n for n, e in entries.iteritems()
485 files = [n for n, e in entries.iteritems()
486 if e.kind == svn.core.svn_node_file]
486 if e.kind == svn.core.svn_node_file]
487 self.removed = set()
487 self.removed = set()
488
488
489 files.sort()
489 files.sort()
490 files = zip(files, [rev] * len(files))
490 files = zip(files, [rev] * len(files))
491 return (files, copies)
491 return (files, copies)
492
492
493 def getchanges(self, rev, full):
493 def getchanges(self, rev, full):
494 # reuse cache from getchangedfiles
494 # reuse cache from getchangedfiles
495 if self._changescache[0] == rev and not full:
495 if self._changescache[0] == rev and not full:
496 (files, copies) = self._changescache[1]
496 (files, copies) = self._changescache[1]
497 else:
497 else:
498 (files, copies) = self._getchanges(rev, full)
498 (files, copies) = self._getchanges(rev, full)
499 # caller caches the result, so free it here to release memory
499 # caller caches the result, so free it here to release memory
500 del self.paths[rev]
500 del self.paths[rev]
501 return (files, copies, set())
501 return (files, copies, set())
502
502
503 def getchangedfiles(self, rev, i):
503 def getchangedfiles(self, rev, i):
504 # called from filemap - cache computed values for reuse in getchanges
504 # called from filemap - cache computed values for reuse in getchanges
505 (files, copies) = self._getchanges(rev, False)
505 (files, copies) = self._getchanges(rev, False)
506 self._changescache = (rev, (files, copies))
506 self._changescache = (rev, (files, copies))
507 return [f[0] for f in files]
507 return [f[0] for f in files]
508
508
509 def getcommit(self, rev):
509 def getcommit(self, rev):
510 if rev not in self.commits:
510 if rev not in self.commits:
511 uuid, module, revnum = revsplit(rev)
511 uuid, module, revnum = revsplit(rev)
512 self.module = module
512 self.module = module
513 self.reparent(module)
513 self.reparent(module)
514 # We assume that:
514 # We assume that:
515 # - requests for revisions after "stop" come from the
515 # - requests for revisions after "stop" come from the
516 # revision graph backward traversal. Cache all of them
516 # revision graph backward traversal. Cache all of them
517 # down to stop, they will be used eventually.
517 # down to stop, they will be used eventually.
518 # - requests for revisions before "stop" come to get
518 # - requests for revisions before "stop" come to get
519 # isolated branches parents. Just fetch what is needed.
519 # isolated branches parents. Just fetch what is needed.
520 stop = self.lastrevs.get(module, 0)
520 stop = self.lastrevs.get(module, 0)
521 if revnum < stop:
521 if revnum < stop:
522 stop = revnum + 1
522 stop = revnum + 1
523 self._fetch_revisions(revnum, stop)
523 self._fetch_revisions(revnum, stop)
524 if rev not in self.commits:
524 if rev not in self.commits:
525 raise error.Abort(_('svn: revision %s not found') % revnum)
525 raise error.Abort(_('svn: revision %s not found') % revnum)
526 revcommit = self.commits[rev]
526 revcommit = self.commits[rev]
527 # caller caches the result, so free it here to release memory
527 # caller caches the result, so free it here to release memory
528 del self.commits[rev]
528 del self.commits[rev]
529 return revcommit
529 return revcommit
530
530
531 def checkrevformat(self, revstr, mapname='splicemap'):
531 def checkrevformat(self, revstr, mapname='splicemap'):
532 """ fails if revision format does not match the correct format"""
532 """ fails if revision format does not match the correct format"""
533 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
533 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
534 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
534 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
535 '{12,12}(.*)\@[0-9]+$',revstr):
535 '{12,12}(.*)\@[0-9]+$',revstr):
536 raise error.Abort(_('%s entry %s is not a valid revision'
536 raise error.Abort(_('%s entry %s is not a valid revision'
537 ' identifier') % (mapname, revstr))
537 ' identifier') % (mapname, revstr))
538
538
539 def numcommits(self):
539 def numcommits(self):
540 return int(self.head.rsplit('@', 1)[1]) - self.startrev
540 return int(self.head.rsplit('@', 1)[1]) - self.startrev
541
541
542 def gettags(self):
542 def gettags(self):
543 tags = {}
543 tags = {}
544 if self.tags is None:
544 if self.tags is None:
545 return tags
545 return tags
546
546
547 # svn tags are just a convention, project branches left in a
547 # svn tags are just a convention, project branches left in a
548 # 'tags' directory. There is no other relationship than
548 # 'tags' directory. There is no other relationship than
549 # ancestry, which is expensive to discover and makes them hard
549 # ancestry, which is expensive to discover and makes them hard
550 # to update incrementally. Worse, past revisions may be
550 # to update incrementally. Worse, past revisions may be
551 # referenced by tags far away in the future, requiring a deep
551 # referenced by tags far away in the future, requiring a deep
552 # history traversal on every calculation. Current code
552 # history traversal on every calculation. Current code
553 # performs a single backward traversal, tracking moves within
553 # performs a single backward traversal, tracking moves within
554 # the tags directory (tag renaming) and recording a new tag
554 # the tags directory (tag renaming) and recording a new tag
555 # everytime a project is copied from outside the tags
555 # everytime a project is copied from outside the tags
556 # directory. It also lists deleted tags, this behaviour may
556 # directory. It also lists deleted tags, this behaviour may
557 # change in the future.
557 # change in the future.
558 pendings = []
558 pendings = []
559 tagspath = self.tags
559 tagspath = self.tags
560 start = svn.ra.get_latest_revnum(self.ra)
560 start = svn.ra.get_latest_revnum(self.ra)
561 stream = self._getlog([self.tags], start, self.startrev)
561 stream = self._getlog([self.tags], start, self.startrev)
562 try:
562 try:
563 for entry in stream:
563 for entry in stream:
564 origpaths, revnum, author, date, message = entry
564 origpaths, revnum, author, date, message = entry
565 if not origpaths:
565 if not origpaths:
566 origpaths = []
566 origpaths = []
567 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
567 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
568 in origpaths.iteritems() if e.copyfrom_path]
568 in origpaths.iteritems() if e.copyfrom_path]
569 # Apply moves/copies from more specific to general
569 # Apply moves/copies from more specific to general
570 copies.sort(reverse=True)
570 copies.sort(reverse=True)
571
571
572 srctagspath = tagspath
572 srctagspath = tagspath
573 if copies and copies[-1][2] == tagspath:
573 if copies and copies[-1][2] == tagspath:
574 # Track tags directory moves
574 # Track tags directory moves
575 srctagspath = copies.pop()[0]
575 srctagspath = copies.pop()[0]
576
576
577 for source, sourcerev, dest in copies:
577 for source, sourcerev, dest in copies:
578 if not dest.startswith(tagspath + '/'):
578 if not dest.startswith(tagspath + '/'):
579 continue
579 continue
580 for tag in pendings:
580 for tag in pendings:
581 if tag[0].startswith(dest):
581 if tag[0].startswith(dest):
582 tagpath = source + tag[0][len(dest):]
582 tagpath = source + tag[0][len(dest):]
583 tag[:2] = [tagpath, sourcerev]
583 tag[:2] = [tagpath, sourcerev]
584 break
584 break
585 else:
585 else:
586 pendings.append([source, sourcerev, dest])
586 pendings.append([source, sourcerev, dest])
587
587
588 # Filter out tags with children coming from different
588 # Filter out tags with children coming from different
589 # parts of the repository like:
589 # parts of the repository like:
590 # /tags/tag.1 (from /trunk:10)
590 # /tags/tag.1 (from /trunk:10)
591 # /tags/tag.1/foo (from /branches/foo:12)
591 # /tags/tag.1/foo (from /branches/foo:12)
592 # Here/tags/tag.1 discarded as well as its children.
592 # Here/tags/tag.1 discarded as well as its children.
593 # It happens with tools like cvs2svn. Such tags cannot
593 # It happens with tools like cvs2svn. Such tags cannot
594 # be represented in mercurial.
594 # be represented in mercurial.
595 addeds = dict((p, e.copyfrom_path) for p, e
595 addeds = dict((p, e.copyfrom_path) for p, e
596 in origpaths.iteritems()
596 in origpaths.iteritems()
597 if e.action == 'A' and e.copyfrom_path)
597 if e.action == 'A' and e.copyfrom_path)
598 badroots = set()
598 badroots = set()
599 for destroot in addeds:
599 for destroot in addeds:
600 for source, sourcerev, dest in pendings:
600 for source, sourcerev, dest in pendings:
601 if (not dest.startswith(destroot + '/')
601 if (not dest.startswith(destroot + '/')
602 or source.startswith(addeds[destroot] + '/')):
602 or source.startswith(addeds[destroot] + '/')):
603 continue
603 continue
604 badroots.add(destroot)
604 badroots.add(destroot)
605 break
605 break
606
606
607 for badroot in badroots:
607 for badroot in badroots:
608 pendings = [p for p in pendings if p[2] != badroot
608 pendings = [p for p in pendings if p[2] != badroot
609 and not p[2].startswith(badroot + '/')]
609 and not p[2].startswith(badroot + '/')]
610
610
611 # Tell tag renamings from tag creations
611 # Tell tag renamings from tag creations
612 renamings = []
612 renamings = []
613 for source, sourcerev, dest in pendings:
613 for source, sourcerev, dest in pendings:
614 tagname = dest.split('/')[-1]
614 tagname = dest.split('/')[-1]
615 if source.startswith(srctagspath):
615 if source.startswith(srctagspath):
616 renamings.append([source, sourcerev, tagname])
616 renamings.append([source, sourcerev, tagname])
617 continue
617 continue
618 if tagname in tags:
618 if tagname in tags:
619 # Keep the latest tag value
619 # Keep the latest tag value
620 continue
620 continue
621 # From revision may be fake, get one with changes
621 # From revision may be fake, get one with changes
622 try:
622 try:
623 tagid = self.latest(source, sourcerev)
623 tagid = self.latest(source, sourcerev)
624 if tagid and tagname not in tags:
624 if tagid and tagname not in tags:
625 tags[tagname] = tagid
625 tags[tagname] = tagid
626 except SvnPathNotFound:
626 except SvnPathNotFound:
627 # It happens when we are following directories
627 # It happens when we are following directories
628 # we assumed were copied with their parents
628 # we assumed were copied with their parents
629 # but were really created in the tag
629 # but were really created in the tag
630 # directory.
630 # directory.
631 pass
631 pass
632 pendings = renamings
632 pendings = renamings
633 tagspath = srctagspath
633 tagspath = srctagspath
634 finally:
634 finally:
635 stream.close()
635 stream.close()
636 return tags
636 return tags
637
637
638 def converted(self, rev, destrev):
638 def converted(self, rev, destrev):
639 if not self.wc:
639 if not self.wc:
640 return
640 return
641 if self.convertfp is None:
641 if self.convertfp is None:
642 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
642 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
643 'a')
643 'a')
644 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
644 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
645 self.convertfp.flush()
645 self.convertfp.flush()
646
646
647 def revid(self, revnum, module=None):
647 def revid(self, revnum, module=None):
648 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
648 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
649
649
650 def revnum(self, rev):
650 def revnum(self, rev):
651 return int(rev.split('@')[-1])
651 return int(rev.split('@')[-1])
652
652
653 def latest(self, path, stop=None):
653 def latest(self, path, stop=None):
654 """Find the latest revid affecting path, up to stop revision
654 """Find the latest revid affecting path, up to stop revision
655 number. If stop is None, default to repository latest
655 number. If stop is None, default to repository latest
656 revision. It may return a revision in a different module,
656 revision. It may return a revision in a different module,
657 since a branch may be moved without a change being
657 since a branch may be moved without a change being
658 reported. Return None if computed module does not belong to
658 reported. Return None if computed module does not belong to
659 rootmodule subtree.
659 rootmodule subtree.
660 """
660 """
661 def findchanges(path, start, stop=None):
661 def findchanges(path, start, stop=None):
662 stream = self._getlog([path], start, stop or 1)
662 stream = self._getlog([path], start, stop or 1)
663 try:
663 try:
664 for entry in stream:
664 for entry in stream:
665 paths, revnum, author, date, message = entry
665 paths, revnum, author, date, message = entry
666 if stop is None and paths:
666 if stop is None and paths:
667 # We do not know the latest changed revision,
667 # We do not know the latest changed revision,
668 # keep the first one with changed paths.
668 # keep the first one with changed paths.
669 break
669 break
670 if revnum <= stop:
670 if revnum <= stop:
671 break
671 break
672
672
673 for p in paths:
673 for p in paths:
674 if (not path.startswith(p) or
674 if (not path.startswith(p) or
675 not paths[p].copyfrom_path):
675 not paths[p].copyfrom_path):
676 continue
676 continue
677 newpath = paths[p].copyfrom_path + path[len(p):]
677 newpath = paths[p].copyfrom_path + path[len(p):]
678 self.ui.debug("branch renamed from %s to %s at %d\n" %
678 self.ui.debug("branch renamed from %s to %s at %d\n" %
679 (path, newpath, revnum))
679 (path, newpath, revnum))
680 path = newpath
680 path = newpath
681 break
681 break
682 if not paths:
682 if not paths:
683 revnum = None
683 revnum = None
684 return revnum, path
684 return revnum, path
685 finally:
685 finally:
686 stream.close()
686 stream.close()
687
687
688 if not path.startswith(self.rootmodule):
688 if not path.startswith(self.rootmodule):
689 # Requests on foreign branches may be forbidden at server level
689 # Requests on foreign branches may be forbidden at server level
690 self.ui.debug('ignoring foreign branch %r\n' % path)
690 self.ui.debug('ignoring foreign branch %r\n' % path)
691 return None
691 return None
692
692
693 if stop is None:
693 if stop is None:
694 stop = svn.ra.get_latest_revnum(self.ra)
694 stop = svn.ra.get_latest_revnum(self.ra)
695 try:
695 try:
696 prevmodule = self.reparent('')
696 prevmodule = self.reparent('')
697 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
697 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
698 self.reparent(prevmodule)
698 self.reparent(prevmodule)
699 except svn.core.SubversionException:
699 except svn.core.SubversionException:
700 dirent = None
700 dirent = None
701 if not dirent:
701 if not dirent:
702 raise SvnPathNotFound(_('%s not found up to revision %d')
702 raise SvnPathNotFound(_('%s not found up to revision %d')
703 % (path, stop))
703 % (path, stop))
704
704
705 # stat() gives us the previous revision on this line of
705 # stat() gives us the previous revision on this line of
706 # development, but it might be in *another module*. Fetch the
706 # development, but it might be in *another module*. Fetch the
707 # log and detect renames down to the latest revision.
707 # log and detect renames down to the latest revision.
708 revnum, realpath = findchanges(path, stop, dirent.created_rev)
708 revnum, realpath = findchanges(path, stop, dirent.created_rev)
709 if revnum is None:
709 if revnum is None:
710 # Tools like svnsync can create empty revision, when
710 # Tools like svnsync can create empty revision, when
711 # synchronizing only a subtree for instance. These empty
711 # synchronizing only a subtree for instance. These empty
712 # revisions created_rev still have their original values
712 # revisions created_rev still have their original values
713 # despite all changes having disappeared and can be
713 # despite all changes having disappeared and can be
714 # returned by ra.stat(), at least when stating the root
714 # returned by ra.stat(), at least when stating the root
715 # module. In that case, do not trust created_rev and scan
715 # module. In that case, do not trust created_rev and scan
716 # the whole history.
716 # the whole history.
717 revnum, realpath = findchanges(path, stop)
717 revnum, realpath = findchanges(path, stop)
718 if revnum is None:
718 if revnum is None:
719 self.ui.debug('ignoring empty branch %r\n' % realpath)
719 self.ui.debug('ignoring empty branch %r\n' % realpath)
720 return None
720 return None
721
721
722 if not realpath.startswith(self.rootmodule):
722 if not realpath.startswith(self.rootmodule):
723 self.ui.debug('ignoring foreign branch %r\n' % realpath)
723 self.ui.debug('ignoring foreign branch %r\n' % realpath)
724 return None
724 return None
725 return self.revid(revnum, realpath)
725 return self.revid(revnum, realpath)
726
726
727 def reparent(self, module):
727 def reparent(self, module):
728 """Reparent the svn transport and return the previous parent."""
728 """Reparent the svn transport and return the previous parent."""
729 if self.prevmodule == module:
729 if self.prevmodule == module:
730 return module
730 return module
731 svnurl = self.baseurl + quote(module)
731 svnurl = self.baseurl + quote(module)
732 prevmodule = self.prevmodule
732 prevmodule = self.prevmodule
733 if prevmodule is None:
733 if prevmodule is None:
734 prevmodule = ''
734 prevmodule = ''
735 self.ui.debug("reparent to %s\n" % svnurl)
735 self.ui.debug("reparent to %s\n" % svnurl)
736 svn.ra.reparent(self.ra, svnurl)
736 svn.ra.reparent(self.ra, svnurl)
737 self.prevmodule = module
737 self.prevmodule = module
738 return prevmodule
738 return prevmodule
739
739
740 def expandpaths(self, rev, paths, parents):
740 def expandpaths(self, rev, paths, parents):
741 changed, removed = set(), set()
741 changed, removed = set(), set()
742 copies = {}
742 copies = {}
743
743
744 new_module, revnum = revsplit(rev)[1:]
744 new_module, revnum = revsplit(rev)[1:]
745 if new_module != self.module:
745 if new_module != self.module:
746 self.module = new_module
746 self.module = new_module
747 self.reparent(self.module)
747 self.reparent(self.module)
748
748
749 for i, (path, ent) in enumerate(paths):
749 for i, (path, ent) in enumerate(paths):
750 self.ui.progress(_('scanning paths'), i, item=path,
750 self.ui.progress(_('scanning paths'), i, item=path,
751 total=len(paths), unit=_('paths'))
751 total=len(paths), unit=_('paths'))
752 entrypath = self.getrelpath(path)
752 entrypath = self.getrelpath(path)
753
753
754 kind = self._checkpath(entrypath, revnum)
754 kind = self._checkpath(entrypath, revnum)
755 if kind == svn.core.svn_node_file:
755 if kind == svn.core.svn_node_file:
756 changed.add(self.recode(entrypath))
756 changed.add(self.recode(entrypath))
757 if not ent.copyfrom_path or not parents:
757 if not ent.copyfrom_path or not parents:
758 continue
758 continue
759 # Copy sources not in parent revisions cannot be
759 # Copy sources not in parent revisions cannot be
760 # represented, ignore their origin for now
760 # represented, ignore their origin for now
761 pmodule, prevnum = revsplit(parents[0])[1:]
761 pmodule, prevnum = revsplit(parents[0])[1:]
762 if ent.copyfrom_rev < prevnum:
762 if ent.copyfrom_rev < prevnum:
763 continue
763 continue
764 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
764 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
765 if not copyfrom_path:
765 if not copyfrom_path:
766 continue
766 continue
767 self.ui.debug("copied to %s from %s@%s\n" %
767 self.ui.debug("copied to %s from %s@%s\n" %
768 (entrypath, copyfrom_path, ent.copyfrom_rev))
768 (entrypath, copyfrom_path, ent.copyfrom_rev))
769 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
769 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
770 elif kind == 0: # gone, but had better be a deleted *file*
770 elif kind == 0: # gone, but had better be a deleted *file*
771 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
771 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
772 pmodule, prevnum = revsplit(parents[0])[1:]
772 pmodule, prevnum = revsplit(parents[0])[1:]
773 parentpath = pmodule + "/" + entrypath
773 parentpath = pmodule + "/" + entrypath
774 fromkind = self._checkpath(entrypath, prevnum, pmodule)
774 fromkind = self._checkpath(entrypath, prevnum, pmodule)
775
775
776 if fromkind == svn.core.svn_node_file:
776 if fromkind == svn.core.svn_node_file:
777 removed.add(self.recode(entrypath))
777 removed.add(self.recode(entrypath))
778 elif fromkind == svn.core.svn_node_dir:
778 elif fromkind == svn.core.svn_node_dir:
779 oroot = parentpath.strip('/')
779 oroot = parentpath.strip('/')
780 nroot = path.strip('/')
780 nroot = path.strip('/')
781 children = self._iterfiles(oroot, prevnum)
781 children = self._iterfiles(oroot, prevnum)
782 for childpath in children:
782 for childpath in children:
783 childpath = childpath.replace(oroot, nroot)
783 childpath = childpath.replace(oroot, nroot)
784 childpath = self.getrelpath("/" + childpath, pmodule)
784 childpath = self.getrelpath("/" + childpath, pmodule)
785 if childpath:
785 if childpath:
786 removed.add(self.recode(childpath))
786 removed.add(self.recode(childpath))
787 else:
787 else:
788 self.ui.debug('unknown path in revision %d: %s\n' % \
788 self.ui.debug('unknown path in revision %d: %s\n' % \
789 (revnum, path))
789 (revnum, path))
790 elif kind == svn.core.svn_node_dir:
790 elif kind == svn.core.svn_node_dir:
791 if ent.action == 'M':
791 if ent.action == 'M':
792 # If the directory just had a prop change,
792 # If the directory just had a prop change,
793 # then we shouldn't need to look for its children.
793 # then we shouldn't need to look for its children.
794 continue
794 continue
795 if ent.action == 'R' and parents:
795 if ent.action == 'R' and parents:
796 # If a directory is replacing a file, mark the previous
796 # If a directory is replacing a file, mark the previous
797 # file as deleted
797 # file as deleted
798 pmodule, prevnum = revsplit(parents[0])[1:]
798 pmodule, prevnum = revsplit(parents[0])[1:]
799 pkind = self._checkpath(entrypath, prevnum, pmodule)
799 pkind = self._checkpath(entrypath, prevnum, pmodule)
800 if pkind == svn.core.svn_node_file:
800 if pkind == svn.core.svn_node_file:
801 removed.add(self.recode(entrypath))
801 removed.add(self.recode(entrypath))
802 elif pkind == svn.core.svn_node_dir:
802 elif pkind == svn.core.svn_node_dir:
803 # We do not know what files were kept or removed,
803 # We do not know what files were kept or removed,
804 # mark them all as changed.
804 # mark them all as changed.
805 for childpath in self._iterfiles(pmodule, prevnum):
805 for childpath in self._iterfiles(pmodule, prevnum):
806 childpath = self.getrelpath("/" + childpath)
806 childpath = self.getrelpath("/" + childpath)
807 if childpath:
807 if childpath:
808 changed.add(self.recode(childpath))
808 changed.add(self.recode(childpath))
809
809
810 for childpath in self._iterfiles(path, revnum):
810 for childpath in self._iterfiles(path, revnum):
811 childpath = self.getrelpath("/" + childpath)
811 childpath = self.getrelpath("/" + childpath)
812 if childpath:
812 if childpath:
813 changed.add(self.recode(childpath))
813 changed.add(self.recode(childpath))
814
814
815 # Handle directory copies
815 # Handle directory copies
816 if not ent.copyfrom_path or not parents:
816 if not ent.copyfrom_path or not parents:
817 continue
817 continue
818 # Copy sources not in parent revisions cannot be
818 # Copy sources not in parent revisions cannot be
819 # represented, ignore their origin for now
819 # represented, ignore their origin for now
820 pmodule, prevnum = revsplit(parents[0])[1:]
820 pmodule, prevnum = revsplit(parents[0])[1:]
821 if ent.copyfrom_rev < prevnum:
821 if ent.copyfrom_rev < prevnum:
822 continue
822 continue
823 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
823 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
824 if not copyfrompath:
824 if not copyfrompath:
825 continue
825 continue
826 self.ui.debug("mark %s came from %s:%d\n"
826 self.ui.debug("mark %s came from %s:%d\n"
827 % (path, copyfrompath, ent.copyfrom_rev))
827 % (path, copyfrompath, ent.copyfrom_rev))
828 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
828 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
829 for childpath in children:
829 for childpath in children:
830 childpath = self.getrelpath("/" + childpath, pmodule)
830 childpath = self.getrelpath("/" + childpath, pmodule)
831 if not childpath:
831 if not childpath:
832 continue
832 continue
833 copytopath = path + childpath[len(copyfrompath):]
833 copytopath = path + childpath[len(copyfrompath):]
834 copytopath = self.getrelpath(copytopath)
834 copytopath = self.getrelpath(copytopath)
835 copies[self.recode(copytopath)] = self.recode(childpath)
835 copies[self.recode(copytopath)] = self.recode(childpath)
836
836
837 self.ui.progress(_('scanning paths'), None)
837 self.ui.progress(_('scanning paths'), None)
838 changed.update(removed)
838 changed.update(removed)
839 return (list(changed), removed, copies)
839 return (list(changed), removed, copies)
840
840
841 def _fetch_revisions(self, from_revnum, to_revnum):
841 def _fetch_revisions(self, from_revnum, to_revnum):
842 if from_revnum < to_revnum:
842 if from_revnum < to_revnum:
843 from_revnum, to_revnum = to_revnum, from_revnum
843 from_revnum, to_revnum = to_revnum, from_revnum
844
844
845 self.child_cset = None
845 self.child_cset = None
846
846
847 def parselogentry(orig_paths, revnum, author, date, message):
847 def parselogentry(orig_paths, revnum, author, date, message):
848 """Return the parsed commit object or None, and True if
848 """Return the parsed commit object or None, and True if
849 the revision is a branch root.
849 the revision is a branch root.
850 """
850 """
851 self.ui.debug("parsing revision %d (%d changes)\n" %
851 self.ui.debug("parsing revision %d (%d changes)\n" %
852 (revnum, len(orig_paths)))
852 (revnum, len(orig_paths)))
853
853
854 branched = False
854 branched = False
855 rev = self.revid(revnum)
855 rev = self.revid(revnum)
856 # branch log might return entries for a parent we already have
856 # branch log might return entries for a parent we already have
857
857
858 if rev in self.commits or revnum < to_revnum:
858 if rev in self.commits or revnum < to_revnum:
859 return None, branched
859 return None, branched
860
860
861 parents = []
861 parents = []
862 # check whether this revision is the start of a branch or part
862 # check whether this revision is the start of a branch or part
863 # of a branch renaming
863 # of a branch renaming
864 orig_paths = sorted(orig_paths.iteritems())
864 orig_paths = sorted(orig_paths.iteritems())
865 root_paths = [(p, e) for p, e in orig_paths
865 root_paths = [(p, e) for p, e in orig_paths
866 if self.module.startswith(p)]
866 if self.module.startswith(p)]
867 if root_paths:
867 if root_paths:
868 path, ent = root_paths[-1]
868 path, ent = root_paths[-1]
869 if ent.copyfrom_path:
869 if ent.copyfrom_path:
870 branched = True
870 branched = True
871 newpath = ent.copyfrom_path + self.module[len(path):]
871 newpath = ent.copyfrom_path + self.module[len(path):]
872 # ent.copyfrom_rev may not be the actual last revision
872 # ent.copyfrom_rev may not be the actual last revision
873 previd = self.latest(newpath, ent.copyfrom_rev)
873 previd = self.latest(newpath, ent.copyfrom_rev)
874 if previd is not None:
874 if previd is not None:
875 prevmodule, prevnum = revsplit(previd)[1:]
875 prevmodule, prevnum = revsplit(previd)[1:]
876 if prevnum >= self.startrev:
876 if prevnum >= self.startrev:
877 parents = [previd]
877 parents = [previd]
878 self.ui.note(
878 self.ui.note(
879 _('found parent of branch %s at %d: %s\n') %
879 _('found parent of branch %s at %d: %s\n') %
880 (self.module, prevnum, prevmodule))
880 (self.module, prevnum, prevmodule))
881 else:
881 else:
882 self.ui.debug("no copyfrom path, don't know what to do.\n")
882 self.ui.debug("no copyfrom path, don't know what to do.\n")
883
883
884 paths = []
884 paths = []
885 # filter out unrelated paths
885 # filter out unrelated paths
886 for path, ent in orig_paths:
886 for path, ent in orig_paths:
887 if self.getrelpath(path) is None:
887 if self.getrelpath(path) is None:
888 continue
888 continue
889 paths.append((path, ent))
889 paths.append((path, ent))
890
890
891 # Example SVN datetime. Includes microseconds.
891 # Example SVN datetime. Includes microseconds.
892 # ISO-8601 conformant
892 # ISO-8601 conformant
893 # '2007-01-04T17:35:00.902377Z'
893 # '2007-01-04T17:35:00.902377Z'
894 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
894 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
895 if self.ui.configbool('convert', 'localtimezone'):
895 if self.ui.configbool('convert', 'localtimezone'):
896 date = makedatetimestamp(date[0])
896 date = makedatetimestamp(date[0])
897
897
898 if message:
898 if message:
899 log = self.recode(message)
899 log = self.recode(message)
900 else:
900 else:
901 log = ''
901 log = ''
902
902
903 if author:
903 if author:
904 author = self.recode(author)
904 author = self.recode(author)
905 else:
905 else:
906 author = ''
906 author = ''
907
907
908 try:
908 try:
909 branch = self.module.split("/")[-1]
909 branch = self.module.split("/")[-1]
910 if branch == self.trunkname:
910 if branch == self.trunkname:
911 branch = None
911 branch = None
912 except IndexError:
912 except IndexError:
913 branch = None
913 branch = None
914
914
915 cset = commit(author=author,
915 cset = commit(author=author,
916 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
916 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
917 desc=log,
917 desc=log,
918 parents=parents,
918 parents=parents,
919 branch=branch,
919 branch=branch,
920 rev=rev)
920 rev=rev)
921
921
922 self.commits[rev] = cset
922 self.commits[rev] = cset
923 # The parents list is *shared* among self.paths and the
923 # The parents list is *shared* among self.paths and the
924 # commit object. Both will be updated below.
924 # commit object. Both will be updated below.
925 self.paths[rev] = (paths, cset.parents)
925 self.paths[rev] = (paths, cset.parents)
926 if self.child_cset and not self.child_cset.parents:
926 if self.child_cset and not self.child_cset.parents:
927 self.child_cset.parents[:] = [rev]
927 self.child_cset.parents[:] = [rev]
928 self.child_cset = cset
928 self.child_cset = cset
929 return cset, branched
929 return cset, branched
930
930
931 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
931 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
932 (self.module, from_revnum, to_revnum))
932 (self.module, from_revnum, to_revnum))
933
933
934 try:
934 try:
935 firstcset = None
935 firstcset = None
936 lastonbranch = False
936 lastonbranch = False
937 stream = self._getlog([self.module], from_revnum, to_revnum)
937 stream = self._getlog([self.module], from_revnum, to_revnum)
938 try:
938 try:
939 for entry in stream:
939 for entry in stream:
940 paths, revnum, author, date, message = entry
940 paths, revnum, author, date, message = entry
941 if revnum < self.startrev:
941 if revnum < self.startrev:
942 lastonbranch = True
942 lastonbranch = True
943 break
943 break
944 if not paths:
944 if not paths:
945 self.ui.debug('revision %d has no entries\n' % revnum)
945 self.ui.debug('revision %d has no entries\n' % revnum)
946 # If we ever leave the loop on an empty
946 # If we ever leave the loop on an empty
947 # revision, do not try to get a parent branch
947 # revision, do not try to get a parent branch
948 lastonbranch = lastonbranch or revnum == 0
948 lastonbranch = lastonbranch or revnum == 0
949 continue
949 continue
950 cset, lastonbranch = parselogentry(paths, revnum, author,
950 cset, lastonbranch = parselogentry(paths, revnum, author,
951 date, message)
951 date, message)
952 if cset:
952 if cset:
953 firstcset = cset
953 firstcset = cset
954 if lastonbranch:
954 if lastonbranch:
955 break
955 break
956 finally:
956 finally:
957 stream.close()
957 stream.close()
958
958
959 if not lastonbranch and firstcset and not firstcset.parents:
959 if not lastonbranch and firstcset and not firstcset.parents:
960 # The first revision of the sequence (the last fetched one)
960 # The first revision of the sequence (the last fetched one)
961 # has invalid parents if not a branch root. Find the parent
961 # has invalid parents if not a branch root. Find the parent
962 # revision now, if any.
962 # revision now, if any.
963 try:
963 try:
964 firstrevnum = self.revnum(firstcset.rev)
964 firstrevnum = self.revnum(firstcset.rev)
965 if firstrevnum > 1:
965 if firstrevnum > 1:
966 latest = self.latest(self.module, firstrevnum - 1)
966 latest = self.latest(self.module, firstrevnum - 1)
967 if latest:
967 if latest:
968 firstcset.parents.append(latest)
968 firstcset.parents.append(latest)
969 except SvnPathNotFound:
969 except SvnPathNotFound:
970 pass
970 pass
971 except svn.core.SubversionException as xxx_todo_changeme:
971 except svn.core.SubversionException as xxx_todo_changeme:
972 (inst, num) = xxx_todo_changeme.args
972 (inst, num) = xxx_todo_changeme.args
973 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
973 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
974 raise error.Abort(_('svn: branch has no revision %s')
974 raise error.Abort(_('svn: branch has no revision %s')
975 % to_revnum)
975 % to_revnum)
976 raise
976 raise
977
977
978 def getfile(self, file, rev):
978 def getfile(self, file, rev):
979 # TODO: ra.get_file transmits the whole file instead of diffs.
979 # TODO: ra.get_file transmits the whole file instead of diffs.
980 if file in self.removed:
980 if file in self.removed:
981 return None, None
981 return None, None
982 mode = ''
982 mode = ''
983 try:
983 try:
984 new_module, revnum = revsplit(rev)[1:]
984 new_module, revnum = revsplit(rev)[1:]
985 if self.module != new_module:
985 if self.module != new_module:
986 self.module = new_module
986 self.module = new_module
987 self.reparent(self.module)
987 self.reparent(self.module)
988 io = stringio()
988 io = stringio()
989 info = svn.ra.get_file(self.ra, file, revnum, io)
989 info = svn.ra.get_file(self.ra, file, revnum, io)
990 data = io.getvalue()
990 data = io.getvalue()
991 # ra.get_file() seems to keep a reference on the input buffer
991 # ra.get_file() seems to keep a reference on the input buffer
992 # preventing collection. Release it explicitly.
992 # preventing collection. Release it explicitly.
993 io.close()
993 io.close()
994 if isinstance(info, list):
994 if isinstance(info, list):
995 info = info[-1]
995 info = info[-1]
996 mode = ("svn:executable" in info) and 'x' or ''
996 mode = ("svn:executable" in info) and 'x' or ''
997 mode = ("svn:special" in info) and 'l' or mode
997 mode = ("svn:special" in info) and 'l' or mode
998 except svn.core.SubversionException as e:
998 except svn.core.SubversionException as e:
999 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
999 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
1000 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
1000 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
1001 if e.apr_err in notfound: # File not found
1001 if e.apr_err in notfound: # File not found
1002 return None, None
1002 return None, None
1003 raise
1003 raise
1004 if mode == 'l':
1004 if mode == 'l':
1005 link_prefix = "link "
1005 link_prefix = "link "
1006 if data.startswith(link_prefix):
1006 if data.startswith(link_prefix):
1007 data = data[len(link_prefix):]
1007 data = data[len(link_prefix):]
1008 return data, mode
1008 return data, mode
1009
1009
1010 def _iterfiles(self, path, revnum):
1010 def _iterfiles(self, path, revnum):
1011 """Enumerate all files in path at revnum, recursively."""
1011 """Enumerate all files in path at revnum, recursively."""
1012 path = path.strip('/')
1012 path = path.strip('/')
1013 pool = svn.core.Pool()
1013 pool = svn.core.Pool()
1014 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
1014 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
1015 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1015 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1016 if path:
1016 if path:
1017 path += '/'
1017 path += '/'
1018 return ((path + p) for p, e in entries.iteritems()
1018 return ((path + p) for p, e in entries.iteritems()
1019 if e.kind == svn.core.svn_node_file)
1019 if e.kind == svn.core.svn_node_file)
1020
1020
1021 def getrelpath(self, path, module=None):
1021 def getrelpath(self, path, module=None):
1022 if module is None:
1022 if module is None:
1023 module = self.module
1023 module = self.module
1024 # Given the repository url of this wc, say
1024 # Given the repository url of this wc, say
1025 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1025 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1026 # extract the "entry" portion (a relative path) from what
1026 # extract the "entry" portion (a relative path) from what
1027 # svn log --xml says, i.e.
1027 # svn log --xml says, i.e.
1028 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1028 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1029 # that is to say "tests/PloneTestCase.py"
1029 # that is to say "tests/PloneTestCase.py"
1030 if path.startswith(module):
1030 if path.startswith(module):
1031 relative = path.rstrip('/')[len(module):]
1031 relative = path.rstrip('/')[len(module):]
1032 if relative.startswith('/'):
1032 if relative.startswith('/'):
1033 return relative[1:]
1033 return relative[1:]
1034 elif relative == '':
1034 elif relative == '':
1035 return relative
1035 return relative
1036
1036
1037 # The path is outside our tracked tree...
1037 # The path is outside our tracked tree...
1038 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1038 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1039 return None
1039 return None
1040
1040
1041 def _checkpath(self, path, revnum, module=None):
1041 def _checkpath(self, path, revnum, module=None):
1042 if module is not None:
1042 if module is not None:
1043 prevmodule = self.reparent('')
1043 prevmodule = self.reparent('')
1044 path = module + '/' + path
1044 path = module + '/' + path
1045 try:
1045 try:
1046 # ra.check_path does not like leading slashes very much, it leads
1046 # ra.check_path does not like leading slashes very much, it leads
1047 # to PROPFIND subversion errors
1047 # to PROPFIND subversion errors
1048 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1048 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1049 finally:
1049 finally:
1050 if module is not None:
1050 if module is not None:
1051 self.reparent(prevmodule)
1051 self.reparent(prevmodule)
1052
1052
1053 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1053 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1054 strict_node_history=False):
1054 strict_node_history=False):
1055 # Normalize path names, svn >= 1.5 only wants paths relative to
1055 # Normalize path names, svn >= 1.5 only wants paths relative to
1056 # supplied URL
1056 # supplied URL
1057 relpaths = []
1057 relpaths = []
1058 for p in paths:
1058 for p in paths:
1059 if not p.startswith('/'):
1059 if not p.startswith('/'):
1060 p = self.module + '/' + p
1060 p = self.module + '/' + p
1061 relpaths.append(p.strip('/'))
1061 relpaths.append(p.strip('/'))
1062 args = [self.baseurl, relpaths, start, end, limit,
1062 args = [self.baseurl, relpaths, start, end, limit,
1063 discover_changed_paths, strict_node_history]
1063 discover_changed_paths, strict_node_history]
1064 # developer config: convert.svn.debugsvnlog
1064 # developer config: convert.svn.debugsvnlog
1065 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1065 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1066 return directlogstream(*args)
1066 return directlogstream(*args)
1067 arg = encodeargs(args)
1067 arg = encodeargs(args)
1068 hgexe = util.hgexecutable()
1068 hgexe = util.hgexecutable()
1069 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1069 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1070 stdin, stdout = util.popen2(util.quotecommand(cmd))
1070 stdin, stdout = util.popen2(util.quotecommand(cmd))
1071 stdin.write(arg)
1071 stdin.write(arg)
1072 try:
1072 try:
1073 stdin.close()
1073 stdin.close()
1074 except IOError:
1074 except IOError:
1075 raise error.Abort(_('Mercurial failed to run itself, check'
1075 raise error.Abort(_('Mercurial failed to run itself, check'
1076 ' hg executable is in PATH'))
1076 ' hg executable is in PATH'))
1077 return logstream(stdout)
1077 return logstream(stdout)
1078
1078
1079 pre_revprop_change = '''#!/bin/sh
1079 pre_revprop_change = '''#!/bin/sh
1080
1080
1081 REPOS="$1"
1081 REPOS="$1"
1082 REV="$2"
1082 REV="$2"
1083 USER="$3"
1083 USER="$3"
1084 PROPNAME="$4"
1084 PROPNAME="$4"
1085 ACTION="$5"
1085 ACTION="$5"
1086
1086
1087 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1087 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1088 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1088 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1089 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1089 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1090
1090
1091 echo "Changing prohibited revision property" >&2
1091 echo "Changing prohibited revision property" >&2
1092 exit 1
1092 exit 1
1093 '''
1093 '''
1094
1094
1095 class svn_sink(converter_sink, commandline):
1095 class svn_sink(converter_sink, commandline):
1096 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1096 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1097 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1097 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1098
1098
1099 def prerun(self):
1099 def prerun(self):
1100 if self.wc:
1100 if self.wc:
1101 os.chdir(self.wc)
1101 os.chdir(self.wc)
1102
1102
1103 def postrun(self):
1103 def postrun(self):
1104 if self.wc:
1104 if self.wc:
1105 os.chdir(self.cwd)
1105 os.chdir(self.cwd)
1106
1106
1107 def join(self, name):
1107 def join(self, name):
1108 return os.path.join(self.wc, '.svn', name)
1108 return os.path.join(self.wc, '.svn', name)
1109
1109
1110 def revmapfile(self):
1110 def revmapfile(self):
1111 return self.join('hg-shamap')
1111 return self.join('hg-shamap')
1112
1112
1113 def authorfile(self):
1113 def authorfile(self):
1114 return self.join('hg-authormap')
1114 return self.join('hg-authormap')
1115
1115
1116 def __init__(self, ui, path):
1116 def __init__(self, ui, path):
1117
1117
1118 converter_sink.__init__(self, ui, path)
1118 converter_sink.__init__(self, ui, path)
1119 commandline.__init__(self, ui, 'svn')
1119 commandline.__init__(self, ui, 'svn')
1120 self.delete = []
1120 self.delete = []
1121 self.setexec = []
1121 self.setexec = []
1122 self.delexec = []
1122 self.delexec = []
1123 self.copies = []
1123 self.copies = []
1124 self.wc = None
1124 self.wc = None
1125 self.cwd = os.getcwd()
1125 self.cwd = os.getcwd()
1126
1126
1127 created = False
1127 created = False
1128 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1128 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1129 self.wc = os.path.realpath(path)
1129 self.wc = os.path.realpath(path)
1130 self.run0('update')
1130 self.run0('update')
1131 else:
1131 else:
1132 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1132 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1133 path = os.path.realpath(path)
1133 path = os.path.realpath(path)
1134 if os.path.isdir(os.path.dirname(path)):
1134 if os.path.isdir(os.path.dirname(path)):
1135 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1135 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1136 ui.status(_('initializing svn repository %r\n') %
1136 ui.status(_('initializing svn repository %r\n') %
1137 os.path.basename(path))
1137 os.path.basename(path))
1138 commandline(ui, 'svnadmin').run0('create', path)
1138 commandline(ui, 'svnadmin').run0('create', path)
1139 created = path
1139 created = path
1140 path = util.normpath(path)
1140 path = util.normpath(path)
1141 if not path.startswith('/'):
1141 if not path.startswith('/'):
1142 path = '/' + path
1142 path = '/' + path
1143 path = 'file://' + path
1143 path = 'file://' + path
1144
1144
1145 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1145 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1146 ui.status(_('initializing svn working copy %r\n')
1146 ui.status(_('initializing svn working copy %r\n')
1147 % os.path.basename(wcpath))
1147 % os.path.basename(wcpath))
1148 self.run0('checkout', path, wcpath)
1148 self.run0('checkout', path, wcpath)
1149
1149
1150 self.wc = wcpath
1150 self.wc = wcpath
1151 self.opener = scmutil.opener(self.wc)
1151 self.opener = scmutil.opener(self.wc)
1152 self.wopener = scmutil.opener(self.wc)
1152 self.wopener = scmutil.opener(self.wc)
1153 self.childmap = mapfile(ui, self.join('hg-childmap'))
1153 self.childmap = mapfile(ui, self.join('hg-childmap'))
1154 if util.checkexec(self.wc):
1154 if util.checkexec(self.wc):
1155 self.is_exec = util.isexec
1155 self.is_exec = util.isexec
1156 else:
1156 else:
1157 self.is_exec = None
1157 self.is_exec = None
1158
1158
1159 if created:
1159 if created:
1160 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1160 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1161 fp = open(hook, 'w')
1161 fp = open(hook, 'w')
1162 fp.write(pre_revprop_change)
1162 fp.write(pre_revprop_change)
1163 fp.close()
1163 fp.close()
1164 util.setflags(hook, False, True)
1164 util.setflags(hook, False, True)
1165
1165
1166 output = self.run0('info')
1166 output = self.run0('info')
1167 self.uuid = self.uuid_re.search(output).group(1).strip()
1167 self.uuid = self.uuid_re.search(output).group(1).strip()
1168
1168
1169 def wjoin(self, *names):
1169 def wjoin(self, *names):
1170 return os.path.join(self.wc, *names)
1170 return os.path.join(self.wc, *names)
1171
1171
1172 @propertycache
1172 @propertycache
1173 def manifest(self):
1173 def manifest(self):
1174 # As of svn 1.7, the "add" command fails when receiving
1174 # As of svn 1.7, the "add" command fails when receiving
1175 # already tracked entries, so we have to track and filter them
1175 # already tracked entries, so we have to track and filter them
1176 # ourselves.
1176 # ourselves.
1177 m = set()
1177 m = set()
1178 output = self.run0('ls', recursive=True, xml=True)
1178 output = self.run0('ls', recursive=True, xml=True)
1179 doc = xml.dom.minidom.parseString(output)
1179 doc = xml.dom.minidom.parseString(output)
1180 for e in doc.getElementsByTagName('entry'):
1180 for e in doc.getElementsByTagName('entry'):
1181 for n in e.childNodes:
1181 for n in e.childNodes:
1182 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1182 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1183 continue
1183 continue
1184 name = ''.join(c.data for c in n.childNodes
1184 name = ''.join(c.data for c in n.childNodes
1185 if c.nodeType == c.TEXT_NODE)
1185 if c.nodeType == c.TEXT_NODE)
1186 # Entries are compared with names coming from
1186 # Entries are compared with names coming from
1187 # mercurial, so bytes with undefined encoding. Our
1187 # mercurial, so bytes with undefined encoding. Our
1188 # best bet is to assume they are in local
1188 # best bet is to assume they are in local
1189 # encoding. They will be passed to command line calls
1189 # encoding. They will be passed to command line calls
1190 # later anyway, so they better be.
1190 # later anyway, so they better be.
1191 m.add(encoding.tolocal(name.encode('utf-8')))
1191 m.add(encoding.tolocal(name.encode('utf-8')))
1192 break
1192 break
1193 return m
1193 return m
1194
1194
1195 def putfile(self, filename, flags, data):
1195 def putfile(self, filename, flags, data):
1196 if 'l' in flags:
1196 if 'l' in flags:
1197 self.wopener.symlink(data, filename)
1197 self.wopener.symlink(data, filename)
1198 else:
1198 else:
1199 try:
1199 try:
1200 if os.path.islink(self.wjoin(filename)):
1200 if os.path.islink(self.wjoin(filename)):
1201 os.unlink(filename)
1201 os.unlink(filename)
1202 except OSError:
1202 except OSError:
1203 pass
1203 pass
1204 self.wopener.write(filename, data)
1204 self.wopener.write(filename, data)
1205
1205
1206 if self.is_exec:
1206 if self.is_exec:
1207 if self.is_exec(self.wjoin(filename)):
1207 if self.is_exec(self.wjoin(filename)):
1208 if 'x' not in flags:
1208 if 'x' not in flags:
1209 self.delexec.append(filename)
1209 self.delexec.append(filename)
1210 else:
1210 else:
1211 if 'x' in flags:
1211 if 'x' in flags:
1212 self.setexec.append(filename)
1212 self.setexec.append(filename)
1213 util.setflags(self.wjoin(filename), False, 'x' in flags)
1213 util.setflags(self.wjoin(filename), False, 'x' in flags)
1214
1214
1215 def _copyfile(self, source, dest):
1215 def _copyfile(self, source, dest):
1216 # SVN's copy command pukes if the destination file exists, but
1216 # SVN's copy command pukes if the destination file exists, but
1217 # our copyfile method expects to record a copy that has
1217 # our copyfile method expects to record a copy that has
1218 # already occurred. Cross the semantic gap.
1218 # already occurred. Cross the semantic gap.
1219 wdest = self.wjoin(dest)
1219 wdest = self.wjoin(dest)
1220 exists = os.path.lexists(wdest)
1220 exists = os.path.lexists(wdest)
1221 if exists:
1221 if exists:
1222 fd, tempname = tempfile.mkstemp(
1222 fd, tempname = tempfile.mkstemp(
1223 prefix='hg-copy-', dir=os.path.dirname(wdest))
1223 prefix='hg-copy-', dir=os.path.dirname(wdest))
1224 os.close(fd)
1224 os.close(fd)
1225 os.unlink(tempname)
1225 os.unlink(tempname)
1226 os.rename(wdest, tempname)
1226 os.rename(wdest, tempname)
1227 try:
1227 try:
1228 self.run0('copy', source, dest)
1228 self.run0('copy', source, dest)
1229 finally:
1229 finally:
1230 self.manifest.add(dest)
1230 self.manifest.add(dest)
1231 if exists:
1231 if exists:
1232 try:
1232 try:
1233 os.unlink(wdest)
1233 os.unlink(wdest)
1234 except OSError:
1234 except OSError:
1235 pass
1235 pass
1236 os.rename(tempname, wdest)
1236 os.rename(tempname, wdest)
1237
1237
1238 def dirs_of(self, files):
1238 def dirs_of(self, files):
1239 dirs = set()
1239 dirs = set()
1240 for f in files:
1240 for f in files:
1241 if os.path.isdir(self.wjoin(f)):
1241 if os.path.isdir(self.wjoin(f)):
1242 dirs.add(f)
1242 dirs.add(f)
1243 for i in strutil.rfindall(f, '/'):
1243 for i in strutil.rfindall(f, '/'):
1244 dirs.add(f[:i])
1244 dirs.add(f[:i])
1245 return dirs
1245 return dirs
1246
1246
1247 def add_dirs(self, files):
1247 def add_dirs(self, files):
1248 add_dirs = [d for d in sorted(self.dirs_of(files))
1248 add_dirs = [d for d in sorted(self.dirs_of(files))
1249 if d not in self.manifest]
1249 if d not in self.manifest]
1250 if add_dirs:
1250 if add_dirs:
1251 self.manifest.update(add_dirs)
1251 self.manifest.update(add_dirs)
1252 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1252 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1253 return add_dirs
1253 return add_dirs
1254
1254
1255 def add_files(self, files):
1255 def add_files(self, files):
1256 files = [f for f in files if f not in self.manifest]
1256 files = [f for f in files if f not in self.manifest]
1257 if files:
1257 if files:
1258 self.manifest.update(files)
1258 self.manifest.update(files)
1259 self.xargs(files, 'add', quiet=True)
1259 self.xargs(files, 'add', quiet=True)
1260 return files
1260 return files
1261
1261
1262 def addchild(self, parent, child):
1262 def addchild(self, parent, child):
1263 self.childmap[parent] = child
1263 self.childmap[parent] = child
1264
1264
1265 def revid(self, rev):
1265 def revid(self, rev):
1266 return u"svn:%s@%s" % (self.uuid, rev)
1266 return u"svn:%s@%s" % (self.uuid, rev)
1267
1267
1268 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1268 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1269 cleanp2):
1269 cleanp2):
1270 for parent in parents:
1270 for parent in parents:
1271 try:
1271 try:
1272 return self.revid(self.childmap[parent])
1272 return self.revid(self.childmap[parent])
1273 except KeyError:
1273 except KeyError:
1274 pass
1274 pass
1275
1275
1276 # Apply changes to working copy
1276 # Apply changes to working copy
1277 for f, v in files:
1277 for f, v in files:
1278 data, mode = source.getfile(f, v)
1278 data, mode = source.getfile(f, v)
1279 if data is None:
1279 if data is None:
1280 self.delete.append(f)
1280 self.delete.append(f)
1281 else:
1281 else:
1282 self.putfile(f, mode, data)
1282 self.putfile(f, mode, data)
1283 if f in copies:
1283 if f in copies:
1284 self.copies.append([copies[f], f])
1284 self.copies.append([copies[f], f])
1285 if full:
1285 if full:
1286 self.delete.extend(sorted(self.manifest.difference(files)))
1286 self.delete.extend(sorted(self.manifest.difference(files)))
1287 files = [f[0] for f in files]
1287 files = [f[0] for f in files]
1288
1288
1289 entries = set(self.delete)
1289 entries = set(self.delete)
1290 files = frozenset(files)
1290 files = frozenset(files)
1291 entries.update(self.add_dirs(files.difference(entries)))
1291 entries.update(self.add_dirs(files.difference(entries)))
1292 if self.copies:
1292 if self.copies:
1293 for s, d in self.copies:
1293 for s, d in self.copies:
1294 self._copyfile(s, d)
1294 self._copyfile(s, d)
1295 self.copies = []
1295 self.copies = []
1296 if self.delete:
1296 if self.delete:
1297 self.xargs(self.delete, 'delete')
1297 self.xargs(self.delete, 'delete')
1298 for f in self.delete:
1298 for f in self.delete:
1299 self.manifest.remove(f)
1299 self.manifest.remove(f)
1300 self.delete = []
1300 self.delete = []
1301 entries.update(self.add_files(files.difference(entries)))
1301 entries.update(self.add_files(files.difference(entries)))
1302 if self.delexec:
1302 if self.delexec:
1303 self.xargs(self.delexec, 'propdel', 'svn:executable')
1303 self.xargs(self.delexec, 'propdel', 'svn:executable')
1304 self.delexec = []
1304 self.delexec = []
1305 if self.setexec:
1305 if self.setexec:
1306 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1306 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1307 self.setexec = []
1307 self.setexec = []
1308
1308
1309 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1309 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1310 fp = os.fdopen(fd, 'w')
1310 fp = os.fdopen(fd, 'w')
1311 fp.write(commit.desc)
1311 fp.write(commit.desc)
1312 fp.close()
1312 fp.close()
1313 try:
1313 try:
1314 output = self.run0('commit',
1314 output = self.run0('commit',
1315 username=util.shortuser(commit.author),
1315 username=util.shortuser(commit.author),
1316 file=messagefile,
1316 file=messagefile,
1317 encoding='utf-8')
1317 encoding='utf-8')
1318 try:
1318 try:
1319 rev = self.commit_re.search(output).group(1)
1319 rev = self.commit_re.search(output).group(1)
1320 except AttributeError:
1320 except AttributeError:
1321 if parents and not files:
1321 if parents and not files:
1322 return parents[0]
1322 return parents[0]
1323 self.ui.warn(_('unexpected svn output:\n'))
1323 self.ui.warn(_('unexpected svn output:\n'))
1324 self.ui.warn(output)
1324 self.ui.warn(output)
1325 raise error.Abort(_('unable to cope with svn output'))
1325 raise error.Abort(_('unable to cope with svn output'))
1326 if commit.rev:
1326 if commit.rev:
1327 self.run('propset', 'hg:convert-rev', commit.rev,
1327 self.run('propset', 'hg:convert-rev', commit.rev,
1328 revprop=True, revision=rev)
1328 revprop=True, revision=rev)
1329 if commit.branch and commit.branch != 'default':
1329 if commit.branch and commit.branch != 'default':
1330 self.run('propset', 'hg:convert-branch', commit.branch,
1330 self.run('propset', 'hg:convert-branch', commit.branch,
1331 revprop=True, revision=rev)
1331 revprop=True, revision=rev)
1332 for parent in parents:
1332 for parent in parents:
1333 self.addchild(parent, rev)
1333 self.addchild(parent, rev)
1334 return self.revid(rev)
1334 return self.revid(rev)
1335 finally:
1335 finally:
1336 os.unlink(messagefile)
1336 os.unlink(messagefile)
1337
1337
1338 def puttags(self, tags):
1338 def puttags(self, tags):
1339 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1339 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1340 return None, None
1340 return None, None
1341
1341
1342 def hascommitfrommap(self, rev):
1342 def hascommitfrommap(self, rev):
1343 # We trust that revisions referenced in a map still is present
1343 # We trust that revisions referenced in a map still is present
1344 # TODO: implement something better if necessary and feasible
1344 # TODO: implement something better if necessary and feasible
1345 return True
1345 return True
1346
1346
1347 def hascommitforsplicemap(self, rev):
1347 def hascommitforsplicemap(self, rev):
1348 # This is not correct as one can convert to an existing subversion
1348 # This is not correct as one can convert to an existing subversion
1349 # repository and childmap would not list all revisions. Too bad.
1349 # repository and childmap would not list all revisions. Too bad.
1350 if rev in self.childmap:
1350 if rev in self.childmap:
1351 return True
1351 return True
1352 raise error.Abort(_('splice map revision %s not found in subversion '
1352 raise error.Abort(_('splice map revision %s not found in subversion '
1353 'child map (revision lookups are not implemented)')
1353 'child map (revision lookups are not implemented)')
1354 % rev)
1354 % rev)
@@ -1,178 +1,180 b''
1 # Copyright 2011 Fog Creek Software
1 # Copyright 2011 Fog Creek Software
2 #
2 #
3 # This software may be used and distributed according to the terms of the
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
4 # GNU General Public License version 2 or any later version.
5
5
6 import os
6 import os
7 import urllib2
8 import re
7 import re
9
8
10 from mercurial import error, httppeer, util, wireproto
9 from mercurial import error, httppeer, util, wireproto
11 from mercurial.i18n import _
10 from mercurial.i18n import _
12
11
12 urlerr = util.urlerr
13 urlreq = util.urlreq
14
13 import lfutil
15 import lfutil
14
16
15 LARGEFILES_REQUIRED_MSG = ('\nThis repository uses the largefiles extension.'
17 LARGEFILES_REQUIRED_MSG = ('\nThis repository uses the largefiles extension.'
16 '\n\nPlease enable it in your Mercurial config '
18 '\n\nPlease enable it in your Mercurial config '
17 'file.\n')
19 'file.\n')
18
20
19 # these will all be replaced by largefiles.uisetup
21 # these will all be replaced by largefiles.uisetup
20 capabilitiesorig = None
22 capabilitiesorig = None
21 ssholdcallstream = None
23 ssholdcallstream = None
22 httpoldcallstream = None
24 httpoldcallstream = None
23
25
24 def putlfile(repo, proto, sha):
26 def putlfile(repo, proto, sha):
25 '''Server command for putting a largefile into a repository's local store
27 '''Server command for putting a largefile into a repository's local store
26 and into the user cache.'''
28 and into the user cache.'''
27 proto.redirect()
29 proto.redirect()
28
30
29 path = lfutil.storepath(repo, sha)
31 path = lfutil.storepath(repo, sha)
30 util.makedirs(os.path.dirname(path))
32 util.makedirs(os.path.dirname(path))
31 tmpfp = util.atomictempfile(path, createmode=repo.store.createmode)
33 tmpfp = util.atomictempfile(path, createmode=repo.store.createmode)
32
34
33 try:
35 try:
34 proto.getfile(tmpfp)
36 proto.getfile(tmpfp)
35 tmpfp._fp.seek(0)
37 tmpfp._fp.seek(0)
36 if sha != lfutil.hexsha1(tmpfp._fp):
38 if sha != lfutil.hexsha1(tmpfp._fp):
37 raise IOError(0, _('largefile contents do not match hash'))
39 raise IOError(0, _('largefile contents do not match hash'))
38 tmpfp.close()
40 tmpfp.close()
39 lfutil.linktousercache(repo, sha)
41 lfutil.linktousercache(repo, sha)
40 except IOError as e:
42 except IOError as e:
41 repo.ui.warn(_('largefiles: failed to put %s into store: %s\n') %
43 repo.ui.warn(_('largefiles: failed to put %s into store: %s\n') %
42 (sha, e.strerror))
44 (sha, e.strerror))
43 return wireproto.pushres(1)
45 return wireproto.pushres(1)
44 finally:
46 finally:
45 tmpfp.discard()
47 tmpfp.discard()
46
48
47 return wireproto.pushres(0)
49 return wireproto.pushres(0)
48
50
49 def getlfile(repo, proto, sha):
51 def getlfile(repo, proto, sha):
50 '''Server command for retrieving a largefile from the repository-local
52 '''Server command for retrieving a largefile from the repository-local
51 cache or user cache.'''
53 cache or user cache.'''
52 filename = lfutil.findfile(repo, sha)
54 filename = lfutil.findfile(repo, sha)
53 if not filename:
55 if not filename:
54 raise error.Abort(_('requested largefile %s not present in cache')
56 raise error.Abort(_('requested largefile %s not present in cache')
55 % sha)
57 % sha)
56 f = open(filename, 'rb')
58 f = open(filename, 'rb')
57 length = os.fstat(f.fileno())[6]
59 length = os.fstat(f.fileno())[6]
58
60
59 # Since we can't set an HTTP content-length header here, and
61 # Since we can't set an HTTP content-length header here, and
60 # Mercurial core provides no way to give the length of a streamres
62 # Mercurial core provides no way to give the length of a streamres
61 # (and reading the entire file into RAM would be ill-advised), we
63 # (and reading the entire file into RAM would be ill-advised), we
62 # just send the length on the first line of the response, like the
64 # just send the length on the first line of the response, like the
63 # ssh proto does for string responses.
65 # ssh proto does for string responses.
64 def generator():
66 def generator():
65 yield '%d\n' % length
67 yield '%d\n' % length
66 for chunk in util.filechunkiter(f):
68 for chunk in util.filechunkiter(f):
67 yield chunk
69 yield chunk
68 return wireproto.streamres(generator())
70 return wireproto.streamres(generator())
69
71
70 def statlfile(repo, proto, sha):
72 def statlfile(repo, proto, sha):
71 '''Server command for checking if a largefile is present - returns '2\n' if
73 '''Server command for checking if a largefile is present - returns '2\n' if
72 the largefile is missing, '0\n' if it seems to be in good condition.
74 the largefile is missing, '0\n' if it seems to be in good condition.
73
75
74 The value 1 is reserved for mismatched checksum, but that is too expensive
76 The value 1 is reserved for mismatched checksum, but that is too expensive
75 to be verified on every stat and must be caught be running 'hg verify'
77 to be verified on every stat and must be caught be running 'hg verify'
76 server side.'''
78 server side.'''
77 filename = lfutil.findfile(repo, sha)
79 filename = lfutil.findfile(repo, sha)
78 if not filename:
80 if not filename:
79 return '2\n'
81 return '2\n'
80 return '0\n'
82 return '0\n'
81
83
82 def wirereposetup(ui, repo):
84 def wirereposetup(ui, repo):
83 class lfileswirerepository(repo.__class__):
85 class lfileswirerepository(repo.__class__):
84 def putlfile(self, sha, fd):
86 def putlfile(self, sha, fd):
85 # unfortunately, httprepository._callpush tries to convert its
87 # unfortunately, httprepository._callpush tries to convert its
86 # input file-like into a bundle before sending it, so we can't use
88 # input file-like into a bundle before sending it, so we can't use
87 # it ...
89 # it ...
88 if issubclass(self.__class__, httppeer.httppeer):
90 if issubclass(self.__class__, httppeer.httppeer):
89 res = self._call('putlfile', data=fd, sha=sha,
91 res = self._call('putlfile', data=fd, sha=sha,
90 headers={'content-type':'application/mercurial-0.1'})
92 headers={'content-type':'application/mercurial-0.1'})
91 try:
93 try:
92 d, output = res.split('\n', 1)
94 d, output = res.split('\n', 1)
93 for l in output.splitlines(True):
95 for l in output.splitlines(True):
94 self.ui.warn(_('remote: '), l) # assume l ends with \n
96 self.ui.warn(_('remote: '), l) # assume l ends with \n
95 return int(d)
97 return int(d)
96 except ValueError:
98 except ValueError:
97 self.ui.warn(_('unexpected putlfile response: %r\n') % res)
99 self.ui.warn(_('unexpected putlfile response: %r\n') % res)
98 return 1
100 return 1
99 # ... but we can't use sshrepository._call because the data=
101 # ... but we can't use sshrepository._call because the data=
100 # argument won't get sent, and _callpush does exactly what we want
102 # argument won't get sent, and _callpush does exactly what we want
101 # in this case: send the data straight through
103 # in this case: send the data straight through
102 else:
104 else:
103 try:
105 try:
104 ret, output = self._callpush("putlfile", fd, sha=sha)
106 ret, output = self._callpush("putlfile", fd, sha=sha)
105 if ret == "":
107 if ret == "":
106 raise error.ResponseError(_('putlfile failed:'),
108 raise error.ResponseError(_('putlfile failed:'),
107 output)
109 output)
108 return int(ret)
110 return int(ret)
109 except IOError:
111 except IOError:
110 return 1
112 return 1
111 except ValueError:
113 except ValueError:
112 raise error.ResponseError(
114 raise error.ResponseError(
113 _('putlfile failed (unexpected response):'), ret)
115 _('putlfile failed (unexpected response):'), ret)
114
116
115 def getlfile(self, sha):
117 def getlfile(self, sha):
116 """returns an iterable with the chunks of the file with sha sha"""
118 """returns an iterable with the chunks of the file with sha sha"""
117 stream = self._callstream("getlfile", sha=sha)
119 stream = self._callstream("getlfile", sha=sha)
118 length = stream.readline()
120 length = stream.readline()
119 try:
121 try:
120 length = int(length)
122 length = int(length)
121 except ValueError:
123 except ValueError:
122 self._abort(error.ResponseError(_("unexpected response:"),
124 self._abort(error.ResponseError(_("unexpected response:"),
123 length))
125 length))
124
126
125 # SSH streams will block if reading more than length
127 # SSH streams will block if reading more than length
126 for chunk in util.filechunkiter(stream, 128 * 1024, length):
128 for chunk in util.filechunkiter(stream, 128 * 1024, length):
127 yield chunk
129 yield chunk
128 # HTTP streams must hit the end to process the last empty
130 # HTTP streams must hit the end to process the last empty
129 # chunk of Chunked-Encoding so the connection can be reused.
131 # chunk of Chunked-Encoding so the connection can be reused.
130 if issubclass(self.__class__, httppeer.httppeer):
132 if issubclass(self.__class__, httppeer.httppeer):
131 chunk = stream.read(1)
133 chunk = stream.read(1)
132 if chunk:
134 if chunk:
133 self._abort(error.ResponseError(_("unexpected response:"),
135 self._abort(error.ResponseError(_("unexpected response:"),
134 chunk))
136 chunk))
135
137
136 @wireproto.batchable
138 @wireproto.batchable
137 def statlfile(self, sha):
139 def statlfile(self, sha):
138 f = wireproto.future()
140 f = wireproto.future()
139 result = {'sha': sha}
141 result = {'sha': sha}
140 yield result, f
142 yield result, f
141 try:
143 try:
142 yield int(f.value)
144 yield int(f.value)
143 except (ValueError, urllib2.HTTPError):
145 except (ValueError, urlerr.httperror):
144 # If the server returns anything but an integer followed by a
146 # If the server returns anything but an integer followed by a
145 # newline, newline, it's not speaking our language; if we get
147 # newline, newline, it's not speaking our language; if we get
146 # an HTTP error, we can't be sure the largefile is present;
148 # an HTTP error, we can't be sure the largefile is present;
147 # either way, consider it missing.
149 # either way, consider it missing.
148 yield 2
150 yield 2
149
151
150 repo.__class__ = lfileswirerepository
152 repo.__class__ = lfileswirerepository
151
153
152 # advertise the largefiles=serve capability
154 # advertise the largefiles=serve capability
153 def capabilities(repo, proto):
155 def capabilities(repo, proto):
154 '''Wrap server command to announce largefile server capability'''
156 '''Wrap server command to announce largefile server capability'''
155 return capabilitiesorig(repo, proto) + ' largefiles=serve'
157 return capabilitiesorig(repo, proto) + ' largefiles=serve'
156
158
157 def heads(repo, proto):
159 def heads(repo, proto):
158 '''Wrap server command - largefile capable clients will know to call
160 '''Wrap server command - largefile capable clients will know to call
159 lheads instead'''
161 lheads instead'''
160 if lfutil.islfilesrepo(repo):
162 if lfutil.islfilesrepo(repo):
161 return wireproto.ooberror(LARGEFILES_REQUIRED_MSG)
163 return wireproto.ooberror(LARGEFILES_REQUIRED_MSG)
162 return wireproto.heads(repo, proto)
164 return wireproto.heads(repo, proto)
163
165
164 def sshrepocallstream(self, cmd, **args):
166 def sshrepocallstream(self, cmd, **args):
165 if cmd == 'heads' and self.capable('largefiles'):
167 if cmd == 'heads' and self.capable('largefiles'):
166 cmd = 'lheads'
168 cmd = 'lheads'
167 if cmd == 'batch' and self.capable('largefiles'):
169 if cmd == 'batch' and self.capable('largefiles'):
168 args['cmds'] = args['cmds'].replace('heads ', 'lheads ')
170 args['cmds'] = args['cmds'].replace('heads ', 'lheads ')
169 return ssholdcallstream(self, cmd, **args)
171 return ssholdcallstream(self, cmd, **args)
170
172
171 headsre = re.compile(r'(^|;)heads\b')
173 headsre = re.compile(r'(^|;)heads\b')
172
174
173 def httprepocallstream(self, cmd, **args):
175 def httprepocallstream(self, cmd, **args):
174 if cmd == 'heads' and self.capable('largefiles'):
176 if cmd == 'heads' and self.capable('largefiles'):
175 cmd = 'lheads'
177 cmd = 'lheads'
176 if cmd == 'batch' and self.capable('largefiles'):
178 if cmd == 'batch' and self.capable('largefiles'):
177 args['cmds'] = headsre.sub('lheads', args['cmds'])
179 args['cmds'] = headsre.sub('lheads', args['cmds'])
178 return httpoldcallstream(self, cmd, **args)
180 return httpoldcallstream(self, cmd, **args)
@@ -1,113 +1,114 b''
1 # Copyright 2010-2011 Fog Creek Software
1 # Copyright 2010-2011 Fog Creek Software
2 # Copyright 2010-2011 Unity Technologies
2 # Copyright 2010-2011 Unity Technologies
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6
6
7 '''remote largefile store; the base class for wirestore'''
7 '''remote largefile store; the base class for wirestore'''
8
8
9 import urllib2
10
11 from mercurial import util, wireproto, error
9 from mercurial import util, wireproto, error
12 from mercurial.i18n import _
10 from mercurial.i18n import _
13
11
12 urlerr = util.urlerr
13 urlreq = util.urlreq
14
14 import lfutil
15 import lfutil
15 import basestore
16 import basestore
16
17
17 class remotestore(basestore.basestore):
18 class remotestore(basestore.basestore):
18 '''a largefile store accessed over a network'''
19 '''a largefile store accessed over a network'''
19 def __init__(self, ui, repo, url):
20 def __init__(self, ui, repo, url):
20 super(remotestore, self).__init__(ui, repo, url)
21 super(remotestore, self).__init__(ui, repo, url)
21
22
22 def put(self, source, hash):
23 def put(self, source, hash):
23 if self.sendfile(source, hash):
24 if self.sendfile(source, hash):
24 raise error.Abort(
25 raise error.Abort(
25 _('remotestore: could not put %s to remote store %s')
26 _('remotestore: could not put %s to remote store %s')
26 % (source, util.hidepassword(self.url)))
27 % (source, util.hidepassword(self.url)))
27 self.ui.debug(
28 self.ui.debug(
28 _('remotestore: put %s to remote store %s\n')
29 _('remotestore: put %s to remote store %s\n')
29 % (source, util.hidepassword(self.url)))
30 % (source, util.hidepassword(self.url)))
30
31
31 def exists(self, hashes):
32 def exists(self, hashes):
32 return dict((h, s == 0) for (h, s) in # dict-from-generator
33 return dict((h, s == 0) for (h, s) in # dict-from-generator
33 self._stat(hashes).iteritems())
34 self._stat(hashes).iteritems())
34
35
35 def sendfile(self, filename, hash):
36 def sendfile(self, filename, hash):
36 self.ui.debug('remotestore: sendfile(%s, %s)\n' % (filename, hash))
37 self.ui.debug('remotestore: sendfile(%s, %s)\n' % (filename, hash))
37 fd = None
38 fd = None
38 try:
39 try:
39 fd = lfutil.httpsendfile(self.ui, filename)
40 fd = lfutil.httpsendfile(self.ui, filename)
40 return self._put(hash, fd)
41 return self._put(hash, fd)
41 except IOError as e:
42 except IOError as e:
42 raise error.Abort(
43 raise error.Abort(
43 _('remotestore: could not open file %s: %s')
44 _('remotestore: could not open file %s: %s')
44 % (filename, str(e)))
45 % (filename, str(e)))
45 finally:
46 finally:
46 if fd:
47 if fd:
47 fd.close()
48 fd.close()
48
49
49 def _getfile(self, tmpfile, filename, hash):
50 def _getfile(self, tmpfile, filename, hash):
50 try:
51 try:
51 chunks = self._get(hash)
52 chunks = self._get(hash)
52 except urllib2.HTTPError as e:
53 except urlerr.httperror as e:
53 # 401s get converted to error.Aborts; everything else is fine being
54 # 401s get converted to error.Aborts; everything else is fine being
54 # turned into a StoreError
55 # turned into a StoreError
55 raise basestore.StoreError(filename, hash, self.url, str(e))
56 raise basestore.StoreError(filename, hash, self.url, str(e))
56 except urllib2.URLError as e:
57 except urlerr.urlerror as e:
57 # This usually indicates a connection problem, so don't
58 # This usually indicates a connection problem, so don't
58 # keep trying with the other files... they will probably
59 # keep trying with the other files... they will probably
59 # all fail too.
60 # all fail too.
60 raise error.Abort('%s: %s' %
61 raise error.Abort('%s: %s' %
61 (util.hidepassword(self.url), e.reason))
62 (util.hidepassword(self.url), e.reason))
62 except IOError as e:
63 except IOError as e:
63 raise basestore.StoreError(filename, hash, self.url, str(e))
64 raise basestore.StoreError(filename, hash, self.url, str(e))
64
65
65 return lfutil.copyandhash(chunks, tmpfile)
66 return lfutil.copyandhash(chunks, tmpfile)
66
67
67 def _verifyfile(self, cctx, cset, contents, standin, verified):
68 def _verifyfile(self, cctx, cset, contents, standin, verified):
68 filename = lfutil.splitstandin(standin)
69 filename = lfutil.splitstandin(standin)
69 if not filename:
70 if not filename:
70 return False
71 return False
71 fctx = cctx[standin]
72 fctx = cctx[standin]
72 key = (filename, fctx.filenode())
73 key = (filename, fctx.filenode())
73 if key in verified:
74 if key in verified:
74 return False
75 return False
75
76
76 verified.add(key)
77 verified.add(key)
77
78
78 expecthash = fctx.data()[0:40]
79 expecthash = fctx.data()[0:40]
79 stat = self._stat([expecthash])[expecthash]
80 stat = self._stat([expecthash])[expecthash]
80 if not stat:
81 if not stat:
81 return False
82 return False
82 elif stat == 1:
83 elif stat == 1:
83 self.ui.warn(
84 self.ui.warn(
84 _('changeset %s: %s: contents differ\n')
85 _('changeset %s: %s: contents differ\n')
85 % (cset, filename))
86 % (cset, filename))
86 return True # failed
87 return True # failed
87 elif stat == 2:
88 elif stat == 2:
88 self.ui.warn(
89 self.ui.warn(
89 _('changeset %s: %s missing\n')
90 _('changeset %s: %s missing\n')
90 % (cset, filename))
91 % (cset, filename))
91 return True # failed
92 return True # failed
92 else:
93 else:
93 raise RuntimeError('verify failed: unexpected response from '
94 raise RuntimeError('verify failed: unexpected response from '
94 'statlfile (%r)' % stat)
95 'statlfile (%r)' % stat)
95
96
96 def batch(self):
97 def batch(self):
97 '''Support for remote batching.'''
98 '''Support for remote batching.'''
98 return wireproto.remotebatch(self)
99 return wireproto.remotebatch(self)
99
100
100 def _put(self, hash, fd):
101 def _put(self, hash, fd):
101 '''Put file with the given hash in the remote store.'''
102 '''Put file with the given hash in the remote store.'''
102 raise NotImplementedError('abstract method')
103 raise NotImplementedError('abstract method')
103
104
104 def _get(self, hash):
105 def _get(self, hash):
105 '''Get file with the given hash from the remote store.'''
106 '''Get file with the given hash from the remote store.'''
106 raise NotImplementedError('abstract method')
107 raise NotImplementedError('abstract method')
107
108
108 def _stat(self, hashes):
109 def _stat(self, hashes):
109 '''Get information about availability of files specified by
110 '''Get information about availability of files specified by
110 hashes in the remote store. Return dictionary mapping hashes
111 hashes in the remote store. Return dictionary mapping hashes
111 to return code where 0 means that file is available, other
112 to return code where 0 means that file is available, other
112 values if not.'''
113 values if not.'''
113 raise NotImplementedError('abstract method')
114 raise NotImplementedError('abstract method')
@@ -1,1606 +1,1608 b''
1 # bundle2.py - generic container format to transmit arbitrary data.
1 # bundle2.py - generic container format to transmit arbitrary data.
2 #
2 #
3 # Copyright 2013 Facebook, Inc.
3 # Copyright 2013 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """Handling of the new bundle2 format
7 """Handling of the new bundle2 format
8
8
9 The goal of bundle2 is to act as an atomically packet to transmit a set of
9 The goal of bundle2 is to act as an atomically packet to transmit a set of
10 payloads in an application agnostic way. It consist in a sequence of "parts"
10 payloads in an application agnostic way. It consist in a sequence of "parts"
11 that will be handed to and processed by the application layer.
11 that will be handed to and processed by the application layer.
12
12
13
13
14 General format architecture
14 General format architecture
15 ===========================
15 ===========================
16
16
17 The format is architectured as follow
17 The format is architectured as follow
18
18
19 - magic string
19 - magic string
20 - stream level parameters
20 - stream level parameters
21 - payload parts (any number)
21 - payload parts (any number)
22 - end of stream marker.
22 - end of stream marker.
23
23
24 the Binary format
24 the Binary format
25 ============================
25 ============================
26
26
27 All numbers are unsigned and big-endian.
27 All numbers are unsigned and big-endian.
28
28
29 stream level parameters
29 stream level parameters
30 ------------------------
30 ------------------------
31
31
32 Binary format is as follow
32 Binary format is as follow
33
33
34 :params size: int32
34 :params size: int32
35
35
36 The total number of Bytes used by the parameters
36 The total number of Bytes used by the parameters
37
37
38 :params value: arbitrary number of Bytes
38 :params value: arbitrary number of Bytes
39
39
40 A blob of `params size` containing the serialized version of all stream level
40 A blob of `params size` containing the serialized version of all stream level
41 parameters.
41 parameters.
42
42
43 The blob contains a space separated list of parameters. Parameters with value
43 The blob contains a space separated list of parameters. Parameters with value
44 are stored in the form `<name>=<value>`. Both name and value are urlquoted.
44 are stored in the form `<name>=<value>`. Both name and value are urlquoted.
45
45
46 Empty name are obviously forbidden.
46 Empty name are obviously forbidden.
47
47
48 Name MUST start with a letter. If this first letter is lower case, the
48 Name MUST start with a letter. If this first letter is lower case, the
49 parameter is advisory and can be safely ignored. However when the first
49 parameter is advisory and can be safely ignored. However when the first
50 letter is capital, the parameter is mandatory and the bundling process MUST
50 letter is capital, the parameter is mandatory and the bundling process MUST
51 stop if he is not able to proceed it.
51 stop if he is not able to proceed it.
52
52
53 Stream parameters use a simple textual format for two main reasons:
53 Stream parameters use a simple textual format for two main reasons:
54
54
55 - Stream level parameters should remain simple and we want to discourage any
55 - Stream level parameters should remain simple and we want to discourage any
56 crazy usage.
56 crazy usage.
57 - Textual data allow easy human inspection of a bundle2 header in case of
57 - Textual data allow easy human inspection of a bundle2 header in case of
58 troubles.
58 troubles.
59
59
60 Any Applicative level options MUST go into a bundle2 part instead.
60 Any Applicative level options MUST go into a bundle2 part instead.
61
61
62 Payload part
62 Payload part
63 ------------------------
63 ------------------------
64
64
65 Binary format is as follow
65 Binary format is as follow
66
66
67 :header size: int32
67 :header size: int32
68
68
69 The total number of Bytes used by the part header. When the header is empty
69 The total number of Bytes used by the part header. When the header is empty
70 (size = 0) this is interpreted as the end of stream marker.
70 (size = 0) this is interpreted as the end of stream marker.
71
71
72 :header:
72 :header:
73
73
74 The header defines how to interpret the part. It contains two piece of
74 The header defines how to interpret the part. It contains two piece of
75 data: the part type, and the part parameters.
75 data: the part type, and the part parameters.
76
76
77 The part type is used to route an application level handler, that can
77 The part type is used to route an application level handler, that can
78 interpret payload.
78 interpret payload.
79
79
80 Part parameters are passed to the application level handler. They are
80 Part parameters are passed to the application level handler. They are
81 meant to convey information that will help the application level object to
81 meant to convey information that will help the application level object to
82 interpret the part payload.
82 interpret the part payload.
83
83
84 The binary format of the header is has follow
84 The binary format of the header is has follow
85
85
86 :typesize: (one byte)
86 :typesize: (one byte)
87
87
88 :parttype: alphanumerical part name (restricted to [a-zA-Z0-9_:-]*)
88 :parttype: alphanumerical part name (restricted to [a-zA-Z0-9_:-]*)
89
89
90 :partid: A 32bits integer (unique in the bundle) that can be used to refer
90 :partid: A 32bits integer (unique in the bundle) that can be used to refer
91 to this part.
91 to this part.
92
92
93 :parameters:
93 :parameters:
94
94
95 Part's parameter may have arbitrary content, the binary structure is::
95 Part's parameter may have arbitrary content, the binary structure is::
96
96
97 <mandatory-count><advisory-count><param-sizes><param-data>
97 <mandatory-count><advisory-count><param-sizes><param-data>
98
98
99 :mandatory-count: 1 byte, number of mandatory parameters
99 :mandatory-count: 1 byte, number of mandatory parameters
100
100
101 :advisory-count: 1 byte, number of advisory parameters
101 :advisory-count: 1 byte, number of advisory parameters
102
102
103 :param-sizes:
103 :param-sizes:
104
104
105 N couple of bytes, where N is the total number of parameters. Each
105 N couple of bytes, where N is the total number of parameters. Each
106 couple contains (<size-of-key>, <size-of-value) for one parameter.
106 couple contains (<size-of-key>, <size-of-value) for one parameter.
107
107
108 :param-data:
108 :param-data:
109
109
110 A blob of bytes from which each parameter key and value can be
110 A blob of bytes from which each parameter key and value can be
111 retrieved using the list of size couples stored in the previous
111 retrieved using the list of size couples stored in the previous
112 field.
112 field.
113
113
114 Mandatory parameters comes first, then the advisory ones.
114 Mandatory parameters comes first, then the advisory ones.
115
115
116 Each parameter's key MUST be unique within the part.
116 Each parameter's key MUST be unique within the part.
117
117
118 :payload:
118 :payload:
119
119
120 payload is a series of `<chunksize><chunkdata>`.
120 payload is a series of `<chunksize><chunkdata>`.
121
121
122 `chunksize` is an int32, `chunkdata` are plain bytes (as much as
122 `chunksize` is an int32, `chunkdata` are plain bytes (as much as
123 `chunksize` says)` The payload part is concluded by a zero size chunk.
123 `chunksize` says)` The payload part is concluded by a zero size chunk.
124
124
125 The current implementation always produces either zero or one chunk.
125 The current implementation always produces either zero or one chunk.
126 This is an implementation limitation that will ultimately be lifted.
126 This is an implementation limitation that will ultimately be lifted.
127
127
128 `chunksize` can be negative to trigger special case processing. No such
128 `chunksize` can be negative to trigger special case processing. No such
129 processing is in place yet.
129 processing is in place yet.
130
130
131 Bundle processing
131 Bundle processing
132 ============================
132 ============================
133
133
134 Each part is processed in order using a "part handler". Handler are registered
134 Each part is processed in order using a "part handler". Handler are registered
135 for a certain part type.
135 for a certain part type.
136
136
137 The matching of a part to its handler is case insensitive. The case of the
137 The matching of a part to its handler is case insensitive. The case of the
138 part type is used to know if a part is mandatory or advisory. If the Part type
138 part type is used to know if a part is mandatory or advisory. If the Part type
139 contains any uppercase char it is considered mandatory. When no handler is
139 contains any uppercase char it is considered mandatory. When no handler is
140 known for a Mandatory part, the process is aborted and an exception is raised.
140 known for a Mandatory part, the process is aborted and an exception is raised.
141 If the part is advisory and no handler is known, the part is ignored. When the
141 If the part is advisory and no handler is known, the part is ignored. When the
142 process is aborted, the full bundle is still read from the stream to keep the
142 process is aborted, the full bundle is still read from the stream to keep the
143 channel usable. But none of the part read from an abort are processed. In the
143 channel usable. But none of the part read from an abort are processed. In the
144 future, dropping the stream may become an option for channel we do not care to
144 future, dropping the stream may become an option for channel we do not care to
145 preserve.
145 preserve.
146 """
146 """
147
147
148 from __future__ import absolute_import
148 from __future__ import absolute_import
149
149
150 import errno
150 import errno
151 import re
151 import re
152 import string
152 import string
153 import struct
153 import struct
154 import sys
154 import sys
155 import urllib
156
155
157 from .i18n import _
156 from .i18n import _
158 from . import (
157 from . import (
159 changegroup,
158 changegroup,
160 error,
159 error,
161 obsolete,
160 obsolete,
162 pushkey,
161 pushkey,
163 tags,
162 tags,
164 url,
163 url,
165 util,
164 util,
166 )
165 )
167
166
167 urlerr = util.urlerr
168 urlreq = util.urlreq
169
168 _pack = struct.pack
170 _pack = struct.pack
169 _unpack = struct.unpack
171 _unpack = struct.unpack
170
172
171 _fstreamparamsize = '>i'
173 _fstreamparamsize = '>i'
172 _fpartheadersize = '>i'
174 _fpartheadersize = '>i'
173 _fparttypesize = '>B'
175 _fparttypesize = '>B'
174 _fpartid = '>I'
176 _fpartid = '>I'
175 _fpayloadsize = '>i'
177 _fpayloadsize = '>i'
176 _fpartparamcount = '>BB'
178 _fpartparamcount = '>BB'
177
179
178 preferedchunksize = 4096
180 preferedchunksize = 4096
179
181
180 _parttypeforbidden = re.compile('[^a-zA-Z0-9_:-]')
182 _parttypeforbidden = re.compile('[^a-zA-Z0-9_:-]')
181
183
182 def outdebug(ui, message):
184 def outdebug(ui, message):
183 """debug regarding output stream (bundling)"""
185 """debug regarding output stream (bundling)"""
184 if ui.configbool('devel', 'bundle2.debug', False):
186 if ui.configbool('devel', 'bundle2.debug', False):
185 ui.debug('bundle2-output: %s\n' % message)
187 ui.debug('bundle2-output: %s\n' % message)
186
188
187 def indebug(ui, message):
189 def indebug(ui, message):
188 """debug on input stream (unbundling)"""
190 """debug on input stream (unbundling)"""
189 if ui.configbool('devel', 'bundle2.debug', False):
191 if ui.configbool('devel', 'bundle2.debug', False):
190 ui.debug('bundle2-input: %s\n' % message)
192 ui.debug('bundle2-input: %s\n' % message)
191
193
192 def validateparttype(parttype):
194 def validateparttype(parttype):
193 """raise ValueError if a parttype contains invalid character"""
195 """raise ValueError if a parttype contains invalid character"""
194 if _parttypeforbidden.search(parttype):
196 if _parttypeforbidden.search(parttype):
195 raise ValueError(parttype)
197 raise ValueError(parttype)
196
198
197 def _makefpartparamsizes(nbparams):
199 def _makefpartparamsizes(nbparams):
198 """return a struct format to read part parameter sizes
200 """return a struct format to read part parameter sizes
199
201
200 The number parameters is variable so we need to build that format
202 The number parameters is variable so we need to build that format
201 dynamically.
203 dynamically.
202 """
204 """
203 return '>'+('BB'*nbparams)
205 return '>'+('BB'*nbparams)
204
206
205 parthandlermapping = {}
207 parthandlermapping = {}
206
208
207 def parthandler(parttype, params=()):
209 def parthandler(parttype, params=()):
208 """decorator that register a function as a bundle2 part handler
210 """decorator that register a function as a bundle2 part handler
209
211
210 eg::
212 eg::
211
213
212 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
214 @parthandler('myparttype', ('mandatory', 'param', 'handled'))
213 def myparttypehandler(...):
215 def myparttypehandler(...):
214 '''process a part of type "my part".'''
216 '''process a part of type "my part".'''
215 ...
217 ...
216 """
218 """
217 validateparttype(parttype)
219 validateparttype(parttype)
218 def _decorator(func):
220 def _decorator(func):
219 lparttype = parttype.lower() # enforce lower case matching.
221 lparttype = parttype.lower() # enforce lower case matching.
220 assert lparttype not in parthandlermapping
222 assert lparttype not in parthandlermapping
221 parthandlermapping[lparttype] = func
223 parthandlermapping[lparttype] = func
222 func.params = frozenset(params)
224 func.params = frozenset(params)
223 return func
225 return func
224 return _decorator
226 return _decorator
225
227
226 class unbundlerecords(object):
228 class unbundlerecords(object):
227 """keep record of what happens during and unbundle
229 """keep record of what happens during and unbundle
228
230
229 New records are added using `records.add('cat', obj)`. Where 'cat' is a
231 New records are added using `records.add('cat', obj)`. Where 'cat' is a
230 category of record and obj is an arbitrary object.
232 category of record and obj is an arbitrary object.
231
233
232 `records['cat']` will return all entries of this category 'cat'.
234 `records['cat']` will return all entries of this category 'cat'.
233
235
234 Iterating on the object itself will yield `('category', obj)` tuples
236 Iterating on the object itself will yield `('category', obj)` tuples
235 for all entries.
237 for all entries.
236
238
237 All iterations happens in chronological order.
239 All iterations happens in chronological order.
238 """
240 """
239
241
240 def __init__(self):
242 def __init__(self):
241 self._categories = {}
243 self._categories = {}
242 self._sequences = []
244 self._sequences = []
243 self._replies = {}
245 self._replies = {}
244
246
245 def add(self, category, entry, inreplyto=None):
247 def add(self, category, entry, inreplyto=None):
246 """add a new record of a given category.
248 """add a new record of a given category.
247
249
248 The entry can then be retrieved in the list returned by
250 The entry can then be retrieved in the list returned by
249 self['category']."""
251 self['category']."""
250 self._categories.setdefault(category, []).append(entry)
252 self._categories.setdefault(category, []).append(entry)
251 self._sequences.append((category, entry))
253 self._sequences.append((category, entry))
252 if inreplyto is not None:
254 if inreplyto is not None:
253 self.getreplies(inreplyto).add(category, entry)
255 self.getreplies(inreplyto).add(category, entry)
254
256
255 def getreplies(self, partid):
257 def getreplies(self, partid):
256 """get the records that are replies to a specific part"""
258 """get the records that are replies to a specific part"""
257 return self._replies.setdefault(partid, unbundlerecords())
259 return self._replies.setdefault(partid, unbundlerecords())
258
260
259 def __getitem__(self, cat):
261 def __getitem__(self, cat):
260 return tuple(self._categories.get(cat, ()))
262 return tuple(self._categories.get(cat, ()))
261
263
262 def __iter__(self):
264 def __iter__(self):
263 return iter(self._sequences)
265 return iter(self._sequences)
264
266
265 def __len__(self):
267 def __len__(self):
266 return len(self._sequences)
268 return len(self._sequences)
267
269
268 def __nonzero__(self):
270 def __nonzero__(self):
269 return bool(self._sequences)
271 return bool(self._sequences)
270
272
271 class bundleoperation(object):
273 class bundleoperation(object):
272 """an object that represents a single bundling process
274 """an object that represents a single bundling process
273
275
274 Its purpose is to carry unbundle-related objects and states.
276 Its purpose is to carry unbundle-related objects and states.
275
277
276 A new object should be created at the beginning of each bundle processing.
278 A new object should be created at the beginning of each bundle processing.
277 The object is to be returned by the processing function.
279 The object is to be returned by the processing function.
278
280
279 The object has very little content now it will ultimately contain:
281 The object has very little content now it will ultimately contain:
280 * an access to the repo the bundle is applied to,
282 * an access to the repo the bundle is applied to,
281 * a ui object,
283 * a ui object,
282 * a way to retrieve a transaction to add changes to the repo,
284 * a way to retrieve a transaction to add changes to the repo,
283 * a way to record the result of processing each part,
285 * a way to record the result of processing each part,
284 * a way to construct a bundle response when applicable.
286 * a way to construct a bundle response when applicable.
285 """
287 """
286
288
287 def __init__(self, repo, transactiongetter, captureoutput=True):
289 def __init__(self, repo, transactiongetter, captureoutput=True):
288 self.repo = repo
290 self.repo = repo
289 self.ui = repo.ui
291 self.ui = repo.ui
290 self.records = unbundlerecords()
292 self.records = unbundlerecords()
291 self.gettransaction = transactiongetter
293 self.gettransaction = transactiongetter
292 self.reply = None
294 self.reply = None
293 self.captureoutput = captureoutput
295 self.captureoutput = captureoutput
294
296
295 class TransactionUnavailable(RuntimeError):
297 class TransactionUnavailable(RuntimeError):
296 pass
298 pass
297
299
298 def _notransaction():
300 def _notransaction():
299 """default method to get a transaction while processing a bundle
301 """default method to get a transaction while processing a bundle
300
302
301 Raise an exception to highlight the fact that no transaction was expected
303 Raise an exception to highlight the fact that no transaction was expected
302 to be created"""
304 to be created"""
303 raise TransactionUnavailable()
305 raise TransactionUnavailable()
304
306
305 def applybundle(repo, unbundler, tr, source=None, url=None, op=None):
307 def applybundle(repo, unbundler, tr, source=None, url=None, op=None):
306 # transform me into unbundler.apply() as soon as the freeze is lifted
308 # transform me into unbundler.apply() as soon as the freeze is lifted
307 tr.hookargs['bundle2'] = '1'
309 tr.hookargs['bundle2'] = '1'
308 if source is not None and 'source' not in tr.hookargs:
310 if source is not None and 'source' not in tr.hookargs:
309 tr.hookargs['source'] = source
311 tr.hookargs['source'] = source
310 if url is not None and 'url' not in tr.hookargs:
312 if url is not None and 'url' not in tr.hookargs:
311 tr.hookargs['url'] = url
313 tr.hookargs['url'] = url
312 return processbundle(repo, unbundler, lambda: tr, op=op)
314 return processbundle(repo, unbundler, lambda: tr, op=op)
313
315
314 def processbundle(repo, unbundler, transactiongetter=None, op=None):
316 def processbundle(repo, unbundler, transactiongetter=None, op=None):
315 """This function process a bundle, apply effect to/from a repo
317 """This function process a bundle, apply effect to/from a repo
316
318
317 It iterates over each part then searches for and uses the proper handling
319 It iterates over each part then searches for and uses the proper handling
318 code to process the part. Parts are processed in order.
320 code to process the part. Parts are processed in order.
319
321
320 This is very early version of this function that will be strongly reworked
322 This is very early version of this function that will be strongly reworked
321 before final usage.
323 before final usage.
322
324
323 Unknown Mandatory part will abort the process.
325 Unknown Mandatory part will abort the process.
324
326
325 It is temporarily possible to provide a prebuilt bundleoperation to the
327 It is temporarily possible to provide a prebuilt bundleoperation to the
326 function. This is used to ensure output is properly propagated in case of
328 function. This is used to ensure output is properly propagated in case of
327 an error during the unbundling. This output capturing part will likely be
329 an error during the unbundling. This output capturing part will likely be
328 reworked and this ability will probably go away in the process.
330 reworked and this ability will probably go away in the process.
329 """
331 """
330 if op is None:
332 if op is None:
331 if transactiongetter is None:
333 if transactiongetter is None:
332 transactiongetter = _notransaction
334 transactiongetter = _notransaction
333 op = bundleoperation(repo, transactiongetter)
335 op = bundleoperation(repo, transactiongetter)
334 # todo:
336 # todo:
335 # - replace this is a init function soon.
337 # - replace this is a init function soon.
336 # - exception catching
338 # - exception catching
337 unbundler.params
339 unbundler.params
338 if repo.ui.debugflag:
340 if repo.ui.debugflag:
339 msg = ['bundle2-input-bundle:']
341 msg = ['bundle2-input-bundle:']
340 if unbundler.params:
342 if unbundler.params:
341 msg.append(' %i params')
343 msg.append(' %i params')
342 if op.gettransaction is None:
344 if op.gettransaction is None:
343 msg.append(' no-transaction')
345 msg.append(' no-transaction')
344 else:
346 else:
345 msg.append(' with-transaction')
347 msg.append(' with-transaction')
346 msg.append('\n')
348 msg.append('\n')
347 repo.ui.debug(''.join(msg))
349 repo.ui.debug(''.join(msg))
348 iterparts = enumerate(unbundler.iterparts())
350 iterparts = enumerate(unbundler.iterparts())
349 part = None
351 part = None
350 nbpart = 0
352 nbpart = 0
351 try:
353 try:
352 for nbpart, part in iterparts:
354 for nbpart, part in iterparts:
353 _processpart(op, part)
355 _processpart(op, part)
354 except BaseException as exc:
356 except BaseException as exc:
355 for nbpart, part in iterparts:
357 for nbpart, part in iterparts:
356 # consume the bundle content
358 # consume the bundle content
357 part.seek(0, 2)
359 part.seek(0, 2)
358 # Small hack to let caller code distinguish exceptions from bundle2
360 # Small hack to let caller code distinguish exceptions from bundle2
359 # processing from processing the old format. This is mostly
361 # processing from processing the old format. This is mostly
360 # needed to handle different return codes to unbundle according to the
362 # needed to handle different return codes to unbundle according to the
361 # type of bundle. We should probably clean up or drop this return code
363 # type of bundle. We should probably clean up or drop this return code
362 # craziness in a future version.
364 # craziness in a future version.
363 exc.duringunbundle2 = True
365 exc.duringunbundle2 = True
364 salvaged = []
366 salvaged = []
365 replycaps = None
367 replycaps = None
366 if op.reply is not None:
368 if op.reply is not None:
367 salvaged = op.reply.salvageoutput()
369 salvaged = op.reply.salvageoutput()
368 replycaps = op.reply.capabilities
370 replycaps = op.reply.capabilities
369 exc._replycaps = replycaps
371 exc._replycaps = replycaps
370 exc._bundle2salvagedoutput = salvaged
372 exc._bundle2salvagedoutput = salvaged
371 raise
373 raise
372 finally:
374 finally:
373 repo.ui.debug('bundle2-input-bundle: %i parts total\n' % nbpart)
375 repo.ui.debug('bundle2-input-bundle: %i parts total\n' % nbpart)
374
376
375 return op
377 return op
376
378
377 def _processpart(op, part):
379 def _processpart(op, part):
378 """process a single part from a bundle
380 """process a single part from a bundle
379
381
380 The part is guaranteed to have been fully consumed when the function exits
382 The part is guaranteed to have been fully consumed when the function exits
381 (even if an exception is raised)."""
383 (even if an exception is raised)."""
382 status = 'unknown' # used by debug output
384 status = 'unknown' # used by debug output
383 try:
385 try:
384 try:
386 try:
385 handler = parthandlermapping.get(part.type)
387 handler = parthandlermapping.get(part.type)
386 if handler is None:
388 if handler is None:
387 status = 'unsupported-type'
389 status = 'unsupported-type'
388 raise error.BundleUnknownFeatureError(parttype=part.type)
390 raise error.BundleUnknownFeatureError(parttype=part.type)
389 indebug(op.ui, 'found a handler for part %r' % part.type)
391 indebug(op.ui, 'found a handler for part %r' % part.type)
390 unknownparams = part.mandatorykeys - handler.params
392 unknownparams = part.mandatorykeys - handler.params
391 if unknownparams:
393 if unknownparams:
392 unknownparams = list(unknownparams)
394 unknownparams = list(unknownparams)
393 unknownparams.sort()
395 unknownparams.sort()
394 status = 'unsupported-params (%s)' % unknownparams
396 status = 'unsupported-params (%s)' % unknownparams
395 raise error.BundleUnknownFeatureError(parttype=part.type,
397 raise error.BundleUnknownFeatureError(parttype=part.type,
396 params=unknownparams)
398 params=unknownparams)
397 status = 'supported'
399 status = 'supported'
398 except error.BundleUnknownFeatureError as exc:
400 except error.BundleUnknownFeatureError as exc:
399 if part.mandatory: # mandatory parts
401 if part.mandatory: # mandatory parts
400 raise
402 raise
401 indebug(op.ui, 'ignoring unsupported advisory part %s' % exc)
403 indebug(op.ui, 'ignoring unsupported advisory part %s' % exc)
402 return # skip to part processing
404 return # skip to part processing
403 finally:
405 finally:
404 if op.ui.debugflag:
406 if op.ui.debugflag:
405 msg = ['bundle2-input-part: "%s"' % part.type]
407 msg = ['bundle2-input-part: "%s"' % part.type]
406 if not part.mandatory:
408 if not part.mandatory:
407 msg.append(' (advisory)')
409 msg.append(' (advisory)')
408 nbmp = len(part.mandatorykeys)
410 nbmp = len(part.mandatorykeys)
409 nbap = len(part.params) - nbmp
411 nbap = len(part.params) - nbmp
410 if nbmp or nbap:
412 if nbmp or nbap:
411 msg.append(' (params:')
413 msg.append(' (params:')
412 if nbmp:
414 if nbmp:
413 msg.append(' %i mandatory' % nbmp)
415 msg.append(' %i mandatory' % nbmp)
414 if nbap:
416 if nbap:
415 msg.append(' %i advisory' % nbmp)
417 msg.append(' %i advisory' % nbmp)
416 msg.append(')')
418 msg.append(')')
417 msg.append(' %s\n' % status)
419 msg.append(' %s\n' % status)
418 op.ui.debug(''.join(msg))
420 op.ui.debug(''.join(msg))
419
421
420 # handler is called outside the above try block so that we don't
422 # handler is called outside the above try block so that we don't
421 # risk catching KeyErrors from anything other than the
423 # risk catching KeyErrors from anything other than the
422 # parthandlermapping lookup (any KeyError raised by handler()
424 # parthandlermapping lookup (any KeyError raised by handler()
423 # itself represents a defect of a different variety).
425 # itself represents a defect of a different variety).
424 output = None
426 output = None
425 if op.captureoutput and op.reply is not None:
427 if op.captureoutput and op.reply is not None:
426 op.ui.pushbuffer(error=True, subproc=True)
428 op.ui.pushbuffer(error=True, subproc=True)
427 output = ''
429 output = ''
428 try:
430 try:
429 handler(op, part)
431 handler(op, part)
430 finally:
432 finally:
431 if output is not None:
433 if output is not None:
432 output = op.ui.popbuffer()
434 output = op.ui.popbuffer()
433 if output:
435 if output:
434 outpart = op.reply.newpart('output', data=output,
436 outpart = op.reply.newpart('output', data=output,
435 mandatory=False)
437 mandatory=False)
436 outpart.addparam('in-reply-to', str(part.id), mandatory=False)
438 outpart.addparam('in-reply-to', str(part.id), mandatory=False)
437 finally:
439 finally:
438 # consume the part content to not corrupt the stream.
440 # consume the part content to not corrupt the stream.
439 part.seek(0, 2)
441 part.seek(0, 2)
440
442
441
443
442 def decodecaps(blob):
444 def decodecaps(blob):
443 """decode a bundle2 caps bytes blob into a dictionary
445 """decode a bundle2 caps bytes blob into a dictionary
444
446
445 The blob is a list of capabilities (one per line)
447 The blob is a list of capabilities (one per line)
446 Capabilities may have values using a line of the form::
448 Capabilities may have values using a line of the form::
447
449
448 capability=value1,value2,value3
450 capability=value1,value2,value3
449
451
450 The values are always a list."""
452 The values are always a list."""
451 caps = {}
453 caps = {}
452 for line in blob.splitlines():
454 for line in blob.splitlines():
453 if not line:
455 if not line:
454 continue
456 continue
455 if '=' not in line:
457 if '=' not in line:
456 key, vals = line, ()
458 key, vals = line, ()
457 else:
459 else:
458 key, vals = line.split('=', 1)
460 key, vals = line.split('=', 1)
459 vals = vals.split(',')
461 vals = vals.split(',')
460 key = urllib.unquote(key)
462 key = urlreq.unquote(key)
461 vals = [urllib.unquote(v) for v in vals]
463 vals = [urlreq.unquote(v) for v in vals]
462 caps[key] = vals
464 caps[key] = vals
463 return caps
465 return caps
464
466
465 def encodecaps(caps):
467 def encodecaps(caps):
466 """encode a bundle2 caps dictionary into a bytes blob"""
468 """encode a bundle2 caps dictionary into a bytes blob"""
467 chunks = []
469 chunks = []
468 for ca in sorted(caps):
470 for ca in sorted(caps):
469 vals = caps[ca]
471 vals = caps[ca]
470 ca = urllib.quote(ca)
472 ca = urlreq.quote(ca)
471 vals = [urllib.quote(v) for v in vals]
473 vals = [urlreq.quote(v) for v in vals]
472 if vals:
474 if vals:
473 ca = "%s=%s" % (ca, ','.join(vals))
475 ca = "%s=%s" % (ca, ','.join(vals))
474 chunks.append(ca)
476 chunks.append(ca)
475 return '\n'.join(chunks)
477 return '\n'.join(chunks)
476
478
477 bundletypes = {
479 bundletypes = {
478 "": ("", None), # only when using unbundle on ssh and old http servers
480 "": ("", None), # only when using unbundle on ssh and old http servers
479 # since the unification ssh accepts a header but there
481 # since the unification ssh accepts a header but there
480 # is no capability signaling it.
482 # is no capability signaling it.
481 "HG20": (), # special-cased below
483 "HG20": (), # special-cased below
482 "HG10UN": ("HG10UN", None),
484 "HG10UN": ("HG10UN", None),
483 "HG10BZ": ("HG10", 'BZ'),
485 "HG10BZ": ("HG10", 'BZ'),
484 "HG10GZ": ("HG10GZ", 'GZ'),
486 "HG10GZ": ("HG10GZ", 'GZ'),
485 }
487 }
486
488
487 # hgweb uses this list to communicate its preferred type
489 # hgweb uses this list to communicate its preferred type
488 bundlepriority = ['HG10GZ', 'HG10BZ', 'HG10UN']
490 bundlepriority = ['HG10GZ', 'HG10BZ', 'HG10UN']
489
491
490 class bundle20(object):
492 class bundle20(object):
491 """represent an outgoing bundle2 container
493 """represent an outgoing bundle2 container
492
494
493 Use the `addparam` method to add stream level parameter. and `newpart` to
495 Use the `addparam` method to add stream level parameter. and `newpart` to
494 populate it. Then call `getchunks` to retrieve all the binary chunks of
496 populate it. Then call `getchunks` to retrieve all the binary chunks of
495 data that compose the bundle2 container."""
497 data that compose the bundle2 container."""
496
498
497 _magicstring = 'HG20'
499 _magicstring = 'HG20'
498
500
499 def __init__(self, ui, capabilities=()):
501 def __init__(self, ui, capabilities=()):
500 self.ui = ui
502 self.ui = ui
501 self._params = []
503 self._params = []
502 self._parts = []
504 self._parts = []
503 self.capabilities = dict(capabilities)
505 self.capabilities = dict(capabilities)
504 self._compressor = util.compressors[None]()
506 self._compressor = util.compressors[None]()
505
507
506 def setcompression(self, alg):
508 def setcompression(self, alg):
507 """setup core part compression to <alg>"""
509 """setup core part compression to <alg>"""
508 if alg is None:
510 if alg is None:
509 return
511 return
510 assert not any(n.lower() == 'Compression' for n, v in self._params)
512 assert not any(n.lower() == 'Compression' for n, v in self._params)
511 self.addparam('Compression', alg)
513 self.addparam('Compression', alg)
512 self._compressor = util.compressors[alg]()
514 self._compressor = util.compressors[alg]()
513
515
514 @property
516 @property
515 def nbparts(self):
517 def nbparts(self):
516 """total number of parts added to the bundler"""
518 """total number of parts added to the bundler"""
517 return len(self._parts)
519 return len(self._parts)
518
520
519 # methods used to defines the bundle2 content
521 # methods used to defines the bundle2 content
520 def addparam(self, name, value=None):
522 def addparam(self, name, value=None):
521 """add a stream level parameter"""
523 """add a stream level parameter"""
522 if not name:
524 if not name:
523 raise ValueError('empty parameter name')
525 raise ValueError('empty parameter name')
524 if name[0] not in string.letters:
526 if name[0] not in string.letters:
525 raise ValueError('non letter first character: %r' % name)
527 raise ValueError('non letter first character: %r' % name)
526 self._params.append((name, value))
528 self._params.append((name, value))
527
529
528 def addpart(self, part):
530 def addpart(self, part):
529 """add a new part to the bundle2 container
531 """add a new part to the bundle2 container
530
532
531 Parts contains the actual applicative payload."""
533 Parts contains the actual applicative payload."""
532 assert part.id is None
534 assert part.id is None
533 part.id = len(self._parts) # very cheap counter
535 part.id = len(self._parts) # very cheap counter
534 self._parts.append(part)
536 self._parts.append(part)
535
537
536 def newpart(self, typeid, *args, **kwargs):
538 def newpart(self, typeid, *args, **kwargs):
537 """create a new part and add it to the containers
539 """create a new part and add it to the containers
538
540
539 As the part is directly added to the containers. For now, this means
541 As the part is directly added to the containers. For now, this means
540 that any failure to properly initialize the part after calling
542 that any failure to properly initialize the part after calling
541 ``newpart`` should result in a failure of the whole bundling process.
543 ``newpart`` should result in a failure of the whole bundling process.
542
544
543 You can still fall back to manually create and add if you need better
545 You can still fall back to manually create and add if you need better
544 control."""
546 control."""
545 part = bundlepart(typeid, *args, **kwargs)
547 part = bundlepart(typeid, *args, **kwargs)
546 self.addpart(part)
548 self.addpart(part)
547 return part
549 return part
548
550
549 # methods used to generate the bundle2 stream
551 # methods used to generate the bundle2 stream
550 def getchunks(self):
552 def getchunks(self):
551 if self.ui.debugflag:
553 if self.ui.debugflag:
552 msg = ['bundle2-output-bundle: "%s",' % self._magicstring]
554 msg = ['bundle2-output-bundle: "%s",' % self._magicstring]
553 if self._params:
555 if self._params:
554 msg.append(' (%i params)' % len(self._params))
556 msg.append(' (%i params)' % len(self._params))
555 msg.append(' %i parts total\n' % len(self._parts))
557 msg.append(' %i parts total\n' % len(self._parts))
556 self.ui.debug(''.join(msg))
558 self.ui.debug(''.join(msg))
557 outdebug(self.ui, 'start emission of %s stream' % self._magicstring)
559 outdebug(self.ui, 'start emission of %s stream' % self._magicstring)
558 yield self._magicstring
560 yield self._magicstring
559 param = self._paramchunk()
561 param = self._paramchunk()
560 outdebug(self.ui, 'bundle parameter: %s' % param)
562 outdebug(self.ui, 'bundle parameter: %s' % param)
561 yield _pack(_fstreamparamsize, len(param))
563 yield _pack(_fstreamparamsize, len(param))
562 if param:
564 if param:
563 yield param
565 yield param
564 # starting compression
566 # starting compression
565 for chunk in self._getcorechunk():
567 for chunk in self._getcorechunk():
566 yield self._compressor.compress(chunk)
568 yield self._compressor.compress(chunk)
567 yield self._compressor.flush()
569 yield self._compressor.flush()
568
570
569 def _paramchunk(self):
571 def _paramchunk(self):
570 """return a encoded version of all stream parameters"""
572 """return a encoded version of all stream parameters"""
571 blocks = []
573 blocks = []
572 for par, value in self._params:
574 for par, value in self._params:
573 par = urllib.quote(par)
575 par = urlreq.quote(par)
574 if value is not None:
576 if value is not None:
575 value = urllib.quote(value)
577 value = urlreq.quote(value)
576 par = '%s=%s' % (par, value)
578 par = '%s=%s' % (par, value)
577 blocks.append(par)
579 blocks.append(par)
578 return ' '.join(blocks)
580 return ' '.join(blocks)
579
581
580 def _getcorechunk(self):
582 def _getcorechunk(self):
581 """yield chunk for the core part of the bundle
583 """yield chunk for the core part of the bundle
582
584
583 (all but headers and parameters)"""
585 (all but headers and parameters)"""
584 outdebug(self.ui, 'start of parts')
586 outdebug(self.ui, 'start of parts')
585 for part in self._parts:
587 for part in self._parts:
586 outdebug(self.ui, 'bundle part: "%s"' % part.type)
588 outdebug(self.ui, 'bundle part: "%s"' % part.type)
587 for chunk in part.getchunks(ui=self.ui):
589 for chunk in part.getchunks(ui=self.ui):
588 yield chunk
590 yield chunk
589 outdebug(self.ui, 'end of bundle')
591 outdebug(self.ui, 'end of bundle')
590 yield _pack(_fpartheadersize, 0)
592 yield _pack(_fpartheadersize, 0)
591
593
592
594
593 def salvageoutput(self):
595 def salvageoutput(self):
594 """return a list with a copy of all output parts in the bundle
596 """return a list with a copy of all output parts in the bundle
595
597
596 This is meant to be used during error handling to make sure we preserve
598 This is meant to be used during error handling to make sure we preserve
597 server output"""
599 server output"""
598 salvaged = []
600 salvaged = []
599 for part in self._parts:
601 for part in self._parts:
600 if part.type.startswith('output'):
602 if part.type.startswith('output'):
601 salvaged.append(part.copy())
603 salvaged.append(part.copy())
602 return salvaged
604 return salvaged
603
605
604
606
605 class unpackermixin(object):
607 class unpackermixin(object):
606 """A mixin to extract bytes and struct data from a stream"""
608 """A mixin to extract bytes and struct data from a stream"""
607
609
608 def __init__(self, fp):
610 def __init__(self, fp):
609 self._fp = fp
611 self._fp = fp
610 self._seekable = (util.safehasattr(fp, 'seek') and
612 self._seekable = (util.safehasattr(fp, 'seek') and
611 util.safehasattr(fp, 'tell'))
613 util.safehasattr(fp, 'tell'))
612
614
613 def _unpack(self, format):
615 def _unpack(self, format):
614 """unpack this struct format from the stream"""
616 """unpack this struct format from the stream"""
615 data = self._readexact(struct.calcsize(format))
617 data = self._readexact(struct.calcsize(format))
616 return _unpack(format, data)
618 return _unpack(format, data)
617
619
618 def _readexact(self, size):
620 def _readexact(self, size):
619 """read exactly <size> bytes from the stream"""
621 """read exactly <size> bytes from the stream"""
620 return changegroup.readexactly(self._fp, size)
622 return changegroup.readexactly(self._fp, size)
621
623
622 def seek(self, offset, whence=0):
624 def seek(self, offset, whence=0):
623 """move the underlying file pointer"""
625 """move the underlying file pointer"""
624 if self._seekable:
626 if self._seekable:
625 return self._fp.seek(offset, whence)
627 return self._fp.seek(offset, whence)
626 else:
628 else:
627 raise NotImplementedError(_('File pointer is not seekable'))
629 raise NotImplementedError(_('File pointer is not seekable'))
628
630
629 def tell(self):
631 def tell(self):
630 """return the file offset, or None if file is not seekable"""
632 """return the file offset, or None if file is not seekable"""
631 if self._seekable:
633 if self._seekable:
632 try:
634 try:
633 return self._fp.tell()
635 return self._fp.tell()
634 except IOError as e:
636 except IOError as e:
635 if e.errno == errno.ESPIPE:
637 if e.errno == errno.ESPIPE:
636 self._seekable = False
638 self._seekable = False
637 else:
639 else:
638 raise
640 raise
639 return None
641 return None
640
642
641 def close(self):
643 def close(self):
642 """close underlying file"""
644 """close underlying file"""
643 if util.safehasattr(self._fp, 'close'):
645 if util.safehasattr(self._fp, 'close'):
644 return self._fp.close()
646 return self._fp.close()
645
647
646 def getunbundler(ui, fp, magicstring=None):
648 def getunbundler(ui, fp, magicstring=None):
647 """return a valid unbundler object for a given magicstring"""
649 """return a valid unbundler object for a given magicstring"""
648 if magicstring is None:
650 if magicstring is None:
649 magicstring = changegroup.readexactly(fp, 4)
651 magicstring = changegroup.readexactly(fp, 4)
650 magic, version = magicstring[0:2], magicstring[2:4]
652 magic, version = magicstring[0:2], magicstring[2:4]
651 if magic != 'HG':
653 if magic != 'HG':
652 raise error.Abort(_('not a Mercurial bundle'))
654 raise error.Abort(_('not a Mercurial bundle'))
653 unbundlerclass = formatmap.get(version)
655 unbundlerclass = formatmap.get(version)
654 if unbundlerclass is None:
656 if unbundlerclass is None:
655 raise error.Abort(_('unknown bundle version %s') % version)
657 raise error.Abort(_('unknown bundle version %s') % version)
656 unbundler = unbundlerclass(ui, fp)
658 unbundler = unbundlerclass(ui, fp)
657 indebug(ui, 'start processing of %s stream' % magicstring)
659 indebug(ui, 'start processing of %s stream' % magicstring)
658 return unbundler
660 return unbundler
659
661
660 class unbundle20(unpackermixin):
662 class unbundle20(unpackermixin):
661 """interpret a bundle2 stream
663 """interpret a bundle2 stream
662
664
663 This class is fed with a binary stream and yields parts through its
665 This class is fed with a binary stream and yields parts through its
664 `iterparts` methods."""
666 `iterparts` methods."""
665
667
666 _magicstring = 'HG20'
668 _magicstring = 'HG20'
667
669
668 def __init__(self, ui, fp):
670 def __init__(self, ui, fp):
669 """If header is specified, we do not read it out of the stream."""
671 """If header is specified, we do not read it out of the stream."""
670 self.ui = ui
672 self.ui = ui
671 self._decompressor = util.decompressors[None]
673 self._decompressor = util.decompressors[None]
672 self._compressed = None
674 self._compressed = None
673 super(unbundle20, self).__init__(fp)
675 super(unbundle20, self).__init__(fp)
674
676
675 @util.propertycache
677 @util.propertycache
676 def params(self):
678 def params(self):
677 """dictionary of stream level parameters"""
679 """dictionary of stream level parameters"""
678 indebug(self.ui, 'reading bundle2 stream parameters')
680 indebug(self.ui, 'reading bundle2 stream parameters')
679 params = {}
681 params = {}
680 paramssize = self._unpack(_fstreamparamsize)[0]
682 paramssize = self._unpack(_fstreamparamsize)[0]
681 if paramssize < 0:
683 if paramssize < 0:
682 raise error.BundleValueError('negative bundle param size: %i'
684 raise error.BundleValueError('negative bundle param size: %i'
683 % paramssize)
685 % paramssize)
684 if paramssize:
686 if paramssize:
685 params = self._readexact(paramssize)
687 params = self._readexact(paramssize)
686 params = self._processallparams(params)
688 params = self._processallparams(params)
687 return params
689 return params
688
690
689 def _processallparams(self, paramsblock):
691 def _processallparams(self, paramsblock):
690 """"""
692 """"""
691 params = {}
693 params = {}
692 for p in paramsblock.split(' '):
694 for p in paramsblock.split(' '):
693 p = p.split('=', 1)
695 p = p.split('=', 1)
694 p = [urllib.unquote(i) for i in p]
696 p = [urlreq.unquote(i) for i in p]
695 if len(p) < 2:
697 if len(p) < 2:
696 p.append(None)
698 p.append(None)
697 self._processparam(*p)
699 self._processparam(*p)
698 params[p[0]] = p[1]
700 params[p[0]] = p[1]
699 return params
701 return params
700
702
701
703
702 def _processparam(self, name, value):
704 def _processparam(self, name, value):
703 """process a parameter, applying its effect if needed
705 """process a parameter, applying its effect if needed
704
706
705 Parameter starting with a lower case letter are advisory and will be
707 Parameter starting with a lower case letter are advisory and will be
706 ignored when unknown. Those starting with an upper case letter are
708 ignored when unknown. Those starting with an upper case letter are
707 mandatory and will this function will raise a KeyError when unknown.
709 mandatory and will this function will raise a KeyError when unknown.
708
710
709 Note: no option are currently supported. Any input will be either
711 Note: no option are currently supported. Any input will be either
710 ignored or failing.
712 ignored or failing.
711 """
713 """
712 if not name:
714 if not name:
713 raise ValueError('empty parameter name')
715 raise ValueError('empty parameter name')
714 if name[0] not in string.letters:
716 if name[0] not in string.letters:
715 raise ValueError('non letter first character: %r' % name)
717 raise ValueError('non letter first character: %r' % name)
716 try:
718 try:
717 handler = b2streamparamsmap[name.lower()]
719 handler = b2streamparamsmap[name.lower()]
718 except KeyError:
720 except KeyError:
719 if name[0].islower():
721 if name[0].islower():
720 indebug(self.ui, "ignoring unknown parameter %r" % name)
722 indebug(self.ui, "ignoring unknown parameter %r" % name)
721 else:
723 else:
722 raise error.BundleUnknownFeatureError(params=(name,))
724 raise error.BundleUnknownFeatureError(params=(name,))
723 else:
725 else:
724 handler(self, name, value)
726 handler(self, name, value)
725
727
726 def _forwardchunks(self):
728 def _forwardchunks(self):
727 """utility to transfer a bundle2 as binary
729 """utility to transfer a bundle2 as binary
728
730
729 This is made necessary by the fact the 'getbundle' command over 'ssh'
731 This is made necessary by the fact the 'getbundle' command over 'ssh'
730 have no way to know then the reply end, relying on the bundle to be
732 have no way to know then the reply end, relying on the bundle to be
731 interpreted to know its end. This is terrible and we are sorry, but we
733 interpreted to know its end. This is terrible and we are sorry, but we
732 needed to move forward to get general delta enabled.
734 needed to move forward to get general delta enabled.
733 """
735 """
734 yield self._magicstring
736 yield self._magicstring
735 assert 'params' not in vars(self)
737 assert 'params' not in vars(self)
736 paramssize = self._unpack(_fstreamparamsize)[0]
738 paramssize = self._unpack(_fstreamparamsize)[0]
737 if paramssize < 0:
739 if paramssize < 0:
738 raise error.BundleValueError('negative bundle param size: %i'
740 raise error.BundleValueError('negative bundle param size: %i'
739 % paramssize)
741 % paramssize)
740 yield _pack(_fstreamparamsize, paramssize)
742 yield _pack(_fstreamparamsize, paramssize)
741 if paramssize:
743 if paramssize:
742 params = self._readexact(paramssize)
744 params = self._readexact(paramssize)
743 self._processallparams(params)
745 self._processallparams(params)
744 yield params
746 yield params
745 assert self._decompressor is util.decompressors[None]
747 assert self._decompressor is util.decompressors[None]
746 # From there, payload might need to be decompressed
748 # From there, payload might need to be decompressed
747 self._fp = self._decompressor(self._fp)
749 self._fp = self._decompressor(self._fp)
748 emptycount = 0
750 emptycount = 0
749 while emptycount < 2:
751 while emptycount < 2:
750 # so we can brainlessly loop
752 # so we can brainlessly loop
751 assert _fpartheadersize == _fpayloadsize
753 assert _fpartheadersize == _fpayloadsize
752 size = self._unpack(_fpartheadersize)[0]
754 size = self._unpack(_fpartheadersize)[0]
753 yield _pack(_fpartheadersize, size)
755 yield _pack(_fpartheadersize, size)
754 if size:
756 if size:
755 emptycount = 0
757 emptycount = 0
756 else:
758 else:
757 emptycount += 1
759 emptycount += 1
758 continue
760 continue
759 if size == flaginterrupt:
761 if size == flaginterrupt:
760 continue
762 continue
761 elif size < 0:
763 elif size < 0:
762 raise error.BundleValueError('negative chunk size: %i')
764 raise error.BundleValueError('negative chunk size: %i')
763 yield self._readexact(size)
765 yield self._readexact(size)
764
766
765
767
766 def iterparts(self):
768 def iterparts(self):
767 """yield all parts contained in the stream"""
769 """yield all parts contained in the stream"""
768 # make sure param have been loaded
770 # make sure param have been loaded
769 self.params
771 self.params
770 # From there, payload need to be decompressed
772 # From there, payload need to be decompressed
771 self._fp = self._decompressor(self._fp)
773 self._fp = self._decompressor(self._fp)
772 indebug(self.ui, 'start extraction of bundle2 parts')
774 indebug(self.ui, 'start extraction of bundle2 parts')
773 headerblock = self._readpartheader()
775 headerblock = self._readpartheader()
774 while headerblock is not None:
776 while headerblock is not None:
775 part = unbundlepart(self.ui, headerblock, self._fp)
777 part = unbundlepart(self.ui, headerblock, self._fp)
776 yield part
778 yield part
777 part.seek(0, 2)
779 part.seek(0, 2)
778 headerblock = self._readpartheader()
780 headerblock = self._readpartheader()
779 indebug(self.ui, 'end of bundle2 stream')
781 indebug(self.ui, 'end of bundle2 stream')
780
782
781 def _readpartheader(self):
783 def _readpartheader(self):
782 """reads a part header size and return the bytes blob
784 """reads a part header size and return the bytes blob
783
785
784 returns None if empty"""
786 returns None if empty"""
785 headersize = self._unpack(_fpartheadersize)[0]
787 headersize = self._unpack(_fpartheadersize)[0]
786 if headersize < 0:
788 if headersize < 0:
787 raise error.BundleValueError('negative part header size: %i'
789 raise error.BundleValueError('negative part header size: %i'
788 % headersize)
790 % headersize)
789 indebug(self.ui, 'part header size: %i' % headersize)
791 indebug(self.ui, 'part header size: %i' % headersize)
790 if headersize:
792 if headersize:
791 return self._readexact(headersize)
793 return self._readexact(headersize)
792 return None
794 return None
793
795
794 def compressed(self):
796 def compressed(self):
795 self.params # load params
797 self.params # load params
796 return self._compressed
798 return self._compressed
797
799
798 formatmap = {'20': unbundle20}
800 formatmap = {'20': unbundle20}
799
801
800 b2streamparamsmap = {}
802 b2streamparamsmap = {}
801
803
802 def b2streamparamhandler(name):
804 def b2streamparamhandler(name):
803 """register a handler for a stream level parameter"""
805 """register a handler for a stream level parameter"""
804 def decorator(func):
806 def decorator(func):
805 assert name not in formatmap
807 assert name not in formatmap
806 b2streamparamsmap[name] = func
808 b2streamparamsmap[name] = func
807 return func
809 return func
808 return decorator
810 return decorator
809
811
810 @b2streamparamhandler('compression')
812 @b2streamparamhandler('compression')
811 def processcompression(unbundler, param, value):
813 def processcompression(unbundler, param, value):
812 """read compression parameter and install payload decompression"""
814 """read compression parameter and install payload decompression"""
813 if value not in util.decompressors:
815 if value not in util.decompressors:
814 raise error.BundleUnknownFeatureError(params=(param,),
816 raise error.BundleUnknownFeatureError(params=(param,),
815 values=(value,))
817 values=(value,))
816 unbundler._decompressor = util.decompressors[value]
818 unbundler._decompressor = util.decompressors[value]
817 if value is not None:
819 if value is not None:
818 unbundler._compressed = True
820 unbundler._compressed = True
819
821
820 class bundlepart(object):
822 class bundlepart(object):
821 """A bundle2 part contains application level payload
823 """A bundle2 part contains application level payload
822
824
823 The part `type` is used to route the part to the application level
825 The part `type` is used to route the part to the application level
824 handler.
826 handler.
825
827
826 The part payload is contained in ``part.data``. It could be raw bytes or a
828 The part payload is contained in ``part.data``. It could be raw bytes or a
827 generator of byte chunks.
829 generator of byte chunks.
828
830
829 You can add parameters to the part using the ``addparam`` method.
831 You can add parameters to the part using the ``addparam`` method.
830 Parameters can be either mandatory (default) or advisory. Remote side
832 Parameters can be either mandatory (default) or advisory. Remote side
831 should be able to safely ignore the advisory ones.
833 should be able to safely ignore the advisory ones.
832
834
833 Both data and parameters cannot be modified after the generation has begun.
835 Both data and parameters cannot be modified after the generation has begun.
834 """
836 """
835
837
836 def __init__(self, parttype, mandatoryparams=(), advisoryparams=(),
838 def __init__(self, parttype, mandatoryparams=(), advisoryparams=(),
837 data='', mandatory=True):
839 data='', mandatory=True):
838 validateparttype(parttype)
840 validateparttype(parttype)
839 self.id = None
841 self.id = None
840 self.type = parttype
842 self.type = parttype
841 self._data = data
843 self._data = data
842 self._mandatoryparams = list(mandatoryparams)
844 self._mandatoryparams = list(mandatoryparams)
843 self._advisoryparams = list(advisoryparams)
845 self._advisoryparams = list(advisoryparams)
844 # checking for duplicated entries
846 # checking for duplicated entries
845 self._seenparams = set()
847 self._seenparams = set()
846 for pname, __ in self._mandatoryparams + self._advisoryparams:
848 for pname, __ in self._mandatoryparams + self._advisoryparams:
847 if pname in self._seenparams:
849 if pname in self._seenparams:
848 raise RuntimeError('duplicated params: %s' % pname)
850 raise RuntimeError('duplicated params: %s' % pname)
849 self._seenparams.add(pname)
851 self._seenparams.add(pname)
850 # status of the part's generation:
852 # status of the part's generation:
851 # - None: not started,
853 # - None: not started,
852 # - False: currently generated,
854 # - False: currently generated,
853 # - True: generation done.
855 # - True: generation done.
854 self._generated = None
856 self._generated = None
855 self.mandatory = mandatory
857 self.mandatory = mandatory
856
858
857 def copy(self):
859 def copy(self):
858 """return a copy of the part
860 """return a copy of the part
859
861
860 The new part have the very same content but no partid assigned yet.
862 The new part have the very same content but no partid assigned yet.
861 Parts with generated data cannot be copied."""
863 Parts with generated data cannot be copied."""
862 assert not util.safehasattr(self.data, 'next')
864 assert not util.safehasattr(self.data, 'next')
863 return self.__class__(self.type, self._mandatoryparams,
865 return self.__class__(self.type, self._mandatoryparams,
864 self._advisoryparams, self._data, self.mandatory)
866 self._advisoryparams, self._data, self.mandatory)
865
867
866 # methods used to defines the part content
868 # methods used to defines the part content
867 @property
869 @property
868 def data(self):
870 def data(self):
869 return self._data
871 return self._data
870
872
871 @data.setter
873 @data.setter
872 def data(self, data):
874 def data(self, data):
873 if self._generated is not None:
875 if self._generated is not None:
874 raise error.ReadOnlyPartError('part is being generated')
876 raise error.ReadOnlyPartError('part is being generated')
875 self._data = data
877 self._data = data
876
878
877 @property
879 @property
878 def mandatoryparams(self):
880 def mandatoryparams(self):
879 # make it an immutable tuple to force people through ``addparam``
881 # make it an immutable tuple to force people through ``addparam``
880 return tuple(self._mandatoryparams)
882 return tuple(self._mandatoryparams)
881
883
882 @property
884 @property
883 def advisoryparams(self):
885 def advisoryparams(self):
884 # make it an immutable tuple to force people through ``addparam``
886 # make it an immutable tuple to force people through ``addparam``
885 return tuple(self._advisoryparams)
887 return tuple(self._advisoryparams)
886
888
887 def addparam(self, name, value='', mandatory=True):
889 def addparam(self, name, value='', mandatory=True):
888 if self._generated is not None:
890 if self._generated is not None:
889 raise error.ReadOnlyPartError('part is being generated')
891 raise error.ReadOnlyPartError('part is being generated')
890 if name in self._seenparams:
892 if name in self._seenparams:
891 raise ValueError('duplicated params: %s' % name)
893 raise ValueError('duplicated params: %s' % name)
892 self._seenparams.add(name)
894 self._seenparams.add(name)
893 params = self._advisoryparams
895 params = self._advisoryparams
894 if mandatory:
896 if mandatory:
895 params = self._mandatoryparams
897 params = self._mandatoryparams
896 params.append((name, value))
898 params.append((name, value))
897
899
898 # methods used to generates the bundle2 stream
900 # methods used to generates the bundle2 stream
899 def getchunks(self, ui):
901 def getchunks(self, ui):
900 if self._generated is not None:
902 if self._generated is not None:
901 raise RuntimeError('part can only be consumed once')
903 raise RuntimeError('part can only be consumed once')
902 self._generated = False
904 self._generated = False
903
905
904 if ui.debugflag:
906 if ui.debugflag:
905 msg = ['bundle2-output-part: "%s"' % self.type]
907 msg = ['bundle2-output-part: "%s"' % self.type]
906 if not self.mandatory:
908 if not self.mandatory:
907 msg.append(' (advisory)')
909 msg.append(' (advisory)')
908 nbmp = len(self.mandatoryparams)
910 nbmp = len(self.mandatoryparams)
909 nbap = len(self.advisoryparams)
911 nbap = len(self.advisoryparams)
910 if nbmp or nbap:
912 if nbmp or nbap:
911 msg.append(' (params:')
913 msg.append(' (params:')
912 if nbmp:
914 if nbmp:
913 msg.append(' %i mandatory' % nbmp)
915 msg.append(' %i mandatory' % nbmp)
914 if nbap:
916 if nbap:
915 msg.append(' %i advisory' % nbmp)
917 msg.append(' %i advisory' % nbmp)
916 msg.append(')')
918 msg.append(')')
917 if not self.data:
919 if not self.data:
918 msg.append(' empty payload')
920 msg.append(' empty payload')
919 elif util.safehasattr(self.data, 'next'):
921 elif util.safehasattr(self.data, 'next'):
920 msg.append(' streamed payload')
922 msg.append(' streamed payload')
921 else:
923 else:
922 msg.append(' %i bytes payload' % len(self.data))
924 msg.append(' %i bytes payload' % len(self.data))
923 msg.append('\n')
925 msg.append('\n')
924 ui.debug(''.join(msg))
926 ui.debug(''.join(msg))
925
927
926 #### header
928 #### header
927 if self.mandatory:
929 if self.mandatory:
928 parttype = self.type.upper()
930 parttype = self.type.upper()
929 else:
931 else:
930 parttype = self.type.lower()
932 parttype = self.type.lower()
931 outdebug(ui, 'part %s: "%s"' % (self.id, parttype))
933 outdebug(ui, 'part %s: "%s"' % (self.id, parttype))
932 ## parttype
934 ## parttype
933 header = [_pack(_fparttypesize, len(parttype)),
935 header = [_pack(_fparttypesize, len(parttype)),
934 parttype, _pack(_fpartid, self.id),
936 parttype, _pack(_fpartid, self.id),
935 ]
937 ]
936 ## parameters
938 ## parameters
937 # count
939 # count
938 manpar = self.mandatoryparams
940 manpar = self.mandatoryparams
939 advpar = self.advisoryparams
941 advpar = self.advisoryparams
940 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
942 header.append(_pack(_fpartparamcount, len(manpar), len(advpar)))
941 # size
943 # size
942 parsizes = []
944 parsizes = []
943 for key, value in manpar:
945 for key, value in manpar:
944 parsizes.append(len(key))
946 parsizes.append(len(key))
945 parsizes.append(len(value))
947 parsizes.append(len(value))
946 for key, value in advpar:
948 for key, value in advpar:
947 parsizes.append(len(key))
949 parsizes.append(len(key))
948 parsizes.append(len(value))
950 parsizes.append(len(value))
949 paramsizes = _pack(_makefpartparamsizes(len(parsizes) / 2), *parsizes)
951 paramsizes = _pack(_makefpartparamsizes(len(parsizes) / 2), *parsizes)
950 header.append(paramsizes)
952 header.append(paramsizes)
951 # key, value
953 # key, value
952 for key, value in manpar:
954 for key, value in manpar:
953 header.append(key)
955 header.append(key)
954 header.append(value)
956 header.append(value)
955 for key, value in advpar:
957 for key, value in advpar:
956 header.append(key)
958 header.append(key)
957 header.append(value)
959 header.append(value)
958 ## finalize header
960 ## finalize header
959 headerchunk = ''.join(header)
961 headerchunk = ''.join(header)
960 outdebug(ui, 'header chunk size: %i' % len(headerchunk))
962 outdebug(ui, 'header chunk size: %i' % len(headerchunk))
961 yield _pack(_fpartheadersize, len(headerchunk))
963 yield _pack(_fpartheadersize, len(headerchunk))
962 yield headerchunk
964 yield headerchunk
963 ## payload
965 ## payload
964 try:
966 try:
965 for chunk in self._payloadchunks():
967 for chunk in self._payloadchunks():
966 outdebug(ui, 'payload chunk size: %i' % len(chunk))
968 outdebug(ui, 'payload chunk size: %i' % len(chunk))
967 yield _pack(_fpayloadsize, len(chunk))
969 yield _pack(_fpayloadsize, len(chunk))
968 yield chunk
970 yield chunk
969 except GeneratorExit:
971 except GeneratorExit:
970 # GeneratorExit means that nobody is listening for our
972 # GeneratorExit means that nobody is listening for our
971 # results anyway, so just bail quickly rather than trying
973 # results anyway, so just bail quickly rather than trying
972 # to produce an error part.
974 # to produce an error part.
973 ui.debug('bundle2-generatorexit\n')
975 ui.debug('bundle2-generatorexit\n')
974 raise
976 raise
975 except BaseException as exc:
977 except BaseException as exc:
976 # backup exception data for later
978 # backup exception data for later
977 ui.debug('bundle2-input-stream-interrupt: encoding exception %s'
979 ui.debug('bundle2-input-stream-interrupt: encoding exception %s'
978 % exc)
980 % exc)
979 exc_info = sys.exc_info()
981 exc_info = sys.exc_info()
980 msg = 'unexpected error: %s' % exc
982 msg = 'unexpected error: %s' % exc
981 interpart = bundlepart('error:abort', [('message', msg)],
983 interpart = bundlepart('error:abort', [('message', msg)],
982 mandatory=False)
984 mandatory=False)
983 interpart.id = 0
985 interpart.id = 0
984 yield _pack(_fpayloadsize, -1)
986 yield _pack(_fpayloadsize, -1)
985 for chunk in interpart.getchunks(ui=ui):
987 for chunk in interpart.getchunks(ui=ui):
986 yield chunk
988 yield chunk
987 outdebug(ui, 'closing payload chunk')
989 outdebug(ui, 'closing payload chunk')
988 # abort current part payload
990 # abort current part payload
989 yield _pack(_fpayloadsize, 0)
991 yield _pack(_fpayloadsize, 0)
990 raise exc_info[0], exc_info[1], exc_info[2]
992 raise exc_info[0], exc_info[1], exc_info[2]
991 # end of payload
993 # end of payload
992 outdebug(ui, 'closing payload chunk')
994 outdebug(ui, 'closing payload chunk')
993 yield _pack(_fpayloadsize, 0)
995 yield _pack(_fpayloadsize, 0)
994 self._generated = True
996 self._generated = True
995
997
996 def _payloadchunks(self):
998 def _payloadchunks(self):
997 """yield chunks of a the part payload
999 """yield chunks of a the part payload
998
1000
999 Exists to handle the different methods to provide data to a part."""
1001 Exists to handle the different methods to provide data to a part."""
1000 # we only support fixed size data now.
1002 # we only support fixed size data now.
1001 # This will be improved in the future.
1003 # This will be improved in the future.
1002 if util.safehasattr(self.data, 'next'):
1004 if util.safehasattr(self.data, 'next'):
1003 buff = util.chunkbuffer(self.data)
1005 buff = util.chunkbuffer(self.data)
1004 chunk = buff.read(preferedchunksize)
1006 chunk = buff.read(preferedchunksize)
1005 while chunk:
1007 while chunk:
1006 yield chunk
1008 yield chunk
1007 chunk = buff.read(preferedchunksize)
1009 chunk = buff.read(preferedchunksize)
1008 elif len(self.data):
1010 elif len(self.data):
1009 yield self.data
1011 yield self.data
1010
1012
1011
1013
1012 flaginterrupt = -1
1014 flaginterrupt = -1
1013
1015
1014 class interrupthandler(unpackermixin):
1016 class interrupthandler(unpackermixin):
1015 """read one part and process it with restricted capability
1017 """read one part and process it with restricted capability
1016
1018
1017 This allows to transmit exception raised on the producer size during part
1019 This allows to transmit exception raised on the producer size during part
1018 iteration while the consumer is reading a part.
1020 iteration while the consumer is reading a part.
1019
1021
1020 Part processed in this manner only have access to a ui object,"""
1022 Part processed in this manner only have access to a ui object,"""
1021
1023
1022 def __init__(self, ui, fp):
1024 def __init__(self, ui, fp):
1023 super(interrupthandler, self).__init__(fp)
1025 super(interrupthandler, self).__init__(fp)
1024 self.ui = ui
1026 self.ui = ui
1025
1027
1026 def _readpartheader(self):
1028 def _readpartheader(self):
1027 """reads a part header size and return the bytes blob
1029 """reads a part header size and return the bytes blob
1028
1030
1029 returns None if empty"""
1031 returns None if empty"""
1030 headersize = self._unpack(_fpartheadersize)[0]
1032 headersize = self._unpack(_fpartheadersize)[0]
1031 if headersize < 0:
1033 if headersize < 0:
1032 raise error.BundleValueError('negative part header size: %i'
1034 raise error.BundleValueError('negative part header size: %i'
1033 % headersize)
1035 % headersize)
1034 indebug(self.ui, 'part header size: %i\n' % headersize)
1036 indebug(self.ui, 'part header size: %i\n' % headersize)
1035 if headersize:
1037 if headersize:
1036 return self._readexact(headersize)
1038 return self._readexact(headersize)
1037 return None
1039 return None
1038
1040
1039 def __call__(self):
1041 def __call__(self):
1040
1042
1041 self.ui.debug('bundle2-input-stream-interrupt:'
1043 self.ui.debug('bundle2-input-stream-interrupt:'
1042 ' opening out of band context\n')
1044 ' opening out of band context\n')
1043 indebug(self.ui, 'bundle2 stream interruption, looking for a part.')
1045 indebug(self.ui, 'bundle2 stream interruption, looking for a part.')
1044 headerblock = self._readpartheader()
1046 headerblock = self._readpartheader()
1045 if headerblock is None:
1047 if headerblock is None:
1046 indebug(self.ui, 'no part found during interruption.')
1048 indebug(self.ui, 'no part found during interruption.')
1047 return
1049 return
1048 part = unbundlepart(self.ui, headerblock, self._fp)
1050 part = unbundlepart(self.ui, headerblock, self._fp)
1049 op = interruptoperation(self.ui)
1051 op = interruptoperation(self.ui)
1050 _processpart(op, part)
1052 _processpart(op, part)
1051 self.ui.debug('bundle2-input-stream-interrupt:'
1053 self.ui.debug('bundle2-input-stream-interrupt:'
1052 ' closing out of band context\n')
1054 ' closing out of band context\n')
1053
1055
1054 class interruptoperation(object):
1056 class interruptoperation(object):
1055 """A limited operation to be use by part handler during interruption
1057 """A limited operation to be use by part handler during interruption
1056
1058
1057 It only have access to an ui object.
1059 It only have access to an ui object.
1058 """
1060 """
1059
1061
1060 def __init__(self, ui):
1062 def __init__(self, ui):
1061 self.ui = ui
1063 self.ui = ui
1062 self.reply = None
1064 self.reply = None
1063 self.captureoutput = False
1065 self.captureoutput = False
1064
1066
1065 @property
1067 @property
1066 def repo(self):
1068 def repo(self):
1067 raise RuntimeError('no repo access from stream interruption')
1069 raise RuntimeError('no repo access from stream interruption')
1068
1070
1069 def gettransaction(self):
1071 def gettransaction(self):
1070 raise TransactionUnavailable('no repo access from stream interruption')
1072 raise TransactionUnavailable('no repo access from stream interruption')
1071
1073
1072 class unbundlepart(unpackermixin):
1074 class unbundlepart(unpackermixin):
1073 """a bundle part read from a bundle"""
1075 """a bundle part read from a bundle"""
1074
1076
1075 def __init__(self, ui, header, fp):
1077 def __init__(self, ui, header, fp):
1076 super(unbundlepart, self).__init__(fp)
1078 super(unbundlepart, self).__init__(fp)
1077 self.ui = ui
1079 self.ui = ui
1078 # unbundle state attr
1080 # unbundle state attr
1079 self._headerdata = header
1081 self._headerdata = header
1080 self._headeroffset = 0
1082 self._headeroffset = 0
1081 self._initialized = False
1083 self._initialized = False
1082 self.consumed = False
1084 self.consumed = False
1083 # part data
1085 # part data
1084 self.id = None
1086 self.id = None
1085 self.type = None
1087 self.type = None
1086 self.mandatoryparams = None
1088 self.mandatoryparams = None
1087 self.advisoryparams = None
1089 self.advisoryparams = None
1088 self.params = None
1090 self.params = None
1089 self.mandatorykeys = ()
1091 self.mandatorykeys = ()
1090 self._payloadstream = None
1092 self._payloadstream = None
1091 self._readheader()
1093 self._readheader()
1092 self._mandatory = None
1094 self._mandatory = None
1093 self._chunkindex = [] #(payload, file) position tuples for chunk starts
1095 self._chunkindex = [] #(payload, file) position tuples for chunk starts
1094 self._pos = 0
1096 self._pos = 0
1095
1097
1096 def _fromheader(self, size):
1098 def _fromheader(self, size):
1097 """return the next <size> byte from the header"""
1099 """return the next <size> byte from the header"""
1098 offset = self._headeroffset
1100 offset = self._headeroffset
1099 data = self._headerdata[offset:(offset + size)]
1101 data = self._headerdata[offset:(offset + size)]
1100 self._headeroffset = offset + size
1102 self._headeroffset = offset + size
1101 return data
1103 return data
1102
1104
1103 def _unpackheader(self, format):
1105 def _unpackheader(self, format):
1104 """read given format from header
1106 """read given format from header
1105
1107
1106 This automatically compute the size of the format to read."""
1108 This automatically compute the size of the format to read."""
1107 data = self._fromheader(struct.calcsize(format))
1109 data = self._fromheader(struct.calcsize(format))
1108 return _unpack(format, data)
1110 return _unpack(format, data)
1109
1111
1110 def _initparams(self, mandatoryparams, advisoryparams):
1112 def _initparams(self, mandatoryparams, advisoryparams):
1111 """internal function to setup all logic related parameters"""
1113 """internal function to setup all logic related parameters"""
1112 # make it read only to prevent people touching it by mistake.
1114 # make it read only to prevent people touching it by mistake.
1113 self.mandatoryparams = tuple(mandatoryparams)
1115 self.mandatoryparams = tuple(mandatoryparams)
1114 self.advisoryparams = tuple(advisoryparams)
1116 self.advisoryparams = tuple(advisoryparams)
1115 # user friendly UI
1117 # user friendly UI
1116 self.params = dict(self.mandatoryparams)
1118 self.params = dict(self.mandatoryparams)
1117 self.params.update(dict(self.advisoryparams))
1119 self.params.update(dict(self.advisoryparams))
1118 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
1120 self.mandatorykeys = frozenset(p[0] for p in mandatoryparams)
1119
1121
1120 def _payloadchunks(self, chunknum=0):
1122 def _payloadchunks(self, chunknum=0):
1121 '''seek to specified chunk and start yielding data'''
1123 '''seek to specified chunk and start yielding data'''
1122 if len(self._chunkindex) == 0:
1124 if len(self._chunkindex) == 0:
1123 assert chunknum == 0, 'Must start with chunk 0'
1125 assert chunknum == 0, 'Must start with chunk 0'
1124 self._chunkindex.append((0, super(unbundlepart, self).tell()))
1126 self._chunkindex.append((0, super(unbundlepart, self).tell()))
1125 else:
1127 else:
1126 assert chunknum < len(self._chunkindex), \
1128 assert chunknum < len(self._chunkindex), \
1127 'Unknown chunk %d' % chunknum
1129 'Unknown chunk %d' % chunknum
1128 super(unbundlepart, self).seek(self._chunkindex[chunknum][1])
1130 super(unbundlepart, self).seek(self._chunkindex[chunknum][1])
1129
1131
1130 pos = self._chunkindex[chunknum][0]
1132 pos = self._chunkindex[chunknum][0]
1131 payloadsize = self._unpack(_fpayloadsize)[0]
1133 payloadsize = self._unpack(_fpayloadsize)[0]
1132 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
1134 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
1133 while payloadsize:
1135 while payloadsize:
1134 if payloadsize == flaginterrupt:
1136 if payloadsize == flaginterrupt:
1135 # interruption detection, the handler will now read a
1137 # interruption detection, the handler will now read a
1136 # single part and process it.
1138 # single part and process it.
1137 interrupthandler(self.ui, self._fp)()
1139 interrupthandler(self.ui, self._fp)()
1138 elif payloadsize < 0:
1140 elif payloadsize < 0:
1139 msg = 'negative payload chunk size: %i' % payloadsize
1141 msg = 'negative payload chunk size: %i' % payloadsize
1140 raise error.BundleValueError(msg)
1142 raise error.BundleValueError(msg)
1141 else:
1143 else:
1142 result = self._readexact(payloadsize)
1144 result = self._readexact(payloadsize)
1143 chunknum += 1
1145 chunknum += 1
1144 pos += payloadsize
1146 pos += payloadsize
1145 if chunknum == len(self._chunkindex):
1147 if chunknum == len(self._chunkindex):
1146 self._chunkindex.append((pos,
1148 self._chunkindex.append((pos,
1147 super(unbundlepart, self).tell()))
1149 super(unbundlepart, self).tell()))
1148 yield result
1150 yield result
1149 payloadsize = self._unpack(_fpayloadsize)[0]
1151 payloadsize = self._unpack(_fpayloadsize)[0]
1150 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
1152 indebug(self.ui, 'payload chunk size: %i' % payloadsize)
1151
1153
1152 def _findchunk(self, pos):
1154 def _findchunk(self, pos):
1153 '''for a given payload position, return a chunk number and offset'''
1155 '''for a given payload position, return a chunk number and offset'''
1154 for chunk, (ppos, fpos) in enumerate(self._chunkindex):
1156 for chunk, (ppos, fpos) in enumerate(self._chunkindex):
1155 if ppos == pos:
1157 if ppos == pos:
1156 return chunk, 0
1158 return chunk, 0
1157 elif ppos > pos:
1159 elif ppos > pos:
1158 return chunk - 1, pos - self._chunkindex[chunk - 1][0]
1160 return chunk - 1, pos - self._chunkindex[chunk - 1][0]
1159 raise ValueError('Unknown chunk')
1161 raise ValueError('Unknown chunk')
1160
1162
1161 def _readheader(self):
1163 def _readheader(self):
1162 """read the header and setup the object"""
1164 """read the header and setup the object"""
1163 typesize = self._unpackheader(_fparttypesize)[0]
1165 typesize = self._unpackheader(_fparttypesize)[0]
1164 self.type = self._fromheader(typesize)
1166 self.type = self._fromheader(typesize)
1165 indebug(self.ui, 'part type: "%s"' % self.type)
1167 indebug(self.ui, 'part type: "%s"' % self.type)
1166 self.id = self._unpackheader(_fpartid)[0]
1168 self.id = self._unpackheader(_fpartid)[0]
1167 indebug(self.ui, 'part id: "%s"' % self.id)
1169 indebug(self.ui, 'part id: "%s"' % self.id)
1168 # extract mandatory bit from type
1170 # extract mandatory bit from type
1169 self.mandatory = (self.type != self.type.lower())
1171 self.mandatory = (self.type != self.type.lower())
1170 self.type = self.type.lower()
1172 self.type = self.type.lower()
1171 ## reading parameters
1173 ## reading parameters
1172 # param count
1174 # param count
1173 mancount, advcount = self._unpackheader(_fpartparamcount)
1175 mancount, advcount = self._unpackheader(_fpartparamcount)
1174 indebug(self.ui, 'part parameters: %i' % (mancount + advcount))
1176 indebug(self.ui, 'part parameters: %i' % (mancount + advcount))
1175 # param size
1177 # param size
1176 fparamsizes = _makefpartparamsizes(mancount + advcount)
1178 fparamsizes = _makefpartparamsizes(mancount + advcount)
1177 paramsizes = self._unpackheader(fparamsizes)
1179 paramsizes = self._unpackheader(fparamsizes)
1178 # make it a list of couple again
1180 # make it a list of couple again
1179 paramsizes = zip(paramsizes[::2], paramsizes[1::2])
1181 paramsizes = zip(paramsizes[::2], paramsizes[1::2])
1180 # split mandatory from advisory
1182 # split mandatory from advisory
1181 mansizes = paramsizes[:mancount]
1183 mansizes = paramsizes[:mancount]
1182 advsizes = paramsizes[mancount:]
1184 advsizes = paramsizes[mancount:]
1183 # retrieve param value
1185 # retrieve param value
1184 manparams = []
1186 manparams = []
1185 for key, value in mansizes:
1187 for key, value in mansizes:
1186 manparams.append((self._fromheader(key), self._fromheader(value)))
1188 manparams.append((self._fromheader(key), self._fromheader(value)))
1187 advparams = []
1189 advparams = []
1188 for key, value in advsizes:
1190 for key, value in advsizes:
1189 advparams.append((self._fromheader(key), self._fromheader(value)))
1191 advparams.append((self._fromheader(key), self._fromheader(value)))
1190 self._initparams(manparams, advparams)
1192 self._initparams(manparams, advparams)
1191 ## part payload
1193 ## part payload
1192 self._payloadstream = util.chunkbuffer(self._payloadchunks())
1194 self._payloadstream = util.chunkbuffer(self._payloadchunks())
1193 # we read the data, tell it
1195 # we read the data, tell it
1194 self._initialized = True
1196 self._initialized = True
1195
1197
1196 def read(self, size=None):
1198 def read(self, size=None):
1197 """read payload data"""
1199 """read payload data"""
1198 if not self._initialized:
1200 if not self._initialized:
1199 self._readheader()
1201 self._readheader()
1200 if size is None:
1202 if size is None:
1201 data = self._payloadstream.read()
1203 data = self._payloadstream.read()
1202 else:
1204 else:
1203 data = self._payloadstream.read(size)
1205 data = self._payloadstream.read(size)
1204 self._pos += len(data)
1206 self._pos += len(data)
1205 if size is None or len(data) < size:
1207 if size is None or len(data) < size:
1206 if not self.consumed and self._pos:
1208 if not self.consumed and self._pos:
1207 self.ui.debug('bundle2-input-part: total payload size %i\n'
1209 self.ui.debug('bundle2-input-part: total payload size %i\n'
1208 % self._pos)
1210 % self._pos)
1209 self.consumed = True
1211 self.consumed = True
1210 return data
1212 return data
1211
1213
1212 def tell(self):
1214 def tell(self):
1213 return self._pos
1215 return self._pos
1214
1216
1215 def seek(self, offset, whence=0):
1217 def seek(self, offset, whence=0):
1216 if whence == 0:
1218 if whence == 0:
1217 newpos = offset
1219 newpos = offset
1218 elif whence == 1:
1220 elif whence == 1:
1219 newpos = self._pos + offset
1221 newpos = self._pos + offset
1220 elif whence == 2:
1222 elif whence == 2:
1221 if not self.consumed:
1223 if not self.consumed:
1222 self.read()
1224 self.read()
1223 newpos = self._chunkindex[-1][0] - offset
1225 newpos = self._chunkindex[-1][0] - offset
1224 else:
1226 else:
1225 raise ValueError('Unknown whence value: %r' % (whence,))
1227 raise ValueError('Unknown whence value: %r' % (whence,))
1226
1228
1227 if newpos > self._chunkindex[-1][0] and not self.consumed:
1229 if newpos > self._chunkindex[-1][0] and not self.consumed:
1228 self.read()
1230 self.read()
1229 if not 0 <= newpos <= self._chunkindex[-1][0]:
1231 if not 0 <= newpos <= self._chunkindex[-1][0]:
1230 raise ValueError('Offset out of range')
1232 raise ValueError('Offset out of range')
1231
1233
1232 if self._pos != newpos:
1234 if self._pos != newpos:
1233 chunk, internaloffset = self._findchunk(newpos)
1235 chunk, internaloffset = self._findchunk(newpos)
1234 self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk))
1236 self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk))
1235 adjust = self.read(internaloffset)
1237 adjust = self.read(internaloffset)
1236 if len(adjust) != internaloffset:
1238 if len(adjust) != internaloffset:
1237 raise error.Abort(_('Seek failed\n'))
1239 raise error.Abort(_('Seek failed\n'))
1238 self._pos = newpos
1240 self._pos = newpos
1239
1241
1240 # These are only the static capabilities.
1242 # These are only the static capabilities.
1241 # Check the 'getrepocaps' function for the rest.
1243 # Check the 'getrepocaps' function for the rest.
1242 capabilities = {'HG20': (),
1244 capabilities = {'HG20': (),
1243 'error': ('abort', 'unsupportedcontent', 'pushraced',
1245 'error': ('abort', 'unsupportedcontent', 'pushraced',
1244 'pushkey'),
1246 'pushkey'),
1245 'listkeys': (),
1247 'listkeys': (),
1246 'pushkey': (),
1248 'pushkey': (),
1247 'digests': tuple(sorted(util.DIGESTS.keys())),
1249 'digests': tuple(sorted(util.DIGESTS.keys())),
1248 'remote-changegroup': ('http', 'https'),
1250 'remote-changegroup': ('http', 'https'),
1249 'hgtagsfnodes': (),
1251 'hgtagsfnodes': (),
1250 }
1252 }
1251
1253
1252 def getrepocaps(repo, allowpushback=False):
1254 def getrepocaps(repo, allowpushback=False):
1253 """return the bundle2 capabilities for a given repo
1255 """return the bundle2 capabilities for a given repo
1254
1256
1255 Exists to allow extensions (like evolution) to mutate the capabilities.
1257 Exists to allow extensions (like evolution) to mutate the capabilities.
1256 """
1258 """
1257 caps = capabilities.copy()
1259 caps = capabilities.copy()
1258 caps['changegroup'] = tuple(sorted(
1260 caps['changegroup'] = tuple(sorted(
1259 changegroup.supportedincomingversions(repo)))
1261 changegroup.supportedincomingversions(repo)))
1260 if obsolete.isenabled(repo, obsolete.exchangeopt):
1262 if obsolete.isenabled(repo, obsolete.exchangeopt):
1261 supportedformat = tuple('V%i' % v for v in obsolete.formats)
1263 supportedformat = tuple('V%i' % v for v in obsolete.formats)
1262 caps['obsmarkers'] = supportedformat
1264 caps['obsmarkers'] = supportedformat
1263 if allowpushback:
1265 if allowpushback:
1264 caps['pushback'] = ()
1266 caps['pushback'] = ()
1265 return caps
1267 return caps
1266
1268
1267 def bundle2caps(remote):
1269 def bundle2caps(remote):
1268 """return the bundle capabilities of a peer as dict"""
1270 """return the bundle capabilities of a peer as dict"""
1269 raw = remote.capable('bundle2')
1271 raw = remote.capable('bundle2')
1270 if not raw and raw != '':
1272 if not raw and raw != '':
1271 return {}
1273 return {}
1272 capsblob = urllib.unquote(remote.capable('bundle2'))
1274 capsblob = urlreq.unquote(remote.capable('bundle2'))
1273 return decodecaps(capsblob)
1275 return decodecaps(capsblob)
1274
1276
1275 def obsmarkersversion(caps):
1277 def obsmarkersversion(caps):
1276 """extract the list of supported obsmarkers versions from a bundle2caps dict
1278 """extract the list of supported obsmarkers versions from a bundle2caps dict
1277 """
1279 """
1278 obscaps = caps.get('obsmarkers', ())
1280 obscaps = caps.get('obsmarkers', ())
1279 return [int(c[1:]) for c in obscaps if c.startswith('V')]
1281 return [int(c[1:]) for c in obscaps if c.startswith('V')]
1280
1282
1281 def writebundle(ui, cg, filename, bundletype, vfs=None, compression=None):
1283 def writebundle(ui, cg, filename, bundletype, vfs=None, compression=None):
1282 """Write a bundle file and return its filename.
1284 """Write a bundle file and return its filename.
1283
1285
1284 Existing files will not be overwritten.
1286 Existing files will not be overwritten.
1285 If no filename is specified, a temporary file is created.
1287 If no filename is specified, a temporary file is created.
1286 bz2 compression can be turned off.
1288 bz2 compression can be turned off.
1287 The bundle file will be deleted in case of errors.
1289 The bundle file will be deleted in case of errors.
1288 """
1290 """
1289
1291
1290 if bundletype == "HG20":
1292 if bundletype == "HG20":
1291 bundle = bundle20(ui)
1293 bundle = bundle20(ui)
1292 bundle.setcompression(compression)
1294 bundle.setcompression(compression)
1293 part = bundle.newpart('changegroup', data=cg.getchunks())
1295 part = bundle.newpart('changegroup', data=cg.getchunks())
1294 part.addparam('version', cg.version)
1296 part.addparam('version', cg.version)
1295 chunkiter = bundle.getchunks()
1297 chunkiter = bundle.getchunks()
1296 else:
1298 else:
1297 # compression argument is only for the bundle2 case
1299 # compression argument is only for the bundle2 case
1298 assert compression is None
1300 assert compression is None
1299 if cg.version != '01':
1301 if cg.version != '01':
1300 raise error.Abort(_('old bundle types only supports v1 '
1302 raise error.Abort(_('old bundle types only supports v1 '
1301 'changegroups'))
1303 'changegroups'))
1302 header, comp = bundletypes[bundletype]
1304 header, comp = bundletypes[bundletype]
1303 if comp not in util.compressors:
1305 if comp not in util.compressors:
1304 raise error.Abort(_('unknown stream compression type: %s')
1306 raise error.Abort(_('unknown stream compression type: %s')
1305 % comp)
1307 % comp)
1306 z = util.compressors[comp]()
1308 z = util.compressors[comp]()
1307 subchunkiter = cg.getchunks()
1309 subchunkiter = cg.getchunks()
1308 def chunkiter():
1310 def chunkiter():
1309 yield header
1311 yield header
1310 for chunk in subchunkiter:
1312 for chunk in subchunkiter:
1311 yield z.compress(chunk)
1313 yield z.compress(chunk)
1312 yield z.flush()
1314 yield z.flush()
1313 chunkiter = chunkiter()
1315 chunkiter = chunkiter()
1314
1316
1315 # parse the changegroup data, otherwise we will block
1317 # parse the changegroup data, otherwise we will block
1316 # in case of sshrepo because we don't know the end of the stream
1318 # in case of sshrepo because we don't know the end of the stream
1317 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1319 return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs)
1318
1320
1319 @parthandler('changegroup', ('version', 'nbchanges', 'treemanifest'))
1321 @parthandler('changegroup', ('version', 'nbchanges', 'treemanifest'))
1320 def handlechangegroup(op, inpart):
1322 def handlechangegroup(op, inpart):
1321 """apply a changegroup part on the repo
1323 """apply a changegroup part on the repo
1322
1324
1323 This is a very early implementation that will massive rework before being
1325 This is a very early implementation that will massive rework before being
1324 inflicted to any end-user.
1326 inflicted to any end-user.
1325 """
1327 """
1326 # Make sure we trigger a transaction creation
1328 # Make sure we trigger a transaction creation
1327 #
1329 #
1328 # The addchangegroup function will get a transaction object by itself, but
1330 # The addchangegroup function will get a transaction object by itself, but
1329 # we need to make sure we trigger the creation of a transaction object used
1331 # we need to make sure we trigger the creation of a transaction object used
1330 # for the whole processing scope.
1332 # for the whole processing scope.
1331 op.gettransaction()
1333 op.gettransaction()
1332 unpackerversion = inpart.params.get('version', '01')
1334 unpackerversion = inpart.params.get('version', '01')
1333 # We should raise an appropriate exception here
1335 # We should raise an appropriate exception here
1334 cg = changegroup.getunbundler(unpackerversion, inpart, None)
1336 cg = changegroup.getunbundler(unpackerversion, inpart, None)
1335 # the source and url passed here are overwritten by the one contained in
1337 # the source and url passed here are overwritten by the one contained in
1336 # the transaction.hookargs argument. So 'bundle2' is a placeholder
1338 # the transaction.hookargs argument. So 'bundle2' is a placeholder
1337 nbchangesets = None
1339 nbchangesets = None
1338 if 'nbchanges' in inpart.params:
1340 if 'nbchanges' in inpart.params:
1339 nbchangesets = int(inpart.params.get('nbchanges'))
1341 nbchangesets = int(inpart.params.get('nbchanges'))
1340 if ('treemanifest' in inpart.params and
1342 if ('treemanifest' in inpart.params and
1341 'treemanifest' not in op.repo.requirements):
1343 'treemanifest' not in op.repo.requirements):
1342 if len(op.repo.changelog) != 0:
1344 if len(op.repo.changelog) != 0:
1343 raise error.Abort(_(
1345 raise error.Abort(_(
1344 "bundle contains tree manifests, but local repo is "
1346 "bundle contains tree manifests, but local repo is "
1345 "non-empty and does not use tree manifests"))
1347 "non-empty and does not use tree manifests"))
1346 op.repo.requirements.add('treemanifest')
1348 op.repo.requirements.add('treemanifest')
1347 op.repo._applyopenerreqs()
1349 op.repo._applyopenerreqs()
1348 op.repo._writerequirements()
1350 op.repo._writerequirements()
1349 ret = cg.apply(op.repo, 'bundle2', 'bundle2', expectedtotal=nbchangesets)
1351 ret = cg.apply(op.repo, 'bundle2', 'bundle2', expectedtotal=nbchangesets)
1350 op.records.add('changegroup', {'return': ret})
1352 op.records.add('changegroup', {'return': ret})
1351 if op.reply is not None:
1353 if op.reply is not None:
1352 # This is definitely not the final form of this
1354 # This is definitely not the final form of this
1353 # return. But one need to start somewhere.
1355 # return. But one need to start somewhere.
1354 part = op.reply.newpart('reply:changegroup', mandatory=False)
1356 part = op.reply.newpart('reply:changegroup', mandatory=False)
1355 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1357 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1356 part.addparam('return', '%i' % ret, mandatory=False)
1358 part.addparam('return', '%i' % ret, mandatory=False)
1357 assert not inpart.read()
1359 assert not inpart.read()
1358
1360
1359 _remotechangegroupparams = tuple(['url', 'size', 'digests'] +
1361 _remotechangegroupparams = tuple(['url', 'size', 'digests'] +
1360 ['digest:%s' % k for k in util.DIGESTS.keys()])
1362 ['digest:%s' % k for k in util.DIGESTS.keys()])
1361 @parthandler('remote-changegroup', _remotechangegroupparams)
1363 @parthandler('remote-changegroup', _remotechangegroupparams)
1362 def handleremotechangegroup(op, inpart):
1364 def handleremotechangegroup(op, inpart):
1363 """apply a bundle10 on the repo, given an url and validation information
1365 """apply a bundle10 on the repo, given an url and validation information
1364
1366
1365 All the information about the remote bundle to import are given as
1367 All the information about the remote bundle to import are given as
1366 parameters. The parameters include:
1368 parameters. The parameters include:
1367 - url: the url to the bundle10.
1369 - url: the url to the bundle10.
1368 - size: the bundle10 file size. It is used to validate what was
1370 - size: the bundle10 file size. It is used to validate what was
1369 retrieved by the client matches the server knowledge about the bundle.
1371 retrieved by the client matches the server knowledge about the bundle.
1370 - digests: a space separated list of the digest types provided as
1372 - digests: a space separated list of the digest types provided as
1371 parameters.
1373 parameters.
1372 - digest:<digest-type>: the hexadecimal representation of the digest with
1374 - digest:<digest-type>: the hexadecimal representation of the digest with
1373 that name. Like the size, it is used to validate what was retrieved by
1375 that name. Like the size, it is used to validate what was retrieved by
1374 the client matches what the server knows about the bundle.
1376 the client matches what the server knows about the bundle.
1375
1377
1376 When multiple digest types are given, all of them are checked.
1378 When multiple digest types are given, all of them are checked.
1377 """
1379 """
1378 try:
1380 try:
1379 raw_url = inpart.params['url']
1381 raw_url = inpart.params['url']
1380 except KeyError:
1382 except KeyError:
1381 raise error.Abort(_('remote-changegroup: missing "%s" param') % 'url')
1383 raise error.Abort(_('remote-changegroup: missing "%s" param') % 'url')
1382 parsed_url = util.url(raw_url)
1384 parsed_url = util.url(raw_url)
1383 if parsed_url.scheme not in capabilities['remote-changegroup']:
1385 if parsed_url.scheme not in capabilities['remote-changegroup']:
1384 raise error.Abort(_('remote-changegroup does not support %s urls') %
1386 raise error.Abort(_('remote-changegroup does not support %s urls') %
1385 parsed_url.scheme)
1387 parsed_url.scheme)
1386
1388
1387 try:
1389 try:
1388 size = int(inpart.params['size'])
1390 size = int(inpart.params['size'])
1389 except ValueError:
1391 except ValueError:
1390 raise error.Abort(_('remote-changegroup: invalid value for param "%s"')
1392 raise error.Abort(_('remote-changegroup: invalid value for param "%s"')
1391 % 'size')
1393 % 'size')
1392 except KeyError:
1394 except KeyError:
1393 raise error.Abort(_('remote-changegroup: missing "%s" param') % 'size')
1395 raise error.Abort(_('remote-changegroup: missing "%s" param') % 'size')
1394
1396
1395 digests = {}
1397 digests = {}
1396 for typ in inpart.params.get('digests', '').split():
1398 for typ in inpart.params.get('digests', '').split():
1397 param = 'digest:%s' % typ
1399 param = 'digest:%s' % typ
1398 try:
1400 try:
1399 value = inpart.params[param]
1401 value = inpart.params[param]
1400 except KeyError:
1402 except KeyError:
1401 raise error.Abort(_('remote-changegroup: missing "%s" param') %
1403 raise error.Abort(_('remote-changegroup: missing "%s" param') %
1402 param)
1404 param)
1403 digests[typ] = value
1405 digests[typ] = value
1404
1406
1405 real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests)
1407 real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests)
1406
1408
1407 # Make sure we trigger a transaction creation
1409 # Make sure we trigger a transaction creation
1408 #
1410 #
1409 # The addchangegroup function will get a transaction object by itself, but
1411 # The addchangegroup function will get a transaction object by itself, but
1410 # we need to make sure we trigger the creation of a transaction object used
1412 # we need to make sure we trigger the creation of a transaction object used
1411 # for the whole processing scope.
1413 # for the whole processing scope.
1412 op.gettransaction()
1414 op.gettransaction()
1413 from . import exchange
1415 from . import exchange
1414 cg = exchange.readbundle(op.repo.ui, real_part, raw_url)
1416 cg = exchange.readbundle(op.repo.ui, real_part, raw_url)
1415 if not isinstance(cg, changegroup.cg1unpacker):
1417 if not isinstance(cg, changegroup.cg1unpacker):
1416 raise error.Abort(_('%s: not a bundle version 1.0') %
1418 raise error.Abort(_('%s: not a bundle version 1.0') %
1417 util.hidepassword(raw_url))
1419 util.hidepassword(raw_url))
1418 ret = cg.apply(op.repo, 'bundle2', 'bundle2')
1420 ret = cg.apply(op.repo, 'bundle2', 'bundle2')
1419 op.records.add('changegroup', {'return': ret})
1421 op.records.add('changegroup', {'return': ret})
1420 if op.reply is not None:
1422 if op.reply is not None:
1421 # This is definitely not the final form of this
1423 # This is definitely not the final form of this
1422 # return. But one need to start somewhere.
1424 # return. But one need to start somewhere.
1423 part = op.reply.newpart('reply:changegroup')
1425 part = op.reply.newpart('reply:changegroup')
1424 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1426 part.addparam('in-reply-to', str(inpart.id), mandatory=False)
1425 part.addparam('return', '%i' % ret, mandatory=False)
1427 part.addparam('return', '%i' % ret, mandatory=False)
1426 try:
1428 try:
1427 real_part.validate()
1429 real_part.validate()
1428 except error.Abort as e:
1430 except error.Abort as e:
1429 raise error.Abort(_('bundle at %s is corrupted:\n%s') %
1431 raise error.Abort(_('bundle at %s is corrupted:\n%s') %
1430 (util.hidepassword(raw_url), str(e)))
1432 (util.hidepassword(raw_url), str(e)))
1431 assert not inpart.read()
1433 assert not inpart.read()
1432
1434
1433 @parthandler('reply:changegroup', ('return', 'in-reply-to'))
1435 @parthandler('reply:changegroup', ('return', 'in-reply-to'))
1434 def handlereplychangegroup(op, inpart):
1436 def handlereplychangegroup(op, inpart):
1435 ret = int(inpart.params['return'])
1437 ret = int(inpart.params['return'])
1436 replyto = int(inpart.params['in-reply-to'])
1438 replyto = int(inpart.params['in-reply-to'])
1437 op.records.add('changegroup', {'return': ret}, replyto)
1439 op.records.add('changegroup', {'return': ret}, replyto)
1438
1440
1439 @parthandler('check:heads')
1441 @parthandler('check:heads')
1440 def handlecheckheads(op, inpart):
1442 def handlecheckheads(op, inpart):
1441 """check that head of the repo did not change
1443 """check that head of the repo did not change
1442
1444
1443 This is used to detect a push race when using unbundle.
1445 This is used to detect a push race when using unbundle.
1444 This replaces the "heads" argument of unbundle."""
1446 This replaces the "heads" argument of unbundle."""
1445 h = inpart.read(20)
1447 h = inpart.read(20)
1446 heads = []
1448 heads = []
1447 while len(h) == 20:
1449 while len(h) == 20:
1448 heads.append(h)
1450 heads.append(h)
1449 h = inpart.read(20)
1451 h = inpart.read(20)
1450 assert not h
1452 assert not h
1451 # Trigger a transaction so that we are guaranteed to have the lock now.
1453 # Trigger a transaction so that we are guaranteed to have the lock now.
1452 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1454 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1453 op.gettransaction()
1455 op.gettransaction()
1454 if heads != op.repo.heads():
1456 if heads != op.repo.heads():
1455 raise error.PushRaced('repository changed while pushing - '
1457 raise error.PushRaced('repository changed while pushing - '
1456 'please try again')
1458 'please try again')
1457
1459
1458 @parthandler('output')
1460 @parthandler('output')
1459 def handleoutput(op, inpart):
1461 def handleoutput(op, inpart):
1460 """forward output captured on the server to the client"""
1462 """forward output captured on the server to the client"""
1461 for line in inpart.read().splitlines():
1463 for line in inpart.read().splitlines():
1462 op.ui.status(('remote: %s\n' % line))
1464 op.ui.status(('remote: %s\n' % line))
1463
1465
1464 @parthandler('replycaps')
1466 @parthandler('replycaps')
1465 def handlereplycaps(op, inpart):
1467 def handlereplycaps(op, inpart):
1466 """Notify that a reply bundle should be created
1468 """Notify that a reply bundle should be created
1467
1469
1468 The payload contains the capabilities information for the reply"""
1470 The payload contains the capabilities information for the reply"""
1469 caps = decodecaps(inpart.read())
1471 caps = decodecaps(inpart.read())
1470 if op.reply is None:
1472 if op.reply is None:
1471 op.reply = bundle20(op.ui, caps)
1473 op.reply = bundle20(op.ui, caps)
1472
1474
1473 class AbortFromPart(error.Abort):
1475 class AbortFromPart(error.Abort):
1474 """Sub-class of Abort that denotes an error from a bundle2 part."""
1476 """Sub-class of Abort that denotes an error from a bundle2 part."""
1475
1477
1476 @parthandler('error:abort', ('message', 'hint'))
1478 @parthandler('error:abort', ('message', 'hint'))
1477 def handleerrorabort(op, inpart):
1479 def handleerrorabort(op, inpart):
1478 """Used to transmit abort error over the wire"""
1480 """Used to transmit abort error over the wire"""
1479 raise AbortFromPart(inpart.params['message'],
1481 raise AbortFromPart(inpart.params['message'],
1480 hint=inpart.params.get('hint'))
1482 hint=inpart.params.get('hint'))
1481
1483
1482 @parthandler('error:pushkey', ('namespace', 'key', 'new', 'old', 'ret',
1484 @parthandler('error:pushkey', ('namespace', 'key', 'new', 'old', 'ret',
1483 'in-reply-to'))
1485 'in-reply-to'))
1484 def handleerrorpushkey(op, inpart):
1486 def handleerrorpushkey(op, inpart):
1485 """Used to transmit failure of a mandatory pushkey over the wire"""
1487 """Used to transmit failure of a mandatory pushkey over the wire"""
1486 kwargs = {}
1488 kwargs = {}
1487 for name in ('namespace', 'key', 'new', 'old', 'ret'):
1489 for name in ('namespace', 'key', 'new', 'old', 'ret'):
1488 value = inpart.params.get(name)
1490 value = inpart.params.get(name)
1489 if value is not None:
1491 if value is not None:
1490 kwargs[name] = value
1492 kwargs[name] = value
1491 raise error.PushkeyFailed(inpart.params['in-reply-to'], **kwargs)
1493 raise error.PushkeyFailed(inpart.params['in-reply-to'], **kwargs)
1492
1494
1493 @parthandler('error:unsupportedcontent', ('parttype', 'params'))
1495 @parthandler('error:unsupportedcontent', ('parttype', 'params'))
1494 def handleerrorunsupportedcontent(op, inpart):
1496 def handleerrorunsupportedcontent(op, inpart):
1495 """Used to transmit unknown content error over the wire"""
1497 """Used to transmit unknown content error over the wire"""
1496 kwargs = {}
1498 kwargs = {}
1497 parttype = inpart.params.get('parttype')
1499 parttype = inpart.params.get('parttype')
1498 if parttype is not None:
1500 if parttype is not None:
1499 kwargs['parttype'] = parttype
1501 kwargs['parttype'] = parttype
1500 params = inpart.params.get('params')
1502 params = inpart.params.get('params')
1501 if params is not None:
1503 if params is not None:
1502 kwargs['params'] = params.split('\0')
1504 kwargs['params'] = params.split('\0')
1503
1505
1504 raise error.BundleUnknownFeatureError(**kwargs)
1506 raise error.BundleUnknownFeatureError(**kwargs)
1505
1507
1506 @parthandler('error:pushraced', ('message',))
1508 @parthandler('error:pushraced', ('message',))
1507 def handleerrorpushraced(op, inpart):
1509 def handleerrorpushraced(op, inpart):
1508 """Used to transmit push race error over the wire"""
1510 """Used to transmit push race error over the wire"""
1509 raise error.ResponseError(_('push failed:'), inpart.params['message'])
1511 raise error.ResponseError(_('push failed:'), inpart.params['message'])
1510
1512
1511 @parthandler('listkeys', ('namespace',))
1513 @parthandler('listkeys', ('namespace',))
1512 def handlelistkeys(op, inpart):
1514 def handlelistkeys(op, inpart):
1513 """retrieve pushkey namespace content stored in a bundle2"""
1515 """retrieve pushkey namespace content stored in a bundle2"""
1514 namespace = inpart.params['namespace']
1516 namespace = inpart.params['namespace']
1515 r = pushkey.decodekeys(inpart.read())
1517 r = pushkey.decodekeys(inpart.read())
1516 op.records.add('listkeys', (namespace, r))
1518 op.records.add('listkeys', (namespace, r))
1517
1519
1518 @parthandler('pushkey', ('namespace', 'key', 'old', 'new'))
1520 @parthandler('pushkey', ('namespace', 'key', 'old', 'new'))
1519 def handlepushkey(op, inpart):
1521 def handlepushkey(op, inpart):
1520 """process a pushkey request"""
1522 """process a pushkey request"""
1521 dec = pushkey.decode
1523 dec = pushkey.decode
1522 namespace = dec(inpart.params['namespace'])
1524 namespace = dec(inpart.params['namespace'])
1523 key = dec(inpart.params['key'])
1525 key = dec(inpart.params['key'])
1524 old = dec(inpart.params['old'])
1526 old = dec(inpart.params['old'])
1525 new = dec(inpart.params['new'])
1527 new = dec(inpart.params['new'])
1526 # Grab the transaction to ensure that we have the lock before performing the
1528 # Grab the transaction to ensure that we have the lock before performing the
1527 # pushkey.
1529 # pushkey.
1528 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1530 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1529 op.gettransaction()
1531 op.gettransaction()
1530 ret = op.repo.pushkey(namespace, key, old, new)
1532 ret = op.repo.pushkey(namespace, key, old, new)
1531 record = {'namespace': namespace,
1533 record = {'namespace': namespace,
1532 'key': key,
1534 'key': key,
1533 'old': old,
1535 'old': old,
1534 'new': new}
1536 'new': new}
1535 op.records.add('pushkey', record)
1537 op.records.add('pushkey', record)
1536 if op.reply is not None:
1538 if op.reply is not None:
1537 rpart = op.reply.newpart('reply:pushkey')
1539 rpart = op.reply.newpart('reply:pushkey')
1538 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1540 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1539 rpart.addparam('return', '%i' % ret, mandatory=False)
1541 rpart.addparam('return', '%i' % ret, mandatory=False)
1540 if inpart.mandatory and not ret:
1542 if inpart.mandatory and not ret:
1541 kwargs = {}
1543 kwargs = {}
1542 for key in ('namespace', 'key', 'new', 'old', 'ret'):
1544 for key in ('namespace', 'key', 'new', 'old', 'ret'):
1543 if key in inpart.params:
1545 if key in inpart.params:
1544 kwargs[key] = inpart.params[key]
1546 kwargs[key] = inpart.params[key]
1545 raise error.PushkeyFailed(partid=str(inpart.id), **kwargs)
1547 raise error.PushkeyFailed(partid=str(inpart.id), **kwargs)
1546
1548
1547 @parthandler('reply:pushkey', ('return', 'in-reply-to'))
1549 @parthandler('reply:pushkey', ('return', 'in-reply-to'))
1548 def handlepushkeyreply(op, inpart):
1550 def handlepushkeyreply(op, inpart):
1549 """retrieve the result of a pushkey request"""
1551 """retrieve the result of a pushkey request"""
1550 ret = int(inpart.params['return'])
1552 ret = int(inpart.params['return'])
1551 partid = int(inpart.params['in-reply-to'])
1553 partid = int(inpart.params['in-reply-to'])
1552 op.records.add('pushkey', {'return': ret}, partid)
1554 op.records.add('pushkey', {'return': ret}, partid)
1553
1555
1554 @parthandler('obsmarkers')
1556 @parthandler('obsmarkers')
1555 def handleobsmarker(op, inpart):
1557 def handleobsmarker(op, inpart):
1556 """add a stream of obsmarkers to the repo"""
1558 """add a stream of obsmarkers to the repo"""
1557 tr = op.gettransaction()
1559 tr = op.gettransaction()
1558 markerdata = inpart.read()
1560 markerdata = inpart.read()
1559 if op.ui.config('experimental', 'obsmarkers-exchange-debug', False):
1561 if op.ui.config('experimental', 'obsmarkers-exchange-debug', False):
1560 op.ui.write(('obsmarker-exchange: %i bytes received\n')
1562 op.ui.write(('obsmarker-exchange: %i bytes received\n')
1561 % len(markerdata))
1563 % len(markerdata))
1562 # The mergemarkers call will crash if marker creation is not enabled.
1564 # The mergemarkers call will crash if marker creation is not enabled.
1563 # we want to avoid this if the part is advisory.
1565 # we want to avoid this if the part is advisory.
1564 if not inpart.mandatory and op.repo.obsstore.readonly:
1566 if not inpart.mandatory and op.repo.obsstore.readonly:
1565 op.repo.ui.debug('ignoring obsolescence markers, feature not enabled')
1567 op.repo.ui.debug('ignoring obsolescence markers, feature not enabled')
1566 return
1568 return
1567 new = op.repo.obsstore.mergemarkers(tr, markerdata)
1569 new = op.repo.obsstore.mergemarkers(tr, markerdata)
1568 if new:
1570 if new:
1569 op.repo.ui.status(_('%i new obsolescence markers\n') % new)
1571 op.repo.ui.status(_('%i new obsolescence markers\n') % new)
1570 op.records.add('obsmarkers', {'new': new})
1572 op.records.add('obsmarkers', {'new': new})
1571 if op.reply is not None:
1573 if op.reply is not None:
1572 rpart = op.reply.newpart('reply:obsmarkers')
1574 rpart = op.reply.newpart('reply:obsmarkers')
1573 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1575 rpart.addparam('in-reply-to', str(inpart.id), mandatory=False)
1574 rpart.addparam('new', '%i' % new, mandatory=False)
1576 rpart.addparam('new', '%i' % new, mandatory=False)
1575
1577
1576
1578
1577 @parthandler('reply:obsmarkers', ('new', 'in-reply-to'))
1579 @parthandler('reply:obsmarkers', ('new', 'in-reply-to'))
1578 def handleobsmarkerreply(op, inpart):
1580 def handleobsmarkerreply(op, inpart):
1579 """retrieve the result of a pushkey request"""
1581 """retrieve the result of a pushkey request"""
1580 ret = int(inpart.params['new'])
1582 ret = int(inpart.params['new'])
1581 partid = int(inpart.params['in-reply-to'])
1583 partid = int(inpart.params['in-reply-to'])
1582 op.records.add('obsmarkers', {'new': ret}, partid)
1584 op.records.add('obsmarkers', {'new': ret}, partid)
1583
1585
1584 @parthandler('hgtagsfnodes')
1586 @parthandler('hgtagsfnodes')
1585 def handlehgtagsfnodes(op, inpart):
1587 def handlehgtagsfnodes(op, inpart):
1586 """Applies .hgtags fnodes cache entries to the local repo.
1588 """Applies .hgtags fnodes cache entries to the local repo.
1587
1589
1588 Payload is pairs of 20 byte changeset nodes and filenodes.
1590 Payload is pairs of 20 byte changeset nodes and filenodes.
1589 """
1591 """
1590 # Grab the transaction so we ensure that we have the lock at this point.
1592 # Grab the transaction so we ensure that we have the lock at this point.
1591 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1593 if op.ui.configbool('experimental', 'bundle2lazylocking'):
1592 op.gettransaction()
1594 op.gettransaction()
1593 cache = tags.hgtagsfnodescache(op.repo.unfiltered())
1595 cache = tags.hgtagsfnodescache(op.repo.unfiltered())
1594
1596
1595 count = 0
1597 count = 0
1596 while True:
1598 while True:
1597 node = inpart.read(20)
1599 node = inpart.read(20)
1598 fnode = inpart.read(20)
1600 fnode = inpart.read(20)
1599 if len(node) < 20 or len(fnode) < 20:
1601 if len(node) < 20 or len(fnode) < 20:
1600 op.ui.debug('ignoring incomplete received .hgtags fnodes data\n')
1602 op.ui.debug('ignoring incomplete received .hgtags fnodes data\n')
1601 break
1603 break
1602 cache.setfnode(node, fnode)
1604 cache.setfnode(node, fnode)
1603 count += 1
1605 count += 1
1604
1606
1605 cache.write()
1607 cache.write()
1606 op.ui.debug('applied %i hgtags fnodes cache entries\n' % count)
1608 op.ui.debug('applied %i hgtags fnodes cache entries\n' % count)
@@ -1,467 +1,472 b''
1 # This library is free software; you can redistribute it and/or
1 # This library is free software; you can redistribute it and/or
2 # modify it under the terms of the GNU Lesser General Public
2 # modify it under the terms of the GNU Lesser General Public
3 # License as published by the Free Software Foundation; either
3 # License as published by the Free Software Foundation; either
4 # version 2.1 of the License, or (at your option) any later version.
4 # version 2.1 of the License, or (at your option) any later version.
5 #
5 #
6 # This library is distributed in the hope that it will be useful,
6 # This library is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 # Lesser General Public License for more details.
9 # Lesser General Public License for more details.
10 #
10 #
11 # You should have received a copy of the GNU Lesser General Public
11 # You should have received a copy of the GNU Lesser General Public
12 # License along with this library; if not, see
12 # License along with this library; if not, see
13 # <http://www.gnu.org/licenses/>.
13 # <http://www.gnu.org/licenses/>.
14
14
15 # This file is part of urlgrabber, a high-level cross-protocol url-grabber
15 # This file is part of urlgrabber, a high-level cross-protocol url-grabber
16 # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
16 # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
17
17
18 # $Id: byterange.py,v 1.9 2005/02/14 21:55:07 mstenner Exp $
18 # $Id: byterange.py,v 1.9 2005/02/14 21:55:07 mstenner Exp $
19
19
20 from __future__ import absolute_import
20 from __future__ import absolute_import
21
21
22 import email
22 import email
23 import ftplib
23 import ftplib
24 import mimetypes
24 import mimetypes
25 import os
25 import os
26 import re
26 import re
27 import socket
27 import socket
28 import stat
28 import stat
29 import urllib
29
30 import urllib2
30 from . import (
31 util,
32 )
33
34 urlerr = util.urlerr
35 urlreq = util.urlreq
31
36
32 addclosehook = urllib.addclosehook
37 addclosehook = urlreq.addclosehook
33 addinfourl = urllib.addinfourl
38 addinfourl = urlreq.addinfourl
34 splitattr = urllib.splitattr
39 splitattr = urlreq.splitattr
35 splitpasswd = urllib.splitpasswd
40 splitpasswd = urlreq.splitpasswd
36 splitport = urllib.splitport
41 splitport = urlreq.splitport
37 splituser = urllib.splituser
42 splituser = urlreq.splituser
38 unquote = urllib.unquote
43 unquote = urlreq.unquote
39
44
40 class RangeError(IOError):
45 class RangeError(IOError):
41 """Error raised when an unsatisfiable range is requested."""
46 """Error raised when an unsatisfiable range is requested."""
42 pass
47 pass
43
48
44 class HTTPRangeHandler(urllib2.BaseHandler):
49 class HTTPRangeHandler(urlreq.basehandler):
45 """Handler that enables HTTP Range headers.
50 """Handler that enables HTTP Range headers.
46
51
47 This was extremely simple. The Range header is a HTTP feature to
52 This was extremely simple. The Range header is a HTTP feature to
48 begin with so all this class does is tell urllib2 that the
53 begin with so all this class does is tell urllib2 that the
49 "206 Partial Content" response from the HTTP server is what we
54 "206 Partial Content" response from the HTTP server is what we
50 expected.
55 expected.
51
56
52 Example:
57 Example:
53 import urllib2
58 import urllib2
54 import byterange
59 import byterange
55
60
56 range_handler = range.HTTPRangeHandler()
61 range_handler = range.HTTPRangeHandler()
57 opener = urllib2.build_opener(range_handler)
62 opener = urlreq.buildopener(range_handler)
58
63
59 # install it
64 # install it
60 urllib2.install_opener(opener)
65 urlreq.installopener(opener)
61
66
62 # create Request and set Range header
67 # create Request and set Range header
63 req = urllib2.Request('http://www.python.org/')
68 req = urlreq.request('http://www.python.org/')
64 req.header['Range'] = 'bytes=30-50'
69 req.header['Range'] = 'bytes=30-50'
65 f = urllib2.urlopen(req)
70 f = urlreq.urlopen(req)
66 """
71 """
67
72
68 def http_error_206(self, req, fp, code, msg, hdrs):
73 def http_error_206(self, req, fp, code, msg, hdrs):
69 # 206 Partial Content Response
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 r.code = code
76 r.code = code
72 r.msg = msg
77 r.msg = msg
73 return r
78 return r
74
79
75 def http_error_416(self, req, fp, code, msg, hdrs):
80 def http_error_416(self, req, fp, code, msg, hdrs):
76 # HTTP's Range Not Satisfiable error
81 # HTTP's Range Not Satisfiable error
77 raise RangeError('Requested Range Not Satisfiable')
82 raise RangeError('Requested Range Not Satisfiable')
78
83
79 class RangeableFileObject(object):
84 class RangeableFileObject(object):
80 """File object wrapper to enable raw range handling.
85 """File object wrapper to enable raw range handling.
81 This was implemented primarily for handling range
86 This was implemented primarily for handling range
82 specifications for file:// urls. This object effectively makes
87 specifications for file:// urls. This object effectively makes
83 a file object look like it consists only of a range of bytes in
88 a file object look like it consists only of a range of bytes in
84 the stream.
89 the stream.
85
90
86 Examples:
91 Examples:
87 # expose 10 bytes, starting at byte position 20, from
92 # expose 10 bytes, starting at byte position 20, from
88 # /etc/aliases.
93 # /etc/aliases.
89 >>> fo = RangeableFileObject(file('/etc/passwd', 'r'), (20,30))
94 >>> fo = RangeableFileObject(file('/etc/passwd', 'r'), (20,30))
90 # seek seeks within the range (to position 23 in this case)
95 # seek seeks within the range (to position 23 in this case)
91 >>> fo.seek(3)
96 >>> fo.seek(3)
92 # tell tells where your at _within the range_ (position 3 in
97 # tell tells where your at _within the range_ (position 3 in
93 # this case)
98 # this case)
94 >>> fo.tell()
99 >>> fo.tell()
95 # read EOFs if an attempt is made to read past the last
100 # read EOFs if an attempt is made to read past the last
96 # byte in the range. the following will return only 7 bytes.
101 # byte in the range. the following will return only 7 bytes.
97 >>> fo.read(30)
102 >>> fo.read(30)
98 """
103 """
99
104
100 def __init__(self, fo, rangetup):
105 def __init__(self, fo, rangetup):
101 """Create a RangeableFileObject.
106 """Create a RangeableFileObject.
102 fo -- a file like object. only the read() method need be
107 fo -- a file like object. only the read() method need be
103 supported but supporting an optimized seek() is
108 supported but supporting an optimized seek() is
104 preferable.
109 preferable.
105 rangetup -- a (firstbyte,lastbyte) tuple specifying the range
110 rangetup -- a (firstbyte,lastbyte) tuple specifying the range
106 to work over.
111 to work over.
107 The file object provided is assumed to be at byte offset 0.
112 The file object provided is assumed to be at byte offset 0.
108 """
113 """
109 self.fo = fo
114 self.fo = fo
110 (self.firstbyte, self.lastbyte) = range_tuple_normalize(rangetup)
115 (self.firstbyte, self.lastbyte) = range_tuple_normalize(rangetup)
111 self.realpos = 0
116 self.realpos = 0
112 self._do_seek(self.firstbyte)
117 self._do_seek(self.firstbyte)
113
118
114 def __getattr__(self, name):
119 def __getattr__(self, name):
115 """This effectively allows us to wrap at the instance level.
120 """This effectively allows us to wrap at the instance level.
116 Any attribute not found in _this_ object will be searched for
121 Any attribute not found in _this_ object will be searched for
117 in self.fo. This includes methods."""
122 in self.fo. This includes methods."""
118 return getattr(self.fo, name)
123 return getattr(self.fo, name)
119
124
120 def tell(self):
125 def tell(self):
121 """Return the position within the range.
126 """Return the position within the range.
122 This is different from fo.seek in that position 0 is the
127 This is different from fo.seek in that position 0 is the
123 first byte position of the range tuple. For example, if
128 first byte position of the range tuple. For example, if
124 this object was created with a range tuple of (500,899),
129 this object was created with a range tuple of (500,899),
125 tell() will return 0 when at byte position 500 of the file.
130 tell() will return 0 when at byte position 500 of the file.
126 """
131 """
127 return (self.realpos - self.firstbyte)
132 return (self.realpos - self.firstbyte)
128
133
129 def seek(self, offset, whence=0):
134 def seek(self, offset, whence=0):
130 """Seek within the byte range.
135 """Seek within the byte range.
131 Positioning is identical to that described under tell().
136 Positioning is identical to that described under tell().
132 """
137 """
133 assert whence in (0, 1, 2)
138 assert whence in (0, 1, 2)
134 if whence == 0: # absolute seek
139 if whence == 0: # absolute seek
135 realoffset = self.firstbyte + offset
140 realoffset = self.firstbyte + offset
136 elif whence == 1: # relative seek
141 elif whence == 1: # relative seek
137 realoffset = self.realpos + offset
142 realoffset = self.realpos + offset
138 elif whence == 2: # absolute from end of file
143 elif whence == 2: # absolute from end of file
139 # XXX: are we raising the right Error here?
144 # XXX: are we raising the right Error here?
140 raise IOError('seek from end of file not supported.')
145 raise IOError('seek from end of file not supported.')
141
146
142 # do not allow seek past lastbyte in range
147 # do not allow seek past lastbyte in range
143 if self.lastbyte and (realoffset >= self.lastbyte):
148 if self.lastbyte and (realoffset >= self.lastbyte):
144 realoffset = self.lastbyte
149 realoffset = self.lastbyte
145
150
146 self._do_seek(realoffset - self.realpos)
151 self._do_seek(realoffset - self.realpos)
147
152
148 def read(self, size=-1):
153 def read(self, size=-1):
149 """Read within the range.
154 """Read within the range.
150 This method will limit the size read based on the range.
155 This method will limit the size read based on the range.
151 """
156 """
152 size = self._calc_read_size(size)
157 size = self._calc_read_size(size)
153 rslt = self.fo.read(size)
158 rslt = self.fo.read(size)
154 self.realpos += len(rslt)
159 self.realpos += len(rslt)
155 return rslt
160 return rslt
156
161
157 def readline(self, size=-1):
162 def readline(self, size=-1):
158 """Read lines within the range.
163 """Read lines within the range.
159 This method will limit the size read based on the range.
164 This method will limit the size read based on the range.
160 """
165 """
161 size = self._calc_read_size(size)
166 size = self._calc_read_size(size)
162 rslt = self.fo.readline(size)
167 rslt = self.fo.readline(size)
163 self.realpos += len(rslt)
168 self.realpos += len(rslt)
164 return rslt
169 return rslt
165
170
166 def _calc_read_size(self, size):
171 def _calc_read_size(self, size):
167 """Handles calculating the amount of data to read based on
172 """Handles calculating the amount of data to read based on
168 the range.
173 the range.
169 """
174 """
170 if self.lastbyte:
175 if self.lastbyte:
171 if size > -1:
176 if size > -1:
172 if ((self.realpos + size) >= self.lastbyte):
177 if ((self.realpos + size) >= self.lastbyte):
173 size = (self.lastbyte - self.realpos)
178 size = (self.lastbyte - self.realpos)
174 else:
179 else:
175 size = (self.lastbyte - self.realpos)
180 size = (self.lastbyte - self.realpos)
176 return size
181 return size
177
182
178 def _do_seek(self, offset):
183 def _do_seek(self, offset):
179 """Seek based on whether wrapped object supports seek().
184 """Seek based on whether wrapped object supports seek().
180 offset is relative to the current position (self.realpos).
185 offset is relative to the current position (self.realpos).
181 """
186 """
182 assert offset >= 0
187 assert offset >= 0
183 seek = getattr(self.fo, 'seek', self._poor_mans_seek)
188 seek = getattr(self.fo, 'seek', self._poor_mans_seek)
184 seek(self.realpos + offset)
189 seek(self.realpos + offset)
185 self.realpos += offset
190 self.realpos += offset
186
191
187 def _poor_mans_seek(self, offset):
192 def _poor_mans_seek(self, offset):
188 """Seek by calling the wrapped file objects read() method.
193 """Seek by calling the wrapped file objects read() method.
189 This is used for file like objects that do not have native
194 This is used for file like objects that do not have native
190 seek support. The wrapped objects read() method is called
195 seek support. The wrapped objects read() method is called
191 to manually seek to the desired position.
196 to manually seek to the desired position.
192 offset -- read this number of bytes from the wrapped
197 offset -- read this number of bytes from the wrapped
193 file object.
198 file object.
194 raise RangeError if we encounter EOF before reaching the
199 raise RangeError if we encounter EOF before reaching the
195 specified offset.
200 specified offset.
196 """
201 """
197 pos = 0
202 pos = 0
198 bufsize = 1024
203 bufsize = 1024
199 while pos < offset:
204 while pos < offset:
200 if (pos + bufsize) > offset:
205 if (pos + bufsize) > offset:
201 bufsize = offset - pos
206 bufsize = offset - pos
202 buf = self.fo.read(bufsize)
207 buf = self.fo.read(bufsize)
203 if len(buf) != bufsize:
208 if len(buf) != bufsize:
204 raise RangeError('Requested Range Not Satisfiable')
209 raise RangeError('Requested Range Not Satisfiable')
205 pos += bufsize
210 pos += bufsize
206
211
207 class FileRangeHandler(urllib2.FileHandler):
212 class FileRangeHandler(urlreq.filehandler):
208 """FileHandler subclass that adds Range support.
213 """FileHandler subclass that adds Range support.
209 This class handles Range headers exactly like an HTTP
214 This class handles Range headers exactly like an HTTP
210 server would.
215 server would.
211 """
216 """
212 def open_local_file(self, req):
217 def open_local_file(self, req):
213 host = req.get_host()
218 host = req.get_host()
214 file = req.get_selector()
219 file = req.get_selector()
215 localfile = urllib.url2pathname(file)
220 localfile = urlreq.url2pathname(file)
216 stats = os.stat(localfile)
221 stats = os.stat(localfile)
217 size = stats[stat.ST_SIZE]
222 size = stats[stat.ST_SIZE]
218 modified = email.Utils.formatdate(stats[stat.ST_MTIME])
223 modified = email.Utils.formatdate(stats[stat.ST_MTIME])
219 mtype = mimetypes.guess_type(file)[0]
224 mtype = mimetypes.guess_type(file)[0]
220 if host:
225 if host:
221 host, port = urllib.splitport(host)
226 host, port = urlreq.splitport(host)
222 if port or socket.gethostbyname(host) not in self.get_names():
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 fo = open(localfile,'rb')
229 fo = open(localfile,'rb')
225 brange = req.headers.get('Range', None)
230 brange = req.headers.get('Range', None)
226 brange = range_header_to_tuple(brange)
231 brange = range_header_to_tuple(brange)
227 assert brange != ()
232 assert brange != ()
228 if brange:
233 if brange:
229 (fb, lb) = brange
234 (fb, lb) = brange
230 if lb == '':
235 if lb == '':
231 lb = size
236 lb = size
232 if fb < 0 or fb > size or lb > size:
237 if fb < 0 or fb > size or lb > size:
233 raise RangeError('Requested Range Not Satisfiable')
238 raise RangeError('Requested Range Not Satisfiable')
234 size = (lb - fb)
239 size = (lb - fb)
235 fo = RangeableFileObject(fo, (fb, lb))
240 fo = RangeableFileObject(fo, (fb, lb))
236 headers = email.message_from_string(
241 headers = email.message_from_string(
237 'Content-Type: %s\nContent-Length: %d\nLast-Modified: %s\n' %
242 'Content-Type: %s\nContent-Length: %d\nLast-Modified: %s\n' %
238 (mtype or 'text/plain', size, modified))
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 # FTP Range Support
247 # FTP Range Support
243 # Unfortunately, a large amount of base FTP code had to be copied
248 # Unfortunately, a large amount of base FTP code had to be copied
244 # from urllib and urllib2 in order to insert the FTP REST command.
249 # from urllib and urllib2 in order to insert the FTP REST command.
245 # Code modifications for range support have been commented as
250 # Code modifications for range support have been commented as
246 # follows:
251 # follows:
247 # -- range support modifications start/end here
252 # -- range support modifications start/end here
248
253
249 class FTPRangeHandler(urllib2.FTPHandler):
254 class FTPRangeHandler(urlreq.ftphandler):
250 def ftp_open(self, req):
255 def ftp_open(self, req):
251 host = req.get_host()
256 host = req.get_host()
252 if not host:
257 if not host:
253 raise IOError('ftp error', 'no host given')
258 raise IOError('ftp error', 'no host given')
254 host, port = splitport(host)
259 host, port = splitport(host)
255 if port is None:
260 if port is None:
256 port = ftplib.FTP_PORT
261 port = ftplib.FTP_PORT
257 else:
262 else:
258 port = int(port)
263 port = int(port)
259
264
260 # username/password handling
265 # username/password handling
261 user, host = splituser(host)
266 user, host = splituser(host)
262 if user:
267 if user:
263 user, passwd = splitpasswd(user)
268 user, passwd = splitpasswd(user)
264 else:
269 else:
265 passwd = None
270 passwd = None
266 host = unquote(host)
271 host = unquote(host)
267 user = unquote(user or '')
272 user = unquote(user or '')
268 passwd = unquote(passwd or '')
273 passwd = unquote(passwd or '')
269
274
270 try:
275 try:
271 host = socket.gethostbyname(host)
276 host = socket.gethostbyname(host)
272 except socket.error as msg:
277 except socket.error as msg:
273 raise urllib2.URLError(msg)
278 raise urlerr.urlerror(msg)
274 path, attrs = splitattr(req.get_selector())
279 path, attrs = splitattr(req.get_selector())
275 dirs = path.split('/')
280 dirs = path.split('/')
276 dirs = map(unquote, dirs)
281 dirs = map(unquote, dirs)
277 dirs, file = dirs[:-1], dirs[-1]
282 dirs, file = dirs[:-1], dirs[-1]
278 if dirs and not dirs[0]:
283 if dirs and not dirs[0]:
279 dirs = dirs[1:]
284 dirs = dirs[1:]
280 try:
285 try:
281 fw = self.connect_ftp(user, passwd, host, port, dirs)
286 fw = self.connect_ftp(user, passwd, host, port, dirs)
282 if file:
287 if file:
283 type = 'I'
288 type = 'I'
284 else:
289 else:
285 type = 'D'
290 type = 'D'
286
291
287 for attr in attrs:
292 for attr in attrs:
288 attr, value = splitattr(attr)
293 attr, value = splitattr(attr)
289 if attr.lower() == 'type' and \
294 if attr.lower() == 'type' and \
290 value in ('a', 'A', 'i', 'I', 'd', 'D'):
295 value in ('a', 'A', 'i', 'I', 'd', 'D'):
291 type = value.upper()
296 type = value.upper()
292
297
293 # -- range support modifications start here
298 # -- range support modifications start here
294 rest = None
299 rest = None
295 range_tup = range_header_to_tuple(req.headers.get('Range', None))
300 range_tup = range_header_to_tuple(req.headers.get('Range', None))
296 assert range_tup != ()
301 assert range_tup != ()
297 if range_tup:
302 if range_tup:
298 (fb, lb) = range_tup
303 (fb, lb) = range_tup
299 if fb > 0:
304 if fb > 0:
300 rest = fb
305 rest = fb
301 # -- range support modifications end here
306 # -- range support modifications end here
302
307
303 fp, retrlen = fw.retrfile(file, type, rest)
308 fp, retrlen = fw.retrfile(file, type, rest)
304
309
305 # -- range support modifications start here
310 # -- range support modifications start here
306 if range_tup:
311 if range_tup:
307 (fb, lb) = range_tup
312 (fb, lb) = range_tup
308 if lb == '':
313 if lb == '':
309 if retrlen is None or retrlen == 0:
314 if retrlen is None or retrlen == 0:
310 raise RangeError('Requested Range Not Satisfiable due'
315 raise RangeError('Requested Range Not Satisfiable due'
311 ' to unobtainable file length.')
316 ' to unobtainable file length.')
312 lb = retrlen
317 lb = retrlen
313 retrlen = lb - fb
318 retrlen = lb - fb
314 if retrlen < 0:
319 if retrlen < 0:
315 # beginning of range is larger than file
320 # beginning of range is larger than file
316 raise RangeError('Requested Range Not Satisfiable')
321 raise RangeError('Requested Range Not Satisfiable')
317 else:
322 else:
318 retrlen = lb - fb
323 retrlen = lb - fb
319 fp = RangeableFileObject(fp, (0, retrlen))
324 fp = RangeableFileObject(fp, (0, retrlen))
320 # -- range support modifications end here
325 # -- range support modifications end here
321
326
322 headers = ""
327 headers = ""
323 mtype = mimetypes.guess_type(req.get_full_url())[0]
328 mtype = mimetypes.guess_type(req.get_full_url())[0]
324 if mtype:
329 if mtype:
325 headers += "Content-Type: %s\n" % mtype
330 headers += "Content-Type: %s\n" % mtype
326 if retrlen is not None and retrlen >= 0:
331 if retrlen is not None and retrlen >= 0:
327 headers += "Content-Length: %d\n" % retrlen
332 headers += "Content-Length: %d\n" % retrlen
328 headers = email.message_from_string(headers)
333 headers = email.message_from_string(headers)
329 return addinfourl(fp, headers, req.get_full_url())
334 return addinfourl(fp, headers, req.get_full_url())
330 except ftplib.all_errors as msg:
335 except ftplib.all_errors as msg:
331 raise IOError('ftp error', msg)
336 raise IOError('ftp error', msg)
332
337
333 def connect_ftp(self, user, passwd, host, port, dirs):
338 def connect_ftp(self, user, passwd, host, port, dirs):
334 fw = ftpwrapper(user, passwd, host, port, dirs)
339 fw = ftpwrapper(user, passwd, host, port, dirs)
335 return fw
340 return fw
336
341
337 class ftpwrapper(urllib.ftpwrapper):
342 class ftpwrapper(urlreq.ftpwrapper):
338 # range support note:
343 # range support note:
339 # this ftpwrapper code is copied directly from
344 # this ftpwrapper code is copied directly from
340 # urllib. The only enhancement is to add the rest
345 # urllib. The only enhancement is to add the rest
341 # argument and pass it on to ftp.ntransfercmd
346 # argument and pass it on to ftp.ntransfercmd
342 def retrfile(self, file, type, rest=None):
347 def retrfile(self, file, type, rest=None):
343 self.endtransfer()
348 self.endtransfer()
344 if type in ('d', 'D'):
349 if type in ('d', 'D'):
345 cmd = 'TYPE A'
350 cmd = 'TYPE A'
346 isdir = 1
351 isdir = 1
347 else:
352 else:
348 cmd = 'TYPE ' + type
353 cmd = 'TYPE ' + type
349 isdir = 0
354 isdir = 0
350 try:
355 try:
351 self.ftp.voidcmd(cmd)
356 self.ftp.voidcmd(cmd)
352 except ftplib.all_errors:
357 except ftplib.all_errors:
353 self.init()
358 self.init()
354 self.ftp.voidcmd(cmd)
359 self.ftp.voidcmd(cmd)
355 conn = None
360 conn = None
356 if file and not isdir:
361 if file and not isdir:
357 # Use nlst to see if the file exists at all
362 # Use nlst to see if the file exists at all
358 try:
363 try:
359 self.ftp.nlst(file)
364 self.ftp.nlst(file)
360 except ftplib.error_perm as reason:
365 except ftplib.error_perm as reason:
361 raise IOError('ftp error', reason)
366 raise IOError('ftp error', reason)
362 # Restore the transfer mode!
367 # Restore the transfer mode!
363 self.ftp.voidcmd(cmd)
368 self.ftp.voidcmd(cmd)
364 # Try to retrieve as a file
369 # Try to retrieve as a file
365 try:
370 try:
366 cmd = 'RETR ' + file
371 cmd = 'RETR ' + file
367 conn = self.ftp.ntransfercmd(cmd, rest)
372 conn = self.ftp.ntransfercmd(cmd, rest)
368 except ftplib.error_perm as reason:
373 except ftplib.error_perm as reason:
369 if str(reason).startswith('501'):
374 if str(reason).startswith('501'):
370 # workaround for REST not supported error
375 # workaround for REST not supported error
371 fp, retrlen = self.retrfile(file, type)
376 fp, retrlen = self.retrfile(file, type)
372 fp = RangeableFileObject(fp, (rest,''))
377 fp = RangeableFileObject(fp, (rest,''))
373 return (fp, retrlen)
378 return (fp, retrlen)
374 elif not str(reason).startswith('550'):
379 elif not str(reason).startswith('550'):
375 raise IOError('ftp error', reason)
380 raise IOError('ftp error', reason)
376 if not conn:
381 if not conn:
377 # Set transfer mode to ASCII!
382 # Set transfer mode to ASCII!
378 self.ftp.voidcmd('TYPE A')
383 self.ftp.voidcmd('TYPE A')
379 # Try a directory listing
384 # Try a directory listing
380 if file:
385 if file:
381 cmd = 'LIST ' + file
386 cmd = 'LIST ' + file
382 else:
387 else:
383 cmd = 'LIST'
388 cmd = 'LIST'
384 conn = self.ftp.ntransfercmd(cmd)
389 conn = self.ftp.ntransfercmd(cmd)
385 self.busy = 1
390 self.busy = 1
386 # Pass back both a suitably decorated object and a retrieval length
391 # Pass back both a suitably decorated object and a retrieval length
387 return (addclosehook(conn[0].makefile('rb'),
392 return (addclosehook(conn[0].makefile('rb'),
388 self.endtransfer), conn[1])
393 self.endtransfer), conn[1])
389
394
390
395
391 ####################################################################
396 ####################################################################
392 # Range Tuple Functions
397 # Range Tuple Functions
393 # XXX: These range tuple functions might go better in a class.
398 # XXX: These range tuple functions might go better in a class.
394
399
395 _rangere = None
400 _rangere = None
396 def range_header_to_tuple(range_header):
401 def range_header_to_tuple(range_header):
397 """Get a (firstbyte,lastbyte) tuple from a Range header value.
402 """Get a (firstbyte,lastbyte) tuple from a Range header value.
398
403
399 Range headers have the form "bytes=<firstbyte>-<lastbyte>". This
404 Range headers have the form "bytes=<firstbyte>-<lastbyte>". This
400 function pulls the firstbyte and lastbyte values and returns
405 function pulls the firstbyte and lastbyte values and returns
401 a (firstbyte,lastbyte) tuple. If lastbyte is not specified in
406 a (firstbyte,lastbyte) tuple. If lastbyte is not specified in
402 the header value, it is returned as an empty string in the
407 the header value, it is returned as an empty string in the
403 tuple.
408 tuple.
404
409
405 Return None if range_header is None
410 Return None if range_header is None
406 Return () if range_header does not conform to the range spec
411 Return () if range_header does not conform to the range spec
407 pattern.
412 pattern.
408
413
409 """
414 """
410 global _rangere
415 global _rangere
411 if range_header is None:
416 if range_header is None:
412 return None
417 return None
413 if _rangere is None:
418 if _rangere is None:
414 _rangere = re.compile(r'^bytes=(\d{1,})-(\d*)')
419 _rangere = re.compile(r'^bytes=(\d{1,})-(\d*)')
415 match = _rangere.match(range_header)
420 match = _rangere.match(range_header)
416 if match:
421 if match:
417 tup = range_tuple_normalize(match.group(1, 2))
422 tup = range_tuple_normalize(match.group(1, 2))
418 if tup and tup[1]:
423 if tup and tup[1]:
419 tup = (tup[0], tup[1]+1)
424 tup = (tup[0], tup[1]+1)
420 return tup
425 return tup
421 return ()
426 return ()
422
427
423 def range_tuple_to_header(range_tup):
428 def range_tuple_to_header(range_tup):
424 """Convert a range tuple to a Range header value.
429 """Convert a range tuple to a Range header value.
425 Return a string of the form "bytes=<firstbyte>-<lastbyte>" or None
430 Return a string of the form "bytes=<firstbyte>-<lastbyte>" or None
426 if no range is needed.
431 if no range is needed.
427 """
432 """
428 if range_tup is None:
433 if range_tup is None:
429 return None
434 return None
430 range_tup = range_tuple_normalize(range_tup)
435 range_tup = range_tuple_normalize(range_tup)
431 if range_tup:
436 if range_tup:
432 if range_tup[1]:
437 if range_tup[1]:
433 range_tup = (range_tup[0], range_tup[1] - 1)
438 range_tup = (range_tup[0], range_tup[1] - 1)
434 return 'bytes=%s-%s' % range_tup
439 return 'bytes=%s-%s' % range_tup
435
440
436 def range_tuple_normalize(range_tup):
441 def range_tuple_normalize(range_tup):
437 """Normalize a (first_byte,last_byte) range tuple.
442 """Normalize a (first_byte,last_byte) range tuple.
438 Return a tuple whose first element is guaranteed to be an int
443 Return a tuple whose first element is guaranteed to be an int
439 and whose second element will be '' (meaning: the last byte) or
444 and whose second element will be '' (meaning: the last byte) or
440 an int. Finally, return None if the normalized tuple == (0,'')
445 an int. Finally, return None if the normalized tuple == (0,'')
441 as that is equivalent to retrieving the entire file.
446 as that is equivalent to retrieving the entire file.
442 """
447 """
443 if range_tup is None:
448 if range_tup is None:
444 return None
449 return None
445 # handle first byte
450 # handle first byte
446 fb = range_tup[0]
451 fb = range_tup[0]
447 if fb in (None, ''):
452 if fb in (None, ''):
448 fb = 0
453 fb = 0
449 else:
454 else:
450 fb = int(fb)
455 fb = int(fb)
451 # handle last byte
456 # handle last byte
452 try:
457 try:
453 lb = range_tup[1]
458 lb = range_tup[1]
454 except IndexError:
459 except IndexError:
455 lb = ''
460 lb = ''
456 else:
461 else:
457 if lb is None:
462 if lb is None:
458 lb = ''
463 lb = ''
459 elif lb != '':
464 elif lb != '':
460 lb = int(lb)
465 lb = int(lb)
461 # check if range is over the entire file
466 # check if range is over the entire file
462 if (fb, lb) == (0, ''):
467 if (fb, lb) == (0, ''):
463 return None
468 return None
464 # check that the range is valid
469 # check that the range is valid
465 if lb < fb:
470 if lb < fb:
466 raise RangeError('Invalid byte range: %s-%s' % (fb, lb))
471 raise RangeError('Invalid byte range: %s-%s' % (fb, lb))
467 return (fb, lb)
472 return (fb, lb)
@@ -1,1929 +1,1930 b''
1 # exchange.py - utility to exchange data between repos.
1 # exchange.py - utility to exchange data between repos.
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import urllib
12 import urllib2
13
11
14 from .i18n import _
12 from .i18n import _
15 from .node import (
13 from .node import (
16 hex,
14 hex,
17 nullid,
15 nullid,
18 )
16 )
19 from . import (
17 from . import (
20 base85,
18 base85,
21 bookmarks as bookmod,
19 bookmarks as bookmod,
22 bundle2,
20 bundle2,
23 changegroup,
21 changegroup,
24 discovery,
22 discovery,
25 error,
23 error,
26 lock as lockmod,
24 lock as lockmod,
27 obsolete,
25 obsolete,
28 phases,
26 phases,
29 pushkey,
27 pushkey,
30 scmutil,
28 scmutil,
31 sslutil,
29 sslutil,
32 streamclone,
30 streamclone,
33 tags,
31 tags,
34 url as urlmod,
32 url as urlmod,
35 util,
33 util,
36 )
34 )
37
35
36 urlerr = util.urlerr
37 urlreq = util.urlreq
38
38 # Maps bundle compression human names to internal representation.
39 # Maps bundle compression human names to internal representation.
39 _bundlespeccompressions = {'none': None,
40 _bundlespeccompressions = {'none': None,
40 'bzip2': 'BZ',
41 'bzip2': 'BZ',
41 'gzip': 'GZ',
42 'gzip': 'GZ',
42 }
43 }
43
44
44 # Maps bundle version human names to changegroup versions.
45 # Maps bundle version human names to changegroup versions.
45 _bundlespeccgversions = {'v1': '01',
46 _bundlespeccgversions = {'v1': '01',
46 'v2': '02',
47 'v2': '02',
47 'packed1': 's1',
48 'packed1': 's1',
48 'bundle2': '02', #legacy
49 'bundle2': '02', #legacy
49 }
50 }
50
51
51 def parsebundlespec(repo, spec, strict=True, externalnames=False):
52 def parsebundlespec(repo, spec, strict=True, externalnames=False):
52 """Parse a bundle string specification into parts.
53 """Parse a bundle string specification into parts.
53
54
54 Bundle specifications denote a well-defined bundle/exchange format.
55 Bundle specifications denote a well-defined bundle/exchange format.
55 The content of a given specification should not change over time in
56 The content of a given specification should not change over time in
56 order to ensure that bundles produced by a newer version of Mercurial are
57 order to ensure that bundles produced by a newer version of Mercurial are
57 readable from an older version.
58 readable from an older version.
58
59
59 The string currently has the form:
60 The string currently has the form:
60
61
61 <compression>-<type>[;<parameter0>[;<parameter1>]]
62 <compression>-<type>[;<parameter0>[;<parameter1>]]
62
63
63 Where <compression> is one of the supported compression formats
64 Where <compression> is one of the supported compression formats
64 and <type> is (currently) a version string. A ";" can follow the type and
65 and <type> is (currently) a version string. A ";" can follow the type and
65 all text afterwards is interpretted as URI encoded, ";" delimited key=value
66 all text afterwards is interpretted as URI encoded, ";" delimited key=value
66 pairs.
67 pairs.
67
68
68 If ``strict`` is True (the default) <compression> is required. Otherwise,
69 If ``strict`` is True (the default) <compression> is required. Otherwise,
69 it is optional.
70 it is optional.
70
71
71 If ``externalnames`` is False (the default), the human-centric names will
72 If ``externalnames`` is False (the default), the human-centric names will
72 be converted to their internal representation.
73 be converted to their internal representation.
73
74
74 Returns a 3-tuple of (compression, version, parameters). Compression will
75 Returns a 3-tuple of (compression, version, parameters). Compression will
75 be ``None`` if not in strict mode and a compression isn't defined.
76 be ``None`` if not in strict mode and a compression isn't defined.
76
77
77 An ``InvalidBundleSpecification`` is raised when the specification is
78 An ``InvalidBundleSpecification`` is raised when the specification is
78 not syntactically well formed.
79 not syntactically well formed.
79
80
80 An ``UnsupportedBundleSpecification`` is raised when the compression or
81 An ``UnsupportedBundleSpecification`` is raised when the compression or
81 bundle type/version is not recognized.
82 bundle type/version is not recognized.
82
83
83 Note: this function will likely eventually return a more complex data
84 Note: this function will likely eventually return a more complex data
84 structure, including bundle2 part information.
85 structure, including bundle2 part information.
85 """
86 """
86 def parseparams(s):
87 def parseparams(s):
87 if ';' not in s:
88 if ';' not in s:
88 return s, {}
89 return s, {}
89
90
90 params = {}
91 params = {}
91 version, paramstr = s.split(';', 1)
92 version, paramstr = s.split(';', 1)
92
93
93 for p in paramstr.split(';'):
94 for p in paramstr.split(';'):
94 if '=' not in p:
95 if '=' not in p:
95 raise error.InvalidBundleSpecification(
96 raise error.InvalidBundleSpecification(
96 _('invalid bundle specification: '
97 _('invalid bundle specification: '
97 'missing "=" in parameter: %s') % p)
98 'missing "=" in parameter: %s') % p)
98
99
99 key, value = p.split('=', 1)
100 key, value = p.split('=', 1)
100 key = urllib.unquote(key)
101 key = urlreq.unquote(key)
101 value = urllib.unquote(value)
102 value = urlreq.unquote(value)
102 params[key] = value
103 params[key] = value
103
104
104 return version, params
105 return version, params
105
106
106
107
107 if strict and '-' not in spec:
108 if strict and '-' not in spec:
108 raise error.InvalidBundleSpecification(
109 raise error.InvalidBundleSpecification(
109 _('invalid bundle specification; '
110 _('invalid bundle specification; '
110 'must be prefixed with compression: %s') % spec)
111 'must be prefixed with compression: %s') % spec)
111
112
112 if '-' in spec:
113 if '-' in spec:
113 compression, version = spec.split('-', 1)
114 compression, version = spec.split('-', 1)
114
115
115 if compression not in _bundlespeccompressions:
116 if compression not in _bundlespeccompressions:
116 raise error.UnsupportedBundleSpecification(
117 raise error.UnsupportedBundleSpecification(
117 _('%s compression is not supported') % compression)
118 _('%s compression is not supported') % compression)
118
119
119 version, params = parseparams(version)
120 version, params = parseparams(version)
120
121
121 if version not in _bundlespeccgversions:
122 if version not in _bundlespeccgversions:
122 raise error.UnsupportedBundleSpecification(
123 raise error.UnsupportedBundleSpecification(
123 _('%s is not a recognized bundle version') % version)
124 _('%s is not a recognized bundle version') % version)
124 else:
125 else:
125 # Value could be just the compression or just the version, in which
126 # Value could be just the compression or just the version, in which
126 # case some defaults are assumed (but only when not in strict mode).
127 # case some defaults are assumed (but only when not in strict mode).
127 assert not strict
128 assert not strict
128
129
129 spec, params = parseparams(spec)
130 spec, params = parseparams(spec)
130
131
131 if spec in _bundlespeccompressions:
132 if spec in _bundlespeccompressions:
132 compression = spec
133 compression = spec
133 version = 'v1'
134 version = 'v1'
134 if 'generaldelta' in repo.requirements:
135 if 'generaldelta' in repo.requirements:
135 version = 'v2'
136 version = 'v2'
136 elif spec in _bundlespeccgversions:
137 elif spec in _bundlespeccgversions:
137 if spec == 'packed1':
138 if spec == 'packed1':
138 compression = 'none'
139 compression = 'none'
139 else:
140 else:
140 compression = 'bzip2'
141 compression = 'bzip2'
141 version = spec
142 version = spec
142 else:
143 else:
143 raise error.UnsupportedBundleSpecification(
144 raise error.UnsupportedBundleSpecification(
144 _('%s is not a recognized bundle specification') % spec)
145 _('%s is not a recognized bundle specification') % spec)
145
146
146 # The specification for packed1 can optionally declare the data formats
147 # The specification for packed1 can optionally declare the data formats
147 # required to apply it. If we see this metadata, compare against what the
148 # required to apply it. If we see this metadata, compare against what the
148 # repo supports and error if the bundle isn't compatible.
149 # repo supports and error if the bundle isn't compatible.
149 if version == 'packed1' and 'requirements' in params:
150 if version == 'packed1' and 'requirements' in params:
150 requirements = set(params['requirements'].split(','))
151 requirements = set(params['requirements'].split(','))
151 missingreqs = requirements - repo.supportedformats
152 missingreqs = requirements - repo.supportedformats
152 if missingreqs:
153 if missingreqs:
153 raise error.UnsupportedBundleSpecification(
154 raise error.UnsupportedBundleSpecification(
154 _('missing support for repository features: %s') %
155 _('missing support for repository features: %s') %
155 ', '.join(sorted(missingreqs)))
156 ', '.join(sorted(missingreqs)))
156
157
157 if not externalnames:
158 if not externalnames:
158 compression = _bundlespeccompressions[compression]
159 compression = _bundlespeccompressions[compression]
159 version = _bundlespeccgversions[version]
160 version = _bundlespeccgversions[version]
160 return compression, version, params
161 return compression, version, params
161
162
162 def readbundle(ui, fh, fname, vfs=None):
163 def readbundle(ui, fh, fname, vfs=None):
163 header = changegroup.readexactly(fh, 4)
164 header = changegroup.readexactly(fh, 4)
164
165
165 alg = None
166 alg = None
166 if not fname:
167 if not fname:
167 fname = "stream"
168 fname = "stream"
168 if not header.startswith('HG') and header.startswith('\0'):
169 if not header.startswith('HG') and header.startswith('\0'):
169 fh = changegroup.headerlessfixup(fh, header)
170 fh = changegroup.headerlessfixup(fh, header)
170 header = "HG10"
171 header = "HG10"
171 alg = 'UN'
172 alg = 'UN'
172 elif vfs:
173 elif vfs:
173 fname = vfs.join(fname)
174 fname = vfs.join(fname)
174
175
175 magic, version = header[0:2], header[2:4]
176 magic, version = header[0:2], header[2:4]
176
177
177 if magic != 'HG':
178 if magic != 'HG':
178 raise error.Abort(_('%s: not a Mercurial bundle') % fname)
179 raise error.Abort(_('%s: not a Mercurial bundle') % fname)
179 if version == '10':
180 if version == '10':
180 if alg is None:
181 if alg is None:
181 alg = changegroup.readexactly(fh, 2)
182 alg = changegroup.readexactly(fh, 2)
182 return changegroup.cg1unpacker(fh, alg)
183 return changegroup.cg1unpacker(fh, alg)
183 elif version.startswith('2'):
184 elif version.startswith('2'):
184 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
185 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
185 elif version == 'S1':
186 elif version == 'S1':
186 return streamclone.streamcloneapplier(fh)
187 return streamclone.streamcloneapplier(fh)
187 else:
188 else:
188 raise error.Abort(_('%s: unknown bundle version %s') % (fname, version))
189 raise error.Abort(_('%s: unknown bundle version %s') % (fname, version))
189
190
190 def getbundlespec(ui, fh):
191 def getbundlespec(ui, fh):
191 """Infer the bundlespec from a bundle file handle.
192 """Infer the bundlespec from a bundle file handle.
192
193
193 The input file handle is seeked and the original seek position is not
194 The input file handle is seeked and the original seek position is not
194 restored.
195 restored.
195 """
196 """
196 def speccompression(alg):
197 def speccompression(alg):
197 for k, v in _bundlespeccompressions.items():
198 for k, v in _bundlespeccompressions.items():
198 if v == alg:
199 if v == alg:
199 return k
200 return k
200 return None
201 return None
201
202
202 b = readbundle(ui, fh, None)
203 b = readbundle(ui, fh, None)
203 if isinstance(b, changegroup.cg1unpacker):
204 if isinstance(b, changegroup.cg1unpacker):
204 alg = b._type
205 alg = b._type
205 if alg == '_truncatedBZ':
206 if alg == '_truncatedBZ':
206 alg = 'BZ'
207 alg = 'BZ'
207 comp = speccompression(alg)
208 comp = speccompression(alg)
208 if not comp:
209 if not comp:
209 raise error.Abort(_('unknown compression algorithm: %s') % alg)
210 raise error.Abort(_('unknown compression algorithm: %s') % alg)
210 return '%s-v1' % comp
211 return '%s-v1' % comp
211 elif isinstance(b, bundle2.unbundle20):
212 elif isinstance(b, bundle2.unbundle20):
212 if 'Compression' in b.params:
213 if 'Compression' in b.params:
213 comp = speccompression(b.params['Compression'])
214 comp = speccompression(b.params['Compression'])
214 if not comp:
215 if not comp:
215 raise error.Abort(_('unknown compression algorithm: %s') % comp)
216 raise error.Abort(_('unknown compression algorithm: %s') % comp)
216 else:
217 else:
217 comp = 'none'
218 comp = 'none'
218
219
219 version = None
220 version = None
220 for part in b.iterparts():
221 for part in b.iterparts():
221 if part.type == 'changegroup':
222 if part.type == 'changegroup':
222 version = part.params['version']
223 version = part.params['version']
223 if version in ('01', '02'):
224 if version in ('01', '02'):
224 version = 'v2'
225 version = 'v2'
225 else:
226 else:
226 raise error.Abort(_('changegroup version %s does not have '
227 raise error.Abort(_('changegroup version %s does not have '
227 'a known bundlespec') % version,
228 'a known bundlespec') % version,
228 hint=_('try upgrading your Mercurial '
229 hint=_('try upgrading your Mercurial '
229 'client'))
230 'client'))
230
231
231 if not version:
232 if not version:
232 raise error.Abort(_('could not identify changegroup version in '
233 raise error.Abort(_('could not identify changegroup version in '
233 'bundle'))
234 'bundle'))
234
235
235 return '%s-%s' % (comp, version)
236 return '%s-%s' % (comp, version)
236 elif isinstance(b, streamclone.streamcloneapplier):
237 elif isinstance(b, streamclone.streamcloneapplier):
237 requirements = streamclone.readbundle1header(fh)[2]
238 requirements = streamclone.readbundle1header(fh)[2]
238 params = 'requirements=%s' % ','.join(sorted(requirements))
239 params = 'requirements=%s' % ','.join(sorted(requirements))
239 return 'none-packed1;%s' % urllib.quote(params)
240 return 'none-packed1;%s' % urlreq.quote(params)
240 else:
241 else:
241 raise error.Abort(_('unknown bundle type: %s') % b)
242 raise error.Abort(_('unknown bundle type: %s') % b)
242
243
243 def buildobsmarkerspart(bundler, markers):
244 def buildobsmarkerspart(bundler, markers):
244 """add an obsmarker part to the bundler with <markers>
245 """add an obsmarker part to the bundler with <markers>
245
246
246 No part is created if markers is empty.
247 No part is created if markers is empty.
247 Raises ValueError if the bundler doesn't support any known obsmarker format.
248 Raises ValueError if the bundler doesn't support any known obsmarker format.
248 """
249 """
249 if markers:
250 if markers:
250 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
251 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
251 version = obsolete.commonversion(remoteversions)
252 version = obsolete.commonversion(remoteversions)
252 if version is None:
253 if version is None:
253 raise ValueError('bundler does not support common obsmarker format')
254 raise ValueError('bundler does not support common obsmarker format')
254 stream = obsolete.encodemarkers(markers, True, version=version)
255 stream = obsolete.encodemarkers(markers, True, version=version)
255 return bundler.newpart('obsmarkers', data=stream)
256 return bundler.newpart('obsmarkers', data=stream)
256 return None
257 return None
257
258
258 def _canusebundle2(op):
259 def _canusebundle2(op):
259 """return true if a pull/push can use bundle2
260 """return true if a pull/push can use bundle2
260
261
261 Feel free to nuke this function when we drop the experimental option"""
262 Feel free to nuke this function when we drop the experimental option"""
262 return (op.repo.ui.configbool('experimental', 'bundle2-exp', True)
263 return (op.repo.ui.configbool('experimental', 'bundle2-exp', True)
263 and op.remote.capable('bundle2'))
264 and op.remote.capable('bundle2'))
264
265
265
266
266 class pushoperation(object):
267 class pushoperation(object):
267 """A object that represent a single push operation
268 """A object that represent a single push operation
268
269
269 Its purpose is to carry push related state and very common operations.
270 Its purpose is to carry push related state and very common operations.
270
271
271 A new pushoperation should be created at the beginning of each push and
272 A new pushoperation should be created at the beginning of each push and
272 discarded afterward.
273 discarded afterward.
273 """
274 """
274
275
275 def __init__(self, repo, remote, force=False, revs=None, newbranch=False,
276 def __init__(self, repo, remote, force=False, revs=None, newbranch=False,
276 bookmarks=()):
277 bookmarks=()):
277 # repo we push from
278 # repo we push from
278 self.repo = repo
279 self.repo = repo
279 self.ui = repo.ui
280 self.ui = repo.ui
280 # repo we push to
281 # repo we push to
281 self.remote = remote
282 self.remote = remote
282 # force option provided
283 # force option provided
283 self.force = force
284 self.force = force
284 # revs to be pushed (None is "all")
285 # revs to be pushed (None is "all")
285 self.revs = revs
286 self.revs = revs
286 # bookmark explicitly pushed
287 # bookmark explicitly pushed
287 self.bookmarks = bookmarks
288 self.bookmarks = bookmarks
288 # allow push of new branch
289 # allow push of new branch
289 self.newbranch = newbranch
290 self.newbranch = newbranch
290 # did a local lock get acquired?
291 # did a local lock get acquired?
291 self.locallocked = None
292 self.locallocked = None
292 # step already performed
293 # step already performed
293 # (used to check what steps have been already performed through bundle2)
294 # (used to check what steps have been already performed through bundle2)
294 self.stepsdone = set()
295 self.stepsdone = set()
295 # Integer version of the changegroup push result
296 # Integer version of the changegroup push result
296 # - None means nothing to push
297 # - None means nothing to push
297 # - 0 means HTTP error
298 # - 0 means HTTP error
298 # - 1 means we pushed and remote head count is unchanged *or*
299 # - 1 means we pushed and remote head count is unchanged *or*
299 # we have outgoing changesets but refused to push
300 # we have outgoing changesets but refused to push
300 # - other values as described by addchangegroup()
301 # - other values as described by addchangegroup()
301 self.cgresult = None
302 self.cgresult = None
302 # Boolean value for the bookmark push
303 # Boolean value for the bookmark push
303 self.bkresult = None
304 self.bkresult = None
304 # discover.outgoing object (contains common and outgoing data)
305 # discover.outgoing object (contains common and outgoing data)
305 self.outgoing = None
306 self.outgoing = None
306 # all remote heads before the push
307 # all remote heads before the push
307 self.remoteheads = None
308 self.remoteheads = None
308 # testable as a boolean indicating if any nodes are missing locally.
309 # testable as a boolean indicating if any nodes are missing locally.
309 self.incoming = None
310 self.incoming = None
310 # phases changes that must be pushed along side the changesets
311 # phases changes that must be pushed along side the changesets
311 self.outdatedphases = None
312 self.outdatedphases = None
312 # phases changes that must be pushed if changeset push fails
313 # phases changes that must be pushed if changeset push fails
313 self.fallbackoutdatedphases = None
314 self.fallbackoutdatedphases = None
314 # outgoing obsmarkers
315 # outgoing obsmarkers
315 self.outobsmarkers = set()
316 self.outobsmarkers = set()
316 # outgoing bookmarks
317 # outgoing bookmarks
317 self.outbookmarks = []
318 self.outbookmarks = []
318 # transaction manager
319 # transaction manager
319 self.trmanager = None
320 self.trmanager = None
320 # map { pushkey partid -> callback handling failure}
321 # map { pushkey partid -> callback handling failure}
321 # used to handle exception from mandatory pushkey part failure
322 # used to handle exception from mandatory pushkey part failure
322 self.pkfailcb = {}
323 self.pkfailcb = {}
323
324
324 @util.propertycache
325 @util.propertycache
325 def futureheads(self):
326 def futureheads(self):
326 """future remote heads if the changeset push succeeds"""
327 """future remote heads if the changeset push succeeds"""
327 return self.outgoing.missingheads
328 return self.outgoing.missingheads
328
329
329 @util.propertycache
330 @util.propertycache
330 def fallbackheads(self):
331 def fallbackheads(self):
331 """future remote heads if the changeset push fails"""
332 """future remote heads if the changeset push fails"""
332 if self.revs is None:
333 if self.revs is None:
333 # not target to push, all common are relevant
334 # not target to push, all common are relevant
334 return self.outgoing.commonheads
335 return self.outgoing.commonheads
335 unfi = self.repo.unfiltered()
336 unfi = self.repo.unfiltered()
336 # I want cheads = heads(::missingheads and ::commonheads)
337 # I want cheads = heads(::missingheads and ::commonheads)
337 # (missingheads is revs with secret changeset filtered out)
338 # (missingheads is revs with secret changeset filtered out)
338 #
339 #
339 # This can be expressed as:
340 # This can be expressed as:
340 # cheads = ( (missingheads and ::commonheads)
341 # cheads = ( (missingheads and ::commonheads)
341 # + (commonheads and ::missingheads))"
342 # + (commonheads and ::missingheads))"
342 # )
343 # )
343 #
344 #
344 # while trying to push we already computed the following:
345 # while trying to push we already computed the following:
345 # common = (::commonheads)
346 # common = (::commonheads)
346 # missing = ((commonheads::missingheads) - commonheads)
347 # missing = ((commonheads::missingheads) - commonheads)
347 #
348 #
348 # We can pick:
349 # We can pick:
349 # * missingheads part of common (::commonheads)
350 # * missingheads part of common (::commonheads)
350 common = self.outgoing.common
351 common = self.outgoing.common
351 nm = self.repo.changelog.nodemap
352 nm = self.repo.changelog.nodemap
352 cheads = [node for node in self.revs if nm[node] in common]
353 cheads = [node for node in self.revs if nm[node] in common]
353 # and
354 # and
354 # * commonheads parents on missing
355 # * commonheads parents on missing
355 revset = unfi.set('%ln and parents(roots(%ln))',
356 revset = unfi.set('%ln and parents(roots(%ln))',
356 self.outgoing.commonheads,
357 self.outgoing.commonheads,
357 self.outgoing.missing)
358 self.outgoing.missing)
358 cheads.extend(c.node() for c in revset)
359 cheads.extend(c.node() for c in revset)
359 return cheads
360 return cheads
360
361
361 @property
362 @property
362 def commonheads(self):
363 def commonheads(self):
363 """set of all common heads after changeset bundle push"""
364 """set of all common heads after changeset bundle push"""
364 if self.cgresult:
365 if self.cgresult:
365 return self.futureheads
366 return self.futureheads
366 else:
367 else:
367 return self.fallbackheads
368 return self.fallbackheads
368
369
369 # mapping of message used when pushing bookmark
370 # mapping of message used when pushing bookmark
370 bookmsgmap = {'update': (_("updating bookmark %s\n"),
371 bookmsgmap = {'update': (_("updating bookmark %s\n"),
371 _('updating bookmark %s failed!\n')),
372 _('updating bookmark %s failed!\n')),
372 'export': (_("exporting bookmark %s\n"),
373 'export': (_("exporting bookmark %s\n"),
373 _('exporting bookmark %s failed!\n')),
374 _('exporting bookmark %s failed!\n')),
374 'delete': (_("deleting remote bookmark %s\n"),
375 'delete': (_("deleting remote bookmark %s\n"),
375 _('deleting remote bookmark %s failed!\n')),
376 _('deleting remote bookmark %s failed!\n')),
376 }
377 }
377
378
378
379
379 def push(repo, remote, force=False, revs=None, newbranch=False, bookmarks=(),
380 def push(repo, remote, force=False, revs=None, newbranch=False, bookmarks=(),
380 opargs=None):
381 opargs=None):
381 '''Push outgoing changesets (limited by revs) from a local
382 '''Push outgoing changesets (limited by revs) from a local
382 repository to remote. Return an integer:
383 repository to remote. Return an integer:
383 - None means nothing to push
384 - None means nothing to push
384 - 0 means HTTP error
385 - 0 means HTTP error
385 - 1 means we pushed and remote head count is unchanged *or*
386 - 1 means we pushed and remote head count is unchanged *or*
386 we have outgoing changesets but refused to push
387 we have outgoing changesets but refused to push
387 - other values as described by addchangegroup()
388 - other values as described by addchangegroup()
388 '''
389 '''
389 if opargs is None:
390 if opargs is None:
390 opargs = {}
391 opargs = {}
391 pushop = pushoperation(repo, remote, force, revs, newbranch, bookmarks,
392 pushop = pushoperation(repo, remote, force, revs, newbranch, bookmarks,
392 **opargs)
393 **opargs)
393 if pushop.remote.local():
394 if pushop.remote.local():
394 missing = (set(pushop.repo.requirements)
395 missing = (set(pushop.repo.requirements)
395 - pushop.remote.local().supported)
396 - pushop.remote.local().supported)
396 if missing:
397 if missing:
397 msg = _("required features are not"
398 msg = _("required features are not"
398 " supported in the destination:"
399 " supported in the destination:"
399 " %s") % (', '.join(sorted(missing)))
400 " %s") % (', '.join(sorted(missing)))
400 raise error.Abort(msg)
401 raise error.Abort(msg)
401
402
402 # there are two ways to push to remote repo:
403 # there are two ways to push to remote repo:
403 #
404 #
404 # addchangegroup assumes local user can lock remote
405 # addchangegroup assumes local user can lock remote
405 # repo (local filesystem, old ssh servers).
406 # repo (local filesystem, old ssh servers).
406 #
407 #
407 # unbundle assumes local user cannot lock remote repo (new ssh
408 # unbundle assumes local user cannot lock remote repo (new ssh
408 # servers, http servers).
409 # servers, http servers).
409
410
410 if not pushop.remote.canpush():
411 if not pushop.remote.canpush():
411 raise error.Abort(_("destination does not support push"))
412 raise error.Abort(_("destination does not support push"))
412 # get local lock as we might write phase data
413 # get local lock as we might write phase data
413 localwlock = locallock = None
414 localwlock = locallock = None
414 try:
415 try:
415 # bundle2 push may receive a reply bundle touching bookmarks or other
416 # bundle2 push may receive a reply bundle touching bookmarks or other
416 # things requiring the wlock. Take it now to ensure proper ordering.
417 # things requiring the wlock. Take it now to ensure proper ordering.
417 maypushback = pushop.ui.configbool('experimental', 'bundle2.pushback')
418 maypushback = pushop.ui.configbool('experimental', 'bundle2.pushback')
418 if _canusebundle2(pushop) and maypushback:
419 if _canusebundle2(pushop) and maypushback:
419 localwlock = pushop.repo.wlock()
420 localwlock = pushop.repo.wlock()
420 locallock = pushop.repo.lock()
421 locallock = pushop.repo.lock()
421 pushop.locallocked = True
422 pushop.locallocked = True
422 except IOError as err:
423 except IOError as err:
423 pushop.locallocked = False
424 pushop.locallocked = False
424 if err.errno != errno.EACCES:
425 if err.errno != errno.EACCES:
425 raise
426 raise
426 # source repo cannot be locked.
427 # source repo cannot be locked.
427 # We do not abort the push, but just disable the local phase
428 # We do not abort the push, but just disable the local phase
428 # synchronisation.
429 # synchronisation.
429 msg = 'cannot lock source repository: %s\n' % err
430 msg = 'cannot lock source repository: %s\n' % err
430 pushop.ui.debug(msg)
431 pushop.ui.debug(msg)
431 try:
432 try:
432 if pushop.locallocked:
433 if pushop.locallocked:
433 pushop.trmanager = transactionmanager(pushop.repo,
434 pushop.trmanager = transactionmanager(pushop.repo,
434 'push-response',
435 'push-response',
435 pushop.remote.url())
436 pushop.remote.url())
436 pushop.repo.checkpush(pushop)
437 pushop.repo.checkpush(pushop)
437 lock = None
438 lock = None
438 unbundle = pushop.remote.capable('unbundle')
439 unbundle = pushop.remote.capable('unbundle')
439 if not unbundle:
440 if not unbundle:
440 lock = pushop.remote.lock()
441 lock = pushop.remote.lock()
441 try:
442 try:
442 _pushdiscovery(pushop)
443 _pushdiscovery(pushop)
443 if _canusebundle2(pushop):
444 if _canusebundle2(pushop):
444 _pushbundle2(pushop)
445 _pushbundle2(pushop)
445 _pushchangeset(pushop)
446 _pushchangeset(pushop)
446 _pushsyncphase(pushop)
447 _pushsyncphase(pushop)
447 _pushobsolete(pushop)
448 _pushobsolete(pushop)
448 _pushbookmark(pushop)
449 _pushbookmark(pushop)
449 finally:
450 finally:
450 if lock is not None:
451 if lock is not None:
451 lock.release()
452 lock.release()
452 if pushop.trmanager:
453 if pushop.trmanager:
453 pushop.trmanager.close()
454 pushop.trmanager.close()
454 finally:
455 finally:
455 if pushop.trmanager:
456 if pushop.trmanager:
456 pushop.trmanager.release()
457 pushop.trmanager.release()
457 if locallock is not None:
458 if locallock is not None:
458 locallock.release()
459 locallock.release()
459 if localwlock is not None:
460 if localwlock is not None:
460 localwlock.release()
461 localwlock.release()
461
462
462 return pushop
463 return pushop
463
464
464 # list of steps to perform discovery before push
465 # list of steps to perform discovery before push
465 pushdiscoveryorder = []
466 pushdiscoveryorder = []
466
467
467 # Mapping between step name and function
468 # Mapping between step name and function
468 #
469 #
469 # This exists to help extensions wrap steps if necessary
470 # This exists to help extensions wrap steps if necessary
470 pushdiscoverymapping = {}
471 pushdiscoverymapping = {}
471
472
472 def pushdiscovery(stepname):
473 def pushdiscovery(stepname):
473 """decorator for function performing discovery before push
474 """decorator for function performing discovery before push
474
475
475 The function is added to the step -> function mapping and appended to the
476 The function is added to the step -> function mapping and appended to the
476 list of steps. Beware that decorated function will be added in order (this
477 list of steps. Beware that decorated function will be added in order (this
477 may matter).
478 may matter).
478
479
479 You can only use this decorator for a new step, if you want to wrap a step
480 You can only use this decorator for a new step, if you want to wrap a step
480 from an extension, change the pushdiscovery dictionary directly."""
481 from an extension, change the pushdiscovery dictionary directly."""
481 def dec(func):
482 def dec(func):
482 assert stepname not in pushdiscoverymapping
483 assert stepname not in pushdiscoverymapping
483 pushdiscoverymapping[stepname] = func
484 pushdiscoverymapping[stepname] = func
484 pushdiscoveryorder.append(stepname)
485 pushdiscoveryorder.append(stepname)
485 return func
486 return func
486 return dec
487 return dec
487
488
488 def _pushdiscovery(pushop):
489 def _pushdiscovery(pushop):
489 """Run all discovery steps"""
490 """Run all discovery steps"""
490 for stepname in pushdiscoveryorder:
491 for stepname in pushdiscoveryorder:
491 step = pushdiscoverymapping[stepname]
492 step = pushdiscoverymapping[stepname]
492 step(pushop)
493 step(pushop)
493
494
494 @pushdiscovery('changeset')
495 @pushdiscovery('changeset')
495 def _pushdiscoverychangeset(pushop):
496 def _pushdiscoverychangeset(pushop):
496 """discover the changeset that need to be pushed"""
497 """discover the changeset that need to be pushed"""
497 fci = discovery.findcommonincoming
498 fci = discovery.findcommonincoming
498 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
499 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
499 common, inc, remoteheads = commoninc
500 common, inc, remoteheads = commoninc
500 fco = discovery.findcommonoutgoing
501 fco = discovery.findcommonoutgoing
501 outgoing = fco(pushop.repo, pushop.remote, onlyheads=pushop.revs,
502 outgoing = fco(pushop.repo, pushop.remote, onlyheads=pushop.revs,
502 commoninc=commoninc, force=pushop.force)
503 commoninc=commoninc, force=pushop.force)
503 pushop.outgoing = outgoing
504 pushop.outgoing = outgoing
504 pushop.remoteheads = remoteheads
505 pushop.remoteheads = remoteheads
505 pushop.incoming = inc
506 pushop.incoming = inc
506
507
507 @pushdiscovery('phase')
508 @pushdiscovery('phase')
508 def _pushdiscoveryphase(pushop):
509 def _pushdiscoveryphase(pushop):
509 """discover the phase that needs to be pushed
510 """discover the phase that needs to be pushed
510
511
511 (computed for both success and failure case for changesets push)"""
512 (computed for both success and failure case for changesets push)"""
512 outgoing = pushop.outgoing
513 outgoing = pushop.outgoing
513 unfi = pushop.repo.unfiltered()
514 unfi = pushop.repo.unfiltered()
514 remotephases = pushop.remote.listkeys('phases')
515 remotephases = pushop.remote.listkeys('phases')
515 publishing = remotephases.get('publishing', False)
516 publishing = remotephases.get('publishing', False)
516 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
517 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
517 and remotephases # server supports phases
518 and remotephases # server supports phases
518 and not pushop.outgoing.missing # no changesets to be pushed
519 and not pushop.outgoing.missing # no changesets to be pushed
519 and publishing):
520 and publishing):
520 # When:
521 # When:
521 # - this is a subrepo push
522 # - this is a subrepo push
522 # - and remote support phase
523 # - and remote support phase
523 # - and no changeset are to be pushed
524 # - and no changeset are to be pushed
524 # - and remote is publishing
525 # - and remote is publishing
525 # We may be in issue 3871 case!
526 # We may be in issue 3871 case!
526 # We drop the possible phase synchronisation done by
527 # We drop the possible phase synchronisation done by
527 # courtesy to publish changesets possibly locally draft
528 # courtesy to publish changesets possibly locally draft
528 # on the remote.
529 # on the remote.
529 remotephases = {'publishing': 'True'}
530 remotephases = {'publishing': 'True'}
530 ana = phases.analyzeremotephases(pushop.repo,
531 ana = phases.analyzeremotephases(pushop.repo,
531 pushop.fallbackheads,
532 pushop.fallbackheads,
532 remotephases)
533 remotephases)
533 pheads, droots = ana
534 pheads, droots = ana
534 extracond = ''
535 extracond = ''
535 if not publishing:
536 if not publishing:
536 extracond = ' and public()'
537 extracond = ' and public()'
537 revset = 'heads((%%ln::%%ln) %s)' % extracond
538 revset = 'heads((%%ln::%%ln) %s)' % extracond
538 # Get the list of all revs draft on remote by public here.
539 # Get the list of all revs draft on remote by public here.
539 # XXX Beware that revset break if droots is not strictly
540 # XXX Beware that revset break if droots is not strictly
540 # XXX root we may want to ensure it is but it is costly
541 # XXX root we may want to ensure it is but it is costly
541 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
542 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
542 if not outgoing.missing:
543 if not outgoing.missing:
543 future = fallback
544 future = fallback
544 else:
545 else:
545 # adds changeset we are going to push as draft
546 # adds changeset we are going to push as draft
546 #
547 #
547 # should not be necessary for publishing server, but because of an
548 # should not be necessary for publishing server, but because of an
548 # issue fixed in xxxxx we have to do it anyway.
549 # issue fixed in xxxxx we have to do it anyway.
549 fdroots = list(unfi.set('roots(%ln + %ln::)',
550 fdroots = list(unfi.set('roots(%ln + %ln::)',
550 outgoing.missing, droots))
551 outgoing.missing, droots))
551 fdroots = [f.node() for f in fdroots]
552 fdroots = [f.node() for f in fdroots]
552 future = list(unfi.set(revset, fdroots, pushop.futureheads))
553 future = list(unfi.set(revset, fdroots, pushop.futureheads))
553 pushop.outdatedphases = future
554 pushop.outdatedphases = future
554 pushop.fallbackoutdatedphases = fallback
555 pushop.fallbackoutdatedphases = fallback
555
556
556 @pushdiscovery('obsmarker')
557 @pushdiscovery('obsmarker')
557 def _pushdiscoveryobsmarkers(pushop):
558 def _pushdiscoveryobsmarkers(pushop):
558 if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt)
559 if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt)
559 and pushop.repo.obsstore
560 and pushop.repo.obsstore
560 and 'obsolete' in pushop.remote.listkeys('namespaces')):
561 and 'obsolete' in pushop.remote.listkeys('namespaces')):
561 repo = pushop.repo
562 repo = pushop.repo
562 # very naive computation, that can be quite expensive on big repo.
563 # very naive computation, that can be quite expensive on big repo.
563 # However: evolution is currently slow on them anyway.
564 # However: evolution is currently slow on them anyway.
564 nodes = (c.node() for c in repo.set('::%ln', pushop.futureheads))
565 nodes = (c.node() for c in repo.set('::%ln', pushop.futureheads))
565 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
566 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
566
567
567 @pushdiscovery('bookmarks')
568 @pushdiscovery('bookmarks')
568 def _pushdiscoverybookmarks(pushop):
569 def _pushdiscoverybookmarks(pushop):
569 ui = pushop.ui
570 ui = pushop.ui
570 repo = pushop.repo.unfiltered()
571 repo = pushop.repo.unfiltered()
571 remote = pushop.remote
572 remote = pushop.remote
572 ui.debug("checking for updated bookmarks\n")
573 ui.debug("checking for updated bookmarks\n")
573 ancestors = ()
574 ancestors = ()
574 if pushop.revs:
575 if pushop.revs:
575 revnums = map(repo.changelog.rev, pushop.revs)
576 revnums = map(repo.changelog.rev, pushop.revs)
576 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
577 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
577 remotebookmark = remote.listkeys('bookmarks')
578 remotebookmark = remote.listkeys('bookmarks')
578
579
579 explicit = set([repo._bookmarks.expandname(bookmark)
580 explicit = set([repo._bookmarks.expandname(bookmark)
580 for bookmark in pushop.bookmarks])
581 for bookmark in pushop.bookmarks])
581
582
582 comp = bookmod.compare(repo, repo._bookmarks, remotebookmark, srchex=hex)
583 comp = bookmod.compare(repo, repo._bookmarks, remotebookmark, srchex=hex)
583 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
584 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
584 for b, scid, dcid in advsrc:
585 for b, scid, dcid in advsrc:
585 if b in explicit:
586 if b in explicit:
586 explicit.remove(b)
587 explicit.remove(b)
587 if not ancestors or repo[scid].rev() in ancestors:
588 if not ancestors or repo[scid].rev() in ancestors:
588 pushop.outbookmarks.append((b, dcid, scid))
589 pushop.outbookmarks.append((b, dcid, scid))
589 # search added bookmark
590 # search added bookmark
590 for b, scid, dcid in addsrc:
591 for b, scid, dcid in addsrc:
591 if b in explicit:
592 if b in explicit:
592 explicit.remove(b)
593 explicit.remove(b)
593 pushop.outbookmarks.append((b, '', scid))
594 pushop.outbookmarks.append((b, '', scid))
594 # search for overwritten bookmark
595 # search for overwritten bookmark
595 for b, scid, dcid in advdst + diverge + differ:
596 for b, scid, dcid in advdst + diverge + differ:
596 if b in explicit:
597 if b in explicit:
597 explicit.remove(b)
598 explicit.remove(b)
598 pushop.outbookmarks.append((b, dcid, scid))
599 pushop.outbookmarks.append((b, dcid, scid))
599 # search for bookmark to delete
600 # search for bookmark to delete
600 for b, scid, dcid in adddst:
601 for b, scid, dcid in adddst:
601 if b in explicit:
602 if b in explicit:
602 explicit.remove(b)
603 explicit.remove(b)
603 # treat as "deleted locally"
604 # treat as "deleted locally"
604 pushop.outbookmarks.append((b, dcid, ''))
605 pushop.outbookmarks.append((b, dcid, ''))
605 # identical bookmarks shouldn't get reported
606 # identical bookmarks shouldn't get reported
606 for b, scid, dcid in same:
607 for b, scid, dcid in same:
607 if b in explicit:
608 if b in explicit:
608 explicit.remove(b)
609 explicit.remove(b)
609
610
610 if explicit:
611 if explicit:
611 explicit = sorted(explicit)
612 explicit = sorted(explicit)
612 # we should probably list all of them
613 # we should probably list all of them
613 ui.warn(_('bookmark %s does not exist on the local '
614 ui.warn(_('bookmark %s does not exist on the local '
614 'or remote repository!\n') % explicit[0])
615 'or remote repository!\n') % explicit[0])
615 pushop.bkresult = 2
616 pushop.bkresult = 2
616
617
617 pushop.outbookmarks.sort()
618 pushop.outbookmarks.sort()
618
619
619 def _pushcheckoutgoing(pushop):
620 def _pushcheckoutgoing(pushop):
620 outgoing = pushop.outgoing
621 outgoing = pushop.outgoing
621 unfi = pushop.repo.unfiltered()
622 unfi = pushop.repo.unfiltered()
622 if not outgoing.missing:
623 if not outgoing.missing:
623 # nothing to push
624 # nothing to push
624 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
625 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
625 return False
626 return False
626 # something to push
627 # something to push
627 if not pushop.force:
628 if not pushop.force:
628 # if repo.obsstore == False --> no obsolete
629 # if repo.obsstore == False --> no obsolete
629 # then, save the iteration
630 # then, save the iteration
630 if unfi.obsstore:
631 if unfi.obsstore:
631 # this message are here for 80 char limit reason
632 # this message are here for 80 char limit reason
632 mso = _("push includes obsolete changeset: %s!")
633 mso = _("push includes obsolete changeset: %s!")
633 mst = {"unstable": _("push includes unstable changeset: %s!"),
634 mst = {"unstable": _("push includes unstable changeset: %s!"),
634 "bumped": _("push includes bumped changeset: %s!"),
635 "bumped": _("push includes bumped changeset: %s!"),
635 "divergent": _("push includes divergent changeset: %s!")}
636 "divergent": _("push includes divergent changeset: %s!")}
636 # If we are to push if there is at least one
637 # If we are to push if there is at least one
637 # obsolete or unstable changeset in missing, at
638 # obsolete or unstable changeset in missing, at
638 # least one of the missinghead will be obsolete or
639 # least one of the missinghead will be obsolete or
639 # unstable. So checking heads only is ok
640 # unstable. So checking heads only is ok
640 for node in outgoing.missingheads:
641 for node in outgoing.missingheads:
641 ctx = unfi[node]
642 ctx = unfi[node]
642 if ctx.obsolete():
643 if ctx.obsolete():
643 raise error.Abort(mso % ctx)
644 raise error.Abort(mso % ctx)
644 elif ctx.troubled():
645 elif ctx.troubled():
645 raise error.Abort(mst[ctx.troubles()[0]] % ctx)
646 raise error.Abort(mst[ctx.troubles()[0]] % ctx)
646
647
647 discovery.checkheads(pushop)
648 discovery.checkheads(pushop)
648 return True
649 return True
649
650
650 # List of names of steps to perform for an outgoing bundle2, order matters.
651 # List of names of steps to perform for an outgoing bundle2, order matters.
651 b2partsgenorder = []
652 b2partsgenorder = []
652
653
653 # Mapping between step name and function
654 # Mapping between step name and function
654 #
655 #
655 # This exists to help extensions wrap steps if necessary
656 # This exists to help extensions wrap steps if necessary
656 b2partsgenmapping = {}
657 b2partsgenmapping = {}
657
658
658 def b2partsgenerator(stepname, idx=None):
659 def b2partsgenerator(stepname, idx=None):
659 """decorator for function generating bundle2 part
660 """decorator for function generating bundle2 part
660
661
661 The function is added to the step -> function mapping and appended to the
662 The function is added to the step -> function mapping and appended to the
662 list of steps. Beware that decorated functions will be added in order
663 list of steps. Beware that decorated functions will be added in order
663 (this may matter).
664 (this may matter).
664
665
665 You can only use this decorator for new steps, if you want to wrap a step
666 You can only use this decorator for new steps, if you want to wrap a step
666 from an extension, attack the b2partsgenmapping dictionary directly."""
667 from an extension, attack the b2partsgenmapping dictionary directly."""
667 def dec(func):
668 def dec(func):
668 assert stepname not in b2partsgenmapping
669 assert stepname not in b2partsgenmapping
669 b2partsgenmapping[stepname] = func
670 b2partsgenmapping[stepname] = func
670 if idx is None:
671 if idx is None:
671 b2partsgenorder.append(stepname)
672 b2partsgenorder.append(stepname)
672 else:
673 else:
673 b2partsgenorder.insert(idx, stepname)
674 b2partsgenorder.insert(idx, stepname)
674 return func
675 return func
675 return dec
676 return dec
676
677
677 def _pushb2ctxcheckheads(pushop, bundler):
678 def _pushb2ctxcheckheads(pushop, bundler):
678 """Generate race condition checking parts
679 """Generate race condition checking parts
679
680
680 Exists as an independent function to aid extensions
681 Exists as an independent function to aid extensions
681 """
682 """
682 if not pushop.force:
683 if not pushop.force:
683 bundler.newpart('check:heads', data=iter(pushop.remoteheads))
684 bundler.newpart('check:heads', data=iter(pushop.remoteheads))
684
685
685 @b2partsgenerator('changeset')
686 @b2partsgenerator('changeset')
686 def _pushb2ctx(pushop, bundler):
687 def _pushb2ctx(pushop, bundler):
687 """handle changegroup push through bundle2
688 """handle changegroup push through bundle2
688
689
689 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
690 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
690 """
691 """
691 if 'changesets' in pushop.stepsdone:
692 if 'changesets' in pushop.stepsdone:
692 return
693 return
693 pushop.stepsdone.add('changesets')
694 pushop.stepsdone.add('changesets')
694 # Send known heads to the server for race detection.
695 # Send known heads to the server for race detection.
695 if not _pushcheckoutgoing(pushop):
696 if not _pushcheckoutgoing(pushop):
696 return
697 return
697 pushop.repo.prepushoutgoinghooks(pushop)
698 pushop.repo.prepushoutgoinghooks(pushop)
698
699
699 _pushb2ctxcheckheads(pushop, bundler)
700 _pushb2ctxcheckheads(pushop, bundler)
700
701
701 b2caps = bundle2.bundle2caps(pushop.remote)
702 b2caps = bundle2.bundle2caps(pushop.remote)
702 version = '01'
703 version = '01'
703 cgversions = b2caps.get('changegroup')
704 cgversions = b2caps.get('changegroup')
704 if cgversions: # 3.1 and 3.2 ship with an empty value
705 if cgversions: # 3.1 and 3.2 ship with an empty value
705 cgversions = [v for v in cgversions
706 cgversions = [v for v in cgversions
706 if v in changegroup.supportedoutgoingversions(
707 if v in changegroup.supportedoutgoingversions(
707 pushop.repo)]
708 pushop.repo)]
708 if not cgversions:
709 if not cgversions:
709 raise ValueError(_('no common changegroup version'))
710 raise ValueError(_('no common changegroup version'))
710 version = max(cgversions)
711 version = max(cgversions)
711 cg = changegroup.getlocalchangegroupraw(pushop.repo, 'push',
712 cg = changegroup.getlocalchangegroupraw(pushop.repo, 'push',
712 pushop.outgoing,
713 pushop.outgoing,
713 version=version)
714 version=version)
714 cgpart = bundler.newpart('changegroup', data=cg)
715 cgpart = bundler.newpart('changegroup', data=cg)
715 if cgversions:
716 if cgversions:
716 cgpart.addparam('version', version)
717 cgpart.addparam('version', version)
717 if 'treemanifest' in pushop.repo.requirements:
718 if 'treemanifest' in pushop.repo.requirements:
718 cgpart.addparam('treemanifest', '1')
719 cgpart.addparam('treemanifest', '1')
719 def handlereply(op):
720 def handlereply(op):
720 """extract addchangegroup returns from server reply"""
721 """extract addchangegroup returns from server reply"""
721 cgreplies = op.records.getreplies(cgpart.id)
722 cgreplies = op.records.getreplies(cgpart.id)
722 assert len(cgreplies['changegroup']) == 1
723 assert len(cgreplies['changegroup']) == 1
723 pushop.cgresult = cgreplies['changegroup'][0]['return']
724 pushop.cgresult = cgreplies['changegroup'][0]['return']
724 return handlereply
725 return handlereply
725
726
726 @b2partsgenerator('phase')
727 @b2partsgenerator('phase')
727 def _pushb2phases(pushop, bundler):
728 def _pushb2phases(pushop, bundler):
728 """handle phase push through bundle2"""
729 """handle phase push through bundle2"""
729 if 'phases' in pushop.stepsdone:
730 if 'phases' in pushop.stepsdone:
730 return
731 return
731 b2caps = bundle2.bundle2caps(pushop.remote)
732 b2caps = bundle2.bundle2caps(pushop.remote)
732 if not 'pushkey' in b2caps:
733 if not 'pushkey' in b2caps:
733 return
734 return
734 pushop.stepsdone.add('phases')
735 pushop.stepsdone.add('phases')
735 part2node = []
736 part2node = []
736
737
737 def handlefailure(pushop, exc):
738 def handlefailure(pushop, exc):
738 targetid = int(exc.partid)
739 targetid = int(exc.partid)
739 for partid, node in part2node:
740 for partid, node in part2node:
740 if partid == targetid:
741 if partid == targetid:
741 raise error.Abort(_('updating %s to public failed') % node)
742 raise error.Abort(_('updating %s to public failed') % node)
742
743
743 enc = pushkey.encode
744 enc = pushkey.encode
744 for newremotehead in pushop.outdatedphases:
745 for newremotehead in pushop.outdatedphases:
745 part = bundler.newpart('pushkey')
746 part = bundler.newpart('pushkey')
746 part.addparam('namespace', enc('phases'))
747 part.addparam('namespace', enc('phases'))
747 part.addparam('key', enc(newremotehead.hex()))
748 part.addparam('key', enc(newremotehead.hex()))
748 part.addparam('old', enc(str(phases.draft)))
749 part.addparam('old', enc(str(phases.draft)))
749 part.addparam('new', enc(str(phases.public)))
750 part.addparam('new', enc(str(phases.public)))
750 part2node.append((part.id, newremotehead))
751 part2node.append((part.id, newremotehead))
751 pushop.pkfailcb[part.id] = handlefailure
752 pushop.pkfailcb[part.id] = handlefailure
752
753
753 def handlereply(op):
754 def handlereply(op):
754 for partid, node in part2node:
755 for partid, node in part2node:
755 partrep = op.records.getreplies(partid)
756 partrep = op.records.getreplies(partid)
756 results = partrep['pushkey']
757 results = partrep['pushkey']
757 assert len(results) <= 1
758 assert len(results) <= 1
758 msg = None
759 msg = None
759 if not results:
760 if not results:
760 msg = _('server ignored update of %s to public!\n') % node
761 msg = _('server ignored update of %s to public!\n') % node
761 elif not int(results[0]['return']):
762 elif not int(results[0]['return']):
762 msg = _('updating %s to public failed!\n') % node
763 msg = _('updating %s to public failed!\n') % node
763 if msg is not None:
764 if msg is not None:
764 pushop.ui.warn(msg)
765 pushop.ui.warn(msg)
765 return handlereply
766 return handlereply
766
767
767 @b2partsgenerator('obsmarkers')
768 @b2partsgenerator('obsmarkers')
768 def _pushb2obsmarkers(pushop, bundler):
769 def _pushb2obsmarkers(pushop, bundler):
769 if 'obsmarkers' in pushop.stepsdone:
770 if 'obsmarkers' in pushop.stepsdone:
770 return
771 return
771 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
772 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
772 if obsolete.commonversion(remoteversions) is None:
773 if obsolete.commonversion(remoteversions) is None:
773 return
774 return
774 pushop.stepsdone.add('obsmarkers')
775 pushop.stepsdone.add('obsmarkers')
775 if pushop.outobsmarkers:
776 if pushop.outobsmarkers:
776 markers = sorted(pushop.outobsmarkers)
777 markers = sorted(pushop.outobsmarkers)
777 buildobsmarkerspart(bundler, markers)
778 buildobsmarkerspart(bundler, markers)
778
779
779 @b2partsgenerator('bookmarks')
780 @b2partsgenerator('bookmarks')
780 def _pushb2bookmarks(pushop, bundler):
781 def _pushb2bookmarks(pushop, bundler):
781 """handle bookmark push through bundle2"""
782 """handle bookmark push through bundle2"""
782 if 'bookmarks' in pushop.stepsdone:
783 if 'bookmarks' in pushop.stepsdone:
783 return
784 return
784 b2caps = bundle2.bundle2caps(pushop.remote)
785 b2caps = bundle2.bundle2caps(pushop.remote)
785 if 'pushkey' not in b2caps:
786 if 'pushkey' not in b2caps:
786 return
787 return
787 pushop.stepsdone.add('bookmarks')
788 pushop.stepsdone.add('bookmarks')
788 part2book = []
789 part2book = []
789 enc = pushkey.encode
790 enc = pushkey.encode
790
791
791 def handlefailure(pushop, exc):
792 def handlefailure(pushop, exc):
792 targetid = int(exc.partid)
793 targetid = int(exc.partid)
793 for partid, book, action in part2book:
794 for partid, book, action in part2book:
794 if partid == targetid:
795 if partid == targetid:
795 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
796 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
796 # we should not be called for part we did not generated
797 # we should not be called for part we did not generated
797 assert False
798 assert False
798
799
799 for book, old, new in pushop.outbookmarks:
800 for book, old, new in pushop.outbookmarks:
800 part = bundler.newpart('pushkey')
801 part = bundler.newpart('pushkey')
801 part.addparam('namespace', enc('bookmarks'))
802 part.addparam('namespace', enc('bookmarks'))
802 part.addparam('key', enc(book))
803 part.addparam('key', enc(book))
803 part.addparam('old', enc(old))
804 part.addparam('old', enc(old))
804 part.addparam('new', enc(new))
805 part.addparam('new', enc(new))
805 action = 'update'
806 action = 'update'
806 if not old:
807 if not old:
807 action = 'export'
808 action = 'export'
808 elif not new:
809 elif not new:
809 action = 'delete'
810 action = 'delete'
810 part2book.append((part.id, book, action))
811 part2book.append((part.id, book, action))
811 pushop.pkfailcb[part.id] = handlefailure
812 pushop.pkfailcb[part.id] = handlefailure
812
813
813 def handlereply(op):
814 def handlereply(op):
814 ui = pushop.ui
815 ui = pushop.ui
815 for partid, book, action in part2book:
816 for partid, book, action in part2book:
816 partrep = op.records.getreplies(partid)
817 partrep = op.records.getreplies(partid)
817 results = partrep['pushkey']
818 results = partrep['pushkey']
818 assert len(results) <= 1
819 assert len(results) <= 1
819 if not results:
820 if not results:
820 pushop.ui.warn(_('server ignored bookmark %s update\n') % book)
821 pushop.ui.warn(_('server ignored bookmark %s update\n') % book)
821 else:
822 else:
822 ret = int(results[0]['return'])
823 ret = int(results[0]['return'])
823 if ret:
824 if ret:
824 ui.status(bookmsgmap[action][0] % book)
825 ui.status(bookmsgmap[action][0] % book)
825 else:
826 else:
826 ui.warn(bookmsgmap[action][1] % book)
827 ui.warn(bookmsgmap[action][1] % book)
827 if pushop.bkresult is not None:
828 if pushop.bkresult is not None:
828 pushop.bkresult = 1
829 pushop.bkresult = 1
829 return handlereply
830 return handlereply
830
831
831
832
832 def _pushbundle2(pushop):
833 def _pushbundle2(pushop):
833 """push data to the remote using bundle2
834 """push data to the remote using bundle2
834
835
835 The only currently supported type of data is changegroup but this will
836 The only currently supported type of data is changegroup but this will
836 evolve in the future."""
837 evolve in the future."""
837 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
838 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
838 pushback = (pushop.trmanager
839 pushback = (pushop.trmanager
839 and pushop.ui.configbool('experimental', 'bundle2.pushback'))
840 and pushop.ui.configbool('experimental', 'bundle2.pushback'))
840
841
841 # create reply capability
842 # create reply capability
842 capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo,
843 capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo,
843 allowpushback=pushback))
844 allowpushback=pushback))
844 bundler.newpart('replycaps', data=capsblob)
845 bundler.newpart('replycaps', data=capsblob)
845 replyhandlers = []
846 replyhandlers = []
846 for partgenname in b2partsgenorder:
847 for partgenname in b2partsgenorder:
847 partgen = b2partsgenmapping[partgenname]
848 partgen = b2partsgenmapping[partgenname]
848 ret = partgen(pushop, bundler)
849 ret = partgen(pushop, bundler)
849 if callable(ret):
850 if callable(ret):
850 replyhandlers.append(ret)
851 replyhandlers.append(ret)
851 # do not push if nothing to push
852 # do not push if nothing to push
852 if bundler.nbparts <= 1:
853 if bundler.nbparts <= 1:
853 return
854 return
854 stream = util.chunkbuffer(bundler.getchunks())
855 stream = util.chunkbuffer(bundler.getchunks())
855 try:
856 try:
856 try:
857 try:
857 reply = pushop.remote.unbundle(stream, ['force'], 'push')
858 reply = pushop.remote.unbundle(stream, ['force'], 'push')
858 except error.BundleValueError as exc:
859 except error.BundleValueError as exc:
859 raise error.Abort('missing support for %s' % exc)
860 raise error.Abort('missing support for %s' % exc)
860 try:
861 try:
861 trgetter = None
862 trgetter = None
862 if pushback:
863 if pushback:
863 trgetter = pushop.trmanager.transaction
864 trgetter = pushop.trmanager.transaction
864 op = bundle2.processbundle(pushop.repo, reply, trgetter)
865 op = bundle2.processbundle(pushop.repo, reply, trgetter)
865 except error.BundleValueError as exc:
866 except error.BundleValueError as exc:
866 raise error.Abort('missing support for %s' % exc)
867 raise error.Abort('missing support for %s' % exc)
867 except bundle2.AbortFromPart as exc:
868 except bundle2.AbortFromPart as exc:
868 pushop.ui.status(_('remote: %s\n') % exc)
869 pushop.ui.status(_('remote: %s\n') % exc)
869 raise error.Abort(_('push failed on remote'), hint=exc.hint)
870 raise error.Abort(_('push failed on remote'), hint=exc.hint)
870 except error.PushkeyFailed as exc:
871 except error.PushkeyFailed as exc:
871 partid = int(exc.partid)
872 partid = int(exc.partid)
872 if partid not in pushop.pkfailcb:
873 if partid not in pushop.pkfailcb:
873 raise
874 raise
874 pushop.pkfailcb[partid](pushop, exc)
875 pushop.pkfailcb[partid](pushop, exc)
875 for rephand in replyhandlers:
876 for rephand in replyhandlers:
876 rephand(op)
877 rephand(op)
877
878
878 def _pushchangeset(pushop):
879 def _pushchangeset(pushop):
879 """Make the actual push of changeset bundle to remote repo"""
880 """Make the actual push of changeset bundle to remote repo"""
880 if 'changesets' in pushop.stepsdone:
881 if 'changesets' in pushop.stepsdone:
881 return
882 return
882 pushop.stepsdone.add('changesets')
883 pushop.stepsdone.add('changesets')
883 if not _pushcheckoutgoing(pushop):
884 if not _pushcheckoutgoing(pushop):
884 return
885 return
885 pushop.repo.prepushoutgoinghooks(pushop)
886 pushop.repo.prepushoutgoinghooks(pushop)
886 outgoing = pushop.outgoing
887 outgoing = pushop.outgoing
887 unbundle = pushop.remote.capable('unbundle')
888 unbundle = pushop.remote.capable('unbundle')
888 # TODO: get bundlecaps from remote
889 # TODO: get bundlecaps from remote
889 bundlecaps = None
890 bundlecaps = None
890 # create a changegroup from local
891 # create a changegroup from local
891 if pushop.revs is None and not (outgoing.excluded
892 if pushop.revs is None and not (outgoing.excluded
892 or pushop.repo.changelog.filteredrevs):
893 or pushop.repo.changelog.filteredrevs):
893 # push everything,
894 # push everything,
894 # use the fast path, no race possible on push
895 # use the fast path, no race possible on push
895 bundler = changegroup.cg1packer(pushop.repo, bundlecaps)
896 bundler = changegroup.cg1packer(pushop.repo, bundlecaps)
896 cg = changegroup.getsubset(pushop.repo,
897 cg = changegroup.getsubset(pushop.repo,
897 outgoing,
898 outgoing,
898 bundler,
899 bundler,
899 'push',
900 'push',
900 fastpath=True)
901 fastpath=True)
901 else:
902 else:
902 cg = changegroup.getlocalchangegroup(pushop.repo, 'push', outgoing,
903 cg = changegroup.getlocalchangegroup(pushop.repo, 'push', outgoing,
903 bundlecaps)
904 bundlecaps)
904
905
905 # apply changegroup to remote
906 # apply changegroup to remote
906 if unbundle:
907 if unbundle:
907 # local repo finds heads on server, finds out what
908 # local repo finds heads on server, finds out what
908 # revs it must push. once revs transferred, if server
909 # revs it must push. once revs transferred, if server
909 # finds it has different heads (someone else won
910 # finds it has different heads (someone else won
910 # commit/push race), server aborts.
911 # commit/push race), server aborts.
911 if pushop.force:
912 if pushop.force:
912 remoteheads = ['force']
913 remoteheads = ['force']
913 else:
914 else:
914 remoteheads = pushop.remoteheads
915 remoteheads = pushop.remoteheads
915 # ssh: return remote's addchangegroup()
916 # ssh: return remote's addchangegroup()
916 # http: return remote's addchangegroup() or 0 for error
917 # http: return remote's addchangegroup() or 0 for error
917 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads,
918 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads,
918 pushop.repo.url())
919 pushop.repo.url())
919 else:
920 else:
920 # we return an integer indicating remote head count
921 # we return an integer indicating remote head count
921 # change
922 # change
922 pushop.cgresult = pushop.remote.addchangegroup(cg, 'push',
923 pushop.cgresult = pushop.remote.addchangegroup(cg, 'push',
923 pushop.repo.url())
924 pushop.repo.url())
924
925
925 def _pushsyncphase(pushop):
926 def _pushsyncphase(pushop):
926 """synchronise phase information locally and remotely"""
927 """synchronise phase information locally and remotely"""
927 cheads = pushop.commonheads
928 cheads = pushop.commonheads
928 # even when we don't push, exchanging phase data is useful
929 # even when we don't push, exchanging phase data is useful
929 remotephases = pushop.remote.listkeys('phases')
930 remotephases = pushop.remote.listkeys('phases')
930 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
931 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
931 and remotephases # server supports phases
932 and remotephases # server supports phases
932 and pushop.cgresult is None # nothing was pushed
933 and pushop.cgresult is None # nothing was pushed
933 and remotephases.get('publishing', False)):
934 and remotephases.get('publishing', False)):
934 # When:
935 # When:
935 # - this is a subrepo push
936 # - this is a subrepo push
936 # - and remote support phase
937 # - and remote support phase
937 # - and no changeset was pushed
938 # - and no changeset was pushed
938 # - and remote is publishing
939 # - and remote is publishing
939 # We may be in issue 3871 case!
940 # We may be in issue 3871 case!
940 # We drop the possible phase synchronisation done by
941 # We drop the possible phase synchronisation done by
941 # courtesy to publish changesets possibly locally draft
942 # courtesy to publish changesets possibly locally draft
942 # on the remote.
943 # on the remote.
943 remotephases = {'publishing': 'True'}
944 remotephases = {'publishing': 'True'}
944 if not remotephases: # old server or public only reply from non-publishing
945 if not remotephases: # old server or public only reply from non-publishing
945 _localphasemove(pushop, cheads)
946 _localphasemove(pushop, cheads)
946 # don't push any phase data as there is nothing to push
947 # don't push any phase data as there is nothing to push
947 else:
948 else:
948 ana = phases.analyzeremotephases(pushop.repo, cheads,
949 ana = phases.analyzeremotephases(pushop.repo, cheads,
949 remotephases)
950 remotephases)
950 pheads, droots = ana
951 pheads, droots = ana
951 ### Apply remote phase on local
952 ### Apply remote phase on local
952 if remotephases.get('publishing', False):
953 if remotephases.get('publishing', False):
953 _localphasemove(pushop, cheads)
954 _localphasemove(pushop, cheads)
954 else: # publish = False
955 else: # publish = False
955 _localphasemove(pushop, pheads)
956 _localphasemove(pushop, pheads)
956 _localphasemove(pushop, cheads, phases.draft)
957 _localphasemove(pushop, cheads, phases.draft)
957 ### Apply local phase on remote
958 ### Apply local phase on remote
958
959
959 if pushop.cgresult:
960 if pushop.cgresult:
960 if 'phases' in pushop.stepsdone:
961 if 'phases' in pushop.stepsdone:
961 # phases already pushed though bundle2
962 # phases already pushed though bundle2
962 return
963 return
963 outdated = pushop.outdatedphases
964 outdated = pushop.outdatedphases
964 else:
965 else:
965 outdated = pushop.fallbackoutdatedphases
966 outdated = pushop.fallbackoutdatedphases
966
967
967 pushop.stepsdone.add('phases')
968 pushop.stepsdone.add('phases')
968
969
969 # filter heads already turned public by the push
970 # filter heads already turned public by the push
970 outdated = [c for c in outdated if c.node() not in pheads]
971 outdated = [c for c in outdated if c.node() not in pheads]
971 # fallback to independent pushkey command
972 # fallback to independent pushkey command
972 for newremotehead in outdated:
973 for newremotehead in outdated:
973 r = pushop.remote.pushkey('phases',
974 r = pushop.remote.pushkey('phases',
974 newremotehead.hex(),
975 newremotehead.hex(),
975 str(phases.draft),
976 str(phases.draft),
976 str(phases.public))
977 str(phases.public))
977 if not r:
978 if not r:
978 pushop.ui.warn(_('updating %s to public failed!\n')
979 pushop.ui.warn(_('updating %s to public failed!\n')
979 % newremotehead)
980 % newremotehead)
980
981
981 def _localphasemove(pushop, nodes, phase=phases.public):
982 def _localphasemove(pushop, nodes, phase=phases.public):
982 """move <nodes> to <phase> in the local source repo"""
983 """move <nodes> to <phase> in the local source repo"""
983 if pushop.trmanager:
984 if pushop.trmanager:
984 phases.advanceboundary(pushop.repo,
985 phases.advanceboundary(pushop.repo,
985 pushop.trmanager.transaction(),
986 pushop.trmanager.transaction(),
986 phase,
987 phase,
987 nodes)
988 nodes)
988 else:
989 else:
989 # repo is not locked, do not change any phases!
990 # repo is not locked, do not change any phases!
990 # Informs the user that phases should have been moved when
991 # Informs the user that phases should have been moved when
991 # applicable.
992 # applicable.
992 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
993 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
993 phasestr = phases.phasenames[phase]
994 phasestr = phases.phasenames[phase]
994 if actualmoves:
995 if actualmoves:
995 pushop.ui.status(_('cannot lock source repo, skipping '
996 pushop.ui.status(_('cannot lock source repo, skipping '
996 'local %s phase update\n') % phasestr)
997 'local %s phase update\n') % phasestr)
997
998
998 def _pushobsolete(pushop):
999 def _pushobsolete(pushop):
999 """utility function to push obsolete markers to a remote"""
1000 """utility function to push obsolete markers to a remote"""
1000 if 'obsmarkers' in pushop.stepsdone:
1001 if 'obsmarkers' in pushop.stepsdone:
1001 return
1002 return
1002 repo = pushop.repo
1003 repo = pushop.repo
1003 remote = pushop.remote
1004 remote = pushop.remote
1004 pushop.stepsdone.add('obsmarkers')
1005 pushop.stepsdone.add('obsmarkers')
1005 if pushop.outobsmarkers:
1006 if pushop.outobsmarkers:
1006 pushop.ui.debug('try to push obsolete markers to remote\n')
1007 pushop.ui.debug('try to push obsolete markers to remote\n')
1007 rslts = []
1008 rslts = []
1008 remotedata = obsolete._pushkeyescape(sorted(pushop.outobsmarkers))
1009 remotedata = obsolete._pushkeyescape(sorted(pushop.outobsmarkers))
1009 for key in sorted(remotedata, reverse=True):
1010 for key in sorted(remotedata, reverse=True):
1010 # reverse sort to ensure we end with dump0
1011 # reverse sort to ensure we end with dump0
1011 data = remotedata[key]
1012 data = remotedata[key]
1012 rslts.append(remote.pushkey('obsolete', key, '', data))
1013 rslts.append(remote.pushkey('obsolete', key, '', data))
1013 if [r for r in rslts if not r]:
1014 if [r for r in rslts if not r]:
1014 msg = _('failed to push some obsolete markers!\n')
1015 msg = _('failed to push some obsolete markers!\n')
1015 repo.ui.warn(msg)
1016 repo.ui.warn(msg)
1016
1017
1017 def _pushbookmark(pushop):
1018 def _pushbookmark(pushop):
1018 """Update bookmark position on remote"""
1019 """Update bookmark position on remote"""
1019 if pushop.cgresult == 0 or 'bookmarks' in pushop.stepsdone:
1020 if pushop.cgresult == 0 or 'bookmarks' in pushop.stepsdone:
1020 return
1021 return
1021 pushop.stepsdone.add('bookmarks')
1022 pushop.stepsdone.add('bookmarks')
1022 ui = pushop.ui
1023 ui = pushop.ui
1023 remote = pushop.remote
1024 remote = pushop.remote
1024
1025
1025 for b, old, new in pushop.outbookmarks:
1026 for b, old, new in pushop.outbookmarks:
1026 action = 'update'
1027 action = 'update'
1027 if not old:
1028 if not old:
1028 action = 'export'
1029 action = 'export'
1029 elif not new:
1030 elif not new:
1030 action = 'delete'
1031 action = 'delete'
1031 if remote.pushkey('bookmarks', b, old, new):
1032 if remote.pushkey('bookmarks', b, old, new):
1032 ui.status(bookmsgmap[action][0] % b)
1033 ui.status(bookmsgmap[action][0] % b)
1033 else:
1034 else:
1034 ui.warn(bookmsgmap[action][1] % b)
1035 ui.warn(bookmsgmap[action][1] % b)
1035 # discovery can have set the value form invalid entry
1036 # discovery can have set the value form invalid entry
1036 if pushop.bkresult is not None:
1037 if pushop.bkresult is not None:
1037 pushop.bkresult = 1
1038 pushop.bkresult = 1
1038
1039
1039 class pulloperation(object):
1040 class pulloperation(object):
1040 """A object that represent a single pull operation
1041 """A object that represent a single pull operation
1041
1042
1042 It purpose is to carry pull related state and very common operation.
1043 It purpose is to carry pull related state and very common operation.
1043
1044
1044 A new should be created at the beginning of each pull and discarded
1045 A new should be created at the beginning of each pull and discarded
1045 afterward.
1046 afterward.
1046 """
1047 """
1047
1048
1048 def __init__(self, repo, remote, heads=None, force=False, bookmarks=(),
1049 def __init__(self, repo, remote, heads=None, force=False, bookmarks=(),
1049 remotebookmarks=None, streamclonerequested=None):
1050 remotebookmarks=None, streamclonerequested=None):
1050 # repo we pull into
1051 # repo we pull into
1051 self.repo = repo
1052 self.repo = repo
1052 # repo we pull from
1053 # repo we pull from
1053 self.remote = remote
1054 self.remote = remote
1054 # revision we try to pull (None is "all")
1055 # revision we try to pull (None is "all")
1055 self.heads = heads
1056 self.heads = heads
1056 # bookmark pulled explicitly
1057 # bookmark pulled explicitly
1057 self.explicitbookmarks = bookmarks
1058 self.explicitbookmarks = bookmarks
1058 # do we force pull?
1059 # do we force pull?
1059 self.force = force
1060 self.force = force
1060 # whether a streaming clone was requested
1061 # whether a streaming clone was requested
1061 self.streamclonerequested = streamclonerequested
1062 self.streamclonerequested = streamclonerequested
1062 # transaction manager
1063 # transaction manager
1063 self.trmanager = None
1064 self.trmanager = None
1064 # set of common changeset between local and remote before pull
1065 # set of common changeset between local and remote before pull
1065 self.common = None
1066 self.common = None
1066 # set of pulled head
1067 # set of pulled head
1067 self.rheads = None
1068 self.rheads = None
1068 # list of missing changeset to fetch remotely
1069 # list of missing changeset to fetch remotely
1069 self.fetch = None
1070 self.fetch = None
1070 # remote bookmarks data
1071 # remote bookmarks data
1071 self.remotebookmarks = remotebookmarks
1072 self.remotebookmarks = remotebookmarks
1072 # result of changegroup pulling (used as return code by pull)
1073 # result of changegroup pulling (used as return code by pull)
1073 self.cgresult = None
1074 self.cgresult = None
1074 # list of step already done
1075 # list of step already done
1075 self.stepsdone = set()
1076 self.stepsdone = set()
1076 # Whether we attempted a clone from pre-generated bundles.
1077 # Whether we attempted a clone from pre-generated bundles.
1077 self.clonebundleattempted = False
1078 self.clonebundleattempted = False
1078
1079
1079 @util.propertycache
1080 @util.propertycache
1080 def pulledsubset(self):
1081 def pulledsubset(self):
1081 """heads of the set of changeset target by the pull"""
1082 """heads of the set of changeset target by the pull"""
1082 # compute target subset
1083 # compute target subset
1083 if self.heads is None:
1084 if self.heads is None:
1084 # We pulled every thing possible
1085 # We pulled every thing possible
1085 # sync on everything common
1086 # sync on everything common
1086 c = set(self.common)
1087 c = set(self.common)
1087 ret = list(self.common)
1088 ret = list(self.common)
1088 for n in self.rheads:
1089 for n in self.rheads:
1089 if n not in c:
1090 if n not in c:
1090 ret.append(n)
1091 ret.append(n)
1091 return ret
1092 return ret
1092 else:
1093 else:
1093 # We pulled a specific subset
1094 # We pulled a specific subset
1094 # sync on this subset
1095 # sync on this subset
1095 return self.heads
1096 return self.heads
1096
1097
1097 @util.propertycache
1098 @util.propertycache
1098 def canusebundle2(self):
1099 def canusebundle2(self):
1099 return _canusebundle2(self)
1100 return _canusebundle2(self)
1100
1101
1101 @util.propertycache
1102 @util.propertycache
1102 def remotebundle2caps(self):
1103 def remotebundle2caps(self):
1103 return bundle2.bundle2caps(self.remote)
1104 return bundle2.bundle2caps(self.remote)
1104
1105
1105 def gettransaction(self):
1106 def gettransaction(self):
1106 # deprecated; talk to trmanager directly
1107 # deprecated; talk to trmanager directly
1107 return self.trmanager.transaction()
1108 return self.trmanager.transaction()
1108
1109
1109 class transactionmanager(object):
1110 class transactionmanager(object):
1110 """An object to manage the life cycle of a transaction
1111 """An object to manage the life cycle of a transaction
1111
1112
1112 It creates the transaction on demand and calls the appropriate hooks when
1113 It creates the transaction on demand and calls the appropriate hooks when
1113 closing the transaction."""
1114 closing the transaction."""
1114 def __init__(self, repo, source, url):
1115 def __init__(self, repo, source, url):
1115 self.repo = repo
1116 self.repo = repo
1116 self.source = source
1117 self.source = source
1117 self.url = url
1118 self.url = url
1118 self._tr = None
1119 self._tr = None
1119
1120
1120 def transaction(self):
1121 def transaction(self):
1121 """Return an open transaction object, constructing if necessary"""
1122 """Return an open transaction object, constructing if necessary"""
1122 if not self._tr:
1123 if not self._tr:
1123 trname = '%s\n%s' % (self.source, util.hidepassword(self.url))
1124 trname = '%s\n%s' % (self.source, util.hidepassword(self.url))
1124 self._tr = self.repo.transaction(trname)
1125 self._tr = self.repo.transaction(trname)
1125 self._tr.hookargs['source'] = self.source
1126 self._tr.hookargs['source'] = self.source
1126 self._tr.hookargs['url'] = self.url
1127 self._tr.hookargs['url'] = self.url
1127 return self._tr
1128 return self._tr
1128
1129
1129 def close(self):
1130 def close(self):
1130 """close transaction if created"""
1131 """close transaction if created"""
1131 if self._tr is not None:
1132 if self._tr is not None:
1132 self._tr.close()
1133 self._tr.close()
1133
1134
1134 def release(self):
1135 def release(self):
1135 """release transaction if created"""
1136 """release transaction if created"""
1136 if self._tr is not None:
1137 if self._tr is not None:
1137 self._tr.release()
1138 self._tr.release()
1138
1139
1139 def pull(repo, remote, heads=None, force=False, bookmarks=(), opargs=None,
1140 def pull(repo, remote, heads=None, force=False, bookmarks=(), opargs=None,
1140 streamclonerequested=None):
1141 streamclonerequested=None):
1141 """Fetch repository data from a remote.
1142 """Fetch repository data from a remote.
1142
1143
1143 This is the main function used to retrieve data from a remote repository.
1144 This is the main function used to retrieve data from a remote repository.
1144
1145
1145 ``repo`` is the local repository to clone into.
1146 ``repo`` is the local repository to clone into.
1146 ``remote`` is a peer instance.
1147 ``remote`` is a peer instance.
1147 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1148 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1148 default) means to pull everything from the remote.
1149 default) means to pull everything from the remote.
1149 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1150 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1150 default, all remote bookmarks are pulled.
1151 default, all remote bookmarks are pulled.
1151 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1152 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1152 initialization.
1153 initialization.
1153 ``streamclonerequested`` is a boolean indicating whether a "streaming
1154 ``streamclonerequested`` is a boolean indicating whether a "streaming
1154 clone" is requested. A "streaming clone" is essentially a raw file copy
1155 clone" is requested. A "streaming clone" is essentially a raw file copy
1155 of revlogs from the server. This only works when the local repository is
1156 of revlogs from the server. This only works when the local repository is
1156 empty. The default value of ``None`` means to respect the server
1157 empty. The default value of ``None`` means to respect the server
1157 configuration for preferring stream clones.
1158 configuration for preferring stream clones.
1158
1159
1159 Returns the ``pulloperation`` created for this pull.
1160 Returns the ``pulloperation`` created for this pull.
1160 """
1161 """
1161 if opargs is None:
1162 if opargs is None:
1162 opargs = {}
1163 opargs = {}
1163 pullop = pulloperation(repo, remote, heads, force, bookmarks=bookmarks,
1164 pullop = pulloperation(repo, remote, heads, force, bookmarks=bookmarks,
1164 streamclonerequested=streamclonerequested, **opargs)
1165 streamclonerequested=streamclonerequested, **opargs)
1165 if pullop.remote.local():
1166 if pullop.remote.local():
1166 missing = set(pullop.remote.requirements) - pullop.repo.supported
1167 missing = set(pullop.remote.requirements) - pullop.repo.supported
1167 if missing:
1168 if missing:
1168 msg = _("required features are not"
1169 msg = _("required features are not"
1169 " supported in the destination:"
1170 " supported in the destination:"
1170 " %s") % (', '.join(sorted(missing)))
1171 " %s") % (', '.join(sorted(missing)))
1171 raise error.Abort(msg)
1172 raise error.Abort(msg)
1172
1173
1173 lock = pullop.repo.lock()
1174 lock = pullop.repo.lock()
1174 try:
1175 try:
1175 pullop.trmanager = transactionmanager(repo, 'pull', remote.url())
1176 pullop.trmanager = transactionmanager(repo, 'pull', remote.url())
1176 streamclone.maybeperformlegacystreamclone(pullop)
1177 streamclone.maybeperformlegacystreamclone(pullop)
1177 # This should ideally be in _pullbundle2(). However, it needs to run
1178 # This should ideally be in _pullbundle2(). However, it needs to run
1178 # before discovery to avoid extra work.
1179 # before discovery to avoid extra work.
1179 _maybeapplyclonebundle(pullop)
1180 _maybeapplyclonebundle(pullop)
1180 _pulldiscovery(pullop)
1181 _pulldiscovery(pullop)
1181 if pullop.canusebundle2:
1182 if pullop.canusebundle2:
1182 _pullbundle2(pullop)
1183 _pullbundle2(pullop)
1183 _pullchangeset(pullop)
1184 _pullchangeset(pullop)
1184 _pullphase(pullop)
1185 _pullphase(pullop)
1185 _pullbookmarks(pullop)
1186 _pullbookmarks(pullop)
1186 _pullobsolete(pullop)
1187 _pullobsolete(pullop)
1187 pullop.trmanager.close()
1188 pullop.trmanager.close()
1188 finally:
1189 finally:
1189 pullop.trmanager.release()
1190 pullop.trmanager.release()
1190 lock.release()
1191 lock.release()
1191
1192
1192 return pullop
1193 return pullop
1193
1194
1194 # list of steps to perform discovery before pull
1195 # list of steps to perform discovery before pull
1195 pulldiscoveryorder = []
1196 pulldiscoveryorder = []
1196
1197
1197 # Mapping between step name and function
1198 # Mapping between step name and function
1198 #
1199 #
1199 # This exists to help extensions wrap steps if necessary
1200 # This exists to help extensions wrap steps if necessary
1200 pulldiscoverymapping = {}
1201 pulldiscoverymapping = {}
1201
1202
1202 def pulldiscovery(stepname):
1203 def pulldiscovery(stepname):
1203 """decorator for function performing discovery before pull
1204 """decorator for function performing discovery before pull
1204
1205
1205 The function is added to the step -> function mapping and appended to the
1206 The function is added to the step -> function mapping and appended to the
1206 list of steps. Beware that decorated function will be added in order (this
1207 list of steps. Beware that decorated function will be added in order (this
1207 may matter).
1208 may matter).
1208
1209
1209 You can only use this decorator for a new step, if you want to wrap a step
1210 You can only use this decorator for a new step, if you want to wrap a step
1210 from an extension, change the pulldiscovery dictionary directly."""
1211 from an extension, change the pulldiscovery dictionary directly."""
1211 def dec(func):
1212 def dec(func):
1212 assert stepname not in pulldiscoverymapping
1213 assert stepname not in pulldiscoverymapping
1213 pulldiscoverymapping[stepname] = func
1214 pulldiscoverymapping[stepname] = func
1214 pulldiscoveryorder.append(stepname)
1215 pulldiscoveryorder.append(stepname)
1215 return func
1216 return func
1216 return dec
1217 return dec
1217
1218
1218 def _pulldiscovery(pullop):
1219 def _pulldiscovery(pullop):
1219 """Run all discovery steps"""
1220 """Run all discovery steps"""
1220 for stepname in pulldiscoveryorder:
1221 for stepname in pulldiscoveryorder:
1221 step = pulldiscoverymapping[stepname]
1222 step = pulldiscoverymapping[stepname]
1222 step(pullop)
1223 step(pullop)
1223
1224
1224 @pulldiscovery('b1:bookmarks')
1225 @pulldiscovery('b1:bookmarks')
1225 def _pullbookmarkbundle1(pullop):
1226 def _pullbookmarkbundle1(pullop):
1226 """fetch bookmark data in bundle1 case
1227 """fetch bookmark data in bundle1 case
1227
1228
1228 If not using bundle2, we have to fetch bookmarks before changeset
1229 If not using bundle2, we have to fetch bookmarks before changeset
1229 discovery to reduce the chance and impact of race conditions."""
1230 discovery to reduce the chance and impact of race conditions."""
1230 if pullop.remotebookmarks is not None:
1231 if pullop.remotebookmarks is not None:
1231 return
1232 return
1232 if pullop.canusebundle2 and 'listkeys' in pullop.remotebundle2caps:
1233 if pullop.canusebundle2 and 'listkeys' in pullop.remotebundle2caps:
1233 # all known bundle2 servers now support listkeys, but lets be nice with
1234 # all known bundle2 servers now support listkeys, but lets be nice with
1234 # new implementation.
1235 # new implementation.
1235 return
1236 return
1236 pullop.remotebookmarks = pullop.remote.listkeys('bookmarks')
1237 pullop.remotebookmarks = pullop.remote.listkeys('bookmarks')
1237
1238
1238
1239
1239 @pulldiscovery('changegroup')
1240 @pulldiscovery('changegroup')
1240 def _pulldiscoverychangegroup(pullop):
1241 def _pulldiscoverychangegroup(pullop):
1241 """discovery phase for the pull
1242 """discovery phase for the pull
1242
1243
1243 Current handle changeset discovery only, will change handle all discovery
1244 Current handle changeset discovery only, will change handle all discovery
1244 at some point."""
1245 at some point."""
1245 tmp = discovery.findcommonincoming(pullop.repo,
1246 tmp = discovery.findcommonincoming(pullop.repo,
1246 pullop.remote,
1247 pullop.remote,
1247 heads=pullop.heads,
1248 heads=pullop.heads,
1248 force=pullop.force)
1249 force=pullop.force)
1249 common, fetch, rheads = tmp
1250 common, fetch, rheads = tmp
1250 nm = pullop.repo.unfiltered().changelog.nodemap
1251 nm = pullop.repo.unfiltered().changelog.nodemap
1251 if fetch and rheads:
1252 if fetch and rheads:
1252 # If a remote heads in filtered locally, lets drop it from the unknown
1253 # If a remote heads in filtered locally, lets drop it from the unknown
1253 # remote heads and put in back in common.
1254 # remote heads and put in back in common.
1254 #
1255 #
1255 # This is a hackish solution to catch most of "common but locally
1256 # This is a hackish solution to catch most of "common but locally
1256 # hidden situation". We do not performs discovery on unfiltered
1257 # hidden situation". We do not performs discovery on unfiltered
1257 # repository because it end up doing a pathological amount of round
1258 # repository because it end up doing a pathological amount of round
1258 # trip for w huge amount of changeset we do not care about.
1259 # trip for w huge amount of changeset we do not care about.
1259 #
1260 #
1260 # If a set of such "common but filtered" changeset exist on the server
1261 # If a set of such "common but filtered" changeset exist on the server
1261 # but are not including a remote heads, we'll not be able to detect it,
1262 # but are not including a remote heads, we'll not be able to detect it,
1262 scommon = set(common)
1263 scommon = set(common)
1263 filteredrheads = []
1264 filteredrheads = []
1264 for n in rheads:
1265 for n in rheads:
1265 if n in nm:
1266 if n in nm:
1266 if n not in scommon:
1267 if n not in scommon:
1267 common.append(n)
1268 common.append(n)
1268 else:
1269 else:
1269 filteredrheads.append(n)
1270 filteredrheads.append(n)
1270 if not filteredrheads:
1271 if not filteredrheads:
1271 fetch = []
1272 fetch = []
1272 rheads = filteredrheads
1273 rheads = filteredrheads
1273 pullop.common = common
1274 pullop.common = common
1274 pullop.fetch = fetch
1275 pullop.fetch = fetch
1275 pullop.rheads = rheads
1276 pullop.rheads = rheads
1276
1277
1277 def _pullbundle2(pullop):
1278 def _pullbundle2(pullop):
1278 """pull data using bundle2
1279 """pull data using bundle2
1279
1280
1280 For now, the only supported data are changegroup."""
1281 For now, the only supported data are changegroup."""
1281 kwargs = {'bundlecaps': caps20to10(pullop.repo)}
1282 kwargs = {'bundlecaps': caps20to10(pullop.repo)}
1282
1283
1283 streaming, streamreqs = streamclone.canperformstreamclone(pullop)
1284 streaming, streamreqs = streamclone.canperformstreamclone(pullop)
1284
1285
1285 # pulling changegroup
1286 # pulling changegroup
1286 pullop.stepsdone.add('changegroup')
1287 pullop.stepsdone.add('changegroup')
1287
1288
1288 kwargs['common'] = pullop.common
1289 kwargs['common'] = pullop.common
1289 kwargs['heads'] = pullop.heads or pullop.rheads
1290 kwargs['heads'] = pullop.heads or pullop.rheads
1290 kwargs['cg'] = pullop.fetch
1291 kwargs['cg'] = pullop.fetch
1291 if 'listkeys' in pullop.remotebundle2caps:
1292 if 'listkeys' in pullop.remotebundle2caps:
1292 kwargs['listkeys'] = ['phase']
1293 kwargs['listkeys'] = ['phase']
1293 if pullop.remotebookmarks is None:
1294 if pullop.remotebookmarks is None:
1294 # make sure to always includes bookmark data when migrating
1295 # make sure to always includes bookmark data when migrating
1295 # `hg incoming --bundle` to using this function.
1296 # `hg incoming --bundle` to using this function.
1296 kwargs['listkeys'].append('bookmarks')
1297 kwargs['listkeys'].append('bookmarks')
1297
1298
1298 # If this is a full pull / clone and the server supports the clone bundles
1299 # If this is a full pull / clone and the server supports the clone bundles
1299 # feature, tell the server whether we attempted a clone bundle. The
1300 # feature, tell the server whether we attempted a clone bundle. The
1300 # presence of this flag indicates the client supports clone bundles. This
1301 # presence of this flag indicates the client supports clone bundles. This
1301 # will enable the server to treat clients that support clone bundles
1302 # will enable the server to treat clients that support clone bundles
1302 # differently from those that don't.
1303 # differently from those that don't.
1303 if (pullop.remote.capable('clonebundles')
1304 if (pullop.remote.capable('clonebundles')
1304 and pullop.heads is None and list(pullop.common) == [nullid]):
1305 and pullop.heads is None and list(pullop.common) == [nullid]):
1305 kwargs['cbattempted'] = pullop.clonebundleattempted
1306 kwargs['cbattempted'] = pullop.clonebundleattempted
1306
1307
1307 if streaming:
1308 if streaming:
1308 pullop.repo.ui.status(_('streaming all changes\n'))
1309 pullop.repo.ui.status(_('streaming all changes\n'))
1309 elif not pullop.fetch:
1310 elif not pullop.fetch:
1310 pullop.repo.ui.status(_("no changes found\n"))
1311 pullop.repo.ui.status(_("no changes found\n"))
1311 pullop.cgresult = 0
1312 pullop.cgresult = 0
1312 else:
1313 else:
1313 if pullop.heads is None and list(pullop.common) == [nullid]:
1314 if pullop.heads is None and list(pullop.common) == [nullid]:
1314 pullop.repo.ui.status(_("requesting all changes\n"))
1315 pullop.repo.ui.status(_("requesting all changes\n"))
1315 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1316 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1316 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1317 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1317 if obsolete.commonversion(remoteversions) is not None:
1318 if obsolete.commonversion(remoteversions) is not None:
1318 kwargs['obsmarkers'] = True
1319 kwargs['obsmarkers'] = True
1319 pullop.stepsdone.add('obsmarkers')
1320 pullop.stepsdone.add('obsmarkers')
1320 _pullbundle2extraprepare(pullop, kwargs)
1321 _pullbundle2extraprepare(pullop, kwargs)
1321 bundle = pullop.remote.getbundle('pull', **kwargs)
1322 bundle = pullop.remote.getbundle('pull', **kwargs)
1322 try:
1323 try:
1323 op = bundle2.processbundle(pullop.repo, bundle, pullop.gettransaction)
1324 op = bundle2.processbundle(pullop.repo, bundle, pullop.gettransaction)
1324 except error.BundleValueError as exc:
1325 except error.BundleValueError as exc:
1325 raise error.Abort('missing support for %s' % exc)
1326 raise error.Abort('missing support for %s' % exc)
1326
1327
1327 if pullop.fetch:
1328 if pullop.fetch:
1328 results = [cg['return'] for cg in op.records['changegroup']]
1329 results = [cg['return'] for cg in op.records['changegroup']]
1329 pullop.cgresult = changegroup.combineresults(results)
1330 pullop.cgresult = changegroup.combineresults(results)
1330
1331
1331 # processing phases change
1332 # processing phases change
1332 for namespace, value in op.records['listkeys']:
1333 for namespace, value in op.records['listkeys']:
1333 if namespace == 'phases':
1334 if namespace == 'phases':
1334 _pullapplyphases(pullop, value)
1335 _pullapplyphases(pullop, value)
1335
1336
1336 # processing bookmark update
1337 # processing bookmark update
1337 for namespace, value in op.records['listkeys']:
1338 for namespace, value in op.records['listkeys']:
1338 if namespace == 'bookmarks':
1339 if namespace == 'bookmarks':
1339 pullop.remotebookmarks = value
1340 pullop.remotebookmarks = value
1340
1341
1341 # bookmark data were either already there or pulled in the bundle
1342 # bookmark data were either already there or pulled in the bundle
1342 if pullop.remotebookmarks is not None:
1343 if pullop.remotebookmarks is not None:
1343 _pullbookmarks(pullop)
1344 _pullbookmarks(pullop)
1344
1345
1345 def _pullbundle2extraprepare(pullop, kwargs):
1346 def _pullbundle2extraprepare(pullop, kwargs):
1346 """hook function so that extensions can extend the getbundle call"""
1347 """hook function so that extensions can extend the getbundle call"""
1347 pass
1348 pass
1348
1349
1349 def _pullchangeset(pullop):
1350 def _pullchangeset(pullop):
1350 """pull changeset from unbundle into the local repo"""
1351 """pull changeset from unbundle into the local repo"""
1351 # We delay the open of the transaction as late as possible so we
1352 # We delay the open of the transaction as late as possible so we
1352 # don't open transaction for nothing or you break future useful
1353 # don't open transaction for nothing or you break future useful
1353 # rollback call
1354 # rollback call
1354 if 'changegroup' in pullop.stepsdone:
1355 if 'changegroup' in pullop.stepsdone:
1355 return
1356 return
1356 pullop.stepsdone.add('changegroup')
1357 pullop.stepsdone.add('changegroup')
1357 if not pullop.fetch:
1358 if not pullop.fetch:
1358 pullop.repo.ui.status(_("no changes found\n"))
1359 pullop.repo.ui.status(_("no changes found\n"))
1359 pullop.cgresult = 0
1360 pullop.cgresult = 0
1360 return
1361 return
1361 pullop.gettransaction()
1362 pullop.gettransaction()
1362 if pullop.heads is None and list(pullop.common) == [nullid]:
1363 if pullop.heads is None and list(pullop.common) == [nullid]:
1363 pullop.repo.ui.status(_("requesting all changes\n"))
1364 pullop.repo.ui.status(_("requesting all changes\n"))
1364 elif pullop.heads is None and pullop.remote.capable('changegroupsubset'):
1365 elif pullop.heads is None and pullop.remote.capable('changegroupsubset'):
1365 # issue1320, avoid a race if remote changed after discovery
1366 # issue1320, avoid a race if remote changed after discovery
1366 pullop.heads = pullop.rheads
1367 pullop.heads = pullop.rheads
1367
1368
1368 if pullop.remote.capable('getbundle'):
1369 if pullop.remote.capable('getbundle'):
1369 # TODO: get bundlecaps from remote
1370 # TODO: get bundlecaps from remote
1370 cg = pullop.remote.getbundle('pull', common=pullop.common,
1371 cg = pullop.remote.getbundle('pull', common=pullop.common,
1371 heads=pullop.heads or pullop.rheads)
1372 heads=pullop.heads or pullop.rheads)
1372 elif pullop.heads is None:
1373 elif pullop.heads is None:
1373 cg = pullop.remote.changegroup(pullop.fetch, 'pull')
1374 cg = pullop.remote.changegroup(pullop.fetch, 'pull')
1374 elif not pullop.remote.capable('changegroupsubset'):
1375 elif not pullop.remote.capable('changegroupsubset'):
1375 raise error.Abort(_("partial pull cannot be done because "
1376 raise error.Abort(_("partial pull cannot be done because "
1376 "other repository doesn't support "
1377 "other repository doesn't support "
1377 "changegroupsubset."))
1378 "changegroupsubset."))
1378 else:
1379 else:
1379 cg = pullop.remote.changegroupsubset(pullop.fetch, pullop.heads, 'pull')
1380 cg = pullop.remote.changegroupsubset(pullop.fetch, pullop.heads, 'pull')
1380 pullop.cgresult = cg.apply(pullop.repo, 'pull', pullop.remote.url())
1381 pullop.cgresult = cg.apply(pullop.repo, 'pull', pullop.remote.url())
1381
1382
1382 def _pullphase(pullop):
1383 def _pullphase(pullop):
1383 # Get remote phases data from remote
1384 # Get remote phases data from remote
1384 if 'phases' in pullop.stepsdone:
1385 if 'phases' in pullop.stepsdone:
1385 return
1386 return
1386 remotephases = pullop.remote.listkeys('phases')
1387 remotephases = pullop.remote.listkeys('phases')
1387 _pullapplyphases(pullop, remotephases)
1388 _pullapplyphases(pullop, remotephases)
1388
1389
1389 def _pullapplyphases(pullop, remotephases):
1390 def _pullapplyphases(pullop, remotephases):
1390 """apply phase movement from observed remote state"""
1391 """apply phase movement from observed remote state"""
1391 if 'phases' in pullop.stepsdone:
1392 if 'phases' in pullop.stepsdone:
1392 return
1393 return
1393 pullop.stepsdone.add('phases')
1394 pullop.stepsdone.add('phases')
1394 publishing = bool(remotephases.get('publishing', False))
1395 publishing = bool(remotephases.get('publishing', False))
1395 if remotephases and not publishing:
1396 if remotephases and not publishing:
1396 # remote is new and unpublishing
1397 # remote is new and unpublishing
1397 pheads, _dr = phases.analyzeremotephases(pullop.repo,
1398 pheads, _dr = phases.analyzeremotephases(pullop.repo,
1398 pullop.pulledsubset,
1399 pullop.pulledsubset,
1399 remotephases)
1400 remotephases)
1400 dheads = pullop.pulledsubset
1401 dheads = pullop.pulledsubset
1401 else:
1402 else:
1402 # Remote is old or publishing all common changesets
1403 # Remote is old or publishing all common changesets
1403 # should be seen as public
1404 # should be seen as public
1404 pheads = pullop.pulledsubset
1405 pheads = pullop.pulledsubset
1405 dheads = []
1406 dheads = []
1406 unfi = pullop.repo.unfiltered()
1407 unfi = pullop.repo.unfiltered()
1407 phase = unfi._phasecache.phase
1408 phase = unfi._phasecache.phase
1408 rev = unfi.changelog.nodemap.get
1409 rev = unfi.changelog.nodemap.get
1409 public = phases.public
1410 public = phases.public
1410 draft = phases.draft
1411 draft = phases.draft
1411
1412
1412 # exclude changesets already public locally and update the others
1413 # exclude changesets already public locally and update the others
1413 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
1414 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
1414 if pheads:
1415 if pheads:
1415 tr = pullop.gettransaction()
1416 tr = pullop.gettransaction()
1416 phases.advanceboundary(pullop.repo, tr, public, pheads)
1417 phases.advanceboundary(pullop.repo, tr, public, pheads)
1417
1418
1418 # exclude changesets already draft locally and update the others
1419 # exclude changesets already draft locally and update the others
1419 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
1420 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
1420 if dheads:
1421 if dheads:
1421 tr = pullop.gettransaction()
1422 tr = pullop.gettransaction()
1422 phases.advanceboundary(pullop.repo, tr, draft, dheads)
1423 phases.advanceboundary(pullop.repo, tr, draft, dheads)
1423
1424
1424 def _pullbookmarks(pullop):
1425 def _pullbookmarks(pullop):
1425 """process the remote bookmark information to update the local one"""
1426 """process the remote bookmark information to update the local one"""
1426 if 'bookmarks' in pullop.stepsdone:
1427 if 'bookmarks' in pullop.stepsdone:
1427 return
1428 return
1428 pullop.stepsdone.add('bookmarks')
1429 pullop.stepsdone.add('bookmarks')
1429 repo = pullop.repo
1430 repo = pullop.repo
1430 remotebookmarks = pullop.remotebookmarks
1431 remotebookmarks = pullop.remotebookmarks
1431 bookmod.updatefromremote(repo.ui, repo, remotebookmarks,
1432 bookmod.updatefromremote(repo.ui, repo, remotebookmarks,
1432 pullop.remote.url(),
1433 pullop.remote.url(),
1433 pullop.gettransaction,
1434 pullop.gettransaction,
1434 explicit=pullop.explicitbookmarks)
1435 explicit=pullop.explicitbookmarks)
1435
1436
1436 def _pullobsolete(pullop):
1437 def _pullobsolete(pullop):
1437 """utility function to pull obsolete markers from a remote
1438 """utility function to pull obsolete markers from a remote
1438
1439
1439 The `gettransaction` is function that return the pull transaction, creating
1440 The `gettransaction` is function that return the pull transaction, creating
1440 one if necessary. We return the transaction to inform the calling code that
1441 one if necessary. We return the transaction to inform the calling code that
1441 a new transaction have been created (when applicable).
1442 a new transaction have been created (when applicable).
1442
1443
1443 Exists mostly to allow overriding for experimentation purpose"""
1444 Exists mostly to allow overriding for experimentation purpose"""
1444 if 'obsmarkers' in pullop.stepsdone:
1445 if 'obsmarkers' in pullop.stepsdone:
1445 return
1446 return
1446 pullop.stepsdone.add('obsmarkers')
1447 pullop.stepsdone.add('obsmarkers')
1447 tr = None
1448 tr = None
1448 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1449 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1449 pullop.repo.ui.debug('fetching remote obsolete markers\n')
1450 pullop.repo.ui.debug('fetching remote obsolete markers\n')
1450 remoteobs = pullop.remote.listkeys('obsolete')
1451 remoteobs = pullop.remote.listkeys('obsolete')
1451 if 'dump0' in remoteobs:
1452 if 'dump0' in remoteobs:
1452 tr = pullop.gettransaction()
1453 tr = pullop.gettransaction()
1453 markers = []
1454 markers = []
1454 for key in sorted(remoteobs, reverse=True):
1455 for key in sorted(remoteobs, reverse=True):
1455 if key.startswith('dump'):
1456 if key.startswith('dump'):
1456 data = base85.b85decode(remoteobs[key])
1457 data = base85.b85decode(remoteobs[key])
1457 version, newmarks = obsolete._readmarkers(data)
1458 version, newmarks = obsolete._readmarkers(data)
1458 markers += newmarks
1459 markers += newmarks
1459 if markers:
1460 if markers:
1460 pullop.repo.obsstore.add(tr, markers)
1461 pullop.repo.obsstore.add(tr, markers)
1461 pullop.repo.invalidatevolatilesets()
1462 pullop.repo.invalidatevolatilesets()
1462 return tr
1463 return tr
1463
1464
1464 def caps20to10(repo):
1465 def caps20to10(repo):
1465 """return a set with appropriate options to use bundle20 during getbundle"""
1466 """return a set with appropriate options to use bundle20 during getbundle"""
1466 caps = set(['HG20'])
1467 caps = set(['HG20'])
1467 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo))
1468 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo))
1468 caps.add('bundle2=' + urllib.quote(capsblob))
1469 caps.add('bundle2=' + urlreq.quote(capsblob))
1469 return caps
1470 return caps
1470
1471
1471 # List of names of steps to perform for a bundle2 for getbundle, order matters.
1472 # List of names of steps to perform for a bundle2 for getbundle, order matters.
1472 getbundle2partsorder = []
1473 getbundle2partsorder = []
1473
1474
1474 # Mapping between step name and function
1475 # Mapping between step name and function
1475 #
1476 #
1476 # This exists to help extensions wrap steps if necessary
1477 # This exists to help extensions wrap steps if necessary
1477 getbundle2partsmapping = {}
1478 getbundle2partsmapping = {}
1478
1479
1479 def getbundle2partsgenerator(stepname, idx=None):
1480 def getbundle2partsgenerator(stepname, idx=None):
1480 """decorator for function generating bundle2 part for getbundle
1481 """decorator for function generating bundle2 part for getbundle
1481
1482
1482 The function is added to the step -> function mapping and appended to the
1483 The function is added to the step -> function mapping and appended to the
1483 list of steps. Beware that decorated functions will be added in order
1484 list of steps. Beware that decorated functions will be added in order
1484 (this may matter).
1485 (this may matter).
1485
1486
1486 You can only use this decorator for new steps, if you want to wrap a step
1487 You can only use this decorator for new steps, if you want to wrap a step
1487 from an extension, attack the getbundle2partsmapping dictionary directly."""
1488 from an extension, attack the getbundle2partsmapping dictionary directly."""
1488 def dec(func):
1489 def dec(func):
1489 assert stepname not in getbundle2partsmapping
1490 assert stepname not in getbundle2partsmapping
1490 getbundle2partsmapping[stepname] = func
1491 getbundle2partsmapping[stepname] = func
1491 if idx is None:
1492 if idx is None:
1492 getbundle2partsorder.append(stepname)
1493 getbundle2partsorder.append(stepname)
1493 else:
1494 else:
1494 getbundle2partsorder.insert(idx, stepname)
1495 getbundle2partsorder.insert(idx, stepname)
1495 return func
1496 return func
1496 return dec
1497 return dec
1497
1498
1498 def bundle2requested(bundlecaps):
1499 def bundle2requested(bundlecaps):
1499 if bundlecaps is not None:
1500 if bundlecaps is not None:
1500 return any(cap.startswith('HG2') for cap in bundlecaps)
1501 return any(cap.startswith('HG2') for cap in bundlecaps)
1501 return False
1502 return False
1502
1503
1503 def getbundle(repo, source, heads=None, common=None, bundlecaps=None,
1504 def getbundle(repo, source, heads=None, common=None, bundlecaps=None,
1504 **kwargs):
1505 **kwargs):
1505 """return a full bundle (with potentially multiple kind of parts)
1506 """return a full bundle (with potentially multiple kind of parts)
1506
1507
1507 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
1508 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
1508 passed. For now, the bundle can contain only changegroup, but this will
1509 passed. For now, the bundle can contain only changegroup, but this will
1509 changes when more part type will be available for bundle2.
1510 changes when more part type will be available for bundle2.
1510
1511
1511 This is different from changegroup.getchangegroup that only returns an HG10
1512 This is different from changegroup.getchangegroup that only returns an HG10
1512 changegroup bundle. They may eventually get reunited in the future when we
1513 changegroup bundle. They may eventually get reunited in the future when we
1513 have a clearer idea of the API we what to query different data.
1514 have a clearer idea of the API we what to query different data.
1514
1515
1515 The implementation is at a very early stage and will get massive rework
1516 The implementation is at a very early stage and will get massive rework
1516 when the API of bundle is refined.
1517 when the API of bundle is refined.
1517 """
1518 """
1518 usebundle2 = bundle2requested(bundlecaps)
1519 usebundle2 = bundle2requested(bundlecaps)
1519 # bundle10 case
1520 # bundle10 case
1520 if not usebundle2:
1521 if not usebundle2:
1521 if bundlecaps and not kwargs.get('cg', True):
1522 if bundlecaps and not kwargs.get('cg', True):
1522 raise ValueError(_('request for bundle10 must include changegroup'))
1523 raise ValueError(_('request for bundle10 must include changegroup'))
1523
1524
1524 if kwargs:
1525 if kwargs:
1525 raise ValueError(_('unsupported getbundle arguments: %s')
1526 raise ValueError(_('unsupported getbundle arguments: %s')
1526 % ', '.join(sorted(kwargs.keys())))
1527 % ', '.join(sorted(kwargs.keys())))
1527 return changegroup.getchangegroup(repo, source, heads=heads,
1528 return changegroup.getchangegroup(repo, source, heads=heads,
1528 common=common, bundlecaps=bundlecaps)
1529 common=common, bundlecaps=bundlecaps)
1529
1530
1530 # bundle20 case
1531 # bundle20 case
1531 b2caps = {}
1532 b2caps = {}
1532 for bcaps in bundlecaps:
1533 for bcaps in bundlecaps:
1533 if bcaps.startswith('bundle2='):
1534 if bcaps.startswith('bundle2='):
1534 blob = urllib.unquote(bcaps[len('bundle2='):])
1535 blob = urlreq.unquote(bcaps[len('bundle2='):])
1535 b2caps.update(bundle2.decodecaps(blob))
1536 b2caps.update(bundle2.decodecaps(blob))
1536 bundler = bundle2.bundle20(repo.ui, b2caps)
1537 bundler = bundle2.bundle20(repo.ui, b2caps)
1537
1538
1538 kwargs['heads'] = heads
1539 kwargs['heads'] = heads
1539 kwargs['common'] = common
1540 kwargs['common'] = common
1540
1541
1541 for name in getbundle2partsorder:
1542 for name in getbundle2partsorder:
1542 func = getbundle2partsmapping[name]
1543 func = getbundle2partsmapping[name]
1543 func(bundler, repo, source, bundlecaps=bundlecaps, b2caps=b2caps,
1544 func(bundler, repo, source, bundlecaps=bundlecaps, b2caps=b2caps,
1544 **kwargs)
1545 **kwargs)
1545
1546
1546 return util.chunkbuffer(bundler.getchunks())
1547 return util.chunkbuffer(bundler.getchunks())
1547
1548
1548 @getbundle2partsgenerator('changegroup')
1549 @getbundle2partsgenerator('changegroup')
1549 def _getbundlechangegrouppart(bundler, repo, source, bundlecaps=None,
1550 def _getbundlechangegrouppart(bundler, repo, source, bundlecaps=None,
1550 b2caps=None, heads=None, common=None, **kwargs):
1551 b2caps=None, heads=None, common=None, **kwargs):
1551 """add a changegroup part to the requested bundle"""
1552 """add a changegroup part to the requested bundle"""
1552 cg = None
1553 cg = None
1553 if kwargs.get('cg', True):
1554 if kwargs.get('cg', True):
1554 # build changegroup bundle here.
1555 # build changegroup bundle here.
1555 version = '01'
1556 version = '01'
1556 cgversions = b2caps.get('changegroup')
1557 cgversions = b2caps.get('changegroup')
1557 if cgversions: # 3.1 and 3.2 ship with an empty value
1558 if cgversions: # 3.1 and 3.2 ship with an empty value
1558 cgversions = [v for v in cgversions
1559 cgversions = [v for v in cgversions
1559 if v in changegroup.supportedoutgoingversions(repo)]
1560 if v in changegroup.supportedoutgoingversions(repo)]
1560 if not cgversions:
1561 if not cgversions:
1561 raise ValueError(_('no common changegroup version'))
1562 raise ValueError(_('no common changegroup version'))
1562 version = max(cgversions)
1563 version = max(cgversions)
1563 outgoing = changegroup.computeoutgoing(repo, heads, common)
1564 outgoing = changegroup.computeoutgoing(repo, heads, common)
1564 cg = changegroup.getlocalchangegroupraw(repo, source, outgoing,
1565 cg = changegroup.getlocalchangegroupraw(repo, source, outgoing,
1565 bundlecaps=bundlecaps,
1566 bundlecaps=bundlecaps,
1566 version=version)
1567 version=version)
1567
1568
1568 if cg:
1569 if cg:
1569 part = bundler.newpart('changegroup', data=cg)
1570 part = bundler.newpart('changegroup', data=cg)
1570 if cgversions:
1571 if cgversions:
1571 part.addparam('version', version)
1572 part.addparam('version', version)
1572 part.addparam('nbchanges', str(len(outgoing.missing)), mandatory=False)
1573 part.addparam('nbchanges', str(len(outgoing.missing)), mandatory=False)
1573 if 'treemanifest' in repo.requirements:
1574 if 'treemanifest' in repo.requirements:
1574 part.addparam('treemanifest', '1')
1575 part.addparam('treemanifest', '1')
1575
1576
1576 @getbundle2partsgenerator('listkeys')
1577 @getbundle2partsgenerator('listkeys')
1577 def _getbundlelistkeysparts(bundler, repo, source, bundlecaps=None,
1578 def _getbundlelistkeysparts(bundler, repo, source, bundlecaps=None,
1578 b2caps=None, **kwargs):
1579 b2caps=None, **kwargs):
1579 """add parts containing listkeys namespaces to the requested bundle"""
1580 """add parts containing listkeys namespaces to the requested bundle"""
1580 listkeys = kwargs.get('listkeys', ())
1581 listkeys = kwargs.get('listkeys', ())
1581 for namespace in listkeys:
1582 for namespace in listkeys:
1582 part = bundler.newpart('listkeys')
1583 part = bundler.newpart('listkeys')
1583 part.addparam('namespace', namespace)
1584 part.addparam('namespace', namespace)
1584 keys = repo.listkeys(namespace).items()
1585 keys = repo.listkeys(namespace).items()
1585 part.data = pushkey.encodekeys(keys)
1586 part.data = pushkey.encodekeys(keys)
1586
1587
1587 @getbundle2partsgenerator('obsmarkers')
1588 @getbundle2partsgenerator('obsmarkers')
1588 def _getbundleobsmarkerpart(bundler, repo, source, bundlecaps=None,
1589 def _getbundleobsmarkerpart(bundler, repo, source, bundlecaps=None,
1589 b2caps=None, heads=None, **kwargs):
1590 b2caps=None, heads=None, **kwargs):
1590 """add an obsolescence markers part to the requested bundle"""
1591 """add an obsolescence markers part to the requested bundle"""
1591 if kwargs.get('obsmarkers', False):
1592 if kwargs.get('obsmarkers', False):
1592 if heads is None:
1593 if heads is None:
1593 heads = repo.heads()
1594 heads = repo.heads()
1594 subset = [c.node() for c in repo.set('::%ln', heads)]
1595 subset = [c.node() for c in repo.set('::%ln', heads)]
1595 markers = repo.obsstore.relevantmarkers(subset)
1596 markers = repo.obsstore.relevantmarkers(subset)
1596 markers = sorted(markers)
1597 markers = sorted(markers)
1597 buildobsmarkerspart(bundler, markers)
1598 buildobsmarkerspart(bundler, markers)
1598
1599
1599 @getbundle2partsgenerator('hgtagsfnodes')
1600 @getbundle2partsgenerator('hgtagsfnodes')
1600 def _getbundletagsfnodes(bundler, repo, source, bundlecaps=None,
1601 def _getbundletagsfnodes(bundler, repo, source, bundlecaps=None,
1601 b2caps=None, heads=None, common=None,
1602 b2caps=None, heads=None, common=None,
1602 **kwargs):
1603 **kwargs):
1603 """Transfer the .hgtags filenodes mapping.
1604 """Transfer the .hgtags filenodes mapping.
1604
1605
1605 Only values for heads in this bundle will be transferred.
1606 Only values for heads in this bundle will be transferred.
1606
1607
1607 The part data consists of pairs of 20 byte changeset node and .hgtags
1608 The part data consists of pairs of 20 byte changeset node and .hgtags
1608 filenodes raw values.
1609 filenodes raw values.
1609 """
1610 """
1610 # Don't send unless:
1611 # Don't send unless:
1611 # - changeset are being exchanged,
1612 # - changeset are being exchanged,
1612 # - the client supports it.
1613 # - the client supports it.
1613 if not (kwargs.get('cg', True) and 'hgtagsfnodes' in b2caps):
1614 if not (kwargs.get('cg', True) and 'hgtagsfnodes' in b2caps):
1614 return
1615 return
1615
1616
1616 outgoing = changegroup.computeoutgoing(repo, heads, common)
1617 outgoing = changegroup.computeoutgoing(repo, heads, common)
1617
1618
1618 if not outgoing.missingheads:
1619 if not outgoing.missingheads:
1619 return
1620 return
1620
1621
1621 cache = tags.hgtagsfnodescache(repo.unfiltered())
1622 cache = tags.hgtagsfnodescache(repo.unfiltered())
1622 chunks = []
1623 chunks = []
1623
1624
1624 # .hgtags fnodes are only relevant for head changesets. While we could
1625 # .hgtags fnodes are only relevant for head changesets. While we could
1625 # transfer values for all known nodes, there will likely be little to
1626 # transfer values for all known nodes, there will likely be little to
1626 # no benefit.
1627 # no benefit.
1627 #
1628 #
1628 # We don't bother using a generator to produce output data because
1629 # We don't bother using a generator to produce output data because
1629 # a) we only have 40 bytes per head and even esoteric numbers of heads
1630 # a) we only have 40 bytes per head and even esoteric numbers of heads
1630 # consume little memory (1M heads is 40MB) b) we don't want to send the
1631 # consume little memory (1M heads is 40MB) b) we don't want to send the
1631 # part if we don't have entries and knowing if we have entries requires
1632 # part if we don't have entries and knowing if we have entries requires
1632 # cache lookups.
1633 # cache lookups.
1633 for node in outgoing.missingheads:
1634 for node in outgoing.missingheads:
1634 # Don't compute missing, as this may slow down serving.
1635 # Don't compute missing, as this may slow down serving.
1635 fnode = cache.getfnode(node, computemissing=False)
1636 fnode = cache.getfnode(node, computemissing=False)
1636 if fnode is not None:
1637 if fnode is not None:
1637 chunks.extend([node, fnode])
1638 chunks.extend([node, fnode])
1638
1639
1639 if chunks:
1640 if chunks:
1640 bundler.newpart('hgtagsfnodes', data=''.join(chunks))
1641 bundler.newpart('hgtagsfnodes', data=''.join(chunks))
1641
1642
1642 def check_heads(repo, their_heads, context):
1643 def check_heads(repo, their_heads, context):
1643 """check if the heads of a repo have been modified
1644 """check if the heads of a repo have been modified
1644
1645
1645 Used by peer for unbundling.
1646 Used by peer for unbundling.
1646 """
1647 """
1647 heads = repo.heads()
1648 heads = repo.heads()
1648 heads_hash = util.sha1(''.join(sorted(heads))).digest()
1649 heads_hash = util.sha1(''.join(sorted(heads))).digest()
1649 if not (their_heads == ['force'] or their_heads == heads or
1650 if not (their_heads == ['force'] or their_heads == heads or
1650 their_heads == ['hashed', heads_hash]):
1651 their_heads == ['hashed', heads_hash]):
1651 # someone else committed/pushed/unbundled while we
1652 # someone else committed/pushed/unbundled while we
1652 # were transferring data
1653 # were transferring data
1653 raise error.PushRaced('repository changed while %s - '
1654 raise error.PushRaced('repository changed while %s - '
1654 'please try again' % context)
1655 'please try again' % context)
1655
1656
1656 def unbundle(repo, cg, heads, source, url):
1657 def unbundle(repo, cg, heads, source, url):
1657 """Apply a bundle to a repo.
1658 """Apply a bundle to a repo.
1658
1659
1659 this function makes sure the repo is locked during the application and have
1660 this function makes sure the repo is locked during the application and have
1660 mechanism to check that no push race occurred between the creation of the
1661 mechanism to check that no push race occurred between the creation of the
1661 bundle and its application.
1662 bundle and its application.
1662
1663
1663 If the push was raced as PushRaced exception is raised."""
1664 If the push was raced as PushRaced exception is raised."""
1664 r = 0
1665 r = 0
1665 # need a transaction when processing a bundle2 stream
1666 # need a transaction when processing a bundle2 stream
1666 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
1667 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
1667 lockandtr = [None, None, None]
1668 lockandtr = [None, None, None]
1668 recordout = None
1669 recordout = None
1669 # quick fix for output mismatch with bundle2 in 3.4
1670 # quick fix for output mismatch with bundle2 in 3.4
1670 captureoutput = repo.ui.configbool('experimental', 'bundle2-output-capture',
1671 captureoutput = repo.ui.configbool('experimental', 'bundle2-output-capture',
1671 False)
1672 False)
1672 if url.startswith('remote:http:') or url.startswith('remote:https:'):
1673 if url.startswith('remote:http:') or url.startswith('remote:https:'):
1673 captureoutput = True
1674 captureoutput = True
1674 try:
1675 try:
1675 check_heads(repo, heads, 'uploading changes')
1676 check_heads(repo, heads, 'uploading changes')
1676 # push can proceed
1677 # push can proceed
1677 if util.safehasattr(cg, 'params'):
1678 if util.safehasattr(cg, 'params'):
1678 r = None
1679 r = None
1679 try:
1680 try:
1680 def gettransaction():
1681 def gettransaction():
1681 if not lockandtr[2]:
1682 if not lockandtr[2]:
1682 lockandtr[0] = repo.wlock()
1683 lockandtr[0] = repo.wlock()
1683 lockandtr[1] = repo.lock()
1684 lockandtr[1] = repo.lock()
1684 lockandtr[2] = repo.transaction(source)
1685 lockandtr[2] = repo.transaction(source)
1685 lockandtr[2].hookargs['source'] = source
1686 lockandtr[2].hookargs['source'] = source
1686 lockandtr[2].hookargs['url'] = url
1687 lockandtr[2].hookargs['url'] = url
1687 lockandtr[2].hookargs['bundle2'] = '1'
1688 lockandtr[2].hookargs['bundle2'] = '1'
1688 return lockandtr[2]
1689 return lockandtr[2]
1689
1690
1690 # Do greedy locking by default until we're satisfied with lazy
1691 # Do greedy locking by default until we're satisfied with lazy
1691 # locking.
1692 # locking.
1692 if not repo.ui.configbool('experimental', 'bundle2lazylocking'):
1693 if not repo.ui.configbool('experimental', 'bundle2lazylocking'):
1693 gettransaction()
1694 gettransaction()
1694
1695
1695 op = bundle2.bundleoperation(repo, gettransaction,
1696 op = bundle2.bundleoperation(repo, gettransaction,
1696 captureoutput=captureoutput)
1697 captureoutput=captureoutput)
1697 try:
1698 try:
1698 op = bundle2.processbundle(repo, cg, op=op)
1699 op = bundle2.processbundle(repo, cg, op=op)
1699 finally:
1700 finally:
1700 r = op.reply
1701 r = op.reply
1701 if captureoutput and r is not None:
1702 if captureoutput and r is not None:
1702 repo.ui.pushbuffer(error=True, subproc=True)
1703 repo.ui.pushbuffer(error=True, subproc=True)
1703 def recordout(output):
1704 def recordout(output):
1704 r.newpart('output', data=output, mandatory=False)
1705 r.newpart('output', data=output, mandatory=False)
1705 if lockandtr[2] is not None:
1706 if lockandtr[2] is not None:
1706 lockandtr[2].close()
1707 lockandtr[2].close()
1707 except BaseException as exc:
1708 except BaseException as exc:
1708 exc.duringunbundle2 = True
1709 exc.duringunbundle2 = True
1709 if captureoutput and r is not None:
1710 if captureoutput and r is not None:
1710 parts = exc._bundle2salvagedoutput = r.salvageoutput()
1711 parts = exc._bundle2salvagedoutput = r.salvageoutput()
1711 def recordout(output):
1712 def recordout(output):
1712 part = bundle2.bundlepart('output', data=output,
1713 part = bundle2.bundlepart('output', data=output,
1713 mandatory=False)
1714 mandatory=False)
1714 parts.append(part)
1715 parts.append(part)
1715 raise
1716 raise
1716 else:
1717 else:
1717 lockandtr[1] = repo.lock()
1718 lockandtr[1] = repo.lock()
1718 r = cg.apply(repo, source, url)
1719 r = cg.apply(repo, source, url)
1719 finally:
1720 finally:
1720 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
1721 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
1721 if recordout is not None:
1722 if recordout is not None:
1722 recordout(repo.ui.popbuffer())
1723 recordout(repo.ui.popbuffer())
1723 return r
1724 return r
1724
1725
1725 def _maybeapplyclonebundle(pullop):
1726 def _maybeapplyclonebundle(pullop):
1726 """Apply a clone bundle from a remote, if possible."""
1727 """Apply a clone bundle from a remote, if possible."""
1727
1728
1728 repo = pullop.repo
1729 repo = pullop.repo
1729 remote = pullop.remote
1730 remote = pullop.remote
1730
1731
1731 if not repo.ui.configbool('ui', 'clonebundles', True):
1732 if not repo.ui.configbool('ui', 'clonebundles', True):
1732 return
1733 return
1733
1734
1734 # Only run if local repo is empty.
1735 # Only run if local repo is empty.
1735 if len(repo):
1736 if len(repo):
1736 return
1737 return
1737
1738
1738 if pullop.heads:
1739 if pullop.heads:
1739 return
1740 return
1740
1741
1741 if not remote.capable('clonebundles'):
1742 if not remote.capable('clonebundles'):
1742 return
1743 return
1743
1744
1744 res = remote._call('clonebundles')
1745 res = remote._call('clonebundles')
1745
1746
1746 # If we call the wire protocol command, that's good enough to record the
1747 # If we call the wire protocol command, that's good enough to record the
1747 # attempt.
1748 # attempt.
1748 pullop.clonebundleattempted = True
1749 pullop.clonebundleattempted = True
1749
1750
1750 entries = parseclonebundlesmanifest(repo, res)
1751 entries = parseclonebundlesmanifest(repo, res)
1751 if not entries:
1752 if not entries:
1752 repo.ui.note(_('no clone bundles available on remote; '
1753 repo.ui.note(_('no clone bundles available on remote; '
1753 'falling back to regular clone\n'))
1754 'falling back to regular clone\n'))
1754 return
1755 return
1755
1756
1756 entries = filterclonebundleentries(repo, entries)
1757 entries = filterclonebundleentries(repo, entries)
1757 if not entries:
1758 if not entries:
1758 # There is a thundering herd concern here. However, if a server
1759 # There is a thundering herd concern here. However, if a server
1759 # operator doesn't advertise bundles appropriate for its clients,
1760 # operator doesn't advertise bundles appropriate for its clients,
1760 # they deserve what's coming. Furthermore, from a client's
1761 # they deserve what's coming. Furthermore, from a client's
1761 # perspective, no automatic fallback would mean not being able to
1762 # perspective, no automatic fallback would mean not being able to
1762 # clone!
1763 # clone!
1763 repo.ui.warn(_('no compatible clone bundles available on server; '
1764 repo.ui.warn(_('no compatible clone bundles available on server; '
1764 'falling back to regular clone\n'))
1765 'falling back to regular clone\n'))
1765 repo.ui.warn(_('(you may want to report this to the server '
1766 repo.ui.warn(_('(you may want to report this to the server '
1766 'operator)\n'))
1767 'operator)\n'))
1767 return
1768 return
1768
1769
1769 entries = sortclonebundleentries(repo.ui, entries)
1770 entries = sortclonebundleentries(repo.ui, entries)
1770
1771
1771 url = entries[0]['URL']
1772 url = entries[0]['URL']
1772 repo.ui.status(_('applying clone bundle from %s\n') % url)
1773 repo.ui.status(_('applying clone bundle from %s\n') % url)
1773 if trypullbundlefromurl(repo.ui, repo, url):
1774 if trypullbundlefromurl(repo.ui, repo, url):
1774 repo.ui.status(_('finished applying clone bundle\n'))
1775 repo.ui.status(_('finished applying clone bundle\n'))
1775 # Bundle failed.
1776 # Bundle failed.
1776 #
1777 #
1777 # We abort by default to avoid the thundering herd of
1778 # We abort by default to avoid the thundering herd of
1778 # clients flooding a server that was expecting expensive
1779 # clients flooding a server that was expecting expensive
1779 # clone load to be offloaded.
1780 # clone load to be offloaded.
1780 elif repo.ui.configbool('ui', 'clonebundlefallback', False):
1781 elif repo.ui.configbool('ui', 'clonebundlefallback', False):
1781 repo.ui.warn(_('falling back to normal clone\n'))
1782 repo.ui.warn(_('falling back to normal clone\n'))
1782 else:
1783 else:
1783 raise error.Abort(_('error applying bundle'),
1784 raise error.Abort(_('error applying bundle'),
1784 hint=_('if this error persists, consider contacting '
1785 hint=_('if this error persists, consider contacting '
1785 'the server operator or disable clone '
1786 'the server operator or disable clone '
1786 'bundles via '
1787 'bundles via '
1787 '"--config ui.clonebundles=false"'))
1788 '"--config ui.clonebundles=false"'))
1788
1789
1789 def parseclonebundlesmanifest(repo, s):
1790 def parseclonebundlesmanifest(repo, s):
1790 """Parses the raw text of a clone bundles manifest.
1791 """Parses the raw text of a clone bundles manifest.
1791
1792
1792 Returns a list of dicts. The dicts have a ``URL`` key corresponding
1793 Returns a list of dicts. The dicts have a ``URL`` key corresponding
1793 to the URL and other keys are the attributes for the entry.
1794 to the URL and other keys are the attributes for the entry.
1794 """
1795 """
1795 m = []
1796 m = []
1796 for line in s.splitlines():
1797 for line in s.splitlines():
1797 fields = line.split()
1798 fields = line.split()
1798 if not fields:
1799 if not fields:
1799 continue
1800 continue
1800 attrs = {'URL': fields[0]}
1801 attrs = {'URL': fields[0]}
1801 for rawattr in fields[1:]:
1802 for rawattr in fields[1:]:
1802 key, value = rawattr.split('=', 1)
1803 key, value = rawattr.split('=', 1)
1803 key = urllib.unquote(key)
1804 key = urlreq.unquote(key)
1804 value = urllib.unquote(value)
1805 value = urlreq.unquote(value)
1805 attrs[key] = value
1806 attrs[key] = value
1806
1807
1807 # Parse BUNDLESPEC into components. This makes client-side
1808 # Parse BUNDLESPEC into components. This makes client-side
1808 # preferences easier to specify since you can prefer a single
1809 # preferences easier to specify since you can prefer a single
1809 # component of the BUNDLESPEC.
1810 # component of the BUNDLESPEC.
1810 if key == 'BUNDLESPEC':
1811 if key == 'BUNDLESPEC':
1811 try:
1812 try:
1812 comp, version, params = parsebundlespec(repo, value,
1813 comp, version, params = parsebundlespec(repo, value,
1813 externalnames=True)
1814 externalnames=True)
1814 attrs['COMPRESSION'] = comp
1815 attrs['COMPRESSION'] = comp
1815 attrs['VERSION'] = version
1816 attrs['VERSION'] = version
1816 except error.InvalidBundleSpecification:
1817 except error.InvalidBundleSpecification:
1817 pass
1818 pass
1818 except error.UnsupportedBundleSpecification:
1819 except error.UnsupportedBundleSpecification:
1819 pass
1820 pass
1820
1821
1821 m.append(attrs)
1822 m.append(attrs)
1822
1823
1823 return m
1824 return m
1824
1825
1825 def filterclonebundleentries(repo, entries):
1826 def filterclonebundleentries(repo, entries):
1826 """Remove incompatible clone bundle manifest entries.
1827 """Remove incompatible clone bundle manifest entries.
1827
1828
1828 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
1829 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
1829 and returns a new list consisting of only the entries that this client
1830 and returns a new list consisting of only the entries that this client
1830 should be able to apply.
1831 should be able to apply.
1831
1832
1832 There is no guarantee we'll be able to apply all returned entries because
1833 There is no guarantee we'll be able to apply all returned entries because
1833 the metadata we use to filter on may be missing or wrong.
1834 the metadata we use to filter on may be missing or wrong.
1834 """
1835 """
1835 newentries = []
1836 newentries = []
1836 for entry in entries:
1837 for entry in entries:
1837 spec = entry.get('BUNDLESPEC')
1838 spec = entry.get('BUNDLESPEC')
1838 if spec:
1839 if spec:
1839 try:
1840 try:
1840 parsebundlespec(repo, spec, strict=True)
1841 parsebundlespec(repo, spec, strict=True)
1841 except error.InvalidBundleSpecification as e:
1842 except error.InvalidBundleSpecification as e:
1842 repo.ui.debug(str(e) + '\n')
1843 repo.ui.debug(str(e) + '\n')
1843 continue
1844 continue
1844 except error.UnsupportedBundleSpecification as e:
1845 except error.UnsupportedBundleSpecification as e:
1845 repo.ui.debug('filtering %s because unsupported bundle '
1846 repo.ui.debug('filtering %s because unsupported bundle '
1846 'spec: %s\n' % (entry['URL'], str(e)))
1847 'spec: %s\n' % (entry['URL'], str(e)))
1847 continue
1848 continue
1848
1849
1849 if 'REQUIRESNI' in entry and not sslutil.hassni:
1850 if 'REQUIRESNI' in entry and not sslutil.hassni:
1850 repo.ui.debug('filtering %s because SNI not supported\n' %
1851 repo.ui.debug('filtering %s because SNI not supported\n' %
1851 entry['URL'])
1852 entry['URL'])
1852 continue
1853 continue
1853
1854
1854 newentries.append(entry)
1855 newentries.append(entry)
1855
1856
1856 return newentries
1857 return newentries
1857
1858
1858 def sortclonebundleentries(ui, entries):
1859 def sortclonebundleentries(ui, entries):
1859 prefers = ui.configlist('ui', 'clonebundleprefers', default=[])
1860 prefers = ui.configlist('ui', 'clonebundleprefers', default=[])
1860 if not prefers:
1861 if not prefers:
1861 return list(entries)
1862 return list(entries)
1862
1863
1863 prefers = [p.split('=', 1) for p in prefers]
1864 prefers = [p.split('=', 1) for p in prefers]
1864
1865
1865 # Our sort function.
1866 # Our sort function.
1866 def compareentry(a, b):
1867 def compareentry(a, b):
1867 for prefkey, prefvalue in prefers:
1868 for prefkey, prefvalue in prefers:
1868 avalue = a.get(prefkey)
1869 avalue = a.get(prefkey)
1869 bvalue = b.get(prefkey)
1870 bvalue = b.get(prefkey)
1870
1871
1871 # Special case for b missing attribute and a matches exactly.
1872 # Special case for b missing attribute and a matches exactly.
1872 if avalue is not None and bvalue is None and avalue == prefvalue:
1873 if avalue is not None and bvalue is None and avalue == prefvalue:
1873 return -1
1874 return -1
1874
1875
1875 # Special case for a missing attribute and b matches exactly.
1876 # Special case for a missing attribute and b matches exactly.
1876 if bvalue is not None and avalue is None and bvalue == prefvalue:
1877 if bvalue is not None and avalue is None and bvalue == prefvalue:
1877 return 1
1878 return 1
1878
1879
1879 # We can't compare unless attribute present on both.
1880 # We can't compare unless attribute present on both.
1880 if avalue is None or bvalue is None:
1881 if avalue is None or bvalue is None:
1881 continue
1882 continue
1882
1883
1883 # Same values should fall back to next attribute.
1884 # Same values should fall back to next attribute.
1884 if avalue == bvalue:
1885 if avalue == bvalue:
1885 continue
1886 continue
1886
1887
1887 # Exact matches come first.
1888 # Exact matches come first.
1888 if avalue == prefvalue:
1889 if avalue == prefvalue:
1889 return -1
1890 return -1
1890 if bvalue == prefvalue:
1891 if bvalue == prefvalue:
1891 return 1
1892 return 1
1892
1893
1893 # Fall back to next attribute.
1894 # Fall back to next attribute.
1894 continue
1895 continue
1895
1896
1896 # If we got here we couldn't sort by attributes and prefers. Fall
1897 # If we got here we couldn't sort by attributes and prefers. Fall
1897 # back to index order.
1898 # back to index order.
1898 return 0
1899 return 0
1899
1900
1900 return sorted(entries, cmp=compareentry)
1901 return sorted(entries, cmp=compareentry)
1901
1902
1902 def trypullbundlefromurl(ui, repo, url):
1903 def trypullbundlefromurl(ui, repo, url):
1903 """Attempt to apply a bundle from a URL."""
1904 """Attempt to apply a bundle from a URL."""
1904 lock = repo.lock()
1905 lock = repo.lock()
1905 try:
1906 try:
1906 tr = repo.transaction('bundleurl')
1907 tr = repo.transaction('bundleurl')
1907 try:
1908 try:
1908 try:
1909 try:
1909 fh = urlmod.open(ui, url)
1910 fh = urlmod.open(ui, url)
1910 cg = readbundle(ui, fh, 'stream')
1911 cg = readbundle(ui, fh, 'stream')
1911
1912
1912 if isinstance(cg, bundle2.unbundle20):
1913 if isinstance(cg, bundle2.unbundle20):
1913 bundle2.processbundle(repo, cg, lambda: tr)
1914 bundle2.processbundle(repo, cg, lambda: tr)
1914 elif isinstance(cg, streamclone.streamcloneapplier):
1915 elif isinstance(cg, streamclone.streamcloneapplier):
1915 cg.apply(repo)
1916 cg.apply(repo)
1916 else:
1917 else:
1917 cg.apply(repo, 'clonebundles', url)
1918 cg.apply(repo, 'clonebundles', url)
1918 tr.close()
1919 tr.close()
1919 return True
1920 return True
1920 except urllib2.HTTPError as e:
1921 except urlerr.httperror as e:
1921 ui.warn(_('HTTP error fetching bundle: %s\n') % str(e))
1922 ui.warn(_('HTTP error fetching bundle: %s\n') % str(e))
1922 except urllib2.URLError as e:
1923 except urlerr.urlerror as e:
1923 ui.warn(_('error fetching bundle: %s\n') % e.reason[1])
1924 ui.warn(_('error fetching bundle: %s\n') % e.reason[1])
1924
1925
1925 return False
1926 return False
1926 finally:
1927 finally:
1927 tr.release()
1928 tr.release()
1928 finally:
1929 finally:
1929 lock.release()
1930 lock.release()
@@ -1,115 +1,117 b''
1 #
1 #
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import cgi
10 import cgi
11 import urllib
12 import zlib
11 import zlib
13
12
14 from .common import (
13 from .common import (
15 HTTP_OK,
14 HTTP_OK,
16 )
15 )
17
16
18 from .. import (
17 from .. import (
19 util,
18 util,
20 wireproto,
19 wireproto,
21 )
20 )
22 stringio = util.stringio
21 stringio = util.stringio
23
22
23 urlerr = util.urlerr
24 urlreq = util.urlreq
25
24 HGTYPE = 'application/mercurial-0.1'
26 HGTYPE = 'application/mercurial-0.1'
25 HGERRTYPE = 'application/hg-error'
27 HGERRTYPE = 'application/hg-error'
26
28
27 class webproto(wireproto.abstractserverproto):
29 class webproto(wireproto.abstractserverproto):
28 def __init__(self, req, ui):
30 def __init__(self, req, ui):
29 self.req = req
31 self.req = req
30 self.response = ''
32 self.response = ''
31 self.ui = ui
33 self.ui = ui
32 def getargs(self, args):
34 def getargs(self, args):
33 knownargs = self._args()
35 knownargs = self._args()
34 data = {}
36 data = {}
35 keys = args.split()
37 keys = args.split()
36 for k in keys:
38 for k in keys:
37 if k == '*':
39 if k == '*':
38 star = {}
40 star = {}
39 for key in knownargs.keys():
41 for key in knownargs.keys():
40 if key != 'cmd' and key not in keys:
42 if key != 'cmd' and key not in keys:
41 star[key] = knownargs[key][0]
43 star[key] = knownargs[key][0]
42 data['*'] = star
44 data['*'] = star
43 else:
45 else:
44 data[k] = knownargs[k][0]
46 data[k] = knownargs[k][0]
45 return [data[k] for k in keys]
47 return [data[k] for k in keys]
46 def _args(self):
48 def _args(self):
47 args = self.req.form.copy()
49 args = self.req.form.copy()
48 postlen = int(self.req.env.get('HTTP_X_HGARGS_POST', 0))
50 postlen = int(self.req.env.get('HTTP_X_HGARGS_POST', 0))
49 if postlen:
51 if postlen:
50 args.update(cgi.parse_qs(
52 args.update(cgi.parse_qs(
51 self.req.read(postlen), keep_blank_values=True))
53 self.req.read(postlen), keep_blank_values=True))
52 return args
54 return args
53 chunks = []
55 chunks = []
54 i = 1
56 i = 1
55 while True:
57 while True:
56 h = self.req.env.get('HTTP_X_HGARG_' + str(i))
58 h = self.req.env.get('HTTP_X_HGARG_' + str(i))
57 if h is None:
59 if h is None:
58 break
60 break
59 chunks += [h]
61 chunks += [h]
60 i += 1
62 i += 1
61 args.update(cgi.parse_qs(''.join(chunks), keep_blank_values=True))
63 args.update(cgi.parse_qs(''.join(chunks), keep_blank_values=True))
62 return args
64 return args
63 def getfile(self, fp):
65 def getfile(self, fp):
64 length = int(self.req.env['CONTENT_LENGTH'])
66 length = int(self.req.env['CONTENT_LENGTH'])
65 for s in util.filechunkiter(self.req, limit=length):
67 for s in util.filechunkiter(self.req, limit=length):
66 fp.write(s)
68 fp.write(s)
67 def redirect(self):
69 def redirect(self):
68 self.oldio = self.ui.fout, self.ui.ferr
70 self.oldio = self.ui.fout, self.ui.ferr
69 self.ui.ferr = self.ui.fout = stringio()
71 self.ui.ferr = self.ui.fout = stringio()
70 def restore(self):
72 def restore(self):
71 val = self.ui.fout.getvalue()
73 val = self.ui.fout.getvalue()
72 self.ui.ferr, self.ui.fout = self.oldio
74 self.ui.ferr, self.ui.fout = self.oldio
73 return val
75 return val
74 def groupchunks(self, cg):
76 def groupchunks(self, cg):
75 z = zlib.compressobj()
77 z = zlib.compressobj()
76 while True:
78 while True:
77 chunk = cg.read(4096)
79 chunk = cg.read(4096)
78 if not chunk:
80 if not chunk:
79 break
81 break
80 yield z.compress(chunk)
82 yield z.compress(chunk)
81 yield z.flush()
83 yield z.flush()
82 def _client(self):
84 def _client(self):
83 return 'remote:%s:%s:%s' % (
85 return 'remote:%s:%s:%s' % (
84 self.req.env.get('wsgi.url_scheme') or 'http',
86 self.req.env.get('wsgi.url_scheme') or 'http',
85 urllib.quote(self.req.env.get('REMOTE_HOST', '')),
87 urlreq.quote(self.req.env.get('REMOTE_HOST', '')),
86 urllib.quote(self.req.env.get('REMOTE_USER', '')))
88 urlreq.quote(self.req.env.get('REMOTE_USER', '')))
87
89
88 def iscmd(cmd):
90 def iscmd(cmd):
89 return cmd in wireproto.commands
91 return cmd in wireproto.commands
90
92
91 def call(repo, req, cmd):
93 def call(repo, req, cmd):
92 p = webproto(req, repo.ui)
94 p = webproto(req, repo.ui)
93 rsp = wireproto.dispatch(repo, p, cmd)
95 rsp = wireproto.dispatch(repo, p, cmd)
94 if isinstance(rsp, str):
96 if isinstance(rsp, str):
95 req.respond(HTTP_OK, HGTYPE, body=rsp)
97 req.respond(HTTP_OK, HGTYPE, body=rsp)
96 return []
98 return []
97 elif isinstance(rsp, wireproto.streamres):
99 elif isinstance(rsp, wireproto.streamres):
98 req.respond(HTTP_OK, HGTYPE)
100 req.respond(HTTP_OK, HGTYPE)
99 return rsp.gen
101 return rsp.gen
100 elif isinstance(rsp, wireproto.pushres):
102 elif isinstance(rsp, wireproto.pushres):
101 val = p.restore()
103 val = p.restore()
102 rsp = '%d\n%s' % (rsp.res, val)
104 rsp = '%d\n%s' % (rsp.res, val)
103 req.respond(HTTP_OK, HGTYPE, body=rsp)
105 req.respond(HTTP_OK, HGTYPE, body=rsp)
104 return []
106 return []
105 elif isinstance(rsp, wireproto.pusherr):
107 elif isinstance(rsp, wireproto.pusherr):
106 # drain the incoming bundle
108 # drain the incoming bundle
107 req.drain()
109 req.drain()
108 p.restore()
110 p.restore()
109 rsp = '0\n%s\n' % rsp.res
111 rsp = '0\n%s\n' % rsp.res
110 req.respond(HTTP_OK, HGTYPE, body=rsp)
112 req.respond(HTTP_OK, HGTYPE, body=rsp)
111 return []
113 return []
112 elif isinstance(rsp, wireproto.ooberror):
114 elif isinstance(rsp, wireproto.ooberror):
113 rsp = rsp.message
115 rsp = rsp.message
114 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
116 req.respond(HTTP_OK, HGERRTYPE, body=rsp)
115 return []
117 return []
@@ -1,322 +1,324 b''
1 # hgweb/server.py - The standalone hg web server.
1 # hgweb/server.py - The standalone hg web server.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import BaseHTTPServer
11 import BaseHTTPServer
12 import SocketServer
12 import SocketServer
13 import errno
13 import errno
14 import os
14 import os
15 import socket
15 import socket
16 import sys
16 import sys
17 import traceback
17 import traceback
18 import urllib
19
18
20 from ..i18n import _
19 from ..i18n import _
21
20
22 from .. import (
21 from .. import (
23 error,
22 error,
24 util,
23 util,
25 )
24 )
26
25
26 urlerr = util.urlerr
27 urlreq = util.urlreq
28
27 from . import (
29 from . import (
28 common,
30 common,
29 )
31 )
30
32
31 def _splitURI(uri):
33 def _splitURI(uri):
32 """Return path and query that has been split from uri
34 """Return path and query that has been split from uri
33
35
34 Just like CGI environment, the path is unquoted, the query is
36 Just like CGI environment, the path is unquoted, the query is
35 not.
37 not.
36 """
38 """
37 if '?' in uri:
39 if '?' in uri:
38 path, query = uri.split('?', 1)
40 path, query = uri.split('?', 1)
39 else:
41 else:
40 path, query = uri, ''
42 path, query = uri, ''
41 return urllib.unquote(path), query
43 return urlreq.unquote(path), query
42
44
43 class _error_logger(object):
45 class _error_logger(object):
44 def __init__(self, handler):
46 def __init__(self, handler):
45 self.handler = handler
47 self.handler = handler
46 def flush(self):
48 def flush(self):
47 pass
49 pass
48 def write(self, str):
50 def write(self, str):
49 self.writelines(str.split('\n'))
51 self.writelines(str.split('\n'))
50 def writelines(self, seq):
52 def writelines(self, seq):
51 for msg in seq:
53 for msg in seq:
52 self.handler.log_error("HG error: %s", msg)
54 self.handler.log_error("HG error: %s", msg)
53
55
54 class _httprequesthandler(BaseHTTPServer.BaseHTTPRequestHandler):
56 class _httprequesthandler(BaseHTTPServer.BaseHTTPRequestHandler):
55
57
56 url_scheme = 'http'
58 url_scheme = 'http'
57
59
58 @staticmethod
60 @staticmethod
59 def preparehttpserver(httpserver, ssl_cert):
61 def preparehttpserver(httpserver, ssl_cert):
60 """Prepare .socket of new HTTPServer instance"""
62 """Prepare .socket of new HTTPServer instance"""
61 pass
63 pass
62
64
63 def __init__(self, *args, **kargs):
65 def __init__(self, *args, **kargs):
64 self.protocol_version = 'HTTP/1.1'
66 self.protocol_version = 'HTTP/1.1'
65 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
67 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
66
68
67 def _log_any(self, fp, format, *args):
69 def _log_any(self, fp, format, *args):
68 fp.write("%s - - [%s] %s\n" % (self.client_address[0],
70 fp.write("%s - - [%s] %s\n" % (self.client_address[0],
69 self.log_date_time_string(),
71 self.log_date_time_string(),
70 format % args))
72 format % args))
71 fp.flush()
73 fp.flush()
72
74
73 def log_error(self, format, *args):
75 def log_error(self, format, *args):
74 self._log_any(self.server.errorlog, format, *args)
76 self._log_any(self.server.errorlog, format, *args)
75
77
76 def log_message(self, format, *args):
78 def log_message(self, format, *args):
77 self._log_any(self.server.accesslog, format, *args)
79 self._log_any(self.server.accesslog, format, *args)
78
80
79 def log_request(self, code='-', size='-'):
81 def log_request(self, code='-', size='-'):
80 xheaders = []
82 xheaders = []
81 if util.safehasattr(self, 'headers'):
83 if util.safehasattr(self, 'headers'):
82 xheaders = [h for h in self.headers.items()
84 xheaders = [h for h in self.headers.items()
83 if h[0].startswith('x-')]
85 if h[0].startswith('x-')]
84 self.log_message('"%s" %s %s%s',
86 self.log_message('"%s" %s %s%s',
85 self.requestline, str(code), str(size),
87 self.requestline, str(code), str(size),
86 ''.join([' %s:%s' % h for h in sorted(xheaders)]))
88 ''.join([' %s:%s' % h for h in sorted(xheaders)]))
87
89
88 def do_write(self):
90 def do_write(self):
89 try:
91 try:
90 self.do_hgweb()
92 self.do_hgweb()
91 except socket.error as inst:
93 except socket.error as inst:
92 if inst[0] != errno.EPIPE:
94 if inst[0] != errno.EPIPE:
93 raise
95 raise
94
96
95 def do_POST(self):
97 def do_POST(self):
96 try:
98 try:
97 self.do_write()
99 self.do_write()
98 except Exception:
100 except Exception:
99 self._start_response("500 Internal Server Error", [])
101 self._start_response("500 Internal Server Error", [])
100 self._write("Internal Server Error")
102 self._write("Internal Server Error")
101 self._done()
103 self._done()
102 tb = "".join(traceback.format_exception(*sys.exc_info()))
104 tb = "".join(traceback.format_exception(*sys.exc_info()))
103 self.log_error("Exception happened during processing "
105 self.log_error("Exception happened during processing "
104 "request '%s':\n%s", self.path, tb)
106 "request '%s':\n%s", self.path, tb)
105
107
106 def do_GET(self):
108 def do_GET(self):
107 self.do_POST()
109 self.do_POST()
108
110
109 def do_hgweb(self):
111 def do_hgweb(self):
110 path, query = _splitURI(self.path)
112 path, query = _splitURI(self.path)
111
113
112 env = {}
114 env = {}
113 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
115 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
114 env['REQUEST_METHOD'] = self.command
116 env['REQUEST_METHOD'] = self.command
115 env['SERVER_NAME'] = self.server.server_name
117 env['SERVER_NAME'] = self.server.server_name
116 env['SERVER_PORT'] = str(self.server.server_port)
118 env['SERVER_PORT'] = str(self.server.server_port)
117 env['REQUEST_URI'] = self.path
119 env['REQUEST_URI'] = self.path
118 env['SCRIPT_NAME'] = self.server.prefix
120 env['SCRIPT_NAME'] = self.server.prefix
119 env['PATH_INFO'] = path[len(self.server.prefix):]
121 env['PATH_INFO'] = path[len(self.server.prefix):]
120 env['REMOTE_HOST'] = self.client_address[0]
122 env['REMOTE_HOST'] = self.client_address[0]
121 env['REMOTE_ADDR'] = self.client_address[0]
123 env['REMOTE_ADDR'] = self.client_address[0]
122 if query:
124 if query:
123 env['QUERY_STRING'] = query
125 env['QUERY_STRING'] = query
124
126
125 if self.headers.typeheader is None:
127 if self.headers.typeheader is None:
126 env['CONTENT_TYPE'] = self.headers.type
128 env['CONTENT_TYPE'] = self.headers.type
127 else:
129 else:
128 env['CONTENT_TYPE'] = self.headers.typeheader
130 env['CONTENT_TYPE'] = self.headers.typeheader
129 length = self.headers.getheader('content-length')
131 length = self.headers.getheader('content-length')
130 if length:
132 if length:
131 env['CONTENT_LENGTH'] = length
133 env['CONTENT_LENGTH'] = length
132 for header in [h for h in self.headers.keys()
134 for header in [h for h in self.headers.keys()
133 if h not in ('content-type', 'content-length')]:
135 if h not in ('content-type', 'content-length')]:
134 hkey = 'HTTP_' + header.replace('-', '_').upper()
136 hkey = 'HTTP_' + header.replace('-', '_').upper()
135 hval = self.headers.getheader(header)
137 hval = self.headers.getheader(header)
136 hval = hval.replace('\n', '').strip()
138 hval = hval.replace('\n', '').strip()
137 if hval:
139 if hval:
138 env[hkey] = hval
140 env[hkey] = hval
139 env['SERVER_PROTOCOL'] = self.request_version
141 env['SERVER_PROTOCOL'] = self.request_version
140 env['wsgi.version'] = (1, 0)
142 env['wsgi.version'] = (1, 0)
141 env['wsgi.url_scheme'] = self.url_scheme
143 env['wsgi.url_scheme'] = self.url_scheme
142 if env.get('HTTP_EXPECT', '').lower() == '100-continue':
144 if env.get('HTTP_EXPECT', '').lower() == '100-continue':
143 self.rfile = common.continuereader(self.rfile, self.wfile.write)
145 self.rfile = common.continuereader(self.rfile, self.wfile.write)
144
146
145 env['wsgi.input'] = self.rfile
147 env['wsgi.input'] = self.rfile
146 env['wsgi.errors'] = _error_logger(self)
148 env['wsgi.errors'] = _error_logger(self)
147 env['wsgi.multithread'] = isinstance(self.server,
149 env['wsgi.multithread'] = isinstance(self.server,
148 SocketServer.ThreadingMixIn)
150 SocketServer.ThreadingMixIn)
149 env['wsgi.multiprocess'] = isinstance(self.server,
151 env['wsgi.multiprocess'] = isinstance(self.server,
150 SocketServer.ForkingMixIn)
152 SocketServer.ForkingMixIn)
151 env['wsgi.run_once'] = 0
153 env['wsgi.run_once'] = 0
152
154
153 self.saved_status = None
155 self.saved_status = None
154 self.saved_headers = []
156 self.saved_headers = []
155 self.sent_headers = False
157 self.sent_headers = False
156 self.length = None
158 self.length = None
157 self._chunked = None
159 self._chunked = None
158 for chunk in self.server.application(env, self._start_response):
160 for chunk in self.server.application(env, self._start_response):
159 self._write(chunk)
161 self._write(chunk)
160 if not self.sent_headers:
162 if not self.sent_headers:
161 self.send_headers()
163 self.send_headers()
162 self._done()
164 self._done()
163
165
164 def send_headers(self):
166 def send_headers(self):
165 if not self.saved_status:
167 if not self.saved_status:
166 raise AssertionError("Sending headers before "
168 raise AssertionError("Sending headers before "
167 "start_response() called")
169 "start_response() called")
168 saved_status = self.saved_status.split(None, 1)
170 saved_status = self.saved_status.split(None, 1)
169 saved_status[0] = int(saved_status[0])
171 saved_status[0] = int(saved_status[0])
170 self.send_response(*saved_status)
172 self.send_response(*saved_status)
171 self.length = None
173 self.length = None
172 self._chunked = False
174 self._chunked = False
173 for h in self.saved_headers:
175 for h in self.saved_headers:
174 self.send_header(*h)
176 self.send_header(*h)
175 if h[0].lower() == 'content-length':
177 if h[0].lower() == 'content-length':
176 self.length = int(h[1])
178 self.length = int(h[1])
177 if (self.length is None and
179 if (self.length is None and
178 saved_status[0] != common.HTTP_NOT_MODIFIED):
180 saved_status[0] != common.HTTP_NOT_MODIFIED):
179 self._chunked = (not self.close_connection and
181 self._chunked = (not self.close_connection and
180 self.request_version == "HTTP/1.1")
182 self.request_version == "HTTP/1.1")
181 if self._chunked:
183 if self._chunked:
182 self.send_header('Transfer-Encoding', 'chunked')
184 self.send_header('Transfer-Encoding', 'chunked')
183 else:
185 else:
184 self.send_header('Connection', 'close')
186 self.send_header('Connection', 'close')
185 self.end_headers()
187 self.end_headers()
186 self.sent_headers = True
188 self.sent_headers = True
187
189
188 def _start_response(self, http_status, headers, exc_info=None):
190 def _start_response(self, http_status, headers, exc_info=None):
189 code, msg = http_status.split(None, 1)
191 code, msg = http_status.split(None, 1)
190 code = int(code)
192 code = int(code)
191 self.saved_status = http_status
193 self.saved_status = http_status
192 bad_headers = ('connection', 'transfer-encoding')
194 bad_headers = ('connection', 'transfer-encoding')
193 self.saved_headers = [h for h in headers
195 self.saved_headers = [h for h in headers
194 if h[0].lower() not in bad_headers]
196 if h[0].lower() not in bad_headers]
195 return self._write
197 return self._write
196
198
197 def _write(self, data):
199 def _write(self, data):
198 if not self.saved_status:
200 if not self.saved_status:
199 raise AssertionError("data written before start_response() called")
201 raise AssertionError("data written before start_response() called")
200 elif not self.sent_headers:
202 elif not self.sent_headers:
201 self.send_headers()
203 self.send_headers()
202 if self.length is not None:
204 if self.length is not None:
203 if len(data) > self.length:
205 if len(data) > self.length:
204 raise AssertionError("Content-length header sent, but more "
206 raise AssertionError("Content-length header sent, but more "
205 "bytes than specified are being written.")
207 "bytes than specified are being written.")
206 self.length = self.length - len(data)
208 self.length = self.length - len(data)
207 elif self._chunked and data:
209 elif self._chunked and data:
208 data = '%x\r\n%s\r\n' % (len(data), data)
210 data = '%x\r\n%s\r\n' % (len(data), data)
209 self.wfile.write(data)
211 self.wfile.write(data)
210 self.wfile.flush()
212 self.wfile.flush()
211
213
212 def _done(self):
214 def _done(self):
213 if self._chunked:
215 if self._chunked:
214 self.wfile.write('0\r\n\r\n')
216 self.wfile.write('0\r\n\r\n')
215 self.wfile.flush()
217 self.wfile.flush()
216
218
217 class _httprequesthandlerssl(_httprequesthandler):
219 class _httprequesthandlerssl(_httprequesthandler):
218 """HTTPS handler based on Python's ssl module"""
220 """HTTPS handler based on Python's ssl module"""
219
221
220 url_scheme = 'https'
222 url_scheme = 'https'
221
223
222 @staticmethod
224 @staticmethod
223 def preparehttpserver(httpserver, ssl_cert):
225 def preparehttpserver(httpserver, ssl_cert):
224 try:
226 try:
225 import ssl
227 import ssl
226 ssl.wrap_socket
228 ssl.wrap_socket
227 except ImportError:
229 except ImportError:
228 raise error.Abort(_("SSL support is unavailable"))
230 raise error.Abort(_("SSL support is unavailable"))
229 httpserver.socket = ssl.wrap_socket(
231 httpserver.socket = ssl.wrap_socket(
230 httpserver.socket, server_side=True,
232 httpserver.socket, server_side=True,
231 certfile=ssl_cert, ssl_version=ssl.PROTOCOL_TLSv1)
233 certfile=ssl_cert, ssl_version=ssl.PROTOCOL_TLSv1)
232
234
233 def setup(self):
235 def setup(self):
234 self.connection = self.request
236 self.connection = self.request
235 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
237 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
236 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
238 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
237
239
238 try:
240 try:
239 import threading
241 import threading
240 threading.activeCount() # silence pyflakes and bypass demandimport
242 threading.activeCount() # silence pyflakes and bypass demandimport
241 _mixin = SocketServer.ThreadingMixIn
243 _mixin = SocketServer.ThreadingMixIn
242 except ImportError:
244 except ImportError:
243 if util.safehasattr(os, "fork"):
245 if util.safehasattr(os, "fork"):
244 _mixin = SocketServer.ForkingMixIn
246 _mixin = SocketServer.ForkingMixIn
245 else:
247 else:
246 class _mixin(object):
248 class _mixin(object):
247 pass
249 pass
248
250
249 def openlog(opt, default):
251 def openlog(opt, default):
250 if opt and opt != '-':
252 if opt and opt != '-':
251 return open(opt, 'a')
253 return open(opt, 'a')
252 return default
254 return default
253
255
254 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
256 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
255
257
256 # SO_REUSEADDR has broken semantics on windows
258 # SO_REUSEADDR has broken semantics on windows
257 if os.name == 'nt':
259 if os.name == 'nt':
258 allow_reuse_address = 0
260 allow_reuse_address = 0
259
261
260 def __init__(self, ui, app, addr, handler, **kwargs):
262 def __init__(self, ui, app, addr, handler, **kwargs):
261 BaseHTTPServer.HTTPServer.__init__(self, addr, handler, **kwargs)
263 BaseHTTPServer.HTTPServer.__init__(self, addr, handler, **kwargs)
262 self.daemon_threads = True
264 self.daemon_threads = True
263 self.application = app
265 self.application = app
264
266
265 handler.preparehttpserver(self, ui.config('web', 'certificate'))
267 handler.preparehttpserver(self, ui.config('web', 'certificate'))
266
268
267 prefix = ui.config('web', 'prefix', '')
269 prefix = ui.config('web', 'prefix', '')
268 if prefix:
270 if prefix:
269 prefix = '/' + prefix.strip('/')
271 prefix = '/' + prefix.strip('/')
270 self.prefix = prefix
272 self.prefix = prefix
271
273
272 alog = openlog(ui.config('web', 'accesslog', '-'), sys.stdout)
274 alog = openlog(ui.config('web', 'accesslog', '-'), sys.stdout)
273 elog = openlog(ui.config('web', 'errorlog', '-'), sys.stderr)
275 elog = openlog(ui.config('web', 'errorlog', '-'), sys.stderr)
274 self.accesslog = alog
276 self.accesslog = alog
275 self.errorlog = elog
277 self.errorlog = elog
276
278
277 self.addr, self.port = self.socket.getsockname()[0:2]
279 self.addr, self.port = self.socket.getsockname()[0:2]
278 self.fqaddr = socket.getfqdn(addr[0])
280 self.fqaddr = socket.getfqdn(addr[0])
279
281
280 class IPv6HTTPServer(MercurialHTTPServer):
282 class IPv6HTTPServer(MercurialHTTPServer):
281 address_family = getattr(socket, 'AF_INET6', None)
283 address_family = getattr(socket, 'AF_INET6', None)
282 def __init__(self, *args, **kwargs):
284 def __init__(self, *args, **kwargs):
283 if self.address_family is None:
285 if self.address_family is None:
284 raise error.RepoError(_('IPv6 is not available on this system'))
286 raise error.RepoError(_('IPv6 is not available on this system'))
285 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
287 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
286
288
287 def create_server(ui, app):
289 def create_server(ui, app):
288
290
289 if ui.config('web', 'certificate'):
291 if ui.config('web', 'certificate'):
290 handler = _httprequesthandlerssl
292 handler = _httprequesthandlerssl
291 else:
293 else:
292 handler = _httprequesthandler
294 handler = _httprequesthandler
293
295
294 if ui.configbool('web', 'ipv6'):
296 if ui.configbool('web', 'ipv6'):
295 cls = IPv6HTTPServer
297 cls = IPv6HTTPServer
296 else:
298 else:
297 cls = MercurialHTTPServer
299 cls = MercurialHTTPServer
298
300
299 # ugly hack due to python issue5853 (for threaded use)
301 # ugly hack due to python issue5853 (for threaded use)
300 try:
302 try:
301 import mimetypes
303 import mimetypes
302 mimetypes.init()
304 mimetypes.init()
303 except UnicodeDecodeError:
305 except UnicodeDecodeError:
304 # Python 2.x's mimetypes module attempts to decode strings
306 # Python 2.x's mimetypes module attempts to decode strings
305 # from Windows' ANSI APIs as ascii (fail), then re-encode them
307 # from Windows' ANSI APIs as ascii (fail), then re-encode them
306 # as ascii (clown fail), because the default Python Unicode
308 # as ascii (clown fail), because the default Python Unicode
307 # codec is hardcoded as ascii.
309 # codec is hardcoded as ascii.
308
310
309 sys.argv # unwrap demand-loader so that reload() works
311 sys.argv # unwrap demand-loader so that reload() works
310 reload(sys) # resurrect sys.setdefaultencoding()
312 reload(sys) # resurrect sys.setdefaultencoding()
311 oldenc = sys.getdefaultencoding()
313 oldenc = sys.getdefaultencoding()
312 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
314 sys.setdefaultencoding("latin1") # or any full 8-bit encoding
313 mimetypes.init()
315 mimetypes.init()
314 sys.setdefaultencoding(oldenc)
316 sys.setdefaultencoding(oldenc)
315
317
316 address = ui.config('web', 'address', '')
318 address = ui.config('web', 'address', '')
317 port = util.getport(ui.config('web', 'port', 8000))
319 port = util.getport(ui.config('web', 'port', 8000))
318 try:
320 try:
319 return cls(ui, app, (address, port), handler)
321 return cls(ui, app, (address, port), handler)
320 except socket.error as inst:
322 except socket.error as inst:
321 raise error.Abort(_("cannot start server at '%s:%d': %s")
323 raise error.Abort(_("cannot start server at '%s:%d': %s")
322 % (address, port, inst.args[1]))
324 % (address, port, inst.args[1]))
@@ -1,288 +1,289 b''
1 # httpconnection.py - urllib2 handler for new http support
1 # httpconnection.py - urllib2 handler for new http support
2 #
2 #
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 # Copyright 2011 Google, Inc.
6 # Copyright 2011 Google, Inc.
7 #
7 #
8 # This software may be used and distributed according to the terms of the
8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2 or any later version.
9 # GNU General Public License version 2 or any later version.
10
10
11 from __future__ import absolute_import
11 from __future__ import absolute_import
12
12
13 import logging
13 import logging
14 import os
14 import os
15 import socket
15 import socket
16 import urllib
17 import urllib2
18
16
19 from .i18n import _
17 from .i18n import _
20 from . import (
18 from . import (
21 httpclient,
19 httpclient,
22 sslutil,
20 sslutil,
23 util,
21 util,
24 )
22 )
25
23
24 urlerr = util.urlerr
25 urlreq = util.urlreq
26
26 # moved here from url.py to avoid a cycle
27 # moved here from url.py to avoid a cycle
27 class httpsendfile(object):
28 class httpsendfile(object):
28 """This is a wrapper around the objects returned by python's "open".
29 """This is a wrapper around the objects returned by python's "open".
29
30
30 Its purpose is to send file-like objects via HTTP.
31 Its purpose is to send file-like objects via HTTP.
31 It do however not define a __len__ attribute because the length
32 It do however not define a __len__ attribute because the length
32 might be more than Py_ssize_t can handle.
33 might be more than Py_ssize_t can handle.
33 """
34 """
34
35
35 def __init__(self, ui, *args, **kwargs):
36 def __init__(self, ui, *args, **kwargs):
36 self.ui = ui
37 self.ui = ui
37 self._data = open(*args, **kwargs)
38 self._data = open(*args, **kwargs)
38 self.seek = self._data.seek
39 self.seek = self._data.seek
39 self.close = self._data.close
40 self.close = self._data.close
40 self.write = self._data.write
41 self.write = self._data.write
41 self.length = os.fstat(self._data.fileno()).st_size
42 self.length = os.fstat(self._data.fileno()).st_size
42 self._pos = 0
43 self._pos = 0
43 self._total = self.length // 1024 * 2
44 self._total = self.length // 1024 * 2
44
45
45 def read(self, *args, **kwargs):
46 def read(self, *args, **kwargs):
46 try:
47 try:
47 ret = self._data.read(*args, **kwargs)
48 ret = self._data.read(*args, **kwargs)
48 except EOFError:
49 except EOFError:
49 self.ui.progress(_('sending'), None)
50 self.ui.progress(_('sending'), None)
50 self._pos += len(ret)
51 self._pos += len(ret)
51 # We pass double the max for total because we currently have
52 # We pass double the max for total because we currently have
52 # to send the bundle twice in the case of a server that
53 # to send the bundle twice in the case of a server that
53 # requires authentication. Since we can't know until we try
54 # requires authentication. Since we can't know until we try
54 # once whether authentication will be required, just lie to
55 # once whether authentication will be required, just lie to
55 # the user and maybe the push succeeds suddenly at 50%.
56 # the user and maybe the push succeeds suddenly at 50%.
56 self.ui.progress(_('sending'), self._pos // 1024,
57 self.ui.progress(_('sending'), self._pos // 1024,
57 unit=_('kb'), total=self._total)
58 unit=_('kb'), total=self._total)
58 return ret
59 return ret
59
60
60 # moved here from url.py to avoid a cycle
61 # moved here from url.py to avoid a cycle
61 def readauthforuri(ui, uri, user):
62 def readauthforuri(ui, uri, user):
62 # Read configuration
63 # Read configuration
63 config = dict()
64 config = dict()
64 for key, val in ui.configitems('auth'):
65 for key, val in ui.configitems('auth'):
65 if '.' not in key:
66 if '.' not in key:
66 ui.warn(_("ignoring invalid [auth] key '%s'\n") % key)
67 ui.warn(_("ignoring invalid [auth] key '%s'\n") % key)
67 continue
68 continue
68 group, setting = key.rsplit('.', 1)
69 group, setting = key.rsplit('.', 1)
69 gdict = config.setdefault(group, dict())
70 gdict = config.setdefault(group, dict())
70 if setting in ('username', 'cert', 'key'):
71 if setting in ('username', 'cert', 'key'):
71 val = util.expandpath(val)
72 val = util.expandpath(val)
72 gdict[setting] = val
73 gdict[setting] = val
73
74
74 # Find the best match
75 # Find the best match
75 scheme, hostpath = uri.split('://', 1)
76 scheme, hostpath = uri.split('://', 1)
76 bestuser = None
77 bestuser = None
77 bestlen = 0
78 bestlen = 0
78 bestauth = None
79 bestauth = None
79 for group, auth in config.iteritems():
80 for group, auth in config.iteritems():
80 if user and user != auth.get('username', user):
81 if user and user != auth.get('username', user):
81 # If a username was set in the URI, the entry username
82 # If a username was set in the URI, the entry username
82 # must either match it or be unset
83 # must either match it or be unset
83 continue
84 continue
84 prefix = auth.get('prefix')
85 prefix = auth.get('prefix')
85 if not prefix:
86 if not prefix:
86 continue
87 continue
87 p = prefix.split('://', 1)
88 p = prefix.split('://', 1)
88 if len(p) > 1:
89 if len(p) > 1:
89 schemes, prefix = [p[0]], p[1]
90 schemes, prefix = [p[0]], p[1]
90 else:
91 else:
91 schemes = (auth.get('schemes') or 'https').split()
92 schemes = (auth.get('schemes') or 'https').split()
92 if (prefix == '*' or hostpath.startswith(prefix)) and \
93 if (prefix == '*' or hostpath.startswith(prefix)) and \
93 (len(prefix) > bestlen or (len(prefix) == bestlen and \
94 (len(prefix) > bestlen or (len(prefix) == bestlen and \
94 not bestuser and 'username' in auth)) \
95 not bestuser and 'username' in auth)) \
95 and scheme in schemes:
96 and scheme in schemes:
96 bestlen = len(prefix)
97 bestlen = len(prefix)
97 bestauth = group, auth
98 bestauth = group, auth
98 bestuser = auth.get('username')
99 bestuser = auth.get('username')
99 if user and not bestuser:
100 if user and not bestuser:
100 auth['username'] = user
101 auth['username'] = user
101 return bestauth
102 return bestauth
102
103
103 # Mercurial (at least until we can remove the old codepath) requires
104 # Mercurial (at least until we can remove the old codepath) requires
104 # that the http response object be sufficiently file-like, so we
105 # that the http response object be sufficiently file-like, so we
105 # provide a close() method here.
106 # provide a close() method here.
106 class HTTPResponse(httpclient.HTTPResponse):
107 class HTTPResponse(httpclient.HTTPResponse):
107 def close(self):
108 def close(self):
108 pass
109 pass
109
110
110 class HTTPConnection(httpclient.HTTPConnection):
111 class HTTPConnection(httpclient.HTTPConnection):
111 response_class = HTTPResponse
112 response_class = HTTPResponse
112 def request(self, method, uri, body=None, headers=None):
113 def request(self, method, uri, body=None, headers=None):
113 if headers is None:
114 if headers is None:
114 headers = {}
115 headers = {}
115 if isinstance(body, httpsendfile):
116 if isinstance(body, httpsendfile):
116 body.seek(0)
117 body.seek(0)
117 httpclient.HTTPConnection.request(self, method, uri, body=body,
118 httpclient.HTTPConnection.request(self, method, uri, body=body,
118 headers=headers)
119 headers=headers)
119
120
120
121
121 _configuredlogging = False
122 _configuredlogging = False
122 LOGFMT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'
123 LOGFMT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'
123 # Subclass BOTH of these because otherwise urllib2 "helpfully"
124 # Subclass BOTH of these because otherwise urllib2 "helpfully"
124 # reinserts them since it notices we don't include any subclasses of
125 # reinserts them since it notices we don't include any subclasses of
125 # them.
126 # them.
126 class http2handler(urllib2.HTTPHandler, urllib2.HTTPSHandler):
127 class http2handler(urlreq.httphandler, urlreq.httpshandler):
127 def __init__(self, ui, pwmgr):
128 def __init__(self, ui, pwmgr):
128 global _configuredlogging
129 global _configuredlogging
129 urllib2.AbstractHTTPHandler.__init__(self)
130 urlreq.abstracthttphandler.__init__(self)
130 self.ui = ui
131 self.ui = ui
131 self.pwmgr = pwmgr
132 self.pwmgr = pwmgr
132 self._connections = {}
133 self._connections = {}
133 # developer config: ui.http2debuglevel
134 # developer config: ui.http2debuglevel
134 loglevel = ui.config('ui', 'http2debuglevel', default=None)
135 loglevel = ui.config('ui', 'http2debuglevel', default=None)
135 if loglevel and not _configuredlogging:
136 if loglevel and not _configuredlogging:
136 _configuredlogging = True
137 _configuredlogging = True
137 logger = logging.getLogger('mercurial.httpclient')
138 logger = logging.getLogger('mercurial.httpclient')
138 logger.setLevel(getattr(logging, loglevel.upper()))
139 logger.setLevel(getattr(logging, loglevel.upper()))
139 handler = logging.StreamHandler()
140 handler = logging.StreamHandler()
140 handler.setFormatter(logging.Formatter(LOGFMT))
141 handler.setFormatter(logging.Formatter(LOGFMT))
141 logger.addHandler(handler)
142 logger.addHandler(handler)
142
143
143 def close_all(self):
144 def close_all(self):
144 """Close and remove all connection objects being kept for reuse."""
145 """Close and remove all connection objects being kept for reuse."""
145 for openconns in self._connections.values():
146 for openconns in self._connections.values():
146 for conn in openconns:
147 for conn in openconns:
147 conn.close()
148 conn.close()
148 self._connections = {}
149 self._connections = {}
149
150
150 # shamelessly borrowed from urllib2.AbstractHTTPHandler
151 # shamelessly borrowed from urllib2.AbstractHTTPHandler
151 def do_open(self, http_class, req, use_ssl):
152 def do_open(self, http_class, req, use_ssl):
152 """Return an addinfourl object for the request, using http_class.
153 """Return an addinfourl object for the request, using http_class.
153
154
154 http_class must implement the HTTPConnection API from httplib.
155 http_class must implement the HTTPConnection API from httplib.
155 The addinfourl return value is a file-like object. It also
156 The addinfourl return value is a file-like object. It also
156 has methods and attributes including:
157 has methods and attributes including:
157 - info(): return a mimetools.Message object for the headers
158 - info(): return a mimetools.Message object for the headers
158 - geturl(): return the original request URL
159 - geturl(): return the original request URL
159 - code: HTTP status code
160 - code: HTTP status code
160 """
161 """
161 # If using a proxy, the host returned by get_host() is
162 # If using a proxy, the host returned by get_host() is
162 # actually the proxy. On Python 2.6.1, the real destination
163 # actually the proxy. On Python 2.6.1, the real destination
163 # hostname is encoded in the URI in the urllib2 request
164 # hostname is encoded in the URI in the urllib2 request
164 # object. On Python 2.6.5, it's stored in the _tunnel_host
165 # object. On Python 2.6.5, it's stored in the _tunnel_host
165 # attribute which has no accessor.
166 # attribute which has no accessor.
166 tunhost = getattr(req, '_tunnel_host', None)
167 tunhost = getattr(req, '_tunnel_host', None)
167 host = req.get_host()
168 host = req.get_host()
168 if tunhost:
169 if tunhost:
169 proxyhost = host
170 proxyhost = host
170 host = tunhost
171 host = tunhost
171 elif req.has_proxy():
172 elif req.has_proxy():
172 proxyhost = req.get_host()
173 proxyhost = req.get_host()
173 host = req.get_selector().split('://', 1)[1].split('/', 1)[0]
174 host = req.get_selector().split('://', 1)[1].split('/', 1)[0]
174 else:
175 else:
175 proxyhost = None
176 proxyhost = None
176
177
177 if proxyhost:
178 if proxyhost:
178 if ':' in proxyhost:
179 if ':' in proxyhost:
179 # Note: this means we'll explode if we try and use an
180 # Note: this means we'll explode if we try and use an
180 # IPv6 http proxy. This isn't a regression, so we
181 # IPv6 http proxy. This isn't a regression, so we
181 # won't worry about it for now.
182 # won't worry about it for now.
182 proxyhost, proxyport = proxyhost.rsplit(':', 1)
183 proxyhost, proxyport = proxyhost.rsplit(':', 1)
183 else:
184 else:
184 proxyport = 3128 # squid default
185 proxyport = 3128 # squid default
185 proxy = (proxyhost, proxyport)
186 proxy = (proxyhost, proxyport)
186 else:
187 else:
187 proxy = None
188 proxy = None
188
189
189 if not host:
190 if not host:
190 raise urllib2.URLError('no host given')
191 raise urlerr.urlerror('no host given')
191
192
192 connkey = use_ssl, host, proxy
193 connkey = use_ssl, host, proxy
193 allconns = self._connections.get(connkey, [])
194 allconns = self._connections.get(connkey, [])
194 conns = [c for c in allconns if not c.busy()]
195 conns = [c for c in allconns if not c.busy()]
195 if conns:
196 if conns:
196 h = conns[0]
197 h = conns[0]
197 else:
198 else:
198 if allconns:
199 if allconns:
199 self.ui.debug('all connections for %s busy, making a new '
200 self.ui.debug('all connections for %s busy, making a new '
200 'one\n' % host)
201 'one\n' % host)
201 timeout = None
202 timeout = None
202 if req.timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
203 if req.timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
203 timeout = req.timeout
204 timeout = req.timeout
204 h = http_class(host, timeout=timeout, proxy_hostport=proxy)
205 h = http_class(host, timeout=timeout, proxy_hostport=proxy)
205 self._connections.setdefault(connkey, []).append(h)
206 self._connections.setdefault(connkey, []).append(h)
206
207
207 headers = dict(req.headers)
208 headers = dict(req.headers)
208 headers.update(req.unredirected_hdrs)
209 headers.update(req.unredirected_hdrs)
209 headers = dict(
210 headers = dict(
210 (name.title(), val) for name, val in headers.items())
211 (name.title(), val) for name, val in headers.items())
211 try:
212 try:
212 path = req.get_selector()
213 path = req.get_selector()
213 if '://' in path:
214 if '://' in path:
214 path = path.split('://', 1)[1].split('/', 1)[1]
215 path = path.split('://', 1)[1].split('/', 1)[1]
215 if path[0] != '/':
216 if path[0] != '/':
216 path = '/' + path
217 path = '/' + path
217 h.request(req.get_method(), path, req.data, headers)
218 h.request(req.get_method(), path, req.data, headers)
218 r = h.getresponse()
219 r = h.getresponse()
219 except socket.error as err: # XXX what error?
220 except socket.error as err: # XXX what error?
220 raise urllib2.URLError(err)
221 raise urlerr.urlerror(err)
221
222
222 # Pick apart the HTTPResponse object to get the addinfourl
223 # Pick apart the HTTPResponse object to get the addinfourl
223 # object initialized properly.
224 # object initialized properly.
224 r.recv = r.read
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 resp.code = r.status
228 resp.code = r.status
228 resp.msg = r.reason
229 resp.msg = r.reason
229 return resp
230 return resp
230
231
231 # httplib always uses the given host/port as the socket connect
232 # httplib always uses the given host/port as the socket connect
232 # target, and then allows full URIs in the request path, which it
233 # target, and then allows full URIs in the request path, which it
233 # then observes and treats as a signal to do proxying instead.
234 # then observes and treats as a signal to do proxying instead.
234 def http_open(self, req):
235 def http_open(self, req):
235 if req.get_full_url().startswith('https'):
236 if req.get_full_url().startswith('https'):
236 return self.https_open(req)
237 return self.https_open(req)
237 def makehttpcon(*args, **kwargs):
238 def makehttpcon(*args, **kwargs):
238 k2 = dict(kwargs)
239 k2 = dict(kwargs)
239 k2['use_ssl'] = False
240 k2['use_ssl'] = False
240 return HTTPConnection(*args, **k2)
241 return HTTPConnection(*args, **k2)
241 return self.do_open(makehttpcon, req, False)
242 return self.do_open(makehttpcon, req, False)
242
243
243 def https_open(self, req):
244 def https_open(self, req):
244 # req.get_full_url() does not contain credentials and we may
245 # req.get_full_url() does not contain credentials and we may
245 # need them to match the certificates.
246 # need them to match the certificates.
246 url = req.get_full_url()
247 url = req.get_full_url()
247 user, password = self.pwmgr.find_stored_password(url)
248 user, password = self.pwmgr.find_stored_password(url)
248 res = readauthforuri(self.ui, url, user)
249 res = readauthforuri(self.ui, url, user)
249 if res:
250 if res:
250 group, auth = res
251 group, auth = res
251 self.auth = auth
252 self.auth = auth
252 self.ui.debug("using auth.%s.* for authentication\n" % group)
253 self.ui.debug("using auth.%s.* for authentication\n" % group)
253 else:
254 else:
254 self.auth = None
255 self.auth = None
255 return self.do_open(self._makesslconnection, req, True)
256 return self.do_open(self._makesslconnection, req, True)
256
257
257 def _makesslconnection(self, host, port=443, *args, **kwargs):
258 def _makesslconnection(self, host, port=443, *args, **kwargs):
258 keyfile = None
259 keyfile = None
259 certfile = None
260 certfile = None
260
261
261 if args: # key_file
262 if args: # key_file
262 keyfile = args.pop(0)
263 keyfile = args.pop(0)
263 if args: # cert_file
264 if args: # cert_file
264 certfile = args.pop(0)
265 certfile = args.pop(0)
265
266
266 # if the user has specified different key/cert files in
267 # if the user has specified different key/cert files in
267 # hgrc, we prefer these
268 # hgrc, we prefer these
268 if self.auth and 'key' in self.auth and 'cert' in self.auth:
269 if self.auth and 'key' in self.auth and 'cert' in self.auth:
269 keyfile = self.auth['key']
270 keyfile = self.auth['key']
270 certfile = self.auth['cert']
271 certfile = self.auth['cert']
271
272
272 # let host port take precedence
273 # let host port take precedence
273 if ':' in host and '[' not in host or ']:' in host:
274 if ':' in host and '[' not in host or ']:' in host:
274 host, port = host.rsplit(':', 1)
275 host, port = host.rsplit(':', 1)
275 port = int(port)
276 port = int(port)
276 if '[' in host:
277 if '[' in host:
277 host = host[1:-1]
278 host = host[1:-1]
278
279
279 kwargs['keyfile'] = keyfile
280 kwargs['keyfile'] = keyfile
280 kwargs['certfile'] = certfile
281 kwargs['certfile'] = certfile
281
282
282 kwargs.update(sslutil.sslkwargs(self.ui, host))
283 kwargs.update(sslutil.sslkwargs(self.ui, host))
283
284
284 con = HTTPConnection(host, port, use_ssl=True,
285 con = HTTPConnection(host, port, use_ssl=True,
285 ssl_wrap_socket=sslutil.wrapsocket,
286 ssl_wrap_socket=sslutil.wrapsocket,
286 ssl_validator=sslutil.validator(self.ui, host),
287 ssl_validator=sslutil.validator(self.ui, host),
287 **kwargs)
288 **kwargs)
288 return con
289 return con
@@ -1,307 +1,308 b''
1 # httppeer.py - HTTP repository proxy classes for mercurial
1 # httppeer.py - HTTP repository proxy classes for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import httplib
12 import httplib
13 import os
13 import os
14 import socket
14 import socket
15 import tempfile
15 import tempfile
16 import urllib
17 import urllib2
18 import zlib
16 import zlib
19
17
20 from .i18n import _
18 from .i18n import _
21 from .node import nullid
19 from .node import nullid
22 from . import (
20 from . import (
23 bundle2,
21 bundle2,
24 error,
22 error,
25 httpconnection,
23 httpconnection,
26 statichttprepo,
24 statichttprepo,
27 url,
25 url,
28 util,
26 util,
29 wireproto,
27 wireproto,
30 )
28 )
31
29
30 urlerr = util.urlerr
31 urlreq = util.urlreq
32
32 def zgenerator(f):
33 def zgenerator(f):
33 zd = zlib.decompressobj()
34 zd = zlib.decompressobj()
34 try:
35 try:
35 for chunk in util.filechunkiter(f):
36 for chunk in util.filechunkiter(f):
36 while chunk:
37 while chunk:
37 yield zd.decompress(chunk, 2**18)
38 yield zd.decompress(chunk, 2**18)
38 chunk = zd.unconsumed_tail
39 chunk = zd.unconsumed_tail
39 except httplib.HTTPException:
40 except httplib.HTTPException:
40 raise IOError(None, _('connection ended unexpectedly'))
41 raise IOError(None, _('connection ended unexpectedly'))
41 yield zd.flush()
42 yield zd.flush()
42
43
43 class httppeer(wireproto.wirepeer):
44 class httppeer(wireproto.wirepeer):
44 def __init__(self, ui, path):
45 def __init__(self, ui, path):
45 self.path = path
46 self.path = path
46 self.caps = None
47 self.caps = None
47 self.handler = None
48 self.handler = None
48 self.urlopener = None
49 self.urlopener = None
49 self.requestbuilder = None
50 self.requestbuilder = None
50 u = util.url(path)
51 u = util.url(path)
51 if u.query or u.fragment:
52 if u.query or u.fragment:
52 raise error.Abort(_('unsupported URL component: "%s"') %
53 raise error.Abort(_('unsupported URL component: "%s"') %
53 (u.query or u.fragment))
54 (u.query or u.fragment))
54
55
55 # urllib cannot handle URLs with embedded user or passwd
56 # urllib cannot handle URLs with embedded user or passwd
56 self._url, authinfo = u.authinfo()
57 self._url, authinfo = u.authinfo()
57
58
58 self.ui = ui
59 self.ui = ui
59 self.ui.debug('using %s\n' % self._url)
60 self.ui.debug('using %s\n' % self._url)
60
61
61 self.urlopener = url.opener(ui, authinfo)
62 self.urlopener = url.opener(ui, authinfo)
62 self.requestbuilder = urllib2.Request
63 self.requestbuilder = urlreq.request
63
64
64 def __del__(self):
65 def __del__(self):
65 if self.urlopener:
66 if self.urlopener:
66 for h in self.urlopener.handlers:
67 for h in self.urlopener.handlers:
67 h.close()
68 h.close()
68 getattr(h, "close_all", lambda : None)()
69 getattr(h, "close_all", lambda : None)()
69
70
70 def url(self):
71 def url(self):
71 return self.path
72 return self.path
72
73
73 # look up capabilities only when needed
74 # look up capabilities only when needed
74
75
75 def _fetchcaps(self):
76 def _fetchcaps(self):
76 self.caps = set(self._call('capabilities').split())
77 self.caps = set(self._call('capabilities').split())
77
78
78 def _capabilities(self):
79 def _capabilities(self):
79 if self.caps is None:
80 if self.caps is None:
80 try:
81 try:
81 self._fetchcaps()
82 self._fetchcaps()
82 except error.RepoError:
83 except error.RepoError:
83 self.caps = set()
84 self.caps = set()
84 self.ui.debug('capabilities: %s\n' %
85 self.ui.debug('capabilities: %s\n' %
85 (' '.join(self.caps or ['none'])))
86 (' '.join(self.caps or ['none'])))
86 return self.caps
87 return self.caps
87
88
88 def lock(self):
89 def lock(self):
89 raise error.Abort(_('operation not supported over http'))
90 raise error.Abort(_('operation not supported over http'))
90
91
91 def _callstream(self, cmd, **args):
92 def _callstream(self, cmd, **args):
92 if cmd == 'pushkey':
93 if cmd == 'pushkey':
93 args['data'] = ''
94 args['data'] = ''
94 data = args.pop('data', None)
95 data = args.pop('data', None)
95 headers = args.pop('headers', {})
96 headers = args.pop('headers', {})
96
97
97 self.ui.debug("sending %s command\n" % cmd)
98 self.ui.debug("sending %s command\n" % cmd)
98 q = [('cmd', cmd)]
99 q = [('cmd', cmd)]
99 headersize = 0
100 headersize = 0
100 # Important: don't use self.capable() here or else you end up
101 # Important: don't use self.capable() here or else you end up
101 # with infinite recursion when trying to look up capabilities
102 # with infinite recursion when trying to look up capabilities
102 # for the first time.
103 # for the first time.
103 postargsok = self.caps is not None and 'httppostargs' in self.caps
104 postargsok = self.caps is not None and 'httppostargs' in self.caps
104 # TODO: support for httppostargs when data is a file-like
105 # TODO: support for httppostargs when data is a file-like
105 # object rather than a basestring
106 # object rather than a basestring
106 canmungedata = not data or isinstance(data, basestring)
107 canmungedata = not data or isinstance(data, basestring)
107 if postargsok and canmungedata:
108 if postargsok and canmungedata:
108 strargs = urllib.urlencode(sorted(args.items()))
109 strargs = urlreq.urlencode(sorted(args.items()))
109 if strargs:
110 if strargs:
110 if not data:
111 if not data:
111 data = strargs
112 data = strargs
112 elif isinstance(data, basestring):
113 elif isinstance(data, basestring):
113 data = strargs + data
114 data = strargs + data
114 headers['X-HgArgs-Post'] = len(strargs)
115 headers['X-HgArgs-Post'] = len(strargs)
115 else:
116 else:
116 if len(args) > 0:
117 if len(args) > 0:
117 httpheader = self.capable('httpheader')
118 httpheader = self.capable('httpheader')
118 if httpheader:
119 if httpheader:
119 headersize = int(httpheader.split(',', 1)[0])
120 headersize = int(httpheader.split(',', 1)[0])
120 if headersize > 0:
121 if headersize > 0:
121 # The headers can typically carry more data than the URL.
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 headerfmt = 'X-HgArg-%s'
124 headerfmt = 'X-HgArg-%s'
124 contentlen = headersize - len(headerfmt % '000' + ': \r\n')
125 contentlen = headersize - len(headerfmt % '000' + ': \r\n')
125 headernum = 0
126 headernum = 0
126 varyheaders = []
127 varyheaders = []
127 for i in xrange(0, len(encargs), contentlen):
128 for i in xrange(0, len(encargs), contentlen):
128 headernum += 1
129 headernum += 1
129 header = headerfmt % str(headernum)
130 header = headerfmt % str(headernum)
130 headers[header] = encargs[i:i + contentlen]
131 headers[header] = encargs[i:i + contentlen]
131 varyheaders.append(header)
132 varyheaders.append(header)
132 headers['Vary'] = ','.join(varyheaders)
133 headers['Vary'] = ','.join(varyheaders)
133 else:
134 else:
134 q += sorted(args.items())
135 q += sorted(args.items())
135 qs = '?%s' % urllib.urlencode(q)
136 qs = '?%s' % urlreq.urlencode(q)
136 cu = "%s%s" % (self._url, qs)
137 cu = "%s%s" % (self._url, qs)
137 size = 0
138 size = 0
138 if util.safehasattr(data, 'length'):
139 if util.safehasattr(data, 'length'):
139 size = data.length
140 size = data.length
140 elif data is not None:
141 elif data is not None:
141 size = len(data)
142 size = len(data)
142 if size and self.ui.configbool('ui', 'usehttp2', False):
143 if size and self.ui.configbool('ui', 'usehttp2', False):
143 headers['Expect'] = '100-Continue'
144 headers['Expect'] = '100-Continue'
144 headers['X-HgHttp2'] = '1'
145 headers['X-HgHttp2'] = '1'
145 if data is not None and 'Content-Type' not in headers:
146 if data is not None and 'Content-Type' not in headers:
146 headers['Content-Type'] = 'application/mercurial-0.1'
147 headers['Content-Type'] = 'application/mercurial-0.1'
147 req = self.requestbuilder(cu, data, headers)
148 req = self.requestbuilder(cu, data, headers)
148 if data is not None:
149 if data is not None:
149 self.ui.debug("sending %s bytes\n" % size)
150 self.ui.debug("sending %s bytes\n" % size)
150 req.add_unredirected_header('Content-Length', '%d' % size)
151 req.add_unredirected_header('Content-Length', '%d' % size)
151 try:
152 try:
152 resp = self.urlopener.open(req)
153 resp = self.urlopener.open(req)
153 except urllib2.HTTPError as inst:
154 except urlerr.httperror as inst:
154 if inst.code == 401:
155 if inst.code == 401:
155 raise error.Abort(_('authorization failed'))
156 raise error.Abort(_('authorization failed'))
156 raise
157 raise
157 except httplib.HTTPException as inst:
158 except httplib.HTTPException as inst:
158 self.ui.debug('http error while sending %s command\n' % cmd)
159 self.ui.debug('http error while sending %s command\n' % cmd)
159 self.ui.traceback()
160 self.ui.traceback()
160 raise IOError(None, inst)
161 raise IOError(None, inst)
161 except IndexError:
162 except IndexError:
162 # this only happens with Python 2.3, later versions raise URLError
163 # this only happens with Python 2.3, later versions raise URLError
163 raise error.Abort(_('http error, possibly caused by proxy setting'))
164 raise error.Abort(_('http error, possibly caused by proxy setting'))
164 # record the url we got redirected to
165 # record the url we got redirected to
165 resp_url = resp.geturl()
166 resp_url = resp.geturl()
166 if resp_url.endswith(qs):
167 if resp_url.endswith(qs):
167 resp_url = resp_url[:-len(qs)]
168 resp_url = resp_url[:-len(qs)]
168 if self._url.rstrip('/') != resp_url.rstrip('/'):
169 if self._url.rstrip('/') != resp_url.rstrip('/'):
169 if not self.ui.quiet:
170 if not self.ui.quiet:
170 self.ui.warn(_('real URL is %s\n') % resp_url)
171 self.ui.warn(_('real URL is %s\n') % resp_url)
171 self._url = resp_url
172 self._url = resp_url
172 try:
173 try:
173 proto = resp.getheader('content-type')
174 proto = resp.getheader('content-type')
174 except AttributeError:
175 except AttributeError:
175 proto = resp.headers.get('content-type', '')
176 proto = resp.headers.get('content-type', '')
176
177
177 safeurl = util.hidepassword(self._url)
178 safeurl = util.hidepassword(self._url)
178 if proto.startswith('application/hg-error'):
179 if proto.startswith('application/hg-error'):
179 raise error.OutOfBandError(resp.read())
180 raise error.OutOfBandError(resp.read())
180 # accept old "text/plain" and "application/hg-changegroup" for now
181 # accept old "text/plain" and "application/hg-changegroup" for now
181 if not (proto.startswith('application/mercurial-') or
182 if not (proto.startswith('application/mercurial-') or
182 (proto.startswith('text/plain')
183 (proto.startswith('text/plain')
183 and not resp.headers.get('content-length')) or
184 and not resp.headers.get('content-length')) or
184 proto.startswith('application/hg-changegroup')):
185 proto.startswith('application/hg-changegroup')):
185 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
186 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
186 raise error.RepoError(
187 raise error.RepoError(
187 _("'%s' does not appear to be an hg repository:\n"
188 _("'%s' does not appear to be an hg repository:\n"
188 "---%%<--- (%s)\n%s\n---%%<---\n")
189 "---%%<--- (%s)\n%s\n---%%<---\n")
189 % (safeurl, proto or 'no content-type', resp.read(1024)))
190 % (safeurl, proto or 'no content-type', resp.read(1024)))
190
191
191 if proto.startswith('application/mercurial-'):
192 if proto.startswith('application/mercurial-'):
192 try:
193 try:
193 version = proto.split('-', 1)[1]
194 version = proto.split('-', 1)[1]
194 version_info = tuple([int(n) for n in version.split('.')])
195 version_info = tuple([int(n) for n in version.split('.')])
195 except ValueError:
196 except ValueError:
196 raise error.RepoError(_("'%s' sent a broken Content-Type "
197 raise error.RepoError(_("'%s' sent a broken Content-Type "
197 "header (%s)") % (safeurl, proto))
198 "header (%s)") % (safeurl, proto))
198 if version_info > (0, 1):
199 if version_info > (0, 1):
199 raise error.RepoError(_("'%s' uses newer protocol %s") %
200 raise error.RepoError(_("'%s' uses newer protocol %s") %
200 (safeurl, version))
201 (safeurl, version))
201
202
202 return resp
203 return resp
203
204
204 def _call(self, cmd, **args):
205 def _call(self, cmd, **args):
205 fp = self._callstream(cmd, **args)
206 fp = self._callstream(cmd, **args)
206 try:
207 try:
207 return fp.read()
208 return fp.read()
208 finally:
209 finally:
209 # if using keepalive, allow connection to be reused
210 # if using keepalive, allow connection to be reused
210 fp.close()
211 fp.close()
211
212
212 def _callpush(self, cmd, cg, **args):
213 def _callpush(self, cmd, cg, **args):
213 # have to stream bundle to a temp file because we do not have
214 # have to stream bundle to a temp file because we do not have
214 # http 1.1 chunked transfer.
215 # http 1.1 chunked transfer.
215
216
216 types = self.capable('unbundle')
217 types = self.capable('unbundle')
217 try:
218 try:
218 types = types.split(',')
219 types = types.split(',')
219 except AttributeError:
220 except AttributeError:
220 # servers older than d1b16a746db6 will send 'unbundle' as a
221 # servers older than d1b16a746db6 will send 'unbundle' as a
221 # boolean capability. They only support headerless/uncompressed
222 # boolean capability. They only support headerless/uncompressed
222 # bundles.
223 # bundles.
223 types = [""]
224 types = [""]
224 for x in types:
225 for x in types:
225 if x in bundle2.bundletypes:
226 if x in bundle2.bundletypes:
226 type = x
227 type = x
227 break
228 break
228
229
229 tempname = bundle2.writebundle(self.ui, cg, None, type)
230 tempname = bundle2.writebundle(self.ui, cg, None, type)
230 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
231 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
231 headers = {'Content-Type': 'application/mercurial-0.1'}
232 headers = {'Content-Type': 'application/mercurial-0.1'}
232
233
233 try:
234 try:
234 r = self._call(cmd, data=fp, headers=headers, **args)
235 r = self._call(cmd, data=fp, headers=headers, **args)
235 vals = r.split('\n', 1)
236 vals = r.split('\n', 1)
236 if len(vals) < 2:
237 if len(vals) < 2:
237 raise error.ResponseError(_("unexpected response:"), r)
238 raise error.ResponseError(_("unexpected response:"), r)
238 return vals
239 return vals
239 except socket.error as err:
240 except socket.error as err:
240 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
241 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
241 raise error.Abort(_('push failed: %s') % err.args[1])
242 raise error.Abort(_('push failed: %s') % err.args[1])
242 raise error.Abort(err.args[1])
243 raise error.Abort(err.args[1])
243 finally:
244 finally:
244 fp.close()
245 fp.close()
245 os.unlink(tempname)
246 os.unlink(tempname)
246
247
247 def _calltwowaystream(self, cmd, fp, **args):
248 def _calltwowaystream(self, cmd, fp, **args):
248 fh = None
249 fh = None
249 fp_ = None
250 fp_ = None
250 filename = None
251 filename = None
251 try:
252 try:
252 # dump bundle to disk
253 # dump bundle to disk
253 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
254 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
254 fh = os.fdopen(fd, "wb")
255 fh = os.fdopen(fd, "wb")
255 d = fp.read(4096)
256 d = fp.read(4096)
256 while d:
257 while d:
257 fh.write(d)
258 fh.write(d)
258 d = fp.read(4096)
259 d = fp.read(4096)
259 fh.close()
260 fh.close()
260 # start http push
261 # start http push
261 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
262 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
262 headers = {'Content-Type': 'application/mercurial-0.1'}
263 headers = {'Content-Type': 'application/mercurial-0.1'}
263 return self._callstream(cmd, data=fp_, headers=headers, **args)
264 return self._callstream(cmd, data=fp_, headers=headers, **args)
264 finally:
265 finally:
265 if fp_ is not None:
266 if fp_ is not None:
266 fp_.close()
267 fp_.close()
267 if fh is not None:
268 if fh is not None:
268 fh.close()
269 fh.close()
269 os.unlink(filename)
270 os.unlink(filename)
270
271
271 def _callcompressable(self, cmd, **args):
272 def _callcompressable(self, cmd, **args):
272 stream = self._callstream(cmd, **args)
273 stream = self._callstream(cmd, **args)
273 return util.chunkbuffer(zgenerator(stream))
274 return util.chunkbuffer(zgenerator(stream))
274
275
275 def _abort(self, exception):
276 def _abort(self, exception):
276 raise exception
277 raise exception
277
278
278 class httpspeer(httppeer):
279 class httpspeer(httppeer):
279 def __init__(self, ui, path):
280 def __init__(self, ui, path):
280 if not url.has_https:
281 if not url.has_https:
281 raise error.Abort(_('Python support for SSL and HTTPS '
282 raise error.Abort(_('Python support for SSL and HTTPS '
282 'is not installed'))
283 'is not installed'))
283 httppeer.__init__(self, ui, path)
284 httppeer.__init__(self, ui, path)
284
285
285 def instance(ui, path, create):
286 def instance(ui, path, create):
286 if create:
287 if create:
287 raise error.Abort(_('cannot create new http repository'))
288 raise error.Abort(_('cannot create new http repository'))
288 try:
289 try:
289 if path.startswith('https:'):
290 if path.startswith('https:'):
290 inst = httpspeer(ui, path)
291 inst = httpspeer(ui, path)
291 else:
292 else:
292 inst = httppeer(ui, path)
293 inst = httppeer(ui, path)
293 try:
294 try:
294 # Try to do useful work when checking compatibility.
295 # Try to do useful work when checking compatibility.
295 # Usually saves a roundtrip since we want the caps anyway.
296 # Usually saves a roundtrip since we want the caps anyway.
296 inst._fetchcaps()
297 inst._fetchcaps()
297 except error.RepoError:
298 except error.RepoError:
298 # No luck, try older compatibility check.
299 # No luck, try older compatibility check.
299 inst.between([(nullid, nullid)])
300 inst.between([(nullid, nullid)])
300 return inst
301 return inst
301 except error.RepoError as httpexception:
302 except error.RepoError as httpexception:
302 try:
303 try:
303 r = statichttprepo.instance(ui, "static-" + path, create)
304 r = statichttprepo.instance(ui, "static-" + path, create)
304 ui.note('(falling back to static-http)\n')
305 ui.note('(falling back to static-http)\n')
305 return r
306 return r
306 except error.RepoError:
307 except error.RepoError:
307 raise httpexception # use the original http RepoError instead
308 raise httpexception # use the original http RepoError instead
@@ -1,753 +1,759 b''
1 # This library is free software; you can redistribute it and/or
1 # This library is free software; you can redistribute it and/or
2 # modify it under the terms of the GNU Lesser General Public
2 # modify it under the terms of the GNU Lesser General Public
3 # License as published by the Free Software Foundation; either
3 # License as published by the Free Software Foundation; either
4 # version 2.1 of the License, or (at your option) any later version.
4 # version 2.1 of the License, or (at your option) any later version.
5 #
5 #
6 # This library is distributed in the hope that it will be useful,
6 # This library is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9 # Lesser General Public License for more details.
9 # Lesser General Public License for more details.
10 #
10 #
11 # You should have received a copy of the GNU Lesser General Public
11 # You should have received a copy of the GNU Lesser General Public
12 # License along with this library; if not, see
12 # License along with this library; if not, see
13 # <http://www.gnu.org/licenses/>.
13 # <http://www.gnu.org/licenses/>.
14
14
15 # This file is part of urlgrabber, a high-level cross-protocol url-grabber
15 # This file is part of urlgrabber, a high-level cross-protocol url-grabber
16 # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
16 # Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
17
17
18 # Modified by Benoit Boissinot:
18 # Modified by Benoit Boissinot:
19 # - fix for digest auth (inspired from urllib2.py @ Python v2.4)
19 # - fix for digest auth (inspired from urllib2.py @ Python v2.4)
20 # Modified by Dirkjan Ochtman:
20 # Modified by Dirkjan Ochtman:
21 # - import md5 function from a local util module
21 # - import md5 function from a local util module
22 # Modified by Augie Fackler:
22 # Modified by Augie Fackler:
23 # - add safesend method and use it to prevent broken pipe errors
23 # - add safesend method and use it to prevent broken pipe errors
24 # on large POST requests
24 # on large POST requests
25
25
26 """An HTTP handler for urllib2 that supports HTTP 1.1 and keepalive.
26 """An HTTP handler for urllib2 that supports HTTP 1.1 and keepalive.
27
27
28 >>> import urllib2
28 >>> import urllib2
29 >>> from keepalive import HTTPHandler
29 >>> from keepalive import HTTPHandler
30 >>> keepalive_handler = HTTPHandler()
30 >>> keepalive_handler = HTTPHandler()
31 >>> opener = urllib2.build_opener(keepalive_handler)
31 >>> opener = urlreq.buildopener(keepalive_handler)
32 >>> urllib2.install_opener(opener)
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 If a connection to a given host is requested, and all of the existing
36 If a connection to a given host is requested, and all of the existing
37 connections are still in use, another connection will be opened. If
37 connections are still in use, another connection will be opened. If
38 the handler tries to use an existing connection but it fails in some
38 the handler tries to use an existing connection but it fails in some
39 way, it will be closed and removed from the pool.
39 way, it will be closed and removed from the pool.
40
40
41 To remove the handler, simply re-run build_opener with no arguments, and
41 To remove the handler, simply re-run build_opener with no arguments, and
42 install that opener.
42 install that opener.
43
43
44 You can explicitly close connections by using the close_connection()
44 You can explicitly close connections by using the close_connection()
45 method of the returned file-like object (described below) or you can
45 method of the returned file-like object (described below) or you can
46 use the handler methods:
46 use the handler methods:
47
47
48 close_connection(host)
48 close_connection(host)
49 close_all()
49 close_all()
50 open_connections()
50 open_connections()
51
51
52 NOTE: using the close_connection and close_all methods of the handler
52 NOTE: using the close_connection and close_all methods of the handler
53 should be done with care when using multiple threads.
53 should be done with care when using multiple threads.
54 * there is nothing that prevents another thread from creating new
54 * there is nothing that prevents another thread from creating new
55 connections immediately after connections are closed
55 connections immediately after connections are closed
56 * no checks are done to prevent in-use connections from being closed
56 * no checks are done to prevent in-use connections from being closed
57
57
58 >>> keepalive_handler.close_all()
58 >>> keepalive_handler.close_all()
59
59
60 EXTRA ATTRIBUTES AND METHODS
60 EXTRA ATTRIBUTES AND METHODS
61
61
62 Upon a status of 200, the object returned has a few additional
62 Upon a status of 200, the object returned has a few additional
63 attributes and methods, which should not be used if you want to
63 attributes and methods, which should not be used if you want to
64 remain consistent with the normal urllib2-returned objects:
64 remain consistent with the normal urllib2-returned objects:
65
65
66 close_connection() - close the connection to the host
66 close_connection() - close the connection to the host
67 readlines() - you know, readlines()
67 readlines() - you know, readlines()
68 status - the return status (i.e. 404)
68 status - the return status (i.e. 404)
69 reason - english translation of status (i.e. 'File not found')
69 reason - english translation of status (i.e. 'File not found')
70
70
71 If you want the best of both worlds, use this inside an
71 If you want the best of both worlds, use this inside an
72 AttributeError-catching try:
72 AttributeError-catching try:
73
73
74 >>> try: status = fo.status
74 >>> try: status = fo.status
75 >>> except AttributeError: status = None
75 >>> except AttributeError: status = None
76
76
77 Unfortunately, these are ONLY there if status == 200, so it's not
77 Unfortunately, these are ONLY there if status == 200, so it's not
78 easy to distinguish between non-200 responses. The reason is that
78 easy to distinguish between non-200 responses. The reason is that
79 urllib2 tries to do clever things with error codes 301, 302, 401,
79 urllib2 tries to do clever things with error codes 301, 302, 401,
80 and 407, and it wraps the object upon return.
80 and 407, and it wraps the object upon return.
81
81
82 For python versions earlier than 2.4, you can avoid this fancy error
82 For python versions earlier than 2.4, you can avoid this fancy error
83 handling by setting the module-level global HANDLE_ERRORS to zero.
83 handling by setting the module-level global HANDLE_ERRORS to zero.
84 You see, prior to 2.4, it's the HTTP Handler's job to determine what
84 You see, prior to 2.4, it's the HTTP Handler's job to determine what
85 to handle specially, and what to just pass up. HANDLE_ERRORS == 0
85 to handle specially, and what to just pass up. HANDLE_ERRORS == 0
86 means "pass everything up". In python 2.4, however, this job no
86 means "pass everything up". In python 2.4, however, this job no
87 longer belongs to the HTTP Handler and is now done by a NEW handler,
87 longer belongs to the HTTP Handler and is now done by a NEW handler,
88 HTTPErrorProcessor. Here's the bottom line:
88 HTTPErrorProcessor. Here's the bottom line:
89
89
90 python version < 2.4
90 python version < 2.4
91 HANDLE_ERRORS == 1 (default) pass up 200, treat the rest as
91 HANDLE_ERRORS == 1 (default) pass up 200, treat the rest as
92 errors
92 errors
93 HANDLE_ERRORS == 0 pass everything up, error processing is
93 HANDLE_ERRORS == 0 pass everything up, error processing is
94 left to the calling code
94 left to the calling code
95 python version >= 2.4
95 python version >= 2.4
96 HANDLE_ERRORS == 1 pass up 200, treat the rest as errors
96 HANDLE_ERRORS == 1 pass up 200, treat the rest as errors
97 HANDLE_ERRORS == 0 (default) pass everything up, let the
97 HANDLE_ERRORS == 0 (default) pass everything up, let the
98 other handlers (specifically,
98 other handlers (specifically,
99 HTTPErrorProcessor) decide what to do
99 HTTPErrorProcessor) decide what to do
100
100
101 In practice, setting the variable either way makes little difference
101 In practice, setting the variable either way makes little difference
102 in python 2.4, so for the most consistent behavior across versions,
102 in python 2.4, so for the most consistent behavior across versions,
103 you probably just want to use the defaults, which will give you
103 you probably just want to use the defaults, which will give you
104 exceptions on errors.
104 exceptions on errors.
105
105
106 """
106 """
107
107
108 # $Id: keepalive.py,v 1.14 2006/04/04 21:00:32 mstenner Exp $
108 # $Id: keepalive.py,v 1.14 2006/04/04 21:00:32 mstenner Exp $
109
109
110 from __future__ import absolute_import, print_function
110 from __future__ import absolute_import, print_function
111
111
112 import errno
112 import errno
113 import httplib
113 import httplib
114 import socket
114 import socket
115 import sys
115 import sys
116 import thread
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 DEBUG = None
125 DEBUG = None
120
126
121 if sys.version_info < (2, 4):
127 if sys.version_info < (2, 4):
122 HANDLE_ERRORS = 1
128 HANDLE_ERRORS = 1
123 else: HANDLE_ERRORS = 0
129 else: HANDLE_ERRORS = 0
124
130
125 class ConnectionManager(object):
131 class ConnectionManager(object):
126 """
132 """
127 The connection manager must be able to:
133 The connection manager must be able to:
128 * keep track of all existing
134 * keep track of all existing
129 """
135 """
130 def __init__(self):
136 def __init__(self):
131 self._lock = thread.allocate_lock()
137 self._lock = thread.allocate_lock()
132 self._hostmap = {} # map hosts to a list of connections
138 self._hostmap = {} # map hosts to a list of connections
133 self._connmap = {} # map connections to host
139 self._connmap = {} # map connections to host
134 self._readymap = {} # map connection to ready state
140 self._readymap = {} # map connection to ready state
135
141
136 def add(self, host, connection, ready):
142 def add(self, host, connection, ready):
137 self._lock.acquire()
143 self._lock.acquire()
138 try:
144 try:
139 if host not in self._hostmap:
145 if host not in self._hostmap:
140 self._hostmap[host] = []
146 self._hostmap[host] = []
141 self._hostmap[host].append(connection)
147 self._hostmap[host].append(connection)
142 self._connmap[connection] = host
148 self._connmap[connection] = host
143 self._readymap[connection] = ready
149 self._readymap[connection] = ready
144 finally:
150 finally:
145 self._lock.release()
151 self._lock.release()
146
152
147 def remove(self, connection):
153 def remove(self, connection):
148 self._lock.acquire()
154 self._lock.acquire()
149 try:
155 try:
150 try:
156 try:
151 host = self._connmap[connection]
157 host = self._connmap[connection]
152 except KeyError:
158 except KeyError:
153 pass
159 pass
154 else:
160 else:
155 del self._connmap[connection]
161 del self._connmap[connection]
156 del self._readymap[connection]
162 del self._readymap[connection]
157 self._hostmap[host].remove(connection)
163 self._hostmap[host].remove(connection)
158 if not self._hostmap[host]: del self._hostmap[host]
164 if not self._hostmap[host]: del self._hostmap[host]
159 finally:
165 finally:
160 self._lock.release()
166 self._lock.release()
161
167
162 def set_ready(self, connection, ready):
168 def set_ready(self, connection, ready):
163 try:
169 try:
164 self._readymap[connection] = ready
170 self._readymap[connection] = ready
165 except KeyError:
171 except KeyError:
166 pass
172 pass
167
173
168 def get_ready_conn(self, host):
174 def get_ready_conn(self, host):
169 conn = None
175 conn = None
170 self._lock.acquire()
176 self._lock.acquire()
171 try:
177 try:
172 if host in self._hostmap:
178 if host in self._hostmap:
173 for c in self._hostmap[host]:
179 for c in self._hostmap[host]:
174 if self._readymap[c]:
180 if self._readymap[c]:
175 self._readymap[c] = 0
181 self._readymap[c] = 0
176 conn = c
182 conn = c
177 break
183 break
178 finally:
184 finally:
179 self._lock.release()
185 self._lock.release()
180 return conn
186 return conn
181
187
182 def get_all(self, host=None):
188 def get_all(self, host=None):
183 if host:
189 if host:
184 return list(self._hostmap.get(host, []))
190 return list(self._hostmap.get(host, []))
185 else:
191 else:
186 return dict(self._hostmap)
192 return dict(self._hostmap)
187
193
188 class KeepAliveHandler(object):
194 class KeepAliveHandler(object):
189 def __init__(self):
195 def __init__(self):
190 self._cm = ConnectionManager()
196 self._cm = ConnectionManager()
191
197
192 #### Connection Management
198 #### Connection Management
193 def open_connections(self):
199 def open_connections(self):
194 """return a list of connected hosts and the number of connections
200 """return a list of connected hosts and the number of connections
195 to each. [('foo.com:80', 2), ('bar.org', 1)]"""
201 to each. [('foo.com:80', 2), ('bar.org', 1)]"""
196 return [(host, len(li)) for (host, li) in self._cm.get_all().items()]
202 return [(host, len(li)) for (host, li) in self._cm.get_all().items()]
197
203
198 def close_connection(self, host):
204 def close_connection(self, host):
199 """close connection(s) to <host>
205 """close connection(s) to <host>
200 host is the host:port spec, as in 'www.cnn.com:8080' as passed in.
206 host is the host:port spec, as in 'www.cnn.com:8080' as passed in.
201 no error occurs if there is no connection to that host."""
207 no error occurs if there is no connection to that host."""
202 for h in self._cm.get_all(host):
208 for h in self._cm.get_all(host):
203 self._cm.remove(h)
209 self._cm.remove(h)
204 h.close()
210 h.close()
205
211
206 def close_all(self):
212 def close_all(self):
207 """close all open connections"""
213 """close all open connections"""
208 for host, conns in self._cm.get_all().iteritems():
214 for host, conns in self._cm.get_all().iteritems():
209 for h in conns:
215 for h in conns:
210 self._cm.remove(h)
216 self._cm.remove(h)
211 h.close()
217 h.close()
212
218
213 def _request_closed(self, request, host, connection):
219 def _request_closed(self, request, host, connection):
214 """tells us that this request is now closed and that the
220 """tells us that this request is now closed and that the
215 connection is ready for another request"""
221 connection is ready for another request"""
216 self._cm.set_ready(connection, 1)
222 self._cm.set_ready(connection, 1)
217
223
218 def _remove_connection(self, host, connection, close=0):
224 def _remove_connection(self, host, connection, close=0):
219 if close:
225 if close:
220 connection.close()
226 connection.close()
221 self._cm.remove(connection)
227 self._cm.remove(connection)
222
228
223 #### Transaction Execution
229 #### Transaction Execution
224 def http_open(self, req):
230 def http_open(self, req):
225 return self.do_open(HTTPConnection, req)
231 return self.do_open(HTTPConnection, req)
226
232
227 def do_open(self, http_class, req):
233 def do_open(self, http_class, req):
228 host = req.get_host()
234 host = req.get_host()
229 if not host:
235 if not host:
230 raise urllib2.URLError('no host given')
236 raise urlerr.urlerror('no host given')
231
237
232 try:
238 try:
233 h = self._cm.get_ready_conn(host)
239 h = self._cm.get_ready_conn(host)
234 while h:
240 while h:
235 r = self._reuse_connection(h, req, host)
241 r = self._reuse_connection(h, req, host)
236
242
237 # if this response is non-None, then it worked and we're
243 # if this response is non-None, then it worked and we're
238 # done. Break out, skipping the else block.
244 # done. Break out, skipping the else block.
239 if r:
245 if r:
240 break
246 break
241
247
242 # connection is bad - possibly closed by server
248 # connection is bad - possibly closed by server
243 # discard it and ask for the next free connection
249 # discard it and ask for the next free connection
244 h.close()
250 h.close()
245 self._cm.remove(h)
251 self._cm.remove(h)
246 h = self._cm.get_ready_conn(host)
252 h = self._cm.get_ready_conn(host)
247 else:
253 else:
248 # no (working) free connections were found. Create a new one.
254 # no (working) free connections were found. Create a new one.
249 h = http_class(host)
255 h = http_class(host)
250 if DEBUG:
256 if DEBUG:
251 DEBUG.info("creating new connection to %s (%d)",
257 DEBUG.info("creating new connection to %s (%d)",
252 host, id(h))
258 host, id(h))
253 self._cm.add(host, h, 0)
259 self._cm.add(host, h, 0)
254 self._start_transaction(h, req)
260 self._start_transaction(h, req)
255 r = h.getresponse()
261 r = h.getresponse()
256 except (socket.error, httplib.HTTPException) as err:
262 except (socket.error, httplib.HTTPException) as err:
257 raise urllib2.URLError(err)
263 raise urlerr.urlerror(err)
258
264
259 # if not a persistent connection, don't try to reuse it
265 # if not a persistent connection, don't try to reuse it
260 if r.will_close:
266 if r.will_close:
261 self._cm.remove(h)
267 self._cm.remove(h)
262
268
263 if DEBUG:
269 if DEBUG:
264 DEBUG.info("STATUS: %s, %s", r.status, r.reason)
270 DEBUG.info("STATUS: %s, %s", r.status, r.reason)
265 r._handler = self
271 r._handler = self
266 r._host = host
272 r._host = host
267 r._url = req.get_full_url()
273 r._url = req.get_full_url()
268 r._connection = h
274 r._connection = h
269 r.code = r.status
275 r.code = r.status
270 r.headers = r.msg
276 r.headers = r.msg
271 r.msg = r.reason
277 r.msg = r.reason
272
278
273 if r.status == 200 or not HANDLE_ERRORS:
279 if r.status == 200 or not HANDLE_ERRORS:
274 return r
280 return r
275 else:
281 else:
276 return self.parent.error('http', req, r,
282 return self.parent.error('http', req, r,
277 r.status, r.msg, r.headers)
283 r.status, r.msg, r.headers)
278
284
279 def _reuse_connection(self, h, req, host):
285 def _reuse_connection(self, h, req, host):
280 """start the transaction with a re-used connection
286 """start the transaction with a re-used connection
281 return a response object (r) upon success or None on failure.
287 return a response object (r) upon success or None on failure.
282 This DOES not close or remove bad connections in cases where
288 This DOES not close or remove bad connections in cases where
283 it returns. However, if an unexpected exception occurs, it
289 it returns. However, if an unexpected exception occurs, it
284 will close and remove the connection before re-raising.
290 will close and remove the connection before re-raising.
285 """
291 """
286 try:
292 try:
287 self._start_transaction(h, req)
293 self._start_transaction(h, req)
288 r = h.getresponse()
294 r = h.getresponse()
289 # note: just because we got something back doesn't mean it
295 # note: just because we got something back doesn't mean it
290 # worked. We'll check the version below, too.
296 # worked. We'll check the version below, too.
291 except (socket.error, httplib.HTTPException):
297 except (socket.error, httplib.HTTPException):
292 r = None
298 r = None
293 except: # re-raises
299 except: # re-raises
294 # adding this block just in case we've missed
300 # adding this block just in case we've missed
295 # something we will still raise the exception, but
301 # something we will still raise the exception, but
296 # lets try and close the connection and remove it
302 # lets try and close the connection and remove it
297 # first. We previously got into a nasty loop
303 # first. We previously got into a nasty loop
298 # where an exception was uncaught, and so the
304 # where an exception was uncaught, and so the
299 # connection stayed open. On the next try, the
305 # connection stayed open. On the next try, the
300 # same exception was raised, etc. The trade-off is
306 # same exception was raised, etc. The trade-off is
301 # that it's now possible this call will raise
307 # that it's now possible this call will raise
302 # a DIFFERENT exception
308 # a DIFFERENT exception
303 if DEBUG:
309 if DEBUG:
304 DEBUG.error("unexpected exception - closing "
310 DEBUG.error("unexpected exception - closing "
305 "connection to %s (%d)", host, id(h))
311 "connection to %s (%d)", host, id(h))
306 self._cm.remove(h)
312 self._cm.remove(h)
307 h.close()
313 h.close()
308 raise
314 raise
309
315
310 if r is None or r.version == 9:
316 if r is None or r.version == 9:
311 # httplib falls back to assuming HTTP 0.9 if it gets a
317 # httplib falls back to assuming HTTP 0.9 if it gets a
312 # bad header back. This is most likely to happen if
318 # bad header back. This is most likely to happen if
313 # the socket has been closed by the server since we
319 # the socket has been closed by the server since we
314 # last used the connection.
320 # last used the connection.
315 if DEBUG:
321 if DEBUG:
316 DEBUG.info("failed to re-use connection to %s (%d)",
322 DEBUG.info("failed to re-use connection to %s (%d)",
317 host, id(h))
323 host, id(h))
318 r = None
324 r = None
319 else:
325 else:
320 if DEBUG:
326 if DEBUG:
321 DEBUG.info("re-using connection to %s (%d)", host, id(h))
327 DEBUG.info("re-using connection to %s (%d)", host, id(h))
322
328
323 return r
329 return r
324
330
325 def _start_transaction(self, h, req):
331 def _start_transaction(self, h, req):
326 # What follows mostly reimplements HTTPConnection.request()
332 # What follows mostly reimplements HTTPConnection.request()
327 # except it adds self.parent.addheaders in the mix.
333 # except it adds self.parent.addheaders in the mix.
328 headers = req.headers.copy()
334 headers = req.headers.copy()
329 if sys.version_info >= (2, 4):
335 if sys.version_info >= (2, 4):
330 headers.update(req.unredirected_hdrs)
336 headers.update(req.unredirected_hdrs)
331 headers.update(self.parent.addheaders)
337 headers.update(self.parent.addheaders)
332 headers = dict((n.lower(), v) for n, v in headers.items())
338 headers = dict((n.lower(), v) for n, v in headers.items())
333 skipheaders = {}
339 skipheaders = {}
334 for n in ('host', 'accept-encoding'):
340 for n in ('host', 'accept-encoding'):
335 if n in headers:
341 if n in headers:
336 skipheaders['skip_' + n.replace('-', '_')] = 1
342 skipheaders['skip_' + n.replace('-', '_')] = 1
337 try:
343 try:
338 if req.has_data():
344 if req.has_data():
339 data = req.get_data()
345 data = req.get_data()
340 h.putrequest('POST', req.get_selector(), **skipheaders)
346 h.putrequest('POST', req.get_selector(), **skipheaders)
341 if 'content-type' not in headers:
347 if 'content-type' not in headers:
342 h.putheader('Content-type',
348 h.putheader('Content-type',
343 'application/x-www-form-urlencoded')
349 'application/x-www-form-urlencoded')
344 if 'content-length' not in headers:
350 if 'content-length' not in headers:
345 h.putheader('Content-length', '%d' % len(data))
351 h.putheader('Content-length', '%d' % len(data))
346 else:
352 else:
347 h.putrequest('GET', req.get_selector(), **skipheaders)
353 h.putrequest('GET', req.get_selector(), **skipheaders)
348 except socket.error as err:
354 except socket.error as err:
349 raise urllib2.URLError(err)
355 raise urlerr.urlerror(err)
350 for k, v in headers.items():
356 for k, v in headers.items():
351 h.putheader(k, v)
357 h.putheader(k, v)
352 h.endheaders()
358 h.endheaders()
353 if req.has_data():
359 if req.has_data():
354 h.send(data)
360 h.send(data)
355
361
356 class HTTPHandler(KeepAliveHandler, urllib2.HTTPHandler):
362 class HTTPHandler(KeepAliveHandler, urlreq.httphandler):
357 pass
363 pass
358
364
359 class HTTPResponse(httplib.HTTPResponse):
365 class HTTPResponse(httplib.HTTPResponse):
360 # we need to subclass HTTPResponse in order to
366 # we need to subclass HTTPResponse in order to
361 # 1) add readline() and readlines() methods
367 # 1) add readline() and readlines() methods
362 # 2) add close_connection() methods
368 # 2) add close_connection() methods
363 # 3) add info() and geturl() methods
369 # 3) add info() and geturl() methods
364
370
365 # in order to add readline(), read must be modified to deal with a
371 # in order to add readline(), read must be modified to deal with a
366 # buffer. example: readline must read a buffer and then spit back
372 # buffer. example: readline must read a buffer and then spit back
367 # one line at a time. The only real alternative is to read one
373 # one line at a time. The only real alternative is to read one
368 # BYTE at a time (ick). Once something has been read, it can't be
374 # BYTE at a time (ick). Once something has been read, it can't be
369 # put back (ok, maybe it can, but that's even uglier than this),
375 # put back (ok, maybe it can, but that's even uglier than this),
370 # so if you THEN do a normal read, you must first take stuff from
376 # so if you THEN do a normal read, you must first take stuff from
371 # the buffer.
377 # the buffer.
372
378
373 # the read method wraps the original to accommodate buffering,
379 # the read method wraps the original to accommodate buffering,
374 # although read() never adds to the buffer.
380 # although read() never adds to the buffer.
375 # Both readline and readlines have been stolen with almost no
381 # Both readline and readlines have been stolen with almost no
376 # modification from socket.py
382 # modification from socket.py
377
383
378
384
379 def __init__(self, sock, debuglevel=0, strict=0, method=None):
385 def __init__(self, sock, debuglevel=0, strict=0, method=None):
380 httplib.HTTPResponse.__init__(self, sock, debuglevel, method)
386 httplib.HTTPResponse.__init__(self, sock, debuglevel, method)
381 self.fileno = sock.fileno
387 self.fileno = sock.fileno
382 self.code = None
388 self.code = None
383 self._rbuf = ''
389 self._rbuf = ''
384 self._rbufsize = 8096
390 self._rbufsize = 8096
385 self._handler = None # inserted by the handler later
391 self._handler = None # inserted by the handler later
386 self._host = None # (same)
392 self._host = None # (same)
387 self._url = None # (same)
393 self._url = None # (same)
388 self._connection = None # (same)
394 self._connection = None # (same)
389
395
390 _raw_read = httplib.HTTPResponse.read
396 _raw_read = httplib.HTTPResponse.read
391
397
392 def close(self):
398 def close(self):
393 if self.fp:
399 if self.fp:
394 self.fp.close()
400 self.fp.close()
395 self.fp = None
401 self.fp = None
396 if self._handler:
402 if self._handler:
397 self._handler._request_closed(self, self._host,
403 self._handler._request_closed(self, self._host,
398 self._connection)
404 self._connection)
399
405
400 def close_connection(self):
406 def close_connection(self):
401 self._handler._remove_connection(self._host, self._connection, close=1)
407 self._handler._remove_connection(self._host, self._connection, close=1)
402 self.close()
408 self.close()
403
409
404 def info(self):
410 def info(self):
405 return self.headers
411 return self.headers
406
412
407 def geturl(self):
413 def geturl(self):
408 return self._url
414 return self._url
409
415
410 def read(self, amt=None):
416 def read(self, amt=None):
411 # the _rbuf test is only in this first if for speed. It's not
417 # the _rbuf test is only in this first if for speed. It's not
412 # logically necessary
418 # logically necessary
413 if self._rbuf and not amt is None:
419 if self._rbuf and not amt is None:
414 L = len(self._rbuf)
420 L = len(self._rbuf)
415 if amt > L:
421 if amt > L:
416 amt -= L
422 amt -= L
417 else:
423 else:
418 s = self._rbuf[:amt]
424 s = self._rbuf[:amt]
419 self._rbuf = self._rbuf[amt:]
425 self._rbuf = self._rbuf[amt:]
420 return s
426 return s
421
427
422 s = self._rbuf + self._raw_read(amt)
428 s = self._rbuf + self._raw_read(amt)
423 self._rbuf = ''
429 self._rbuf = ''
424 return s
430 return s
425
431
426 # stolen from Python SVN #68532 to fix issue1088
432 # stolen from Python SVN #68532 to fix issue1088
427 def _read_chunked(self, amt):
433 def _read_chunked(self, amt):
428 chunk_left = self.chunk_left
434 chunk_left = self.chunk_left
429 value = ''
435 value = ''
430
436
431 # XXX This accumulates chunks by repeated string concatenation,
437 # XXX This accumulates chunks by repeated string concatenation,
432 # which is not efficient as the number or size of chunks gets big.
438 # which is not efficient as the number or size of chunks gets big.
433 while True:
439 while True:
434 if chunk_left is None:
440 if chunk_left is None:
435 line = self.fp.readline()
441 line = self.fp.readline()
436 i = line.find(';')
442 i = line.find(';')
437 if i >= 0:
443 if i >= 0:
438 line = line[:i] # strip chunk-extensions
444 line = line[:i] # strip chunk-extensions
439 try:
445 try:
440 chunk_left = int(line, 16)
446 chunk_left = int(line, 16)
441 except ValueError:
447 except ValueError:
442 # close the connection as protocol synchronization is
448 # close the connection as protocol synchronization is
443 # probably lost
449 # probably lost
444 self.close()
450 self.close()
445 raise httplib.IncompleteRead(value)
451 raise httplib.IncompleteRead(value)
446 if chunk_left == 0:
452 if chunk_left == 0:
447 break
453 break
448 if amt is None:
454 if amt is None:
449 value += self._safe_read(chunk_left)
455 value += self._safe_read(chunk_left)
450 elif amt < chunk_left:
456 elif amt < chunk_left:
451 value += self._safe_read(amt)
457 value += self._safe_read(amt)
452 self.chunk_left = chunk_left - amt
458 self.chunk_left = chunk_left - amt
453 return value
459 return value
454 elif amt == chunk_left:
460 elif amt == chunk_left:
455 value += self._safe_read(amt)
461 value += self._safe_read(amt)
456 self._safe_read(2) # toss the CRLF at the end of the chunk
462 self._safe_read(2) # toss the CRLF at the end of the chunk
457 self.chunk_left = None
463 self.chunk_left = None
458 return value
464 return value
459 else:
465 else:
460 value += self._safe_read(chunk_left)
466 value += self._safe_read(chunk_left)
461 amt -= chunk_left
467 amt -= chunk_left
462
468
463 # we read the whole chunk, get another
469 # we read the whole chunk, get another
464 self._safe_read(2) # toss the CRLF at the end of the chunk
470 self._safe_read(2) # toss the CRLF at the end of the chunk
465 chunk_left = None
471 chunk_left = None
466
472
467 # read and discard trailer up to the CRLF terminator
473 # read and discard trailer up to the CRLF terminator
468 ### note: we shouldn't have any trailers!
474 ### note: we shouldn't have any trailers!
469 while True:
475 while True:
470 line = self.fp.readline()
476 line = self.fp.readline()
471 if not line:
477 if not line:
472 # a vanishingly small number of sites EOF without
478 # a vanishingly small number of sites EOF without
473 # sending the trailer
479 # sending the trailer
474 break
480 break
475 if line == '\r\n':
481 if line == '\r\n':
476 break
482 break
477
483
478 # we read everything; close the "file"
484 # we read everything; close the "file"
479 self.close()
485 self.close()
480
486
481 return value
487 return value
482
488
483 def readline(self, limit=-1):
489 def readline(self, limit=-1):
484 i = self._rbuf.find('\n')
490 i = self._rbuf.find('\n')
485 while i < 0 and not (0 < limit <= len(self._rbuf)):
491 while i < 0 and not (0 < limit <= len(self._rbuf)):
486 new = self._raw_read(self._rbufsize)
492 new = self._raw_read(self._rbufsize)
487 if not new:
493 if not new:
488 break
494 break
489 i = new.find('\n')
495 i = new.find('\n')
490 if i >= 0:
496 if i >= 0:
491 i = i + len(self._rbuf)
497 i = i + len(self._rbuf)
492 self._rbuf = self._rbuf + new
498 self._rbuf = self._rbuf + new
493 if i < 0:
499 if i < 0:
494 i = len(self._rbuf)
500 i = len(self._rbuf)
495 else:
501 else:
496 i = i + 1
502 i = i + 1
497 if 0 <= limit < len(self._rbuf):
503 if 0 <= limit < len(self._rbuf):
498 i = limit
504 i = limit
499 data, self._rbuf = self._rbuf[:i], self._rbuf[i:]
505 data, self._rbuf = self._rbuf[:i], self._rbuf[i:]
500 return data
506 return data
501
507
502 def readlines(self, sizehint=0):
508 def readlines(self, sizehint=0):
503 total = 0
509 total = 0
504 list = []
510 list = []
505 while True:
511 while True:
506 line = self.readline()
512 line = self.readline()
507 if not line:
513 if not line:
508 break
514 break
509 list.append(line)
515 list.append(line)
510 total += len(line)
516 total += len(line)
511 if sizehint and total >= sizehint:
517 if sizehint and total >= sizehint:
512 break
518 break
513 return list
519 return list
514
520
515 def safesend(self, str):
521 def safesend(self, str):
516 """Send `str' to the server.
522 """Send `str' to the server.
517
523
518 Shamelessly ripped off from httplib to patch a bad behavior.
524 Shamelessly ripped off from httplib to patch a bad behavior.
519 """
525 """
520 # _broken_pipe_resp is an attribute we set in this function
526 # _broken_pipe_resp is an attribute we set in this function
521 # if the socket is closed while we're sending data but
527 # if the socket is closed while we're sending data but
522 # the server sent us a response before hanging up.
528 # the server sent us a response before hanging up.
523 # In that case, we want to pretend to send the rest of the
529 # In that case, we want to pretend to send the rest of the
524 # outgoing data, and then let the user use getresponse()
530 # outgoing data, and then let the user use getresponse()
525 # (which we wrap) to get this last response before
531 # (which we wrap) to get this last response before
526 # opening a new socket.
532 # opening a new socket.
527 if getattr(self, '_broken_pipe_resp', None) is not None:
533 if getattr(self, '_broken_pipe_resp', None) is not None:
528 return
534 return
529
535
530 if self.sock is None:
536 if self.sock is None:
531 if self.auto_open:
537 if self.auto_open:
532 self.connect()
538 self.connect()
533 else:
539 else:
534 raise httplib.NotConnected
540 raise httplib.NotConnected
535
541
536 # send the data to the server. if we get a broken pipe, then close
542 # send the data to the server. if we get a broken pipe, then close
537 # the socket. we want to reconnect when somebody tries to send again.
543 # the socket. we want to reconnect when somebody tries to send again.
538 #
544 #
539 # NOTE: we DO propagate the error, though, because we cannot simply
545 # NOTE: we DO propagate the error, though, because we cannot simply
540 # ignore the error... the caller will know if they can retry.
546 # ignore the error... the caller will know if they can retry.
541 if self.debuglevel > 0:
547 if self.debuglevel > 0:
542 print("send:", repr(str))
548 print("send:", repr(str))
543 try:
549 try:
544 blocksize = 8192
550 blocksize = 8192
545 read = getattr(str, 'read', None)
551 read = getattr(str, 'read', None)
546 if read is not None:
552 if read is not None:
547 if self.debuglevel > 0:
553 if self.debuglevel > 0:
548 print("sending a read()able")
554 print("sending a read()able")
549 data = read(blocksize)
555 data = read(blocksize)
550 while data:
556 while data:
551 self.sock.sendall(data)
557 self.sock.sendall(data)
552 data = read(blocksize)
558 data = read(blocksize)
553 else:
559 else:
554 self.sock.sendall(str)
560 self.sock.sendall(str)
555 except socket.error as v:
561 except socket.error as v:
556 reraise = True
562 reraise = True
557 if v[0] == errno.EPIPE: # Broken pipe
563 if v[0] == errno.EPIPE: # Broken pipe
558 if self._HTTPConnection__state == httplib._CS_REQ_SENT:
564 if self._HTTPConnection__state == httplib._CS_REQ_SENT:
559 self._broken_pipe_resp = None
565 self._broken_pipe_resp = None
560 self._broken_pipe_resp = self.getresponse()
566 self._broken_pipe_resp = self.getresponse()
561 reraise = False
567 reraise = False
562 self.close()
568 self.close()
563 if reraise:
569 if reraise:
564 raise
570 raise
565
571
566 def wrapgetresponse(cls):
572 def wrapgetresponse(cls):
567 """Wraps getresponse in cls with a broken-pipe sane version.
573 """Wraps getresponse in cls with a broken-pipe sane version.
568 """
574 """
569 def safegetresponse(self):
575 def safegetresponse(self):
570 # In safesend() we might set the _broken_pipe_resp
576 # In safesend() we might set the _broken_pipe_resp
571 # attribute, in which case the socket has already
577 # attribute, in which case the socket has already
572 # been closed and we just need to give them the response
578 # been closed and we just need to give them the response
573 # back. Otherwise, we use the normal response path.
579 # back. Otherwise, we use the normal response path.
574 r = getattr(self, '_broken_pipe_resp', None)
580 r = getattr(self, '_broken_pipe_resp', None)
575 if r is not None:
581 if r is not None:
576 return r
582 return r
577 return cls.getresponse(self)
583 return cls.getresponse(self)
578 safegetresponse.__doc__ = cls.getresponse.__doc__
584 safegetresponse.__doc__ = cls.getresponse.__doc__
579 return safegetresponse
585 return safegetresponse
580
586
581 class HTTPConnection(httplib.HTTPConnection):
587 class HTTPConnection(httplib.HTTPConnection):
582 # use the modified response class
588 # use the modified response class
583 response_class = HTTPResponse
589 response_class = HTTPResponse
584 send = safesend
590 send = safesend
585 getresponse = wrapgetresponse(httplib.HTTPConnection)
591 getresponse = wrapgetresponse(httplib.HTTPConnection)
586
592
587
593
588 #########################################################################
594 #########################################################################
589 ##### TEST FUNCTIONS
595 ##### TEST FUNCTIONS
590 #########################################################################
596 #########################################################################
591
597
592 def error_handler(url):
598 def error_handler(url):
593 global HANDLE_ERRORS
599 global HANDLE_ERRORS
594 orig = HANDLE_ERRORS
600 orig = HANDLE_ERRORS
595 keepalive_handler = HTTPHandler()
601 keepalive_handler = HTTPHandler()
596 opener = urllib2.build_opener(keepalive_handler)
602 opener = urlreq.buildopener(keepalive_handler)
597 urllib2.install_opener(opener)
603 urlreq.installopener(opener)
598 pos = {0: 'off', 1: 'on'}
604 pos = {0: 'off', 1: 'on'}
599 for i in (0, 1):
605 for i in (0, 1):
600 print(" fancy error handling %s (HANDLE_ERRORS = %i)" % (pos[i], i))
606 print(" fancy error handling %s (HANDLE_ERRORS = %i)" % (pos[i], i))
601 HANDLE_ERRORS = i
607 HANDLE_ERRORS = i
602 try:
608 try:
603 fo = urllib2.urlopen(url)
609 fo = urlreq.urlopen(url)
604 fo.read()
610 fo.read()
605 fo.close()
611 fo.close()
606 try:
612 try:
607 status, reason = fo.status, fo.reason
613 status, reason = fo.status, fo.reason
608 except AttributeError:
614 except AttributeError:
609 status, reason = None, None
615 status, reason = None, None
610 except IOError as e:
616 except IOError as e:
611 print(" EXCEPTION: %s" % e)
617 print(" EXCEPTION: %s" % e)
612 raise
618 raise
613 else:
619 else:
614 print(" status = %s, reason = %s" % (status, reason))
620 print(" status = %s, reason = %s" % (status, reason))
615 HANDLE_ERRORS = orig
621 HANDLE_ERRORS = orig
616 hosts = keepalive_handler.open_connections()
622 hosts = keepalive_handler.open_connections()
617 print("open connections:", hosts)
623 print("open connections:", hosts)
618 keepalive_handler.close_all()
624 keepalive_handler.close_all()
619
625
620 def continuity(url):
626 def continuity(url):
621 from . import util
627 from . import util
622 md5 = util.md5
628 md5 = util.md5
623 format = '%25s: %s'
629 format = '%25s: %s'
624
630
625 # first fetch the file with the normal http handler
631 # first fetch the file with the normal http handler
626 opener = urllib2.build_opener()
632 opener = urlreq.buildopener()
627 urllib2.install_opener(opener)
633 urlreq.installopener(opener)
628 fo = urllib2.urlopen(url)
634 fo = urlreq.urlopen(url)
629 foo = fo.read()
635 foo = fo.read()
630 fo.close()
636 fo.close()
631 m = md5(foo)
637 m = md5(foo)
632 print(format % ('normal urllib', m.hexdigest()))
638 print(format % ('normal urllib', m.hexdigest()))
633
639
634 # now install the keepalive handler and try again
640 # now install the keepalive handler and try again
635 opener = urllib2.build_opener(HTTPHandler())
641 opener = urlreq.buildopener(HTTPHandler())
636 urllib2.install_opener(opener)
642 urlreq.installopener(opener)
637
643
638 fo = urllib2.urlopen(url)
644 fo = urlreq.urlopen(url)
639 foo = fo.read()
645 foo = fo.read()
640 fo.close()
646 fo.close()
641 m = md5(foo)
647 m = md5(foo)
642 print(format % ('keepalive read', m.hexdigest()))
648 print(format % ('keepalive read', m.hexdigest()))
643
649
644 fo = urllib2.urlopen(url)
650 fo = urlreq.urlopen(url)
645 foo = ''
651 foo = ''
646 while True:
652 while True:
647 f = fo.readline()
653 f = fo.readline()
648 if f:
654 if f:
649 foo = foo + f
655 foo = foo + f
650 else: break
656 else: break
651 fo.close()
657 fo.close()
652 m = md5(foo)
658 m = md5(foo)
653 print(format % ('keepalive readline', m.hexdigest()))
659 print(format % ('keepalive readline', m.hexdigest()))
654
660
655 def comp(N, url):
661 def comp(N, url):
656 print(' making %i connections to:\n %s' % (N, url))
662 print(' making %i connections to:\n %s' % (N, url))
657
663
658 sys.stdout.write(' first using the normal urllib handlers')
664 sys.stdout.write(' first using the normal urllib handlers')
659 # first use normal opener
665 # first use normal opener
660 opener = urllib2.build_opener()
666 opener = urlreq.buildopener()
661 urllib2.install_opener(opener)
667 urlreq.installopener(opener)
662 t1 = fetch(N, url)
668 t1 = fetch(N, url)
663 print(' TIME: %.3f s' % t1)
669 print(' TIME: %.3f s' % t1)
664
670
665 sys.stdout.write(' now using the keepalive handler ')
671 sys.stdout.write(' now using the keepalive handler ')
666 # now install the keepalive handler and try again
672 # now install the keepalive handler and try again
667 opener = urllib2.build_opener(HTTPHandler())
673 opener = urlreq.buildopener(HTTPHandler())
668 urllib2.install_opener(opener)
674 urlreq.installopener(opener)
669 t2 = fetch(N, url)
675 t2 = fetch(N, url)
670 print(' TIME: %.3f s' % t2)
676 print(' TIME: %.3f s' % t2)
671 print(' improvement factor: %.2f' % (t1 / t2))
677 print(' improvement factor: %.2f' % (t1 / t2))
672
678
673 def fetch(N, url, delay=0):
679 def fetch(N, url, delay=0):
674 import time
680 import time
675 lens = []
681 lens = []
676 starttime = time.time()
682 starttime = time.time()
677 for i in range(N):
683 for i in range(N):
678 if delay and i > 0:
684 if delay and i > 0:
679 time.sleep(delay)
685 time.sleep(delay)
680 fo = urllib2.urlopen(url)
686 fo = urlreq.urlopen(url)
681 foo = fo.read()
687 foo = fo.read()
682 fo.close()
688 fo.close()
683 lens.append(len(foo))
689 lens.append(len(foo))
684 diff = time.time() - starttime
690 diff = time.time() - starttime
685
691
686 j = 0
692 j = 0
687 for i in lens[1:]:
693 for i in lens[1:]:
688 j = j + 1
694 j = j + 1
689 if not i == lens[0]:
695 if not i == lens[0]:
690 print("WARNING: inconsistent length on read %i: %i" % (j, i))
696 print("WARNING: inconsistent length on read %i: %i" % (j, i))
691
697
692 return diff
698 return diff
693
699
694 def test_timeout(url):
700 def test_timeout(url):
695 global DEBUG
701 global DEBUG
696 dbbackup = DEBUG
702 dbbackup = DEBUG
697 class FakeLogger(object):
703 class FakeLogger(object):
698 def debug(self, msg, *args):
704 def debug(self, msg, *args):
699 print(msg % args)
705 print(msg % args)
700 info = warning = error = debug
706 info = warning = error = debug
701 DEBUG = FakeLogger()
707 DEBUG = FakeLogger()
702 print(" fetching the file to establish a connection")
708 print(" fetching the file to establish a connection")
703 fo = urllib2.urlopen(url)
709 fo = urlreq.urlopen(url)
704 data1 = fo.read()
710 data1 = fo.read()
705 fo.close()
711 fo.close()
706
712
707 i = 20
713 i = 20
708 print(" waiting %i seconds for the server to close the connection" % i)
714 print(" waiting %i seconds for the server to close the connection" % i)
709 while i > 0:
715 while i > 0:
710 sys.stdout.write('\r %2i' % i)
716 sys.stdout.write('\r %2i' % i)
711 sys.stdout.flush()
717 sys.stdout.flush()
712 time.sleep(1)
718 time.sleep(1)
713 i -= 1
719 i -= 1
714 sys.stderr.write('\r')
720 sys.stderr.write('\r')
715
721
716 print(" fetching the file a second time")
722 print(" fetching the file a second time")
717 fo = urllib2.urlopen(url)
723 fo = urlreq.urlopen(url)
718 data2 = fo.read()
724 data2 = fo.read()
719 fo.close()
725 fo.close()
720
726
721 if data1 == data2:
727 if data1 == data2:
722 print(' data are identical')
728 print(' data are identical')
723 else:
729 else:
724 print(' ERROR: DATA DIFFER')
730 print(' ERROR: DATA DIFFER')
725
731
726 DEBUG = dbbackup
732 DEBUG = dbbackup
727
733
728
734
729 def test(url, N=10):
735 def test(url, N=10):
730 print("checking error handler (do this on a non-200)")
736 print("checking error handler (do this on a non-200)")
731 try: error_handler(url)
737 try: error_handler(url)
732 except IOError:
738 except IOError:
733 print("exiting - exception will prevent further tests")
739 print("exiting - exception will prevent further tests")
734 sys.exit()
740 sys.exit()
735 print('')
741 print('')
736 print("performing continuity test (making sure stuff isn't corrupted)")
742 print("performing continuity test (making sure stuff isn't corrupted)")
737 continuity(url)
743 continuity(url)
738 print('')
744 print('')
739 print("performing speed comparison")
745 print("performing speed comparison")
740 comp(N, url)
746 comp(N, url)
741 print('')
747 print('')
742 print("performing dropped-connection check")
748 print("performing dropped-connection check")
743 test_timeout(url)
749 test_timeout(url)
744
750
745 if __name__ == '__main__':
751 if __name__ == '__main__':
746 import time
752 import time
747 try:
753 try:
748 N = int(sys.argv[1])
754 N = int(sys.argv[1])
749 url = sys.argv[2]
755 url = sys.argv[2]
750 except (IndexError, ValueError):
756 except (IndexError, ValueError):
751 print("%s <integer> <url>" % sys.argv[0])
757 print("%s <integer> <url>" % sys.argv[0])
752 else:
758 else:
753 test(url, N)
759 test(url, N)
@@ -1,1982 +1,1983 b''
1 # localrepo.py - read/write repository class for mercurial
1 # localrepo.py - read/write repository class for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import inspect
11 import inspect
12 import os
12 import os
13 import random
13 import random
14 import time
14 import time
15 import urllib
16 import weakref
15 import weakref
17
16
18 from .i18n import _
17 from .i18n import _
19 from .node import (
18 from .node import (
20 hex,
19 hex,
21 nullid,
20 nullid,
22 short,
21 short,
23 wdirrev,
22 wdirrev,
24 )
23 )
25 from . import (
24 from . import (
26 bookmarks,
25 bookmarks,
27 branchmap,
26 branchmap,
28 bundle2,
27 bundle2,
29 changegroup,
28 changegroup,
30 changelog,
29 changelog,
31 cmdutil,
30 cmdutil,
32 context,
31 context,
33 dirstate,
32 dirstate,
34 encoding,
33 encoding,
35 error,
34 error,
36 exchange,
35 exchange,
37 extensions,
36 extensions,
38 filelog,
37 filelog,
39 hook,
38 hook,
40 lock as lockmod,
39 lock as lockmod,
41 manifest,
40 manifest,
42 match as matchmod,
41 match as matchmod,
43 merge as mergemod,
42 merge as mergemod,
44 namespaces,
43 namespaces,
45 obsolete,
44 obsolete,
46 pathutil,
45 pathutil,
47 peer,
46 peer,
48 phases,
47 phases,
49 pushkey,
48 pushkey,
50 repoview,
49 repoview,
51 revset,
50 revset,
52 scmutil,
51 scmutil,
53 store,
52 store,
54 subrepo,
53 subrepo,
55 tags as tagsmod,
54 tags as tagsmod,
56 transaction,
55 transaction,
57 util,
56 util,
58 )
57 )
59
58
60 release = lockmod.release
59 release = lockmod.release
61 propertycache = util.propertycache
60 propertycache = util.propertycache
61 urlerr = util.urlerr
62 urlreq = util.urlreq
62 filecache = scmutil.filecache
63 filecache = scmutil.filecache
63
64
64 class repofilecache(filecache):
65 class repofilecache(filecache):
65 """All filecache usage on repo are done for logic that should be unfiltered
66 """All filecache usage on repo are done for logic that should be unfiltered
66 """
67 """
67
68
68 def __get__(self, repo, type=None):
69 def __get__(self, repo, type=None):
69 return super(repofilecache, self).__get__(repo.unfiltered(), type)
70 return super(repofilecache, self).__get__(repo.unfiltered(), type)
70 def __set__(self, repo, value):
71 def __set__(self, repo, value):
71 return super(repofilecache, self).__set__(repo.unfiltered(), value)
72 return super(repofilecache, self).__set__(repo.unfiltered(), value)
72 def __delete__(self, repo):
73 def __delete__(self, repo):
73 return super(repofilecache, self).__delete__(repo.unfiltered())
74 return super(repofilecache, self).__delete__(repo.unfiltered())
74
75
75 class storecache(repofilecache):
76 class storecache(repofilecache):
76 """filecache for files in the store"""
77 """filecache for files in the store"""
77 def join(self, obj, fname):
78 def join(self, obj, fname):
78 return obj.sjoin(fname)
79 return obj.sjoin(fname)
79
80
80 class unfilteredpropertycache(propertycache):
81 class unfilteredpropertycache(propertycache):
81 """propertycache that apply to unfiltered repo only"""
82 """propertycache that apply to unfiltered repo only"""
82
83
83 def __get__(self, repo, type=None):
84 def __get__(self, repo, type=None):
84 unfi = repo.unfiltered()
85 unfi = repo.unfiltered()
85 if unfi is repo:
86 if unfi is repo:
86 return super(unfilteredpropertycache, self).__get__(unfi)
87 return super(unfilteredpropertycache, self).__get__(unfi)
87 return getattr(unfi, self.name)
88 return getattr(unfi, self.name)
88
89
89 class filteredpropertycache(propertycache):
90 class filteredpropertycache(propertycache):
90 """propertycache that must take filtering in account"""
91 """propertycache that must take filtering in account"""
91
92
92 def cachevalue(self, obj, value):
93 def cachevalue(self, obj, value):
93 object.__setattr__(obj, self.name, value)
94 object.__setattr__(obj, self.name, value)
94
95
95
96
96 def hasunfilteredcache(repo, name):
97 def hasunfilteredcache(repo, name):
97 """check if a repo has an unfilteredpropertycache value for <name>"""
98 """check if a repo has an unfilteredpropertycache value for <name>"""
98 return name in vars(repo.unfiltered())
99 return name in vars(repo.unfiltered())
99
100
100 def unfilteredmethod(orig):
101 def unfilteredmethod(orig):
101 """decorate method that always need to be run on unfiltered version"""
102 """decorate method that always need to be run on unfiltered version"""
102 def wrapper(repo, *args, **kwargs):
103 def wrapper(repo, *args, **kwargs):
103 return orig(repo.unfiltered(), *args, **kwargs)
104 return orig(repo.unfiltered(), *args, **kwargs)
104 return wrapper
105 return wrapper
105
106
106 moderncaps = set(('lookup', 'branchmap', 'pushkey', 'known', 'getbundle',
107 moderncaps = set(('lookup', 'branchmap', 'pushkey', 'known', 'getbundle',
107 'unbundle'))
108 'unbundle'))
108 legacycaps = moderncaps.union(set(['changegroupsubset']))
109 legacycaps = moderncaps.union(set(['changegroupsubset']))
109
110
110 class localpeer(peer.peerrepository):
111 class localpeer(peer.peerrepository):
111 '''peer for a local repo; reflects only the most recent API'''
112 '''peer for a local repo; reflects only the most recent API'''
112
113
113 def __init__(self, repo, caps=moderncaps):
114 def __init__(self, repo, caps=moderncaps):
114 peer.peerrepository.__init__(self)
115 peer.peerrepository.__init__(self)
115 self._repo = repo.filtered('served')
116 self._repo = repo.filtered('served')
116 self.ui = repo.ui
117 self.ui = repo.ui
117 self._caps = repo._restrictcapabilities(caps)
118 self._caps = repo._restrictcapabilities(caps)
118 self.requirements = repo.requirements
119 self.requirements = repo.requirements
119 self.supportedformats = repo.supportedformats
120 self.supportedformats = repo.supportedformats
120
121
121 def close(self):
122 def close(self):
122 self._repo.close()
123 self._repo.close()
123
124
124 def _capabilities(self):
125 def _capabilities(self):
125 return self._caps
126 return self._caps
126
127
127 def local(self):
128 def local(self):
128 return self._repo
129 return self._repo
129
130
130 def canpush(self):
131 def canpush(self):
131 return True
132 return True
132
133
133 def url(self):
134 def url(self):
134 return self._repo.url()
135 return self._repo.url()
135
136
136 def lookup(self, key):
137 def lookup(self, key):
137 return self._repo.lookup(key)
138 return self._repo.lookup(key)
138
139
139 def branchmap(self):
140 def branchmap(self):
140 return self._repo.branchmap()
141 return self._repo.branchmap()
141
142
142 def heads(self):
143 def heads(self):
143 return self._repo.heads()
144 return self._repo.heads()
144
145
145 def known(self, nodes):
146 def known(self, nodes):
146 return self._repo.known(nodes)
147 return self._repo.known(nodes)
147
148
148 def getbundle(self, source, heads=None, common=None, bundlecaps=None,
149 def getbundle(self, source, heads=None, common=None, bundlecaps=None,
149 **kwargs):
150 **kwargs):
150 cg = exchange.getbundle(self._repo, source, heads=heads,
151 cg = exchange.getbundle(self._repo, source, heads=heads,
151 common=common, bundlecaps=bundlecaps, **kwargs)
152 common=common, bundlecaps=bundlecaps, **kwargs)
152 if bundlecaps is not None and 'HG20' in bundlecaps:
153 if bundlecaps is not None and 'HG20' in bundlecaps:
153 # When requesting a bundle2, getbundle returns a stream to make the
154 # When requesting a bundle2, getbundle returns a stream to make the
154 # wire level function happier. We need to build a proper object
155 # wire level function happier. We need to build a proper object
155 # from it in local peer.
156 # from it in local peer.
156 cg = bundle2.getunbundler(self.ui, cg)
157 cg = bundle2.getunbundler(self.ui, cg)
157 return cg
158 return cg
158
159
159 # TODO We might want to move the next two calls into legacypeer and add
160 # TODO We might want to move the next two calls into legacypeer and add
160 # unbundle instead.
161 # unbundle instead.
161
162
162 def unbundle(self, cg, heads, url):
163 def unbundle(self, cg, heads, url):
163 """apply a bundle on a repo
164 """apply a bundle on a repo
164
165
165 This function handles the repo locking itself."""
166 This function handles the repo locking itself."""
166 try:
167 try:
167 try:
168 try:
168 cg = exchange.readbundle(self.ui, cg, None)
169 cg = exchange.readbundle(self.ui, cg, None)
169 ret = exchange.unbundle(self._repo, cg, heads, 'push', url)
170 ret = exchange.unbundle(self._repo, cg, heads, 'push', url)
170 if util.safehasattr(ret, 'getchunks'):
171 if util.safehasattr(ret, 'getchunks'):
171 # This is a bundle20 object, turn it into an unbundler.
172 # This is a bundle20 object, turn it into an unbundler.
172 # This little dance should be dropped eventually when the
173 # This little dance should be dropped eventually when the
173 # API is finally improved.
174 # API is finally improved.
174 stream = util.chunkbuffer(ret.getchunks())
175 stream = util.chunkbuffer(ret.getchunks())
175 ret = bundle2.getunbundler(self.ui, stream)
176 ret = bundle2.getunbundler(self.ui, stream)
176 return ret
177 return ret
177 except Exception as exc:
178 except Exception as exc:
178 # If the exception contains output salvaged from a bundle2
179 # If the exception contains output salvaged from a bundle2
179 # reply, we need to make sure it is printed before continuing
180 # reply, we need to make sure it is printed before continuing
180 # to fail. So we build a bundle2 with such output and consume
181 # to fail. So we build a bundle2 with such output and consume
181 # it directly.
182 # it directly.
182 #
183 #
183 # This is not very elegant but allows a "simple" solution for
184 # This is not very elegant but allows a "simple" solution for
184 # issue4594
185 # issue4594
185 output = getattr(exc, '_bundle2salvagedoutput', ())
186 output = getattr(exc, '_bundle2salvagedoutput', ())
186 if output:
187 if output:
187 bundler = bundle2.bundle20(self._repo.ui)
188 bundler = bundle2.bundle20(self._repo.ui)
188 for out in output:
189 for out in output:
189 bundler.addpart(out)
190 bundler.addpart(out)
190 stream = util.chunkbuffer(bundler.getchunks())
191 stream = util.chunkbuffer(bundler.getchunks())
191 b = bundle2.getunbundler(self.ui, stream)
192 b = bundle2.getunbundler(self.ui, stream)
192 bundle2.processbundle(self._repo, b)
193 bundle2.processbundle(self._repo, b)
193 raise
194 raise
194 except error.PushRaced as exc:
195 except error.PushRaced as exc:
195 raise error.ResponseError(_('push failed:'), str(exc))
196 raise error.ResponseError(_('push failed:'), str(exc))
196
197
197 def lock(self):
198 def lock(self):
198 return self._repo.lock()
199 return self._repo.lock()
199
200
200 def addchangegroup(self, cg, source, url):
201 def addchangegroup(self, cg, source, url):
201 return cg.apply(self._repo, source, url)
202 return cg.apply(self._repo, source, url)
202
203
203 def pushkey(self, namespace, key, old, new):
204 def pushkey(self, namespace, key, old, new):
204 return self._repo.pushkey(namespace, key, old, new)
205 return self._repo.pushkey(namespace, key, old, new)
205
206
206 def listkeys(self, namespace):
207 def listkeys(self, namespace):
207 return self._repo.listkeys(namespace)
208 return self._repo.listkeys(namespace)
208
209
209 def debugwireargs(self, one, two, three=None, four=None, five=None):
210 def debugwireargs(self, one, two, three=None, four=None, five=None):
210 '''used to test argument passing over the wire'''
211 '''used to test argument passing over the wire'''
211 return "%s %s %s %s %s" % (one, two, three, four, five)
212 return "%s %s %s %s %s" % (one, two, three, four, five)
212
213
213 class locallegacypeer(localpeer):
214 class locallegacypeer(localpeer):
214 '''peer extension which implements legacy methods too; used for tests with
215 '''peer extension which implements legacy methods too; used for tests with
215 restricted capabilities'''
216 restricted capabilities'''
216
217
217 def __init__(self, repo):
218 def __init__(self, repo):
218 localpeer.__init__(self, repo, caps=legacycaps)
219 localpeer.__init__(self, repo, caps=legacycaps)
219
220
220 def branches(self, nodes):
221 def branches(self, nodes):
221 return self._repo.branches(nodes)
222 return self._repo.branches(nodes)
222
223
223 def between(self, pairs):
224 def between(self, pairs):
224 return self._repo.between(pairs)
225 return self._repo.between(pairs)
225
226
226 def changegroup(self, basenodes, source):
227 def changegroup(self, basenodes, source):
227 return changegroup.changegroup(self._repo, basenodes, source)
228 return changegroup.changegroup(self._repo, basenodes, source)
228
229
229 def changegroupsubset(self, bases, heads, source):
230 def changegroupsubset(self, bases, heads, source):
230 return changegroup.changegroupsubset(self._repo, bases, heads, source)
231 return changegroup.changegroupsubset(self._repo, bases, heads, source)
231
232
232 class localrepository(object):
233 class localrepository(object):
233
234
234 supportedformats = set(('revlogv1', 'generaldelta', 'treemanifest',
235 supportedformats = set(('revlogv1', 'generaldelta', 'treemanifest',
235 'manifestv2'))
236 'manifestv2'))
236 _basesupported = supportedformats | set(('store', 'fncache', 'shared',
237 _basesupported = supportedformats | set(('store', 'fncache', 'shared',
237 'dotencode'))
238 'dotencode'))
238 openerreqs = set(('revlogv1', 'generaldelta', 'treemanifest', 'manifestv2'))
239 openerreqs = set(('revlogv1', 'generaldelta', 'treemanifest', 'manifestv2'))
239 filtername = None
240 filtername = None
240
241
241 # a list of (ui, featureset) functions.
242 # a list of (ui, featureset) functions.
242 # only functions defined in module of enabled extensions are invoked
243 # only functions defined in module of enabled extensions are invoked
243 featuresetupfuncs = set()
244 featuresetupfuncs = set()
244
245
245 def __init__(self, baseui, path=None, create=False):
246 def __init__(self, baseui, path=None, create=False):
246 self.requirements = set()
247 self.requirements = set()
247 self.wvfs = scmutil.vfs(path, expandpath=True, realpath=True)
248 self.wvfs = scmutil.vfs(path, expandpath=True, realpath=True)
248 self.wopener = self.wvfs
249 self.wopener = self.wvfs
249 self.root = self.wvfs.base
250 self.root = self.wvfs.base
250 self.path = self.wvfs.join(".hg")
251 self.path = self.wvfs.join(".hg")
251 self.origroot = path
252 self.origroot = path
252 self.auditor = pathutil.pathauditor(self.root, self._checknested)
253 self.auditor = pathutil.pathauditor(self.root, self._checknested)
253 self.nofsauditor = pathutil.pathauditor(self.root, self._checknested,
254 self.nofsauditor = pathutil.pathauditor(self.root, self._checknested,
254 realfs=False)
255 realfs=False)
255 self.vfs = scmutil.vfs(self.path)
256 self.vfs = scmutil.vfs(self.path)
256 self.opener = self.vfs
257 self.opener = self.vfs
257 self.baseui = baseui
258 self.baseui = baseui
258 self.ui = baseui.copy()
259 self.ui = baseui.copy()
259 self.ui.copy = baseui.copy # prevent copying repo configuration
260 self.ui.copy = baseui.copy # prevent copying repo configuration
260 # A list of callback to shape the phase if no data were found.
261 # A list of callback to shape the phase if no data were found.
261 # Callback are in the form: func(repo, roots) --> processed root.
262 # Callback are in the form: func(repo, roots) --> processed root.
262 # This list it to be filled by extension during repo setup
263 # This list it to be filled by extension during repo setup
263 self._phasedefaults = []
264 self._phasedefaults = []
264 try:
265 try:
265 self.ui.readconfig(self.join("hgrc"), self.root)
266 self.ui.readconfig(self.join("hgrc"), self.root)
266 extensions.loadall(self.ui)
267 extensions.loadall(self.ui)
267 except IOError:
268 except IOError:
268 pass
269 pass
269
270
270 if self.featuresetupfuncs:
271 if self.featuresetupfuncs:
271 self.supported = set(self._basesupported) # use private copy
272 self.supported = set(self._basesupported) # use private copy
272 extmods = set(m.__name__ for n, m
273 extmods = set(m.__name__ for n, m
273 in extensions.extensions(self.ui))
274 in extensions.extensions(self.ui))
274 for setupfunc in self.featuresetupfuncs:
275 for setupfunc in self.featuresetupfuncs:
275 if setupfunc.__module__ in extmods:
276 if setupfunc.__module__ in extmods:
276 setupfunc(self.ui, self.supported)
277 setupfunc(self.ui, self.supported)
277 else:
278 else:
278 self.supported = self._basesupported
279 self.supported = self._basesupported
279
280
280 if not self.vfs.isdir():
281 if not self.vfs.isdir():
281 if create:
282 if create:
282 self.requirements = newreporequirements(self)
283 self.requirements = newreporequirements(self)
283
284
284 if not self.wvfs.exists():
285 if not self.wvfs.exists():
285 self.wvfs.makedirs()
286 self.wvfs.makedirs()
286 self.vfs.makedir(notindexed=True)
287 self.vfs.makedir(notindexed=True)
287
288
288 if 'store' in self.requirements:
289 if 'store' in self.requirements:
289 self.vfs.mkdir("store")
290 self.vfs.mkdir("store")
290
291
291 # create an invalid changelog
292 # create an invalid changelog
292 self.vfs.append(
293 self.vfs.append(
293 "00changelog.i",
294 "00changelog.i",
294 '\0\0\0\2' # represents revlogv2
295 '\0\0\0\2' # represents revlogv2
295 ' dummy changelog to prevent using the old repo layout'
296 ' dummy changelog to prevent using the old repo layout'
296 )
297 )
297 else:
298 else:
298 raise error.RepoError(_("repository %s not found") % path)
299 raise error.RepoError(_("repository %s not found") % path)
299 elif create:
300 elif create:
300 raise error.RepoError(_("repository %s already exists") % path)
301 raise error.RepoError(_("repository %s already exists") % path)
301 else:
302 else:
302 try:
303 try:
303 self.requirements = scmutil.readrequires(
304 self.requirements = scmutil.readrequires(
304 self.vfs, self.supported)
305 self.vfs, self.supported)
305 except IOError as inst:
306 except IOError as inst:
306 if inst.errno != errno.ENOENT:
307 if inst.errno != errno.ENOENT:
307 raise
308 raise
308
309
309 self.sharedpath = self.path
310 self.sharedpath = self.path
310 try:
311 try:
311 vfs = scmutil.vfs(self.vfs.read("sharedpath").rstrip('\n'),
312 vfs = scmutil.vfs(self.vfs.read("sharedpath").rstrip('\n'),
312 realpath=True)
313 realpath=True)
313 s = vfs.base
314 s = vfs.base
314 if not vfs.exists():
315 if not vfs.exists():
315 raise error.RepoError(
316 raise error.RepoError(
316 _('.hg/sharedpath points to nonexistent directory %s') % s)
317 _('.hg/sharedpath points to nonexistent directory %s') % s)
317 self.sharedpath = s
318 self.sharedpath = s
318 except IOError as inst:
319 except IOError as inst:
319 if inst.errno != errno.ENOENT:
320 if inst.errno != errno.ENOENT:
320 raise
321 raise
321
322
322 self.store = store.store(
323 self.store = store.store(
323 self.requirements, self.sharedpath, scmutil.vfs)
324 self.requirements, self.sharedpath, scmutil.vfs)
324 self.spath = self.store.path
325 self.spath = self.store.path
325 self.svfs = self.store.vfs
326 self.svfs = self.store.vfs
326 self.sjoin = self.store.join
327 self.sjoin = self.store.join
327 self.vfs.createmode = self.store.createmode
328 self.vfs.createmode = self.store.createmode
328 self._applyopenerreqs()
329 self._applyopenerreqs()
329 if create:
330 if create:
330 self._writerequirements()
331 self._writerequirements()
331
332
332 self._dirstatevalidatewarned = False
333 self._dirstatevalidatewarned = False
333
334
334 self._branchcaches = {}
335 self._branchcaches = {}
335 self._revbranchcache = None
336 self._revbranchcache = None
336 self.filterpats = {}
337 self.filterpats = {}
337 self._datafilters = {}
338 self._datafilters = {}
338 self._transref = self._lockref = self._wlockref = None
339 self._transref = self._lockref = self._wlockref = None
339
340
340 # A cache for various files under .hg/ that tracks file changes,
341 # A cache for various files under .hg/ that tracks file changes,
341 # (used by the filecache decorator)
342 # (used by the filecache decorator)
342 #
343 #
343 # Maps a property name to its util.filecacheentry
344 # Maps a property name to its util.filecacheentry
344 self._filecache = {}
345 self._filecache = {}
345
346
346 # hold sets of revision to be filtered
347 # hold sets of revision to be filtered
347 # should be cleared when something might have changed the filter value:
348 # should be cleared when something might have changed the filter value:
348 # - new changesets,
349 # - new changesets,
349 # - phase change,
350 # - phase change,
350 # - new obsolescence marker,
351 # - new obsolescence marker,
351 # - working directory parent change,
352 # - working directory parent change,
352 # - bookmark changes
353 # - bookmark changes
353 self.filteredrevcache = {}
354 self.filteredrevcache = {}
354
355
355 # generic mapping between names and nodes
356 # generic mapping between names and nodes
356 self.names = namespaces.namespaces()
357 self.names = namespaces.namespaces()
357
358
358 def close(self):
359 def close(self):
359 self._writecaches()
360 self._writecaches()
360
361
361 def _writecaches(self):
362 def _writecaches(self):
362 if self._revbranchcache:
363 if self._revbranchcache:
363 self._revbranchcache.write()
364 self._revbranchcache.write()
364
365
365 def _restrictcapabilities(self, caps):
366 def _restrictcapabilities(self, caps):
366 if self.ui.configbool('experimental', 'bundle2-advertise', True):
367 if self.ui.configbool('experimental', 'bundle2-advertise', True):
367 caps = set(caps)
368 caps = set(caps)
368 capsblob = bundle2.encodecaps(bundle2.getrepocaps(self))
369 capsblob = bundle2.encodecaps(bundle2.getrepocaps(self))
369 caps.add('bundle2=' + urllib.quote(capsblob))
370 caps.add('bundle2=' + urlreq.quote(capsblob))
370 return caps
371 return caps
371
372
372 def _applyopenerreqs(self):
373 def _applyopenerreqs(self):
373 self.svfs.options = dict((r, 1) for r in self.requirements
374 self.svfs.options = dict((r, 1) for r in self.requirements
374 if r in self.openerreqs)
375 if r in self.openerreqs)
375 # experimental config: format.chunkcachesize
376 # experimental config: format.chunkcachesize
376 chunkcachesize = self.ui.configint('format', 'chunkcachesize')
377 chunkcachesize = self.ui.configint('format', 'chunkcachesize')
377 if chunkcachesize is not None:
378 if chunkcachesize is not None:
378 self.svfs.options['chunkcachesize'] = chunkcachesize
379 self.svfs.options['chunkcachesize'] = chunkcachesize
379 # experimental config: format.maxchainlen
380 # experimental config: format.maxchainlen
380 maxchainlen = self.ui.configint('format', 'maxchainlen')
381 maxchainlen = self.ui.configint('format', 'maxchainlen')
381 if maxchainlen is not None:
382 if maxchainlen is not None:
382 self.svfs.options['maxchainlen'] = maxchainlen
383 self.svfs.options['maxchainlen'] = maxchainlen
383 # experimental config: format.manifestcachesize
384 # experimental config: format.manifestcachesize
384 manifestcachesize = self.ui.configint('format', 'manifestcachesize')
385 manifestcachesize = self.ui.configint('format', 'manifestcachesize')
385 if manifestcachesize is not None:
386 if manifestcachesize is not None:
386 self.svfs.options['manifestcachesize'] = manifestcachesize
387 self.svfs.options['manifestcachesize'] = manifestcachesize
387 # experimental config: format.aggressivemergedeltas
388 # experimental config: format.aggressivemergedeltas
388 aggressivemergedeltas = self.ui.configbool('format',
389 aggressivemergedeltas = self.ui.configbool('format',
389 'aggressivemergedeltas', False)
390 'aggressivemergedeltas', False)
390 self.svfs.options['aggressivemergedeltas'] = aggressivemergedeltas
391 self.svfs.options['aggressivemergedeltas'] = aggressivemergedeltas
391 self.svfs.options['lazydeltabase'] = not scmutil.gddeltaconfig(self.ui)
392 self.svfs.options['lazydeltabase'] = not scmutil.gddeltaconfig(self.ui)
392
393
393 def _writerequirements(self):
394 def _writerequirements(self):
394 scmutil.writerequires(self.vfs, self.requirements)
395 scmutil.writerequires(self.vfs, self.requirements)
395
396
396 def _checknested(self, path):
397 def _checknested(self, path):
397 """Determine if path is a legal nested repository."""
398 """Determine if path is a legal nested repository."""
398 if not path.startswith(self.root):
399 if not path.startswith(self.root):
399 return False
400 return False
400 subpath = path[len(self.root) + 1:]
401 subpath = path[len(self.root) + 1:]
401 normsubpath = util.pconvert(subpath)
402 normsubpath = util.pconvert(subpath)
402
403
403 # XXX: Checking against the current working copy is wrong in
404 # XXX: Checking against the current working copy is wrong in
404 # the sense that it can reject things like
405 # the sense that it can reject things like
405 #
406 #
406 # $ hg cat -r 10 sub/x.txt
407 # $ hg cat -r 10 sub/x.txt
407 #
408 #
408 # if sub/ is no longer a subrepository in the working copy
409 # if sub/ is no longer a subrepository in the working copy
409 # parent revision.
410 # parent revision.
410 #
411 #
411 # However, it can of course also allow things that would have
412 # However, it can of course also allow things that would have
412 # been rejected before, such as the above cat command if sub/
413 # been rejected before, such as the above cat command if sub/
413 # is a subrepository now, but was a normal directory before.
414 # is a subrepository now, but was a normal directory before.
414 # The old path auditor would have rejected by mistake since it
415 # The old path auditor would have rejected by mistake since it
415 # panics when it sees sub/.hg/.
416 # panics when it sees sub/.hg/.
416 #
417 #
417 # All in all, checking against the working copy seems sensible
418 # All in all, checking against the working copy seems sensible
418 # since we want to prevent access to nested repositories on
419 # since we want to prevent access to nested repositories on
419 # the filesystem *now*.
420 # the filesystem *now*.
420 ctx = self[None]
421 ctx = self[None]
421 parts = util.splitpath(subpath)
422 parts = util.splitpath(subpath)
422 while parts:
423 while parts:
423 prefix = '/'.join(parts)
424 prefix = '/'.join(parts)
424 if prefix in ctx.substate:
425 if prefix in ctx.substate:
425 if prefix == normsubpath:
426 if prefix == normsubpath:
426 return True
427 return True
427 else:
428 else:
428 sub = ctx.sub(prefix)
429 sub = ctx.sub(prefix)
429 return sub.checknested(subpath[len(prefix) + 1:])
430 return sub.checknested(subpath[len(prefix) + 1:])
430 else:
431 else:
431 parts.pop()
432 parts.pop()
432 return False
433 return False
433
434
434 def peer(self):
435 def peer(self):
435 return localpeer(self) # not cached to avoid reference cycle
436 return localpeer(self) # not cached to avoid reference cycle
436
437
437 def unfiltered(self):
438 def unfiltered(self):
438 """Return unfiltered version of the repository
439 """Return unfiltered version of the repository
439
440
440 Intended to be overwritten by filtered repo."""
441 Intended to be overwritten by filtered repo."""
441 return self
442 return self
442
443
443 def filtered(self, name):
444 def filtered(self, name):
444 """Return a filtered version of a repository"""
445 """Return a filtered version of a repository"""
445 # build a new class with the mixin and the current class
446 # build a new class with the mixin and the current class
446 # (possibly subclass of the repo)
447 # (possibly subclass of the repo)
447 class proxycls(repoview.repoview, self.unfiltered().__class__):
448 class proxycls(repoview.repoview, self.unfiltered().__class__):
448 pass
449 pass
449 return proxycls(self, name)
450 return proxycls(self, name)
450
451
451 @repofilecache('bookmarks', 'bookmarks.current')
452 @repofilecache('bookmarks', 'bookmarks.current')
452 def _bookmarks(self):
453 def _bookmarks(self):
453 return bookmarks.bmstore(self)
454 return bookmarks.bmstore(self)
454
455
455 @property
456 @property
456 def _activebookmark(self):
457 def _activebookmark(self):
457 return self._bookmarks.active
458 return self._bookmarks.active
458
459
459 def bookmarkheads(self, bookmark):
460 def bookmarkheads(self, bookmark):
460 name = bookmark.split('@', 1)[0]
461 name = bookmark.split('@', 1)[0]
461 heads = []
462 heads = []
462 for mark, n in self._bookmarks.iteritems():
463 for mark, n in self._bookmarks.iteritems():
463 if mark.split('@', 1)[0] == name:
464 if mark.split('@', 1)[0] == name:
464 heads.append(n)
465 heads.append(n)
465 return heads
466 return heads
466
467
467 # _phaserevs and _phasesets depend on changelog. what we need is to
468 # _phaserevs and _phasesets depend on changelog. what we need is to
468 # call _phasecache.invalidate() if '00changelog.i' was changed, but it
469 # call _phasecache.invalidate() if '00changelog.i' was changed, but it
469 # can't be easily expressed in filecache mechanism.
470 # can't be easily expressed in filecache mechanism.
470 @storecache('phaseroots', '00changelog.i')
471 @storecache('phaseroots', '00changelog.i')
471 def _phasecache(self):
472 def _phasecache(self):
472 return phases.phasecache(self, self._phasedefaults)
473 return phases.phasecache(self, self._phasedefaults)
473
474
474 @storecache('obsstore')
475 @storecache('obsstore')
475 def obsstore(self):
476 def obsstore(self):
476 # read default format for new obsstore.
477 # read default format for new obsstore.
477 # developer config: format.obsstore-version
478 # developer config: format.obsstore-version
478 defaultformat = self.ui.configint('format', 'obsstore-version', None)
479 defaultformat = self.ui.configint('format', 'obsstore-version', None)
479 # rely on obsstore class default when possible.
480 # rely on obsstore class default when possible.
480 kwargs = {}
481 kwargs = {}
481 if defaultformat is not None:
482 if defaultformat is not None:
482 kwargs['defaultformat'] = defaultformat
483 kwargs['defaultformat'] = defaultformat
483 readonly = not obsolete.isenabled(self, obsolete.createmarkersopt)
484 readonly = not obsolete.isenabled(self, obsolete.createmarkersopt)
484 store = obsolete.obsstore(self.svfs, readonly=readonly,
485 store = obsolete.obsstore(self.svfs, readonly=readonly,
485 **kwargs)
486 **kwargs)
486 if store and readonly:
487 if store and readonly:
487 self.ui.warn(
488 self.ui.warn(
488 _('obsolete feature not enabled but %i markers found!\n')
489 _('obsolete feature not enabled but %i markers found!\n')
489 % len(list(store)))
490 % len(list(store)))
490 return store
491 return store
491
492
492 @storecache('00changelog.i')
493 @storecache('00changelog.i')
493 def changelog(self):
494 def changelog(self):
494 c = changelog.changelog(self.svfs)
495 c = changelog.changelog(self.svfs)
495 if 'HG_PENDING' in os.environ:
496 if 'HG_PENDING' in os.environ:
496 p = os.environ['HG_PENDING']
497 p = os.environ['HG_PENDING']
497 if p.startswith(self.root):
498 if p.startswith(self.root):
498 c.readpending('00changelog.i.a')
499 c.readpending('00changelog.i.a')
499 return c
500 return c
500
501
501 @storecache('00manifest.i')
502 @storecache('00manifest.i')
502 def manifest(self):
503 def manifest(self):
503 return manifest.manifest(self.svfs)
504 return manifest.manifest(self.svfs)
504
505
505 def dirlog(self, dir):
506 def dirlog(self, dir):
506 return self.manifest.dirlog(dir)
507 return self.manifest.dirlog(dir)
507
508
508 @repofilecache('dirstate')
509 @repofilecache('dirstate')
509 def dirstate(self):
510 def dirstate(self):
510 return dirstate.dirstate(self.vfs, self.ui, self.root,
511 return dirstate.dirstate(self.vfs, self.ui, self.root,
511 self._dirstatevalidate)
512 self._dirstatevalidate)
512
513
513 def _dirstatevalidate(self, node):
514 def _dirstatevalidate(self, node):
514 try:
515 try:
515 self.changelog.rev(node)
516 self.changelog.rev(node)
516 return node
517 return node
517 except error.LookupError:
518 except error.LookupError:
518 if not self._dirstatevalidatewarned:
519 if not self._dirstatevalidatewarned:
519 self._dirstatevalidatewarned = True
520 self._dirstatevalidatewarned = True
520 self.ui.warn(_("warning: ignoring unknown"
521 self.ui.warn(_("warning: ignoring unknown"
521 " working parent %s!\n") % short(node))
522 " working parent %s!\n") % short(node))
522 return nullid
523 return nullid
523
524
524 def __getitem__(self, changeid):
525 def __getitem__(self, changeid):
525 if changeid is None or changeid == wdirrev:
526 if changeid is None or changeid == wdirrev:
526 return context.workingctx(self)
527 return context.workingctx(self)
527 if isinstance(changeid, slice):
528 if isinstance(changeid, slice):
528 return [context.changectx(self, i)
529 return [context.changectx(self, i)
529 for i in xrange(*changeid.indices(len(self)))
530 for i in xrange(*changeid.indices(len(self)))
530 if i not in self.changelog.filteredrevs]
531 if i not in self.changelog.filteredrevs]
531 return context.changectx(self, changeid)
532 return context.changectx(self, changeid)
532
533
533 def __contains__(self, changeid):
534 def __contains__(self, changeid):
534 try:
535 try:
535 self[changeid]
536 self[changeid]
536 return True
537 return True
537 except error.RepoLookupError:
538 except error.RepoLookupError:
538 return False
539 return False
539
540
540 def __nonzero__(self):
541 def __nonzero__(self):
541 return True
542 return True
542
543
543 def __len__(self):
544 def __len__(self):
544 return len(self.changelog)
545 return len(self.changelog)
545
546
546 def __iter__(self):
547 def __iter__(self):
547 return iter(self.changelog)
548 return iter(self.changelog)
548
549
549 def revs(self, expr, *args):
550 def revs(self, expr, *args):
550 '''Find revisions matching a revset.
551 '''Find revisions matching a revset.
551
552
552 The revset is specified as a string ``expr`` that may contain
553 The revset is specified as a string ``expr`` that may contain
553 %-formatting to escape certain types. See ``revset.formatspec``.
554 %-formatting to escape certain types. See ``revset.formatspec``.
554
555
555 Return a revset.abstractsmartset, which is a list-like interface
556 Return a revset.abstractsmartset, which is a list-like interface
556 that contains integer revisions.
557 that contains integer revisions.
557 '''
558 '''
558 expr = revset.formatspec(expr, *args)
559 expr = revset.formatspec(expr, *args)
559 m = revset.match(None, expr)
560 m = revset.match(None, expr)
560 return m(self)
561 return m(self)
561
562
562 def set(self, expr, *args):
563 def set(self, expr, *args):
563 '''Find revisions matching a revset and emit changectx instances.
564 '''Find revisions matching a revset and emit changectx instances.
564
565
565 This is a convenience wrapper around ``revs()`` that iterates the
566 This is a convenience wrapper around ``revs()`` that iterates the
566 result and is a generator of changectx instances.
567 result and is a generator of changectx instances.
567 '''
568 '''
568 for r in self.revs(expr, *args):
569 for r in self.revs(expr, *args):
569 yield self[r]
570 yield self[r]
570
571
571 def url(self):
572 def url(self):
572 return 'file:' + self.root
573 return 'file:' + self.root
573
574
574 def hook(self, name, throw=False, **args):
575 def hook(self, name, throw=False, **args):
575 """Call a hook, passing this repo instance.
576 """Call a hook, passing this repo instance.
576
577
577 This a convenience method to aid invoking hooks. Extensions likely
578 This a convenience method to aid invoking hooks. Extensions likely
578 won't call this unless they have registered a custom hook or are
579 won't call this unless they have registered a custom hook or are
579 replacing code that is expected to call a hook.
580 replacing code that is expected to call a hook.
580 """
581 """
581 return hook.hook(self.ui, self, name, throw, **args)
582 return hook.hook(self.ui, self, name, throw, **args)
582
583
583 @unfilteredmethod
584 @unfilteredmethod
584 def _tag(self, names, node, message, local, user, date, extra=None,
585 def _tag(self, names, node, message, local, user, date, extra=None,
585 editor=False):
586 editor=False):
586 if isinstance(names, str):
587 if isinstance(names, str):
587 names = (names,)
588 names = (names,)
588
589
589 branches = self.branchmap()
590 branches = self.branchmap()
590 for name in names:
591 for name in names:
591 self.hook('pretag', throw=True, node=hex(node), tag=name,
592 self.hook('pretag', throw=True, node=hex(node), tag=name,
592 local=local)
593 local=local)
593 if name in branches:
594 if name in branches:
594 self.ui.warn(_("warning: tag %s conflicts with existing"
595 self.ui.warn(_("warning: tag %s conflicts with existing"
595 " branch name\n") % name)
596 " branch name\n") % name)
596
597
597 def writetags(fp, names, munge, prevtags):
598 def writetags(fp, names, munge, prevtags):
598 fp.seek(0, 2)
599 fp.seek(0, 2)
599 if prevtags and prevtags[-1] != '\n':
600 if prevtags and prevtags[-1] != '\n':
600 fp.write('\n')
601 fp.write('\n')
601 for name in names:
602 for name in names:
602 if munge:
603 if munge:
603 m = munge(name)
604 m = munge(name)
604 else:
605 else:
605 m = name
606 m = name
606
607
607 if (self._tagscache.tagtypes and
608 if (self._tagscache.tagtypes and
608 name in self._tagscache.tagtypes):
609 name in self._tagscache.tagtypes):
609 old = self.tags().get(name, nullid)
610 old = self.tags().get(name, nullid)
610 fp.write('%s %s\n' % (hex(old), m))
611 fp.write('%s %s\n' % (hex(old), m))
611 fp.write('%s %s\n' % (hex(node), m))
612 fp.write('%s %s\n' % (hex(node), m))
612 fp.close()
613 fp.close()
613
614
614 prevtags = ''
615 prevtags = ''
615 if local:
616 if local:
616 try:
617 try:
617 fp = self.vfs('localtags', 'r+')
618 fp = self.vfs('localtags', 'r+')
618 except IOError:
619 except IOError:
619 fp = self.vfs('localtags', 'a')
620 fp = self.vfs('localtags', 'a')
620 else:
621 else:
621 prevtags = fp.read()
622 prevtags = fp.read()
622
623
623 # local tags are stored in the current charset
624 # local tags are stored in the current charset
624 writetags(fp, names, None, prevtags)
625 writetags(fp, names, None, prevtags)
625 for name in names:
626 for name in names:
626 self.hook('tag', node=hex(node), tag=name, local=local)
627 self.hook('tag', node=hex(node), tag=name, local=local)
627 return
628 return
628
629
629 try:
630 try:
630 fp = self.wfile('.hgtags', 'rb+')
631 fp = self.wfile('.hgtags', 'rb+')
631 except IOError as e:
632 except IOError as e:
632 if e.errno != errno.ENOENT:
633 if e.errno != errno.ENOENT:
633 raise
634 raise
634 fp = self.wfile('.hgtags', 'ab')
635 fp = self.wfile('.hgtags', 'ab')
635 else:
636 else:
636 prevtags = fp.read()
637 prevtags = fp.read()
637
638
638 # committed tags are stored in UTF-8
639 # committed tags are stored in UTF-8
639 writetags(fp, names, encoding.fromlocal, prevtags)
640 writetags(fp, names, encoding.fromlocal, prevtags)
640
641
641 fp.close()
642 fp.close()
642
643
643 self.invalidatecaches()
644 self.invalidatecaches()
644
645
645 if '.hgtags' not in self.dirstate:
646 if '.hgtags' not in self.dirstate:
646 self[None].add(['.hgtags'])
647 self[None].add(['.hgtags'])
647
648
648 m = matchmod.exact(self.root, '', ['.hgtags'])
649 m = matchmod.exact(self.root, '', ['.hgtags'])
649 tagnode = self.commit(message, user, date, extra=extra, match=m,
650 tagnode = self.commit(message, user, date, extra=extra, match=m,
650 editor=editor)
651 editor=editor)
651
652
652 for name in names:
653 for name in names:
653 self.hook('tag', node=hex(node), tag=name, local=local)
654 self.hook('tag', node=hex(node), tag=name, local=local)
654
655
655 return tagnode
656 return tagnode
656
657
657 def tag(self, names, node, message, local, user, date, editor=False):
658 def tag(self, names, node, message, local, user, date, editor=False):
658 '''tag a revision with one or more symbolic names.
659 '''tag a revision with one or more symbolic names.
659
660
660 names is a list of strings or, when adding a single tag, names may be a
661 names is a list of strings or, when adding a single tag, names may be a
661 string.
662 string.
662
663
663 if local is True, the tags are stored in a per-repository file.
664 if local is True, the tags are stored in a per-repository file.
664 otherwise, they are stored in the .hgtags file, and a new
665 otherwise, they are stored in the .hgtags file, and a new
665 changeset is committed with the change.
666 changeset is committed with the change.
666
667
667 keyword arguments:
668 keyword arguments:
668
669
669 local: whether to store tags in non-version-controlled file
670 local: whether to store tags in non-version-controlled file
670 (default False)
671 (default False)
671
672
672 message: commit message to use if committing
673 message: commit message to use if committing
673
674
674 user: name of user to use if committing
675 user: name of user to use if committing
675
676
676 date: date tuple to use if committing'''
677 date: date tuple to use if committing'''
677
678
678 if not local:
679 if not local:
679 m = matchmod.exact(self.root, '', ['.hgtags'])
680 m = matchmod.exact(self.root, '', ['.hgtags'])
680 if any(self.status(match=m, unknown=True, ignored=True)):
681 if any(self.status(match=m, unknown=True, ignored=True)):
681 raise error.Abort(_('working copy of .hgtags is changed'),
682 raise error.Abort(_('working copy of .hgtags is changed'),
682 hint=_('please commit .hgtags manually'))
683 hint=_('please commit .hgtags manually'))
683
684
684 self.tags() # instantiate the cache
685 self.tags() # instantiate the cache
685 self._tag(names, node, message, local, user, date, editor=editor)
686 self._tag(names, node, message, local, user, date, editor=editor)
686
687
687 @filteredpropertycache
688 @filteredpropertycache
688 def _tagscache(self):
689 def _tagscache(self):
689 '''Returns a tagscache object that contains various tags related
690 '''Returns a tagscache object that contains various tags related
690 caches.'''
691 caches.'''
691
692
692 # This simplifies its cache management by having one decorated
693 # This simplifies its cache management by having one decorated
693 # function (this one) and the rest simply fetch things from it.
694 # function (this one) and the rest simply fetch things from it.
694 class tagscache(object):
695 class tagscache(object):
695 def __init__(self):
696 def __init__(self):
696 # These two define the set of tags for this repository. tags
697 # These two define the set of tags for this repository. tags
697 # maps tag name to node; tagtypes maps tag name to 'global' or
698 # maps tag name to node; tagtypes maps tag name to 'global' or
698 # 'local'. (Global tags are defined by .hgtags across all
699 # 'local'. (Global tags are defined by .hgtags across all
699 # heads, and local tags are defined in .hg/localtags.)
700 # heads, and local tags are defined in .hg/localtags.)
700 # They constitute the in-memory cache of tags.
701 # They constitute the in-memory cache of tags.
701 self.tags = self.tagtypes = None
702 self.tags = self.tagtypes = None
702
703
703 self.nodetagscache = self.tagslist = None
704 self.nodetagscache = self.tagslist = None
704
705
705 cache = tagscache()
706 cache = tagscache()
706 cache.tags, cache.tagtypes = self._findtags()
707 cache.tags, cache.tagtypes = self._findtags()
707
708
708 return cache
709 return cache
709
710
710 def tags(self):
711 def tags(self):
711 '''return a mapping of tag to node'''
712 '''return a mapping of tag to node'''
712 t = {}
713 t = {}
713 if self.changelog.filteredrevs:
714 if self.changelog.filteredrevs:
714 tags, tt = self._findtags()
715 tags, tt = self._findtags()
715 else:
716 else:
716 tags = self._tagscache.tags
717 tags = self._tagscache.tags
717 for k, v in tags.iteritems():
718 for k, v in tags.iteritems():
718 try:
719 try:
719 # ignore tags to unknown nodes
720 # ignore tags to unknown nodes
720 self.changelog.rev(v)
721 self.changelog.rev(v)
721 t[k] = v
722 t[k] = v
722 except (error.LookupError, ValueError):
723 except (error.LookupError, ValueError):
723 pass
724 pass
724 return t
725 return t
725
726
726 def _findtags(self):
727 def _findtags(self):
727 '''Do the hard work of finding tags. Return a pair of dicts
728 '''Do the hard work of finding tags. Return a pair of dicts
728 (tags, tagtypes) where tags maps tag name to node, and tagtypes
729 (tags, tagtypes) where tags maps tag name to node, and tagtypes
729 maps tag name to a string like \'global\' or \'local\'.
730 maps tag name to a string like \'global\' or \'local\'.
730 Subclasses or extensions are free to add their own tags, but
731 Subclasses or extensions are free to add their own tags, but
731 should be aware that the returned dicts will be retained for the
732 should be aware that the returned dicts will be retained for the
732 duration of the localrepo object.'''
733 duration of the localrepo object.'''
733
734
734 # XXX what tagtype should subclasses/extensions use? Currently
735 # XXX what tagtype should subclasses/extensions use? Currently
735 # mq and bookmarks add tags, but do not set the tagtype at all.
736 # mq and bookmarks add tags, but do not set the tagtype at all.
736 # Should each extension invent its own tag type? Should there
737 # Should each extension invent its own tag type? Should there
737 # be one tagtype for all such "virtual" tags? Or is the status
738 # be one tagtype for all such "virtual" tags? Or is the status
738 # quo fine?
739 # quo fine?
739
740
740 alltags = {} # map tag name to (node, hist)
741 alltags = {} # map tag name to (node, hist)
741 tagtypes = {}
742 tagtypes = {}
742
743
743 tagsmod.findglobaltags(self.ui, self, alltags, tagtypes)
744 tagsmod.findglobaltags(self.ui, self, alltags, tagtypes)
744 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
745 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
745
746
746 # Build the return dicts. Have to re-encode tag names because
747 # Build the return dicts. Have to re-encode tag names because
747 # the tags module always uses UTF-8 (in order not to lose info
748 # the tags module always uses UTF-8 (in order not to lose info
748 # writing to the cache), but the rest of Mercurial wants them in
749 # writing to the cache), but the rest of Mercurial wants them in
749 # local encoding.
750 # local encoding.
750 tags = {}
751 tags = {}
751 for (name, (node, hist)) in alltags.iteritems():
752 for (name, (node, hist)) in alltags.iteritems():
752 if node != nullid:
753 if node != nullid:
753 tags[encoding.tolocal(name)] = node
754 tags[encoding.tolocal(name)] = node
754 tags['tip'] = self.changelog.tip()
755 tags['tip'] = self.changelog.tip()
755 tagtypes = dict([(encoding.tolocal(name), value)
756 tagtypes = dict([(encoding.tolocal(name), value)
756 for (name, value) in tagtypes.iteritems()])
757 for (name, value) in tagtypes.iteritems()])
757 return (tags, tagtypes)
758 return (tags, tagtypes)
758
759
759 def tagtype(self, tagname):
760 def tagtype(self, tagname):
760 '''
761 '''
761 return the type of the given tag. result can be:
762 return the type of the given tag. result can be:
762
763
763 'local' : a local tag
764 'local' : a local tag
764 'global' : a global tag
765 'global' : a global tag
765 None : tag does not exist
766 None : tag does not exist
766 '''
767 '''
767
768
768 return self._tagscache.tagtypes.get(tagname)
769 return self._tagscache.tagtypes.get(tagname)
769
770
770 def tagslist(self):
771 def tagslist(self):
771 '''return a list of tags ordered by revision'''
772 '''return a list of tags ordered by revision'''
772 if not self._tagscache.tagslist:
773 if not self._tagscache.tagslist:
773 l = []
774 l = []
774 for t, n in self.tags().iteritems():
775 for t, n in self.tags().iteritems():
775 l.append((self.changelog.rev(n), t, n))
776 l.append((self.changelog.rev(n), t, n))
776 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
777 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
777
778
778 return self._tagscache.tagslist
779 return self._tagscache.tagslist
779
780
780 def nodetags(self, node):
781 def nodetags(self, node):
781 '''return the tags associated with a node'''
782 '''return the tags associated with a node'''
782 if not self._tagscache.nodetagscache:
783 if not self._tagscache.nodetagscache:
783 nodetagscache = {}
784 nodetagscache = {}
784 for t, n in self._tagscache.tags.iteritems():
785 for t, n in self._tagscache.tags.iteritems():
785 nodetagscache.setdefault(n, []).append(t)
786 nodetagscache.setdefault(n, []).append(t)
786 for tags in nodetagscache.itervalues():
787 for tags in nodetagscache.itervalues():
787 tags.sort()
788 tags.sort()
788 self._tagscache.nodetagscache = nodetagscache
789 self._tagscache.nodetagscache = nodetagscache
789 return self._tagscache.nodetagscache.get(node, [])
790 return self._tagscache.nodetagscache.get(node, [])
790
791
791 def nodebookmarks(self, node):
792 def nodebookmarks(self, node):
792 """return the list of bookmarks pointing to the specified node"""
793 """return the list of bookmarks pointing to the specified node"""
793 marks = []
794 marks = []
794 for bookmark, n in self._bookmarks.iteritems():
795 for bookmark, n in self._bookmarks.iteritems():
795 if n == node:
796 if n == node:
796 marks.append(bookmark)
797 marks.append(bookmark)
797 return sorted(marks)
798 return sorted(marks)
798
799
799 def branchmap(self):
800 def branchmap(self):
800 '''returns a dictionary {branch: [branchheads]} with branchheads
801 '''returns a dictionary {branch: [branchheads]} with branchheads
801 ordered by increasing revision number'''
802 ordered by increasing revision number'''
802 branchmap.updatecache(self)
803 branchmap.updatecache(self)
803 return self._branchcaches[self.filtername]
804 return self._branchcaches[self.filtername]
804
805
805 @unfilteredmethod
806 @unfilteredmethod
806 def revbranchcache(self):
807 def revbranchcache(self):
807 if not self._revbranchcache:
808 if not self._revbranchcache:
808 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
809 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
809 return self._revbranchcache
810 return self._revbranchcache
810
811
811 def branchtip(self, branch, ignoremissing=False):
812 def branchtip(self, branch, ignoremissing=False):
812 '''return the tip node for a given branch
813 '''return the tip node for a given branch
813
814
814 If ignoremissing is True, then this method will not raise an error.
815 If ignoremissing is True, then this method will not raise an error.
815 This is helpful for callers that only expect None for a missing branch
816 This is helpful for callers that only expect None for a missing branch
816 (e.g. namespace).
817 (e.g. namespace).
817
818
818 '''
819 '''
819 try:
820 try:
820 return self.branchmap().branchtip(branch)
821 return self.branchmap().branchtip(branch)
821 except KeyError:
822 except KeyError:
822 if not ignoremissing:
823 if not ignoremissing:
823 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
824 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
824 else:
825 else:
825 pass
826 pass
826
827
827 def lookup(self, key):
828 def lookup(self, key):
828 return self[key].node()
829 return self[key].node()
829
830
830 def lookupbranch(self, key, remote=None):
831 def lookupbranch(self, key, remote=None):
831 repo = remote or self
832 repo = remote or self
832 if key in repo.branchmap():
833 if key in repo.branchmap():
833 return key
834 return key
834
835
835 repo = (remote and remote.local()) and remote or self
836 repo = (remote and remote.local()) and remote or self
836 return repo[key].branch()
837 return repo[key].branch()
837
838
838 def known(self, nodes):
839 def known(self, nodes):
839 cl = self.changelog
840 cl = self.changelog
840 nm = cl.nodemap
841 nm = cl.nodemap
841 filtered = cl.filteredrevs
842 filtered = cl.filteredrevs
842 result = []
843 result = []
843 for n in nodes:
844 for n in nodes:
844 r = nm.get(n)
845 r = nm.get(n)
845 resp = not (r is None or r in filtered)
846 resp = not (r is None or r in filtered)
846 result.append(resp)
847 result.append(resp)
847 return result
848 return result
848
849
849 def local(self):
850 def local(self):
850 return self
851 return self
851
852
852 def publishing(self):
853 def publishing(self):
853 # it's safe (and desirable) to trust the publish flag unconditionally
854 # it's safe (and desirable) to trust the publish flag unconditionally
854 # so that we don't finalize changes shared between users via ssh or nfs
855 # so that we don't finalize changes shared between users via ssh or nfs
855 return self.ui.configbool('phases', 'publish', True, untrusted=True)
856 return self.ui.configbool('phases', 'publish', True, untrusted=True)
856
857
857 def cancopy(self):
858 def cancopy(self):
858 # so statichttprepo's override of local() works
859 # so statichttprepo's override of local() works
859 if not self.local():
860 if not self.local():
860 return False
861 return False
861 if not self.publishing():
862 if not self.publishing():
862 return True
863 return True
863 # if publishing we can't copy if there is filtered content
864 # if publishing we can't copy if there is filtered content
864 return not self.filtered('visible').changelog.filteredrevs
865 return not self.filtered('visible').changelog.filteredrevs
865
866
866 def shared(self):
867 def shared(self):
867 '''the type of shared repository (None if not shared)'''
868 '''the type of shared repository (None if not shared)'''
868 if self.sharedpath != self.path:
869 if self.sharedpath != self.path:
869 return 'store'
870 return 'store'
870 return None
871 return None
871
872
872 def join(self, f, *insidef):
873 def join(self, f, *insidef):
873 return self.vfs.join(os.path.join(f, *insidef))
874 return self.vfs.join(os.path.join(f, *insidef))
874
875
875 def wjoin(self, f, *insidef):
876 def wjoin(self, f, *insidef):
876 return self.vfs.reljoin(self.root, f, *insidef)
877 return self.vfs.reljoin(self.root, f, *insidef)
877
878
878 def file(self, f):
879 def file(self, f):
879 if f[0] == '/':
880 if f[0] == '/':
880 f = f[1:]
881 f = f[1:]
881 return filelog.filelog(self.svfs, f)
882 return filelog.filelog(self.svfs, f)
882
883
883 def parents(self, changeid=None):
884 def parents(self, changeid=None):
884 '''get list of changectxs for parents of changeid'''
885 '''get list of changectxs for parents of changeid'''
885 msg = 'repo.parents() is deprecated, use repo[%r].parents()' % changeid
886 msg = 'repo.parents() is deprecated, use repo[%r].parents()' % changeid
886 self.ui.deprecwarn(msg, '3.7')
887 self.ui.deprecwarn(msg, '3.7')
887 return self[changeid].parents()
888 return self[changeid].parents()
888
889
889 def changectx(self, changeid):
890 def changectx(self, changeid):
890 return self[changeid]
891 return self[changeid]
891
892
892 def setparents(self, p1, p2=nullid):
893 def setparents(self, p1, p2=nullid):
893 self.dirstate.beginparentchange()
894 self.dirstate.beginparentchange()
894 copies = self.dirstate.setparents(p1, p2)
895 copies = self.dirstate.setparents(p1, p2)
895 pctx = self[p1]
896 pctx = self[p1]
896 if copies:
897 if copies:
897 # Adjust copy records, the dirstate cannot do it, it
898 # Adjust copy records, the dirstate cannot do it, it
898 # requires access to parents manifests. Preserve them
899 # requires access to parents manifests. Preserve them
899 # only for entries added to first parent.
900 # only for entries added to first parent.
900 for f in copies:
901 for f in copies:
901 if f not in pctx and copies[f] in pctx:
902 if f not in pctx and copies[f] in pctx:
902 self.dirstate.copy(copies[f], f)
903 self.dirstate.copy(copies[f], f)
903 if p2 == nullid:
904 if p2 == nullid:
904 for f, s in sorted(self.dirstate.copies().items()):
905 for f, s in sorted(self.dirstate.copies().items()):
905 if f not in pctx and s not in pctx:
906 if f not in pctx and s not in pctx:
906 self.dirstate.copy(None, f)
907 self.dirstate.copy(None, f)
907 self.dirstate.endparentchange()
908 self.dirstate.endparentchange()
908
909
909 def filectx(self, path, changeid=None, fileid=None):
910 def filectx(self, path, changeid=None, fileid=None):
910 """changeid can be a changeset revision, node, or tag.
911 """changeid can be a changeset revision, node, or tag.
911 fileid can be a file revision or node."""
912 fileid can be a file revision or node."""
912 return context.filectx(self, path, changeid, fileid)
913 return context.filectx(self, path, changeid, fileid)
913
914
914 def getcwd(self):
915 def getcwd(self):
915 return self.dirstate.getcwd()
916 return self.dirstate.getcwd()
916
917
917 def pathto(self, f, cwd=None):
918 def pathto(self, f, cwd=None):
918 return self.dirstate.pathto(f, cwd)
919 return self.dirstate.pathto(f, cwd)
919
920
920 def wfile(self, f, mode='r'):
921 def wfile(self, f, mode='r'):
921 return self.wvfs(f, mode)
922 return self.wvfs(f, mode)
922
923
923 def _link(self, f):
924 def _link(self, f):
924 return self.wvfs.islink(f)
925 return self.wvfs.islink(f)
925
926
926 def _loadfilter(self, filter):
927 def _loadfilter(self, filter):
927 if filter not in self.filterpats:
928 if filter not in self.filterpats:
928 l = []
929 l = []
929 for pat, cmd in self.ui.configitems(filter):
930 for pat, cmd in self.ui.configitems(filter):
930 if cmd == '!':
931 if cmd == '!':
931 continue
932 continue
932 mf = matchmod.match(self.root, '', [pat])
933 mf = matchmod.match(self.root, '', [pat])
933 fn = None
934 fn = None
934 params = cmd
935 params = cmd
935 for name, filterfn in self._datafilters.iteritems():
936 for name, filterfn in self._datafilters.iteritems():
936 if cmd.startswith(name):
937 if cmd.startswith(name):
937 fn = filterfn
938 fn = filterfn
938 params = cmd[len(name):].lstrip()
939 params = cmd[len(name):].lstrip()
939 break
940 break
940 if not fn:
941 if not fn:
941 fn = lambda s, c, **kwargs: util.filter(s, c)
942 fn = lambda s, c, **kwargs: util.filter(s, c)
942 # Wrap old filters not supporting keyword arguments
943 # Wrap old filters not supporting keyword arguments
943 if not inspect.getargspec(fn)[2]:
944 if not inspect.getargspec(fn)[2]:
944 oldfn = fn
945 oldfn = fn
945 fn = lambda s, c, **kwargs: oldfn(s, c)
946 fn = lambda s, c, **kwargs: oldfn(s, c)
946 l.append((mf, fn, params))
947 l.append((mf, fn, params))
947 self.filterpats[filter] = l
948 self.filterpats[filter] = l
948 return self.filterpats[filter]
949 return self.filterpats[filter]
949
950
950 def _filter(self, filterpats, filename, data):
951 def _filter(self, filterpats, filename, data):
951 for mf, fn, cmd in filterpats:
952 for mf, fn, cmd in filterpats:
952 if mf(filename):
953 if mf(filename):
953 self.ui.debug("filtering %s through %s\n" % (filename, cmd))
954 self.ui.debug("filtering %s through %s\n" % (filename, cmd))
954 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
955 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
955 break
956 break
956
957
957 return data
958 return data
958
959
959 @unfilteredpropertycache
960 @unfilteredpropertycache
960 def _encodefilterpats(self):
961 def _encodefilterpats(self):
961 return self._loadfilter('encode')
962 return self._loadfilter('encode')
962
963
963 @unfilteredpropertycache
964 @unfilteredpropertycache
964 def _decodefilterpats(self):
965 def _decodefilterpats(self):
965 return self._loadfilter('decode')
966 return self._loadfilter('decode')
966
967
967 def adddatafilter(self, name, filter):
968 def adddatafilter(self, name, filter):
968 self._datafilters[name] = filter
969 self._datafilters[name] = filter
969
970
970 def wread(self, filename):
971 def wread(self, filename):
971 if self._link(filename):
972 if self._link(filename):
972 data = self.wvfs.readlink(filename)
973 data = self.wvfs.readlink(filename)
973 else:
974 else:
974 data = self.wvfs.read(filename)
975 data = self.wvfs.read(filename)
975 return self._filter(self._encodefilterpats, filename, data)
976 return self._filter(self._encodefilterpats, filename, data)
976
977
977 def wwrite(self, filename, data, flags, backgroundclose=False):
978 def wwrite(self, filename, data, flags, backgroundclose=False):
978 """write ``data`` into ``filename`` in the working directory
979 """write ``data`` into ``filename`` in the working directory
979
980
980 This returns length of written (maybe decoded) data.
981 This returns length of written (maybe decoded) data.
981 """
982 """
982 data = self._filter(self._decodefilterpats, filename, data)
983 data = self._filter(self._decodefilterpats, filename, data)
983 if 'l' in flags:
984 if 'l' in flags:
984 self.wvfs.symlink(data, filename)
985 self.wvfs.symlink(data, filename)
985 else:
986 else:
986 self.wvfs.write(filename, data, backgroundclose=backgroundclose)
987 self.wvfs.write(filename, data, backgroundclose=backgroundclose)
987 if 'x' in flags:
988 if 'x' in flags:
988 self.wvfs.setflags(filename, False, True)
989 self.wvfs.setflags(filename, False, True)
989 return len(data)
990 return len(data)
990
991
991 def wwritedata(self, filename, data):
992 def wwritedata(self, filename, data):
992 return self._filter(self._decodefilterpats, filename, data)
993 return self._filter(self._decodefilterpats, filename, data)
993
994
994 def currenttransaction(self):
995 def currenttransaction(self):
995 """return the current transaction or None if non exists"""
996 """return the current transaction or None if non exists"""
996 if self._transref:
997 if self._transref:
997 tr = self._transref()
998 tr = self._transref()
998 else:
999 else:
999 tr = None
1000 tr = None
1000
1001
1001 if tr and tr.running():
1002 if tr and tr.running():
1002 return tr
1003 return tr
1003 return None
1004 return None
1004
1005
1005 def transaction(self, desc, report=None):
1006 def transaction(self, desc, report=None):
1006 if (self.ui.configbool('devel', 'all-warnings')
1007 if (self.ui.configbool('devel', 'all-warnings')
1007 or self.ui.configbool('devel', 'check-locks')):
1008 or self.ui.configbool('devel', 'check-locks')):
1008 l = self._lockref and self._lockref()
1009 l = self._lockref and self._lockref()
1009 if l is None or not l.held:
1010 if l is None or not l.held:
1010 self.ui.develwarn('transaction with no lock')
1011 self.ui.develwarn('transaction with no lock')
1011 tr = self.currenttransaction()
1012 tr = self.currenttransaction()
1012 if tr is not None:
1013 if tr is not None:
1013 return tr.nest()
1014 return tr.nest()
1014
1015
1015 # abort here if the journal already exists
1016 # abort here if the journal already exists
1016 if self.svfs.exists("journal"):
1017 if self.svfs.exists("journal"):
1017 raise error.RepoError(
1018 raise error.RepoError(
1018 _("abandoned transaction found"),
1019 _("abandoned transaction found"),
1019 hint=_("run 'hg recover' to clean up transaction"))
1020 hint=_("run 'hg recover' to clean up transaction"))
1020
1021
1021 # make journal.dirstate contain in-memory changes at this point
1022 # make journal.dirstate contain in-memory changes at this point
1022 self.dirstate.write(None)
1023 self.dirstate.write(None)
1023
1024
1024 idbase = "%.40f#%f" % (random.random(), time.time())
1025 idbase = "%.40f#%f" % (random.random(), time.time())
1025 txnid = 'TXN:' + util.sha1(idbase).hexdigest()
1026 txnid = 'TXN:' + util.sha1(idbase).hexdigest()
1026 self.hook('pretxnopen', throw=True, txnname=desc, txnid=txnid)
1027 self.hook('pretxnopen', throw=True, txnname=desc, txnid=txnid)
1027
1028
1028 self._writejournal(desc)
1029 self._writejournal(desc)
1029 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
1030 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
1030 if report:
1031 if report:
1031 rp = report
1032 rp = report
1032 else:
1033 else:
1033 rp = self.ui.warn
1034 rp = self.ui.warn
1034 vfsmap = {'plain': self.vfs} # root of .hg/
1035 vfsmap = {'plain': self.vfs} # root of .hg/
1035 # we must avoid cyclic reference between repo and transaction.
1036 # we must avoid cyclic reference between repo and transaction.
1036 reporef = weakref.ref(self)
1037 reporef = weakref.ref(self)
1037 def validate(tr):
1038 def validate(tr):
1038 """will run pre-closing hooks"""
1039 """will run pre-closing hooks"""
1039 reporef().hook('pretxnclose', throw=True,
1040 reporef().hook('pretxnclose', throw=True,
1040 txnname=desc, **tr.hookargs)
1041 txnname=desc, **tr.hookargs)
1041 def releasefn(tr, success):
1042 def releasefn(tr, success):
1042 repo = reporef()
1043 repo = reporef()
1043 if success:
1044 if success:
1044 # this should be explicitly invoked here, because
1045 # this should be explicitly invoked here, because
1045 # in-memory changes aren't written out at closing
1046 # in-memory changes aren't written out at closing
1046 # transaction, if tr.addfilegenerator (via
1047 # transaction, if tr.addfilegenerator (via
1047 # dirstate.write or so) isn't invoked while
1048 # dirstate.write or so) isn't invoked while
1048 # transaction running
1049 # transaction running
1049 repo.dirstate.write(None)
1050 repo.dirstate.write(None)
1050 else:
1051 else:
1051 # prevent in-memory changes from being written out at
1052 # prevent in-memory changes from being written out at
1052 # the end of outer wlock scope or so
1053 # the end of outer wlock scope or so
1053 repo.dirstate.invalidate()
1054 repo.dirstate.invalidate()
1054
1055
1055 # discard all changes (including ones already written
1056 # discard all changes (including ones already written
1056 # out) in this transaction
1057 # out) in this transaction
1057 repo.vfs.rename('journal.dirstate', 'dirstate')
1058 repo.vfs.rename('journal.dirstate', 'dirstate')
1058
1059
1059 repo.invalidate(clearfilecache=True)
1060 repo.invalidate(clearfilecache=True)
1060
1061
1061 tr = transaction.transaction(rp, self.svfs, vfsmap,
1062 tr = transaction.transaction(rp, self.svfs, vfsmap,
1062 "journal",
1063 "journal",
1063 "undo",
1064 "undo",
1064 aftertrans(renames),
1065 aftertrans(renames),
1065 self.store.createmode,
1066 self.store.createmode,
1066 validator=validate,
1067 validator=validate,
1067 releasefn=releasefn)
1068 releasefn=releasefn)
1068
1069
1069 tr.hookargs['txnid'] = txnid
1070 tr.hookargs['txnid'] = txnid
1070 # note: writing the fncache only during finalize mean that the file is
1071 # note: writing the fncache only during finalize mean that the file is
1071 # outdated when running hooks. As fncache is used for streaming clone,
1072 # outdated when running hooks. As fncache is used for streaming clone,
1072 # this is not expected to break anything that happen during the hooks.
1073 # this is not expected to break anything that happen during the hooks.
1073 tr.addfinalize('flush-fncache', self.store.write)
1074 tr.addfinalize('flush-fncache', self.store.write)
1074 def txnclosehook(tr2):
1075 def txnclosehook(tr2):
1075 """To be run if transaction is successful, will schedule a hook run
1076 """To be run if transaction is successful, will schedule a hook run
1076 """
1077 """
1077 # Don't reference tr2 in hook() so we don't hold a reference.
1078 # Don't reference tr2 in hook() so we don't hold a reference.
1078 # This reduces memory consumption when there are multiple
1079 # This reduces memory consumption when there are multiple
1079 # transactions per lock. This can likely go away if issue5045
1080 # transactions per lock. This can likely go away if issue5045
1080 # fixes the function accumulation.
1081 # fixes the function accumulation.
1081 hookargs = tr2.hookargs
1082 hookargs = tr2.hookargs
1082
1083
1083 def hook():
1084 def hook():
1084 reporef().hook('txnclose', throw=False, txnname=desc,
1085 reporef().hook('txnclose', throw=False, txnname=desc,
1085 **hookargs)
1086 **hookargs)
1086 reporef()._afterlock(hook)
1087 reporef()._afterlock(hook)
1087 tr.addfinalize('txnclose-hook', txnclosehook)
1088 tr.addfinalize('txnclose-hook', txnclosehook)
1088 def txnaborthook(tr2):
1089 def txnaborthook(tr2):
1089 """To be run if transaction is aborted
1090 """To be run if transaction is aborted
1090 """
1091 """
1091 reporef().hook('txnabort', throw=False, txnname=desc,
1092 reporef().hook('txnabort', throw=False, txnname=desc,
1092 **tr2.hookargs)
1093 **tr2.hookargs)
1093 tr.addabort('txnabort-hook', txnaborthook)
1094 tr.addabort('txnabort-hook', txnaborthook)
1094 # avoid eager cache invalidation. in-memory data should be identical
1095 # avoid eager cache invalidation. in-memory data should be identical
1095 # to stored data if transaction has no error.
1096 # to stored data if transaction has no error.
1096 tr.addpostclose('refresh-filecachestats', self._refreshfilecachestats)
1097 tr.addpostclose('refresh-filecachestats', self._refreshfilecachestats)
1097 self._transref = weakref.ref(tr)
1098 self._transref = weakref.ref(tr)
1098 return tr
1099 return tr
1099
1100
1100 def _journalfiles(self):
1101 def _journalfiles(self):
1101 return ((self.svfs, 'journal'),
1102 return ((self.svfs, 'journal'),
1102 (self.vfs, 'journal.dirstate'),
1103 (self.vfs, 'journal.dirstate'),
1103 (self.vfs, 'journal.branch'),
1104 (self.vfs, 'journal.branch'),
1104 (self.vfs, 'journal.desc'),
1105 (self.vfs, 'journal.desc'),
1105 (self.vfs, 'journal.bookmarks'),
1106 (self.vfs, 'journal.bookmarks'),
1106 (self.svfs, 'journal.phaseroots'))
1107 (self.svfs, 'journal.phaseroots'))
1107
1108
1108 def undofiles(self):
1109 def undofiles(self):
1109 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
1110 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
1110
1111
1111 def _writejournal(self, desc):
1112 def _writejournal(self, desc):
1112 self.vfs.write("journal.dirstate",
1113 self.vfs.write("journal.dirstate",
1113 self.vfs.tryread("dirstate"))
1114 self.vfs.tryread("dirstate"))
1114 self.vfs.write("journal.branch",
1115 self.vfs.write("journal.branch",
1115 encoding.fromlocal(self.dirstate.branch()))
1116 encoding.fromlocal(self.dirstate.branch()))
1116 self.vfs.write("journal.desc",
1117 self.vfs.write("journal.desc",
1117 "%d\n%s\n" % (len(self), desc))
1118 "%d\n%s\n" % (len(self), desc))
1118 self.vfs.write("journal.bookmarks",
1119 self.vfs.write("journal.bookmarks",
1119 self.vfs.tryread("bookmarks"))
1120 self.vfs.tryread("bookmarks"))
1120 self.svfs.write("journal.phaseroots",
1121 self.svfs.write("journal.phaseroots",
1121 self.svfs.tryread("phaseroots"))
1122 self.svfs.tryread("phaseroots"))
1122
1123
1123 def recover(self):
1124 def recover(self):
1124 with self.lock():
1125 with self.lock():
1125 if self.svfs.exists("journal"):
1126 if self.svfs.exists("journal"):
1126 self.ui.status(_("rolling back interrupted transaction\n"))
1127 self.ui.status(_("rolling back interrupted transaction\n"))
1127 vfsmap = {'': self.svfs,
1128 vfsmap = {'': self.svfs,
1128 'plain': self.vfs,}
1129 'plain': self.vfs,}
1129 transaction.rollback(self.svfs, vfsmap, "journal",
1130 transaction.rollback(self.svfs, vfsmap, "journal",
1130 self.ui.warn)
1131 self.ui.warn)
1131 self.invalidate()
1132 self.invalidate()
1132 return True
1133 return True
1133 else:
1134 else:
1134 self.ui.warn(_("no interrupted transaction available\n"))
1135 self.ui.warn(_("no interrupted transaction available\n"))
1135 return False
1136 return False
1136
1137
1137 def rollback(self, dryrun=False, force=False):
1138 def rollback(self, dryrun=False, force=False):
1138 wlock = lock = dsguard = None
1139 wlock = lock = dsguard = None
1139 try:
1140 try:
1140 wlock = self.wlock()
1141 wlock = self.wlock()
1141 lock = self.lock()
1142 lock = self.lock()
1142 if self.svfs.exists("undo"):
1143 if self.svfs.exists("undo"):
1143 dsguard = cmdutil.dirstateguard(self, 'rollback')
1144 dsguard = cmdutil.dirstateguard(self, 'rollback')
1144
1145
1145 return self._rollback(dryrun, force, dsguard)
1146 return self._rollback(dryrun, force, dsguard)
1146 else:
1147 else:
1147 self.ui.warn(_("no rollback information available\n"))
1148 self.ui.warn(_("no rollback information available\n"))
1148 return 1
1149 return 1
1149 finally:
1150 finally:
1150 release(dsguard, lock, wlock)
1151 release(dsguard, lock, wlock)
1151
1152
1152 @unfilteredmethod # Until we get smarter cache management
1153 @unfilteredmethod # Until we get smarter cache management
1153 def _rollback(self, dryrun, force, dsguard):
1154 def _rollback(self, dryrun, force, dsguard):
1154 ui = self.ui
1155 ui = self.ui
1155 try:
1156 try:
1156 args = self.vfs.read('undo.desc').splitlines()
1157 args = self.vfs.read('undo.desc').splitlines()
1157 (oldlen, desc, detail) = (int(args[0]), args[1], None)
1158 (oldlen, desc, detail) = (int(args[0]), args[1], None)
1158 if len(args) >= 3:
1159 if len(args) >= 3:
1159 detail = args[2]
1160 detail = args[2]
1160 oldtip = oldlen - 1
1161 oldtip = oldlen - 1
1161
1162
1162 if detail and ui.verbose:
1163 if detail and ui.verbose:
1163 msg = (_('repository tip rolled back to revision %s'
1164 msg = (_('repository tip rolled back to revision %s'
1164 ' (undo %s: %s)\n')
1165 ' (undo %s: %s)\n')
1165 % (oldtip, desc, detail))
1166 % (oldtip, desc, detail))
1166 else:
1167 else:
1167 msg = (_('repository tip rolled back to revision %s'
1168 msg = (_('repository tip rolled back to revision %s'
1168 ' (undo %s)\n')
1169 ' (undo %s)\n')
1169 % (oldtip, desc))
1170 % (oldtip, desc))
1170 except IOError:
1171 except IOError:
1171 msg = _('rolling back unknown transaction\n')
1172 msg = _('rolling back unknown transaction\n')
1172 desc = None
1173 desc = None
1173
1174
1174 if not force and self['.'] != self['tip'] and desc == 'commit':
1175 if not force and self['.'] != self['tip'] and desc == 'commit':
1175 raise error.Abort(
1176 raise error.Abort(
1176 _('rollback of last commit while not checked out '
1177 _('rollback of last commit while not checked out '
1177 'may lose data'), hint=_('use -f to force'))
1178 'may lose data'), hint=_('use -f to force'))
1178
1179
1179 ui.status(msg)
1180 ui.status(msg)
1180 if dryrun:
1181 if dryrun:
1181 return 0
1182 return 0
1182
1183
1183 parents = self.dirstate.parents()
1184 parents = self.dirstate.parents()
1184 self.destroying()
1185 self.destroying()
1185 vfsmap = {'plain': self.vfs, '': self.svfs}
1186 vfsmap = {'plain': self.vfs, '': self.svfs}
1186 transaction.rollback(self.svfs, vfsmap, 'undo', ui.warn)
1187 transaction.rollback(self.svfs, vfsmap, 'undo', ui.warn)
1187 if self.vfs.exists('undo.bookmarks'):
1188 if self.vfs.exists('undo.bookmarks'):
1188 self.vfs.rename('undo.bookmarks', 'bookmarks')
1189 self.vfs.rename('undo.bookmarks', 'bookmarks')
1189 if self.svfs.exists('undo.phaseroots'):
1190 if self.svfs.exists('undo.phaseroots'):
1190 self.svfs.rename('undo.phaseroots', 'phaseroots')
1191 self.svfs.rename('undo.phaseroots', 'phaseroots')
1191 self.invalidate()
1192 self.invalidate()
1192
1193
1193 parentgone = (parents[0] not in self.changelog.nodemap or
1194 parentgone = (parents[0] not in self.changelog.nodemap or
1194 parents[1] not in self.changelog.nodemap)
1195 parents[1] not in self.changelog.nodemap)
1195 if parentgone:
1196 if parentgone:
1196 # prevent dirstateguard from overwriting already restored one
1197 # prevent dirstateguard from overwriting already restored one
1197 dsguard.close()
1198 dsguard.close()
1198
1199
1199 self.vfs.rename('undo.dirstate', 'dirstate')
1200 self.vfs.rename('undo.dirstate', 'dirstate')
1200 try:
1201 try:
1201 branch = self.vfs.read('undo.branch')
1202 branch = self.vfs.read('undo.branch')
1202 self.dirstate.setbranch(encoding.tolocal(branch))
1203 self.dirstate.setbranch(encoding.tolocal(branch))
1203 except IOError:
1204 except IOError:
1204 ui.warn(_('named branch could not be reset: '
1205 ui.warn(_('named branch could not be reset: '
1205 'current branch is still \'%s\'\n')
1206 'current branch is still \'%s\'\n')
1206 % self.dirstate.branch())
1207 % self.dirstate.branch())
1207
1208
1208 self.dirstate.invalidate()
1209 self.dirstate.invalidate()
1209 parents = tuple([p.rev() for p in self[None].parents()])
1210 parents = tuple([p.rev() for p in self[None].parents()])
1210 if len(parents) > 1:
1211 if len(parents) > 1:
1211 ui.status(_('working directory now based on '
1212 ui.status(_('working directory now based on '
1212 'revisions %d and %d\n') % parents)
1213 'revisions %d and %d\n') % parents)
1213 else:
1214 else:
1214 ui.status(_('working directory now based on '
1215 ui.status(_('working directory now based on '
1215 'revision %d\n') % parents)
1216 'revision %d\n') % parents)
1216 mergemod.mergestate.clean(self, self['.'].node())
1217 mergemod.mergestate.clean(self, self['.'].node())
1217
1218
1218 # TODO: if we know which new heads may result from this rollback, pass
1219 # TODO: if we know which new heads may result from this rollback, pass
1219 # them to destroy(), which will prevent the branchhead cache from being
1220 # them to destroy(), which will prevent the branchhead cache from being
1220 # invalidated.
1221 # invalidated.
1221 self.destroyed()
1222 self.destroyed()
1222 return 0
1223 return 0
1223
1224
1224 def invalidatecaches(self):
1225 def invalidatecaches(self):
1225
1226
1226 if '_tagscache' in vars(self):
1227 if '_tagscache' in vars(self):
1227 # can't use delattr on proxy
1228 # can't use delattr on proxy
1228 del self.__dict__['_tagscache']
1229 del self.__dict__['_tagscache']
1229
1230
1230 self.unfiltered()._branchcaches.clear()
1231 self.unfiltered()._branchcaches.clear()
1231 self.invalidatevolatilesets()
1232 self.invalidatevolatilesets()
1232
1233
1233 def invalidatevolatilesets(self):
1234 def invalidatevolatilesets(self):
1234 self.filteredrevcache.clear()
1235 self.filteredrevcache.clear()
1235 obsolete.clearobscaches(self)
1236 obsolete.clearobscaches(self)
1236
1237
1237 def invalidatedirstate(self):
1238 def invalidatedirstate(self):
1238 '''Invalidates the dirstate, causing the next call to dirstate
1239 '''Invalidates the dirstate, causing the next call to dirstate
1239 to check if it was modified since the last time it was read,
1240 to check if it was modified since the last time it was read,
1240 rereading it if it has.
1241 rereading it if it has.
1241
1242
1242 This is different to dirstate.invalidate() that it doesn't always
1243 This is different to dirstate.invalidate() that it doesn't always
1243 rereads the dirstate. Use dirstate.invalidate() if you want to
1244 rereads the dirstate. Use dirstate.invalidate() if you want to
1244 explicitly read the dirstate again (i.e. restoring it to a previous
1245 explicitly read the dirstate again (i.e. restoring it to a previous
1245 known good state).'''
1246 known good state).'''
1246 if hasunfilteredcache(self, 'dirstate'):
1247 if hasunfilteredcache(self, 'dirstate'):
1247 for k in self.dirstate._filecache:
1248 for k in self.dirstate._filecache:
1248 try:
1249 try:
1249 delattr(self.dirstate, k)
1250 delattr(self.dirstate, k)
1250 except AttributeError:
1251 except AttributeError:
1251 pass
1252 pass
1252 delattr(self.unfiltered(), 'dirstate')
1253 delattr(self.unfiltered(), 'dirstate')
1253
1254
1254 def invalidate(self, clearfilecache=False):
1255 def invalidate(self, clearfilecache=False):
1255 unfiltered = self.unfiltered() # all file caches are stored unfiltered
1256 unfiltered = self.unfiltered() # all file caches are stored unfiltered
1256 for k in self._filecache.keys():
1257 for k in self._filecache.keys():
1257 # dirstate is invalidated separately in invalidatedirstate()
1258 # dirstate is invalidated separately in invalidatedirstate()
1258 if k == 'dirstate':
1259 if k == 'dirstate':
1259 continue
1260 continue
1260
1261
1261 if clearfilecache:
1262 if clearfilecache:
1262 del self._filecache[k]
1263 del self._filecache[k]
1263 try:
1264 try:
1264 delattr(unfiltered, k)
1265 delattr(unfiltered, k)
1265 except AttributeError:
1266 except AttributeError:
1266 pass
1267 pass
1267 self.invalidatecaches()
1268 self.invalidatecaches()
1268 self.store.invalidatecaches()
1269 self.store.invalidatecaches()
1269
1270
1270 def invalidateall(self):
1271 def invalidateall(self):
1271 '''Fully invalidates both store and non-store parts, causing the
1272 '''Fully invalidates both store and non-store parts, causing the
1272 subsequent operation to reread any outside changes.'''
1273 subsequent operation to reread any outside changes.'''
1273 # extension should hook this to invalidate its caches
1274 # extension should hook this to invalidate its caches
1274 self.invalidate()
1275 self.invalidate()
1275 self.invalidatedirstate()
1276 self.invalidatedirstate()
1276
1277
1277 def _refreshfilecachestats(self, tr):
1278 def _refreshfilecachestats(self, tr):
1278 """Reload stats of cached files so that they are flagged as valid"""
1279 """Reload stats of cached files so that they are flagged as valid"""
1279 for k, ce in self._filecache.items():
1280 for k, ce in self._filecache.items():
1280 if k == 'dirstate' or k not in self.__dict__:
1281 if k == 'dirstate' or k not in self.__dict__:
1281 continue
1282 continue
1282 ce.refresh()
1283 ce.refresh()
1283
1284
1284 def _lock(self, vfs, lockname, wait, releasefn, acquirefn, desc,
1285 def _lock(self, vfs, lockname, wait, releasefn, acquirefn, desc,
1285 inheritchecker=None, parentenvvar=None):
1286 inheritchecker=None, parentenvvar=None):
1286 parentlock = None
1287 parentlock = None
1287 # the contents of parentenvvar are used by the underlying lock to
1288 # the contents of parentenvvar are used by the underlying lock to
1288 # determine whether it can be inherited
1289 # determine whether it can be inherited
1289 if parentenvvar is not None:
1290 if parentenvvar is not None:
1290 parentlock = os.environ.get(parentenvvar)
1291 parentlock = os.environ.get(parentenvvar)
1291 try:
1292 try:
1292 l = lockmod.lock(vfs, lockname, 0, releasefn=releasefn,
1293 l = lockmod.lock(vfs, lockname, 0, releasefn=releasefn,
1293 acquirefn=acquirefn, desc=desc,
1294 acquirefn=acquirefn, desc=desc,
1294 inheritchecker=inheritchecker,
1295 inheritchecker=inheritchecker,
1295 parentlock=parentlock)
1296 parentlock=parentlock)
1296 except error.LockHeld as inst:
1297 except error.LockHeld as inst:
1297 if not wait:
1298 if not wait:
1298 raise
1299 raise
1299 self.ui.warn(_("waiting for lock on %s held by %r\n") %
1300 self.ui.warn(_("waiting for lock on %s held by %r\n") %
1300 (desc, inst.locker))
1301 (desc, inst.locker))
1301 # default to 600 seconds timeout
1302 # default to 600 seconds timeout
1302 l = lockmod.lock(vfs, lockname,
1303 l = lockmod.lock(vfs, lockname,
1303 int(self.ui.config("ui", "timeout", "600")),
1304 int(self.ui.config("ui", "timeout", "600")),
1304 releasefn=releasefn, acquirefn=acquirefn,
1305 releasefn=releasefn, acquirefn=acquirefn,
1305 desc=desc)
1306 desc=desc)
1306 self.ui.warn(_("got lock after %s seconds\n") % l.delay)
1307 self.ui.warn(_("got lock after %s seconds\n") % l.delay)
1307 return l
1308 return l
1308
1309
1309 def _afterlock(self, callback):
1310 def _afterlock(self, callback):
1310 """add a callback to be run when the repository is fully unlocked
1311 """add a callback to be run when the repository is fully unlocked
1311
1312
1312 The callback will be executed when the outermost lock is released
1313 The callback will be executed when the outermost lock is released
1313 (with wlock being higher level than 'lock')."""
1314 (with wlock being higher level than 'lock')."""
1314 for ref in (self._wlockref, self._lockref):
1315 for ref in (self._wlockref, self._lockref):
1315 l = ref and ref()
1316 l = ref and ref()
1316 if l and l.held:
1317 if l and l.held:
1317 l.postrelease.append(callback)
1318 l.postrelease.append(callback)
1318 break
1319 break
1319 else: # no lock have been found.
1320 else: # no lock have been found.
1320 callback()
1321 callback()
1321
1322
1322 def lock(self, wait=True):
1323 def lock(self, wait=True):
1323 '''Lock the repository store (.hg/store) and return a weak reference
1324 '''Lock the repository store (.hg/store) and return a weak reference
1324 to the lock. Use this before modifying the store (e.g. committing or
1325 to the lock. Use this before modifying the store (e.g. committing or
1325 stripping). If you are opening a transaction, get a lock as well.)
1326 stripping). If you are opening a transaction, get a lock as well.)
1326
1327
1327 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1328 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1328 'wlock' first to avoid a dead-lock hazard.'''
1329 'wlock' first to avoid a dead-lock hazard.'''
1329 l = self._lockref and self._lockref()
1330 l = self._lockref and self._lockref()
1330 if l is not None and l.held:
1331 if l is not None and l.held:
1331 l.lock()
1332 l.lock()
1332 return l
1333 return l
1333
1334
1334 l = self._lock(self.svfs, "lock", wait, None,
1335 l = self._lock(self.svfs, "lock", wait, None,
1335 self.invalidate, _('repository %s') % self.origroot)
1336 self.invalidate, _('repository %s') % self.origroot)
1336 self._lockref = weakref.ref(l)
1337 self._lockref = weakref.ref(l)
1337 return l
1338 return l
1338
1339
1339 def _wlockchecktransaction(self):
1340 def _wlockchecktransaction(self):
1340 if self.currenttransaction() is not None:
1341 if self.currenttransaction() is not None:
1341 raise error.LockInheritanceContractViolation(
1342 raise error.LockInheritanceContractViolation(
1342 'wlock cannot be inherited in the middle of a transaction')
1343 'wlock cannot be inherited in the middle of a transaction')
1343
1344
1344 def wlock(self, wait=True):
1345 def wlock(self, wait=True):
1345 '''Lock the non-store parts of the repository (everything under
1346 '''Lock the non-store parts of the repository (everything under
1346 .hg except .hg/store) and return a weak reference to the lock.
1347 .hg except .hg/store) and return a weak reference to the lock.
1347
1348
1348 Use this before modifying files in .hg.
1349 Use this before modifying files in .hg.
1349
1350
1350 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1351 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
1351 'wlock' first to avoid a dead-lock hazard.'''
1352 'wlock' first to avoid a dead-lock hazard.'''
1352 l = self._wlockref and self._wlockref()
1353 l = self._wlockref and self._wlockref()
1353 if l is not None and l.held:
1354 if l is not None and l.held:
1354 l.lock()
1355 l.lock()
1355 return l
1356 return l
1356
1357
1357 # We do not need to check for non-waiting lock acquisition. Such
1358 # We do not need to check for non-waiting lock acquisition. Such
1358 # acquisition would not cause dead-lock as they would just fail.
1359 # acquisition would not cause dead-lock as they would just fail.
1359 if wait and (self.ui.configbool('devel', 'all-warnings')
1360 if wait and (self.ui.configbool('devel', 'all-warnings')
1360 or self.ui.configbool('devel', 'check-locks')):
1361 or self.ui.configbool('devel', 'check-locks')):
1361 l = self._lockref and self._lockref()
1362 l = self._lockref and self._lockref()
1362 if l is not None and l.held:
1363 if l is not None and l.held:
1363 self.ui.develwarn('"wlock" acquired after "lock"')
1364 self.ui.develwarn('"wlock" acquired after "lock"')
1364
1365
1365 def unlock():
1366 def unlock():
1366 if self.dirstate.pendingparentchange():
1367 if self.dirstate.pendingparentchange():
1367 self.dirstate.invalidate()
1368 self.dirstate.invalidate()
1368 else:
1369 else:
1369 self.dirstate.write(None)
1370 self.dirstate.write(None)
1370
1371
1371 self._filecache['dirstate'].refresh()
1372 self._filecache['dirstate'].refresh()
1372
1373
1373 l = self._lock(self.vfs, "wlock", wait, unlock,
1374 l = self._lock(self.vfs, "wlock", wait, unlock,
1374 self.invalidatedirstate, _('working directory of %s') %
1375 self.invalidatedirstate, _('working directory of %s') %
1375 self.origroot,
1376 self.origroot,
1376 inheritchecker=self._wlockchecktransaction,
1377 inheritchecker=self._wlockchecktransaction,
1377 parentenvvar='HG_WLOCK_LOCKER')
1378 parentenvvar='HG_WLOCK_LOCKER')
1378 self._wlockref = weakref.ref(l)
1379 self._wlockref = weakref.ref(l)
1379 return l
1380 return l
1380
1381
1381 def _currentlock(self, lockref):
1382 def _currentlock(self, lockref):
1382 """Returns the lock if it's held, or None if it's not."""
1383 """Returns the lock if it's held, or None if it's not."""
1383 if lockref is None:
1384 if lockref is None:
1384 return None
1385 return None
1385 l = lockref()
1386 l = lockref()
1386 if l is None or not l.held:
1387 if l is None or not l.held:
1387 return None
1388 return None
1388 return l
1389 return l
1389
1390
1390 def currentwlock(self):
1391 def currentwlock(self):
1391 """Returns the wlock if it's held, or None if it's not."""
1392 """Returns the wlock if it's held, or None if it's not."""
1392 return self._currentlock(self._wlockref)
1393 return self._currentlock(self._wlockref)
1393
1394
1394 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist):
1395 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist):
1395 """
1396 """
1396 commit an individual file as part of a larger transaction
1397 commit an individual file as part of a larger transaction
1397 """
1398 """
1398
1399
1399 fname = fctx.path()
1400 fname = fctx.path()
1400 fparent1 = manifest1.get(fname, nullid)
1401 fparent1 = manifest1.get(fname, nullid)
1401 fparent2 = manifest2.get(fname, nullid)
1402 fparent2 = manifest2.get(fname, nullid)
1402 if isinstance(fctx, context.filectx):
1403 if isinstance(fctx, context.filectx):
1403 node = fctx.filenode()
1404 node = fctx.filenode()
1404 if node in [fparent1, fparent2]:
1405 if node in [fparent1, fparent2]:
1405 self.ui.debug('reusing %s filelog entry\n' % fname)
1406 self.ui.debug('reusing %s filelog entry\n' % fname)
1406 return node
1407 return node
1407
1408
1408 flog = self.file(fname)
1409 flog = self.file(fname)
1409 meta = {}
1410 meta = {}
1410 copy = fctx.renamed()
1411 copy = fctx.renamed()
1411 if copy and copy[0] != fname:
1412 if copy and copy[0] != fname:
1412 # Mark the new revision of this file as a copy of another
1413 # Mark the new revision of this file as a copy of another
1413 # file. This copy data will effectively act as a parent
1414 # file. This copy data will effectively act as a parent
1414 # of this new revision. If this is a merge, the first
1415 # of this new revision. If this is a merge, the first
1415 # parent will be the nullid (meaning "look up the copy data")
1416 # parent will be the nullid (meaning "look up the copy data")
1416 # and the second one will be the other parent. For example:
1417 # and the second one will be the other parent. For example:
1417 #
1418 #
1418 # 0 --- 1 --- 3 rev1 changes file foo
1419 # 0 --- 1 --- 3 rev1 changes file foo
1419 # \ / rev2 renames foo to bar and changes it
1420 # \ / rev2 renames foo to bar and changes it
1420 # \- 2 -/ rev3 should have bar with all changes and
1421 # \- 2 -/ rev3 should have bar with all changes and
1421 # should record that bar descends from
1422 # should record that bar descends from
1422 # bar in rev2 and foo in rev1
1423 # bar in rev2 and foo in rev1
1423 #
1424 #
1424 # this allows this merge to succeed:
1425 # this allows this merge to succeed:
1425 #
1426 #
1426 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
1427 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
1427 # \ / merging rev3 and rev4 should use bar@rev2
1428 # \ / merging rev3 and rev4 should use bar@rev2
1428 # \- 2 --- 4 as the merge base
1429 # \- 2 --- 4 as the merge base
1429 #
1430 #
1430
1431
1431 cfname = copy[0]
1432 cfname = copy[0]
1432 crev = manifest1.get(cfname)
1433 crev = manifest1.get(cfname)
1433 newfparent = fparent2
1434 newfparent = fparent2
1434
1435
1435 if manifest2: # branch merge
1436 if manifest2: # branch merge
1436 if fparent2 == nullid or crev is None: # copied on remote side
1437 if fparent2 == nullid or crev is None: # copied on remote side
1437 if cfname in manifest2:
1438 if cfname in manifest2:
1438 crev = manifest2[cfname]
1439 crev = manifest2[cfname]
1439 newfparent = fparent1
1440 newfparent = fparent1
1440
1441
1441 # Here, we used to search backwards through history to try to find
1442 # Here, we used to search backwards through history to try to find
1442 # where the file copy came from if the source of a copy was not in
1443 # where the file copy came from if the source of a copy was not in
1443 # the parent directory. However, this doesn't actually make sense to
1444 # the parent directory. However, this doesn't actually make sense to
1444 # do (what does a copy from something not in your working copy even
1445 # do (what does a copy from something not in your working copy even
1445 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
1446 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
1446 # the user that copy information was dropped, so if they didn't
1447 # the user that copy information was dropped, so if they didn't
1447 # expect this outcome it can be fixed, but this is the correct
1448 # expect this outcome it can be fixed, but this is the correct
1448 # behavior in this circumstance.
1449 # behavior in this circumstance.
1449
1450
1450 if crev:
1451 if crev:
1451 self.ui.debug(" %s: copy %s:%s\n" % (fname, cfname, hex(crev)))
1452 self.ui.debug(" %s: copy %s:%s\n" % (fname, cfname, hex(crev)))
1452 meta["copy"] = cfname
1453 meta["copy"] = cfname
1453 meta["copyrev"] = hex(crev)
1454 meta["copyrev"] = hex(crev)
1454 fparent1, fparent2 = nullid, newfparent
1455 fparent1, fparent2 = nullid, newfparent
1455 else:
1456 else:
1456 self.ui.warn(_("warning: can't find ancestor for '%s' "
1457 self.ui.warn(_("warning: can't find ancestor for '%s' "
1457 "copied from '%s'!\n") % (fname, cfname))
1458 "copied from '%s'!\n") % (fname, cfname))
1458
1459
1459 elif fparent1 == nullid:
1460 elif fparent1 == nullid:
1460 fparent1, fparent2 = fparent2, nullid
1461 fparent1, fparent2 = fparent2, nullid
1461 elif fparent2 != nullid:
1462 elif fparent2 != nullid:
1462 # is one parent an ancestor of the other?
1463 # is one parent an ancestor of the other?
1463 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
1464 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
1464 if fparent1 in fparentancestors:
1465 if fparent1 in fparentancestors:
1465 fparent1, fparent2 = fparent2, nullid
1466 fparent1, fparent2 = fparent2, nullid
1466 elif fparent2 in fparentancestors:
1467 elif fparent2 in fparentancestors:
1467 fparent2 = nullid
1468 fparent2 = nullid
1468
1469
1469 # is the file changed?
1470 # is the file changed?
1470 text = fctx.data()
1471 text = fctx.data()
1471 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
1472 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
1472 changelist.append(fname)
1473 changelist.append(fname)
1473 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
1474 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
1474 # are just the flags changed during merge?
1475 # are just the flags changed during merge?
1475 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
1476 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
1476 changelist.append(fname)
1477 changelist.append(fname)
1477
1478
1478 return fparent1
1479 return fparent1
1479
1480
1480 def checkcommitpatterns(self, wctx, vdirs, match, status, fail):
1481 def checkcommitpatterns(self, wctx, vdirs, match, status, fail):
1481 """check for commit arguments that aren't commitable"""
1482 """check for commit arguments that aren't commitable"""
1482 if match.isexact() or match.prefix():
1483 if match.isexact() or match.prefix():
1483 matched = set(status.modified + status.added + status.removed)
1484 matched = set(status.modified + status.added + status.removed)
1484
1485
1485 for f in match.files():
1486 for f in match.files():
1486 f = self.dirstate.normalize(f)
1487 f = self.dirstate.normalize(f)
1487 if f == '.' or f in matched or f in wctx.substate:
1488 if f == '.' or f in matched or f in wctx.substate:
1488 continue
1489 continue
1489 if f in status.deleted:
1490 if f in status.deleted:
1490 fail(f, _('file not found!'))
1491 fail(f, _('file not found!'))
1491 if f in vdirs: # visited directory
1492 if f in vdirs: # visited directory
1492 d = f + '/'
1493 d = f + '/'
1493 for mf in matched:
1494 for mf in matched:
1494 if mf.startswith(d):
1495 if mf.startswith(d):
1495 break
1496 break
1496 else:
1497 else:
1497 fail(f, _("no match under directory!"))
1498 fail(f, _("no match under directory!"))
1498 elif f not in self.dirstate:
1499 elif f not in self.dirstate:
1499 fail(f, _("file not tracked!"))
1500 fail(f, _("file not tracked!"))
1500
1501
1501 @unfilteredmethod
1502 @unfilteredmethod
1502 def commit(self, text="", user=None, date=None, match=None, force=False,
1503 def commit(self, text="", user=None, date=None, match=None, force=False,
1503 editor=False, extra=None):
1504 editor=False, extra=None):
1504 """Add a new revision to current repository.
1505 """Add a new revision to current repository.
1505
1506
1506 Revision information is gathered from the working directory,
1507 Revision information is gathered from the working directory,
1507 match can be used to filter the committed files. If editor is
1508 match can be used to filter the committed files. If editor is
1508 supplied, it is called to get a commit message.
1509 supplied, it is called to get a commit message.
1509 """
1510 """
1510 if extra is None:
1511 if extra is None:
1511 extra = {}
1512 extra = {}
1512
1513
1513 def fail(f, msg):
1514 def fail(f, msg):
1514 raise error.Abort('%s: %s' % (f, msg))
1515 raise error.Abort('%s: %s' % (f, msg))
1515
1516
1516 if not match:
1517 if not match:
1517 match = matchmod.always(self.root, '')
1518 match = matchmod.always(self.root, '')
1518
1519
1519 if not force:
1520 if not force:
1520 vdirs = []
1521 vdirs = []
1521 match.explicitdir = vdirs.append
1522 match.explicitdir = vdirs.append
1522 match.bad = fail
1523 match.bad = fail
1523
1524
1524 wlock = lock = tr = None
1525 wlock = lock = tr = None
1525 try:
1526 try:
1526 wlock = self.wlock()
1527 wlock = self.wlock()
1527 lock = self.lock() # for recent changelog (see issue4368)
1528 lock = self.lock() # for recent changelog (see issue4368)
1528
1529
1529 wctx = self[None]
1530 wctx = self[None]
1530 merge = len(wctx.parents()) > 1
1531 merge = len(wctx.parents()) > 1
1531
1532
1532 if not force and merge and match.ispartial():
1533 if not force and merge and match.ispartial():
1533 raise error.Abort(_('cannot partially commit a merge '
1534 raise error.Abort(_('cannot partially commit a merge '
1534 '(do not specify files or patterns)'))
1535 '(do not specify files or patterns)'))
1535
1536
1536 status = self.status(match=match, clean=force)
1537 status = self.status(match=match, clean=force)
1537 if force:
1538 if force:
1538 status.modified.extend(status.clean) # mq may commit clean files
1539 status.modified.extend(status.clean) # mq may commit clean files
1539
1540
1540 # check subrepos
1541 # check subrepos
1541 subs = []
1542 subs = []
1542 commitsubs = set()
1543 commitsubs = set()
1543 newstate = wctx.substate.copy()
1544 newstate = wctx.substate.copy()
1544 # only manage subrepos and .hgsubstate if .hgsub is present
1545 # only manage subrepos and .hgsubstate if .hgsub is present
1545 if '.hgsub' in wctx:
1546 if '.hgsub' in wctx:
1546 # we'll decide whether to track this ourselves, thanks
1547 # we'll decide whether to track this ourselves, thanks
1547 for c in status.modified, status.added, status.removed:
1548 for c in status.modified, status.added, status.removed:
1548 if '.hgsubstate' in c:
1549 if '.hgsubstate' in c:
1549 c.remove('.hgsubstate')
1550 c.remove('.hgsubstate')
1550
1551
1551 # compare current state to last committed state
1552 # compare current state to last committed state
1552 # build new substate based on last committed state
1553 # build new substate based on last committed state
1553 oldstate = wctx.p1().substate
1554 oldstate = wctx.p1().substate
1554 for s in sorted(newstate.keys()):
1555 for s in sorted(newstate.keys()):
1555 if not match(s):
1556 if not match(s):
1556 # ignore working copy, use old state if present
1557 # ignore working copy, use old state if present
1557 if s in oldstate:
1558 if s in oldstate:
1558 newstate[s] = oldstate[s]
1559 newstate[s] = oldstate[s]
1559 continue
1560 continue
1560 if not force:
1561 if not force:
1561 raise error.Abort(
1562 raise error.Abort(
1562 _("commit with new subrepo %s excluded") % s)
1563 _("commit with new subrepo %s excluded") % s)
1563 dirtyreason = wctx.sub(s).dirtyreason(True)
1564 dirtyreason = wctx.sub(s).dirtyreason(True)
1564 if dirtyreason:
1565 if dirtyreason:
1565 if not self.ui.configbool('ui', 'commitsubrepos'):
1566 if not self.ui.configbool('ui', 'commitsubrepos'):
1566 raise error.Abort(dirtyreason,
1567 raise error.Abort(dirtyreason,
1567 hint=_("use --subrepos for recursive commit"))
1568 hint=_("use --subrepos for recursive commit"))
1568 subs.append(s)
1569 subs.append(s)
1569 commitsubs.add(s)
1570 commitsubs.add(s)
1570 else:
1571 else:
1571 bs = wctx.sub(s).basestate()
1572 bs = wctx.sub(s).basestate()
1572 newstate[s] = (newstate[s][0], bs, newstate[s][2])
1573 newstate[s] = (newstate[s][0], bs, newstate[s][2])
1573 if oldstate.get(s, (None, None, None))[1] != bs:
1574 if oldstate.get(s, (None, None, None))[1] != bs:
1574 subs.append(s)
1575 subs.append(s)
1575
1576
1576 # check for removed subrepos
1577 # check for removed subrepos
1577 for p in wctx.parents():
1578 for p in wctx.parents():
1578 r = [s for s in p.substate if s not in newstate]
1579 r = [s for s in p.substate if s not in newstate]
1579 subs += [s for s in r if match(s)]
1580 subs += [s for s in r if match(s)]
1580 if subs:
1581 if subs:
1581 if (not match('.hgsub') and
1582 if (not match('.hgsub') and
1582 '.hgsub' in (wctx.modified() + wctx.added())):
1583 '.hgsub' in (wctx.modified() + wctx.added())):
1583 raise error.Abort(
1584 raise error.Abort(
1584 _("can't commit subrepos without .hgsub"))
1585 _("can't commit subrepos without .hgsub"))
1585 status.modified.insert(0, '.hgsubstate')
1586 status.modified.insert(0, '.hgsubstate')
1586
1587
1587 elif '.hgsub' in status.removed:
1588 elif '.hgsub' in status.removed:
1588 # clean up .hgsubstate when .hgsub is removed
1589 # clean up .hgsubstate when .hgsub is removed
1589 if ('.hgsubstate' in wctx and
1590 if ('.hgsubstate' in wctx and
1590 '.hgsubstate' not in (status.modified + status.added +
1591 '.hgsubstate' not in (status.modified + status.added +
1591 status.removed)):
1592 status.removed)):
1592 status.removed.insert(0, '.hgsubstate')
1593 status.removed.insert(0, '.hgsubstate')
1593
1594
1594 # make sure all explicit patterns are matched
1595 # make sure all explicit patterns are matched
1595 if not force:
1596 if not force:
1596 self.checkcommitpatterns(wctx, vdirs, match, status, fail)
1597 self.checkcommitpatterns(wctx, vdirs, match, status, fail)
1597
1598
1598 cctx = context.workingcommitctx(self, status,
1599 cctx = context.workingcommitctx(self, status,
1599 text, user, date, extra)
1600 text, user, date, extra)
1600
1601
1601 # internal config: ui.allowemptycommit
1602 # internal config: ui.allowemptycommit
1602 allowemptycommit = (wctx.branch() != wctx.p1().branch()
1603 allowemptycommit = (wctx.branch() != wctx.p1().branch()
1603 or extra.get('close') or merge or cctx.files()
1604 or extra.get('close') or merge or cctx.files()
1604 or self.ui.configbool('ui', 'allowemptycommit'))
1605 or self.ui.configbool('ui', 'allowemptycommit'))
1605 if not allowemptycommit:
1606 if not allowemptycommit:
1606 return None
1607 return None
1607
1608
1608 if merge and cctx.deleted():
1609 if merge and cctx.deleted():
1609 raise error.Abort(_("cannot commit merge with missing files"))
1610 raise error.Abort(_("cannot commit merge with missing files"))
1610
1611
1611 ms = mergemod.mergestate.read(self)
1612 ms = mergemod.mergestate.read(self)
1612
1613
1613 if list(ms.unresolved()):
1614 if list(ms.unresolved()):
1614 raise error.Abort(_('unresolved merge conflicts '
1615 raise error.Abort(_('unresolved merge conflicts '
1615 '(see "hg help resolve")'))
1616 '(see "hg help resolve")'))
1616 if ms.mdstate() != 's' or list(ms.driverresolved()):
1617 if ms.mdstate() != 's' or list(ms.driverresolved()):
1617 raise error.Abort(_('driver-resolved merge conflicts'),
1618 raise error.Abort(_('driver-resolved merge conflicts'),
1618 hint=_('run "hg resolve --all" to resolve'))
1619 hint=_('run "hg resolve --all" to resolve'))
1619
1620
1620 if editor:
1621 if editor:
1621 cctx._text = editor(self, cctx, subs)
1622 cctx._text = editor(self, cctx, subs)
1622 edited = (text != cctx._text)
1623 edited = (text != cctx._text)
1623
1624
1624 # Save commit message in case this transaction gets rolled back
1625 # Save commit message in case this transaction gets rolled back
1625 # (e.g. by a pretxncommit hook). Leave the content alone on
1626 # (e.g. by a pretxncommit hook). Leave the content alone on
1626 # the assumption that the user will use the same editor again.
1627 # the assumption that the user will use the same editor again.
1627 msgfn = self.savecommitmessage(cctx._text)
1628 msgfn = self.savecommitmessage(cctx._text)
1628
1629
1629 # commit subs and write new state
1630 # commit subs and write new state
1630 if subs:
1631 if subs:
1631 for s in sorted(commitsubs):
1632 for s in sorted(commitsubs):
1632 sub = wctx.sub(s)
1633 sub = wctx.sub(s)
1633 self.ui.status(_('committing subrepository %s\n') %
1634 self.ui.status(_('committing subrepository %s\n') %
1634 subrepo.subrelpath(sub))
1635 subrepo.subrelpath(sub))
1635 sr = sub.commit(cctx._text, user, date)
1636 sr = sub.commit(cctx._text, user, date)
1636 newstate[s] = (newstate[s][0], sr)
1637 newstate[s] = (newstate[s][0], sr)
1637 subrepo.writestate(self, newstate)
1638 subrepo.writestate(self, newstate)
1638
1639
1639 p1, p2 = self.dirstate.parents()
1640 p1, p2 = self.dirstate.parents()
1640 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
1641 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
1641 try:
1642 try:
1642 self.hook("precommit", throw=True, parent1=hookp1,
1643 self.hook("precommit", throw=True, parent1=hookp1,
1643 parent2=hookp2)
1644 parent2=hookp2)
1644 tr = self.transaction('commit')
1645 tr = self.transaction('commit')
1645 ret = self.commitctx(cctx, True)
1646 ret = self.commitctx(cctx, True)
1646 except: # re-raises
1647 except: # re-raises
1647 if edited:
1648 if edited:
1648 self.ui.write(
1649 self.ui.write(
1649 _('note: commit message saved in %s\n') % msgfn)
1650 _('note: commit message saved in %s\n') % msgfn)
1650 raise
1651 raise
1651 # update bookmarks, dirstate and mergestate
1652 # update bookmarks, dirstate and mergestate
1652 bookmarks.update(self, [p1, p2], ret)
1653 bookmarks.update(self, [p1, p2], ret)
1653 cctx.markcommitted(ret)
1654 cctx.markcommitted(ret)
1654 ms.reset()
1655 ms.reset()
1655 tr.close()
1656 tr.close()
1656
1657
1657 finally:
1658 finally:
1658 lockmod.release(tr, lock, wlock)
1659 lockmod.release(tr, lock, wlock)
1659
1660
1660 def commithook(node=hex(ret), parent1=hookp1, parent2=hookp2):
1661 def commithook(node=hex(ret), parent1=hookp1, parent2=hookp2):
1661 # hack for command that use a temporary commit (eg: histedit)
1662 # hack for command that use a temporary commit (eg: histedit)
1662 # temporary commit got stripped before hook release
1663 # temporary commit got stripped before hook release
1663 if self.changelog.hasnode(ret):
1664 if self.changelog.hasnode(ret):
1664 self.hook("commit", node=node, parent1=parent1,
1665 self.hook("commit", node=node, parent1=parent1,
1665 parent2=parent2)
1666 parent2=parent2)
1666 self._afterlock(commithook)
1667 self._afterlock(commithook)
1667 return ret
1668 return ret
1668
1669
1669 @unfilteredmethod
1670 @unfilteredmethod
1670 def commitctx(self, ctx, error=False):
1671 def commitctx(self, ctx, error=False):
1671 """Add a new revision to current repository.
1672 """Add a new revision to current repository.
1672 Revision information is passed via the context argument.
1673 Revision information is passed via the context argument.
1673 """
1674 """
1674
1675
1675 tr = None
1676 tr = None
1676 p1, p2 = ctx.p1(), ctx.p2()
1677 p1, p2 = ctx.p1(), ctx.p2()
1677 user = ctx.user()
1678 user = ctx.user()
1678
1679
1679 lock = self.lock()
1680 lock = self.lock()
1680 try:
1681 try:
1681 tr = self.transaction("commit")
1682 tr = self.transaction("commit")
1682 trp = weakref.proxy(tr)
1683 trp = weakref.proxy(tr)
1683
1684
1684 if ctx.files():
1685 if ctx.files():
1685 m1 = p1.manifest()
1686 m1 = p1.manifest()
1686 m2 = p2.manifest()
1687 m2 = p2.manifest()
1687 m = m1.copy()
1688 m = m1.copy()
1688
1689
1689 # check in files
1690 # check in files
1690 added = []
1691 added = []
1691 changed = []
1692 changed = []
1692 removed = list(ctx.removed())
1693 removed = list(ctx.removed())
1693 linkrev = len(self)
1694 linkrev = len(self)
1694 self.ui.note(_("committing files:\n"))
1695 self.ui.note(_("committing files:\n"))
1695 for f in sorted(ctx.modified() + ctx.added()):
1696 for f in sorted(ctx.modified() + ctx.added()):
1696 self.ui.note(f + "\n")
1697 self.ui.note(f + "\n")
1697 try:
1698 try:
1698 fctx = ctx[f]
1699 fctx = ctx[f]
1699 if fctx is None:
1700 if fctx is None:
1700 removed.append(f)
1701 removed.append(f)
1701 else:
1702 else:
1702 added.append(f)
1703 added.append(f)
1703 m[f] = self._filecommit(fctx, m1, m2, linkrev,
1704 m[f] = self._filecommit(fctx, m1, m2, linkrev,
1704 trp, changed)
1705 trp, changed)
1705 m.setflag(f, fctx.flags())
1706 m.setflag(f, fctx.flags())
1706 except OSError as inst:
1707 except OSError as inst:
1707 self.ui.warn(_("trouble committing %s!\n") % f)
1708 self.ui.warn(_("trouble committing %s!\n") % f)
1708 raise
1709 raise
1709 except IOError as inst:
1710 except IOError as inst:
1710 errcode = getattr(inst, 'errno', errno.ENOENT)
1711 errcode = getattr(inst, 'errno', errno.ENOENT)
1711 if error or errcode and errcode != errno.ENOENT:
1712 if error or errcode and errcode != errno.ENOENT:
1712 self.ui.warn(_("trouble committing %s!\n") % f)
1713 self.ui.warn(_("trouble committing %s!\n") % f)
1713 raise
1714 raise
1714
1715
1715 # update manifest
1716 # update manifest
1716 self.ui.note(_("committing manifest\n"))
1717 self.ui.note(_("committing manifest\n"))
1717 removed = [f for f in sorted(removed) if f in m1 or f in m2]
1718 removed = [f for f in sorted(removed) if f in m1 or f in m2]
1718 drop = [f for f in removed if f in m]
1719 drop = [f for f in removed if f in m]
1719 for f in drop:
1720 for f in drop:
1720 del m[f]
1721 del m[f]
1721 mn = self.manifest.add(m, trp, linkrev,
1722 mn = self.manifest.add(m, trp, linkrev,
1722 p1.manifestnode(), p2.manifestnode(),
1723 p1.manifestnode(), p2.manifestnode(),
1723 added, drop)
1724 added, drop)
1724 files = changed + removed
1725 files = changed + removed
1725 else:
1726 else:
1726 mn = p1.manifestnode()
1727 mn = p1.manifestnode()
1727 files = []
1728 files = []
1728
1729
1729 # update changelog
1730 # update changelog
1730 self.ui.note(_("committing changelog\n"))
1731 self.ui.note(_("committing changelog\n"))
1731 self.changelog.delayupdate(tr)
1732 self.changelog.delayupdate(tr)
1732 n = self.changelog.add(mn, files, ctx.description(),
1733 n = self.changelog.add(mn, files, ctx.description(),
1733 trp, p1.node(), p2.node(),
1734 trp, p1.node(), p2.node(),
1734 user, ctx.date(), ctx.extra().copy())
1735 user, ctx.date(), ctx.extra().copy())
1735 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
1736 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
1736 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
1737 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
1737 parent2=xp2)
1738 parent2=xp2)
1738 # set the new commit is proper phase
1739 # set the new commit is proper phase
1739 targetphase = subrepo.newcommitphase(self.ui, ctx)
1740 targetphase = subrepo.newcommitphase(self.ui, ctx)
1740 if targetphase:
1741 if targetphase:
1741 # retract boundary do not alter parent changeset.
1742 # retract boundary do not alter parent changeset.
1742 # if a parent have higher the resulting phase will
1743 # if a parent have higher the resulting phase will
1743 # be compliant anyway
1744 # be compliant anyway
1744 #
1745 #
1745 # if minimal phase was 0 we don't need to retract anything
1746 # if minimal phase was 0 we don't need to retract anything
1746 phases.retractboundary(self, tr, targetphase, [n])
1747 phases.retractboundary(self, tr, targetphase, [n])
1747 tr.close()
1748 tr.close()
1748 branchmap.updatecache(self.filtered('served'))
1749 branchmap.updatecache(self.filtered('served'))
1749 return n
1750 return n
1750 finally:
1751 finally:
1751 if tr:
1752 if tr:
1752 tr.release()
1753 tr.release()
1753 lock.release()
1754 lock.release()
1754
1755
1755 @unfilteredmethod
1756 @unfilteredmethod
1756 def destroying(self):
1757 def destroying(self):
1757 '''Inform the repository that nodes are about to be destroyed.
1758 '''Inform the repository that nodes are about to be destroyed.
1758 Intended for use by strip and rollback, so there's a common
1759 Intended for use by strip and rollback, so there's a common
1759 place for anything that has to be done before destroying history.
1760 place for anything that has to be done before destroying history.
1760
1761
1761 This is mostly useful for saving state that is in memory and waiting
1762 This is mostly useful for saving state that is in memory and waiting
1762 to be flushed when the current lock is released. Because a call to
1763 to be flushed when the current lock is released. Because a call to
1763 destroyed is imminent, the repo will be invalidated causing those
1764 destroyed is imminent, the repo will be invalidated causing those
1764 changes to stay in memory (waiting for the next unlock), or vanish
1765 changes to stay in memory (waiting for the next unlock), or vanish
1765 completely.
1766 completely.
1766 '''
1767 '''
1767 # When using the same lock to commit and strip, the phasecache is left
1768 # When using the same lock to commit and strip, the phasecache is left
1768 # dirty after committing. Then when we strip, the repo is invalidated,
1769 # dirty after committing. Then when we strip, the repo is invalidated,
1769 # causing those changes to disappear.
1770 # causing those changes to disappear.
1770 if '_phasecache' in vars(self):
1771 if '_phasecache' in vars(self):
1771 self._phasecache.write()
1772 self._phasecache.write()
1772
1773
1773 @unfilteredmethod
1774 @unfilteredmethod
1774 def destroyed(self):
1775 def destroyed(self):
1775 '''Inform the repository that nodes have been destroyed.
1776 '''Inform the repository that nodes have been destroyed.
1776 Intended for use by strip and rollback, so there's a common
1777 Intended for use by strip and rollback, so there's a common
1777 place for anything that has to be done after destroying history.
1778 place for anything that has to be done after destroying history.
1778 '''
1779 '''
1779 # When one tries to:
1780 # When one tries to:
1780 # 1) destroy nodes thus calling this method (e.g. strip)
1781 # 1) destroy nodes thus calling this method (e.g. strip)
1781 # 2) use phasecache somewhere (e.g. commit)
1782 # 2) use phasecache somewhere (e.g. commit)
1782 #
1783 #
1783 # then 2) will fail because the phasecache contains nodes that were
1784 # then 2) will fail because the phasecache contains nodes that were
1784 # removed. We can either remove phasecache from the filecache,
1785 # removed. We can either remove phasecache from the filecache,
1785 # causing it to reload next time it is accessed, or simply filter
1786 # causing it to reload next time it is accessed, or simply filter
1786 # the removed nodes now and write the updated cache.
1787 # the removed nodes now and write the updated cache.
1787 self._phasecache.filterunknown(self)
1788 self._phasecache.filterunknown(self)
1788 self._phasecache.write()
1789 self._phasecache.write()
1789
1790
1790 # update the 'served' branch cache to help read only server process
1791 # update the 'served' branch cache to help read only server process
1791 # Thanks to branchcache collaboration this is done from the nearest
1792 # Thanks to branchcache collaboration this is done from the nearest
1792 # filtered subset and it is expected to be fast.
1793 # filtered subset and it is expected to be fast.
1793 branchmap.updatecache(self.filtered('served'))
1794 branchmap.updatecache(self.filtered('served'))
1794
1795
1795 # Ensure the persistent tag cache is updated. Doing it now
1796 # Ensure the persistent tag cache is updated. Doing it now
1796 # means that the tag cache only has to worry about destroyed
1797 # means that the tag cache only has to worry about destroyed
1797 # heads immediately after a strip/rollback. That in turn
1798 # heads immediately after a strip/rollback. That in turn
1798 # guarantees that "cachetip == currenttip" (comparing both rev
1799 # guarantees that "cachetip == currenttip" (comparing both rev
1799 # and node) always means no nodes have been added or destroyed.
1800 # and node) always means no nodes have been added or destroyed.
1800
1801
1801 # XXX this is suboptimal when qrefresh'ing: we strip the current
1802 # XXX this is suboptimal when qrefresh'ing: we strip the current
1802 # head, refresh the tag cache, then immediately add a new head.
1803 # head, refresh the tag cache, then immediately add a new head.
1803 # But I think doing it this way is necessary for the "instant
1804 # But I think doing it this way is necessary for the "instant
1804 # tag cache retrieval" case to work.
1805 # tag cache retrieval" case to work.
1805 self.invalidate()
1806 self.invalidate()
1806
1807
1807 def walk(self, match, node=None):
1808 def walk(self, match, node=None):
1808 '''
1809 '''
1809 walk recursively through the directory tree or a given
1810 walk recursively through the directory tree or a given
1810 changeset, finding all files matched by the match
1811 changeset, finding all files matched by the match
1811 function
1812 function
1812 '''
1813 '''
1813 return self[node].walk(match)
1814 return self[node].walk(match)
1814
1815
1815 def status(self, node1='.', node2=None, match=None,
1816 def status(self, node1='.', node2=None, match=None,
1816 ignored=False, clean=False, unknown=False,
1817 ignored=False, clean=False, unknown=False,
1817 listsubrepos=False):
1818 listsubrepos=False):
1818 '''a convenience method that calls node1.status(node2)'''
1819 '''a convenience method that calls node1.status(node2)'''
1819 return self[node1].status(node2, match, ignored, clean, unknown,
1820 return self[node1].status(node2, match, ignored, clean, unknown,
1820 listsubrepos)
1821 listsubrepos)
1821
1822
1822 def heads(self, start=None):
1823 def heads(self, start=None):
1823 heads = self.changelog.heads(start)
1824 heads = self.changelog.heads(start)
1824 # sort the output in rev descending order
1825 # sort the output in rev descending order
1825 return sorted(heads, key=self.changelog.rev, reverse=True)
1826 return sorted(heads, key=self.changelog.rev, reverse=True)
1826
1827
1827 def branchheads(self, branch=None, start=None, closed=False):
1828 def branchheads(self, branch=None, start=None, closed=False):
1828 '''return a (possibly filtered) list of heads for the given branch
1829 '''return a (possibly filtered) list of heads for the given branch
1829
1830
1830 Heads are returned in topological order, from newest to oldest.
1831 Heads are returned in topological order, from newest to oldest.
1831 If branch is None, use the dirstate branch.
1832 If branch is None, use the dirstate branch.
1832 If start is not None, return only heads reachable from start.
1833 If start is not None, return only heads reachable from start.
1833 If closed is True, return heads that are marked as closed as well.
1834 If closed is True, return heads that are marked as closed as well.
1834 '''
1835 '''
1835 if branch is None:
1836 if branch is None:
1836 branch = self[None].branch()
1837 branch = self[None].branch()
1837 branches = self.branchmap()
1838 branches = self.branchmap()
1838 if branch not in branches:
1839 if branch not in branches:
1839 return []
1840 return []
1840 # the cache returns heads ordered lowest to highest
1841 # the cache returns heads ordered lowest to highest
1841 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
1842 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
1842 if start is not None:
1843 if start is not None:
1843 # filter out the heads that cannot be reached from startrev
1844 # filter out the heads that cannot be reached from startrev
1844 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
1845 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
1845 bheads = [h for h in bheads if h in fbheads]
1846 bheads = [h for h in bheads if h in fbheads]
1846 return bheads
1847 return bheads
1847
1848
1848 def branches(self, nodes):
1849 def branches(self, nodes):
1849 if not nodes:
1850 if not nodes:
1850 nodes = [self.changelog.tip()]
1851 nodes = [self.changelog.tip()]
1851 b = []
1852 b = []
1852 for n in nodes:
1853 for n in nodes:
1853 t = n
1854 t = n
1854 while True:
1855 while True:
1855 p = self.changelog.parents(n)
1856 p = self.changelog.parents(n)
1856 if p[1] != nullid or p[0] == nullid:
1857 if p[1] != nullid or p[0] == nullid:
1857 b.append((t, n, p[0], p[1]))
1858 b.append((t, n, p[0], p[1]))
1858 break
1859 break
1859 n = p[0]
1860 n = p[0]
1860 return b
1861 return b
1861
1862
1862 def between(self, pairs):
1863 def between(self, pairs):
1863 r = []
1864 r = []
1864
1865
1865 for top, bottom in pairs:
1866 for top, bottom in pairs:
1866 n, l, i = top, [], 0
1867 n, l, i = top, [], 0
1867 f = 1
1868 f = 1
1868
1869
1869 while n != bottom and n != nullid:
1870 while n != bottom and n != nullid:
1870 p = self.changelog.parents(n)[0]
1871 p = self.changelog.parents(n)[0]
1871 if i == f:
1872 if i == f:
1872 l.append(n)
1873 l.append(n)
1873 f = f * 2
1874 f = f * 2
1874 n = p
1875 n = p
1875 i += 1
1876 i += 1
1876
1877
1877 r.append(l)
1878 r.append(l)
1878
1879
1879 return r
1880 return r
1880
1881
1881 def checkpush(self, pushop):
1882 def checkpush(self, pushop):
1882 """Extensions can override this function if additional checks have
1883 """Extensions can override this function if additional checks have
1883 to be performed before pushing, or call it if they override push
1884 to be performed before pushing, or call it if they override push
1884 command.
1885 command.
1885 """
1886 """
1886 pass
1887 pass
1887
1888
1888 @unfilteredpropertycache
1889 @unfilteredpropertycache
1889 def prepushoutgoinghooks(self):
1890 def prepushoutgoinghooks(self):
1890 """Return util.hooks consists of a pushop with repo, remote, outgoing
1891 """Return util.hooks consists of a pushop with repo, remote, outgoing
1891 methods, which are called before pushing changesets.
1892 methods, which are called before pushing changesets.
1892 """
1893 """
1893 return util.hooks()
1894 return util.hooks()
1894
1895
1895 def pushkey(self, namespace, key, old, new):
1896 def pushkey(self, namespace, key, old, new):
1896 try:
1897 try:
1897 tr = self.currenttransaction()
1898 tr = self.currenttransaction()
1898 hookargs = {}
1899 hookargs = {}
1899 if tr is not None:
1900 if tr is not None:
1900 hookargs.update(tr.hookargs)
1901 hookargs.update(tr.hookargs)
1901 hookargs['namespace'] = namespace
1902 hookargs['namespace'] = namespace
1902 hookargs['key'] = key
1903 hookargs['key'] = key
1903 hookargs['old'] = old
1904 hookargs['old'] = old
1904 hookargs['new'] = new
1905 hookargs['new'] = new
1905 self.hook('prepushkey', throw=True, **hookargs)
1906 self.hook('prepushkey', throw=True, **hookargs)
1906 except error.HookAbort as exc:
1907 except error.HookAbort as exc:
1907 self.ui.write_err(_("pushkey-abort: %s\n") % exc)
1908 self.ui.write_err(_("pushkey-abort: %s\n") % exc)
1908 if exc.hint:
1909 if exc.hint:
1909 self.ui.write_err(_("(%s)\n") % exc.hint)
1910 self.ui.write_err(_("(%s)\n") % exc.hint)
1910 return False
1911 return False
1911 self.ui.debug('pushing key for "%s:%s"\n' % (namespace, key))
1912 self.ui.debug('pushing key for "%s:%s"\n' % (namespace, key))
1912 ret = pushkey.push(self, namespace, key, old, new)
1913 ret = pushkey.push(self, namespace, key, old, new)
1913 def runhook():
1914 def runhook():
1914 self.hook('pushkey', namespace=namespace, key=key, old=old, new=new,
1915 self.hook('pushkey', namespace=namespace, key=key, old=old, new=new,
1915 ret=ret)
1916 ret=ret)
1916 self._afterlock(runhook)
1917 self._afterlock(runhook)
1917 return ret
1918 return ret
1918
1919
1919 def listkeys(self, namespace):
1920 def listkeys(self, namespace):
1920 self.hook('prelistkeys', throw=True, namespace=namespace)
1921 self.hook('prelistkeys', throw=True, namespace=namespace)
1921 self.ui.debug('listing keys for "%s"\n' % namespace)
1922 self.ui.debug('listing keys for "%s"\n' % namespace)
1922 values = pushkey.list(self, namespace)
1923 values = pushkey.list(self, namespace)
1923 self.hook('listkeys', namespace=namespace, values=values)
1924 self.hook('listkeys', namespace=namespace, values=values)
1924 return values
1925 return values
1925
1926
1926 def debugwireargs(self, one, two, three=None, four=None, five=None):
1927 def debugwireargs(self, one, two, three=None, four=None, five=None):
1927 '''used to test argument passing over the wire'''
1928 '''used to test argument passing over the wire'''
1928 return "%s %s %s %s %s" % (one, two, three, four, five)
1929 return "%s %s %s %s %s" % (one, two, three, four, five)
1929
1930
1930 def savecommitmessage(self, text):
1931 def savecommitmessage(self, text):
1931 fp = self.vfs('last-message.txt', 'wb')
1932 fp = self.vfs('last-message.txt', 'wb')
1932 try:
1933 try:
1933 fp.write(text)
1934 fp.write(text)
1934 finally:
1935 finally:
1935 fp.close()
1936 fp.close()
1936 return self.pathto(fp.name[len(self.root) + 1:])
1937 return self.pathto(fp.name[len(self.root) + 1:])
1937
1938
1938 # used to avoid circular references so destructors work
1939 # used to avoid circular references so destructors work
1939 def aftertrans(files):
1940 def aftertrans(files):
1940 renamefiles = [tuple(t) for t in files]
1941 renamefiles = [tuple(t) for t in files]
1941 def a():
1942 def a():
1942 for vfs, src, dest in renamefiles:
1943 for vfs, src, dest in renamefiles:
1943 try:
1944 try:
1944 vfs.rename(src, dest)
1945 vfs.rename(src, dest)
1945 except OSError: # journal file does not yet exist
1946 except OSError: # journal file does not yet exist
1946 pass
1947 pass
1947 return a
1948 return a
1948
1949
1949 def undoname(fn):
1950 def undoname(fn):
1950 base, name = os.path.split(fn)
1951 base, name = os.path.split(fn)
1951 assert name.startswith('journal')
1952 assert name.startswith('journal')
1952 return os.path.join(base, name.replace('journal', 'undo', 1))
1953 return os.path.join(base, name.replace('journal', 'undo', 1))
1953
1954
1954 def instance(ui, path, create):
1955 def instance(ui, path, create):
1955 return localrepository(ui, util.urllocalpath(path), create)
1956 return localrepository(ui, util.urllocalpath(path), create)
1956
1957
1957 def islocal(path):
1958 def islocal(path):
1958 return True
1959 return True
1959
1960
1960 def newreporequirements(repo):
1961 def newreporequirements(repo):
1961 """Determine the set of requirements for a new local repository.
1962 """Determine the set of requirements for a new local repository.
1962
1963
1963 Extensions can wrap this function to specify custom requirements for
1964 Extensions can wrap this function to specify custom requirements for
1964 new repositories.
1965 new repositories.
1965 """
1966 """
1966 ui = repo.ui
1967 ui = repo.ui
1967 requirements = set(['revlogv1'])
1968 requirements = set(['revlogv1'])
1968 if ui.configbool('format', 'usestore', True):
1969 if ui.configbool('format', 'usestore', True):
1969 requirements.add('store')
1970 requirements.add('store')
1970 if ui.configbool('format', 'usefncache', True):
1971 if ui.configbool('format', 'usefncache', True):
1971 requirements.add('fncache')
1972 requirements.add('fncache')
1972 if ui.configbool('format', 'dotencode', True):
1973 if ui.configbool('format', 'dotencode', True):
1973 requirements.add('dotencode')
1974 requirements.add('dotencode')
1974
1975
1975 if scmutil.gdinitconfig(ui):
1976 if scmutil.gdinitconfig(ui):
1976 requirements.add('generaldelta')
1977 requirements.add('generaldelta')
1977 if ui.configbool('experimental', 'treemanifest', False):
1978 if ui.configbool('experimental', 'treemanifest', False):
1978 requirements.add('treemanifest')
1979 requirements.add('treemanifest')
1979 if ui.configbool('experimental', 'manifestv2', False):
1980 if ui.configbool('experimental', 'manifestv2', False):
1980 requirements.add('manifestv2')
1981 requirements.add('manifestv2')
1981
1982
1982 return requirements
1983 return requirements
@@ -1,186 +1,187 b''
1 # statichttprepo.py - simple http repository class for mercurial
1 # statichttprepo.py - simple http repository class for mercurial
2 #
2 #
3 # This provides read-only repo access to repositories exported via static http
3 # This provides read-only repo access to repositories exported via static http
4 #
4 #
5 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import errno
12 import errno
13 import os
13 import os
14 import urllib
15 import urllib2
16
14
17 from .i18n import _
15 from .i18n import _
18 from . import (
16 from . import (
19 byterange,
17 byterange,
20 changelog,
18 changelog,
21 error,
19 error,
22 localrepo,
20 localrepo,
23 manifest,
21 manifest,
24 namespaces,
22 namespaces,
25 scmutil,
23 scmutil,
26 store,
24 store,
27 url,
25 url,
28 util,
26 util,
29 )
27 )
30
28
29 urlerr = util.urlerr
30 urlreq = util.urlreq
31
31 class httprangereader(object):
32 class httprangereader(object):
32 def __init__(self, url, opener):
33 def __init__(self, url, opener):
33 # we assume opener has HTTPRangeHandler
34 # we assume opener has HTTPRangeHandler
34 self.url = url
35 self.url = url
35 self.pos = 0
36 self.pos = 0
36 self.opener = opener
37 self.opener = opener
37 self.name = url
38 self.name = url
38
39
39 def __enter__(self):
40 def __enter__(self):
40 return self
41 return self
41
42
42 def __exit__(self, exc_type, exc_value, traceback):
43 def __exit__(self, exc_type, exc_value, traceback):
43 self.close()
44 self.close()
44
45
45 def seek(self, pos):
46 def seek(self, pos):
46 self.pos = pos
47 self.pos = pos
47 def read(self, bytes=None):
48 def read(self, bytes=None):
48 req = urllib2.Request(self.url)
49 req = urlreq.request(self.url)
49 end = ''
50 end = ''
50 if bytes:
51 if bytes:
51 end = self.pos + bytes - 1
52 end = self.pos + bytes - 1
52 if self.pos or end:
53 if self.pos or end:
53 req.add_header('Range', 'bytes=%d-%s' % (self.pos, end))
54 req.add_header('Range', 'bytes=%d-%s' % (self.pos, end))
54
55
55 try:
56 try:
56 f = self.opener.open(req)
57 f = self.opener.open(req)
57 data = f.read()
58 data = f.read()
58 code = f.code
59 code = f.code
59 except urllib2.HTTPError as inst:
60 except urlerr.httperror as inst:
60 num = inst.code == 404 and errno.ENOENT or None
61 num = inst.code == 404 and errno.ENOENT or None
61 raise IOError(num, inst)
62 raise IOError(num, inst)
62 except urllib2.URLError as inst:
63 except urlerr.urlerror as inst:
63 raise IOError(None, inst.reason[1])
64 raise IOError(None, inst.reason[1])
64
65
65 if code == 200:
66 if code == 200:
66 # HTTPRangeHandler does nothing if remote does not support
67 # HTTPRangeHandler does nothing if remote does not support
67 # Range headers and returns the full entity. Let's slice it.
68 # Range headers and returns the full entity. Let's slice it.
68 if bytes:
69 if bytes:
69 data = data[self.pos:self.pos + bytes]
70 data = data[self.pos:self.pos + bytes]
70 else:
71 else:
71 data = data[self.pos:]
72 data = data[self.pos:]
72 elif bytes:
73 elif bytes:
73 data = data[:bytes]
74 data = data[:bytes]
74 self.pos += len(data)
75 self.pos += len(data)
75 return data
76 return data
76 def readlines(self):
77 def readlines(self):
77 return self.read().splitlines(True)
78 return self.read().splitlines(True)
78 def __iter__(self):
79 def __iter__(self):
79 return iter(self.readlines())
80 return iter(self.readlines())
80 def close(self):
81 def close(self):
81 pass
82 pass
82
83
83 def build_opener(ui, authinfo):
84 def build_opener(ui, authinfo):
84 # urllib cannot handle URLs with embedded user or passwd
85 # urllib cannot handle URLs with embedded user or passwd
85 urlopener = url.opener(ui, authinfo)
86 urlopener = url.opener(ui, authinfo)
86 urlopener.add_handler(byterange.HTTPRangeHandler())
87 urlopener.add_handler(byterange.HTTPRangeHandler())
87
88
88 class statichttpvfs(scmutil.abstractvfs):
89 class statichttpvfs(scmutil.abstractvfs):
89 def __init__(self, base):
90 def __init__(self, base):
90 self.base = base
91 self.base = base
91
92
92 def __call__(self, path, mode='r', *args, **kw):
93 def __call__(self, path, mode='r', *args, **kw):
93 if mode not in ('r', 'rb'):
94 if mode not in ('r', 'rb'):
94 raise IOError('Permission denied')
95 raise IOError('Permission denied')
95 f = "/".join((self.base, urllib.quote(path)))
96 f = "/".join((self.base, urlreq.quote(path)))
96 return httprangereader(f, urlopener)
97 return httprangereader(f, urlopener)
97
98
98 def join(self, path):
99 def join(self, path):
99 if path:
100 if path:
100 return os.path.join(self.base, path)
101 return os.path.join(self.base, path)
101 else:
102 else:
102 return self.base
103 return self.base
103
104
104 return statichttpvfs
105 return statichttpvfs
105
106
106 class statichttppeer(localrepo.localpeer):
107 class statichttppeer(localrepo.localpeer):
107 def local(self):
108 def local(self):
108 return None
109 return None
109 def canpush(self):
110 def canpush(self):
110 return False
111 return False
111
112
112 class statichttprepository(localrepo.localrepository):
113 class statichttprepository(localrepo.localrepository):
113 supported = localrepo.localrepository._basesupported
114 supported = localrepo.localrepository._basesupported
114
115
115 def __init__(self, ui, path):
116 def __init__(self, ui, path):
116 self._url = path
117 self._url = path
117 self.ui = ui
118 self.ui = ui
118
119
119 self.root = path
120 self.root = path
120 u = util.url(path.rstrip('/') + "/.hg")
121 u = util.url(path.rstrip('/') + "/.hg")
121 self.path, authinfo = u.authinfo()
122 self.path, authinfo = u.authinfo()
122
123
123 opener = build_opener(ui, authinfo)
124 opener = build_opener(ui, authinfo)
124 self.opener = opener(self.path)
125 self.opener = opener(self.path)
125 self.vfs = self.opener
126 self.vfs = self.opener
126 self._phasedefaults = []
127 self._phasedefaults = []
127
128
128 self.names = namespaces.namespaces()
129 self.names = namespaces.namespaces()
129
130
130 try:
131 try:
131 requirements = scmutil.readrequires(self.vfs, self.supported)
132 requirements = scmutil.readrequires(self.vfs, self.supported)
132 except IOError as inst:
133 except IOError as inst:
133 if inst.errno != errno.ENOENT:
134 if inst.errno != errno.ENOENT:
134 raise
135 raise
135 requirements = set()
136 requirements = set()
136
137
137 # check if it is a non-empty old-style repository
138 # check if it is a non-empty old-style repository
138 try:
139 try:
139 fp = self.vfs("00changelog.i")
140 fp = self.vfs("00changelog.i")
140 fp.read(1)
141 fp.read(1)
141 fp.close()
142 fp.close()
142 except IOError as inst:
143 except IOError as inst:
143 if inst.errno != errno.ENOENT:
144 if inst.errno != errno.ENOENT:
144 raise
145 raise
145 # we do not care about empty old-style repositories here
146 # we do not care about empty old-style repositories here
146 msg = _("'%s' does not appear to be an hg repository") % path
147 msg = _("'%s' does not appear to be an hg repository") % path
147 raise error.RepoError(msg)
148 raise error.RepoError(msg)
148
149
149 # setup store
150 # setup store
150 self.store = store.store(requirements, self.path, opener)
151 self.store = store.store(requirements, self.path, opener)
151 self.spath = self.store.path
152 self.spath = self.store.path
152 self.svfs = self.store.opener
153 self.svfs = self.store.opener
153 self.sjoin = self.store.join
154 self.sjoin = self.store.join
154 self._filecache = {}
155 self._filecache = {}
155 self.requirements = requirements
156 self.requirements = requirements
156
157
157 self.manifest = manifest.manifest(self.svfs)
158 self.manifest = manifest.manifest(self.svfs)
158 self.changelog = changelog.changelog(self.svfs)
159 self.changelog = changelog.changelog(self.svfs)
159 self._tags = None
160 self._tags = None
160 self.nodetagscache = None
161 self.nodetagscache = None
161 self._branchcaches = {}
162 self._branchcaches = {}
162 self._revbranchcache = None
163 self._revbranchcache = None
163 self.encodepats = None
164 self.encodepats = None
164 self.decodepats = None
165 self.decodepats = None
165 self._transref = None
166 self._transref = None
166
167
167 def _restrictcapabilities(self, caps):
168 def _restrictcapabilities(self, caps):
168 caps = super(statichttprepository, self)._restrictcapabilities(caps)
169 caps = super(statichttprepository, self)._restrictcapabilities(caps)
169 return caps.difference(["pushkey"])
170 return caps.difference(["pushkey"])
170
171
171 def url(self):
172 def url(self):
172 return self._url
173 return self._url
173
174
174 def local(self):
175 def local(self):
175 return False
176 return False
176
177
177 def peer(self):
178 def peer(self):
178 return statichttppeer(self)
179 return statichttppeer(self)
179
180
180 def lock(self, wait=True):
181 def lock(self, wait=True):
181 raise error.Abort(_('cannot lock static-http repository'))
182 raise error.Abort(_('cannot lock static-http repository'))
182
183
183 def instance(ui, path, create):
184 def instance(ui, path, create):
184 if create:
185 if create:
185 raise error.Abort(_('cannot create new static-http repository'))
186 raise error.Abort(_('cannot create new static-http repository'))
186 return statichttprepository(ui, path[7:])
187 return statichttprepository(ui, path[7:])
@@ -1,431 +1,433 b''
1 # template-filters.py - common template expansion filters
1 # template-filters.py - common template expansion filters
2 #
2 #
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import cgi
10 import cgi
11 import os
11 import os
12 import re
12 import re
13 import time
13 import time
14 import urllib
15
14
16 from . import (
15 from . import (
17 encoding,
16 encoding,
18 hbisect,
17 hbisect,
19 node,
18 node,
20 registrar,
19 registrar,
21 templatekw,
20 templatekw,
22 util,
21 util,
23 )
22 )
24
23
24 urlerr = util.urlerr
25 urlreq = util.urlreq
26
25 # filters are callables like:
27 # filters are callables like:
26 # fn(obj)
28 # fn(obj)
27 # with:
29 # with:
28 # obj - object to be filtered (text, date, list and so on)
30 # obj - object to be filtered (text, date, list and so on)
29 filters = {}
31 filters = {}
30
32
31 templatefilter = registrar.templatefilter(filters)
33 templatefilter = registrar.templatefilter(filters)
32
34
33 @templatefilter('addbreaks')
35 @templatefilter('addbreaks')
34 def addbreaks(text):
36 def addbreaks(text):
35 """Any text. Add an XHTML "<br />" tag before the end of
37 """Any text. Add an XHTML "<br />" tag before the end of
36 every line except the last.
38 every line except the last.
37 """
39 """
38 return text.replace('\n', '<br/>\n')
40 return text.replace('\n', '<br/>\n')
39
41
40 agescales = [("year", 3600 * 24 * 365, 'Y'),
42 agescales = [("year", 3600 * 24 * 365, 'Y'),
41 ("month", 3600 * 24 * 30, 'M'),
43 ("month", 3600 * 24 * 30, 'M'),
42 ("week", 3600 * 24 * 7, 'W'),
44 ("week", 3600 * 24 * 7, 'W'),
43 ("day", 3600 * 24, 'd'),
45 ("day", 3600 * 24, 'd'),
44 ("hour", 3600, 'h'),
46 ("hour", 3600, 'h'),
45 ("minute", 60, 'm'),
47 ("minute", 60, 'm'),
46 ("second", 1, 's')]
48 ("second", 1, 's')]
47
49
48 @templatefilter('age')
50 @templatefilter('age')
49 def age(date, abbrev=False):
51 def age(date, abbrev=False):
50 """Date. Returns a human-readable date/time difference between the
52 """Date. Returns a human-readable date/time difference between the
51 given date/time and the current date/time.
53 given date/time and the current date/time.
52 """
54 """
53
55
54 def plural(t, c):
56 def plural(t, c):
55 if c == 1:
57 if c == 1:
56 return t
58 return t
57 return t + "s"
59 return t + "s"
58 def fmt(t, c, a):
60 def fmt(t, c, a):
59 if abbrev:
61 if abbrev:
60 return "%d%s" % (c, a)
62 return "%d%s" % (c, a)
61 return "%d %s" % (c, plural(t, c))
63 return "%d %s" % (c, plural(t, c))
62
64
63 now = time.time()
65 now = time.time()
64 then = date[0]
66 then = date[0]
65 future = False
67 future = False
66 if then > now:
68 if then > now:
67 future = True
69 future = True
68 delta = max(1, int(then - now))
70 delta = max(1, int(then - now))
69 if delta > agescales[0][1] * 30:
71 if delta > agescales[0][1] * 30:
70 return 'in the distant future'
72 return 'in the distant future'
71 else:
73 else:
72 delta = max(1, int(now - then))
74 delta = max(1, int(now - then))
73 if delta > agescales[0][1] * 2:
75 if delta > agescales[0][1] * 2:
74 return util.shortdate(date)
76 return util.shortdate(date)
75
77
76 for t, s, a in agescales:
78 for t, s, a in agescales:
77 n = delta // s
79 n = delta // s
78 if n >= 2 or s == 1:
80 if n >= 2 or s == 1:
79 if future:
81 if future:
80 return '%s from now' % fmt(t, n, a)
82 return '%s from now' % fmt(t, n, a)
81 return '%s ago' % fmt(t, n, a)
83 return '%s ago' % fmt(t, n, a)
82
84
83 @templatefilter('basename')
85 @templatefilter('basename')
84 def basename(path):
86 def basename(path):
85 """Any text. Treats the text as a path, and returns the last
87 """Any text. Treats the text as a path, and returns the last
86 component of the path after splitting by the path separator
88 component of the path after splitting by the path separator
87 (ignoring trailing separators). For example, "foo/bar/baz" becomes
89 (ignoring trailing separators). For example, "foo/bar/baz" becomes
88 "baz" and "foo/bar//" becomes "bar".
90 "baz" and "foo/bar//" becomes "bar".
89 """
91 """
90 return os.path.basename(path)
92 return os.path.basename(path)
91
93
92 @templatefilter('count')
94 @templatefilter('count')
93 def count(i):
95 def count(i):
94 """List or text. Returns the length as an integer."""
96 """List or text. Returns the length as an integer."""
95 return len(i)
97 return len(i)
96
98
97 @templatefilter('domain')
99 @templatefilter('domain')
98 def domain(author):
100 def domain(author):
99 """Any text. Finds the first string that looks like an email
101 """Any text. Finds the first string that looks like an email
100 address, and extracts just the domain component. Example: ``User
102 address, and extracts just the domain component. Example: ``User
101 <user@example.com>`` becomes ``example.com``.
103 <user@example.com>`` becomes ``example.com``.
102 """
104 """
103 f = author.find('@')
105 f = author.find('@')
104 if f == -1:
106 if f == -1:
105 return ''
107 return ''
106 author = author[f + 1:]
108 author = author[f + 1:]
107 f = author.find('>')
109 f = author.find('>')
108 if f >= 0:
110 if f >= 0:
109 author = author[:f]
111 author = author[:f]
110 return author
112 return author
111
113
112 @templatefilter('email')
114 @templatefilter('email')
113 def email(text):
115 def email(text):
114 """Any text. Extracts the first string that looks like an email
116 """Any text. Extracts the first string that looks like an email
115 address. Example: ``User <user@example.com>`` becomes
117 address. Example: ``User <user@example.com>`` becomes
116 ``user@example.com``.
118 ``user@example.com``.
117 """
119 """
118 return util.email(text)
120 return util.email(text)
119
121
120 @templatefilter('escape')
122 @templatefilter('escape')
121 def escape(text):
123 def escape(text):
122 """Any text. Replaces the special XML/XHTML characters "&", "<"
124 """Any text. Replaces the special XML/XHTML characters "&", "<"
123 and ">" with XML entities, and filters out NUL characters.
125 and ">" with XML entities, and filters out NUL characters.
124 """
126 """
125 return cgi.escape(text.replace('\0', ''), True)
127 return cgi.escape(text.replace('\0', ''), True)
126
128
127 para_re = None
129 para_re = None
128 space_re = None
130 space_re = None
129
131
130 def fill(text, width, initindent='', hangindent=''):
132 def fill(text, width, initindent='', hangindent=''):
131 '''fill many paragraphs with optional indentation.'''
133 '''fill many paragraphs with optional indentation.'''
132 global para_re, space_re
134 global para_re, space_re
133 if para_re is None:
135 if para_re is None:
134 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
136 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
135 space_re = re.compile(r' +')
137 space_re = re.compile(r' +')
136
138
137 def findparas():
139 def findparas():
138 start = 0
140 start = 0
139 while True:
141 while True:
140 m = para_re.search(text, start)
142 m = para_re.search(text, start)
141 if not m:
143 if not m:
142 uctext = unicode(text[start:], encoding.encoding)
144 uctext = unicode(text[start:], encoding.encoding)
143 w = len(uctext)
145 w = len(uctext)
144 while 0 < w and uctext[w - 1].isspace():
146 while 0 < w and uctext[w - 1].isspace():
145 w -= 1
147 w -= 1
146 yield (uctext[:w].encode(encoding.encoding),
148 yield (uctext[:w].encode(encoding.encoding),
147 uctext[w:].encode(encoding.encoding))
149 uctext[w:].encode(encoding.encoding))
148 break
150 break
149 yield text[start:m.start(0)], m.group(1)
151 yield text[start:m.start(0)], m.group(1)
150 start = m.end(1)
152 start = m.end(1)
151
153
152 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
154 return "".join([util.wrap(space_re.sub(' ', util.wrap(para, width)),
153 width, initindent, hangindent) + rest
155 width, initindent, hangindent) + rest
154 for para, rest in findparas()])
156 for para, rest in findparas()])
155
157
156 @templatefilter('fill68')
158 @templatefilter('fill68')
157 def fill68(text):
159 def fill68(text):
158 """Any text. Wraps the text to fit in 68 columns."""
160 """Any text. Wraps the text to fit in 68 columns."""
159 return fill(text, 68)
161 return fill(text, 68)
160
162
161 @templatefilter('fill76')
163 @templatefilter('fill76')
162 def fill76(text):
164 def fill76(text):
163 """Any text. Wraps the text to fit in 76 columns."""
165 """Any text. Wraps the text to fit in 76 columns."""
164 return fill(text, 76)
166 return fill(text, 76)
165
167
166 @templatefilter('firstline')
168 @templatefilter('firstline')
167 def firstline(text):
169 def firstline(text):
168 """Any text. Returns the first line of text."""
170 """Any text. Returns the first line of text."""
169 try:
171 try:
170 return text.splitlines(True)[0].rstrip('\r\n')
172 return text.splitlines(True)[0].rstrip('\r\n')
171 except IndexError:
173 except IndexError:
172 return ''
174 return ''
173
175
174 @templatefilter('hex')
176 @templatefilter('hex')
175 def hexfilter(text):
177 def hexfilter(text):
176 """Any text. Convert a binary Mercurial node identifier into
178 """Any text. Convert a binary Mercurial node identifier into
177 its long hexadecimal representation.
179 its long hexadecimal representation.
178 """
180 """
179 return node.hex(text)
181 return node.hex(text)
180
182
181 @templatefilter('hgdate')
183 @templatefilter('hgdate')
182 def hgdate(text):
184 def hgdate(text):
183 """Date. Returns the date as a pair of numbers: "1157407993
185 """Date. Returns the date as a pair of numbers: "1157407993
184 25200" (Unix timestamp, timezone offset).
186 25200" (Unix timestamp, timezone offset).
185 """
187 """
186 return "%d %d" % text
188 return "%d %d" % text
187
189
188 @templatefilter('isodate')
190 @templatefilter('isodate')
189 def isodate(text):
191 def isodate(text):
190 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
192 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
191 +0200".
193 +0200".
192 """
194 """
193 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
195 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
194
196
195 @templatefilter('isodatesec')
197 @templatefilter('isodatesec')
196 def isodatesec(text):
198 def isodatesec(text):
197 """Date. Returns the date in ISO 8601 format, including
199 """Date. Returns the date in ISO 8601 format, including
198 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
200 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
199 filter.
201 filter.
200 """
202 """
201 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
203 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
202
204
203 def indent(text, prefix):
205 def indent(text, prefix):
204 '''indent each non-empty line of text after first with prefix.'''
206 '''indent each non-empty line of text after first with prefix.'''
205 lines = text.splitlines()
207 lines = text.splitlines()
206 num_lines = len(lines)
208 num_lines = len(lines)
207 endswithnewline = text[-1:] == '\n'
209 endswithnewline = text[-1:] == '\n'
208 def indenter():
210 def indenter():
209 for i in xrange(num_lines):
211 for i in xrange(num_lines):
210 l = lines[i]
212 l = lines[i]
211 if i and l.strip():
213 if i and l.strip():
212 yield prefix
214 yield prefix
213 yield l
215 yield l
214 if i < num_lines - 1 or endswithnewline:
216 if i < num_lines - 1 or endswithnewline:
215 yield '\n'
217 yield '\n'
216 return "".join(indenter())
218 return "".join(indenter())
217
219
218 @templatefilter('json')
220 @templatefilter('json')
219 def json(obj):
221 def json(obj):
220 if obj is None or obj is False or obj is True:
222 if obj is None or obj is False or obj is True:
221 return {None: 'null', False: 'false', True: 'true'}[obj]
223 return {None: 'null', False: 'false', True: 'true'}[obj]
222 elif isinstance(obj, int) or isinstance(obj, float):
224 elif isinstance(obj, int) or isinstance(obj, float):
223 return str(obj)
225 return str(obj)
224 elif isinstance(obj, str):
226 elif isinstance(obj, str):
225 return '"%s"' % encoding.jsonescape(obj, paranoid=True)
227 return '"%s"' % encoding.jsonescape(obj, paranoid=True)
226 elif util.safehasattr(obj, 'keys'):
228 elif util.safehasattr(obj, 'keys'):
227 out = []
229 out = []
228 for k, v in sorted(obj.iteritems()):
230 for k, v in sorted(obj.iteritems()):
229 s = '%s: %s' % (json(k), json(v))
231 s = '%s: %s' % (json(k), json(v))
230 out.append(s)
232 out.append(s)
231 return '{' + ', '.join(out) + '}'
233 return '{' + ', '.join(out) + '}'
232 elif util.safehasattr(obj, '__iter__'):
234 elif util.safehasattr(obj, '__iter__'):
233 out = []
235 out = []
234 for i in obj:
236 for i in obj:
235 out.append(json(i))
237 out.append(json(i))
236 return '[' + ', '.join(out) + ']'
238 return '[' + ', '.join(out) + ']'
237 elif util.safehasattr(obj, '__call__'):
239 elif util.safehasattr(obj, '__call__'):
238 return json(obj())
240 return json(obj())
239 else:
241 else:
240 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
242 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
241
243
242 @templatefilter('lower')
244 @templatefilter('lower')
243 def lower(text):
245 def lower(text):
244 """Any text. Converts the text to lowercase."""
246 """Any text. Converts the text to lowercase."""
245 return encoding.lower(text)
247 return encoding.lower(text)
246
248
247 @templatefilter('nonempty')
249 @templatefilter('nonempty')
248 def nonempty(str):
250 def nonempty(str):
249 """Any text. Returns '(none)' if the string is empty."""
251 """Any text. Returns '(none)' if the string is empty."""
250 return str or "(none)"
252 return str or "(none)"
251
253
252 @templatefilter('obfuscate')
254 @templatefilter('obfuscate')
253 def obfuscate(text):
255 def obfuscate(text):
254 """Any text. Returns the input text rendered as a sequence of
256 """Any text. Returns the input text rendered as a sequence of
255 XML entities.
257 XML entities.
256 """
258 """
257 text = unicode(text, encoding.encoding, 'replace')
259 text = unicode(text, encoding.encoding, 'replace')
258 return ''.join(['&#%d;' % ord(c) for c in text])
260 return ''.join(['&#%d;' % ord(c) for c in text])
259
261
260 @templatefilter('permissions')
262 @templatefilter('permissions')
261 def permissions(flags):
263 def permissions(flags):
262 if "l" in flags:
264 if "l" in flags:
263 return "lrwxrwxrwx"
265 return "lrwxrwxrwx"
264 if "x" in flags:
266 if "x" in flags:
265 return "-rwxr-xr-x"
267 return "-rwxr-xr-x"
266 return "-rw-r--r--"
268 return "-rw-r--r--"
267
269
268 @templatefilter('person')
270 @templatefilter('person')
269 def person(author):
271 def person(author):
270 """Any text. Returns the name before an email address,
272 """Any text. Returns the name before an email address,
271 interpreting it as per RFC 5322.
273 interpreting it as per RFC 5322.
272
274
273 >>> person('foo@bar')
275 >>> person('foo@bar')
274 'foo'
276 'foo'
275 >>> person('Foo Bar <foo@bar>')
277 >>> person('Foo Bar <foo@bar>')
276 'Foo Bar'
278 'Foo Bar'
277 >>> person('"Foo Bar" <foo@bar>')
279 >>> person('"Foo Bar" <foo@bar>')
278 'Foo Bar'
280 'Foo Bar'
279 >>> person('"Foo \"buz\" Bar" <foo@bar>')
281 >>> person('"Foo \"buz\" Bar" <foo@bar>')
280 'Foo "buz" Bar'
282 'Foo "buz" Bar'
281 >>> # The following are invalid, but do exist in real-life
283 >>> # The following are invalid, but do exist in real-life
282 ...
284 ...
283 >>> person('Foo "buz" Bar <foo@bar>')
285 >>> person('Foo "buz" Bar <foo@bar>')
284 'Foo "buz" Bar'
286 'Foo "buz" Bar'
285 >>> person('"Foo Bar <foo@bar>')
287 >>> person('"Foo Bar <foo@bar>')
286 'Foo Bar'
288 'Foo Bar'
287 """
289 """
288 if '@' not in author:
290 if '@' not in author:
289 return author
291 return author
290 f = author.find('<')
292 f = author.find('<')
291 if f != -1:
293 if f != -1:
292 return author[:f].strip(' "').replace('\\"', '"')
294 return author[:f].strip(' "').replace('\\"', '"')
293 f = author.find('@')
295 f = author.find('@')
294 return author[:f].replace('.', ' ')
296 return author[:f].replace('.', ' ')
295
297
296 @templatefilter('revescape')
298 @templatefilter('revescape')
297 def revescape(text):
299 def revescape(text):
298 """Any text. Escapes all "special" characters, except @.
300 """Any text. Escapes all "special" characters, except @.
299 Forward slashes are escaped twice to prevent web servers from prematurely
301 Forward slashes are escaped twice to prevent web servers from prematurely
300 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
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 @templatefilter('rfc3339date')
306 @templatefilter('rfc3339date')
305 def rfc3339date(text):
307 def rfc3339date(text):
306 """Date. Returns a date using the Internet date format
308 """Date. Returns a date using the Internet date format
307 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
309 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
308 """
310 """
309 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
311 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
310
312
311 @templatefilter('rfc822date')
313 @templatefilter('rfc822date')
312 def rfc822date(text):
314 def rfc822date(text):
313 """Date. Returns a date using the same format used in email
315 """Date. Returns a date using the same format used in email
314 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
316 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
315 """
317 """
316 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
318 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
317
319
318 @templatefilter('short')
320 @templatefilter('short')
319 def short(text):
321 def short(text):
320 """Changeset hash. Returns the short form of a changeset hash,
322 """Changeset hash. Returns the short form of a changeset hash,
321 i.e. a 12 hexadecimal digit string.
323 i.e. a 12 hexadecimal digit string.
322 """
324 """
323 return text[:12]
325 return text[:12]
324
326
325 @templatefilter('shortbisect')
327 @templatefilter('shortbisect')
326 def shortbisect(text):
328 def shortbisect(text):
327 """Any text. Treats `text` as a bisection status, and
329 """Any text. Treats `text` as a bisection status, and
328 returns a single-character representing the status (G: good, B: bad,
330 returns a single-character representing the status (G: good, B: bad,
329 S: skipped, U: untested, I: ignored). Returns single space if `text`
331 S: skipped, U: untested, I: ignored). Returns single space if `text`
330 is not a valid bisection status.
332 is not a valid bisection status.
331 """
333 """
332 return hbisect.shortlabel(text) or ' '
334 return hbisect.shortlabel(text) or ' '
333
335
334 @templatefilter('shortdate')
336 @templatefilter('shortdate')
335 def shortdate(text):
337 def shortdate(text):
336 """Date. Returns a date like "2006-09-18"."""
338 """Date. Returns a date like "2006-09-18"."""
337 return util.shortdate(text)
339 return util.shortdate(text)
338
340
339 @templatefilter('splitlines')
341 @templatefilter('splitlines')
340 def splitlines(text):
342 def splitlines(text):
341 """Any text. Split text into a list of lines."""
343 """Any text. Split text into a list of lines."""
342 return templatekw.showlist('line', text.splitlines(), 'lines')
344 return templatekw.showlist('line', text.splitlines(), 'lines')
343
345
344 @templatefilter('stringescape')
346 @templatefilter('stringescape')
345 def stringescape(text):
347 def stringescape(text):
346 return text.encode('string_escape')
348 return text.encode('string_escape')
347
349
348 @templatefilter('stringify')
350 @templatefilter('stringify')
349 def stringify(thing):
351 def stringify(thing):
350 """Any type. Turns the value into text by converting values into
352 """Any type. Turns the value into text by converting values into
351 text and concatenating them.
353 text and concatenating them.
352 """
354 """
353 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
355 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
354 return "".join([stringify(t) for t in thing if t is not None])
356 return "".join([stringify(t) for t in thing if t is not None])
355 if thing is None:
357 if thing is None:
356 return ""
358 return ""
357 return str(thing)
359 return str(thing)
358
360
359 @templatefilter('stripdir')
361 @templatefilter('stripdir')
360 def stripdir(text):
362 def stripdir(text):
361 """Treat the text as path and strip a directory level, if
363 """Treat the text as path and strip a directory level, if
362 possible. For example, "foo" and "foo/bar" becomes "foo".
364 possible. For example, "foo" and "foo/bar" becomes "foo".
363 """
365 """
364 dir = os.path.dirname(text)
366 dir = os.path.dirname(text)
365 if dir == "":
367 if dir == "":
366 return os.path.basename(text)
368 return os.path.basename(text)
367 else:
369 else:
368 return dir
370 return dir
369
371
370 @templatefilter('tabindent')
372 @templatefilter('tabindent')
371 def tabindent(text):
373 def tabindent(text):
372 """Any text. Returns the text, with every non-empty line
374 """Any text. Returns the text, with every non-empty line
373 except the first starting with a tab character.
375 except the first starting with a tab character.
374 """
376 """
375 return indent(text, '\t')
377 return indent(text, '\t')
376
378
377 @templatefilter('upper')
379 @templatefilter('upper')
378 def upper(text):
380 def upper(text):
379 """Any text. Converts the text to uppercase."""
381 """Any text. Converts the text to uppercase."""
380 return encoding.upper(text)
382 return encoding.upper(text)
381
383
382 @templatefilter('urlescape')
384 @templatefilter('urlescape')
383 def urlescape(text):
385 def urlescape(text):
384 """Any text. Escapes all "special" characters. For example,
386 """Any text. Escapes all "special" characters. For example,
385 "foo bar" becomes "foo%20bar".
387 "foo bar" becomes "foo%20bar".
386 """
388 """
387 return urllib.quote(text)
389 return urlreq.quote(text)
388
390
389 @templatefilter('user')
391 @templatefilter('user')
390 def userfilter(text):
392 def userfilter(text):
391 """Any text. Returns a short representation of a user name or email
393 """Any text. Returns a short representation of a user name or email
392 address."""
394 address."""
393 return util.shortuser(text)
395 return util.shortuser(text)
394
396
395 @templatefilter('emailuser')
397 @templatefilter('emailuser')
396 def emailuser(text):
398 def emailuser(text):
397 """Any text. Returns the user portion of an email address."""
399 """Any text. Returns the user portion of an email address."""
398 return util.emailuser(text)
400 return util.emailuser(text)
399
401
400 @templatefilter('utf8')
402 @templatefilter('utf8')
401 def utf8(text):
403 def utf8(text):
402 """Any text. Converts from the local character encoding to UTF-8."""
404 """Any text. Converts from the local character encoding to UTF-8."""
403 return encoding.fromlocal(text)
405 return encoding.fromlocal(text)
404
406
405 @templatefilter('xmlescape')
407 @templatefilter('xmlescape')
406 def xmlescape(text):
408 def xmlescape(text):
407 text = (text
409 text = (text
408 .replace('&', '&amp;')
410 .replace('&', '&amp;')
409 .replace('<', '&lt;')
411 .replace('<', '&lt;')
410 .replace('>', '&gt;')
412 .replace('>', '&gt;')
411 .replace('"', '&quot;')
413 .replace('"', '&quot;')
412 .replace("'", '&#39;')) # &apos; invalid in HTML
414 .replace("'", '&#39;')) # &apos; invalid in HTML
413 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
415 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
414
416
415 def websub(text, websubtable):
417 def websub(text, websubtable):
416 """:websub: Any text. Only applies to hgweb. Applies the regular
418 """:websub: Any text. Only applies to hgweb. Applies the regular
417 expression replacements defined in the websub section.
419 expression replacements defined in the websub section.
418 """
420 """
419 if websubtable:
421 if websubtable:
420 for regexp, format in websubtable:
422 for regexp, format in websubtable:
421 text = regexp.sub(format, text)
423 text = regexp.sub(format, text)
422 return text
424 return text
423
425
424 def loadfilter(ui, extname, registrarobj):
426 def loadfilter(ui, extname, registrarobj):
425 """Load template filter from specified registrarobj
427 """Load template filter from specified registrarobj
426 """
428 """
427 for name, func in registrarobj._table.iteritems():
429 for name, func in registrarobj._table.iteritems():
428 filters[name] = func
430 filters[name] = func
429
431
430 # tell hggettext to extract docstrings from these functions:
432 # tell hggettext to extract docstrings from these functions:
431 i18nfunctions = filters.values()
433 i18nfunctions = filters.values()
@@ -1,513 +1,514 b''
1 # url.py - HTTP handling for mercurial
1 # url.py - HTTP handling for mercurial
2 #
2 #
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import base64
12 import base64
13 import httplib
13 import httplib
14 import os
14 import os
15 import socket
15 import socket
16 import urllib
17 import urllib2
18
16
19 from .i18n import _
17 from .i18n import _
20 from . import (
18 from . import (
21 error,
19 error,
22 httpconnection as httpconnectionmod,
20 httpconnection as httpconnectionmod,
23 keepalive,
21 keepalive,
24 sslutil,
22 sslutil,
25 util,
23 util,
26 )
24 )
27 stringio = util.stringio
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 def __init__(self, ui):
31 def __init__(self, ui):
31 urllib2.HTTPPasswordMgrWithDefaultRealm.__init__(self)
32 urlreq.httppasswordmgrwithdefaultrealm.__init__(self)
32 self.ui = ui
33 self.ui = ui
33
34
34 def find_user_password(self, realm, authuri):
35 def find_user_password(self, realm, authuri):
35 authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
36 authinfo = urlreq.httppasswordmgrwithdefaultrealm.find_user_password(
36 self, realm, authuri)
37 self, realm, authuri)
37 user, passwd = authinfo
38 user, passwd = authinfo
38 if user and passwd:
39 if user and passwd:
39 self._writedebug(user, passwd)
40 self._writedebug(user, passwd)
40 return (user, passwd)
41 return (user, passwd)
41
42
42 if not user or not passwd:
43 if not user or not passwd:
43 res = httpconnectionmod.readauthforuri(self.ui, authuri, user)
44 res = httpconnectionmod.readauthforuri(self.ui, authuri, user)
44 if res:
45 if res:
45 group, auth = res
46 group, auth = res
46 user, passwd = auth.get('username'), auth.get('password')
47 user, passwd = auth.get('username'), auth.get('password')
47 self.ui.debug("using auth.%s.* for authentication\n" % group)
48 self.ui.debug("using auth.%s.* for authentication\n" % group)
48 if not user or not passwd:
49 if not user or not passwd:
49 u = util.url(authuri)
50 u = util.url(authuri)
50 u.query = None
51 u.query = None
51 if not self.ui.interactive():
52 if not self.ui.interactive():
52 raise error.Abort(_('http authorization required for %s') %
53 raise error.Abort(_('http authorization required for %s') %
53 util.hidepassword(str(u)))
54 util.hidepassword(str(u)))
54
55
55 self.ui.write(_("http authorization required for %s\n") %
56 self.ui.write(_("http authorization required for %s\n") %
56 util.hidepassword(str(u)))
57 util.hidepassword(str(u)))
57 self.ui.write(_("realm: %s\n") % realm)
58 self.ui.write(_("realm: %s\n") % realm)
58 if user:
59 if user:
59 self.ui.write(_("user: %s\n") % user)
60 self.ui.write(_("user: %s\n") % user)
60 else:
61 else:
61 user = self.ui.prompt(_("user:"), default=None)
62 user = self.ui.prompt(_("user:"), default=None)
62
63
63 if not passwd:
64 if not passwd:
64 passwd = self.ui.getpass()
65 passwd = self.ui.getpass()
65
66
66 self.add_password(realm, authuri, user, passwd)
67 self.add_password(realm, authuri, user, passwd)
67 self._writedebug(user, passwd)
68 self._writedebug(user, passwd)
68 return (user, passwd)
69 return (user, passwd)
69
70
70 def _writedebug(self, user, passwd):
71 def _writedebug(self, user, passwd):
71 msg = _('http auth: user %s, password %s\n')
72 msg = _('http auth: user %s, password %s\n')
72 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
73 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
73
74
74 def find_stored_password(self, authuri):
75 def find_stored_password(self, authuri):
75 return urllib2.HTTPPasswordMgrWithDefaultRealm.find_user_password(
76 return urlreq.httppasswordmgrwithdefaultrealm.find_user_password(
76 self, None, authuri)
77 self, None, authuri)
77
78
78 class proxyhandler(urllib2.ProxyHandler):
79 class proxyhandler(urlreq.proxyhandler):
79 def __init__(self, ui):
80 def __init__(self, ui):
80 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
81 proxyurl = ui.config("http_proxy", "host") or os.getenv('http_proxy')
81 # XXX proxyauthinfo = None
82 # XXX proxyauthinfo = None
82
83
83 if proxyurl:
84 if proxyurl:
84 # proxy can be proper url or host[:port]
85 # proxy can be proper url or host[:port]
85 if not (proxyurl.startswith('http:') or
86 if not (proxyurl.startswith('http:') or
86 proxyurl.startswith('https:')):
87 proxyurl.startswith('https:')):
87 proxyurl = 'http://' + proxyurl + '/'
88 proxyurl = 'http://' + proxyurl + '/'
88 proxy = util.url(proxyurl)
89 proxy = util.url(proxyurl)
89 if not proxy.user:
90 if not proxy.user:
90 proxy.user = ui.config("http_proxy", "user")
91 proxy.user = ui.config("http_proxy", "user")
91 proxy.passwd = ui.config("http_proxy", "passwd")
92 proxy.passwd = ui.config("http_proxy", "passwd")
92
93
93 # see if we should use a proxy for this url
94 # see if we should use a proxy for this url
94 no_list = ["localhost", "127.0.0.1"]
95 no_list = ["localhost", "127.0.0.1"]
95 no_list.extend([p.lower() for
96 no_list.extend([p.lower() for
96 p in ui.configlist("http_proxy", "no")])
97 p in ui.configlist("http_proxy", "no")])
97 no_list.extend([p.strip().lower() for
98 no_list.extend([p.strip().lower() for
98 p in os.getenv("no_proxy", '').split(',')
99 p in os.getenv("no_proxy", '').split(',')
99 if p.strip()])
100 if p.strip()])
100 # "http_proxy.always" config is for running tests on localhost
101 # "http_proxy.always" config is for running tests on localhost
101 if ui.configbool("http_proxy", "always"):
102 if ui.configbool("http_proxy", "always"):
102 self.no_list = []
103 self.no_list = []
103 else:
104 else:
104 self.no_list = no_list
105 self.no_list = no_list
105
106
106 proxyurl = str(proxy)
107 proxyurl = str(proxy)
107 proxies = {'http': proxyurl, 'https': proxyurl}
108 proxies = {'http': proxyurl, 'https': proxyurl}
108 ui.debug('proxying through http://%s:%s\n' %
109 ui.debug('proxying through http://%s:%s\n' %
109 (proxy.host, proxy.port))
110 (proxy.host, proxy.port))
110 else:
111 else:
111 proxies = {}
112 proxies = {}
112
113
113 # urllib2 takes proxy values from the environment and those
114 # urllib2 takes proxy values from the environment and those
114 # will take precedence if found. So, if there's a config entry
115 # will take precedence if found. So, if there's a config entry
115 # defining a proxy, drop the environment ones
116 # defining a proxy, drop the environment ones
116 if ui.config("http_proxy", "host"):
117 if ui.config("http_proxy", "host"):
117 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
118 for env in ["HTTP_PROXY", "http_proxy", "no_proxy"]:
118 try:
119 try:
119 if env in os.environ:
120 if env in os.environ:
120 del os.environ[env]
121 del os.environ[env]
121 except OSError:
122 except OSError:
122 pass
123 pass
123
124
124 urllib2.ProxyHandler.__init__(self, proxies)
125 urlreq.proxyhandler.__init__(self, proxies)
125 self.ui = ui
126 self.ui = ui
126
127
127 def proxy_open(self, req, proxy, type_):
128 def proxy_open(self, req, proxy, type_):
128 host = req.get_host().split(':')[0]
129 host = req.get_host().split(':')[0]
129 for e in self.no_list:
130 for e in self.no_list:
130 if host == e:
131 if host == e:
131 return None
132 return None
132 if e.startswith('*.') and host.endswith(e[2:]):
133 if e.startswith('*.') and host.endswith(e[2:]):
133 return None
134 return None
134 if e.startswith('.') and host.endswith(e[1:]):
135 if e.startswith('.') and host.endswith(e[1:]):
135 return None
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 def _gen_sendfile(orgsend):
140 def _gen_sendfile(orgsend):
140 def _sendfile(self, data):
141 def _sendfile(self, data):
141 # send a file
142 # send a file
142 if isinstance(data, httpconnectionmod.httpsendfile):
143 if isinstance(data, httpconnectionmod.httpsendfile):
143 # if auth required, some data sent twice, so rewind here
144 # if auth required, some data sent twice, so rewind here
144 data.seek(0)
145 data.seek(0)
145 for chunk in util.filechunkiter(data):
146 for chunk in util.filechunkiter(data):
146 orgsend(self, chunk)
147 orgsend(self, chunk)
147 else:
148 else:
148 orgsend(self, data)
149 orgsend(self, data)
149 return _sendfile
150 return _sendfile
150
151
151 has_https = util.safehasattr(urllib2, 'HTTPSHandler')
152 has_https = util.safehasattr(urlreq, 'httpshandler')
152 if has_https:
153 if has_https:
153 try:
154 try:
154 _create_connection = socket.create_connection
155 _create_connection = socket.create_connection
155 except AttributeError:
156 except AttributeError:
156 _GLOBAL_DEFAULT_TIMEOUT = object()
157 _GLOBAL_DEFAULT_TIMEOUT = object()
157
158
158 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
159 def _create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
159 source_address=None):
160 source_address=None):
160 # lifted from Python 2.6
161 # lifted from Python 2.6
161
162
162 msg = "getaddrinfo returns an empty list"
163 msg = "getaddrinfo returns an empty list"
163 host, port = address
164 host, port = address
164 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
165 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
165 af, socktype, proto, canonname, sa = res
166 af, socktype, proto, canonname, sa = res
166 sock = None
167 sock = None
167 try:
168 try:
168 sock = socket.socket(af, socktype, proto)
169 sock = socket.socket(af, socktype, proto)
169 if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
170 if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
170 sock.settimeout(timeout)
171 sock.settimeout(timeout)
171 if source_address:
172 if source_address:
172 sock.bind(source_address)
173 sock.bind(source_address)
173 sock.connect(sa)
174 sock.connect(sa)
174 return sock
175 return sock
175
176
176 except socket.error as msg:
177 except socket.error as msg:
177 if sock is not None:
178 if sock is not None:
178 sock.close()
179 sock.close()
179
180
180 raise socket.error(msg)
181 raise socket.error(msg)
181
182
182 class httpconnection(keepalive.HTTPConnection):
183 class httpconnection(keepalive.HTTPConnection):
183 # must be able to send big bundle as stream.
184 # must be able to send big bundle as stream.
184 send = _gen_sendfile(keepalive.HTTPConnection.send)
185 send = _gen_sendfile(keepalive.HTTPConnection.send)
185
186
186 def connect(self):
187 def connect(self):
187 if has_https and self.realhostport: # use CONNECT proxy
188 if has_https and self.realhostport: # use CONNECT proxy
188 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
189 self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
189 self.sock.connect((self.host, self.port))
190 self.sock.connect((self.host, self.port))
190 if _generic_proxytunnel(self):
191 if _generic_proxytunnel(self):
191 # we do not support client X.509 certificates
192 # we do not support client X.509 certificates
192 self.sock = sslutil.wrapsocket(self.sock, None, None, None,
193 self.sock = sslutil.wrapsocket(self.sock, None, None, None,
193 serverhostname=self.host)
194 serverhostname=self.host)
194 else:
195 else:
195 keepalive.HTTPConnection.connect(self)
196 keepalive.HTTPConnection.connect(self)
196
197
197 def getresponse(self):
198 def getresponse(self):
198 proxyres = getattr(self, 'proxyres', None)
199 proxyres = getattr(self, 'proxyres', None)
199 if proxyres:
200 if proxyres:
200 if proxyres.will_close:
201 if proxyres.will_close:
201 self.close()
202 self.close()
202 self.proxyres = None
203 self.proxyres = None
203 return proxyres
204 return proxyres
204 return keepalive.HTTPConnection.getresponse(self)
205 return keepalive.HTTPConnection.getresponse(self)
205
206
206 # general transaction handler to support different ways to handle
207 # general transaction handler to support different ways to handle
207 # HTTPS proxying before and after Python 2.6.3.
208 # HTTPS proxying before and after Python 2.6.3.
208 def _generic_start_transaction(handler, h, req):
209 def _generic_start_transaction(handler, h, req):
209 tunnel_host = getattr(req, '_tunnel_host', None)
210 tunnel_host = getattr(req, '_tunnel_host', None)
210 if tunnel_host:
211 if tunnel_host:
211 if tunnel_host[:7] not in ['http://', 'https:/']:
212 if tunnel_host[:7] not in ['http://', 'https:/']:
212 tunnel_host = 'https://' + tunnel_host
213 tunnel_host = 'https://' + tunnel_host
213 new_tunnel = True
214 new_tunnel = True
214 else:
215 else:
215 tunnel_host = req.get_selector()
216 tunnel_host = req.get_selector()
216 new_tunnel = False
217 new_tunnel = False
217
218
218 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
219 if new_tunnel or tunnel_host == req.get_full_url(): # has proxy
219 u = util.url(tunnel_host)
220 u = util.url(tunnel_host)
220 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
221 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
221 h.realhostport = ':'.join([u.host, (u.port or '443')])
222 h.realhostport = ':'.join([u.host, (u.port or '443')])
222 h.headers = req.headers.copy()
223 h.headers = req.headers.copy()
223 h.headers.update(handler.parent.addheaders)
224 h.headers.update(handler.parent.addheaders)
224 return
225 return
225
226
226 h.realhostport = None
227 h.realhostport = None
227 h.headers = None
228 h.headers = None
228
229
229 def _generic_proxytunnel(self):
230 def _generic_proxytunnel(self):
230 proxyheaders = dict(
231 proxyheaders = dict(
231 [(x, self.headers[x]) for x in self.headers
232 [(x, self.headers[x]) for x in self.headers
232 if x.lower().startswith('proxy-')])
233 if x.lower().startswith('proxy-')])
233 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
234 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
234 for header in proxyheaders.iteritems():
235 for header in proxyheaders.iteritems():
235 self.send('%s: %s\r\n' % header)
236 self.send('%s: %s\r\n' % header)
236 self.send('\r\n')
237 self.send('\r\n')
237
238
238 # majority of the following code is duplicated from
239 # majority of the following code is duplicated from
239 # httplib.HTTPConnection as there are no adequate places to
240 # httplib.HTTPConnection as there are no adequate places to
240 # override functions to provide the needed functionality
241 # override functions to provide the needed functionality
241 res = self.response_class(self.sock,
242 res = self.response_class(self.sock,
242 strict=self.strict,
243 strict=self.strict,
243 method=self._method)
244 method=self._method)
244
245
245 while True:
246 while True:
246 version, status, reason = res._read_status()
247 version, status, reason = res._read_status()
247 if status != httplib.CONTINUE:
248 if status != httplib.CONTINUE:
248 break
249 break
249 while True:
250 while True:
250 skip = res.fp.readline().strip()
251 skip = res.fp.readline().strip()
251 if not skip:
252 if not skip:
252 break
253 break
253 res.status = status
254 res.status = status
254 res.reason = reason.strip()
255 res.reason = reason.strip()
255
256
256 if res.status == 200:
257 if res.status == 200:
257 while True:
258 while True:
258 line = res.fp.readline()
259 line = res.fp.readline()
259 if line == '\r\n':
260 if line == '\r\n':
260 break
261 break
261 return True
262 return True
262
263
263 if version == 'HTTP/1.0':
264 if version == 'HTTP/1.0':
264 res.version = 10
265 res.version = 10
265 elif version.startswith('HTTP/1.'):
266 elif version.startswith('HTTP/1.'):
266 res.version = 11
267 res.version = 11
267 elif version == 'HTTP/0.9':
268 elif version == 'HTTP/0.9':
268 res.version = 9
269 res.version = 9
269 else:
270 else:
270 raise httplib.UnknownProtocol(version)
271 raise httplib.UnknownProtocol(version)
271
272
272 if res.version == 9:
273 if res.version == 9:
273 res.length = None
274 res.length = None
274 res.chunked = 0
275 res.chunked = 0
275 res.will_close = 1
276 res.will_close = 1
276 res.msg = httplib.HTTPMessage(stringio())
277 res.msg = httplib.HTTPMessage(stringio())
277 return False
278 return False
278
279
279 res.msg = httplib.HTTPMessage(res.fp)
280 res.msg = httplib.HTTPMessage(res.fp)
280 res.msg.fp = None
281 res.msg.fp = None
281
282
282 # are we using the chunked-style of transfer encoding?
283 # are we using the chunked-style of transfer encoding?
283 trenc = res.msg.getheader('transfer-encoding')
284 trenc = res.msg.getheader('transfer-encoding')
284 if trenc and trenc.lower() == "chunked":
285 if trenc and trenc.lower() == "chunked":
285 res.chunked = 1
286 res.chunked = 1
286 res.chunk_left = None
287 res.chunk_left = None
287 else:
288 else:
288 res.chunked = 0
289 res.chunked = 0
289
290
290 # will the connection close at the end of the response?
291 # will the connection close at the end of the response?
291 res.will_close = res._check_close()
292 res.will_close = res._check_close()
292
293
293 # do we have a Content-Length?
294 # do we have a Content-Length?
294 # NOTE: RFC 2616, section 4.4, #3 says we ignore this if
295 # NOTE: RFC 2616, section 4.4, #3 says we ignore this if
295 # transfer-encoding is "chunked"
296 # transfer-encoding is "chunked"
296 length = res.msg.getheader('content-length')
297 length = res.msg.getheader('content-length')
297 if length and not res.chunked:
298 if length and not res.chunked:
298 try:
299 try:
299 res.length = int(length)
300 res.length = int(length)
300 except ValueError:
301 except ValueError:
301 res.length = None
302 res.length = None
302 else:
303 else:
303 if res.length < 0: # ignore nonsensical negative lengths
304 if res.length < 0: # ignore nonsensical negative lengths
304 res.length = None
305 res.length = None
305 else:
306 else:
306 res.length = None
307 res.length = None
307
308
308 # does the body have a fixed length? (of zero)
309 # does the body have a fixed length? (of zero)
309 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
310 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
310 100 <= status < 200 or # 1xx codes
311 100 <= status < 200 or # 1xx codes
311 res._method == 'HEAD'):
312 res._method == 'HEAD'):
312 res.length = 0
313 res.length = 0
313
314
314 # if the connection remains open, and we aren't using chunked, and
315 # if the connection remains open, and we aren't using chunked, and
315 # a content-length was not provided, then assume that the connection
316 # a content-length was not provided, then assume that the connection
316 # WILL close.
317 # WILL close.
317 if (not res.will_close and
318 if (not res.will_close and
318 not res.chunked and
319 not res.chunked and
319 res.length is None):
320 res.length is None):
320 res.will_close = 1
321 res.will_close = 1
321
322
322 self.proxyres = res
323 self.proxyres = res
323
324
324 return False
325 return False
325
326
326 class httphandler(keepalive.HTTPHandler):
327 class httphandler(keepalive.HTTPHandler):
327 def http_open(self, req):
328 def http_open(self, req):
328 return self.do_open(httpconnection, req)
329 return self.do_open(httpconnection, req)
329
330
330 def _start_transaction(self, h, req):
331 def _start_transaction(self, h, req):
331 _generic_start_transaction(self, h, req)
332 _generic_start_transaction(self, h, req)
332 return keepalive.HTTPHandler._start_transaction(self, h, req)
333 return keepalive.HTTPHandler._start_transaction(self, h, req)
333
334
334 if has_https:
335 if has_https:
335 class httpsconnection(httplib.HTTPConnection):
336 class httpsconnection(httplib.HTTPConnection):
336 response_class = keepalive.HTTPResponse
337 response_class = keepalive.HTTPResponse
337 default_port = httplib.HTTPS_PORT
338 default_port = httplib.HTTPS_PORT
338 # must be able to send big bundle as stream.
339 # must be able to send big bundle as stream.
339 send = _gen_sendfile(keepalive.safesend)
340 send = _gen_sendfile(keepalive.safesend)
340 getresponse = keepalive.wrapgetresponse(httplib.HTTPConnection)
341 getresponse = keepalive.wrapgetresponse(httplib.HTTPConnection)
341
342
342 def __init__(self, host, port=None, key_file=None, cert_file=None,
343 def __init__(self, host, port=None, key_file=None, cert_file=None,
343 *args, **kwargs):
344 *args, **kwargs):
344 httplib.HTTPConnection.__init__(self, host, port, *args, **kwargs)
345 httplib.HTTPConnection.__init__(self, host, port, *args, **kwargs)
345 self.key_file = key_file
346 self.key_file = key_file
346 self.cert_file = cert_file
347 self.cert_file = cert_file
347
348
348 def connect(self):
349 def connect(self):
349 self.sock = _create_connection((self.host, self.port))
350 self.sock = _create_connection((self.host, self.port))
350
351
351 host = self.host
352 host = self.host
352 if self.realhostport: # use CONNECT proxy
353 if self.realhostport: # use CONNECT proxy
353 _generic_proxytunnel(self)
354 _generic_proxytunnel(self)
354 host = self.realhostport.rsplit(':', 1)[0]
355 host = self.realhostport.rsplit(':', 1)[0]
355 self.sock = sslutil.wrapsocket(
356 self.sock = sslutil.wrapsocket(
356 self.sock, self.key_file, self.cert_file, serverhostname=host,
357 self.sock, self.key_file, self.cert_file, serverhostname=host,
357 **sslutil.sslkwargs(self.ui, host))
358 **sslutil.sslkwargs(self.ui, host))
358 sslutil.validator(self.ui, host)(self.sock)
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 def __init__(self, ui):
362 def __init__(self, ui):
362 keepalive.KeepAliveHandler.__init__(self)
363 keepalive.KeepAliveHandler.__init__(self)
363 urllib2.HTTPSHandler.__init__(self)
364 urlreq.httpshandler.__init__(self)
364 self.ui = ui
365 self.ui = ui
365 self.pwmgr = passwordmgr(self.ui)
366 self.pwmgr = passwordmgr(self.ui)
366
367
367 def _start_transaction(self, h, req):
368 def _start_transaction(self, h, req):
368 _generic_start_transaction(self, h, req)
369 _generic_start_transaction(self, h, req)
369 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
370 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
370
371
371 def https_open(self, req):
372 def https_open(self, req):
372 # req.get_full_url() does not contain credentials and we may
373 # req.get_full_url() does not contain credentials and we may
373 # need them to match the certificates.
374 # need them to match the certificates.
374 url = req.get_full_url()
375 url = req.get_full_url()
375 user, password = self.pwmgr.find_stored_password(url)
376 user, password = self.pwmgr.find_stored_password(url)
376 res = httpconnectionmod.readauthforuri(self.ui, url, user)
377 res = httpconnectionmod.readauthforuri(self.ui, url, user)
377 if res:
378 if res:
378 group, auth = res
379 group, auth = res
379 self.auth = auth
380 self.auth = auth
380 self.ui.debug("using auth.%s.* for authentication\n" % group)
381 self.ui.debug("using auth.%s.* for authentication\n" % group)
381 else:
382 else:
382 self.auth = None
383 self.auth = None
383 return self.do_open(self._makeconnection, req)
384 return self.do_open(self._makeconnection, req)
384
385
385 def _makeconnection(self, host, port=None, *args, **kwargs):
386 def _makeconnection(self, host, port=None, *args, **kwargs):
386 keyfile = None
387 keyfile = None
387 certfile = None
388 certfile = None
388
389
389 if len(args) >= 1: # key_file
390 if len(args) >= 1: # key_file
390 keyfile = args[0]
391 keyfile = args[0]
391 if len(args) >= 2: # cert_file
392 if len(args) >= 2: # cert_file
392 certfile = args[1]
393 certfile = args[1]
393 args = args[2:]
394 args = args[2:]
394
395
395 # if the user has specified different key/cert files in
396 # if the user has specified different key/cert files in
396 # hgrc, we prefer these
397 # hgrc, we prefer these
397 if self.auth and 'key' in self.auth and 'cert' in self.auth:
398 if self.auth and 'key' in self.auth and 'cert' in self.auth:
398 keyfile = self.auth['key']
399 keyfile = self.auth['key']
399 certfile = self.auth['cert']
400 certfile = self.auth['cert']
400
401
401 conn = httpsconnection(host, port, keyfile, certfile, *args,
402 conn = httpsconnection(host, port, keyfile, certfile, *args,
402 **kwargs)
403 **kwargs)
403 conn.ui = self.ui
404 conn.ui = self.ui
404 return conn
405 return conn
405
406
406 class httpdigestauthhandler(urllib2.HTTPDigestAuthHandler):
407 class httpdigestauthhandler(urlreq.httpdigestauthhandler):
407 def __init__(self, *args, **kwargs):
408 def __init__(self, *args, **kwargs):
408 urllib2.HTTPDigestAuthHandler.__init__(self, *args, **kwargs)
409 urlreq.httpdigestauthhandler.__init__(self, *args, **kwargs)
409 self.retried_req = None
410 self.retried_req = None
410
411
411 def reset_retry_count(self):
412 def reset_retry_count(self):
412 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
413 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
413 # forever. We disable reset_retry_count completely and reset in
414 # forever. We disable reset_retry_count completely and reset in
414 # http_error_auth_reqed instead.
415 # http_error_auth_reqed instead.
415 pass
416 pass
416
417
417 def http_error_auth_reqed(self, auth_header, host, req, headers):
418 def http_error_auth_reqed(self, auth_header, host, req, headers):
418 # Reset the retry counter once for each request.
419 # Reset the retry counter once for each request.
419 if req is not self.retried_req:
420 if req is not self.retried_req:
420 self.retried_req = req
421 self.retried_req = req
421 self.retried = 0
422 self.retried = 0
422 return urllib2.HTTPDigestAuthHandler.http_error_auth_reqed(
423 return urlreq.httpdigestauthhandler.http_error_auth_reqed(
423 self, auth_header, host, req, headers)
424 self, auth_header, host, req, headers)
424
425
425 class httpbasicauthhandler(urllib2.HTTPBasicAuthHandler):
426 class httpbasicauthhandler(urlreq.httpbasicauthhandler):
426 def __init__(self, *args, **kwargs):
427 def __init__(self, *args, **kwargs):
427 self.auth = None
428 self.auth = None
428 urllib2.HTTPBasicAuthHandler.__init__(self, *args, **kwargs)
429 urlreq.httpbasicauthhandler.__init__(self, *args, **kwargs)
429 self.retried_req = None
430 self.retried_req = None
430
431
431 def http_request(self, request):
432 def http_request(self, request):
432 if self.auth:
433 if self.auth:
433 request.add_unredirected_header(self.auth_header, self.auth)
434 request.add_unredirected_header(self.auth_header, self.auth)
434
435
435 return request
436 return request
436
437
437 def https_request(self, request):
438 def https_request(self, request):
438 if self.auth:
439 if self.auth:
439 request.add_unredirected_header(self.auth_header, self.auth)
440 request.add_unredirected_header(self.auth_header, self.auth)
440
441
441 return request
442 return request
442
443
443 def reset_retry_count(self):
444 def reset_retry_count(self):
444 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
445 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
445 # forever. We disable reset_retry_count completely and reset in
446 # forever. We disable reset_retry_count completely and reset in
446 # http_error_auth_reqed instead.
447 # http_error_auth_reqed instead.
447 pass
448 pass
448
449
449 def http_error_auth_reqed(self, auth_header, host, req, headers):
450 def http_error_auth_reqed(self, auth_header, host, req, headers):
450 # Reset the retry counter once for each request.
451 # Reset the retry counter once for each request.
451 if req is not self.retried_req:
452 if req is not self.retried_req:
452 self.retried_req = req
453 self.retried_req = req
453 self.retried = 0
454 self.retried = 0
454 return urllib2.HTTPBasicAuthHandler.http_error_auth_reqed(
455 return urlreq.httpbasicauthhandler.http_error_auth_reqed(
455 self, auth_header, host, req, headers)
456 self, auth_header, host, req, headers)
456
457
457 def retry_http_basic_auth(self, host, req, realm):
458 def retry_http_basic_auth(self, host, req, realm):
458 user, pw = self.passwd.find_user_password(realm, req.get_full_url())
459 user, pw = self.passwd.find_user_password(realm, req.get_full_url())
459 if pw is not None:
460 if pw is not None:
460 raw = "%s:%s" % (user, pw)
461 raw = "%s:%s" % (user, pw)
461 auth = 'Basic %s' % base64.b64encode(raw).strip()
462 auth = 'Basic %s' % base64.b64encode(raw).strip()
462 if req.headers.get(self.auth_header, None) == auth:
463 if req.headers.get(self.auth_header, None) == auth:
463 return None
464 return None
464 self.auth = auth
465 self.auth = auth
465 req.add_unredirected_header(self.auth_header, auth)
466 req.add_unredirected_header(self.auth_header, auth)
466 return self.parent.open(req)
467 return self.parent.open(req)
467 else:
468 else:
468 return None
469 return None
469
470
470 handlerfuncs = []
471 handlerfuncs = []
471
472
472 def opener(ui, authinfo=None):
473 def opener(ui, authinfo=None):
473 '''
474 '''
474 construct an opener suitable for urllib2
475 construct an opener suitable for urllib2
475 authinfo will be added to the password manager
476 authinfo will be added to the password manager
476 '''
477 '''
477 # experimental config: ui.usehttp2
478 # experimental config: ui.usehttp2
478 if ui.configbool('ui', 'usehttp2', False):
479 if ui.configbool('ui', 'usehttp2', False):
479 handlers = [httpconnectionmod.http2handler(ui, passwordmgr(ui))]
480 handlers = [httpconnectionmod.http2handler(ui, passwordmgr(ui))]
480 else:
481 else:
481 handlers = [httphandler()]
482 handlers = [httphandler()]
482 if has_https:
483 if has_https:
483 handlers.append(httpshandler(ui))
484 handlers.append(httpshandler(ui))
484
485
485 handlers.append(proxyhandler(ui))
486 handlers.append(proxyhandler(ui))
486
487
487 passmgr = passwordmgr(ui)
488 passmgr = passwordmgr(ui)
488 if authinfo is not None:
489 if authinfo is not None:
489 passmgr.add_password(*authinfo)
490 passmgr.add_password(*authinfo)
490 user, passwd = authinfo[2:4]
491 user, passwd = authinfo[2:4]
491 ui.debug('http auth: user %s, password %s\n' %
492 ui.debug('http auth: user %s, password %s\n' %
492 (user, passwd and '*' * len(passwd) or 'not set'))
493 (user, passwd and '*' * len(passwd) or 'not set'))
493
494
494 handlers.extend((httpbasicauthhandler(passmgr),
495 handlers.extend((httpbasicauthhandler(passmgr),
495 httpdigestauthhandler(passmgr)))
496 httpdigestauthhandler(passmgr)))
496 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
497 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
497 opener = urllib2.build_opener(*handlers)
498 opener = urlreq.buildopener(*handlers)
498
499
499 # 1.0 here is the _protocol_ version
500 # 1.0 here is the _protocol_ version
500 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
501 opener.addheaders = [('User-agent', 'mercurial/proto-1.0')]
501 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
502 opener.addheaders.append(('Accept', 'application/mercurial-0.1'))
502 return opener
503 return opener
503
504
504 def open(ui, url_, data=None):
505 def open(ui, url_, data=None):
505 u = util.url(url_)
506 u = util.url(url_)
506 if u.scheme:
507 if u.scheme:
507 u.scheme = u.scheme.lower()
508 u.scheme = u.scheme.lower()
508 url_, authinfo = u.authinfo()
509 url_, authinfo = u.authinfo()
509 else:
510 else:
510 path = util.normpath(os.path.abspath(url_))
511 path = util.normpath(os.path.abspath(url_))
511 url_ = 'file://' + urllib.pathname2url(path)
512 url_ = 'file://' + urlreq.pathname2url(path)
512 authinfo = None
513 authinfo = None
513 return opener(ui, authinfo).open(url_, data)
514 return opener(ui, authinfo).open(url_, data)
@@ -1,2755 +1,2758 b''
1 # util.py - Mercurial utility functions and platform specific implementations
1 # util.py - Mercurial utility functions and platform specific implementations
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 """Mercurial utility functions and platform specific implementations.
10 """Mercurial utility functions and platform specific implementations.
11
11
12 This contains helper routines that are independent of the SCM core and
12 This contains helper routines that are independent of the SCM core and
13 hide platform-specific details from the core.
13 hide platform-specific details from the core.
14 """
14 """
15
15
16 from __future__ import absolute_import
16 from __future__ import absolute_import
17
17
18 import bz2
18 import bz2
19 import calendar
19 import calendar
20 import collections
20 import collections
21 import datetime
21 import datetime
22 import errno
22 import errno
23 import gc
23 import gc
24 import hashlib
24 import hashlib
25 import imp
25 import imp
26 import os
26 import os
27 import re as remod
27 import re as remod
28 import shutil
28 import shutil
29 import signal
29 import signal
30 import socket
30 import socket
31 import subprocess
31 import subprocess
32 import sys
32 import sys
33 import tempfile
33 import tempfile
34 import textwrap
34 import textwrap
35 import time
35 import time
36 import traceback
36 import traceback
37 import urllib
38 import zlib
37 import zlib
39
38
40 from . import (
39 from . import (
41 encoding,
40 encoding,
42 error,
41 error,
43 i18n,
42 i18n,
44 osutil,
43 osutil,
45 parsers,
44 parsers,
46 pycompat,
45 pycompat,
47 )
46 )
48
47
49 for attr in (
48 for attr in (
50 'empty',
49 'empty',
51 'queue',
50 'queue',
52 'urlerr',
51 'urlerr',
53 'urlreq',
52 # we do import urlreq, but we do it outside the loop
53 #'urlreq',
54 'stringio',
54 'stringio',
55 ):
55 ):
56 globals()[attr] = getattr(pycompat, attr)
56 globals()[attr] = getattr(pycompat, attr)
57
57
58 # This line is to make pyflakes happy:
59 urlreq = pycompat.urlreq
60
58 if os.name == 'nt':
61 if os.name == 'nt':
59 from . import windows as platform
62 from . import windows as platform
60 else:
63 else:
61 from . import posix as platform
64 from . import posix as platform
62
65
63 md5 = hashlib.md5
66 md5 = hashlib.md5
64 sha1 = hashlib.sha1
67 sha1 = hashlib.sha1
65 sha512 = hashlib.sha512
68 sha512 = hashlib.sha512
66 _ = i18n._
69 _ = i18n._
67
70
68 cachestat = platform.cachestat
71 cachestat = platform.cachestat
69 checkexec = platform.checkexec
72 checkexec = platform.checkexec
70 checklink = platform.checklink
73 checklink = platform.checklink
71 copymode = platform.copymode
74 copymode = platform.copymode
72 executablepath = platform.executablepath
75 executablepath = platform.executablepath
73 expandglobs = platform.expandglobs
76 expandglobs = platform.expandglobs
74 explainexit = platform.explainexit
77 explainexit = platform.explainexit
75 findexe = platform.findexe
78 findexe = platform.findexe
76 gethgcmd = platform.gethgcmd
79 gethgcmd = platform.gethgcmd
77 getuser = platform.getuser
80 getuser = platform.getuser
78 getpid = os.getpid
81 getpid = os.getpid
79 groupmembers = platform.groupmembers
82 groupmembers = platform.groupmembers
80 groupname = platform.groupname
83 groupname = platform.groupname
81 hidewindow = platform.hidewindow
84 hidewindow = platform.hidewindow
82 isexec = platform.isexec
85 isexec = platform.isexec
83 isowner = platform.isowner
86 isowner = platform.isowner
84 localpath = platform.localpath
87 localpath = platform.localpath
85 lookupreg = platform.lookupreg
88 lookupreg = platform.lookupreg
86 makedir = platform.makedir
89 makedir = platform.makedir
87 nlinks = platform.nlinks
90 nlinks = platform.nlinks
88 normpath = platform.normpath
91 normpath = platform.normpath
89 normcase = platform.normcase
92 normcase = platform.normcase
90 normcasespec = platform.normcasespec
93 normcasespec = platform.normcasespec
91 normcasefallback = platform.normcasefallback
94 normcasefallback = platform.normcasefallback
92 openhardlinks = platform.openhardlinks
95 openhardlinks = platform.openhardlinks
93 oslink = platform.oslink
96 oslink = platform.oslink
94 parsepatchoutput = platform.parsepatchoutput
97 parsepatchoutput = platform.parsepatchoutput
95 pconvert = platform.pconvert
98 pconvert = platform.pconvert
96 poll = platform.poll
99 poll = platform.poll
97 popen = platform.popen
100 popen = platform.popen
98 posixfile = platform.posixfile
101 posixfile = platform.posixfile
99 quotecommand = platform.quotecommand
102 quotecommand = platform.quotecommand
100 readpipe = platform.readpipe
103 readpipe = platform.readpipe
101 rename = platform.rename
104 rename = platform.rename
102 removedirs = platform.removedirs
105 removedirs = platform.removedirs
103 samedevice = platform.samedevice
106 samedevice = platform.samedevice
104 samefile = platform.samefile
107 samefile = platform.samefile
105 samestat = platform.samestat
108 samestat = platform.samestat
106 setbinary = platform.setbinary
109 setbinary = platform.setbinary
107 setflags = platform.setflags
110 setflags = platform.setflags
108 setsignalhandler = platform.setsignalhandler
111 setsignalhandler = platform.setsignalhandler
109 shellquote = platform.shellquote
112 shellquote = platform.shellquote
110 spawndetached = platform.spawndetached
113 spawndetached = platform.spawndetached
111 split = platform.split
114 split = platform.split
112 sshargs = platform.sshargs
115 sshargs = platform.sshargs
113 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
116 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
114 statisexec = platform.statisexec
117 statisexec = platform.statisexec
115 statislink = platform.statislink
118 statislink = platform.statislink
116 termwidth = platform.termwidth
119 termwidth = platform.termwidth
117 testpid = platform.testpid
120 testpid = platform.testpid
118 umask = platform.umask
121 umask = platform.umask
119 unlink = platform.unlink
122 unlink = platform.unlink
120 unlinkpath = platform.unlinkpath
123 unlinkpath = platform.unlinkpath
121 username = platform.username
124 username = platform.username
122
125
123 # Python compatibility
126 # Python compatibility
124
127
125 _notset = object()
128 _notset = object()
126
129
127 # disable Python's problematic floating point timestamps (issue4836)
130 # disable Python's problematic floating point timestamps (issue4836)
128 # (Python hypocritically says you shouldn't change this behavior in
131 # (Python hypocritically says you shouldn't change this behavior in
129 # libraries, and sure enough Mercurial is not a library.)
132 # libraries, and sure enough Mercurial is not a library.)
130 os.stat_float_times(False)
133 os.stat_float_times(False)
131
134
132 def safehasattr(thing, attr):
135 def safehasattr(thing, attr):
133 return getattr(thing, attr, _notset) is not _notset
136 return getattr(thing, attr, _notset) is not _notset
134
137
135 DIGESTS = {
138 DIGESTS = {
136 'md5': md5,
139 'md5': md5,
137 'sha1': sha1,
140 'sha1': sha1,
138 'sha512': sha512,
141 'sha512': sha512,
139 }
142 }
140 # List of digest types from strongest to weakest
143 # List of digest types from strongest to weakest
141 DIGESTS_BY_STRENGTH = ['sha512', 'sha1', 'md5']
144 DIGESTS_BY_STRENGTH = ['sha512', 'sha1', 'md5']
142
145
143 for k in DIGESTS_BY_STRENGTH:
146 for k in DIGESTS_BY_STRENGTH:
144 assert k in DIGESTS
147 assert k in DIGESTS
145
148
146 class digester(object):
149 class digester(object):
147 """helper to compute digests.
150 """helper to compute digests.
148
151
149 This helper can be used to compute one or more digests given their name.
152 This helper can be used to compute one or more digests given their name.
150
153
151 >>> d = digester(['md5', 'sha1'])
154 >>> d = digester(['md5', 'sha1'])
152 >>> d.update('foo')
155 >>> d.update('foo')
153 >>> [k for k in sorted(d)]
156 >>> [k for k in sorted(d)]
154 ['md5', 'sha1']
157 ['md5', 'sha1']
155 >>> d['md5']
158 >>> d['md5']
156 'acbd18db4cc2f85cedef654fccc4a4d8'
159 'acbd18db4cc2f85cedef654fccc4a4d8'
157 >>> d['sha1']
160 >>> d['sha1']
158 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
161 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
159 >>> digester.preferred(['md5', 'sha1'])
162 >>> digester.preferred(['md5', 'sha1'])
160 'sha1'
163 'sha1'
161 """
164 """
162
165
163 def __init__(self, digests, s=''):
166 def __init__(self, digests, s=''):
164 self._hashes = {}
167 self._hashes = {}
165 for k in digests:
168 for k in digests:
166 if k not in DIGESTS:
169 if k not in DIGESTS:
167 raise Abort(_('unknown digest type: %s') % k)
170 raise Abort(_('unknown digest type: %s') % k)
168 self._hashes[k] = DIGESTS[k]()
171 self._hashes[k] = DIGESTS[k]()
169 if s:
172 if s:
170 self.update(s)
173 self.update(s)
171
174
172 def update(self, data):
175 def update(self, data):
173 for h in self._hashes.values():
176 for h in self._hashes.values():
174 h.update(data)
177 h.update(data)
175
178
176 def __getitem__(self, key):
179 def __getitem__(self, key):
177 if key not in DIGESTS:
180 if key not in DIGESTS:
178 raise Abort(_('unknown digest type: %s') % k)
181 raise Abort(_('unknown digest type: %s') % k)
179 return self._hashes[key].hexdigest()
182 return self._hashes[key].hexdigest()
180
183
181 def __iter__(self):
184 def __iter__(self):
182 return iter(self._hashes)
185 return iter(self._hashes)
183
186
184 @staticmethod
187 @staticmethod
185 def preferred(supported):
188 def preferred(supported):
186 """returns the strongest digest type in both supported and DIGESTS."""
189 """returns the strongest digest type in both supported and DIGESTS."""
187
190
188 for k in DIGESTS_BY_STRENGTH:
191 for k in DIGESTS_BY_STRENGTH:
189 if k in supported:
192 if k in supported:
190 return k
193 return k
191 return None
194 return None
192
195
193 class digestchecker(object):
196 class digestchecker(object):
194 """file handle wrapper that additionally checks content against a given
197 """file handle wrapper that additionally checks content against a given
195 size and digests.
198 size and digests.
196
199
197 d = digestchecker(fh, size, {'md5': '...'})
200 d = digestchecker(fh, size, {'md5': '...'})
198
201
199 When multiple digests are given, all of them are validated.
202 When multiple digests are given, all of them are validated.
200 """
203 """
201
204
202 def __init__(self, fh, size, digests):
205 def __init__(self, fh, size, digests):
203 self._fh = fh
206 self._fh = fh
204 self._size = size
207 self._size = size
205 self._got = 0
208 self._got = 0
206 self._digests = dict(digests)
209 self._digests = dict(digests)
207 self._digester = digester(self._digests.keys())
210 self._digester = digester(self._digests.keys())
208
211
209 def read(self, length=-1):
212 def read(self, length=-1):
210 content = self._fh.read(length)
213 content = self._fh.read(length)
211 self._digester.update(content)
214 self._digester.update(content)
212 self._got += len(content)
215 self._got += len(content)
213 return content
216 return content
214
217
215 def validate(self):
218 def validate(self):
216 if self._size != self._got:
219 if self._size != self._got:
217 raise Abort(_('size mismatch: expected %d, got %d') %
220 raise Abort(_('size mismatch: expected %d, got %d') %
218 (self._size, self._got))
221 (self._size, self._got))
219 for k, v in self._digests.items():
222 for k, v in self._digests.items():
220 if v != self._digester[k]:
223 if v != self._digester[k]:
221 # i18n: first parameter is a digest name
224 # i18n: first parameter is a digest name
222 raise Abort(_('%s mismatch: expected %s, got %s') %
225 raise Abort(_('%s mismatch: expected %s, got %s') %
223 (k, v, self._digester[k]))
226 (k, v, self._digester[k]))
224
227
225 try:
228 try:
226 buffer = buffer
229 buffer = buffer
227 except NameError:
230 except NameError:
228 if sys.version_info[0] < 3:
231 if sys.version_info[0] < 3:
229 def buffer(sliceable, offset=0):
232 def buffer(sliceable, offset=0):
230 return sliceable[offset:]
233 return sliceable[offset:]
231 else:
234 else:
232 def buffer(sliceable, offset=0):
235 def buffer(sliceable, offset=0):
233 return memoryview(sliceable)[offset:]
236 return memoryview(sliceable)[offset:]
234
237
235 closefds = os.name == 'posix'
238 closefds = os.name == 'posix'
236
239
237 _chunksize = 4096
240 _chunksize = 4096
238
241
239 class bufferedinputpipe(object):
242 class bufferedinputpipe(object):
240 """a manually buffered input pipe
243 """a manually buffered input pipe
241
244
242 Python will not let us use buffered IO and lazy reading with 'polling' at
245 Python will not let us use buffered IO and lazy reading with 'polling' at
243 the same time. We cannot probe the buffer state and select will not detect
246 the same time. We cannot probe the buffer state and select will not detect
244 that data are ready to read if they are already buffered.
247 that data are ready to read if they are already buffered.
245
248
246 This class let us work around that by implementing its own buffering
249 This class let us work around that by implementing its own buffering
247 (allowing efficient readline) while offering a way to know if the buffer is
250 (allowing efficient readline) while offering a way to know if the buffer is
248 empty from the output (allowing collaboration of the buffer with polling).
251 empty from the output (allowing collaboration of the buffer with polling).
249
252
250 This class lives in the 'util' module because it makes use of the 'os'
253 This class lives in the 'util' module because it makes use of the 'os'
251 module from the python stdlib.
254 module from the python stdlib.
252 """
255 """
253
256
254 def __init__(self, input):
257 def __init__(self, input):
255 self._input = input
258 self._input = input
256 self._buffer = []
259 self._buffer = []
257 self._eof = False
260 self._eof = False
258 self._lenbuf = 0
261 self._lenbuf = 0
259
262
260 @property
263 @property
261 def hasbuffer(self):
264 def hasbuffer(self):
262 """True is any data is currently buffered
265 """True is any data is currently buffered
263
266
264 This will be used externally a pre-step for polling IO. If there is
267 This will be used externally a pre-step for polling IO. If there is
265 already data then no polling should be set in place."""
268 already data then no polling should be set in place."""
266 return bool(self._buffer)
269 return bool(self._buffer)
267
270
268 @property
271 @property
269 def closed(self):
272 def closed(self):
270 return self._input.closed
273 return self._input.closed
271
274
272 def fileno(self):
275 def fileno(self):
273 return self._input.fileno()
276 return self._input.fileno()
274
277
275 def close(self):
278 def close(self):
276 return self._input.close()
279 return self._input.close()
277
280
278 def read(self, size):
281 def read(self, size):
279 while (not self._eof) and (self._lenbuf < size):
282 while (not self._eof) and (self._lenbuf < size):
280 self._fillbuffer()
283 self._fillbuffer()
281 return self._frombuffer(size)
284 return self._frombuffer(size)
282
285
283 def readline(self, *args, **kwargs):
286 def readline(self, *args, **kwargs):
284 if 1 < len(self._buffer):
287 if 1 < len(self._buffer):
285 # this should not happen because both read and readline end with a
288 # this should not happen because both read and readline end with a
286 # _frombuffer call that collapse it.
289 # _frombuffer call that collapse it.
287 self._buffer = [''.join(self._buffer)]
290 self._buffer = [''.join(self._buffer)]
288 self._lenbuf = len(self._buffer[0])
291 self._lenbuf = len(self._buffer[0])
289 lfi = -1
292 lfi = -1
290 if self._buffer:
293 if self._buffer:
291 lfi = self._buffer[-1].find('\n')
294 lfi = self._buffer[-1].find('\n')
292 while (not self._eof) and lfi < 0:
295 while (not self._eof) and lfi < 0:
293 self._fillbuffer()
296 self._fillbuffer()
294 if self._buffer:
297 if self._buffer:
295 lfi = self._buffer[-1].find('\n')
298 lfi = self._buffer[-1].find('\n')
296 size = lfi + 1
299 size = lfi + 1
297 if lfi < 0: # end of file
300 if lfi < 0: # end of file
298 size = self._lenbuf
301 size = self._lenbuf
299 elif 1 < len(self._buffer):
302 elif 1 < len(self._buffer):
300 # we need to take previous chunks into account
303 # we need to take previous chunks into account
301 size += self._lenbuf - len(self._buffer[-1])
304 size += self._lenbuf - len(self._buffer[-1])
302 return self._frombuffer(size)
305 return self._frombuffer(size)
303
306
304 def _frombuffer(self, size):
307 def _frombuffer(self, size):
305 """return at most 'size' data from the buffer
308 """return at most 'size' data from the buffer
306
309
307 The data are removed from the buffer."""
310 The data are removed from the buffer."""
308 if size == 0 or not self._buffer:
311 if size == 0 or not self._buffer:
309 return ''
312 return ''
310 buf = self._buffer[0]
313 buf = self._buffer[0]
311 if 1 < len(self._buffer):
314 if 1 < len(self._buffer):
312 buf = ''.join(self._buffer)
315 buf = ''.join(self._buffer)
313
316
314 data = buf[:size]
317 data = buf[:size]
315 buf = buf[len(data):]
318 buf = buf[len(data):]
316 if buf:
319 if buf:
317 self._buffer = [buf]
320 self._buffer = [buf]
318 self._lenbuf = len(buf)
321 self._lenbuf = len(buf)
319 else:
322 else:
320 self._buffer = []
323 self._buffer = []
321 self._lenbuf = 0
324 self._lenbuf = 0
322 return data
325 return data
323
326
324 def _fillbuffer(self):
327 def _fillbuffer(self):
325 """read data to the buffer"""
328 """read data to the buffer"""
326 data = os.read(self._input.fileno(), _chunksize)
329 data = os.read(self._input.fileno(), _chunksize)
327 if not data:
330 if not data:
328 self._eof = True
331 self._eof = True
329 else:
332 else:
330 self._lenbuf += len(data)
333 self._lenbuf += len(data)
331 self._buffer.append(data)
334 self._buffer.append(data)
332
335
333 def popen2(cmd, env=None, newlines=False):
336 def popen2(cmd, env=None, newlines=False):
334 # Setting bufsize to -1 lets the system decide the buffer size.
337 # Setting bufsize to -1 lets the system decide the buffer size.
335 # The default for bufsize is 0, meaning unbuffered. This leads to
338 # The default for bufsize is 0, meaning unbuffered. This leads to
336 # poor performance on Mac OS X: http://bugs.python.org/issue4194
339 # poor performance on Mac OS X: http://bugs.python.org/issue4194
337 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
340 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
338 close_fds=closefds,
341 close_fds=closefds,
339 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
342 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
340 universal_newlines=newlines,
343 universal_newlines=newlines,
341 env=env)
344 env=env)
342 return p.stdin, p.stdout
345 return p.stdin, p.stdout
343
346
344 def popen3(cmd, env=None, newlines=False):
347 def popen3(cmd, env=None, newlines=False):
345 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
348 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
346 return stdin, stdout, stderr
349 return stdin, stdout, stderr
347
350
348 def popen4(cmd, env=None, newlines=False, bufsize=-1):
351 def popen4(cmd, env=None, newlines=False, bufsize=-1):
349 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
352 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
350 close_fds=closefds,
353 close_fds=closefds,
351 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
354 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
352 stderr=subprocess.PIPE,
355 stderr=subprocess.PIPE,
353 universal_newlines=newlines,
356 universal_newlines=newlines,
354 env=env)
357 env=env)
355 return p.stdin, p.stdout, p.stderr, p
358 return p.stdin, p.stdout, p.stderr, p
356
359
357 def version():
360 def version():
358 """Return version information if available."""
361 """Return version information if available."""
359 try:
362 try:
360 from . import __version__
363 from . import __version__
361 return __version__.version
364 return __version__.version
362 except ImportError:
365 except ImportError:
363 return 'unknown'
366 return 'unknown'
364
367
365 def versiontuple(v=None, n=4):
368 def versiontuple(v=None, n=4):
366 """Parses a Mercurial version string into an N-tuple.
369 """Parses a Mercurial version string into an N-tuple.
367
370
368 The version string to be parsed is specified with the ``v`` argument.
371 The version string to be parsed is specified with the ``v`` argument.
369 If it isn't defined, the current Mercurial version string will be parsed.
372 If it isn't defined, the current Mercurial version string will be parsed.
370
373
371 ``n`` can be 2, 3, or 4. Here is how some version strings map to
374 ``n`` can be 2, 3, or 4. Here is how some version strings map to
372 returned values:
375 returned values:
373
376
374 >>> v = '3.6.1+190-df9b73d2d444'
377 >>> v = '3.6.1+190-df9b73d2d444'
375 >>> versiontuple(v, 2)
378 >>> versiontuple(v, 2)
376 (3, 6)
379 (3, 6)
377 >>> versiontuple(v, 3)
380 >>> versiontuple(v, 3)
378 (3, 6, 1)
381 (3, 6, 1)
379 >>> versiontuple(v, 4)
382 >>> versiontuple(v, 4)
380 (3, 6, 1, '190-df9b73d2d444')
383 (3, 6, 1, '190-df9b73d2d444')
381
384
382 >>> versiontuple('3.6.1+190-df9b73d2d444+20151118')
385 >>> versiontuple('3.6.1+190-df9b73d2d444+20151118')
383 (3, 6, 1, '190-df9b73d2d444+20151118')
386 (3, 6, 1, '190-df9b73d2d444+20151118')
384
387
385 >>> v = '3.6'
388 >>> v = '3.6'
386 >>> versiontuple(v, 2)
389 >>> versiontuple(v, 2)
387 (3, 6)
390 (3, 6)
388 >>> versiontuple(v, 3)
391 >>> versiontuple(v, 3)
389 (3, 6, None)
392 (3, 6, None)
390 >>> versiontuple(v, 4)
393 >>> versiontuple(v, 4)
391 (3, 6, None, None)
394 (3, 6, None, None)
392 """
395 """
393 if not v:
396 if not v:
394 v = version()
397 v = version()
395 parts = v.split('+', 1)
398 parts = v.split('+', 1)
396 if len(parts) == 1:
399 if len(parts) == 1:
397 vparts, extra = parts[0], None
400 vparts, extra = parts[0], None
398 else:
401 else:
399 vparts, extra = parts
402 vparts, extra = parts
400
403
401 vints = []
404 vints = []
402 for i in vparts.split('.'):
405 for i in vparts.split('.'):
403 try:
406 try:
404 vints.append(int(i))
407 vints.append(int(i))
405 except ValueError:
408 except ValueError:
406 break
409 break
407 # (3, 6) -> (3, 6, None)
410 # (3, 6) -> (3, 6, None)
408 while len(vints) < 3:
411 while len(vints) < 3:
409 vints.append(None)
412 vints.append(None)
410
413
411 if n == 2:
414 if n == 2:
412 return (vints[0], vints[1])
415 return (vints[0], vints[1])
413 if n == 3:
416 if n == 3:
414 return (vints[0], vints[1], vints[2])
417 return (vints[0], vints[1], vints[2])
415 if n == 4:
418 if n == 4:
416 return (vints[0], vints[1], vints[2], extra)
419 return (vints[0], vints[1], vints[2], extra)
417
420
418 # used by parsedate
421 # used by parsedate
419 defaultdateformats = (
422 defaultdateformats = (
420 '%Y-%m-%d %H:%M:%S',
423 '%Y-%m-%d %H:%M:%S',
421 '%Y-%m-%d %I:%M:%S%p',
424 '%Y-%m-%d %I:%M:%S%p',
422 '%Y-%m-%d %H:%M',
425 '%Y-%m-%d %H:%M',
423 '%Y-%m-%d %I:%M%p',
426 '%Y-%m-%d %I:%M%p',
424 '%Y-%m-%d',
427 '%Y-%m-%d',
425 '%m-%d',
428 '%m-%d',
426 '%m/%d',
429 '%m/%d',
427 '%m/%d/%y',
430 '%m/%d/%y',
428 '%m/%d/%Y',
431 '%m/%d/%Y',
429 '%a %b %d %H:%M:%S %Y',
432 '%a %b %d %H:%M:%S %Y',
430 '%a %b %d %I:%M:%S%p %Y',
433 '%a %b %d %I:%M:%S%p %Y',
431 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
434 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
432 '%b %d %H:%M:%S %Y',
435 '%b %d %H:%M:%S %Y',
433 '%b %d %I:%M:%S%p %Y',
436 '%b %d %I:%M:%S%p %Y',
434 '%b %d %H:%M:%S',
437 '%b %d %H:%M:%S',
435 '%b %d %I:%M:%S%p',
438 '%b %d %I:%M:%S%p',
436 '%b %d %H:%M',
439 '%b %d %H:%M',
437 '%b %d %I:%M%p',
440 '%b %d %I:%M%p',
438 '%b %d %Y',
441 '%b %d %Y',
439 '%b %d',
442 '%b %d',
440 '%H:%M:%S',
443 '%H:%M:%S',
441 '%I:%M:%S%p',
444 '%I:%M:%S%p',
442 '%H:%M',
445 '%H:%M',
443 '%I:%M%p',
446 '%I:%M%p',
444 )
447 )
445
448
446 extendeddateformats = defaultdateformats + (
449 extendeddateformats = defaultdateformats + (
447 "%Y",
450 "%Y",
448 "%Y-%m",
451 "%Y-%m",
449 "%b",
452 "%b",
450 "%b %Y",
453 "%b %Y",
451 )
454 )
452
455
453 def cachefunc(func):
456 def cachefunc(func):
454 '''cache the result of function calls'''
457 '''cache the result of function calls'''
455 # XXX doesn't handle keywords args
458 # XXX doesn't handle keywords args
456 if func.__code__.co_argcount == 0:
459 if func.__code__.co_argcount == 0:
457 cache = []
460 cache = []
458 def f():
461 def f():
459 if len(cache) == 0:
462 if len(cache) == 0:
460 cache.append(func())
463 cache.append(func())
461 return cache[0]
464 return cache[0]
462 return f
465 return f
463 cache = {}
466 cache = {}
464 if func.__code__.co_argcount == 1:
467 if func.__code__.co_argcount == 1:
465 # we gain a small amount of time because
468 # we gain a small amount of time because
466 # we don't need to pack/unpack the list
469 # we don't need to pack/unpack the list
467 def f(arg):
470 def f(arg):
468 if arg not in cache:
471 if arg not in cache:
469 cache[arg] = func(arg)
472 cache[arg] = func(arg)
470 return cache[arg]
473 return cache[arg]
471 else:
474 else:
472 def f(*args):
475 def f(*args):
473 if args not in cache:
476 if args not in cache:
474 cache[args] = func(*args)
477 cache[args] = func(*args)
475 return cache[args]
478 return cache[args]
476
479
477 return f
480 return f
478
481
479 class sortdict(dict):
482 class sortdict(dict):
480 '''a simple sorted dictionary'''
483 '''a simple sorted dictionary'''
481 def __init__(self, data=None):
484 def __init__(self, data=None):
482 self._list = []
485 self._list = []
483 if data:
486 if data:
484 self.update(data)
487 self.update(data)
485 def copy(self):
488 def copy(self):
486 return sortdict(self)
489 return sortdict(self)
487 def __setitem__(self, key, val):
490 def __setitem__(self, key, val):
488 if key in self:
491 if key in self:
489 self._list.remove(key)
492 self._list.remove(key)
490 self._list.append(key)
493 self._list.append(key)
491 dict.__setitem__(self, key, val)
494 dict.__setitem__(self, key, val)
492 def __iter__(self):
495 def __iter__(self):
493 return self._list.__iter__()
496 return self._list.__iter__()
494 def update(self, src):
497 def update(self, src):
495 if isinstance(src, dict):
498 if isinstance(src, dict):
496 src = src.iteritems()
499 src = src.iteritems()
497 for k, v in src:
500 for k, v in src:
498 self[k] = v
501 self[k] = v
499 def clear(self):
502 def clear(self):
500 dict.clear(self)
503 dict.clear(self)
501 self._list = []
504 self._list = []
502 def items(self):
505 def items(self):
503 return [(k, self[k]) for k in self._list]
506 return [(k, self[k]) for k in self._list]
504 def __delitem__(self, key):
507 def __delitem__(self, key):
505 dict.__delitem__(self, key)
508 dict.__delitem__(self, key)
506 self._list.remove(key)
509 self._list.remove(key)
507 def pop(self, key, *args, **kwargs):
510 def pop(self, key, *args, **kwargs):
508 dict.pop(self, key, *args, **kwargs)
511 dict.pop(self, key, *args, **kwargs)
509 try:
512 try:
510 self._list.remove(key)
513 self._list.remove(key)
511 except ValueError:
514 except ValueError:
512 pass
515 pass
513 def keys(self):
516 def keys(self):
514 return self._list
517 return self._list
515 def iterkeys(self):
518 def iterkeys(self):
516 return self._list.__iter__()
519 return self._list.__iter__()
517 def iteritems(self):
520 def iteritems(self):
518 for k in self._list:
521 for k in self._list:
519 yield k, self[k]
522 yield k, self[k]
520 def insert(self, index, key, val):
523 def insert(self, index, key, val):
521 self._list.insert(index, key)
524 self._list.insert(index, key)
522 dict.__setitem__(self, key, val)
525 dict.__setitem__(self, key, val)
523
526
524 class _lrucachenode(object):
527 class _lrucachenode(object):
525 """A node in a doubly linked list.
528 """A node in a doubly linked list.
526
529
527 Holds a reference to nodes on either side as well as a key-value
530 Holds a reference to nodes on either side as well as a key-value
528 pair for the dictionary entry.
531 pair for the dictionary entry.
529 """
532 """
530 __slots__ = ('next', 'prev', 'key', 'value')
533 __slots__ = ('next', 'prev', 'key', 'value')
531
534
532 def __init__(self):
535 def __init__(self):
533 self.next = None
536 self.next = None
534 self.prev = None
537 self.prev = None
535
538
536 self.key = _notset
539 self.key = _notset
537 self.value = None
540 self.value = None
538
541
539 def markempty(self):
542 def markempty(self):
540 """Mark the node as emptied."""
543 """Mark the node as emptied."""
541 self.key = _notset
544 self.key = _notset
542
545
543 class lrucachedict(object):
546 class lrucachedict(object):
544 """Dict that caches most recent accesses and sets.
547 """Dict that caches most recent accesses and sets.
545
548
546 The dict consists of an actual backing dict - indexed by original
549 The dict consists of an actual backing dict - indexed by original
547 key - and a doubly linked circular list defining the order of entries in
550 key - and a doubly linked circular list defining the order of entries in
548 the cache.
551 the cache.
549
552
550 The head node is the newest entry in the cache. If the cache is full,
553 The head node is the newest entry in the cache. If the cache is full,
551 we recycle head.prev and make it the new head. Cache accesses result in
554 we recycle head.prev and make it the new head. Cache accesses result in
552 the node being moved to before the existing head and being marked as the
555 the node being moved to before the existing head and being marked as the
553 new head node.
556 new head node.
554 """
557 """
555 def __init__(self, max):
558 def __init__(self, max):
556 self._cache = {}
559 self._cache = {}
557
560
558 self._head = head = _lrucachenode()
561 self._head = head = _lrucachenode()
559 head.prev = head
562 head.prev = head
560 head.next = head
563 head.next = head
561 self._size = 1
564 self._size = 1
562 self._capacity = max
565 self._capacity = max
563
566
564 def __len__(self):
567 def __len__(self):
565 return len(self._cache)
568 return len(self._cache)
566
569
567 def __contains__(self, k):
570 def __contains__(self, k):
568 return k in self._cache
571 return k in self._cache
569
572
570 def __iter__(self):
573 def __iter__(self):
571 # We don't have to iterate in cache order, but why not.
574 # We don't have to iterate in cache order, but why not.
572 n = self._head
575 n = self._head
573 for i in range(len(self._cache)):
576 for i in range(len(self._cache)):
574 yield n.key
577 yield n.key
575 n = n.next
578 n = n.next
576
579
577 def __getitem__(self, k):
580 def __getitem__(self, k):
578 node = self._cache[k]
581 node = self._cache[k]
579 self._movetohead(node)
582 self._movetohead(node)
580 return node.value
583 return node.value
581
584
582 def __setitem__(self, k, v):
585 def __setitem__(self, k, v):
583 node = self._cache.get(k)
586 node = self._cache.get(k)
584 # Replace existing value and mark as newest.
587 # Replace existing value and mark as newest.
585 if node is not None:
588 if node is not None:
586 node.value = v
589 node.value = v
587 self._movetohead(node)
590 self._movetohead(node)
588 return
591 return
589
592
590 if self._size < self._capacity:
593 if self._size < self._capacity:
591 node = self._addcapacity()
594 node = self._addcapacity()
592 else:
595 else:
593 # Grab the last/oldest item.
596 # Grab the last/oldest item.
594 node = self._head.prev
597 node = self._head.prev
595
598
596 # At capacity. Kill the old entry.
599 # At capacity. Kill the old entry.
597 if node.key is not _notset:
600 if node.key is not _notset:
598 del self._cache[node.key]
601 del self._cache[node.key]
599
602
600 node.key = k
603 node.key = k
601 node.value = v
604 node.value = v
602 self._cache[k] = node
605 self._cache[k] = node
603 # And mark it as newest entry. No need to adjust order since it
606 # And mark it as newest entry. No need to adjust order since it
604 # is already self._head.prev.
607 # is already self._head.prev.
605 self._head = node
608 self._head = node
606
609
607 def __delitem__(self, k):
610 def __delitem__(self, k):
608 node = self._cache.pop(k)
611 node = self._cache.pop(k)
609 node.markempty()
612 node.markempty()
610
613
611 # Temporarily mark as newest item before re-adjusting head to make
614 # Temporarily mark as newest item before re-adjusting head to make
612 # this node the oldest item.
615 # this node the oldest item.
613 self._movetohead(node)
616 self._movetohead(node)
614 self._head = node.next
617 self._head = node.next
615
618
616 # Additional dict methods.
619 # Additional dict methods.
617
620
618 def get(self, k, default=None):
621 def get(self, k, default=None):
619 try:
622 try:
620 return self._cache[k]
623 return self._cache[k]
621 except KeyError:
624 except KeyError:
622 return default
625 return default
623
626
624 def clear(self):
627 def clear(self):
625 n = self._head
628 n = self._head
626 while n.key is not _notset:
629 while n.key is not _notset:
627 n.markempty()
630 n.markempty()
628 n = n.next
631 n = n.next
629
632
630 self._cache.clear()
633 self._cache.clear()
631
634
632 def copy(self):
635 def copy(self):
633 result = lrucachedict(self._capacity)
636 result = lrucachedict(self._capacity)
634 n = self._head.prev
637 n = self._head.prev
635 # Iterate in oldest-to-newest order, so the copy has the right ordering
638 # Iterate in oldest-to-newest order, so the copy has the right ordering
636 for i in range(len(self._cache)):
639 for i in range(len(self._cache)):
637 result[n.key] = n.value
640 result[n.key] = n.value
638 n = n.prev
641 n = n.prev
639 return result
642 return result
640
643
641 def _movetohead(self, node):
644 def _movetohead(self, node):
642 """Mark a node as the newest, making it the new head.
645 """Mark a node as the newest, making it the new head.
643
646
644 When a node is accessed, it becomes the freshest entry in the LRU
647 When a node is accessed, it becomes the freshest entry in the LRU
645 list, which is denoted by self._head.
648 list, which is denoted by self._head.
646
649
647 Visually, let's make ``N`` the new head node (* denotes head):
650 Visually, let's make ``N`` the new head node (* denotes head):
648
651
649 previous/oldest <-> head <-> next/next newest
652 previous/oldest <-> head <-> next/next newest
650
653
651 ----<->--- A* ---<->-----
654 ----<->--- A* ---<->-----
652 | |
655 | |
653 E <-> D <-> N <-> C <-> B
656 E <-> D <-> N <-> C <-> B
654
657
655 To:
658 To:
656
659
657 ----<->--- N* ---<->-----
660 ----<->--- N* ---<->-----
658 | |
661 | |
659 E <-> D <-> C <-> B <-> A
662 E <-> D <-> C <-> B <-> A
660
663
661 This requires the following moves:
664 This requires the following moves:
662
665
663 C.next = D (node.prev.next = node.next)
666 C.next = D (node.prev.next = node.next)
664 D.prev = C (node.next.prev = node.prev)
667 D.prev = C (node.next.prev = node.prev)
665 E.next = N (head.prev.next = node)
668 E.next = N (head.prev.next = node)
666 N.prev = E (node.prev = head.prev)
669 N.prev = E (node.prev = head.prev)
667 N.next = A (node.next = head)
670 N.next = A (node.next = head)
668 A.prev = N (head.prev = node)
671 A.prev = N (head.prev = node)
669 """
672 """
670 head = self._head
673 head = self._head
671 # C.next = D
674 # C.next = D
672 node.prev.next = node.next
675 node.prev.next = node.next
673 # D.prev = C
676 # D.prev = C
674 node.next.prev = node.prev
677 node.next.prev = node.prev
675 # N.prev = E
678 # N.prev = E
676 node.prev = head.prev
679 node.prev = head.prev
677 # N.next = A
680 # N.next = A
678 # It is tempting to do just "head" here, however if node is
681 # It is tempting to do just "head" here, however if node is
679 # adjacent to head, this will do bad things.
682 # adjacent to head, this will do bad things.
680 node.next = head.prev.next
683 node.next = head.prev.next
681 # E.next = N
684 # E.next = N
682 node.next.prev = node
685 node.next.prev = node
683 # A.prev = N
686 # A.prev = N
684 node.prev.next = node
687 node.prev.next = node
685
688
686 self._head = node
689 self._head = node
687
690
688 def _addcapacity(self):
691 def _addcapacity(self):
689 """Add a node to the circular linked list.
692 """Add a node to the circular linked list.
690
693
691 The new node is inserted before the head node.
694 The new node is inserted before the head node.
692 """
695 """
693 head = self._head
696 head = self._head
694 node = _lrucachenode()
697 node = _lrucachenode()
695 head.prev.next = node
698 head.prev.next = node
696 node.prev = head.prev
699 node.prev = head.prev
697 node.next = head
700 node.next = head
698 head.prev = node
701 head.prev = node
699 self._size += 1
702 self._size += 1
700 return node
703 return node
701
704
702 def lrucachefunc(func):
705 def lrucachefunc(func):
703 '''cache most recent results of function calls'''
706 '''cache most recent results of function calls'''
704 cache = {}
707 cache = {}
705 order = collections.deque()
708 order = collections.deque()
706 if func.__code__.co_argcount == 1:
709 if func.__code__.co_argcount == 1:
707 def f(arg):
710 def f(arg):
708 if arg not in cache:
711 if arg not in cache:
709 if len(cache) > 20:
712 if len(cache) > 20:
710 del cache[order.popleft()]
713 del cache[order.popleft()]
711 cache[arg] = func(arg)
714 cache[arg] = func(arg)
712 else:
715 else:
713 order.remove(arg)
716 order.remove(arg)
714 order.append(arg)
717 order.append(arg)
715 return cache[arg]
718 return cache[arg]
716 else:
719 else:
717 def f(*args):
720 def f(*args):
718 if args not in cache:
721 if args not in cache:
719 if len(cache) > 20:
722 if len(cache) > 20:
720 del cache[order.popleft()]
723 del cache[order.popleft()]
721 cache[args] = func(*args)
724 cache[args] = func(*args)
722 else:
725 else:
723 order.remove(args)
726 order.remove(args)
724 order.append(args)
727 order.append(args)
725 return cache[args]
728 return cache[args]
726
729
727 return f
730 return f
728
731
729 class propertycache(object):
732 class propertycache(object):
730 def __init__(self, func):
733 def __init__(self, func):
731 self.func = func
734 self.func = func
732 self.name = func.__name__
735 self.name = func.__name__
733 def __get__(self, obj, type=None):
736 def __get__(self, obj, type=None):
734 result = self.func(obj)
737 result = self.func(obj)
735 self.cachevalue(obj, result)
738 self.cachevalue(obj, result)
736 return result
739 return result
737
740
738 def cachevalue(self, obj, value):
741 def cachevalue(self, obj, value):
739 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
742 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
740 obj.__dict__[self.name] = value
743 obj.__dict__[self.name] = value
741
744
742 def pipefilter(s, cmd):
745 def pipefilter(s, cmd):
743 '''filter string S through command CMD, returning its output'''
746 '''filter string S through command CMD, returning its output'''
744 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
747 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
745 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
748 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
746 pout, perr = p.communicate(s)
749 pout, perr = p.communicate(s)
747 return pout
750 return pout
748
751
749 def tempfilter(s, cmd):
752 def tempfilter(s, cmd):
750 '''filter string S through a pair of temporary files with CMD.
753 '''filter string S through a pair of temporary files with CMD.
751 CMD is used as a template to create the real command to be run,
754 CMD is used as a template to create the real command to be run,
752 with the strings INFILE and OUTFILE replaced by the real names of
755 with the strings INFILE and OUTFILE replaced by the real names of
753 the temporary files generated.'''
756 the temporary files generated.'''
754 inname, outname = None, None
757 inname, outname = None, None
755 try:
758 try:
756 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
759 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
757 fp = os.fdopen(infd, 'wb')
760 fp = os.fdopen(infd, 'wb')
758 fp.write(s)
761 fp.write(s)
759 fp.close()
762 fp.close()
760 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
763 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
761 os.close(outfd)
764 os.close(outfd)
762 cmd = cmd.replace('INFILE', inname)
765 cmd = cmd.replace('INFILE', inname)
763 cmd = cmd.replace('OUTFILE', outname)
766 cmd = cmd.replace('OUTFILE', outname)
764 code = os.system(cmd)
767 code = os.system(cmd)
765 if sys.platform == 'OpenVMS' and code & 1:
768 if sys.platform == 'OpenVMS' and code & 1:
766 code = 0
769 code = 0
767 if code:
770 if code:
768 raise Abort(_("command '%s' failed: %s") %
771 raise Abort(_("command '%s' failed: %s") %
769 (cmd, explainexit(code)))
772 (cmd, explainexit(code)))
770 return readfile(outname)
773 return readfile(outname)
771 finally:
774 finally:
772 try:
775 try:
773 if inname:
776 if inname:
774 os.unlink(inname)
777 os.unlink(inname)
775 except OSError:
778 except OSError:
776 pass
779 pass
777 try:
780 try:
778 if outname:
781 if outname:
779 os.unlink(outname)
782 os.unlink(outname)
780 except OSError:
783 except OSError:
781 pass
784 pass
782
785
783 filtertable = {
786 filtertable = {
784 'tempfile:': tempfilter,
787 'tempfile:': tempfilter,
785 'pipe:': pipefilter,
788 'pipe:': pipefilter,
786 }
789 }
787
790
788 def filter(s, cmd):
791 def filter(s, cmd):
789 "filter a string through a command that transforms its input to its output"
792 "filter a string through a command that transforms its input to its output"
790 for name, fn in filtertable.iteritems():
793 for name, fn in filtertable.iteritems():
791 if cmd.startswith(name):
794 if cmd.startswith(name):
792 return fn(s, cmd[len(name):].lstrip())
795 return fn(s, cmd[len(name):].lstrip())
793 return pipefilter(s, cmd)
796 return pipefilter(s, cmd)
794
797
795 def binary(s):
798 def binary(s):
796 """return true if a string is binary data"""
799 """return true if a string is binary data"""
797 return bool(s and '\0' in s)
800 return bool(s and '\0' in s)
798
801
799 def increasingchunks(source, min=1024, max=65536):
802 def increasingchunks(source, min=1024, max=65536):
800 '''return no less than min bytes per chunk while data remains,
803 '''return no less than min bytes per chunk while data remains,
801 doubling min after each chunk until it reaches max'''
804 doubling min after each chunk until it reaches max'''
802 def log2(x):
805 def log2(x):
803 if not x:
806 if not x:
804 return 0
807 return 0
805 i = 0
808 i = 0
806 while x:
809 while x:
807 x >>= 1
810 x >>= 1
808 i += 1
811 i += 1
809 return i - 1
812 return i - 1
810
813
811 buf = []
814 buf = []
812 blen = 0
815 blen = 0
813 for chunk in source:
816 for chunk in source:
814 buf.append(chunk)
817 buf.append(chunk)
815 blen += len(chunk)
818 blen += len(chunk)
816 if blen >= min:
819 if blen >= min:
817 if min < max:
820 if min < max:
818 min = min << 1
821 min = min << 1
819 nmin = 1 << log2(blen)
822 nmin = 1 << log2(blen)
820 if nmin > min:
823 if nmin > min:
821 min = nmin
824 min = nmin
822 if min > max:
825 if min > max:
823 min = max
826 min = max
824 yield ''.join(buf)
827 yield ''.join(buf)
825 blen = 0
828 blen = 0
826 buf = []
829 buf = []
827 if buf:
830 if buf:
828 yield ''.join(buf)
831 yield ''.join(buf)
829
832
830 Abort = error.Abort
833 Abort = error.Abort
831
834
832 def always(fn):
835 def always(fn):
833 return True
836 return True
834
837
835 def never(fn):
838 def never(fn):
836 return False
839 return False
837
840
838 def nogc(func):
841 def nogc(func):
839 """disable garbage collector
842 """disable garbage collector
840
843
841 Python's garbage collector triggers a GC each time a certain number of
844 Python's garbage collector triggers a GC each time a certain number of
842 container objects (the number being defined by gc.get_threshold()) are
845 container objects (the number being defined by gc.get_threshold()) are
843 allocated even when marked not to be tracked by the collector. Tracking has
846 allocated even when marked not to be tracked by the collector. Tracking has
844 no effect on when GCs are triggered, only on what objects the GC looks
847 no effect on when GCs are triggered, only on what objects the GC looks
845 into. As a workaround, disable GC while building complex (huge)
848 into. As a workaround, disable GC while building complex (huge)
846 containers.
849 containers.
847
850
848 This garbage collector issue have been fixed in 2.7.
851 This garbage collector issue have been fixed in 2.7.
849 """
852 """
850 def wrapper(*args, **kwargs):
853 def wrapper(*args, **kwargs):
851 gcenabled = gc.isenabled()
854 gcenabled = gc.isenabled()
852 gc.disable()
855 gc.disable()
853 try:
856 try:
854 return func(*args, **kwargs)
857 return func(*args, **kwargs)
855 finally:
858 finally:
856 if gcenabled:
859 if gcenabled:
857 gc.enable()
860 gc.enable()
858 return wrapper
861 return wrapper
859
862
860 def pathto(root, n1, n2):
863 def pathto(root, n1, n2):
861 '''return the relative path from one place to another.
864 '''return the relative path from one place to another.
862 root should use os.sep to separate directories
865 root should use os.sep to separate directories
863 n1 should use os.sep to separate directories
866 n1 should use os.sep to separate directories
864 n2 should use "/" to separate directories
867 n2 should use "/" to separate directories
865 returns an os.sep-separated path.
868 returns an os.sep-separated path.
866
869
867 If n1 is a relative path, it's assumed it's
870 If n1 is a relative path, it's assumed it's
868 relative to root.
871 relative to root.
869 n2 should always be relative to root.
872 n2 should always be relative to root.
870 '''
873 '''
871 if not n1:
874 if not n1:
872 return localpath(n2)
875 return localpath(n2)
873 if os.path.isabs(n1):
876 if os.path.isabs(n1):
874 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
877 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
875 return os.path.join(root, localpath(n2))
878 return os.path.join(root, localpath(n2))
876 n2 = '/'.join((pconvert(root), n2))
879 n2 = '/'.join((pconvert(root), n2))
877 a, b = splitpath(n1), n2.split('/')
880 a, b = splitpath(n1), n2.split('/')
878 a.reverse()
881 a.reverse()
879 b.reverse()
882 b.reverse()
880 while a and b and a[-1] == b[-1]:
883 while a and b and a[-1] == b[-1]:
881 a.pop()
884 a.pop()
882 b.pop()
885 b.pop()
883 b.reverse()
886 b.reverse()
884 return os.sep.join((['..'] * len(a)) + b) or '.'
887 return os.sep.join((['..'] * len(a)) + b) or '.'
885
888
886 def mainfrozen():
889 def mainfrozen():
887 """return True if we are a frozen executable.
890 """return True if we are a frozen executable.
888
891
889 The code supports py2exe (most common, Windows only) and tools/freeze
892 The code supports py2exe (most common, Windows only) and tools/freeze
890 (portable, not much used).
893 (portable, not much used).
891 """
894 """
892 return (safehasattr(sys, "frozen") or # new py2exe
895 return (safehasattr(sys, "frozen") or # new py2exe
893 safehasattr(sys, "importers") or # old py2exe
896 safehasattr(sys, "importers") or # old py2exe
894 imp.is_frozen("__main__")) # tools/freeze
897 imp.is_frozen("__main__")) # tools/freeze
895
898
896 # the location of data files matching the source code
899 # the location of data files matching the source code
897 if mainfrozen() and getattr(sys, 'frozen', None) != 'macosx_app':
900 if mainfrozen() and getattr(sys, 'frozen', None) != 'macosx_app':
898 # executable version (py2exe) doesn't support __file__
901 # executable version (py2exe) doesn't support __file__
899 datapath = os.path.dirname(sys.executable)
902 datapath = os.path.dirname(sys.executable)
900 else:
903 else:
901 datapath = os.path.dirname(__file__)
904 datapath = os.path.dirname(__file__)
902
905
903 i18n.setdatapath(datapath)
906 i18n.setdatapath(datapath)
904
907
905 _hgexecutable = None
908 _hgexecutable = None
906
909
907 def hgexecutable():
910 def hgexecutable():
908 """return location of the 'hg' executable.
911 """return location of the 'hg' executable.
909
912
910 Defaults to $HG or 'hg' in the search path.
913 Defaults to $HG or 'hg' in the search path.
911 """
914 """
912 if _hgexecutable is None:
915 if _hgexecutable is None:
913 hg = os.environ.get('HG')
916 hg = os.environ.get('HG')
914 mainmod = sys.modules['__main__']
917 mainmod = sys.modules['__main__']
915 if hg:
918 if hg:
916 _sethgexecutable(hg)
919 _sethgexecutable(hg)
917 elif mainfrozen():
920 elif mainfrozen():
918 if getattr(sys, 'frozen', None) == 'macosx_app':
921 if getattr(sys, 'frozen', None) == 'macosx_app':
919 # Env variable set by py2app
922 # Env variable set by py2app
920 _sethgexecutable(os.environ['EXECUTABLEPATH'])
923 _sethgexecutable(os.environ['EXECUTABLEPATH'])
921 else:
924 else:
922 _sethgexecutable(sys.executable)
925 _sethgexecutable(sys.executable)
923 elif os.path.basename(getattr(mainmod, '__file__', '')) == 'hg':
926 elif os.path.basename(getattr(mainmod, '__file__', '')) == 'hg':
924 _sethgexecutable(mainmod.__file__)
927 _sethgexecutable(mainmod.__file__)
925 else:
928 else:
926 exe = findexe('hg') or os.path.basename(sys.argv[0])
929 exe = findexe('hg') or os.path.basename(sys.argv[0])
927 _sethgexecutable(exe)
930 _sethgexecutable(exe)
928 return _hgexecutable
931 return _hgexecutable
929
932
930 def _sethgexecutable(path):
933 def _sethgexecutable(path):
931 """set location of the 'hg' executable"""
934 """set location of the 'hg' executable"""
932 global _hgexecutable
935 global _hgexecutable
933 _hgexecutable = path
936 _hgexecutable = path
934
937
935 def _isstdout(f):
938 def _isstdout(f):
936 fileno = getattr(f, 'fileno', None)
939 fileno = getattr(f, 'fileno', None)
937 return fileno and fileno() == sys.__stdout__.fileno()
940 return fileno and fileno() == sys.__stdout__.fileno()
938
941
939 def system(cmd, environ=None, cwd=None, onerr=None, errprefix=None, out=None):
942 def system(cmd, environ=None, cwd=None, onerr=None, errprefix=None, out=None):
940 '''enhanced shell command execution.
943 '''enhanced shell command execution.
941 run with environment maybe modified, maybe in different dir.
944 run with environment maybe modified, maybe in different dir.
942
945
943 if command fails and onerr is None, return status, else raise onerr
946 if command fails and onerr is None, return status, else raise onerr
944 object as exception.
947 object as exception.
945
948
946 if out is specified, it is assumed to be a file-like object that has a
949 if out is specified, it is assumed to be a file-like object that has a
947 write() method. stdout and stderr will be redirected to out.'''
950 write() method. stdout and stderr will be redirected to out.'''
948 if environ is None:
951 if environ is None:
949 environ = {}
952 environ = {}
950 try:
953 try:
951 sys.stdout.flush()
954 sys.stdout.flush()
952 except Exception:
955 except Exception:
953 pass
956 pass
954 def py2shell(val):
957 def py2shell(val):
955 'convert python object into string that is useful to shell'
958 'convert python object into string that is useful to shell'
956 if val is None or val is False:
959 if val is None or val is False:
957 return '0'
960 return '0'
958 if val is True:
961 if val is True:
959 return '1'
962 return '1'
960 return str(val)
963 return str(val)
961 origcmd = cmd
964 origcmd = cmd
962 cmd = quotecommand(cmd)
965 cmd = quotecommand(cmd)
963 if sys.platform == 'plan9' and (sys.version_info[0] == 2
966 if sys.platform == 'plan9' and (sys.version_info[0] == 2
964 and sys.version_info[1] < 7):
967 and sys.version_info[1] < 7):
965 # subprocess kludge to work around issues in half-baked Python
968 # subprocess kludge to work around issues in half-baked Python
966 # ports, notably bichued/python:
969 # ports, notably bichued/python:
967 if not cwd is None:
970 if not cwd is None:
968 os.chdir(cwd)
971 os.chdir(cwd)
969 rc = os.system(cmd)
972 rc = os.system(cmd)
970 else:
973 else:
971 env = dict(os.environ)
974 env = dict(os.environ)
972 env.update((k, py2shell(v)) for k, v in environ.iteritems())
975 env.update((k, py2shell(v)) for k, v in environ.iteritems())
973 env['HG'] = hgexecutable()
976 env['HG'] = hgexecutable()
974 if out is None or _isstdout(out):
977 if out is None or _isstdout(out):
975 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
978 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
976 env=env, cwd=cwd)
979 env=env, cwd=cwd)
977 else:
980 else:
978 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
981 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
979 env=env, cwd=cwd, stdout=subprocess.PIPE,
982 env=env, cwd=cwd, stdout=subprocess.PIPE,
980 stderr=subprocess.STDOUT)
983 stderr=subprocess.STDOUT)
981 while True:
984 while True:
982 line = proc.stdout.readline()
985 line = proc.stdout.readline()
983 if not line:
986 if not line:
984 break
987 break
985 out.write(line)
988 out.write(line)
986 proc.wait()
989 proc.wait()
987 rc = proc.returncode
990 rc = proc.returncode
988 if sys.platform == 'OpenVMS' and rc & 1:
991 if sys.platform == 'OpenVMS' and rc & 1:
989 rc = 0
992 rc = 0
990 if rc and onerr:
993 if rc and onerr:
991 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
994 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
992 explainexit(rc)[0])
995 explainexit(rc)[0])
993 if errprefix:
996 if errprefix:
994 errmsg = '%s: %s' % (errprefix, errmsg)
997 errmsg = '%s: %s' % (errprefix, errmsg)
995 raise onerr(errmsg)
998 raise onerr(errmsg)
996 return rc
999 return rc
997
1000
998 def checksignature(func):
1001 def checksignature(func):
999 '''wrap a function with code to check for calling errors'''
1002 '''wrap a function with code to check for calling errors'''
1000 def check(*args, **kwargs):
1003 def check(*args, **kwargs):
1001 try:
1004 try:
1002 return func(*args, **kwargs)
1005 return func(*args, **kwargs)
1003 except TypeError:
1006 except TypeError:
1004 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
1007 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
1005 raise error.SignatureError
1008 raise error.SignatureError
1006 raise
1009 raise
1007
1010
1008 return check
1011 return check
1009
1012
1010 def copyfile(src, dest, hardlink=False, copystat=False):
1013 def copyfile(src, dest, hardlink=False, copystat=False):
1011 '''copy a file, preserving mode and optionally other stat info like
1014 '''copy a file, preserving mode and optionally other stat info like
1012 atime/mtime'''
1015 atime/mtime'''
1013 if os.path.lexists(dest):
1016 if os.path.lexists(dest):
1014 unlink(dest)
1017 unlink(dest)
1015 # hardlinks are problematic on CIFS, quietly ignore this flag
1018 # hardlinks are problematic on CIFS, quietly ignore this flag
1016 # until we find a way to work around it cleanly (issue4546)
1019 # until we find a way to work around it cleanly (issue4546)
1017 if False and hardlink:
1020 if False and hardlink:
1018 try:
1021 try:
1019 oslink(src, dest)
1022 oslink(src, dest)
1020 return
1023 return
1021 except (IOError, OSError):
1024 except (IOError, OSError):
1022 pass # fall back to normal copy
1025 pass # fall back to normal copy
1023 if os.path.islink(src):
1026 if os.path.islink(src):
1024 os.symlink(os.readlink(src), dest)
1027 os.symlink(os.readlink(src), dest)
1025 # copytime is ignored for symlinks, but in general copytime isn't needed
1028 # copytime is ignored for symlinks, but in general copytime isn't needed
1026 # for them anyway
1029 # for them anyway
1027 else:
1030 else:
1028 try:
1031 try:
1029 shutil.copyfile(src, dest)
1032 shutil.copyfile(src, dest)
1030 if copystat:
1033 if copystat:
1031 # copystat also copies mode
1034 # copystat also copies mode
1032 shutil.copystat(src, dest)
1035 shutil.copystat(src, dest)
1033 else:
1036 else:
1034 shutil.copymode(src, dest)
1037 shutil.copymode(src, dest)
1035 except shutil.Error as inst:
1038 except shutil.Error as inst:
1036 raise Abort(str(inst))
1039 raise Abort(str(inst))
1037
1040
1038 def copyfiles(src, dst, hardlink=None, progress=lambda t, pos: None):
1041 def copyfiles(src, dst, hardlink=None, progress=lambda t, pos: None):
1039 """Copy a directory tree using hardlinks if possible."""
1042 """Copy a directory tree using hardlinks if possible."""
1040 num = 0
1043 num = 0
1041
1044
1042 if hardlink is None:
1045 if hardlink is None:
1043 hardlink = (os.stat(src).st_dev ==
1046 hardlink = (os.stat(src).st_dev ==
1044 os.stat(os.path.dirname(dst)).st_dev)
1047 os.stat(os.path.dirname(dst)).st_dev)
1045 if hardlink:
1048 if hardlink:
1046 topic = _('linking')
1049 topic = _('linking')
1047 else:
1050 else:
1048 topic = _('copying')
1051 topic = _('copying')
1049
1052
1050 if os.path.isdir(src):
1053 if os.path.isdir(src):
1051 os.mkdir(dst)
1054 os.mkdir(dst)
1052 for name, kind in osutil.listdir(src):
1055 for name, kind in osutil.listdir(src):
1053 srcname = os.path.join(src, name)
1056 srcname = os.path.join(src, name)
1054 dstname = os.path.join(dst, name)
1057 dstname = os.path.join(dst, name)
1055 def nprog(t, pos):
1058 def nprog(t, pos):
1056 if pos is not None:
1059 if pos is not None:
1057 return progress(t, pos + num)
1060 return progress(t, pos + num)
1058 hardlink, n = copyfiles(srcname, dstname, hardlink, progress=nprog)
1061 hardlink, n = copyfiles(srcname, dstname, hardlink, progress=nprog)
1059 num += n
1062 num += n
1060 else:
1063 else:
1061 if hardlink:
1064 if hardlink:
1062 try:
1065 try:
1063 oslink(src, dst)
1066 oslink(src, dst)
1064 except (IOError, OSError):
1067 except (IOError, OSError):
1065 hardlink = False
1068 hardlink = False
1066 shutil.copy(src, dst)
1069 shutil.copy(src, dst)
1067 else:
1070 else:
1068 shutil.copy(src, dst)
1071 shutil.copy(src, dst)
1069 num += 1
1072 num += 1
1070 progress(topic, num)
1073 progress(topic, num)
1071 progress(topic, None)
1074 progress(topic, None)
1072
1075
1073 return hardlink, num
1076 return hardlink, num
1074
1077
1075 _winreservednames = '''con prn aux nul
1078 _winreservednames = '''con prn aux nul
1076 com1 com2 com3 com4 com5 com6 com7 com8 com9
1079 com1 com2 com3 com4 com5 com6 com7 com8 com9
1077 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
1080 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
1078 _winreservedchars = ':*?"<>|'
1081 _winreservedchars = ':*?"<>|'
1079 def checkwinfilename(path):
1082 def checkwinfilename(path):
1080 r'''Check that the base-relative path is a valid filename on Windows.
1083 r'''Check that the base-relative path is a valid filename on Windows.
1081 Returns None if the path is ok, or a UI string describing the problem.
1084 Returns None if the path is ok, or a UI string describing the problem.
1082
1085
1083 >>> checkwinfilename("just/a/normal/path")
1086 >>> checkwinfilename("just/a/normal/path")
1084 >>> checkwinfilename("foo/bar/con.xml")
1087 >>> checkwinfilename("foo/bar/con.xml")
1085 "filename contains 'con', which is reserved on Windows"
1088 "filename contains 'con', which is reserved on Windows"
1086 >>> checkwinfilename("foo/con.xml/bar")
1089 >>> checkwinfilename("foo/con.xml/bar")
1087 "filename contains 'con', which is reserved on Windows"
1090 "filename contains 'con', which is reserved on Windows"
1088 >>> checkwinfilename("foo/bar/xml.con")
1091 >>> checkwinfilename("foo/bar/xml.con")
1089 >>> checkwinfilename("foo/bar/AUX/bla.txt")
1092 >>> checkwinfilename("foo/bar/AUX/bla.txt")
1090 "filename contains 'AUX', which is reserved on Windows"
1093 "filename contains 'AUX', which is reserved on Windows"
1091 >>> checkwinfilename("foo/bar/bla:.txt")
1094 >>> checkwinfilename("foo/bar/bla:.txt")
1092 "filename contains ':', which is reserved on Windows"
1095 "filename contains ':', which is reserved on Windows"
1093 >>> checkwinfilename("foo/bar/b\07la.txt")
1096 >>> checkwinfilename("foo/bar/b\07la.txt")
1094 "filename contains '\\x07', which is invalid on Windows"
1097 "filename contains '\\x07', which is invalid on Windows"
1095 >>> checkwinfilename("foo/bar/bla ")
1098 >>> checkwinfilename("foo/bar/bla ")
1096 "filename ends with ' ', which is not allowed on Windows"
1099 "filename ends with ' ', which is not allowed on Windows"
1097 >>> checkwinfilename("../bar")
1100 >>> checkwinfilename("../bar")
1098 >>> checkwinfilename("foo\\")
1101 >>> checkwinfilename("foo\\")
1099 "filename ends with '\\', which is invalid on Windows"
1102 "filename ends with '\\', which is invalid on Windows"
1100 >>> checkwinfilename("foo\\/bar")
1103 >>> checkwinfilename("foo\\/bar")
1101 "directory name ends with '\\', which is invalid on Windows"
1104 "directory name ends with '\\', which is invalid on Windows"
1102 '''
1105 '''
1103 if path.endswith('\\'):
1106 if path.endswith('\\'):
1104 return _("filename ends with '\\', which is invalid on Windows")
1107 return _("filename ends with '\\', which is invalid on Windows")
1105 if '\\/' in path:
1108 if '\\/' in path:
1106 return _("directory name ends with '\\', which is invalid on Windows")
1109 return _("directory name ends with '\\', which is invalid on Windows")
1107 for n in path.replace('\\', '/').split('/'):
1110 for n in path.replace('\\', '/').split('/'):
1108 if not n:
1111 if not n:
1109 continue
1112 continue
1110 for c in n:
1113 for c in n:
1111 if c in _winreservedchars:
1114 if c in _winreservedchars:
1112 return _("filename contains '%s', which is reserved "
1115 return _("filename contains '%s', which is reserved "
1113 "on Windows") % c
1116 "on Windows") % c
1114 if ord(c) <= 31:
1117 if ord(c) <= 31:
1115 return _("filename contains %r, which is invalid "
1118 return _("filename contains %r, which is invalid "
1116 "on Windows") % c
1119 "on Windows") % c
1117 base = n.split('.')[0]
1120 base = n.split('.')[0]
1118 if base and base.lower() in _winreservednames:
1121 if base and base.lower() in _winreservednames:
1119 return _("filename contains '%s', which is reserved "
1122 return _("filename contains '%s', which is reserved "
1120 "on Windows") % base
1123 "on Windows") % base
1121 t = n[-1]
1124 t = n[-1]
1122 if t in '. ' and n not in '..':
1125 if t in '. ' and n not in '..':
1123 return _("filename ends with '%s', which is not allowed "
1126 return _("filename ends with '%s', which is not allowed "
1124 "on Windows") % t
1127 "on Windows") % t
1125
1128
1126 if os.name == 'nt':
1129 if os.name == 'nt':
1127 checkosfilename = checkwinfilename
1130 checkosfilename = checkwinfilename
1128 else:
1131 else:
1129 checkosfilename = platform.checkosfilename
1132 checkosfilename = platform.checkosfilename
1130
1133
1131 def makelock(info, pathname):
1134 def makelock(info, pathname):
1132 try:
1135 try:
1133 return os.symlink(info, pathname)
1136 return os.symlink(info, pathname)
1134 except OSError as why:
1137 except OSError as why:
1135 if why.errno == errno.EEXIST:
1138 if why.errno == errno.EEXIST:
1136 raise
1139 raise
1137 except AttributeError: # no symlink in os
1140 except AttributeError: # no symlink in os
1138 pass
1141 pass
1139
1142
1140 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
1143 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
1141 os.write(ld, info)
1144 os.write(ld, info)
1142 os.close(ld)
1145 os.close(ld)
1143
1146
1144 def readlock(pathname):
1147 def readlock(pathname):
1145 try:
1148 try:
1146 return os.readlink(pathname)
1149 return os.readlink(pathname)
1147 except OSError as why:
1150 except OSError as why:
1148 if why.errno not in (errno.EINVAL, errno.ENOSYS):
1151 if why.errno not in (errno.EINVAL, errno.ENOSYS):
1149 raise
1152 raise
1150 except AttributeError: # no symlink in os
1153 except AttributeError: # no symlink in os
1151 pass
1154 pass
1152 fp = posixfile(pathname)
1155 fp = posixfile(pathname)
1153 r = fp.read()
1156 r = fp.read()
1154 fp.close()
1157 fp.close()
1155 return r
1158 return r
1156
1159
1157 def fstat(fp):
1160 def fstat(fp):
1158 '''stat file object that may not have fileno method.'''
1161 '''stat file object that may not have fileno method.'''
1159 try:
1162 try:
1160 return os.fstat(fp.fileno())
1163 return os.fstat(fp.fileno())
1161 except AttributeError:
1164 except AttributeError:
1162 return os.stat(fp.name)
1165 return os.stat(fp.name)
1163
1166
1164 # File system features
1167 # File system features
1165
1168
1166 def checkcase(path):
1169 def checkcase(path):
1167 """
1170 """
1168 Return true if the given path is on a case-sensitive filesystem
1171 Return true if the given path is on a case-sensitive filesystem
1169
1172
1170 Requires a path (like /foo/.hg) ending with a foldable final
1173 Requires a path (like /foo/.hg) ending with a foldable final
1171 directory component.
1174 directory component.
1172 """
1175 """
1173 s1 = os.lstat(path)
1176 s1 = os.lstat(path)
1174 d, b = os.path.split(path)
1177 d, b = os.path.split(path)
1175 b2 = b.upper()
1178 b2 = b.upper()
1176 if b == b2:
1179 if b == b2:
1177 b2 = b.lower()
1180 b2 = b.lower()
1178 if b == b2:
1181 if b == b2:
1179 return True # no evidence against case sensitivity
1182 return True # no evidence against case sensitivity
1180 p2 = os.path.join(d, b2)
1183 p2 = os.path.join(d, b2)
1181 try:
1184 try:
1182 s2 = os.lstat(p2)
1185 s2 = os.lstat(p2)
1183 if s2 == s1:
1186 if s2 == s1:
1184 return False
1187 return False
1185 return True
1188 return True
1186 except OSError:
1189 except OSError:
1187 return True
1190 return True
1188
1191
1189 try:
1192 try:
1190 import re2
1193 import re2
1191 _re2 = None
1194 _re2 = None
1192 except ImportError:
1195 except ImportError:
1193 _re2 = False
1196 _re2 = False
1194
1197
1195 class _re(object):
1198 class _re(object):
1196 def _checkre2(self):
1199 def _checkre2(self):
1197 global _re2
1200 global _re2
1198 try:
1201 try:
1199 # check if match works, see issue3964
1202 # check if match works, see issue3964
1200 _re2 = bool(re2.match(r'\[([^\[]+)\]', '[ui]'))
1203 _re2 = bool(re2.match(r'\[([^\[]+)\]', '[ui]'))
1201 except ImportError:
1204 except ImportError:
1202 _re2 = False
1205 _re2 = False
1203
1206
1204 def compile(self, pat, flags=0):
1207 def compile(self, pat, flags=0):
1205 '''Compile a regular expression, using re2 if possible
1208 '''Compile a regular expression, using re2 if possible
1206
1209
1207 For best performance, use only re2-compatible regexp features. The
1210 For best performance, use only re2-compatible regexp features. The
1208 only flags from the re module that are re2-compatible are
1211 only flags from the re module that are re2-compatible are
1209 IGNORECASE and MULTILINE.'''
1212 IGNORECASE and MULTILINE.'''
1210 if _re2 is None:
1213 if _re2 is None:
1211 self._checkre2()
1214 self._checkre2()
1212 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
1215 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
1213 if flags & remod.IGNORECASE:
1216 if flags & remod.IGNORECASE:
1214 pat = '(?i)' + pat
1217 pat = '(?i)' + pat
1215 if flags & remod.MULTILINE:
1218 if flags & remod.MULTILINE:
1216 pat = '(?m)' + pat
1219 pat = '(?m)' + pat
1217 try:
1220 try:
1218 return re2.compile(pat)
1221 return re2.compile(pat)
1219 except re2.error:
1222 except re2.error:
1220 pass
1223 pass
1221 return remod.compile(pat, flags)
1224 return remod.compile(pat, flags)
1222
1225
1223 @propertycache
1226 @propertycache
1224 def escape(self):
1227 def escape(self):
1225 '''Return the version of escape corresponding to self.compile.
1228 '''Return the version of escape corresponding to self.compile.
1226
1229
1227 This is imperfect because whether re2 or re is used for a particular
1230 This is imperfect because whether re2 or re is used for a particular
1228 function depends on the flags, etc, but it's the best we can do.
1231 function depends on the flags, etc, but it's the best we can do.
1229 '''
1232 '''
1230 global _re2
1233 global _re2
1231 if _re2 is None:
1234 if _re2 is None:
1232 self._checkre2()
1235 self._checkre2()
1233 if _re2:
1236 if _re2:
1234 return re2.escape
1237 return re2.escape
1235 else:
1238 else:
1236 return remod.escape
1239 return remod.escape
1237
1240
1238 re = _re()
1241 re = _re()
1239
1242
1240 _fspathcache = {}
1243 _fspathcache = {}
1241 def fspath(name, root):
1244 def fspath(name, root):
1242 '''Get name in the case stored in the filesystem
1245 '''Get name in the case stored in the filesystem
1243
1246
1244 The name should be relative to root, and be normcase-ed for efficiency.
1247 The name should be relative to root, and be normcase-ed for efficiency.
1245
1248
1246 Note that this function is unnecessary, and should not be
1249 Note that this function is unnecessary, and should not be
1247 called, for case-sensitive filesystems (simply because it's expensive).
1250 called, for case-sensitive filesystems (simply because it's expensive).
1248
1251
1249 The root should be normcase-ed, too.
1252 The root should be normcase-ed, too.
1250 '''
1253 '''
1251 def _makefspathcacheentry(dir):
1254 def _makefspathcacheentry(dir):
1252 return dict((normcase(n), n) for n in os.listdir(dir))
1255 return dict((normcase(n), n) for n in os.listdir(dir))
1253
1256
1254 seps = os.sep
1257 seps = os.sep
1255 if os.altsep:
1258 if os.altsep:
1256 seps = seps + os.altsep
1259 seps = seps + os.altsep
1257 # Protect backslashes. This gets silly very quickly.
1260 # Protect backslashes. This gets silly very quickly.
1258 seps.replace('\\','\\\\')
1261 seps.replace('\\','\\\\')
1259 pattern = remod.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
1262 pattern = remod.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
1260 dir = os.path.normpath(root)
1263 dir = os.path.normpath(root)
1261 result = []
1264 result = []
1262 for part, sep in pattern.findall(name):
1265 for part, sep in pattern.findall(name):
1263 if sep:
1266 if sep:
1264 result.append(sep)
1267 result.append(sep)
1265 continue
1268 continue
1266
1269
1267 if dir not in _fspathcache:
1270 if dir not in _fspathcache:
1268 _fspathcache[dir] = _makefspathcacheentry(dir)
1271 _fspathcache[dir] = _makefspathcacheentry(dir)
1269 contents = _fspathcache[dir]
1272 contents = _fspathcache[dir]
1270
1273
1271 found = contents.get(part)
1274 found = contents.get(part)
1272 if not found:
1275 if not found:
1273 # retry "once per directory" per "dirstate.walk" which
1276 # retry "once per directory" per "dirstate.walk" which
1274 # may take place for each patches of "hg qpush", for example
1277 # may take place for each patches of "hg qpush", for example
1275 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
1278 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
1276 found = contents.get(part)
1279 found = contents.get(part)
1277
1280
1278 result.append(found or part)
1281 result.append(found or part)
1279 dir = os.path.join(dir, part)
1282 dir = os.path.join(dir, part)
1280
1283
1281 return ''.join(result)
1284 return ''.join(result)
1282
1285
1283 def checknlink(testfile):
1286 def checknlink(testfile):
1284 '''check whether hardlink count reporting works properly'''
1287 '''check whether hardlink count reporting works properly'''
1285
1288
1286 # testfile may be open, so we need a separate file for checking to
1289 # testfile may be open, so we need a separate file for checking to
1287 # work around issue2543 (or testfile may get lost on Samba shares)
1290 # work around issue2543 (or testfile may get lost on Samba shares)
1288 f1 = testfile + ".hgtmp1"
1291 f1 = testfile + ".hgtmp1"
1289 if os.path.lexists(f1):
1292 if os.path.lexists(f1):
1290 return False
1293 return False
1291 try:
1294 try:
1292 posixfile(f1, 'w').close()
1295 posixfile(f1, 'w').close()
1293 except IOError:
1296 except IOError:
1294 return False
1297 return False
1295
1298
1296 f2 = testfile + ".hgtmp2"
1299 f2 = testfile + ".hgtmp2"
1297 fd = None
1300 fd = None
1298 try:
1301 try:
1299 oslink(f1, f2)
1302 oslink(f1, f2)
1300 # nlinks() may behave differently for files on Windows shares if
1303 # nlinks() may behave differently for files on Windows shares if
1301 # the file is open.
1304 # the file is open.
1302 fd = posixfile(f2)
1305 fd = posixfile(f2)
1303 return nlinks(f2) > 1
1306 return nlinks(f2) > 1
1304 except OSError:
1307 except OSError:
1305 return False
1308 return False
1306 finally:
1309 finally:
1307 if fd is not None:
1310 if fd is not None:
1308 fd.close()
1311 fd.close()
1309 for f in (f1, f2):
1312 for f in (f1, f2):
1310 try:
1313 try:
1311 os.unlink(f)
1314 os.unlink(f)
1312 except OSError:
1315 except OSError:
1313 pass
1316 pass
1314
1317
1315 def endswithsep(path):
1318 def endswithsep(path):
1316 '''Check path ends with os.sep or os.altsep.'''
1319 '''Check path ends with os.sep or os.altsep.'''
1317 return path.endswith(os.sep) or os.altsep and path.endswith(os.altsep)
1320 return path.endswith(os.sep) or os.altsep and path.endswith(os.altsep)
1318
1321
1319 def splitpath(path):
1322 def splitpath(path):
1320 '''Split path by os.sep.
1323 '''Split path by os.sep.
1321 Note that this function does not use os.altsep because this is
1324 Note that this function does not use os.altsep because this is
1322 an alternative of simple "xxx.split(os.sep)".
1325 an alternative of simple "xxx.split(os.sep)".
1323 It is recommended to use os.path.normpath() before using this
1326 It is recommended to use os.path.normpath() before using this
1324 function if need.'''
1327 function if need.'''
1325 return path.split(os.sep)
1328 return path.split(os.sep)
1326
1329
1327 def gui():
1330 def gui():
1328 '''Are we running in a GUI?'''
1331 '''Are we running in a GUI?'''
1329 if sys.platform == 'darwin':
1332 if sys.platform == 'darwin':
1330 if 'SSH_CONNECTION' in os.environ:
1333 if 'SSH_CONNECTION' in os.environ:
1331 # handle SSH access to a box where the user is logged in
1334 # handle SSH access to a box where the user is logged in
1332 return False
1335 return False
1333 elif getattr(osutil, 'isgui', None):
1336 elif getattr(osutil, 'isgui', None):
1334 # check if a CoreGraphics session is available
1337 # check if a CoreGraphics session is available
1335 return osutil.isgui()
1338 return osutil.isgui()
1336 else:
1339 else:
1337 # pure build; use a safe default
1340 # pure build; use a safe default
1338 return True
1341 return True
1339 else:
1342 else:
1340 return os.name == "nt" or os.environ.get("DISPLAY")
1343 return os.name == "nt" or os.environ.get("DISPLAY")
1341
1344
1342 def mktempcopy(name, emptyok=False, createmode=None):
1345 def mktempcopy(name, emptyok=False, createmode=None):
1343 """Create a temporary file with the same contents from name
1346 """Create a temporary file with the same contents from name
1344
1347
1345 The permission bits are copied from the original file.
1348 The permission bits are copied from the original file.
1346
1349
1347 If the temporary file is going to be truncated immediately, you
1350 If the temporary file is going to be truncated immediately, you
1348 can use emptyok=True as an optimization.
1351 can use emptyok=True as an optimization.
1349
1352
1350 Returns the name of the temporary file.
1353 Returns the name of the temporary file.
1351 """
1354 """
1352 d, fn = os.path.split(name)
1355 d, fn = os.path.split(name)
1353 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1356 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1354 os.close(fd)
1357 os.close(fd)
1355 # Temporary files are created with mode 0600, which is usually not
1358 # Temporary files are created with mode 0600, which is usually not
1356 # what we want. If the original file already exists, just copy
1359 # what we want. If the original file already exists, just copy
1357 # its mode. Otherwise, manually obey umask.
1360 # its mode. Otherwise, manually obey umask.
1358 copymode(name, temp, createmode)
1361 copymode(name, temp, createmode)
1359 if emptyok:
1362 if emptyok:
1360 return temp
1363 return temp
1361 try:
1364 try:
1362 try:
1365 try:
1363 ifp = posixfile(name, "rb")
1366 ifp = posixfile(name, "rb")
1364 except IOError as inst:
1367 except IOError as inst:
1365 if inst.errno == errno.ENOENT:
1368 if inst.errno == errno.ENOENT:
1366 return temp
1369 return temp
1367 if not getattr(inst, 'filename', None):
1370 if not getattr(inst, 'filename', None):
1368 inst.filename = name
1371 inst.filename = name
1369 raise
1372 raise
1370 ofp = posixfile(temp, "wb")
1373 ofp = posixfile(temp, "wb")
1371 for chunk in filechunkiter(ifp):
1374 for chunk in filechunkiter(ifp):
1372 ofp.write(chunk)
1375 ofp.write(chunk)
1373 ifp.close()
1376 ifp.close()
1374 ofp.close()
1377 ofp.close()
1375 except: # re-raises
1378 except: # re-raises
1376 try: os.unlink(temp)
1379 try: os.unlink(temp)
1377 except OSError: pass
1380 except OSError: pass
1378 raise
1381 raise
1379 return temp
1382 return temp
1380
1383
1381 class atomictempfile(object):
1384 class atomictempfile(object):
1382 '''writable file object that atomically updates a file
1385 '''writable file object that atomically updates a file
1383
1386
1384 All writes will go to a temporary copy of the original file. Call
1387 All writes will go to a temporary copy of the original file. Call
1385 close() when you are done writing, and atomictempfile will rename
1388 close() when you are done writing, and atomictempfile will rename
1386 the temporary copy to the original name, making the changes
1389 the temporary copy to the original name, making the changes
1387 visible. If the object is destroyed without being closed, all your
1390 visible. If the object is destroyed without being closed, all your
1388 writes are discarded.
1391 writes are discarded.
1389 '''
1392 '''
1390 def __init__(self, name, mode='w+b', createmode=None):
1393 def __init__(self, name, mode='w+b', createmode=None):
1391 self.__name = name # permanent name
1394 self.__name = name # permanent name
1392 self._tempname = mktempcopy(name, emptyok=('w' in mode),
1395 self._tempname = mktempcopy(name, emptyok=('w' in mode),
1393 createmode=createmode)
1396 createmode=createmode)
1394 self._fp = posixfile(self._tempname, mode)
1397 self._fp = posixfile(self._tempname, mode)
1395
1398
1396 # delegated methods
1399 # delegated methods
1397 self.write = self._fp.write
1400 self.write = self._fp.write
1398 self.seek = self._fp.seek
1401 self.seek = self._fp.seek
1399 self.tell = self._fp.tell
1402 self.tell = self._fp.tell
1400 self.fileno = self._fp.fileno
1403 self.fileno = self._fp.fileno
1401
1404
1402 def close(self):
1405 def close(self):
1403 if not self._fp.closed:
1406 if not self._fp.closed:
1404 self._fp.close()
1407 self._fp.close()
1405 rename(self._tempname, localpath(self.__name))
1408 rename(self._tempname, localpath(self.__name))
1406
1409
1407 def discard(self):
1410 def discard(self):
1408 if not self._fp.closed:
1411 if not self._fp.closed:
1409 try:
1412 try:
1410 os.unlink(self._tempname)
1413 os.unlink(self._tempname)
1411 except OSError:
1414 except OSError:
1412 pass
1415 pass
1413 self._fp.close()
1416 self._fp.close()
1414
1417
1415 def __del__(self):
1418 def __del__(self):
1416 if safehasattr(self, '_fp'): # constructor actually did something
1419 if safehasattr(self, '_fp'): # constructor actually did something
1417 self.discard()
1420 self.discard()
1418
1421
1419 def makedirs(name, mode=None, notindexed=False):
1422 def makedirs(name, mode=None, notindexed=False):
1420 """recursive directory creation with parent mode inheritance"""
1423 """recursive directory creation with parent mode inheritance"""
1421 try:
1424 try:
1422 makedir(name, notindexed)
1425 makedir(name, notindexed)
1423 except OSError as err:
1426 except OSError as err:
1424 if err.errno == errno.EEXIST:
1427 if err.errno == errno.EEXIST:
1425 return
1428 return
1426 if err.errno != errno.ENOENT or not name:
1429 if err.errno != errno.ENOENT or not name:
1427 raise
1430 raise
1428 parent = os.path.dirname(os.path.abspath(name))
1431 parent = os.path.dirname(os.path.abspath(name))
1429 if parent == name:
1432 if parent == name:
1430 raise
1433 raise
1431 makedirs(parent, mode, notindexed)
1434 makedirs(parent, mode, notindexed)
1432 makedir(name, notindexed)
1435 makedir(name, notindexed)
1433 if mode is not None:
1436 if mode is not None:
1434 os.chmod(name, mode)
1437 os.chmod(name, mode)
1435
1438
1436 def ensuredirs(name, mode=None, notindexed=False):
1439 def ensuredirs(name, mode=None, notindexed=False):
1437 """race-safe recursive directory creation
1440 """race-safe recursive directory creation
1438
1441
1439 Newly created directories are marked as "not to be indexed by
1442 Newly created directories are marked as "not to be indexed by
1440 the content indexing service", if ``notindexed`` is specified
1443 the content indexing service", if ``notindexed`` is specified
1441 for "write" mode access.
1444 for "write" mode access.
1442 """
1445 """
1443 if os.path.isdir(name):
1446 if os.path.isdir(name):
1444 return
1447 return
1445 parent = os.path.dirname(os.path.abspath(name))
1448 parent = os.path.dirname(os.path.abspath(name))
1446 if parent != name:
1449 if parent != name:
1447 ensuredirs(parent, mode, notindexed)
1450 ensuredirs(parent, mode, notindexed)
1448 try:
1451 try:
1449 makedir(name, notindexed)
1452 makedir(name, notindexed)
1450 except OSError as err:
1453 except OSError as err:
1451 if err.errno == errno.EEXIST and os.path.isdir(name):
1454 if err.errno == errno.EEXIST and os.path.isdir(name):
1452 # someone else seems to have won a directory creation race
1455 # someone else seems to have won a directory creation race
1453 return
1456 return
1454 raise
1457 raise
1455 if mode is not None:
1458 if mode is not None:
1456 os.chmod(name, mode)
1459 os.chmod(name, mode)
1457
1460
1458 def readfile(path):
1461 def readfile(path):
1459 with open(path, 'rb') as fp:
1462 with open(path, 'rb') as fp:
1460 return fp.read()
1463 return fp.read()
1461
1464
1462 def writefile(path, text):
1465 def writefile(path, text):
1463 with open(path, 'wb') as fp:
1466 with open(path, 'wb') as fp:
1464 fp.write(text)
1467 fp.write(text)
1465
1468
1466 def appendfile(path, text):
1469 def appendfile(path, text):
1467 with open(path, 'ab') as fp:
1470 with open(path, 'ab') as fp:
1468 fp.write(text)
1471 fp.write(text)
1469
1472
1470 class chunkbuffer(object):
1473 class chunkbuffer(object):
1471 """Allow arbitrary sized chunks of data to be efficiently read from an
1474 """Allow arbitrary sized chunks of data to be efficiently read from an
1472 iterator over chunks of arbitrary size."""
1475 iterator over chunks of arbitrary size."""
1473
1476
1474 def __init__(self, in_iter):
1477 def __init__(self, in_iter):
1475 """in_iter is the iterator that's iterating over the input chunks.
1478 """in_iter is the iterator that's iterating over the input chunks.
1476 targetsize is how big a buffer to try to maintain."""
1479 targetsize is how big a buffer to try to maintain."""
1477 def splitbig(chunks):
1480 def splitbig(chunks):
1478 for chunk in chunks:
1481 for chunk in chunks:
1479 if len(chunk) > 2**20:
1482 if len(chunk) > 2**20:
1480 pos = 0
1483 pos = 0
1481 while pos < len(chunk):
1484 while pos < len(chunk):
1482 end = pos + 2 ** 18
1485 end = pos + 2 ** 18
1483 yield chunk[pos:end]
1486 yield chunk[pos:end]
1484 pos = end
1487 pos = end
1485 else:
1488 else:
1486 yield chunk
1489 yield chunk
1487 self.iter = splitbig(in_iter)
1490 self.iter = splitbig(in_iter)
1488 self._queue = collections.deque()
1491 self._queue = collections.deque()
1489 self._chunkoffset = 0
1492 self._chunkoffset = 0
1490
1493
1491 def read(self, l=None):
1494 def read(self, l=None):
1492 """Read L bytes of data from the iterator of chunks of data.
1495 """Read L bytes of data from the iterator of chunks of data.
1493 Returns less than L bytes if the iterator runs dry.
1496 Returns less than L bytes if the iterator runs dry.
1494
1497
1495 If size parameter is omitted, read everything"""
1498 If size parameter is omitted, read everything"""
1496 if l is None:
1499 if l is None:
1497 return ''.join(self.iter)
1500 return ''.join(self.iter)
1498
1501
1499 left = l
1502 left = l
1500 buf = []
1503 buf = []
1501 queue = self._queue
1504 queue = self._queue
1502 while left > 0:
1505 while left > 0:
1503 # refill the queue
1506 # refill the queue
1504 if not queue:
1507 if not queue:
1505 target = 2**18
1508 target = 2**18
1506 for chunk in self.iter:
1509 for chunk in self.iter:
1507 queue.append(chunk)
1510 queue.append(chunk)
1508 target -= len(chunk)
1511 target -= len(chunk)
1509 if target <= 0:
1512 if target <= 0:
1510 break
1513 break
1511 if not queue:
1514 if not queue:
1512 break
1515 break
1513
1516
1514 # The easy way to do this would be to queue.popleft(), modify the
1517 # The easy way to do this would be to queue.popleft(), modify the
1515 # chunk (if necessary), then queue.appendleft(). However, for cases
1518 # chunk (if necessary), then queue.appendleft(). However, for cases
1516 # where we read partial chunk content, this incurs 2 dequeue
1519 # where we read partial chunk content, this incurs 2 dequeue
1517 # mutations and creates a new str for the remaining chunk in the
1520 # mutations and creates a new str for the remaining chunk in the
1518 # queue. Our code below avoids this overhead.
1521 # queue. Our code below avoids this overhead.
1519
1522
1520 chunk = queue[0]
1523 chunk = queue[0]
1521 chunkl = len(chunk)
1524 chunkl = len(chunk)
1522 offset = self._chunkoffset
1525 offset = self._chunkoffset
1523
1526
1524 # Use full chunk.
1527 # Use full chunk.
1525 if offset == 0 and left >= chunkl:
1528 if offset == 0 and left >= chunkl:
1526 left -= chunkl
1529 left -= chunkl
1527 queue.popleft()
1530 queue.popleft()
1528 buf.append(chunk)
1531 buf.append(chunk)
1529 # self._chunkoffset remains at 0.
1532 # self._chunkoffset remains at 0.
1530 continue
1533 continue
1531
1534
1532 chunkremaining = chunkl - offset
1535 chunkremaining = chunkl - offset
1533
1536
1534 # Use all of unconsumed part of chunk.
1537 # Use all of unconsumed part of chunk.
1535 if left >= chunkremaining:
1538 if left >= chunkremaining:
1536 left -= chunkremaining
1539 left -= chunkremaining
1537 queue.popleft()
1540 queue.popleft()
1538 # offset == 0 is enabled by block above, so this won't merely
1541 # offset == 0 is enabled by block above, so this won't merely
1539 # copy via ``chunk[0:]``.
1542 # copy via ``chunk[0:]``.
1540 buf.append(chunk[offset:])
1543 buf.append(chunk[offset:])
1541 self._chunkoffset = 0
1544 self._chunkoffset = 0
1542
1545
1543 # Partial chunk needed.
1546 # Partial chunk needed.
1544 else:
1547 else:
1545 buf.append(chunk[offset:offset + left])
1548 buf.append(chunk[offset:offset + left])
1546 self._chunkoffset += left
1549 self._chunkoffset += left
1547 left -= chunkremaining
1550 left -= chunkremaining
1548
1551
1549 return ''.join(buf)
1552 return ''.join(buf)
1550
1553
1551 def filechunkiter(f, size=65536, limit=None):
1554 def filechunkiter(f, size=65536, limit=None):
1552 """Create a generator that produces the data in the file size
1555 """Create a generator that produces the data in the file size
1553 (default 65536) bytes at a time, up to optional limit (default is
1556 (default 65536) bytes at a time, up to optional limit (default is
1554 to read all data). Chunks may be less than size bytes if the
1557 to read all data). Chunks may be less than size bytes if the
1555 chunk is the last chunk in the file, or the file is a socket or
1558 chunk is the last chunk in the file, or the file is a socket or
1556 some other type of file that sometimes reads less data than is
1559 some other type of file that sometimes reads less data than is
1557 requested."""
1560 requested."""
1558 assert size >= 0
1561 assert size >= 0
1559 assert limit is None or limit >= 0
1562 assert limit is None or limit >= 0
1560 while True:
1563 while True:
1561 if limit is None:
1564 if limit is None:
1562 nbytes = size
1565 nbytes = size
1563 else:
1566 else:
1564 nbytes = min(limit, size)
1567 nbytes = min(limit, size)
1565 s = nbytes and f.read(nbytes)
1568 s = nbytes and f.read(nbytes)
1566 if not s:
1569 if not s:
1567 break
1570 break
1568 if limit:
1571 if limit:
1569 limit -= len(s)
1572 limit -= len(s)
1570 yield s
1573 yield s
1571
1574
1572 def makedate(timestamp=None):
1575 def makedate(timestamp=None):
1573 '''Return a unix timestamp (or the current time) as a (unixtime,
1576 '''Return a unix timestamp (or the current time) as a (unixtime,
1574 offset) tuple based off the local timezone.'''
1577 offset) tuple based off the local timezone.'''
1575 if timestamp is None:
1578 if timestamp is None:
1576 timestamp = time.time()
1579 timestamp = time.time()
1577 if timestamp < 0:
1580 if timestamp < 0:
1578 hint = _("check your clock")
1581 hint = _("check your clock")
1579 raise Abort(_("negative timestamp: %d") % timestamp, hint=hint)
1582 raise Abort(_("negative timestamp: %d") % timestamp, hint=hint)
1580 delta = (datetime.datetime.utcfromtimestamp(timestamp) -
1583 delta = (datetime.datetime.utcfromtimestamp(timestamp) -
1581 datetime.datetime.fromtimestamp(timestamp))
1584 datetime.datetime.fromtimestamp(timestamp))
1582 tz = delta.days * 86400 + delta.seconds
1585 tz = delta.days * 86400 + delta.seconds
1583 return timestamp, tz
1586 return timestamp, tz
1584
1587
1585 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1588 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1586 """represent a (unixtime, offset) tuple as a localized time.
1589 """represent a (unixtime, offset) tuple as a localized time.
1587 unixtime is seconds since the epoch, and offset is the time zone's
1590 unixtime is seconds since the epoch, and offset is the time zone's
1588 number of seconds away from UTC.
1591 number of seconds away from UTC.
1589
1592
1590 >>> datestr((0, 0))
1593 >>> datestr((0, 0))
1591 'Thu Jan 01 00:00:00 1970 +0000'
1594 'Thu Jan 01 00:00:00 1970 +0000'
1592 >>> datestr((42, 0))
1595 >>> datestr((42, 0))
1593 'Thu Jan 01 00:00:42 1970 +0000'
1596 'Thu Jan 01 00:00:42 1970 +0000'
1594 >>> datestr((-42, 0))
1597 >>> datestr((-42, 0))
1595 'Wed Dec 31 23:59:18 1969 +0000'
1598 'Wed Dec 31 23:59:18 1969 +0000'
1596 >>> datestr((0x7fffffff, 0))
1599 >>> datestr((0x7fffffff, 0))
1597 'Tue Jan 19 03:14:07 2038 +0000'
1600 'Tue Jan 19 03:14:07 2038 +0000'
1598 >>> datestr((-0x80000000, 0))
1601 >>> datestr((-0x80000000, 0))
1599 'Fri Dec 13 20:45:52 1901 +0000'
1602 'Fri Dec 13 20:45:52 1901 +0000'
1600 """
1603 """
1601 t, tz = date or makedate()
1604 t, tz = date or makedate()
1602 if "%1" in format or "%2" in format or "%z" in format:
1605 if "%1" in format or "%2" in format or "%z" in format:
1603 sign = (tz > 0) and "-" or "+"
1606 sign = (tz > 0) and "-" or "+"
1604 minutes = abs(tz) // 60
1607 minutes = abs(tz) // 60
1605 q, r = divmod(minutes, 60)
1608 q, r = divmod(minutes, 60)
1606 format = format.replace("%z", "%1%2")
1609 format = format.replace("%z", "%1%2")
1607 format = format.replace("%1", "%c%02d" % (sign, q))
1610 format = format.replace("%1", "%c%02d" % (sign, q))
1608 format = format.replace("%2", "%02d" % r)
1611 format = format.replace("%2", "%02d" % r)
1609 d = t - tz
1612 d = t - tz
1610 if d > 0x7fffffff:
1613 if d > 0x7fffffff:
1611 d = 0x7fffffff
1614 d = 0x7fffffff
1612 elif d < -0x80000000:
1615 elif d < -0x80000000:
1613 d = -0x80000000
1616 d = -0x80000000
1614 # Never use time.gmtime() and datetime.datetime.fromtimestamp()
1617 # Never use time.gmtime() and datetime.datetime.fromtimestamp()
1615 # because they use the gmtime() system call which is buggy on Windows
1618 # because they use the gmtime() system call which is buggy on Windows
1616 # for negative values.
1619 # for negative values.
1617 t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d)
1620 t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d)
1618 s = t.strftime(format)
1621 s = t.strftime(format)
1619 return s
1622 return s
1620
1623
1621 def shortdate(date=None):
1624 def shortdate(date=None):
1622 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1625 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1623 return datestr(date, format='%Y-%m-%d')
1626 return datestr(date, format='%Y-%m-%d')
1624
1627
1625 def parsetimezone(tz):
1628 def parsetimezone(tz):
1626 """parse a timezone string and return an offset integer"""
1629 """parse a timezone string and return an offset integer"""
1627 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1630 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1628 sign = (tz[0] == "+") and 1 or -1
1631 sign = (tz[0] == "+") and 1 or -1
1629 hours = int(tz[1:3])
1632 hours = int(tz[1:3])
1630 minutes = int(tz[3:5])
1633 minutes = int(tz[3:5])
1631 return -sign * (hours * 60 + minutes) * 60
1634 return -sign * (hours * 60 + minutes) * 60
1632 if tz == "GMT" or tz == "UTC":
1635 if tz == "GMT" or tz == "UTC":
1633 return 0
1636 return 0
1634 return None
1637 return None
1635
1638
1636 def strdate(string, format, defaults=[]):
1639 def strdate(string, format, defaults=[]):
1637 """parse a localized time string and return a (unixtime, offset) tuple.
1640 """parse a localized time string and return a (unixtime, offset) tuple.
1638 if the string cannot be parsed, ValueError is raised."""
1641 if the string cannot be parsed, ValueError is raised."""
1639 # NOTE: unixtime = localunixtime + offset
1642 # NOTE: unixtime = localunixtime + offset
1640 offset, date = parsetimezone(string.split()[-1]), string
1643 offset, date = parsetimezone(string.split()[-1]), string
1641 if offset is not None:
1644 if offset is not None:
1642 date = " ".join(string.split()[:-1])
1645 date = " ".join(string.split()[:-1])
1643
1646
1644 # add missing elements from defaults
1647 # add missing elements from defaults
1645 usenow = False # default to using biased defaults
1648 usenow = False # default to using biased defaults
1646 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
1649 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
1647 found = [True for p in part if ("%"+p) in format]
1650 found = [True for p in part if ("%"+p) in format]
1648 if not found:
1651 if not found:
1649 date += "@" + defaults[part][usenow]
1652 date += "@" + defaults[part][usenow]
1650 format += "@%" + part[0]
1653 format += "@%" + part[0]
1651 else:
1654 else:
1652 # We've found a specific time element, less specific time
1655 # We've found a specific time element, less specific time
1653 # elements are relative to today
1656 # elements are relative to today
1654 usenow = True
1657 usenow = True
1655
1658
1656 timetuple = time.strptime(date, format)
1659 timetuple = time.strptime(date, format)
1657 localunixtime = int(calendar.timegm(timetuple))
1660 localunixtime = int(calendar.timegm(timetuple))
1658 if offset is None:
1661 if offset is None:
1659 # local timezone
1662 # local timezone
1660 unixtime = int(time.mktime(timetuple))
1663 unixtime = int(time.mktime(timetuple))
1661 offset = unixtime - localunixtime
1664 offset = unixtime - localunixtime
1662 else:
1665 else:
1663 unixtime = localunixtime + offset
1666 unixtime = localunixtime + offset
1664 return unixtime, offset
1667 return unixtime, offset
1665
1668
1666 def parsedate(date, formats=None, bias=None):
1669 def parsedate(date, formats=None, bias=None):
1667 """parse a localized date/time and return a (unixtime, offset) tuple.
1670 """parse a localized date/time and return a (unixtime, offset) tuple.
1668
1671
1669 The date may be a "unixtime offset" string or in one of the specified
1672 The date may be a "unixtime offset" string or in one of the specified
1670 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1673 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1671
1674
1672 >>> parsedate(' today ') == parsedate(\
1675 >>> parsedate(' today ') == parsedate(\
1673 datetime.date.today().strftime('%b %d'))
1676 datetime.date.today().strftime('%b %d'))
1674 True
1677 True
1675 >>> parsedate( 'yesterday ') == parsedate((datetime.date.today() -\
1678 >>> parsedate( 'yesterday ') == parsedate((datetime.date.today() -\
1676 datetime.timedelta(days=1)\
1679 datetime.timedelta(days=1)\
1677 ).strftime('%b %d'))
1680 ).strftime('%b %d'))
1678 True
1681 True
1679 >>> now, tz = makedate()
1682 >>> now, tz = makedate()
1680 >>> strnow, strtz = parsedate('now')
1683 >>> strnow, strtz = parsedate('now')
1681 >>> (strnow - now) < 1
1684 >>> (strnow - now) < 1
1682 True
1685 True
1683 >>> tz == strtz
1686 >>> tz == strtz
1684 True
1687 True
1685 """
1688 """
1686 if bias is None:
1689 if bias is None:
1687 bias = {}
1690 bias = {}
1688 if not date:
1691 if not date:
1689 return 0, 0
1692 return 0, 0
1690 if isinstance(date, tuple) and len(date) == 2:
1693 if isinstance(date, tuple) and len(date) == 2:
1691 return date
1694 return date
1692 if not formats:
1695 if not formats:
1693 formats = defaultdateformats
1696 formats = defaultdateformats
1694 date = date.strip()
1697 date = date.strip()
1695
1698
1696 if date == 'now' or date == _('now'):
1699 if date == 'now' or date == _('now'):
1697 return makedate()
1700 return makedate()
1698 if date == 'today' or date == _('today'):
1701 if date == 'today' or date == _('today'):
1699 date = datetime.date.today().strftime('%b %d')
1702 date = datetime.date.today().strftime('%b %d')
1700 elif date == 'yesterday' or date == _('yesterday'):
1703 elif date == 'yesterday' or date == _('yesterday'):
1701 date = (datetime.date.today() -
1704 date = (datetime.date.today() -
1702 datetime.timedelta(days=1)).strftime('%b %d')
1705 datetime.timedelta(days=1)).strftime('%b %d')
1703
1706
1704 try:
1707 try:
1705 when, offset = map(int, date.split(' '))
1708 when, offset = map(int, date.split(' '))
1706 except ValueError:
1709 except ValueError:
1707 # fill out defaults
1710 # fill out defaults
1708 now = makedate()
1711 now = makedate()
1709 defaults = {}
1712 defaults = {}
1710 for part in ("d", "mb", "yY", "HI", "M", "S"):
1713 for part in ("d", "mb", "yY", "HI", "M", "S"):
1711 # this piece is for rounding the specific end of unknowns
1714 # this piece is for rounding the specific end of unknowns
1712 b = bias.get(part)
1715 b = bias.get(part)
1713 if b is None:
1716 if b is None:
1714 if part[0] in "HMS":
1717 if part[0] in "HMS":
1715 b = "00"
1718 b = "00"
1716 else:
1719 else:
1717 b = "0"
1720 b = "0"
1718
1721
1719 # this piece is for matching the generic end to today's date
1722 # this piece is for matching the generic end to today's date
1720 n = datestr(now, "%" + part[0])
1723 n = datestr(now, "%" + part[0])
1721
1724
1722 defaults[part] = (b, n)
1725 defaults[part] = (b, n)
1723
1726
1724 for format in formats:
1727 for format in formats:
1725 try:
1728 try:
1726 when, offset = strdate(date, format, defaults)
1729 when, offset = strdate(date, format, defaults)
1727 except (ValueError, OverflowError):
1730 except (ValueError, OverflowError):
1728 pass
1731 pass
1729 else:
1732 else:
1730 break
1733 break
1731 else:
1734 else:
1732 raise Abort(_('invalid date: %r') % date)
1735 raise Abort(_('invalid date: %r') % date)
1733 # validate explicit (probably user-specified) date and
1736 # validate explicit (probably user-specified) date and
1734 # time zone offset. values must fit in signed 32 bits for
1737 # time zone offset. values must fit in signed 32 bits for
1735 # current 32-bit linux runtimes. timezones go from UTC-12
1738 # current 32-bit linux runtimes. timezones go from UTC-12
1736 # to UTC+14
1739 # to UTC+14
1737 if when < -0x80000000 or when > 0x7fffffff:
1740 if when < -0x80000000 or when > 0x7fffffff:
1738 raise Abort(_('date exceeds 32 bits: %d') % when)
1741 raise Abort(_('date exceeds 32 bits: %d') % when)
1739 if offset < -50400 or offset > 43200:
1742 if offset < -50400 or offset > 43200:
1740 raise Abort(_('impossible time zone offset: %d') % offset)
1743 raise Abort(_('impossible time zone offset: %d') % offset)
1741 return when, offset
1744 return when, offset
1742
1745
1743 def matchdate(date):
1746 def matchdate(date):
1744 """Return a function that matches a given date match specifier
1747 """Return a function that matches a given date match specifier
1745
1748
1746 Formats include:
1749 Formats include:
1747
1750
1748 '{date}' match a given date to the accuracy provided
1751 '{date}' match a given date to the accuracy provided
1749
1752
1750 '<{date}' on or before a given date
1753 '<{date}' on or before a given date
1751
1754
1752 '>{date}' on or after a given date
1755 '>{date}' on or after a given date
1753
1756
1754 >>> p1 = parsedate("10:29:59")
1757 >>> p1 = parsedate("10:29:59")
1755 >>> p2 = parsedate("10:30:00")
1758 >>> p2 = parsedate("10:30:00")
1756 >>> p3 = parsedate("10:30:59")
1759 >>> p3 = parsedate("10:30:59")
1757 >>> p4 = parsedate("10:31:00")
1760 >>> p4 = parsedate("10:31:00")
1758 >>> p5 = parsedate("Sep 15 10:30:00 1999")
1761 >>> p5 = parsedate("Sep 15 10:30:00 1999")
1759 >>> f = matchdate("10:30")
1762 >>> f = matchdate("10:30")
1760 >>> f(p1[0])
1763 >>> f(p1[0])
1761 False
1764 False
1762 >>> f(p2[0])
1765 >>> f(p2[0])
1763 True
1766 True
1764 >>> f(p3[0])
1767 >>> f(p3[0])
1765 True
1768 True
1766 >>> f(p4[0])
1769 >>> f(p4[0])
1767 False
1770 False
1768 >>> f(p5[0])
1771 >>> f(p5[0])
1769 False
1772 False
1770 """
1773 """
1771
1774
1772 def lower(date):
1775 def lower(date):
1773 d = {'mb': "1", 'd': "1"}
1776 d = {'mb': "1", 'd': "1"}
1774 return parsedate(date, extendeddateformats, d)[0]
1777 return parsedate(date, extendeddateformats, d)[0]
1775
1778
1776 def upper(date):
1779 def upper(date):
1777 d = {'mb': "12", 'HI': "23", 'M': "59", 'S': "59"}
1780 d = {'mb': "12", 'HI': "23", 'M': "59", 'S': "59"}
1778 for days in ("31", "30", "29"):
1781 for days in ("31", "30", "29"):
1779 try:
1782 try:
1780 d["d"] = days
1783 d["d"] = days
1781 return parsedate(date, extendeddateformats, d)[0]
1784 return parsedate(date, extendeddateformats, d)[0]
1782 except Abort:
1785 except Abort:
1783 pass
1786 pass
1784 d["d"] = "28"
1787 d["d"] = "28"
1785 return parsedate(date, extendeddateformats, d)[0]
1788 return parsedate(date, extendeddateformats, d)[0]
1786
1789
1787 date = date.strip()
1790 date = date.strip()
1788
1791
1789 if not date:
1792 if not date:
1790 raise Abort(_("dates cannot consist entirely of whitespace"))
1793 raise Abort(_("dates cannot consist entirely of whitespace"))
1791 elif date[0] == "<":
1794 elif date[0] == "<":
1792 if not date[1:]:
1795 if not date[1:]:
1793 raise Abort(_("invalid day spec, use '<DATE'"))
1796 raise Abort(_("invalid day spec, use '<DATE'"))
1794 when = upper(date[1:])
1797 when = upper(date[1:])
1795 return lambda x: x <= when
1798 return lambda x: x <= when
1796 elif date[0] == ">":
1799 elif date[0] == ">":
1797 if not date[1:]:
1800 if not date[1:]:
1798 raise Abort(_("invalid day spec, use '>DATE'"))
1801 raise Abort(_("invalid day spec, use '>DATE'"))
1799 when = lower(date[1:])
1802 when = lower(date[1:])
1800 return lambda x: x >= when
1803 return lambda x: x >= when
1801 elif date[0] == "-":
1804 elif date[0] == "-":
1802 try:
1805 try:
1803 days = int(date[1:])
1806 days = int(date[1:])
1804 except ValueError:
1807 except ValueError:
1805 raise Abort(_("invalid day spec: %s") % date[1:])
1808 raise Abort(_("invalid day spec: %s") % date[1:])
1806 if days < 0:
1809 if days < 0:
1807 raise Abort(_('%s must be nonnegative (see "hg help dates")')
1810 raise Abort(_('%s must be nonnegative (see "hg help dates")')
1808 % date[1:])
1811 % date[1:])
1809 when = makedate()[0] - days * 3600 * 24
1812 when = makedate()[0] - days * 3600 * 24
1810 return lambda x: x >= when
1813 return lambda x: x >= when
1811 elif " to " in date:
1814 elif " to " in date:
1812 a, b = date.split(" to ")
1815 a, b = date.split(" to ")
1813 start, stop = lower(a), upper(b)
1816 start, stop = lower(a), upper(b)
1814 return lambda x: x >= start and x <= stop
1817 return lambda x: x >= start and x <= stop
1815 else:
1818 else:
1816 start, stop = lower(date), upper(date)
1819 start, stop = lower(date), upper(date)
1817 return lambda x: x >= start and x <= stop
1820 return lambda x: x >= start and x <= stop
1818
1821
1819 def stringmatcher(pattern):
1822 def stringmatcher(pattern):
1820 """
1823 """
1821 accepts a string, possibly starting with 're:' or 'literal:' prefix.
1824 accepts a string, possibly starting with 're:' or 'literal:' prefix.
1822 returns the matcher name, pattern, and matcher function.
1825 returns the matcher name, pattern, and matcher function.
1823 missing or unknown prefixes are treated as literal matches.
1826 missing or unknown prefixes are treated as literal matches.
1824
1827
1825 helper for tests:
1828 helper for tests:
1826 >>> def test(pattern, *tests):
1829 >>> def test(pattern, *tests):
1827 ... kind, pattern, matcher = stringmatcher(pattern)
1830 ... kind, pattern, matcher = stringmatcher(pattern)
1828 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
1831 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
1829
1832
1830 exact matching (no prefix):
1833 exact matching (no prefix):
1831 >>> test('abcdefg', 'abc', 'def', 'abcdefg')
1834 >>> test('abcdefg', 'abc', 'def', 'abcdefg')
1832 ('literal', 'abcdefg', [False, False, True])
1835 ('literal', 'abcdefg', [False, False, True])
1833
1836
1834 regex matching ('re:' prefix)
1837 regex matching ('re:' prefix)
1835 >>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar')
1838 >>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar')
1836 ('re', 'a.+b', [False, False, True])
1839 ('re', 'a.+b', [False, False, True])
1837
1840
1838 force exact matches ('literal:' prefix)
1841 force exact matches ('literal:' prefix)
1839 >>> test('literal:re:foobar', 'foobar', 're:foobar')
1842 >>> test('literal:re:foobar', 'foobar', 're:foobar')
1840 ('literal', 're:foobar', [False, True])
1843 ('literal', 're:foobar', [False, True])
1841
1844
1842 unknown prefixes are ignored and treated as literals
1845 unknown prefixes are ignored and treated as literals
1843 >>> test('foo:bar', 'foo', 'bar', 'foo:bar')
1846 >>> test('foo:bar', 'foo', 'bar', 'foo:bar')
1844 ('literal', 'foo:bar', [False, False, True])
1847 ('literal', 'foo:bar', [False, False, True])
1845 """
1848 """
1846 if pattern.startswith('re:'):
1849 if pattern.startswith('re:'):
1847 pattern = pattern[3:]
1850 pattern = pattern[3:]
1848 try:
1851 try:
1849 regex = remod.compile(pattern)
1852 regex = remod.compile(pattern)
1850 except remod.error as e:
1853 except remod.error as e:
1851 raise error.ParseError(_('invalid regular expression: %s')
1854 raise error.ParseError(_('invalid regular expression: %s')
1852 % e)
1855 % e)
1853 return 're', pattern, regex.search
1856 return 're', pattern, regex.search
1854 elif pattern.startswith('literal:'):
1857 elif pattern.startswith('literal:'):
1855 pattern = pattern[8:]
1858 pattern = pattern[8:]
1856 return 'literal', pattern, pattern.__eq__
1859 return 'literal', pattern, pattern.__eq__
1857
1860
1858 def shortuser(user):
1861 def shortuser(user):
1859 """Return a short representation of a user name or email address."""
1862 """Return a short representation of a user name or email address."""
1860 f = user.find('@')
1863 f = user.find('@')
1861 if f >= 0:
1864 if f >= 0:
1862 user = user[:f]
1865 user = user[:f]
1863 f = user.find('<')
1866 f = user.find('<')
1864 if f >= 0:
1867 if f >= 0:
1865 user = user[f + 1:]
1868 user = user[f + 1:]
1866 f = user.find(' ')
1869 f = user.find(' ')
1867 if f >= 0:
1870 if f >= 0:
1868 user = user[:f]
1871 user = user[:f]
1869 f = user.find('.')
1872 f = user.find('.')
1870 if f >= 0:
1873 if f >= 0:
1871 user = user[:f]
1874 user = user[:f]
1872 return user
1875 return user
1873
1876
1874 def emailuser(user):
1877 def emailuser(user):
1875 """Return the user portion of an email address."""
1878 """Return the user portion of an email address."""
1876 f = user.find('@')
1879 f = user.find('@')
1877 if f >= 0:
1880 if f >= 0:
1878 user = user[:f]
1881 user = user[:f]
1879 f = user.find('<')
1882 f = user.find('<')
1880 if f >= 0:
1883 if f >= 0:
1881 user = user[f + 1:]
1884 user = user[f + 1:]
1882 return user
1885 return user
1883
1886
1884 def email(author):
1887 def email(author):
1885 '''get email of author.'''
1888 '''get email of author.'''
1886 r = author.find('>')
1889 r = author.find('>')
1887 if r == -1:
1890 if r == -1:
1888 r = None
1891 r = None
1889 return author[author.find('<') + 1:r]
1892 return author[author.find('<') + 1:r]
1890
1893
1891 def ellipsis(text, maxlength=400):
1894 def ellipsis(text, maxlength=400):
1892 """Trim string to at most maxlength (default: 400) columns in display."""
1895 """Trim string to at most maxlength (default: 400) columns in display."""
1893 return encoding.trim(text, maxlength, ellipsis='...')
1896 return encoding.trim(text, maxlength, ellipsis='...')
1894
1897
1895 def unitcountfn(*unittable):
1898 def unitcountfn(*unittable):
1896 '''return a function that renders a readable count of some quantity'''
1899 '''return a function that renders a readable count of some quantity'''
1897
1900
1898 def go(count):
1901 def go(count):
1899 for multiplier, divisor, format in unittable:
1902 for multiplier, divisor, format in unittable:
1900 if count >= divisor * multiplier:
1903 if count >= divisor * multiplier:
1901 return format % (count / float(divisor))
1904 return format % (count / float(divisor))
1902 return unittable[-1][2] % count
1905 return unittable[-1][2] % count
1903
1906
1904 return go
1907 return go
1905
1908
1906 bytecount = unitcountfn(
1909 bytecount = unitcountfn(
1907 (100, 1 << 30, _('%.0f GB')),
1910 (100, 1 << 30, _('%.0f GB')),
1908 (10, 1 << 30, _('%.1f GB')),
1911 (10, 1 << 30, _('%.1f GB')),
1909 (1, 1 << 30, _('%.2f GB')),
1912 (1, 1 << 30, _('%.2f GB')),
1910 (100, 1 << 20, _('%.0f MB')),
1913 (100, 1 << 20, _('%.0f MB')),
1911 (10, 1 << 20, _('%.1f MB')),
1914 (10, 1 << 20, _('%.1f MB')),
1912 (1, 1 << 20, _('%.2f MB')),
1915 (1, 1 << 20, _('%.2f MB')),
1913 (100, 1 << 10, _('%.0f KB')),
1916 (100, 1 << 10, _('%.0f KB')),
1914 (10, 1 << 10, _('%.1f KB')),
1917 (10, 1 << 10, _('%.1f KB')),
1915 (1, 1 << 10, _('%.2f KB')),
1918 (1, 1 << 10, _('%.2f KB')),
1916 (1, 1, _('%.0f bytes')),
1919 (1, 1, _('%.0f bytes')),
1917 )
1920 )
1918
1921
1919 def uirepr(s):
1922 def uirepr(s):
1920 # Avoid double backslash in Windows path repr()
1923 # Avoid double backslash in Windows path repr()
1921 return repr(s).replace('\\\\', '\\')
1924 return repr(s).replace('\\\\', '\\')
1922
1925
1923 # delay import of textwrap
1926 # delay import of textwrap
1924 def MBTextWrapper(**kwargs):
1927 def MBTextWrapper(**kwargs):
1925 class tw(textwrap.TextWrapper):
1928 class tw(textwrap.TextWrapper):
1926 """
1929 """
1927 Extend TextWrapper for width-awareness.
1930 Extend TextWrapper for width-awareness.
1928
1931
1929 Neither number of 'bytes' in any encoding nor 'characters' is
1932 Neither number of 'bytes' in any encoding nor 'characters' is
1930 appropriate to calculate terminal columns for specified string.
1933 appropriate to calculate terminal columns for specified string.
1931
1934
1932 Original TextWrapper implementation uses built-in 'len()' directly,
1935 Original TextWrapper implementation uses built-in 'len()' directly,
1933 so overriding is needed to use width information of each characters.
1936 so overriding is needed to use width information of each characters.
1934
1937
1935 In addition, characters classified into 'ambiguous' width are
1938 In addition, characters classified into 'ambiguous' width are
1936 treated as wide in East Asian area, but as narrow in other.
1939 treated as wide in East Asian area, but as narrow in other.
1937
1940
1938 This requires use decision to determine width of such characters.
1941 This requires use decision to determine width of such characters.
1939 """
1942 """
1940 def _cutdown(self, ucstr, space_left):
1943 def _cutdown(self, ucstr, space_left):
1941 l = 0
1944 l = 0
1942 colwidth = encoding.ucolwidth
1945 colwidth = encoding.ucolwidth
1943 for i in xrange(len(ucstr)):
1946 for i in xrange(len(ucstr)):
1944 l += colwidth(ucstr[i])
1947 l += colwidth(ucstr[i])
1945 if space_left < l:
1948 if space_left < l:
1946 return (ucstr[:i], ucstr[i:])
1949 return (ucstr[:i], ucstr[i:])
1947 return ucstr, ''
1950 return ucstr, ''
1948
1951
1949 # overriding of base class
1952 # overriding of base class
1950 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
1953 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
1951 space_left = max(width - cur_len, 1)
1954 space_left = max(width - cur_len, 1)
1952
1955
1953 if self.break_long_words:
1956 if self.break_long_words:
1954 cut, res = self._cutdown(reversed_chunks[-1], space_left)
1957 cut, res = self._cutdown(reversed_chunks[-1], space_left)
1955 cur_line.append(cut)
1958 cur_line.append(cut)
1956 reversed_chunks[-1] = res
1959 reversed_chunks[-1] = res
1957 elif not cur_line:
1960 elif not cur_line:
1958 cur_line.append(reversed_chunks.pop())
1961 cur_line.append(reversed_chunks.pop())
1959
1962
1960 # this overriding code is imported from TextWrapper of Python 2.6
1963 # this overriding code is imported from TextWrapper of Python 2.6
1961 # to calculate columns of string by 'encoding.ucolwidth()'
1964 # to calculate columns of string by 'encoding.ucolwidth()'
1962 def _wrap_chunks(self, chunks):
1965 def _wrap_chunks(self, chunks):
1963 colwidth = encoding.ucolwidth
1966 colwidth = encoding.ucolwidth
1964
1967
1965 lines = []
1968 lines = []
1966 if self.width <= 0:
1969 if self.width <= 0:
1967 raise ValueError("invalid width %r (must be > 0)" % self.width)
1970 raise ValueError("invalid width %r (must be > 0)" % self.width)
1968
1971
1969 # Arrange in reverse order so items can be efficiently popped
1972 # Arrange in reverse order so items can be efficiently popped
1970 # from a stack of chucks.
1973 # from a stack of chucks.
1971 chunks.reverse()
1974 chunks.reverse()
1972
1975
1973 while chunks:
1976 while chunks:
1974
1977
1975 # Start the list of chunks that will make up the current line.
1978 # Start the list of chunks that will make up the current line.
1976 # cur_len is just the length of all the chunks in cur_line.
1979 # cur_len is just the length of all the chunks in cur_line.
1977 cur_line = []
1980 cur_line = []
1978 cur_len = 0
1981 cur_len = 0
1979
1982
1980 # Figure out which static string will prefix this line.
1983 # Figure out which static string will prefix this line.
1981 if lines:
1984 if lines:
1982 indent = self.subsequent_indent
1985 indent = self.subsequent_indent
1983 else:
1986 else:
1984 indent = self.initial_indent
1987 indent = self.initial_indent
1985
1988
1986 # Maximum width for this line.
1989 # Maximum width for this line.
1987 width = self.width - len(indent)
1990 width = self.width - len(indent)
1988
1991
1989 # First chunk on line is whitespace -- drop it, unless this
1992 # First chunk on line is whitespace -- drop it, unless this
1990 # is the very beginning of the text (i.e. no lines started yet).
1993 # is the very beginning of the text (i.e. no lines started yet).
1991 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
1994 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
1992 del chunks[-1]
1995 del chunks[-1]
1993
1996
1994 while chunks:
1997 while chunks:
1995 l = colwidth(chunks[-1])
1998 l = colwidth(chunks[-1])
1996
1999
1997 # Can at least squeeze this chunk onto the current line.
2000 # Can at least squeeze this chunk onto the current line.
1998 if cur_len + l <= width:
2001 if cur_len + l <= width:
1999 cur_line.append(chunks.pop())
2002 cur_line.append(chunks.pop())
2000 cur_len += l
2003 cur_len += l
2001
2004
2002 # Nope, this line is full.
2005 # Nope, this line is full.
2003 else:
2006 else:
2004 break
2007 break
2005
2008
2006 # The current line is full, and the next chunk is too big to
2009 # The current line is full, and the next chunk is too big to
2007 # fit on *any* line (not just this one).
2010 # fit on *any* line (not just this one).
2008 if chunks and colwidth(chunks[-1]) > width:
2011 if chunks and colwidth(chunks[-1]) > width:
2009 self._handle_long_word(chunks, cur_line, cur_len, width)
2012 self._handle_long_word(chunks, cur_line, cur_len, width)
2010
2013
2011 # If the last chunk on this line is all whitespace, drop it.
2014 # If the last chunk on this line is all whitespace, drop it.
2012 if (self.drop_whitespace and
2015 if (self.drop_whitespace and
2013 cur_line and cur_line[-1].strip() == ''):
2016 cur_line and cur_line[-1].strip() == ''):
2014 del cur_line[-1]
2017 del cur_line[-1]
2015
2018
2016 # Convert current line back to a string and store it in list
2019 # Convert current line back to a string and store it in list
2017 # of all lines (return value).
2020 # of all lines (return value).
2018 if cur_line:
2021 if cur_line:
2019 lines.append(indent + ''.join(cur_line))
2022 lines.append(indent + ''.join(cur_line))
2020
2023
2021 return lines
2024 return lines
2022
2025
2023 global MBTextWrapper
2026 global MBTextWrapper
2024 MBTextWrapper = tw
2027 MBTextWrapper = tw
2025 return tw(**kwargs)
2028 return tw(**kwargs)
2026
2029
2027 def wrap(line, width, initindent='', hangindent=''):
2030 def wrap(line, width, initindent='', hangindent=''):
2028 maxindent = max(len(hangindent), len(initindent))
2031 maxindent = max(len(hangindent), len(initindent))
2029 if width <= maxindent:
2032 if width <= maxindent:
2030 # adjust for weird terminal size
2033 # adjust for weird terminal size
2031 width = max(78, maxindent + 1)
2034 width = max(78, maxindent + 1)
2032 line = line.decode(encoding.encoding, encoding.encodingmode)
2035 line = line.decode(encoding.encoding, encoding.encodingmode)
2033 initindent = initindent.decode(encoding.encoding, encoding.encodingmode)
2036 initindent = initindent.decode(encoding.encoding, encoding.encodingmode)
2034 hangindent = hangindent.decode(encoding.encoding, encoding.encodingmode)
2037 hangindent = hangindent.decode(encoding.encoding, encoding.encodingmode)
2035 wrapper = MBTextWrapper(width=width,
2038 wrapper = MBTextWrapper(width=width,
2036 initial_indent=initindent,
2039 initial_indent=initindent,
2037 subsequent_indent=hangindent)
2040 subsequent_indent=hangindent)
2038 return wrapper.fill(line).encode(encoding.encoding)
2041 return wrapper.fill(line).encode(encoding.encoding)
2039
2042
2040 def iterlines(iterator):
2043 def iterlines(iterator):
2041 for chunk in iterator:
2044 for chunk in iterator:
2042 for line in chunk.splitlines():
2045 for line in chunk.splitlines():
2043 yield line
2046 yield line
2044
2047
2045 def expandpath(path):
2048 def expandpath(path):
2046 return os.path.expanduser(os.path.expandvars(path))
2049 return os.path.expanduser(os.path.expandvars(path))
2047
2050
2048 def hgcmd():
2051 def hgcmd():
2049 """Return the command used to execute current hg
2052 """Return the command used to execute current hg
2050
2053
2051 This is different from hgexecutable() because on Windows we want
2054 This is different from hgexecutable() because on Windows we want
2052 to avoid things opening new shell windows like batch files, so we
2055 to avoid things opening new shell windows like batch files, so we
2053 get either the python call or current executable.
2056 get either the python call or current executable.
2054 """
2057 """
2055 if mainfrozen():
2058 if mainfrozen():
2056 if getattr(sys, 'frozen', None) == 'macosx_app':
2059 if getattr(sys, 'frozen', None) == 'macosx_app':
2057 # Env variable set by py2app
2060 # Env variable set by py2app
2058 return [os.environ['EXECUTABLEPATH']]
2061 return [os.environ['EXECUTABLEPATH']]
2059 else:
2062 else:
2060 return [sys.executable]
2063 return [sys.executable]
2061 return gethgcmd()
2064 return gethgcmd()
2062
2065
2063 def rundetached(args, condfn):
2066 def rundetached(args, condfn):
2064 """Execute the argument list in a detached process.
2067 """Execute the argument list in a detached process.
2065
2068
2066 condfn is a callable which is called repeatedly and should return
2069 condfn is a callable which is called repeatedly and should return
2067 True once the child process is known to have started successfully.
2070 True once the child process is known to have started successfully.
2068 At this point, the child process PID is returned. If the child
2071 At this point, the child process PID is returned. If the child
2069 process fails to start or finishes before condfn() evaluates to
2072 process fails to start or finishes before condfn() evaluates to
2070 True, return -1.
2073 True, return -1.
2071 """
2074 """
2072 # Windows case is easier because the child process is either
2075 # Windows case is easier because the child process is either
2073 # successfully starting and validating the condition or exiting
2076 # successfully starting and validating the condition or exiting
2074 # on failure. We just poll on its PID. On Unix, if the child
2077 # on failure. We just poll on its PID. On Unix, if the child
2075 # process fails to start, it will be left in a zombie state until
2078 # process fails to start, it will be left in a zombie state until
2076 # the parent wait on it, which we cannot do since we expect a long
2079 # the parent wait on it, which we cannot do since we expect a long
2077 # running process on success. Instead we listen for SIGCHLD telling
2080 # running process on success. Instead we listen for SIGCHLD telling
2078 # us our child process terminated.
2081 # us our child process terminated.
2079 terminated = set()
2082 terminated = set()
2080 def handler(signum, frame):
2083 def handler(signum, frame):
2081 terminated.add(os.wait())
2084 terminated.add(os.wait())
2082 prevhandler = None
2085 prevhandler = None
2083 SIGCHLD = getattr(signal, 'SIGCHLD', None)
2086 SIGCHLD = getattr(signal, 'SIGCHLD', None)
2084 if SIGCHLD is not None:
2087 if SIGCHLD is not None:
2085 prevhandler = signal.signal(SIGCHLD, handler)
2088 prevhandler = signal.signal(SIGCHLD, handler)
2086 try:
2089 try:
2087 pid = spawndetached(args)
2090 pid = spawndetached(args)
2088 while not condfn():
2091 while not condfn():
2089 if ((pid in terminated or not testpid(pid))
2092 if ((pid in terminated or not testpid(pid))
2090 and not condfn()):
2093 and not condfn()):
2091 return -1
2094 return -1
2092 time.sleep(0.1)
2095 time.sleep(0.1)
2093 return pid
2096 return pid
2094 finally:
2097 finally:
2095 if prevhandler is not None:
2098 if prevhandler is not None:
2096 signal.signal(signal.SIGCHLD, prevhandler)
2099 signal.signal(signal.SIGCHLD, prevhandler)
2097
2100
2098 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2101 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2099 """Return the result of interpolating items in the mapping into string s.
2102 """Return the result of interpolating items in the mapping into string s.
2100
2103
2101 prefix is a single character string, or a two character string with
2104 prefix is a single character string, or a two character string with
2102 a backslash as the first character if the prefix needs to be escaped in
2105 a backslash as the first character if the prefix needs to be escaped in
2103 a regular expression.
2106 a regular expression.
2104
2107
2105 fn is an optional function that will be applied to the replacement text
2108 fn is an optional function that will be applied to the replacement text
2106 just before replacement.
2109 just before replacement.
2107
2110
2108 escape_prefix is an optional flag that allows using doubled prefix for
2111 escape_prefix is an optional flag that allows using doubled prefix for
2109 its escaping.
2112 its escaping.
2110 """
2113 """
2111 fn = fn or (lambda s: s)
2114 fn = fn or (lambda s: s)
2112 patterns = '|'.join(mapping.keys())
2115 patterns = '|'.join(mapping.keys())
2113 if escape_prefix:
2116 if escape_prefix:
2114 patterns += '|' + prefix
2117 patterns += '|' + prefix
2115 if len(prefix) > 1:
2118 if len(prefix) > 1:
2116 prefix_char = prefix[1:]
2119 prefix_char = prefix[1:]
2117 else:
2120 else:
2118 prefix_char = prefix
2121 prefix_char = prefix
2119 mapping[prefix_char] = prefix_char
2122 mapping[prefix_char] = prefix_char
2120 r = remod.compile(r'%s(%s)' % (prefix, patterns))
2123 r = remod.compile(r'%s(%s)' % (prefix, patterns))
2121 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2124 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2122
2125
2123 def getport(port):
2126 def getport(port):
2124 """Return the port for a given network service.
2127 """Return the port for a given network service.
2125
2128
2126 If port is an integer, it's returned as is. If it's a string, it's
2129 If port is an integer, it's returned as is. If it's a string, it's
2127 looked up using socket.getservbyname(). If there's no matching
2130 looked up using socket.getservbyname(). If there's no matching
2128 service, error.Abort is raised.
2131 service, error.Abort is raised.
2129 """
2132 """
2130 try:
2133 try:
2131 return int(port)
2134 return int(port)
2132 except ValueError:
2135 except ValueError:
2133 pass
2136 pass
2134
2137
2135 try:
2138 try:
2136 return socket.getservbyname(port)
2139 return socket.getservbyname(port)
2137 except socket.error:
2140 except socket.error:
2138 raise Abort(_("no port number associated with service '%s'") % port)
2141 raise Abort(_("no port number associated with service '%s'") % port)
2139
2142
2140 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
2143 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
2141 '0': False, 'no': False, 'false': False, 'off': False,
2144 '0': False, 'no': False, 'false': False, 'off': False,
2142 'never': False}
2145 'never': False}
2143
2146
2144 def parsebool(s):
2147 def parsebool(s):
2145 """Parse s into a boolean.
2148 """Parse s into a boolean.
2146
2149
2147 If s is not a valid boolean, returns None.
2150 If s is not a valid boolean, returns None.
2148 """
2151 """
2149 return _booleans.get(s.lower(), None)
2152 return _booleans.get(s.lower(), None)
2150
2153
2151 _hexdig = '0123456789ABCDEFabcdef'
2154 _hexdig = '0123456789ABCDEFabcdef'
2152 _hextochr = dict((a + b, chr(int(a + b, 16)))
2155 _hextochr = dict((a + b, chr(int(a + b, 16)))
2153 for a in _hexdig for b in _hexdig)
2156 for a in _hexdig for b in _hexdig)
2154
2157
2155 def _urlunquote(s):
2158 def _urlunquote(s):
2156 """Decode HTTP/HTML % encoding.
2159 """Decode HTTP/HTML % encoding.
2157
2160
2158 >>> _urlunquote('abc%20def')
2161 >>> _urlunquote('abc%20def')
2159 'abc def'
2162 'abc def'
2160 """
2163 """
2161 res = s.split('%')
2164 res = s.split('%')
2162 # fastpath
2165 # fastpath
2163 if len(res) == 1:
2166 if len(res) == 1:
2164 return s
2167 return s
2165 s = res[0]
2168 s = res[0]
2166 for item in res[1:]:
2169 for item in res[1:]:
2167 try:
2170 try:
2168 s += _hextochr[item[:2]] + item[2:]
2171 s += _hextochr[item[:2]] + item[2:]
2169 except KeyError:
2172 except KeyError:
2170 s += '%' + item
2173 s += '%' + item
2171 except UnicodeDecodeError:
2174 except UnicodeDecodeError:
2172 s += unichr(int(item[:2], 16)) + item[2:]
2175 s += unichr(int(item[:2], 16)) + item[2:]
2173 return s
2176 return s
2174
2177
2175 class url(object):
2178 class url(object):
2176 r"""Reliable URL parser.
2179 r"""Reliable URL parser.
2177
2180
2178 This parses URLs and provides attributes for the following
2181 This parses URLs and provides attributes for the following
2179 components:
2182 components:
2180
2183
2181 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2184 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2182
2185
2183 Missing components are set to None. The only exception is
2186 Missing components are set to None. The only exception is
2184 fragment, which is set to '' if present but empty.
2187 fragment, which is set to '' if present but empty.
2185
2188
2186 If parsefragment is False, fragment is included in query. If
2189 If parsefragment is False, fragment is included in query. If
2187 parsequery is False, query is included in path. If both are
2190 parsequery is False, query is included in path. If both are
2188 False, both fragment and query are included in path.
2191 False, both fragment and query are included in path.
2189
2192
2190 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2193 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2191
2194
2192 Note that for backward compatibility reasons, bundle URLs do not
2195 Note that for backward compatibility reasons, bundle URLs do not
2193 take host names. That means 'bundle://../' has a path of '../'.
2196 take host names. That means 'bundle://../' has a path of '../'.
2194
2197
2195 Examples:
2198 Examples:
2196
2199
2197 >>> url('http://www.ietf.org/rfc/rfc2396.txt')
2200 >>> url('http://www.ietf.org/rfc/rfc2396.txt')
2198 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2201 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2199 >>> url('ssh://[::1]:2200//home/joe/repo')
2202 >>> url('ssh://[::1]:2200//home/joe/repo')
2200 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2203 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2201 >>> url('file:///home/joe/repo')
2204 >>> url('file:///home/joe/repo')
2202 <url scheme: 'file', path: '/home/joe/repo'>
2205 <url scheme: 'file', path: '/home/joe/repo'>
2203 >>> url('file:///c:/temp/foo/')
2206 >>> url('file:///c:/temp/foo/')
2204 <url scheme: 'file', path: 'c:/temp/foo/'>
2207 <url scheme: 'file', path: 'c:/temp/foo/'>
2205 >>> url('bundle:foo')
2208 >>> url('bundle:foo')
2206 <url scheme: 'bundle', path: 'foo'>
2209 <url scheme: 'bundle', path: 'foo'>
2207 >>> url('bundle://../foo')
2210 >>> url('bundle://../foo')
2208 <url scheme: 'bundle', path: '../foo'>
2211 <url scheme: 'bundle', path: '../foo'>
2209 >>> url(r'c:\foo\bar')
2212 >>> url(r'c:\foo\bar')
2210 <url path: 'c:\\foo\\bar'>
2213 <url path: 'c:\\foo\\bar'>
2211 >>> url(r'\\blah\blah\blah')
2214 >>> url(r'\\blah\blah\blah')
2212 <url path: '\\\\blah\\blah\\blah'>
2215 <url path: '\\\\blah\\blah\\blah'>
2213 >>> url(r'\\blah\blah\blah#baz')
2216 >>> url(r'\\blah\blah\blah#baz')
2214 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2217 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2215 >>> url(r'file:///C:\users\me')
2218 >>> url(r'file:///C:\users\me')
2216 <url scheme: 'file', path: 'C:\\users\\me'>
2219 <url scheme: 'file', path: 'C:\\users\\me'>
2217
2220
2218 Authentication credentials:
2221 Authentication credentials:
2219
2222
2220 >>> url('ssh://joe:xyz@x/repo')
2223 >>> url('ssh://joe:xyz@x/repo')
2221 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
2224 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
2222 >>> url('ssh://joe@x/repo')
2225 >>> url('ssh://joe@x/repo')
2223 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
2226 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
2224
2227
2225 Query strings and fragments:
2228 Query strings and fragments:
2226
2229
2227 >>> url('http://host/a?b#c')
2230 >>> url('http://host/a?b#c')
2228 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
2231 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
2229 >>> url('http://host/a?b#c', parsequery=False, parsefragment=False)
2232 >>> url('http://host/a?b#c', parsequery=False, parsefragment=False)
2230 <url scheme: 'http', host: 'host', path: 'a?b#c'>
2233 <url scheme: 'http', host: 'host', path: 'a?b#c'>
2231 """
2234 """
2232
2235
2233 _safechars = "!~*'()+"
2236 _safechars = "!~*'()+"
2234 _safepchars = "/!~*'()+:\\"
2237 _safepchars = "/!~*'()+:\\"
2235 _matchscheme = remod.compile(r'^[a-zA-Z0-9+.\-]+:').match
2238 _matchscheme = remod.compile(r'^[a-zA-Z0-9+.\-]+:').match
2236
2239
2237 def __init__(self, path, parsequery=True, parsefragment=True):
2240 def __init__(self, path, parsequery=True, parsefragment=True):
2238 # We slowly chomp away at path until we have only the path left
2241 # We slowly chomp away at path until we have only the path left
2239 self.scheme = self.user = self.passwd = self.host = None
2242 self.scheme = self.user = self.passwd = self.host = None
2240 self.port = self.path = self.query = self.fragment = None
2243 self.port = self.path = self.query = self.fragment = None
2241 self._localpath = True
2244 self._localpath = True
2242 self._hostport = ''
2245 self._hostport = ''
2243 self._origpath = path
2246 self._origpath = path
2244
2247
2245 if parsefragment and '#' in path:
2248 if parsefragment and '#' in path:
2246 path, self.fragment = path.split('#', 1)
2249 path, self.fragment = path.split('#', 1)
2247 if not path:
2250 if not path:
2248 path = None
2251 path = None
2249
2252
2250 # special case for Windows drive letters and UNC paths
2253 # special case for Windows drive letters and UNC paths
2251 if hasdriveletter(path) or path.startswith(r'\\'):
2254 if hasdriveletter(path) or path.startswith(r'\\'):
2252 self.path = path
2255 self.path = path
2253 return
2256 return
2254
2257
2255 # For compatibility reasons, we can't handle bundle paths as
2258 # For compatibility reasons, we can't handle bundle paths as
2256 # normal URLS
2259 # normal URLS
2257 if path.startswith('bundle:'):
2260 if path.startswith('bundle:'):
2258 self.scheme = 'bundle'
2261 self.scheme = 'bundle'
2259 path = path[7:]
2262 path = path[7:]
2260 if path.startswith('//'):
2263 if path.startswith('//'):
2261 path = path[2:]
2264 path = path[2:]
2262 self.path = path
2265 self.path = path
2263 return
2266 return
2264
2267
2265 if self._matchscheme(path):
2268 if self._matchscheme(path):
2266 parts = path.split(':', 1)
2269 parts = path.split(':', 1)
2267 if parts[0]:
2270 if parts[0]:
2268 self.scheme, path = parts
2271 self.scheme, path = parts
2269 self._localpath = False
2272 self._localpath = False
2270
2273
2271 if not path:
2274 if not path:
2272 path = None
2275 path = None
2273 if self._localpath:
2276 if self._localpath:
2274 self.path = ''
2277 self.path = ''
2275 return
2278 return
2276 else:
2279 else:
2277 if self._localpath:
2280 if self._localpath:
2278 self.path = path
2281 self.path = path
2279 return
2282 return
2280
2283
2281 if parsequery and '?' in path:
2284 if parsequery and '?' in path:
2282 path, self.query = path.split('?', 1)
2285 path, self.query = path.split('?', 1)
2283 if not path:
2286 if not path:
2284 path = None
2287 path = None
2285 if not self.query:
2288 if not self.query:
2286 self.query = None
2289 self.query = None
2287
2290
2288 # // is required to specify a host/authority
2291 # // is required to specify a host/authority
2289 if path and path.startswith('//'):
2292 if path and path.startswith('//'):
2290 parts = path[2:].split('/', 1)
2293 parts = path[2:].split('/', 1)
2291 if len(parts) > 1:
2294 if len(parts) > 1:
2292 self.host, path = parts
2295 self.host, path = parts
2293 else:
2296 else:
2294 self.host = parts[0]
2297 self.host = parts[0]
2295 path = None
2298 path = None
2296 if not self.host:
2299 if not self.host:
2297 self.host = None
2300 self.host = None
2298 # path of file:///d is /d
2301 # path of file:///d is /d
2299 # path of file:///d:/ is d:/, not /d:/
2302 # path of file:///d:/ is d:/, not /d:/
2300 if path and not hasdriveletter(path):
2303 if path and not hasdriveletter(path):
2301 path = '/' + path
2304 path = '/' + path
2302
2305
2303 if self.host and '@' in self.host:
2306 if self.host and '@' in self.host:
2304 self.user, self.host = self.host.rsplit('@', 1)
2307 self.user, self.host = self.host.rsplit('@', 1)
2305 if ':' in self.user:
2308 if ':' in self.user:
2306 self.user, self.passwd = self.user.split(':', 1)
2309 self.user, self.passwd = self.user.split(':', 1)
2307 if not self.host:
2310 if not self.host:
2308 self.host = None
2311 self.host = None
2309
2312
2310 # Don't split on colons in IPv6 addresses without ports
2313 # Don't split on colons in IPv6 addresses without ports
2311 if (self.host and ':' in self.host and
2314 if (self.host and ':' in self.host and
2312 not (self.host.startswith('[') and self.host.endswith(']'))):
2315 not (self.host.startswith('[') and self.host.endswith(']'))):
2313 self._hostport = self.host
2316 self._hostport = self.host
2314 self.host, self.port = self.host.rsplit(':', 1)
2317 self.host, self.port = self.host.rsplit(':', 1)
2315 if not self.host:
2318 if not self.host:
2316 self.host = None
2319 self.host = None
2317
2320
2318 if (self.host and self.scheme == 'file' and
2321 if (self.host and self.scheme == 'file' and
2319 self.host not in ('localhost', '127.0.0.1', '[::1]')):
2322 self.host not in ('localhost', '127.0.0.1', '[::1]')):
2320 raise Abort(_('file:// URLs can only refer to localhost'))
2323 raise Abort(_('file:// URLs can only refer to localhost'))
2321
2324
2322 self.path = path
2325 self.path = path
2323
2326
2324 # leave the query string escaped
2327 # leave the query string escaped
2325 for a in ('user', 'passwd', 'host', 'port',
2328 for a in ('user', 'passwd', 'host', 'port',
2326 'path', 'fragment'):
2329 'path', 'fragment'):
2327 v = getattr(self, a)
2330 v = getattr(self, a)
2328 if v is not None:
2331 if v is not None:
2329 setattr(self, a, _urlunquote(v))
2332 setattr(self, a, _urlunquote(v))
2330
2333
2331 def __repr__(self):
2334 def __repr__(self):
2332 attrs = []
2335 attrs = []
2333 for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path',
2336 for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path',
2334 'query', 'fragment'):
2337 'query', 'fragment'):
2335 v = getattr(self, a)
2338 v = getattr(self, a)
2336 if v is not None:
2339 if v is not None:
2337 attrs.append('%s: %r' % (a, v))
2340 attrs.append('%s: %r' % (a, v))
2338 return '<url %s>' % ', '.join(attrs)
2341 return '<url %s>' % ', '.join(attrs)
2339
2342
2340 def __str__(self):
2343 def __str__(self):
2341 r"""Join the URL's components back into a URL string.
2344 r"""Join the URL's components back into a URL string.
2342
2345
2343 Examples:
2346 Examples:
2344
2347
2345 >>> str(url('http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
2348 >>> str(url('http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
2346 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
2349 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
2347 >>> str(url('http://user:pw@host:80/?foo=bar&baz=42'))
2350 >>> str(url('http://user:pw@host:80/?foo=bar&baz=42'))
2348 'http://user:pw@host:80/?foo=bar&baz=42'
2351 'http://user:pw@host:80/?foo=bar&baz=42'
2349 >>> str(url('http://user:pw@host:80/?foo=bar%3dbaz'))
2352 >>> str(url('http://user:pw@host:80/?foo=bar%3dbaz'))
2350 'http://user:pw@host:80/?foo=bar%3dbaz'
2353 'http://user:pw@host:80/?foo=bar%3dbaz'
2351 >>> str(url('ssh://user:pw@[::1]:2200//home/joe#'))
2354 >>> str(url('ssh://user:pw@[::1]:2200//home/joe#'))
2352 'ssh://user:pw@[::1]:2200//home/joe#'
2355 'ssh://user:pw@[::1]:2200//home/joe#'
2353 >>> str(url('http://localhost:80//'))
2356 >>> str(url('http://localhost:80//'))
2354 'http://localhost:80//'
2357 'http://localhost:80//'
2355 >>> str(url('http://localhost:80/'))
2358 >>> str(url('http://localhost:80/'))
2356 'http://localhost:80/'
2359 'http://localhost:80/'
2357 >>> str(url('http://localhost:80'))
2360 >>> str(url('http://localhost:80'))
2358 'http://localhost:80/'
2361 'http://localhost:80/'
2359 >>> str(url('bundle:foo'))
2362 >>> str(url('bundle:foo'))
2360 'bundle:foo'
2363 'bundle:foo'
2361 >>> str(url('bundle://../foo'))
2364 >>> str(url('bundle://../foo'))
2362 'bundle:../foo'
2365 'bundle:../foo'
2363 >>> str(url('path'))
2366 >>> str(url('path'))
2364 'path'
2367 'path'
2365 >>> str(url('file:///tmp/foo/bar'))
2368 >>> str(url('file:///tmp/foo/bar'))
2366 'file:///tmp/foo/bar'
2369 'file:///tmp/foo/bar'
2367 >>> str(url('file:///c:/tmp/foo/bar'))
2370 >>> str(url('file:///c:/tmp/foo/bar'))
2368 'file:///c:/tmp/foo/bar'
2371 'file:///c:/tmp/foo/bar'
2369 >>> print url(r'bundle:foo\bar')
2372 >>> print url(r'bundle:foo\bar')
2370 bundle:foo\bar
2373 bundle:foo\bar
2371 >>> print url(r'file:///D:\data\hg')
2374 >>> print url(r'file:///D:\data\hg')
2372 file:///D:\data\hg
2375 file:///D:\data\hg
2373 """
2376 """
2374 if self._localpath:
2377 if self._localpath:
2375 s = self.path
2378 s = self.path
2376 if self.scheme == 'bundle':
2379 if self.scheme == 'bundle':
2377 s = 'bundle:' + s
2380 s = 'bundle:' + s
2378 if self.fragment:
2381 if self.fragment:
2379 s += '#' + self.fragment
2382 s += '#' + self.fragment
2380 return s
2383 return s
2381
2384
2382 s = self.scheme + ':'
2385 s = self.scheme + ':'
2383 if self.user or self.passwd or self.host:
2386 if self.user or self.passwd or self.host:
2384 s += '//'
2387 s += '//'
2385 elif self.scheme and (not self.path or self.path.startswith('/')
2388 elif self.scheme and (not self.path or self.path.startswith('/')
2386 or hasdriveletter(self.path)):
2389 or hasdriveletter(self.path)):
2387 s += '//'
2390 s += '//'
2388 if hasdriveletter(self.path):
2391 if hasdriveletter(self.path):
2389 s += '/'
2392 s += '/'
2390 if self.user:
2393 if self.user:
2391 s += urllib.quote(self.user, safe=self._safechars)
2394 s += urlreq.quote(self.user, safe=self._safechars)
2392 if self.passwd:
2395 if self.passwd:
2393 s += ':' + urllib.quote(self.passwd, safe=self._safechars)
2396 s += ':' + urlreq.quote(self.passwd, safe=self._safechars)
2394 if self.user or self.passwd:
2397 if self.user or self.passwd:
2395 s += '@'
2398 s += '@'
2396 if self.host:
2399 if self.host:
2397 if not (self.host.startswith('[') and self.host.endswith(']')):
2400 if not (self.host.startswith('[') and self.host.endswith(']')):
2398 s += urllib.quote(self.host)
2401 s += urlreq.quote(self.host)
2399 else:
2402 else:
2400 s += self.host
2403 s += self.host
2401 if self.port:
2404 if self.port:
2402 s += ':' + urllib.quote(self.port)
2405 s += ':' + urlreq.quote(self.port)
2403 if self.host:
2406 if self.host:
2404 s += '/'
2407 s += '/'
2405 if self.path:
2408 if self.path:
2406 # TODO: similar to the query string, we should not unescape the
2409 # TODO: similar to the query string, we should not unescape the
2407 # path when we store it, the path might contain '%2f' = '/',
2410 # path when we store it, the path might contain '%2f' = '/',
2408 # which we should *not* escape.
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 if self.query:
2413 if self.query:
2411 # we store the query in escaped form.
2414 # we store the query in escaped form.
2412 s += '?' + self.query
2415 s += '?' + self.query
2413 if self.fragment is not None:
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 return s
2418 return s
2416
2419
2417 def authinfo(self):
2420 def authinfo(self):
2418 user, passwd = self.user, self.passwd
2421 user, passwd = self.user, self.passwd
2419 try:
2422 try:
2420 self.user, self.passwd = None, None
2423 self.user, self.passwd = None, None
2421 s = str(self)
2424 s = str(self)
2422 finally:
2425 finally:
2423 self.user, self.passwd = user, passwd
2426 self.user, self.passwd = user, passwd
2424 if not self.user:
2427 if not self.user:
2425 return (s, None)
2428 return (s, None)
2426 # authinfo[1] is passed to urllib2 password manager, and its
2429 # authinfo[1] is passed to urllib2 password manager, and its
2427 # URIs must not contain credentials. The host is passed in the
2430 # URIs must not contain credentials. The host is passed in the
2428 # URIs list because Python < 2.4.3 uses only that to search for
2431 # URIs list because Python < 2.4.3 uses only that to search for
2429 # a password.
2432 # a password.
2430 return (s, (None, (s, self.host),
2433 return (s, (None, (s, self.host),
2431 self.user, self.passwd or ''))
2434 self.user, self.passwd or ''))
2432
2435
2433 def isabs(self):
2436 def isabs(self):
2434 if self.scheme and self.scheme != 'file':
2437 if self.scheme and self.scheme != 'file':
2435 return True # remote URL
2438 return True # remote URL
2436 if hasdriveletter(self.path):
2439 if hasdriveletter(self.path):
2437 return True # absolute for our purposes - can't be joined()
2440 return True # absolute for our purposes - can't be joined()
2438 if self.path.startswith(r'\\'):
2441 if self.path.startswith(r'\\'):
2439 return True # Windows UNC path
2442 return True # Windows UNC path
2440 if self.path.startswith('/'):
2443 if self.path.startswith('/'):
2441 return True # POSIX-style
2444 return True # POSIX-style
2442 return False
2445 return False
2443
2446
2444 def localpath(self):
2447 def localpath(self):
2445 if self.scheme == 'file' or self.scheme == 'bundle':
2448 if self.scheme == 'file' or self.scheme == 'bundle':
2446 path = self.path or '/'
2449 path = self.path or '/'
2447 # For Windows, we need to promote hosts containing drive
2450 # For Windows, we need to promote hosts containing drive
2448 # letters to paths with drive letters.
2451 # letters to paths with drive letters.
2449 if hasdriveletter(self._hostport):
2452 if hasdriveletter(self._hostport):
2450 path = self._hostport + '/' + self.path
2453 path = self._hostport + '/' + self.path
2451 elif (self.host is not None and self.path
2454 elif (self.host is not None and self.path
2452 and not hasdriveletter(path)):
2455 and not hasdriveletter(path)):
2453 path = '/' + path
2456 path = '/' + path
2454 return path
2457 return path
2455 return self._origpath
2458 return self._origpath
2456
2459
2457 def islocal(self):
2460 def islocal(self):
2458 '''whether localpath will return something that posixfile can open'''
2461 '''whether localpath will return something that posixfile can open'''
2459 return (not self.scheme or self.scheme == 'file'
2462 return (not self.scheme or self.scheme == 'file'
2460 or self.scheme == 'bundle')
2463 or self.scheme == 'bundle')
2461
2464
2462 def hasscheme(path):
2465 def hasscheme(path):
2463 return bool(url(path).scheme)
2466 return bool(url(path).scheme)
2464
2467
2465 def hasdriveletter(path):
2468 def hasdriveletter(path):
2466 return path and path[1:2] == ':' and path[0:1].isalpha()
2469 return path and path[1:2] == ':' and path[0:1].isalpha()
2467
2470
2468 def urllocalpath(path):
2471 def urllocalpath(path):
2469 return url(path, parsequery=False, parsefragment=False).localpath()
2472 return url(path, parsequery=False, parsefragment=False).localpath()
2470
2473
2471 def hidepassword(u):
2474 def hidepassword(u):
2472 '''hide user credential in a url string'''
2475 '''hide user credential in a url string'''
2473 u = url(u)
2476 u = url(u)
2474 if u.passwd:
2477 if u.passwd:
2475 u.passwd = '***'
2478 u.passwd = '***'
2476 return str(u)
2479 return str(u)
2477
2480
2478 def removeauth(u):
2481 def removeauth(u):
2479 '''remove all authentication information from a url string'''
2482 '''remove all authentication information from a url string'''
2480 u = url(u)
2483 u = url(u)
2481 u.user = u.passwd = None
2484 u.user = u.passwd = None
2482 return str(u)
2485 return str(u)
2483
2486
2484 def isatty(fp):
2487 def isatty(fp):
2485 try:
2488 try:
2486 return fp.isatty()
2489 return fp.isatty()
2487 except AttributeError:
2490 except AttributeError:
2488 return False
2491 return False
2489
2492
2490 timecount = unitcountfn(
2493 timecount = unitcountfn(
2491 (1, 1e3, _('%.0f s')),
2494 (1, 1e3, _('%.0f s')),
2492 (100, 1, _('%.1f s')),
2495 (100, 1, _('%.1f s')),
2493 (10, 1, _('%.2f s')),
2496 (10, 1, _('%.2f s')),
2494 (1, 1, _('%.3f s')),
2497 (1, 1, _('%.3f s')),
2495 (100, 0.001, _('%.1f ms')),
2498 (100, 0.001, _('%.1f ms')),
2496 (10, 0.001, _('%.2f ms')),
2499 (10, 0.001, _('%.2f ms')),
2497 (1, 0.001, _('%.3f ms')),
2500 (1, 0.001, _('%.3f ms')),
2498 (100, 0.000001, _('%.1f us')),
2501 (100, 0.000001, _('%.1f us')),
2499 (10, 0.000001, _('%.2f us')),
2502 (10, 0.000001, _('%.2f us')),
2500 (1, 0.000001, _('%.3f us')),
2503 (1, 0.000001, _('%.3f us')),
2501 (100, 0.000000001, _('%.1f ns')),
2504 (100, 0.000000001, _('%.1f ns')),
2502 (10, 0.000000001, _('%.2f ns')),
2505 (10, 0.000000001, _('%.2f ns')),
2503 (1, 0.000000001, _('%.3f ns')),
2506 (1, 0.000000001, _('%.3f ns')),
2504 )
2507 )
2505
2508
2506 _timenesting = [0]
2509 _timenesting = [0]
2507
2510
2508 def timed(func):
2511 def timed(func):
2509 '''Report the execution time of a function call to stderr.
2512 '''Report the execution time of a function call to stderr.
2510
2513
2511 During development, use as a decorator when you need to measure
2514 During development, use as a decorator when you need to measure
2512 the cost of a function, e.g. as follows:
2515 the cost of a function, e.g. as follows:
2513
2516
2514 @util.timed
2517 @util.timed
2515 def foo(a, b, c):
2518 def foo(a, b, c):
2516 pass
2519 pass
2517 '''
2520 '''
2518
2521
2519 def wrapper(*args, **kwargs):
2522 def wrapper(*args, **kwargs):
2520 start = time.time()
2523 start = time.time()
2521 indent = 2
2524 indent = 2
2522 _timenesting[0] += indent
2525 _timenesting[0] += indent
2523 try:
2526 try:
2524 return func(*args, **kwargs)
2527 return func(*args, **kwargs)
2525 finally:
2528 finally:
2526 elapsed = time.time() - start
2529 elapsed = time.time() - start
2527 _timenesting[0] -= indent
2530 _timenesting[0] -= indent
2528 sys.stderr.write('%s%s: %s\n' %
2531 sys.stderr.write('%s%s: %s\n' %
2529 (' ' * _timenesting[0], func.__name__,
2532 (' ' * _timenesting[0], func.__name__,
2530 timecount(elapsed)))
2533 timecount(elapsed)))
2531 return wrapper
2534 return wrapper
2532
2535
2533 _sizeunits = (('m', 2**20), ('k', 2**10), ('g', 2**30),
2536 _sizeunits = (('m', 2**20), ('k', 2**10), ('g', 2**30),
2534 ('kb', 2**10), ('mb', 2**20), ('gb', 2**30), ('b', 1))
2537 ('kb', 2**10), ('mb', 2**20), ('gb', 2**30), ('b', 1))
2535
2538
2536 def sizetoint(s):
2539 def sizetoint(s):
2537 '''Convert a space specifier to a byte count.
2540 '''Convert a space specifier to a byte count.
2538
2541
2539 >>> sizetoint('30')
2542 >>> sizetoint('30')
2540 30
2543 30
2541 >>> sizetoint('2.2kb')
2544 >>> sizetoint('2.2kb')
2542 2252
2545 2252
2543 >>> sizetoint('6M')
2546 >>> sizetoint('6M')
2544 6291456
2547 6291456
2545 '''
2548 '''
2546 t = s.strip().lower()
2549 t = s.strip().lower()
2547 try:
2550 try:
2548 for k, u in _sizeunits:
2551 for k, u in _sizeunits:
2549 if t.endswith(k):
2552 if t.endswith(k):
2550 return int(float(t[:-len(k)]) * u)
2553 return int(float(t[:-len(k)]) * u)
2551 return int(t)
2554 return int(t)
2552 except ValueError:
2555 except ValueError:
2553 raise error.ParseError(_("couldn't parse size: %s") % s)
2556 raise error.ParseError(_("couldn't parse size: %s") % s)
2554
2557
2555 class hooks(object):
2558 class hooks(object):
2556 '''A collection of hook functions that can be used to extend a
2559 '''A collection of hook functions that can be used to extend a
2557 function's behavior. Hooks are called in lexicographic order,
2560 function's behavior. Hooks are called in lexicographic order,
2558 based on the names of their sources.'''
2561 based on the names of their sources.'''
2559
2562
2560 def __init__(self):
2563 def __init__(self):
2561 self._hooks = []
2564 self._hooks = []
2562
2565
2563 def add(self, source, hook):
2566 def add(self, source, hook):
2564 self._hooks.append((source, hook))
2567 self._hooks.append((source, hook))
2565
2568
2566 def __call__(self, *args):
2569 def __call__(self, *args):
2567 self._hooks.sort(key=lambda x: x[0])
2570 self._hooks.sort(key=lambda x: x[0])
2568 results = []
2571 results = []
2569 for source, hook in self._hooks:
2572 for source, hook in self._hooks:
2570 results.append(hook(*args))
2573 results.append(hook(*args))
2571 return results
2574 return results
2572
2575
2573 def getstackframes(skip=0, line=' %-*s in %s\n', fileline='%s:%s'):
2576 def getstackframes(skip=0, line=' %-*s in %s\n', fileline='%s:%s'):
2574 '''Yields lines for a nicely formatted stacktrace.
2577 '''Yields lines for a nicely formatted stacktrace.
2575 Skips the 'skip' last entries.
2578 Skips the 'skip' last entries.
2576 Each file+linenumber is formatted according to fileline.
2579 Each file+linenumber is formatted according to fileline.
2577 Each line is formatted according to line.
2580 Each line is formatted according to line.
2578 If line is None, it yields:
2581 If line is None, it yields:
2579 length of longest filepath+line number,
2582 length of longest filepath+line number,
2580 filepath+linenumber,
2583 filepath+linenumber,
2581 function
2584 function
2582
2585
2583 Not be used in production code but very convenient while developing.
2586 Not be used in production code but very convenient while developing.
2584 '''
2587 '''
2585 entries = [(fileline % (fn, ln), func)
2588 entries = [(fileline % (fn, ln), func)
2586 for fn, ln, func, _text in traceback.extract_stack()[:-skip - 1]]
2589 for fn, ln, func, _text in traceback.extract_stack()[:-skip - 1]]
2587 if entries:
2590 if entries:
2588 fnmax = max(len(entry[0]) for entry in entries)
2591 fnmax = max(len(entry[0]) for entry in entries)
2589 for fnln, func in entries:
2592 for fnln, func in entries:
2590 if line is None:
2593 if line is None:
2591 yield (fnmax, fnln, func)
2594 yield (fnmax, fnln, func)
2592 else:
2595 else:
2593 yield line % (fnmax, fnln, func)
2596 yield line % (fnmax, fnln, func)
2594
2597
2595 def debugstacktrace(msg='stacktrace', skip=0, f=sys.stderr, otherf=sys.stdout):
2598 def debugstacktrace(msg='stacktrace', skip=0, f=sys.stderr, otherf=sys.stdout):
2596 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
2599 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
2597 Skips the 'skip' last entries. By default it will flush stdout first.
2600 Skips the 'skip' last entries. By default it will flush stdout first.
2598 It can be used everywhere and intentionally does not require an ui object.
2601 It can be used everywhere and intentionally does not require an ui object.
2599 Not be used in production code but very convenient while developing.
2602 Not be used in production code but very convenient while developing.
2600 '''
2603 '''
2601 if otherf:
2604 if otherf:
2602 otherf.flush()
2605 otherf.flush()
2603 f.write('%s at:\n' % msg)
2606 f.write('%s at:\n' % msg)
2604 for line in getstackframes(skip + 1):
2607 for line in getstackframes(skip + 1):
2605 f.write(line)
2608 f.write(line)
2606 f.flush()
2609 f.flush()
2607
2610
2608 class dirs(object):
2611 class dirs(object):
2609 '''a multiset of directory names from a dirstate or manifest'''
2612 '''a multiset of directory names from a dirstate or manifest'''
2610
2613
2611 def __init__(self, map, skip=None):
2614 def __init__(self, map, skip=None):
2612 self._dirs = {}
2615 self._dirs = {}
2613 addpath = self.addpath
2616 addpath = self.addpath
2614 if safehasattr(map, 'iteritems') and skip is not None:
2617 if safehasattr(map, 'iteritems') and skip is not None:
2615 for f, s in map.iteritems():
2618 for f, s in map.iteritems():
2616 if s[0] != skip:
2619 if s[0] != skip:
2617 addpath(f)
2620 addpath(f)
2618 else:
2621 else:
2619 for f in map:
2622 for f in map:
2620 addpath(f)
2623 addpath(f)
2621
2624
2622 def addpath(self, path):
2625 def addpath(self, path):
2623 dirs = self._dirs
2626 dirs = self._dirs
2624 for base in finddirs(path):
2627 for base in finddirs(path):
2625 if base in dirs:
2628 if base in dirs:
2626 dirs[base] += 1
2629 dirs[base] += 1
2627 return
2630 return
2628 dirs[base] = 1
2631 dirs[base] = 1
2629
2632
2630 def delpath(self, path):
2633 def delpath(self, path):
2631 dirs = self._dirs
2634 dirs = self._dirs
2632 for base in finddirs(path):
2635 for base in finddirs(path):
2633 if dirs[base] > 1:
2636 if dirs[base] > 1:
2634 dirs[base] -= 1
2637 dirs[base] -= 1
2635 return
2638 return
2636 del dirs[base]
2639 del dirs[base]
2637
2640
2638 def __iter__(self):
2641 def __iter__(self):
2639 return self._dirs.iterkeys()
2642 return self._dirs.iterkeys()
2640
2643
2641 def __contains__(self, d):
2644 def __contains__(self, d):
2642 return d in self._dirs
2645 return d in self._dirs
2643
2646
2644 if safehasattr(parsers, 'dirs'):
2647 if safehasattr(parsers, 'dirs'):
2645 dirs = parsers.dirs
2648 dirs = parsers.dirs
2646
2649
2647 def finddirs(path):
2650 def finddirs(path):
2648 pos = path.rfind('/')
2651 pos = path.rfind('/')
2649 while pos != -1:
2652 while pos != -1:
2650 yield path[:pos]
2653 yield path[:pos]
2651 pos = path.rfind('/', 0, pos)
2654 pos = path.rfind('/', 0, pos)
2652
2655
2653 # compression utility
2656 # compression utility
2654
2657
2655 class nocompress(object):
2658 class nocompress(object):
2656 def compress(self, x):
2659 def compress(self, x):
2657 return x
2660 return x
2658 def flush(self):
2661 def flush(self):
2659 return ""
2662 return ""
2660
2663
2661 compressors = {
2664 compressors = {
2662 None: nocompress,
2665 None: nocompress,
2663 # lambda to prevent early import
2666 # lambda to prevent early import
2664 'BZ': lambda: bz2.BZ2Compressor(),
2667 'BZ': lambda: bz2.BZ2Compressor(),
2665 'GZ': lambda: zlib.compressobj(),
2668 'GZ': lambda: zlib.compressobj(),
2666 }
2669 }
2667 # also support the old form by courtesies
2670 # also support the old form by courtesies
2668 compressors['UN'] = compressors[None]
2671 compressors['UN'] = compressors[None]
2669
2672
2670 def _makedecompressor(decompcls):
2673 def _makedecompressor(decompcls):
2671 def generator(f):
2674 def generator(f):
2672 d = decompcls()
2675 d = decompcls()
2673 for chunk in filechunkiter(f):
2676 for chunk in filechunkiter(f):
2674 yield d.decompress(chunk)
2677 yield d.decompress(chunk)
2675 def func(fh):
2678 def func(fh):
2676 return chunkbuffer(generator(fh))
2679 return chunkbuffer(generator(fh))
2677 return func
2680 return func
2678
2681
2679 class ctxmanager(object):
2682 class ctxmanager(object):
2680 '''A context manager for use in 'with' blocks to allow multiple
2683 '''A context manager for use in 'with' blocks to allow multiple
2681 contexts to be entered at once. This is both safer and more
2684 contexts to be entered at once. This is both safer and more
2682 flexible than contextlib.nested.
2685 flexible than contextlib.nested.
2683
2686
2684 Once Mercurial supports Python 2.7+, this will become mostly
2687 Once Mercurial supports Python 2.7+, this will become mostly
2685 unnecessary.
2688 unnecessary.
2686 '''
2689 '''
2687
2690
2688 def __init__(self, *args):
2691 def __init__(self, *args):
2689 '''Accepts a list of no-argument functions that return context
2692 '''Accepts a list of no-argument functions that return context
2690 managers. These will be invoked at __call__ time.'''
2693 managers. These will be invoked at __call__ time.'''
2691 self._pending = args
2694 self._pending = args
2692 self._atexit = []
2695 self._atexit = []
2693
2696
2694 def __enter__(self):
2697 def __enter__(self):
2695 return self
2698 return self
2696
2699
2697 def enter(self):
2700 def enter(self):
2698 '''Create and enter context managers in the order in which they were
2701 '''Create and enter context managers in the order in which they were
2699 passed to the constructor.'''
2702 passed to the constructor.'''
2700 values = []
2703 values = []
2701 for func in self._pending:
2704 for func in self._pending:
2702 obj = func()
2705 obj = func()
2703 values.append(obj.__enter__())
2706 values.append(obj.__enter__())
2704 self._atexit.append(obj.__exit__)
2707 self._atexit.append(obj.__exit__)
2705 del self._pending
2708 del self._pending
2706 return values
2709 return values
2707
2710
2708 def atexit(self, func, *args, **kwargs):
2711 def atexit(self, func, *args, **kwargs):
2709 '''Add a function to call when this context manager exits. The
2712 '''Add a function to call when this context manager exits. The
2710 ordering of multiple atexit calls is unspecified, save that
2713 ordering of multiple atexit calls is unspecified, save that
2711 they will happen before any __exit__ functions.'''
2714 they will happen before any __exit__ functions.'''
2712 def wrapper(exc_type, exc_val, exc_tb):
2715 def wrapper(exc_type, exc_val, exc_tb):
2713 func(*args, **kwargs)
2716 func(*args, **kwargs)
2714 self._atexit.append(wrapper)
2717 self._atexit.append(wrapper)
2715 return func
2718 return func
2716
2719
2717 def __exit__(self, exc_type, exc_val, exc_tb):
2720 def __exit__(self, exc_type, exc_val, exc_tb):
2718 '''Context managers are exited in the reverse order from which
2721 '''Context managers are exited in the reverse order from which
2719 they were created.'''
2722 they were created.'''
2720 received = exc_type is not None
2723 received = exc_type is not None
2721 suppressed = False
2724 suppressed = False
2722 pending = None
2725 pending = None
2723 self._atexit.reverse()
2726 self._atexit.reverse()
2724 for exitfunc in self._atexit:
2727 for exitfunc in self._atexit:
2725 try:
2728 try:
2726 if exitfunc(exc_type, exc_val, exc_tb):
2729 if exitfunc(exc_type, exc_val, exc_tb):
2727 suppressed = True
2730 suppressed = True
2728 exc_type = None
2731 exc_type = None
2729 exc_val = None
2732 exc_val = None
2730 exc_tb = None
2733 exc_tb = None
2731 except BaseException:
2734 except BaseException:
2732 pending = sys.exc_info()
2735 pending = sys.exc_info()
2733 exc_type, exc_val, exc_tb = pending = sys.exc_info()
2736 exc_type, exc_val, exc_tb = pending = sys.exc_info()
2734 del self._atexit
2737 del self._atexit
2735 if pending:
2738 if pending:
2736 raise exc_val
2739 raise exc_val
2737 return received and suppressed
2740 return received and suppressed
2738
2741
2739 def _bz2():
2742 def _bz2():
2740 d = bz2.BZ2Decompressor()
2743 d = bz2.BZ2Decompressor()
2741 # Bzip2 stream start with BZ, but we stripped it.
2744 # Bzip2 stream start with BZ, but we stripped it.
2742 # we put it back for good measure.
2745 # we put it back for good measure.
2743 d.decompress('BZ')
2746 d.decompress('BZ')
2744 return d
2747 return d
2745
2748
2746 decompressors = {None: lambda fh: fh,
2749 decompressors = {None: lambda fh: fh,
2747 '_truncatedBZ': _makedecompressor(_bz2),
2750 '_truncatedBZ': _makedecompressor(_bz2),
2748 'BZ': _makedecompressor(lambda: bz2.BZ2Decompressor()),
2751 'BZ': _makedecompressor(lambda: bz2.BZ2Decompressor()),
2749 'GZ': _makedecompressor(lambda: zlib.decompressobj()),
2752 'GZ': _makedecompressor(lambda: zlib.decompressobj()),
2750 }
2753 }
2751 # also support the old form by courtesies
2754 # also support the old form by courtesies
2752 decompressors['UN'] = decompressors[None]
2755 decompressors['UN'] = decompressors[None]
2753
2756
2754 # convenient shortcut
2757 # convenient shortcut
2755 dst = debugstacktrace
2758 dst = debugstacktrace
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
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