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