##// END OF EJS Templates
git: disallow odd characters in path of git:// URLs...
Mads Kiilerich -
r8533:a8a51a3b stable
parent child Browse files
Show More
@@ -1,725 +1,737 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.git.repository
3 vcs.backends.git.repository
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Git repository implementation.
6 Git repository 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 errno
12 import errno
13 import logging
13 import logging
14 import os
14 import os
15 import re
15 import re
16 import time
16 import time
17 import urllib.error
17 import urllib.error
18 import urllib.parse
18 import urllib.parse
19 import urllib.request
19 import urllib.request
20 from collections import OrderedDict
20 from collections import OrderedDict
21
21
22 import mercurial.util # import url as hg_url
22 import mercurial.util # import url as hg_url
23 from dulwich.config import ConfigFile
23 from dulwich.config import ConfigFile
24 from dulwich.objects import Tag
24 from dulwich.objects import Tag
25 from dulwich.repo import NotGitRepository, Repo
25 from dulwich.repo import NotGitRepository, Repo
26
26
27 from kallithea.lib.vcs import subprocessio
27 from kallithea.lib.vcs import subprocessio
28 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
28 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
29 from kallithea.lib.vcs.conf import settings
29 from kallithea.lib.vcs.conf import settings
30 from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
30 from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
31 TagDoesNotExistError)
31 TagDoesNotExistError)
32 from kallithea.lib.vcs.utils import ascii_str, date_fromtimestamp, makedate, safe_bytes, safe_str
32 from kallithea.lib.vcs.utils import ascii_str, date_fromtimestamp, makedate, safe_bytes, safe_str
33 from kallithea.lib.vcs.utils.helpers import get_urllib_request_handlers
33 from kallithea.lib.vcs.utils.helpers import get_urllib_request_handlers
34 from kallithea.lib.vcs.utils.lazy import LazyProperty
34 from kallithea.lib.vcs.utils.lazy import LazyProperty
35 from kallithea.lib.vcs.utils.paths import abspath, get_user_home
35 from kallithea.lib.vcs.utils.paths import abspath, get_user_home
36
36
37 from .changeset import GitChangeset
37 from .changeset import GitChangeset
38 from .inmemory import GitInMemoryChangeset
38 from .inmemory import GitInMemoryChangeset
39 from .workdir import GitWorkdir
39 from .workdir import GitWorkdir
40
40
41
41
42 SHA_PATTERN = re.compile(r'^([0-9a-fA-F]{12}|[0-9a-fA-F]{40})$')
42 SHA_PATTERN = re.compile(r'^([0-9a-fA-F]{12}|[0-9a-fA-F]{40})$')
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 class GitRepository(BaseRepository):
47 class GitRepository(BaseRepository):
48 """
48 """
49 Git repository backend.
49 Git repository backend.
50 """
50 """
51 DEFAULT_BRANCH_NAME = 'master'
51 DEFAULT_BRANCH_NAME = 'master'
52 scm = 'git'
52 scm = 'git'
53
53
54 def __init__(self, repo_path, create=False, src_url=None,
54 def __init__(self, repo_path, create=False, src_url=None,
55 update_after_clone=False, bare=False):
55 update_after_clone=False, bare=False):
56
56
57 self.path = abspath(repo_path)
57 self.path = abspath(repo_path)
58 self.repo = self._get_repo(create, src_url, update_after_clone, bare)
58 self.repo = self._get_repo(create, src_url, update_after_clone, bare)
59 self.bare = self.repo.bare
59 self.bare = self.repo.bare
60
60
61 @property
61 @property
62 def _config_files(self):
62 def _config_files(self):
63 return [
63 return [
64 self.bare and abspath(self.path, 'config')
64 self.bare and abspath(self.path, 'config')
65 or abspath(self.path, '.git', 'config'),
65 or abspath(self.path, '.git', 'config'),
66 abspath(get_user_home(), '.gitconfig'),
66 abspath(get_user_home(), '.gitconfig'),
67 ]
67 ]
68
68
69 @property
69 @property
70 def _repo(self):
70 def _repo(self):
71 return self.repo
71 return self.repo
72
72
73 @property
73 @property
74 def head(self):
74 def head(self):
75 try:
75 try:
76 return self._repo.head()
76 return self._repo.head()
77 except KeyError:
77 except KeyError:
78 return None
78 return None
79
79
80 @property
80 @property
81 def _empty(self):
81 def _empty(self):
82 """
82 """
83 Checks if repository is empty ie. without any changesets
83 Checks if repository is empty ie. without any changesets
84 """
84 """
85
85
86 try:
86 try:
87 self.revisions[0]
87 self.revisions[0]
88 except (KeyError, IndexError):
88 except (KeyError, IndexError):
89 return True
89 return True
90 return False
90 return False
91
91
92 @LazyProperty
92 @LazyProperty
93 def revisions(self):
93 def revisions(self):
94 """
94 """
95 Returns list of revisions' ids, in ascending order. Being lazy
95 Returns list of revisions' ids, in ascending order. Being lazy
96 attribute allows external tools to inject shas from cache.
96 attribute allows external tools to inject shas from cache.
97 """
97 """
98 return self._get_all_revisions()
98 return self._get_all_revisions()
99
99
100 @classmethod
100 @classmethod
101 def _run_git_command(cls, cmd, cwd=None):
101 def _run_git_command(cls, cmd, cwd=None):
102 """
102 """
103 Runs given ``cmd`` as git command and returns output bytes in a tuple
103 Runs given ``cmd`` as git command and returns output bytes in a tuple
104 (stdout, stderr) ... or raise RepositoryError.
104 (stdout, stderr) ... or raise RepositoryError.
105
105
106 :param cmd: git command to be executed
106 :param cmd: git command to be executed
107 :param cwd: passed directly to subprocess
107 :param cwd: passed directly to subprocess
108 """
108 """
109 # need to clean fix GIT_DIR !
109 # need to clean fix GIT_DIR !
110 gitenv = dict(os.environ)
110 gitenv = dict(os.environ)
111 gitenv.pop('GIT_DIR', None)
111 gitenv.pop('GIT_DIR', None)
112 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
112 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
113
113
114 assert isinstance(cmd, list), cmd
114 assert isinstance(cmd, list), cmd
115 cmd = [settings.GIT_EXECUTABLE_PATH, '-c', 'core.quotepath=false'] + cmd
115 cmd = [settings.GIT_EXECUTABLE_PATH, '-c', 'core.quotepath=false'] + cmd
116 try:
116 try:
117 p = subprocessio.SubprocessIOChunker(cmd, cwd=cwd, env=gitenv, shell=False)
117 p = subprocessio.SubprocessIOChunker(cmd, cwd=cwd, env=gitenv, shell=False)
118 except (EnvironmentError, OSError) as err:
118 except (EnvironmentError, OSError) as err:
119 # output from the failing process is in str(EnvironmentError)
119 # output from the failing process is in str(EnvironmentError)
120 msg = ("Couldn't run git command %s.\n"
120 msg = ("Couldn't run git command %s.\n"
121 "Subprocess failed with '%s': %s\n" %
121 "Subprocess failed with '%s': %s\n" %
122 (cmd, type(err).__name__, err)
122 (cmd, type(err).__name__, err)
123 ).strip()
123 ).strip()
124 log.error(msg)
124 log.error(msg)
125 raise RepositoryError(msg)
125 raise RepositoryError(msg)
126
126
127 try:
127 try:
128 stdout = b''.join(p.output)
128 stdout = b''.join(p.output)
129 stderr = b''.join(p.error)
129 stderr = b''.join(p.error)
130 finally:
130 finally:
131 p.close()
131 p.close()
132 # TODO: introduce option to make commands fail if they have any stderr output?
132 # TODO: introduce option to make commands fail if they have any stderr output?
133 if stderr:
133 if stderr:
134 log.debug('stderr from %s:\n%s', cmd, stderr)
134 log.debug('stderr from %s:\n%s', cmd, stderr)
135 else:
135 else:
136 log.debug('stderr from %s: None', cmd)
136 log.debug('stderr from %s: None', cmd)
137 return stdout, stderr
137 return stdout, stderr
138
138
139 def run_git_command(self, cmd):
139 def run_git_command(self, cmd):
140 """
140 """
141 Runs given ``cmd`` as git command with cwd set to current repo.
141 Runs given ``cmd`` as git command with cwd set to current repo.
142 Returns stdout as unicode str ... or raise RepositoryError.
142 Returns stdout as unicode str ... or raise RepositoryError.
143 """
143 """
144 cwd = None
144 cwd = None
145 if os.path.isdir(self.path):
145 if os.path.isdir(self.path):
146 cwd = self.path
146 cwd = self.path
147 stdout, _stderr = self._run_git_command(cmd, cwd=cwd)
147 stdout, _stderr = self._run_git_command(cmd, cwd=cwd)
148 return safe_str(stdout)
148 return safe_str(stdout)
149
149
150 @classmethod
150 @classmethod
151 def _check_url(cls, url):
151 def _check_url(cls, url):
152 """
152 """
153 Function will check given url and try to verify if it's a valid
153 Function will check given url and try to verify if it's a valid
154 link. Sometimes it may happened that git will issue basic
154 link. Sometimes it may happened that git will issue basic
155 auth request that can cause whole API to hang when used from python
155 auth request that can cause whole API to hang when used from python
156 or other external calls.
156 or other external calls.
157
157
158 On failures it'll raise urllib2.HTTPError, exception is also thrown
158 On failures it'll raise urllib2.HTTPError, exception is also thrown
159 when the return code is non 200
159 when the return code is non 200
160 """
160 """
161 # check first if it's not an local url
161 # check first if it's not an local url
162 if os.path.isabs(url) and os.path.isdir(url):
162 if os.path.isabs(url) and os.path.isdir(url):
163 return True
163 return True
164
164
165 if url.startswith('git://'):
165 if url.startswith('git://'):
166 try:
167 _git_colon, _empty, _host, path = url.split('/', 3)
168 except ValueError:
169 raise urllib.error.URLError("Invalid URL: %r" % url)
170 # Mitigate problems elsewhere with incorrect handling of encoded paths.
171 # Don't trust urllib.parse.unquote but be prepared for more flexible implementations elsewhere.
172 # Space is the only allowed whitespace character - directly or % encoded. No other % or \ is allowed.
173 for c in path.replace('%20', ' '):
174 if c in '%\\':
175 raise urllib.error.URLError("Invalid escape character in path: '%s'" % c)
176 if c.isspace() and c != ' ':
177 raise urllib.error.URLError("Invalid whitespace character in path: %r" % c)
166 return True
178 return True
167
179
168 if not url.startswith('http://') and not url.startswith('https://'):
180 if not url.startswith('http://') and not url.startswith('https://'):
169 raise urllib.error.URLError("Unsupported protocol in URL %s" % url)
181 raise urllib.error.URLError("Unsupported protocol in URL %s" % url)
170
182
171 url_obj = mercurial.util.url(safe_bytes(url))
183 url_obj = mercurial.util.url(safe_bytes(url))
172 test_uri, handlers = get_urllib_request_handlers(url_obj)
184 test_uri, handlers = get_urllib_request_handlers(url_obj)
173 if not test_uri.endswith(b'info/refs'):
185 if not test_uri.endswith(b'info/refs'):
174 test_uri = test_uri.rstrip(b'/') + b'/info/refs'
186 test_uri = test_uri.rstrip(b'/') + b'/info/refs'
175
187
176 url_obj.passwd = b'*****'
188 url_obj.passwd = b'*****'
177 cleaned_uri = str(url_obj)
189 cleaned_uri = str(url_obj)
178
190
179 o = urllib.request.build_opener(*handlers)
191 o = urllib.request.build_opener(*handlers)
180 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
192 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
181
193
182 req = urllib.request.Request(
194 req = urllib.request.Request(
183 "%s?%s" % (
195 "%s?%s" % (
184 safe_str(test_uri),
196 safe_str(test_uri),
185 urllib.parse.urlencode({"service": 'git-upload-pack'})
197 urllib.parse.urlencode({"service": 'git-upload-pack'})
186 ))
198 ))
187
199
188 try:
200 try:
189 resp = o.open(req)
201 resp = o.open(req)
190 if resp.code != 200:
202 if resp.code != 200:
191 raise Exception('Return Code is not 200')
203 raise Exception('Return Code is not 200')
192 except Exception as e:
204 except Exception as e:
193 # means it cannot be cloned
205 # means it cannot be cloned
194 raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
206 raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
195
207
196 # now detect if it's proper git repo
208 # now detect if it's proper git repo
197 gitdata = resp.read()
209 gitdata = resp.read()
198 if b'service=git-upload-pack' not in gitdata:
210 if b'service=git-upload-pack' not in gitdata:
199 raise urllib.error.URLError(
211 raise urllib.error.URLError(
200 "url [%s] does not look like an git" % cleaned_uri)
212 "url [%s] does not look like an git" % cleaned_uri)
201
213
202 return True
214 return True
203
215
204 def _get_repo(self, create, src_url=None, update_after_clone=False,
216 def _get_repo(self, create, src_url=None, update_after_clone=False,
205 bare=False):
217 bare=False):
206 if create and os.path.exists(self.path):
218 if create and os.path.exists(self.path):
207 raise RepositoryError("Location already exist")
219 raise RepositoryError("Location already exist")
208 if src_url and not create:
220 if src_url and not create:
209 raise RepositoryError("Create should be set to True if src_url is "
221 raise RepositoryError("Create should be set to True if src_url is "
210 "given (clone operation creates repository)")
222 "given (clone operation creates repository)")
211 try:
223 try:
212 if create and src_url:
224 if create and src_url:
213 GitRepository._check_url(src_url)
225 GitRepository._check_url(src_url)
214 self.clone(src_url, update_after_clone, bare)
226 self.clone(src_url, update_after_clone, bare)
215 return Repo(self.path)
227 return Repo(self.path)
216 elif create:
228 elif create:
217 os.makedirs(self.path)
229 os.makedirs(self.path)
218 if bare:
230 if bare:
219 return Repo.init_bare(self.path)
231 return Repo.init_bare(self.path)
220 else:
232 else:
221 return Repo.init(self.path)
233 return Repo.init(self.path)
222 else:
234 else:
223 return Repo(self.path)
235 return Repo(self.path)
224 except (NotGitRepository, OSError) as err:
236 except (NotGitRepository, OSError) as err:
225 raise RepositoryError(err)
237 raise RepositoryError(err)
226
238
227 def _get_all_revisions(self):
239 def _get_all_revisions(self):
228 # we must check if this repo is not empty, since later command
240 # we must check if this repo is not empty, since later command
229 # fails if it is. And it's cheaper to ask than throw the subprocess
241 # fails if it is. And it's cheaper to ask than throw the subprocess
230 # errors
242 # errors
231 try:
243 try:
232 self._repo.head()
244 self._repo.head()
233 except KeyError:
245 except KeyError:
234 return []
246 return []
235
247
236 rev_filter = settings.GIT_REV_FILTER
248 rev_filter = settings.GIT_REV_FILTER
237 cmd = ['rev-list', rev_filter, '--reverse', '--date-order']
249 cmd = ['rev-list', rev_filter, '--reverse', '--date-order']
238 try:
250 try:
239 so = self.run_git_command(cmd)
251 so = self.run_git_command(cmd)
240 except RepositoryError:
252 except RepositoryError:
241 # Can be raised for empty repositories
253 # Can be raised for empty repositories
242 return []
254 return []
243 return so.splitlines()
255 return so.splitlines()
244
256
245 def _get_all_revisions2(self):
257 def _get_all_revisions2(self):
246 # alternate implementation using dulwich
258 # alternate implementation using dulwich
247 includes = [ascii_str(sha) for key, (sha, type_) in self._parsed_refs.items()
259 includes = [ascii_str(sha) for key, (sha, type_) in self._parsed_refs.items()
248 if type_ != b'T']
260 if type_ != b'T']
249 return [c.commit.id for c in self._repo.get_walker(include=includes)]
261 return [c.commit.id for c in self._repo.get_walker(include=includes)]
250
262
251 def _get_revision(self, revision):
263 def _get_revision(self, revision):
252 """
264 """
253 Given any revision identifier, returns a 40 char string with revision hash.
265 Given any revision identifier, returns a 40 char string with revision hash.
254 """
266 """
255 if self._empty:
267 if self._empty:
256 raise EmptyRepositoryError("There are no changesets yet")
268 raise EmptyRepositoryError("There are no changesets yet")
257
269
258 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
270 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
259 revision = -1
271 revision = -1
260
272
261 if isinstance(revision, int):
273 if isinstance(revision, int):
262 try:
274 try:
263 return self.revisions[revision]
275 return self.revisions[revision]
264 except IndexError:
276 except IndexError:
265 msg = "Revision %r does not exist for %s" % (revision, self.name)
277 msg = "Revision %r does not exist for %s" % (revision, self.name)
266 raise ChangesetDoesNotExistError(msg)
278 raise ChangesetDoesNotExistError(msg)
267
279
268 if isinstance(revision, str):
280 if isinstance(revision, str):
269 if revision.isdigit() and (len(revision) < 12 or len(revision) == revision.count('0')):
281 if revision.isdigit() and (len(revision) < 12 or len(revision) == revision.count('0')):
270 try:
282 try:
271 return self.revisions[int(revision)]
283 return self.revisions[int(revision)]
272 except IndexError:
284 except IndexError:
273 msg = "Revision %r does not exist for %s" % (revision, self)
285 msg = "Revision %r does not exist for %s" % (revision, self)
274 raise ChangesetDoesNotExistError(msg)
286 raise ChangesetDoesNotExistError(msg)
275
287
276 # get by branch/tag name
288 # get by branch/tag name
277 _ref_revision = self._parsed_refs.get(safe_bytes(revision))
289 _ref_revision = self._parsed_refs.get(safe_bytes(revision))
278 if _ref_revision: # and _ref_revision[1] in [b'H', b'RH', b'T']:
290 if _ref_revision: # and _ref_revision[1] in [b'H', b'RH', b'T']:
279 return ascii_str(_ref_revision[0])
291 return ascii_str(_ref_revision[0])
280
292
281 if revision in self.revisions:
293 if revision in self.revisions:
282 return revision
294 return revision
283
295
284 # maybe it's a tag ? we don't have them in self.revisions
296 # maybe it's a tag ? we don't have them in self.revisions
285 if revision in self.tags.values():
297 if revision in self.tags.values():
286 return revision
298 return revision
287
299
288 if SHA_PATTERN.match(revision):
300 if SHA_PATTERN.match(revision):
289 msg = "Revision %r does not exist for %s" % (revision, self.name)
301 msg = "Revision %r does not exist for %s" % (revision, self.name)
290 raise ChangesetDoesNotExistError(msg)
302 raise ChangesetDoesNotExistError(msg)
291
303
292 raise ChangesetDoesNotExistError("Given revision %r not recognized" % revision)
304 raise ChangesetDoesNotExistError("Given revision %r not recognized" % revision)
293
305
294 def get_ref_revision(self, ref_type, ref_name):
306 def get_ref_revision(self, ref_type, ref_name):
295 """
307 """
296 Returns ``GitChangeset`` object representing repository's
308 Returns ``GitChangeset`` object representing repository's
297 changeset at the given ``revision``.
309 changeset at the given ``revision``.
298 """
310 """
299 return self._get_revision(ref_name)
311 return self._get_revision(ref_name)
300
312
301 def _get_archives(self, archive_name='tip'):
313 def _get_archives(self, archive_name='tip'):
302
314
303 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
315 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
304 yield {"type": i[0], "extension": i[1], "node": archive_name}
316 yield {"type": i[0], "extension": i[1], "node": archive_name}
305
317
306 def _get_url(self, url):
318 def _get_url(self, url):
307 """
319 """
308 Returns normalized url. If schema is not given, would fall to
320 Returns normalized url. If schema is not given, would fall to
309 filesystem (``file:///``) schema.
321 filesystem (``file:///``) schema.
310 """
322 """
311 if url != 'default' and '://' not in url:
323 if url != 'default' and '://' not in url:
312 url = ':///'.join(('file', url))
324 url = ':///'.join(('file', url))
313 return url
325 return url
314
326
315 @LazyProperty
327 @LazyProperty
316 def name(self):
328 def name(self):
317 return os.path.basename(self.path)
329 return os.path.basename(self.path)
318
330
319 @LazyProperty
331 @LazyProperty
320 def last_change(self):
332 def last_change(self):
321 """
333 """
322 Returns last change made on this repository as datetime object
334 Returns last change made on this repository as datetime object
323 """
335 """
324 return date_fromtimestamp(self._get_mtime(), makedate()[1])
336 return date_fromtimestamp(self._get_mtime(), makedate()[1])
325
337
326 def _get_mtime(self):
338 def _get_mtime(self):
327 try:
339 try:
328 return time.mktime(self.get_changeset().date.timetuple())
340 return time.mktime(self.get_changeset().date.timetuple())
329 except RepositoryError:
341 except RepositoryError:
330 idx_loc = '' if self.bare else '.git'
342 idx_loc = '' if self.bare else '.git'
331 # fallback to filesystem
343 # fallback to filesystem
332 in_path = os.path.join(self.path, idx_loc, "index")
344 in_path = os.path.join(self.path, idx_loc, "index")
333 he_path = os.path.join(self.path, idx_loc, "HEAD")
345 he_path = os.path.join(self.path, idx_loc, "HEAD")
334 if os.path.exists(in_path):
346 if os.path.exists(in_path):
335 return os.stat(in_path).st_mtime
347 return os.stat(in_path).st_mtime
336 else:
348 else:
337 return os.stat(he_path).st_mtime
349 return os.stat(he_path).st_mtime
338
350
339 @LazyProperty
351 @LazyProperty
340 def description(self):
352 def description(self):
341 return safe_str(self._repo.get_description() or b'unknown')
353 return safe_str(self._repo.get_description() or b'unknown')
342
354
343 @LazyProperty
355 @LazyProperty
344 def contact(self):
356 def contact(self):
345 undefined_contact = 'Unknown'
357 undefined_contact = 'Unknown'
346 return undefined_contact
358 return undefined_contact
347
359
348 @property
360 @property
349 def branches(self):
361 def branches(self):
350 if not self.revisions:
362 if not self.revisions:
351 return {}
363 return {}
352 _branches = [(safe_str(key), ascii_str(sha))
364 _branches = [(safe_str(key), ascii_str(sha))
353 for key, (sha, type_) in self._parsed_refs.items() if type_ == b'H']
365 for key, (sha, type_) in self._parsed_refs.items() if type_ == b'H']
354 return OrderedDict(sorted(_branches, key=(lambda ctx: ctx[0]), reverse=False))
366 return OrderedDict(sorted(_branches, key=(lambda ctx: ctx[0]), reverse=False))
355
367
356 @LazyProperty
368 @LazyProperty
357 def closed_branches(self):
369 def closed_branches(self):
358 return {}
370 return {}
359
371
360 @LazyProperty
372 @LazyProperty
361 def tags(self):
373 def tags(self):
362 return self._get_tags()
374 return self._get_tags()
363
375
364 def _get_tags(self):
376 def _get_tags(self):
365 if not self.revisions:
377 if not self.revisions:
366 return {}
378 return {}
367 _tags = [(safe_str(key), ascii_str(sha))
379 _tags = [(safe_str(key), ascii_str(sha))
368 for key, (sha, type_) in self._parsed_refs.items() if type_ == b'T']
380 for key, (sha, type_) in self._parsed_refs.items() if type_ == b'T']
369 return OrderedDict(sorted(_tags, key=(lambda ctx: ctx[0]), reverse=True))
381 return OrderedDict(sorted(_tags, key=(lambda ctx: ctx[0]), reverse=True))
370
382
371 def tag(self, name, user, revision=None, message=None, date=None,
383 def tag(self, name, user, revision=None, message=None, date=None,
372 **kwargs):
384 **kwargs):
373 """
385 """
374 Creates and returns a tag for the given ``revision``.
386 Creates and returns a tag for the given ``revision``.
375
387
376 :param name: name for new tag
388 :param name: name for new tag
377 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
389 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
378 :param revision: changeset id for which new tag would be created
390 :param revision: changeset id for which new tag would be created
379 :param message: message of the tag's commit
391 :param message: message of the tag's commit
380 :param date: date of tag's commit
392 :param date: date of tag's commit
381
393
382 :raises TagAlreadyExistError: if tag with same name already exists
394 :raises TagAlreadyExistError: if tag with same name already exists
383 """
395 """
384 if name in self.tags:
396 if name in self.tags:
385 raise TagAlreadyExistError("Tag %s already exists" % name)
397 raise TagAlreadyExistError("Tag %s already exists" % name)
386 changeset = self.get_changeset(revision)
398 changeset = self.get_changeset(revision)
387 message = message or "Added tag %s for commit %s" % (name,
399 message = message or "Added tag %s for commit %s" % (name,
388 changeset.raw_id)
400 changeset.raw_id)
389 self._repo.refs[b"refs/tags/%s" % safe_bytes(name)] = changeset._commit.id
401 self._repo.refs[b"refs/tags/%s" % safe_bytes(name)] = changeset._commit.id
390
402
391 self._parsed_refs = self._get_parsed_refs()
403 self._parsed_refs = self._get_parsed_refs()
392 self.tags = self._get_tags()
404 self.tags = self._get_tags()
393 return changeset
405 return changeset
394
406
395 def remove_tag(self, name, user, message=None, date=None):
407 def remove_tag(self, name, user, message=None, date=None):
396 """
408 """
397 Removes tag with the given ``name``.
409 Removes tag with the given ``name``.
398
410
399 :param name: name of the tag to be removed
411 :param name: name of the tag to be removed
400 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
412 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
401 :param message: message of the tag's removal commit
413 :param message: message of the tag's removal commit
402 :param date: date of tag's removal commit
414 :param date: date of tag's removal commit
403
415
404 :raises TagDoesNotExistError: if tag with given name does not exists
416 :raises TagDoesNotExistError: if tag with given name does not exists
405 """
417 """
406 if name not in self.tags:
418 if name not in self.tags:
407 raise TagDoesNotExistError("Tag %s does not exist" % name)
419 raise TagDoesNotExistError("Tag %s does not exist" % name)
408 # self._repo.refs is a DiskRefsContainer, and .path gives the full absolute path of '.git'
420 # self._repo.refs is a DiskRefsContainer, and .path gives the full absolute path of '.git'
409 tagpath = os.path.join(safe_str(self._repo.refs.path), 'refs', 'tags', name)
421 tagpath = os.path.join(safe_str(self._repo.refs.path), 'refs', 'tags', name)
410 try:
422 try:
411 os.remove(tagpath)
423 os.remove(tagpath)
412 self._parsed_refs = self._get_parsed_refs()
424 self._parsed_refs = self._get_parsed_refs()
413 self.tags = self._get_tags()
425 self.tags = self._get_tags()
414 except OSError as e:
426 except OSError as e:
415 raise RepositoryError(e.strerror)
427 raise RepositoryError(e.strerror)
416
428
417 @LazyProperty
429 @LazyProperty
418 def bookmarks(self):
430 def bookmarks(self):
419 """
431 """
420 Gets bookmarks for this repository
432 Gets bookmarks for this repository
421 """
433 """
422 return {}
434 return {}
423
435
424 @LazyProperty
436 @LazyProperty
425 def _parsed_refs(self):
437 def _parsed_refs(self):
426 return self._get_parsed_refs()
438 return self._get_parsed_refs()
427
439
428 def _get_parsed_refs(self):
440 def _get_parsed_refs(self):
429 """Return refs as a dict, like:
441 """Return refs as a dict, like:
430 { b'v0.2.0': [b'599ba911aa24d2981225f3966eb659dfae9e9f30', b'T'] }
442 { b'v0.2.0': [b'599ba911aa24d2981225f3966eb659dfae9e9f30', b'T'] }
431 """
443 """
432 _repo = self._repo
444 _repo = self._repo
433 refs = _repo.get_refs()
445 refs = _repo.get_refs()
434 keys = [(b'refs/heads/', b'H'),
446 keys = [(b'refs/heads/', b'H'),
435 (b'refs/remotes/origin/', b'RH'),
447 (b'refs/remotes/origin/', b'RH'),
436 (b'refs/tags/', b'T')]
448 (b'refs/tags/', b'T')]
437 _refs = {}
449 _refs = {}
438 for ref, sha in refs.items():
450 for ref, sha in refs.items():
439 for k, type_ in keys:
451 for k, type_ in keys:
440 if ref.startswith(k):
452 if ref.startswith(k):
441 _key = ref[len(k):]
453 _key = ref[len(k):]
442 if type_ == b'T':
454 if type_ == b'T':
443 obj = _repo.get_object(sha)
455 obj = _repo.get_object(sha)
444 if isinstance(obj, Tag):
456 if isinstance(obj, Tag):
445 sha = _repo.get_object(sha).object[1]
457 sha = _repo.get_object(sha).object[1]
446 _refs[_key] = [sha, type_]
458 _refs[_key] = [sha, type_]
447 break
459 break
448 return _refs
460 return _refs
449
461
450 def _heads(self, reverse=False):
462 def _heads(self, reverse=False):
451 refs = self._repo.get_refs()
463 refs = self._repo.get_refs()
452 heads = {}
464 heads = {}
453
465
454 for key, val in refs.items():
466 for key, val in refs.items():
455 for ref_key in [b'refs/heads/', b'refs/remotes/origin/']:
467 for ref_key in [b'refs/heads/', b'refs/remotes/origin/']:
456 if key.startswith(ref_key):
468 if key.startswith(ref_key):
457 n = key[len(ref_key):]
469 n = key[len(ref_key):]
458 if n not in [b'HEAD']:
470 if n not in [b'HEAD']:
459 heads[n] = val
471 heads[n] = val
460
472
461 return heads if reverse else dict((y, x) for x, y in heads.items())
473 return heads if reverse else dict((y, x) for x, y in heads.items())
462
474
463 def get_changeset(self, revision=None):
475 def get_changeset(self, revision=None):
464 """
476 """
465 Returns ``GitChangeset`` object representing commit from git repository
477 Returns ``GitChangeset`` object representing commit from git repository
466 at the given revision or head (most recent commit) if None given.
478 at the given revision or head (most recent commit) if None given.
467 """
479 """
468 if isinstance(revision, GitChangeset):
480 if isinstance(revision, GitChangeset):
469 return revision
481 return revision
470 return GitChangeset(repository=self, revision=self._get_revision(revision))
482 return GitChangeset(repository=self, revision=self._get_revision(revision))
471
483
472 def get_changesets(self, start=None, end=None, start_date=None,
484 def get_changesets(self, start=None, end=None, start_date=None,
473 end_date=None, branch_name=None, reverse=False, max_revisions=None):
485 end_date=None, branch_name=None, reverse=False, max_revisions=None):
474 """
486 """
475 Returns iterator of ``GitChangeset`` objects from start to end (both
487 Returns iterator of ``GitChangeset`` objects from start to end (both
476 are inclusive), in ascending date order (unless ``reverse`` is set).
488 are inclusive), in ascending date order (unless ``reverse`` is set).
477
489
478 :param start: changeset ID, as str; first returned changeset
490 :param start: changeset ID, as str; first returned changeset
479 :param end: changeset ID, as str; last returned changeset
491 :param end: changeset ID, as str; last returned changeset
480 :param start_date: if specified, changesets with commit date less than
492 :param start_date: if specified, changesets with commit date less than
481 ``start_date`` would be filtered out from returned set
493 ``start_date`` would be filtered out from returned set
482 :param end_date: if specified, changesets with commit date greater than
494 :param end_date: if specified, changesets with commit date greater than
483 ``end_date`` would be filtered out from returned set
495 ``end_date`` would be filtered out from returned set
484 :param branch_name: if specified, changesets not reachable from given
496 :param branch_name: if specified, changesets not reachable from given
485 branch would be filtered out from returned set
497 branch would be filtered out from returned set
486 :param reverse: if ``True``, returned generator would be reversed
498 :param reverse: if ``True``, returned generator would be reversed
487 (meaning that returned changesets would have descending date order)
499 (meaning that returned changesets would have descending date order)
488
500
489 :raise BranchDoesNotExistError: If given ``branch_name`` does not
501 :raise BranchDoesNotExistError: If given ``branch_name`` does not
490 exist.
502 exist.
491 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
503 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
492 ``end`` could not be found.
504 ``end`` could not be found.
493
505
494 """
506 """
495 if branch_name and branch_name not in self.branches:
507 if branch_name and branch_name not in self.branches:
496 raise BranchDoesNotExistError("Branch '%s' not found"
508 raise BranchDoesNotExistError("Branch '%s' not found"
497 % branch_name)
509 % branch_name)
498 # actually we should check now if it's not an empty repo to not spaw
510 # actually we should check now if it's not an empty repo to not spaw
499 # subprocess commands
511 # subprocess commands
500 if self._empty:
512 if self._empty:
501 raise EmptyRepositoryError("There are no changesets yet")
513 raise EmptyRepositoryError("There are no changesets yet")
502
514
503 # %H at format means (full) commit hash, initial hashes are retrieved
515 # %H at format means (full) commit hash, initial hashes are retrieved
504 # in ascending date order
516 # in ascending date order
505 cmd = ['log', '--date-order', '--reverse', '--pretty=format:%H']
517 cmd = ['log', '--date-order', '--reverse', '--pretty=format:%H']
506 if max_revisions:
518 if max_revisions:
507 cmd += ['--max-count=%s' % max_revisions]
519 cmd += ['--max-count=%s' % max_revisions]
508 if start_date:
520 if start_date:
509 cmd += ['--since', start_date.strftime('%m/%d/%y %H:%M:%S')]
521 cmd += ['--since', start_date.strftime('%m/%d/%y %H:%M:%S')]
510 if end_date:
522 if end_date:
511 cmd += ['--until', end_date.strftime('%m/%d/%y %H:%M:%S')]
523 cmd += ['--until', end_date.strftime('%m/%d/%y %H:%M:%S')]
512 if branch_name:
524 if branch_name:
513 cmd.append(branch_name)
525 cmd.append(branch_name)
514 else:
526 else:
515 cmd.append(settings.GIT_REV_FILTER)
527 cmd.append(settings.GIT_REV_FILTER)
516
528
517 revs = self.run_git_command(cmd).splitlines()
529 revs = self.run_git_command(cmd).splitlines()
518 start_pos = 0
530 start_pos = 0
519 end_pos = len(revs)
531 end_pos = len(revs)
520 if start:
532 if start:
521 _start = self._get_revision(start)
533 _start = self._get_revision(start)
522 try:
534 try:
523 start_pos = revs.index(_start)
535 start_pos = revs.index(_start)
524 except ValueError:
536 except ValueError:
525 pass
537 pass
526
538
527 if end is not None:
539 if end is not None:
528 _end = self._get_revision(end)
540 _end = self._get_revision(end)
529 try:
541 try:
530 end_pos = revs.index(_end)
542 end_pos = revs.index(_end)
531 except ValueError:
543 except ValueError:
532 pass
544 pass
533
545
534 if None not in [start, end] and start_pos > end_pos:
546 if None not in [start, end] and start_pos > end_pos:
535 raise RepositoryError('start cannot be after end')
547 raise RepositoryError('start cannot be after end')
536
548
537 if end_pos is not None:
549 if end_pos is not None:
538 end_pos += 1
550 end_pos += 1
539
551
540 revs = revs[start_pos:end_pos]
552 revs = revs[start_pos:end_pos]
541 if reverse:
553 if reverse:
542 revs.reverse()
554 revs.reverse()
543
555
544 return CollectionGenerator(self, revs)
556 return CollectionGenerator(self, revs)
545
557
546 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
558 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
547 context=3):
559 context=3):
548 """
560 """
549 Returns (git like) *diff*, as plain bytes text. Shows changes
561 Returns (git like) *diff*, as plain bytes text. Shows changes
550 introduced by ``rev2`` since ``rev1``.
562 introduced by ``rev2`` since ``rev1``.
551
563
552 :param rev1: Entry point from which diff is shown. Can be
564 :param rev1: Entry point from which diff is shown. Can be
553 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
565 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
554 the changes since empty state of the repository until ``rev2``
566 the changes since empty state of the repository until ``rev2``
555 :param rev2: Until which revision changes should be shown.
567 :param rev2: Until which revision changes should be shown.
556 :param ignore_whitespace: If set to ``True``, would not show whitespace
568 :param ignore_whitespace: If set to ``True``, would not show whitespace
557 changes. Defaults to ``False``.
569 changes. Defaults to ``False``.
558 :param context: How many lines before/after changed lines should be
570 :param context: How many lines before/after changed lines should be
559 shown. Defaults to ``3``. Due to limitations in Git, if
571 shown. Defaults to ``3``. Due to limitations in Git, if
560 value passed-in is greater than ``2**31-1``
572 value passed-in is greater than ``2**31-1``
561 (``2147483647``), it will be set to ``2147483647``
573 (``2147483647``), it will be set to ``2147483647``
562 instead. If negative value is passed-in, it will be set to
574 instead. If negative value is passed-in, it will be set to
563 ``0`` instead.
575 ``0`` instead.
564 """
576 """
565
577
566 # Git internally uses a signed long int for storing context
578 # Git internally uses a signed long int for storing context
567 # size (number of lines to show before and after the
579 # size (number of lines to show before and after the
568 # differences). This can result in integer overflow, so we
580 # differences). This can result in integer overflow, so we
569 # ensure the requested context is smaller by one than the
581 # ensure the requested context is smaller by one than the
570 # number that would cause the overflow. It is highly unlikely
582 # number that would cause the overflow. It is highly unlikely
571 # that a single file will contain that many lines, so this
583 # that a single file will contain that many lines, so this
572 # kind of change should not cause any realistic consequences.
584 # kind of change should not cause any realistic consequences.
573 overflowed_long_int = 2**31
585 overflowed_long_int = 2**31
574
586
575 if context >= overflowed_long_int:
587 if context >= overflowed_long_int:
576 context = overflowed_long_int - 1
588 context = overflowed_long_int - 1
577
589
578 # Negative context values make no sense, and will result in
590 # Negative context values make no sense, and will result in
579 # errors. Ensure this does not happen.
591 # errors. Ensure this does not happen.
580 if context < 0:
592 if context < 0:
581 context = 0
593 context = 0
582
594
583 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
595 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
584 if ignore_whitespace:
596 if ignore_whitespace:
585 flags.append('-w')
597 flags.append('-w')
586
598
587 if hasattr(rev1, 'raw_id'):
599 if hasattr(rev1, 'raw_id'):
588 rev1 = getattr(rev1, 'raw_id')
600 rev1 = getattr(rev1, 'raw_id')
589
601
590 if hasattr(rev2, 'raw_id'):
602 if hasattr(rev2, 'raw_id'):
591 rev2 = getattr(rev2, 'raw_id')
603 rev2 = getattr(rev2, 'raw_id')
592
604
593 if rev1 == self.EMPTY_CHANGESET:
605 if rev1 == self.EMPTY_CHANGESET:
594 rev2 = self.get_changeset(rev2).raw_id
606 rev2 = self.get_changeset(rev2).raw_id
595 cmd = ['show'] + flags + [rev2]
607 cmd = ['show'] + flags + [rev2]
596 else:
608 else:
597 rev1 = self.get_changeset(rev1).raw_id
609 rev1 = self.get_changeset(rev1).raw_id
598 rev2 = self.get_changeset(rev2).raw_id
610 rev2 = self.get_changeset(rev2).raw_id
599 cmd = ['diff'] + flags + [rev1, rev2]
611 cmd = ['diff'] + flags + [rev1, rev2]
600
612
601 if path:
613 if path:
602 cmd += ['--', path]
614 cmd += ['--', path]
603
615
604 stdout, stderr = self._run_git_command(cmd, cwd=self.path)
616 stdout, stderr = self._run_git_command(cmd, cwd=self.path)
605 # If we used 'show' command, strip first few lines (until actual diff
617 # If we used 'show' command, strip first few lines (until actual diff
606 # starts)
618 # starts)
607 if rev1 == self.EMPTY_CHANGESET:
619 if rev1 == self.EMPTY_CHANGESET:
608 parts = stdout.split(b'\ndiff ', 1)
620 parts = stdout.split(b'\ndiff ', 1)
609 if len(parts) > 1:
621 if len(parts) > 1:
610 stdout = b'diff ' + parts[1]
622 stdout = b'diff ' + parts[1]
611 return stdout
623 return stdout
612
624
613 @LazyProperty
625 @LazyProperty
614 def in_memory_changeset(self):
626 def in_memory_changeset(self):
615 """
627 """
616 Returns ``GitInMemoryChangeset`` object for this repository.
628 Returns ``GitInMemoryChangeset`` object for this repository.
617 """
629 """
618 return GitInMemoryChangeset(self)
630 return GitInMemoryChangeset(self)
619
631
620 def clone(self, url, update_after_clone=True, bare=False):
632 def clone(self, url, update_after_clone=True, bare=False):
621 """
633 """
622 Tries to clone changes from external location.
634 Tries to clone changes from external location.
623
635
624 :param update_after_clone: If set to ``False``, git won't checkout
636 :param update_after_clone: If set to ``False``, git won't checkout
625 working directory
637 working directory
626 :param bare: If set to ``True``, repository would be cloned into
638 :param bare: If set to ``True``, repository would be cloned into
627 *bare* git repository (no working directory at all).
639 *bare* git repository (no working directory at all).
628 """
640 """
629 url = self._get_url(url)
641 url = self._get_url(url)
630 cmd = ['clone', '-q']
642 cmd = ['clone', '-q']
631 if bare:
643 if bare:
632 cmd.append('--bare')
644 cmd.append('--bare')
633 elif not update_after_clone:
645 elif not update_after_clone:
634 cmd.append('--no-checkout')
646 cmd.append('--no-checkout')
635 cmd += ['--', url, self.path]
647 cmd += ['--', url, self.path]
636 # If error occurs run_git_command raises RepositoryError already
648 # If error occurs run_git_command raises RepositoryError already
637 self.run_git_command(cmd)
649 self.run_git_command(cmd)
638
650
639 def pull(self, url):
651 def pull(self, url):
640 """
652 """
641 Tries to pull changes from external location.
653 Tries to pull changes from external location.
642 """
654 """
643 url = self._get_url(url)
655 url = self._get_url(url)
644 cmd = ['pull', '--ff-only', url]
656 cmd = ['pull', '--ff-only', url]
645 # If error occurs run_git_command raises RepositoryError already
657 # If error occurs run_git_command raises RepositoryError already
646 self.run_git_command(cmd)
658 self.run_git_command(cmd)
647
659
648 def fetch(self, url):
660 def fetch(self, url):
649 """
661 """
650 Tries to pull changes from external location.
662 Tries to pull changes from external location.
651 """
663 """
652 url = self._get_url(url)
664 url = self._get_url(url)
653 so = self.run_git_command(['ls-remote', '-h', url])
665 so = self.run_git_command(['ls-remote', '-h', url])
654 cmd = ['fetch', url, '--']
666 cmd = ['fetch', url, '--']
655 for line in (x for x in so.splitlines()):
667 for line in (x for x in so.splitlines()):
656 sha, ref = line.split('\t')
668 sha, ref = line.split('\t')
657 cmd.append('+%s:%s' % (ref, ref))
669 cmd.append('+%s:%s' % (ref, ref))
658 self.run_git_command(cmd)
670 self.run_git_command(cmd)
659
671
660 def _update_server_info(self):
672 def _update_server_info(self):
661 """
673 """
662 runs gits update-server-info command in this repo instance
674 runs gits update-server-info command in this repo instance
663 """
675 """
664 from dulwich.server import update_server_info
676 from dulwich.server import update_server_info
665 try:
677 try:
666 update_server_info(self._repo)
678 update_server_info(self._repo)
667 except OSError as e:
679 except OSError as e:
668 if e.errno not in [errno.ENOENT, errno.EROFS]:
680 if e.errno not in [errno.ENOENT, errno.EROFS]:
669 raise
681 raise
670 # Workaround for dulwich crashing on for example its own dulwich/tests/data/repos/simple_merge.git/info/refs.lock
682 # Workaround for dulwich crashing on for example its own dulwich/tests/data/repos/simple_merge.git/info/refs.lock
671 log.error('Ignoring %s running update-server-info: %s', type(e).__name__, e)
683 log.error('Ignoring %s running update-server-info: %s', type(e).__name__, e)
672
684
673 @LazyProperty
685 @LazyProperty
674 def workdir(self):
686 def workdir(self):
675 """
687 """
676 Returns ``Workdir`` instance for this repository.
688 Returns ``Workdir`` instance for this repository.
677 """
689 """
678 return GitWorkdir(self)
690 return GitWorkdir(self)
679
691
680 def get_config_value(self, section, name, config_file=None):
692 def get_config_value(self, section, name, config_file=None):
681 """
693 """
682 Returns configuration value for a given [``section``] and ``name``.
694 Returns configuration value for a given [``section``] and ``name``.
683
695
684 :param section: Section we want to retrieve value from
696 :param section: Section we want to retrieve value from
685 :param name: Name of configuration we want to retrieve
697 :param name: Name of configuration we want to retrieve
686 :param config_file: A path to file which should be used to retrieve
698 :param config_file: A path to file which should be used to retrieve
687 configuration from (might also be a list of file paths)
699 configuration from (might also be a list of file paths)
688 """
700 """
689 if config_file is None:
701 if config_file is None:
690 config_file = []
702 config_file = []
691 elif isinstance(config_file, str):
703 elif isinstance(config_file, str):
692 config_file = [config_file]
704 config_file = [config_file]
693
705
694 def gen_configs():
706 def gen_configs():
695 for path in config_file + self._config_files:
707 for path in config_file + self._config_files:
696 try:
708 try:
697 yield ConfigFile.from_path(path)
709 yield ConfigFile.from_path(path)
698 except (IOError, OSError, ValueError):
710 except (IOError, OSError, ValueError):
699 continue
711 continue
700
712
701 for config in gen_configs():
713 for config in gen_configs():
702 try:
714 try:
703 value = config.get(section, name)
715 value = config.get(section, name)
704 except KeyError:
716 except KeyError:
705 continue
717 continue
706 return None if value is None else safe_str(value)
718 return None if value is None else safe_str(value)
707 return None
719 return None
708
720
709 def get_user_name(self, config_file=None):
721 def get_user_name(self, config_file=None):
710 """
722 """
711 Returns user's name from global configuration file.
723 Returns user's name from global configuration file.
712
724
713 :param config_file: A path to file which should be used to retrieve
725 :param config_file: A path to file which should be used to retrieve
714 configuration from (might also be a list of file paths)
726 configuration from (might also be a list of file paths)
715 """
727 """
716 return self.get_config_value('user', 'name', config_file)
728 return self.get_config_value('user', 'name', config_file)
717
729
718 def get_user_email(self, config_file=None):
730 def get_user_email(self, config_file=None):
719 """
731 """
720 Returns user's email from global configuration file.
732 Returns user's email from global configuration file.
721
733
722 :param config_file: A path to file which should be used to retrieve
734 :param config_file: A path to file which should be used to retrieve
723 configuration from (might also be a list of file paths)
735 configuration from (might also be a list of file paths)
724 """
736 """
725 return self.get_config_value('user', 'email', config_file)
737 return self.get_config_value('user', 'email', config_file)
General Comments 0
You need to be logged in to leave comments. Login now