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