##// END OF EJS Templates
Added EmptyChangeset into VCS module
marcink -
r2234:ef35dce6 beta
parent child Browse files
Show More
@@ -1,911 +1,956
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
12
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 @LazyProperty
83 @LazyProperty
84 def alias(self):
84 def alias(self):
85 for k, v in settings.BACKENDS.items():
85 for k, v in settings.BACKENDS.items():
86 if v.split('.')[-1] == str(self.__class__.__name__):
86 if v.split('.')[-1] == str(self.__class__.__name__):
87 return k
87 return k
88
88
89 @LazyProperty
89 @LazyProperty
90 def name(self):
90 def name(self):
91 raise NotImplementedError
91 raise NotImplementedError
92
92
93 @LazyProperty
93 @LazyProperty
94 def owner(self):
94 def owner(self):
95 raise NotImplementedError
95 raise NotImplementedError
96
96
97 @LazyProperty
97 @LazyProperty
98 def description(self):
98 def description(self):
99 raise NotImplementedError
99 raise NotImplementedError
100
100
101 @LazyProperty
101 @LazyProperty
102 def size(self):
102 def size(self):
103 """
103 """
104 Returns combined size in bytes for all repository files
104 Returns combined size in bytes for all repository files
105 """
105 """
106
106
107 size = 0
107 size = 0
108 try:
108 try:
109 tip = self.get_changeset()
109 tip = self.get_changeset()
110 for topnode, dirs, files in tip.walk('/'):
110 for topnode, dirs, files in tip.walk('/'):
111 for f in files:
111 for f in files:
112 size += tip.get_file_size(f.path)
112 size += tip.get_file_size(f.path)
113 for dir in dirs:
113 for dir in dirs:
114 for f in files:
114 for f in files:
115 size += tip.get_file_size(f.path)
115 size += tip.get_file_size(f.path)
116
116
117 except RepositoryError, e:
117 except RepositoryError, e:
118 pass
118 pass
119 return size
119 return size
120
120
121 def is_valid(self):
121 def is_valid(self):
122 """
122 """
123 Validates repository.
123 Validates repository.
124 """
124 """
125 raise NotImplementedError
125 raise NotImplementedError
126
126
127 def get_last_change(self):
127 def get_last_change(self):
128 self.get_changesets()
128 self.get_changesets()
129
129
130 #==========================================================================
130 #==========================================================================
131 # CHANGESETS
131 # CHANGESETS
132 #==========================================================================
132 #==========================================================================
133
133
134 def get_changeset(self, revision=None):
134 def get_changeset(self, revision=None):
135 """
135 """
136 Returns instance of ``Changeset`` class. If ``revision`` is None, most
136 Returns instance of ``Changeset`` class. If ``revision`` is None, most
137 recent changeset is returned.
137 recent changeset is returned.
138
138
139 :raises ``EmptyRepositoryError``: if there are no revisions
139 :raises ``EmptyRepositoryError``: if there are no revisions
140 """
140 """
141 raise NotImplementedError
141 raise NotImplementedError
142
142
143 def __iter__(self):
143 def __iter__(self):
144 """
144 """
145 Allows Repository objects to be iterated.
145 Allows Repository objects to be iterated.
146
146
147 *Requires* implementation of ``__getitem__`` method.
147 *Requires* implementation of ``__getitem__`` method.
148 """
148 """
149 for revision in self.revisions:
149 for revision in self.revisions:
150 yield self.get_changeset(revision)
150 yield self.get_changeset(revision)
151
151
152 def get_changesets(self, start=None, end=None, start_date=None,
152 def get_changesets(self, start=None, end=None, start_date=None,
153 end_date=None, branch_name=None, reverse=False):
153 end_date=None, branch_name=None, reverse=False):
154 """
154 """
155 Returns iterator of ``MercurialChangeset`` objects from start to end
155 Returns iterator of ``MercurialChangeset`` objects from start to end
156 not inclusive This should behave just like a list, ie. end is not
156 not inclusive This should behave just like a list, ie. end is not
157 inclusive
157 inclusive
158
158
159 :param start: None or str
159 :param start: None or str
160 :param end: None or str
160 :param end: None or str
161 :param start_date:
161 :param start_date:
162 :param end_date:
162 :param end_date:
163 :param branch_name:
163 :param branch_name:
164 :param reversed:
164 :param reversed:
165 """
165 """
166 raise NotImplementedError
166 raise NotImplementedError
167
167
168 def __getslice__(self, i, j):
168 def __getslice__(self, i, j):
169 """
169 """
170 Returns a iterator of sliced repository
170 Returns a iterator of sliced repository
171 """
171 """
172 for rev in self.revisions[i:j]:
172 for rev in self.revisions[i:j]:
173 yield self.get_changeset(rev)
173 yield self.get_changeset(rev)
174
174
175 def __getitem__(self, key):
175 def __getitem__(self, key):
176 return self.get_changeset(key)
176 return self.get_changeset(key)
177
177
178 def count(self):
178 def count(self):
179 return len(self.revisions)
179 return len(self.revisions)
180
180
181 def tag(self, name, user, revision=None, message=None, date=None, **opts):
181 def tag(self, name, user, revision=None, message=None, date=None, **opts):
182 """
182 """
183 Creates and returns a tag for the given ``revision``.
183 Creates and returns a tag for the given ``revision``.
184
184
185 :param name: name for new tag
185 :param name: name for new tag
186 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
186 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
187 :param revision: changeset id for which new tag would be created
187 :param revision: changeset id for which new tag would be created
188 :param message: message of the tag's commit
188 :param message: message of the tag's commit
189 :param date: date of tag's commit
189 :param date: date of tag's commit
190
190
191 :raises TagAlreadyExistError: if tag with same name already exists
191 :raises TagAlreadyExistError: if tag with same name already exists
192 """
192 """
193 raise NotImplementedError
193 raise NotImplementedError
194
194
195 def remove_tag(self, name, user, message=None, date=None):
195 def remove_tag(self, name, user, message=None, date=None):
196 """
196 """
197 Removes tag with the given ``name``.
197 Removes tag with the given ``name``.
198
198
199 :param name: name of the tag to be removed
199 :param name: name of the tag to be removed
200 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
200 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
201 :param message: message of the tag's removal commit
201 :param message: message of the tag's removal commit
202 :param date: date of tag's removal commit
202 :param date: date of tag's removal commit
203
203
204 :raises TagDoesNotExistError: if tag with given name does not exists
204 :raises TagDoesNotExistError: if tag with given name does not exists
205 """
205 """
206 raise NotImplementedError
206 raise NotImplementedError
207
207
208 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
208 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
209 context=3):
209 context=3):
210 """
210 """
211 Returns (git like) *diff*, as plain text. Shows changes introduced by
211 Returns (git like) *diff*, as plain text. Shows changes introduced by
212 ``rev2`` since ``rev1``.
212 ``rev2`` since ``rev1``.
213
213
214 :param rev1: Entry point from which diff is shown. Can be
214 :param rev1: Entry point from which diff is shown. Can be
215 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
215 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
216 the changes since empty state of the repository until ``rev2``
216 the changes since empty state of the repository until ``rev2``
217 :param rev2: Until which revision changes should be shown.
217 :param rev2: Until which revision changes should be shown.
218 :param ignore_whitespace: If set to ``True``, would not show whitespace
218 :param ignore_whitespace: If set to ``True``, would not show whitespace
219 changes. Defaults to ``False``.
219 changes. Defaults to ``False``.
220 :param context: How many lines before/after changed lines should be
220 :param context: How many lines before/after changed lines should be
221 shown. Defaults to ``3``.
221 shown. Defaults to ``3``.
222 """
222 """
223 raise NotImplementedError
223 raise NotImplementedError
224
224
225 # ========== #
225 # ========== #
226 # COMMIT API #
226 # COMMIT API #
227 # ========== #
227 # ========== #
228
228
229 @LazyProperty
229 @LazyProperty
230 def in_memory_changeset(self):
230 def in_memory_changeset(self):
231 """
231 """
232 Returns ``InMemoryChangeset`` object for this repository.
232 Returns ``InMemoryChangeset`` object for this repository.
233 """
233 """
234 raise NotImplementedError
234 raise NotImplementedError
235
235
236 def add(self, filenode, **kwargs):
236 def add(self, filenode, **kwargs):
237 """
237 """
238 Commit api function that will add given ``FileNode`` into this
238 Commit api function that will add given ``FileNode`` into this
239 repository.
239 repository.
240
240
241 :raises ``NodeAlreadyExistsError``: if there is a file with same path
241 :raises ``NodeAlreadyExistsError``: if there is a file with same path
242 already in repository
242 already in repository
243 :raises ``NodeAlreadyAddedError``: if given node is already marked as
243 :raises ``NodeAlreadyAddedError``: if given node is already marked as
244 *added*
244 *added*
245 """
245 """
246 raise NotImplementedError
246 raise NotImplementedError
247
247
248 def remove(self, filenode, **kwargs):
248 def remove(self, filenode, **kwargs):
249 """
249 """
250 Commit api function that will remove given ``FileNode`` into this
250 Commit api function that will remove given ``FileNode`` into this
251 repository.
251 repository.
252
252
253 :raises ``EmptyRepositoryError``: if there are no changesets yet
253 :raises ``EmptyRepositoryError``: if there are no changesets yet
254 :raises ``NodeDoesNotExistError``: if there is no file with given path
254 :raises ``NodeDoesNotExistError``: if there is no file with given path
255 """
255 """
256 raise NotImplementedError
256 raise NotImplementedError
257
257
258 def commit(self, message, **kwargs):
258 def commit(self, message, **kwargs):
259 """
259 """
260 Persists current changes made on this repository and returns newly
260 Persists current changes made on this repository and returns newly
261 created changeset.
261 created changeset.
262
262
263 :raises ``NothingChangedError``: if no changes has been made
263 :raises ``NothingChangedError``: if no changes has been made
264 """
264 """
265 raise NotImplementedError
265 raise NotImplementedError
266
266
267 def get_state(self):
267 def get_state(self):
268 """
268 """
269 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
269 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
270 containing ``FileNode`` objects.
270 containing ``FileNode`` objects.
271 """
271 """
272 raise NotImplementedError
272 raise NotImplementedError
273
273
274 def get_config_value(self, section, name, config_file=None):
274 def get_config_value(self, section, name, config_file=None):
275 """
275 """
276 Returns configuration value for a given [``section``] and ``name``.
276 Returns configuration value for a given [``section``] and ``name``.
277
277
278 :param section: Section we want to retrieve value from
278 :param section: Section we want to retrieve value from
279 :param name: Name of configuration we want to retrieve
279 :param name: Name of configuration we want to retrieve
280 :param config_file: A path to file which should be used to retrieve
280 :param config_file: A path to file which should be used to retrieve
281 configuration from (might also be a list of file paths)
281 configuration from (might also be a list of file paths)
282 """
282 """
283 raise NotImplementedError
283 raise NotImplementedError
284
284
285 def get_user_name(self, config_file=None):
285 def get_user_name(self, config_file=None):
286 """
286 """
287 Returns user's name from global configuration file.
287 Returns user's name from global configuration file.
288
288
289 :param config_file: A path to file which should be used to retrieve
289 :param config_file: A path to file which should be used to retrieve
290 configuration from (might also be a list of file paths)
290 configuration from (might also be a list of file paths)
291 """
291 """
292 raise NotImplementedError
292 raise NotImplementedError
293
293
294 def get_user_email(self, config_file=None):
294 def get_user_email(self, config_file=None):
295 """
295 """
296 Returns user's email from global configuration file.
296 Returns user's email from global configuration file.
297
297
298 :param config_file: A path to file which should be used to retrieve
298 :param config_file: A path to file which should be used to retrieve
299 configuration from (might also be a list of file paths)
299 configuration from (might also be a list of file paths)
300 """
300 """
301 raise NotImplementedError
301 raise NotImplementedError
302
302
303 # =========== #
303 # =========== #
304 # WORKDIR API #
304 # WORKDIR API #
305 # =========== #
305 # =========== #
306
306
307 @LazyProperty
307 @LazyProperty
308 def workdir(self):
308 def workdir(self):
309 """
309 """
310 Returns ``Workdir`` instance for this repository.
310 Returns ``Workdir`` instance for this repository.
311 """
311 """
312 raise NotImplementedError
312 raise NotImplementedError
313
313
314
314
315 class BaseChangeset(object):
315 class BaseChangeset(object):
316 """
316 """
317 Each backend should implement it's changeset representation.
317 Each backend should implement it's changeset representation.
318
318
319 **Attributes**
319 **Attributes**
320
320
321 ``repository``
321 ``repository``
322 repository object within which changeset exists
322 repository object within which changeset exists
323
323
324 ``id``
324 ``id``
325 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
325 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
326
326
327 ``raw_id``
327 ``raw_id``
328 raw changeset representation (i.e. full 40 length sha for git
328 raw changeset representation (i.e. full 40 length sha for git
329 backend)
329 backend)
330
330
331 ``short_id``
331 ``short_id``
332 shortened (if apply) version of ``raw_id``; it would be simple
332 shortened (if apply) version of ``raw_id``; it would be simple
333 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
333 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
334 as ``raw_id`` for subversion
334 as ``raw_id`` for subversion
335
335
336 ``revision``
336 ``revision``
337 revision number as integer
337 revision number as integer
338
338
339 ``files``
339 ``files``
340 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
340 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
341
341
342 ``dirs``
342 ``dirs``
343 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
343 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
344
344
345 ``nodes``
345 ``nodes``
346 combined list of ``Node`` objects
346 combined list of ``Node`` objects
347
347
348 ``author``
348 ``author``
349 author of the changeset, as unicode
349 author of the changeset, as unicode
350
350
351 ``message``
351 ``message``
352 message of the changeset, as unicode
352 message of the changeset, as unicode
353
353
354 ``parents``
354 ``parents``
355 list of parent changesets
355 list of parent changesets
356
356
357 ``last``
357 ``last``
358 ``True`` if this is last changeset in repository, ``False``
358 ``True`` if this is last changeset in repository, ``False``
359 otherwise; trying to access this attribute while there is no
359 otherwise; trying to access this attribute while there is no
360 changesets would raise ``EmptyRepositoryError``
360 changesets would raise ``EmptyRepositoryError``
361 """
361 """
362 def __str__(self):
362 def __str__(self):
363 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
363 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
364 self.short_id)
364 self.short_id)
365
365
366 def __repr__(self):
366 def __repr__(self):
367 return self.__str__()
367 return self.__str__()
368
368
369 def __unicode__(self):
369 def __unicode__(self):
370 return u'%s:%s' % (self.revision, self.short_id)
370 return u'%s:%s' % (self.revision, self.short_id)
371
371
372 def __eq__(self, other):
372 def __eq__(self, other):
373 return self.raw_id == other.raw_id
373 return self.raw_id == other.raw_id
374
374
375 @LazyProperty
375 @LazyProperty
376 def last(self):
376 def last(self):
377 if self.repository is None:
377 if self.repository is None:
378 raise ChangesetError("Cannot check if it's most recent revision")
378 raise ChangesetError("Cannot check if it's most recent revision")
379 return self.raw_id == self.repository.revisions[-1]
379 return self.raw_id == self.repository.revisions[-1]
380
380
381 @LazyProperty
381 @LazyProperty
382 def parents(self):
382 def parents(self):
383 """
383 """
384 Returns list of parents changesets.
384 Returns list of parents changesets.
385 """
385 """
386 raise NotImplementedError
386 raise NotImplementedError
387
387
388 @LazyProperty
388 @LazyProperty
389 def id(self):
389 def id(self):
390 """
390 """
391 Returns string identifying this changeset.
391 Returns string identifying this changeset.
392 """
392 """
393 raise NotImplementedError
393 raise NotImplementedError
394
394
395 @LazyProperty
395 @LazyProperty
396 def raw_id(self):
396 def raw_id(self):
397 """
397 """
398 Returns raw string identifying this changeset.
398 Returns raw string identifying this changeset.
399 """
399 """
400 raise NotImplementedError
400 raise NotImplementedError
401
401
402 @LazyProperty
402 @LazyProperty
403 def short_id(self):
403 def short_id(self):
404 """
404 """
405 Returns shortened version of ``raw_id`` attribute, as string,
405 Returns shortened version of ``raw_id`` attribute, as string,
406 identifying this changeset, useful for web representation.
406 identifying this changeset, useful for web representation.
407 """
407 """
408 raise NotImplementedError
408 raise NotImplementedError
409
409
410 @LazyProperty
410 @LazyProperty
411 def revision(self):
411 def revision(self):
412 """
412 """
413 Returns integer identifying this changeset.
413 Returns integer identifying this changeset.
414
414
415 """
415 """
416 raise NotImplementedError
416 raise NotImplementedError
417
417
418 @LazyProperty
418 @LazyProperty
419 def author(self):
419 def author(self):
420 """
420 """
421 Returns Author for given commit
421 Returns Author for given commit
422 """
422 """
423
423
424 raise NotImplementedError
424 raise NotImplementedError
425
425
426 @LazyProperty
426 @LazyProperty
427 def author_name(self):
427 def author_name(self):
428 """
428 """
429 Returns Author name for given commit
429 Returns Author name for given commit
430 """
430 """
431
431
432 return author_name(self.author)
432 return author_name(self.author)
433
433
434 @LazyProperty
434 @LazyProperty
435 def author_email(self):
435 def author_email(self):
436 """
436 """
437 Returns Author email address for given commit
437 Returns Author email address for given commit
438 """
438 """
439
439
440 return author_email(self.author)
440 return author_email(self.author)
441
441
442 def get_file_mode(self, path):
442 def get_file_mode(self, path):
443 """
443 """
444 Returns stat mode of the file at the given ``path``.
444 Returns stat mode of the file at the given ``path``.
445 """
445 """
446 raise NotImplementedError
446 raise NotImplementedError
447
447
448 def get_file_content(self, path):
448 def get_file_content(self, path):
449 """
449 """
450 Returns content of the file at the given ``path``.
450 Returns content of the file at the given ``path``.
451 """
451 """
452 raise NotImplementedError
452 raise NotImplementedError
453
453
454 def get_file_size(self, path):
454 def get_file_size(self, path):
455 """
455 """
456 Returns size of the file at the given ``path``.
456 Returns size of the file at the given ``path``.
457 """
457 """
458 raise NotImplementedError
458 raise NotImplementedError
459
459
460 def get_file_changeset(self, path):
460 def get_file_changeset(self, path):
461 """
461 """
462 Returns last commit of the file at the given ``path``.
462 Returns last commit of the file at the given ``path``.
463 """
463 """
464 raise NotImplementedError
464 raise NotImplementedError
465
465
466 def get_file_history(self, path):
466 def get_file_history(self, path):
467 """
467 """
468 Returns history of file as reversed list of ``Changeset`` objects for
468 Returns history of file as reversed list of ``Changeset`` objects for
469 which file at given ``path`` has been modified.
469 which file at given ``path`` has been modified.
470 """
470 """
471 raise NotImplementedError
471 raise NotImplementedError
472
472
473 def get_nodes(self, path):
473 def get_nodes(self, path):
474 """
474 """
475 Returns combined ``DirNode`` and ``FileNode`` objects list representing
475 Returns combined ``DirNode`` and ``FileNode`` objects list representing
476 state of changeset at the given ``path``.
476 state of changeset at the given ``path``.
477
477
478 :raises ``ChangesetError``: if node at the given ``path`` is not
478 :raises ``ChangesetError``: if node at the given ``path`` is not
479 instance of ``DirNode``
479 instance of ``DirNode``
480 """
480 """
481 raise NotImplementedError
481 raise NotImplementedError
482
482
483 def get_node(self, path):
483 def get_node(self, path):
484 """
484 """
485 Returns ``Node`` object from the given ``path``.
485 Returns ``Node`` object from the given ``path``.
486
486
487 :raises ``NodeDoesNotExistError``: if there is no node at the given
487 :raises ``NodeDoesNotExistError``: if there is no node at the given
488 ``path``
488 ``path``
489 """
489 """
490 raise NotImplementedError
490 raise NotImplementedError
491
491
492 def fill_archive(self, stream=None, kind='tgz', prefix=None):
492 def fill_archive(self, stream=None, kind='tgz', prefix=None):
493 """
493 """
494 Fills up given stream.
494 Fills up given stream.
495
495
496 :param stream: file like object.
496 :param stream: file like object.
497 :param kind: one of following: ``zip``, ``tar``, ``tgz``
497 :param kind: one of following: ``zip``, ``tar``, ``tgz``
498 or ``tbz2``. Default: ``tgz``.
498 or ``tbz2``. Default: ``tgz``.
499 :param prefix: name of root directory in archive.
499 :param prefix: name of root directory in archive.
500 Default is repository name and changeset's raw_id joined with dash.
500 Default is repository name and changeset's raw_id joined with dash.
501
501
502 repo-tip.<kind>
502 repo-tip.<kind>
503 """
503 """
504
504
505 raise NotImplementedError
505 raise NotImplementedError
506
506
507 def get_chunked_archive(self, **kwargs):
507 def get_chunked_archive(self, **kwargs):
508 """
508 """
509 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
509 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
510
510
511 :param chunk_size: extra parameter which controls size of returned
511 :param chunk_size: extra parameter which controls size of returned
512 chunks. Default:8k.
512 chunks. Default:8k.
513 """
513 """
514
514
515 chunk_size = kwargs.pop('chunk_size', 8192)
515 chunk_size = kwargs.pop('chunk_size', 8192)
516 stream = kwargs.get('stream')
516 stream = kwargs.get('stream')
517 self.fill_archive(**kwargs)
517 self.fill_archive(**kwargs)
518 while True:
518 while True:
519 data = stream.read(chunk_size)
519 data = stream.read(chunk_size)
520 if not data:
520 if not data:
521 break
521 break
522 yield data
522 yield data
523
523
524 @LazyProperty
524 @LazyProperty
525 def root(self):
525 def root(self):
526 """
526 """
527 Returns ``RootNode`` object for this changeset.
527 Returns ``RootNode`` object for this changeset.
528 """
528 """
529 return self.get_node('')
529 return self.get_node('')
530
530
531 def next(self, branch=None):
531 def next(self, branch=None):
532 """
532 """
533 Returns next changeset from current, if branch is gives it will return
533 Returns next changeset from current, if branch is gives it will return
534 next changeset belonging to this branch
534 next changeset belonging to this branch
535
535
536 :param branch: show changesets within the given named branch
536 :param branch: show changesets within the given named branch
537 """
537 """
538 raise NotImplementedError
538 raise NotImplementedError
539
539
540 def prev(self, branch=None):
540 def prev(self, branch=None):
541 """
541 """
542 Returns previous changeset from current, if branch is gives it will
542 Returns previous changeset from current, if branch is gives it will
543 return previous changeset belonging to this branch
543 return previous changeset belonging to this branch
544
544
545 :param branch: show changesets within the given named branch
545 :param branch: show changesets within the given named branch
546 """
546 """
547 raise NotImplementedError
547 raise NotImplementedError
548
548
549 @LazyProperty
549 @LazyProperty
550 def added(self):
550 def added(self):
551 """
551 """
552 Returns list of added ``FileNode`` objects.
552 Returns list of added ``FileNode`` objects.
553 """
553 """
554 raise NotImplementedError
554 raise NotImplementedError
555
555
556 @LazyProperty
556 @LazyProperty
557 def changed(self):
557 def changed(self):
558 """
558 """
559 Returns list of modified ``FileNode`` objects.
559 Returns list of modified ``FileNode`` objects.
560 """
560 """
561 raise NotImplementedError
561 raise NotImplementedError
562
562
563 @LazyProperty
563 @LazyProperty
564 def removed(self):
564 def removed(self):
565 """
565 """
566 Returns list of removed ``FileNode`` objects.
566 Returns list of removed ``FileNode`` objects.
567 """
567 """
568 raise NotImplementedError
568 raise NotImplementedError
569
569
570 @LazyProperty
570 @LazyProperty
571 def size(self):
571 def size(self):
572 """
572 """
573 Returns total number of bytes from contents of all filenodes.
573 Returns total number of bytes from contents of all filenodes.
574 """
574 """
575 return sum((node.size for node in self.get_filenodes_generator()))
575 return sum((node.size for node in self.get_filenodes_generator()))
576
576
577 def walk(self, topurl=''):
577 def walk(self, topurl=''):
578 """
578 """
579 Similar to os.walk method. Insted of filesystem it walks through
579 Similar to os.walk method. Insted of filesystem it walks through
580 changeset starting at given ``topurl``. Returns generator of tuples
580 changeset starting at given ``topurl``. Returns generator of tuples
581 (topnode, dirnodes, filenodes).
581 (topnode, dirnodes, filenodes).
582 """
582 """
583 topnode = self.get_node(topurl)
583 topnode = self.get_node(topurl)
584 yield (topnode, topnode.dirs, topnode.files)
584 yield (topnode, topnode.dirs, topnode.files)
585 for dirnode in topnode.dirs:
585 for dirnode in topnode.dirs:
586 for tup in self.walk(dirnode.path):
586 for tup in self.walk(dirnode.path):
587 yield tup
587 yield tup
588
588
589 def get_filenodes_generator(self):
589 def get_filenodes_generator(self):
590 """
590 """
591 Returns generator that yields *all* file nodes.
591 Returns generator that yields *all* file nodes.
592 """
592 """
593 for topnode, dirs, files in self.walk():
593 for topnode, dirs, files in self.walk():
594 for node in files:
594 for node in files:
595 yield node
595 yield node
596
596
597 def as_dict(self):
597 def as_dict(self):
598 """
598 """
599 Returns dictionary with changeset's attributes and their values.
599 Returns dictionary with changeset's attributes and their values.
600 """
600 """
601 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
601 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
602 'revision', 'date', 'message'])
602 'revision', 'date', 'message'])
603 data['author'] = {'name': self.author_name, 'email': self.author_email}
603 data['author'] = {'name': self.author_name, 'email': self.author_email}
604 data['added'] = [node.path for node in self.added]
604 data['added'] = [node.path for node in self.added]
605 data['changed'] = [node.path for node in self.changed]
605 data['changed'] = [node.path for node in self.changed]
606 data['removed'] = [node.path for node in self.removed]
606 data['removed'] = [node.path for node in self.removed]
607 return data
607 return data
608
608
609
609
610 class BaseWorkdir(object):
610 class BaseWorkdir(object):
611 """
611 """
612 Working directory representation of single repository.
612 Working directory representation of single repository.
613
613
614 :attribute: repository: repository object of working directory
614 :attribute: repository: repository object of working directory
615 """
615 """
616
616
617 def __init__(self, repository):
617 def __init__(self, repository):
618 self.repository = repository
618 self.repository = repository
619
619
620 def get_branch(self):
620 def get_branch(self):
621 """
621 """
622 Returns name of current branch.
622 Returns name of current branch.
623 """
623 """
624 raise NotImplementedError
624 raise NotImplementedError
625
625
626 def get_changeset(self):
626 def get_changeset(self):
627 """
627 """
628 Returns current changeset.
628 Returns current changeset.
629 """
629 """
630 raise NotImplementedError
630 raise NotImplementedError
631
631
632 def get_added(self):
632 def get_added(self):
633 """
633 """
634 Returns list of ``FileNode`` objects marked as *new* in working
634 Returns list of ``FileNode`` objects marked as *new* in working
635 directory.
635 directory.
636 """
636 """
637 raise NotImplementedError
637 raise NotImplementedError
638
638
639 def get_changed(self):
639 def get_changed(self):
640 """
640 """
641 Returns list of ``FileNode`` objects *changed* in working directory.
641 Returns list of ``FileNode`` objects *changed* in working directory.
642 """
642 """
643 raise NotImplementedError
643 raise NotImplementedError
644
644
645 def get_removed(self):
645 def get_removed(self):
646 """
646 """
647 Returns list of ``RemovedFileNode`` objects marked as *removed* in
647 Returns list of ``RemovedFileNode`` objects marked as *removed* in
648 working directory.
648 working directory.
649 """
649 """
650 raise NotImplementedError
650 raise NotImplementedError
651
651
652 def get_untracked(self):
652 def get_untracked(self):
653 """
653 """
654 Returns list of ``FileNode`` objects which are present within working
654 Returns list of ``FileNode`` objects which are present within working
655 directory however are not tracked by repository.
655 directory however are not tracked by repository.
656 """
656 """
657 raise NotImplementedError
657 raise NotImplementedError
658
658
659 def get_status(self):
659 def get_status(self):
660 """
660 """
661 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
661 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
662 lists.
662 lists.
663 """
663 """
664 raise NotImplementedError
664 raise NotImplementedError
665
665
666 def commit(self, message, **kwargs):
666 def commit(self, message, **kwargs):
667 """
667 """
668 Commits local (from working directory) changes and returns newly
668 Commits local (from working directory) changes and returns newly
669 created
669 created
670 ``Changeset``. Updates repository's ``revisions`` list.
670 ``Changeset``. Updates repository's ``revisions`` list.
671
671
672 :raises ``CommitError``: if any error occurs while committing
672 :raises ``CommitError``: if any error occurs while committing
673 """
673 """
674 raise NotImplementedError
674 raise NotImplementedError
675
675
676 def update(self, revision=None):
676 def update(self, revision=None):
677 """
677 """
678 Fetches content of the given revision and populates it within working
678 Fetches content of the given revision and populates it within working
679 directory.
679 directory.
680 """
680 """
681 raise NotImplementedError
681 raise NotImplementedError
682
682
683 def checkout_branch(self, branch=None):
683 def checkout_branch(self, branch=None):
684 """
684 """
685 Checks out ``branch`` or the backend's default branch.
685 Checks out ``branch`` or the backend's default branch.
686
686
687 Raises ``BranchDoesNotExistError`` if the branch does not exist.
687 Raises ``BranchDoesNotExistError`` if the branch does not exist.
688 """
688 """
689 raise NotImplementedError
689 raise NotImplementedError
690
690
691
691
692 class BaseInMemoryChangeset(object):
692 class BaseInMemoryChangeset(object):
693 """
693 """
694 Represents differences between repository's state (most recent head) and
694 Represents differences between repository's state (most recent head) and
695 changes made *in place*.
695 changes made *in place*.
696
696
697 **Attributes**
697 **Attributes**
698
698
699 ``repository``
699 ``repository``
700 repository object for this in-memory-changeset
700 repository object for this in-memory-changeset
701
701
702 ``added``
702 ``added``
703 list of ``FileNode`` objects marked as *added*
703 list of ``FileNode`` objects marked as *added*
704
704
705 ``changed``
705 ``changed``
706 list of ``FileNode`` objects marked as *changed*
706 list of ``FileNode`` objects marked as *changed*
707
707
708 ``removed``
708 ``removed``
709 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
709 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
710 *removed*
710 *removed*
711
711
712 ``parents``
712 ``parents``
713 list of ``Changeset`` representing parents of in-memory changeset.
713 list of ``Changeset`` representing parents of in-memory changeset.
714 Should always be 2-element sequence.
714 Should always be 2-element sequence.
715
715
716 """
716 """
717
717
718 def __init__(self, repository):
718 def __init__(self, repository):
719 self.repository = repository
719 self.repository = repository
720 self.added = []
720 self.added = []
721 self.changed = []
721 self.changed = []
722 self.removed = []
722 self.removed = []
723 self.parents = []
723 self.parents = []
724
724
725 def add(self, *filenodes):
725 def add(self, *filenodes):
726 """
726 """
727 Marks given ``FileNode`` objects as *to be committed*.
727 Marks given ``FileNode`` objects as *to be committed*.
728
728
729 :raises ``NodeAlreadyExistsError``: if node with same path exists at
729 :raises ``NodeAlreadyExistsError``: if node with same path exists at
730 latest changeset
730 latest changeset
731 :raises ``NodeAlreadyAddedError``: if node with same path is already
731 :raises ``NodeAlreadyAddedError``: if node with same path is already
732 marked as *added*
732 marked as *added*
733 """
733 """
734 # Check if not already marked as *added* first
734 # Check if not already marked as *added* first
735 for node in filenodes:
735 for node in filenodes:
736 if node.path in (n.path for n in self.added):
736 if node.path in (n.path for n in self.added):
737 raise NodeAlreadyAddedError("Such FileNode %s is already "
737 raise NodeAlreadyAddedError("Such FileNode %s is already "
738 "marked for addition" % node.path)
738 "marked for addition" % node.path)
739 for node in filenodes:
739 for node in filenodes:
740 self.added.append(node)
740 self.added.append(node)
741
741
742 def change(self, *filenodes):
742 def change(self, *filenodes):
743 """
743 """
744 Marks given ``FileNode`` objects to be *changed* in next commit.
744 Marks given ``FileNode`` objects to be *changed* in next commit.
745
745
746 :raises ``EmptyRepositoryError``: if there are no changesets yet
746 :raises ``EmptyRepositoryError``: if there are no changesets yet
747 :raises ``NodeAlreadyExistsError``: if node with same path is already
747 :raises ``NodeAlreadyExistsError``: if node with same path is already
748 marked to be *changed*
748 marked to be *changed*
749 :raises ``NodeAlreadyRemovedError``: if node with same path is already
749 :raises ``NodeAlreadyRemovedError``: if node with same path is already
750 marked to be *removed*
750 marked to be *removed*
751 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
751 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
752 changeset
752 changeset
753 :raises ``NodeNotChangedError``: if node hasn't really be changed
753 :raises ``NodeNotChangedError``: if node hasn't really be changed
754 """
754 """
755 for node in filenodes:
755 for node in filenodes:
756 if node.path in (n.path for n in self.removed):
756 if node.path in (n.path for n in self.removed):
757 raise NodeAlreadyRemovedError("Node at %s is already marked "
757 raise NodeAlreadyRemovedError("Node at %s is already marked "
758 "as removed" % node.path)
758 "as removed" % node.path)
759 try:
759 try:
760 self.repository.get_changeset()
760 self.repository.get_changeset()
761 except EmptyRepositoryError:
761 except EmptyRepositoryError:
762 raise EmptyRepositoryError("Nothing to change - try to *add* new "
762 raise EmptyRepositoryError("Nothing to change - try to *add* new "
763 "nodes rather than changing them")
763 "nodes rather than changing them")
764 for node in filenodes:
764 for node in filenodes:
765 if node.path in (n.path for n in self.changed):
765 if node.path in (n.path for n in self.changed):
766 raise NodeAlreadyChangedError("Node at '%s' is already "
766 raise NodeAlreadyChangedError("Node at '%s' is already "
767 "marked as changed" % node.path)
767 "marked as changed" % node.path)
768 self.changed.append(node)
768 self.changed.append(node)
769
769
770 def remove(self, *filenodes):
770 def remove(self, *filenodes):
771 """
771 """
772 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
772 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
773 *removed* in next commit.
773 *removed* in next commit.
774
774
775 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
775 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
776 be *removed*
776 be *removed*
777 :raises ``NodeAlreadyChangedError``: if node has been already marked to
777 :raises ``NodeAlreadyChangedError``: if node has been already marked to
778 be *changed*
778 be *changed*
779 """
779 """
780 for node in filenodes:
780 for node in filenodes:
781 if node.path in (n.path for n in self.removed):
781 if node.path in (n.path for n in self.removed):
782 raise NodeAlreadyRemovedError("Node is already marked to "
782 raise NodeAlreadyRemovedError("Node is already marked to "
783 "for removal at %s" % node.path)
783 "for removal at %s" % node.path)
784 if node.path in (n.path for n in self.changed):
784 if node.path in (n.path for n in self.changed):
785 raise NodeAlreadyChangedError("Node is already marked to "
785 raise NodeAlreadyChangedError("Node is already marked to "
786 "be changed at %s" % node.path)
786 "be changed at %s" % node.path)
787 # We only mark node as *removed* - real removal is done by
787 # We only mark node as *removed* - real removal is done by
788 # commit method
788 # commit method
789 self.removed.append(node)
789 self.removed.append(node)
790
790
791 def reset(self):
791 def reset(self):
792 """
792 """
793 Resets this instance to initial state (cleans ``added``, ``changed``
793 Resets this instance to initial state (cleans ``added``, ``changed``
794 and ``removed`` lists).
794 and ``removed`` lists).
795 """
795 """
796 self.added = []
796 self.added = []
797 self.changed = []
797 self.changed = []
798 self.removed = []
798 self.removed = []
799 self.parents = []
799 self.parents = []
800
800
801 def get_ipaths(self):
801 def get_ipaths(self):
802 """
802 """
803 Returns generator of paths from nodes marked as added, changed or
803 Returns generator of paths from nodes marked as added, changed or
804 removed.
804 removed.
805 """
805 """
806 for node in chain(self.added, self.changed, self.removed):
806 for node in chain(self.added, self.changed, self.removed):
807 yield node.path
807 yield node.path
808
808
809 def get_paths(self):
809 def get_paths(self):
810 """
810 """
811 Returns list of paths from nodes marked as added, changed or removed.
811 Returns list of paths from nodes marked as added, changed or removed.
812 """
812 """
813 return list(self.get_ipaths())
813 return list(self.get_ipaths())
814
814
815 def check_integrity(self, parents=None):
815 def check_integrity(self, parents=None):
816 """
816 """
817 Checks in-memory changeset's integrity. Also, sets parents if not
817 Checks in-memory changeset's integrity. Also, sets parents if not
818 already set.
818 already set.
819
819
820 :raises CommitError: if any error occurs (i.e.
820 :raises CommitError: if any error occurs (i.e.
821 ``NodeDoesNotExistError``).
821 ``NodeDoesNotExistError``).
822 """
822 """
823 if not self.parents:
823 if not self.parents:
824 parents = parents or []
824 parents = parents or []
825 if len(parents) == 0:
825 if len(parents) == 0:
826 try:
826 try:
827 parents = [self.repository.get_changeset(), None]
827 parents = [self.repository.get_changeset(), None]
828 except EmptyRepositoryError:
828 except EmptyRepositoryError:
829 parents = [None, None]
829 parents = [None, None]
830 elif len(parents) == 1:
830 elif len(parents) == 1:
831 parents += [None]
831 parents += [None]
832 self.parents = parents
832 self.parents = parents
833
833
834 # Local parents, only if not None
834 # Local parents, only if not None
835 parents = [p for p in self.parents if p]
835 parents = [p for p in self.parents if p]
836
836
837 # Check nodes marked as added
837 # Check nodes marked as added
838 for p in parents:
838 for p in parents:
839 for node in self.added:
839 for node in self.added:
840 try:
840 try:
841 p.get_node(node.path)
841 p.get_node(node.path)
842 except NodeDoesNotExistError:
842 except NodeDoesNotExistError:
843 pass
843 pass
844 else:
844 else:
845 raise NodeAlreadyExistsError("Node at %s already exists "
845 raise NodeAlreadyExistsError("Node at %s already exists "
846 "at %s" % (node.path, p))
846 "at %s" % (node.path, p))
847
847
848 # Check nodes marked as changed
848 # Check nodes marked as changed
849 missing = set(self.changed)
849 missing = set(self.changed)
850 not_changed = set(self.changed)
850 not_changed = set(self.changed)
851 if self.changed and not parents:
851 if self.changed and not parents:
852 raise NodeDoesNotExistError(str(self.changed[0].path))
852 raise NodeDoesNotExistError(str(self.changed[0].path))
853 for p in parents:
853 for p in parents:
854 for node in self.changed:
854 for node in self.changed:
855 try:
855 try:
856 old = p.get_node(node.path)
856 old = p.get_node(node.path)
857 missing.remove(node)
857 missing.remove(node)
858 if old.content != node.content:
858 if old.content != node.content:
859 not_changed.remove(node)
859 not_changed.remove(node)
860 except NodeDoesNotExistError:
860 except NodeDoesNotExistError:
861 pass
861 pass
862 if self.changed and missing:
862 if self.changed and missing:
863 raise NodeDoesNotExistError("Node at %s is missing "
863 raise NodeDoesNotExistError("Node at %s is missing "
864 "(parents: %s)" % (node.path, parents))
864 "(parents: %s)" % (node.path, parents))
865
865
866 if self.changed and not_changed:
866 if self.changed and not_changed:
867 raise NodeNotChangedError("Node at %s wasn't actually changed "
867 raise NodeNotChangedError("Node at %s wasn't actually changed "
868 "since parents' changesets: %s" % (not_changed.pop().path,
868 "since parents' changesets: %s" % (not_changed.pop().path,
869 parents)
869 parents)
870 )
870 )
871
871
872 # Check nodes marked as removed
872 # Check nodes marked as removed
873 if self.removed and not parents:
873 if self.removed and not parents:
874 raise NodeDoesNotExistError("Cannot remove node at %s as there "
874 raise NodeDoesNotExistError("Cannot remove node at %s as there "
875 "were no parents specified" % self.removed[0].path)
875 "were no parents specified" % self.removed[0].path)
876 really_removed = set()
876 really_removed = set()
877 for p in parents:
877 for p in parents:
878 for node in self.removed:
878 for node in self.removed:
879 try:
879 try:
880 p.get_node(node.path)
880 p.get_node(node.path)
881 really_removed.add(node)
881 really_removed.add(node)
882 except ChangesetError:
882 except ChangesetError:
883 pass
883 pass
884 not_removed = set(self.removed) - really_removed
884 not_removed = set(self.removed) - really_removed
885 if not_removed:
885 if not_removed:
886 raise NodeDoesNotExistError("Cannot remove node at %s from "
886 raise NodeDoesNotExistError("Cannot remove node at %s from "
887 "following parents: %s" % (not_removed[0], parents))
887 "following parents: %s" % (not_removed[0], parents))
888
888
889 def commit(self, message, author, parents=None, branch=None, date=None,
889 def commit(self, message, author, parents=None, branch=None, date=None,
890 **kwargs):
890 **kwargs):
891 """
891 """
892 Performs in-memory commit (doesn't check workdir in any way) and
892 Performs in-memory commit (doesn't check workdir in any way) and
893 returns newly created ``Changeset``. Updates repository's
893 returns newly created ``Changeset``. Updates repository's
894 ``revisions``.
894 ``revisions``.
895
895
896 .. note::
896 .. note::
897 While overriding this method each backend's should call
897 While overriding this method each backend's should call
898 ``self.check_integrity(parents)`` in the first place.
898 ``self.check_integrity(parents)`` in the first place.
899
899
900 :param message: message of the commit
900 :param message: message of the commit
901 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
901 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
902 :param parents: single parent or sequence of parents from which commit
902 :param parents: single parent or sequence of parents from which commit
903 would be derieved
903 would be derieved
904 :param date: ``datetime.datetime`` instance. Defaults to
904 :param date: ``datetime.datetime`` instance. Defaults to
905 ``datetime.datetime.now()``.
905 ``datetime.datetime.now()``.
906 :param branch: branch name, as string. If none given, default backend's
906 :param branch: branch name, as string. If none given, default backend's
907 branch would be used.
907 branch would be used.
908
908
909 :raises ``CommitError``: if any error occurs while committing
909 :raises ``CommitError``: if any error occurs while committing
910 """
910 """
911 raise NotImplementedError
911 raise NotImplementedError
912
913
914 class EmptyChangeset(BaseChangeset):
915 """
916 An dummy empty changeset. It's possible to pass hash when creating
917 an EmptyChangeset
918 """
919
920 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
921 alias=None):
922 self._empty_cs = cs
923 self.revision = -1
924 self.message = ''
925 self.author = ''
926 self.date = ''
927 self.repository = repo
928 self.requested_revision = requested_revision
929 self.alias = alias
930
931 @LazyProperty
932 def raw_id(self):
933 """
934 Returns raw string identifying this changeset, useful for web
935 representation.
936 """
937
938 return self._empty_cs
939
940 @LazyProperty
941 def branch(self):
942 from rhodecode.lib.vcs.backends import get_backend
943 return get_backend(self.alias).DEFAULT_BRANCH_NAME
944
945 @LazyProperty
946 def short_id(self):
947 return self.raw_id[:12]
948
949 def get_file_changeset(self, path):
950 return self
951
952 def get_file_content(self, path):
953 return u''
954
955 def get_file_size(self, path):
956 return 0 No newline at end of file
@@ -1,611 +1,611
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.nodes
3 vcs.nodes
4 ~~~~~~~~~
4 ~~~~~~~~~
5
5
6 Module holding everything related to vcs nodes.
6 Module holding everything related to vcs nodes.
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 import os
11 import os
12 import stat
12 import stat
13 import posixpath
13 import posixpath
14 import mimetypes
14 import mimetypes
15
15
16 from pygments import lexers
16 from pygments import lexers
17
17
18 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 from rhodecode.lib.vcs.utils.lazy import LazyProperty
19 from rhodecode.lib.vcs.utils import safe_unicode, safe_str
19 from rhodecode.lib.vcs.utils import safe_unicode, safe_str
20 from rhodecode.lib.vcs.exceptions import NodeError
20 from rhodecode.lib.vcs.exceptions import NodeError
21 from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
21 from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
22 from rhodecode.lib.utils import EmptyChangeset
22 from rhodecode.lib.vcs.backends.base import EmptyChangeset
23
23
24
24
25 class NodeKind:
25 class NodeKind:
26 SUBMODULE = -1
26 SUBMODULE = -1
27 DIR = 1
27 DIR = 1
28 FILE = 2
28 FILE = 2
29
29
30
30
31 class NodeState:
31 class NodeState:
32 ADDED = u'added'
32 ADDED = u'added'
33 CHANGED = u'changed'
33 CHANGED = u'changed'
34 NOT_CHANGED = u'not changed'
34 NOT_CHANGED = u'not changed'
35 REMOVED = u'removed'
35 REMOVED = u'removed'
36
36
37
37
38 class NodeGeneratorBase(object):
38 class NodeGeneratorBase(object):
39 """
39 """
40 Base class for removed added and changed filenodes, it's a lazy generator
40 Base class for removed added and changed filenodes, it's a lazy generator
41 class that will create filenodes only on iteration or call
41 class that will create filenodes only on iteration or call
42
42
43 The len method doesn't need to create filenodes at all
43 The len method doesn't need to create filenodes at all
44 """
44 """
45
45
46 def __init__(self, current_paths, cs):
46 def __init__(self, current_paths, cs):
47 self.cs = cs
47 self.cs = cs
48 self.current_paths = current_paths
48 self.current_paths = current_paths
49
49
50 def __call__(self):
50 def __call__(self):
51 return [n for n in self]
51 return [n for n in self]
52
52
53 def __getslice__(self, i, j):
53 def __getslice__(self, i, j):
54 for p in self.current_paths[i:j]:
54 for p in self.current_paths[i:j]:
55 yield self.cs.get_node(p)
55 yield self.cs.get_node(p)
56
56
57 def __len__(self):
57 def __len__(self):
58 return len(self.current_paths)
58 return len(self.current_paths)
59
59
60 def __iter__(self):
60 def __iter__(self):
61 for p in self.current_paths:
61 for p in self.current_paths:
62 yield self.cs.get_node(p)
62 yield self.cs.get_node(p)
63
63
64
64
65 class AddedFileNodesGenerator(NodeGeneratorBase):
65 class AddedFileNodesGenerator(NodeGeneratorBase):
66 """
66 """
67 Class holding Added files for current changeset
67 Class holding Added files for current changeset
68 """
68 """
69 pass
69 pass
70
70
71
71
72 class ChangedFileNodesGenerator(NodeGeneratorBase):
72 class ChangedFileNodesGenerator(NodeGeneratorBase):
73 """
73 """
74 Class holding Changed files for current changeset
74 Class holding Changed files for current changeset
75 """
75 """
76 pass
76 pass
77
77
78
78
79 class RemovedFileNodesGenerator(NodeGeneratorBase):
79 class RemovedFileNodesGenerator(NodeGeneratorBase):
80 """
80 """
81 Class holding removed files for current changeset
81 Class holding removed files for current changeset
82 """
82 """
83 def __iter__(self):
83 def __iter__(self):
84 for p in self.current_paths:
84 for p in self.current_paths:
85 yield RemovedFileNode(path=p)
85 yield RemovedFileNode(path=p)
86
86
87 def __getslice__(self, i, j):
87 def __getslice__(self, i, j):
88 for p in self.current_paths[i:j]:
88 for p in self.current_paths[i:j]:
89 yield RemovedFileNode(path=p)
89 yield RemovedFileNode(path=p)
90
90
91
91
92 class Node(object):
92 class Node(object):
93 """
93 """
94 Simplest class representing file or directory on repository. SCM backends
94 Simplest class representing file or directory on repository. SCM backends
95 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
95 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
96 directly.
96 directly.
97
97
98 Node's ``path`` cannot start with slash as we operate on *relative* paths
98 Node's ``path`` cannot start with slash as we operate on *relative* paths
99 only. Moreover, every single node is identified by the ``path`` attribute,
99 only. Moreover, every single node is identified by the ``path`` attribute,
100 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
100 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
101 """
101 """
102
102
103 def __init__(self, path, kind):
103 def __init__(self, path, kind):
104 if path.startswith('/'):
104 if path.startswith('/'):
105 raise NodeError("Cannot initialize Node objects with slash at "
105 raise NodeError("Cannot initialize Node objects with slash at "
106 "the beginning as only relative paths are supported")
106 "the beginning as only relative paths are supported")
107 self.path = path.rstrip('/')
107 self.path = path.rstrip('/')
108 if path == '' and kind != NodeKind.DIR:
108 if path == '' and kind != NodeKind.DIR:
109 raise NodeError("Only DirNode and its subclasses may be "
109 raise NodeError("Only DirNode and its subclasses may be "
110 "initialized with empty path")
110 "initialized with empty path")
111 self.kind = kind
111 self.kind = kind
112 #self.dirs, self.files = [], []
112 #self.dirs, self.files = [], []
113 if self.is_root() and not self.is_dir():
113 if self.is_root() and not self.is_dir():
114 raise NodeError("Root node cannot be FILE kind")
114 raise NodeError("Root node cannot be FILE kind")
115
115
116 @LazyProperty
116 @LazyProperty
117 def parent(self):
117 def parent(self):
118 parent_path = self.get_parent_path()
118 parent_path = self.get_parent_path()
119 if parent_path:
119 if parent_path:
120 if self.changeset:
120 if self.changeset:
121 return self.changeset.get_node(parent_path)
121 return self.changeset.get_node(parent_path)
122 return DirNode(parent_path)
122 return DirNode(parent_path)
123 return None
123 return None
124
124
125 @LazyProperty
125 @LazyProperty
126 def unicode_path(self):
126 def unicode_path(self):
127 return safe_unicode(self.path)
127 return safe_unicode(self.path)
128
128
129 @LazyProperty
129 @LazyProperty
130 def name(self):
130 def name(self):
131 """
131 """
132 Returns name of the node so if its path
132 Returns name of the node so if its path
133 then only last part is returned.
133 then only last part is returned.
134 """
134 """
135 return safe_unicode(self.path.rstrip('/').split('/')[-1])
135 return safe_unicode(self.path.rstrip('/').split('/')[-1])
136
136
137 def _get_kind(self):
137 def _get_kind(self):
138 return self._kind
138 return self._kind
139
139
140 def _set_kind(self, kind):
140 def _set_kind(self, kind):
141 if hasattr(self, '_kind'):
141 if hasattr(self, '_kind'):
142 raise NodeError("Cannot change node's kind")
142 raise NodeError("Cannot change node's kind")
143 else:
143 else:
144 self._kind = kind
144 self._kind = kind
145 # Post setter check (path's trailing slash)
145 # Post setter check (path's trailing slash)
146 if self.path.endswith('/'):
146 if self.path.endswith('/'):
147 raise NodeError("Node's path cannot end with slash")
147 raise NodeError("Node's path cannot end with slash")
148
148
149 kind = property(_get_kind, _set_kind)
149 kind = property(_get_kind, _set_kind)
150
150
151 def __cmp__(self, other):
151 def __cmp__(self, other):
152 """
152 """
153 Comparator using name of the node, needed for quick list sorting.
153 Comparator using name of the node, needed for quick list sorting.
154 """
154 """
155 kind_cmp = cmp(self.kind, other.kind)
155 kind_cmp = cmp(self.kind, other.kind)
156 if kind_cmp:
156 if kind_cmp:
157 return kind_cmp
157 return kind_cmp
158 return cmp(self.name, other.name)
158 return cmp(self.name, other.name)
159
159
160 def __eq__(self, other):
160 def __eq__(self, other):
161 for attr in ['name', 'path', 'kind']:
161 for attr in ['name', 'path', 'kind']:
162 if getattr(self, attr) != getattr(other, attr):
162 if getattr(self, attr) != getattr(other, attr):
163 return False
163 return False
164 if self.is_file():
164 if self.is_file():
165 if self.content != other.content:
165 if self.content != other.content:
166 return False
166 return False
167 else:
167 else:
168 # For DirNode's check without entering each dir
168 # For DirNode's check without entering each dir
169 self_nodes_paths = list(sorted(n.path for n in self.nodes))
169 self_nodes_paths = list(sorted(n.path for n in self.nodes))
170 other_nodes_paths = list(sorted(n.path for n in self.nodes))
170 other_nodes_paths = list(sorted(n.path for n in self.nodes))
171 if self_nodes_paths != other_nodes_paths:
171 if self_nodes_paths != other_nodes_paths:
172 return False
172 return False
173 return True
173 return True
174
174
175 def __nq__(self, other):
175 def __nq__(self, other):
176 return not self.__eq__(other)
176 return not self.__eq__(other)
177
177
178 def __repr__(self):
178 def __repr__(self):
179 return '<%s %r>' % (self.__class__.__name__, self.path)
179 return '<%s %r>' % (self.__class__.__name__, self.path)
180
180
181 def __str__(self):
181 def __str__(self):
182 return self.__repr__()
182 return self.__repr__()
183
183
184 def __unicode__(self):
184 def __unicode__(self):
185 return self.name
185 return self.name
186
186
187 def get_parent_path(self):
187 def get_parent_path(self):
188 """
188 """
189 Returns node's parent path or empty string if node is root.
189 Returns node's parent path or empty string if node is root.
190 """
190 """
191 if self.is_root():
191 if self.is_root():
192 return ''
192 return ''
193 return posixpath.dirname(self.path.rstrip('/')) + '/'
193 return posixpath.dirname(self.path.rstrip('/')) + '/'
194
194
195 def is_file(self):
195 def is_file(self):
196 """
196 """
197 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
197 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
198 otherwise.
198 otherwise.
199 """
199 """
200 return self.kind == NodeKind.FILE
200 return self.kind == NodeKind.FILE
201
201
202 def is_dir(self):
202 def is_dir(self):
203 """
203 """
204 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
204 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
205 otherwise.
205 otherwise.
206 """
206 """
207 return self.kind == NodeKind.DIR
207 return self.kind == NodeKind.DIR
208
208
209 def is_root(self):
209 def is_root(self):
210 """
210 """
211 Returns ``True`` if node is a root node and ``False`` otherwise.
211 Returns ``True`` if node is a root node and ``False`` otherwise.
212 """
212 """
213 return self.kind == NodeKind.DIR and self.path == ''
213 return self.kind == NodeKind.DIR and self.path == ''
214
214
215 def is_submodule(self):
215 def is_submodule(self):
216 """
216 """
217 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
217 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
218 otherwise.
218 otherwise.
219 """
219 """
220 return self.kind == NodeKind.SUBMODULE
220 return self.kind == NodeKind.SUBMODULE
221
221
222 @LazyProperty
222 @LazyProperty
223 def added(self):
223 def added(self):
224 return self.state is NodeState.ADDED
224 return self.state is NodeState.ADDED
225
225
226 @LazyProperty
226 @LazyProperty
227 def changed(self):
227 def changed(self):
228 return self.state is NodeState.CHANGED
228 return self.state is NodeState.CHANGED
229
229
230 @LazyProperty
230 @LazyProperty
231 def not_changed(self):
231 def not_changed(self):
232 return self.state is NodeState.NOT_CHANGED
232 return self.state is NodeState.NOT_CHANGED
233
233
234 @LazyProperty
234 @LazyProperty
235 def removed(self):
235 def removed(self):
236 return self.state is NodeState.REMOVED
236 return self.state is NodeState.REMOVED
237
237
238
238
239 class FileNode(Node):
239 class FileNode(Node):
240 """
240 """
241 Class representing file nodes.
241 Class representing file nodes.
242
242
243 :attribute: path: path to the node, relative to repostiory's root
243 :attribute: path: path to the node, relative to repostiory's root
244 :attribute: content: if given arbitrary sets content of the file
244 :attribute: content: if given arbitrary sets content of the file
245 :attribute: changeset: if given, first time content is accessed, callback
245 :attribute: changeset: if given, first time content is accessed, callback
246 :attribute: mode: octal stat mode for a node. Default is 0100644.
246 :attribute: mode: octal stat mode for a node. Default is 0100644.
247 """
247 """
248
248
249 def __init__(self, path, content=None, changeset=None, mode=None):
249 def __init__(self, path, content=None, changeset=None, mode=None):
250 """
250 """
251 Only one of ``content`` and ``changeset`` may be given. Passing both
251 Only one of ``content`` and ``changeset`` may be given. Passing both
252 would raise ``NodeError`` exception.
252 would raise ``NodeError`` exception.
253
253
254 :param path: relative path to the node
254 :param path: relative path to the node
255 :param content: content may be passed to constructor
255 :param content: content may be passed to constructor
256 :param changeset: if given, will use it to lazily fetch content
256 :param changeset: if given, will use it to lazily fetch content
257 :param mode: octal representation of ST_MODE (i.e. 0100644)
257 :param mode: octal representation of ST_MODE (i.e. 0100644)
258 """
258 """
259
259
260 if content and changeset:
260 if content and changeset:
261 raise NodeError("Cannot use both content and changeset")
261 raise NodeError("Cannot use both content and changeset")
262 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
262 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
263 self.changeset = changeset
263 self.changeset = changeset
264 self._content = content
264 self._content = content
265 self._mode = mode or 0100644
265 self._mode = mode or 0100644
266
266
267 @LazyProperty
267 @LazyProperty
268 def mode(self):
268 def mode(self):
269 """
269 """
270 Returns lazily mode of the FileNode. If ``changeset`` is not set, would
270 Returns lazily mode of the FileNode. If ``changeset`` is not set, would
271 use value given at initialization or 0100644 (default).
271 use value given at initialization or 0100644 (default).
272 """
272 """
273 if self.changeset:
273 if self.changeset:
274 mode = self.changeset.get_file_mode(self.path)
274 mode = self.changeset.get_file_mode(self.path)
275 else:
275 else:
276 mode = self._mode
276 mode = self._mode
277 return mode
277 return mode
278
278
279 @property
279 @property
280 def content(self):
280 def content(self):
281 """
281 """
282 Returns lazily content of the FileNode. If possible, would try to
282 Returns lazily content of the FileNode. If possible, would try to
283 decode content from UTF-8.
283 decode content from UTF-8.
284 """
284 """
285 if self.changeset:
285 if self.changeset:
286 content = self.changeset.get_file_content(self.path)
286 content = self.changeset.get_file_content(self.path)
287 else:
287 else:
288 content = self._content
288 content = self._content
289
289
290 if bool(content and '\0' in content):
290 if bool(content and '\0' in content):
291 return content
291 return content
292 return safe_unicode(content)
292 return safe_unicode(content)
293
293
294 @LazyProperty
294 @LazyProperty
295 def size(self):
295 def size(self):
296 if self.changeset:
296 if self.changeset:
297 return self.changeset.get_file_size(self.path)
297 return self.changeset.get_file_size(self.path)
298 raise NodeError("Cannot retrieve size of the file without related "
298 raise NodeError("Cannot retrieve size of the file without related "
299 "changeset attribute")
299 "changeset attribute")
300
300
301 @LazyProperty
301 @LazyProperty
302 def message(self):
302 def message(self):
303 if self.changeset:
303 if self.changeset:
304 return self.last_changeset.message
304 return self.last_changeset.message
305 raise NodeError("Cannot retrieve message of the file without related "
305 raise NodeError("Cannot retrieve message of the file without related "
306 "changeset attribute")
306 "changeset attribute")
307
307
308 @LazyProperty
308 @LazyProperty
309 def last_changeset(self):
309 def last_changeset(self):
310 if self.changeset:
310 if self.changeset:
311 return self.changeset.get_file_changeset(self.path)
311 return self.changeset.get_file_changeset(self.path)
312 raise NodeError("Cannot retrieve last changeset of the file without "
312 raise NodeError("Cannot retrieve last changeset of the file without "
313 "related changeset attribute")
313 "related changeset attribute")
314
314
315 def get_mimetype(self):
315 def get_mimetype(self):
316 """
316 """
317 Mimetype is calculated based on the file's content. If ``_mimetype``
317 Mimetype is calculated based on the file's content. If ``_mimetype``
318 attribute is available, it will be returned (backends which store
318 attribute is available, it will be returned (backends which store
319 mimetypes or can easily recognize them, should set this private
319 mimetypes or can easily recognize them, should set this private
320 attribute to indicate that type should *NOT* be calculated).
320 attribute to indicate that type should *NOT* be calculated).
321 """
321 """
322 if hasattr(self, '_mimetype'):
322 if hasattr(self, '_mimetype'):
323 if (isinstance(self._mimetype, (tuple, list,)) and
323 if (isinstance(self._mimetype, (tuple, list,)) and
324 len(self._mimetype) == 2):
324 len(self._mimetype) == 2):
325 return self._mimetype
325 return self._mimetype
326 else:
326 else:
327 raise NodeError('given _mimetype attribute must be an 2 '
327 raise NodeError('given _mimetype attribute must be an 2 '
328 'element list or tuple')
328 'element list or tuple')
329
329
330 mtype, encoding = mimetypes.guess_type(self.name)
330 mtype, encoding = mimetypes.guess_type(self.name)
331
331
332 if mtype is None:
332 if mtype is None:
333 if self.is_binary:
333 if self.is_binary:
334 mtype = 'application/octet-stream'
334 mtype = 'application/octet-stream'
335 encoding = None
335 encoding = None
336 else:
336 else:
337 mtype = 'text/plain'
337 mtype = 'text/plain'
338 encoding = None
338 encoding = None
339 return mtype, encoding
339 return mtype, encoding
340
340
341 @LazyProperty
341 @LazyProperty
342 def mimetype(self):
342 def mimetype(self):
343 """
343 """
344 Wrapper around full mimetype info. It returns only type of fetched
344 Wrapper around full mimetype info. It returns only type of fetched
345 mimetype without the encoding part. use get_mimetype function to fetch
345 mimetype without the encoding part. use get_mimetype function to fetch
346 full set of (type,encoding)
346 full set of (type,encoding)
347 """
347 """
348 return self.get_mimetype()[0]
348 return self.get_mimetype()[0]
349
349
350 @LazyProperty
350 @LazyProperty
351 def mimetype_main(self):
351 def mimetype_main(self):
352 return self.mimetype.split('/')[0]
352 return self.mimetype.split('/')[0]
353
353
354 @LazyProperty
354 @LazyProperty
355 def lexer(self):
355 def lexer(self):
356 """
356 """
357 Returns pygment's lexer class. Would try to guess lexer taking file's
357 Returns pygment's lexer class. Would try to guess lexer taking file's
358 content, name and mimetype.
358 content, name and mimetype.
359 """
359 """
360 try:
360 try:
361 lexer = lexers.guess_lexer_for_filename(self.name, self.content)
361 lexer = lexers.guess_lexer_for_filename(self.name, self.content)
362 except lexers.ClassNotFound:
362 except lexers.ClassNotFound:
363 lexer = lexers.TextLexer()
363 lexer = lexers.TextLexer()
364 # returns first alias
364 # returns first alias
365 return lexer
365 return lexer
366
366
367 @LazyProperty
367 @LazyProperty
368 def lexer_alias(self):
368 def lexer_alias(self):
369 """
369 """
370 Returns first alias of the lexer guessed for this file.
370 Returns first alias of the lexer guessed for this file.
371 """
371 """
372 return self.lexer.aliases[0]
372 return self.lexer.aliases[0]
373
373
374 @LazyProperty
374 @LazyProperty
375 def history(self):
375 def history(self):
376 """
376 """
377 Returns a list of changeset for this file in which the file was changed
377 Returns a list of changeset for this file in which the file was changed
378 """
378 """
379 if self.changeset is None:
379 if self.changeset is None:
380 raise NodeError('Unable to get changeset for this FileNode')
380 raise NodeError('Unable to get changeset for this FileNode')
381 return self.changeset.get_file_history(self.path)
381 return self.changeset.get_file_history(self.path)
382
382
383 @LazyProperty
383 @LazyProperty
384 def annotate(self):
384 def annotate(self):
385 """
385 """
386 Returns a list of three element tuples with lineno,changeset and line
386 Returns a list of three element tuples with lineno,changeset and line
387 """
387 """
388 if self.changeset is None:
388 if self.changeset is None:
389 raise NodeError('Unable to get changeset for this FileNode')
389 raise NodeError('Unable to get changeset for this FileNode')
390 return self.changeset.get_file_annotate(self.path)
390 return self.changeset.get_file_annotate(self.path)
391
391
392 @LazyProperty
392 @LazyProperty
393 def state(self):
393 def state(self):
394 if not self.changeset:
394 if not self.changeset:
395 raise NodeError("Cannot check state of the node if it's not "
395 raise NodeError("Cannot check state of the node if it's not "
396 "linked with changeset")
396 "linked with changeset")
397 elif self.path in (node.path for node in self.changeset.added):
397 elif self.path in (node.path for node in self.changeset.added):
398 return NodeState.ADDED
398 return NodeState.ADDED
399 elif self.path in (node.path for node in self.changeset.changed):
399 elif self.path in (node.path for node in self.changeset.changed):
400 return NodeState.CHANGED
400 return NodeState.CHANGED
401 else:
401 else:
402 return NodeState.NOT_CHANGED
402 return NodeState.NOT_CHANGED
403
403
404 @property
404 @property
405 def is_binary(self):
405 def is_binary(self):
406 """
406 """
407 Returns True if file has binary content.
407 Returns True if file has binary content.
408 """
408 """
409 _bin = '\0' in self.content
409 _bin = '\0' in self.content
410 return _bin
410 return _bin
411
411
412 @LazyProperty
412 @LazyProperty
413 def extension(self):
413 def extension(self):
414 """Returns filenode extension"""
414 """Returns filenode extension"""
415 return self.name.split('.')[-1]
415 return self.name.split('.')[-1]
416
416
417 def is_executable(self):
417 def is_executable(self):
418 """
418 """
419 Returns ``True`` if file has executable flag turned on.
419 Returns ``True`` if file has executable flag turned on.
420 """
420 """
421 return bool(self.mode & stat.S_IXUSR)
421 return bool(self.mode & stat.S_IXUSR)
422
422
423 def __repr__(self):
423 def __repr__(self):
424 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
424 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
425 self.changeset.short_id)
425 self.changeset.short_id)
426
426
427
427
428 class RemovedFileNode(FileNode):
428 class RemovedFileNode(FileNode):
429 """
429 """
430 Dummy FileNode class - trying to access any public attribute except path,
430 Dummy FileNode class - trying to access any public attribute except path,
431 name, kind or state (or methods/attributes checking those two) would raise
431 name, kind or state (or methods/attributes checking those two) would raise
432 RemovedFileNodeError.
432 RemovedFileNodeError.
433 """
433 """
434 ALLOWED_ATTRIBUTES = ['name', 'path', 'state', 'is_root', 'is_file',
434 ALLOWED_ATTRIBUTES = ['name', 'path', 'state', 'is_root', 'is_file',
435 'is_dir', 'kind', 'added', 'changed', 'not_changed', 'removed']
435 'is_dir', 'kind', 'added', 'changed', 'not_changed', 'removed']
436
436
437 def __init__(self, path):
437 def __init__(self, path):
438 """
438 """
439 :param path: relative path to the node
439 :param path: relative path to the node
440 """
440 """
441 super(RemovedFileNode, self).__init__(path=path)
441 super(RemovedFileNode, self).__init__(path=path)
442
442
443 def __getattribute__(self, attr):
443 def __getattribute__(self, attr):
444 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
444 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
445 return super(RemovedFileNode, self).__getattribute__(attr)
445 return super(RemovedFileNode, self).__getattribute__(attr)
446 raise RemovedFileNodeError("Cannot access attribute %s on "
446 raise RemovedFileNodeError("Cannot access attribute %s on "
447 "RemovedFileNode" % attr)
447 "RemovedFileNode" % attr)
448
448
449 @LazyProperty
449 @LazyProperty
450 def state(self):
450 def state(self):
451 return NodeState.REMOVED
451 return NodeState.REMOVED
452
452
453
453
454 class DirNode(Node):
454 class DirNode(Node):
455 """
455 """
456 DirNode stores list of files and directories within this node.
456 DirNode stores list of files and directories within this node.
457 Nodes may be used standalone but within repository context they
457 Nodes may be used standalone but within repository context they
458 lazily fetch data within same repositorty's changeset.
458 lazily fetch data within same repositorty's changeset.
459 """
459 """
460
460
461 def __init__(self, path, nodes=(), changeset=None):
461 def __init__(self, path, nodes=(), changeset=None):
462 """
462 """
463 Only one of ``nodes`` and ``changeset`` may be given. Passing both
463 Only one of ``nodes`` and ``changeset`` may be given. Passing both
464 would raise ``NodeError`` exception.
464 would raise ``NodeError`` exception.
465
465
466 :param path: relative path to the node
466 :param path: relative path to the node
467 :param nodes: content may be passed to constructor
467 :param nodes: content may be passed to constructor
468 :param changeset: if given, will use it to lazily fetch content
468 :param changeset: if given, will use it to lazily fetch content
469 :param size: always 0 for ``DirNode``
469 :param size: always 0 for ``DirNode``
470 """
470 """
471 if nodes and changeset:
471 if nodes and changeset:
472 raise NodeError("Cannot use both nodes and changeset")
472 raise NodeError("Cannot use both nodes and changeset")
473 super(DirNode, self).__init__(path, NodeKind.DIR)
473 super(DirNode, self).__init__(path, NodeKind.DIR)
474 self.changeset = changeset
474 self.changeset = changeset
475 self._nodes = nodes
475 self._nodes = nodes
476
476
477 @LazyProperty
477 @LazyProperty
478 def content(self):
478 def content(self):
479 raise NodeError("%s represents a dir and has no ``content`` attribute"
479 raise NodeError("%s represents a dir and has no ``content`` attribute"
480 % self)
480 % self)
481
481
482 @LazyProperty
482 @LazyProperty
483 def nodes(self):
483 def nodes(self):
484 if self.changeset:
484 if self.changeset:
485 nodes = self.changeset.get_nodes(self.path)
485 nodes = self.changeset.get_nodes(self.path)
486 else:
486 else:
487 nodes = self._nodes
487 nodes = self._nodes
488 self._nodes_dict = dict((node.path, node) for node in nodes)
488 self._nodes_dict = dict((node.path, node) for node in nodes)
489 return sorted(nodes)
489 return sorted(nodes)
490
490
491 @LazyProperty
491 @LazyProperty
492 def files(self):
492 def files(self):
493 return sorted((node for node in self.nodes if node.is_file()))
493 return sorted((node for node in self.nodes if node.is_file()))
494
494
495 @LazyProperty
495 @LazyProperty
496 def dirs(self):
496 def dirs(self):
497 return sorted((node for node in self.nodes if node.is_dir()))
497 return sorted((node for node in self.nodes if node.is_dir()))
498
498
499 def __iter__(self):
499 def __iter__(self):
500 for node in self.nodes:
500 for node in self.nodes:
501 yield node
501 yield node
502
502
503 def get_node(self, path):
503 def get_node(self, path):
504 """
504 """
505 Returns node from within this particular ``DirNode``, so it is now
505 Returns node from within this particular ``DirNode``, so it is now
506 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
506 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
507 'docs'. In order to access deeper nodes one must fetch nodes between
507 'docs'. In order to access deeper nodes one must fetch nodes between
508 them first - this would work::
508 them first - this would work::
509
509
510 docs = root.get_node('docs')
510 docs = root.get_node('docs')
511 docs.get_node('api').get_node('index.rst')
511 docs.get_node('api').get_node('index.rst')
512
512
513 :param: path - relative to the current node
513 :param: path - relative to the current node
514
514
515 .. note::
515 .. note::
516 To access lazily (as in example above) node have to be initialized
516 To access lazily (as in example above) node have to be initialized
517 with related changeset object - without it node is out of
517 with related changeset object - without it node is out of
518 context and may know nothing about anything else than nearest
518 context and may know nothing about anything else than nearest
519 (located at same level) nodes.
519 (located at same level) nodes.
520 """
520 """
521 try:
521 try:
522 path = path.rstrip('/')
522 path = path.rstrip('/')
523 if path == '':
523 if path == '':
524 raise NodeError("Cannot retrieve node without path")
524 raise NodeError("Cannot retrieve node without path")
525 self.nodes # access nodes first in order to set _nodes_dict
525 self.nodes # access nodes first in order to set _nodes_dict
526 paths = path.split('/')
526 paths = path.split('/')
527 if len(paths) == 1:
527 if len(paths) == 1:
528 if not self.is_root():
528 if not self.is_root():
529 path = '/'.join((self.path, paths[0]))
529 path = '/'.join((self.path, paths[0]))
530 else:
530 else:
531 path = paths[0]
531 path = paths[0]
532 return self._nodes_dict[path]
532 return self._nodes_dict[path]
533 elif len(paths) > 1:
533 elif len(paths) > 1:
534 if self.changeset is None:
534 if self.changeset is None:
535 raise NodeError("Cannot access deeper "
535 raise NodeError("Cannot access deeper "
536 "nodes without changeset")
536 "nodes without changeset")
537 else:
537 else:
538 path1, path2 = paths[0], '/'.join(paths[1:])
538 path1, path2 = paths[0], '/'.join(paths[1:])
539 return self.get_node(path1).get_node(path2)
539 return self.get_node(path1).get_node(path2)
540 else:
540 else:
541 raise KeyError
541 raise KeyError
542 except KeyError:
542 except KeyError:
543 raise NodeError("Node does not exist at %s" % path)
543 raise NodeError("Node does not exist at %s" % path)
544
544
545 @LazyProperty
545 @LazyProperty
546 def state(self):
546 def state(self):
547 raise NodeError("Cannot access state of DirNode")
547 raise NodeError("Cannot access state of DirNode")
548
548
549 @LazyProperty
549 @LazyProperty
550 def size(self):
550 def size(self):
551 size = 0
551 size = 0
552 for root, dirs, files in self.changeset.walk(self.path):
552 for root, dirs, files in self.changeset.walk(self.path):
553 for f in files:
553 for f in files:
554 size += f.size
554 size += f.size
555
555
556 return size
556 return size
557
557
558 def __repr__(self):
558 def __repr__(self):
559 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
559 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
560 self.changeset.short_id)
560 self.changeset.short_id)
561
561
562
562
563 class RootNode(DirNode):
563 class RootNode(DirNode):
564 """
564 """
565 DirNode being the root node of the repository.
565 DirNode being the root node of the repository.
566 """
566 """
567
567
568 def __init__(self, nodes=(), changeset=None):
568 def __init__(self, nodes=(), changeset=None):
569 super(RootNode, self).__init__(path='', nodes=nodes,
569 super(RootNode, self).__init__(path='', nodes=nodes,
570 changeset=changeset)
570 changeset=changeset)
571
571
572 def __repr__(self):
572 def __repr__(self):
573 return '<%s>' % self.__class__.__name__
573 return '<%s>' % self.__class__.__name__
574
574
575
575
576 class SubModuleNode(Node):
576 class SubModuleNode(Node):
577 """
577 """
578 represents a SubModule of Git or SubRepo of Mercurial
578 represents a SubModule of Git or SubRepo of Mercurial
579 """
579 """
580 is_binary = False
580 is_binary = False
581 size = 0
581 size = 0
582
582
583 def __init__(self, name, url=None, changeset=None, alias=None):
583 def __init__(self, name, url=None, changeset=None, alias=None):
584 self.path = name
584 self.path = name
585 self.kind = NodeKind.SUBMODULE
585 self.kind = NodeKind.SUBMODULE
586 self.alias = alias
586 self.alias = alias
587 # we have to use emptyChangeset here since this can point to svn/git/hg
587 # we have to use emptyChangeset here since this can point to svn/git/hg
588 # submodules we cannot get from repository
588 # submodules we cannot get from repository
589 self.changeset = EmptyChangeset(str(changeset), alias=alias)
589 self.changeset = EmptyChangeset(str(changeset), alias=alias)
590 self.url = url or self._extract_submodule_url()
590 self.url = url or self._extract_submodule_url()
591
591
592 def __repr__(self):
592 def __repr__(self):
593 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
593 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
594 self.changeset.short_id)
594 self.changeset.short_id)
595
595
596 def _extract_submodule_url(self):
596 def _extract_submodule_url(self):
597 if self.alias == 'git':
597 if self.alias == 'git':
598 #TODO: find a way to parse gits submodule file and extract the
598 #TODO: find a way to parse gits submodule file and extract the
599 # linking URL
599 # linking URL
600 return self.path
600 return self.path
601 if self.alias == 'hg':
601 if self.alias == 'hg':
602 return self.path
602 return self.path
603
603
604 @LazyProperty
604 @LazyProperty
605 def name(self):
605 def name(self):
606 """
606 """
607 Returns name of the node so if its path
607 Returns name of the node so if its path
608 then only last part is returned.
608 then only last part is returned.
609 """
609 """
610 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
610 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
611 return u'%s @ %s' % (org, self.changeset.short_id)
611 return u'%s @ %s' % (org, self.changeset.short_id)
General Comments 0
You need to be logged in to leave comments. Login now