##// END OF EJS Templates
fixes #630 git statistics do too much work making them slow....
marcink -
r2968:4abfb1af beta
parent child Browse files
Show More
@@ -1,489 +1,493 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 from rhodecode.lib.vcs.conf import settings
6 6 from rhodecode.lib.vcs.exceptions import RepositoryError
7 7 from rhodecode.lib.vcs.exceptions import ChangesetError
8 8 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
9 9 from rhodecode.lib.vcs.exceptions import VCSError
10 10 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 11 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
12 12 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
13 13 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
14 RemovedFileNode, SubModuleNode
14 RemovedFileNode, SubModuleNode, ChangedFileNodesGenerator,\
15 AddedFileNodesGenerator, RemovedFileNodesGenerator
15 16 from rhodecode.lib.vcs.utils import safe_unicode
16 17 from rhodecode.lib.vcs.utils import date_fromtimestamp
17 18 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 19
19 20
20 21 class GitChangeset(BaseChangeset):
21 22 """
22 23 Represents state of the repository at single revision.
23 24 """
24 25
25 26 def __init__(self, repository, revision):
26 27 self._stat_modes = {}
27 28 self.repository = repository
28 29
29 30 try:
30 31 commit = self.repository._repo.get_object(revision)
31 32 if isinstance(commit, objects.Tag):
32 33 revision = commit.object[1]
33 34 commit = self.repository._repo.get_object(commit.object[1])
34 35 except KeyError:
35 36 raise RepositoryError("Cannot get object with id %s" % revision)
36 37 self.raw_id = revision
37 38 self.id = self.raw_id
38 39 self.short_id = self.raw_id[:12]
39 40 self._commit = commit
40 41
41 42 self._tree_id = commit.tree
42 43 self._commiter_property = 'committer'
43 44 self._date_property = 'commit_time'
44 45 self._date_tz_property = 'commit_timezone'
45 46 self.revision = repository.revisions.index(revision)
46 47
47 48 self.message = safe_unicode(commit.message)
48 49 #self.branch = None
49 50 self.tags = []
50 51 self.nodes = {}
51 52 self._paths = {}
52 53
53 54 @LazyProperty
54 55 def author(self):
55 56 return safe_unicode(getattr(self._commit, self._commiter_property))
56 57
57 58 @LazyProperty
58 59 def date(self):
59 60 return date_fromtimestamp(getattr(self._commit, self._date_property),
60 61 getattr(self._commit, self._date_tz_property))
61 62
62 63 @LazyProperty
63 64 def _timestamp(self):
64 65 return getattr(self._commit, self._date_property)
65 66
66 67 @LazyProperty
67 68 def status(self):
68 69 """
69 70 Returns modified, added, removed, deleted files for current changeset
70 71 """
71 72 return self.changed, self.added, self.removed
72 73
73 74 @LazyProperty
74 75 def branch(self):
75 76
76 77 heads = self.repository._heads(reverse=False)
77 78
78 79 ref = heads.get(self.raw_id)
79 80 if ref:
80 81 return safe_unicode(ref)
81 82
82 83 def _fix_path(self, path):
83 84 """
84 85 Paths are stored without trailing slash so we need to get rid off it if
85 86 needed.
86 87 """
87 88 if path.endswith('/'):
88 89 path = path.rstrip('/')
89 90 return path
90 91
91 92 def _get_id_for_path(self, path):
92 93
93 94 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
94 95 if not path in self._paths:
95 96 path = path.strip('/')
96 97 # set root tree
97 98 tree = self.repository._repo[self._tree_id]
98 99 if path == '':
99 100 self._paths[''] = tree.id
100 101 return tree.id
101 102 splitted = path.split('/')
102 103 dirs, name = splitted[:-1], splitted[-1]
103 104 curdir = ''
104 105
105 106 # initially extract things from root dir
106 107 for item, stat, id in tree.iteritems():
107 108 if curdir:
108 109 name = '/'.join((curdir, item))
109 110 else:
110 111 name = item
111 112 self._paths[name] = id
112 113 self._stat_modes[name] = stat
113 114
114 115 for dir in dirs:
115 116 if curdir:
116 117 curdir = '/'.join((curdir, dir))
117 118 else:
118 119 curdir = dir
119 120 dir_id = None
120 121 for item, stat, id in tree.iteritems():
121 122 if dir == item:
122 123 dir_id = id
123 124 if dir_id:
124 125 # Update tree
125 126 tree = self.repository._repo[dir_id]
126 127 if not isinstance(tree, objects.Tree):
127 128 raise ChangesetError('%s is not a directory' % curdir)
128 129 else:
129 130 raise ChangesetError('%s have not been found' % curdir)
130 131
131 132 # cache all items from the given traversed tree
132 133 for item, stat, id in tree.iteritems():
133 134 if curdir:
134 135 name = '/'.join((curdir, item))
135 136 else:
136 137 name = item
137 138 self._paths[name] = id
138 139 self._stat_modes[name] = stat
139 140 if not path in self._paths:
140 141 raise NodeDoesNotExistError("There is no file nor directory "
141 142 "at the given path %r at revision %r"
142 143 % (path, self.short_id))
143 144 return self._paths[path]
144 145
145 146 def _get_kind(self, path):
146 147 obj = self.repository._repo[self._get_id_for_path(path)]
147 148 if isinstance(obj, objects.Blob):
148 149 return NodeKind.FILE
149 150 elif isinstance(obj, objects.Tree):
150 151 return NodeKind.DIR
151 152
152 153 def _get_file_nodes(self):
153 154 return chain(*(t[2] for t in self.walk()))
154 155
155 156 @LazyProperty
156 157 def parents(self):
157 158 """
158 159 Returns list of parents changesets.
159 160 """
160 161 return [self.repository.get_changeset(parent)
161 162 for parent in self._commit.parents]
162 163
163 164 def next(self, branch=None):
164 165
165 166 if branch and self.branch != branch:
166 167 raise VCSError('Branch option used on changeset not belonging '
167 168 'to that branch')
168 169
169 170 def _next(changeset, branch):
170 171 try:
171 172 next_ = changeset.revision + 1
172 173 next_rev = changeset.repository.revisions[next_]
173 174 except IndexError:
174 175 raise ChangesetDoesNotExistError
175 176 cs = changeset.repository.get_changeset(next_rev)
176 177
177 178 if branch and branch != cs.branch:
178 179 return _next(cs, branch)
179 180
180 181 return cs
181 182
182 183 return _next(self, branch)
183 184
184 185 def prev(self, branch=None):
185 186 if branch and self.branch != branch:
186 187 raise VCSError('Branch option used on changeset not belonging '
187 188 'to that branch')
188 189
189 190 def _prev(changeset, branch):
190 191 try:
191 192 prev_ = changeset.revision - 1
192 193 if prev_ < 0:
193 194 raise IndexError
194 195 prev_rev = changeset.repository.revisions[prev_]
195 196 except IndexError:
196 197 raise ChangesetDoesNotExistError
197 198
198 199 cs = changeset.repository.get_changeset(prev_rev)
199 200
200 201 if branch and branch != cs.branch:
201 202 return _prev(cs, branch)
202 203
203 204 return cs
204 205
205 206 return _prev(self, branch)
206 207
207 208 def diff(self, ignore_whitespace=True, context=3):
208 209 rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET
209 210 rev2 = self
210 211 return ''.join(self.repository.get_diff(rev1, rev2,
211 212 ignore_whitespace=ignore_whitespace,
212 213 context=context))
213 214
214 215 def get_file_mode(self, path):
215 216 """
216 217 Returns stat mode of the file at the given ``path``.
217 218 """
218 219 # ensure path is traversed
219 220 self._get_id_for_path(path)
220 221 return self._stat_modes[path]
221 222
222 223 def get_file_content(self, path):
223 224 """
224 225 Returns content of the file at given ``path``.
225 226 """
226 227 id = self._get_id_for_path(path)
227 228 blob = self.repository._repo[id]
228 229 return blob.as_pretty_string()
229 230
230 231 def get_file_size(self, path):
231 232 """
232 233 Returns size of the file at given ``path``.
233 234 """
234 235 id = self._get_id_for_path(path)
235 236 blob = self.repository._repo[id]
236 237 return blob.raw_length()
237 238
238 239 def get_file_changeset(self, path):
239 240 """
240 241 Returns last commit of the file at the given ``path``.
241 242 """
242 243 node = self.get_node(path)
243 244 return node.history[0]
244 245
245 246 def get_file_history(self, path):
246 247 """
247 248 Returns history of file as reversed list of ``Changeset`` objects for
248 249 which file at given ``path`` has been modified.
249 250
250 251 TODO: This function now uses os underlying 'git' and 'grep' commands
251 252 which is generally not good. Should be replaced with algorithm
252 253 iterating commits.
253 254 """
254 255 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
255 256 self.id, path
256 257 )
257 258 so, se = self.repository.run_git_command(cmd)
258 259 ids = re.findall(r'[0-9a-fA-F]{40}', so)
259 260 return [self.repository.get_changeset(id) for id in ids]
260 261
261 262 def get_file_annotate(self, path):
262 263 """
263 264 Returns a list of three element tuples with lineno,changeset and line
264 265
265 266 TODO: This function now uses os underlying 'git' command which is
266 267 generally not good. Should be replaced with algorithm iterating
267 268 commits.
268 269 """
269 270 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
270 271 # -l ==> outputs long shas (and we need all 40 characters)
271 272 # --root ==> doesn't put '^' character for bounderies
272 273 # -r sha ==> blames for the given revision
273 274 so, se = self.repository.run_git_command(cmd)
274 275
275 276 annotate = []
276 277 for i, blame_line in enumerate(so.split('\n')[:-1]):
277 278 ln_no = i + 1
278 279 id, line = re.split(r' ', blame_line, 1)
279 280 annotate.append((ln_no, self.repository.get_changeset(id), line))
280 281 return annotate
281 282
282 283 def fill_archive(self, stream=None, kind='tgz', prefix=None,
283 284 subrepos=False):
284 285 """
285 286 Fills up given stream.
286 287
287 288 :param stream: file like object.
288 289 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
289 290 Default: ``tgz``.
290 291 :param prefix: name of root directory in archive.
291 292 Default is repository name and changeset's raw_id joined with dash
292 293 (``repo-tip.<KIND>``).
293 294 :param subrepos: include subrepos in this archive.
294 295
295 296 :raise ImproperArchiveTypeError: If given kind is wrong.
296 297 :raise VcsError: If given stream is None
297 298
298 299 """
299 300 allowed_kinds = settings.ARCHIVE_SPECS.keys()
300 301 if kind not in allowed_kinds:
301 302 raise ImproperArchiveTypeError('Archive kind not supported use one'
302 303 'of %s', allowed_kinds)
303 304
304 305 if prefix is None:
305 306 prefix = '%s-%s' % (self.repository.name, self.short_id)
306 307 elif prefix.startswith('/'):
307 308 raise VCSError("Prefix cannot start with leading slash")
308 309 elif prefix.strip() == '':
309 310 raise VCSError("Prefix cannot be empty")
310 311
311 312 if kind == 'zip':
312 313 frmt = 'zip'
313 314 else:
314 315 frmt = 'tar'
315 316 cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
316 317 self.raw_id)
317 318 if kind == 'tgz':
318 319 cmd += ' | gzip -9'
319 320 elif kind == 'tbz2':
320 321 cmd += ' | bzip2 -9'
321 322
322 323 if stream is None:
323 324 raise VCSError('You need to pass in a valid stream for filling'
324 325 ' with archival data')
325 326 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
326 327 cwd=self.repository.path)
327 328
328 329 buffer_size = 1024 * 8
329 330 chunk = popen.stdout.read(buffer_size)
330 331 while chunk:
331 332 stream.write(chunk)
332 333 chunk = popen.stdout.read(buffer_size)
333 334 # Make sure all descriptors would be read
334 335 popen.communicate()
335 336
336 337 def get_nodes(self, path):
337 338 if self._get_kind(path) != NodeKind.DIR:
338 339 raise ChangesetError("Directory does not exist for revision %r at "
339 340 " %r" % (self.revision, path))
340 341 path = self._fix_path(path)
341 342 id = self._get_id_for_path(path)
342 343 tree = self.repository._repo[id]
343 344 dirnodes = []
344 345 filenodes = []
345 346 als = self.repository.alias
346 347 for name, stat, id in tree.iteritems():
347 348 if objects.S_ISGITLINK(stat):
348 349 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
349 350 alias=als))
350 351 continue
351 352
352 353 obj = self.repository._repo.get_object(id)
353 354 if path != '':
354 355 obj_path = '/'.join((path, name))
355 356 else:
356 357 obj_path = name
357 358 if obj_path not in self._stat_modes:
358 359 self._stat_modes[obj_path] = stat
359 360 if isinstance(obj, objects.Tree):
360 361 dirnodes.append(DirNode(obj_path, changeset=self))
361 362 elif isinstance(obj, objects.Blob):
362 363 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
363 364 else:
364 365 raise ChangesetError("Requested object should be Tree "
365 366 "or Blob, is %r" % type(obj))
366 367 nodes = dirnodes + filenodes
367 368 for node in nodes:
368 369 if not node.path in self.nodes:
369 370 self.nodes[node.path] = node
370 371 nodes.sort()
371 372 return nodes
372 373
373 374 def get_node(self, path):
374 375 if isinstance(path, unicode):
375 376 path = path.encode('utf-8')
376 377 path = self._fix_path(path)
377 378 if not path in self.nodes:
378 379 try:
379 380 id_ = self._get_id_for_path(path)
380 381 except ChangesetError:
381 382 raise NodeDoesNotExistError("Cannot find one of parents' "
382 383 "directories for a given path: %s" % path)
383 384
384 385 _GL = lambda m: m and objects.S_ISGITLINK(m)
385 386 if _GL(self._stat_modes.get(path)):
386 387 node = SubModuleNode(path, url=None, changeset=id_,
387 388 alias=self.repository.alias)
388 389 else:
389 390 obj = self.repository._repo.get_object(id_)
390 391
391 392 if isinstance(obj, objects.Tree):
392 393 if path == '':
393 394 node = RootNode(changeset=self)
394 395 else:
395 396 node = DirNode(path, changeset=self)
396 397 node._tree = obj
397 398 elif isinstance(obj, objects.Blob):
398 399 node = FileNode(path, changeset=self)
399 400 node._blob = obj
400 401 else:
401 402 raise NodeDoesNotExistError("There is no file nor directory "
402 403 "at the given path %r at revision %r"
403 404 % (path, self.short_id))
404 405 # cache node
405 406 self.nodes[path] = node
406 407 return self.nodes[path]
407 408
408 409 @LazyProperty
409 410 def affected_files(self):
410 411 """
411 412 Get's a fast accessible file changes for given changeset
412 413 """
413 414 a, m, d = self._changes_cache
414 415 return list(a.union(m).union(d))
415 416
416 417 @LazyProperty
417 418 def _diff_name_status(self):
418 419 output = []
419 420 for parent in self.parents:
420 421 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id,
421 422 self.raw_id)
422 423 so, se = self.repository.run_git_command(cmd)
423 424 output.append(so.strip())
424 425 return '\n'.join(output)
425 426
426 427 @LazyProperty
427 428 def _changes_cache(self):
428 429 added = set()
429 430 modified = set()
430 431 deleted = set()
431 432 _r = self.repository._repo
432 433
433 434 parents = self.parents
434 435 if not self.parents:
435 436 parents = [EmptyChangeset()]
436 437 for parent in parents:
437 438 if isinstance(parent, EmptyChangeset):
438 439 oid = None
439 440 else:
440 441 oid = _r[parent.raw_id].tree
441 442 changes = _r.object_store.tree_changes(oid, _r[self.raw_id].tree)
442 443 for (oldpath, newpath), (_, _), (_, _) in changes:
443 444 if newpath and oldpath:
444 445 modified.add(newpath)
445 446 elif newpath and not oldpath:
446 447 added.add(newpath)
447 448 elif not newpath and oldpath:
448 449 deleted.add(oldpath)
449 450 return added, modified, deleted
450 451
451 452 def _get_paths_for_status(self, status):
452 453 """
453 454 Returns sorted list of paths for given ``status``.
454 455
455 456 :param status: one of: *added*, *modified* or *deleted*
456 457 """
457 458 a, m, d = self._changes_cache
458 459 return sorted({
459 460 'added': list(a),
460 461 'modified': list(m),
461 462 'deleted': list(d)}[status]
462 463 )
463 464
464 465 @LazyProperty
465 466 def added(self):
466 467 """
467 468 Returns list of added ``FileNode`` objects.
468 469 """
469 470 if not self.parents:
470 471 return list(self._get_file_nodes())
471 return [self.get_node(path) for path in self._get_paths_for_status('added')]
472 return AddedFileNodesGenerator([n for n in
473 self._get_paths_for_status('added')], self)
472 474
473 475 @LazyProperty
474 476 def changed(self):
475 477 """
476 478 Returns list of modified ``FileNode`` objects.
477 479 """
478 480 if not self.parents:
479 481 return []
480 return [self.get_node(path) for path in self._get_paths_for_status('modified')]
482 return ChangedFileNodesGenerator([n for n in
483 self._get_paths_for_status('modified')], self)
481 484
482 485 @LazyProperty
483 486 def removed(self):
484 487 """
485 488 Returns list of removed ``FileNode`` objects.
486 489 """
487 490 if not self.parents:
488 491 return []
489 return [RemovedFileNode(path) for path in self._get_paths_for_status('deleted')]
492 return RemovedFileNodesGenerator([n for n in
493 self._get_paths_for_status('deleted')], self)
@@ -1,344 +1,348 b''
1 1 from __future__ import with_statement
2 2
3 3 from rhodecode.lib import vcs
4 4 import datetime
5 5 from base import BackendTestMixin
6 6 from conf import SCM_TESTS
7 7 from rhodecode.lib.vcs.backends.base import BaseChangeset
8 from rhodecode.lib.vcs.nodes import FileNode
8 from rhodecode.lib.vcs.nodes import FileNode, AddedFileNodesGenerator,\
9 ChangedFileNodesGenerator, RemovedFileNodesGenerator
9 10 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
10 11 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 12 from rhodecode.lib.vcs.exceptions import RepositoryError
12 13 from rhodecode.lib.vcs.utils.compat import unittest
13 14
14 15
15 16 class TestBaseChangeset(unittest.TestCase):
16 17
17 18 def test_as_dict(self):
18 19 changeset = BaseChangeset()
19 20 changeset.id = 'ID'
20 21 changeset.raw_id = 'RAW_ID'
21 22 changeset.short_id = 'SHORT_ID'
22 23 changeset.revision = 1009
23 24 changeset.date = datetime.datetime(2011, 1, 30, 1, 45)
24 25 changeset.message = 'Message of a commit'
25 26 changeset.author = 'Joe Doe <joe.doe@example.com>'
26 27 changeset.added = [FileNode('foo/bar/baz'), FileNode('foobar')]
27 28 changeset.changed = []
28 29 changeset.removed = []
29 30 self.assertEqual(changeset.as_dict(), {
30 31 'id': 'ID',
31 32 'raw_id': 'RAW_ID',
32 33 'short_id': 'SHORT_ID',
33 34 'revision': 1009,
34 35 'date': datetime.datetime(2011, 1, 30, 1, 45),
35 36 'message': 'Message of a commit',
36 37 'author': {
37 38 'name': 'Joe Doe',
38 39 'email': 'joe.doe@example.com',
39 40 },
40 41 'added': ['foo/bar/baz', 'foobar'],
41 42 'changed': [],
42 43 'removed': [],
43 44 })
44 45
45 46 class ChangesetsWithCommitsTestCaseixin(BackendTestMixin):
46 47 recreate_repo_per_test = True
47 48
48 49 @classmethod
49 50 def _get_commits(cls):
50 51 start_date = datetime.datetime(2010, 1, 1, 20)
51 52 for x in xrange(5):
52 53 yield {
53 54 'message': 'Commit %d' % x,
54 55 'author': 'Joe Doe <joe.doe@example.com>',
55 56 'date': start_date + datetime.timedelta(hours=12 * x),
56 57 'added': [
57 58 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
58 59 ],
59 60 }
60 61
61 62 def test_new_branch(self):
62 63 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
63 64 content='Documentation\n'))
64 65 foobar_tip = self.imc.commit(
65 66 message=u'New branch: foobar',
66 67 author=u'joe',
67 68 branch='foobar',
68 69 )
69 70 self.assertTrue('foobar' in self.repo.branches)
70 71 self.assertEqual(foobar_tip.branch, 'foobar')
71 72 # 'foobar' should be the only branch that contains the new commit
72 73 self.assertNotEqual(*self.repo.branches.values())
73 74
74 75 def test_new_head_in_default_branch(self):
75 76 tip = self.repo.get_changeset()
76 77 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
77 78 content='Documentation\n'))
78 79 foobar_tip = self.imc.commit(
79 80 message=u'New branch: foobar',
80 81 author=u'joe',
81 82 branch='foobar',
82 83 parents=[tip],
83 84 )
84 85 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
85 86 content='Documentation\nand more...\n'))
86 87 newtip = self.imc.commit(
87 88 message=u'At default branch',
88 89 author=u'joe',
89 90 branch=foobar_tip.branch,
90 91 parents=[foobar_tip],
91 92 )
92 93
93 94 newest_tip = self.imc.commit(
94 95 message=u'Merged with %s' % foobar_tip.raw_id,
95 96 author=u'joe',
96 97 branch=self.backend_class.DEFAULT_BRANCH_NAME,
97 98 parents=[newtip, foobar_tip],
98 99 )
99 100
100 101 self.assertEqual(newest_tip.branch,
101 102 self.backend_class.DEFAULT_BRANCH_NAME)
102 103
103 104 def test_get_changesets_respects_branch_name(self):
104 105 tip = self.repo.get_changeset()
105 106 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
106 107 content='Documentation\n'))
107 108 doc_changeset = self.imc.commit(
108 109 message=u'New branch: docs',
109 110 author=u'joe',
110 111 branch='docs',
111 112 )
112 113 self.imc.add(vcs.nodes.FileNode('newfile', content=''))
113 114 self.imc.commit(
114 115 message=u'Back in default branch',
115 116 author=u'joe',
116 117 parents=[tip],
117 118 )
118 119 default_branch_changesets = self.repo.get_changesets(
119 120 branch_name=self.repo.DEFAULT_BRANCH_NAME)
120 121 self.assertNotIn(doc_changeset, default_branch_changesets)
121 122
122 123 def test_get_changeset_by_branch(self):
123 124 for branch, sha in self.repo.branches.iteritems():
124 125 self.assertEqual(sha, self.repo.get_changeset(branch).raw_id)
125 126
126 127 def test_get_changeset_by_tag(self):
127 128 for tag, sha in self.repo.tags.iteritems():
128 129 self.assertEqual(sha, self.repo.get_changeset(tag).raw_id)
129 130
130 131
131 132 class ChangesetsTestCaseMixin(BackendTestMixin):
132 133 recreate_repo_per_test = False
133 134
134 135 @classmethod
135 136 def _get_commits(cls):
136 137 start_date = datetime.datetime(2010, 1, 1, 20)
137 138 for x in xrange(5):
138 139 yield {
139 140 'message': u'Commit %d' % x,
140 141 'author': u'Joe Doe <joe.doe@example.com>',
141 142 'date': start_date + datetime.timedelta(hours=12 * x),
142 143 'added': [
143 144 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
144 145 ],
145 146 }
146 147
147 148 def test_simple(self):
148 149 tip = self.repo.get_changeset()
149 150 self.assertEqual(tip.date, datetime.datetime(2010, 1, 3, 20))
150 151
151 152 def test_get_changesets_is_ordered_by_date(self):
152 153 changesets = list(self.repo.get_changesets())
153 154 ordered_by_date = sorted(changesets,
154 155 key=lambda cs: cs.date)
155 156 self.assertItemsEqual(changesets, ordered_by_date)
156 157
157 158 def test_get_changesets_respects_start(self):
158 159 second_id = self.repo.revisions[1]
159 160 changesets = list(self.repo.get_changesets(start=second_id))
160 161 self.assertEqual(len(changesets), 4)
161 162
162 163 def test_get_changesets_numerical_id_respects_start(self):
163 164 second_id = 1
164 165 changesets = list(self.repo.get_changesets(start=second_id))
165 166 self.assertEqual(len(changesets), 4)
166 167
167 168 def test_get_changesets_includes_start_changeset(self):
168 169 second_id = self.repo.revisions[1]
169 170 changesets = list(self.repo.get_changesets(start=second_id))
170 171 self.assertEqual(changesets[0].raw_id, second_id)
171 172
172 173 def test_get_changesets_respects_end(self):
173 174 second_id = self.repo.revisions[1]
174 175 changesets = list(self.repo.get_changesets(end=second_id))
175 176 self.assertEqual(changesets[-1].raw_id, second_id)
176 177 self.assertEqual(len(changesets), 2)
177 178
178 179 def test_get_changesets_numerical_id_respects_end(self):
179 180 second_id = 1
180 181 changesets = list(self.repo.get_changesets(end=second_id))
181 182 self.assertEqual(changesets.index(changesets[-1]), second_id)
182 183 self.assertEqual(len(changesets), 2)
183 184
184 185 def test_get_changesets_respects_both_start_and_end(self):
185 186 second_id = self.repo.revisions[1]
186 187 third_id = self.repo.revisions[2]
187 188 changesets = list(self.repo.get_changesets(start=second_id,
188 189 end=third_id))
189 190 self.assertEqual(len(changesets), 2)
190 191
191 192 def test_get_changesets_numerical_id_respects_both_start_and_end(self):
192 193 changesets = list(self.repo.get_changesets(start=2, end=3))
193 194 self.assertEqual(len(changesets), 2)
194 195
195 196 def test_get_changesets_includes_end_changeset(self):
196 197 second_id = self.repo.revisions[1]
197 198 changesets = list(self.repo.get_changesets(end=second_id))
198 199 self.assertEqual(changesets[-1].raw_id, second_id)
199 200
200 201 def test_get_changesets_respects_start_date(self):
201 202 start_date = datetime.datetime(2010, 2, 1)
202 203 for cs in self.repo.get_changesets(start_date=start_date):
203 204 self.assertGreaterEqual(cs.date, start_date)
204 205
205 206 def test_get_changesets_respects_end_date(self):
206 207 end_date = datetime.datetime(2010, 2, 1)
207 208 for cs in self.repo.get_changesets(end_date=end_date):
208 209 self.assertLessEqual(cs.date, end_date)
209 210
210 211 def test_get_changesets_respects_reverse(self):
211 212 changesets_id_list = [cs.raw_id for cs in
212 213 self.repo.get_changesets(reverse=True)]
213 214 self.assertItemsEqual(changesets_id_list, reversed(self.repo.revisions))
214 215
215 216 def test_get_filenodes_generator(self):
216 217 tip = self.repo.get_changeset()
217 218 filepaths = [node.path for node in tip.get_filenodes_generator()]
218 219 self.assertItemsEqual(filepaths, ['file_%d.txt' % x for x in xrange(5)])
219 220
220 221 def test_size(self):
221 222 tip = self.repo.get_changeset()
222 223 size = 5 * len('Foobar N') # Size of 5 files
223 224 self.assertEqual(tip.size, size)
224 225
225 226 def test_author(self):
226 227 tip = self.repo.get_changeset()
227 228 self.assertEqual(tip.author, u'Joe Doe <joe.doe@example.com>')
228 229
229 230 def test_author_name(self):
230 231 tip = self.repo.get_changeset()
231 232 self.assertEqual(tip.author_name, u'Joe Doe')
232 233
233 234 def test_author_email(self):
234 235 tip = self.repo.get_changeset()
235 236 self.assertEqual(tip.author_email, u'joe.doe@example.com')
236 237
237 238 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_start(self):
238 239 with self.assertRaises(ChangesetDoesNotExistError):
239 240 list(self.repo.get_changesets(start='foobar'))
240 241
241 242 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_end(self):
242 243 with self.assertRaises(ChangesetDoesNotExistError):
243 244 list(self.repo.get_changesets(end='foobar'))
244 245
245 246 def test_get_changesets_raise_branchdoesnotexist_for_wrong_branch_name(self):
246 247 with self.assertRaises(BranchDoesNotExistError):
247 248 list(self.repo.get_changesets(branch_name='foobar'))
248 249
249 250 def test_get_changesets_raise_repositoryerror_for_wrong_start_end(self):
250 251 start = self.repo.revisions[-1]
251 252 end = self.repo.revisions[0]
252 253 with self.assertRaises(RepositoryError):
253 254 list(self.repo.get_changesets(start=start, end=end))
254 255
255 256 def test_get_changesets_numerical_id_reversed(self):
256 257 with self.assertRaises(RepositoryError):
257 258 [x for x in self.repo.get_changesets(start=3, end=2)]
258 259
259 260 def test_get_changesets_numerical_id_respects_both_start_and_end_last(self):
260 261 with self.assertRaises(RepositoryError):
261 262 last = len(self.repo.revisions)
262 263 list(self.repo.get_changesets(start=last-1, end=last-2))
263 264
264 265 def test_get_changesets_numerical_id_last_zero_error(self):
265 266 with self.assertRaises(RepositoryError):
266 267 last = len(self.repo.revisions)
267 268 list(self.repo.get_changesets(start=last-1, end=0))
268 269
269 270
270 271 class ChangesetsChangesTestCaseMixin(BackendTestMixin):
271 272 recreate_repo_per_test = False
272 273
273 274 @classmethod
274 275 def _get_commits(cls):
275 276 return [
276 277 {
277 278 'message': u'Initial',
278 279 'author': u'Joe Doe <joe.doe@example.com>',
279 280 'date': datetime.datetime(2010, 1, 1, 20),
280 281 'added': [
281 282 FileNode('foo/bar', content='foo'),
282 283 FileNode('foobar', content='foo'),
283 284 FileNode('qwe', content='foo'),
284 285 ],
285 286 },
286 287 {
287 288 'message': u'Massive changes',
288 289 'author': u'Joe Doe <joe.doe@example.com>',
289 290 'date': datetime.datetime(2010, 1, 1, 22),
290 291 'added': [FileNode('fallout', content='War never changes')],
291 292 'changed': [
292 293 FileNode('foo/bar', content='baz'),
293 294 FileNode('foobar', content='baz'),
294 295 ],
295 296 'removed': [FileNode('qwe')],
296 297 },
297 298 ]
298 299
299 300 def test_initial_commit(self):
300 301 changeset = self.repo.get_changeset(0)
301 302 self.assertItemsEqual(changeset.added, [
302 303 changeset.get_node('foo/bar'),
303 304 changeset.get_node('foobar'),
304 305 changeset.get_node('qwe'),
305 306 ])
306 307 self.assertItemsEqual(changeset.changed, [])
307 308 self.assertItemsEqual(changeset.removed, [])
308 309
309 310 def test_head_added(self):
310 311 changeset = self.repo.get_changeset()
312 self.assertTrue(isinstance(changeset.added, AddedFileNodesGenerator))
311 313 self.assertItemsEqual(changeset.added, [
312 314 changeset.get_node('fallout'),
313 315 ])
316 self.assertTrue(isinstance(changeset.changed, ChangedFileNodesGenerator))
314 317 self.assertItemsEqual(changeset.changed, [
315 318 changeset.get_node('foo/bar'),
316 319 changeset.get_node('foobar'),
317 320 ])
321 self.assertTrue(isinstance(changeset.removed, RemovedFileNodesGenerator))
318 322 self.assertEqual(len(changeset.removed), 1)
319 323 self.assertEqual(list(changeset.removed)[0].path, 'qwe')
320 324
321 325
322 326 # For each backend create test case class
323 327 for alias in SCM_TESTS:
324 328 attrs = {
325 329 'backend_alias': alias,
326 330 }
327 331 # tests with additional commits
328 332 cls_name = ''.join(('%s changesets with commits test' % alias).title().split())
329 333 bases = (ChangesetsWithCommitsTestCaseixin, unittest.TestCase)
330 334 globals()[cls_name] = type(cls_name, bases, attrs)
331 335
332 336 # tests without additional commits
333 337 cls_name = ''.join(('%s changesets test' % alias).title().split())
334 338 bases = (ChangesetsTestCaseMixin, unittest.TestCase)
335 339 globals()[cls_name] = type(cls_name, bases, attrs)
336 340
337 341 # tests changes
338 342 cls_name = ''.join(('%s changesets changes test' % alias).title().split())
339 343 bases = (ChangesetsChangesTestCaseMixin, unittest.TestCase)
340 344 globals()[cls_name] = type(cls_name, bases, attrs)
341 345
342 346
343 347 if __name__ == '__main__':
344 348 unittest.main()
General Comments 0
You need to be logged in to leave comments. Login now