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