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