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