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