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