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