##// END OF EJS Templates
hg: always create new localrepository instance...
Gregory Szorc -
r26240:2b1434e5 default
parent child Browse files
Show More
@@ -1,894 +1,899
1 # hg.py - repository classes for mercurial
1 # hg.py - repository classes for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 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 os
12 import os
13 import shutil
13 import shutil
14
14
15 from .i18n import _
15 from .i18n import _
16 from .node import nullid
16 from .node import nullid
17
17
18 from . import (
18 from . import (
19 bookmarks,
19 bookmarks,
20 bundlerepo,
20 bundlerepo,
21 cmdutil,
21 cmdutil,
22 discovery,
22 discovery,
23 error,
23 error,
24 exchange,
24 exchange,
25 extensions,
25 extensions,
26 httppeer,
26 httppeer,
27 localrepo,
27 localrepo,
28 lock,
28 lock,
29 merge as mergemod,
29 merge as mergemod,
30 node,
30 node,
31 phases,
31 phases,
32 repoview,
32 repoview,
33 scmutil,
33 scmutil,
34 sshpeer,
34 sshpeer,
35 statichttprepo,
35 statichttprepo,
36 ui as uimod,
36 ui as uimod,
37 unionrepo,
37 unionrepo,
38 url,
38 url,
39 util,
39 util,
40 verify as verifymod,
40 verify as verifymod,
41 )
41 )
42
42
43 release = lock.release
43 release = lock.release
44
44
45 def _local(path):
45 def _local(path):
46 path = util.expandpath(util.urllocalpath(path))
46 path = util.expandpath(util.urllocalpath(path))
47 return (os.path.isfile(path) and bundlerepo or localrepo)
47 return (os.path.isfile(path) and bundlerepo or localrepo)
48
48
49 def addbranchrevs(lrepo, other, branches, revs):
49 def addbranchrevs(lrepo, other, branches, revs):
50 peer = other.peer() # a courtesy to callers using a localrepo for other
50 peer = other.peer() # a courtesy to callers using a localrepo for other
51 hashbranch, branches = branches
51 hashbranch, branches = branches
52 if not hashbranch and not branches:
52 if not hashbranch and not branches:
53 x = revs or None
53 x = revs or None
54 if util.safehasattr(revs, 'first'):
54 if util.safehasattr(revs, 'first'):
55 y = revs.first()
55 y = revs.first()
56 elif revs:
56 elif revs:
57 y = revs[0]
57 y = revs[0]
58 else:
58 else:
59 y = None
59 y = None
60 return x, y
60 return x, y
61 if revs:
61 if revs:
62 revs = list(revs)
62 revs = list(revs)
63 else:
63 else:
64 revs = []
64 revs = []
65
65
66 if not peer.capable('branchmap'):
66 if not peer.capable('branchmap'):
67 if branches:
67 if branches:
68 raise util.Abort(_("remote branch lookup not supported"))
68 raise util.Abort(_("remote branch lookup not supported"))
69 revs.append(hashbranch)
69 revs.append(hashbranch)
70 return revs, revs[0]
70 return revs, revs[0]
71 branchmap = peer.branchmap()
71 branchmap = peer.branchmap()
72
72
73 def primary(branch):
73 def primary(branch):
74 if branch == '.':
74 if branch == '.':
75 if not lrepo:
75 if not lrepo:
76 raise util.Abort(_("dirstate branch not accessible"))
76 raise util.Abort(_("dirstate branch not accessible"))
77 branch = lrepo.dirstate.branch()
77 branch = lrepo.dirstate.branch()
78 if branch in branchmap:
78 if branch in branchmap:
79 revs.extend(node.hex(r) for r in reversed(branchmap[branch]))
79 revs.extend(node.hex(r) for r in reversed(branchmap[branch]))
80 return True
80 return True
81 else:
81 else:
82 return False
82 return False
83
83
84 for branch in branches:
84 for branch in branches:
85 if not primary(branch):
85 if not primary(branch):
86 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
86 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
87 if hashbranch:
87 if hashbranch:
88 if not primary(hashbranch):
88 if not primary(hashbranch):
89 revs.append(hashbranch)
89 revs.append(hashbranch)
90 return revs, revs[0]
90 return revs, revs[0]
91
91
92 def parseurl(path, branches=None):
92 def parseurl(path, branches=None):
93 '''parse url#branch, returning (url, (branch, branches))'''
93 '''parse url#branch, returning (url, (branch, branches))'''
94
94
95 u = util.url(path)
95 u = util.url(path)
96 branch = None
96 branch = None
97 if u.fragment:
97 if u.fragment:
98 branch = u.fragment
98 branch = u.fragment
99 u.fragment = None
99 u.fragment = None
100 return str(u), (branch, branches or [])
100 return str(u), (branch, branches or [])
101
101
102 schemes = {
102 schemes = {
103 'bundle': bundlerepo,
103 'bundle': bundlerepo,
104 'union': unionrepo,
104 'union': unionrepo,
105 'file': _local,
105 'file': _local,
106 'http': httppeer,
106 'http': httppeer,
107 'https': httppeer,
107 'https': httppeer,
108 'ssh': sshpeer,
108 'ssh': sshpeer,
109 'static-http': statichttprepo,
109 'static-http': statichttprepo,
110 }
110 }
111
111
112 def _peerlookup(path):
112 def _peerlookup(path):
113 u = util.url(path)
113 u = util.url(path)
114 scheme = u.scheme or 'file'
114 scheme = u.scheme or 'file'
115 thing = schemes.get(scheme) or schemes['file']
115 thing = schemes.get(scheme) or schemes['file']
116 try:
116 try:
117 return thing(path)
117 return thing(path)
118 except TypeError:
118 except TypeError:
119 # we can't test callable(thing) because 'thing' can be an unloaded
119 # we can't test callable(thing) because 'thing' can be an unloaded
120 # module that implements __call__
120 # module that implements __call__
121 if not util.safehasattr(thing, 'instance'):
121 if not util.safehasattr(thing, 'instance'):
122 raise
122 raise
123 return thing
123 return thing
124
124
125 def islocal(repo):
125 def islocal(repo):
126 '''return true if repo (or path pointing to repo) is local'''
126 '''return true if repo (or path pointing to repo) is local'''
127 if isinstance(repo, str):
127 if isinstance(repo, str):
128 try:
128 try:
129 return _peerlookup(repo).islocal(repo)
129 return _peerlookup(repo).islocal(repo)
130 except AttributeError:
130 except AttributeError:
131 return False
131 return False
132 return repo.local()
132 return repo.local()
133
133
134 def openpath(ui, path):
134 def openpath(ui, path):
135 '''open path with open if local, url.open if remote'''
135 '''open path with open if local, url.open if remote'''
136 pathurl = util.url(path, parsequery=False, parsefragment=False)
136 pathurl = util.url(path, parsequery=False, parsefragment=False)
137 if pathurl.islocal():
137 if pathurl.islocal():
138 return util.posixfile(pathurl.localpath(), 'rb')
138 return util.posixfile(pathurl.localpath(), 'rb')
139 else:
139 else:
140 return url.open(ui, path)
140 return url.open(ui, path)
141
141
142 # a list of (ui, repo) functions called for wire peer initialization
142 # a list of (ui, repo) functions called for wire peer initialization
143 wirepeersetupfuncs = []
143 wirepeersetupfuncs = []
144
144
145 def _peerorrepo(ui, path, create=False):
145 def _peerorrepo(ui, path, create=False):
146 """return a repository object for the specified path"""
146 """return a repository object for the specified path"""
147 obj = _peerlookup(path).instance(ui, path, create)
147 obj = _peerlookup(path).instance(ui, path, create)
148 ui = getattr(obj, "ui", ui)
148 ui = getattr(obj, "ui", ui)
149 for name, module in extensions.extensions(ui):
149 for name, module in extensions.extensions(ui):
150 hook = getattr(module, 'reposetup', None)
150 hook = getattr(module, 'reposetup', None)
151 if hook:
151 if hook:
152 hook(ui, obj)
152 hook(ui, obj)
153 if not obj.local():
153 if not obj.local():
154 for f in wirepeersetupfuncs:
154 for f in wirepeersetupfuncs:
155 f(ui, obj)
155 f(ui, obj)
156 return obj
156 return obj
157
157
158 def repository(ui, path='', create=False):
158 def repository(ui, path='', create=False):
159 """return a repository object for the specified path"""
159 """return a repository object for the specified path"""
160 peer = _peerorrepo(ui, path, create)
160 peer = _peerorrepo(ui, path, create)
161 repo = peer.local()
161 repo = peer.local()
162 if not repo:
162 if not repo:
163 raise util.Abort(_("repository '%s' is not local") %
163 raise util.Abort(_("repository '%s' is not local") %
164 (path or peer.url()))
164 (path or peer.url()))
165 return repo.filtered('visible')
165 return repo.filtered('visible')
166
166
167 def peer(uiorrepo, opts, path, create=False):
167 def peer(uiorrepo, opts, path, create=False):
168 '''return a repository peer for the specified path'''
168 '''return a repository peer for the specified path'''
169 rui = remoteui(uiorrepo, opts)
169 rui = remoteui(uiorrepo, opts)
170 return _peerorrepo(rui, path, create).peer()
170 return _peerorrepo(rui, path, create).peer()
171
171
172 def defaultdest(source):
172 def defaultdest(source):
173 '''return default destination of clone if none is given
173 '''return default destination of clone if none is given
174
174
175 >>> defaultdest('foo')
175 >>> defaultdest('foo')
176 'foo'
176 'foo'
177 >>> defaultdest('/foo/bar')
177 >>> defaultdest('/foo/bar')
178 'bar'
178 'bar'
179 >>> defaultdest('/')
179 >>> defaultdest('/')
180 ''
180 ''
181 >>> defaultdest('')
181 >>> defaultdest('')
182 ''
182 ''
183 >>> defaultdest('http://example.org/')
183 >>> defaultdest('http://example.org/')
184 ''
184 ''
185 >>> defaultdest('http://example.org/foo/')
185 >>> defaultdest('http://example.org/foo/')
186 'foo'
186 'foo'
187 '''
187 '''
188 path = util.url(source).path
188 path = util.url(source).path
189 if not path:
189 if not path:
190 return ''
190 return ''
191 return os.path.basename(os.path.normpath(path))
191 return os.path.basename(os.path.normpath(path))
192
192
193 def share(ui, source, dest=None, update=True, bookmarks=True):
193 def share(ui, source, dest=None, update=True, bookmarks=True):
194 '''create a shared repository'''
194 '''create a shared repository'''
195
195
196 if not islocal(source):
196 if not islocal(source):
197 raise util.Abort(_('can only share local repositories'))
197 raise util.Abort(_('can only share local repositories'))
198
198
199 if not dest:
199 if not dest:
200 dest = defaultdest(source)
200 dest = defaultdest(source)
201 else:
201 else:
202 dest = ui.expandpath(dest)
202 dest = ui.expandpath(dest)
203
203
204 if isinstance(source, str):
204 if isinstance(source, str):
205 origsource = ui.expandpath(source)
205 origsource = ui.expandpath(source)
206 source, branches = parseurl(origsource)
206 source, branches = parseurl(origsource)
207 srcrepo = repository(ui, source)
207 srcrepo = repository(ui, source)
208 rev, checkout = addbranchrevs(srcrepo, srcrepo, branches, None)
208 rev, checkout = addbranchrevs(srcrepo, srcrepo, branches, None)
209 else:
209 else:
210 srcrepo = source.local()
210 srcrepo = source.local()
211 origsource = source = srcrepo.url()
211 origsource = source = srcrepo.url()
212 checkout = None
212 checkout = None
213
213
214 sharedpath = srcrepo.sharedpath # if our source is already sharing
214 sharedpath = srcrepo.sharedpath # if our source is already sharing
215
215
216 destwvfs = scmutil.vfs(dest, realpath=True)
216 destwvfs = scmutil.vfs(dest, realpath=True)
217 destvfs = scmutil.vfs(os.path.join(destwvfs.base, '.hg'), realpath=True)
217 destvfs = scmutil.vfs(os.path.join(destwvfs.base, '.hg'), realpath=True)
218
218
219 if destvfs.lexists():
219 if destvfs.lexists():
220 raise util.Abort(_('destination already exists'))
220 raise util.Abort(_('destination already exists'))
221
221
222 if not destwvfs.isdir():
222 if not destwvfs.isdir():
223 destwvfs.mkdir()
223 destwvfs.mkdir()
224 destvfs.makedir()
224 destvfs.makedir()
225
225
226 requirements = ''
226 requirements = ''
227 try:
227 try:
228 requirements = srcrepo.vfs.read('requires')
228 requirements = srcrepo.vfs.read('requires')
229 except IOError as inst:
229 except IOError as inst:
230 if inst.errno != errno.ENOENT:
230 if inst.errno != errno.ENOENT:
231 raise
231 raise
232
232
233 requirements += 'shared\n'
233 requirements += 'shared\n'
234 destvfs.write('requires', requirements)
234 destvfs.write('requires', requirements)
235 destvfs.write('sharedpath', sharedpath)
235 destvfs.write('sharedpath', sharedpath)
236
236
237 r = repository(ui, destwvfs.base)
237 r = repository(ui, destwvfs.base)
238
238
239 default = srcrepo.ui.config('paths', 'default')
239 default = srcrepo.ui.config('paths', 'default')
240 if default:
240 if default:
241 fp = r.vfs("hgrc", "w", text=True)
241 fp = r.vfs("hgrc", "w", text=True)
242 fp.write("[paths]\n")
242 fp.write("[paths]\n")
243 fp.write("default = %s\n" % default)
243 fp.write("default = %s\n" % default)
244 fp.close()
244 fp.close()
245
245
246 if update:
246 if update:
247 r.ui.status(_("updating working directory\n"))
247 r.ui.status(_("updating working directory\n"))
248 if update is not True:
248 if update is not True:
249 checkout = update
249 checkout = update
250 for test in (checkout, 'default', 'tip'):
250 for test in (checkout, 'default', 'tip'):
251 if test is None:
251 if test is None:
252 continue
252 continue
253 try:
253 try:
254 uprev = r.lookup(test)
254 uprev = r.lookup(test)
255 break
255 break
256 except error.RepoLookupError:
256 except error.RepoLookupError:
257 continue
257 continue
258 _update(r, uprev)
258 _update(r, uprev)
259
259
260 if bookmarks:
260 if bookmarks:
261 fp = r.vfs('shared', 'w')
261 fp = r.vfs('shared', 'w')
262 fp.write('bookmarks\n')
262 fp.write('bookmarks\n')
263 fp.close()
263 fp.close()
264
264
265 def copystore(ui, srcrepo, destpath):
265 def copystore(ui, srcrepo, destpath):
266 '''copy files from store of srcrepo in destpath
266 '''copy files from store of srcrepo in destpath
267
267
268 returns destlock
268 returns destlock
269 '''
269 '''
270 destlock = None
270 destlock = None
271 try:
271 try:
272 hardlink = None
272 hardlink = None
273 num = 0
273 num = 0
274 closetopic = [None]
274 closetopic = [None]
275 def prog(topic, pos):
275 def prog(topic, pos):
276 if pos is None:
276 if pos is None:
277 closetopic[0] = topic
277 closetopic[0] = topic
278 else:
278 else:
279 ui.progress(topic, pos + num)
279 ui.progress(topic, pos + num)
280 srcpublishing = srcrepo.publishing()
280 srcpublishing = srcrepo.publishing()
281 srcvfs = scmutil.vfs(srcrepo.sharedpath)
281 srcvfs = scmutil.vfs(srcrepo.sharedpath)
282 dstvfs = scmutil.vfs(destpath)
282 dstvfs = scmutil.vfs(destpath)
283 for f in srcrepo.store.copylist():
283 for f in srcrepo.store.copylist():
284 if srcpublishing and f.endswith('phaseroots'):
284 if srcpublishing and f.endswith('phaseroots'):
285 continue
285 continue
286 dstbase = os.path.dirname(f)
286 dstbase = os.path.dirname(f)
287 if dstbase and not dstvfs.exists(dstbase):
287 if dstbase and not dstvfs.exists(dstbase):
288 dstvfs.mkdir(dstbase)
288 dstvfs.mkdir(dstbase)
289 if srcvfs.exists(f):
289 if srcvfs.exists(f):
290 if f.endswith('data'):
290 if f.endswith('data'):
291 # 'dstbase' may be empty (e.g. revlog format 0)
291 # 'dstbase' may be empty (e.g. revlog format 0)
292 lockfile = os.path.join(dstbase, "lock")
292 lockfile = os.path.join(dstbase, "lock")
293 # lock to avoid premature writing to the target
293 # lock to avoid premature writing to the target
294 destlock = lock.lock(dstvfs, lockfile)
294 destlock = lock.lock(dstvfs, lockfile)
295 hardlink, n = util.copyfiles(srcvfs.join(f), dstvfs.join(f),
295 hardlink, n = util.copyfiles(srcvfs.join(f), dstvfs.join(f),
296 hardlink, progress=prog)
296 hardlink, progress=prog)
297 num += n
297 num += n
298 if hardlink:
298 if hardlink:
299 ui.debug("linked %d files\n" % num)
299 ui.debug("linked %d files\n" % num)
300 if closetopic[0]:
300 if closetopic[0]:
301 ui.progress(closetopic[0], None)
301 ui.progress(closetopic[0], None)
302 else:
302 else:
303 ui.debug("copied %d files\n" % num)
303 ui.debug("copied %d files\n" % num)
304 if closetopic[0]:
304 if closetopic[0]:
305 ui.progress(closetopic[0], None)
305 ui.progress(closetopic[0], None)
306 return destlock
306 return destlock
307 except: # re-raises
307 except: # re-raises
308 release(destlock)
308 release(destlock)
309 raise
309 raise
310
310
311 def clonewithshare(ui, peeropts, sharepath, source, srcpeer, dest, pull=False,
311 def clonewithshare(ui, peeropts, sharepath, source, srcpeer, dest, pull=False,
312 rev=None, update=True, stream=False):
312 rev=None, update=True, stream=False):
313 """Perform a clone using a shared repo.
313 """Perform a clone using a shared repo.
314
314
315 The store for the repository will be located at <sharepath>/.hg. The
315 The store for the repository will be located at <sharepath>/.hg. The
316 specified revisions will be cloned or pulled from "source". A shared repo
316 specified revisions will be cloned or pulled from "source". A shared repo
317 will be created at "dest" and a working copy will be created if "update" is
317 will be created at "dest" and a working copy will be created if "update" is
318 True.
318 True.
319 """
319 """
320 revs = None
320 revs = None
321 if rev:
321 if rev:
322 if not srcpeer.capable('lookup'):
322 if not srcpeer.capable('lookup'):
323 raise util.Abort(_("src repository does not support "
323 raise util.Abort(_("src repository does not support "
324 "revision lookup and so doesn't "
324 "revision lookup and so doesn't "
325 "support clone by revision"))
325 "support clone by revision"))
326 revs = [srcpeer.lookup(r) for r in rev]
326 revs = [srcpeer.lookup(r) for r in rev]
327
327
328 basename = os.path.basename(sharepath)
328 basename = os.path.basename(sharepath)
329
329
330 if os.path.exists(sharepath):
330 if os.path.exists(sharepath):
331 ui.status(_('(sharing from existing pooled repository %s)\n') %
331 ui.status(_('(sharing from existing pooled repository %s)\n') %
332 basename)
332 basename)
333 else:
333 else:
334 ui.status(_('(sharing from new pooled repository %s)\n') % basename)
334 ui.status(_('(sharing from new pooled repository %s)\n') % basename)
335 # Always use pull mode because hardlinks in share mode don't work well.
335 # Always use pull mode because hardlinks in share mode don't work well.
336 # Never update because working copies aren't necessary in share mode.
336 # Never update because working copies aren't necessary in share mode.
337 clone(ui, peeropts, source, dest=sharepath, pull=True,
337 clone(ui, peeropts, source, dest=sharepath, pull=True,
338 rev=rev, update=False, stream=stream)
338 rev=rev, update=False, stream=stream)
339
339
340 sharerepo = repository(ui, path=sharepath)
340 sharerepo = repository(ui, path=sharepath)
341 share(ui, sharerepo, dest=dest, update=update, bookmarks=False)
341 share(ui, sharerepo, dest=dest, update=update, bookmarks=False)
342
342
343 # We need to perform a pull against the dest repo to fetch bookmarks
343 # We need to perform a pull against the dest repo to fetch bookmarks
344 # and other non-store data that isn't shared by default. In the case of
344 # and other non-store data that isn't shared by default. In the case of
345 # non-existing shared repo, this means we pull from the remote twice. This
345 # non-existing shared repo, this means we pull from the remote twice. This
346 # is a bit weird. But at the time it was implemented, there wasn't an easy
346 # is a bit weird. But at the time it was implemented, there wasn't an easy
347 # way to pull just non-changegroup data.
347 # way to pull just non-changegroup data.
348 destrepo = repository(ui, path=dest)
348 destrepo = repository(ui, path=dest)
349 exchange.pull(destrepo, srcpeer, heads=revs)
349 exchange.pull(destrepo, srcpeer, heads=revs)
350
350
351 return srcpeer, peer(ui, peeropts, dest)
351 return srcpeer, peer(ui, peeropts, dest)
352
352
353 def clone(ui, peeropts, source, dest=None, pull=False, rev=None,
353 def clone(ui, peeropts, source, dest=None, pull=False, rev=None,
354 update=True, stream=False, branch=None, shareopts=None):
354 update=True, stream=False, branch=None, shareopts=None):
355 """Make a copy of an existing repository.
355 """Make a copy of an existing repository.
356
356
357 Create a copy of an existing repository in a new directory. The
357 Create a copy of an existing repository in a new directory. The
358 source and destination are URLs, as passed to the repository
358 source and destination are URLs, as passed to the repository
359 function. Returns a pair of repository peers, the source and
359 function. Returns a pair of repository peers, the source and
360 newly created destination.
360 newly created destination.
361
361
362 The location of the source is added to the new repository's
362 The location of the source is added to the new repository's
363 .hg/hgrc file, as the default to be used for future pulls and
363 .hg/hgrc file, as the default to be used for future pulls and
364 pushes.
364 pushes.
365
365
366 If an exception is raised, the partly cloned/updated destination
366 If an exception is raised, the partly cloned/updated destination
367 repository will be deleted.
367 repository will be deleted.
368
368
369 Arguments:
369 Arguments:
370
370
371 source: repository object or URL
371 source: repository object or URL
372
372
373 dest: URL of destination repository to create (defaults to base
373 dest: URL of destination repository to create (defaults to base
374 name of source repository)
374 name of source repository)
375
375
376 pull: always pull from source repository, even in local case or if the
376 pull: always pull from source repository, even in local case or if the
377 server prefers streaming
377 server prefers streaming
378
378
379 stream: stream raw data uncompressed from repository (fast over
379 stream: stream raw data uncompressed from repository (fast over
380 LAN, slow over WAN)
380 LAN, slow over WAN)
381
381
382 rev: revision to clone up to (implies pull=True)
382 rev: revision to clone up to (implies pull=True)
383
383
384 update: update working directory after clone completes, if
384 update: update working directory after clone completes, if
385 destination is local repository (True means update to default rev,
385 destination is local repository (True means update to default rev,
386 anything else is treated as a revision)
386 anything else is treated as a revision)
387
387
388 branch: branches to clone
388 branch: branches to clone
389
389
390 shareopts: dict of options to control auto sharing behavior. The "pool" key
390 shareopts: dict of options to control auto sharing behavior. The "pool" key
391 activates auto sharing mode and defines the directory for stores. The
391 activates auto sharing mode and defines the directory for stores. The
392 "mode" key determines how to construct the directory name of the shared
392 "mode" key determines how to construct the directory name of the shared
393 repository. "identity" means the name is derived from the node of the first
393 repository. "identity" means the name is derived from the node of the first
394 changeset in the repository. "remote" means the name is derived from the
394 changeset in the repository. "remote" means the name is derived from the
395 remote's path/URL. Defaults to "identity."
395 remote's path/URL. Defaults to "identity."
396 """
396 """
397
397
398 if isinstance(source, str):
398 if isinstance(source, str):
399 origsource = ui.expandpath(source)
399 origsource = ui.expandpath(source)
400 source, branch = parseurl(origsource, branch)
400 source, branch = parseurl(origsource, branch)
401 srcpeer = peer(ui, peeropts, source)
401 srcpeer = peer(ui, peeropts, source)
402 else:
402 else:
403 srcpeer = source.peer() # in case we were called with a localrepo
403 srcpeer = source.peer() # in case we were called with a localrepo
404 branch = (None, branch or [])
404 branch = (None, branch or [])
405 origsource = source = srcpeer.url()
405 origsource = source = srcpeer.url()
406 rev, checkout = addbranchrevs(srcpeer, srcpeer, branch, rev)
406 rev, checkout = addbranchrevs(srcpeer, srcpeer, branch, rev)
407
407
408 if dest is None:
408 if dest is None:
409 dest = defaultdest(source)
409 dest = defaultdest(source)
410 if dest:
410 if dest:
411 ui.status(_("destination directory: %s\n") % dest)
411 ui.status(_("destination directory: %s\n") % dest)
412 else:
412 else:
413 dest = ui.expandpath(dest)
413 dest = ui.expandpath(dest)
414
414
415 dest = util.urllocalpath(dest)
415 dest = util.urllocalpath(dest)
416 source = util.urllocalpath(source)
416 source = util.urllocalpath(source)
417
417
418 if not dest:
418 if not dest:
419 raise util.Abort(_("empty destination path is not valid"))
419 raise util.Abort(_("empty destination path is not valid"))
420
420
421 destvfs = scmutil.vfs(dest, expandpath=True)
421 destvfs = scmutil.vfs(dest, expandpath=True)
422 if destvfs.lexists():
422 if destvfs.lexists():
423 if not destvfs.isdir():
423 if not destvfs.isdir():
424 raise util.Abort(_("destination '%s' already exists") % dest)
424 raise util.Abort(_("destination '%s' already exists") % dest)
425 elif destvfs.listdir():
425 elif destvfs.listdir():
426 raise util.Abort(_("destination '%s' is not empty") % dest)
426 raise util.Abort(_("destination '%s' is not empty") % dest)
427
427
428 shareopts = shareopts or {}
428 shareopts = shareopts or {}
429 sharepool = shareopts.get('pool')
429 sharepool = shareopts.get('pool')
430 sharenamemode = shareopts.get('mode')
430 sharenamemode = shareopts.get('mode')
431 if sharepool and islocal(dest):
431 if sharepool and islocal(dest):
432 sharepath = None
432 sharepath = None
433 if sharenamemode == 'identity':
433 if sharenamemode == 'identity':
434 # Resolve the name from the initial changeset in the remote
434 # Resolve the name from the initial changeset in the remote
435 # repository. This returns nullid when the remote is empty. It
435 # repository. This returns nullid when the remote is empty. It
436 # raises RepoLookupError if revision 0 is filtered or otherwise
436 # raises RepoLookupError if revision 0 is filtered or otherwise
437 # not available. If we fail to resolve, sharing is not enabled.
437 # not available. If we fail to resolve, sharing is not enabled.
438 try:
438 try:
439 rootnode = srcpeer.lookup('0')
439 rootnode = srcpeer.lookup('0')
440 if rootnode != node.nullid:
440 if rootnode != node.nullid:
441 sharepath = os.path.join(sharepool, node.hex(rootnode))
441 sharepath = os.path.join(sharepool, node.hex(rootnode))
442 else:
442 else:
443 ui.status(_('(not using pooled storage: '
443 ui.status(_('(not using pooled storage: '
444 'remote appears to be empty)\n'))
444 'remote appears to be empty)\n'))
445 except error.RepoLookupError:
445 except error.RepoLookupError:
446 ui.status(_('(not using pooled storage: '
446 ui.status(_('(not using pooled storage: '
447 'unable to resolve identity of remote)\n'))
447 'unable to resolve identity of remote)\n'))
448 elif sharenamemode == 'remote':
448 elif sharenamemode == 'remote':
449 sharepath = os.path.join(sharepool, util.sha1(source).hexdigest())
449 sharepath = os.path.join(sharepool, util.sha1(source).hexdigest())
450 else:
450 else:
451 raise util.Abort('unknown share naming mode: %s' % sharenamemode)
451 raise util.Abort('unknown share naming mode: %s' % sharenamemode)
452
452
453 if sharepath:
453 if sharepath:
454 return clonewithshare(ui, peeropts, sharepath, source, srcpeer,
454 return clonewithshare(ui, peeropts, sharepath, source, srcpeer,
455 dest, pull=pull, rev=rev, update=update,
455 dest, pull=pull, rev=rev, update=update,
456 stream=stream)
456 stream=stream)
457
457
458 srclock = destlock = cleandir = None
458 srclock = destlock = cleandir = None
459 srcrepo = srcpeer.local()
459 srcrepo = srcpeer.local()
460 try:
460 try:
461 abspath = origsource
461 abspath = origsource
462 if islocal(origsource):
462 if islocal(origsource):
463 abspath = os.path.abspath(util.urllocalpath(origsource))
463 abspath = os.path.abspath(util.urllocalpath(origsource))
464
464
465 if islocal(dest):
465 if islocal(dest):
466 cleandir = dest
466 cleandir = dest
467
467
468 copy = False
468 copy = False
469 if (srcrepo and srcrepo.cancopy() and islocal(dest)
469 if (srcrepo and srcrepo.cancopy() and islocal(dest)
470 and not phases.hassecret(srcrepo)):
470 and not phases.hassecret(srcrepo)):
471 copy = not pull and not rev
471 copy = not pull and not rev
472
472
473 if copy:
473 if copy:
474 try:
474 try:
475 # we use a lock here because if we race with commit, we
475 # we use a lock here because if we race with commit, we
476 # can end up with extra data in the cloned revlogs that's
476 # can end up with extra data in the cloned revlogs that's
477 # not pointed to by changesets, thus causing verify to
477 # not pointed to by changesets, thus causing verify to
478 # fail
478 # fail
479 srclock = srcrepo.lock(wait=False)
479 srclock = srcrepo.lock(wait=False)
480 except error.LockError:
480 except error.LockError:
481 copy = False
481 copy = False
482
482
483 if copy:
483 if copy:
484 srcrepo.hook('preoutgoing', throw=True, source='clone')
484 srcrepo.hook('preoutgoing', throw=True, source='clone')
485 hgdir = os.path.realpath(os.path.join(dest, ".hg"))
485 hgdir = os.path.realpath(os.path.join(dest, ".hg"))
486 if not os.path.exists(dest):
486 if not os.path.exists(dest):
487 os.mkdir(dest)
487 os.mkdir(dest)
488 else:
488 else:
489 # only clean up directories we create ourselves
489 # only clean up directories we create ourselves
490 cleandir = hgdir
490 cleandir = hgdir
491 try:
491 try:
492 destpath = hgdir
492 destpath = hgdir
493 util.makedir(destpath, notindexed=True)
493 util.makedir(destpath, notindexed=True)
494 except OSError as inst:
494 except OSError as inst:
495 if inst.errno == errno.EEXIST:
495 if inst.errno == errno.EEXIST:
496 cleandir = None
496 cleandir = None
497 raise util.Abort(_("destination '%s' already exists")
497 raise util.Abort(_("destination '%s' already exists")
498 % dest)
498 % dest)
499 raise
499 raise
500
500
501 destlock = copystore(ui, srcrepo, destpath)
501 destlock = copystore(ui, srcrepo, destpath)
502 # copy bookmarks over
502 # copy bookmarks over
503 srcbookmarks = srcrepo.join('bookmarks')
503 srcbookmarks = srcrepo.join('bookmarks')
504 dstbookmarks = os.path.join(destpath, 'bookmarks')
504 dstbookmarks = os.path.join(destpath, 'bookmarks')
505 if os.path.exists(srcbookmarks):
505 if os.path.exists(srcbookmarks):
506 util.copyfile(srcbookmarks, dstbookmarks)
506 util.copyfile(srcbookmarks, dstbookmarks)
507
507
508 # Recomputing branch cache might be slow on big repos,
508 # Recomputing branch cache might be slow on big repos,
509 # so just copy it
509 # so just copy it
510 def copybranchcache(fname):
510 def copybranchcache(fname):
511 srcbranchcache = srcrepo.join('cache/%s' % fname)
511 srcbranchcache = srcrepo.join('cache/%s' % fname)
512 dstbranchcache = os.path.join(dstcachedir, fname)
512 dstbranchcache = os.path.join(dstcachedir, fname)
513 if os.path.exists(srcbranchcache):
513 if os.path.exists(srcbranchcache):
514 if not os.path.exists(dstcachedir):
514 if not os.path.exists(dstcachedir):
515 os.mkdir(dstcachedir)
515 os.mkdir(dstcachedir)
516 util.copyfile(srcbranchcache, dstbranchcache)
516 util.copyfile(srcbranchcache, dstbranchcache)
517
517
518 dstcachedir = os.path.join(destpath, 'cache')
518 dstcachedir = os.path.join(destpath, 'cache')
519 # In local clones we're copying all nodes, not just served
519 # In local clones we're copying all nodes, not just served
520 # ones. Therefore copy all branch caches over.
520 # ones. Therefore copy all branch caches over.
521 copybranchcache('branch2')
521 copybranchcache('branch2')
522 for cachename in repoview.filtertable:
522 for cachename in repoview.filtertable:
523 copybranchcache('branch2-%s' % cachename)
523 copybranchcache('branch2-%s' % cachename)
524
524
525 # we need to re-init the repo after manually copying the data
525 # we need to re-init the repo after manually copying the data
526 # into it
526 # into it
527 destpeer = peer(srcrepo, peeropts, dest)
527 destpeer = peer(srcrepo, peeropts, dest)
528 srcrepo.hook('outgoing', source='clone',
528 srcrepo.hook('outgoing', source='clone',
529 node=node.hex(node.nullid))
529 node=node.hex(node.nullid))
530 else:
530 else:
531 try:
531 try:
532 destpeer = peer(srcrepo or ui, peeropts, dest, create=True)
532 destpeer = peer(srcrepo or ui, peeropts, dest, create=True)
533 # only pass ui when no srcrepo
533 # only pass ui when no srcrepo
534 except OSError as inst:
534 except OSError as inst:
535 if inst.errno == errno.EEXIST:
535 if inst.errno == errno.EEXIST:
536 cleandir = None
536 cleandir = None
537 raise util.Abort(_("destination '%s' already exists")
537 raise util.Abort(_("destination '%s' already exists")
538 % dest)
538 % dest)
539 raise
539 raise
540
540
541 revs = None
541 revs = None
542 if rev:
542 if rev:
543 if not srcpeer.capable('lookup'):
543 if not srcpeer.capable('lookup'):
544 raise util.Abort(_("src repository does not support "
544 raise util.Abort(_("src repository does not support "
545 "revision lookup and so doesn't "
545 "revision lookup and so doesn't "
546 "support clone by revision"))
546 "support clone by revision"))
547 revs = [srcpeer.lookup(r) for r in rev]
547 revs = [srcpeer.lookup(r) for r in rev]
548 checkout = revs[0]
548 checkout = revs[0]
549 if destpeer.local():
549 if destpeer.local():
550 if not stream:
550 if not stream:
551 if pull:
551 if pull:
552 stream = False
552 stream = False
553 else:
553 else:
554 stream = None
554 stream = None
555 destpeer.local().clone(srcpeer, heads=revs, stream=stream)
555 destpeer.local().clone(srcpeer, heads=revs, stream=stream)
556 elif srcrepo:
556 elif srcrepo:
557 exchange.push(srcrepo, destpeer, revs=revs,
557 exchange.push(srcrepo, destpeer, revs=revs,
558 bookmarks=srcrepo._bookmarks.keys())
558 bookmarks=srcrepo._bookmarks.keys())
559 else:
559 else:
560 raise util.Abort(_("clone from remote to remote not supported"))
560 raise util.Abort(_("clone from remote to remote not supported"))
561
561
562 cleandir = None
562 cleandir = None
563
563
564 destrepo = destpeer.local()
564 destrepo = destpeer.local()
565 if destrepo:
565 if destrepo:
566 template = uimod.samplehgrcs['cloned']
566 template = uimod.samplehgrcs['cloned']
567 fp = destrepo.vfs("hgrc", "w", text=True)
567 fp = destrepo.vfs("hgrc", "w", text=True)
568 u = util.url(abspath)
568 u = util.url(abspath)
569 u.passwd = None
569 u.passwd = None
570 defaulturl = str(u)
570 defaulturl = str(u)
571 fp.write(template % defaulturl)
571 fp.write(template % defaulturl)
572 fp.close()
572 fp.close()
573
573
574 destrepo.ui.setconfig('paths', 'default', defaulturl, 'clone')
574 destrepo.ui.setconfig('paths', 'default', defaulturl, 'clone')
575
575
576 if update:
576 if update:
577 if update is not True:
577 if update is not True:
578 checkout = srcpeer.lookup(update)
578 checkout = srcpeer.lookup(update)
579 uprev = None
579 uprev = None
580 status = None
580 status = None
581 if checkout is not None:
581 if checkout is not None:
582 try:
582 try:
583 uprev = destrepo.lookup(checkout)
583 uprev = destrepo.lookup(checkout)
584 except error.RepoLookupError:
584 except error.RepoLookupError:
585 try:
585 try:
586 uprev = destrepo.lookup(update)
586 uprev = destrepo.lookup(update)
587 except error.RepoLookupError:
587 except error.RepoLookupError:
588 pass
588 pass
589 if uprev is None:
589 if uprev is None:
590 try:
590 try:
591 uprev = destrepo._bookmarks['@']
591 uprev = destrepo._bookmarks['@']
592 update = '@'
592 update = '@'
593 bn = destrepo[uprev].branch()
593 bn = destrepo[uprev].branch()
594 if bn == 'default':
594 if bn == 'default':
595 status = _("updating to bookmark @\n")
595 status = _("updating to bookmark @\n")
596 else:
596 else:
597 status = (_("updating to bookmark @ on branch %s\n")
597 status = (_("updating to bookmark @ on branch %s\n")
598 % bn)
598 % bn)
599 except KeyError:
599 except KeyError:
600 try:
600 try:
601 uprev = destrepo.branchtip('default')
601 uprev = destrepo.branchtip('default')
602 except error.RepoLookupError:
602 except error.RepoLookupError:
603 uprev = destrepo.lookup('tip')
603 uprev = destrepo.lookup('tip')
604 if not status:
604 if not status:
605 bn = destrepo[uprev].branch()
605 bn = destrepo[uprev].branch()
606 status = _("updating to branch %s\n") % bn
606 status = _("updating to branch %s\n") % bn
607 destrepo.ui.status(status)
607 destrepo.ui.status(status)
608 _update(destrepo, uprev)
608 _update(destrepo, uprev)
609 if update in destrepo._bookmarks:
609 if update in destrepo._bookmarks:
610 bookmarks.activate(destrepo, update)
610 bookmarks.activate(destrepo, update)
611 finally:
611 finally:
612 release(srclock, destlock)
612 release(srclock, destlock)
613 if cleandir is not None:
613 if cleandir is not None:
614 shutil.rmtree(cleandir, True)
614 shutil.rmtree(cleandir, True)
615 if srcpeer is not None:
615 if srcpeer is not None:
616 srcpeer.close()
616 srcpeer.close()
617 return srcpeer, destpeer
617 return srcpeer, destpeer
618
618
619 def _showstats(repo, stats):
619 def _showstats(repo, stats):
620 repo.ui.status(_("%d files updated, %d files merged, "
620 repo.ui.status(_("%d files updated, %d files merged, "
621 "%d files removed, %d files unresolved\n") % stats)
621 "%d files removed, %d files unresolved\n") % stats)
622
622
623 def updaterepo(repo, node, overwrite):
623 def updaterepo(repo, node, overwrite):
624 """Update the working directory to node.
624 """Update the working directory to node.
625
625
626 When overwrite is set, changes are clobbered, merged else
626 When overwrite is set, changes are clobbered, merged else
627
627
628 returns stats (see pydoc mercurial.merge.applyupdates)"""
628 returns stats (see pydoc mercurial.merge.applyupdates)"""
629 return mergemod.update(repo, node, False, overwrite, None,
629 return mergemod.update(repo, node, False, overwrite, None,
630 labels=['working copy', 'destination'])
630 labels=['working copy', 'destination'])
631
631
632 def update(repo, node):
632 def update(repo, node):
633 """update the working directory to node, merging linear changes"""
633 """update the working directory to node, merging linear changes"""
634 stats = updaterepo(repo, node, False)
634 stats = updaterepo(repo, node, False)
635 _showstats(repo, stats)
635 _showstats(repo, stats)
636 if stats[3]:
636 if stats[3]:
637 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
637 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
638 return stats[3] > 0
638 return stats[3] > 0
639
639
640 # naming conflict in clone()
640 # naming conflict in clone()
641 _update = update
641 _update = update
642
642
643 def clean(repo, node, show_stats=True):
643 def clean(repo, node, show_stats=True):
644 """forcibly switch the working directory to node, clobbering changes"""
644 """forcibly switch the working directory to node, clobbering changes"""
645 stats = updaterepo(repo, node, True)
645 stats = updaterepo(repo, node, True)
646 util.unlinkpath(repo.join('graftstate'), ignoremissing=True)
646 util.unlinkpath(repo.join('graftstate'), ignoremissing=True)
647 if show_stats:
647 if show_stats:
648 _showstats(repo, stats)
648 _showstats(repo, stats)
649 return stats[3] > 0
649 return stats[3] > 0
650
650
651 def merge(repo, node, force=None, remind=True):
651 def merge(repo, node, force=None, remind=True):
652 """Branch merge with node, resolving changes. Return true if any
652 """Branch merge with node, resolving changes. Return true if any
653 unresolved conflicts."""
653 unresolved conflicts."""
654 stats = mergemod.update(repo, node, True, force, False)
654 stats = mergemod.update(repo, node, True, force, False)
655 _showstats(repo, stats)
655 _showstats(repo, stats)
656 if stats[3]:
656 if stats[3]:
657 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
657 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
658 "or 'hg update -C .' to abandon\n"))
658 "or 'hg update -C .' to abandon\n"))
659 elif remind:
659 elif remind:
660 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
660 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
661 return stats[3] > 0
661 return stats[3] > 0
662
662
663 def _incoming(displaychlist, subreporecurse, ui, repo, source,
663 def _incoming(displaychlist, subreporecurse, ui, repo, source,
664 opts, buffered=False):
664 opts, buffered=False):
665 """
665 """
666 Helper for incoming / gincoming.
666 Helper for incoming / gincoming.
667 displaychlist gets called with
667 displaychlist gets called with
668 (remoterepo, incomingchangesetlist, displayer) parameters,
668 (remoterepo, incomingchangesetlist, displayer) parameters,
669 and is supposed to contain only code that can't be unified.
669 and is supposed to contain only code that can't be unified.
670 """
670 """
671 source, branches = parseurl(ui.expandpath(source), opts.get('branch'))
671 source, branches = parseurl(ui.expandpath(source), opts.get('branch'))
672 other = peer(repo, opts, source)
672 other = peer(repo, opts, source)
673 ui.status(_('comparing with %s\n') % util.hidepassword(source))
673 ui.status(_('comparing with %s\n') % util.hidepassword(source))
674 revs, checkout = addbranchrevs(repo, other, branches, opts.get('rev'))
674 revs, checkout = addbranchrevs(repo, other, branches, opts.get('rev'))
675
675
676 if revs:
676 if revs:
677 revs = [other.lookup(rev) for rev in revs]
677 revs = [other.lookup(rev) for rev in revs]
678 other, chlist, cleanupfn = bundlerepo.getremotechanges(ui, repo, other,
678 other, chlist, cleanupfn = bundlerepo.getremotechanges(ui, repo, other,
679 revs, opts["bundle"], opts["force"])
679 revs, opts["bundle"], opts["force"])
680 try:
680 try:
681 if not chlist:
681 if not chlist:
682 ui.status(_("no changes found\n"))
682 ui.status(_("no changes found\n"))
683 return subreporecurse()
683 return subreporecurse()
684
684
685 displayer = cmdutil.show_changeset(ui, other, opts, buffered)
685 displayer = cmdutil.show_changeset(ui, other, opts, buffered)
686 displaychlist(other, chlist, displayer)
686 displaychlist(other, chlist, displayer)
687 displayer.close()
687 displayer.close()
688 finally:
688 finally:
689 cleanupfn()
689 cleanupfn()
690 subreporecurse()
690 subreporecurse()
691 return 0 # exit code is zero since we found incoming changes
691 return 0 # exit code is zero since we found incoming changes
692
692
693 def incoming(ui, repo, source, opts):
693 def incoming(ui, repo, source, opts):
694 def subreporecurse():
694 def subreporecurse():
695 ret = 1
695 ret = 1
696 if opts.get('subrepos'):
696 if opts.get('subrepos'):
697 ctx = repo[None]
697 ctx = repo[None]
698 for subpath in sorted(ctx.substate):
698 for subpath in sorted(ctx.substate):
699 sub = ctx.sub(subpath)
699 sub = ctx.sub(subpath)
700 ret = min(ret, sub.incoming(ui, source, opts))
700 ret = min(ret, sub.incoming(ui, source, opts))
701 return ret
701 return ret
702
702
703 def display(other, chlist, displayer):
703 def display(other, chlist, displayer):
704 limit = cmdutil.loglimit(opts)
704 limit = cmdutil.loglimit(opts)
705 if opts.get('newest_first'):
705 if opts.get('newest_first'):
706 chlist.reverse()
706 chlist.reverse()
707 count = 0
707 count = 0
708 for n in chlist:
708 for n in chlist:
709 if limit is not None and count >= limit:
709 if limit is not None and count >= limit:
710 break
710 break
711 parents = [p for p in other.changelog.parents(n) if p != nullid]
711 parents = [p for p in other.changelog.parents(n) if p != nullid]
712 if opts.get('no_merges') and len(parents) == 2:
712 if opts.get('no_merges') and len(parents) == 2:
713 continue
713 continue
714 count += 1
714 count += 1
715 displayer.show(other[n])
715 displayer.show(other[n])
716 return _incoming(display, subreporecurse, ui, repo, source, opts)
716 return _incoming(display, subreporecurse, ui, repo, source, opts)
717
717
718 def _outgoing(ui, repo, dest, opts):
718 def _outgoing(ui, repo, dest, opts):
719 dest = ui.expandpath(dest or 'default-push', dest or 'default')
719 dest = ui.expandpath(dest or 'default-push', dest or 'default')
720 dest, branches = parseurl(dest, opts.get('branch'))
720 dest, branches = parseurl(dest, opts.get('branch'))
721 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
721 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
722 revs, checkout = addbranchrevs(repo, repo, branches, opts.get('rev'))
722 revs, checkout = addbranchrevs(repo, repo, branches, opts.get('rev'))
723 if revs:
723 if revs:
724 revs = [repo.lookup(rev) for rev in scmutil.revrange(repo, revs)]
724 revs = [repo.lookup(rev) for rev in scmutil.revrange(repo, revs)]
725
725
726 other = peer(repo, opts, dest)
726 other = peer(repo, opts, dest)
727 outgoing = discovery.findcommonoutgoing(repo.unfiltered(), other, revs,
727 outgoing = discovery.findcommonoutgoing(repo.unfiltered(), other, revs,
728 force=opts.get('force'))
728 force=opts.get('force'))
729 o = outgoing.missing
729 o = outgoing.missing
730 if not o:
730 if not o:
731 scmutil.nochangesfound(repo.ui, repo, outgoing.excluded)
731 scmutil.nochangesfound(repo.ui, repo, outgoing.excluded)
732 return o, other
732 return o, other
733
733
734 def outgoing(ui, repo, dest, opts):
734 def outgoing(ui, repo, dest, opts):
735 def recurse():
735 def recurse():
736 ret = 1
736 ret = 1
737 if opts.get('subrepos'):
737 if opts.get('subrepos'):
738 ctx = repo[None]
738 ctx = repo[None]
739 for subpath in sorted(ctx.substate):
739 for subpath in sorted(ctx.substate):
740 sub = ctx.sub(subpath)
740 sub = ctx.sub(subpath)
741 ret = min(ret, sub.outgoing(ui, dest, opts))
741 ret = min(ret, sub.outgoing(ui, dest, opts))
742 return ret
742 return ret
743
743
744 limit = cmdutil.loglimit(opts)
744 limit = cmdutil.loglimit(opts)
745 o, other = _outgoing(ui, repo, dest, opts)
745 o, other = _outgoing(ui, repo, dest, opts)
746 if not o:
746 if not o:
747 cmdutil.outgoinghooks(ui, repo, other, opts, o)
747 cmdutil.outgoinghooks(ui, repo, other, opts, o)
748 return recurse()
748 return recurse()
749
749
750 if opts.get('newest_first'):
750 if opts.get('newest_first'):
751 o.reverse()
751 o.reverse()
752 displayer = cmdutil.show_changeset(ui, repo, opts)
752 displayer = cmdutil.show_changeset(ui, repo, opts)
753 count = 0
753 count = 0
754 for n in o:
754 for n in o:
755 if limit is not None and count >= limit:
755 if limit is not None and count >= limit:
756 break
756 break
757 parents = [p for p in repo.changelog.parents(n) if p != nullid]
757 parents = [p for p in repo.changelog.parents(n) if p != nullid]
758 if opts.get('no_merges') and len(parents) == 2:
758 if opts.get('no_merges') and len(parents) == 2:
759 continue
759 continue
760 count += 1
760 count += 1
761 displayer.show(repo[n])
761 displayer.show(repo[n])
762 displayer.close()
762 displayer.close()
763 cmdutil.outgoinghooks(ui, repo, other, opts, o)
763 cmdutil.outgoinghooks(ui, repo, other, opts, o)
764 recurse()
764 recurse()
765 return 0 # exit code is zero since we found outgoing changes
765 return 0 # exit code is zero since we found outgoing changes
766
766
767 def revert(repo, node, choose):
767 def revert(repo, node, choose):
768 """revert changes to revision in node without updating dirstate"""
768 """revert changes to revision in node without updating dirstate"""
769 return mergemod.update(repo, node, False, True, choose)[3] > 0
769 return mergemod.update(repo, node, False, True, choose)[3] > 0
770
770
771 def verify(repo):
771 def verify(repo):
772 """verify the consistency of a repository"""
772 """verify the consistency of a repository"""
773 ret = verifymod.verify(repo)
773 ret = verifymod.verify(repo)
774
774
775 # Broken subrepo references in hidden csets don't seem worth worrying about,
775 # Broken subrepo references in hidden csets don't seem worth worrying about,
776 # since they can't be pushed/pulled, and --hidden can be used if they are a
776 # since they can't be pushed/pulled, and --hidden can be used if they are a
777 # concern.
777 # concern.
778
778
779 # pathto() is needed for -R case
779 # pathto() is needed for -R case
780 revs = repo.revs("filelog(%s)",
780 revs = repo.revs("filelog(%s)",
781 util.pathto(repo.root, repo.getcwd(), '.hgsubstate'))
781 util.pathto(repo.root, repo.getcwd(), '.hgsubstate'))
782
782
783 if revs:
783 if revs:
784 repo.ui.status(_('checking subrepo links\n'))
784 repo.ui.status(_('checking subrepo links\n'))
785 for rev in revs:
785 for rev in revs:
786 ctx = repo[rev]
786 ctx = repo[rev]
787 try:
787 try:
788 for subpath in ctx.substate:
788 for subpath in ctx.substate:
789 ret = ctx.sub(subpath).verify() or ret
789 ret = ctx.sub(subpath).verify() or ret
790 except Exception:
790 except Exception:
791 repo.ui.warn(_('.hgsubstate is corrupt in revision %s\n') %
791 repo.ui.warn(_('.hgsubstate is corrupt in revision %s\n') %
792 node.short(ctx.node()))
792 node.short(ctx.node()))
793
793
794 return ret
794 return ret
795
795
796 def remoteui(src, opts):
796 def remoteui(src, opts):
797 'build a remote ui from ui or repo and opts'
797 'build a remote ui from ui or repo and opts'
798 if util.safehasattr(src, 'baseui'): # looks like a repository
798 if util.safehasattr(src, 'baseui'): # looks like a repository
799 dst = src.baseui.copy() # drop repo-specific config
799 dst = src.baseui.copy() # drop repo-specific config
800 src = src.ui # copy target options from repo
800 src = src.ui # copy target options from repo
801 else: # assume it's a global ui object
801 else: # assume it's a global ui object
802 dst = src.copy() # keep all global options
802 dst = src.copy() # keep all global options
803
803
804 # copy ssh-specific options
804 # copy ssh-specific options
805 for o in 'ssh', 'remotecmd':
805 for o in 'ssh', 'remotecmd':
806 v = opts.get(o) or src.config('ui', o)
806 v = opts.get(o) or src.config('ui', o)
807 if v:
807 if v:
808 dst.setconfig("ui", o, v, 'copied')
808 dst.setconfig("ui", o, v, 'copied')
809
809
810 # copy bundle-specific options
810 # copy bundle-specific options
811 r = src.config('bundle', 'mainreporoot')
811 r = src.config('bundle', 'mainreporoot')
812 if r:
812 if r:
813 dst.setconfig('bundle', 'mainreporoot', r, 'copied')
813 dst.setconfig('bundle', 'mainreporoot', r, 'copied')
814
814
815 # copy selected local settings to the remote ui
815 # copy selected local settings to the remote ui
816 for sect in ('auth', 'hostfingerprints', 'http_proxy'):
816 for sect in ('auth', 'hostfingerprints', 'http_proxy'):
817 for key, val in src.configitems(sect):
817 for key, val in src.configitems(sect):
818 dst.setconfig(sect, key, val, 'copied')
818 dst.setconfig(sect, key, val, 'copied')
819 v = src.config('web', 'cacerts')
819 v = src.config('web', 'cacerts')
820 if v == '!':
820 if v == '!':
821 dst.setconfig('web', 'cacerts', v, 'copied')
821 dst.setconfig('web', 'cacerts', v, 'copied')
822 elif v:
822 elif v:
823 dst.setconfig('web', 'cacerts', util.expandpath(v), 'copied')
823 dst.setconfig('web', 'cacerts', util.expandpath(v), 'copied')
824
824
825 return dst
825 return dst
826
826
827 # Files of interest
827 # Files of interest
828 # Used to check if the repository has changed looking at mtime and size of
828 # Used to check if the repository has changed looking at mtime and size of
829 # theses files.
829 # theses files.
830 foi = [('spath', '00changelog.i'),
830 foi = [('spath', '00changelog.i'),
831 ('spath', 'phaseroots'), # ! phase can change content at the same size
831 ('spath', 'phaseroots'), # ! phase can change content at the same size
832 ('spath', 'obsstore'),
832 ('spath', 'obsstore'),
833 ('path', 'bookmarks'), # ! bookmark can change content at the same size
833 ('path', 'bookmarks'), # ! bookmark can change content at the same size
834 ]
834 ]
835
835
836 class cachedlocalrepo(object):
836 class cachedlocalrepo(object):
837 """Holds a localrepository that can be cached and reused."""
837 """Holds a localrepository that can be cached and reused."""
838
838
839 def __init__(self, repo):
839 def __init__(self, repo):
840 """Create a new cached repo from an existing repo.
840 """Create a new cached repo from an existing repo.
841
841
842 We assume the passed in repo was recently created. If the
842 We assume the passed in repo was recently created. If the
843 repo has changed between when it was created and when it was
843 repo has changed between when it was created and when it was
844 turned into a cache, it may not refresh properly.
844 turned into a cache, it may not refresh properly.
845 """
845 """
846 assert isinstance(repo, localrepo.localrepository)
846 assert isinstance(repo, localrepo.localrepository)
847 self._repo = repo
847 self._repo = repo
848 self._state, self.mtime = self._repostate()
848 self._state, self.mtime = self._repostate()
849
849
850 def fetch(self):
850 def fetch(self):
851 """Refresh (if necessary) and return a repository.
851 """Refresh (if necessary) and return a repository.
852
852
853 If the cached instance is out of date, it will be recreated
853 If the cached instance is out of date, it will be recreated
854 automatically and returned.
854 automatically and returned.
855
855
856 Returns a tuple of the repo and a boolean indicating whether a new
856 Returns a tuple of the repo and a boolean indicating whether a new
857 repo instance was created.
857 repo instance was created.
858 """
858 """
859 # We compare the mtimes and sizes of some well-known files to
859 # We compare the mtimes and sizes of some well-known files to
860 # determine if the repo changed. This is not precise, as mtimes
860 # determine if the repo changed. This is not precise, as mtimes
861 # are susceptible to clock skew and imprecise filesystems and
861 # are susceptible to clock skew and imprecise filesystems and
862 # file content can change while maintaining the same size.
862 # file content can change while maintaining the same size.
863
863
864 state, mtime = self._repostate()
864 state, mtime = self._repostate()
865 if state == self._state:
865 if state == self._state:
866 return self._repo, False
866 return self._repo, False
867
867
868 self._repo = repository(self._repo.baseui, self._repo.url())
868 self._repo = repository(self._repo.baseui, self._repo.url())
869 self._state = state
869 self._state = state
870 self.mtime = mtime
870 self.mtime = mtime
871
871
872 return self._repo, True
872 return self._repo, True
873
873
874 def _repostate(self):
874 def _repostate(self):
875 state = []
875 state = []
876 maxmtime = -1
876 maxmtime = -1
877 for attr, fname in foi:
877 for attr, fname in foi:
878 prefix = getattr(self._repo, attr)
878 prefix = getattr(self._repo, attr)
879 p = os.path.join(prefix, fname)
879 p = os.path.join(prefix, fname)
880 try:
880 try:
881 st = os.stat(p)
881 st = os.stat(p)
882 except OSError:
882 except OSError:
883 st = os.stat(prefix)
883 st = os.stat(prefix)
884 state.append((st.st_mtime, st.st_size))
884 state.append((st.st_mtime, st.st_size))
885 maxmtime = max(maxmtime, st.st_mtime)
885 maxmtime = max(maxmtime, st.st_mtime)
886
886
887 return tuple(state), maxmtime
887 return tuple(state), maxmtime
888
888
889 def copy(self):
889 def copy(self):
890 """Obtain a copy of this class instance."""
890 """Obtain a copy of this class instance.
891 c = cachedlocalrepo(self._repo)
891
892 A new localrepository instance is obtained. The new instance should be
893 completely independent of the original.
894 """
895 repo = repository(self._repo.baseui, self._repo.origroot)
896 c = cachedlocalrepo(repo)
892 c._state = self._state
897 c._state = self._state
893 c.mtime = self.mtime
898 c.mtime = self.mtime
894 return c
899 return c
@@ -1,437 +1,437
1 # hgweb/hgweb_mod.py - Web interface for a repository.
1 # hgweb/hgweb_mod.py - Web interface for a repository.
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 import contextlib
9 import contextlib
10 import os
10 import os
11 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
11 from mercurial import ui, hg, hook, error, encoding, templater, util, repoview
12 from mercurial.templatefilters import websub
12 from mercurial.templatefilters import websub
13 from common import ErrorResponse, permhooks, caching
13 from common import ErrorResponse, permhooks, caching
14 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
14 from common import HTTP_OK, HTTP_NOT_MODIFIED, HTTP_BAD_REQUEST
15 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 from common import HTTP_NOT_FOUND, HTTP_SERVER_ERROR
16 from request import wsgirequest
16 from request import wsgirequest
17 import webcommands, protocol, webutil
17 import webcommands, protocol, webutil
18
18
19 perms = {
19 perms = {
20 'changegroup': 'pull',
20 'changegroup': 'pull',
21 'changegroupsubset': 'pull',
21 'changegroupsubset': 'pull',
22 'getbundle': 'pull',
22 'getbundle': 'pull',
23 'stream_out': 'pull',
23 'stream_out': 'pull',
24 'listkeys': 'pull',
24 'listkeys': 'pull',
25 'unbundle': 'push',
25 'unbundle': 'push',
26 'pushkey': 'push',
26 'pushkey': 'push',
27 }
27 }
28
28
29 def makebreadcrumb(url, prefix=''):
29 def makebreadcrumb(url, prefix=''):
30 '''Return a 'URL breadcrumb' list
30 '''Return a 'URL breadcrumb' list
31
31
32 A 'URL breadcrumb' is a list of URL-name pairs,
32 A 'URL breadcrumb' is a list of URL-name pairs,
33 corresponding to each of the path items on a URL.
33 corresponding to each of the path items on a URL.
34 This can be used to create path navigation entries.
34 This can be used to create path navigation entries.
35 '''
35 '''
36 if url.endswith('/'):
36 if url.endswith('/'):
37 url = url[:-1]
37 url = url[:-1]
38 if prefix:
38 if prefix:
39 url = '/' + prefix + url
39 url = '/' + prefix + url
40 relpath = url
40 relpath = url
41 if relpath.startswith('/'):
41 if relpath.startswith('/'):
42 relpath = relpath[1:]
42 relpath = relpath[1:]
43
43
44 breadcrumb = []
44 breadcrumb = []
45 urlel = url
45 urlel = url
46 pathitems = [''] + relpath.split('/')
46 pathitems = [''] + relpath.split('/')
47 for pathel in reversed(pathitems):
47 for pathel in reversed(pathitems):
48 if not pathel or not urlel:
48 if not pathel or not urlel:
49 break
49 break
50 breadcrumb.append({'url': urlel, 'name': pathel})
50 breadcrumb.append({'url': urlel, 'name': pathel})
51 urlel = os.path.dirname(urlel)
51 urlel = os.path.dirname(urlel)
52 return reversed(breadcrumb)
52 return reversed(breadcrumb)
53
53
54 class requestcontext(object):
54 class requestcontext(object):
55 """Holds state/context for an individual request.
55 """Holds state/context for an individual request.
56
56
57 Servers can be multi-threaded. Holding state on the WSGI application
57 Servers can be multi-threaded. Holding state on the WSGI application
58 is prone to race conditions. Instances of this class exist to hold
58 is prone to race conditions. Instances of this class exist to hold
59 mutable and race-free state for requests.
59 mutable and race-free state for requests.
60 """
60 """
61 def __init__(self, app, repo):
61 def __init__(self, app, repo):
62 self.repo = repo
62 self.repo = repo
63 self.reponame = app.reponame
63 self.reponame = app.reponame
64
64
65 self.archives = ('zip', 'gz', 'bz2')
65 self.archives = ('zip', 'gz', 'bz2')
66
66
67 self.maxchanges = self.configint('web', 'maxchanges', 10)
67 self.maxchanges = self.configint('web', 'maxchanges', 10)
68 self.stripecount = self.configint('web', 'stripes', 1)
68 self.stripecount = self.configint('web', 'stripes', 1)
69 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
69 self.maxshortchanges = self.configint('web', 'maxshortchanges', 60)
70 self.maxfiles = self.configint('web', 'maxfiles', 10)
70 self.maxfiles = self.configint('web', 'maxfiles', 10)
71 self.allowpull = self.configbool('web', 'allowpull', True)
71 self.allowpull = self.configbool('web', 'allowpull', True)
72
72
73 # we use untrusted=False to prevent a repo owner from using
73 # we use untrusted=False to prevent a repo owner from using
74 # web.templates in .hg/hgrc to get access to any file readable
74 # web.templates in .hg/hgrc to get access to any file readable
75 # by the user running the CGI script
75 # by the user running the CGI script
76 self.templatepath = self.config('web', 'templates', untrusted=False)
76 self.templatepath = self.config('web', 'templates', untrusted=False)
77
77
78 # This object is more expensive to build than simple config values.
78 # This object is more expensive to build than simple config values.
79 # It is shared across requests. The app will replace the object
79 # It is shared across requests. The app will replace the object
80 # if it is updated. Since this is a reference and nothing should
80 # if it is updated. Since this is a reference and nothing should
81 # modify the underlying object, it should be constant for the lifetime
81 # modify the underlying object, it should be constant for the lifetime
82 # of the request.
82 # of the request.
83 self.websubtable = app.websubtable
83 self.websubtable = app.websubtable
84
84
85 # Trust the settings from the .hg/hgrc files by default.
85 # Trust the settings from the .hg/hgrc files by default.
86 def config(self, section, name, default=None, untrusted=True):
86 def config(self, section, name, default=None, untrusted=True):
87 return self.repo.ui.config(section, name, default,
87 return self.repo.ui.config(section, name, default,
88 untrusted=untrusted)
88 untrusted=untrusted)
89
89
90 def configbool(self, section, name, default=False, untrusted=True):
90 def configbool(self, section, name, default=False, untrusted=True):
91 return self.repo.ui.configbool(section, name, default,
91 return self.repo.ui.configbool(section, name, default,
92 untrusted=untrusted)
92 untrusted=untrusted)
93
93
94 def configint(self, section, name, default=None, untrusted=True):
94 def configint(self, section, name, default=None, untrusted=True):
95 return self.repo.ui.configint(section, name, default,
95 return self.repo.ui.configint(section, name, default,
96 untrusted=untrusted)
96 untrusted=untrusted)
97
97
98 def configlist(self, section, name, default=None, untrusted=True):
98 def configlist(self, section, name, default=None, untrusted=True):
99 return self.repo.ui.configlist(section, name, default,
99 return self.repo.ui.configlist(section, name, default,
100 untrusted=untrusted)
100 untrusted=untrusted)
101
101
102 archivespecs = {
102 archivespecs = {
103 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
103 'bz2': ('application/x-bzip2', 'tbz2', '.tar.bz2', None),
104 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
104 'gz': ('application/x-gzip', 'tgz', '.tar.gz', None),
105 'zip': ('application/zip', 'zip', '.zip', None),
105 'zip': ('application/zip', 'zip', '.zip', None),
106 }
106 }
107
107
108 def archivelist(self, nodeid):
108 def archivelist(self, nodeid):
109 allowed = self.configlist('web', 'allow_archive')
109 allowed = self.configlist('web', 'allow_archive')
110 for typ, spec in self.archivespecs.iteritems():
110 for typ, spec in self.archivespecs.iteritems():
111 if typ in allowed or self.configbool('web', 'allow%s' % typ):
111 if typ in allowed or self.configbool('web', 'allow%s' % typ):
112 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
112 yield {'type': typ, 'extension': spec[2], 'node': nodeid}
113
113
114 def templater(self, req):
114 def templater(self, req):
115 # determine scheme, port and server name
115 # determine scheme, port and server name
116 # this is needed to create absolute urls
116 # this is needed to create absolute urls
117
117
118 proto = req.env.get('wsgi.url_scheme')
118 proto = req.env.get('wsgi.url_scheme')
119 if proto == 'https':
119 if proto == 'https':
120 proto = 'https'
120 proto = 'https'
121 default_port = '443'
121 default_port = '443'
122 else:
122 else:
123 proto = 'http'
123 proto = 'http'
124 default_port = '80'
124 default_port = '80'
125
125
126 port = req.env['SERVER_PORT']
126 port = req.env['SERVER_PORT']
127 port = port != default_port and (':' + port) or ''
127 port = port != default_port and (':' + port) or ''
128 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
128 urlbase = '%s://%s%s' % (proto, req.env['SERVER_NAME'], port)
129 logourl = self.config('web', 'logourl', 'http://mercurial.selenic.com/')
129 logourl = self.config('web', 'logourl', 'http://mercurial.selenic.com/')
130 logoimg = self.config('web', 'logoimg', 'hglogo.png')
130 logoimg = self.config('web', 'logoimg', 'hglogo.png')
131 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
131 staticurl = self.config('web', 'staticurl') or req.url + 'static/'
132 if not staticurl.endswith('/'):
132 if not staticurl.endswith('/'):
133 staticurl += '/'
133 staticurl += '/'
134
134
135 # some functions for the templater
135 # some functions for the templater
136
136
137 def motd(**map):
137 def motd(**map):
138 yield self.config('web', 'motd', '')
138 yield self.config('web', 'motd', '')
139
139
140 # figure out which style to use
140 # figure out which style to use
141
141
142 vars = {}
142 vars = {}
143 styles = (
143 styles = (
144 req.form.get('style', [None])[0],
144 req.form.get('style', [None])[0],
145 self.config('web', 'style'),
145 self.config('web', 'style'),
146 'paper',
146 'paper',
147 )
147 )
148 style, mapfile = templater.stylemap(styles, self.templatepath)
148 style, mapfile = templater.stylemap(styles, self.templatepath)
149 if style == styles[0]:
149 if style == styles[0]:
150 vars['style'] = style
150 vars['style'] = style
151
151
152 start = req.url[-1] == '?' and '&' or '?'
152 start = req.url[-1] == '?' and '&' or '?'
153 sessionvars = webutil.sessionvars(vars, start)
153 sessionvars = webutil.sessionvars(vars, start)
154
154
155 if not self.reponame:
155 if not self.reponame:
156 self.reponame = (self.config('web', 'name')
156 self.reponame = (self.config('web', 'name')
157 or req.env.get('REPO_NAME')
157 or req.env.get('REPO_NAME')
158 or req.url.strip('/') or self.repo.root)
158 or req.url.strip('/') or self.repo.root)
159
159
160 def websubfilter(text):
160 def websubfilter(text):
161 return websub(text, self.websubtable)
161 return websub(text, self.websubtable)
162
162
163 # create the templater
163 # create the templater
164
164
165 tmpl = templater.templater(mapfile,
165 tmpl = templater.templater(mapfile,
166 filters={'websub': websubfilter},
166 filters={'websub': websubfilter},
167 defaults={'url': req.url,
167 defaults={'url': req.url,
168 'logourl': logourl,
168 'logourl': logourl,
169 'logoimg': logoimg,
169 'logoimg': logoimg,
170 'staticurl': staticurl,
170 'staticurl': staticurl,
171 'urlbase': urlbase,
171 'urlbase': urlbase,
172 'repo': self.reponame,
172 'repo': self.reponame,
173 'encoding': encoding.encoding,
173 'encoding': encoding.encoding,
174 'motd': motd,
174 'motd': motd,
175 'sessionvars': sessionvars,
175 'sessionvars': sessionvars,
176 'pathdef': makebreadcrumb(req.url),
176 'pathdef': makebreadcrumb(req.url),
177 'style': style,
177 'style': style,
178 })
178 })
179 return tmpl
179 return tmpl
180
180
181
181
182 class hgweb(object):
182 class hgweb(object):
183 """HTTP server for individual repositories.
183 """HTTP server for individual repositories.
184
184
185 Instances of this class serve HTTP responses for a particular
185 Instances of this class serve HTTP responses for a particular
186 repository.
186 repository.
187
187
188 Instances are typically used as WSGI applications.
188 Instances are typically used as WSGI applications.
189
189
190 Some servers are multi-threaded. On these servers, there may
190 Some servers are multi-threaded. On these servers, there may
191 be multiple active threads inside __call__.
191 be multiple active threads inside __call__.
192 """
192 """
193 def __init__(self, repo, name=None, baseui=None):
193 def __init__(self, repo, name=None, baseui=None):
194 if isinstance(repo, str):
194 if isinstance(repo, str):
195 if baseui:
195 if baseui:
196 u = baseui.copy()
196 u = baseui.copy()
197 else:
197 else:
198 u = ui.ui()
198 u = ui.ui()
199 r = hg.repository(u, repo)
199 r = hg.repository(u, repo)
200 else:
200 else:
201 # we trust caller to give us a private copy
201 # we trust caller to give us a private copy
202 r = repo
202 r = repo
203
203
204 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
204 r.ui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
205 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
205 r.baseui.setconfig('ui', 'report_untrusted', 'off', 'hgweb')
206 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
206 r.ui.setconfig('ui', 'nontty', 'true', 'hgweb')
207 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
207 r.baseui.setconfig('ui', 'nontty', 'true', 'hgweb')
208 # displaying bundling progress bar while serving feel wrong and may
208 # displaying bundling progress bar while serving feel wrong and may
209 # break some wsgi implementation.
209 # break some wsgi implementation.
210 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
210 r.ui.setconfig('progress', 'disable', 'true', 'hgweb')
211 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
211 r.baseui.setconfig('progress', 'disable', 'true', 'hgweb')
212 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
212 self._repos = [hg.cachedlocalrepo(self._webifyrepo(r))]
213 self._lastrepo = self._repos[0]
213 self._lastrepo = self._repos[0]
214 hook.redirect(True)
214 hook.redirect(True)
215 self.reponame = name
215 self.reponame = name
216
216
217 def _webifyrepo(self, repo):
217 def _webifyrepo(self, repo):
218 repo = getwebview(repo)
218 repo = getwebview(repo)
219 self.websubtable = webutil.getwebsubs(repo)
219 self.websubtable = webutil.getwebsubs(repo)
220 return repo
220 return repo
221
221
222 @contextlib.contextmanager
222 @contextlib.contextmanager
223 def _obtainrepo(self):
223 def _obtainrepo(self):
224 """Obtain a repo unique to the caller.
224 """Obtain a repo unique to the caller.
225
225
226 Internally we maintain a stack of cachedlocalrepo instances
226 Internally we maintain a stack of cachedlocalrepo instances
227 to be handed out. If one is available, we pop it and return it,
227 to be handed out. If one is available, we pop it and return it,
228 ensuring it is up to date in the process. If one is not available,
228 ensuring it is up to date in the process. If one is not available,
229 we clone the most recently used repo instance and return it.
229 we clone the most recently used repo instance and return it.
230
230
231 It is currently possible for the stack to grow without bounds
231 It is currently possible for the stack to grow without bounds
232 if the server allows infinite threads. However, servers should
232 if the server allows infinite threads. However, servers should
233 have a thread limit, thus establishing our limit.
233 have a thread limit, thus establishing our limit.
234 """
234 """
235 if self._repos:
235 if self._repos:
236 cached = self._repos.pop()
236 cached = self._repos.pop()
237 r, created = cached.fetch()
237 r, created = cached.fetch()
238 if created:
239 r = self._webifyrepo(r)
240 else:
238 else:
241 cached = self._lastrepo.copy()
239 cached = self._lastrepo.copy()
242 r, created = cached.fetch()
240 r, created = cached.fetch()
241 if created:
242 r = self._webifyrepo(r)
243
243
244 self._lastrepo = cached
244 self._lastrepo = cached
245 self.mtime = cached.mtime
245 self.mtime = cached.mtime
246 try:
246 try:
247 yield r
247 yield r
248 finally:
248 finally:
249 self._repos.append(cached)
249 self._repos.append(cached)
250
250
251 def run(self):
251 def run(self):
252 """Start a server from CGI environment.
252 """Start a server from CGI environment.
253
253
254 Modern servers should be using WSGI and should avoid this
254 Modern servers should be using WSGI and should avoid this
255 method, if possible.
255 method, if possible.
256 """
256 """
257 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
257 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
258 raise RuntimeError("This function is only intended to be "
258 raise RuntimeError("This function is only intended to be "
259 "called while running as a CGI script.")
259 "called while running as a CGI script.")
260 import mercurial.hgweb.wsgicgi as wsgicgi
260 import mercurial.hgweb.wsgicgi as wsgicgi
261 wsgicgi.launch(self)
261 wsgicgi.launch(self)
262
262
263 def __call__(self, env, respond):
263 def __call__(self, env, respond):
264 """Run the WSGI application.
264 """Run the WSGI application.
265
265
266 This may be called by multiple threads.
266 This may be called by multiple threads.
267 """
267 """
268 req = wsgirequest(env, respond)
268 req = wsgirequest(env, respond)
269 return self.run_wsgi(req)
269 return self.run_wsgi(req)
270
270
271 def run_wsgi(self, req):
271 def run_wsgi(self, req):
272 """Internal method to run the WSGI application.
272 """Internal method to run the WSGI application.
273
273
274 This is typically only called by Mercurial. External consumers
274 This is typically only called by Mercurial. External consumers
275 should be using instances of this class as the WSGI application.
275 should be using instances of this class as the WSGI application.
276 """
276 """
277 with self._obtainrepo() as repo:
277 with self._obtainrepo() as repo:
278 return self._runwsgi(req, repo)
278 return self._runwsgi(req, repo)
279
279
280 def _runwsgi(self, req, repo):
280 def _runwsgi(self, req, repo):
281 rctx = requestcontext(self, repo)
281 rctx = requestcontext(self, repo)
282
282
283 # This state is global across all threads.
283 # This state is global across all threads.
284 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
284 encoding.encoding = rctx.config('web', 'encoding', encoding.encoding)
285 rctx.repo.ui.environ = req.env
285 rctx.repo.ui.environ = req.env
286
286
287 # work with CGI variables to create coherent structure
287 # work with CGI variables to create coherent structure
288 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
288 # use SCRIPT_NAME, PATH_INFO and QUERY_STRING as well as our REPO_NAME
289
289
290 req.url = req.env['SCRIPT_NAME']
290 req.url = req.env['SCRIPT_NAME']
291 if not req.url.endswith('/'):
291 if not req.url.endswith('/'):
292 req.url += '/'
292 req.url += '/'
293 if 'REPO_NAME' in req.env:
293 if 'REPO_NAME' in req.env:
294 req.url += req.env['REPO_NAME'] + '/'
294 req.url += req.env['REPO_NAME'] + '/'
295
295
296 if 'PATH_INFO' in req.env:
296 if 'PATH_INFO' in req.env:
297 parts = req.env['PATH_INFO'].strip('/').split('/')
297 parts = req.env['PATH_INFO'].strip('/').split('/')
298 repo_parts = req.env.get('REPO_NAME', '').split('/')
298 repo_parts = req.env.get('REPO_NAME', '').split('/')
299 if parts[:len(repo_parts)] == repo_parts:
299 if parts[:len(repo_parts)] == repo_parts:
300 parts = parts[len(repo_parts):]
300 parts = parts[len(repo_parts):]
301 query = '/'.join(parts)
301 query = '/'.join(parts)
302 else:
302 else:
303 query = req.env['QUERY_STRING'].split('&', 1)[0]
303 query = req.env['QUERY_STRING'].split('&', 1)[0]
304 query = query.split(';', 1)[0]
304 query = query.split(';', 1)[0]
305
305
306 # process this if it's a protocol request
306 # process this if it's a protocol request
307 # protocol bits don't need to create any URLs
307 # protocol bits don't need to create any URLs
308 # and the clients always use the old URL structure
308 # and the clients always use the old URL structure
309
309
310 cmd = req.form.get('cmd', [''])[0]
310 cmd = req.form.get('cmd', [''])[0]
311 if protocol.iscmd(cmd):
311 if protocol.iscmd(cmd):
312 try:
312 try:
313 if query:
313 if query:
314 raise ErrorResponse(HTTP_NOT_FOUND)
314 raise ErrorResponse(HTTP_NOT_FOUND)
315 if cmd in perms:
315 if cmd in perms:
316 self.check_perm(rctx, req, perms[cmd])
316 self.check_perm(rctx, req, perms[cmd])
317 return protocol.call(rctx.repo, req, cmd)
317 return protocol.call(rctx.repo, req, cmd)
318 except ErrorResponse as inst:
318 except ErrorResponse as inst:
319 # A client that sends unbundle without 100-continue will
319 # A client that sends unbundle without 100-continue will
320 # break if we respond early.
320 # break if we respond early.
321 if (cmd == 'unbundle' and
321 if (cmd == 'unbundle' and
322 (req.env.get('HTTP_EXPECT',
322 (req.env.get('HTTP_EXPECT',
323 '').lower() != '100-continue') or
323 '').lower() != '100-continue') or
324 req.env.get('X-HgHttp2', '')):
324 req.env.get('X-HgHttp2', '')):
325 req.drain()
325 req.drain()
326 else:
326 else:
327 req.headers.append(('Connection', 'Close'))
327 req.headers.append(('Connection', 'Close'))
328 req.respond(inst, protocol.HGTYPE,
328 req.respond(inst, protocol.HGTYPE,
329 body='0\n%s\n' % inst)
329 body='0\n%s\n' % inst)
330 return ''
330 return ''
331
331
332 # translate user-visible url structure to internal structure
332 # translate user-visible url structure to internal structure
333
333
334 args = query.split('/', 2)
334 args = query.split('/', 2)
335 if 'cmd' not in req.form and args and args[0]:
335 if 'cmd' not in req.form and args and args[0]:
336
336
337 cmd = args.pop(0)
337 cmd = args.pop(0)
338 style = cmd.rfind('-')
338 style = cmd.rfind('-')
339 if style != -1:
339 if style != -1:
340 req.form['style'] = [cmd[:style]]
340 req.form['style'] = [cmd[:style]]
341 cmd = cmd[style + 1:]
341 cmd = cmd[style + 1:]
342
342
343 # avoid accepting e.g. style parameter as command
343 # avoid accepting e.g. style parameter as command
344 if util.safehasattr(webcommands, cmd):
344 if util.safehasattr(webcommands, cmd):
345 req.form['cmd'] = [cmd]
345 req.form['cmd'] = [cmd]
346
346
347 if cmd == 'static':
347 if cmd == 'static':
348 req.form['file'] = ['/'.join(args)]
348 req.form['file'] = ['/'.join(args)]
349 else:
349 else:
350 if args and args[0]:
350 if args and args[0]:
351 node = args.pop(0).replace('%2F', '/')
351 node = args.pop(0).replace('%2F', '/')
352 req.form['node'] = [node]
352 req.form['node'] = [node]
353 if args:
353 if args:
354 req.form['file'] = args
354 req.form['file'] = args
355
355
356 ua = req.env.get('HTTP_USER_AGENT', '')
356 ua = req.env.get('HTTP_USER_AGENT', '')
357 if cmd == 'rev' and 'mercurial' in ua:
357 if cmd == 'rev' and 'mercurial' in ua:
358 req.form['style'] = ['raw']
358 req.form['style'] = ['raw']
359
359
360 if cmd == 'archive':
360 if cmd == 'archive':
361 fn = req.form['node'][0]
361 fn = req.form['node'][0]
362 for type_, spec in rctx.archivespecs.iteritems():
362 for type_, spec in rctx.archivespecs.iteritems():
363 ext = spec[2]
363 ext = spec[2]
364 if fn.endswith(ext):
364 if fn.endswith(ext):
365 req.form['node'] = [fn[:-len(ext)]]
365 req.form['node'] = [fn[:-len(ext)]]
366 req.form['type'] = [type_]
366 req.form['type'] = [type_]
367
367
368 # process the web interface request
368 # process the web interface request
369
369
370 try:
370 try:
371 tmpl = rctx.templater(req)
371 tmpl = rctx.templater(req)
372 ctype = tmpl('mimetype', encoding=encoding.encoding)
372 ctype = tmpl('mimetype', encoding=encoding.encoding)
373 ctype = templater.stringify(ctype)
373 ctype = templater.stringify(ctype)
374
374
375 # check read permissions non-static content
375 # check read permissions non-static content
376 if cmd != 'static':
376 if cmd != 'static':
377 self.check_perm(rctx, req, None)
377 self.check_perm(rctx, req, None)
378
378
379 if cmd == '':
379 if cmd == '':
380 req.form['cmd'] = [tmpl.cache['default']]
380 req.form['cmd'] = [tmpl.cache['default']]
381 cmd = req.form['cmd'][0]
381 cmd = req.form['cmd'][0]
382
382
383 if rctx.configbool('web', 'cache', True):
383 if rctx.configbool('web', 'cache', True):
384 caching(self, req) # sets ETag header or raises NOT_MODIFIED
384 caching(self, req) # sets ETag header or raises NOT_MODIFIED
385 if cmd not in webcommands.__all__:
385 if cmd not in webcommands.__all__:
386 msg = 'no such method: %s' % cmd
386 msg = 'no such method: %s' % cmd
387 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
387 raise ErrorResponse(HTTP_BAD_REQUEST, msg)
388 elif cmd == 'file' and 'raw' in req.form.get('style', []):
388 elif cmd == 'file' and 'raw' in req.form.get('style', []):
389 rctx.ctype = ctype
389 rctx.ctype = ctype
390 content = webcommands.rawfile(rctx, req, tmpl)
390 content = webcommands.rawfile(rctx, req, tmpl)
391 else:
391 else:
392 content = getattr(webcommands, cmd)(rctx, req, tmpl)
392 content = getattr(webcommands, cmd)(rctx, req, tmpl)
393 req.respond(HTTP_OK, ctype)
393 req.respond(HTTP_OK, ctype)
394
394
395 return content
395 return content
396
396
397 except (error.LookupError, error.RepoLookupError) as err:
397 except (error.LookupError, error.RepoLookupError) as err:
398 req.respond(HTTP_NOT_FOUND, ctype)
398 req.respond(HTTP_NOT_FOUND, ctype)
399 msg = str(err)
399 msg = str(err)
400 if (util.safehasattr(err, 'name') and
400 if (util.safehasattr(err, 'name') and
401 not isinstance(err, error.ManifestLookupError)):
401 not isinstance(err, error.ManifestLookupError)):
402 msg = 'revision not found: %s' % err.name
402 msg = 'revision not found: %s' % err.name
403 return tmpl('error', error=msg)
403 return tmpl('error', error=msg)
404 except (error.RepoError, error.RevlogError) as inst:
404 except (error.RepoError, error.RevlogError) as inst:
405 req.respond(HTTP_SERVER_ERROR, ctype)
405 req.respond(HTTP_SERVER_ERROR, ctype)
406 return tmpl('error', error=str(inst))
406 return tmpl('error', error=str(inst))
407 except ErrorResponse as inst:
407 except ErrorResponse as inst:
408 req.respond(inst, ctype)
408 req.respond(inst, ctype)
409 if inst.code == HTTP_NOT_MODIFIED:
409 if inst.code == HTTP_NOT_MODIFIED:
410 # Not allowed to return a body on a 304
410 # Not allowed to return a body on a 304
411 return ['']
411 return ['']
412 return tmpl('error', error=str(inst))
412 return tmpl('error', error=str(inst))
413
413
414 def check_perm(self, rctx, req, op):
414 def check_perm(self, rctx, req, op):
415 for permhook in permhooks:
415 for permhook in permhooks:
416 permhook(rctx, req, op)
416 permhook(rctx, req, op)
417
417
418 def getwebview(repo):
418 def getwebview(repo):
419 """The 'web.view' config controls changeset filter to hgweb. Possible
419 """The 'web.view' config controls changeset filter to hgweb. Possible
420 values are ``served``, ``visible`` and ``all``. Default is ``served``.
420 values are ``served``, ``visible`` and ``all``. Default is ``served``.
421 The ``served`` filter only shows changesets that can be pulled from the
421 The ``served`` filter only shows changesets that can be pulled from the
422 hgweb instance. The``visible`` filter includes secret changesets but
422 hgweb instance. The``visible`` filter includes secret changesets but
423 still excludes "hidden" one.
423 still excludes "hidden" one.
424
424
425 See the repoview module for details.
425 See the repoview module for details.
426
426
427 The option has been around undocumented since Mercurial 2.5, but no
427 The option has been around undocumented since Mercurial 2.5, but no
428 user ever asked about it. So we better keep it undocumented for now."""
428 user ever asked about it. So we better keep it undocumented for now."""
429 viewconfig = repo.ui.config('web', 'view', 'served',
429 viewconfig = repo.ui.config('web', 'view', 'served',
430 untrusted=True)
430 untrusted=True)
431 if viewconfig == 'all':
431 if viewconfig == 'all':
432 return repo.unfiltered()
432 return repo.unfiltered()
433 elif viewconfig in repoview.filtertable:
433 elif viewconfig in repoview.filtertable:
434 return repo.filtered(viewconfig)
434 return repo.filtered(viewconfig)
435 else:
435 else:
436 return repo.filtered('served')
436 return repo.filtered('served')
437
437
General Comments 0
You need to be logged in to leave comments. Login now