##// END OF EJS Templates
hg: reimplement branch listings more efficiently...
Mads Kiilerich -
r4728:1cd9bdf1 default
parent child Browse files
Show More
@@ -1,624 +1,606 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.hg.repository
3 vcs.backends.hg.repository
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Mercurial repository implementation.
6 Mercurial repository implementation.
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import os
12 import os
13 import time
13 import time
14 import urllib
14 import urllib
15 import urllib2
15 import urllib2
16 import logging
16 import logging
17 import datetime
17 import datetime
18
18
19 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
19 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
20
20
21 from kallithea.lib.vcs.exceptions import (
21 from kallithea.lib.vcs.exceptions import (
22 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
22 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
23 RepositoryError, VCSError, TagAlreadyExistError, TagDoesNotExistError
23 RepositoryError, VCSError, TagAlreadyExistError, TagDoesNotExistError
24 )
24 )
25 from kallithea.lib.vcs.utils import (
25 from kallithea.lib.vcs.utils import (
26 author_email, author_name, date_fromtimestamp, makedate, safe_unicode, safe_str,
26 author_email, author_name, date_fromtimestamp, makedate, safe_unicode, safe_str,
27 )
27 )
28 from kallithea.lib.vcs.utils.lazy import LazyProperty
28 from kallithea.lib.vcs.utils.lazy import LazyProperty
29 from kallithea.lib.vcs.utils.ordered_dict import OrderedDict
29 from kallithea.lib.vcs.utils.ordered_dict import OrderedDict
30 from kallithea.lib.vcs.utils.paths import abspath
30 from kallithea.lib.vcs.utils.paths import abspath
31 from kallithea.lib.vcs.utils.hgcompat import (
31 from kallithea.lib.vcs.utils.hgcompat import (
32 ui, nullid, match, patch, diffopts, clone, get_contact, pull,
32 ui, nullid, match, patch, diffopts, clone, get_contact, pull,
33 localrepository, RepoLookupError, Abort, RepoError, hex, scmutil, hg_url,
33 localrepository, RepoLookupError, Abort, RepoError, hex, scmutil, hg_url,
34 httpbasicauthhandler, httpdigestauthhandler, peer, httppeer
34 httpbasicauthhandler, httpdigestauthhandler, peer, httppeer
35 )
35 )
36
36
37 from .changeset import MercurialChangeset
37 from .changeset import MercurialChangeset
38 from .inmemory import MercurialInMemoryChangeset
38 from .inmemory import MercurialInMemoryChangeset
39 from .workdir import MercurialWorkdir
39 from .workdir import MercurialWorkdir
40
40
41 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
42
42
43
43
44 class MercurialRepository(BaseRepository):
44 class MercurialRepository(BaseRepository):
45 """
45 """
46 Mercurial repository backend
46 Mercurial repository backend
47 """
47 """
48 DEFAULT_BRANCH_NAME = 'default'
48 DEFAULT_BRANCH_NAME = 'default'
49 scm = 'hg'
49 scm = 'hg'
50
50
51 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
51 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
52 update_after_clone=False):
52 update_after_clone=False):
53 """
53 """
54 Raises RepositoryError if repository could not be find at the given
54 Raises RepositoryError if repository could not be find at the given
55 ``repo_path``.
55 ``repo_path``.
56
56
57 :param repo_path: local path of the repository
57 :param repo_path: local path of the repository
58 :param create=False: if set to True, would try to create repository if
58 :param create=False: if set to True, would try to create repository if
59 it does not exist rather than raising exception
59 it does not exist rather than raising exception
60 :param baseui=None: user data
60 :param baseui=None: user data
61 :param src_url=None: would try to clone repository from given location
61 :param src_url=None: would try to clone repository from given location
62 :param update_after_clone=False: sets update of working copy after
62 :param update_after_clone=False: sets update of working copy after
63 making a clone
63 making a clone
64 """
64 """
65
65
66 if not isinstance(repo_path, str):
66 if not isinstance(repo_path, str):
67 raise VCSError('Mercurial backend requires repository path to '
67 raise VCSError('Mercurial backend requires repository path to '
68 'be instance of <str> got %s instead' %
68 'be instance of <str> got %s instead' %
69 type(repo_path))
69 type(repo_path))
70
70
71 self.path = abspath(repo_path)
71 self.path = abspath(repo_path)
72 self.baseui = baseui or ui.ui()
72 self.baseui = baseui or ui.ui()
73 # We've set path and ui, now we can set _repo itself
73 # We've set path and ui, now we can set _repo itself
74 self._repo = self._get_repo(create, src_url, update_after_clone)
74 self._repo = self._get_repo(create, src_url, update_after_clone)
75
75
76 @property
76 @property
77 def _empty(self):
77 def _empty(self):
78 """
78 """
79 Checks if repository is empty ie. without any changesets
79 Checks if repository is empty ie. without any changesets
80 """
80 """
81 # TODO: Following raises errors when using InMemoryChangeset...
81 # TODO: Following raises errors when using InMemoryChangeset...
82 # return len(self._repo.changelog) == 0
82 # return len(self._repo.changelog) == 0
83 return len(self.revisions) == 0
83 return len(self.revisions) == 0
84
84
85 @LazyProperty
85 @LazyProperty
86 def revisions(self):
86 def revisions(self):
87 """
87 """
88 Returns list of revisions' ids, in ascending order. Being lazy
88 Returns list of revisions' ids, in ascending order. Being lazy
89 attribute allows external tools to inject shas from cache.
89 attribute allows external tools to inject shas from cache.
90 """
90 """
91 return self._get_all_revisions()
91 return self._get_all_revisions()
92
92
93 @LazyProperty
93 @LazyProperty
94 def name(self):
94 def name(self):
95 return os.path.basename(self.path)
95 return os.path.basename(self.path)
96
96
97 @LazyProperty
97 @LazyProperty
98 def branches(self):
98 def branches(self):
99 return self._get_branches()
99 return self._get_branches()
100
100
101 @LazyProperty
101 @LazyProperty
102 def closed_branches(self):
102 def closed_branches(self):
103 return self._get_branches(normal=False, closed=True)
103 return self._get_branches(normal=False, closed=True)
104
104
105 @LazyProperty
105 @LazyProperty
106 def allbranches(self):
106 def allbranches(self):
107 """
107 """
108 List all branches, including closed branches.
108 List all branches, including closed branches.
109 """
109 """
110 return self._get_branches(closed=True)
110 return self._get_branches(closed=True)
111
111
112 def _get_branches(self, normal=True, closed=False):
112 def _get_branches(self, normal=True, closed=False):
113 """
113 """
114 Gets branches for this repository
114 Gets branches for this repository
115 Returns only not closed branches by default
115 Returns only not closed branches by default
116
116
117 :param closed: return also closed branches for mercurial
117 :param closed: return also closed branches for mercurial
118 :param normal: return also normal branches
118 :param normal: return also normal branches
119 """
119 """
120
120
121 if self._empty:
121 if self._empty:
122 return {}
122 return {}
123
123
124 def _branchtags(localrepo):
124 bt = OrderedDict()
125 """
125 for bn, _heads, tip, isclosed in sorted(self._repo.branchmap().iterbranches()):
126 Patched version of mercurial branchtags to not return the closed
126 if isclosed:
127 branches
127 if closed:
128
128 bt[safe_unicode(bn)] = hex(tip)
129 :param localrepo: locarepository instance
129 else:
130 """
130 if normal:
131 bt[safe_unicode(bn)] = hex(tip)
131
132
132 bt = {}
133 return bt
133 bt_closed = {}
134 for bn, heads in localrepo.branchmap().iteritems():
135 tip = heads[-1]
136 if 'close' in localrepo.changelog.read(tip)[5]:
137 bt_closed[bn] = tip
138 else:
139 bt[bn] = tip
140
141 if not normal:
142 return bt_closed
143 if closed:
144 bt.update(bt_closed)
145 return bt
146
147 sortkey = lambda ctx: ctx[0] # sort by name
148 _branches = [(safe_unicode(n), hex(h),) for n, h in
149 _branchtags(self._repo).items()]
150
151 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
152
134
153 @LazyProperty
135 @LazyProperty
154 def tags(self):
136 def tags(self):
155 """
137 """
156 Gets tags for this repository
138 Gets tags for this repository
157 """
139 """
158 return self._get_tags()
140 return self._get_tags()
159
141
160 def _get_tags(self):
142 def _get_tags(self):
161 if self._empty:
143 if self._empty:
162 return {}
144 return {}
163
145
164 sortkey = lambda ctx: ctx[0] # sort by name
146 sortkey = lambda ctx: ctx[0] # sort by name
165 _tags = [(safe_unicode(n), hex(h),) for n, h in
147 _tags = [(safe_unicode(n), hex(h),) for n, h in
166 self._repo.tags().items()]
148 self._repo.tags().items()]
167
149
168 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
150 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
169
151
170 def tag(self, name, user, revision=None, message=None, date=None,
152 def tag(self, name, user, revision=None, message=None, date=None,
171 **kwargs):
153 **kwargs):
172 """
154 """
173 Creates and returns a tag for the given ``revision``.
155 Creates and returns a tag for the given ``revision``.
174
156
175 :param name: name for new tag
157 :param name: name for new tag
176 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
158 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
177 :param revision: changeset id for which new tag would be created
159 :param revision: changeset id for which new tag would be created
178 :param message: message of the tag's commit
160 :param message: message of the tag's commit
179 :param date: date of tag's commit
161 :param date: date of tag's commit
180
162
181 :raises TagAlreadyExistError: if tag with same name already exists
163 :raises TagAlreadyExistError: if tag with same name already exists
182 """
164 """
183 if name in self.tags:
165 if name in self.tags:
184 raise TagAlreadyExistError("Tag %s already exists" % name)
166 raise TagAlreadyExistError("Tag %s already exists" % name)
185 changeset = self.get_changeset(revision)
167 changeset = self.get_changeset(revision)
186 local = kwargs.setdefault('local', False)
168 local = kwargs.setdefault('local', False)
187
169
188 if message is None:
170 if message is None:
189 message = "Added tag %s for changeset %s" % (name,
171 message = "Added tag %s for changeset %s" % (name,
190 changeset.short_id)
172 changeset.short_id)
191
173
192 if date is None:
174 if date is None:
193 date = datetime.datetime.now().ctime()
175 date = datetime.datetime.now().ctime()
194
176
195 try:
177 try:
196 self._repo.tag(name, changeset._ctx.node(), message, local, user,
178 self._repo.tag(name, changeset._ctx.node(), message, local, user,
197 date)
179 date)
198 except Abort, e:
180 except Abort, e:
199 raise RepositoryError(e.message)
181 raise RepositoryError(e.message)
200
182
201 # Reinitialize tags
183 # Reinitialize tags
202 self.tags = self._get_tags()
184 self.tags = self._get_tags()
203 tag_id = self.tags[name]
185 tag_id = self.tags[name]
204
186
205 return self.get_changeset(revision=tag_id)
187 return self.get_changeset(revision=tag_id)
206
188
207 def remove_tag(self, name, user, message=None, date=None):
189 def remove_tag(self, name, user, message=None, date=None):
208 """
190 """
209 Removes tag with the given ``name``.
191 Removes tag with the given ``name``.
210
192
211 :param name: name of the tag to be removed
193 :param name: name of the tag to be removed
212 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
194 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
213 :param message: message of the tag's removal commit
195 :param message: message of the tag's removal commit
214 :param date: date of tag's removal commit
196 :param date: date of tag's removal commit
215
197
216 :raises TagDoesNotExistError: if tag with given name does not exists
198 :raises TagDoesNotExistError: if tag with given name does not exists
217 """
199 """
218 if name not in self.tags:
200 if name not in self.tags:
219 raise TagDoesNotExistError("Tag %s does not exist" % name)
201 raise TagDoesNotExistError("Tag %s does not exist" % name)
220 if message is None:
202 if message is None:
221 message = "Removed tag %s" % name
203 message = "Removed tag %s" % name
222 if date is None:
204 if date is None:
223 date = datetime.datetime.now().ctime()
205 date = datetime.datetime.now().ctime()
224 local = False
206 local = False
225
207
226 try:
208 try:
227 self._repo.tag(name, nullid, message, local, user, date)
209 self._repo.tag(name, nullid, message, local, user, date)
228 self.tags = self._get_tags()
210 self.tags = self._get_tags()
229 except Abort, e:
211 except Abort, e:
230 raise RepositoryError(e.message)
212 raise RepositoryError(e.message)
231
213
232 @LazyProperty
214 @LazyProperty
233 def bookmarks(self):
215 def bookmarks(self):
234 """
216 """
235 Gets bookmarks for this repository
217 Gets bookmarks for this repository
236 """
218 """
237 return self._get_bookmarks()
219 return self._get_bookmarks()
238
220
239 def _get_bookmarks(self):
221 def _get_bookmarks(self):
240 if self._empty:
222 if self._empty:
241 return {}
223 return {}
242
224
243 sortkey = lambda ctx: ctx[0] # sort by name
225 sortkey = lambda ctx: ctx[0] # sort by name
244 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
226 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
245 self._repo._bookmarks.items()]
227 self._repo._bookmarks.items()]
246 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
228 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
247
229
248 def _get_all_revisions(self):
230 def _get_all_revisions(self):
249
231
250 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
232 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
251
233
252 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
234 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
253 context=3):
235 context=3):
254 """
236 """
255 Returns (git like) *diff*, as plain text. Shows changes introduced by
237 Returns (git like) *diff*, as plain text. Shows changes introduced by
256 ``rev2`` since ``rev1``.
238 ``rev2`` since ``rev1``.
257
239
258 :param rev1: Entry point from which diff is shown. Can be
240 :param rev1: Entry point from which diff is shown. Can be
259 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
241 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
260 the changes since empty state of the repository until ``rev2``
242 the changes since empty state of the repository until ``rev2``
261 :param rev2: Until which revision changes should be shown.
243 :param rev2: Until which revision changes should be shown.
262 :param ignore_whitespace: If set to ``True``, would not show whitespace
244 :param ignore_whitespace: If set to ``True``, would not show whitespace
263 changes. Defaults to ``False``.
245 changes. Defaults to ``False``.
264 :param context: How many lines before/after changed lines should be
246 :param context: How many lines before/after changed lines should be
265 shown. Defaults to ``3``.
247 shown. Defaults to ``3``.
266 """
248 """
267 if hasattr(rev1, 'raw_id'):
249 if hasattr(rev1, 'raw_id'):
268 rev1 = getattr(rev1, 'raw_id')
250 rev1 = getattr(rev1, 'raw_id')
269
251
270 if hasattr(rev2, 'raw_id'):
252 if hasattr(rev2, 'raw_id'):
271 rev2 = getattr(rev2, 'raw_id')
253 rev2 = getattr(rev2, 'raw_id')
272
254
273 # Check if given revisions are present at repository (may raise
255 # Check if given revisions are present at repository (may raise
274 # ChangesetDoesNotExistError)
256 # ChangesetDoesNotExistError)
275 if rev1 != self.EMPTY_CHANGESET:
257 if rev1 != self.EMPTY_CHANGESET:
276 self.get_changeset(rev1)
258 self.get_changeset(rev1)
277 self.get_changeset(rev2)
259 self.get_changeset(rev2)
278 if path:
260 if path:
279 file_filter = match(self.path, '', [path])
261 file_filter = match(self.path, '', [path])
280 else:
262 else:
281 file_filter = None
263 file_filter = None
282
264
283 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
265 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
284 opts=diffopts(git=True,
266 opts=diffopts(git=True,
285 ignorews=ignore_whitespace,
267 ignorews=ignore_whitespace,
286 context=context)))
268 context=context)))
287
269
288 @classmethod
270 @classmethod
289 def _check_url(cls, url, repoui=None):
271 def _check_url(cls, url, repoui=None):
290 """
272 """
291 Function will check given url and try to verify if it's a valid
273 Function will check given url and try to verify if it's a valid
292 link. Sometimes it may happened that mercurial will issue basic
274 link. Sometimes it may happened that mercurial will issue basic
293 auth request that can cause whole API to hang when used from python
275 auth request that can cause whole API to hang when used from python
294 or other external calls.
276 or other external calls.
295
277
296 On failures it'll raise urllib2.HTTPError, exception is also thrown
278 On failures it'll raise urllib2.HTTPError, exception is also thrown
297 when the return code is non 200
279 when the return code is non 200
298 """
280 """
299 # check first if it's not an local url
281 # check first if it's not an local url
300 if os.path.isdir(url) or url.startswith('file:'):
282 if os.path.isdir(url) or url.startswith('file:'):
301 return True
283 return True
302
284
303 if '+' in url[:url.find('://')]:
285 if '+' in url[:url.find('://')]:
304 url = url[url.find('+') + 1:]
286 url = url[url.find('+') + 1:]
305
287
306 handlers = []
288 handlers = []
307 url_obj = hg_url(url)
289 url_obj = hg_url(url)
308 test_uri, authinfo = url_obj.authinfo()
290 test_uri, authinfo = url_obj.authinfo()
309 url_obj.passwd = '*****'
291 url_obj.passwd = '*****'
310 cleaned_uri = str(url_obj)
292 cleaned_uri = str(url_obj)
311
293
312 if authinfo:
294 if authinfo:
313 #create a password manager
295 #create a password manager
314 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
296 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
315 passmgr.add_password(*authinfo)
297 passmgr.add_password(*authinfo)
316
298
317 handlers.extend((httpbasicauthhandler(passmgr),
299 handlers.extend((httpbasicauthhandler(passmgr),
318 httpdigestauthhandler(passmgr)))
300 httpdigestauthhandler(passmgr)))
319
301
320 o = urllib2.build_opener(*handlers)
302 o = urllib2.build_opener(*handlers)
321 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
303 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
322 ('Accept', 'application/mercurial-0.1')]
304 ('Accept', 'application/mercurial-0.1')]
323
305
324 q = {"cmd": 'between'}
306 q = {"cmd": 'between'}
325 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
307 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
326 qs = '?%s' % urllib.urlencode(q)
308 qs = '?%s' % urllib.urlencode(q)
327 cu = "%s%s" % (test_uri, qs)
309 cu = "%s%s" % (test_uri, qs)
328 req = urllib2.Request(cu, None, {})
310 req = urllib2.Request(cu, None, {})
329
311
330 try:
312 try:
331 resp = o.open(req)
313 resp = o.open(req)
332 if resp.code != 200:
314 if resp.code != 200:
333 raise Exception('Return Code is not 200')
315 raise Exception('Return Code is not 200')
334 except Exception, e:
316 except Exception, e:
335 # means it cannot be cloned
317 # means it cannot be cloned
336 raise urllib2.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
318 raise urllib2.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
337
319
338 # now check if it's a proper hg repo
320 # now check if it's a proper hg repo
339 try:
321 try:
340 httppeer(repoui or ui.ui(), url).lookup('tip')
322 httppeer(repoui or ui.ui(), url).lookup('tip')
341 except Exception, e:
323 except Exception, e:
342 raise urllib2.URLError(
324 raise urllib2.URLError(
343 "url [%s] does not look like an hg repo org_exc: %s"
325 "url [%s] does not look like an hg repo org_exc: %s"
344 % (cleaned_uri, e))
326 % (cleaned_uri, e))
345
327
346 return True
328 return True
347
329
348 def _get_repo(self, create, src_url=None, update_after_clone=False):
330 def _get_repo(self, create, src_url=None, update_after_clone=False):
349 """
331 """
350 Function will check for mercurial repository in given path and return
332 Function will check for mercurial repository in given path and return
351 a localrepo object. If there is no repository in that path it will
333 a localrepo object. If there is no repository in that path it will
352 raise an exception unless ``create`` parameter is set to True - in
334 raise an exception unless ``create`` parameter is set to True - in
353 that case repository would be created and returned.
335 that case repository would be created and returned.
354 If ``src_url`` is given, would try to clone repository from the
336 If ``src_url`` is given, would try to clone repository from the
355 location at given clone_point. Additionally it'll make update to
337 location at given clone_point. Additionally it'll make update to
356 working copy accordingly to ``update_after_clone`` flag
338 working copy accordingly to ``update_after_clone`` flag
357 """
339 """
358
340
359 try:
341 try:
360 if src_url:
342 if src_url:
361 url = str(self._get_url(src_url))
343 url = str(self._get_url(src_url))
362 opts = {}
344 opts = {}
363 if not update_after_clone:
345 if not update_after_clone:
364 opts.update({'noupdate': True})
346 opts.update({'noupdate': True})
365 try:
347 try:
366 MercurialRepository._check_url(url, self.baseui)
348 MercurialRepository._check_url(url, self.baseui)
367 clone(self.baseui, url, self.path, **opts)
349 clone(self.baseui, url, self.path, **opts)
368 # except urllib2.URLError:
350 # except urllib2.URLError:
369 # raise Abort("Got HTTP 404 error")
351 # raise Abort("Got HTTP 404 error")
370 except Exception:
352 except Exception:
371 raise
353 raise
372
354
373 # Don't try to create if we've already cloned repo
355 # Don't try to create if we've already cloned repo
374 create = False
356 create = False
375 return localrepository(self.baseui, self.path, create=create)
357 return localrepository(self.baseui, self.path, create=create)
376 except (Abort, RepoError), err:
358 except (Abort, RepoError), err:
377 if create:
359 if create:
378 msg = "Cannot create repository at %s. Original error was %s"\
360 msg = "Cannot create repository at %s. Original error was %s"\
379 % (self.path, err)
361 % (self.path, err)
380 else:
362 else:
381 msg = "Not valid repository at %s. Original error was %s"\
363 msg = "Not valid repository at %s. Original error was %s"\
382 % (self.path, err)
364 % (self.path, err)
383 raise RepositoryError(msg)
365 raise RepositoryError(msg)
384
366
385 @LazyProperty
367 @LazyProperty
386 def in_memory_changeset(self):
368 def in_memory_changeset(self):
387 return MercurialInMemoryChangeset(self)
369 return MercurialInMemoryChangeset(self)
388
370
389 @LazyProperty
371 @LazyProperty
390 def description(self):
372 def description(self):
391 undefined_description = u'unknown'
373 undefined_description = u'unknown'
392 _desc = self._repo.ui.config('web', 'description', None, untrusted=True)
374 _desc = self._repo.ui.config('web', 'description', None, untrusted=True)
393 return safe_unicode(_desc or undefined_description)
375 return safe_unicode(_desc or undefined_description)
394
376
395 @LazyProperty
377 @LazyProperty
396 def contact(self):
378 def contact(self):
397 undefined_contact = u'Unknown'
379 undefined_contact = u'Unknown'
398 return safe_unicode(get_contact(self._repo.ui.config)
380 return safe_unicode(get_contact(self._repo.ui.config)
399 or undefined_contact)
381 or undefined_contact)
400
382
401 @LazyProperty
383 @LazyProperty
402 def last_change(self):
384 def last_change(self):
403 """
385 """
404 Returns last change made on this repository as datetime object
386 Returns last change made on this repository as datetime object
405 """
387 """
406 return date_fromtimestamp(self._get_mtime(), makedate()[1])
388 return date_fromtimestamp(self._get_mtime(), makedate()[1])
407
389
408 def _get_mtime(self):
390 def _get_mtime(self):
409 try:
391 try:
410 return time.mktime(self.get_changeset().date.timetuple())
392 return time.mktime(self.get_changeset().date.timetuple())
411 except RepositoryError:
393 except RepositoryError:
412 #fallback to filesystem
394 #fallback to filesystem
413 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
395 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
414 st_path = os.path.join(self.path, '.hg', "store")
396 st_path = os.path.join(self.path, '.hg', "store")
415 if os.path.exists(cl_path):
397 if os.path.exists(cl_path):
416 return os.stat(cl_path).st_mtime
398 return os.stat(cl_path).st_mtime
417 else:
399 else:
418 return os.stat(st_path).st_mtime
400 return os.stat(st_path).st_mtime
419
401
420 def _get_revision(self, revision):
402 def _get_revision(self, revision):
421 """
403 """
422 Gets an ID revision given as str. This will always return a fill
404 Gets an ID revision given as str. This will always return a fill
423 40 char revision number
405 40 char revision number
424
406
425 :param revision: str or int or None
407 :param revision: str or int or None
426 """
408 """
427 if isinstance(revision, unicode):
409 if isinstance(revision, unicode):
428 revision = safe_str(revision)
410 revision = safe_str(revision)
429
411
430 if self._empty:
412 if self._empty:
431 raise EmptyRepositoryError("There are no changesets yet")
413 raise EmptyRepositoryError("There are no changesets yet")
432
414
433 if revision in [-1, 'tip', None]:
415 if revision in [-1, 'tip', None]:
434 revision = 'tip'
416 revision = 'tip'
435
417
436 try:
418 try:
437 revision = hex(self._repo.lookup(revision))
419 revision = hex(self._repo.lookup(revision))
438 except (LookupError, ):
420 except (LookupError, ):
439 msg = ("Ambiguous identifier `%s` for %s" % (revision, self))
421 msg = ("Ambiguous identifier `%s` for %s" % (revision, self))
440 raise ChangesetDoesNotExistError(msg)
422 raise ChangesetDoesNotExistError(msg)
441 except (IndexError, ValueError, RepoLookupError, TypeError):
423 except (IndexError, ValueError, RepoLookupError, TypeError):
442 msg = ("Revision %s does not exist for %s" % (revision, self))
424 msg = ("Revision %s does not exist for %s" % (revision, self))
443 raise ChangesetDoesNotExistError(msg)
425 raise ChangesetDoesNotExistError(msg)
444
426
445 return revision
427 return revision
446
428
447 def get_ref_revision(self, ref_type, ref_name):
429 def get_ref_revision(self, ref_type, ref_name):
448 """
430 """
449 Returns revision number for the given reference.
431 Returns revision number for the given reference.
450 """
432 """
451 ref_name = safe_str(ref_name)
433 ref_name = safe_str(ref_name)
452 if ref_type == 'rev' and not ref_name.strip('0'):
434 if ref_type == 'rev' and not ref_name.strip('0'):
453 return self.EMPTY_CHANGESET
435 return self.EMPTY_CHANGESET
454 # lookup up the exact node id
436 # lookup up the exact node id
455 _revset_predicates = {
437 _revset_predicates = {
456 'branch': 'branch',
438 'branch': 'branch',
457 'book': 'bookmark',
439 'book': 'bookmark',
458 'tag': 'tag',
440 'tag': 'tag',
459 'rev': 'id',
441 'rev': 'id',
460 }
442 }
461 # avoid expensive branch(x) iteration over whole repo
443 # avoid expensive branch(x) iteration over whole repo
462 rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type]
444 rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type]
463 try:
445 try:
464 revs = self._repo.revs(rev_spec, ref_name, ref_name)
446 revs = self._repo.revs(rev_spec, ref_name, ref_name)
465 except LookupError:
447 except LookupError:
466 msg = ("Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name))
448 msg = ("Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name))
467 raise ChangesetDoesNotExistError(msg)
449 raise ChangesetDoesNotExistError(msg)
468 except RepoLookupError:
450 except RepoLookupError:
469 msg = ("Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name))
451 msg = ("Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name))
470 raise ChangesetDoesNotExistError(msg)
452 raise ChangesetDoesNotExistError(msg)
471 if revs:
453 if revs:
472 revision = revs[-1]
454 revision = revs[-1]
473 else:
455 else:
474 # TODO: just report 'not found'?
456 # TODO: just report 'not found'?
475 revision = ref_name
457 revision = ref_name
476
458
477 return self._get_revision(revision)
459 return self._get_revision(revision)
478
460
479 def _get_archives(self, archive_name='tip'):
461 def _get_archives(self, archive_name='tip'):
480 allowed = self.baseui.configlist("web", "allow_archive",
462 allowed = self.baseui.configlist("web", "allow_archive",
481 untrusted=True)
463 untrusted=True)
482 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
464 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
483 if i[0] in allowed or self._repo.ui.configbool("web",
465 if i[0] in allowed or self._repo.ui.configbool("web",
484 "allow" + i[0],
466 "allow" + i[0],
485 untrusted=True):
467 untrusted=True):
486 yield {"type": i[0], "extension": i[1], "node": archive_name}
468 yield {"type": i[0], "extension": i[1], "node": archive_name}
487
469
488 def _get_url(self, url):
470 def _get_url(self, url):
489 """
471 """
490 Returns normalized url. If schema is not given, would fall
472 Returns normalized url. If schema is not given, would fall
491 to filesystem
473 to filesystem
492 (``file:///``) schema.
474 (``file:///``) schema.
493 """
475 """
494 url = str(url)
476 url = str(url)
495 if url != 'default' and not '://' in url:
477 if url != 'default' and not '://' in url:
496 url = "file:" + urllib.pathname2url(url)
478 url = "file:" + urllib.pathname2url(url)
497 return url
479 return url
498
480
499 def get_hook_location(self):
481 def get_hook_location(self):
500 """
482 """
501 returns absolute path to location where hooks are stored
483 returns absolute path to location where hooks are stored
502 """
484 """
503 return os.path.join(self.path, '.hg', '.hgrc')
485 return os.path.join(self.path, '.hg', '.hgrc')
504
486
505 def get_changeset(self, revision=None):
487 def get_changeset(self, revision=None):
506 """
488 """
507 Returns ``MercurialChangeset`` object representing repository's
489 Returns ``MercurialChangeset`` object representing repository's
508 changeset at the given ``revision``.
490 changeset at the given ``revision``.
509 """
491 """
510 revision = self._get_revision(revision)
492 revision = self._get_revision(revision)
511 changeset = MercurialChangeset(repository=self, revision=revision)
493 changeset = MercurialChangeset(repository=self, revision=revision)
512 return changeset
494 return changeset
513
495
514 def get_changesets(self, start=None, end=None, start_date=None,
496 def get_changesets(self, start=None, end=None, start_date=None,
515 end_date=None, branch_name=None, reverse=False):
497 end_date=None, branch_name=None, reverse=False):
516 """
498 """
517 Returns iterator of ``MercurialChangeset`` objects from start to end
499 Returns iterator of ``MercurialChangeset`` objects from start to end
518 (both are inclusive)
500 (both are inclusive)
519
501
520 :param start: None, str, int or mercurial lookup format
502 :param start: None, str, int or mercurial lookup format
521 :param end: None, str, int or mercurial lookup format
503 :param end: None, str, int or mercurial lookup format
522 :param start_date:
504 :param start_date:
523 :param end_date:
505 :param end_date:
524 :param branch_name:
506 :param branch_name:
525 :param reversed: return changesets in reversed order
507 :param reversed: return changesets in reversed order
526 """
508 """
527
509
528 start_raw_id = self._get_revision(start)
510 start_raw_id = self._get_revision(start)
529 start_pos = self.revisions.index(start_raw_id) if start else None
511 start_pos = self.revisions.index(start_raw_id) if start else None
530 end_raw_id = self._get_revision(end)
512 end_raw_id = self._get_revision(end)
531 end_pos = self.revisions.index(end_raw_id) if end else None
513 end_pos = self.revisions.index(end_raw_id) if end else None
532
514
533 if None not in [start, end] and start_pos > end_pos:
515 if None not in [start, end] and start_pos > end_pos:
534 raise RepositoryError("Start revision '%s' cannot be "
516 raise RepositoryError("Start revision '%s' cannot be "
535 "after end revision '%s'" % (start, end))
517 "after end revision '%s'" % (start, end))
536
518
537 if branch_name and branch_name not in self.allbranches.keys():
519 if branch_name and branch_name not in self.allbranches.keys():
538 msg = ("Branch %s not found in %s" % (branch_name, self))
520 msg = ("Branch %s not found in %s" % (branch_name, self))
539 raise BranchDoesNotExistError(msg)
521 raise BranchDoesNotExistError(msg)
540 if end_pos is not None:
522 if end_pos is not None:
541 end_pos += 1
523 end_pos += 1
542 #filter branches
524 #filter branches
543 filter_ = []
525 filter_ = []
544 if branch_name:
526 if branch_name:
545 filter_.append('branch("%s")' % (branch_name))
527 filter_.append('branch("%s")' % (branch_name))
546
528
547 if start_date and not end_date:
529 if start_date and not end_date:
548 filter_.append('date(">%s")' % start_date)
530 filter_.append('date(">%s")' % start_date)
549 if end_date and not start_date:
531 if end_date and not start_date:
550 filter_.append('date("<%s")' % end_date)
532 filter_.append('date("<%s")' % end_date)
551 if start_date and end_date:
533 if start_date and end_date:
552 filter_.append('date(">%s") and date("<%s")' % (start_date, end_date))
534 filter_.append('date(">%s") and date("<%s")' % (start_date, end_date))
553 if filter_:
535 if filter_:
554 revisions = scmutil.revrange(self._repo, filter_)
536 revisions = scmutil.revrange(self._repo, filter_)
555 else:
537 else:
556 revisions = self.revisions
538 revisions = self.revisions
557
539
558 revs = revisions[start_pos:end_pos]
540 revs = revisions[start_pos:end_pos]
559 if reverse:
541 if reverse:
560 revs = reversed(revs)
542 revs = reversed(revs)
561
543
562 return CollectionGenerator(self, revs)
544 return CollectionGenerator(self, revs)
563
545
564 def pull(self, url):
546 def pull(self, url):
565 """
547 """
566 Tries to pull changes from external location.
548 Tries to pull changes from external location.
567 """
549 """
568 url = self._get_url(url)
550 url = self._get_url(url)
569 try:
551 try:
570 other = peer(self._repo, {}, url)
552 other = peer(self._repo, {}, url)
571 self._repo.pull(other, heads=None, force=None)
553 self._repo.pull(other, heads=None, force=None)
572 except Abort, err:
554 except Abort, err:
573 # Propagate error but with vcs's type
555 # Propagate error but with vcs's type
574 raise RepositoryError(str(err))
556 raise RepositoryError(str(err))
575
557
576 @LazyProperty
558 @LazyProperty
577 def workdir(self):
559 def workdir(self):
578 """
560 """
579 Returns ``Workdir`` instance for this repository.
561 Returns ``Workdir`` instance for this repository.
580 """
562 """
581 return MercurialWorkdir(self)
563 return MercurialWorkdir(self)
582
564
583 def get_config_value(self, section, name=None, config_file=None):
565 def get_config_value(self, section, name=None, config_file=None):
584 """
566 """
585 Returns configuration value for a given [``section``] and ``name``.
567 Returns configuration value for a given [``section``] and ``name``.
586
568
587 :param section: Section we want to retrieve value from
569 :param section: Section we want to retrieve value from
588 :param name: Name of configuration we want to retrieve
570 :param name: Name of configuration we want to retrieve
589 :param config_file: A path to file which should be used to retrieve
571 :param config_file: A path to file which should be used to retrieve
590 configuration from (might also be a list of file paths)
572 configuration from (might also be a list of file paths)
591 """
573 """
592 if config_file is None:
574 if config_file is None:
593 config_file = []
575 config_file = []
594 elif isinstance(config_file, basestring):
576 elif isinstance(config_file, basestring):
595 config_file = [config_file]
577 config_file = [config_file]
596
578
597 config = self._repo.ui
579 config = self._repo.ui
598 for path in config_file:
580 for path in config_file:
599 config.readconfig(path)
581 config.readconfig(path)
600 return config.config(section, name)
582 return config.config(section, name)
601
583
602 def get_user_name(self, config_file=None):
584 def get_user_name(self, config_file=None):
603 """
585 """
604 Returns user's name from global configuration file.
586 Returns user's name from global configuration file.
605
587
606 :param config_file: A path to file which should be used to retrieve
588 :param config_file: A path to file which should be used to retrieve
607 configuration from (might also be a list of file paths)
589 configuration from (might also be a list of file paths)
608 """
590 """
609 username = self.get_config_value('ui', 'username')
591 username = self.get_config_value('ui', 'username')
610 if username:
592 if username:
611 return author_name(username)
593 return author_name(username)
612 return None
594 return None
613
595
614 def get_user_email(self, config_file=None):
596 def get_user_email(self, config_file=None):
615 """
597 """
616 Returns user's email from global configuration file.
598 Returns user's email from global configuration file.
617
599
618 :param config_file: A path to file which should be used to retrieve
600 :param config_file: A path to file which should be used to retrieve
619 configuration from (might also be a list of file paths)
601 configuration from (might also be a list of file paths)
620 """
602 """
621 username = self.get_config_value('ui', 'username')
603 username = self.get_config_value('ui', 'username')
622 if username:
604 if username:
623 return author_email(username)
605 return author_email(username)
624 return None
606 return None
General Comments 0
You need to be logged in to leave comments. Login now