##// END OF EJS Templates
synced with latest vcs
marcink -
r3805:a5c234e9 beta
parent child Browse files
Show More
@@ -1,43 +1,43 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs
4 4 ~~~
5 5
6 6 Various version Control System (vcs) management abstraction layer for
7 7 Python.
8 8
9 9 :created_on: Apr 8, 2010
10 10 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
11 11 """
12 12
13 VERSION = (0, 4, 0, 'dev')
13 VERSION = (0, 5, 0, 'dev')
14 14
15 15 __version__ = '.'.join((str(each) for each in VERSION[:4]))
16 16
17 17 __all__ = [
18 18 'get_version', 'get_repo', 'get_backend',
19 19 'VCSError', 'RepositoryError', 'ChangesetError'
20 20 ]
21 21
22 22 import sys
23 23 from rhodecode.lib.vcs.backends import get_repo, get_backend
24 24 from rhodecode.lib.vcs.exceptions import VCSError, RepositoryError, ChangesetError
25 25
26 26
27 27 def get_version():
28 28 """
29 29 Returns shorter version (digit parts only) as string.
30 30 """
31 31 return '.'.join((str(each) for each in VERSION[:3]))
32 32
33 33
34 34 def main(argv=None):
35 35 if argv is None:
36 36 argv = sys.argv
37 37 from rhodecode.lib.vcs.cli import ExecutionManager
38 38 manager = ExecutionManager(argv)
39 39 manager.execute()
40 40 return 0
41 41
42 42 if __name__ == '__main__':
43 43 sys.exit(main(sys.argv))
@@ -1,552 +1,553 b''
1 1 import re
2 2 from itertools import chain
3 3 from dulwich import objects
4 4 from subprocess import Popen, PIPE
5 5
6 6 from rhodecode.lib.vcs.conf import settings
7 7 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
8 8 from rhodecode.lib.vcs.exceptions import (
9 9 RepositoryError, ChangesetError, NodeDoesNotExistError, VCSError,
10 10 ChangesetDoesNotExistError, ImproperArchiveTypeError
11 11 )
12 12 from rhodecode.lib.vcs.nodes import (
13 13 FileNode, DirNode, NodeKind, RootNode, RemovedFileNode, SubModuleNode,
14 14 ChangedFileNodesGenerator, AddedFileNodesGenerator, RemovedFileNodesGenerator
15 15 )
16 16 from rhodecode.lib.vcs.utils import (
17 17 safe_unicode, safe_str, safe_int, date_fromtimestamp
18 18 )
19 19 from rhodecode.lib.vcs.utils.lazy import LazyProperty
20 20
21 21
22 22 class GitChangeset(BaseChangeset):
23 23 """
24 24 Represents state of the repository at single revision.
25 25 """
26 26
27 27 def __init__(self, repository, revision):
28 28 self._stat_modes = {}
29 29 self.repository = repository
30 30
31 31 try:
32 commit = self.repository._repo.get_object(revision)
32 commit = self.repository._repo[revision]
33 33 if isinstance(commit, objects.Tag):
34 34 revision = commit.object[1]
35 35 commit = self.repository._repo.get_object(commit.object[1])
36 36 except KeyError:
37 37 raise RepositoryError("Cannot get object with id %s" % revision)
38 38 self.raw_id = revision
39 39 self.id = self.raw_id
40 40 self.short_id = self.raw_id[:12]
41 41 self._commit = commit
42
43 42 self._tree_id = commit.tree
44 43 self._committer_property = 'committer'
45 44 self._author_property = 'author'
46 45 self._date_property = 'commit_time'
47 46 self._date_tz_property = 'commit_timezone'
48 47 self.revision = repository.revisions.index(revision)
49 48
50 self.message = safe_unicode(commit.message)
51
52 49 self.nodes = {}
53 50 self._paths = {}
54 51
55 52 @LazyProperty
53 def message(self):
54 return safe_unicode(self._commit.message)
55
56 @LazyProperty
56 57 def committer(self):
57 58 return safe_unicode(getattr(self._commit, self._committer_property))
58 59
59 60 @LazyProperty
60 61 def author(self):
61 62 return safe_unicode(getattr(self._commit, self._author_property))
62 63
63 64 @LazyProperty
64 65 def date(self):
65 66 return date_fromtimestamp(getattr(self._commit, self._date_property),
66 67 getattr(self._commit, self._date_tz_property))
67 68
68 69 @LazyProperty
69 70 def _timestamp(self):
70 71 return getattr(self._commit, self._date_property)
71 72
72 73 @LazyProperty
73 74 def status(self):
74 75 """
75 76 Returns modified, added, removed, deleted files for current changeset
76 77 """
77 78 return self.changed, self.added, self.removed
78 79
79 80 @LazyProperty
80 81 def tags(self):
81 82 _tags = []
82 83 for tname, tsha in self.repository.tags.iteritems():
83 84 if tsha == self.raw_id:
84 85 _tags.append(tname)
85 86 return _tags
86 87
87 88 @LazyProperty
88 89 def branch(self):
89 90
90 91 heads = self.repository._heads(reverse=False)
91 92
92 93 ref = heads.get(self.raw_id)
93 94 if ref:
94 95 return safe_unicode(ref)
95 96
96 97 def _fix_path(self, path):
97 98 """
98 99 Paths are stored without trailing slash so we need to get rid off it if
99 100 needed.
100 101 """
101 102 if path.endswith('/'):
102 103 path = path.rstrip('/')
103 104 return path
104 105
105 106 def _get_id_for_path(self, path):
106 107
107 108 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
108 109 if not path in self._paths:
109 110 path = path.strip('/')
110 111 # set root tree
111 112 tree = self.repository._repo[self._tree_id]
112 113 if path == '':
113 114 self._paths[''] = tree.id
114 115 return tree.id
115 116 splitted = path.split('/')
116 117 dirs, name = splitted[:-1], splitted[-1]
117 118 curdir = ''
118 119
119 120 # initially extract things from root dir
120 121 for item, stat, id in tree.iteritems():
121 122 if curdir:
122 123 name = '/'.join((curdir, item))
123 124 else:
124 125 name = item
125 126 self._paths[name] = id
126 127 self._stat_modes[name] = stat
127 128
128 129 for dir in dirs:
129 130 if curdir:
130 131 curdir = '/'.join((curdir, dir))
131 132 else:
132 133 curdir = dir
133 134 dir_id = None
134 135 for item, stat, id in tree.iteritems():
135 136 if dir == item:
136 137 dir_id = id
137 138 if dir_id:
138 139 # Update tree
139 140 tree = self.repository._repo[dir_id]
140 141 if not isinstance(tree, objects.Tree):
141 142 raise ChangesetError('%s is not a directory' % curdir)
142 143 else:
143 144 raise ChangesetError('%s have not been found' % curdir)
144 145
145 146 # cache all items from the given traversed tree
146 147 for item, stat, id in tree.iteritems():
147 148 if curdir:
148 149 name = '/'.join((curdir, item))
149 150 else:
150 151 name = item
151 152 self._paths[name] = id
152 153 self._stat_modes[name] = stat
153 154 if not path in self._paths:
154 155 raise NodeDoesNotExistError("There is no file nor directory "
155 156 "at the given path '%s' at revision %s"
156 157 % (path, self.short_id))
157 158 return self._paths[path]
158 159
159 160 def _get_kind(self, path):
160 161 obj = self.repository._repo[self._get_id_for_path(path)]
161 162 if isinstance(obj, objects.Blob):
162 163 return NodeKind.FILE
163 164 elif isinstance(obj, objects.Tree):
164 165 return NodeKind.DIR
165 166
166 167 def _get_filectx(self, path):
167 168 path = self._fix_path(path)
168 169 if self._get_kind(path) != NodeKind.FILE:
169 170 raise ChangesetError("File does not exist for revision %s at "
170 171 " '%s'" % (self.raw_id, path))
171 172 return path
172 173
173 174 def _get_file_nodes(self):
174 175 return chain(*(t[2] for t in self.walk()))
175 176
176 177 @LazyProperty
177 178 def parents(self):
178 179 """
179 180 Returns list of parents changesets.
180 181 """
181 182 return [self.repository.get_changeset(parent)
182 183 for parent in self._commit.parents]
183 184
184 185 @LazyProperty
185 186 def children(self):
186 187 """
187 188 Returns list of children changesets.
188 189 """
189 190 rev_filter = _git_path = settings.GIT_REV_FILTER
190 191 so, se = self.repository.run_git_command(
191 192 "rev-list %s --children | grep '^%s'" % (rev_filter, self.raw_id)
192 193 )
193 194
194 195 children = []
195 196 for l in so.splitlines():
196 197 childs = l.split(' ')[1:]
197 198 children.extend(childs)
198 199 return [self.repository.get_changeset(cs) for cs in children]
199 200
200 201 def next(self, branch=None):
201 202
202 203 if branch and self.branch != branch:
203 204 raise VCSError('Branch option used on changeset not belonging '
204 205 'to that branch')
205 206
206 207 def _next(changeset, branch):
207 208 try:
208 209 next_ = changeset.revision + 1
209 210 next_rev = changeset.repository.revisions[next_]
210 211 except IndexError:
211 212 raise ChangesetDoesNotExistError
212 213 cs = changeset.repository.get_changeset(next_rev)
213 214
214 215 if branch and branch != cs.branch:
215 216 return _next(cs, branch)
216 217
217 218 return cs
218 219
219 220 return _next(self, branch)
220 221
221 222 def prev(self, branch=None):
222 223 if branch and self.branch != branch:
223 224 raise VCSError('Branch option used on changeset not belonging '
224 225 'to that branch')
225 226
226 227 def _prev(changeset, branch):
227 228 try:
228 229 prev_ = changeset.revision - 1
229 230 if prev_ < 0:
230 231 raise IndexError
231 232 prev_rev = changeset.repository.revisions[prev_]
232 233 except IndexError:
233 234 raise ChangesetDoesNotExistError
234 235
235 236 cs = changeset.repository.get_changeset(prev_rev)
236 237
237 238 if branch and branch != cs.branch:
238 239 return _prev(cs, branch)
239 240
240 241 return cs
241 242
242 243 return _prev(self, branch)
243 244
244 245 def diff(self, ignore_whitespace=True, context=3):
245 246 rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET
246 247 rev2 = self
247 248 return ''.join(self.repository.get_diff(rev1, rev2,
248 249 ignore_whitespace=ignore_whitespace,
249 250 context=context))
250 251
251 252 def get_file_mode(self, path):
252 253 """
253 254 Returns stat mode of the file at the given ``path``.
254 255 """
255 256 # ensure path is traversed
256 257 self._get_id_for_path(path)
257 258 return self._stat_modes[path]
258 259
259 260 def get_file_content(self, path):
260 261 """
261 262 Returns content of the file at given ``path``.
262 263 """
263 264 id = self._get_id_for_path(path)
264 265 blob = self.repository._repo[id]
265 266 return blob.as_pretty_string()
266 267
267 268 def get_file_size(self, path):
268 269 """
269 270 Returns size of the file at given ``path``.
270 271 """
271 272 id = self._get_id_for_path(path)
272 273 blob = self.repository._repo[id]
273 274 return blob.raw_length()
274 275
275 276 def get_file_changeset(self, path):
276 277 """
277 278 Returns last commit of the file at the given ``path``.
278 279 """
279 280 return self.get_file_history(path, limit=1)[0]
280 281
281 282 def get_file_history(self, path, limit=None):
282 283 """
283 284 Returns history of file as reversed list of ``Changeset`` objects for
284 285 which file at given ``path`` has been modified.
285 286
286 287 TODO: This function now uses os underlying 'git' and 'grep' commands
287 288 which is generally not good. Should be replaced with algorithm
288 289 iterating commits.
289 290 """
290 291 self._get_filectx(path)
291 292 cs_id = safe_str(self.id)
292 293 f_path = safe_str(path)
293 294
294 295 if limit:
295 296 cmd = 'log -n %s --pretty="format: %%H" -s -p %s -- "%s"' % (
296 297 safe_int(limit, 0), cs_id, f_path
297 298 )
298 299
299 300 else:
300 301 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
301 302 cs_id, f_path
302 303 )
303 304 so, se = self.repository.run_git_command(cmd)
304 305 ids = re.findall(r'[0-9a-fA-F]{40}', so)
305 306 return [self.repository.get_changeset(id) for id in ids]
306 307
307 308 def get_file_history_2(self, path):
308 309 """
309 310 Returns history of file as reversed list of ``Changeset`` objects for
310 311 which file at given ``path`` has been modified.
311 312
312 313 """
313 314 self._get_filectx(path)
314 315 from dulwich.walk import Walker
315 316 include = [self.id]
316 317 walker = Walker(self.repository._repo.object_store, include,
317 318 paths=[path], max_entries=1)
318 319 return [self.repository.get_changeset(sha)
319 320 for sha in (x.commit.id for x in walker)]
320 321
321 322 def get_file_annotate(self, path):
322 323 """
323 324 Returns a generator of four element tuples with
324 325 lineno, sha, changeset lazy loader and line
325 326
326 327 TODO: This function now uses os underlying 'git' command which is
327 328 generally not good. Should be replaced with algorithm iterating
328 329 commits.
329 330 """
330 331 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
331 332 # -l ==> outputs long shas (and we need all 40 characters)
332 333 # --root ==> doesn't put '^' character for bounderies
333 334 # -r sha ==> blames for the given revision
334 335 so, se = self.repository.run_git_command(cmd)
335 336
336 337 for i, blame_line in enumerate(so.split('\n')[:-1]):
337 338 ln_no = i + 1
338 339 sha, line = re.split(r' ', blame_line, 1)
339 340 yield (ln_no, sha, lambda: self.repository.get_changeset(sha), line)
340 341
341 342 def fill_archive(self, stream=None, kind='tgz', prefix=None,
342 343 subrepos=False):
343 344 """
344 345 Fills up given stream.
345 346
346 347 :param stream: file like object.
347 348 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
348 349 Default: ``tgz``.
349 350 :param prefix: name of root directory in archive.
350 351 Default is repository name and changeset's raw_id joined with dash
351 352 (``repo-tip.<KIND>``).
352 353 :param subrepos: include subrepos in this archive.
353 354
354 355 :raise ImproperArchiveTypeError: If given kind is wrong.
355 356 :raise VcsError: If given stream is None
356 357
357 358 """
358 359 allowed_kinds = settings.ARCHIVE_SPECS.keys()
359 360 if kind not in allowed_kinds:
360 361 raise ImproperArchiveTypeError('Archive kind not supported use one'
361 362 'of %s', allowed_kinds)
362 363
363 364 if prefix is None:
364 365 prefix = '%s-%s' % (self.repository.name, self.short_id)
365 366 elif prefix.startswith('/'):
366 367 raise VCSError("Prefix cannot start with leading slash")
367 368 elif prefix.strip() == '':
368 369 raise VCSError("Prefix cannot be empty")
369 370
370 371 if kind == 'zip':
371 372 frmt = 'zip'
372 373 else:
373 374 frmt = 'tar'
374 375 _git_path = settings.GIT_EXECUTABLE_PATH
375 376 cmd = '%s archive --format=%s --prefix=%s/ %s' % (_git_path,
376 377 frmt, prefix, self.raw_id)
377 378 if kind == 'tgz':
378 379 cmd += ' | gzip -9'
379 380 elif kind == 'tbz2':
380 381 cmd += ' | bzip2 -9'
381 382
382 383 if stream is None:
383 384 raise VCSError('You need to pass in a valid stream for filling'
384 385 ' with archival data')
385 386 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
386 387 cwd=self.repository.path)
387 388
388 389 buffer_size = 1024 * 8
389 390 chunk = popen.stdout.read(buffer_size)
390 391 while chunk:
391 392 stream.write(chunk)
392 393 chunk = popen.stdout.read(buffer_size)
393 394 # Make sure all descriptors would be read
394 395 popen.communicate()
395 396
396 397 def get_nodes(self, path):
397 398 if self._get_kind(path) != NodeKind.DIR:
398 399 raise ChangesetError("Directory does not exist for revision %s at "
399 400 " '%s'" % (self.revision, path))
400 401 path = self._fix_path(path)
401 402 id = self._get_id_for_path(path)
402 403 tree = self.repository._repo[id]
403 404 dirnodes = []
404 405 filenodes = []
405 406 als = self.repository.alias
406 407 for name, stat, id in tree.iteritems():
407 408 if objects.S_ISGITLINK(stat):
408 409 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
409 410 alias=als))
410 411 continue
411 412
412 413 obj = self.repository._repo.get_object(id)
413 414 if path != '':
414 415 obj_path = '/'.join((path, name))
415 416 else:
416 417 obj_path = name
417 418 if obj_path not in self._stat_modes:
418 419 self._stat_modes[obj_path] = stat
419 420 if isinstance(obj, objects.Tree):
420 421 dirnodes.append(DirNode(obj_path, changeset=self))
421 422 elif isinstance(obj, objects.Blob):
422 423 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
423 424 else:
424 425 raise ChangesetError("Requested object should be Tree "
425 426 "or Blob, is %r" % type(obj))
426 427 nodes = dirnodes + filenodes
427 428 for node in nodes:
428 429 if not node.path in self.nodes:
429 430 self.nodes[node.path] = node
430 431 nodes.sort()
431 432 return nodes
432 433
433 434 def get_node(self, path):
434 435 if isinstance(path, unicode):
435 436 path = path.encode('utf-8')
436 437 path = self._fix_path(path)
437 438 if not path in self.nodes:
438 439 try:
439 440 id_ = self._get_id_for_path(path)
440 441 except ChangesetError:
441 442 raise NodeDoesNotExistError("Cannot find one of parents' "
442 443 "directories for a given path: %s" % path)
443 444
444 445 _GL = lambda m: m and objects.S_ISGITLINK(m)
445 446 if _GL(self._stat_modes.get(path)):
446 447 node = SubModuleNode(path, url=None, changeset=id_,
447 448 alias=self.repository.alias)
448 449 else:
449 450 obj = self.repository._repo.get_object(id_)
450 451
451 452 if isinstance(obj, objects.Tree):
452 453 if path == '':
453 454 node = RootNode(changeset=self)
454 455 else:
455 456 node = DirNode(path, changeset=self)
456 457 node._tree = obj
457 458 elif isinstance(obj, objects.Blob):
458 459 node = FileNode(path, changeset=self)
459 460 node._blob = obj
460 461 else:
461 462 raise NodeDoesNotExistError("There is no file nor directory "
462 463 "at the given path '%s' at revision %s"
463 464 % (path, self.short_id))
464 465 # cache node
465 466 self.nodes[path] = node
466 467 return self.nodes[path]
467 468
468 469 @LazyProperty
469 470 def affected_files(self):
470 471 """
471 472 Get's a fast accessible file changes for given changeset
472 473 """
473 474 added, modified, deleted = self._changes_cache
474 475 return list(added.union(modified).union(deleted))
475 476
476 477 @LazyProperty
477 478 def _diff_name_status(self):
478 479 output = []
479 480 for parent in self.parents:
480 481 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id,
481 482 self.raw_id)
482 483 so, se = self.repository.run_git_command(cmd)
483 484 output.append(so.strip())
484 485 return '\n'.join(output)
485 486
486 487 @LazyProperty
487 488 def _changes_cache(self):
488 489 added = set()
489 490 modified = set()
490 491 deleted = set()
491 492 _r = self.repository._repo
492 493
493 494 parents = self.parents
494 495 if not self.parents:
495 496 parents = [EmptyChangeset()]
496 497 for parent in parents:
497 498 if isinstance(parent, EmptyChangeset):
498 499 oid = None
499 500 else:
500 501 oid = _r[parent.raw_id].tree
501 502 changes = _r.object_store.tree_changes(oid, _r[self.raw_id].tree)
502 503 for (oldpath, newpath), (_, _), (_, _) in changes:
503 504 if newpath and oldpath:
504 505 modified.add(newpath)
505 506 elif newpath and not oldpath:
506 507 added.add(newpath)
507 508 elif not newpath and oldpath:
508 509 deleted.add(oldpath)
509 510 return added, modified, deleted
510 511
511 512 def _get_paths_for_status(self, status):
512 513 """
513 514 Returns sorted list of paths for given ``status``.
514 515
515 516 :param status: one of: *added*, *modified* or *deleted*
516 517 """
517 518 added, modified, deleted = self._changes_cache
518 519 return sorted({
519 520 'added': list(added),
520 521 'modified': list(modified),
521 522 'deleted': list(deleted)}[status]
522 523 )
523 524
524 525 @LazyProperty
525 526 def added(self):
526 527 """
527 528 Returns list of added ``FileNode`` objects.
528 529 """
529 530 if not self.parents:
530 531 return list(self._get_file_nodes())
531 532 return AddedFileNodesGenerator([n for n in
532 533 self._get_paths_for_status('added')], self)
533 534
534 535 @LazyProperty
535 536 def changed(self):
536 537 """
537 538 Returns list of modified ``FileNode`` objects.
538 539 """
539 540 if not self.parents:
540 541 return []
541 542 return ChangedFileNodesGenerator([n for n in
542 543 self._get_paths_for_status('modified')], self)
543 544
544 545 @LazyProperty
545 546 def removed(self):
546 547 """
547 548 Returns list of removed ``FileNode`` objects.
548 549 """
549 550 if not self.parents:
550 551 return []
551 552 return RemovedFileNodesGenerator([n for n in
552 553 self._get_paths_for_status('deleted')], self)
@@ -1,688 +1,694 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.git.repository
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Git repository 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 urllib
16 16 import urllib2
17 17 import logging
18 18 import posixpath
19 19 import string
20 20
21 21 from dulwich.objects import Tag
22 22 from dulwich.repo import Repo, NotGitRepository
23 23
24 24 from rhodecode.lib.vcs import subprocessio
25 25 from rhodecode.lib.vcs.backends.base import BaseRepository, CollectionGenerator
26 26 from rhodecode.lib.vcs.conf import settings
27 27
28 28 from rhodecode.lib.vcs.exceptions import (
29 29 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
30 30 RepositoryError, TagAlreadyExistError, TagDoesNotExistError
31 31 )
32 32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
33 33 from rhodecode.lib.vcs.utils.lazy import LazyProperty
34 34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
35 35 from rhodecode.lib.vcs.utils.paths import abspath, get_user_home
36 36
37 37 from rhodecode.lib.vcs.utils.hgcompat import (
38 38 hg_url, httpbasicauthhandler, httpdigestauthhandler
39 39 )
40 40
41 41 from .changeset import GitChangeset
42 42 from .config import ConfigFile
43 43 from .inmemory import GitInMemoryChangeset
44 44 from .workdir import GitWorkdir
45 45
46 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
47
46 48 log = logging.getLogger(__name__)
47 49
48 50
49 51 class GitRepository(BaseRepository):
50 52 """
51 53 Git repository backend.
52 54 """
53 55 DEFAULT_BRANCH_NAME = 'master'
54 56 scm = 'git'
55 57
56 58 def __init__(self, repo_path, create=False, src_url=None,
57 59 update_after_clone=False, bare=False):
58 60
59 61 self.path = abspath(repo_path)
60 62 repo = self._get_repo(create, src_url, update_after_clone, bare)
61 63 self.bare = repo.bare
62 64
63 self._config_files = [
64 bare and abspath(self.path, 'config')
65 or abspath(self.path, '.git', 'config'),
66 abspath(get_user_home(), '.gitconfig'),
67 ]
65 @property
66 def _config_files(self):
67 return [
68 self.bare and abspath(self.path, 'config')
69 or abspath(self.path, '.git', 'config'),
70 abspath(get_user_home(), '.gitconfig'),
71 ]
68 72
69 73 @property
70 74 def _repo(self):
71 75 return Repo(self.path)
72 76
73 77 @property
74 78 def head(self):
75 79 try:
76 80 return self._repo.head()
77 81 except KeyError:
78 82 return None
79 83
80 84 @LazyProperty
81 85 def revisions(self):
82 86 """
83 87 Returns list of revisions' ids, in ascending order. Being lazy
84 88 attribute allows external tools to inject shas from cache.
85 89 """
86 90 return self._get_all_revisions()
87 91
88 92 @classmethod
89 93 def _run_git_command(cls, cmd, **opts):
90 94 """
91 95 Runs given ``cmd`` as git command and returns tuple
92 96 (stdout, stderr).
93 97
94 98 :param cmd: git command to be executed
95 99 :param opts: env options to pass into Subprocess command
96 100 """
97 101
98 102 if '_bare' in opts:
99 103 _copts = []
100 104 del opts['_bare']
101 105 else:
102 106 _copts = ['-c', 'core.quotepath=false', ]
103 107 safe_call = False
104 108 if '_safe' in opts:
105 109 #no exc on failure
106 110 del opts['_safe']
107 111 safe_call = True
108 112
109 113 _str_cmd = False
110 114 if isinstance(cmd, basestring):
111 115 cmd = [cmd]
112 116 _str_cmd = True
113 117
114 118 gitenv = os.environ
115 119 # need to clean fix GIT_DIR !
116 120 if 'GIT_DIR' in gitenv:
117 121 del gitenv['GIT_DIR']
118 122 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
119 123
120 124 _git_path = settings.GIT_EXECUTABLE_PATH
121 125 cmd = [_git_path] + _copts + cmd
122 126 if _str_cmd:
123 127 cmd = ' '.join(cmd)
124 128 try:
125 129 _opts = dict(
126 130 env=gitenv,
127 131 shell=False,
128 132 )
129 133 _opts.update(opts)
130 134 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
131 135 except (EnvironmentError, OSError), err:
132 136 tb_err = ("Couldn't run git command (%s).\n"
133 137 "Original error was:%s\n" % (cmd, err))
134 138 log.error(tb_err)
135 139 if safe_call:
136 140 return '', err
137 141 else:
138 142 raise RepositoryError(tb_err)
139 143
140 144 return ''.join(p.output), ''.join(p.error)
141 145
142 146 def run_git_command(self, cmd):
143 147 opts = {}
144 148 if os.path.isdir(self.path):
145 149 opts['cwd'] = self.path
146 150 return self._run_git_command(cmd, **opts)
147 151
148 152 @classmethod
149 153 def _check_url(cls, url):
150 154 """
151 155 Functon will check given url and try to verify if it's a valid
152 156 link. Sometimes it may happened that mercurial will issue basic
153 157 auth request that can cause whole API to hang when used from python
154 158 or other external calls.
155 159
156 160 On failures it'll raise urllib2.HTTPError
157 161 """
158 162
159 163 # check first if it's not an local url
160 164 if os.path.isdir(url) or url.startswith('file:'):
161 165 return True
162 166
163 167 if('+' in url[:url.find('://')]):
164 168 url = url[url.find('+') + 1:]
165 169
166 170 handlers = []
167 171 test_uri, authinfo = hg_url(url).authinfo()
168 172 if not test_uri.endswith('info/refs'):
169 173 test_uri = test_uri.rstrip('/') + '/info/refs'
170 174 if authinfo:
171 175 #create a password manager
172 176 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
173 177 passmgr.add_password(*authinfo)
174 178
175 179 handlers.extend((httpbasicauthhandler(passmgr),
176 180 httpdigestauthhandler(passmgr)))
177 181
178 182 o = urllib2.build_opener(*handlers)
179 183 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
180 184
181 185 q = {"service": 'git-upload-pack'}
182 186 qs = '?%s' % urllib.urlencode(q)
183 187 cu = "%s%s" % (test_uri, qs)
184 188 req = urllib2.Request(cu, None, {})
185 189
186 190 try:
187 191 resp = o.open(req)
188 192 return resp.code == 200
189 193 except Exception, e:
190 194 # means it cannot be cloned
191 195 raise urllib2.URLError("[%s] %s" % (url, e))
192 196
193 197 def _get_repo(self, create, src_url=None, update_after_clone=False,
194 198 bare=False):
195 199 if create and os.path.exists(self.path):
196 200 raise RepositoryError("Location already exist")
197 201 if src_url and not create:
198 202 raise RepositoryError("Create should be set to True if src_url is "
199 203 "given (clone operation creates repository)")
200 204 try:
201 205 if create and src_url:
202 206 GitRepository._check_url(src_url)
203 207 self.clone(src_url, update_after_clone, bare)
204 208 return Repo(self.path)
205 209 elif create:
206 210 os.mkdir(self.path)
207 211 if bare:
208 212 return Repo.init_bare(self.path)
209 213 else:
210 214 return Repo.init(self.path)
211 215 else:
212 216 return self._repo
213 217 except (NotGitRepository, OSError), err:
214 218 raise RepositoryError(err)
215 219
216 220 def _get_all_revisions(self):
217 221 # we must check if this repo is not empty, since later command
218 222 # fails if it is. And it's cheaper to ask than throw the subprocess
219 223 # errors
220 224 try:
221 225 self._repo.head()
222 226 except KeyError:
223 227 return []
224 228
225 229 rev_filter = _git_path = settings.GIT_REV_FILTER
226 230 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
227 231 try:
228 232 so, se = self.run_git_command(cmd)
229 233 except RepositoryError:
230 234 # Can be raised for empty repositories
231 235 return []
232 236 return so.splitlines()
233 237
234 238 def _get_all_revisions2(self):
235 239 #alternate implementation using dulwich
236 240 includes = [x[1][0] for x in self._parsed_refs.iteritems()
237 241 if x[1][1] != 'T']
238 242 return [c.commit.id for c in self._repo.get_walker(include=includes)]
239 243
240 244 def _get_revision(self, revision):
241 245 """
242 246 For git backend we always return integer here. This way we ensure
243 247 that changset's revision attribute would become integer.
244 248 """
245 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
246 is_bstr = lambda o: isinstance(o, (str, unicode))
249
247 250 is_null = lambda o: len(o) == revision.count('0')
248 251
249 if len(self.revisions) == 0:
252 try:
253 self.revisions[0]
254 except (KeyError, IndexError):
250 255 raise EmptyRepositoryError("There are no changesets yet")
251 256
252 257 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
253 revision = self.revisions[-1]
258 return self.revisions[-1]
254 259
255 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
260 is_bstr = isinstance(revision, (str, unicode))
261 if ((is_bstr and revision.isdigit() and len(revision) < 12)
256 262 or isinstance(revision, int) or is_null(revision)):
257 263 try:
258 264 revision = self.revisions[int(revision)]
259 265 except Exception:
260 266 raise ChangesetDoesNotExistError("Revision %s does not exist "
261 267 "for this repository" % (revision))
262 268
263 elif is_bstr(revision):
269 elif is_bstr:
264 270 # get by branch/tag name
265 271 _ref_revision = self._parsed_refs.get(revision)
266 _tags_shas = self.tags.values()
267 272 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
268 273 return _ref_revision[0]
269 274
275 _tags_shas = self.tags.values()
270 276 # maybe it's a tag ? we don't have them in self.revisions
271 elif revision in _tags_shas:
277 if revision in _tags_shas:
272 278 return _tags_shas[_tags_shas.index(revision)]
273 279
274 elif not pattern.match(revision) or revision not in self.revisions:
280 elif not SHA_PATTERN.match(revision) or revision not in self.revisions:
275 281 raise ChangesetDoesNotExistError("Revision %s does not exist "
276 282 "for this repository" % (revision))
277 283
278 284 # Ensure we return full id
279 if not pattern.match(str(revision)):
285 if not SHA_PATTERN.match(str(revision)):
280 286 raise ChangesetDoesNotExistError("Given revision %s not recognized"
281 287 % revision)
282 288 return revision
283 289
284 290 def _get_archives(self, archive_name='tip'):
285 291
286 292 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
287 293 yield {"type": i[0], "extension": i[1], "node": archive_name}
288 294
289 295 def _get_url(self, url):
290 296 """
291 297 Returns normalized url. If schema is not given, would fall to
292 298 filesystem (``file:///``) schema.
293 299 """
294 300 url = str(url)
295 301 if url != 'default' and not '://' in url:
296 302 url = ':///'.join(('file', url))
297 303 return url
298 304
299 305 def get_hook_location(self):
300 306 """
301 307 returns absolute path to location where hooks are stored
302 308 """
303 309 loc = os.path.join(self.path, 'hooks')
304 310 if not self.bare:
305 311 loc = os.path.join(self.path, '.git', 'hooks')
306 312 return loc
307 313
308 314 @LazyProperty
309 315 def name(self):
310 316 return os.path.basename(self.path)
311 317
312 318 @LazyProperty
313 319 def last_change(self):
314 320 """
315 321 Returns last change made on this repository as datetime object
316 322 """
317 323 return date_fromtimestamp(self._get_mtime(), makedate()[1])
318 324
319 325 def _get_mtime(self):
320 326 try:
321 327 return time.mktime(self.get_changeset().date.timetuple())
322 328 except RepositoryError:
323 329 idx_loc = '' if self.bare else '.git'
324 330 # fallback to filesystem
325 331 in_path = os.path.join(self.path, idx_loc, "index")
326 332 he_path = os.path.join(self.path, idx_loc, "HEAD")
327 333 if os.path.exists(in_path):
328 334 return os.stat(in_path).st_mtime
329 335 else:
330 336 return os.stat(he_path).st_mtime
331 337
332 338 @LazyProperty
333 339 def description(self):
334 340 idx_loc = '' if self.bare else '.git'
335 341 undefined_description = u'unknown'
336 342 description_path = os.path.join(self.path, idx_loc, 'description')
337 343 if os.path.isfile(description_path):
338 344 return safe_unicode(open(description_path).read())
339 345 else:
340 346 return undefined_description
341 347
342 348 @LazyProperty
343 349 def contact(self):
344 350 undefined_contact = u'Unknown'
345 351 return undefined_contact
346 352
347 353 @property
348 354 def branches(self):
349 355 if not self.revisions:
350 356 return {}
351 357 sortkey = lambda ctx: ctx[0]
352 358 _branches = [(x[0], x[1][0])
353 359 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
354 360 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
355 361
356 362 @LazyProperty
357 363 def tags(self):
358 364 return self._get_tags()
359 365
360 366 def _get_tags(self):
361 367 if not self.revisions:
362 368 return {}
363 369
364 370 sortkey = lambda ctx: ctx[0]
365 371 _tags = [(x[0], x[1][0])
366 372 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
367 373 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
368 374
369 375 def tag(self, name, user, revision=None, message=None, date=None,
370 376 **kwargs):
371 377 """
372 378 Creates and returns a tag for the given ``revision``.
373 379
374 380 :param name: name for new tag
375 381 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
376 382 :param revision: changeset id for which new tag would be created
377 383 :param message: message of the tag's commit
378 384 :param date: date of tag's commit
379 385
380 386 :raises TagAlreadyExistError: if tag with same name already exists
381 387 """
382 388 if name in self.tags:
383 389 raise TagAlreadyExistError("Tag %s already exists" % name)
384 390 changeset = self.get_changeset(revision)
385 391 message = message or "Added tag %s for commit %s" % (name,
386 392 changeset.raw_id)
387 393 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
388 394
389 395 self._parsed_refs = self._get_parsed_refs()
390 396 self.tags = self._get_tags()
391 397 return changeset
392 398
393 399 def remove_tag(self, name, user, message=None, date=None):
394 400 """
395 401 Removes tag with the given ``name``.
396 402
397 403 :param name: name of the tag to be removed
398 404 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
399 405 :param message: message of the tag's removal commit
400 406 :param date: date of tag's removal commit
401 407
402 408 :raises TagDoesNotExistError: if tag with given name does not exists
403 409 """
404 410 if name not in self.tags:
405 411 raise TagDoesNotExistError("Tag %s does not exist" % name)
406 412 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
407 413 try:
408 414 os.remove(tagpath)
409 415 self._parsed_refs = self._get_parsed_refs()
410 416 self.tags = self._get_tags()
411 417 except OSError, e:
412 418 raise RepositoryError(e.strerror)
413 419
414 420 @LazyProperty
415 421 def _parsed_refs(self):
416 422 return self._get_parsed_refs()
417 423
418 424 def _get_parsed_refs(self):
419 425 # cache the property
420 426 _repo = self._repo
421 427 refs = _repo.get_refs()
422 428 keys = [('refs/heads/', 'H'),
423 429 ('refs/remotes/origin/', 'RH'),
424 430 ('refs/tags/', 'T')]
425 431 _refs = {}
426 432 for ref, sha in refs.iteritems():
427 433 for k, type_ in keys:
428 434 if ref.startswith(k):
429 435 _key = ref[len(k):]
430 436 if type_ == 'T':
431 437 obj = _repo.get_object(sha)
432 438 if isinstance(obj, Tag):
433 439 sha = _repo.get_object(sha).object[1]
434 440 _refs[_key] = [sha, type_]
435 441 break
436 442 return _refs
437 443
438 444 def _heads(self, reverse=False):
439 445 refs = self._repo.get_refs()
440 446 heads = {}
441 447
442 448 for key, val in refs.items():
443 449 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
444 450 if key.startswith(ref_key):
445 451 n = key[len(ref_key):]
446 452 if n not in ['HEAD']:
447 453 heads[n] = val
448 454
449 455 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
450 456
451 457 def get_changeset(self, revision=None):
452 458 """
453 459 Returns ``GitChangeset`` object representing commit from git repository
454 460 at the given revision or head (most recent commit) if None given.
455 461 """
456 462 if isinstance(revision, GitChangeset):
457 463 return revision
458 464 revision = self._get_revision(revision)
459 465 changeset = GitChangeset(repository=self, revision=revision)
460 466 return changeset
461 467
462 468 def get_changesets(self, start=None, end=None, start_date=None,
463 469 end_date=None, branch_name=None, reverse=False):
464 470 """
465 471 Returns iterator of ``GitChangeset`` objects from start to end (both
466 472 are inclusive), in ascending date order (unless ``reverse`` is set).
467 473
468 474 :param start: changeset ID, as str; first returned changeset
469 475 :param end: changeset ID, as str; last returned changeset
470 476 :param start_date: if specified, changesets with commit date less than
471 477 ``start_date`` would be filtered out from returned set
472 478 :param end_date: if specified, changesets with commit date greater than
473 479 ``end_date`` would be filtered out from returned set
474 480 :param branch_name: if specified, changesets not reachable from given
475 481 branch would be filtered out from returned set
476 482 :param reverse: if ``True``, returned generator would be reversed
477 483 (meaning that returned changesets would have descending date order)
478 484
479 485 :raise BranchDoesNotExistError: If given ``branch_name`` does not
480 486 exist.
481 487 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
482 488 ``end`` could not be found.
483 489
484 490 """
485 491 if branch_name and branch_name not in self.branches:
486 492 raise BranchDoesNotExistError("Branch '%s' not found" \
487 493 % branch_name)
488 494 # %H at format means (full) commit hash, initial hashes are retrieved
489 495 # in ascending date order
490 496 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
491 497 cmd_params = {}
492 498 if start_date:
493 499 cmd_template += ' --since "$since"'
494 500 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
495 501 if end_date:
496 502 cmd_template += ' --until "$until"'
497 503 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
498 504 if branch_name:
499 505 cmd_template += ' $branch_name'
500 506 cmd_params['branch_name'] = branch_name
501 507 else:
502 508 rev_filter = _git_path = settings.GIT_REV_FILTER
503 509 cmd_template += ' %s' % (rev_filter)
504 510
505 511 cmd = string.Template(cmd_template).safe_substitute(**cmd_params)
506 512 revs = self.run_git_command(cmd)[0].splitlines()
507 513 start_pos = 0
508 514 end_pos = len(revs)
509 515 if start:
510 516 _start = self._get_revision(start)
511 517 try:
512 518 start_pos = revs.index(_start)
513 519 except ValueError:
514 520 pass
515 521
516 522 if end is not None:
517 523 _end = self._get_revision(end)
518 524 try:
519 525 end_pos = revs.index(_end)
520 526 except ValueError:
521 527 pass
522 528
523 529 if None not in [start, end] and start_pos > end_pos:
524 530 raise RepositoryError('start cannot be after end')
525 531
526 532 if end_pos is not None:
527 533 end_pos += 1
528 534
529 535 revs = revs[start_pos:end_pos]
530 536 if reverse:
531 537 revs = reversed(revs)
532 538 return CollectionGenerator(self, revs)
533 539
534 540 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
535 541 context=3):
536 542 """
537 543 Returns (git like) *diff*, as plain text. Shows changes introduced by
538 544 ``rev2`` since ``rev1``.
539 545
540 546 :param rev1: Entry point from which diff is shown. Can be
541 547 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
542 548 the changes since empty state of the repository until ``rev2``
543 549 :param rev2: Until which revision changes should be shown.
544 550 :param ignore_whitespace: If set to ``True``, would not show whitespace
545 551 changes. Defaults to ``False``.
546 552 :param context: How many lines before/after changed lines should be
547 553 shown. Defaults to ``3``.
548 554 """
549 555 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
550 556 if ignore_whitespace:
551 557 flags.append('-w')
552 558
553 559 if hasattr(rev1, 'raw_id'):
554 560 rev1 = getattr(rev1, 'raw_id')
555 561
556 562 if hasattr(rev2, 'raw_id'):
557 563 rev2 = getattr(rev2, 'raw_id')
558 564
559 565 if rev1 == self.EMPTY_CHANGESET:
560 566 rev2 = self.get_changeset(rev2).raw_id
561 567 cmd = ' '.join(['show'] + flags + [rev2])
562 568 else:
563 569 rev1 = self.get_changeset(rev1).raw_id
564 570 rev2 = self.get_changeset(rev2).raw_id
565 571 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
566 572
567 573 if path:
568 574 cmd += ' -- "%s"' % path
569 575
570 576 stdout, stderr = self.run_git_command(cmd)
571 577 # If we used 'show' command, strip first few lines (until actual diff
572 578 # starts)
573 579 if rev1 == self.EMPTY_CHANGESET:
574 580 lines = stdout.splitlines()
575 581 x = 0
576 582 for line in lines:
577 583 if line.startswith('diff'):
578 584 break
579 585 x += 1
580 586 # Append new line just like 'diff' command do
581 587 stdout = '\n'.join(lines[x:]) + '\n'
582 588 return stdout
583 589
584 590 @LazyProperty
585 591 def in_memory_changeset(self):
586 592 """
587 593 Returns ``GitInMemoryChangeset`` object for this repository.
588 594 """
589 595 return GitInMemoryChangeset(self)
590 596
591 597 def clone(self, url, update_after_clone=True, bare=False):
592 598 """
593 599 Tries to clone changes from external location.
594 600
595 601 :param update_after_clone: If set to ``False``, git won't checkout
596 602 working directory
597 603 :param bare: If set to ``True``, repository would be cloned into
598 604 *bare* git repository (no working directory at all).
599 605 """
600 606 url = self._get_url(url)
601 607 cmd = ['clone']
602 608 if bare:
603 609 cmd.append('--bare')
604 610 elif not update_after_clone:
605 611 cmd.append('--no-checkout')
606 612 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
607 613 cmd = ' '.join(cmd)
608 614 # If error occurs run_git_command raises RepositoryError already
609 615 self.run_git_command(cmd)
610 616
611 617 def pull(self, url):
612 618 """
613 619 Tries to pull changes from external location.
614 620 """
615 621 url = self._get_url(url)
616 622 cmd = ['pull']
617 623 cmd.append("--ff-only")
618 624 cmd.append(url)
619 625 cmd = ' '.join(cmd)
620 626 # If error occurs run_git_command raises RepositoryError already
621 627 self.run_git_command(cmd)
622 628
623 629 def fetch(self, url):
624 630 """
625 631 Tries to pull changes from external location.
626 632 """
627 633 url = self._get_url(url)
628 634 so, se = self.run_git_command('ls-remote -h %s' % url)
629 635 refs = []
630 636 for line in (x for x in so.splitlines()):
631 637 sha, ref = line.split('\t')
632 638 refs.append(ref)
633 639 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
634 640 cmd = '''fetch %s -- %s''' % (url, refs)
635 641 self.run_git_command(cmd)
636 642
637 643 @LazyProperty
638 644 def workdir(self):
639 645 """
640 646 Returns ``Workdir`` instance for this repository.
641 647 """
642 648 return GitWorkdir(self)
643 649
644 650 def get_config_value(self, section, name, config_file=None):
645 651 """
646 652 Returns configuration value for a given [``section``] and ``name``.
647 653
648 654 :param section: Section we want to retrieve value from
649 655 :param name: Name of configuration we want to retrieve
650 656 :param config_file: A path to file which should be used to retrieve
651 657 configuration from (might also be a list of file paths)
652 658 """
653 659 if config_file is None:
654 660 config_file = []
655 661 elif isinstance(config_file, basestring):
656 662 config_file = [config_file]
657 663
658 664 def gen_configs():
659 665 for path in config_file + self._config_files:
660 666 try:
661 667 yield ConfigFile.from_path(path)
662 668 except (IOError, OSError, ValueError):
663 669 continue
664 670
665 671 for config in gen_configs():
666 672 try:
667 673 return config.get(section, name)
668 674 except KeyError:
669 675 continue
670 676 return None
671 677
672 678 def get_user_name(self, config_file=None):
673 679 """
674 680 Returns user's name from global configuration file.
675 681
676 682 :param config_file: A path to file which should be used to retrieve
677 683 configuration from (might also be a list of file paths)
678 684 """
679 685 return self.get_config_value('user', 'name', config_file)
680 686
681 687 def get_user_email(self, config_file=None):
682 688 """
683 689 Returns user's email from global configuration file.
684 690
685 691 :param config_file: A path to file which should be used to retrieve
686 692 configuration from (might also be a list of file paths)
687 693 """
688 694 return self.get_config_value('user', 'email', config_file)
@@ -1,569 +1,571 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 vcs.backends.hg.repository
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Mercurial repository 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 time
14 14 import urllib
15 15 import urllib2
16 16 import logging
17 17 import datetime
18 18
19 19
20 20 from rhodecode.lib.vcs.backends.base import BaseRepository, CollectionGenerator
21 21 from rhodecode.lib.vcs.conf import settings
22 22
23 23 from rhodecode.lib.vcs.exceptions import (
24 24 BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError,
25 25 RepositoryError, VCSError, TagAlreadyExistError, TagDoesNotExistError
26 26 )
27 27 from rhodecode.lib.vcs.utils import (
28 28 author_email, author_name, date_fromtimestamp, makedate, safe_unicode
29 29 )
30 30 from rhodecode.lib.vcs.utils.lazy import LazyProperty
31 31 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
32 32 from rhodecode.lib.vcs.utils.paths import abspath
33 33 from rhodecode.lib.vcs.utils.hgcompat import (
34 34 ui, nullid, match, patch, diffopts, clone, get_contact, pull,
35 35 localrepository, RepoLookupError, Abort, RepoError, hex, scmutil, hg_url,
36 36 httpbasicauthhandler, httpdigestauthhandler
37 37 )
38 38
39 39 from .changeset import MercurialChangeset
40 40 from .inmemory import MercurialInMemoryChangeset
41 41 from .workdir import MercurialWorkdir
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class MercurialRepository(BaseRepository):
47 47 """
48 48 Mercurial repository backend
49 49 """
50 50 DEFAULT_BRANCH_NAME = 'default'
51 51 scm = 'hg'
52 52
53 53 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
54 54 update_after_clone=False):
55 55 """
56 56 Raises RepositoryError if repository could not be find at the given
57 57 ``repo_path``.
58 58
59 59 :param repo_path: local path of the repository
60 60 :param create=False: if set to True, would try to create repository if
61 61 it does not exist rather than raising exception
62 62 :param baseui=None: user data
63 63 :param src_url=None: would try to clone repository from given location
64 64 :param update_after_clone=False: sets update of working copy after
65 65 making a clone
66 66 """
67 67
68 68 if not isinstance(repo_path, str):
69 69 raise VCSError('Mercurial backend requires repository path to '
70 70 'be instance of <str> got %s instead' %
71 71 type(repo_path))
72 72
73 73 self.path = abspath(repo_path)
74 74 self.baseui = baseui or ui.ui()
75 75 # We've set path and ui, now we can set _repo itself
76 76 self._repo = self._get_repo(create, src_url, update_after_clone)
77 77
78 78 @property
79 79 def _empty(self):
80 80 """
81 81 Checks if repository is empty without any changesets
82 82 """
83 83 # TODO: Following raises errors when using InMemoryChangeset...
84 84 # return len(self._repo.changelog) == 0
85 85 return len(self.revisions) == 0
86 86
87 87 @LazyProperty
88 88 def revisions(self):
89 89 """
90 90 Returns list of revisions' ids, in ascending order. Being lazy
91 91 attribute allows external tools to inject shas from cache.
92 92 """
93 93 return self._get_all_revisions()
94 94
95 95 @LazyProperty
96 96 def name(self):
97 97 return os.path.basename(self.path)
98 98
99 99 @LazyProperty
100 100 def branches(self):
101 101 return self._get_branches()
102 102
103 103 @LazyProperty
104 104 def allbranches(self):
105 105 """
106 106 List all branches, including closed branches.
107 107 """
108 108 return self._get_branches(closed=True)
109 109
110 110 def _get_branches(self, closed=False):
111 111 """
112 112 Get's branches for this repository
113 113 Returns only not closed branches by default
114 114
115 115 :param closed: return also closed branches for mercurial
116 116 """
117 117
118 118 if self._empty:
119 119 return {}
120 120
121 121 def _branchtags(localrepo):
122 122 """
123 123 Patched version of mercurial branchtags to not return the closed
124 124 branches
125 125
126 126 :param localrepo: locarepository instance
127 127 """
128 128
129 129 bt = {}
130 130 bt_closed = {}
131 131 for bn, heads in localrepo.branchmap().iteritems():
132 132 tip = heads[-1]
133 133 if 'close' in localrepo.changelog.read(tip)[5]:
134 134 bt_closed[bn] = tip
135 135 else:
136 136 bt[bn] = tip
137 137
138 138 if closed:
139 139 bt.update(bt_closed)
140 140 return bt
141 141
142 142 sortkey = lambda ctx: ctx[0] # sort by name
143 143 _branches = [(safe_unicode(n), hex(h),) for n, h in
144 144 _branchtags(self._repo).items()]
145 145
146 146 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
147 147
148 148 @LazyProperty
149 149 def tags(self):
150 150 """
151 151 Get's tags for this repository
152 152 """
153 153 return self._get_tags()
154 154
155 155 def _get_tags(self):
156 156 if self._empty:
157 157 return {}
158 158
159 159 sortkey = lambda ctx: ctx[0] # sort by name
160 160 _tags = [(safe_unicode(n), hex(h),) for n, h in
161 161 self._repo.tags().items()]
162 162
163 163 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
164 164
165 165 def tag(self, name, user, revision=None, message=None, date=None,
166 166 **kwargs):
167 167 """
168 168 Creates and returns a tag for the given ``revision``.
169 169
170 170 :param name: name for new tag
171 171 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
172 172 :param revision: changeset id for which new tag would be created
173 173 :param message: message of the tag's commit
174 174 :param date: date of tag's commit
175 175
176 176 :raises TagAlreadyExistError: if tag with same name already exists
177 177 """
178 178 if name in self.tags:
179 179 raise TagAlreadyExistError("Tag %s already exists" % name)
180 180 changeset = self.get_changeset(revision)
181 181 local = kwargs.setdefault('local', False)
182 182
183 183 if message is None:
184 184 message = "Added tag %s for changeset %s" % (name,
185 185 changeset.short_id)
186 186
187 187 if date is None:
188 188 date = datetime.datetime.now().ctime()
189 189
190 190 try:
191 191 self._repo.tag(name, changeset._ctx.node(), message, local, user,
192 192 date)
193 193 except Abort, e:
194 194 raise RepositoryError(e.message)
195 195
196 196 # Reinitialize tags
197 197 self.tags = self._get_tags()
198 198 tag_id = self.tags[name]
199 199
200 200 return self.get_changeset(revision=tag_id)
201 201
202 202 def remove_tag(self, name, user, message=None, date=None):
203 203 """
204 204 Removes tag with the given ``name``.
205 205
206 206 :param name: name of the tag to be removed
207 207 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
208 208 :param message: message of the tag's removal commit
209 209 :param date: date of tag's removal commit
210 210
211 211 :raises TagDoesNotExistError: if tag with given name does not exists
212 212 """
213 213 if name not in self.tags:
214 214 raise TagDoesNotExistError("Tag %s does not exist" % name)
215 215 if message is None:
216 216 message = "Removed tag %s" % name
217 217 if date is None:
218 218 date = datetime.datetime.now().ctime()
219 219 local = False
220 220
221 221 try:
222 222 self._repo.tag(name, nullid, message, local, user, date)
223 223 self.tags = self._get_tags()
224 224 except Abort, e:
225 225 raise RepositoryError(e.message)
226 226
227 227 @LazyProperty
228 228 def bookmarks(self):
229 229 """
230 230 Get's bookmarks for this repository
231 231 """
232 232 return self._get_bookmarks()
233 233
234 234 def _get_bookmarks(self):
235 235 if self._empty:
236 236 return {}
237 237
238 238 sortkey = lambda ctx: ctx[0] # sort by name
239 239 _bookmarks = [(safe_unicode(n), hex(h),) for n, h in
240 240 self._repo._bookmarks.items()]
241 241 return OrderedDict(sorted(_bookmarks, key=sortkey, reverse=True))
242 242
243 243 def _get_all_revisions(self):
244 244
245 245 return map(lambda x: hex(x[7]), self._repo.changelog.index)[:-1]
246 246
247 247 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
248 248 context=3):
249 249 """
250 250 Returns (git like) *diff*, as plain text. Shows changes introduced by
251 251 ``rev2`` since ``rev1``.
252 252
253 253 :param rev1: Entry point from which diff is shown. Can be
254 254 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
255 255 the changes since empty state of the repository until ``rev2``
256 256 :param rev2: Until which revision changes should be shown.
257 257 :param ignore_whitespace: If set to ``True``, would not show whitespace
258 258 changes. Defaults to ``False``.
259 259 :param context: How many lines before/after changed lines should be
260 260 shown. Defaults to ``3``.
261 261 """
262 262 if hasattr(rev1, 'raw_id'):
263 263 rev1 = getattr(rev1, 'raw_id')
264 264
265 265 if hasattr(rev2, 'raw_id'):
266 266 rev2 = getattr(rev2, 'raw_id')
267 267
268 268 # Check if given revisions are present at repository (may raise
269 269 # ChangesetDoesNotExistError)
270 270 if rev1 != self.EMPTY_CHANGESET:
271 271 self.get_changeset(rev1)
272 272 self.get_changeset(rev2)
273 273 if path:
274 274 file_filter = match(self.path, '', [path])
275 275 else:
276 276 file_filter = None
277 277
278 278 return ''.join(patch.diff(self._repo, rev1, rev2, match=file_filter,
279 279 opts=diffopts(git=True,
280 280 ignorews=ignore_whitespace,
281 281 context=context)))
282 282
283 283 @classmethod
284 284 def _check_url(cls, url):
285 285 """
286 286 Function will check given url and try to verify if it's a valid
287 287 link. Sometimes it may happened that mercurial will issue basic
288 288 auth request that can cause whole API to hang when used from python
289 289 or other external calls.
290 290
291 291 On failures it'll raise urllib2.HTTPError, return code 200 if url
292 292 is valid or True if it's a local path
293 293 """
294 294
295 295 # check first if it's not an local url
296 296 if os.path.isdir(url) or url.startswith('file:'):
297 297 return True
298 298
299 299 if('+' in url[:url.find('://')]):
300 300 url = url[url.find('+') + 1:]
301 301
302 302 handlers = []
303 303 test_uri, authinfo = hg_url(url).authinfo()
304 304
305 305 if authinfo:
306 306 #create a password manager
307 307 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
308 308 passmgr.add_password(*authinfo)
309 309
310 310 handlers.extend((httpbasicauthhandler(passmgr),
311 311 httpdigestauthhandler(passmgr)))
312 312
313 313 o = urllib2.build_opener(*handlers)
314 314 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
315 315 ('Accept', 'application/mercurial-0.1')]
316 316
317 317 q = {"cmd": 'between'}
318 318 q.update({'pairs': "%s-%s" % ('0' * 40, '0' * 40)})
319 319 qs = '?%s' % urllib.urlencode(q)
320 320 cu = "%s%s" % (test_uri, qs)
321 321 req = urllib2.Request(cu, None, {})
322 322
323 323 try:
324 324 resp = o.open(req)
325 325 return resp.code == 200
326 326 except Exception, e:
327 327 # means it cannot be cloned
328 328 raise urllib2.URLError("[%s] %s" % (url, e))
329 329
330 330 def _get_repo(self, create, src_url=None, update_after_clone=False):
331 331 """
332 332 Function will check for mercurial repository in given path and return
333 333 a localrepo object. If there is no repository in that path it will
334 334 raise an exception unless ``create`` parameter is set to True - in
335 335 that case repository would be created and returned.
336 336 If ``src_url`` is given, would try to clone repository from the
337 337 location at given clone_point. Additionally it'll make update to
338 338 working copy accordingly to ``update_after_clone`` flag
339 339 """
340 340
341 341 try:
342 342 if src_url:
343 343 url = str(self._get_url(src_url))
344 344 opts = {}
345 345 if not update_after_clone:
346 346 opts.update({'noupdate': True})
347 347 try:
348 348 MercurialRepository._check_url(url)
349 349 clone(self.baseui, url, self.path, **opts)
350 350 # except urllib2.URLError:
351 351 # raise Abort("Got HTTP 404 error")
352 352 except Exception:
353 353 raise
354 354
355 355 # Don't try to create if we've already cloned repo
356 356 create = False
357 357 return localrepository(self.baseui, self.path, create=create)
358 358 except (Abort, RepoError), err:
359 359 if create:
360 360 msg = "Cannot create repository at %s. Original error was %s"\
361 361 % (self.path, err)
362 362 else:
363 363 msg = "Not valid repository at %s. Original error was %s"\
364 364 % (self.path, err)
365 365 raise RepositoryError(msg)
366 366
367 367 @LazyProperty
368 368 def in_memory_changeset(self):
369 369 return MercurialInMemoryChangeset(self)
370 370
371 371 @LazyProperty
372 372 def description(self):
373 373 undefined_description = u'unknown'
374 374 return safe_unicode(self._repo.ui.config('web', 'description',
375 375 undefined_description, untrusted=True))
376 376
377 377 @LazyProperty
378 378 def contact(self):
379 379 undefined_contact = u'Unknown'
380 380 return safe_unicode(get_contact(self._repo.ui.config)
381 381 or undefined_contact)
382 382
383 383 @LazyProperty
384 384 def last_change(self):
385 385 """
386 386 Returns last change made on this repository as datetime object
387 387 """
388 388 return date_fromtimestamp(self._get_mtime(), makedate()[1])
389 389
390 390 def _get_mtime(self):
391 391 try:
392 392 return time.mktime(self.get_changeset().date.timetuple())
393 393 except RepositoryError:
394 394 #fallback to filesystem
395 395 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
396 396 st_path = os.path.join(self.path, '.hg', "store")
397 397 if os.path.exists(cl_path):
398 398 return os.stat(cl_path).st_mtime
399 399 else:
400 400 return os.stat(st_path).st_mtime
401 401
402 402 def _get_hidden(self):
403 403 return self._repo.ui.configbool("web", "hidden", untrusted=True)
404 404
405 405 def _get_revision(self, revision):
406 406 """
407 407 Get's an ID revision given as str. This will always return a fill
408 408 40 char revision number
409 409
410 410 :param revision: str or int or None
411 411 """
412 412
413 413 if self._empty:
414 414 raise EmptyRepositoryError("There are no changesets yet")
415 415
416 416 if revision in [-1, 'tip', None]:
417 417 revision = 'tip'
418 418
419 419 try:
420 420 revision = hex(self._repo.lookup(revision))
421 421 except (IndexError, ValueError, RepoLookupError, TypeError):
422 422 raise ChangesetDoesNotExistError("Revision %s does not "
423 423 "exist for this repository"
424 424 % (revision))
425 425 return revision
426 426
427 427 def _get_archives(self, archive_name='tip'):
428 428 allowed = self.baseui.configlist("web", "allow_archive",
429 429 untrusted=True)
430 430 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
431 431 if i[0] in allowed or self._repo.ui.configbool("web",
432 432 "allow" + i[0],
433 433 untrusted=True):
434 434 yield {"type": i[0], "extension": i[1], "node": archive_name}
435 435
436 436 def _get_url(self, url):
437 437 """
438 438 Returns normalized url. If schema is not given, would fall
439 439 to filesystem
440 440 (``file:///``) schema.
441 441 """
442 442 url = str(url)
443 443 if url != 'default' and not '://' in url:
444 444 url = "file:" + urllib.pathname2url(url)
445 445 return url
446 446
447 447 def get_hook_location(self):
448 448 """
449 449 returns absolute path to location where hooks are stored
450 450 """
451 451 return os.path.join(self.path, '.hg', '.hgrc')
452 452
453 453 def get_changeset(self, revision=None):
454 454 """
455 455 Returns ``MercurialChangeset`` object representing repository's
456 456 changeset at the given ``revision``.
457 457 """
458 458 revision = self._get_revision(revision)
459 459 changeset = MercurialChangeset(repository=self, revision=revision)
460 460 return changeset
461 461
462 462 def get_changesets(self, start=None, end=None, start_date=None,
463 463 end_date=None, branch_name=None, reverse=False):
464 464 """
465 465 Returns iterator of ``MercurialChangeset`` objects from start to end
466 466 (both are inclusive)
467 467
468 468 :param start: None, str, int or mercurial lookup format
469 469 :param end: None, str, int or mercurial lookup format
470 470 :param start_date:
471 471 :param end_date:
472 472 :param branch_name:
473 473 :param reversed: return changesets in reversed order
474 474 """
475 475
476 476 start_raw_id = self._get_revision(start)
477 477 start_pos = self.revisions.index(start_raw_id) if start else None
478 478 end_raw_id = self._get_revision(end)
479 479 end_pos = self.revisions.index(end_raw_id) if end else None
480 480
481 481 if None not in [start, end] and start_pos > end_pos:
482 482 raise RepositoryError("Start revision '%s' cannot be "
483 483 "after end revision '%s'" % (start, end))
484 484
485 485 if branch_name and branch_name not in self.allbranches.keys():
486 486 raise BranchDoesNotExistError('Branch %s not found in'
487 487 ' this repository' % branch_name)
488 488 if end_pos is not None:
489 489 end_pos += 1
490 490 #filter branches
491 491 filter_ = []
492 492 if branch_name:
493 493 filter_.append('branch("%s")' % (branch_name))
494 494
495 if start_date:
495 if start_date and not end_date:
496 496 filter_.append('date(">%s")' % start_date)
497 if end_date:
497 if end_date and not start_date:
498 498 filter_.append('date("<%s")' % end_date)
499 if start_date and end_date:
500 filter_.append('date(">%s") and date("<%s")' % (start_date, end_date))
499 501 if filter_:
500 502 revisions = scmutil.revrange(self._repo, filter_)
501 503 else:
502 504 revisions = self.revisions
503 505
504 506 revs = revisions[start_pos:end_pos]
505 507 if reverse:
506 508 revs = reversed(revs)
507 509
508 510 return CollectionGenerator(self, revs)
509 511
510 512 def pull(self, url):
511 513 """
512 514 Tries to pull changes from external location.
513 515 """
514 516 url = self._get_url(url)
515 517 try:
516 518 pull(self.baseui, self._repo, url)
517 519 except Abort, err:
518 520 # Propagate error but with vcs's type
519 521 raise RepositoryError(str(err))
520 522
521 523 @LazyProperty
522 524 def workdir(self):
523 525 """
524 526 Returns ``Workdir`` instance for this repository.
525 527 """
526 528 return MercurialWorkdir(self)
527 529
528 530 def get_config_value(self, section, name=None, config_file=None):
529 531 """
530 532 Returns configuration value for a given [``section``] and ``name``.
531 533
532 534 :param section: Section we want to retrieve value from
533 535 :param name: Name of configuration we want to retrieve
534 536 :param config_file: A path to file which should be used to retrieve
535 537 configuration from (might also be a list of file paths)
536 538 """
537 539 if config_file is None:
538 540 config_file = []
539 541 elif isinstance(config_file, basestring):
540 542 config_file = [config_file]
541 543
542 544 config = self._repo.ui
543 545 for path in config_file:
544 546 config.readconfig(path)
545 547 return config.config(section, name)
546 548
547 549 def get_user_name(self, config_file=None):
548 550 """
549 551 Returns user's name from global configuration file.
550 552
551 553 :param config_file: A path to file which should be used to retrieve
552 554 configuration from (might also be a list of file paths)
553 555 """
554 556 username = self.get_config_value('ui', 'username')
555 557 if username:
556 558 return author_name(username)
557 559 return None
558 560
559 561 def get_user_email(self, config_file=None):
560 562 """
561 563 Returns user's email from global configuration file.
562 564
563 565 :param config_file: A path to file which should be used to retrieve
564 566 configuration from (might also be a list of file paths)
565 567 """
566 568 username = self.get_config_value('ui', 'username')
567 569 if username:
568 570 return author_email(username)
569 571 return None
@@ -1,352 +1,360 b''
1 1 from __future__ import with_statement
2 2
3 3 import datetime
4 4 from rhodecode.lib import vcs
5 5 from rhodecode.tests.vcs.base import BackendTestMixin
6 6 from rhodecode.tests.vcs.conf import SCM_TESTS
7 7
8 8 from rhodecode.lib.vcs.backends.base import BaseChangeset
9 9 from rhodecode.lib.vcs.nodes import (
10 10 FileNode, AddedFileNodesGenerator,
11 11 ChangedFileNodesGenerator, RemovedFileNodesGenerator
12 12 )
13 13 from rhodecode.lib.vcs.exceptions import (
14 14 BranchDoesNotExistError, ChangesetDoesNotExistError,
15 15 RepositoryError
16 16 )
17 17 from rhodecode.lib.vcs.utils.compat import unittest
18 18
19 19
20 20 class TestBaseChangeset(unittest.TestCase):
21 21
22 22 def test_as_dict(self):
23 23 changeset = BaseChangeset()
24 24 changeset.id = 'ID'
25 25 changeset.raw_id = 'RAW_ID'
26 26 changeset.short_id = 'SHORT_ID'
27 27 changeset.revision = 1009
28 28 changeset.date = datetime.datetime(2011, 1, 30, 1, 45)
29 29 changeset.message = 'Message of a commit'
30 30 changeset.author = 'Joe Doe <joe.doe@example.com>'
31 31 changeset.added = [FileNode('foo/bar/baz'), FileNode('foobar')]
32 32 changeset.changed = []
33 33 changeset.removed = []
34 34 self.assertEqual(changeset.as_dict(), {
35 35 'id': 'ID',
36 36 'raw_id': 'RAW_ID',
37 37 'short_id': 'SHORT_ID',
38 38 'revision': 1009,
39 39 'date': datetime.datetime(2011, 1, 30, 1, 45),
40 40 'message': 'Message of a commit',
41 41 'author': {
42 42 'name': 'Joe Doe',
43 43 'email': 'joe.doe@example.com',
44 44 },
45 45 'added': ['foo/bar/baz', 'foobar'],
46 46 'changed': [],
47 47 'removed': [],
48 48 })
49 49
50 50 class ChangesetsWithCommitsTestCaseixin(BackendTestMixin):
51 51 recreate_repo_per_test = True
52 52
53 53 @classmethod
54 54 def _get_commits(cls):
55 55 start_date = datetime.datetime(2010, 1, 1, 20)
56 56 for x in xrange(5):
57 57 yield {
58 58 'message': 'Commit %d' % x,
59 59 'author': 'Joe Doe <joe.doe@example.com>',
60 60 'date': start_date + datetime.timedelta(hours=12 * x),
61 61 'added': [
62 62 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
63 63 ],
64 64 }
65 65
66 66 def test_new_branch(self):
67 67 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
68 68 content='Documentation\n'))
69 69 foobar_tip = self.imc.commit(
70 70 message=u'New branch: foobar',
71 71 author=u'joe',
72 72 branch='foobar',
73 73 )
74 74 self.assertTrue('foobar' in self.repo.branches)
75 75 self.assertEqual(foobar_tip.branch, 'foobar')
76 76 # 'foobar' should be the only branch that contains the new commit
77 77 self.assertNotEqual(*self.repo.branches.values())
78 78
79 79 def test_new_head_in_default_branch(self):
80 80 tip = self.repo.get_changeset()
81 81 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
82 82 content='Documentation\n'))
83 83 foobar_tip = self.imc.commit(
84 84 message=u'New branch: foobar',
85 85 author=u'joe',
86 86 branch='foobar',
87 87 parents=[tip],
88 88 )
89 89 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
90 90 content='Documentation\nand more...\n'))
91 91 newtip = self.imc.commit(
92 92 message=u'At default branch',
93 93 author=u'joe',
94 94 branch=foobar_tip.branch,
95 95 parents=[foobar_tip],
96 96 )
97 97
98 98 newest_tip = self.imc.commit(
99 99 message=u'Merged with %s' % foobar_tip.raw_id,
100 100 author=u'joe',
101 101 branch=self.backend_class.DEFAULT_BRANCH_NAME,
102 102 parents=[newtip, foobar_tip],
103 103 )
104 104
105 105 self.assertEqual(newest_tip.branch,
106 106 self.backend_class.DEFAULT_BRANCH_NAME)
107 107
108 108 def test_get_changesets_respects_branch_name(self):
109 109 tip = self.repo.get_changeset()
110 110 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
111 111 content='Documentation\n'))
112 112 doc_changeset = self.imc.commit(
113 113 message=u'New branch: docs',
114 114 author=u'joe',
115 115 branch='docs',
116 116 )
117 117 self.imc.add(vcs.nodes.FileNode('newfile', content=''))
118 118 self.imc.commit(
119 119 message=u'Back in default branch',
120 120 author=u'joe',
121 121 parents=[tip],
122 122 )
123 123 default_branch_changesets = self.repo.get_changesets(
124 124 branch_name=self.repo.DEFAULT_BRANCH_NAME)
125 125 self.assertNotIn(doc_changeset, default_branch_changesets)
126 126
127 127 def test_get_changeset_by_branch(self):
128 128 for branch, sha in self.repo.branches.iteritems():
129 129 self.assertEqual(sha, self.repo.get_changeset(branch).raw_id)
130 130
131 131 def test_get_changeset_by_tag(self):
132 132 for tag, sha in self.repo.tags.iteritems():
133 133 self.assertEqual(sha, self.repo.get_changeset(tag).raw_id)
134 134
135 135
136 136 class ChangesetsTestCaseMixin(BackendTestMixin):
137 137 recreate_repo_per_test = False
138 138
139 139 @classmethod
140 140 def _get_commits(cls):
141 141 start_date = datetime.datetime(2010, 1, 1, 20)
142 142 for x in xrange(5):
143 143 yield {
144 144 'message': u'Commit %d' % x,
145 145 'author': u'Joe Doe <joe.doe@example.com>',
146 146 'date': start_date + datetime.timedelta(hours=12 * x),
147 147 'added': [
148 148 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
149 149 ],
150 150 }
151 151
152 152 def test_simple(self):
153 153 tip = self.repo.get_changeset()
154 154 self.assertEqual(tip.date, datetime.datetime(2010, 1, 3, 20))
155 155
156 156 def test_get_changesets_is_ordered_by_date(self):
157 157 changesets = list(self.repo.get_changesets())
158 158 ordered_by_date = sorted(changesets,
159 159 key=lambda cs: cs.date)
160 160 self.assertItemsEqual(changesets, ordered_by_date)
161 161
162 162 def test_get_changesets_respects_start(self):
163 163 second_id = self.repo.revisions[1]
164 164 changesets = list(self.repo.get_changesets(start=second_id))
165 165 self.assertEqual(len(changesets), 4)
166 166
167 167 def test_get_changesets_numerical_id_respects_start(self):
168 168 second_id = 1
169 169 changesets = list(self.repo.get_changesets(start=second_id))
170 170 self.assertEqual(len(changesets), 4)
171 171
172 172 def test_get_changesets_includes_start_changeset(self):
173 173 second_id = self.repo.revisions[1]
174 174 changesets = list(self.repo.get_changesets(start=second_id))
175 175 self.assertEqual(changesets[0].raw_id, second_id)
176 176
177 177 def test_get_changesets_respects_end(self):
178 178 second_id = self.repo.revisions[1]
179 179 changesets = list(self.repo.get_changesets(end=second_id))
180 180 self.assertEqual(changesets[-1].raw_id, second_id)
181 181 self.assertEqual(len(changesets), 2)
182 182
183 183 def test_get_changesets_numerical_id_respects_end(self):
184 184 second_id = 1
185 185 changesets = list(self.repo.get_changesets(end=second_id))
186 186 self.assertEqual(changesets.index(changesets[-1]), second_id)
187 187 self.assertEqual(len(changesets), 2)
188 188
189 189 def test_get_changesets_respects_both_start_and_end(self):
190 190 second_id = self.repo.revisions[1]
191 191 third_id = self.repo.revisions[2]
192 192 changesets = list(self.repo.get_changesets(start=second_id,
193 193 end=third_id))
194 194 self.assertEqual(len(changesets), 2)
195 195
196 196 def test_get_changesets_numerical_id_respects_both_start_and_end(self):
197 197 changesets = list(self.repo.get_changesets(start=2, end=3))
198 198 self.assertEqual(len(changesets), 2)
199 199
200 200 def test_get_changesets_includes_end_changeset(self):
201 201 second_id = self.repo.revisions[1]
202 202 changesets = list(self.repo.get_changesets(end=second_id))
203 203 self.assertEqual(changesets[-1].raw_id, second_id)
204 204
205 205 def test_get_changesets_respects_start_date(self):
206 206 start_date = datetime.datetime(2010, 2, 1)
207 207 for cs in self.repo.get_changesets(start_date=start_date):
208 208 self.assertGreaterEqual(cs.date, start_date)
209 209
210 210 def test_get_changesets_respects_end_date(self):
211 start_date = datetime.datetime(2010, 1, 1)
212 end_date = datetime.datetime(2010, 2, 1)
213 for cs in self.repo.get_changesets(start_date=start_date,
214 end_date=end_date):
215 self.assertGreaterEqual(cs.date, start_date)
216 self.assertLessEqual(cs.date, end_date)
217
218 def test_get_changesets_respects_start_date_and_end_date(self):
211 219 end_date = datetime.datetime(2010, 2, 1)
212 220 for cs in self.repo.get_changesets(end_date=end_date):
213 221 self.assertLessEqual(cs.date, end_date)
214 222
215 223 def test_get_changesets_respects_reverse(self):
216 224 changesets_id_list = [cs.raw_id for cs in
217 225 self.repo.get_changesets(reverse=True)]
218 226 self.assertItemsEqual(changesets_id_list, reversed(self.repo.revisions))
219 227
220 228 def test_get_filenodes_generator(self):
221 229 tip = self.repo.get_changeset()
222 230 filepaths = [node.path for node in tip.get_filenodes_generator()]
223 231 self.assertItemsEqual(filepaths, ['file_%d.txt' % x for x in xrange(5)])
224 232
225 233 def test_size(self):
226 234 tip = self.repo.get_changeset()
227 235 size = 5 * len('Foobar N') # Size of 5 files
228 236 self.assertEqual(tip.size, size)
229 237
230 238 def test_author(self):
231 239 tip = self.repo.get_changeset()
232 240 self.assertEqual(tip.author, u'Joe Doe <joe.doe@example.com>')
233 241
234 242 def test_author_name(self):
235 243 tip = self.repo.get_changeset()
236 244 self.assertEqual(tip.author_name, u'Joe Doe')
237 245
238 246 def test_author_email(self):
239 247 tip = self.repo.get_changeset()
240 248 self.assertEqual(tip.author_email, u'joe.doe@example.com')
241 249
242 250 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_start(self):
243 251 with self.assertRaises(ChangesetDoesNotExistError):
244 252 list(self.repo.get_changesets(start='foobar'))
245 253
246 254 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_end(self):
247 255 with self.assertRaises(ChangesetDoesNotExistError):
248 256 list(self.repo.get_changesets(end='foobar'))
249 257
250 258 def test_get_changesets_raise_branchdoesnotexist_for_wrong_branch_name(self):
251 259 with self.assertRaises(BranchDoesNotExistError):
252 260 list(self.repo.get_changesets(branch_name='foobar'))
253 261
254 262 def test_get_changesets_raise_repositoryerror_for_wrong_start_end(self):
255 263 start = self.repo.revisions[-1]
256 264 end = self.repo.revisions[0]
257 265 with self.assertRaises(RepositoryError):
258 266 list(self.repo.get_changesets(start=start, end=end))
259 267
260 268 def test_get_changesets_numerical_id_reversed(self):
261 269 with self.assertRaises(RepositoryError):
262 270 [x for x in self.repo.get_changesets(start=3, end=2)]
263 271
264 272 def test_get_changesets_numerical_id_respects_both_start_and_end_last(self):
265 273 with self.assertRaises(RepositoryError):
266 274 last = len(self.repo.revisions)
267 275 list(self.repo.get_changesets(start=last-1, end=last-2))
268 276
269 277 def test_get_changesets_numerical_id_last_zero_error(self):
270 278 with self.assertRaises(RepositoryError):
271 279 last = len(self.repo.revisions)
272 280 list(self.repo.get_changesets(start=last-1, end=0))
273 281
274 282
275 283 class ChangesetsChangesTestCaseMixin(BackendTestMixin):
276 284 recreate_repo_per_test = False
277 285
278 286 @classmethod
279 287 def _get_commits(cls):
280 288 return [
281 289 {
282 290 'message': u'Initial',
283 291 'author': u'Joe Doe <joe.doe@example.com>',
284 292 'date': datetime.datetime(2010, 1, 1, 20),
285 293 'added': [
286 294 FileNode('foo/bar', content='foo'),
287 295 FileNode('foobar', content='foo'),
288 296 FileNode('qwe', content='foo'),
289 297 ],
290 298 },
291 299 {
292 300 'message': u'Massive changes',
293 301 'author': u'Joe Doe <joe.doe@example.com>',
294 302 'date': datetime.datetime(2010, 1, 1, 22),
295 303 'added': [FileNode('fallout', content='War never changes')],
296 304 'changed': [
297 305 FileNode('foo/bar', content='baz'),
298 306 FileNode('foobar', content='baz'),
299 307 ],
300 308 'removed': [FileNode('qwe')],
301 309 },
302 310 ]
303 311
304 312 def test_initial_commit(self):
305 313 changeset = self.repo.get_changeset(0)
306 314 self.assertItemsEqual(changeset.added, [
307 315 changeset.get_node('foo/bar'),
308 316 changeset.get_node('foobar'),
309 317 changeset.get_node('qwe'),
310 318 ])
311 319 self.assertItemsEqual(changeset.changed, [])
312 320 self.assertItemsEqual(changeset.removed, [])
313 321
314 322 def test_head_added(self):
315 323 changeset = self.repo.get_changeset()
316 324 self.assertTrue(isinstance(changeset.added, AddedFileNodesGenerator))
317 325 self.assertItemsEqual(changeset.added, [
318 326 changeset.get_node('fallout'),
319 327 ])
320 328 self.assertTrue(isinstance(changeset.changed, ChangedFileNodesGenerator))
321 329 self.assertItemsEqual(changeset.changed, [
322 330 changeset.get_node('foo/bar'),
323 331 changeset.get_node('foobar'),
324 332 ])
325 333 self.assertTrue(isinstance(changeset.removed, RemovedFileNodesGenerator))
326 334 self.assertEqual(len(changeset.removed), 1)
327 335 self.assertEqual(list(changeset.removed)[0].path, 'qwe')
328 336
329 337
330 338 # For each backend create test case class
331 339 for alias in SCM_TESTS:
332 340 attrs = {
333 341 'backend_alias': alias,
334 342 }
335 343 # tests with additional commits
336 344 cls_name = ''.join(('%s changesets with commits test' % alias).title().split())
337 345 bases = (ChangesetsWithCommitsTestCaseixin, unittest.TestCase)
338 346 globals()[cls_name] = type(cls_name, bases, attrs)
339 347
340 348 # tests without additional commits
341 349 cls_name = ''.join(('%s changesets test' % alias).title().split())
342 350 bases = (ChangesetsTestCaseMixin, unittest.TestCase)
343 351 globals()[cls_name] = type(cls_name, bases, attrs)
344 352
345 353 # tests changes
346 354 cls_name = ''.join(('%s changesets changes test' % alias).title().split())
347 355 bases = (ChangesetsChangesTestCaseMixin, unittest.TestCase)
348 356 globals()[cls_name] = type(cls_name, bases, attrs)
349 357
350 358
351 359 if __name__ == '__main__':
352 360 unittest.main()
@@ -1,90 +1,98 b''
1 1 from __future__ import with_statement
2 2
3 3 import datetime
4 4 from rhodecode.lib.vcs.nodes import FileNode
5 5 from rhodecode.lib.vcs.utils.compat import unittest
6 6 from rhodecode.tests.vcs.base import BackendTestMixin
7 7 from rhodecode.tests.vcs.conf import SCM_TESTS
8 8
9 9
10 10 class WorkdirTestCaseMixin(BackendTestMixin):
11 11
12 12 @classmethod
13 13 def _get_commits(cls):
14 14 commits = [
15 15 {
16 16 'message': u'Initial commit',
17 17 'author': u'Joe Doe <joe.doe@example.com>',
18 18 'date': datetime.datetime(2010, 1, 1, 20),
19 19 'added': [
20 20 FileNode('foobar', content='Foobar'),
21 21 FileNode('foobar2', content='Foobar II'),
22 22 FileNode('foo/bar/baz', content='baz here!'),
23 23 ],
24 24 },
25 25 {
26 26 'message': u'Changes...',
27 27 'author': u'Jane Doe <jane.doe@example.com>',
28 28 'date': datetime.datetime(2010, 1, 1, 21),
29 29 'added': [
30 30 FileNode('some/new.txt', content='news...'),
31 31 ],
32 32 'changed': [
33 33 FileNode('foobar', 'Foobar I'),
34 34 ],
35 35 'removed': [],
36 36 },
37 37 ]
38 38 return commits
39 39
40 40 def test_get_branch_for_default_branch(self):
41 41 self.assertEqual(self.repo.workdir.get_branch(),
42 42 self.repo.DEFAULT_BRANCH_NAME)
43 43
44 44 def test_get_branch_after_adding_one(self):
45 45 self.imc.add(FileNode('docs/index.txt',
46 46 content='Documentation\n'))
47 47 self.imc.commit(
48 48 message=u'New branch: foobar',
49 49 author=u'joe',
50 50 branch='foobar',
51 51 )
52 self.assertEqual(self.repo.workdir.get_branch(), self.default_branch)
52 53
53 54 def test_get_changeset(self):
55 old_head = self.repo.get_changeset()
54 56 self.imc.add(FileNode('docs/index.txt',
55 57 content='Documentation\n'))
56 58 head = self.imc.commit(
57 59 message=u'New branch: foobar',
58 60 author=u'joe',
59 61 branch='foobar',
60 62 )
63 self.assertEqual(self.repo.workdir.get_branch(), self.default_branch)
64 self.repo.workdir.checkout_branch('foobar')
61 65 self.assertEqual(self.repo.workdir.get_changeset(), head)
62 66
67 # Make sure that old head is still there after update to defualt branch
68 self.repo.workdir.checkout_branch(self.default_branch)
69 self.assertEqual(self.repo.workdir.get_changeset(), old_head)
70
63 71 def test_checkout_branch(self):
64 72 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
65 73 # first, 'foobranch' does not exist.
66 74 self.assertRaises(BranchDoesNotExistError, self.repo.workdir.checkout_branch,
67 75 branch='foobranch')
68 76 # create new branch 'foobranch'.
69 77 self.imc.add(FileNode('file1', content='blah'))
70 78 self.imc.commit(message=u'asd', author=u'john', branch='foobranch')
71 79 # go back to the default branch
72 80 self.repo.workdir.checkout_branch()
73 81 self.assertEqual(self.repo.workdir.get_branch(), self.backend_class.DEFAULT_BRANCH_NAME)
74 82 # checkout 'foobranch'
75 83 self.repo.workdir.checkout_branch('foobranch')
76 84 self.assertEqual(self.repo.workdir.get_branch(), 'foobranch')
77 85
78 86
79 87 # For each backend create test case class
80 88 for alias in SCM_TESTS:
81 89 attrs = {
82 90 'backend_alias': alias,
83 91 }
84 92 cls_name = ''.join(('%s branch test' % alias).title().split())
85 93 bases = (WorkdirTestCaseMixin, unittest.TestCase)
86 94 globals()[cls_name] = type(cls_name, bases, attrs)
87 95
88 96
89 97 if __name__ == '__main__':
90 98 unittest.main()
General Comments 0
You need to be logged in to leave comments. Login now