##// END OF EJS Templates
git: update check for invalid URL characters to work with Python versions that include an attempt at fixing the very same problem...
Mads Kiilerich -
r8681:f0fbb0fe default
parent child Browse files
Show More
@@ -1,819 +1,827 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.client import SubprocessGitClient
23 from dulwich.client import SubprocessGitClient
24 from dulwich.config import ConfigFile
24 from dulwich.config import ConfigFile
25 from dulwich.objects import Tag
25 from dulwich.objects import Tag
26 from dulwich.repo import NotGitRepository, Repo
26 from dulwich.repo import NotGitRepository, Repo
27 from dulwich.server import update_server_info
27 from dulwich.server import update_server_info
28
28
29 from kallithea.lib.vcs import subprocessio
29 from kallithea.lib.vcs import subprocessio
30 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
30 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
31 from kallithea.lib.vcs.conf import settings
31 from kallithea.lib.vcs.conf import settings
32 from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
32 from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
33 TagDoesNotExistError)
33 TagDoesNotExistError)
34 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, date_fromtimestamp, makedate, safe_bytes, safe_str
34 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, date_fromtimestamp, makedate, safe_bytes, safe_str
35 from kallithea.lib.vcs.utils.helpers import get_urllib_request_handlers
35 from kallithea.lib.vcs.utils.helpers import get_urllib_request_handlers
36 from kallithea.lib.vcs.utils.lazy import LazyProperty
36 from kallithea.lib.vcs.utils.lazy import LazyProperty
37 from kallithea.lib.vcs.utils.paths import abspath, get_user_home
37 from kallithea.lib.vcs.utils.paths import abspath, get_user_home
38
38
39 from . import changeset, inmemory, workdir
39 from . import changeset, inmemory, workdir
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, baseui=None):
55 update_after_clone=False, bare=False, baseui=None):
56 baseui # unused
56 baseui # unused
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 @staticmethod
150 @staticmethod
151 def _check_url(url):
151 def _check_url(url):
152 r"""
152 r"""
153 Raise URLError if url doesn't seem like a valid safe Git URL. We
153 Raise URLError if url doesn't seem like a valid safe Git URL. We
154 only allow http, https, git, and ssh URLs.
154 only allow http, https, git, and ssh URLs.
155
155
156 For http and https URLs, make a connection and probe to see if it is valid.
156 For http and https URLs, make a connection and probe to see if it is valid.
157
157
158 >>> GitRepository._check_url('git://example.com/my%20fine repo')
158 >>> GitRepository._check_url('git://example.com/my%20fine repo')
159
159
160 >>> GitRepository._check_url('http://example.com:65537/repo')
160 >>> GitRepository._check_url('http://example.com:65537/repo')
161 Traceback (most recent call last):
161 Traceback (most recent call last):
162 ...
162 ...
163 urllib.error.URLError: <urlopen error Error parsing URL: 'http://example.com:65537/repo'>
163 urllib.error.URLError: <urlopen error Error parsing URL: 'http://example.com:65537/repo'>
164 >>> GitRepository._check_url('foo')
164 >>> GitRepository._check_url('foo')
165 Traceback (most recent call last):
165 Traceback (most recent call last):
166 ...
166 ...
167 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'foo'>
167 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'foo'>
168 >>> GitRepository._check_url('file:///repo')
168 >>> GitRepository._check_url('file:///repo')
169 Traceback (most recent call last):
169 Traceback (most recent call last):
170 ...
170 ...
171 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'file:///repo'>
171 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'file:///repo'>
172 >>> GitRepository._check_url('git+http://example.com/repo')
172 >>> GitRepository._check_url('git+http://example.com/repo')
173 Traceback (most recent call last):
173 Traceback (most recent call last):
174 ...
174 ...
175 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'git+http://example.com/repo'>
175 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'git+http://example.com/repo'>
176 >>> GitRepository._check_url('git://example.com/%09')
176 >>> GitRepository._check_url('git://example.com/%09')
177 Traceback (most recent call last):
177 Traceback (most recent call last):
178 ...
178 ...
179 urllib.error.URLError: <urlopen error Invalid escape character in path: '%'>
179 urllib.error.URLError: <urlopen error Invalid escape character in path: '%'>
180 >>> GitRepository._check_url('git://example.com/%x00')
180 >>> GitRepository._check_url('git://example.com/%x00')
181 Traceback (most recent call last):
181 Traceback (most recent call last):
182 ...
182 ...
183 urllib.error.URLError: <urlopen error Invalid escape character in path: '%'>
183 urllib.error.URLError: <urlopen error Invalid escape character in path: '%'>
184 >>> GitRepository._check_url(r'git://example.com/\u0009')
184 >>> GitRepository._check_url(r'git://example.com/\u0009')
185 Traceback (most recent call last):
185 Traceback (most recent call last):
186 ...
186 ...
187 urllib.error.URLError: <urlopen error Invalid escape character in path: '\'>
187 urllib.error.URLError: <urlopen error Invalid escape character in path: '\'>
188 >>> GitRepository._check_url(r'git://example.com/\t')
188 >>> GitRepository._check_url(r'git://example.com/\t')
189 Traceback (most recent call last):
189 Traceback (most recent call last):
190 ...
190 ...
191 urllib.error.URLError: <urlopen error Invalid escape character in path: '\'>
191 urllib.error.URLError: <urlopen error Invalid escape character in path: '\'>
192 >>> GitRepository._check_url('git://example.com/\t')
192 >>> GitRepository._check_url('git://example.com/\t')
193 Traceback (most recent call last):
193 Traceback (most recent call last):
194 ...
194 ...
195 urllib.error.URLError: <urlopen error Invalid ...>
196
197 The failure above will be one of, depending on the level of WhatWG support:
195 urllib.error.URLError: <urlopen error Invalid whitespace character in path: '\t'>
198 urllib.error.URLError: <urlopen error Invalid whitespace character in path: '\t'>
199 urllib.error.URLError: <urlopen error Invalid url: 'git://example.com/ ' normalizes to 'git://example.com/'>
196 """
200 """
197 try:
201 try:
198 parsed_url = urllib.parse.urlparse(url)
202 parsed_url = urllib.parse.urlparse(url)
199 parsed_url.port # trigger netloc parsing which might raise ValueError
203 parsed_url.port # trigger netloc parsing which might raise ValueError
200 except ValueError:
204 except ValueError:
201 raise urllib.error.URLError("Error parsing URL: %r" % url)
205 raise urllib.error.URLError("Error parsing URL: %r" % url)
202
206
203 # check first if it's not an local url
207 # check first if it's not an local url
204 if os.path.isabs(url) and os.path.isdir(url):
208 if os.path.isabs(url) and os.path.isdir(url):
205 return
209 return
206
210
211 unparsed_url = urllib.parse.urlunparse(parsed_url)
212 if unparsed_url != url:
213 raise urllib.error.URLError("Invalid url: '%s' normalizes to '%s'" % (url, unparsed_url))
214
207 if parsed_url.scheme == 'git':
215 if parsed_url.scheme == 'git':
208 # Mitigate problems elsewhere with incorrect handling of encoded paths.
216 # Mitigate problems elsewhere with incorrect handling of encoded paths.
209 # Don't trust urllib.parse.unquote but be prepared for more flexible implementations elsewhere.
217 # Don't trust urllib.parse.unquote but be prepared for more flexible implementations elsewhere.
210 # Space is the only allowed whitespace character - directly or % encoded. No other % or \ is allowed.
218 # Space is the only allowed whitespace character - directly or % encoded. No other % or \ is allowed.
211 for c in parsed_url.path.replace('%20', ' '):
219 for c in parsed_url.path.replace('%20', ' '):
212 if c in '%\\':
220 if c in '%\\':
213 raise urllib.error.URLError("Invalid escape character in path: '%s'" % c)
221 raise urllib.error.URLError("Invalid escape character in path: '%s'" % c)
214 if c.isspace() and c != ' ':
222 if c.isspace() and c != ' ':
215 raise urllib.error.URLError("Invalid whitespace character in path: %r" % c)
223 raise urllib.error.URLError("Invalid whitespace character in path: %r" % c)
216 return
224 return
217
225
218 if parsed_url.scheme not in ['http', 'https']:
226 if parsed_url.scheme not in ['http', 'https']:
219 raise urllib.error.URLError("Unsupported protocol in URL %r" % url)
227 raise urllib.error.URLError("Unsupported protocol in URL %r" % url)
220
228
221 url_obj = mercurial.util.url(safe_bytes(url))
229 url_obj = mercurial.util.url(safe_bytes(url))
222 test_uri, handlers = get_urllib_request_handlers(url_obj)
230 test_uri, handlers = get_urllib_request_handlers(url_obj)
223 if not test_uri.endswith(b'info/refs'):
231 if not test_uri.endswith(b'info/refs'):
224 test_uri = test_uri.rstrip(b'/') + b'/info/refs'
232 test_uri = test_uri.rstrip(b'/') + b'/info/refs'
225
233
226 url_obj.passwd = b'*****'
234 url_obj.passwd = b'*****'
227 cleaned_uri = str(url_obj)
235 cleaned_uri = str(url_obj)
228
236
229 o = urllib.request.build_opener(*handlers)
237 o = urllib.request.build_opener(*handlers)
230 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
238 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
231
239
232 req = urllib.request.Request(
240 req = urllib.request.Request(
233 "%s?%s" % (
241 "%s?%s" % (
234 safe_str(test_uri),
242 safe_str(test_uri),
235 urllib.parse.urlencode({"service": 'git-upload-pack'})
243 urllib.parse.urlencode({"service": 'git-upload-pack'})
236 ))
244 ))
237
245
238 try:
246 try:
239 resp = o.open(req)
247 resp = o.open(req)
240 if resp.code != 200:
248 if resp.code != 200:
241 raise Exception('Return Code is not 200')
249 raise Exception('Return Code is not 200')
242 except Exception as e:
250 except Exception as e:
243 # means it cannot be cloned
251 # means it cannot be cloned
244 raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
252 raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
245
253
246 # now detect if it's proper git repo
254 # now detect if it's proper git repo
247 gitdata = resp.read()
255 gitdata = resp.read()
248 if b'service=git-upload-pack' not in gitdata:
256 if b'service=git-upload-pack' not in gitdata:
249 raise urllib.error.URLError(
257 raise urllib.error.URLError(
250 "url [%s] does not look like an git" % cleaned_uri)
258 "url [%s] does not look like an git" % cleaned_uri)
251
259
252 def _get_repo(self, create, src_url=None, update_after_clone=False,
260 def _get_repo(self, create, src_url=None, update_after_clone=False,
253 bare=False):
261 bare=False):
254 if create and os.path.exists(self.path):
262 if create and os.path.exists(self.path):
255 raise RepositoryError("Location already exist")
263 raise RepositoryError("Location already exist")
256 if src_url and not create:
264 if src_url and not create:
257 raise RepositoryError("Create should be set to True if src_url is "
265 raise RepositoryError("Create should be set to True if src_url is "
258 "given (clone operation creates repository)")
266 "given (clone operation creates repository)")
259 try:
267 try:
260 if create and src_url:
268 if create and src_url:
261 GitRepository._check_url(src_url)
269 GitRepository._check_url(src_url)
262 self.clone(src_url, update_after_clone, bare)
270 self.clone(src_url, update_after_clone, bare)
263 return Repo(self.path)
271 return Repo(self.path)
264 elif create:
272 elif create:
265 os.makedirs(self.path)
273 os.makedirs(self.path)
266 if bare:
274 if bare:
267 return Repo.init_bare(self.path)
275 return Repo.init_bare(self.path)
268 else:
276 else:
269 return Repo.init(self.path)
277 return Repo.init(self.path)
270 else:
278 else:
271 return Repo(self.path)
279 return Repo(self.path)
272 except (NotGitRepository, OSError) as err:
280 except (NotGitRepository, OSError) as err:
273 raise RepositoryError(err)
281 raise RepositoryError(err)
274
282
275 def _get_all_revisions(self):
283 def _get_all_revisions(self):
276 # we must check if this repo is not empty, since later command
284 # we must check if this repo is not empty, since later command
277 # fails if it is. And it's cheaper to ask than throw the subprocess
285 # fails if it is. And it's cheaper to ask than throw the subprocess
278 # errors
286 # errors
279 try:
287 try:
280 self._repo.head()
288 self._repo.head()
281 except KeyError:
289 except KeyError:
282 return []
290 return []
283
291
284 rev_filter = settings.GIT_REV_FILTER
292 rev_filter = settings.GIT_REV_FILTER
285 cmd = ['rev-list', rev_filter, '--reverse', '--date-order']
293 cmd = ['rev-list', rev_filter, '--reverse', '--date-order']
286 try:
294 try:
287 so = self.run_git_command(cmd)
295 so = self.run_git_command(cmd)
288 except RepositoryError:
296 except RepositoryError:
289 # Can be raised for empty repositories
297 # Can be raised for empty repositories
290 return []
298 return []
291 return so.splitlines()
299 return so.splitlines()
292
300
293 def _get_all_revisions2(self):
301 def _get_all_revisions2(self):
294 # alternate implementation using dulwich
302 # alternate implementation using dulwich
295 includes = [ascii_str(sha) for key, (sha, type_) in self._parsed_refs.items()
303 includes = [ascii_str(sha) for key, (sha, type_) in self._parsed_refs.items()
296 if type_ != b'T']
304 if type_ != b'T']
297 return [c.commit.id for c in self._repo.get_walker(include=includes)]
305 return [c.commit.id for c in self._repo.get_walker(include=includes)]
298
306
299 def _get_revision(self, revision):
307 def _get_revision(self, revision):
300 """
308 """
301 Given any revision identifier, returns a 40 char string with revision hash.
309 Given any revision identifier, returns a 40 char string with revision hash.
302 """
310 """
303 if self._empty:
311 if self._empty:
304 raise EmptyRepositoryError("There are no changesets yet")
312 raise EmptyRepositoryError("There are no changesets yet")
305
313
306 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
314 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
307 revision = -1
315 revision = -1
308
316
309 if isinstance(revision, int):
317 if isinstance(revision, int):
310 try:
318 try:
311 return self.revisions[revision]
319 return self.revisions[revision]
312 except IndexError:
320 except IndexError:
313 msg = "Revision %r does not exist for %s" % (revision, self.name)
321 msg = "Revision %r does not exist for %s" % (revision, self.name)
314 raise ChangesetDoesNotExistError(msg)
322 raise ChangesetDoesNotExistError(msg)
315
323
316 if isinstance(revision, str):
324 if isinstance(revision, str):
317 if revision.isdigit() and (len(revision) < 12 or len(revision) == revision.count('0')):
325 if revision.isdigit() and (len(revision) < 12 or len(revision) == revision.count('0')):
318 try:
326 try:
319 return self.revisions[int(revision)]
327 return self.revisions[int(revision)]
320 except IndexError:
328 except IndexError:
321 msg = "Revision %r does not exist for %s" % (revision, self)
329 msg = "Revision %r does not exist for %s" % (revision, self)
322 raise ChangesetDoesNotExistError(msg)
330 raise ChangesetDoesNotExistError(msg)
323
331
324 # get by branch/tag name
332 # get by branch/tag name
325 _ref_revision = self._parsed_refs.get(safe_bytes(revision))
333 _ref_revision = self._parsed_refs.get(safe_bytes(revision))
326 if _ref_revision: # and _ref_revision[1] in [b'H', b'RH', b'T']:
334 if _ref_revision: # and _ref_revision[1] in [b'H', b'RH', b'T']:
327 return ascii_str(_ref_revision[0])
335 return ascii_str(_ref_revision[0])
328
336
329 if revision in self.revisions:
337 if revision in self.revisions:
330 return revision
338 return revision
331
339
332 # maybe it's a tag ? we don't have them in self.revisions
340 # maybe it's a tag ? we don't have them in self.revisions
333 if revision in self.tags.values():
341 if revision in self.tags.values():
334 return revision
342 return revision
335
343
336 if SHA_PATTERN.match(revision):
344 if SHA_PATTERN.match(revision):
337 msg = "Revision %r does not exist for %s" % (revision, self.name)
345 msg = "Revision %r does not exist for %s" % (revision, self.name)
338 raise ChangesetDoesNotExistError(msg)
346 raise ChangesetDoesNotExistError(msg)
339
347
340 raise ChangesetDoesNotExistError("Given revision %r not recognized" % revision)
348 raise ChangesetDoesNotExistError("Given revision %r not recognized" % revision)
341
349
342 def get_ref_revision(self, ref_type, ref_name):
350 def get_ref_revision(self, ref_type, ref_name):
343 """
351 """
344 Returns ``GitChangeset`` object representing repository's
352 Returns ``GitChangeset`` object representing repository's
345 changeset at the given ``revision``.
353 changeset at the given ``revision``.
346 """
354 """
347 return self._get_revision(ref_name)
355 return self._get_revision(ref_name)
348
356
349 def _get_archives(self, archive_name='tip'):
357 def _get_archives(self, archive_name='tip'):
350
358
351 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
359 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
352 yield {"type": i[0], "extension": i[1], "node": archive_name}
360 yield {"type": i[0], "extension": i[1], "node": archive_name}
353
361
354 def _get_url(self, url):
362 def _get_url(self, url):
355 """
363 """
356 Returns normalized url. If schema is not given, would fall to
364 Returns normalized url. If schema is not given, would fall to
357 filesystem (``file:///``) schema.
365 filesystem (``file:///``) schema.
358 """
366 """
359 if url != 'default' and '://' not in url:
367 if url != 'default' and '://' not in url:
360 url = ':///'.join(('file', url))
368 url = ':///'.join(('file', url))
361 return url
369 return url
362
370
363 @LazyProperty
371 @LazyProperty
364 def name(self):
372 def name(self):
365 return os.path.basename(self.path)
373 return os.path.basename(self.path)
366
374
367 @LazyProperty
375 @LazyProperty
368 def last_change(self):
376 def last_change(self):
369 """
377 """
370 Returns last change made on this repository as datetime object
378 Returns last change made on this repository as datetime object
371 """
379 """
372 return date_fromtimestamp(self._get_mtime(), makedate()[1])
380 return date_fromtimestamp(self._get_mtime(), makedate()[1])
373
381
374 def _get_mtime(self):
382 def _get_mtime(self):
375 try:
383 try:
376 return time.mktime(self.get_changeset().date.timetuple())
384 return time.mktime(self.get_changeset().date.timetuple())
377 except RepositoryError:
385 except RepositoryError:
378 idx_loc = '' if self.bare else '.git'
386 idx_loc = '' if self.bare else '.git'
379 # fallback to filesystem
387 # fallback to filesystem
380 in_path = os.path.join(self.path, idx_loc, "index")
388 in_path = os.path.join(self.path, idx_loc, "index")
381 he_path = os.path.join(self.path, idx_loc, "HEAD")
389 he_path = os.path.join(self.path, idx_loc, "HEAD")
382 if os.path.exists(in_path):
390 if os.path.exists(in_path):
383 return os.stat(in_path).st_mtime
391 return os.stat(in_path).st_mtime
384 else:
392 else:
385 return os.stat(he_path).st_mtime
393 return os.stat(he_path).st_mtime
386
394
387 @LazyProperty
395 @LazyProperty
388 def description(self):
396 def description(self):
389 return safe_str(self._repo.get_description() or b'unknown')
397 return safe_str(self._repo.get_description() or b'unknown')
390
398
391 @property
399 @property
392 def branches(self):
400 def branches(self):
393 if not self.revisions:
401 if not self.revisions:
394 return {}
402 return {}
395 _branches = [(safe_str(key), ascii_str(sha))
403 _branches = [(safe_str(key), ascii_str(sha))
396 for key, (sha, type_) in self._parsed_refs.items() if type_ == b'H']
404 for key, (sha, type_) in self._parsed_refs.items() if type_ == b'H']
397 return OrderedDict(sorted(_branches, key=(lambda ctx: ctx[0]), reverse=False))
405 return OrderedDict(sorted(_branches, key=(lambda ctx: ctx[0]), reverse=False))
398
406
399 @LazyProperty
407 @LazyProperty
400 def closed_branches(self):
408 def closed_branches(self):
401 return {}
409 return {}
402
410
403 @LazyProperty
411 @LazyProperty
404 def tags(self):
412 def tags(self):
405 return self._get_tags()
413 return self._get_tags()
406
414
407 def _get_tags(self):
415 def _get_tags(self):
408 if not self.revisions:
416 if not self.revisions:
409 return {}
417 return {}
410 _tags = [(safe_str(key), ascii_str(sha))
418 _tags = [(safe_str(key), ascii_str(sha))
411 for key, (sha, type_) in self._parsed_refs.items() if type_ == b'T']
419 for key, (sha, type_) in self._parsed_refs.items() if type_ == b'T']
412 return OrderedDict(sorted(_tags, key=(lambda ctx: ctx[0]), reverse=True))
420 return OrderedDict(sorted(_tags, key=(lambda ctx: ctx[0]), reverse=True))
413
421
414 def tag(self, name, user, revision=None, message=None, date=None,
422 def tag(self, name, user, revision=None, message=None, date=None,
415 **kwargs):
423 **kwargs):
416 """
424 """
417 Creates and returns a tag for the given ``revision``.
425 Creates and returns a tag for the given ``revision``.
418
426
419 :param name: name for new tag
427 :param name: name for new tag
420 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
428 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
421 :param revision: changeset id for which new tag would be created
429 :param revision: changeset id for which new tag would be created
422 :param message: message of the tag's commit
430 :param message: message of the tag's commit
423 :param date: date of tag's commit
431 :param date: date of tag's commit
424
432
425 :raises TagAlreadyExistError: if tag with same name already exists
433 :raises TagAlreadyExistError: if tag with same name already exists
426 """
434 """
427 if name in self.tags:
435 if name in self.tags:
428 raise TagAlreadyExistError("Tag %s already exists" % name)
436 raise TagAlreadyExistError("Tag %s already exists" % name)
429 changeset = self.get_changeset(revision)
437 changeset = self.get_changeset(revision)
430 message = message or "Added tag %s for commit %s" % (name,
438 message = message or "Added tag %s for commit %s" % (name,
431 changeset.raw_id)
439 changeset.raw_id)
432 self._repo.refs[b"refs/tags/%s" % safe_bytes(name)] = changeset._commit.id
440 self._repo.refs[b"refs/tags/%s" % safe_bytes(name)] = changeset._commit.id
433
441
434 self._parsed_refs = self._get_parsed_refs()
442 self._parsed_refs = self._get_parsed_refs()
435 self.tags = self._get_tags()
443 self.tags = self._get_tags()
436 return changeset
444 return changeset
437
445
438 def remove_tag(self, name, user, message=None, date=None):
446 def remove_tag(self, name, user, message=None, date=None):
439 """
447 """
440 Removes tag with the given ``name``.
448 Removes tag with the given ``name``.
441
449
442 :param name: name of the tag to be removed
450 :param name: name of the tag to be removed
443 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
451 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
444 :param message: message of the tag's removal commit
452 :param message: message of the tag's removal commit
445 :param date: date of tag's removal commit
453 :param date: date of tag's removal commit
446
454
447 :raises TagDoesNotExistError: if tag with given name does not exists
455 :raises TagDoesNotExistError: if tag with given name does not exists
448 """
456 """
449 if name not in self.tags:
457 if name not in self.tags:
450 raise TagDoesNotExistError("Tag %s does not exist" % name)
458 raise TagDoesNotExistError("Tag %s does not exist" % name)
451 # self._repo.refs is a DiskRefsContainer, and .path gives the full absolute path of '.git'
459 # self._repo.refs is a DiskRefsContainer, and .path gives the full absolute path of '.git'
452 tagpath = os.path.join(safe_str(self._repo.refs.path), 'refs', 'tags', name)
460 tagpath = os.path.join(safe_str(self._repo.refs.path), 'refs', 'tags', name)
453 try:
461 try:
454 os.remove(tagpath)
462 os.remove(tagpath)
455 self._parsed_refs = self._get_parsed_refs()
463 self._parsed_refs = self._get_parsed_refs()
456 self.tags = self._get_tags()
464 self.tags = self._get_tags()
457 except OSError as e:
465 except OSError as e:
458 raise RepositoryError(e.strerror)
466 raise RepositoryError(e.strerror)
459
467
460 @LazyProperty
468 @LazyProperty
461 def bookmarks(self):
469 def bookmarks(self):
462 """
470 """
463 Gets bookmarks for this repository
471 Gets bookmarks for this repository
464 """
472 """
465 return {}
473 return {}
466
474
467 @LazyProperty
475 @LazyProperty
468 def _parsed_refs(self):
476 def _parsed_refs(self):
469 return self._get_parsed_refs()
477 return self._get_parsed_refs()
470
478
471 def _get_parsed_refs(self):
479 def _get_parsed_refs(self):
472 """Return refs as a dict, like:
480 """Return refs as a dict, like:
473 { b'v0.2.0': [b'599ba911aa24d2981225f3966eb659dfae9e9f30', b'T'] }
481 { b'v0.2.0': [b'599ba911aa24d2981225f3966eb659dfae9e9f30', b'T'] }
474 """
482 """
475 _repo = self._repo
483 _repo = self._repo
476 refs = _repo.get_refs()
484 refs = _repo.get_refs()
477 keys = [(b'refs/heads/', b'H'),
485 keys = [(b'refs/heads/', b'H'),
478 (b'refs/remotes/origin/', b'RH'),
486 (b'refs/remotes/origin/', b'RH'),
479 (b'refs/tags/', b'T')]
487 (b'refs/tags/', b'T')]
480 _refs = {}
488 _refs = {}
481 for ref, sha in refs.items():
489 for ref, sha in refs.items():
482 for k, type_ in keys:
490 for k, type_ in keys:
483 if ref.startswith(k):
491 if ref.startswith(k):
484 _key = ref[len(k):]
492 _key = ref[len(k):]
485 if type_ == b'T':
493 if type_ == b'T':
486 obj = _repo.get_object(sha)
494 obj = _repo.get_object(sha)
487 if isinstance(obj, Tag):
495 if isinstance(obj, Tag):
488 sha = _repo.get_object(sha).object[1]
496 sha = _repo.get_object(sha).object[1]
489 _refs[_key] = [sha, type_]
497 _refs[_key] = [sha, type_]
490 break
498 break
491 return _refs
499 return _refs
492
500
493 def _heads(self, reverse=False):
501 def _heads(self, reverse=False):
494 refs = self._repo.get_refs()
502 refs = self._repo.get_refs()
495 heads = {}
503 heads = {}
496
504
497 for key, val in refs.items():
505 for key, val in refs.items():
498 for ref_key in [b'refs/heads/', b'refs/remotes/origin/']:
506 for ref_key in [b'refs/heads/', b'refs/remotes/origin/']:
499 if key.startswith(ref_key):
507 if key.startswith(ref_key):
500 n = key[len(ref_key):]
508 n = key[len(ref_key):]
501 if n not in [b'HEAD']:
509 if n not in [b'HEAD']:
502 heads[n] = val
510 heads[n] = val
503
511
504 return heads if reverse else dict((y, x) for x, y in heads.items())
512 return heads if reverse else dict((y, x) for x, y in heads.items())
505
513
506 def get_changeset(self, revision=None):
514 def get_changeset(self, revision=None):
507 """
515 """
508 Returns ``GitChangeset`` object representing commit from git repository
516 Returns ``GitChangeset`` object representing commit from git repository
509 at the given revision or head (most recent commit) if None given.
517 at the given revision or head (most recent commit) if None given.
510 """
518 """
511 if isinstance(revision, changeset.GitChangeset):
519 if isinstance(revision, changeset.GitChangeset):
512 return revision
520 return revision
513 return changeset.GitChangeset(repository=self, revision=self._get_revision(revision))
521 return changeset.GitChangeset(repository=self, revision=self._get_revision(revision))
514
522
515 def get_changesets(self, start=None, end=None, start_date=None,
523 def get_changesets(self, start=None, end=None, start_date=None,
516 end_date=None, branch_name=None, reverse=False, max_revisions=None):
524 end_date=None, branch_name=None, reverse=False, max_revisions=None):
517 """
525 """
518 Returns iterator of ``GitChangeset`` objects from start to end (both
526 Returns iterator of ``GitChangeset`` objects from start to end (both
519 are inclusive), in ascending date order (unless ``reverse`` is set).
527 are inclusive), in ascending date order (unless ``reverse`` is set).
520
528
521 :param start: changeset ID, as str; first returned changeset
529 :param start: changeset ID, as str; first returned changeset
522 :param end: changeset ID, as str; last returned changeset
530 :param end: changeset ID, as str; last returned changeset
523 :param start_date: if specified, changesets with commit date less than
531 :param start_date: if specified, changesets with commit date less than
524 ``start_date`` would be filtered out from returned set
532 ``start_date`` would be filtered out from returned set
525 :param end_date: if specified, changesets with commit date greater than
533 :param end_date: if specified, changesets with commit date greater than
526 ``end_date`` would be filtered out from returned set
534 ``end_date`` would be filtered out from returned set
527 :param branch_name: if specified, changesets not reachable from given
535 :param branch_name: if specified, changesets not reachable from given
528 branch would be filtered out from returned set
536 branch would be filtered out from returned set
529 :param reverse: if ``True``, returned generator would be reversed
537 :param reverse: if ``True``, returned generator would be reversed
530 (meaning that returned changesets would have descending date order)
538 (meaning that returned changesets would have descending date order)
531
539
532 :raise BranchDoesNotExistError: If given ``branch_name`` does not
540 :raise BranchDoesNotExistError: If given ``branch_name`` does not
533 exist.
541 exist.
534 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
542 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
535 ``end`` could not be found.
543 ``end`` could not be found.
536
544
537 """
545 """
538 if branch_name and branch_name not in self.branches:
546 if branch_name and branch_name not in self.branches:
539 raise BranchDoesNotExistError("Branch '%s' not found"
547 raise BranchDoesNotExistError("Branch '%s' not found"
540 % branch_name)
548 % branch_name)
541 # actually we should check now if it's not an empty repo to not spaw
549 # actually we should check now if it's not an empty repo to not spaw
542 # subprocess commands
550 # subprocess commands
543 if self._empty:
551 if self._empty:
544 raise EmptyRepositoryError("There are no changesets yet")
552 raise EmptyRepositoryError("There are no changesets yet")
545
553
546 # %H at format means (full) commit hash, initial hashes are retrieved
554 # %H at format means (full) commit hash, initial hashes are retrieved
547 # in ascending date order
555 # in ascending date order
548 cmd = ['log', '--date-order', '--reverse', '--pretty=format:%H']
556 cmd = ['log', '--date-order', '--reverse', '--pretty=format:%H']
549 if max_revisions:
557 if max_revisions:
550 cmd += ['--max-count=%s' % max_revisions]
558 cmd += ['--max-count=%s' % max_revisions]
551 if start_date:
559 if start_date:
552 cmd += ['--since', start_date.strftime('%m/%d/%y %H:%M:%S')]
560 cmd += ['--since', start_date.strftime('%m/%d/%y %H:%M:%S')]
553 if end_date:
561 if end_date:
554 cmd += ['--until', end_date.strftime('%m/%d/%y %H:%M:%S')]
562 cmd += ['--until', end_date.strftime('%m/%d/%y %H:%M:%S')]
555 if branch_name:
563 if branch_name:
556 cmd.append(branch_name)
564 cmd.append(branch_name)
557 else:
565 else:
558 cmd.append(settings.GIT_REV_FILTER)
566 cmd.append(settings.GIT_REV_FILTER)
559
567
560 revs = self.run_git_command(cmd).splitlines()
568 revs = self.run_git_command(cmd).splitlines()
561 start_pos = 0
569 start_pos = 0
562 end_pos = len(revs)
570 end_pos = len(revs)
563 if start:
571 if start:
564 _start = self._get_revision(start)
572 _start = self._get_revision(start)
565 try:
573 try:
566 start_pos = revs.index(_start)
574 start_pos = revs.index(_start)
567 except ValueError:
575 except ValueError:
568 pass
576 pass
569
577
570 if end is not None:
578 if end is not None:
571 _end = self._get_revision(end)
579 _end = self._get_revision(end)
572 try:
580 try:
573 end_pos = revs.index(_end)
581 end_pos = revs.index(_end)
574 except ValueError:
582 except ValueError:
575 pass
583 pass
576
584
577 if None not in [start, end] and start_pos > end_pos:
585 if None not in [start, end] and start_pos > end_pos:
578 raise RepositoryError('start cannot be after end')
586 raise RepositoryError('start cannot be after end')
579
587
580 if end_pos is not None:
588 if end_pos is not None:
581 end_pos += 1
589 end_pos += 1
582
590
583 revs = revs[start_pos:end_pos]
591 revs = revs[start_pos:end_pos]
584 if reverse:
592 if reverse:
585 revs.reverse()
593 revs.reverse()
586
594
587 return CollectionGenerator(self, revs)
595 return CollectionGenerator(self, revs)
588
596
589 def get_diff_changesets(self, org_rev, other_repo, other_rev):
597 def get_diff_changesets(self, org_rev, other_repo, other_rev):
590 """
598 """
591 Returns lists of changesets that can be merged from this repo @org_rev
599 Returns lists of changesets that can be merged from this repo @org_rev
592 to other_repo @other_rev
600 to other_repo @other_rev
593 ... and the other way
601 ... and the other way
594 ... and the ancestors that would be used for merge
602 ... and the ancestors that would be used for merge
595
603
596 :param org_rev: the revision we want our compare to be made
604 :param org_rev: the revision we want our compare to be made
597 :param other_repo: repo object, most likely the fork of org_repo. It has
605 :param other_repo: repo object, most likely the fork of org_repo. It has
598 all changesets that we need to obtain
606 all changesets that we need to obtain
599 :param other_rev: revision we want out compare to be made on other_repo
607 :param other_rev: revision we want out compare to be made on other_repo
600 """
608 """
601 org_changesets = []
609 org_changesets = []
602 ancestors = None
610 ancestors = None
603 if org_rev == other_rev:
611 if org_rev == other_rev:
604 other_changesets = []
612 other_changesets = []
605 elif self != other_repo:
613 elif self != other_repo:
606 gitrepo = Repo(self.path)
614 gitrepo = Repo(self.path)
607 SubprocessGitClient(thin_packs=False).fetch(other_repo.path, gitrepo)
615 SubprocessGitClient(thin_packs=False).fetch(other_repo.path, gitrepo)
608
616
609 gitrepo_remote = Repo(other_repo.path)
617 gitrepo_remote = Repo(other_repo.path)
610 SubprocessGitClient(thin_packs=False).fetch(self.path, gitrepo_remote)
618 SubprocessGitClient(thin_packs=False).fetch(self.path, gitrepo_remote)
611
619
612 revs = [
620 revs = [
613 ascii_str(x.commit.id)
621 ascii_str(x.commit.id)
614 for x in gitrepo_remote.get_walker(include=[ascii_bytes(other_rev)],
622 for x in gitrepo_remote.get_walker(include=[ascii_bytes(other_rev)],
615 exclude=[ascii_bytes(org_rev)])
623 exclude=[ascii_bytes(org_rev)])
616 ]
624 ]
617 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
625 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
618 if other_changesets:
626 if other_changesets:
619 ancestors = [other_changesets[0].parents[0].raw_id]
627 ancestors = [other_changesets[0].parents[0].raw_id]
620 else:
628 else:
621 # no changesets from other repo, ancestor is the other_rev
629 # no changesets from other repo, ancestor is the other_rev
622 ancestors = [other_rev]
630 ancestors = [other_rev]
623
631
624 gitrepo.close()
632 gitrepo.close()
625 gitrepo_remote.close()
633 gitrepo_remote.close()
626
634
627 else:
635 else:
628 so = self.run_git_command(
636 so = self.run_git_command(
629 ['log', '--reverse', '--pretty=format:%H',
637 ['log', '--reverse', '--pretty=format:%H',
630 '-s', '%s..%s' % (org_rev, other_rev)]
638 '-s', '%s..%s' % (org_rev, other_rev)]
631 )
639 )
632 other_changesets = [self.get_changeset(cs)
640 other_changesets = [self.get_changeset(cs)
633 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
641 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
634 so = self.run_git_command(
642 so = self.run_git_command(
635 ['merge-base', org_rev, other_rev]
643 ['merge-base', org_rev, other_rev]
636 )
644 )
637 ancestors = [re.findall(r'[0-9a-fA-F]{40}', so)[0]]
645 ancestors = [re.findall(r'[0-9a-fA-F]{40}', so)[0]]
638
646
639 return other_changesets, org_changesets, ancestors
647 return other_changesets, org_changesets, ancestors
640
648
641 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
649 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
642 context=3):
650 context=3):
643 """
651 """
644 Returns (git like) *diff*, as plain bytes text. Shows changes
652 Returns (git like) *diff*, as plain bytes text. Shows changes
645 introduced by ``rev2`` since ``rev1``.
653 introduced by ``rev2`` since ``rev1``.
646
654
647 :param rev1: Entry point from which diff is shown. Can be
655 :param rev1: Entry point from which diff is shown. Can be
648 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
656 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
649 the changes since empty state of the repository until ``rev2``
657 the changes since empty state of the repository until ``rev2``
650 :param rev2: Until which revision changes should be shown.
658 :param rev2: Until which revision changes should be shown.
651 :param ignore_whitespace: If set to ``True``, would not show whitespace
659 :param ignore_whitespace: If set to ``True``, would not show whitespace
652 changes. Defaults to ``False``.
660 changes. Defaults to ``False``.
653 :param context: How many lines before/after changed lines should be
661 :param context: How many lines before/after changed lines should be
654 shown. Defaults to ``3``. Due to limitations in Git, if
662 shown. Defaults to ``3``. Due to limitations in Git, if
655 value passed-in is greater than ``2**31-1``
663 value passed-in is greater than ``2**31-1``
656 (``2147483647``), it will be set to ``2147483647``
664 (``2147483647``), it will be set to ``2147483647``
657 instead. If negative value is passed-in, it will be set to
665 instead. If negative value is passed-in, it will be set to
658 ``0`` instead.
666 ``0`` instead.
659 """
667 """
660
668
661 # Git internally uses a signed long int for storing context
669 # Git internally uses a signed long int for storing context
662 # size (number of lines to show before and after the
670 # size (number of lines to show before and after the
663 # differences). This can result in integer overflow, so we
671 # differences). This can result in integer overflow, so we
664 # ensure the requested context is smaller by one than the
672 # ensure the requested context is smaller by one than the
665 # number that would cause the overflow. It is highly unlikely
673 # number that would cause the overflow. It is highly unlikely
666 # that a single file will contain that many lines, so this
674 # that a single file will contain that many lines, so this
667 # kind of change should not cause any realistic consequences.
675 # kind of change should not cause any realistic consequences.
668 overflowed_long_int = 2**31
676 overflowed_long_int = 2**31
669
677
670 if context >= overflowed_long_int:
678 if context >= overflowed_long_int:
671 context = overflowed_long_int - 1
679 context = overflowed_long_int - 1
672
680
673 # Negative context values make no sense, and will result in
681 # Negative context values make no sense, and will result in
674 # errors. Ensure this does not happen.
682 # errors. Ensure this does not happen.
675 if context < 0:
683 if context < 0:
676 context = 0
684 context = 0
677
685
678 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
686 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
679 if ignore_whitespace:
687 if ignore_whitespace:
680 flags.append('-w')
688 flags.append('-w')
681
689
682 if hasattr(rev1, 'raw_id'):
690 if hasattr(rev1, 'raw_id'):
683 rev1 = getattr(rev1, 'raw_id')
691 rev1 = getattr(rev1, 'raw_id')
684
692
685 if hasattr(rev2, 'raw_id'):
693 if hasattr(rev2, 'raw_id'):
686 rev2 = getattr(rev2, 'raw_id')
694 rev2 = getattr(rev2, 'raw_id')
687
695
688 if rev1 == self.EMPTY_CHANGESET:
696 if rev1 == self.EMPTY_CHANGESET:
689 rev2 = self.get_changeset(rev2).raw_id
697 rev2 = self.get_changeset(rev2).raw_id
690 cmd = ['show'] + flags + [rev2]
698 cmd = ['show'] + flags + [rev2]
691 else:
699 else:
692 rev1 = self.get_changeset(rev1).raw_id
700 rev1 = self.get_changeset(rev1).raw_id
693 rev2 = self.get_changeset(rev2).raw_id
701 rev2 = self.get_changeset(rev2).raw_id
694 cmd = ['diff'] + flags + [rev1, rev2]
702 cmd = ['diff'] + flags + [rev1, rev2]
695
703
696 if path:
704 if path:
697 cmd += ['--', path]
705 cmd += ['--', path]
698
706
699 stdout, stderr = self._run_git_command(cmd, cwd=self.path)
707 stdout, stderr = self._run_git_command(cmd, cwd=self.path)
700 # If we used 'show' command, strip first few lines (until actual diff
708 # If we used 'show' command, strip first few lines (until actual diff
701 # starts)
709 # starts)
702 if rev1 == self.EMPTY_CHANGESET:
710 if rev1 == self.EMPTY_CHANGESET:
703 parts = stdout.split(b'\ndiff ', 1)
711 parts = stdout.split(b'\ndiff ', 1)
704 if len(parts) > 1:
712 if len(parts) > 1:
705 stdout = b'diff ' + parts[1]
713 stdout = b'diff ' + parts[1]
706 return stdout
714 return stdout
707
715
708 @LazyProperty
716 @LazyProperty
709 def in_memory_changeset(self):
717 def in_memory_changeset(self):
710 """
718 """
711 Returns ``GitInMemoryChangeset`` object for this repository.
719 Returns ``GitInMemoryChangeset`` object for this repository.
712 """
720 """
713 return inmemory.GitInMemoryChangeset(self)
721 return inmemory.GitInMemoryChangeset(self)
714
722
715 def clone(self, url, update_after_clone=True, bare=False):
723 def clone(self, url, update_after_clone=True, bare=False):
716 """
724 """
717 Tries to clone changes from external location.
725 Tries to clone changes from external location.
718
726
719 :param update_after_clone: If set to ``False``, git won't checkout
727 :param update_after_clone: If set to ``False``, git won't checkout
720 working directory
728 working directory
721 :param bare: If set to ``True``, repository would be cloned into
729 :param bare: If set to ``True``, repository would be cloned into
722 *bare* git repository (no working directory at all).
730 *bare* git repository (no working directory at all).
723 """
731 """
724 url = self._get_url(url)
732 url = self._get_url(url)
725 cmd = ['clone', '-q']
733 cmd = ['clone', '-q']
726 if bare:
734 if bare:
727 cmd.append('--bare')
735 cmd.append('--bare')
728 elif not update_after_clone:
736 elif not update_after_clone:
729 cmd.append('--no-checkout')
737 cmd.append('--no-checkout')
730 cmd += ['--', url, self.path]
738 cmd += ['--', url, self.path]
731 # If error occurs run_git_command raises RepositoryError already
739 # If error occurs run_git_command raises RepositoryError already
732 self.run_git_command(cmd)
740 self.run_git_command(cmd)
733
741
734 def pull(self, url):
742 def pull(self, url):
735 """
743 """
736 Tries to pull changes from external location.
744 Tries to pull changes from external location.
737 """
745 """
738 url = self._get_url(url)
746 url = self._get_url(url)
739 cmd = ['pull', '--ff-only', url]
747 cmd = ['pull', '--ff-only', url]
740 # If error occurs run_git_command raises RepositoryError already
748 # If error occurs run_git_command raises RepositoryError already
741 self.run_git_command(cmd)
749 self.run_git_command(cmd)
742
750
743 def fetch(self, url):
751 def fetch(self, url):
744 """
752 """
745 Tries to pull changes from external location.
753 Tries to pull changes from external location.
746 """
754 """
747 url = self._get_url(url)
755 url = self._get_url(url)
748 so = self.run_git_command(['ls-remote', '-h', url])
756 so = self.run_git_command(['ls-remote', '-h', url])
749 cmd = ['fetch', url, '--']
757 cmd = ['fetch', url, '--']
750 for line in so.splitlines():
758 for line in so.splitlines():
751 sha, ref = line.split('\t')
759 sha, ref = line.split('\t')
752 cmd.append('+%s:%s' % (ref, ref))
760 cmd.append('+%s:%s' % (ref, ref))
753 self.run_git_command(cmd)
761 self.run_git_command(cmd)
754
762
755 def _update_server_info(self):
763 def _update_server_info(self):
756 """
764 """
757 runs gits update-server-info command in this repo instance
765 runs gits update-server-info command in this repo instance
758 """
766 """
759 try:
767 try:
760 update_server_info(self._repo)
768 update_server_info(self._repo)
761 except OSError as e:
769 except OSError as e:
762 if e.errno not in [errno.ENOENT, errno.EROFS]:
770 if e.errno not in [errno.ENOENT, errno.EROFS]:
763 raise
771 raise
764 # Workaround for dulwich crashing on for example its own dulwich/tests/data/repos/simple_merge.git/info/refs.lock
772 # Workaround for dulwich crashing on for example its own dulwich/tests/data/repos/simple_merge.git/info/refs.lock
765 log.error('Ignoring %s running update-server-info: %s', type(e).__name__, e)
773 log.error('Ignoring %s running update-server-info: %s', type(e).__name__, e)
766
774
767 @LazyProperty
775 @LazyProperty
768 def workdir(self):
776 def workdir(self):
769 """
777 """
770 Returns ``Workdir`` instance for this repository.
778 Returns ``Workdir`` instance for this repository.
771 """
779 """
772 return workdir.GitWorkdir(self)
780 return workdir.GitWorkdir(self)
773
781
774 def get_config_value(self, section, name, config_file=None):
782 def get_config_value(self, section, name, config_file=None):
775 """
783 """
776 Returns configuration value for a given [``section``] and ``name``.
784 Returns configuration value for a given [``section``] and ``name``.
777
785
778 :param section: Section we want to retrieve value from
786 :param section: Section we want to retrieve value from
779 :param name: Name of configuration we want to retrieve
787 :param name: Name of configuration we want to retrieve
780 :param config_file: A path to file which should be used to retrieve
788 :param config_file: A path to file which should be used to retrieve
781 configuration from (might also be a list of file paths)
789 configuration from (might also be a list of file paths)
782 """
790 """
783 if config_file is None:
791 if config_file is None:
784 config_file = []
792 config_file = []
785 elif isinstance(config_file, str):
793 elif isinstance(config_file, str):
786 config_file = [config_file]
794 config_file = [config_file]
787
795
788 def gen_configs():
796 def gen_configs():
789 for path in config_file + self._config_files:
797 for path in config_file + self._config_files:
790 try:
798 try:
791 yield ConfigFile.from_path(path)
799 yield ConfigFile.from_path(path)
792 except (IOError, OSError, ValueError):
800 except (IOError, OSError, ValueError):
793 continue
801 continue
794
802
795 for config in gen_configs():
803 for config in gen_configs():
796 try:
804 try:
797 value = config.get(section, name)
805 value = config.get(section, name)
798 except KeyError:
806 except KeyError:
799 continue
807 continue
800 return None if value is None else safe_str(value)
808 return None if value is None else safe_str(value)
801 return None
809 return None
802
810
803 def get_user_name(self, config_file=None):
811 def get_user_name(self, config_file=None):
804 """
812 """
805 Returns user's name from global configuration file.
813 Returns user's name from global configuration file.
806
814
807 :param config_file: A path to file which should be used to retrieve
815 :param config_file: A path to file which should be used to retrieve
808 configuration from (might also be a list of file paths)
816 configuration from (might also be a list of file paths)
809 """
817 """
810 return self.get_config_value('user', 'name', config_file)
818 return self.get_config_value('user', 'name', config_file)
811
819
812 def get_user_email(self, config_file=None):
820 def get_user_email(self, config_file=None):
813 """
821 """
814 Returns user's email from global configuration file.
822 Returns user's email from global configuration file.
815
823
816 :param config_file: A path to file which should be used to retrieve
824 :param config_file: A path to file which should be used to retrieve
817 configuration from (might also be a list of file paths)
825 configuration from (might also be a list of file paths)
818 """
826 """
819 return self.get_config_value('user', 'email', config_file)
827 return self.get_config_value('user', 'email', config_file)
General Comments 0
You need to be logged in to leave comments. Login now