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