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