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