##// END OF EJS Templates
help: improve hgweb help...
Mads Kiilerich -
r17104:5a9acb0b default
parent child Browse files
Show More
@@ -1,47 +1,50 b''
1 1 Mercurial's internal web server, hgweb, can serve either a single
2 repository, or a collection of them. In the latter case, a special
3 configuration file can be used to specify the repository paths to use
4 and global web configuration options.
2 repository, or a tree of repositories. In the second case, repository
3 paths and global options can be defined using a dedicated
4 configuration file common to :hg:`serve`, ``hgweb.wsgi``,
5 ``hgweb.cgi`` and ``hgweb.fcgi``.
5 6
6 This file uses the same syntax as other Mercurial configuration files,
7 but only the following sections are recognized:
7 This file uses the same syntax as other Mercurial configuration files
8 but recognizes only the following sections:
8 9
9 10 - web
10 11 - paths
11 12 - collections
12 13
13 The ``web`` section can specify all the settings described in the web
14 section of the hgrc(5) documentation. See :hg:`help config` for
15 information on where to find the manual page.
14 The ``web`` options are thorougly described in :hg:`help config`.
15
16 The ``paths`` section maps URL paths to paths of repositories in the
17 filesystem. hgweb will not expose the filesystem directly - only
18 Mercurial repositories can be published and only according to the
19 configuration.
16 20
17 The ``paths`` section provides mappings of physical repository
18 paths to virtual ones. For instance::
21 The left hand side is the path in the URL. Note that hgweb reserves
22 subpaths like ``rev`` or ``file``, try using different names for
23 nested repositories to avoid confusing effects.
24
25 The right hand side is the path in the filesystem. If the specified
26 path ends with ``*`` or ``**`` the filesystem will be searched
27 recursively for repositories below that point.
28 With ``*`` it will not recurse into the repositories it finds (except for
29 ``.hg/patches``).
30 With ``**`` it will also search inside repository working directories
31 and possibly find subrepositories.
32
33 In this example::
19 34
20 35 [paths]
21 projects/a = /foo/bar
22 projects/b = /baz/quux
23 web/root = /real/root/*
24 / = /real/root2/*
25 virtual/root2 = /real/root2/**
36 /projects/a = /srv/tmprepos/a
37 /projects/b = c:/repos/b
38 / = /srv/repos/*
39 /user/bob = /home/bob/repos/**
26 40
27 41 - The first two entries make two repositories in different directories
28 42 appear under the same directory in the web interface
29 - The third entry maps every Mercurial repository found in '/real/root'
30 into 'web/root'. This format is preferred over the [collections] one,
31 since using absolute paths as configuration keys is not supported on every
32 platform (especially on Windows).
33 - The fourth entry is a special case mapping all repositories in
34 '/real/root2' in the root of the virtual directory.
35 - The fifth entry recursively finds all repositories under the real
36 root, and maps their relative paths under the virtual root.
43 - The third entry will publish every Mercurial repository found in
44 ``/srv/repos/``, for instance the repository ``/srv/repos/quux/``
45 will appear as ``http://server/quux/``
46 - The fourth entry will publish both ``http://server/user/bob/quux/``
47 and ``http://server/user/bob/quux/testsubrepo/``
37 48
38 The ``collections`` section provides mappings of trees of physical
39 repositories paths to virtual ones, though the paths syntax is generally
40 preferred. For instance::
41
42 [collections]
43 /foo = /foo
44
45 Here, the left side will be stripped off all repositories found in the
46 right side. Thus ``/foo/bar`` and ``foo/quux/baz`` will be listed as
47 ``bar`` and ``quux/baz`` respectively.
49 The ``collections`` section is deprecated and has been superseeded by
50 ``paths``.
@@ -1,449 +1,449 b''
1 1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import os, re, time
10 10 from mercurial.i18n import _
11 11 from mercurial import ui, hg, scmutil, util, templater
12 12 from mercurial import error, encoding
13 13 from common import ErrorResponse, get_mtime, staticfile, paritygen, \
14 14 get_contact, HTTP_OK, HTTP_NOT_FOUND, HTTP_SERVER_ERROR
15 15 from hgweb_mod import hgweb
16 16 from request import wsgirequest
17 17 import webutil
18 18
19 19 def cleannames(items):
20 20 return [(util.pconvert(name).strip('/'), path) for name, path in items]
21 21
22 22 def findrepos(paths):
23 23 repos = []
24 24 for prefix, root in cleannames(paths):
25 25 roothead, roottail = os.path.split(root)
26 # "foo = /bar/*" makes every subrepo of /bar/ to be
27 # mounted as foo/subrepo
28 # and "foo = /bar/**" also recurses into the subdirectories,
29 # remember to use it without working dir.
26 # "foo = /bar/*" or "foo = /bar/**" lets every repo /bar/N in or below
27 # /bar/ be served as as foo/N .
28 # '*' will not search inside dirs with .hg (except .hg/patches),
29 # '**' will search inside dirs with .hg (and thus also find subrepos).
30 30 try:
31 31 recurse = {'*': False, '**': True}[roottail]
32 32 except KeyError:
33 33 repos.append((prefix, root))
34 34 continue
35 35 roothead = os.path.normpath(os.path.abspath(roothead))
36 36 paths = scmutil.walkrepos(roothead, followsym=True, recurse=recurse)
37 37 repos.extend(urlrepos(prefix, roothead, paths))
38 38 return repos
39 39
40 40 def urlrepos(prefix, roothead, paths):
41 41 """yield url paths and filesystem paths from a list of repo paths
42 42
43 43 >>> conv = lambda seq: [(v, util.pconvert(p)) for v,p in seq]
44 44 >>> conv(urlrepos('hg', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
45 45 [('hg/r', '/opt/r'), ('hg/r/r', '/opt/r/r'), ('hg', '/opt')]
46 46 >>> conv(urlrepos('', '/opt', ['/opt/r', '/opt/r/r', '/opt']))
47 47 [('r', '/opt/r'), ('r/r', '/opt/r/r'), ('', '/opt')]
48 48 """
49 49 for path in paths:
50 50 path = os.path.normpath(path)
51 51 yield (prefix + '/' +
52 52 util.pconvert(path[len(roothead):]).lstrip('/')).strip('/'), path
53 53
54 54 def geturlcgivars(baseurl, port):
55 55 """
56 56 Extract CGI variables from baseurl
57 57
58 58 >>> geturlcgivars("http://host.org/base", "80")
59 59 ('host.org', '80', '/base')
60 60 >>> geturlcgivars("http://host.org:8000/base", "80")
61 61 ('host.org', '8000', '/base')
62 62 >>> geturlcgivars('/base', 8000)
63 63 ('', '8000', '/base')
64 64 >>> geturlcgivars("base", '8000')
65 65 ('', '8000', '/base')
66 66 >>> geturlcgivars("http://host", '8000')
67 67 ('host', '8000', '/')
68 68 >>> geturlcgivars("http://host/", '8000')
69 69 ('host', '8000', '/')
70 70 """
71 71 u = util.url(baseurl)
72 72 name = u.host or ''
73 73 if u.port:
74 74 port = u.port
75 75 path = u.path or ""
76 76 if not path.startswith('/'):
77 77 path = '/' + path
78 78
79 79 return name, str(port), path
80 80
81 81 class hgwebdir(object):
82 82 refreshinterval = 20
83 83
84 84 def __init__(self, conf, baseui=None):
85 85 self.conf = conf
86 86 self.baseui = baseui
87 87 self.lastrefresh = 0
88 88 self.motd = None
89 89 self.refresh()
90 90
91 91 def refresh(self):
92 92 if self.lastrefresh + self.refreshinterval > time.time():
93 93 return
94 94
95 95 if self.baseui:
96 96 u = self.baseui.copy()
97 97 else:
98 98 u = ui.ui()
99 99 u.setconfig('ui', 'report_untrusted', 'off')
100 100 u.setconfig('ui', 'nontty', 'true')
101 101
102 102 if not isinstance(self.conf, (dict, list, tuple)):
103 103 map = {'paths': 'hgweb-paths'}
104 104 if not os.path.exists(self.conf):
105 105 raise util.Abort(_('config file %s not found!') % self.conf)
106 106 u.readconfig(self.conf, remap=map, trust=True)
107 107 paths = []
108 108 for name, ignored in u.configitems('hgweb-paths'):
109 109 for path in u.configlist('hgweb-paths', name):
110 110 paths.append((name, path))
111 111 elif isinstance(self.conf, (list, tuple)):
112 112 paths = self.conf
113 113 elif isinstance(self.conf, dict):
114 114 paths = self.conf.items()
115 115
116 116 repos = findrepos(paths)
117 117 for prefix, root in u.configitems('collections'):
118 118 prefix = util.pconvert(prefix)
119 119 for path in scmutil.walkrepos(root, followsym=True):
120 120 repo = os.path.normpath(path)
121 121 name = util.pconvert(repo)
122 122 if name.startswith(prefix):
123 123 name = name[len(prefix):]
124 124 repos.append((name.lstrip('/'), repo))
125 125
126 126 self.repos = repos
127 127 self.ui = u
128 128 encoding.encoding = self.ui.config('web', 'encoding',
129 129 encoding.encoding)
130 130 self.style = self.ui.config('web', 'style', 'paper')
131 131 self.templatepath = self.ui.config('web', 'templates', None)
132 132 self.stripecount = self.ui.config('web', 'stripes', 1)
133 133 if self.stripecount:
134 134 self.stripecount = int(self.stripecount)
135 135 self._baseurl = self.ui.config('web', 'baseurl')
136 136 self.lastrefresh = time.time()
137 137
138 138 def run(self):
139 139 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
140 140 raise RuntimeError("This function is only intended to be "
141 141 "called while running as a CGI script.")
142 142 import mercurial.hgweb.wsgicgi as wsgicgi
143 143 wsgicgi.launch(self)
144 144
145 145 def __call__(self, env, respond):
146 146 req = wsgirequest(env, respond)
147 147 return self.run_wsgi(req)
148 148
149 149 def read_allowed(self, ui, req):
150 150 """Check allow_read and deny_read config options of a repo's ui object
151 151 to determine user permissions. By default, with neither option set (or
152 152 both empty), allow all users to read the repo. There are two ways a
153 153 user can be denied read access: (1) deny_read is not empty, and the
154 154 user is unauthenticated or deny_read contains user (or *), and (2)
155 155 allow_read is not empty and the user is not in allow_read. Return True
156 156 if user is allowed to read the repo, else return False."""
157 157
158 158 user = req.env.get('REMOTE_USER')
159 159
160 160 deny_read = ui.configlist('web', 'deny_read', untrusted=True)
161 161 if deny_read and (not user or deny_read == ['*'] or user in deny_read):
162 162 return False
163 163
164 164 allow_read = ui.configlist('web', 'allow_read', untrusted=True)
165 165 # by default, allow reading if no allow_read option has been set
166 166 if (not allow_read) or (allow_read == ['*']) or (user in allow_read):
167 167 return True
168 168
169 169 return False
170 170
171 171 def run_wsgi(self, req):
172 172 try:
173 173 try:
174 174 self.refresh()
175 175
176 176 virtual = req.env.get("PATH_INFO", "").strip('/')
177 177 tmpl = self.templater(req)
178 178 ctype = tmpl('mimetype', encoding=encoding.encoding)
179 179 ctype = templater.stringify(ctype)
180 180
181 181 # a static file
182 182 if virtual.startswith('static/') or 'static' in req.form:
183 183 if virtual.startswith('static/'):
184 184 fname = virtual[7:]
185 185 else:
186 186 fname = req.form['static'][0]
187 187 static = templater.templatepath('static')
188 188 return (staticfile(static, fname, req),)
189 189
190 190 # top-level index
191 191 elif not virtual:
192 192 req.respond(HTTP_OK, ctype)
193 193 return self.makeindex(req, tmpl)
194 194
195 195 # nested indexes and hgwebs
196 196
197 197 repos = dict(self.repos)
198 198 virtualrepo = virtual
199 199 while virtualrepo:
200 200 real = repos.get(virtualrepo)
201 201 if real:
202 202 req.env['REPO_NAME'] = virtualrepo
203 203 try:
204 204 repo = hg.repository(self.ui, real)
205 205 return hgweb(repo).run_wsgi(req)
206 206 except IOError, inst:
207 207 msg = inst.strerror
208 208 raise ErrorResponse(HTTP_SERVER_ERROR, msg)
209 209 except error.RepoError, inst:
210 210 raise ErrorResponse(HTTP_SERVER_ERROR, str(inst))
211 211
212 212 up = virtualrepo.rfind('/')
213 213 if up < 0:
214 214 break
215 215 virtualrepo = virtualrepo[:up]
216 216
217 217 # browse subdirectories
218 218 subdir = virtual + '/'
219 219 if [r for r in repos if r.startswith(subdir)]:
220 220 req.respond(HTTP_OK, ctype)
221 221 return self.makeindex(req, tmpl, subdir)
222 222
223 223 # prefixes not found
224 224 req.respond(HTTP_NOT_FOUND, ctype)
225 225 return tmpl("notfound", repo=virtual)
226 226
227 227 except ErrorResponse, err:
228 228 req.respond(err, ctype)
229 229 return tmpl('error', error=err.message or '')
230 230 finally:
231 231 tmpl = None
232 232
233 233 def makeindex(self, req, tmpl, subdir=""):
234 234
235 235 def archivelist(ui, nodeid, url):
236 236 allowed = ui.configlist("web", "allow_archive", untrusted=True)
237 237 archives = []
238 238 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
239 239 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
240 240 untrusted=True):
241 241 archives.append({"type" : i[0], "extension": i[1],
242 242 "node": nodeid, "url": url})
243 243 return archives
244 244
245 245 def rawentries(subdir="", **map):
246 246
247 247 descend = self.ui.configbool('web', 'descend', True)
248 248 collapse = self.ui.configbool('web', 'collapse', False)
249 249 seenrepos = set()
250 250 seendirs = set()
251 251 for name, path in self.repos:
252 252
253 253 if not name.startswith(subdir):
254 254 continue
255 255 name = name[len(subdir):]
256 256 directory = False
257 257
258 258 if '/' in name:
259 259 if not descend:
260 260 continue
261 261
262 262 nameparts = name.split('/')
263 263 rootname = nameparts[0]
264 264
265 265 if not collapse:
266 266 pass
267 267 elif rootname in seendirs:
268 268 continue
269 269 elif rootname in seenrepos:
270 270 pass
271 271 else:
272 272 directory = True
273 273 name = rootname
274 274
275 275 # redefine the path to refer to the directory
276 276 discarded = '/'.join(nameparts[1:])
277 277
278 278 # remove name parts plus accompanying slash
279 279 path = path[:-len(discarded) - 1]
280 280
281 281 parts = [name]
282 282 if 'PATH_INFO' in req.env:
283 283 parts.insert(0, req.env['PATH_INFO'].rstrip('/'))
284 284 if req.env['SCRIPT_NAME']:
285 285 parts.insert(0, req.env['SCRIPT_NAME'])
286 286 url = re.sub(r'/+', '/', '/'.join(parts) + '/')
287 287
288 288 # show either a directory entry or a repository
289 289 if directory:
290 290 # get the directory's time information
291 291 try:
292 292 d = (get_mtime(path), util.makedate()[1])
293 293 except OSError:
294 294 continue
295 295
296 296 row = dict(contact="",
297 297 contact_sort="",
298 298 name=name,
299 299 name_sort=name,
300 300 url=url,
301 301 description="",
302 302 description_sort="",
303 303 lastchange=d,
304 304 lastchange_sort=d[1]-d[0],
305 305 archives=[])
306 306
307 307 seendirs.add(name)
308 308 yield row
309 309 continue
310 310
311 311 u = self.ui.copy()
312 312 try:
313 313 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
314 314 except Exception, e:
315 315 u.warn(_('error reading %s/.hg/hgrc: %s\n') % (path, e))
316 316 continue
317 317 def get(section, name, default=None):
318 318 return u.config(section, name, default, untrusted=True)
319 319
320 320 if u.configbool("web", "hidden", untrusted=True):
321 321 continue
322 322
323 323 if not self.read_allowed(u, req):
324 324 continue
325 325
326 326 # update time with local timezone
327 327 try:
328 328 r = hg.repository(self.ui, path)
329 329 except IOError:
330 330 u.warn(_('error accessing repository at %s\n') % path)
331 331 continue
332 332 except error.RepoError:
333 333 u.warn(_('error accessing repository at %s\n') % path)
334 334 continue
335 335 try:
336 336 d = (get_mtime(r.spath), util.makedate()[1])
337 337 except OSError:
338 338 continue
339 339
340 340 contact = get_contact(get)
341 341 description = get("web", "description", "")
342 342 name = get("web", "name", name)
343 343 row = dict(contact=contact or "unknown",
344 344 contact_sort=contact.upper() or "unknown",
345 345 name=name,
346 346 name_sort=name,
347 347 url=url,
348 348 description=description or "unknown",
349 349 description_sort=description.upper() or "unknown",
350 350 lastchange=d,
351 351 lastchange_sort=d[1]-d[0],
352 352 archives=archivelist(u, "tip", url))
353 353
354 354 seenrepos.add(name)
355 355 yield row
356 356
357 357 sortdefault = None, False
358 358 def entries(sortcolumn="", descending=False, subdir="", **map):
359 359 rows = rawentries(subdir=subdir, **map)
360 360
361 361 if sortcolumn and sortdefault != (sortcolumn, descending):
362 362 sortkey = '%s_sort' % sortcolumn
363 363 rows = sorted(rows, key=lambda x: x[sortkey],
364 364 reverse=descending)
365 365 for row, parity in zip(rows, paritygen(self.stripecount)):
366 366 row['parity'] = parity
367 367 yield row
368 368
369 369 self.refresh()
370 370 sortable = ["name", "description", "contact", "lastchange"]
371 371 sortcolumn, descending = sortdefault
372 372 if 'sort' in req.form:
373 373 sortcolumn = req.form['sort'][0]
374 374 descending = sortcolumn.startswith('-')
375 375 if descending:
376 376 sortcolumn = sortcolumn[1:]
377 377 if sortcolumn not in sortable:
378 378 sortcolumn = ""
379 379
380 380 sort = [("sort_%s" % column,
381 381 "%s%s" % ((not descending and column == sortcolumn)
382 382 and "-" or "", column))
383 383 for column in sortable]
384 384
385 385 self.refresh()
386 386 self.updatereqenv(req.env)
387 387
388 388 return tmpl("index", entries=entries, subdir=subdir,
389 389 sortcolumn=sortcolumn, descending=descending,
390 390 **dict(sort))
391 391
392 392 def templater(self, req):
393 393
394 394 def header(**map):
395 395 yield tmpl('header', encoding=encoding.encoding, **map)
396 396
397 397 def footer(**map):
398 398 yield tmpl("footer", **map)
399 399
400 400 def motd(**map):
401 401 if self.motd is not None:
402 402 yield self.motd
403 403 else:
404 404 yield config('web', 'motd', '')
405 405
406 406 def config(section, name, default=None, untrusted=True):
407 407 return self.ui.config(section, name, default, untrusted)
408 408
409 409 self.updatereqenv(req.env)
410 410
411 411 url = req.env.get('SCRIPT_NAME', '')
412 412 if not url.endswith('/'):
413 413 url += '/'
414 414
415 415 vars = {}
416 416 styles = (
417 417 req.form.get('style', [None])[0],
418 418 config('web', 'style'),
419 419 'paper'
420 420 )
421 421 style, mapfile = templater.stylemap(styles, self.templatepath)
422 422 if style == styles[0]:
423 423 vars['style'] = style
424 424
425 425 start = url[-1] == '?' and '&' or '?'
426 426 sessionvars = webutil.sessionvars(vars, start)
427 427 logourl = config('web', 'logourl', 'http://mercurial.selenic.com/')
428 428 logoimg = config('web', 'logoimg', 'hglogo.png')
429 429 staticurl = config('web', 'staticurl') or url + 'static/'
430 430 if not staticurl.endswith('/'):
431 431 staticurl += '/'
432 432
433 433 tmpl = templater.templater(mapfile,
434 434 defaults={"header": header,
435 435 "footer": footer,
436 436 "motd": motd,
437 437 "url": url,
438 438 "logourl": logourl,
439 439 "logoimg": logoimg,
440 440 "staticurl": staticurl,
441 441 "sessionvars": sessionvars})
442 442 return tmpl
443 443
444 444 def updatereqenv(self, env):
445 445 if self._baseurl is not None:
446 446 name, port, path = geturlcgivars(self._baseurl, env['SERVER_PORT'])
447 447 env['SERVER_NAME'] = name
448 448 env['SERVER_PORT'] = port
449 449 env['SCRIPT_NAME'] = path
@@ -1,886 +1,887 b''
1 1 # scmutil.py - Mercurial core utility functions
2 2 #
3 3 # Copyright Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from i18n import _
9 9 import util, error, osutil, revset, similar, encoding
10 10 import match as matchmod
11 11 import os, errno, re, stat, sys, glob
12 12
13 13 def nochangesfound(ui, secretlist=None):
14 14 '''report no changes for push/pull'''
15 15 if secretlist:
16 16 ui.status(_("no changes found (ignored %d secret changesets)\n")
17 17 % len(secretlist))
18 18 else:
19 19 ui.status(_("no changes found\n"))
20 20
21 21 def checkfilename(f):
22 22 '''Check that the filename f is an acceptable filename for a tracked file'''
23 23 if '\r' in f or '\n' in f:
24 24 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
25 25
26 26 def checkportable(ui, f):
27 27 '''Check if filename f is portable and warn or abort depending on config'''
28 28 checkfilename(f)
29 29 abort, warn = checkportabilityalert(ui)
30 30 if abort or warn:
31 31 msg = util.checkwinfilename(f)
32 32 if msg:
33 33 msg = "%s: %r" % (msg, f)
34 34 if abort:
35 35 raise util.Abort(msg)
36 36 ui.warn(_("warning: %s\n") % msg)
37 37
38 38 def checkportabilityalert(ui):
39 39 '''check if the user's config requests nothing, a warning, or abort for
40 40 non-portable filenames'''
41 41 val = ui.config('ui', 'portablefilenames', 'warn')
42 42 lval = val.lower()
43 43 bval = util.parsebool(val)
44 44 abort = os.name == 'nt' or lval == 'abort'
45 45 warn = bval or lval == 'warn'
46 46 if bval is None and not (warn or abort or lval == 'ignore'):
47 47 raise error.ConfigError(
48 48 _("ui.portablefilenames value is invalid ('%s')") % val)
49 49 return abort, warn
50 50
51 51 class casecollisionauditor(object):
52 52 def __init__(self, ui, abort, existingiter):
53 53 self._ui = ui
54 54 self._abort = abort
55 55 self._map = {}
56 56 for f in existingiter:
57 57 self._map[encoding.lower(f)] = f
58 58
59 59 def __call__(self, f):
60 60 fl = encoding.lower(f)
61 61 map = self._map
62 62 if fl in map and map[fl] != f:
63 63 msg = _('possible case-folding collision for %s') % f
64 64 if self._abort:
65 65 raise util.Abort(msg)
66 66 self._ui.warn(_("warning: %s\n") % msg)
67 67 map[fl] = f
68 68
69 69 class pathauditor(object):
70 70 '''ensure that a filesystem path contains no banned components.
71 71 the following properties of a path are checked:
72 72
73 73 - ends with a directory separator
74 74 - under top-level .hg
75 75 - starts at the root of a windows drive
76 76 - contains ".."
77 77 - traverses a symlink (e.g. a/symlink_here/b)
78 78 - inside a nested repository (a callback can be used to approve
79 79 some nested repositories, e.g., subrepositories)
80 80 '''
81 81
82 82 def __init__(self, root, callback=None):
83 83 self.audited = set()
84 84 self.auditeddir = set()
85 85 self.root = root
86 86 self.callback = callback
87 87 if os.path.lexists(root) and not util.checkcase(root):
88 88 self.normcase = util.normcase
89 89 else:
90 90 self.normcase = lambda x: x
91 91
92 92 def __call__(self, path):
93 93 '''Check the relative path.
94 94 path may contain a pattern (e.g. foodir/**.txt)'''
95 95
96 96 path = util.localpath(path)
97 97 normpath = self.normcase(path)
98 98 if normpath in self.audited:
99 99 return
100 100 # AIX ignores "/" at end of path, others raise EISDIR.
101 101 if util.endswithsep(path):
102 102 raise util.Abort(_("path ends in directory separator: %s") % path)
103 103 parts = util.splitpath(path)
104 104 if (os.path.splitdrive(path)[0]
105 105 or parts[0].lower() in ('.hg', '.hg.', '')
106 106 or os.pardir in parts):
107 107 raise util.Abort(_("path contains illegal component: %s") % path)
108 108 if '.hg' in path.lower():
109 109 lparts = [p.lower() for p in parts]
110 110 for p in '.hg', '.hg.':
111 111 if p in lparts[1:]:
112 112 pos = lparts.index(p)
113 113 base = os.path.join(*parts[:pos])
114 114 raise util.Abort(_("path '%s' is inside nested repo %r")
115 115 % (path, base))
116 116
117 117 normparts = util.splitpath(normpath)
118 118 assert len(parts) == len(normparts)
119 119
120 120 parts.pop()
121 121 normparts.pop()
122 122 prefixes = []
123 123 while parts:
124 124 prefix = os.sep.join(parts)
125 125 normprefix = os.sep.join(normparts)
126 126 if normprefix in self.auditeddir:
127 127 break
128 128 curpath = os.path.join(self.root, prefix)
129 129 try:
130 130 st = os.lstat(curpath)
131 131 except OSError, err:
132 132 # EINVAL can be raised as invalid path syntax under win32.
133 133 # They must be ignored for patterns can be checked too.
134 134 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
135 135 raise
136 136 else:
137 137 if stat.S_ISLNK(st.st_mode):
138 138 raise util.Abort(
139 139 _('path %r traverses symbolic link %r')
140 140 % (path, prefix))
141 141 elif (stat.S_ISDIR(st.st_mode) and
142 142 os.path.isdir(os.path.join(curpath, '.hg'))):
143 143 if not self.callback or not self.callback(curpath):
144 144 raise util.Abort(_("path '%s' is inside nested "
145 145 "repo %r")
146 146 % (path, prefix))
147 147 prefixes.append(normprefix)
148 148 parts.pop()
149 149 normparts.pop()
150 150
151 151 self.audited.add(normpath)
152 152 # only add prefixes to the cache after checking everything: we don't
153 153 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
154 154 self.auditeddir.update(prefixes)
155 155
156 156 class abstractopener(object):
157 157 """Abstract base class; cannot be instantiated"""
158 158
159 159 def __init__(self, *args, **kwargs):
160 160 '''Prevent instantiation; don't call this from subclasses.'''
161 161 raise NotImplementedError('attempted instantiating ' + str(type(self)))
162 162
163 163 def tryread(self, path):
164 164 '''gracefully return an empty string for missing files'''
165 165 try:
166 166 return self.read(path)
167 167 except IOError, inst:
168 168 if inst.errno != errno.ENOENT:
169 169 raise
170 170 return ""
171 171
172 172 def read(self, path):
173 173 fp = self(path, 'rb')
174 174 try:
175 175 return fp.read()
176 176 finally:
177 177 fp.close()
178 178
179 179 def write(self, path, data):
180 180 fp = self(path, 'wb')
181 181 try:
182 182 return fp.write(data)
183 183 finally:
184 184 fp.close()
185 185
186 186 def append(self, path, data):
187 187 fp = self(path, 'ab')
188 188 try:
189 189 return fp.write(data)
190 190 finally:
191 191 fp.close()
192 192
193 193 class opener(abstractopener):
194 194 '''Open files relative to a base directory
195 195
196 196 This class is used to hide the details of COW semantics and
197 197 remote file access from higher level code.
198 198 '''
199 199 def __init__(self, base, audit=True):
200 200 self.base = base
201 201 self._audit = audit
202 202 if audit:
203 203 self.auditor = pathauditor(base)
204 204 else:
205 205 self.auditor = util.always
206 206 self.createmode = None
207 207 self._trustnlink = None
208 208
209 209 @util.propertycache
210 210 def _cansymlink(self):
211 211 return util.checklink(self.base)
212 212
213 213 def _fixfilemode(self, name):
214 214 if self.createmode is None:
215 215 return
216 216 os.chmod(name, self.createmode & 0666)
217 217
218 218 def __call__(self, path, mode="r", text=False, atomictemp=False):
219 219 if self._audit:
220 220 r = util.checkosfilename(path)
221 221 if r:
222 222 raise util.Abort("%s: %r" % (r, path))
223 223 self.auditor(path)
224 224 f = self.join(path)
225 225
226 226 if not text and "b" not in mode:
227 227 mode += "b" # for that other OS
228 228
229 229 nlink = -1
230 230 dirname, basename = os.path.split(f)
231 231 # If basename is empty, then the path is malformed because it points
232 232 # to a directory. Let the posixfile() call below raise IOError.
233 233 if basename and mode not in ('r', 'rb'):
234 234 if atomictemp:
235 235 if not os.path.isdir(dirname):
236 236 util.makedirs(dirname, self.createmode)
237 237 return util.atomictempfile(f, mode, self.createmode)
238 238 try:
239 239 if 'w' in mode:
240 240 util.unlink(f)
241 241 nlink = 0
242 242 else:
243 243 # nlinks() may behave differently for files on Windows
244 244 # shares if the file is open.
245 245 fd = util.posixfile(f)
246 246 nlink = util.nlinks(f)
247 247 if nlink < 1:
248 248 nlink = 2 # force mktempcopy (issue1922)
249 249 fd.close()
250 250 except (OSError, IOError), e:
251 251 if e.errno != errno.ENOENT:
252 252 raise
253 253 nlink = 0
254 254 if not os.path.isdir(dirname):
255 255 util.makedirs(dirname, self.createmode)
256 256 if nlink > 0:
257 257 if self._trustnlink is None:
258 258 self._trustnlink = nlink > 1 or util.checknlink(f)
259 259 if nlink > 1 or not self._trustnlink:
260 260 util.rename(util.mktempcopy(f), f)
261 261 fp = util.posixfile(f, mode)
262 262 if nlink == 0:
263 263 self._fixfilemode(f)
264 264 return fp
265 265
266 266 def symlink(self, src, dst):
267 267 self.auditor(dst)
268 268 linkname = self.join(dst)
269 269 try:
270 270 os.unlink(linkname)
271 271 except OSError:
272 272 pass
273 273
274 274 dirname = os.path.dirname(linkname)
275 275 if not os.path.exists(dirname):
276 276 util.makedirs(dirname, self.createmode)
277 277
278 278 if self._cansymlink:
279 279 try:
280 280 os.symlink(src, linkname)
281 281 except OSError, err:
282 282 raise OSError(err.errno, _('could not symlink to %r: %s') %
283 283 (src, err.strerror), linkname)
284 284 else:
285 285 f = self(dst, "w")
286 286 f.write(src)
287 287 f.close()
288 288 self._fixfilemode(dst)
289 289
290 290 def audit(self, path):
291 291 self.auditor(path)
292 292
293 293 def join(self, path):
294 294 return os.path.join(self.base, path)
295 295
296 296 class filteropener(abstractopener):
297 297 '''Wrapper opener for filtering filenames with a function.'''
298 298
299 299 def __init__(self, opener, filter):
300 300 self._filter = filter
301 301 self._orig = opener
302 302
303 303 def __call__(self, path, *args, **kwargs):
304 304 return self._orig(self._filter(path), *args, **kwargs)
305 305
306 306 def canonpath(root, cwd, myname, auditor=None):
307 307 '''return the canonical path of myname, given cwd and root'''
308 308 if util.endswithsep(root):
309 309 rootsep = root
310 310 else:
311 311 rootsep = root + os.sep
312 312 name = myname
313 313 if not os.path.isabs(name):
314 314 name = os.path.join(root, cwd, name)
315 315 name = os.path.normpath(name)
316 316 if auditor is None:
317 317 auditor = pathauditor(root)
318 318 if name != rootsep and name.startswith(rootsep):
319 319 name = name[len(rootsep):]
320 320 auditor(name)
321 321 return util.pconvert(name)
322 322 elif name == root:
323 323 return ''
324 324 else:
325 325 # Determine whether `name' is in the hierarchy at or beneath `root',
326 326 # by iterating name=dirname(name) until that causes no change (can't
327 327 # check name == '/', because that doesn't work on windows). The list
328 328 # `rel' holds the reversed list of components making up the relative
329 329 # file name we want.
330 330 rel = []
331 331 while True:
332 332 try:
333 333 s = util.samefile(name, root)
334 334 except OSError:
335 335 s = False
336 336 if s:
337 337 if not rel:
338 338 # name was actually the same as root (maybe a symlink)
339 339 return ''
340 340 rel.reverse()
341 341 name = os.path.join(*rel)
342 342 auditor(name)
343 343 return util.pconvert(name)
344 344 dirname, basename = os.path.split(name)
345 345 rel.append(basename)
346 346 if dirname == name:
347 347 break
348 348 name = dirname
349 349
350 350 raise util.Abort('%s not under root' % myname)
351 351
352 352 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
353 '''yield every hg repository under path, recursively.'''
353 '''yield every hg repository under path, always recursively.
354 The recurse flag will only control recursion into repo working dirs'''
354 355 def errhandler(err):
355 356 if err.filename == path:
356 357 raise err
357 358 samestat = getattr(os.path, 'samestat', None)
358 359 if followsym and samestat is not None:
359 360 def adddir(dirlst, dirname):
360 361 match = False
361 362 dirstat = os.stat(dirname)
362 363 for lstdirstat in dirlst:
363 364 if samestat(dirstat, lstdirstat):
364 365 match = True
365 366 break
366 367 if not match:
367 368 dirlst.append(dirstat)
368 369 return not match
369 370 else:
370 371 followsym = False
371 372
372 373 if (seen_dirs is None) and followsym:
373 374 seen_dirs = []
374 375 adddir(seen_dirs, path)
375 376 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
376 377 dirs.sort()
377 378 if '.hg' in dirs:
378 379 yield root # found a repository
379 380 qroot = os.path.join(root, '.hg', 'patches')
380 381 if os.path.isdir(os.path.join(qroot, '.hg')):
381 382 yield qroot # we have a patch queue repo here
382 383 if recurse:
383 384 # avoid recursing inside the .hg directory
384 385 dirs.remove('.hg')
385 386 else:
386 387 dirs[:] = [] # don't descend further
387 388 elif followsym:
388 389 newdirs = []
389 390 for d in dirs:
390 391 fname = os.path.join(root, d)
391 392 if adddir(seen_dirs, fname):
392 393 if os.path.islink(fname):
393 394 for hgname in walkrepos(fname, True, seen_dirs):
394 395 yield hgname
395 396 else:
396 397 newdirs.append(d)
397 398 dirs[:] = newdirs
398 399
399 400 def osrcpath():
400 401 '''return default os-specific hgrc search path'''
401 402 path = systemrcpath()
402 403 path.extend(userrcpath())
403 404 path = [os.path.normpath(f) for f in path]
404 405 return path
405 406
406 407 _rcpath = None
407 408
408 409 def rcpath():
409 410 '''return hgrc search path. if env var HGRCPATH is set, use it.
410 411 for each item in path, if directory, use files ending in .rc,
411 412 else use item.
412 413 make HGRCPATH empty to only look in .hg/hgrc of current repo.
413 414 if no HGRCPATH, use default os-specific path.'''
414 415 global _rcpath
415 416 if _rcpath is None:
416 417 if 'HGRCPATH' in os.environ:
417 418 _rcpath = []
418 419 for p in os.environ['HGRCPATH'].split(os.pathsep):
419 420 if not p:
420 421 continue
421 422 p = util.expandpath(p)
422 423 if os.path.isdir(p):
423 424 for f, kind in osutil.listdir(p):
424 425 if f.endswith('.rc'):
425 426 _rcpath.append(os.path.join(p, f))
426 427 else:
427 428 _rcpath.append(p)
428 429 else:
429 430 _rcpath = osrcpath()
430 431 return _rcpath
431 432
432 433 if os.name != 'nt':
433 434
434 435 def rcfiles(path):
435 436 rcs = [os.path.join(path, 'hgrc')]
436 437 rcdir = os.path.join(path, 'hgrc.d')
437 438 try:
438 439 rcs.extend([os.path.join(rcdir, f)
439 440 for f, kind in osutil.listdir(rcdir)
440 441 if f.endswith(".rc")])
441 442 except OSError:
442 443 pass
443 444 return rcs
444 445
445 446 def systemrcpath():
446 447 path = []
447 448 if sys.platform == 'plan9':
448 449 root = 'lib/mercurial'
449 450 else:
450 451 root = 'etc/mercurial'
451 452 # old mod_python does not set sys.argv
452 453 if len(getattr(sys, 'argv', [])) > 0:
453 454 p = os.path.dirname(os.path.dirname(sys.argv[0]))
454 455 path.extend(rcfiles(os.path.join(p, root)))
455 456 path.extend(rcfiles('/' + root))
456 457 return path
457 458
458 459 def userrcpath():
459 460 if sys.platform == 'plan9':
460 461 return [os.environ['home'] + '/lib/hgrc']
461 462 else:
462 463 return [os.path.expanduser('~/.hgrc')]
463 464
464 465 else:
465 466
466 467 import _winreg
467 468
468 469 def systemrcpath():
469 470 '''return default os-specific hgrc search path'''
470 471 rcpath = []
471 472 filename = util.executablepath()
472 473 # Use mercurial.ini found in directory with hg.exe
473 474 progrc = os.path.join(os.path.dirname(filename), 'mercurial.ini')
474 475 if os.path.isfile(progrc):
475 476 rcpath.append(progrc)
476 477 return rcpath
477 478 # Use hgrc.d found in directory with hg.exe
478 479 progrcd = os.path.join(os.path.dirname(filename), 'hgrc.d')
479 480 if os.path.isdir(progrcd):
480 481 for f, kind in osutil.listdir(progrcd):
481 482 if f.endswith('.rc'):
482 483 rcpath.append(os.path.join(progrcd, f))
483 484 return rcpath
484 485 # else look for a system rcpath in the registry
485 486 value = util.lookupreg('SOFTWARE\\Mercurial', None,
486 487 _winreg.HKEY_LOCAL_MACHINE)
487 488 if not isinstance(value, str) or not value:
488 489 return rcpath
489 490 value = util.localpath(value)
490 491 for p in value.split(os.pathsep):
491 492 if p.lower().endswith('mercurial.ini'):
492 493 rcpath.append(p)
493 494 elif os.path.isdir(p):
494 495 for f, kind in osutil.listdir(p):
495 496 if f.endswith('.rc'):
496 497 rcpath.append(os.path.join(p, f))
497 498 return rcpath
498 499
499 500 def userrcpath():
500 501 '''return os-specific hgrc search path to the user dir'''
501 502 home = os.path.expanduser('~')
502 503 path = [os.path.join(home, 'mercurial.ini'),
503 504 os.path.join(home, '.hgrc')]
504 505 userprofile = os.environ.get('USERPROFILE')
505 506 if userprofile:
506 507 path.append(os.path.join(userprofile, 'mercurial.ini'))
507 508 path.append(os.path.join(userprofile, '.hgrc'))
508 509 return path
509 510
510 511 def revsingle(repo, revspec, default='.'):
511 512 if not revspec:
512 513 return repo[default]
513 514
514 515 l = revrange(repo, [revspec])
515 516 if len(l) < 1:
516 517 raise util.Abort(_('empty revision set'))
517 518 return repo[l[-1]]
518 519
519 520 def revpair(repo, revs):
520 521 if not revs:
521 522 return repo.dirstate.p1(), None
522 523
523 524 l = revrange(repo, revs)
524 525
525 526 if len(l) == 0:
526 527 if revs:
527 528 raise util.Abort(_('empty revision range'))
528 529 return repo.dirstate.p1(), None
529 530
530 531 if len(l) == 1 and len(revs) == 1 and _revrangesep not in revs[0]:
531 532 return repo.lookup(l[0]), None
532 533
533 534 return repo.lookup(l[0]), repo.lookup(l[-1])
534 535
535 536 _revrangesep = ':'
536 537
537 538 def revrange(repo, revs):
538 539 """Yield revision as strings from a list of revision specifications."""
539 540
540 541 def revfix(repo, val, defval):
541 542 if not val and val != 0 and defval is not None:
542 543 return defval
543 544 return repo[val].rev()
544 545
545 546 seen, l = set(), []
546 547 for spec in revs:
547 548 if l and not seen:
548 549 seen = set(l)
549 550 # attempt to parse old-style ranges first to deal with
550 551 # things like old-tag which contain query metacharacters
551 552 try:
552 553 if isinstance(spec, int):
553 554 seen.add(spec)
554 555 l.append(spec)
555 556 continue
556 557
557 558 if _revrangesep in spec:
558 559 start, end = spec.split(_revrangesep, 1)
559 560 start = revfix(repo, start, 0)
560 561 end = revfix(repo, end, len(repo) - 1)
561 562 step = start > end and -1 or 1
562 563 if not seen and not l:
563 564 # by far the most common case: revs = ["-1:0"]
564 565 l = range(start, end + step, step)
565 566 # defer syncing seen until next iteration
566 567 continue
567 568 newrevs = set(xrange(start, end + step, step))
568 569 if seen:
569 570 newrevs.difference_update(seen)
570 571 seen.update(newrevs)
571 572 else:
572 573 seen = newrevs
573 574 l.extend(sorted(newrevs, reverse=start > end))
574 575 continue
575 576 elif spec and spec in repo: # single unquoted rev
576 577 rev = revfix(repo, spec, None)
577 578 if rev in seen:
578 579 continue
579 580 seen.add(rev)
580 581 l.append(rev)
581 582 continue
582 583 except error.RepoLookupError:
583 584 pass
584 585
585 586 # fall through to new-style queries if old-style fails
586 587 m = revset.match(repo.ui, spec)
587 588 dl = [r for r in m(repo, xrange(len(repo))) if r not in seen]
588 589 l.extend(dl)
589 590 seen.update(dl)
590 591
591 592 return l
592 593
593 594 def expandpats(pats):
594 595 if not util.expandglobs:
595 596 return list(pats)
596 597 ret = []
597 598 for p in pats:
598 599 kind, name = matchmod._patsplit(p, None)
599 600 if kind is None:
600 601 try:
601 602 globbed = glob.glob(name)
602 603 except re.error:
603 604 globbed = [name]
604 605 if globbed:
605 606 ret.extend(globbed)
606 607 continue
607 608 ret.append(p)
608 609 return ret
609 610
610 611 def matchandpats(ctx, pats=[], opts={}, globbed=False, default='relpath'):
611 612 if pats == ("",):
612 613 pats = []
613 614 if not globbed and default == 'relpath':
614 615 pats = expandpats(pats or [])
615 616
616 617 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
617 618 default)
618 619 def badfn(f, msg):
619 620 ctx._repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
620 621 m.bad = badfn
621 622 return m, pats
622 623
623 624 def match(ctx, pats=[], opts={}, globbed=False, default='relpath'):
624 625 return matchandpats(ctx, pats, opts, globbed, default)[0]
625 626
626 627 def matchall(repo):
627 628 return matchmod.always(repo.root, repo.getcwd())
628 629
629 630 def matchfiles(repo, files):
630 631 return matchmod.exact(repo.root, repo.getcwd(), files)
631 632
632 633 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
633 634 if dry_run is None:
634 635 dry_run = opts.get('dry_run')
635 636 if similarity is None:
636 637 similarity = float(opts.get('similarity') or 0)
637 638 # we'd use status here, except handling of symlinks and ignore is tricky
638 639 added, unknown, deleted, removed = [], [], [], []
639 640 audit_path = pathauditor(repo.root)
640 641 m = match(repo[None], pats, opts)
641 642 rejected = []
642 643 m.bad = lambda x, y: rejected.append(x)
643 644
644 645 for abs in repo.walk(m):
645 646 target = repo.wjoin(abs)
646 647 good = True
647 648 try:
648 649 audit_path(abs)
649 650 except (OSError, util.Abort):
650 651 good = False
651 652 rel = m.rel(abs)
652 653 exact = m.exact(abs)
653 654 if good and abs not in repo.dirstate:
654 655 unknown.append(abs)
655 656 if repo.ui.verbose or not exact:
656 657 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
657 658 elif (repo.dirstate[abs] != 'r' and
658 659 (not good or not os.path.lexists(target) or
659 660 (os.path.isdir(target) and not os.path.islink(target)))):
660 661 deleted.append(abs)
661 662 if repo.ui.verbose or not exact:
662 663 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
663 664 # for finding renames
664 665 elif repo.dirstate[abs] == 'r':
665 666 removed.append(abs)
666 667 elif repo.dirstate[abs] == 'a':
667 668 added.append(abs)
668 669 copies = {}
669 670 if similarity > 0:
670 671 for old, new, score in similar.findrenames(repo,
671 672 added + unknown, removed + deleted, similarity):
672 673 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
673 674 repo.ui.status(_('recording removal of %s as rename to %s '
674 675 '(%d%% similar)\n') %
675 676 (m.rel(old), m.rel(new), score * 100))
676 677 copies[new] = old
677 678
678 679 if not dry_run:
679 680 wctx = repo[None]
680 681 wlock = repo.wlock()
681 682 try:
682 683 wctx.forget(deleted)
683 684 wctx.add(unknown)
684 685 for new, old in copies.iteritems():
685 686 wctx.copy(old, new)
686 687 finally:
687 688 wlock.release()
688 689
689 690 for f in rejected:
690 691 if f in m.files():
691 692 return 1
692 693 return 0
693 694
694 695 def updatedir(ui, repo, patches, similarity=0):
695 696 '''Update dirstate after patch application according to metadata'''
696 697 if not patches:
697 698 return []
698 699 copies = []
699 700 removes = set()
700 701 cfiles = patches.keys()
701 702 cwd = repo.getcwd()
702 703 if cwd:
703 704 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
704 705 for f in patches:
705 706 gp = patches[f]
706 707 if not gp:
707 708 continue
708 709 if gp.op == 'RENAME':
709 710 copies.append((gp.oldpath, gp.path))
710 711 removes.add(gp.oldpath)
711 712 elif gp.op == 'COPY':
712 713 copies.append((gp.oldpath, gp.path))
713 714 elif gp.op == 'DELETE':
714 715 removes.add(gp.path)
715 716
716 717 wctx = repo[None]
717 718 for src, dst in copies:
718 719 dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
719 720 if (not similarity) and removes:
720 721 wctx.remove(sorted(removes), True)
721 722
722 723 for f in patches:
723 724 gp = patches[f]
724 725 if gp and gp.mode:
725 726 islink, isexec = gp.mode
726 727 dst = repo.wjoin(gp.path)
727 728 # patch won't create empty files
728 729 if gp.op == 'ADD' and not os.path.lexists(dst):
729 730 flags = (isexec and 'x' or '') + (islink and 'l' or '')
730 731 repo.wwrite(gp.path, '', flags)
731 732 util.setflags(dst, islink, isexec)
732 733 addremove(repo, cfiles, similarity=similarity)
733 734 files = patches.keys()
734 735 files.extend([r for r in removes if r not in files])
735 736 return sorted(files)
736 737
737 738 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
738 739 """Update the dirstate to reflect the intent of copying src to dst. For
739 740 different reasons it might not end with dst being marked as copied from src.
740 741 """
741 742 origsrc = repo.dirstate.copied(src) or src
742 743 if dst == origsrc: # copying back a copy?
743 744 if repo.dirstate[dst] not in 'mn' and not dryrun:
744 745 repo.dirstate.normallookup(dst)
745 746 else:
746 747 if repo.dirstate[origsrc] == 'a' and origsrc == src:
747 748 if not ui.quiet:
748 749 ui.warn(_("%s has not been committed yet, so no copy "
749 750 "data will be stored for %s.\n")
750 751 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
751 752 if repo.dirstate[dst] in '?r' and not dryrun:
752 753 wctx.add([dst])
753 754 elif not dryrun:
754 755 wctx.copy(origsrc, dst)
755 756
756 757 def readrequires(opener, supported):
757 758 '''Reads and parses .hg/requires and checks if all entries found
758 759 are in the list of supported features.'''
759 760 requirements = set(opener.read("requires").splitlines())
760 761 missings = []
761 762 for r in requirements:
762 763 if r not in supported:
763 764 if not r or not r[0].isalnum():
764 765 raise error.RequirementError(_(".hg/requires file is corrupt"))
765 766 missings.append(r)
766 767 missings.sort()
767 768 if missings:
768 769 raise error.RequirementError(
769 770 _("unknown repository format: requires features '%s' (upgrade "
770 771 "Mercurial)") % "', '".join(missings))
771 772 return requirements
772 773
773 774 class filecacheentry(object):
774 775 def __init__(self, path):
775 776 self.path = path
776 777 self.cachestat = filecacheentry.stat(self.path)
777 778
778 779 if self.cachestat:
779 780 self._cacheable = self.cachestat.cacheable()
780 781 else:
781 782 # None means we don't know yet
782 783 self._cacheable = None
783 784
784 785 def refresh(self):
785 786 if self.cacheable():
786 787 self.cachestat = filecacheentry.stat(self.path)
787 788
788 789 def cacheable(self):
789 790 if self._cacheable is not None:
790 791 return self._cacheable
791 792
792 793 # we don't know yet, assume it is for now
793 794 return True
794 795
795 796 def changed(self):
796 797 # no point in going further if we can't cache it
797 798 if not self.cacheable():
798 799 return True
799 800
800 801 newstat = filecacheentry.stat(self.path)
801 802
802 803 # we may not know if it's cacheable yet, check again now
803 804 if newstat and self._cacheable is None:
804 805 self._cacheable = newstat.cacheable()
805 806
806 807 # check again
807 808 if not self._cacheable:
808 809 return True
809 810
810 811 if self.cachestat != newstat:
811 812 self.cachestat = newstat
812 813 return True
813 814 else:
814 815 return False
815 816
816 817 @staticmethod
817 818 def stat(path):
818 819 try:
819 820 return util.cachestat(path)
820 821 except OSError, e:
821 822 if e.errno != errno.ENOENT:
822 823 raise
823 824
824 825 class filecache(object):
825 826 '''A property like decorator that tracks a file under .hg/ for updates.
826 827
827 828 Records stat info when called in _filecache.
828 829
829 830 On subsequent calls, compares old stat info with new info, and recreates
830 831 the object when needed, updating the new stat info in _filecache.
831 832
832 833 Mercurial either atomic renames or appends for files under .hg,
833 834 so to ensure the cache is reliable we need the filesystem to be able
834 835 to tell us if a file has been replaced. If it can't, we fallback to
835 836 recreating the object on every call (essentially the same behaviour as
836 837 propertycache).'''
837 838 def __init__(self, path):
838 839 self.path = path
839 840
840 841 def join(self, obj, fname):
841 842 """Used to compute the runtime path of the cached file.
842 843
843 844 Users should subclass filecache and provide their own version of this
844 845 function to call the appropriate join function on 'obj' (an instance
845 846 of the class that its member function was decorated).
846 847 """
847 848 return obj.join(fname)
848 849
849 850 def __call__(self, func):
850 851 self.func = func
851 852 self.name = func.__name__
852 853 return self
853 854
854 855 def __get__(self, obj, type=None):
855 856 # do we need to check if the file changed?
856 857 if self.name in obj.__dict__:
857 858 return obj.__dict__[self.name]
858 859
859 860 entry = obj._filecache.get(self.name)
860 861
861 862 if entry:
862 863 if entry.changed():
863 864 entry.obj = self.func(obj)
864 865 else:
865 866 path = self.join(obj, self.path)
866 867
867 868 # We stat -before- creating the object so our cache doesn't lie if
868 869 # a writer modified between the time we read and stat
869 870 entry = filecacheentry(path)
870 871 entry.obj = self.func(obj)
871 872
872 873 obj._filecache[self.name] = entry
873 874
874 875 obj.__dict__[self.name] = entry.obj
875 876 return entry.obj
876 877
877 878 def __set__(self, obj, value):
878 879 if self.name in obj._filecache:
879 880 obj._filecache[self.name].obj = value # update cached copy
880 881 obj.__dict__[self.name] = value # update copy returned by obj.x
881 882
882 883 def __delete__(self, obj):
883 884 try:
884 885 del obj.__dict__[self.name]
885 886 except KeyError:
886 887 raise AttributeError, self.name
General Comments 0
You need to be logged in to leave comments. Login now