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