##// END OF EJS Templates
hg: use mercurial.node.nullid directly - needed for 5.9...
Mads Kiilerich -
r8702:a0e39afe stable
parent child Browse files
Show More
@@ -1,685 +1,685 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.hg.repository
3 vcs.backends.hg.repository
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Mercurial repository implementation.
6 Mercurial 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 datetime
12 import datetime
13 import logging
13 import logging
14 import os
14 import os
15 import time
15 import time
16 import urllib.error
16 import urllib.error
17 import urllib.parse
17 import urllib.parse
18 import urllib.request
18 import urllib.request
19 from collections import OrderedDict
19 from collections import OrderedDict
20
20
21 import mercurial.commands
21 import mercurial.commands
22 import mercurial.error
22 import mercurial.error
23 import mercurial.exchange
23 import mercurial.exchange
24 import mercurial.hg
24 import mercurial.hg
25 import mercurial.hgweb
25 import mercurial.hgweb
26 import mercurial.httppeer
26 import mercurial.httppeer
27 import mercurial.localrepo
27 import mercurial.localrepo
28 import mercurial.match
28 import mercurial.match
29 import mercurial.mdiff
29 import mercurial.mdiff
30 import mercurial.node
30 import mercurial.node
31 import mercurial.patch
31 import mercurial.patch
32 import mercurial.scmutil
32 import mercurial.scmutil
33 import mercurial.sshpeer
33 import mercurial.sshpeer
34 import mercurial.tags
34 import mercurial.tags
35 import mercurial.ui
35 import mercurial.ui
36 import mercurial.unionrepo
36 import mercurial.unionrepo
37 import mercurial.util
37 import mercurial.util
38
38
39 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
39 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
40 from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
40 from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
41 TagDoesNotExistError, VCSError)
41 TagDoesNotExistError, VCSError)
42 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, author_email, author_name, date_fromtimestamp, makedate, safe_bytes, safe_str
42 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, author_email, author_name, date_fromtimestamp, makedate, safe_bytes, safe_str
43 from kallithea.lib.vcs.utils.helpers import get_urllib_request_handlers
43 from kallithea.lib.vcs.utils.helpers import get_urllib_request_handlers
44 from kallithea.lib.vcs.utils.lazy import LazyProperty
44 from kallithea.lib.vcs.utils.lazy import LazyProperty
45 from kallithea.lib.vcs.utils.paths import abspath
45 from kallithea.lib.vcs.utils.paths import abspath
46
46
47 from . import changeset, inmemory, workdir
47 from . import changeset, inmemory, workdir
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class MercurialRepository(BaseRepository):
53 class MercurialRepository(BaseRepository):
54 """
54 """
55 Mercurial repository backend
55 Mercurial repository backend
56 """
56 """
57 DEFAULT_BRANCH_NAME = 'default'
57 DEFAULT_BRANCH_NAME = 'default'
58 scm = 'hg'
58 scm = 'hg'
59
59
60 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
60 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
61 update_after_clone=False):
61 update_after_clone=False):
62 """
62 """
63 Raises RepositoryError if repository could not be find at the given
63 Raises RepositoryError if repository could not be find at the given
64 ``repo_path``.
64 ``repo_path``.
65
65
66 :param repo_path: local path of the repository
66 :param repo_path: local path of the repository
67 :param create=False: if set to True, would try to create repository if
67 :param create=False: if set to True, would try to create repository if
68 it does not exist rather than raising exception
68 it does not exist rather than raising exception
69 :param baseui=None: user data
69 :param baseui=None: user data
70 :param src_url=None: would try to clone repository from given location
70 :param src_url=None: would try to clone repository from given location
71 :param update_after_clone=False: sets update of working copy after
71 :param update_after_clone=False: sets update of working copy after
72 making a clone
72 making a clone
73 """
73 """
74
74
75 if not isinstance(repo_path, str):
75 if not isinstance(repo_path, str):
76 raise VCSError('Mercurial backend requires repository path to '
76 raise VCSError('Mercurial backend requires repository path to '
77 'be instance of <str> got %s instead' %
77 'be instance of <str> got %s instead' %
78 type(repo_path))
78 type(repo_path))
79 self.path = abspath(repo_path)
79 self.path = abspath(repo_path)
80 self.baseui = baseui or mercurial.ui.ui()
80 self.baseui = baseui or mercurial.ui.ui()
81 # We've set path and ui, now we can set _repo itself
81 # We've set path and ui, now we can set _repo itself
82 self._repo = self._get_repo(create, src_url, update_after_clone)
82 self._repo = self._get_repo(create, src_url, update_after_clone)
83
83
84 @property
84 @property
85 def _empty(self):
85 def _empty(self):
86 """
86 """
87 Checks if repository is empty ie. without any changesets
87 Checks if repository is empty ie. without any changesets
88 """
88 """
89 # TODO: Following raises errors when using InMemoryChangeset...
89 # TODO: Following raises errors when using InMemoryChangeset...
90 # return len(self._repo.changelog) == 0
90 # return len(self._repo.changelog) == 0
91 return len(self.revisions) == 0
91 return len(self.revisions) == 0
92
92
93 @LazyProperty
93 @LazyProperty
94 def revisions(self):
94 def revisions(self):
95 """
95 """
96 Returns list of revisions' ids, in ascending order. Being lazy
96 Returns list of revisions' ids, in ascending order. Being lazy
97 attribute allows external tools to inject shas from cache.
97 attribute allows external tools to inject shas from cache.
98 """
98 """
99 return self._get_all_revisions()
99 return self._get_all_revisions()
100
100
101 @LazyProperty
101 @LazyProperty
102 def name(self):
102 def name(self):
103 return os.path.basename(self.path)
103 return os.path.basename(self.path)
104
104
105 @LazyProperty
105 @LazyProperty
106 def branches(self):
106 def branches(self):
107 return self._get_branches()
107 return self._get_branches()
108
108
109 @LazyProperty
109 @LazyProperty
110 def closed_branches(self):
110 def closed_branches(self):
111 return self._get_branches(normal=False, closed=True)
111 return self._get_branches(normal=False, closed=True)
112
112
113 @LazyProperty
113 @LazyProperty
114 def allbranches(self):
114 def allbranches(self):
115 """
115 """
116 List all branches, including closed branches.
116 List all branches, including closed branches.
117 """
117 """
118 return self._get_branches(closed=True)
118 return self._get_branches(closed=True)
119
119
120 def _get_branches(self, normal=True, closed=False):
120 def _get_branches(self, normal=True, closed=False):
121 """
121 """
122 Gets branches for this repository
122 Gets branches for this repository
123 Returns only not closed branches by default
123 Returns only not closed branches by default
124
124
125 :param closed: return also closed branches for mercurial
125 :param closed: return also closed branches for mercurial
126 :param normal: return also normal branches
126 :param normal: return also normal branches
127 """
127 """
128
128
129 if self._empty:
129 if self._empty:
130 return {}
130 return {}
131
131
132 bt = OrderedDict()
132 bt = OrderedDict()
133 for bn, _heads, node, isclosed in sorted(self._repo.branchmap().iterbranches()):
133 for bn, _heads, node, isclosed in sorted(self._repo.branchmap().iterbranches()):
134 if isclosed:
134 if isclosed:
135 if closed:
135 if closed:
136 bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
136 bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
137 else:
137 else:
138 if normal:
138 if normal:
139 bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
139 bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
140 return bt
140 return bt
141
141
142 @LazyProperty
142 @LazyProperty
143 def tags(self):
143 def tags(self):
144 """
144 """
145 Gets tags for this repository
145 Gets tags for this repository
146 """
146 """
147 return self._get_tags()
147 return self._get_tags()
148
148
149 def _get_tags(self):
149 def _get_tags(self):
150 if self._empty:
150 if self._empty:
151 return {}
151 return {}
152
152
153 return OrderedDict(sorted(
153 return OrderedDict(sorted(
154 ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo.tags().items()),
154 ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo.tags().items()),
155 reverse=True,
155 reverse=True,
156 key=lambda x: x[0], # sort by name
156 key=lambda x: x[0], # sort by name
157 ))
157 ))
158
158
159 def tag(self, name, user, revision=None, message=None, date=None,
159 def tag(self, name, user, revision=None, message=None, date=None,
160 **kwargs):
160 **kwargs):
161 """
161 """
162 Creates and returns a tag for the given ``revision``.
162 Creates and returns a tag for the given ``revision``.
163
163
164 :param name: name for new tag
164 :param name: name for new tag
165 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
165 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
166 :param revision: changeset id for which new tag would be created
166 :param revision: changeset id for which new tag would be created
167 :param message: message of the tag's commit
167 :param message: message of the tag's commit
168 :param date: date of tag's commit
168 :param date: date of tag's commit
169
169
170 :raises TagAlreadyExistError: if tag with same name already exists
170 :raises TagAlreadyExistError: if tag with same name already exists
171 """
171 """
172 if name in self.tags:
172 if name in self.tags:
173 raise TagAlreadyExistError("Tag %s already exists" % name)
173 raise TagAlreadyExistError("Tag %s already exists" % name)
174 changeset = self.get_changeset(revision)
174 changeset = self.get_changeset(revision)
175 local = kwargs.setdefault('local', False)
175 local = kwargs.setdefault('local', False)
176
176
177 if message is None:
177 if message is None:
178 message = "Added tag %s for changeset %s" % (name,
178 message = "Added tag %s for changeset %s" % (name,
179 changeset.short_id)
179 changeset.short_id)
180
180
181 if date is None:
181 if date is None:
182 date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
182 date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
183
183
184 try:
184 try:
185 mercurial.tags.tag(self._repo, safe_bytes(name), changeset._ctx.node(), safe_bytes(message), local, safe_bytes(user), date)
185 mercurial.tags.tag(self._repo, safe_bytes(name), changeset._ctx.node(), safe_bytes(message), local, safe_bytes(user), date)
186 except mercurial.error.Abort as e:
186 except mercurial.error.Abort as e:
187 raise RepositoryError(e.args[0])
187 raise RepositoryError(e.args[0])
188
188
189 # Reinitialize tags
189 # Reinitialize tags
190 self.tags = self._get_tags()
190 self.tags = self._get_tags()
191 tag_id = self.tags[name]
191 tag_id = self.tags[name]
192
192
193 return self.get_changeset(revision=tag_id)
193 return self.get_changeset(revision=tag_id)
194
194
195 def remove_tag(self, name, user, message=None, date=None):
195 def remove_tag(self, name, user, message=None, date=None):
196 """
196 """
197 Removes tag with the given ``name``.
197 Removes tag with the given ``name``.
198
198
199 :param name: name of the tag to be removed
199 :param name: name of the tag to be removed
200 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
200 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
201 :param message: message of the tag's removal commit
201 :param message: message of the tag's removal commit
202 :param date: date of tag's removal commit
202 :param date: date of tag's removal commit
203
203
204 :raises TagDoesNotExistError: if tag with given name does not exists
204 :raises TagDoesNotExistError: if tag with given name does not exists
205 """
205 """
206 if name not in self.tags:
206 if name not in self.tags:
207 raise TagDoesNotExistError("Tag %s does not exist" % name)
207 raise TagDoesNotExistError("Tag %s does not exist" % name)
208 if message is None:
208 if message is None:
209 message = "Removed tag %s" % name
209 message = "Removed tag %s" % name
210 if date is None:
210 if date is None:
211 date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
211 date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
212 local = False
212 local = False
213
213
214 try:
214 try:
215 mercurial.tags.tag(self._repo, safe_bytes(name), mercurial.commands.nullid, safe_bytes(message), local, safe_bytes(user), date)
215 mercurial.tags.tag(self._repo, safe_bytes(name), mercurial.node.nullid, safe_bytes(message), local, safe_bytes(user), date)
216 self.tags = self._get_tags()
216 self.tags = self._get_tags()
217 except mercurial.error.Abort as e:
217 except mercurial.error.Abort as e:
218 raise RepositoryError(e.args[0])
218 raise RepositoryError(e.args[0])
219
219
220 @LazyProperty
220 @LazyProperty
221 def bookmarks(self):
221 def bookmarks(self):
222 """
222 """
223 Gets bookmarks for this repository
223 Gets bookmarks for this repository
224 """
224 """
225 return self._get_bookmarks()
225 return self._get_bookmarks()
226
226
227 def _get_bookmarks(self):
227 def _get_bookmarks(self):
228 if self._empty:
228 if self._empty:
229 return {}
229 return {}
230
230
231 return OrderedDict(sorted(
231 return OrderedDict(sorted(
232 ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo._bookmarks.items()),
232 ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo._bookmarks.items()),
233 reverse=True,
233 reverse=True,
234 key=lambda x: x[0], # sort by name
234 key=lambda x: x[0], # sort by name
235 ))
235 ))
236
236
237 def _get_all_revisions(self):
237 def _get_all_revisions(self):
238 return [ascii_str(self._repo[x].hex()) for x in self._repo.filtered(b'visible').changelog.revs()]
238 return [ascii_str(self._repo[x].hex()) for x in self._repo.filtered(b'visible').changelog.revs()]
239
239
240 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
240 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
241 context=3):
241 context=3):
242 """
242 """
243 Returns (git like) *diff*, as plain text. Shows changes introduced by
243 Returns (git like) *diff*, as plain text. Shows changes introduced by
244 ``rev2`` since ``rev1``.
244 ``rev2`` since ``rev1``.
245
245
246 :param rev1: Entry point from which diff is shown. Can be
246 :param rev1: Entry point from which diff is shown. Can be
247 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
247 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
248 the changes since empty state of the repository until ``rev2``
248 the changes since empty state of the repository until ``rev2``
249 :param rev2: Until which revision changes should be shown.
249 :param rev2: Until which revision changes should be shown.
250 :param ignore_whitespace: If set to ``True``, would not show whitespace
250 :param ignore_whitespace: If set to ``True``, would not show whitespace
251 changes. Defaults to ``False``.
251 changes. Defaults to ``False``.
252 :param context: How many lines before/after changed lines should be
252 :param context: How many lines before/after changed lines should be
253 shown. Defaults to ``3``. If negative value is passed-in, it will be
253 shown. Defaults to ``3``. If negative value is passed-in, it will be
254 set to ``0`` instead.
254 set to ``0`` instead.
255 """
255 """
256
256
257 # Negative context values make no sense, and will result in
257 # Negative context values make no sense, and will result in
258 # errors. Ensure this does not happen.
258 # errors. Ensure this does not happen.
259 if context < 0:
259 if context < 0:
260 context = 0
260 context = 0
261
261
262 if hasattr(rev1, 'raw_id'):
262 if hasattr(rev1, 'raw_id'):
263 rev1 = getattr(rev1, 'raw_id')
263 rev1 = getattr(rev1, 'raw_id')
264
264
265 if hasattr(rev2, 'raw_id'):
265 if hasattr(rev2, 'raw_id'):
266 rev2 = getattr(rev2, 'raw_id')
266 rev2 = getattr(rev2, 'raw_id')
267
267
268 # Check if given revisions are present at repository (may raise
268 # Check if given revisions are present at repository (may raise
269 # ChangesetDoesNotExistError)
269 # ChangesetDoesNotExistError)
270 if rev1 != self.EMPTY_CHANGESET:
270 if rev1 != self.EMPTY_CHANGESET:
271 self.get_changeset(rev1)
271 self.get_changeset(rev1)
272 self.get_changeset(rev2)
272 self.get_changeset(rev2)
273 if path:
273 if path:
274 file_filter = mercurial.match.exact([safe_bytes(path)])
274 file_filter = mercurial.match.exact([safe_bytes(path)])
275 else:
275 else:
276 file_filter = None
276 file_filter = None
277
277
278 return b''.join(mercurial.patch.diff(self._repo, rev1, rev2, match=file_filter,
278 return b''.join(mercurial.patch.diff(self._repo, rev1, rev2, match=file_filter,
279 opts=mercurial.mdiff.diffopts(git=True,
279 opts=mercurial.mdiff.diffopts(git=True,
280 showfunc=True,
280 showfunc=True,
281 ignorews=ignore_whitespace,
281 ignorews=ignore_whitespace,
282 context=context)))
282 context=context)))
283
283
284 @staticmethod
284 @staticmethod
285 def _check_url(url, repoui=None):
285 def _check_url(url, repoui=None):
286 r"""
286 r"""
287 Raise URLError if url doesn't seem like a valid safe Hg URL. We
287 Raise URLError if url doesn't seem like a valid safe Hg URL. We
288 only allow http, https, ssh, and hg-git URLs.
288 only allow http, https, ssh, and hg-git URLs.
289
289
290 For http, https and git URLs, make a connection and probe to see if it is valid.
290 For http, https and git URLs, make a connection and probe to see if it is valid.
291
291
292 On failures it'll raise urllib2.HTTPError, exception is also thrown
292 On failures it'll raise urllib2.HTTPError, exception is also thrown
293 when the return code is non 200
293 when the return code is non 200
294
294
295 >>> MercurialRepository._check_url('file:///repo')
295 >>> MercurialRepository._check_url('file:///repo')
296
296
297 >>> MercurialRepository._check_url('http://example.com:65537/repo')
297 >>> MercurialRepository._check_url('http://example.com:65537/repo')
298 Traceback (most recent call last):
298 Traceback (most recent call last):
299 ...
299 ...
300 urllib.error.URLError: <urlopen error Error parsing URL: 'http://example.com:65537/repo'>
300 urllib.error.URLError: <urlopen error Error parsing URL: 'http://example.com:65537/repo'>
301 >>> MercurialRepository._check_url('foo')
301 >>> MercurialRepository._check_url('foo')
302 Traceback (most recent call last):
302 Traceback (most recent call last):
303 ...
303 ...
304 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'foo'>
304 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'foo'>
305 >>> MercurialRepository._check_url('git+ssh://example.com/my%20fine repo')
305 >>> MercurialRepository._check_url('git+ssh://example.com/my%20fine repo')
306 Traceback (most recent call last):
306 Traceback (most recent call last):
307 ...
307 ...
308 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'git+ssh://example.com/my%20fine repo'>
308 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'git+ssh://example.com/my%20fine repo'>
309 >>> MercurialRepository._check_url('svn+http://example.com/repo')
309 >>> MercurialRepository._check_url('svn+http://example.com/repo')
310 Traceback (most recent call last):
310 Traceback (most recent call last):
311 ...
311 ...
312 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'svn+http://example.com/repo'>
312 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'svn+http://example.com/repo'>
313 """
313 """
314 try:
314 try:
315 parsed_url = urllib.parse.urlparse(url)
315 parsed_url = urllib.parse.urlparse(url)
316 parsed_url.port # trigger netloc parsing which might raise ValueError
316 parsed_url.port # trigger netloc parsing which might raise ValueError
317 except ValueError:
317 except ValueError:
318 raise urllib.error.URLError("Error parsing URL: %r" % url)
318 raise urllib.error.URLError("Error parsing URL: %r" % url)
319
319
320 # check first if it's not an local url
320 # check first if it's not an local url
321 if os.path.isabs(url) and os.path.isdir(url) or parsed_url.scheme == 'file':
321 if os.path.isabs(url) and os.path.isdir(url) or parsed_url.scheme == 'file':
322 # When creating repos, _get_url will use file protocol for local paths
322 # When creating repos, _get_url will use file protocol for local paths
323 return
323 return
324
324
325 if parsed_url.scheme not in ['http', 'https', 'ssh', 'git+http', 'git+https']:
325 if parsed_url.scheme not in ['http', 'https', 'ssh', 'git+http', 'git+https']:
326 raise urllib.error.URLError("Unsupported protocol in URL %r" % url)
326 raise urllib.error.URLError("Unsupported protocol in URL %r" % url)
327
327
328 url = safe_bytes(url)
328 url = safe_bytes(url)
329
329
330 if parsed_url.scheme == 'ssh':
330 if parsed_url.scheme == 'ssh':
331 # in case of invalid uri or authentication issues, sshpeer will
331 # in case of invalid uri or authentication issues, sshpeer will
332 # throw an exception.
332 # throw an exception.
333 mercurial.sshpeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
333 mercurial.sshpeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
334 return
334 return
335
335
336 if '+' in parsed_url.scheme: # strip 'git+' for hg-git URLs
336 if '+' in parsed_url.scheme: # strip 'git+' for hg-git URLs
337 url = url.split(b'+', 1)[1]
337 url = url.split(b'+', 1)[1]
338
338
339 url_obj = mercurial.util.url(url)
339 url_obj = mercurial.util.url(url)
340 test_uri, handlers = get_urllib_request_handlers(url_obj)
340 test_uri, handlers = get_urllib_request_handlers(url_obj)
341
341
342 url_obj.passwd = b'*****'
342 url_obj.passwd = b'*****'
343 cleaned_uri = str(url_obj)
343 cleaned_uri = str(url_obj)
344
344
345 o = urllib.request.build_opener(*handlers)
345 o = urllib.request.build_opener(*handlers)
346 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
346 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
347 ('Accept', 'application/mercurial-0.1')]
347 ('Accept', 'application/mercurial-0.1')]
348
348
349 req = urllib.request.Request(
349 req = urllib.request.Request(
350 "%s?%s" % (
350 "%s?%s" % (
351 safe_str(test_uri),
351 safe_str(test_uri),
352 urllib.parse.urlencode({
352 urllib.parse.urlencode({
353 'cmd': 'between',
353 'cmd': 'between',
354 'pairs': "%s-%s" % ('0' * 40, '0' * 40),
354 'pairs': "%s-%s" % ('0' * 40, '0' * 40),
355 })
355 })
356 ))
356 ))
357
357
358 try:
358 try:
359 resp = o.open(req)
359 resp = o.open(req)
360 if resp.code != 200:
360 if resp.code != 200:
361 raise Exception('Return Code is not 200')
361 raise Exception('Return Code is not 200')
362 except Exception as e:
362 except Exception as e:
363 # means it cannot be cloned
363 # means it cannot be cloned
364 raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
364 raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
365
365
366 if parsed_url.scheme in ['http', 'https']: # skip git+http://... etc
366 if parsed_url.scheme in ['http', 'https']: # skip git+http://... etc
367 # now check if it's a proper hg repo
367 # now check if it's a proper hg repo
368 try:
368 try:
369 mercurial.httppeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
369 mercurial.httppeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
370 except Exception as e:
370 except Exception as e:
371 raise urllib.error.URLError(
371 raise urllib.error.URLError(
372 "url [%s] does not look like an hg repo org_exc: %s"
372 "url [%s] does not look like an hg repo org_exc: %s"
373 % (cleaned_uri, e))
373 % (cleaned_uri, e))
374
374
375 def _get_repo(self, create, src_url=None, update_after_clone=False):
375 def _get_repo(self, create, src_url=None, update_after_clone=False):
376 """
376 """
377 Function will check for mercurial repository in given path and return
377 Function will check for mercurial repository in given path and return
378 a localrepo object. If there is no repository in that path it will
378 a localrepo object. If there is no repository in that path it will
379 raise an exception unless ``create`` parameter is set to True - in
379 raise an exception unless ``create`` parameter is set to True - in
380 that case repository would be created and returned.
380 that case repository would be created and returned.
381 If ``src_url`` is given, would try to clone repository from the
381 If ``src_url`` is given, would try to clone repository from the
382 location at given clone_point. Additionally it'll make update to
382 location at given clone_point. Additionally it'll make update to
383 working copy accordingly to ``update_after_clone`` flag
383 working copy accordingly to ``update_after_clone`` flag
384 """
384 """
385 try:
385 try:
386 if src_url:
386 if src_url:
387 url = self._get_url(src_url)
387 url = self._get_url(src_url)
388 opts = {}
388 opts = {}
389 if not update_after_clone:
389 if not update_after_clone:
390 opts.update({'noupdate': True})
390 opts.update({'noupdate': True})
391 MercurialRepository._check_url(url, self.baseui)
391 MercurialRepository._check_url(url, self.baseui)
392 mercurial.commands.clone(self.baseui, safe_bytes(url), safe_bytes(self.path), **opts)
392 mercurial.commands.clone(self.baseui, safe_bytes(url), safe_bytes(self.path), **opts)
393
393
394 # Don't try to create if we've already cloned repo
394 # Don't try to create if we've already cloned repo
395 create = False
395 create = False
396 return mercurial.localrepo.instance(self.baseui, safe_bytes(self.path), create=create)
396 return mercurial.localrepo.instance(self.baseui, safe_bytes(self.path), create=create)
397 except (mercurial.error.Abort, mercurial.error.RepoError) as err:
397 except (mercurial.error.Abort, mercurial.error.RepoError) as err:
398 if create:
398 if create:
399 msg = "Cannot create repository at %s. Original error was %s" \
399 msg = "Cannot create repository at %s. Original error was %s" \
400 % (self.name, err)
400 % (self.name, err)
401 else:
401 else:
402 msg = "Not valid repository at %s. Original error was %s" \
402 msg = "Not valid repository at %s. Original error was %s" \
403 % (self.name, err)
403 % (self.name, err)
404 raise RepositoryError(msg)
404 raise RepositoryError(msg)
405
405
406 @LazyProperty
406 @LazyProperty
407 def in_memory_changeset(self):
407 def in_memory_changeset(self):
408 return inmemory.MercurialInMemoryChangeset(self)
408 return inmemory.MercurialInMemoryChangeset(self)
409
409
410 @LazyProperty
410 @LazyProperty
411 def description(self):
411 def description(self):
412 _desc = self._repo.ui.config(b'web', b'description', None, untrusted=True)
412 _desc = self._repo.ui.config(b'web', b'description', None, untrusted=True)
413 return safe_str(_desc or b'unknown')
413 return safe_str(_desc or b'unknown')
414
414
415 @LazyProperty
415 @LazyProperty
416 def last_change(self):
416 def last_change(self):
417 """
417 """
418 Returns last change made on this repository as datetime object
418 Returns last change made on this repository as datetime object
419 """
419 """
420 return date_fromtimestamp(self._get_mtime(), makedate()[1])
420 return date_fromtimestamp(self._get_mtime(), makedate()[1])
421
421
422 def _get_mtime(self):
422 def _get_mtime(self):
423 try:
423 try:
424 return time.mktime(self.get_changeset().date.timetuple())
424 return time.mktime(self.get_changeset().date.timetuple())
425 except RepositoryError:
425 except RepositoryError:
426 # fallback to filesystem
426 # fallback to filesystem
427 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
427 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
428 st_path = os.path.join(self.path, '.hg', "store")
428 st_path = os.path.join(self.path, '.hg', "store")
429 if os.path.exists(cl_path):
429 if os.path.exists(cl_path):
430 return os.stat(cl_path).st_mtime
430 return os.stat(cl_path).st_mtime
431 else:
431 else:
432 return os.stat(st_path).st_mtime
432 return os.stat(st_path).st_mtime
433
433
434 def _get_revision(self, revision):
434 def _get_revision(self, revision):
435 """
435 """
436 Given any revision identifier, returns a 40 char string with revision hash.
436 Given any revision identifier, returns a 40 char string with revision hash.
437
437
438 :param revision: str or int or None
438 :param revision: str or int or None
439 """
439 """
440 if self._empty:
440 if self._empty:
441 raise EmptyRepositoryError("There are no changesets yet")
441 raise EmptyRepositoryError("There are no changesets yet")
442
442
443 if revision in [-1, None]:
443 if revision in [-1, None]:
444 revision = b'tip'
444 revision = b'tip'
445 elif isinstance(revision, str):
445 elif isinstance(revision, str):
446 revision = safe_bytes(revision)
446 revision = safe_bytes(revision)
447
447
448 try:
448 try:
449 if isinstance(revision, int):
449 if isinstance(revision, int):
450 return ascii_str(self._repo[revision].hex())
450 return ascii_str(self._repo[revision].hex())
451 return ascii_str(mercurial.scmutil.revsymbol(self._repo, revision).hex())
451 return ascii_str(mercurial.scmutil.revsymbol(self._repo, revision).hex())
452 except (IndexError, ValueError, mercurial.error.RepoLookupError, TypeError):
452 except (IndexError, ValueError, mercurial.error.RepoLookupError, TypeError):
453 msg = "Revision %r does not exist for %s" % (safe_str(revision), self.name)
453 msg = "Revision %r does not exist for %s" % (safe_str(revision), self.name)
454 raise ChangesetDoesNotExistError(msg)
454 raise ChangesetDoesNotExistError(msg)
455 except (LookupError, ):
455 except (LookupError, ):
456 msg = "Ambiguous identifier `%s` for %s" % (safe_str(revision), self.name)
456 msg = "Ambiguous identifier `%s` for %s" % (safe_str(revision), self.name)
457 raise ChangesetDoesNotExistError(msg)
457 raise ChangesetDoesNotExistError(msg)
458
458
459 def get_ref_revision(self, ref_type, ref_name):
459 def get_ref_revision(self, ref_type, ref_name):
460 """
460 """
461 Returns revision number for the given reference.
461 Returns revision number for the given reference.
462 """
462 """
463 if ref_type == 'rev' and not ref_name.strip('0'):
463 if ref_type == 'rev' and not ref_name.strip('0'):
464 return self.EMPTY_CHANGESET
464 return self.EMPTY_CHANGESET
465 # lookup up the exact node id
465 # lookup up the exact node id
466 _revset_predicates = {
466 _revset_predicates = {
467 'branch': 'branch',
467 'branch': 'branch',
468 'book': 'bookmark',
468 'book': 'bookmark',
469 'tag': 'tag',
469 'tag': 'tag',
470 'rev': 'id',
470 'rev': 'id',
471 }
471 }
472 # avoid expensive branch(x) iteration over whole repo
472 # avoid expensive branch(x) iteration over whole repo
473 rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type]
473 rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type]
474 try:
474 try:
475 revs = self._repo.revs(rev_spec, ref_name, ref_name)
475 revs = self._repo.revs(rev_spec, ref_name, ref_name)
476 except LookupError:
476 except LookupError:
477 msg = "Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name)
477 msg = "Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name)
478 raise ChangesetDoesNotExistError(msg)
478 raise ChangesetDoesNotExistError(msg)
479 except mercurial.error.RepoLookupError:
479 except mercurial.error.RepoLookupError:
480 msg = "Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name)
480 msg = "Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name)
481 raise ChangesetDoesNotExistError(msg)
481 raise ChangesetDoesNotExistError(msg)
482 if revs:
482 if revs:
483 revision = revs.last()
483 revision = revs.last()
484 else:
484 else:
485 # TODO: just report 'not found'?
485 # TODO: just report 'not found'?
486 revision = ref_name
486 revision = ref_name
487
487
488 return self._get_revision(revision)
488 return self._get_revision(revision)
489
489
490 def _get_archives(self, archive_name='tip'):
490 def _get_archives(self, archive_name='tip'):
491 allowed = self.baseui.configlist(b"web", b"allow_archive",
491 allowed = self.baseui.configlist(b"web", b"allow_archive",
492 untrusted=True)
492 untrusted=True)
493 for name, ext in [(b'zip', '.zip'), (b'gz', '.tar.gz'), (b'bz2', '.tar.bz2')]:
493 for name, ext in [(b'zip', '.zip'), (b'gz', '.tar.gz'), (b'bz2', '.tar.bz2')]:
494 if name in allowed or self._repo.ui.configbool(b"web",
494 if name in allowed or self._repo.ui.configbool(b"web",
495 b"allow" + name,
495 b"allow" + name,
496 untrusted=True):
496 untrusted=True):
497 yield {"type": safe_str(name), "extension": ext, "node": archive_name}
497 yield {"type": safe_str(name), "extension": ext, "node": archive_name}
498
498
499 def _get_url(self, url):
499 def _get_url(self, url):
500 """
500 """
501 Returns normalized url. If schema is not given, fall back to
501 Returns normalized url. If schema is not given, fall back to
502 filesystem (``file:///``) schema.
502 filesystem (``file:///``) schema.
503 """
503 """
504 if url != 'default' and '://' not in url:
504 if url != 'default' and '://' not in url:
505 url = "file:" + urllib.request.pathname2url(url)
505 url = "file:" + urllib.request.pathname2url(url)
506 return url
506 return url
507
507
508 def get_changeset(self, revision=None):
508 def get_changeset(self, revision=None):
509 """
509 """
510 Returns ``MercurialChangeset`` object representing repository's
510 Returns ``MercurialChangeset`` object representing repository's
511 changeset at the given ``revision``.
511 changeset at the given ``revision``.
512 """
512 """
513 return changeset.MercurialChangeset(repository=self, revision=self._get_revision(revision))
513 return changeset.MercurialChangeset(repository=self, revision=self._get_revision(revision))
514
514
515 def get_changesets(self, start=None, end=None, start_date=None,
515 def get_changesets(self, start=None, end=None, start_date=None,
516 end_date=None, branch_name=None, reverse=False, max_revisions=None):
516 end_date=None, branch_name=None, reverse=False, max_revisions=None):
517 """
517 """
518 Returns iterator of ``MercurialChangeset`` objects from start to end
518 Returns iterator of ``MercurialChangeset`` objects from start to end
519 (both are inclusive)
519 (both are inclusive)
520
520
521 :param start: None, str, int or mercurial lookup format
521 :param start: None, str, int or mercurial lookup format
522 :param end: None, str, int or mercurial lookup format
522 :param end: None, str, int or mercurial lookup format
523 :param start_date:
523 :param start_date:
524 :param end_date:
524 :param end_date:
525 :param branch_name:
525 :param branch_name:
526 :param reversed: return changesets in reversed order
526 :param reversed: return changesets in reversed order
527 """
527 """
528 start_raw_id = self._get_revision(start)
528 start_raw_id = self._get_revision(start)
529 start_pos = None if start is None else self.revisions.index(start_raw_id)
529 start_pos = None if start is None else self.revisions.index(start_raw_id)
530 end_raw_id = self._get_revision(end)
530 end_raw_id = self._get_revision(end)
531 end_pos = None if end is None else self.revisions.index(end_raw_id)
531 end_pos = None if end is None else self.revisions.index(end_raw_id)
532
532
533 if start_pos is not None and end_pos is not None and start_pos > end_pos:
533 if start_pos is not None and end_pos is not None and start_pos > end_pos:
534 raise RepositoryError("Start revision '%s' cannot be "
534 raise RepositoryError("Start revision '%s' cannot be "
535 "after end revision '%s'" % (start, end))
535 "after end revision '%s'" % (start, end))
536
536
537 if branch_name and branch_name not in self.allbranches:
537 if branch_name and branch_name not in self.allbranches:
538 msg = "Branch %r not found in %s" % (branch_name, self.name)
538 msg = "Branch %r not found in %s" % (branch_name, self.name)
539 raise BranchDoesNotExistError(msg)
539 raise BranchDoesNotExistError(msg)
540 if end_pos is not None:
540 if end_pos is not None:
541 end_pos += 1
541 end_pos += 1
542 # filter branches
542 # filter branches
543 filter_ = []
543 filter_ = []
544 if branch_name:
544 if branch_name:
545 filter_.append(b'branch("%s")' % safe_bytes(branch_name))
545 filter_.append(b'branch("%s")' % safe_bytes(branch_name))
546 if start_date:
546 if start_date:
547 filter_.append(b'date(">%s")' % safe_bytes(str(start_date)))
547 filter_.append(b'date(">%s")' % safe_bytes(str(start_date)))
548 if end_date:
548 if end_date:
549 filter_.append(b'date("<%s")' % safe_bytes(str(end_date)))
549 filter_.append(b'date("<%s")' % safe_bytes(str(end_date)))
550 if filter_ or max_revisions:
550 if filter_ or max_revisions:
551 if filter_:
551 if filter_:
552 revspec = b' and '.join(filter_)
552 revspec = b' and '.join(filter_)
553 else:
553 else:
554 revspec = b'all()'
554 revspec = b'all()'
555 if max_revisions:
555 if max_revisions:
556 revspec = b'limit(%s, %d)' % (revspec, max_revisions)
556 revspec = b'limit(%s, %d)' % (revspec, max_revisions)
557 revisions = mercurial.scmutil.revrange(self._repo, [revspec])
557 revisions = mercurial.scmutil.revrange(self._repo, [revspec])
558 else:
558 else:
559 revisions = self.revisions
559 revisions = self.revisions
560
560
561 # this is very much a hack to turn this into a list; a better solution
561 # this is very much a hack to turn this into a list; a better solution
562 # would be to get rid of this function entirely and use revsets
562 # would be to get rid of this function entirely and use revsets
563 revs = list(revisions)[start_pos:end_pos]
563 revs = list(revisions)[start_pos:end_pos]
564 if reverse:
564 if reverse:
565 revs.reverse()
565 revs.reverse()
566
566
567 return CollectionGenerator(self, revs)
567 return CollectionGenerator(self, revs)
568
568
569 def get_diff_changesets(self, org_rev, other_repo, other_rev):
569 def get_diff_changesets(self, org_rev, other_repo, other_rev):
570 """
570 """
571 Returns lists of changesets that can be merged from this repo @org_rev
571 Returns lists of changesets that can be merged from this repo @org_rev
572 to other_repo @other_rev
572 to other_repo @other_rev
573 ... and the other way
573 ... and the other way
574 ... and the ancestors that would be used for merge
574 ... and the ancestors that would be used for merge
575
575
576 :param org_rev: the revision we want our compare to be made
576 :param org_rev: the revision we want our compare to be made
577 :param other_repo: repo object, most likely the fork of org_repo. It has
577 :param other_repo: repo object, most likely the fork of org_repo. It has
578 all changesets that we need to obtain
578 all changesets that we need to obtain
579 :param other_rev: revision we want out compare to be made on other_repo
579 :param other_rev: revision we want out compare to be made on other_repo
580 """
580 """
581 ancestors = None
581 ancestors = None
582 if org_rev == other_rev:
582 if org_rev == other_rev:
583 org_changesets = []
583 org_changesets = []
584 other_changesets = []
584 other_changesets = []
585
585
586 else:
586 else:
587 # case two independent repos
587 # case two independent repos
588 if self != other_repo:
588 if self != other_repo:
589 hgrepo = mercurial.unionrepo.makeunionrepository(other_repo.baseui,
589 hgrepo = mercurial.unionrepo.makeunionrepository(other_repo.baseui,
590 safe_bytes(other_repo.path),
590 safe_bytes(other_repo.path),
591 safe_bytes(self.path))
591 safe_bytes(self.path))
592 # all ancestors of other_rev will be in other_repo and
592 # all ancestors of other_rev will be in other_repo and
593 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
593 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
594
594
595 # no remote compare do it on the same repository
595 # no remote compare do it on the same repository
596 else:
596 else:
597 hgrepo = other_repo._repo
597 hgrepo = other_repo._repo
598
598
599 ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
599 ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
600 hgrepo.revs(b"id(%s) & ::id(%s)", ascii_bytes(other_rev), ascii_bytes(org_rev))]
600 hgrepo.revs(b"id(%s) & ::id(%s)", ascii_bytes(other_rev), ascii_bytes(org_rev))]
601 if ancestors:
601 if ancestors:
602 log.debug("shortcut found: %s is already an ancestor of %s", other_rev, org_rev)
602 log.debug("shortcut found: %s is already an ancestor of %s", other_rev, org_rev)
603 else:
603 else:
604 log.debug("no shortcut found: %s is not an ancestor of %s", other_rev, org_rev)
604 log.debug("no shortcut found: %s is not an ancestor of %s", other_rev, org_rev)
605 ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
605 ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
606 hgrepo.revs(b"heads(::id(%s) & ::id(%s))", ascii_bytes(org_rev), ascii_bytes(other_rev))] # FIXME: expensive!
606 hgrepo.revs(b"heads(::id(%s) & ::id(%s))", ascii_bytes(org_rev), ascii_bytes(other_rev))] # FIXME: expensive!
607
607
608 other_changesets = [
608 other_changesets = [
609 other_repo.get_changeset(rev)
609 other_repo.get_changeset(rev)
610 for rev in hgrepo.revs(
610 for rev in hgrepo.revs(
611 b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
611 b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
612 ascii_bytes(other_rev), ascii_bytes(org_rev), ascii_bytes(org_rev))
612 ascii_bytes(other_rev), ascii_bytes(org_rev), ascii_bytes(org_rev))
613 ]
613 ]
614 org_changesets = [
614 org_changesets = [
615 self.get_changeset(ascii_str(hgrepo[rev].hex()))
615 self.get_changeset(ascii_str(hgrepo[rev].hex()))
616 for rev in hgrepo.revs(
616 for rev in hgrepo.revs(
617 b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
617 b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
618 ascii_bytes(org_rev), ascii_bytes(other_rev), ascii_bytes(other_rev))
618 ascii_bytes(org_rev), ascii_bytes(other_rev), ascii_bytes(other_rev))
619 ]
619 ]
620
620
621 return other_changesets, org_changesets, ancestors
621 return other_changesets, org_changesets, ancestors
622
622
623 def pull(self, url):
623 def pull(self, url):
624 """
624 """
625 Tries to pull changes from external location.
625 Tries to pull changes from external location.
626 """
626 """
627 other = mercurial.hg.peer(self._repo, {}, safe_bytes(self._get_url(url)))
627 other = mercurial.hg.peer(self._repo, {}, safe_bytes(self._get_url(url)))
628 try:
628 try:
629 mercurial.exchange.pull(self._repo, other, heads=None, force=None)
629 mercurial.exchange.pull(self._repo, other, heads=None, force=None)
630 except mercurial.error.Abort as err:
630 except mercurial.error.Abort as err:
631 # Propagate error but with vcs's type
631 # Propagate error but with vcs's type
632 raise RepositoryError(str(err))
632 raise RepositoryError(str(err))
633
633
634 @LazyProperty
634 @LazyProperty
635 def workdir(self):
635 def workdir(self):
636 """
636 """
637 Returns ``Workdir`` instance for this repository.
637 Returns ``Workdir`` instance for this repository.
638 """
638 """
639 return workdir.MercurialWorkdir(self)
639 return workdir.MercurialWorkdir(self)
640
640
641 def get_config_value(self, section, name=None, config_file=None):
641 def get_config_value(self, section, name=None, config_file=None):
642 """
642 """
643 Returns configuration value for a given [``section``] and ``name``.
643 Returns configuration value for a given [``section``] and ``name``.
644
644
645 :param section: Section we want to retrieve value from
645 :param section: Section we want to retrieve value from
646 :param name: Name of configuration we want to retrieve
646 :param name: Name of configuration we want to retrieve
647 :param config_file: A path to file which should be used to retrieve
647 :param config_file: A path to file which should be used to retrieve
648 configuration from (might also be a list of file paths)
648 configuration from (might also be a list of file paths)
649 """
649 """
650 if config_file is None:
650 if config_file is None:
651 config_file = []
651 config_file = []
652 elif isinstance(config_file, str):
652 elif isinstance(config_file, str):
653 config_file = [config_file]
653 config_file = [config_file]
654
654
655 config = self._repo.ui
655 config = self._repo.ui
656 if config_file:
656 if config_file:
657 config = mercurial.ui.ui()
657 config = mercurial.ui.ui()
658 for path in config_file:
658 for path in config_file:
659 config.readconfig(safe_bytes(path))
659 config.readconfig(safe_bytes(path))
660 value = config.config(safe_bytes(section), safe_bytes(name))
660 value = config.config(safe_bytes(section), safe_bytes(name))
661 return value if value is None else safe_str(value)
661 return value if value is None else safe_str(value)
662
662
663 def get_user_name(self, config_file=None):
663 def get_user_name(self, config_file=None):
664 """
664 """
665 Returns user's name from global configuration file.
665 Returns user's name from global configuration file.
666
666
667 :param config_file: A path to file which should be used to retrieve
667 :param config_file: A path to file which should be used to retrieve
668 configuration from (might also be a list of file paths)
668 configuration from (might also be a list of file paths)
669 """
669 """
670 username = self.get_config_value('ui', 'username', config_file=config_file)
670 username = self.get_config_value('ui', 'username', config_file=config_file)
671 if username:
671 if username:
672 return author_name(username)
672 return author_name(username)
673 return None
673 return None
674
674
675 def get_user_email(self, config_file=None):
675 def get_user_email(self, config_file=None):
676 """
676 """
677 Returns user's email from global configuration file.
677 Returns user's email from global configuration file.
678
678
679 :param config_file: A path to file which should be used to retrieve
679 :param config_file: A path to file which should be used to retrieve
680 configuration from (might also be a list of file paths)
680 configuration from (might also be a list of file paths)
681 """
681 """
682 username = self.get_config_value('ui', 'username', config_file=config_file)
682 username = self.get_config_value('ui', 'username', config_file=config_file)
683 if username:
683 if username:
684 return author_email(username)
684 return author_email(username)
685 return None
685 return None
General Comments 0
You need to be logged in to leave comments. Login now