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