##// END OF EJS Templates
Added get_hook_location method for easier hook extraction
marcink -
r3456:cf1fbc9f beta
parent child Browse files
Show More
@@ -1,687 +1,696 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.git
3 vcs.backends.git
4 ~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~
5
5
6 Git backend implementation.
6 Git backend 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 re
13 import re
14 import time
14 import time
15 import posixpath
15 import posixpath
16 import logging
16 import logging
17 import traceback
17 import traceback
18 import urllib
18 import urllib
19 import urllib2
19 import urllib2
20 from dulwich.repo import Repo, NotGitRepository
20 from dulwich.repo import Repo, NotGitRepository
21 from dulwich.objects import Tag
21 from dulwich.objects import Tag
22 from string import Template
22 from string import Template
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.lib.vcs.backends.base import BaseRepository
25 from rhodecode.lib.vcs.backends.base import BaseRepository
26 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
26 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
27 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
27 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
28 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
28 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
29 from rhodecode.lib.vcs.exceptions import RepositoryError
29 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
30 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
31 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
31 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty, ThreadLocalLazyProperty
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty, ThreadLocalLazyProperty
34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
35 from rhodecode.lib.vcs.utils.paths import abspath
35 from rhodecode.lib.vcs.utils.paths import abspath
36 from rhodecode.lib.vcs.utils.paths import get_user_home
36 from rhodecode.lib.vcs.utils.paths import get_user_home
37 from .workdir import GitWorkdir
37 from .workdir import GitWorkdir
38 from .changeset import GitChangeset
38 from .changeset import GitChangeset
39 from .inmemory import GitInMemoryChangeset
39 from .inmemory import GitInMemoryChangeset
40 from .config import ConfigFile
40 from .config import ConfigFile
41 from rhodecode.lib import subprocessio
41 from rhodecode.lib import subprocessio
42
42
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 class GitRepository(BaseRepository):
47 class GitRepository(BaseRepository):
48 """
48 """
49 Git repository backend.
49 Git repository backend.
50 """
50 """
51 DEFAULT_BRANCH_NAME = 'master'
51 DEFAULT_BRANCH_NAME = 'master'
52 scm = 'git'
52 scm = 'git'
53
53
54 def __init__(self, repo_path, create=False, src_url=None,
54 def __init__(self, repo_path, create=False, src_url=None,
55 update_after_clone=False, bare=False):
55 update_after_clone=False, bare=False):
56
56
57 self.path = abspath(repo_path)
57 self.path = abspath(repo_path)
58 repo = self._get_repo(create, src_url, update_after_clone, bare)
58 repo = self._get_repo(create, src_url, update_after_clone, bare)
59 self.bare = repo.bare
59 self.bare = repo.bare
60
60
61 self._config_files = [
61 self._config_files = [
62 bare and abspath(self.path, 'config')
62 bare and abspath(self.path, 'config')
63 or abspath(self.path, '.git', 'config'),
63 or abspath(self.path, '.git', 'config'),
64 abspath(get_user_home(), '.gitconfig'),
64 abspath(get_user_home(), '.gitconfig'),
65 ]
65 ]
66
66
67 @ThreadLocalLazyProperty
67 @ThreadLocalLazyProperty
68 def _repo(self):
68 def _repo(self):
69 repo = Repo(self.path)
69 repo = Repo(self.path)
70 #temporary set that to now at later we will move it to constructor
70 #temporary set that to now at later we will move it to constructor
71 baseui = None
71 baseui = None
72 if baseui is None:
72 if baseui is None:
73 from mercurial.ui import ui
73 from mercurial.ui import ui
74 baseui = ui()
74 baseui = ui()
75 # patch the instance of GitRepo with an "FAKE" ui object to add
75 # patch the instance of GitRepo with an "FAKE" ui object to add
76 # compatibility layer with Mercurial
76 # compatibility layer with Mercurial
77 setattr(repo, 'ui', baseui)
77 setattr(repo, 'ui', baseui)
78 return repo
78 return repo
79
79
80 @property
80 @property
81 def head(self):
81 def head(self):
82 try:
82 try:
83 return self._repo.head()
83 return self._repo.head()
84 except KeyError:
84 except KeyError:
85 return None
85 return None
86
86
87 @LazyProperty
87 @LazyProperty
88 def revisions(self):
88 def revisions(self):
89 """
89 """
90 Returns list of revisions' ids, in ascending order. Being lazy
90 Returns list of revisions' ids, in ascending order. Being lazy
91 attribute allows external tools to inject shas from cache.
91 attribute allows external tools to inject shas from cache.
92 """
92 """
93 return self._get_all_revisions()
93 return self._get_all_revisions()
94
94
95 @classmethod
95 @classmethod
96 def _run_git_command(cls, cmd, **opts):
96 def _run_git_command(cls, cmd, **opts):
97 """
97 """
98 Runs given ``cmd`` as git command and returns tuple
98 Runs given ``cmd`` as git command and returns tuple
99 (stdout, stderr).
99 (stdout, stderr).
100
100
101 :param cmd: git command to be executed
101 :param cmd: git command to be executed
102 :param opts: env options to pass into Subprocess command
102 :param opts: env options to pass into Subprocess command
103 """
103 """
104
104
105 if '_bare' in opts:
105 if '_bare' in opts:
106 _copts = []
106 _copts = []
107 del opts['_bare']
107 del opts['_bare']
108 else:
108 else:
109 _copts = ['-c', 'core.quotepath=false', ]
109 _copts = ['-c', 'core.quotepath=false', ]
110 safe_call = False
110 safe_call = False
111 if '_safe' in opts:
111 if '_safe' in opts:
112 #no exc on failure
112 #no exc on failure
113 del opts['_safe']
113 del opts['_safe']
114 safe_call = True
114 safe_call = True
115
115
116 _str_cmd = False
116 _str_cmd = False
117 if isinstance(cmd, basestring):
117 if isinstance(cmd, basestring):
118 cmd = [cmd]
118 cmd = [cmd]
119 _str_cmd = True
119 _str_cmd = True
120
120
121 gitenv = os.environ
121 gitenv = os.environ
122 # need to clean fix GIT_DIR !
122 # need to clean fix GIT_DIR !
123 if 'GIT_DIR' in gitenv:
123 if 'GIT_DIR' in gitenv:
124 del gitenv['GIT_DIR']
124 del gitenv['GIT_DIR']
125 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
125 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
126
126
127 _git_path = rhodecode.CONFIG.get('git_path', 'git')
127 _git_path = rhodecode.CONFIG.get('git_path', 'git')
128 cmd = [_git_path] + _copts + cmd
128 cmd = [_git_path] + _copts + cmd
129 if _str_cmd:
129 if _str_cmd:
130 cmd = ' '.join(cmd)
130 cmd = ' '.join(cmd)
131 try:
131 try:
132 _opts = dict(
132 _opts = dict(
133 env=gitenv,
133 env=gitenv,
134 shell=False,
134 shell=False,
135 )
135 )
136 _opts.update(opts)
136 _opts.update(opts)
137 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
137 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
138 except (EnvironmentError, OSError), err:
138 except (EnvironmentError, OSError), err:
139 tb_err = ("Couldn't run git command (%s).\n"
139 tb_err = ("Couldn't run git command (%s).\n"
140 "Original error was:%s\n" % (cmd, err))
140 "Original error was:%s\n" % (cmd, err))
141 log.error(tb_err)
141 log.error(tb_err)
142 if safe_call:
142 if safe_call:
143 return '', err
143 return '', err
144 else:
144 else:
145 raise RepositoryError(tb_err)
145 raise RepositoryError(tb_err)
146
146
147 return ''.join(p.output), ''.join(p.error)
147 return ''.join(p.output), ''.join(p.error)
148
148
149 def run_git_command(self, cmd):
149 def run_git_command(self, cmd):
150 opts = {}
150 opts = {}
151 if os.path.isdir(self.path):
151 if os.path.isdir(self.path):
152 opts['cwd'] = self.path
152 opts['cwd'] = self.path
153 return self._run_git_command(cmd, **opts)
153 return self._run_git_command(cmd, **opts)
154
154
155 @classmethod
155 @classmethod
156 def _check_url(cls, url):
156 def _check_url(cls, url):
157 """
157 """
158 Functon will check given url and try to verify if it's a valid
158 Functon will check given url and try to verify if it's a valid
159 link. Sometimes it may happened that mercurial will issue basic
159 link. Sometimes it may happened that mercurial will issue basic
160 auth request that can cause whole API to hang when used from python
160 auth request that can cause whole API to hang when used from python
161 or other external calls.
161 or other external calls.
162
162
163 On failures it'll raise urllib2.HTTPError
163 On failures it'll raise urllib2.HTTPError
164 """
164 """
165 from mercurial.util import url as Url
165 from mercurial.util import url as Url
166
166
167 # those authnadlers are patched for python 2.6.5 bug an
167 # those authnadlers are patched for python 2.6.5 bug an
168 # infinit looping when given invalid resources
168 # infinit looping when given invalid resources
169 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
169 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
170
170
171 # check first if it's not an local url
171 # check first if it's not an local url
172 if os.path.isdir(url) or url.startswith('file:'):
172 if os.path.isdir(url) or url.startswith('file:'):
173 return True
173 return True
174
174
175 if('+' in url[:url.find('://')]):
175 if('+' in url[:url.find('://')]):
176 url = url[url.find('+') + 1:]
176 url = url[url.find('+') + 1:]
177
177
178 handlers = []
178 handlers = []
179 test_uri, authinfo = Url(url).authinfo()
179 test_uri, authinfo = Url(url).authinfo()
180 if not test_uri.endswith('info/refs'):
180 if not test_uri.endswith('info/refs'):
181 test_uri = test_uri.rstrip('/') + '/info/refs'
181 test_uri = test_uri.rstrip('/') + '/info/refs'
182 if authinfo:
182 if authinfo:
183 #create a password manager
183 #create a password manager
184 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
184 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
185 passmgr.add_password(*authinfo)
185 passmgr.add_password(*authinfo)
186
186
187 handlers.extend((httpbasicauthhandler(passmgr),
187 handlers.extend((httpbasicauthhandler(passmgr),
188 httpdigestauthhandler(passmgr)))
188 httpdigestauthhandler(passmgr)))
189
189
190 o = urllib2.build_opener(*handlers)
190 o = urllib2.build_opener(*handlers)
191 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
191 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
192
192
193 q = {"service": 'git-upload-pack'}
193 q = {"service": 'git-upload-pack'}
194 qs = '?%s' % urllib.urlencode(q)
194 qs = '?%s' % urllib.urlencode(q)
195 cu = "%s%s" % (test_uri, qs)
195 cu = "%s%s" % (test_uri, qs)
196 req = urllib2.Request(cu, None, {})
196 req = urllib2.Request(cu, None, {})
197
197
198 try:
198 try:
199 resp = o.open(req)
199 resp = o.open(req)
200 return resp.code == 200
200 return resp.code == 200
201 except Exception, e:
201 except Exception, e:
202 # means it cannot be cloned
202 # means it cannot be cloned
203 raise urllib2.URLError("[%s] %s" % (url, e))
203 raise urllib2.URLError("[%s] %s" % (url, e))
204
204
205 def _get_repo(self, create, src_url=None, update_after_clone=False,
205 def _get_repo(self, create, src_url=None, update_after_clone=False,
206 bare=False):
206 bare=False):
207 if create and os.path.exists(self.path):
207 if create and os.path.exists(self.path):
208 raise RepositoryError("Location already exist")
208 raise RepositoryError("Location already exist")
209 if src_url and not create:
209 if src_url and not create:
210 raise RepositoryError("Create should be set to True if src_url is "
210 raise RepositoryError("Create should be set to True if src_url is "
211 "given (clone operation creates repository)")
211 "given (clone operation creates repository)")
212 try:
212 try:
213 if create and src_url:
213 if create and src_url:
214 GitRepository._check_url(src_url)
214 GitRepository._check_url(src_url)
215 self.clone(src_url, update_after_clone, bare)
215 self.clone(src_url, update_after_clone, bare)
216 return Repo(self.path)
216 return Repo(self.path)
217 elif create:
217 elif create:
218 os.mkdir(self.path)
218 os.mkdir(self.path)
219 if bare:
219 if bare:
220 return Repo.init_bare(self.path)
220 return Repo.init_bare(self.path)
221 else:
221 else:
222 return Repo.init(self.path)
222 return Repo.init(self.path)
223 else:
223 else:
224 return self._repo
224 return self._repo
225 except (NotGitRepository, OSError), err:
225 except (NotGitRepository, OSError), err:
226 raise RepositoryError(err)
226 raise RepositoryError(err)
227
227
228 def _get_all_revisions(self):
228 def _get_all_revisions(self):
229 # we must check if this repo is not empty, since later command
229 # we must check if this repo is not empty, since later command
230 # fails if it is. And it's cheaper to ask than throw the subprocess
230 # fails if it is. And it's cheaper to ask than throw the subprocess
231 # errors
231 # errors
232 try:
232 try:
233 self._repo.head()
233 self._repo.head()
234 except KeyError:
234 except KeyError:
235 return []
235 return []
236 cmd = 'rev-list --all --reverse --date-order'
236 cmd = 'rev-list --all --reverse --date-order'
237 try:
237 try:
238 so, se = self.run_git_command(cmd)
238 so, se = self.run_git_command(cmd)
239 except RepositoryError:
239 except RepositoryError:
240 # Can be raised for empty repositories
240 # Can be raised for empty repositories
241 return []
241 return []
242 return so.splitlines()
242 return so.splitlines()
243
243
244 def _get_all_revisions2(self):
244 def _get_all_revisions2(self):
245 #alternate implementation using dulwich
245 #alternate implementation using dulwich
246 includes = [x[1][0] for x in self._parsed_refs.iteritems()
246 includes = [x[1][0] for x in self._parsed_refs.iteritems()
247 if x[1][1] != 'T']
247 if x[1][1] != 'T']
248 return [c.commit.id for c in self._repo.get_walker(include=includes)]
248 return [c.commit.id for c in self._repo.get_walker(include=includes)]
249
249
250 def _get_revision(self, revision):
250 def _get_revision(self, revision):
251 """
251 """
252 For git backend we always return integer here. This way we ensure
252 For git backend we always return integer here. This way we ensure
253 that changset's revision attribute would become integer.
253 that changset's revision attribute would become integer.
254 """
254 """
255 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
255 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
256 is_bstr = lambda o: isinstance(o, (str, unicode))
256 is_bstr = lambda o: isinstance(o, (str, unicode))
257 is_null = lambda o: len(o) == revision.count('0')
257 is_null = lambda o: len(o) == revision.count('0')
258
258
259 if len(self.revisions) == 0:
259 if len(self.revisions) == 0:
260 raise EmptyRepositoryError("There are no changesets yet")
260 raise EmptyRepositoryError("There are no changesets yet")
261
261
262 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
262 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
263 revision = self.revisions[-1]
263 revision = self.revisions[-1]
264
264
265 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
265 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
266 or isinstance(revision, int) or is_null(revision)):
266 or isinstance(revision, int) or is_null(revision)):
267 try:
267 try:
268 revision = self.revisions[int(revision)]
268 revision = self.revisions[int(revision)]
269 except:
269 except:
270 raise ChangesetDoesNotExistError("Revision %r does not exist "
270 raise ChangesetDoesNotExistError("Revision %r does not exist "
271 "for this repository %s" % (revision, self))
271 "for this repository %s" % (revision, self))
272
272
273 elif is_bstr(revision):
273 elif is_bstr(revision):
274 # get by branch/tag name
274 # get by branch/tag name
275 _ref_revision = self._parsed_refs.get(revision)
275 _ref_revision = self._parsed_refs.get(revision)
276 _tags_shas = self.tags.values()
276 _tags_shas = self.tags.values()
277 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
277 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
278 return _ref_revision[0]
278 return _ref_revision[0]
279
279
280 # maybe it's a tag ? we don't have them in self.revisions
280 # maybe it's a tag ? we don't have them in self.revisions
281 elif revision in _tags_shas:
281 elif revision in _tags_shas:
282 return _tags_shas[_tags_shas.index(revision)]
282 return _tags_shas[_tags_shas.index(revision)]
283
283
284 elif not pattern.match(revision) or revision not in self.revisions:
284 elif not pattern.match(revision) or revision not in self.revisions:
285 raise ChangesetDoesNotExistError("Revision %r does not exist "
285 raise ChangesetDoesNotExistError("Revision %r does not exist "
286 "for this repository %s" % (revision, self))
286 "for this repository %s" % (revision, self))
287
287
288 # Ensure we return full id
288 # Ensure we return full id
289 if not pattern.match(str(revision)):
289 if not pattern.match(str(revision)):
290 raise ChangesetDoesNotExistError("Given revision %r not recognized"
290 raise ChangesetDoesNotExistError("Given revision %r not recognized"
291 % revision)
291 % revision)
292 return revision
292 return revision
293
293
294 def _get_archives(self, archive_name='tip'):
294 def _get_archives(self, archive_name='tip'):
295
295
296 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
296 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
297 yield {"type": i[0], "extension": i[1], "node": archive_name}
297 yield {"type": i[0], "extension": i[1], "node": archive_name}
298
298
299 def _get_url(self, url):
299 def _get_url(self, url):
300 """
300 """
301 Returns normalized url. If schema is not given, would fall to
301 Returns normalized url. If schema is not given, would fall to
302 filesystem (``file:///``) schema.
302 filesystem (``file:///``) schema.
303 """
303 """
304 url = str(url)
304 url = str(url)
305 if url != 'default' and not '://' in url:
305 if url != 'default' and not '://' in url:
306 url = ':///'.join(('file', url))
306 url = ':///'.join(('file', url))
307 return url
307 return url
308
308
309 def get_hook_location(self):
310 """
311 returns absolute path to location where hooks are stored
312 """
313 loc = os.path.join(self.path, 'hooks')
314 if not self.bare:
315 loc = os.path.join(self.path, '.git', 'hooks')
316 return loc
317
309 @LazyProperty
318 @LazyProperty
310 def name(self):
319 def name(self):
311 return os.path.basename(self.path)
320 return os.path.basename(self.path)
312
321
313 @LazyProperty
322 @LazyProperty
314 def last_change(self):
323 def last_change(self):
315 """
324 """
316 Returns last change made on this repository as datetime object
325 Returns last change made on this repository as datetime object
317 """
326 """
318 return date_fromtimestamp(self._get_mtime(), makedate()[1])
327 return date_fromtimestamp(self._get_mtime(), makedate()[1])
319
328
320 def _get_mtime(self):
329 def _get_mtime(self):
321 try:
330 try:
322 return time.mktime(self.get_changeset().date.timetuple())
331 return time.mktime(self.get_changeset().date.timetuple())
323 except RepositoryError:
332 except RepositoryError:
324 idx_loc = '' if self.bare else '.git'
333 idx_loc = '' if self.bare else '.git'
325 # fallback to filesystem
334 # fallback to filesystem
326 in_path = os.path.join(self.path, idx_loc, "index")
335 in_path = os.path.join(self.path, idx_loc, "index")
327 he_path = os.path.join(self.path, idx_loc, "HEAD")
336 he_path = os.path.join(self.path, idx_loc, "HEAD")
328 if os.path.exists(in_path):
337 if os.path.exists(in_path):
329 return os.stat(in_path).st_mtime
338 return os.stat(in_path).st_mtime
330 else:
339 else:
331 return os.stat(he_path).st_mtime
340 return os.stat(he_path).st_mtime
332
341
333 @LazyProperty
342 @LazyProperty
334 def description(self):
343 def description(self):
335 idx_loc = '' if self.bare else '.git'
344 idx_loc = '' if self.bare else '.git'
336 undefined_description = u'unknown'
345 undefined_description = u'unknown'
337 description_path = os.path.join(self.path, idx_loc, 'description')
346 description_path = os.path.join(self.path, idx_loc, 'description')
338 if os.path.isfile(description_path):
347 if os.path.isfile(description_path):
339 return safe_unicode(open(description_path).read())
348 return safe_unicode(open(description_path).read())
340 else:
349 else:
341 return undefined_description
350 return undefined_description
342
351
343 @LazyProperty
352 @LazyProperty
344 def contact(self):
353 def contact(self):
345 undefined_contact = u'Unknown'
354 undefined_contact = u'Unknown'
346 return undefined_contact
355 return undefined_contact
347
356
348 @property
357 @property
349 def branches(self):
358 def branches(self):
350 if not self.revisions:
359 if not self.revisions:
351 return {}
360 return {}
352 sortkey = lambda ctx: ctx[0]
361 sortkey = lambda ctx: ctx[0]
353 _branches = [(x[0], x[1][0])
362 _branches = [(x[0], x[1][0])
354 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
363 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
355 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
364 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
356
365
357 @LazyProperty
366 @LazyProperty
358 def tags(self):
367 def tags(self):
359 return self._get_tags()
368 return self._get_tags()
360
369
361 def _get_tags(self):
370 def _get_tags(self):
362 if not self.revisions:
371 if not self.revisions:
363 return {}
372 return {}
364
373
365 sortkey = lambda ctx: ctx[0]
374 sortkey = lambda ctx: ctx[0]
366 _tags = [(x[0], x[1][0])
375 _tags = [(x[0], x[1][0])
367 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
376 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
368 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
377 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
369
378
370 def tag(self, name, user, revision=None, message=None, date=None,
379 def tag(self, name, user, revision=None, message=None, date=None,
371 **kwargs):
380 **kwargs):
372 """
381 """
373 Creates and returns a tag for the given ``revision``.
382 Creates and returns a tag for the given ``revision``.
374
383
375 :param name: name for new tag
384 :param name: name for new tag
376 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
385 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
377 :param revision: changeset id for which new tag would be created
386 :param revision: changeset id for which new tag would be created
378 :param message: message of the tag's commit
387 :param message: message of the tag's commit
379 :param date: date of tag's commit
388 :param date: date of tag's commit
380
389
381 :raises TagAlreadyExistError: if tag with same name already exists
390 :raises TagAlreadyExistError: if tag with same name already exists
382 """
391 """
383 if name in self.tags:
392 if name in self.tags:
384 raise TagAlreadyExistError("Tag %s already exists" % name)
393 raise TagAlreadyExistError("Tag %s already exists" % name)
385 changeset = self.get_changeset(revision)
394 changeset = self.get_changeset(revision)
386 message = message or "Added tag %s for commit %s" % (name,
395 message = message or "Added tag %s for commit %s" % (name,
387 changeset.raw_id)
396 changeset.raw_id)
388 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
397 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
389
398
390 self._parsed_refs = self._get_parsed_refs()
399 self._parsed_refs = self._get_parsed_refs()
391 self.tags = self._get_tags()
400 self.tags = self._get_tags()
392 return changeset
401 return changeset
393
402
394 def remove_tag(self, name, user, message=None, date=None):
403 def remove_tag(self, name, user, message=None, date=None):
395 """
404 """
396 Removes tag with the given ``name``.
405 Removes tag with the given ``name``.
397
406
398 :param name: name of the tag to be removed
407 :param name: name of the tag to be removed
399 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
408 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
400 :param message: message of the tag's removal commit
409 :param message: message of the tag's removal commit
401 :param date: date of tag's removal commit
410 :param date: date of tag's removal commit
402
411
403 :raises TagDoesNotExistError: if tag with given name does not exists
412 :raises TagDoesNotExistError: if tag with given name does not exists
404 """
413 """
405 if name not in self.tags:
414 if name not in self.tags:
406 raise TagDoesNotExistError("Tag %s does not exist" % name)
415 raise TagDoesNotExistError("Tag %s does not exist" % name)
407 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
416 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
408 try:
417 try:
409 os.remove(tagpath)
418 os.remove(tagpath)
410 self._parsed_refs = self._get_parsed_refs()
419 self._parsed_refs = self._get_parsed_refs()
411 self.tags = self._get_tags()
420 self.tags = self._get_tags()
412 except OSError, e:
421 except OSError, e:
413 raise RepositoryError(e.strerror)
422 raise RepositoryError(e.strerror)
414
423
415 @LazyProperty
424 @LazyProperty
416 def _parsed_refs(self):
425 def _parsed_refs(self):
417 return self._get_parsed_refs()
426 return self._get_parsed_refs()
418
427
419 def _get_parsed_refs(self):
428 def _get_parsed_refs(self):
420 refs = self._repo.get_refs()
429 refs = self._repo.get_refs()
421 keys = [('refs/heads/', 'H'),
430 keys = [('refs/heads/', 'H'),
422 ('refs/remotes/origin/', 'RH'),
431 ('refs/remotes/origin/', 'RH'),
423 ('refs/tags/', 'T')]
432 ('refs/tags/', 'T')]
424 _refs = {}
433 _refs = {}
425 for ref, sha in refs.iteritems():
434 for ref, sha in refs.iteritems():
426 for k, type_ in keys:
435 for k, type_ in keys:
427 if ref.startswith(k):
436 if ref.startswith(k):
428 _key = ref[len(k):]
437 _key = ref[len(k):]
429 if type_ == 'T':
438 if type_ == 'T':
430 obj = self._repo.get_object(sha)
439 obj = self._repo.get_object(sha)
431 if isinstance(obj, Tag):
440 if isinstance(obj, Tag):
432 sha = self._repo.get_object(sha).object[1]
441 sha = self._repo.get_object(sha).object[1]
433 _refs[_key] = [sha, type_]
442 _refs[_key] = [sha, type_]
434 break
443 break
435 return _refs
444 return _refs
436
445
437 def _heads(self, reverse=False):
446 def _heads(self, reverse=False):
438 refs = self._repo.get_refs()
447 refs = self._repo.get_refs()
439 heads = {}
448 heads = {}
440
449
441 for key, val in refs.items():
450 for key, val in refs.items():
442 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
451 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
443 if key.startswith(ref_key):
452 if key.startswith(ref_key):
444 n = key[len(ref_key):]
453 n = key[len(ref_key):]
445 if n not in ['HEAD']:
454 if n not in ['HEAD']:
446 heads[n] = val
455 heads[n] = val
447
456
448 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
457 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
449
458
450 def get_changeset(self, revision=None):
459 def get_changeset(self, revision=None):
451 """
460 """
452 Returns ``GitChangeset`` object representing commit from git repository
461 Returns ``GitChangeset`` object representing commit from git repository
453 at the given revision or head (most recent commit) if None given.
462 at the given revision or head (most recent commit) if None given.
454 """
463 """
455 if isinstance(revision, GitChangeset):
464 if isinstance(revision, GitChangeset):
456 return revision
465 return revision
457 revision = self._get_revision(revision)
466 revision = self._get_revision(revision)
458 changeset = GitChangeset(repository=self, revision=revision)
467 changeset = GitChangeset(repository=self, revision=revision)
459 return changeset
468 return changeset
460
469
461 def get_changesets(self, start=None, end=None, start_date=None,
470 def get_changesets(self, start=None, end=None, start_date=None,
462 end_date=None, branch_name=None, reverse=False):
471 end_date=None, branch_name=None, reverse=False):
463 """
472 """
464 Returns iterator of ``GitChangeset`` objects from start to end (both
473 Returns iterator of ``GitChangeset`` objects from start to end (both
465 are inclusive), in ascending date order (unless ``reverse`` is set).
474 are inclusive), in ascending date order (unless ``reverse`` is set).
466
475
467 :param start: changeset ID, as str; first returned changeset
476 :param start: changeset ID, as str; first returned changeset
468 :param end: changeset ID, as str; last returned changeset
477 :param end: changeset ID, as str; last returned changeset
469 :param start_date: if specified, changesets with commit date less than
478 :param start_date: if specified, changesets with commit date less than
470 ``start_date`` would be filtered out from returned set
479 ``start_date`` would be filtered out from returned set
471 :param end_date: if specified, changesets with commit date greater than
480 :param end_date: if specified, changesets with commit date greater than
472 ``end_date`` would be filtered out from returned set
481 ``end_date`` would be filtered out from returned set
473 :param branch_name: if specified, changesets not reachable from given
482 :param branch_name: if specified, changesets not reachable from given
474 branch would be filtered out from returned set
483 branch would be filtered out from returned set
475 :param reverse: if ``True``, returned generator would be reversed
484 :param reverse: if ``True``, returned generator would be reversed
476 (meaning that returned changesets would have descending date order)
485 (meaning that returned changesets would have descending date order)
477
486
478 :raise BranchDoesNotExistError: If given ``branch_name`` does not
487 :raise BranchDoesNotExistError: If given ``branch_name`` does not
479 exist.
488 exist.
480 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
489 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
481 ``end`` could not be found.
490 ``end`` could not be found.
482
491
483 """
492 """
484 if branch_name and branch_name not in self.branches:
493 if branch_name and branch_name not in self.branches:
485 raise BranchDoesNotExistError("Branch '%s' not found" \
494 raise BranchDoesNotExistError("Branch '%s' not found" \
486 % branch_name)
495 % branch_name)
487 # %H at format means (full) commit hash, initial hashes are retrieved
496 # %H at format means (full) commit hash, initial hashes are retrieved
488 # in ascending date order
497 # in ascending date order
489 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
498 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
490 cmd_params = {}
499 cmd_params = {}
491 if start_date:
500 if start_date:
492 cmd_template += ' --since "$since"'
501 cmd_template += ' --since "$since"'
493 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
502 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
494 if end_date:
503 if end_date:
495 cmd_template += ' --until "$until"'
504 cmd_template += ' --until "$until"'
496 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
505 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
497 if branch_name:
506 if branch_name:
498 cmd_template += ' $branch_name'
507 cmd_template += ' $branch_name'
499 cmd_params['branch_name'] = branch_name
508 cmd_params['branch_name'] = branch_name
500 else:
509 else:
501 cmd_template += ' --all'
510 cmd_template += ' --all'
502
511
503 cmd = Template(cmd_template).safe_substitute(**cmd_params)
512 cmd = Template(cmd_template).safe_substitute(**cmd_params)
504 revs = self.run_git_command(cmd)[0].splitlines()
513 revs = self.run_git_command(cmd)[0].splitlines()
505 start_pos = 0
514 start_pos = 0
506 end_pos = len(revs)
515 end_pos = len(revs)
507 if start:
516 if start:
508 _start = self._get_revision(start)
517 _start = self._get_revision(start)
509 try:
518 try:
510 start_pos = revs.index(_start)
519 start_pos = revs.index(_start)
511 except ValueError:
520 except ValueError:
512 pass
521 pass
513
522
514 if end is not None:
523 if end is not None:
515 _end = self._get_revision(end)
524 _end = self._get_revision(end)
516 try:
525 try:
517 end_pos = revs.index(_end)
526 end_pos = revs.index(_end)
518 except ValueError:
527 except ValueError:
519 pass
528 pass
520
529
521 if None not in [start, end] and start_pos > end_pos:
530 if None not in [start, end] and start_pos > end_pos:
522 raise RepositoryError('start cannot be after end')
531 raise RepositoryError('start cannot be after end')
523
532
524 if end_pos is not None:
533 if end_pos is not None:
525 end_pos += 1
534 end_pos += 1
526
535
527 revs = revs[start_pos:end_pos]
536 revs = revs[start_pos:end_pos]
528 if reverse:
537 if reverse:
529 revs = reversed(revs)
538 revs = reversed(revs)
530 for rev in revs:
539 for rev in revs:
531 yield self.get_changeset(rev)
540 yield self.get_changeset(rev)
532
541
533 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
542 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
534 context=3):
543 context=3):
535 """
544 """
536 Returns (git like) *diff*, as plain text. Shows changes introduced by
545 Returns (git like) *diff*, as plain text. Shows changes introduced by
537 ``rev2`` since ``rev1``.
546 ``rev2`` since ``rev1``.
538
547
539 :param rev1: Entry point from which diff is shown. Can be
548 :param rev1: Entry point from which diff is shown. Can be
540 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
549 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
541 the changes since empty state of the repository until ``rev2``
550 the changes since empty state of the repository until ``rev2``
542 :param rev2: Until which revision changes should be shown.
551 :param rev2: Until which revision changes should be shown.
543 :param ignore_whitespace: If set to ``True``, would not show whitespace
552 :param ignore_whitespace: If set to ``True``, would not show whitespace
544 changes. Defaults to ``False``.
553 changes. Defaults to ``False``.
545 :param context: How many lines before/after changed lines should be
554 :param context: How many lines before/after changed lines should be
546 shown. Defaults to ``3``.
555 shown. Defaults to ``3``.
547 """
556 """
548 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
557 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
549 if ignore_whitespace:
558 if ignore_whitespace:
550 flags.append('-w')
559 flags.append('-w')
551
560
552 if hasattr(rev1, 'raw_id'):
561 if hasattr(rev1, 'raw_id'):
553 rev1 = getattr(rev1, 'raw_id')
562 rev1 = getattr(rev1, 'raw_id')
554
563
555 if hasattr(rev2, 'raw_id'):
564 if hasattr(rev2, 'raw_id'):
556 rev2 = getattr(rev2, 'raw_id')
565 rev2 = getattr(rev2, 'raw_id')
557
566
558 if rev1 == self.EMPTY_CHANGESET:
567 if rev1 == self.EMPTY_CHANGESET:
559 rev2 = self.get_changeset(rev2).raw_id
568 rev2 = self.get_changeset(rev2).raw_id
560 cmd = ' '.join(['show'] + flags + [rev2])
569 cmd = ' '.join(['show'] + flags + [rev2])
561 else:
570 else:
562 rev1 = self.get_changeset(rev1).raw_id
571 rev1 = self.get_changeset(rev1).raw_id
563 rev2 = self.get_changeset(rev2).raw_id
572 rev2 = self.get_changeset(rev2).raw_id
564 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
573 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
565
574
566 if path:
575 if path:
567 cmd += ' -- "%s"' % path
576 cmd += ' -- "%s"' % path
568
577
569 stdout, stderr = self.run_git_command(cmd)
578 stdout, stderr = self.run_git_command(cmd)
570 # If we used 'show' command, strip first few lines (until actual diff
579 # If we used 'show' command, strip first few lines (until actual diff
571 # starts)
580 # starts)
572 if rev1 == self.EMPTY_CHANGESET:
581 if rev1 == self.EMPTY_CHANGESET:
573 lines = stdout.splitlines()
582 lines = stdout.splitlines()
574 x = 0
583 x = 0
575 for line in lines:
584 for line in lines:
576 if line.startswith('diff'):
585 if line.startswith('diff'):
577 break
586 break
578 x += 1
587 x += 1
579 # Append new line just like 'diff' command do
588 # Append new line just like 'diff' command do
580 stdout = '\n'.join(lines[x:]) + '\n'
589 stdout = '\n'.join(lines[x:]) + '\n'
581 return stdout
590 return stdout
582
591
583 @LazyProperty
592 @LazyProperty
584 def in_memory_changeset(self):
593 def in_memory_changeset(self):
585 """
594 """
586 Returns ``GitInMemoryChangeset`` object for this repository.
595 Returns ``GitInMemoryChangeset`` object for this repository.
587 """
596 """
588 return GitInMemoryChangeset(self)
597 return GitInMemoryChangeset(self)
589
598
590 def clone(self, url, update_after_clone=True, bare=False):
599 def clone(self, url, update_after_clone=True, bare=False):
591 """
600 """
592 Tries to clone changes from external location.
601 Tries to clone changes from external location.
593
602
594 :param update_after_clone: If set to ``False``, git won't checkout
603 :param update_after_clone: If set to ``False``, git won't checkout
595 working directory
604 working directory
596 :param bare: If set to ``True``, repository would be cloned into
605 :param bare: If set to ``True``, repository would be cloned into
597 *bare* git repository (no working directory at all).
606 *bare* git repository (no working directory at all).
598 """
607 """
599 url = self._get_url(url)
608 url = self._get_url(url)
600 cmd = ['clone']
609 cmd = ['clone']
601 if bare:
610 if bare:
602 cmd.append('--bare')
611 cmd.append('--bare')
603 elif not update_after_clone:
612 elif not update_after_clone:
604 cmd.append('--no-checkout')
613 cmd.append('--no-checkout')
605 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
614 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
606 cmd = ' '.join(cmd)
615 cmd = ' '.join(cmd)
607 # If error occurs run_git_command raises RepositoryError already
616 # If error occurs run_git_command raises RepositoryError already
608 self.run_git_command(cmd)
617 self.run_git_command(cmd)
609
618
610 def pull(self, url):
619 def pull(self, url):
611 """
620 """
612 Tries to pull changes from external location.
621 Tries to pull changes from external location.
613 """
622 """
614 url = self._get_url(url)
623 url = self._get_url(url)
615 cmd = ['pull']
624 cmd = ['pull']
616 cmd.append("--ff-only")
625 cmd.append("--ff-only")
617 cmd.append(url)
626 cmd.append(url)
618 cmd = ' '.join(cmd)
627 cmd = ' '.join(cmd)
619 # If error occurs run_git_command raises RepositoryError already
628 # If error occurs run_git_command raises RepositoryError already
620 self.run_git_command(cmd)
629 self.run_git_command(cmd)
621
630
622 def fetch(self, url):
631 def fetch(self, url):
623 """
632 """
624 Tries to pull changes from external location.
633 Tries to pull changes from external location.
625 """
634 """
626 url = self._get_url(url)
635 url = self._get_url(url)
627 so, se = self.run_git_command('ls-remote -h %s' % url)
636 so, se = self.run_git_command('ls-remote -h %s' % url)
628 refs = []
637 refs = []
629 for line in (x for x in so.splitlines()):
638 for line in (x for x in so.splitlines()):
630 sha, ref = line.split('\t')
639 sha, ref = line.split('\t')
631 refs.append(ref)
640 refs.append(ref)
632 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
641 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
633 cmd = '''fetch %s -- %s''' % (url, refs)
642 cmd = '''fetch %s -- %s''' % (url, refs)
634 self.run_git_command(cmd)
643 self.run_git_command(cmd)
635
644
636 @LazyProperty
645 @LazyProperty
637 def workdir(self):
646 def workdir(self):
638 """
647 """
639 Returns ``Workdir`` instance for this repository.
648 Returns ``Workdir`` instance for this repository.
640 """
649 """
641 return GitWorkdir(self)
650 return GitWorkdir(self)
642
651
643 def get_config_value(self, section, name, config_file=None):
652 def get_config_value(self, section, name, config_file=None):
644 """
653 """
645 Returns configuration value for a given [``section``] and ``name``.
654 Returns configuration value for a given [``section``] and ``name``.
646
655
647 :param section: Section we want to retrieve value from
656 :param section: Section we want to retrieve value from
648 :param name: Name of configuration we want to retrieve
657 :param name: Name of configuration we want to retrieve
649 :param config_file: A path to file which should be used to retrieve
658 :param config_file: A path to file which should be used to retrieve
650 configuration from (might also be a list of file paths)
659 configuration from (might also be a list of file paths)
651 """
660 """
652 if config_file is None:
661 if config_file is None:
653 config_file = []
662 config_file = []
654 elif isinstance(config_file, basestring):
663 elif isinstance(config_file, basestring):
655 config_file = [config_file]
664 config_file = [config_file]
656
665
657 def gen_configs():
666 def gen_configs():
658 for path in config_file + self._config_files:
667 for path in config_file + self._config_files:
659 try:
668 try:
660 yield ConfigFile.from_path(path)
669 yield ConfigFile.from_path(path)
661 except (IOError, OSError, ValueError):
670 except (IOError, OSError, ValueError):
662 continue
671 continue
663
672
664 for config in gen_configs():
673 for config in gen_configs():
665 try:
674 try:
666 return config.get(section, name)
675 return config.get(section, name)
667 except KeyError:
676 except KeyError:
668 continue
677 continue
669 return None
678 return None
670
679
671 def get_user_name(self, config_file=None):
680 def get_user_name(self, config_file=None):
672 """
681 """
673 Returns user's name from global configuration file.
682 Returns user's name from global configuration file.
674
683
675 :param config_file: A path to file which should be used to retrieve
684 :param config_file: A path to file which should be used to retrieve
676 configuration from (might also be a list of file paths)
685 configuration from (might also be a list of file paths)
677 """
686 """
678 return self.get_config_value('user', 'name', config_file)
687 return self.get_config_value('user', 'name', config_file)
679
688
680 def get_user_email(self, config_file=None):
689 def get_user_email(self, config_file=None):
681 """
690 """
682 Returns user's email from global configuration file.
691 Returns user's email from global configuration file.
683
692
684 :param config_file: A path to file which should be used to retrieve
693 :param config_file: A path to file which should be used to retrieve
685 configuration from (might also be a list of file paths)
694 configuration from (might also be a list of file paths)
686 """
695 """
687 return self.get_config_value('user', 'email', config_file)
696 return self.get_config_value('user', 'email', config_file)
@@ -1,536 +1,542 b''
1 import os
1 import os
2 import time
2 import time
3 import datetime
3 import datetime
4 import urllib
4 import urllib
5 import urllib2
5 import urllib2
6
6
7 from rhodecode.lib.vcs.backends.base import BaseRepository
7 from rhodecode.lib.vcs.backends.base import BaseRepository
8 from .workdir import MercurialWorkdir
8 from .workdir import MercurialWorkdir
9 from .changeset import MercurialChangeset
9 from .changeset import MercurialChangeset
10 from .inmemory import MercurialInMemoryChangeset
10 from .inmemory import MercurialInMemoryChangeset
11
11
12 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError, \
12 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError, \
13 ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, \
13 ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, \
14 VCSError, TagAlreadyExistError, TagDoesNotExistError
14 VCSError, TagAlreadyExistError, TagDoesNotExistError
15 from rhodecode.lib.vcs.utils import author_email, author_name, date_fromtimestamp, \
15 from rhodecode.lib.vcs.utils import author_email, author_name, date_fromtimestamp, \
16 makedate, safe_unicode
16 makedate, safe_unicode
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
17 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
18 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
19 from rhodecode.lib.vcs.utils.paths import abspath
19 from rhodecode.lib.vcs.utils.paths import abspath
20
20
21 from rhodecode.lib.vcs.utils.hgcompat import ui, nullid, match, patch, diffopts, clone, \
21 from rhodecode.lib.vcs.utils.hgcompat import ui, nullid, match, patch, diffopts, clone, \
22 get_contact, pull, localrepository, RepoLookupError, Abort, RepoError, hex
22 get_contact, pull, localrepository, RepoLookupError, Abort, RepoError, hex
23
23
24
24
25 class MercurialRepository(BaseRepository):
25 class MercurialRepository(BaseRepository):
26 """
26 """
27 Mercurial repository backend
27 Mercurial repository backend
28 """
28 """
29 DEFAULT_BRANCH_NAME = 'default'
29 DEFAULT_BRANCH_NAME = 'default'
30 scm = 'hg'
30 scm = 'hg'
31
31
32 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
32 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
33 update_after_clone=False):
33 update_after_clone=False):
34 """
34 """
35 Raises RepositoryError if repository could not be find at the given
35 Raises RepositoryError if repository could not be find at the given
36 ``repo_path``.
36 ``repo_path``.
37
37
38 :param repo_path: local path of the repository
38 :param repo_path: local path of the repository
39 :param create=False: if set to True, would try to create repository if
39 :param create=False: if set to True, would try to create repository if
40 it does not exist rather than raising exception
40 it does not exist rather than raising exception
41 :param baseui=None: user data
41 :param baseui=None: user data
42 :param src_url=None: would try to clone repository from given location
42 :param src_url=None: would try to clone repository from given location
43 :param update_after_clone=False: sets update of working copy after
43 :param update_after_clone=False: sets update of working copy after
44 making a clone
44 making a clone
45 """
45 """
46
46
47 if not isinstance(repo_path, str):
47 if not isinstance(repo_path, str):
48 raise VCSError('Mercurial backend requires repository path to '
48 raise VCSError('Mercurial backend requires repository path to '
49 'be instance of <str> got %s instead' %
49 'be instance of <str> got %s instead' %
50 type(repo_path))
50 type(repo_path))
51
51
52 self.path = abspath(repo_path)
52 self.path = abspath(repo_path)
53 self.baseui = baseui or ui.ui()
53 self.baseui = baseui or ui.ui()
54 # We've set path and ui, now we can set _repo itself
54 # We've set path and ui, now we can set _repo itself
55 self._repo = self._get_repo(create, src_url, update_after_clone)
55 self._repo = self._get_repo(create, src_url, update_after_clone)
56
56
57 @property
57 @property
58 def _empty(self):
58 def _empty(self):
59 """
59 """
60 Checks if repository is empty without any changesets
60 Checks if repository is empty without any changesets
61 """
61 """
62 # TODO: Following raises errors when using InMemoryChangeset...
62 # TODO: Following raises errors when using InMemoryChangeset...
63 # return len(self._repo.changelog) == 0
63 # return len(self._repo.changelog) == 0
64 return len(self.revisions) == 0
64 return len(self.revisions) == 0
65
65
66 @LazyProperty
66 @LazyProperty
67 def revisions(self):
67 def revisions(self):
68 """
68 """
69 Returns list of revisions' ids, in ascending order. Being lazy
69 Returns list of revisions' ids, in ascending order. Being lazy
70 attribute allows external tools to inject shas from cache.
70 attribute allows external tools to inject shas from cache.
71 """
71 """
72 return self._get_all_revisions()
72 return self._get_all_revisions()
73
73
74 @LazyProperty
74 @LazyProperty
75 def name(self):
75 def name(self):
76 return os.path.basename(self.path)
76 return os.path.basename(self.path)
77
77
78 @LazyProperty
78 @LazyProperty
79 def branches(self):
79 def branches(self):
80 return self._get_branches()
80 return self._get_branches()
81
81
82 def _get_branches(self, closed=False):
82 def _get_branches(self, closed=False):
83 """
83 """
84 Get's branches for this repository
84 Get's branches for this repository
85 Returns only not closed branches by default
85 Returns only not closed branches by default
86
86
87 :param closed: return also closed branches for mercurial
87 :param closed: return also closed branches for mercurial
88 """
88 """
89
89
90 if self._empty:
90 if self._empty:
91 return {}
91 return {}
92
92
93 def _branchtags(localrepo):
93 def _branchtags(localrepo):
94 """
94 """
95 Patched version of mercurial branchtags to not return the closed
95 Patched version of mercurial branchtags to not return the closed
96 branches
96 branches
97
97
98 :param localrepo: locarepository instance
98 :param localrepo: locarepository instance
99 """
99 """
100
100
101 bt = {}
101 bt = {}
102 bt_closed = {}
102 bt_closed = {}
103 for bn, heads in localrepo.branchmap().iteritems():
103 for bn, heads in localrepo.branchmap().iteritems():
104 tip = heads[-1]
104 tip = heads[-1]
105 if 'close' in localrepo.changelog.read(tip)[5]:
105 if 'close' in localrepo.changelog.read(tip)[5]:
106 bt_closed[bn] = tip
106 bt_closed[bn] = tip
107 else:
107 else:
108 bt[bn] = tip
108 bt[bn] = tip
109
109
110 if closed:
110 if closed:
111 bt.update(bt_closed)
111 bt.update(bt_closed)
112 return bt
112 return bt
113
113
114 sortkey = lambda ctx: ctx[0] # sort by name
114 sortkey = lambda ctx: ctx[0] # sort by name
115 _branches = [(safe_unicode(n), hex(h),) for n, h in
115 _branches = [(safe_unicode(n), hex(h),) for n, h in
116 _branchtags(self._repo).items()]
116 _branchtags(self._repo).items()]
117
117
118 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
118 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
119
119
120 @LazyProperty
120 @LazyProperty
121 def tags(self):
121 def tags(self):
122 """
122 """
123 Get's tags for this repository
123 Get's tags for this repository
124 """
124 """
125 return self._get_tags()
125 return self._get_tags()
126
126
127 def _get_tags(self):
127 def _get_tags(self):
128 if self._empty:
128 if self._empty:
129 return {}
129 return {}
130
130
131 sortkey = lambda ctx: ctx[0] # sort by name
131 sortkey = lambda ctx: ctx[0] # sort by name
132 _tags = [(safe_unicode(n), hex(h),) for n, h in
132 _tags = [(safe_unicode(n), hex(h),) for n, h in
133 self._repo.tags().items()]
133 self._repo.tags().items()]
134
134
135 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
135 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
136
136
137 def tag(self, name, user, revision=None, message=None, date=None,
137 def tag(self, name, user, revision=None, message=None, date=None,
138 **kwargs):
138 **kwargs):
139 """
139 """
140 Creates and returns a tag for the given ``revision``.
140 Creates and returns a tag for the given ``revision``.
141
141
142 :param name: name for new tag
142 :param name: name for new tag
143 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
143 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
144 :param revision: changeset id for which new tag would be created
144 :param revision: changeset id for which new tag would be created
145 :param message: message of the tag's commit
145 :param message: message of the tag's commit
146 :param date: date of tag's commit
146 :param date: date of tag's commit
147
147
148 :raises TagAlreadyExistError: if tag with same name already exists
148 :raises TagAlreadyExistError: if tag with same name already exists
149 """
149 """
150 if name in self.tags:
150 if name in self.tags:
151 raise TagAlreadyExistError("Tag %s already exists" % name)
151 raise TagAlreadyExistError("Tag %s already exists" % name)
152 changeset = self.get_changeset(revision)
152 changeset = self.get_changeset(revision)
153 local = kwargs.setdefault('local', False)
153 local = kwargs.setdefault('local', False)
154
154
155 if message is None:
155 if message is None:
156 message = "Added tag %s for changeset %s" % (name,
156 message = "Added tag %s for changeset %s" % (name,
157 changeset.short_id)
157 changeset.short_id)
158
158
159 if date is None:
159 if date is None:
160 date = datetime.datetime.now().ctime()
160 date = datetime.datetime.now().ctime()
161
161
162 try:
162 try:
163 self._repo.tag(name, changeset._ctx.node(), message, local, user,
163 self._repo.tag(name, changeset._ctx.node(), message, local, user,
164 date)
164 date)
165 except Abort, e:
165 except Abort, e:
166 raise RepositoryError(e.message)
166 raise RepositoryError(e.message)
167
167
168 # Reinitialize tags
168 # Reinitialize tags
169 self.tags = self._get_tags()
169 self.tags = self._get_tags()
170 tag_id = self.tags[name]
170 tag_id = self.tags[name]
171
171
172 return self.get_changeset(revision=tag_id)
172 return self.get_changeset(revision=tag_id)
173
173
174 def remove_tag(self, name, user, message=None, date=None):
174 def remove_tag(self, name, user, message=None, date=None):
175 """
175 """
176 Removes tag with the given ``name``.
176 Removes tag with the given ``name``.
177
177
178 :param name: name of the tag to be removed
178 :param name: name of the tag to be removed
179 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
179 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
180 :param message: message of the tag's removal commit
180 :param message: message of the tag's removal commit
181 :param date: date of tag's removal commit
181 :param date: date of tag's removal commit
182
182
183 :raises TagDoesNotExistError: if tag with given name does not exists
183 :raises TagDoesNotExistError: if tag with given name does not exists
184 """
184 """
185 if name not in self.tags:
185 if name not in self.tags:
186 raise TagDoesNotExistError("Tag %s does not exist" % name)
186 raise TagDoesNotExistError("Tag %s does not exist" % name)
187 if message is None:
187 if message is None:
188 message = "Removed tag %s" % name
188 message = "Removed tag %s" % name
189 if date is None:
189 if date is None:
190 date = datetime.datetime.now().ctime()
190 date = datetime.datetime.now().ctime()
191 local = False
191 local = False
192
192
193 try:
193 try:
194 self._repo.tag(name, nullid, message, local, user, date)
194 self._repo.tag(name, nullid, message, local, user, date)
195 self.tags = self._get_tags()
195 self.tags = self._get_tags()
196 except Abort, e:
196 except Abort, e:
197 raise RepositoryError(e.message)
197 raise RepositoryError(e.message)
198
198
199 @LazyProperty
199 @LazyProperty
200 def bookmarks(self):
200 def bookmarks(self):
201 """
201 """
202 Get's bookmarks for this repository
202 Get's bookmarks for this repository
203 """
203 """
204 return self._get_bookmarks()
204 return self._get_bookmarks()
205
205
206 def _get_bookmarks(self):
206 def _get_bookmarks(self):
207 if self._empty:
207 if self._empty:
208 return {}
208 return {}
209
209
210 sortkey = lambda ctx: ctx[0] # sort by name
210 sortkey = lambda ctx: ctx[0] # sort by name
211 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
211 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
212 self._repo._bookmarks.items()]
212 self._repo._bookmarks.items()]
213 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
213 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
214
214
215 def _get_all_revisions(self):
215 def _get_all_revisions(self):
216
216
217 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
217 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
218
218
219 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
219 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
220 context=3):
220 context=3):
221 """
221 """
222 Returns (git like) *diff*, as plain text. Shows changes introduced by
222 Returns (git like) *diff*, as plain text. Shows changes introduced by
223 ``rev2`` since ``rev1``.
223 ``rev2`` since ``rev1``.
224
224
225 :param rev1: Entry point from which diff is shown. Can be
225 :param rev1: Entry point from which diff is shown. Can be
226 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
226 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
227 the changes since empty state of the repository until ``rev2``
227 the changes since empty state of the repository until ``rev2``
228 :param rev2: Until which revision changes should be shown.
228 :param rev2: Until which revision changes should be shown.
229 :param ignore_whitespace: If set to ``True``, would not show whitespace
229 :param ignore_whitespace: If set to ``True``, would not show whitespace
230 changes. Defaults to ``False``.
230 changes. Defaults to ``False``.
231 :param context: How many lines before/after changed lines should be
231 :param context: How many lines before/after changed lines should be
232 shown. Defaults to ``3``.
232 shown. Defaults to ``3``.
233 """
233 """
234 if hasattr(rev1, 'raw_id'):
234 if hasattr(rev1, 'raw_id'):
235 rev1 = getattr(rev1, 'raw_id')
235 rev1 = getattr(rev1, 'raw_id')
236
236
237 if hasattr(rev2, 'raw_id'):
237 if hasattr(rev2, 'raw_id'):
238 rev2 = getattr(rev2, 'raw_id')
238 rev2 = getattr(rev2, 'raw_id')
239
239
240 # Check if given revisions are present at repository (may raise
240 # Check if given revisions are present at repository (may raise
241 # ChangesetDoesNotExistError)
241 # ChangesetDoesNotExistError)
242 if rev1 != self.EMPTY_CHANGESET:
242 if rev1 != self.EMPTY_CHANGESET:
243 self.get_changeset(rev1)
243 self.get_changeset(rev1)
244 self.get_changeset(rev2)
244 self.get_changeset(rev2)
245 if path:
245 if path:
246 file_filter = match(self.path, '', [path])
246 file_filter = match(self.path, '', [path])
247 else:
247 else:
248 file_filter = None
248 file_filter = None
249
249
250 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
250 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
251 opts=diffopts(git=True,
251 opts=diffopts(git=True,
252 ignorews=ignore_whitespace,
252 ignorews=ignore_whitespace,
253 context=context)))
253 context=context)))
254
254
255 @classmethod
255 @classmethod
256 def _check_url(cls, url):
256 def _check_url(cls, url):
257 """
257 """
258 Function will check given url and try to verify if it's a valid
258 Function will check given url and try to verify if it's a valid
259 link. Sometimes it may happened that mercurial will issue basic
259 link. Sometimes it may happened that mercurial will issue basic
260 auth request that can cause whole API to hang when used from python
260 auth request that can cause whole API to hang when used from python
261 or other external calls.
261 or other external calls.
262
262
263 On failures it'll raise urllib2.HTTPError, return code 200 if url
263 On failures it'll raise urllib2.HTTPError, return code 200 if url
264 is valid or True if it's a local path
264 is valid or True if it's a local path
265 """
265 """
266
266
267 from mercurial.util import url as Url
267 from mercurial.util import url as Url
268
268
269 # those authnadlers are patched for python 2.6.5 bug an
269 # those authnadlers are patched for python 2.6.5 bug an
270 # infinit looping when given invalid resources
270 # infinit looping when given invalid resources
271 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
271 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
272
272
273 # check first if it's not an local url
273 # check first if it's not an local url
274 if os.path.isdir(url) or url.startswith('file:'):
274 if os.path.isdir(url) or url.startswith('file:'):
275 return True
275 return True
276
276
277 if('+' in url[:url.find('://')]):
277 if('+' in url[:url.find('://')]):
278 url = url[url.find('+') + 1:]
278 url = url[url.find('+') + 1:]
279
279
280 handlers = []
280 handlers = []
281 test_uri, authinfo = Url(url).authinfo()
281 test_uri, authinfo = Url(url).authinfo()
282
282
283 if authinfo:
283 if authinfo:
284 #create a password manager
284 #create a password manager
285 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
285 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
286 passmgr.add_password(*authinfo)
286 passmgr.add_password(*authinfo)
287
287
288 handlers.extend((httpbasicauthhandler(passmgr),
288 handlers.extend((httpbasicauthhandler(passmgr),
289 httpdigestauthhandler(passmgr)))
289 httpdigestauthhandler(passmgr)))
290
290
291 o = urllib2.build_opener(*handlers)
291 o = urllib2.build_opener(*handlers)
292 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
292 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
293 ('Accept', 'application/mercurial-0.1')]
293 ('Accept', 'application/mercurial-0.1')]
294
294
295 q = {"cmd": 'between'}
295 q = {"cmd": 'between'}
296 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
296 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
297 qs = '?%s' % urllib.urlencode(q)
297 qs = '?%s' % urllib.urlencode(q)
298 cu = "%s%s" % (test_uri, qs)
298 cu = "%s%s" % (test_uri, qs)
299 req = urllib2.Request(cu, None, {})
299 req = urllib2.Request(cu, None, {})
300
300
301 try:
301 try:
302 resp = o.open(req)
302 resp = o.open(req)
303 return resp.code == 200
303 return resp.code == 200
304 except Exception, e:
304 except Exception, e:
305 # means it cannot be cloned
305 # means it cannot be cloned
306 raise urllib2.URLError("[%s] %s" % (url, e))
306 raise urllib2.URLError("[%s] %s" % (url, e))
307
307
308 def _get_repo(self, create, src_url=None, update_after_clone=False):
308 def _get_repo(self, create, src_url=None, update_after_clone=False):
309 """
309 """
310 Function will check for mercurial repository in given path and return
310 Function will check for mercurial repository in given path and return
311 a localrepo object. If there is no repository in that path it will
311 a localrepo object. If there is no repository in that path it will
312 raise an exception unless ``create`` parameter is set to True - in
312 raise an exception unless ``create`` parameter is set to True - in
313 that case repository would be created and returned.
313 that case repository would be created and returned.
314 If ``src_url`` is given, would try to clone repository from the
314 If ``src_url`` is given, would try to clone repository from the
315 location at given clone_point. Additionally it'll make update to
315 location at given clone_point. Additionally it'll make update to
316 working copy accordingly to ``update_after_clone`` flag
316 working copy accordingly to ``update_after_clone`` flag
317 """
317 """
318
318
319 try:
319 try:
320 if src_url:
320 if src_url:
321 url = str(self._get_url(src_url))
321 url = str(self._get_url(src_url))
322 opts = {}
322 opts = {}
323 if not update_after_clone:
323 if not update_after_clone:
324 opts.update({'noupdate': True})
324 opts.update({'noupdate': True})
325 try:
325 try:
326 MercurialRepository._check_url(url)
326 MercurialRepository._check_url(url)
327 clone(self.baseui, url, self.path, **opts)
327 clone(self.baseui, url, self.path, **opts)
328 # except urllib2.URLError:
328 # except urllib2.URLError:
329 # raise Abort("Got HTTP 404 error")
329 # raise Abort("Got HTTP 404 error")
330 except Exception:
330 except Exception:
331 raise
331 raise
332
332
333 # Don't try to create if we've already cloned repo
333 # Don't try to create if we've already cloned repo
334 create = False
334 create = False
335 return localrepository(self.baseui, self.path, create=create)
335 return localrepository(self.baseui, self.path, create=create)
336 except (Abort, RepoError), err:
336 except (Abort, RepoError), err:
337 if create:
337 if create:
338 msg = "Cannot create repository at %s. Original error was %s"\
338 msg = "Cannot create repository at %s. Original error was %s"\
339 % (self.path, err)
339 % (self.path, err)
340 else:
340 else:
341 msg = "Not valid repository at %s. Original error was %s"\
341 msg = "Not valid repository at %s. Original error was %s"\
342 % (self.path, err)
342 % (self.path, err)
343 raise RepositoryError(msg)
343 raise RepositoryError(msg)
344
344
345 @LazyProperty
345 @LazyProperty
346 def in_memory_changeset(self):
346 def in_memory_changeset(self):
347 return MercurialInMemoryChangeset(self)
347 return MercurialInMemoryChangeset(self)
348
348
349 @LazyProperty
349 @LazyProperty
350 def description(self):
350 def description(self):
351 undefined_description = u'unknown'
351 undefined_description = u'unknown'
352 return safe_unicode(self._repo.ui.config('web', 'description',
352 return safe_unicode(self._repo.ui.config('web', 'description',
353 undefined_description, untrusted=True))
353 undefined_description, untrusted=True))
354
354
355 @LazyProperty
355 @LazyProperty
356 def contact(self):
356 def contact(self):
357 undefined_contact = u'Unknown'
357 undefined_contact = u'Unknown'
358 return safe_unicode(get_contact(self._repo.ui.config)
358 return safe_unicode(get_contact(self._repo.ui.config)
359 or undefined_contact)
359 or undefined_contact)
360
360
361 @LazyProperty
361 @LazyProperty
362 def last_change(self):
362 def last_change(self):
363 """
363 """
364 Returns last change made on this repository as datetime object
364 Returns last change made on this repository as datetime object
365 """
365 """
366 return date_fromtimestamp(self._get_mtime(), makedate()[1])
366 return date_fromtimestamp(self._get_mtime(), makedate()[1])
367
367
368 def _get_mtime(self):
368 def _get_mtime(self):
369 try:
369 try:
370 return time.mktime(self.get_changeset().date.timetuple())
370 return time.mktime(self.get_changeset().date.timetuple())
371 except RepositoryError:
371 except RepositoryError:
372 #fallback to filesystem
372 #fallback to filesystem
373 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
373 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
374 st_path = os.path.join(self.path, '.hg', "store")
374 st_path = os.path.join(self.path, '.hg', "store")
375 if os.path.exists(cl_path):
375 if os.path.exists(cl_path):
376 return os.stat(cl_path).st_mtime
376 return os.stat(cl_path).st_mtime
377 else:
377 else:
378 return os.stat(st_path).st_mtime
378 return os.stat(st_path).st_mtime
379
379
380 def _get_hidden(self):
380 def _get_hidden(self):
381 return self._repo.ui.configbool("web", "hidden", untrusted=True)
381 return self._repo.ui.configbool("web", "hidden", untrusted=True)
382
382
383 def _get_revision(self, revision):
383 def _get_revision(self, revision):
384 """
384 """
385 Get's an ID revision given as str. This will always return a fill
385 Get's an ID revision given as str. This will always return a fill
386 40 char revision number
386 40 char revision number
387
387
388 :param revision: str or int or None
388 :param revision: str or int or None
389 """
389 """
390
390
391 if self._empty:
391 if self._empty:
392 raise EmptyRepositoryError("There are no changesets yet")
392 raise EmptyRepositoryError("There are no changesets yet")
393
393
394 if revision in [-1, 'tip', None]:
394 if revision in [-1, 'tip', None]:
395 revision = 'tip'
395 revision = 'tip'
396
396
397 try:
397 try:
398 revision = hex(self._repo.lookup(revision))
398 revision = hex(self._repo.lookup(revision))
399 except (IndexError, ValueError, RepoLookupError, TypeError):
399 except (IndexError, ValueError, RepoLookupError, TypeError):
400 raise ChangesetDoesNotExistError("Revision %r does not "
400 raise ChangesetDoesNotExistError("Revision %r does not "
401 "exist for this repository %s" \
401 "exist for this repository %s" \
402 % (revision, self))
402 % (revision, self))
403 return revision
403 return revision
404
404
405 def _get_archives(self, archive_name='tip'):
405 def _get_archives(self, archive_name='tip'):
406 allowed = self.baseui.configlist("web", "allow_archive",
406 allowed = self.baseui.configlist("web", "allow_archive",
407 untrusted=True)
407 untrusted=True)
408 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
408 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
409 if i[0] in allowed or self._repo.ui.configbool("web",
409 if i[0] in allowed or self._repo.ui.configbool("web",
410 "allow" + i[0],
410 "allow" + i[0],
411 untrusted=True):
411 untrusted=True):
412 yield {"type": i[0], "extension": i[1], "node": archive_name}
412 yield {"type": i[0], "extension": i[1], "node": archive_name}
413
413
414 def _get_url(self, url):
414 def _get_url(self, url):
415 """
415 """
416 Returns normalized url. If schema is not given, would fall
416 Returns normalized url. If schema is not given, would fall
417 to filesystem
417 to filesystem
418 (``file:///``) schema.
418 (``file:///``) schema.
419 """
419 """
420 url = str(url)
420 url = str(url)
421 if url != 'default' and not '://' in url:
421 if url != 'default' and not '://' in url:
422 url = "file:" + urllib.pathname2url(url)
422 url = "file:" + urllib.pathname2url(url)
423 return url
423 return url
424
424
425 def get_hook_location(self):
426 """
427 returns absolute path to location where hooks are stored
428 """
429 return os.path.join(self.path, '.hg', '.hgrc')
430
425 def get_changeset(self, revision=None):
431 def get_changeset(self, revision=None):
426 """
432 """
427 Returns ``MercurialChangeset`` object representing repository's
433 Returns ``MercurialChangeset`` object representing repository's
428 changeset at the given ``revision``.
434 changeset at the given ``revision``.
429 """
435 """
430 revision = self._get_revision(revision)
436 revision = self._get_revision(revision)
431 changeset = MercurialChangeset(repository=self, revision=revision)
437 changeset = MercurialChangeset(repository=self, revision=revision)
432 return changeset
438 return changeset
433
439
434 def get_changesets(self, start=None, end=None, start_date=None,
440 def get_changesets(self, start=None, end=None, start_date=None,
435 end_date=None, branch_name=None, reverse=False):
441 end_date=None, branch_name=None, reverse=False):
436 """
442 """
437 Returns iterator of ``MercurialChangeset`` objects from start to end
443 Returns iterator of ``MercurialChangeset`` objects from start to end
438 (both are inclusive)
444 (both are inclusive)
439
445
440 :param start: None, str, int or mercurial lookup format
446 :param start: None, str, int or mercurial lookup format
441 :param end: None, str, int or mercurial lookup format
447 :param end: None, str, int or mercurial lookup format
442 :param start_date:
448 :param start_date:
443 :param end_date:
449 :param end_date:
444 :param branch_name:
450 :param branch_name:
445 :param reversed: return changesets in reversed order
451 :param reversed: return changesets in reversed order
446 """
452 """
447
453
448 start_raw_id = self._get_revision(start)
454 start_raw_id = self._get_revision(start)
449 start_pos = self.revisions.index(start_raw_id) if start else None
455 start_pos = self.revisions.index(start_raw_id) if start else None
450 end_raw_id = self._get_revision(end)
456 end_raw_id = self._get_revision(end)
451 end_pos = self.revisions.index(end_raw_id) if end else None
457 end_pos = self.revisions.index(end_raw_id) if end else None
452
458
453 if None not in [start, end] and start_pos > end_pos:
459 if None not in [start, end] and start_pos > end_pos:
454 raise RepositoryError("start revision '%s' cannot be "
460 raise RepositoryError("start revision '%s' cannot be "
455 "after end revision '%s'" % (start, end))
461 "after end revision '%s'" % (start, end))
456
462
457 if branch_name and branch_name not in self.branches.keys():
463 if branch_name and branch_name not in self.branches.keys():
458 raise BranchDoesNotExistError('Such branch %s does not exists for'
464 raise BranchDoesNotExistError('Such branch %s does not exists for'
459 ' this repository' % branch_name)
465 ' this repository' % branch_name)
460 if end_pos is not None:
466 if end_pos is not None:
461 end_pos += 1
467 end_pos += 1
462
468
463 slice_ = reversed(self.revisions[start_pos:end_pos]) if reverse else \
469 slice_ = reversed(self.revisions[start_pos:end_pos]) if reverse else \
464 self.revisions[start_pos:end_pos]
470 self.revisions[start_pos:end_pos]
465
471
466 for id_ in slice_:
472 for id_ in slice_:
467 cs = self.get_changeset(id_)
473 cs = self.get_changeset(id_)
468 if branch_name and cs.branch != branch_name:
474 if branch_name and cs.branch != branch_name:
469 continue
475 continue
470 if start_date and cs.date < start_date:
476 if start_date and cs.date < start_date:
471 continue
477 continue
472 if end_date and cs.date > end_date:
478 if end_date and cs.date > end_date:
473 continue
479 continue
474
480
475 yield cs
481 yield cs
476
482
477 def pull(self, url):
483 def pull(self, url):
478 """
484 """
479 Tries to pull changes from external location.
485 Tries to pull changes from external location.
480 """
486 """
481 url = self._get_url(url)
487 url = self._get_url(url)
482 try:
488 try:
483 pull(self.baseui, self._repo, url)
489 pull(self.baseui, self._repo, url)
484 except Abort, err:
490 except Abort, err:
485 # Propagate error but with vcs's type
491 # Propagate error but with vcs's type
486 raise RepositoryError(str(err))
492 raise RepositoryError(str(err))
487
493
488 @LazyProperty
494 @LazyProperty
489 def workdir(self):
495 def workdir(self):
490 """
496 """
491 Returns ``Workdir`` instance for this repository.
497 Returns ``Workdir`` instance for this repository.
492 """
498 """
493 return MercurialWorkdir(self)
499 return MercurialWorkdir(self)
494
500
495 def get_config_value(self, section, name, config_file=None):
501 def get_config_value(self, section, name=None, config_file=None):
496 """
502 """
497 Returns configuration value for a given [``section``] and ``name``.
503 Returns configuration value for a given [``section``] and ``name``.
498
504
499 :param section: Section we want to retrieve value from
505 :param section: Section we want to retrieve value from
500 :param name: Name of configuration we want to retrieve
506 :param name: Name of configuration we want to retrieve
501 :param config_file: A path to file which should be used to retrieve
507 :param config_file: A path to file which should be used to retrieve
502 configuration from (might also be a list of file paths)
508 configuration from (might also be a list of file paths)
503 """
509 """
504 if config_file is None:
510 if config_file is None:
505 config_file = []
511 config_file = []
506 elif isinstance(config_file, basestring):
512 elif isinstance(config_file, basestring):
507 config_file = [config_file]
513 config_file = [config_file]
508
514
509 config = self._repo.ui
515 config = self._repo.ui
510 for path in config_file:
516 for path in config_file:
511 config.readconfig(path)
517 config.readconfig(path)
512 return config.config(section, name)
518 return config.config(section, name)
513
519
514 def get_user_name(self, config_file=None):
520 def get_user_name(self, config_file=None):
515 """
521 """
516 Returns user's name from global configuration file.
522 Returns user's name from global configuration file.
517
523
518 :param config_file: A path to file which should be used to retrieve
524 :param config_file: A path to file which should be used to retrieve
519 configuration from (might also be a list of file paths)
525 configuration from (might also be a list of file paths)
520 """
526 """
521 username = self.get_config_value('ui', 'username')
527 username = self.get_config_value('ui', 'username')
522 if username:
528 if username:
523 return author_name(username)
529 return author_name(username)
524 return None
530 return None
525
531
526 def get_user_email(self, config_file=None):
532 def get_user_email(self, config_file=None):
527 """
533 """
528 Returns user's email from global configuration file.
534 Returns user's email from global configuration file.
529
535
530 :param config_file: A path to file which should be used to retrieve
536 :param config_file: A path to file which should be used to retrieve
531 configuration from (might also be a list of file paths)
537 configuration from (might also be a list of file paths)
532 """
538 """
533 username = self.get_config_value('ui', 'username')
539 username = self.get_config_value('ui', 'username')
534 if username:
540 if username:
535 return author_email(username)
541 return author_email(username)
536 return None
542 return None
General Comments 0
You need to be logged in to leave comments. Login now