##// END OF EJS Templates
api: wrap changeset file paths with safe_unicode...
domruf -
r6979:3725f86e default
parent child Browse files
Show More
@@ -1,1076 +1,1076 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.base
3 vcs.backends.base
4 ~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~
5
5
6 Base for all available scm backends
6 Base for all available scm backends
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import datetime
12 import datetime
13 import itertools
13 import itertools
14
14
15 from kallithea.lib.vcs.utils import author_name, author_email, safe_unicode
15 from kallithea.lib.vcs.utils import author_name, author_email, safe_unicode
16 from kallithea.lib.vcs.utils.lazy import LazyProperty
16 from kallithea.lib.vcs.utils.lazy import LazyProperty
17 from kallithea.lib.vcs.utils.helpers import get_dict_for_attrs
17 from kallithea.lib.vcs.utils.helpers import get_dict_for_attrs
18 from kallithea.lib.vcs.conf import settings
18 from kallithea.lib.vcs.conf import settings
19
19
20 from kallithea.lib.vcs.exceptions import (
20 from kallithea.lib.vcs.exceptions import (
21 ChangesetError, EmptyRepositoryError, NodeAlreadyAddedError,
21 ChangesetError, EmptyRepositoryError, NodeAlreadyAddedError,
22 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
22 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
23 NodeDoesNotExistError, NodeNotChangedError, RepositoryError
23 NodeDoesNotExistError, NodeNotChangedError, RepositoryError
24 )
24 )
25
25
26
26
27 class BaseRepository(object):
27 class BaseRepository(object):
28 """
28 """
29 Base Repository for final backends
29 Base Repository for final backends
30
30
31 **Attributes**
31 **Attributes**
32
32
33 ``DEFAULT_BRANCH_NAME``
33 ``DEFAULT_BRANCH_NAME``
34 name of default branch (i.e. "trunk" for svn, "master" for git etc.
34 name of default branch (i.e. "trunk" for svn, "master" for git etc.
35
35
36 ``scm``
36 ``scm``
37 alias of scm, i.e. *git* or *hg*
37 alias of scm, i.e. *git* or *hg*
38
38
39 ``repo``
39 ``repo``
40 object from external api
40 object from external api
41
41
42 ``revisions``
42 ``revisions``
43 list of all available revisions' ids, in ascending order
43 list of all available revisions' ids, in ascending order
44
44
45 ``changesets``
45 ``changesets``
46 storage dict caching returned changesets
46 storage dict caching returned changesets
47
47
48 ``path``
48 ``path``
49 absolute path to the repository
49 absolute path to the repository
50
50
51 ``branches``
51 ``branches``
52 branches as list of changesets
52 branches as list of changesets
53
53
54 ``tags``
54 ``tags``
55 tags as list of changesets
55 tags as list of changesets
56 """
56 """
57 scm = None
57 scm = None
58 DEFAULT_BRANCH_NAME = None
58 DEFAULT_BRANCH_NAME = None
59 EMPTY_CHANGESET = '0' * 40
59 EMPTY_CHANGESET = '0' * 40
60
60
61 def __init__(self, repo_path, create=False, **kwargs):
61 def __init__(self, repo_path, create=False, **kwargs):
62 """
62 """
63 Initializes repository. Raises RepositoryError if repository could
63 Initializes repository. Raises RepositoryError if repository could
64 not be find at the given ``repo_path`` or directory at ``repo_path``
64 not be find at the given ``repo_path`` or directory at ``repo_path``
65 exists and ``create`` is set to True.
65 exists and ``create`` is set to True.
66
66
67 :param repo_path: local path of the repository
67 :param repo_path: local path of the repository
68 :param create=False: if set to True, would try to create repository.
68 :param create=False: if set to True, would try to create repository.
69 :param src_url=None: if set, should be proper url from which repository
69 :param src_url=None: if set, should be proper url from which repository
70 would be cloned; requires ``create`` parameter to be set to True -
70 would be cloned; requires ``create`` parameter to be set to True -
71 raises RepositoryError if src_url is set and create evaluates to
71 raises RepositoryError if src_url is set and create evaluates to
72 False
72 False
73 """
73 """
74 raise NotImplementedError
74 raise NotImplementedError
75
75
76 def __str__(self):
76 def __str__(self):
77 return '<%s at %s>' % (self.__class__.__name__, self.path)
77 return '<%s at %s>' % (self.__class__.__name__, self.path)
78
78
79 def __repr__(self):
79 def __repr__(self):
80 return self.__str__()
80 return self.__str__()
81
81
82 def __len__(self):
82 def __len__(self):
83 return self.count()
83 return self.count()
84
84
85 def __eq__(self, other):
85 def __eq__(self, other):
86 same_instance = isinstance(other, self.__class__)
86 same_instance = isinstance(other, self.__class__)
87 return same_instance and getattr(other, 'path', None) == self.path
87 return same_instance and getattr(other, 'path', None) == self.path
88
88
89 def __ne__(self, other):
89 def __ne__(self, other):
90 return not self.__eq__(other)
90 return not self.__eq__(other)
91
91
92 @LazyProperty
92 @LazyProperty
93 def alias(self):
93 def alias(self):
94 for k, v in settings.BACKENDS.items():
94 for k, v in settings.BACKENDS.items():
95 if v.split('.')[-1] == str(self.__class__.__name__):
95 if v.split('.')[-1] == str(self.__class__.__name__):
96 return k
96 return k
97
97
98 @LazyProperty
98 @LazyProperty
99 def name(self):
99 def name(self):
100 """
100 """
101 Return repository name (without group name)
101 Return repository name (without group name)
102 """
102 """
103 raise NotImplementedError
103 raise NotImplementedError
104
104
105 @property
105 @property
106 def name_unicode(self):
106 def name_unicode(self):
107 return safe_unicode(self.name)
107 return safe_unicode(self.name)
108
108
109 @LazyProperty
109 @LazyProperty
110 def owner(self):
110 def owner(self):
111 raise NotImplementedError
111 raise NotImplementedError
112
112
113 @LazyProperty
113 @LazyProperty
114 def description(self):
114 def description(self):
115 raise NotImplementedError
115 raise NotImplementedError
116
116
117 @LazyProperty
117 @LazyProperty
118 def size(self):
118 def size(self):
119 """
119 """
120 Returns combined size in bytes for all repository files
120 Returns combined size in bytes for all repository files
121 """
121 """
122
122
123 size = 0
123 size = 0
124 try:
124 try:
125 tip = self.get_changeset()
125 tip = self.get_changeset()
126 for topnode, dirs, files in tip.walk('/'):
126 for topnode, dirs, files in tip.walk('/'):
127 for f in files:
127 for f in files:
128 size += tip.get_file_size(f.path)
128 size += tip.get_file_size(f.path)
129
129
130 except RepositoryError as e:
130 except RepositoryError as e:
131 pass
131 pass
132 return size
132 return size
133
133
134 def is_valid(self):
134 def is_valid(self):
135 """
135 """
136 Validates repository.
136 Validates repository.
137 """
137 """
138 raise NotImplementedError
138 raise NotImplementedError
139
139
140 def is_empty(self):
140 def is_empty(self):
141 return self._empty
141 return self._empty
142
142
143 #==========================================================================
143 #==========================================================================
144 # CHANGESETS
144 # CHANGESETS
145 #==========================================================================
145 #==========================================================================
146
146
147 def get_changeset(self, revision=None):
147 def get_changeset(self, revision=None):
148 """
148 """
149 Returns instance of ``Changeset`` class. If ``revision`` is None, most
149 Returns instance of ``Changeset`` class. If ``revision`` is None, most
150 recent changeset is returned.
150 recent changeset is returned.
151
151
152 :raises ``EmptyRepositoryError``: if there are no revisions
152 :raises ``EmptyRepositoryError``: if there are no revisions
153 """
153 """
154 raise NotImplementedError
154 raise NotImplementedError
155
155
156 def __iter__(self):
156 def __iter__(self):
157 """
157 """
158 Allows Repository objects to be iterated.
158 Allows Repository objects to be iterated.
159
159
160 *Requires* implementation of ``__getitem__`` method.
160 *Requires* implementation of ``__getitem__`` method.
161 """
161 """
162 for revision in self.revisions:
162 for revision in self.revisions:
163 yield self.get_changeset(revision)
163 yield self.get_changeset(revision)
164
164
165 def get_changesets(self, start=None, end=None, start_date=None,
165 def get_changesets(self, start=None, end=None, start_date=None,
166 end_date=None, branch_name=None, reverse=False):
166 end_date=None, branch_name=None, reverse=False):
167 """
167 """
168 Returns iterator of ``BaseChangeset`` objects from start to end,
168 Returns iterator of ``BaseChangeset`` objects from start to end,
169 both inclusive.
169 both inclusive.
170
170
171 :param start: None or str
171 :param start: None or str
172 :param end: None or str
172 :param end: None or str
173 :param start_date:
173 :param start_date:
174 :param end_date:
174 :param end_date:
175 :param branch_name:
175 :param branch_name:
176 :param reversed:
176 :param reversed:
177 """
177 """
178 raise NotImplementedError
178 raise NotImplementedError
179
179
180 def __getslice__(self, i, j):
180 def __getslice__(self, i, j):
181 """
181 """
182 Returns a iterator of sliced repository
182 Returns a iterator of sliced repository
183 """
183 """
184 for rev in self.revisions[i:j]:
184 for rev in self.revisions[i:j]:
185 yield self.get_changeset(rev)
185 yield self.get_changeset(rev)
186
186
187 def __getitem__(self, key):
187 def __getitem__(self, key):
188 return self.get_changeset(key)
188 return self.get_changeset(key)
189
189
190 def count(self):
190 def count(self):
191 return len(self.revisions)
191 return len(self.revisions)
192
192
193 def tag(self, name, user, revision=None, message=None, date=None, **opts):
193 def tag(self, name, user, revision=None, message=None, date=None, **opts):
194 """
194 """
195 Creates and returns a tag for the given ``revision``.
195 Creates and returns a tag for the given ``revision``.
196
196
197 :param name: name for new tag
197 :param name: name for new tag
198 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
198 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
199 :param revision: changeset id for which new tag would be created
199 :param revision: changeset id for which new tag would be created
200 :param message: message of the tag's commit
200 :param message: message of the tag's commit
201 :param date: date of tag's commit
201 :param date: date of tag's commit
202
202
203 :raises TagAlreadyExistError: if tag with same name already exists
203 :raises TagAlreadyExistError: if tag with same name already exists
204 """
204 """
205 raise NotImplementedError
205 raise NotImplementedError
206
206
207 def remove_tag(self, name, user, message=None, date=None):
207 def remove_tag(self, name, user, message=None, date=None):
208 """
208 """
209 Removes tag with the given ``name``.
209 Removes tag with the given ``name``.
210
210
211 :param name: name of the tag to be removed
211 :param name: name of the tag to be removed
212 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
212 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
213 :param message: message of the tag's removal commit
213 :param message: message of the tag's removal commit
214 :param date: date of tag's removal commit
214 :param date: date of tag's removal commit
215
215
216 :raises TagDoesNotExistError: if tag with given name does not exists
216 :raises TagDoesNotExistError: if tag with given name does not exists
217 """
217 """
218 raise NotImplementedError
218 raise NotImplementedError
219
219
220 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
220 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
221 context=3):
221 context=3):
222 """
222 """
223 Returns (git like) *diff*, as plain text. Shows changes introduced by
223 Returns (git like) *diff*, as plain text. Shows changes introduced by
224 ``rev2`` since ``rev1``.
224 ``rev2`` since ``rev1``.
225
225
226 :param rev1: Entry point from which diff is shown. Can be
226 :param rev1: Entry point from which diff is shown. Can be
227 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
227 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
228 the changes since empty state of the repository until ``rev2``
228 the changes since empty state of the repository until ``rev2``
229 :param rev2: Until which revision changes should be shown.
229 :param rev2: Until which revision changes should be shown.
230 :param ignore_whitespace: If set to ``True``, would not show whitespace
230 :param ignore_whitespace: If set to ``True``, would not show whitespace
231 changes. Defaults to ``False``.
231 changes. Defaults to ``False``.
232 :param context: How many lines before/after changed lines should be
232 :param context: How many lines before/after changed lines should be
233 shown. Defaults to ``3``.
233 shown. Defaults to ``3``.
234 """
234 """
235 raise NotImplementedError
235 raise NotImplementedError
236
236
237 # ========== #
237 # ========== #
238 # COMMIT API #
238 # COMMIT API #
239 # ========== #
239 # ========== #
240
240
241 @LazyProperty
241 @LazyProperty
242 def in_memory_changeset(self):
242 def in_memory_changeset(self):
243 """
243 """
244 Returns ``InMemoryChangeset`` object for this repository.
244 Returns ``InMemoryChangeset`` object for this repository.
245 """
245 """
246 raise NotImplementedError
246 raise NotImplementedError
247
247
248 def add(self, filenode, **kwargs):
248 def add(self, filenode, **kwargs):
249 """
249 """
250 Commit api function that will add given ``FileNode`` into this
250 Commit api function that will add given ``FileNode`` into this
251 repository.
251 repository.
252
252
253 :raises ``NodeAlreadyExistsError``: if there is a file with same path
253 :raises ``NodeAlreadyExistsError``: if there is a file with same path
254 already in repository
254 already in repository
255 :raises ``NodeAlreadyAddedError``: if given node is already marked as
255 :raises ``NodeAlreadyAddedError``: if given node is already marked as
256 *added*
256 *added*
257 """
257 """
258 raise NotImplementedError
258 raise NotImplementedError
259
259
260 def remove(self, filenode, **kwargs):
260 def remove(self, filenode, **kwargs):
261 """
261 """
262 Commit api function that will remove given ``FileNode`` into this
262 Commit api function that will remove given ``FileNode`` into this
263 repository.
263 repository.
264
264
265 :raises ``EmptyRepositoryError``: if there are no changesets yet
265 :raises ``EmptyRepositoryError``: if there are no changesets yet
266 :raises ``NodeDoesNotExistError``: if there is no file with given path
266 :raises ``NodeDoesNotExistError``: if there is no file with given path
267 """
267 """
268 raise NotImplementedError
268 raise NotImplementedError
269
269
270 def commit(self, message, **kwargs):
270 def commit(self, message, **kwargs):
271 """
271 """
272 Persists current changes made on this repository and returns newly
272 Persists current changes made on this repository and returns newly
273 created changeset.
273 created changeset.
274
274
275 :raises ``NothingChangedError``: if no changes has been made
275 :raises ``NothingChangedError``: if no changes has been made
276 """
276 """
277 raise NotImplementedError
277 raise NotImplementedError
278
278
279 def get_state(self):
279 def get_state(self):
280 """
280 """
281 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
281 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
282 containing ``FileNode`` objects.
282 containing ``FileNode`` objects.
283 """
283 """
284 raise NotImplementedError
284 raise NotImplementedError
285
285
286 def get_config_value(self, section, name, config_file=None):
286 def get_config_value(self, section, name, config_file=None):
287 """
287 """
288 Returns configuration value for a given [``section``] and ``name``.
288 Returns configuration value for a given [``section``] and ``name``.
289
289
290 :param section: Section we want to retrieve value from
290 :param section: Section we want to retrieve value from
291 :param name: Name of configuration we want to retrieve
291 :param name: Name of configuration we want to retrieve
292 :param config_file: A path to file which should be used to retrieve
292 :param config_file: A path to file which should be used to retrieve
293 configuration from (might also be a list of file paths)
293 configuration from (might also be a list of file paths)
294 """
294 """
295 raise NotImplementedError
295 raise NotImplementedError
296
296
297 def get_user_name(self, config_file=None):
297 def get_user_name(self, config_file=None):
298 """
298 """
299 Returns user's name from global configuration file.
299 Returns user's name from global configuration file.
300
300
301 :param config_file: A path to file which should be used to retrieve
301 :param config_file: A path to file which should be used to retrieve
302 configuration from (might also be a list of file paths)
302 configuration from (might also be a list of file paths)
303 """
303 """
304 raise NotImplementedError
304 raise NotImplementedError
305
305
306 def get_user_email(self, config_file=None):
306 def get_user_email(self, config_file=None):
307 """
307 """
308 Returns user's email from global configuration file.
308 Returns user's email from global configuration file.
309
309
310 :param config_file: A path to file which should be used to retrieve
310 :param config_file: A path to file which should be used to retrieve
311 configuration from (might also be a list of file paths)
311 configuration from (might also be a list of file paths)
312 """
312 """
313 raise NotImplementedError
313 raise NotImplementedError
314
314
315 # =========== #
315 # =========== #
316 # WORKDIR API #
316 # WORKDIR API #
317 # =========== #
317 # =========== #
318
318
319 @LazyProperty
319 @LazyProperty
320 def workdir(self):
320 def workdir(self):
321 """
321 """
322 Returns ``Workdir`` instance for this repository.
322 Returns ``Workdir`` instance for this repository.
323 """
323 """
324 raise NotImplementedError
324 raise NotImplementedError
325
325
326
326
327 class BaseChangeset(object):
327 class BaseChangeset(object):
328 """
328 """
329 Each backend should implement it's changeset representation.
329 Each backend should implement it's changeset representation.
330
330
331 **Attributes**
331 **Attributes**
332
332
333 ``repository``
333 ``repository``
334 repository object within which changeset exists
334 repository object within which changeset exists
335
335
336 ``id``
336 ``id``
337 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
337 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
338
338
339 ``raw_id``
339 ``raw_id``
340 raw changeset representation (i.e. full 40 length sha for git
340 raw changeset representation (i.e. full 40 length sha for git
341 backend)
341 backend)
342
342
343 ``short_id``
343 ``short_id``
344 shortened (if apply) version of ``raw_id``; it would be simple
344 shortened (if apply) version of ``raw_id``; it would be simple
345 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
345 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
346 as ``raw_id`` for subversion
346 as ``raw_id`` for subversion
347
347
348 ``revision``
348 ``revision``
349 revision number as integer
349 revision number as integer
350
350
351 ``files``
351 ``files``
352 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
352 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
353
353
354 ``dirs``
354 ``dirs``
355 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
355 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
356
356
357 ``nodes``
357 ``nodes``
358 combined list of ``Node`` objects
358 combined list of ``Node`` objects
359
359
360 ``author``
360 ``author``
361 author of the changeset, as unicode
361 author of the changeset, as unicode
362
362
363 ``message``
363 ``message``
364 message of the changeset, as unicode
364 message of the changeset, as unicode
365
365
366 ``parents``
366 ``parents``
367 list of parent changesets
367 list of parent changesets
368
368
369 ``last``
369 ``last``
370 ``True`` if this is last changeset in repository, ``False``
370 ``True`` if this is last changeset in repository, ``False``
371 otherwise; trying to access this attribute while there is no
371 otherwise; trying to access this attribute while there is no
372 changesets would raise ``EmptyRepositoryError``
372 changesets would raise ``EmptyRepositoryError``
373 """
373 """
374 def __str__(self):
374 def __str__(self):
375 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
375 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
376 self.short_id)
376 self.short_id)
377
377
378 def __repr__(self):
378 def __repr__(self):
379 return self.__str__()
379 return self.__str__()
380
380
381 def __unicode__(self):
381 def __unicode__(self):
382 return u'%s:%s' % (self.revision, self.short_id)
382 return u'%s:%s' % (self.revision, self.short_id)
383
383
384 def __eq__(self, other):
384 def __eq__(self, other):
385 return self.raw_id == other.raw_id
385 return self.raw_id == other.raw_id
386
386
387 def __json__(self, with_file_list=False):
387 def __json__(self, with_file_list=False):
388 if with_file_list:
388 if with_file_list:
389 return dict(
389 return dict(
390 short_id=self.short_id,
390 short_id=self.short_id,
391 raw_id=self.raw_id,
391 raw_id=self.raw_id,
392 revision=self.revision,
392 revision=self.revision,
393 message=self.message,
393 message=self.message,
394 date=self.date,
394 date=self.date,
395 author=self.author,
395 author=self.author,
396 added=[el.path for el in self.added],
396 added=[safe_unicode(el.path) for el in self.added],
397 changed=[el.path for el in self.changed],
397 changed=[safe_unicode(el.path) for el in self.changed],
398 removed=[el.path for el in self.removed],
398 removed=[safe_unicode(el.path) for el in self.removed],
399 )
399 )
400 else:
400 else:
401 return dict(
401 return dict(
402 short_id=self.short_id,
402 short_id=self.short_id,
403 raw_id=self.raw_id,
403 raw_id=self.raw_id,
404 revision=self.revision,
404 revision=self.revision,
405 message=self.message,
405 message=self.message,
406 date=self.date,
406 date=self.date,
407 author=self.author,
407 author=self.author,
408 )
408 )
409
409
410 @LazyProperty
410 @LazyProperty
411 def last(self):
411 def last(self):
412 if self.repository is None:
412 if self.repository is None:
413 raise ChangesetError("Cannot check if it's most recent revision")
413 raise ChangesetError("Cannot check if it's most recent revision")
414 return self.raw_id == self.repository.revisions[-1]
414 return self.raw_id == self.repository.revisions[-1]
415
415
416 @LazyProperty
416 @LazyProperty
417 def parents(self):
417 def parents(self):
418 """
418 """
419 Returns list of parents changesets.
419 Returns list of parents changesets.
420 """
420 """
421 raise NotImplementedError
421 raise NotImplementedError
422
422
423 @LazyProperty
423 @LazyProperty
424 def children(self):
424 def children(self):
425 """
425 """
426 Returns list of children changesets.
426 Returns list of children changesets.
427 """
427 """
428 raise NotImplementedError
428 raise NotImplementedError
429
429
430 @LazyProperty
430 @LazyProperty
431 def id(self):
431 def id(self):
432 """
432 """
433 Returns string identifying this changeset.
433 Returns string identifying this changeset.
434 """
434 """
435 raise NotImplementedError
435 raise NotImplementedError
436
436
437 @LazyProperty
437 @LazyProperty
438 def raw_id(self):
438 def raw_id(self):
439 """
439 """
440 Returns raw string identifying this changeset.
440 Returns raw string identifying this changeset.
441 """
441 """
442 raise NotImplementedError
442 raise NotImplementedError
443
443
444 @LazyProperty
444 @LazyProperty
445 def short_id(self):
445 def short_id(self):
446 """
446 """
447 Returns shortened version of ``raw_id`` attribute, as string,
447 Returns shortened version of ``raw_id`` attribute, as string,
448 identifying this changeset, useful for web representation.
448 identifying this changeset, useful for web representation.
449 """
449 """
450 raise NotImplementedError
450 raise NotImplementedError
451
451
452 @LazyProperty
452 @LazyProperty
453 def revision(self):
453 def revision(self):
454 """
454 """
455 Returns integer identifying this changeset.
455 Returns integer identifying this changeset.
456
456
457 """
457 """
458 raise NotImplementedError
458 raise NotImplementedError
459
459
460 @LazyProperty
460 @LazyProperty
461 def committer(self):
461 def committer(self):
462 """
462 """
463 Returns Committer for given commit
463 Returns Committer for given commit
464 """
464 """
465
465
466 raise NotImplementedError
466 raise NotImplementedError
467
467
468 @LazyProperty
468 @LazyProperty
469 def committer_name(self):
469 def committer_name(self):
470 """
470 """
471 Returns Author name for given commit
471 Returns Author name for given commit
472 """
472 """
473
473
474 return author_name(self.committer)
474 return author_name(self.committer)
475
475
476 @LazyProperty
476 @LazyProperty
477 def committer_email(self):
477 def committer_email(self):
478 """
478 """
479 Returns Author email address for given commit
479 Returns Author email address for given commit
480 """
480 """
481
481
482 return author_email(self.committer)
482 return author_email(self.committer)
483
483
484 @LazyProperty
484 @LazyProperty
485 def author(self):
485 def author(self):
486 """
486 """
487 Returns Author for given commit
487 Returns Author for given commit
488 """
488 """
489
489
490 raise NotImplementedError
490 raise NotImplementedError
491
491
492 @LazyProperty
492 @LazyProperty
493 def author_name(self):
493 def author_name(self):
494 """
494 """
495 Returns Author name for given commit
495 Returns Author name for given commit
496 """
496 """
497
497
498 return author_name(self.author)
498 return author_name(self.author)
499
499
500 @LazyProperty
500 @LazyProperty
501 def author_email(self):
501 def author_email(self):
502 """
502 """
503 Returns Author email address for given commit
503 Returns Author email address for given commit
504 """
504 """
505
505
506 return author_email(self.author)
506 return author_email(self.author)
507
507
508 def get_file_mode(self, path):
508 def get_file_mode(self, path):
509 """
509 """
510 Returns stat mode of the file at the given ``path``.
510 Returns stat mode of the file at the given ``path``.
511 """
511 """
512 raise NotImplementedError
512 raise NotImplementedError
513
513
514 def get_file_content(self, path):
514 def get_file_content(self, path):
515 """
515 """
516 Returns content of the file at the given ``path``.
516 Returns content of the file at the given ``path``.
517 """
517 """
518 raise NotImplementedError
518 raise NotImplementedError
519
519
520 def get_file_size(self, path):
520 def get_file_size(self, path):
521 """
521 """
522 Returns size of the file at the given ``path``.
522 Returns size of the file at the given ``path``.
523 """
523 """
524 raise NotImplementedError
524 raise NotImplementedError
525
525
526 def get_file_changeset(self, path):
526 def get_file_changeset(self, path):
527 """
527 """
528 Returns last commit of the file at the given ``path``.
528 Returns last commit of the file at the given ``path``.
529 """
529 """
530 raise NotImplementedError
530 raise NotImplementedError
531
531
532 def get_file_history(self, path):
532 def get_file_history(self, path):
533 """
533 """
534 Returns history of file as reversed list of ``Changeset`` objects for
534 Returns history of file as reversed list of ``Changeset`` objects for
535 which file at given ``path`` has been modified.
535 which file at given ``path`` has been modified.
536 """
536 """
537 raise NotImplementedError
537 raise NotImplementedError
538
538
539 def get_nodes(self, path):
539 def get_nodes(self, path):
540 """
540 """
541 Returns combined ``DirNode`` and ``FileNode`` objects list representing
541 Returns combined ``DirNode`` and ``FileNode`` objects list representing
542 state of changeset at the given ``path``.
542 state of changeset at the given ``path``.
543
543
544 :raises ``ChangesetError``: if node at the given ``path`` is not
544 :raises ``ChangesetError``: if node at the given ``path`` is not
545 instance of ``DirNode``
545 instance of ``DirNode``
546 """
546 """
547 raise NotImplementedError
547 raise NotImplementedError
548
548
549 def get_node(self, path):
549 def get_node(self, path):
550 """
550 """
551 Returns ``Node`` object from the given ``path``.
551 Returns ``Node`` object from the given ``path``.
552
552
553 :raises ``NodeDoesNotExistError``: if there is no node at the given
553 :raises ``NodeDoesNotExistError``: if there is no node at the given
554 ``path``
554 ``path``
555 """
555 """
556 raise NotImplementedError
556 raise NotImplementedError
557
557
558 def fill_archive(self, stream=None, kind='tgz', prefix=None):
558 def fill_archive(self, stream=None, kind='tgz', prefix=None):
559 """
559 """
560 Fills up given stream.
560 Fills up given stream.
561
561
562 :param stream: file like object.
562 :param stream: file like object.
563 :param kind: one of following: ``zip``, ``tar``, ``tgz``
563 :param kind: one of following: ``zip``, ``tar``, ``tgz``
564 or ``tbz2``. Default: ``tgz``.
564 or ``tbz2``. Default: ``tgz``.
565 :param prefix: name of root directory in archive.
565 :param prefix: name of root directory in archive.
566 Default is repository name and changeset's raw_id joined with dash.
566 Default is repository name and changeset's raw_id joined with dash.
567
567
568 repo-tip.<kind>
568 repo-tip.<kind>
569 """
569 """
570
570
571 raise NotImplementedError
571 raise NotImplementedError
572
572
573 def get_chunked_archive(self, **kwargs):
573 def get_chunked_archive(self, **kwargs):
574 """
574 """
575 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
575 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
576
576
577 :param chunk_size: extra parameter which controls size of returned
577 :param chunk_size: extra parameter which controls size of returned
578 chunks. Default:8k.
578 chunks. Default:8k.
579 """
579 """
580
580
581 chunk_size = kwargs.pop('chunk_size', 8192)
581 chunk_size = kwargs.pop('chunk_size', 8192)
582 stream = kwargs.get('stream')
582 stream = kwargs.get('stream')
583 self.fill_archive(**kwargs)
583 self.fill_archive(**kwargs)
584 while True:
584 while True:
585 data = stream.read(chunk_size)
585 data = stream.read(chunk_size)
586 if not data:
586 if not data:
587 break
587 break
588 yield data
588 yield data
589
589
590 @LazyProperty
590 @LazyProperty
591 def root(self):
591 def root(self):
592 """
592 """
593 Returns ``RootNode`` object for this changeset.
593 Returns ``RootNode`` object for this changeset.
594 """
594 """
595 return self.get_node('')
595 return self.get_node('')
596
596
597 def next(self, branch=None):
597 def next(self, branch=None):
598 """
598 """
599 Returns next changeset from current, if branch is gives it will return
599 Returns next changeset from current, if branch is gives it will return
600 next changeset belonging to this branch
600 next changeset belonging to this branch
601
601
602 :param branch: show changesets within the given named branch
602 :param branch: show changesets within the given named branch
603 """
603 """
604 raise NotImplementedError
604 raise NotImplementedError
605
605
606 def prev(self, branch=None):
606 def prev(self, branch=None):
607 """
607 """
608 Returns previous changeset from current, if branch is gives it will
608 Returns previous changeset from current, if branch is gives it will
609 return previous changeset belonging to this branch
609 return previous changeset belonging to this branch
610
610
611 :param branch: show changesets within the given named branch
611 :param branch: show changesets within the given named branch
612 """
612 """
613 raise NotImplementedError
613 raise NotImplementedError
614
614
615 @LazyProperty
615 @LazyProperty
616 def added(self):
616 def added(self):
617 """
617 """
618 Returns list of added ``FileNode`` objects.
618 Returns list of added ``FileNode`` objects.
619 """
619 """
620 raise NotImplementedError
620 raise NotImplementedError
621
621
622 @LazyProperty
622 @LazyProperty
623 def changed(self):
623 def changed(self):
624 """
624 """
625 Returns list of modified ``FileNode`` objects.
625 Returns list of modified ``FileNode`` objects.
626 """
626 """
627 raise NotImplementedError
627 raise NotImplementedError
628
628
629 @LazyProperty
629 @LazyProperty
630 def removed(self):
630 def removed(self):
631 """
631 """
632 Returns list of removed ``FileNode`` objects.
632 Returns list of removed ``FileNode`` objects.
633 """
633 """
634 raise NotImplementedError
634 raise NotImplementedError
635
635
636 @LazyProperty
636 @LazyProperty
637 def size(self):
637 def size(self):
638 """
638 """
639 Returns total number of bytes from contents of all filenodes.
639 Returns total number of bytes from contents of all filenodes.
640 """
640 """
641 return sum((node.size for node in self.get_filenodes_generator()))
641 return sum((node.size for node in self.get_filenodes_generator()))
642
642
643 def walk(self, topurl=''):
643 def walk(self, topurl=''):
644 """
644 """
645 Similar to os.walk method. Instead of filesystem it walks through
645 Similar to os.walk method. Instead of filesystem it walks through
646 changeset starting at given ``topurl``. Returns generator of tuples
646 changeset starting at given ``topurl``. Returns generator of tuples
647 (topnode, dirnodes, filenodes).
647 (topnode, dirnodes, filenodes).
648 """
648 """
649 topnode = self.get_node(topurl)
649 topnode = self.get_node(topurl)
650 yield (topnode, topnode.dirs, topnode.files)
650 yield (topnode, topnode.dirs, topnode.files)
651 for dirnode in topnode.dirs:
651 for dirnode in topnode.dirs:
652 for tup in self.walk(dirnode.path):
652 for tup in self.walk(dirnode.path):
653 yield tup
653 yield tup
654
654
655 def get_filenodes_generator(self):
655 def get_filenodes_generator(self):
656 """
656 """
657 Returns generator that yields *all* file nodes.
657 Returns generator that yields *all* file nodes.
658 """
658 """
659 for topnode, dirs, files in self.walk():
659 for topnode, dirs, files in self.walk():
660 for node in files:
660 for node in files:
661 yield node
661 yield node
662
662
663 def as_dict(self):
663 def as_dict(self):
664 """
664 """
665 Returns dictionary with changeset's attributes and their values.
665 Returns dictionary with changeset's attributes and their values.
666 """
666 """
667 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
667 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
668 'revision', 'date', 'message'])
668 'revision', 'date', 'message'])
669 data['author'] = {'name': self.author_name, 'email': self.author_email}
669 data['author'] = {'name': self.author_name, 'email': self.author_email}
670 data['added'] = [node.path for node in self.added]
670 data['added'] = [safe_unicode(node.path) for node in self.added]
671 data['changed'] = [node.path for node in self.changed]
671 data['changed'] = [safe_unicode(node.path) for node in self.changed]
672 data['removed'] = [node.path for node in self.removed]
672 data['removed'] = [safe_unicode(node.path) for node in self.removed]
673 return data
673 return data
674
674
675 @LazyProperty
675 @LazyProperty
676 def closesbranch(self):
676 def closesbranch(self):
677 return False
677 return False
678
678
679 @LazyProperty
679 @LazyProperty
680 def obsolete(self):
680 def obsolete(self):
681 return False
681 return False
682
682
683 @LazyProperty
683 @LazyProperty
684 def bumped(self):
684 def bumped(self):
685 return False
685 return False
686
686
687 @LazyProperty
687 @LazyProperty
688 def divergent(self):
688 def divergent(self):
689 return False
689 return False
690
690
691 @LazyProperty
691 @LazyProperty
692 def extinct(self):
692 def extinct(self):
693 return False
693 return False
694
694
695 @LazyProperty
695 @LazyProperty
696 def unstable(self):
696 def unstable(self):
697 return False
697 return False
698
698
699 @LazyProperty
699 @LazyProperty
700 def phase(self):
700 def phase(self):
701 return ''
701 return ''
702
702
703
703
704 class BaseWorkdir(object):
704 class BaseWorkdir(object):
705 """
705 """
706 Working directory representation of single repository.
706 Working directory representation of single repository.
707
707
708 :attribute: repository: repository object of working directory
708 :attribute: repository: repository object of working directory
709 """
709 """
710
710
711 def __init__(self, repository):
711 def __init__(self, repository):
712 self.repository = repository
712 self.repository = repository
713
713
714 def get_branch(self):
714 def get_branch(self):
715 """
715 """
716 Returns name of current branch.
716 Returns name of current branch.
717 """
717 """
718 raise NotImplementedError
718 raise NotImplementedError
719
719
720 def get_changeset(self):
720 def get_changeset(self):
721 """
721 """
722 Returns current changeset.
722 Returns current changeset.
723 """
723 """
724 raise NotImplementedError
724 raise NotImplementedError
725
725
726 def get_added(self):
726 def get_added(self):
727 """
727 """
728 Returns list of ``FileNode`` objects marked as *new* in working
728 Returns list of ``FileNode`` objects marked as *new* in working
729 directory.
729 directory.
730 """
730 """
731 raise NotImplementedError
731 raise NotImplementedError
732
732
733 def get_changed(self):
733 def get_changed(self):
734 """
734 """
735 Returns list of ``FileNode`` objects *changed* in working directory.
735 Returns list of ``FileNode`` objects *changed* in working directory.
736 """
736 """
737 raise NotImplementedError
737 raise NotImplementedError
738
738
739 def get_removed(self):
739 def get_removed(self):
740 """
740 """
741 Returns list of ``RemovedFileNode`` objects marked as *removed* in
741 Returns list of ``RemovedFileNode`` objects marked as *removed* in
742 working directory.
742 working directory.
743 """
743 """
744 raise NotImplementedError
744 raise NotImplementedError
745
745
746 def get_untracked(self):
746 def get_untracked(self):
747 """
747 """
748 Returns list of ``FileNode`` objects which are present within working
748 Returns list of ``FileNode`` objects which are present within working
749 directory however are not tracked by repository.
749 directory however are not tracked by repository.
750 """
750 """
751 raise NotImplementedError
751 raise NotImplementedError
752
752
753 def get_status(self):
753 def get_status(self):
754 """
754 """
755 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
755 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
756 lists.
756 lists.
757 """
757 """
758 raise NotImplementedError
758 raise NotImplementedError
759
759
760 def commit(self, message, **kwargs):
760 def commit(self, message, **kwargs):
761 """
761 """
762 Commits local (from working directory) changes and returns newly
762 Commits local (from working directory) changes and returns newly
763 created
763 created
764 ``Changeset``. Updates repository's ``revisions`` list.
764 ``Changeset``. Updates repository's ``revisions`` list.
765
765
766 :raises ``CommitError``: if any error occurs while committing
766 :raises ``CommitError``: if any error occurs while committing
767 """
767 """
768 raise NotImplementedError
768 raise NotImplementedError
769
769
770 def update(self, revision=None):
770 def update(self, revision=None):
771 """
771 """
772 Fetches content of the given revision and populates it within working
772 Fetches content of the given revision and populates it within working
773 directory.
773 directory.
774 """
774 """
775 raise NotImplementedError
775 raise NotImplementedError
776
776
777 def checkout_branch(self, branch=None):
777 def checkout_branch(self, branch=None):
778 """
778 """
779 Checks out ``branch`` or the backend's default branch.
779 Checks out ``branch`` or the backend's default branch.
780
780
781 Raises ``BranchDoesNotExistError`` if the branch does not exist.
781 Raises ``BranchDoesNotExistError`` if the branch does not exist.
782 """
782 """
783 raise NotImplementedError
783 raise NotImplementedError
784
784
785
785
786 class BaseInMemoryChangeset(object):
786 class BaseInMemoryChangeset(object):
787 """
787 """
788 Represents differences between repository's state (most recent head) and
788 Represents differences between repository's state (most recent head) and
789 changes made *in place*.
789 changes made *in place*.
790
790
791 **Attributes**
791 **Attributes**
792
792
793 ``repository``
793 ``repository``
794 repository object for this in-memory-changeset
794 repository object for this in-memory-changeset
795
795
796 ``added``
796 ``added``
797 list of ``FileNode`` objects marked as *added*
797 list of ``FileNode`` objects marked as *added*
798
798
799 ``changed``
799 ``changed``
800 list of ``FileNode`` objects marked as *changed*
800 list of ``FileNode`` objects marked as *changed*
801
801
802 ``removed``
802 ``removed``
803 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
803 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
804 *removed*
804 *removed*
805
805
806 ``parents``
806 ``parents``
807 list of ``Changeset`` representing parents of in-memory changeset.
807 list of ``Changeset`` representing parents of in-memory changeset.
808 Should always be 2-element sequence.
808 Should always be 2-element sequence.
809
809
810 """
810 """
811
811
812 def __init__(self, repository):
812 def __init__(self, repository):
813 self.repository = repository
813 self.repository = repository
814 self.added = []
814 self.added = []
815 self.changed = []
815 self.changed = []
816 self.removed = []
816 self.removed = []
817 self.parents = []
817 self.parents = []
818
818
819 def add(self, *filenodes):
819 def add(self, *filenodes):
820 """
820 """
821 Marks given ``FileNode`` objects as *to be committed*.
821 Marks given ``FileNode`` objects as *to be committed*.
822
822
823 :raises ``NodeAlreadyExistsError``: if node with same path exists at
823 :raises ``NodeAlreadyExistsError``: if node with same path exists at
824 latest changeset
824 latest changeset
825 :raises ``NodeAlreadyAddedError``: if node with same path is already
825 :raises ``NodeAlreadyAddedError``: if node with same path is already
826 marked as *added*
826 marked as *added*
827 """
827 """
828 # Check if not already marked as *added* first
828 # Check if not already marked as *added* first
829 for node in filenodes:
829 for node in filenodes:
830 if node.path in (n.path for n in self.added):
830 if node.path in (n.path for n in self.added):
831 raise NodeAlreadyAddedError("Such FileNode %s is already "
831 raise NodeAlreadyAddedError("Such FileNode %s is already "
832 "marked for addition" % node.path)
832 "marked for addition" % node.path)
833 for node in filenodes:
833 for node in filenodes:
834 self.added.append(node)
834 self.added.append(node)
835
835
836 def change(self, *filenodes):
836 def change(self, *filenodes):
837 """
837 """
838 Marks given ``FileNode`` objects to be *changed* in next commit.
838 Marks given ``FileNode`` objects to be *changed* in next commit.
839
839
840 :raises ``EmptyRepositoryError``: if there are no changesets yet
840 :raises ``EmptyRepositoryError``: if there are no changesets yet
841 :raises ``NodeAlreadyExistsError``: if node with same path is already
841 :raises ``NodeAlreadyExistsError``: if node with same path is already
842 marked to be *changed*
842 marked to be *changed*
843 :raises ``NodeAlreadyRemovedError``: if node with same path is already
843 :raises ``NodeAlreadyRemovedError``: if node with same path is already
844 marked to be *removed*
844 marked to be *removed*
845 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
845 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
846 changeset
846 changeset
847 :raises ``NodeNotChangedError``: if node hasn't really be changed
847 :raises ``NodeNotChangedError``: if node hasn't really be changed
848 """
848 """
849 for node in filenodes:
849 for node in filenodes:
850 if node.path in (n.path for n in self.removed):
850 if node.path in (n.path for n in self.removed):
851 raise NodeAlreadyRemovedError("Node at %s is already marked "
851 raise NodeAlreadyRemovedError("Node at %s is already marked "
852 "as removed" % node.path)
852 "as removed" % node.path)
853 try:
853 try:
854 self.repository.get_changeset()
854 self.repository.get_changeset()
855 except EmptyRepositoryError:
855 except EmptyRepositoryError:
856 raise EmptyRepositoryError("Nothing to change - try to *add* new "
856 raise EmptyRepositoryError("Nothing to change - try to *add* new "
857 "nodes rather than changing them")
857 "nodes rather than changing them")
858 for node in filenodes:
858 for node in filenodes:
859 if node.path in (n.path for n in self.changed):
859 if node.path in (n.path for n in self.changed):
860 raise NodeAlreadyChangedError("Node at '%s' is already "
860 raise NodeAlreadyChangedError("Node at '%s' is already "
861 "marked as changed" % node.path)
861 "marked as changed" % node.path)
862 self.changed.append(node)
862 self.changed.append(node)
863
863
864 def remove(self, *filenodes):
864 def remove(self, *filenodes):
865 """
865 """
866 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
866 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
867 *removed* in next commit.
867 *removed* in next commit.
868
868
869 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
869 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
870 be *removed*
870 be *removed*
871 :raises ``NodeAlreadyChangedError``: if node has been already marked to
871 :raises ``NodeAlreadyChangedError``: if node has been already marked to
872 be *changed*
872 be *changed*
873 """
873 """
874 for node in filenodes:
874 for node in filenodes:
875 if node.path in (n.path for n in self.removed):
875 if node.path in (n.path for n in self.removed):
876 raise NodeAlreadyRemovedError("Node is already marked to "
876 raise NodeAlreadyRemovedError("Node is already marked to "
877 "for removal at %s" % node.path)
877 "for removal at %s" % node.path)
878 if node.path in (n.path for n in self.changed):
878 if node.path in (n.path for n in self.changed):
879 raise NodeAlreadyChangedError("Node is already marked to "
879 raise NodeAlreadyChangedError("Node is already marked to "
880 "be changed at %s" % node.path)
880 "be changed at %s" % node.path)
881 # We only mark node as *removed* - real removal is done by
881 # We only mark node as *removed* - real removal is done by
882 # commit method
882 # commit method
883 self.removed.append(node)
883 self.removed.append(node)
884
884
885 def reset(self):
885 def reset(self):
886 """
886 """
887 Resets this instance to initial state (cleans ``added``, ``changed``
887 Resets this instance to initial state (cleans ``added``, ``changed``
888 and ``removed`` lists).
888 and ``removed`` lists).
889 """
889 """
890 self.added = []
890 self.added = []
891 self.changed = []
891 self.changed = []
892 self.removed = []
892 self.removed = []
893 self.parents = []
893 self.parents = []
894
894
895 def get_ipaths(self):
895 def get_ipaths(self):
896 """
896 """
897 Returns generator of paths from nodes marked as added, changed or
897 Returns generator of paths from nodes marked as added, changed or
898 removed.
898 removed.
899 """
899 """
900 for node in itertools.chain(self.added, self.changed, self.removed):
900 for node in itertools.chain(self.added, self.changed, self.removed):
901 yield node.path
901 yield node.path
902
902
903 def get_paths(self):
903 def get_paths(self):
904 """
904 """
905 Returns list of paths from nodes marked as added, changed or removed.
905 Returns list of paths from nodes marked as added, changed or removed.
906 """
906 """
907 return list(self.get_ipaths())
907 return list(self.get_ipaths())
908
908
909 def check_integrity(self, parents=None):
909 def check_integrity(self, parents=None):
910 """
910 """
911 Checks in-memory changeset's integrity. Also, sets parents if not
911 Checks in-memory changeset's integrity. Also, sets parents if not
912 already set.
912 already set.
913
913
914 :raises CommitError: if any error occurs (i.e.
914 :raises CommitError: if any error occurs (i.e.
915 ``NodeDoesNotExistError``).
915 ``NodeDoesNotExistError``).
916 """
916 """
917 if not self.parents:
917 if not self.parents:
918 parents = parents or []
918 parents = parents or []
919 if len(parents) == 0:
919 if len(parents) == 0:
920 try:
920 try:
921 parents = [self.repository.get_changeset(), None]
921 parents = [self.repository.get_changeset(), None]
922 except EmptyRepositoryError:
922 except EmptyRepositoryError:
923 parents = [None, None]
923 parents = [None, None]
924 elif len(parents) == 1:
924 elif len(parents) == 1:
925 parents += [None]
925 parents += [None]
926 self.parents = parents
926 self.parents = parents
927
927
928 # Local parents, only if not None
928 # Local parents, only if not None
929 parents = [p for p in self.parents if p]
929 parents = [p for p in self.parents if p]
930
930
931 # Check nodes marked as added
931 # Check nodes marked as added
932 for p in parents:
932 for p in parents:
933 for node in self.added:
933 for node in self.added:
934 try:
934 try:
935 p.get_node(node.path)
935 p.get_node(node.path)
936 except NodeDoesNotExistError:
936 except NodeDoesNotExistError:
937 pass
937 pass
938 else:
938 else:
939 raise NodeAlreadyExistsError("Node at %s already exists "
939 raise NodeAlreadyExistsError("Node at %s already exists "
940 "at %s" % (node.path, p))
940 "at %s" % (node.path, p))
941
941
942 # Check nodes marked as changed
942 # Check nodes marked as changed
943 missing = set(self.changed)
943 missing = set(self.changed)
944 not_changed = set(self.changed)
944 not_changed = set(self.changed)
945 if self.changed and not parents:
945 if self.changed and not parents:
946 raise NodeDoesNotExistError(str(self.changed[0].path))
946 raise NodeDoesNotExistError(str(self.changed[0].path))
947 for p in parents:
947 for p in parents:
948 for node in self.changed:
948 for node in self.changed:
949 try:
949 try:
950 old = p.get_node(node.path)
950 old = p.get_node(node.path)
951 missing.remove(node)
951 missing.remove(node)
952 # if content actually changed, remove node from unchanged
952 # if content actually changed, remove node from unchanged
953 if old.content != node.content:
953 if old.content != node.content:
954 not_changed.remove(node)
954 not_changed.remove(node)
955 except NodeDoesNotExistError:
955 except NodeDoesNotExistError:
956 pass
956 pass
957 if self.changed and missing:
957 if self.changed and missing:
958 raise NodeDoesNotExistError("Node at %s is missing "
958 raise NodeDoesNotExistError("Node at %s is missing "
959 "(parents: %s)" % (node.path, parents))
959 "(parents: %s)" % (node.path, parents))
960
960
961 if self.changed and not_changed:
961 if self.changed and not_changed:
962 raise NodeNotChangedError("Node at %s wasn't actually changed "
962 raise NodeNotChangedError("Node at %s wasn't actually changed "
963 "since parents' changesets: %s" % (not_changed.pop().path,
963 "since parents' changesets: %s" % (not_changed.pop().path,
964 parents)
964 parents)
965 )
965 )
966
966
967 # Check nodes marked as removed
967 # Check nodes marked as removed
968 if self.removed and not parents:
968 if self.removed and not parents:
969 raise NodeDoesNotExistError("Cannot remove node at %s as there "
969 raise NodeDoesNotExistError("Cannot remove node at %s as there "
970 "were no parents specified" % self.removed[0].path)
970 "were no parents specified" % self.removed[0].path)
971 really_removed = set()
971 really_removed = set()
972 for p in parents:
972 for p in parents:
973 for node in self.removed:
973 for node in self.removed:
974 try:
974 try:
975 p.get_node(node.path)
975 p.get_node(node.path)
976 really_removed.add(node)
976 really_removed.add(node)
977 except ChangesetError:
977 except ChangesetError:
978 pass
978 pass
979 not_removed = set(self.removed) - really_removed
979 not_removed = set(self.removed) - really_removed
980 if not_removed:
980 if not_removed:
981 raise NodeDoesNotExistError("Cannot remove node at %s from "
981 raise NodeDoesNotExistError("Cannot remove node at %s from "
982 "following parents: %s" % (not_removed[0], parents))
982 "following parents: %s" % (not_removed[0], parents))
983
983
984 def commit(self, message, author, parents=None, branch=None, date=None,
984 def commit(self, message, author, parents=None, branch=None, date=None,
985 **kwargs):
985 **kwargs):
986 """
986 """
987 Performs in-memory commit (doesn't check workdir in any way) and
987 Performs in-memory commit (doesn't check workdir in any way) and
988 returns newly created ``Changeset``. Updates repository's
988 returns newly created ``Changeset``. Updates repository's
989 ``revisions``.
989 ``revisions``.
990
990
991 .. note::
991 .. note::
992 While overriding this method each backend's should call
992 While overriding this method each backend's should call
993 ``self.check_integrity(parents)`` in the first place.
993 ``self.check_integrity(parents)`` in the first place.
994
994
995 :param message: message of the commit
995 :param message: message of the commit
996 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
996 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
997 :param parents: single parent or sequence of parents from which commit
997 :param parents: single parent or sequence of parents from which commit
998 would be derived
998 would be derived
999 :param date: ``datetime.datetime`` instance. Defaults to
999 :param date: ``datetime.datetime`` instance. Defaults to
1000 ``datetime.datetime.now()``.
1000 ``datetime.datetime.now()``.
1001 :param branch: branch name, as string. If none given, default backend's
1001 :param branch: branch name, as string. If none given, default backend's
1002 branch would be used.
1002 branch would be used.
1003
1003
1004 :raises ``CommitError``: if any error occurs while committing
1004 :raises ``CommitError``: if any error occurs while committing
1005 """
1005 """
1006 raise NotImplementedError
1006 raise NotImplementedError
1007
1007
1008
1008
1009 class EmptyChangeset(BaseChangeset):
1009 class EmptyChangeset(BaseChangeset):
1010 """
1010 """
1011 An dummy empty changeset. It's possible to pass hash when creating
1011 An dummy empty changeset. It's possible to pass hash when creating
1012 an EmptyChangeset
1012 an EmptyChangeset
1013 """
1013 """
1014
1014
1015 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1015 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1016 alias=None, revision=-1, message='', author='', date=None):
1016 alias=None, revision=-1, message='', author='', date=None):
1017 self._empty_cs = cs
1017 self._empty_cs = cs
1018 self.revision = revision
1018 self.revision = revision
1019 self.message = message
1019 self.message = message
1020 self.author = author
1020 self.author = author
1021 self.date = date or datetime.datetime.fromtimestamp(0)
1021 self.date = date or datetime.datetime.fromtimestamp(0)
1022 self.repository = repo
1022 self.repository = repo
1023 self.requested_revision = requested_revision
1023 self.requested_revision = requested_revision
1024 self.alias = alias
1024 self.alias = alias
1025
1025
1026 @LazyProperty
1026 @LazyProperty
1027 def raw_id(self):
1027 def raw_id(self):
1028 """
1028 """
1029 Returns raw string identifying this changeset, useful for web
1029 Returns raw string identifying this changeset, useful for web
1030 representation.
1030 representation.
1031 """
1031 """
1032
1032
1033 return self._empty_cs
1033 return self._empty_cs
1034
1034
1035 @LazyProperty
1035 @LazyProperty
1036 def branch(self):
1036 def branch(self):
1037 from kallithea.lib.vcs.backends import get_backend
1037 from kallithea.lib.vcs.backends import get_backend
1038 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1038 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1039
1039
1040 @LazyProperty
1040 @LazyProperty
1041 def short_id(self):
1041 def short_id(self):
1042 return self.raw_id[:12]
1042 return self.raw_id[:12]
1043
1043
1044 def get_file_changeset(self, path):
1044 def get_file_changeset(self, path):
1045 return self
1045 return self
1046
1046
1047 def get_file_content(self, path):
1047 def get_file_content(self, path):
1048 return u''
1048 return u''
1049
1049
1050 def get_file_size(self, path):
1050 def get_file_size(self, path):
1051 return 0
1051 return 0
1052
1052
1053
1053
1054 class CollectionGenerator(object):
1054 class CollectionGenerator(object):
1055
1055
1056 def __init__(self, repo, revs):
1056 def __init__(self, repo, revs):
1057 self.repo = repo
1057 self.repo = repo
1058 self.revs = revs
1058 self.revs = revs
1059
1059
1060 def __len__(self):
1060 def __len__(self):
1061 return len(self.revs)
1061 return len(self.revs)
1062
1062
1063 def __iter__(self):
1063 def __iter__(self):
1064 for rev in self.revs:
1064 for rev in self.revs:
1065 yield self.repo.get_changeset(rev)
1065 yield self.repo.get_changeset(rev)
1066
1066
1067 def __getitem__(self, what):
1067 def __getitem__(self, what):
1068 """Return either a single element by index, or a sliced collection."""
1068 """Return either a single element by index, or a sliced collection."""
1069 if isinstance(what, slice):
1069 if isinstance(what, slice):
1070 return CollectionGenerator(self.repo, self.revs[what])
1070 return CollectionGenerator(self.repo, self.revs[what])
1071 else:
1071 else:
1072 # single item
1072 # single item
1073 return self.repo.get_changeset(self.revs[what])
1073 return self.repo.get_changeset(self.revs[what])
1074
1074
1075 def __repr__(self):
1075 def __repr__(self):
1076 return '<CollectionGenerator[len:%s]>' % (len(self))
1076 return '<CollectionGenerator[len:%s]>' % (len(self))
@@ -1,392 +1,394 b''
1 # encoding: utf8
1 # encoding: utf8
2
2
3 import time
3 import time
4 import datetime
4 import datetime
5 from kallithea.lib import vcs
5 from kallithea.lib import vcs
6 from kallithea.tests.vcs.base import _BackendTestMixin
6 from kallithea.tests.vcs.base import _BackendTestMixin
7 from kallithea.tests.vcs.conf import SCM_TESTS
7 from kallithea.tests.vcs.conf import SCM_TESTS
8
8
9 from kallithea.lib.vcs.backends.base import BaseChangeset
9 from kallithea.lib.vcs.backends.base import BaseChangeset
10 from kallithea.lib.vcs.nodes import (
10 from kallithea.lib.vcs.nodes import (
11 FileNode, AddedFileNodesGenerator,
11 FileNode, AddedFileNodesGenerator,
12 ChangedFileNodesGenerator, RemovedFileNodesGenerator
12 ChangedFileNodesGenerator, RemovedFileNodesGenerator
13 )
13 )
14 from kallithea.lib.vcs.exceptions import (
14 from kallithea.lib.vcs.exceptions import (
15 BranchDoesNotExistError, ChangesetDoesNotExistError,
15 BranchDoesNotExistError, ChangesetDoesNotExistError,
16 RepositoryError, EmptyRepositoryError
16 RepositoryError, EmptyRepositoryError
17 )
17 )
18 from kallithea.lib.vcs.utils.compat import unittest
18 from kallithea.lib.vcs.utils.compat import unittest
19 from kallithea.tests.vcs.conf import get_new_dir
19 from kallithea.tests.vcs.conf import get_new_dir
20
20
21
21
22 class TestBaseChangeset(unittest.TestCase):
22 class TestBaseChangeset(unittest.TestCase):
23
23
24 def test_as_dict(self):
24 def test_as_dict(self):
25 changeset = BaseChangeset()
25 changeset = BaseChangeset()
26 changeset.id = 'ID'
26 changeset.id = 'ID'
27 changeset.raw_id = 'RAW_ID'
27 changeset.raw_id = 'RAW_ID'
28 changeset.short_id = 'SHORT_ID'
28 changeset.short_id = 'SHORT_ID'
29 changeset.revision = 1009
29 changeset.revision = 1009
30 changeset.date = datetime.datetime(2011, 1, 30, 1, 45)
30 changeset.date = datetime.datetime(2011, 1, 30, 1, 45)
31 changeset.message = 'Message of a commit'
31 changeset.message = 'Message of a commit'
32 changeset.author = 'Joe Doe <joe.doe@example.com>'
32 changeset.author = 'Joe Doe <joe.doe@example.com>'
33 changeset.added = [FileNode('foo/bar/baz'), FileNode('foobar')]
33 changeset.added = [FileNode('foo/bar/baz'), FileNode(u'foobar'), FileNode(u'blåbærgrød')]
34 changeset.changed = []
34 changeset.changed = []
35 changeset.removed = []
35 changeset.removed = []
36 self.assertEqual(changeset.as_dict(), {
36 self.assertEqual(changeset.as_dict(), {
37 'id': 'ID',
37 'id': 'ID',
38 'raw_id': 'RAW_ID',
38 'raw_id': 'RAW_ID',
39 'short_id': 'SHORT_ID',
39 'short_id': 'SHORT_ID',
40 'revision': 1009,
40 'revision': 1009,
41 'date': datetime.datetime(2011, 1, 30, 1, 45),
41 'date': datetime.datetime(2011, 1, 30, 1, 45),
42 'message': 'Message of a commit',
42 'message': 'Message of a commit',
43 'author': {
43 'author': {
44 'name': 'Joe Doe',
44 'name': 'Joe Doe',
45 'email': 'joe.doe@example.com',
45 'email': 'joe.doe@example.com',
46 },
46 },
47 'added': ['foo/bar/baz', 'foobar'],
47 'added': ['foo/bar/baz', 'foobar', u'bl\xe5b\xe6rgr\xf8d'],
48 'changed': [],
48 'changed': [],
49 'removed': [],
49 'removed': [],
50 })
50 })
51
51
52
52
53 class _ChangesetsWithCommitsTestCaseixin(_BackendTestMixin):
53 class _ChangesetsWithCommitsTestCaseixin(_BackendTestMixin):
54 recreate_repo_per_test = True
54 recreate_repo_per_test = True
55
55
56 @classmethod
56 @classmethod
57 def _get_commits(cls):
57 def _get_commits(cls):
58 start_date = datetime.datetime(2010, 1, 1, 20)
58 start_date = datetime.datetime(2010, 1, 1, 20)
59 for x in xrange(5):
59 for x in xrange(5):
60 yield {
60 yield {
61 'message': 'Commit %d' % x,
61 'message': 'Commit %d' % x,
62 'author': 'Joe Doe <joe.doe@example.com>',
62 'author': 'Joe Doe <joe.doe@example.com>',
63 'date': start_date + datetime.timedelta(hours=12 * x),
63 'date': start_date + datetime.timedelta(hours=12 * x),
64 'added': [
64 'added': [
65 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
65 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
66 ],
66 ],
67 }
67 }
68
68
69 def test_new_branch(self):
69 def test_new_branch(self):
70 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
70 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
71 content='Documentation\n'))
71 content='Documentation\n'))
72 foobar_tip = self.imc.commit(
72 foobar_tip = self.imc.commit(
73 message=u'New branch: foobar',
73 message=u'New branch: foobar',
74 author=u'joe',
74 author=u'joe',
75 branch='foobar',
75 branch='foobar',
76 )
76 )
77 self.assertTrue('foobar' in self.repo.branches)
77 self.assertTrue('foobar' in self.repo.branches)
78 self.assertEqual(foobar_tip.branch, 'foobar')
78 self.assertEqual(foobar_tip.branch, 'foobar')
79 # 'foobar' should be the only branch that contains the new commit
79 # 'foobar' should be the only branch that contains the new commit
80 self.assertNotEqual(*self.repo.branches.values())
80 self.assertNotEqual(*self.repo.branches.values())
81
81
82 def test_new_head_in_default_branch(self):
82 def test_new_head_in_default_branch(self):
83 tip = self.repo.get_changeset()
83 tip = self.repo.get_changeset()
84 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
84 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
85 content='Documentation\n'))
85 content='Documentation\n'))
86 foobar_tip = self.imc.commit(
86 foobar_tip = self.imc.commit(
87 message=u'New branch: foobar',
87 message=u'New branch: foobar',
88 author=u'joe',
88 author=u'joe',
89 branch='foobar',
89 branch='foobar',
90 parents=[tip],
90 parents=[tip],
91 )
91 )
92 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
92 self.imc.change(vcs.nodes.FileNode('docs/index.txt',
93 content='Documentation\nand more...\n'))
93 content='Documentation\nand more...\n'))
94 newtip = self.imc.commit(
94 newtip = self.imc.commit(
95 message=u'At default branch',
95 message=u'At default branch',
96 author=u'joe',
96 author=u'joe',
97 branch=foobar_tip.branch,
97 branch=foobar_tip.branch,
98 parents=[foobar_tip],
98 parents=[foobar_tip],
99 )
99 )
100
100
101 newest_tip = self.imc.commit(
101 newest_tip = self.imc.commit(
102 message=u'Merged with %s' % foobar_tip.raw_id,
102 message=u'Merged with %s' % foobar_tip.raw_id,
103 author=u'joe',
103 author=u'joe',
104 branch=self.backend_class.DEFAULT_BRANCH_NAME,
104 branch=self.backend_class.DEFAULT_BRANCH_NAME,
105 parents=[newtip, foobar_tip],
105 parents=[newtip, foobar_tip],
106 )
106 )
107
107
108 self.assertEqual(newest_tip.branch,
108 self.assertEqual(newest_tip.branch,
109 self.backend_class.DEFAULT_BRANCH_NAME)
109 self.backend_class.DEFAULT_BRANCH_NAME)
110
110
111 def test_get_changesets_respects_branch_name(self):
111 def test_get_changesets_respects_branch_name(self):
112 tip = self.repo.get_changeset()
112 tip = self.repo.get_changeset()
113 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
113 self.imc.add(vcs.nodes.FileNode('docs/index.txt',
114 content='Documentation\n'))
114 content='Documentation\n'))
115 doc_changeset = self.imc.commit(
115 doc_changeset = self.imc.commit(
116 message=u'New branch: docs',
116 message=u'New branch: docs',
117 author=u'joe',
117 author=u'joe',
118 branch='docs',
118 branch='docs',
119 )
119 )
120 self.imc.add(vcs.nodes.FileNode('newfile', content=''))
120 self.imc.add(vcs.nodes.FileNode('newfile', content=''))
121 self.imc.commit(
121 self.imc.commit(
122 message=u'Back in default branch',
122 message=u'Back in default branch',
123 author=u'joe',
123 author=u'joe',
124 parents=[tip],
124 parents=[tip],
125 )
125 )
126 default_branch_changesets = self.repo.get_changesets(
126 default_branch_changesets = self.repo.get_changesets(
127 branch_name=self.repo.DEFAULT_BRANCH_NAME)
127 branch_name=self.repo.DEFAULT_BRANCH_NAME)
128 self.assertNotIn(doc_changeset, default_branch_changesets)
128 self.assertNotIn(doc_changeset, default_branch_changesets)
129
129
130 def test_get_changeset_by_branch(self):
130 def test_get_changeset_by_branch(self):
131 for branch, sha in self.repo.branches.iteritems():
131 for branch, sha in self.repo.branches.iteritems():
132 self.assertEqual(sha, self.repo.get_changeset(branch).raw_id)
132 self.assertEqual(sha, self.repo.get_changeset(branch).raw_id)
133
133
134 def test_get_changeset_by_tag(self):
134 def test_get_changeset_by_tag(self):
135 for tag, sha in self.repo.tags.iteritems():
135 for tag, sha in self.repo.tags.iteritems():
136 self.assertEqual(sha, self.repo.get_changeset(tag).raw_id)
136 self.assertEqual(sha, self.repo.get_changeset(tag).raw_id)
137
137
138 def test_get_changeset_parents(self):
138 def test_get_changeset_parents(self):
139 for test_rev in [1, 2, 3]:
139 for test_rev in [1, 2, 3]:
140 sha = self.repo.get_changeset(test_rev-1)
140 sha = self.repo.get_changeset(test_rev-1)
141 self.assertEqual([sha], self.repo.get_changeset(test_rev).parents)
141 self.assertEqual([sha], self.repo.get_changeset(test_rev).parents)
142
142
143 def test_get_changeset_children(self):
143 def test_get_changeset_children(self):
144 for test_rev in [1, 2, 3]:
144 for test_rev in [1, 2, 3]:
145 sha = self.repo.get_changeset(test_rev+1)
145 sha = self.repo.get_changeset(test_rev+1)
146 self.assertEqual([sha], self.repo.get_changeset(test_rev).children)
146 self.assertEqual([sha], self.repo.get_changeset(test_rev).children)
147
147
148
148
149 class _ChangesetsTestCaseMixin(_BackendTestMixin):
149 class _ChangesetsTestCaseMixin(_BackendTestMixin):
150 recreate_repo_per_test = False
150 recreate_repo_per_test = False
151
151
152 @classmethod
152 @classmethod
153 def _get_commits(cls):
153 def _get_commits(cls):
154 start_date = datetime.datetime(2010, 1, 1, 20)
154 start_date = datetime.datetime(2010, 1, 1, 20)
155 for x in xrange(5):
155 for x in xrange(5):
156 yield {
156 yield {
157 'message': u'Commit %d' % x,
157 'message': u'Commit %d' % x,
158 'author': u'Joe Doe <joe.doe@example.com>',
158 'author': u'Joe Doe <joe.doe@example.com>',
159 'date': start_date + datetime.timedelta(hours=12 * x),
159 'date': start_date + datetime.timedelta(hours=12 * x),
160 'added': [
160 'added': [
161 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
161 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
162 ],
162 ],
163 }
163 }
164
164
165 def test_simple(self):
165 def test_simple(self):
166 tip = self.repo.get_changeset()
166 tip = self.repo.get_changeset()
167 self.assertEqual(tip.date, datetime.datetime(2010, 1, 3, 20))
167 self.assertEqual(tip.date, datetime.datetime(2010, 1, 3, 20))
168
168
169 def test_get_changesets_is_ordered_by_date(self):
169 def test_get_changesets_is_ordered_by_date(self):
170 changesets = list(self.repo.get_changesets())
170 changesets = list(self.repo.get_changesets())
171 ordered_by_date = sorted(changesets,
171 ordered_by_date = sorted(changesets,
172 key=lambda cs: cs.date)
172 key=lambda cs: cs.date)
173 self.assertItemsEqual(changesets, ordered_by_date)
173 self.assertItemsEqual(changesets, ordered_by_date)
174
174
175 def test_get_changesets_respects_start(self):
175 def test_get_changesets_respects_start(self):
176 second_id = self.repo.revisions[1]
176 second_id = self.repo.revisions[1]
177 changesets = list(self.repo.get_changesets(start=second_id))
177 changesets = list(self.repo.get_changesets(start=second_id))
178 self.assertEqual(len(changesets), 4)
178 self.assertEqual(len(changesets), 4)
179
179
180 def test_get_changesets_numerical_id_respects_start(self):
180 def test_get_changesets_numerical_id_respects_start(self):
181 second_id = 1
181 second_id = 1
182 changesets = list(self.repo.get_changesets(start=second_id))
182 changesets = list(self.repo.get_changesets(start=second_id))
183 self.assertEqual(len(changesets), 4)
183 self.assertEqual(len(changesets), 4)
184
184
185 def test_get_changesets_includes_start_changeset(self):
185 def test_get_changesets_includes_start_changeset(self):
186 second_id = self.repo.revisions[1]
186 second_id = self.repo.revisions[1]
187 changesets = list(self.repo.get_changesets(start=second_id))
187 changesets = list(self.repo.get_changesets(start=second_id))
188 self.assertEqual(changesets[0].raw_id, second_id)
188 self.assertEqual(changesets[0].raw_id, second_id)
189
189
190 def test_get_changesets_respects_end(self):
190 def test_get_changesets_respects_end(self):
191 second_id = self.repo.revisions[1]
191 second_id = self.repo.revisions[1]
192 changesets = list(self.repo.get_changesets(end=second_id))
192 changesets = list(self.repo.get_changesets(end=second_id))
193 self.assertEqual(changesets[-1].raw_id, second_id)
193 self.assertEqual(changesets[-1].raw_id, second_id)
194 self.assertEqual(len(changesets), 2)
194 self.assertEqual(len(changesets), 2)
195
195
196 def test_get_changesets_numerical_id_respects_end(self):
196 def test_get_changesets_numerical_id_respects_end(self):
197 second_id = 1
197 second_id = 1
198 changesets = list(self.repo.get_changesets(end=second_id))
198 changesets = list(self.repo.get_changesets(end=second_id))
199 self.assertEqual(changesets.index(changesets[-1]), second_id)
199 self.assertEqual(changesets.index(changesets[-1]), second_id)
200 self.assertEqual(len(changesets), 2)
200 self.assertEqual(len(changesets), 2)
201
201
202 def test_get_changesets_respects_both_start_and_end(self):
202 def test_get_changesets_respects_both_start_and_end(self):
203 second_id = self.repo.revisions[1]
203 second_id = self.repo.revisions[1]
204 third_id = self.repo.revisions[2]
204 third_id = self.repo.revisions[2]
205 changesets = list(self.repo.get_changesets(start=second_id,
205 changesets = list(self.repo.get_changesets(start=second_id,
206 end=third_id))
206 end=third_id))
207 self.assertEqual(len(changesets), 2)
207 self.assertEqual(len(changesets), 2)
208
208
209 def test_get_changesets_numerical_id_respects_both_start_and_end(self):
209 def test_get_changesets_numerical_id_respects_both_start_and_end(self):
210 changesets = list(self.repo.get_changesets(start=2, end=3))
210 changesets = list(self.repo.get_changesets(start=2, end=3))
211 self.assertEqual(len(changesets), 2)
211 self.assertEqual(len(changesets), 2)
212
212
213 def test_get_changesets_on_empty_repo_raises_EmptyRepository_error(self):
213 def test_get_changesets_on_empty_repo_raises_EmptyRepository_error(self):
214 Backend = self.get_backend()
214 Backend = self.get_backend()
215 repo_path = get_new_dir(str(time.time()))
215 repo_path = get_new_dir(str(time.time()))
216 repo = Backend(repo_path, create=True)
216 repo = Backend(repo_path, create=True)
217
217
218 with self.assertRaises(EmptyRepositoryError):
218 with self.assertRaises(EmptyRepositoryError):
219 list(repo.get_changesets(start='foobar'))
219 list(repo.get_changesets(start='foobar'))
220
220
221 def test_get_changesets_includes_end_changeset(self):
221 def test_get_changesets_includes_end_changeset(self):
222 second_id = self.repo.revisions[1]
222 second_id = self.repo.revisions[1]
223 changesets = list(self.repo.get_changesets(end=second_id))
223 changesets = list(self.repo.get_changesets(end=second_id))
224 self.assertEqual(changesets[-1].raw_id, second_id)
224 self.assertEqual(changesets[-1].raw_id, second_id)
225
225
226 def test_get_changesets_respects_start_date(self):
226 def test_get_changesets_respects_start_date(self):
227 start_date = datetime.datetime(2010, 2, 1)
227 start_date = datetime.datetime(2010, 2, 1)
228 for cs in self.repo.get_changesets(start_date=start_date):
228 for cs in self.repo.get_changesets(start_date=start_date):
229 self.assertGreaterEqual(cs.date, start_date)
229 self.assertGreaterEqual(cs.date, start_date)
230
230
231 def test_get_changesets_respects_end_date(self):
231 def test_get_changesets_respects_end_date(self):
232 start_date = datetime.datetime(2010, 1, 1)
232 start_date = datetime.datetime(2010, 1, 1)
233 end_date = datetime.datetime(2010, 2, 1)
233 end_date = datetime.datetime(2010, 2, 1)
234 for cs in self.repo.get_changesets(start_date=start_date,
234 for cs in self.repo.get_changesets(start_date=start_date,
235 end_date=end_date):
235 end_date=end_date):
236 self.assertGreaterEqual(cs.date, start_date)
236 self.assertGreaterEqual(cs.date, start_date)
237 self.assertLessEqual(cs.date, end_date)
237 self.assertLessEqual(cs.date, end_date)
238
238
239 def test_get_changesets_respects_start_date_and_end_date(self):
239 def test_get_changesets_respects_start_date_and_end_date(self):
240 end_date = datetime.datetime(2010, 2, 1)
240 end_date = datetime.datetime(2010, 2, 1)
241 for cs in self.repo.get_changesets(end_date=end_date):
241 for cs in self.repo.get_changesets(end_date=end_date):
242 self.assertLessEqual(cs.date, end_date)
242 self.assertLessEqual(cs.date, end_date)
243
243
244 def test_get_changesets_respects_reverse(self):
244 def test_get_changesets_respects_reverse(self):
245 changesets_id_list = [cs.raw_id for cs in
245 changesets_id_list = [cs.raw_id for cs in
246 self.repo.get_changesets(reverse=True)]
246 self.repo.get_changesets(reverse=True)]
247 self.assertItemsEqual(changesets_id_list, reversed(self.repo.revisions))
247 self.assertItemsEqual(changesets_id_list, reversed(self.repo.revisions))
248
248
249 def test_get_filenodes_generator(self):
249 def test_get_filenodes_generator(self):
250 tip = self.repo.get_changeset()
250 tip = self.repo.get_changeset()
251 filepaths = [node.path for node in tip.get_filenodes_generator()]
251 filepaths = [node.path for node in tip.get_filenodes_generator()]
252 self.assertItemsEqual(filepaths, ['file_%d.txt' % x for x in xrange(5)])
252 self.assertItemsEqual(filepaths, ['file_%d.txt' % x for x in xrange(5)])
253
253
254 def test_size(self):
254 def test_size(self):
255 tip = self.repo.get_changeset()
255 tip = self.repo.get_changeset()
256 size = 5 * len('Foobar N') # Size of 5 files
256 size = 5 * len('Foobar N') # Size of 5 files
257 self.assertEqual(tip.size, size)
257 self.assertEqual(tip.size, size)
258
258
259 def test_author(self):
259 def test_author(self):
260 tip = self.repo.get_changeset()
260 tip = self.repo.get_changeset()
261 self.assertEqual(tip.author, u'Joe Doe <joe.doe@example.com>')
261 self.assertEqual(tip.author, u'Joe Doe <joe.doe@example.com>')
262
262
263 def test_author_name(self):
263 def test_author_name(self):
264 tip = self.repo.get_changeset()
264 tip = self.repo.get_changeset()
265 self.assertEqual(tip.author_name, u'Joe Doe')
265 self.assertEqual(tip.author_name, u'Joe Doe')
266
266
267 def test_author_email(self):
267 def test_author_email(self):
268 tip = self.repo.get_changeset()
268 tip = self.repo.get_changeset()
269 self.assertEqual(tip.author_email, u'joe.doe@example.com')
269 self.assertEqual(tip.author_email, u'joe.doe@example.com')
270
270
271 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_start(self):
271 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_start(self):
272 with self.assertRaises(ChangesetDoesNotExistError):
272 with self.assertRaises(ChangesetDoesNotExistError):
273 list(self.repo.get_changesets(start='foobar'))
273 list(self.repo.get_changesets(start='foobar'))
274
274
275 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_end(self):
275 def test_get_changesets_raise_changesetdoesnotexist_for_wrong_end(self):
276 with self.assertRaises(ChangesetDoesNotExistError):
276 with self.assertRaises(ChangesetDoesNotExistError):
277 list(self.repo.get_changesets(end='foobar'))
277 list(self.repo.get_changesets(end='foobar'))
278
278
279 def test_get_changesets_raise_branchdoesnotexist_for_wrong_branch_name(self):
279 def test_get_changesets_raise_branchdoesnotexist_for_wrong_branch_name(self):
280 with self.assertRaises(BranchDoesNotExistError):
280 with self.assertRaises(BranchDoesNotExistError):
281 list(self.repo.get_changesets(branch_name='foobar'))
281 list(self.repo.get_changesets(branch_name='foobar'))
282
282
283 def test_get_changesets_raise_repositoryerror_for_wrong_start_end(self):
283 def test_get_changesets_raise_repositoryerror_for_wrong_start_end(self):
284 start = self.repo.revisions[-1]
284 start = self.repo.revisions[-1]
285 end = self.repo.revisions[0]
285 end = self.repo.revisions[0]
286 with self.assertRaises(RepositoryError):
286 with self.assertRaises(RepositoryError):
287 list(self.repo.get_changesets(start=start, end=end))
287 list(self.repo.get_changesets(start=start, end=end))
288
288
289 def test_get_changesets_numerical_id_reversed(self):
289 def test_get_changesets_numerical_id_reversed(self):
290 with self.assertRaises(RepositoryError):
290 with self.assertRaises(RepositoryError):
291 [x for x in self.repo.get_changesets(start=3, end=2)]
291 [x for x in self.repo.get_changesets(start=3, end=2)]
292
292
293 def test_get_changesets_numerical_id_respects_both_start_and_end_last(self):
293 def test_get_changesets_numerical_id_respects_both_start_and_end_last(self):
294 with self.assertRaises(RepositoryError):
294 with self.assertRaises(RepositoryError):
295 last = len(self.repo.revisions)
295 last = len(self.repo.revisions)
296 list(self.repo.get_changesets(start=last-1, end=last-2))
296 list(self.repo.get_changesets(start=last-1, end=last-2))
297
297
298 def test_get_changesets_numerical_id_last_zero_error(self):
298 def test_get_changesets_numerical_id_last_zero_error(self):
299 with self.assertRaises(RepositoryError):
299 with self.assertRaises(RepositoryError):
300 last = len(self.repo.revisions)
300 last = len(self.repo.revisions)
301 list(self.repo.get_changesets(start=last-1, end=0))
301 list(self.repo.get_changesets(start=last-1, end=0))
302
302
303
303
304 class _ChangesetsChangesTestCaseMixin(_BackendTestMixin):
304 class _ChangesetsChangesTestCaseMixin(_BackendTestMixin):
305 recreate_repo_per_test = False
305 recreate_repo_per_test = False
306
306
307 @classmethod
307 @classmethod
308 def _get_commits(cls):
308 def _get_commits(cls):
309 return [
309 return [
310 {
310 {
311 'message': u'Initial',
311 'message': u'Initial',
312 'author': u'Joe Doe <joe.doe@example.com>',
312 'author': u'Joe Doe <joe.doe@example.com>',
313 'date': datetime.datetime(2010, 1, 1, 20),
313 'date': datetime.datetime(2010, 1, 1, 20),
314 'added': [
314 'added': [
315 FileNode('foo/bar', content='foo'),
315 FileNode('foo/bar', content='foo'),
316 FileNode('foo/bał', content='foo'),
316 FileNode('foo/bał', content='foo'),
317 FileNode('foobar', content='foo'),
317 FileNode('foobar', content='foo'),
318 FileNode('qwe', content='foo'),
318 FileNode('qwe', content='foo'),
319 ],
319 ],
320 },
320 },
321 {
321 {
322 'message': u'Massive changes',
322 'message': u'Massive changes',
323 'author': u'Joe Doe <joe.doe@example.com>',
323 'author': u'Joe Doe <joe.doe@example.com>',
324 'date': datetime.datetime(2010, 1, 1, 22),
324 'date': datetime.datetime(2010, 1, 1, 22),
325 'added': [FileNode('fallout', content='War never changes')],
325 'added': [FileNode('fallout', content='War never changes')],
326 'changed': [
326 'changed': [
327 FileNode('foo/bar', content='baz'),
327 FileNode('foo/bar', content='baz'),
328 FileNode('foobar', content='baz'),
328 FileNode('foobar', content='baz'),
329 ],
329 ],
330 'removed': [FileNode('qwe')],
330 'removed': [FileNode('qwe')],
331 },
331 },
332 ]
332 ]
333
333
334 def test_initial_commit(self):
334 def test_initial_commit(self):
335 changeset = self.repo.get_changeset(0)
335 changeset = self.repo.get_changeset(0)
336 self.assertItemsEqual(changeset.added, [
336 self.assertItemsEqual(changeset.added, [
337 changeset.get_node('foo/bar'),
337 changeset.get_node('foo/bar'),
338 changeset.get_node('foo/bał'),
338 changeset.get_node('foo/bał'),
339 changeset.get_node('foobar'),
339 changeset.get_node('foobar'),
340 changeset.get_node('qwe'),
340 changeset.get_node('qwe'),
341 ])
341 ])
342 self.assertItemsEqual(changeset.changed, [])
342 self.assertItemsEqual(changeset.changed, [])
343 self.assertItemsEqual(changeset.removed, [])
343 self.assertItemsEqual(changeset.removed, [])
344 assert u'foo/ba\u0142' in changeset.as_dict()['added']
345 assert u'foo/ba\u0142' in changeset.__json__(with_file_list=True)['added']
344
346
345 def test_head_added(self):
347 def test_head_added(self):
346 changeset = self.repo.get_changeset()
348 changeset = self.repo.get_changeset()
347 self.assertTrue(isinstance(changeset.added, AddedFileNodesGenerator))
349 self.assertTrue(isinstance(changeset.added, AddedFileNodesGenerator))
348 self.assertItemsEqual(changeset.added, [
350 self.assertItemsEqual(changeset.added, [
349 changeset.get_node('fallout'),
351 changeset.get_node('fallout'),
350 ])
352 ])
351 self.assertTrue(isinstance(changeset.changed, ChangedFileNodesGenerator))
353 self.assertTrue(isinstance(changeset.changed, ChangedFileNodesGenerator))
352 self.assertItemsEqual(changeset.changed, [
354 self.assertItemsEqual(changeset.changed, [
353 changeset.get_node('foo/bar'),
355 changeset.get_node('foo/bar'),
354 changeset.get_node('foobar'),
356 changeset.get_node('foobar'),
355 ])
357 ])
356 self.assertTrue(isinstance(changeset.removed, RemovedFileNodesGenerator))
358 self.assertTrue(isinstance(changeset.removed, RemovedFileNodesGenerator))
357 self.assertEqual(len(changeset.removed), 1)
359 self.assertEqual(len(changeset.removed), 1)
358 self.assertEqual(list(changeset.removed)[0].path, 'qwe')
360 self.assertEqual(list(changeset.removed)[0].path, 'qwe')
359
361
360 def test_get_filemode(self):
362 def test_get_filemode(self):
361 changeset = self.repo.get_changeset()
363 changeset = self.repo.get_changeset()
362 self.assertEqual(33188, changeset.get_file_mode('foo/bar'))
364 self.assertEqual(33188, changeset.get_file_mode('foo/bar'))
363
365
364 def test_get_filemode_non_ascii(self):
366 def test_get_filemode_non_ascii(self):
365 changeset = self.repo.get_changeset()
367 changeset = self.repo.get_changeset()
366 self.assertEqual(33188, changeset.get_file_mode('foo/bał'))
368 self.assertEqual(33188, changeset.get_file_mode('foo/bał'))
367 self.assertEqual(33188, changeset.get_file_mode(u'foo/bał'))
369 self.assertEqual(33188, changeset.get_file_mode(u'foo/bał'))
368
370
369
371
370 # For each backend create test case class
372 # For each backend create test case class
371 for alias in SCM_TESTS:
373 for alias in SCM_TESTS:
372 attrs = {
374 attrs = {
373 'backend_alias': alias,
375 'backend_alias': alias,
374 }
376 }
375 # tests with additional commits
377 # tests with additional commits
376 cls_name = ''.join(('%s changesets with commits test' % alias).title().split())
378 cls_name = alias.title() + 'ChangesetsWithCommitsTest'
377 bases = (_ChangesetsWithCommitsTestCaseixin, unittest.TestCase)
379 bases = (_ChangesetsWithCommitsTestCaseixin, unittest.TestCase)
378 globals()[cls_name] = type(cls_name, bases, attrs)
380 globals()[cls_name] = type(cls_name, bases, attrs)
379
381
380 # tests without additional commits
382 # tests without additional commits
381 cls_name = ''.join(('%s changesets test' % alias).title().split())
383 cls_name = alias.title() + 'ChangesetsTest'
382 bases = (_ChangesetsTestCaseMixin, unittest.TestCase)
384 bases = (_ChangesetsTestCaseMixin, unittest.TestCase)
383 globals()[cls_name] = type(cls_name, bases, attrs)
385 globals()[cls_name] = type(cls_name, bases, attrs)
384
386
385 # tests changes
387 # tests changes
386 cls_name = ''.join(('%s changesets changes test' % alias).title().split())
388 cls_name = alias.title() + 'ChangesetsChangesTest'
387 bases = (_ChangesetsChangesTestCaseMixin, unittest.TestCase)
389 bases = (_ChangesetsChangesTestCaseMixin, unittest.TestCase)
388 globals()[cls_name] = type(cls_name, bases, attrs)
390 globals()[cls_name] = type(cls_name, bases, attrs)
389
391
390
392
391 if __name__ == '__main__':
393 if __name__ == '__main__':
392 unittest.main()
394 unittest.main()
General Comments 0
You need to be logged in to leave comments. Login now