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