##// END OF EJS Templates
Speed up of last_changeset extraction in VCS, in edge cases for git we can get 10x speed improvement by limiting the history extraction if we only need last changeset
marcink -
r3496:58905069 beta
parent child Browse files
Show More
@@ -1,1018 +1,1019 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.base
3 vcs.backends.base
4 ~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~
5
5
6 Base for all available scm backends
6 Base for all available scm backends
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12
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 import datetime
23
24
24
25
25 class BaseRepository(object):
26 class BaseRepository(object):
26 """
27 """
27 Base Repository for final backends
28 Base Repository for final backends
28
29
29 **Attributes**
30 **Attributes**
30
31
31 ``DEFAULT_BRANCH_NAME``
32 ``DEFAULT_BRANCH_NAME``
32 name of default branch (i.e. "trunk" for svn, "master" for git etc.
33 name of default branch (i.e. "trunk" for svn, "master" for git etc.
33
34
34 ``scm``
35 ``scm``
35 alias of scm, i.e. *git* or *hg*
36 alias of scm, i.e. *git* or *hg*
36
37
37 ``repo``
38 ``repo``
38 object from external api
39 object from external api
39
40
40 ``revisions``
41 ``revisions``
41 list of all available revisions' ids, in ascending order
42 list of all available revisions' ids, in ascending order
42
43
43 ``changesets``
44 ``changesets``
44 storage dict caching returned changesets
45 storage dict caching returned changesets
45
46
46 ``path``
47 ``path``
47 absolute path to the repository
48 absolute path to the repository
48
49
49 ``branches``
50 ``branches``
50 branches as list of changesets
51 branches as list of changesets
51
52
52 ``tags``
53 ``tags``
53 tags as list of changesets
54 tags as list of changesets
54 """
55 """
55 scm = None
56 scm = None
56 DEFAULT_BRANCH_NAME = None
57 DEFAULT_BRANCH_NAME = None
57 EMPTY_CHANGESET = '0' * 40
58 EMPTY_CHANGESET = '0' * 40
58
59
59 def __init__(self, repo_path, create=False, **kwargs):
60 def __init__(self, repo_path, create=False, **kwargs):
60 """
61 """
61 Initializes repository. Raises RepositoryError if repository could
62 Initializes repository. Raises RepositoryError if repository could
62 not be find at the given ``repo_path`` or directory at ``repo_path``
63 not be find at the given ``repo_path`` or directory at ``repo_path``
63 exists and ``create`` is set to True.
64 exists and ``create`` is set to True.
64
65
65 :param repo_path: local path of the repository
66 :param repo_path: local path of the repository
66 :param create=False: if set to True, would try to craete repository.
67 :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
68 :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 -
69 would be cloned; requires ``create`` parameter to be set to True -
69 raises RepositoryError if src_url is set and create evaluates to
70 raises RepositoryError if src_url is set and create evaluates to
70 False
71 False
71 """
72 """
72 raise NotImplementedError
73 raise NotImplementedError
73
74
74 def __str__(self):
75 def __str__(self):
75 return '<%s at %s>' % (self.__class__.__name__, self.path)
76 return '<%s at %s>' % (self.__class__.__name__, self.path)
76
77
77 def __repr__(self):
78 def __repr__(self):
78 return self.__str__()
79 return self.__str__()
79
80
80 def __len__(self):
81 def __len__(self):
81 return self.count()
82 return self.count()
82
83
83 @LazyProperty
84 @LazyProperty
84 def alias(self):
85 def alias(self):
85 for k, v in settings.BACKENDS.items():
86 for k, v in settings.BACKENDS.items():
86 if v.split('.')[-1] == str(self.__class__.__name__):
87 if v.split('.')[-1] == str(self.__class__.__name__):
87 return k
88 return k
88
89
89 @LazyProperty
90 @LazyProperty
90 def name(self):
91 def name(self):
91 raise NotImplementedError
92 raise NotImplementedError
92
93
93 @LazyProperty
94 @LazyProperty
94 def owner(self):
95 def owner(self):
95 raise NotImplementedError
96 raise NotImplementedError
96
97
97 @LazyProperty
98 @LazyProperty
98 def description(self):
99 def description(self):
99 raise NotImplementedError
100 raise NotImplementedError
100
101
101 @LazyProperty
102 @LazyProperty
102 def size(self):
103 def size(self):
103 """
104 """
104 Returns combined size in bytes for all repository files
105 Returns combined size in bytes for all repository files
105 """
106 """
106
107
107 size = 0
108 size = 0
108 try:
109 try:
109 tip = self.get_changeset()
110 tip = self.get_changeset()
110 for topnode, dirs, files in tip.walk('/'):
111 for topnode, dirs, files in tip.walk('/'):
111 for f in files:
112 for f in files:
112 size += tip.get_file_size(f.path)
113 size += tip.get_file_size(f.path)
113 for dir in dirs:
114 for dir in dirs:
114 for f in files:
115 for f in files:
115 size += tip.get_file_size(f.path)
116 size += tip.get_file_size(f.path)
116
117
117 except RepositoryError, e:
118 except RepositoryError, e:
118 pass
119 pass
119 return size
120 return size
120
121
121 def is_valid(self):
122 def is_valid(self):
122 """
123 """
123 Validates repository.
124 Validates repository.
124 """
125 """
125 raise NotImplementedError
126 raise NotImplementedError
126
127
127 def get_last_change(self):
128 def get_last_change(self):
128 self.get_changesets()
129 self.get_changesets()
129
130
130 #==========================================================================
131 #==========================================================================
131 # CHANGESETS
132 # CHANGESETS
132 #==========================================================================
133 #==========================================================================
133
134
134 def get_changeset(self, revision=None):
135 def get_changeset(self, revision=None):
135 """
136 """
136 Returns instance of ``Changeset`` class. If ``revision`` is None, most
137 Returns instance of ``Changeset`` class. If ``revision`` is None, most
137 recent changeset is returned.
138 recent changeset is returned.
138
139
139 :raises ``EmptyRepositoryError``: if there are no revisions
140 :raises ``EmptyRepositoryError``: if there are no revisions
140 """
141 """
141 raise NotImplementedError
142 raise NotImplementedError
142
143
143 def __iter__(self):
144 def __iter__(self):
144 """
145 """
145 Allows Repository objects to be iterated.
146 Allows Repository objects to be iterated.
146
147
147 *Requires* implementation of ``__getitem__`` method.
148 *Requires* implementation of ``__getitem__`` method.
148 """
149 """
149 for revision in self.revisions:
150 for revision in self.revisions:
150 yield self.get_changeset(revision)
151 yield self.get_changeset(revision)
151
152
152 def get_changesets(self, start=None, end=None, start_date=None,
153 def get_changesets(self, start=None, end=None, start_date=None,
153 end_date=None, branch_name=None, reverse=False):
154 end_date=None, branch_name=None, reverse=False):
154 """
155 """
155 Returns iterator of ``MercurialChangeset`` objects from start to end
156 Returns iterator of ``MercurialChangeset`` objects from start to end
156 not inclusive This should behave just like a list, ie. end is not
157 not inclusive This should behave just like a list, ie. end is not
157 inclusive
158 inclusive
158
159
159 :param start: None or str
160 :param start: None or str
160 :param end: None or str
161 :param end: None or str
161 :param start_date:
162 :param start_date:
162 :param end_date:
163 :param end_date:
163 :param branch_name:
164 :param branch_name:
164 :param reversed:
165 :param reversed:
165 """
166 """
166 raise NotImplementedError
167 raise NotImplementedError
167
168
168 def __getslice__(self, i, j):
169 def __getslice__(self, i, j):
169 """
170 """
170 Returns a iterator of sliced repository
171 Returns a iterator of sliced repository
171 """
172 """
172 for rev in self.revisions[i:j]:
173 for rev in self.revisions[i:j]:
173 yield self.get_changeset(rev)
174 yield self.get_changeset(rev)
174
175
175 def __getitem__(self, key):
176 def __getitem__(self, key):
176 return self.get_changeset(key)
177 return self.get_changeset(key)
177
178
178 def count(self):
179 def count(self):
179 return len(self.revisions)
180 return len(self.revisions)
180
181
181 def tag(self, name, user, revision=None, message=None, date=None, **opts):
182 def tag(self, name, user, revision=None, message=None, date=None, **opts):
182 """
183 """
183 Creates and returns a tag for the given ``revision``.
184 Creates and returns a tag for the given ``revision``.
184
185
185 :param name: name for new tag
186 :param name: name for new tag
186 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
187 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
187 :param revision: changeset id for which new tag would be created
188 :param revision: changeset id for which new tag would be created
188 :param message: message of the tag's commit
189 :param message: message of the tag's commit
189 :param date: date of tag's commit
190 :param date: date of tag's commit
190
191
191 :raises TagAlreadyExistError: if tag with same name already exists
192 :raises TagAlreadyExistError: if tag with same name already exists
192 """
193 """
193 raise NotImplementedError
194 raise NotImplementedError
194
195
195 def remove_tag(self, name, user, message=None, date=None):
196 def remove_tag(self, name, user, message=None, date=None):
196 """
197 """
197 Removes tag with the given ``name``.
198 Removes tag with the given ``name``.
198
199
199 :param name: name of the tag to be removed
200 :param name: name of the tag to be removed
200 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
201 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
201 :param message: message of the tag's removal commit
202 :param message: message of the tag's removal commit
202 :param date: date of tag's removal commit
203 :param date: date of tag's removal commit
203
204
204 :raises TagDoesNotExistError: if tag with given name does not exists
205 :raises TagDoesNotExistError: if tag with given name does not exists
205 """
206 """
206 raise NotImplementedError
207 raise NotImplementedError
207
208
208 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
209 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
209 context=3):
210 context=3):
210 """
211 """
211 Returns (git like) *diff*, as plain text. Shows changes introduced by
212 Returns (git like) *diff*, as plain text. Shows changes introduced by
212 ``rev2`` since ``rev1``.
213 ``rev2`` since ``rev1``.
213
214
214 :param rev1: Entry point from which diff is shown. Can be
215 :param rev1: Entry point from which diff is shown. Can be
215 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
216 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
216 the changes since empty state of the repository until ``rev2``
217 the changes since empty state of the repository until ``rev2``
217 :param rev2: Until which revision changes should be shown.
218 :param rev2: Until which revision changes should be shown.
218 :param ignore_whitespace: If set to ``True``, would not show whitespace
219 :param ignore_whitespace: If set to ``True``, would not show whitespace
219 changes. Defaults to ``False``.
220 changes. Defaults to ``False``.
220 :param context: How many lines before/after changed lines should be
221 :param context: How many lines before/after changed lines should be
221 shown. Defaults to ``3``.
222 shown. Defaults to ``3``.
222 """
223 """
223 raise NotImplementedError
224 raise NotImplementedError
224
225
225 # ========== #
226 # ========== #
226 # COMMIT API #
227 # COMMIT API #
227 # ========== #
228 # ========== #
228
229
229 @LazyProperty
230 @LazyProperty
230 def in_memory_changeset(self):
231 def in_memory_changeset(self):
231 """
232 """
232 Returns ``InMemoryChangeset`` object for this repository.
233 Returns ``InMemoryChangeset`` object for this repository.
233 """
234 """
234 raise NotImplementedError
235 raise NotImplementedError
235
236
236 def add(self, filenode, **kwargs):
237 def add(self, filenode, **kwargs):
237 """
238 """
238 Commit api function that will add given ``FileNode`` into this
239 Commit api function that will add given ``FileNode`` into this
239 repository.
240 repository.
240
241
241 :raises ``NodeAlreadyExistsError``: if there is a file with same path
242 :raises ``NodeAlreadyExistsError``: if there is a file with same path
242 already in repository
243 already in repository
243 :raises ``NodeAlreadyAddedError``: if given node is already marked as
244 :raises ``NodeAlreadyAddedError``: if given node is already marked as
244 *added*
245 *added*
245 """
246 """
246 raise NotImplementedError
247 raise NotImplementedError
247
248
248 def remove(self, filenode, **kwargs):
249 def remove(self, filenode, **kwargs):
249 """
250 """
250 Commit api function that will remove given ``FileNode`` into this
251 Commit api function that will remove given ``FileNode`` into this
251 repository.
252 repository.
252
253
253 :raises ``EmptyRepositoryError``: if there are no changesets yet
254 :raises ``EmptyRepositoryError``: if there are no changesets yet
254 :raises ``NodeDoesNotExistError``: if there is no file with given path
255 :raises ``NodeDoesNotExistError``: if there is no file with given path
255 """
256 """
256 raise NotImplementedError
257 raise NotImplementedError
257
258
258 def commit(self, message, **kwargs):
259 def commit(self, message, **kwargs):
259 """
260 """
260 Persists current changes made on this repository and returns newly
261 Persists current changes made on this repository and returns newly
261 created changeset.
262 created changeset.
262
263
263 :raises ``NothingChangedError``: if no changes has been made
264 :raises ``NothingChangedError``: if no changes has been made
264 """
265 """
265 raise NotImplementedError
266 raise NotImplementedError
266
267
267 def get_state(self):
268 def get_state(self):
268 """
269 """
269 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
270 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
270 containing ``FileNode`` objects.
271 containing ``FileNode`` objects.
271 """
272 """
272 raise NotImplementedError
273 raise NotImplementedError
273
274
274 def get_config_value(self, section, name, config_file=None):
275 def get_config_value(self, section, name, config_file=None):
275 """
276 """
276 Returns configuration value for a given [``section``] and ``name``.
277 Returns configuration value for a given [``section``] and ``name``.
277
278
278 :param section: Section we want to retrieve value from
279 :param section: Section we want to retrieve value from
279 :param name: Name of configuration we want to retrieve
280 :param name: Name of configuration we want to retrieve
280 :param config_file: A path to file which should be used to retrieve
281 :param config_file: A path to file which should be used to retrieve
281 configuration from (might also be a list of file paths)
282 configuration from (might also be a list of file paths)
282 """
283 """
283 raise NotImplementedError
284 raise NotImplementedError
284
285
285 def get_user_name(self, config_file=None):
286 def get_user_name(self, config_file=None):
286 """
287 """
287 Returns user's name from global configuration file.
288 Returns user's name from global configuration file.
288
289
289 :param config_file: A path to file which should be used to retrieve
290 :param config_file: A path to file which should be used to retrieve
290 configuration from (might also be a list of file paths)
291 configuration from (might also be a list of file paths)
291 """
292 """
292 raise NotImplementedError
293 raise NotImplementedError
293
294
294 def get_user_email(self, config_file=None):
295 def get_user_email(self, config_file=None):
295 """
296 """
296 Returns user's email from global configuration file.
297 Returns user's email from global configuration file.
297
298
298 :param config_file: A path to file which should be used to retrieve
299 :param config_file: A path to file which should be used to retrieve
299 configuration from (might also be a list of file paths)
300 configuration from (might also be a list of file paths)
300 """
301 """
301 raise NotImplementedError
302 raise NotImplementedError
302
303
303 # =========== #
304 # =========== #
304 # WORKDIR API #
305 # WORKDIR API #
305 # =========== #
306 # =========== #
306
307
307 @LazyProperty
308 @LazyProperty
308 def workdir(self):
309 def workdir(self):
309 """
310 """
310 Returns ``Workdir`` instance for this repository.
311 Returns ``Workdir`` instance for this repository.
311 """
312 """
312 raise NotImplementedError
313 raise NotImplementedError
313
314
314 def inject_ui(self, **extras):
315 def inject_ui(self, **extras):
315 """
316 """
316 Injects extra parameters into UI object of this repo
317 Injects extra parameters into UI object of this repo
317 """
318 """
318 required_extras = [
319 required_extras = [
319 'ip',
320 'ip',
320 'username',
321 'username',
321 'action',
322 'action',
322 'repository',
323 'repository',
323 'scm',
324 'scm',
324 'config',
325 'config',
325 'server_url',
326 'server_url',
326 'make_lock',
327 'make_lock',
327 'locked_by',
328 'locked_by',
328 ]
329 ]
329 for req in required_extras:
330 for req in required_extras:
330 if req not in extras:
331 if req not in extras:
331 raise AttributeError('Missing attribute %s in extras' % (req))
332 raise AttributeError('Missing attribute %s in extras' % (req))
332 for k, v in extras.items():
333 for k, v in extras.items():
333 self._repo.ui.setconfig('rhodecode_extras', k, v)
334 self._repo.ui.setconfig('rhodecode_extras', k, v)
334
335
335
336
336 class BaseChangeset(object):
337 class BaseChangeset(object):
337 """
338 """
338 Each backend should implement it's changeset representation.
339 Each backend should implement it's changeset representation.
339
340
340 **Attributes**
341 **Attributes**
341
342
342 ``repository``
343 ``repository``
343 repository object within which changeset exists
344 repository object within which changeset exists
344
345
345 ``id``
346 ``id``
346 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
347 may be ``raw_id`` or i.e. for mercurial's tip just ``tip``
347
348
348 ``raw_id``
349 ``raw_id``
349 raw changeset representation (i.e. full 40 length sha for git
350 raw changeset representation (i.e. full 40 length sha for git
350 backend)
351 backend)
351
352
352 ``short_id``
353 ``short_id``
353 shortened (if apply) version of ``raw_id``; it would be simple
354 shortened (if apply) version of ``raw_id``; it would be simple
354 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
355 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
355 as ``raw_id`` for subversion
356 as ``raw_id`` for subversion
356
357
357 ``revision``
358 ``revision``
358 revision number as integer
359 revision number as integer
359
360
360 ``files``
361 ``files``
361 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
362 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
362
363
363 ``dirs``
364 ``dirs``
364 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
365 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
365
366
366 ``nodes``
367 ``nodes``
367 combined list of ``Node`` objects
368 combined list of ``Node`` objects
368
369
369 ``author``
370 ``author``
370 author of the changeset, as unicode
371 author of the changeset, as unicode
371
372
372 ``message``
373 ``message``
373 message of the changeset, as unicode
374 message of the changeset, as unicode
374
375
375 ``parents``
376 ``parents``
376 list of parent changesets
377 list of parent changesets
377
378
378 ``last``
379 ``last``
379 ``True`` if this is last changeset in repository, ``False``
380 ``True`` if this is last changeset in repository, ``False``
380 otherwise; trying to access this attribute while there is no
381 otherwise; trying to access this attribute while there is no
381 changesets would raise ``EmptyRepositoryError``
382 changesets would raise ``EmptyRepositoryError``
382 """
383 """
383 def __str__(self):
384 def __str__(self):
384 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
385 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
385 self.short_id)
386 self.short_id)
386
387
387 def __repr__(self):
388 def __repr__(self):
388 return self.__str__()
389 return self.__str__()
389
390
390 def __unicode__(self):
391 def __unicode__(self):
391 return u'%s:%s' % (self.revision, self.short_id)
392 return u'%s:%s' % (self.revision, self.short_id)
392
393
393 def __eq__(self, other):
394 def __eq__(self, other):
394 return self.raw_id == other.raw_id
395 return self.raw_id == other.raw_id
395
396
396 def __json__(self):
397 def __json__(self):
397 return dict(
398 return dict(
398 short_id=self.short_id,
399 short_id=self.short_id,
399 raw_id=self.raw_id,
400 raw_id=self.raw_id,
400 revision=self.revision,
401 revision=self.revision,
401 message=self.message,
402 message=self.message,
402 date=self.date,
403 date=self.date,
403 author=self.author,
404 author=self.author,
404 )
405 )
405
406
406 @LazyProperty
407 @LazyProperty
407 def last(self):
408 def last(self):
408 if self.repository is None:
409 if self.repository is None:
409 raise ChangesetError("Cannot check if it's most recent revision")
410 raise ChangesetError("Cannot check if it's most recent revision")
410 return self.raw_id == self.repository.revisions[-1]
411 return self.raw_id == self.repository.revisions[-1]
411
412
412 @LazyProperty
413 @LazyProperty
413 def parents(self):
414 def parents(self):
414 """
415 """
415 Returns list of parents changesets.
416 Returns list of parents changesets.
416 """
417 """
417 raise NotImplementedError
418 raise NotImplementedError
418
419
419 @LazyProperty
420 @LazyProperty
420 def children(self):
421 def children(self):
421 """
422 """
422 Returns list of children changesets.
423 Returns list of children changesets.
423 """
424 """
424 raise NotImplementedError
425 raise NotImplementedError
425
426
426 @LazyProperty
427 @LazyProperty
427 def id(self):
428 def id(self):
428 """
429 """
429 Returns string identifying this changeset.
430 Returns string identifying this changeset.
430 """
431 """
431 raise NotImplementedError
432 raise NotImplementedError
432
433
433 @LazyProperty
434 @LazyProperty
434 def raw_id(self):
435 def raw_id(self):
435 """
436 """
436 Returns raw string identifying this changeset.
437 Returns raw string identifying this changeset.
437 """
438 """
438 raise NotImplementedError
439 raise NotImplementedError
439
440
440 @LazyProperty
441 @LazyProperty
441 def short_id(self):
442 def short_id(self):
442 """
443 """
443 Returns shortened version of ``raw_id`` attribute, as string,
444 Returns shortened version of ``raw_id`` attribute, as string,
444 identifying this changeset, useful for web representation.
445 identifying this changeset, useful for web representation.
445 """
446 """
446 raise NotImplementedError
447 raise NotImplementedError
447
448
448 @LazyProperty
449 @LazyProperty
449 def revision(self):
450 def revision(self):
450 """
451 """
451 Returns integer identifying this changeset.
452 Returns integer identifying this changeset.
452
453
453 """
454 """
454 raise NotImplementedError
455 raise NotImplementedError
455
456
456 @LazyProperty
457 @LazyProperty
457 def committer(self):
458 def committer(self):
458 """
459 """
459 Returns Committer for given commit
460 Returns Committer for given commit
460 """
461 """
461
462
462 raise NotImplementedError
463 raise NotImplementedError
463
464
464 @LazyProperty
465 @LazyProperty
465 def committer_name(self):
466 def committer_name(self):
466 """
467 """
467 Returns Author name for given commit
468 Returns Author name for given commit
468 """
469 """
469
470
470 return author_name(self.committer)
471 return author_name(self.committer)
471
472
472 @LazyProperty
473 @LazyProperty
473 def committer_email(self):
474 def committer_email(self):
474 """
475 """
475 Returns Author email address for given commit
476 Returns Author email address for given commit
476 """
477 """
477
478
478 return author_email(self.committer)
479 return author_email(self.committer)
479
480
480 @LazyProperty
481 @LazyProperty
481 def author(self):
482 def author(self):
482 """
483 """
483 Returns Author for given commit
484 Returns Author for given commit
484 """
485 """
485
486
486 raise NotImplementedError
487 raise NotImplementedError
487
488
488 @LazyProperty
489 @LazyProperty
489 def author_name(self):
490 def author_name(self):
490 """
491 """
491 Returns Author name for given commit
492 Returns Author name for given commit
492 """
493 """
493
494
494 return author_name(self.author)
495 return author_name(self.author)
495
496
496 @LazyProperty
497 @LazyProperty
497 def author_email(self):
498 def author_email(self):
498 """
499 """
499 Returns Author email address for given commit
500 Returns Author email address for given commit
500 """
501 """
501
502
502 return author_email(self.author)
503 return author_email(self.author)
503
504
504 def get_file_mode(self, path):
505 def get_file_mode(self, path):
505 """
506 """
506 Returns stat mode of the file at the given ``path``.
507 Returns stat mode of the file at the given ``path``.
507 """
508 """
508 raise NotImplementedError
509 raise NotImplementedError
509
510
510 def get_file_content(self, path):
511 def get_file_content(self, path):
511 """
512 """
512 Returns content of the file at the given ``path``.
513 Returns content of the file at the given ``path``.
513 """
514 """
514 raise NotImplementedError
515 raise NotImplementedError
515
516
516 def get_file_size(self, path):
517 def get_file_size(self, path):
517 """
518 """
518 Returns size of the file at the given ``path``.
519 Returns size of the file at the given ``path``.
519 """
520 """
520 raise NotImplementedError
521 raise NotImplementedError
521
522
522 def get_file_changeset(self, path):
523 def get_file_changeset(self, path):
523 """
524 """
524 Returns last commit of the file at the given ``path``.
525 Returns last commit of the file at the given ``path``.
525 """
526 """
526 raise NotImplementedError
527 raise NotImplementedError
527
528
528 def get_file_history(self, path):
529 def get_file_history(self, path):
529 """
530 """
530 Returns history of file as reversed list of ``Changeset`` objects for
531 Returns history of file as reversed list of ``Changeset`` objects for
531 which file at given ``path`` has been modified.
532 which file at given ``path`` has been modified.
532 """
533 """
533 raise NotImplementedError
534 raise NotImplementedError
534
535
535 def get_nodes(self, path):
536 def get_nodes(self, path):
536 """
537 """
537 Returns combined ``DirNode`` and ``FileNode`` objects list representing
538 Returns combined ``DirNode`` and ``FileNode`` objects list representing
538 state of changeset at the given ``path``.
539 state of changeset at the given ``path``.
539
540
540 :raises ``ChangesetError``: if node at the given ``path`` is not
541 :raises ``ChangesetError``: if node at the given ``path`` is not
541 instance of ``DirNode``
542 instance of ``DirNode``
542 """
543 """
543 raise NotImplementedError
544 raise NotImplementedError
544
545
545 def get_node(self, path):
546 def get_node(self, path):
546 """
547 """
547 Returns ``Node`` object from the given ``path``.
548 Returns ``Node`` object from the given ``path``.
548
549
549 :raises ``NodeDoesNotExistError``: if there is no node at the given
550 :raises ``NodeDoesNotExistError``: if there is no node at the given
550 ``path``
551 ``path``
551 """
552 """
552 raise NotImplementedError
553 raise NotImplementedError
553
554
554 def fill_archive(self, stream=None, kind='tgz', prefix=None):
555 def fill_archive(self, stream=None, kind='tgz', prefix=None):
555 """
556 """
556 Fills up given stream.
557 Fills up given stream.
557
558
558 :param stream: file like object.
559 :param stream: file like object.
559 :param kind: one of following: ``zip``, ``tar``, ``tgz``
560 :param kind: one of following: ``zip``, ``tar``, ``tgz``
560 or ``tbz2``. Default: ``tgz``.
561 or ``tbz2``. Default: ``tgz``.
561 :param prefix: name of root directory in archive.
562 :param prefix: name of root directory in archive.
562 Default is repository name and changeset's raw_id joined with dash.
563 Default is repository name and changeset's raw_id joined with dash.
563
564
564 repo-tip.<kind>
565 repo-tip.<kind>
565 """
566 """
566
567
567 raise NotImplementedError
568 raise NotImplementedError
568
569
569 def get_chunked_archive(self, **kwargs):
570 def get_chunked_archive(self, **kwargs):
570 """
571 """
571 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
572 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
572
573
573 :param chunk_size: extra parameter which controls size of returned
574 :param chunk_size: extra parameter which controls size of returned
574 chunks. Default:8k.
575 chunks. Default:8k.
575 """
576 """
576
577
577 chunk_size = kwargs.pop('chunk_size', 8192)
578 chunk_size = kwargs.pop('chunk_size', 8192)
578 stream = kwargs.get('stream')
579 stream = kwargs.get('stream')
579 self.fill_archive(**kwargs)
580 self.fill_archive(**kwargs)
580 while True:
581 while True:
581 data = stream.read(chunk_size)
582 data = stream.read(chunk_size)
582 if not data:
583 if not data:
583 break
584 break
584 yield data
585 yield data
585
586
586 @LazyProperty
587 @LazyProperty
587 def root(self):
588 def root(self):
588 """
589 """
589 Returns ``RootNode`` object for this changeset.
590 Returns ``RootNode`` object for this changeset.
590 """
591 """
591 return self.get_node('')
592 return self.get_node('')
592
593
593 def next(self, branch=None):
594 def next(self, branch=None):
594 """
595 """
595 Returns next changeset from current, if branch is gives it will return
596 Returns next changeset from current, if branch is gives it will return
596 next changeset belonging to this branch
597 next changeset belonging to this branch
597
598
598 :param branch: show changesets within the given named branch
599 :param branch: show changesets within the given named branch
599 """
600 """
600 raise NotImplementedError
601 raise NotImplementedError
601
602
602 def prev(self, branch=None):
603 def prev(self, branch=None):
603 """
604 """
604 Returns previous changeset from current, if branch is gives it will
605 Returns previous changeset from current, if branch is gives it will
605 return previous changeset belonging to this branch
606 return previous changeset belonging to this branch
606
607
607 :param branch: show changesets within the given named branch
608 :param branch: show changesets within the given named branch
608 """
609 """
609 raise NotImplementedError
610 raise NotImplementedError
610
611
611 @LazyProperty
612 @LazyProperty
612 def added(self):
613 def added(self):
613 """
614 """
614 Returns list of added ``FileNode`` objects.
615 Returns list of added ``FileNode`` objects.
615 """
616 """
616 raise NotImplementedError
617 raise NotImplementedError
617
618
618 @LazyProperty
619 @LazyProperty
619 def changed(self):
620 def changed(self):
620 """
621 """
621 Returns list of modified ``FileNode`` objects.
622 Returns list of modified ``FileNode`` objects.
622 """
623 """
623 raise NotImplementedError
624 raise NotImplementedError
624
625
625 @LazyProperty
626 @LazyProperty
626 def removed(self):
627 def removed(self):
627 """
628 """
628 Returns list of removed ``FileNode`` objects.
629 Returns list of removed ``FileNode`` objects.
629 """
630 """
630 raise NotImplementedError
631 raise NotImplementedError
631
632
632 @LazyProperty
633 @LazyProperty
633 def size(self):
634 def size(self):
634 """
635 """
635 Returns total number of bytes from contents of all filenodes.
636 Returns total number of bytes from contents of all filenodes.
636 """
637 """
637 return sum((node.size for node in self.get_filenodes_generator()))
638 return sum((node.size for node in self.get_filenodes_generator()))
638
639
639 def walk(self, topurl=''):
640 def walk(self, topurl=''):
640 """
641 """
641 Similar to os.walk method. Insted of filesystem it walks through
642 Similar to os.walk method. Insted of filesystem it walks through
642 changeset starting at given ``topurl``. Returns generator of tuples
643 changeset starting at given ``topurl``. Returns generator of tuples
643 (topnode, dirnodes, filenodes).
644 (topnode, dirnodes, filenodes).
644 """
645 """
645 topnode = self.get_node(topurl)
646 topnode = self.get_node(topurl)
646 yield (topnode, topnode.dirs, topnode.files)
647 yield (topnode, topnode.dirs, topnode.files)
647 for dirnode in topnode.dirs:
648 for dirnode in topnode.dirs:
648 for tup in self.walk(dirnode.path):
649 for tup in self.walk(dirnode.path):
649 yield tup
650 yield tup
650
651
651 def get_filenodes_generator(self):
652 def get_filenodes_generator(self):
652 """
653 """
653 Returns generator that yields *all* file nodes.
654 Returns generator that yields *all* file nodes.
654 """
655 """
655 for topnode, dirs, files in self.walk():
656 for topnode, dirs, files in self.walk():
656 for node in files:
657 for node in files:
657 yield node
658 yield node
658
659
659 def as_dict(self):
660 def as_dict(self):
660 """
661 """
661 Returns dictionary with changeset's attributes and their values.
662 Returns dictionary with changeset's attributes and their values.
662 """
663 """
663 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
664 data = get_dict_for_attrs(self, ['id', 'raw_id', 'short_id',
664 'revision', 'date', 'message'])
665 'revision', 'date', 'message'])
665 data['author'] = {'name': self.author_name, 'email': self.author_email}
666 data['author'] = {'name': self.author_name, 'email': self.author_email}
666 data['added'] = [node.path for node in self.added]
667 data['added'] = [node.path for node in self.added]
667 data['changed'] = [node.path for node in self.changed]
668 data['changed'] = [node.path for node in self.changed]
668 data['removed'] = [node.path for node in self.removed]
669 data['removed'] = [node.path for node in self.removed]
669 return data
670 return data
670
671
671
672
672 class BaseWorkdir(object):
673 class BaseWorkdir(object):
673 """
674 """
674 Working directory representation of single repository.
675 Working directory representation of single repository.
675
676
676 :attribute: repository: repository object of working directory
677 :attribute: repository: repository object of working directory
677 """
678 """
678
679
679 def __init__(self, repository):
680 def __init__(self, repository):
680 self.repository = repository
681 self.repository = repository
681
682
682 def get_branch(self):
683 def get_branch(self):
683 """
684 """
684 Returns name of current branch.
685 Returns name of current branch.
685 """
686 """
686 raise NotImplementedError
687 raise NotImplementedError
687
688
688 def get_changeset(self):
689 def get_changeset(self):
689 """
690 """
690 Returns current changeset.
691 Returns current changeset.
691 """
692 """
692 raise NotImplementedError
693 raise NotImplementedError
693
694
694 def get_added(self):
695 def get_added(self):
695 """
696 """
696 Returns list of ``FileNode`` objects marked as *new* in working
697 Returns list of ``FileNode`` objects marked as *new* in working
697 directory.
698 directory.
698 """
699 """
699 raise NotImplementedError
700 raise NotImplementedError
700
701
701 def get_changed(self):
702 def get_changed(self):
702 """
703 """
703 Returns list of ``FileNode`` objects *changed* in working directory.
704 Returns list of ``FileNode`` objects *changed* in working directory.
704 """
705 """
705 raise NotImplementedError
706 raise NotImplementedError
706
707
707 def get_removed(self):
708 def get_removed(self):
708 """
709 """
709 Returns list of ``RemovedFileNode`` objects marked as *removed* in
710 Returns list of ``RemovedFileNode`` objects marked as *removed* in
710 working directory.
711 working directory.
711 """
712 """
712 raise NotImplementedError
713 raise NotImplementedError
713
714
714 def get_untracked(self):
715 def get_untracked(self):
715 """
716 """
716 Returns list of ``FileNode`` objects which are present within working
717 Returns list of ``FileNode`` objects which are present within working
717 directory however are not tracked by repository.
718 directory however are not tracked by repository.
718 """
719 """
719 raise NotImplementedError
720 raise NotImplementedError
720
721
721 def get_status(self):
722 def get_status(self):
722 """
723 """
723 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
724 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
724 lists.
725 lists.
725 """
726 """
726 raise NotImplementedError
727 raise NotImplementedError
727
728
728 def commit(self, message, **kwargs):
729 def commit(self, message, **kwargs):
729 """
730 """
730 Commits local (from working directory) changes and returns newly
731 Commits local (from working directory) changes and returns newly
731 created
732 created
732 ``Changeset``. Updates repository's ``revisions`` list.
733 ``Changeset``. Updates repository's ``revisions`` list.
733
734
734 :raises ``CommitError``: if any error occurs while committing
735 :raises ``CommitError``: if any error occurs while committing
735 """
736 """
736 raise NotImplementedError
737 raise NotImplementedError
737
738
738 def update(self, revision=None):
739 def update(self, revision=None):
739 """
740 """
740 Fetches content of the given revision and populates it within working
741 Fetches content of the given revision and populates it within working
741 directory.
742 directory.
742 """
743 """
743 raise NotImplementedError
744 raise NotImplementedError
744
745
745 def checkout_branch(self, branch=None):
746 def checkout_branch(self, branch=None):
746 """
747 """
747 Checks out ``branch`` or the backend's default branch.
748 Checks out ``branch`` or the backend's default branch.
748
749
749 Raises ``BranchDoesNotExistError`` if the branch does not exist.
750 Raises ``BranchDoesNotExistError`` if the branch does not exist.
750 """
751 """
751 raise NotImplementedError
752 raise NotImplementedError
752
753
753
754
754 class BaseInMemoryChangeset(object):
755 class BaseInMemoryChangeset(object):
755 """
756 """
756 Represents differences between repository's state (most recent head) and
757 Represents differences between repository's state (most recent head) and
757 changes made *in place*.
758 changes made *in place*.
758
759
759 **Attributes**
760 **Attributes**
760
761
761 ``repository``
762 ``repository``
762 repository object for this in-memory-changeset
763 repository object for this in-memory-changeset
763
764
764 ``added``
765 ``added``
765 list of ``FileNode`` objects marked as *added*
766 list of ``FileNode`` objects marked as *added*
766
767
767 ``changed``
768 ``changed``
768 list of ``FileNode`` objects marked as *changed*
769 list of ``FileNode`` objects marked as *changed*
769
770
770 ``removed``
771 ``removed``
771 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
772 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
772 *removed*
773 *removed*
773
774
774 ``parents``
775 ``parents``
775 list of ``Changeset`` representing parents of in-memory changeset.
776 list of ``Changeset`` representing parents of in-memory changeset.
776 Should always be 2-element sequence.
777 Should always be 2-element sequence.
777
778
778 """
779 """
779
780
780 def __init__(self, repository):
781 def __init__(self, repository):
781 self.repository = repository
782 self.repository = repository
782 self.added = []
783 self.added = []
783 self.changed = []
784 self.changed = []
784 self.removed = []
785 self.removed = []
785 self.parents = []
786 self.parents = []
786
787
787 def add(self, *filenodes):
788 def add(self, *filenodes):
788 """
789 """
789 Marks given ``FileNode`` objects as *to be committed*.
790 Marks given ``FileNode`` objects as *to be committed*.
790
791
791 :raises ``NodeAlreadyExistsError``: if node with same path exists at
792 :raises ``NodeAlreadyExistsError``: if node with same path exists at
792 latest changeset
793 latest changeset
793 :raises ``NodeAlreadyAddedError``: if node with same path is already
794 :raises ``NodeAlreadyAddedError``: if node with same path is already
794 marked as *added*
795 marked as *added*
795 """
796 """
796 # Check if not already marked as *added* first
797 # Check if not already marked as *added* first
797 for node in filenodes:
798 for node in filenodes:
798 if node.path in (n.path for n in self.added):
799 if node.path in (n.path for n in self.added):
799 raise NodeAlreadyAddedError("Such FileNode %s is already "
800 raise NodeAlreadyAddedError("Such FileNode %s is already "
800 "marked for addition" % node.path)
801 "marked for addition" % node.path)
801 for node in filenodes:
802 for node in filenodes:
802 self.added.append(node)
803 self.added.append(node)
803
804
804 def change(self, *filenodes):
805 def change(self, *filenodes):
805 """
806 """
806 Marks given ``FileNode`` objects to be *changed* in next commit.
807 Marks given ``FileNode`` objects to be *changed* in next commit.
807
808
808 :raises ``EmptyRepositoryError``: if there are no changesets yet
809 :raises ``EmptyRepositoryError``: if there are no changesets yet
809 :raises ``NodeAlreadyExistsError``: if node with same path is already
810 :raises ``NodeAlreadyExistsError``: if node with same path is already
810 marked to be *changed*
811 marked to be *changed*
811 :raises ``NodeAlreadyRemovedError``: if node with same path is already
812 :raises ``NodeAlreadyRemovedError``: if node with same path is already
812 marked to be *removed*
813 marked to be *removed*
813 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
814 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
814 changeset
815 changeset
815 :raises ``NodeNotChangedError``: if node hasn't really be changed
816 :raises ``NodeNotChangedError``: if node hasn't really be changed
816 """
817 """
817 for node in filenodes:
818 for node in filenodes:
818 if node.path in (n.path for n in self.removed):
819 if node.path in (n.path for n in self.removed):
819 raise NodeAlreadyRemovedError("Node at %s is already marked "
820 raise NodeAlreadyRemovedError("Node at %s is already marked "
820 "as removed" % node.path)
821 "as removed" % node.path)
821 try:
822 try:
822 self.repository.get_changeset()
823 self.repository.get_changeset()
823 except EmptyRepositoryError:
824 except EmptyRepositoryError:
824 raise EmptyRepositoryError("Nothing to change - try to *add* new "
825 raise EmptyRepositoryError("Nothing to change - try to *add* new "
825 "nodes rather than changing them")
826 "nodes rather than changing them")
826 for node in filenodes:
827 for node in filenodes:
827 if node.path in (n.path for n in self.changed):
828 if node.path in (n.path for n in self.changed):
828 raise NodeAlreadyChangedError("Node at '%s' is already "
829 raise NodeAlreadyChangedError("Node at '%s' is already "
829 "marked as changed" % node.path)
830 "marked as changed" % node.path)
830 self.changed.append(node)
831 self.changed.append(node)
831
832
832 def remove(self, *filenodes):
833 def remove(self, *filenodes):
833 """
834 """
834 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
835 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
835 *removed* in next commit.
836 *removed* in next commit.
836
837
837 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
838 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
838 be *removed*
839 be *removed*
839 :raises ``NodeAlreadyChangedError``: if node has been already marked to
840 :raises ``NodeAlreadyChangedError``: if node has been already marked to
840 be *changed*
841 be *changed*
841 """
842 """
842 for node in filenodes:
843 for node in filenodes:
843 if node.path in (n.path for n in self.removed):
844 if node.path in (n.path for n in self.removed):
844 raise NodeAlreadyRemovedError("Node is already marked to "
845 raise NodeAlreadyRemovedError("Node is already marked to "
845 "for removal at %s" % node.path)
846 "for removal at %s" % node.path)
846 if node.path in (n.path for n in self.changed):
847 if node.path in (n.path for n in self.changed):
847 raise NodeAlreadyChangedError("Node is already marked to "
848 raise NodeAlreadyChangedError("Node is already marked to "
848 "be changed at %s" % node.path)
849 "be changed at %s" % node.path)
849 # We only mark node as *removed* - real removal is done by
850 # We only mark node as *removed* - real removal is done by
850 # commit method
851 # commit method
851 self.removed.append(node)
852 self.removed.append(node)
852
853
853 def reset(self):
854 def reset(self):
854 """
855 """
855 Resets this instance to initial state (cleans ``added``, ``changed``
856 Resets this instance to initial state (cleans ``added``, ``changed``
856 and ``removed`` lists).
857 and ``removed`` lists).
857 """
858 """
858 self.added = []
859 self.added = []
859 self.changed = []
860 self.changed = []
860 self.removed = []
861 self.removed = []
861 self.parents = []
862 self.parents = []
862
863
863 def get_ipaths(self):
864 def get_ipaths(self):
864 """
865 """
865 Returns generator of paths from nodes marked as added, changed or
866 Returns generator of paths from nodes marked as added, changed or
866 removed.
867 removed.
867 """
868 """
868 for node in chain(self.added, self.changed, self.removed):
869 for node in chain(self.added, self.changed, self.removed):
869 yield node.path
870 yield node.path
870
871
871 def get_paths(self):
872 def get_paths(self):
872 """
873 """
873 Returns list of paths from nodes marked as added, changed or removed.
874 Returns list of paths from nodes marked as added, changed or removed.
874 """
875 """
875 return list(self.get_ipaths())
876 return list(self.get_ipaths())
876
877
877 def check_integrity(self, parents=None):
878 def check_integrity(self, parents=None):
878 """
879 """
879 Checks in-memory changeset's integrity. Also, sets parents if not
880 Checks in-memory changeset's integrity. Also, sets parents if not
880 already set.
881 already set.
881
882
882 :raises CommitError: if any error occurs (i.e.
883 :raises CommitError: if any error occurs (i.e.
883 ``NodeDoesNotExistError``).
884 ``NodeDoesNotExistError``).
884 """
885 """
885 if not self.parents:
886 if not self.parents:
886 parents = parents or []
887 parents = parents or []
887 if len(parents) == 0:
888 if len(parents) == 0:
888 try:
889 try:
889 parents = [self.repository.get_changeset(), None]
890 parents = [self.repository.get_changeset(), None]
890 except EmptyRepositoryError:
891 except EmptyRepositoryError:
891 parents = [None, None]
892 parents = [None, None]
892 elif len(parents) == 1:
893 elif len(parents) == 1:
893 parents += [None]
894 parents += [None]
894 self.parents = parents
895 self.parents = parents
895
896
896 # Local parents, only if not None
897 # Local parents, only if not None
897 parents = [p for p in self.parents if p]
898 parents = [p for p in self.parents if p]
898
899
899 # Check nodes marked as added
900 # Check nodes marked as added
900 for p in parents:
901 for p in parents:
901 for node in self.added:
902 for node in self.added:
902 try:
903 try:
903 p.get_node(node.path)
904 p.get_node(node.path)
904 except NodeDoesNotExistError:
905 except NodeDoesNotExistError:
905 pass
906 pass
906 else:
907 else:
907 raise NodeAlreadyExistsError("Node at %s already exists "
908 raise NodeAlreadyExistsError("Node at %s already exists "
908 "at %s" % (node.path, p))
909 "at %s" % (node.path, p))
909
910
910 # Check nodes marked as changed
911 # Check nodes marked as changed
911 missing = set(self.changed)
912 missing = set(self.changed)
912 not_changed = set(self.changed)
913 not_changed = set(self.changed)
913 if self.changed and not parents:
914 if self.changed and not parents:
914 raise NodeDoesNotExistError(str(self.changed[0].path))
915 raise NodeDoesNotExistError(str(self.changed[0].path))
915 for p in parents:
916 for p in parents:
916 for node in self.changed:
917 for node in self.changed:
917 try:
918 try:
918 old = p.get_node(node.path)
919 old = p.get_node(node.path)
919 missing.remove(node)
920 missing.remove(node)
920 if old.content != node.content:
921 if old.content != node.content:
921 not_changed.remove(node)
922 not_changed.remove(node)
922 except NodeDoesNotExistError:
923 except NodeDoesNotExistError:
923 pass
924 pass
924 if self.changed and missing:
925 if self.changed and missing:
925 raise NodeDoesNotExistError("Node at %s is missing "
926 raise NodeDoesNotExistError("Node at %s is missing "
926 "(parents: %s)" % (node.path, parents))
927 "(parents: %s)" % (node.path, parents))
927
928
928 if self.changed and not_changed:
929 if self.changed and not_changed:
929 raise NodeNotChangedError("Node at %s wasn't actually changed "
930 raise NodeNotChangedError("Node at %s wasn't actually changed "
930 "since parents' changesets: %s" % (not_changed.pop().path,
931 "since parents' changesets: %s" % (not_changed.pop().path,
931 parents)
932 parents)
932 )
933 )
933
934
934 # Check nodes marked as removed
935 # Check nodes marked as removed
935 if self.removed and not parents:
936 if self.removed and not parents:
936 raise NodeDoesNotExistError("Cannot remove node at %s as there "
937 raise NodeDoesNotExistError("Cannot remove node at %s as there "
937 "were no parents specified" % self.removed[0].path)
938 "were no parents specified" % self.removed[0].path)
938 really_removed = set()
939 really_removed = set()
939 for p in parents:
940 for p in parents:
940 for node in self.removed:
941 for node in self.removed:
941 try:
942 try:
942 p.get_node(node.path)
943 p.get_node(node.path)
943 really_removed.add(node)
944 really_removed.add(node)
944 except ChangesetError:
945 except ChangesetError:
945 pass
946 pass
946 not_removed = set(self.removed) - really_removed
947 not_removed = set(self.removed) - really_removed
947 if not_removed:
948 if not_removed:
948 raise NodeDoesNotExistError("Cannot remove node at %s from "
949 raise NodeDoesNotExistError("Cannot remove node at %s from "
949 "following parents: %s" % (not_removed[0], parents))
950 "following parents: %s" % (not_removed[0], parents))
950
951
951 def commit(self, message, author, parents=None, branch=None, date=None,
952 def commit(self, message, author, parents=None, branch=None, date=None,
952 **kwargs):
953 **kwargs):
953 """
954 """
954 Performs in-memory commit (doesn't check workdir in any way) and
955 Performs in-memory commit (doesn't check workdir in any way) and
955 returns newly created ``Changeset``. Updates repository's
956 returns newly created ``Changeset``. Updates repository's
956 ``revisions``.
957 ``revisions``.
957
958
958 .. note::
959 .. note::
959 While overriding this method each backend's should call
960 While overriding this method each backend's should call
960 ``self.check_integrity(parents)`` in the first place.
961 ``self.check_integrity(parents)`` in the first place.
961
962
962 :param message: message of the commit
963 :param message: message of the commit
963 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
964 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
964 :param parents: single parent or sequence of parents from which commit
965 :param parents: single parent or sequence of parents from which commit
965 would be derieved
966 would be derieved
966 :param date: ``datetime.datetime`` instance. Defaults to
967 :param date: ``datetime.datetime`` instance. Defaults to
967 ``datetime.datetime.now()``.
968 ``datetime.datetime.now()``.
968 :param branch: branch name, as string. If none given, default backend's
969 :param branch: branch name, as string. If none given, default backend's
969 branch would be used.
970 branch would be used.
970
971
971 :raises ``CommitError``: if any error occurs while committing
972 :raises ``CommitError``: if any error occurs while committing
972 """
973 """
973 raise NotImplementedError
974 raise NotImplementedError
974
975
975
976
976 class EmptyChangeset(BaseChangeset):
977 class EmptyChangeset(BaseChangeset):
977 """
978 """
978 An dummy empty changeset. It's possible to pass hash when creating
979 An dummy empty changeset. It's possible to pass hash when creating
979 an EmptyChangeset
980 an EmptyChangeset
980 """
981 """
981
982
982 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
983 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
983 alias=None, revision=-1, message='', author='', date=''):
984 alias=None, revision=-1, message='', author='', date=None):
984 self._empty_cs = cs
985 self._empty_cs = cs
985 self.revision = revision
986 self.revision = revision
986 self.message = message
987 self.message = message
987 self.author = author
988 self.author = author
988 self.date = date
989 self.date = date or datetime.datetime.fromtimestamp(0)
989 self.repository = repo
990 self.repository = repo
990 self.requested_revision = requested_revision
991 self.requested_revision = requested_revision
991 self.alias = alias
992 self.alias = alias
992
993
993 @LazyProperty
994 @LazyProperty
994 def raw_id(self):
995 def raw_id(self):
995 """
996 """
996 Returns raw string identifying this changeset, useful for web
997 Returns raw string identifying this changeset, useful for web
997 representation.
998 representation.
998 """
999 """
999
1000
1000 return self._empty_cs
1001 return self._empty_cs
1001
1002
1002 @LazyProperty
1003 @LazyProperty
1003 def branch(self):
1004 def branch(self):
1004 from rhodecode.lib.vcs.backends import get_backend
1005 from rhodecode.lib.vcs.backends import get_backend
1005 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1006 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1006
1007
1007 @LazyProperty
1008 @LazyProperty
1008 def short_id(self):
1009 def short_id(self):
1009 return self.raw_id[:12]
1010 return self.raw_id[:12]
1010
1011
1011 def get_file_changeset(self, path):
1012 def get_file_changeset(self, path):
1012 return self
1013 return self
1013
1014
1014 def get_file_content(self, path):
1015 def get_file_content(self, path):
1015 return u''
1016 return u''
1016
1017
1017 def get_file_size(self, path):
1018 def get_file_size(self, path):
1018 return 0
1019 return 0
@@ -1,544 +1,549 b''
1 import re
1 import re
2 from itertools import chain
2 from itertools import chain
3 from dulwich import objects
3 from dulwich import objects
4 from subprocess import Popen, PIPE
4 from subprocess import Popen, PIPE
5 import rhodecode
5 import rhodecode
6 from rhodecode.lib.vcs.conf import settings
6 from rhodecode.lib.vcs.conf import settings
7 from rhodecode.lib.vcs.exceptions import RepositoryError
7 from rhodecode.lib.vcs.exceptions import RepositoryError
8 from rhodecode.lib.vcs.exceptions import ChangesetError
8 from rhodecode.lib.vcs.exceptions import ChangesetError
9 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
9 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
10 from rhodecode.lib.vcs.exceptions import VCSError
10 from rhodecode.lib.vcs.exceptions import VCSError
11 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
11 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
12 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
12 from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError
13 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
13 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
14 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
14 from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \
15 RemovedFileNode, SubModuleNode, ChangedFileNodesGenerator,\
15 RemovedFileNode, SubModuleNode, ChangedFileNodesGenerator,\
16 AddedFileNodesGenerator, RemovedFileNodesGenerator
16 AddedFileNodesGenerator, RemovedFileNodesGenerator
17 from rhodecode.lib.vcs.utils import safe_unicode
17 from rhodecode.lib.vcs.utils import safe_unicode
18 from rhodecode.lib.vcs.utils import date_fromtimestamp
18 from rhodecode.lib.vcs.utils import date_fromtimestamp
19 from rhodecode.lib.vcs.utils.lazy import LazyProperty
19 from rhodecode.lib.vcs.utils.lazy import LazyProperty
20 from rhodecode.lib.utils2 import safe_int
20
21
21
22
22 class GitChangeset(BaseChangeset):
23 class GitChangeset(BaseChangeset):
23 """
24 """
24 Represents state of the repository at single revision.
25 Represents state of the repository at single revision.
25 """
26 """
26
27
27 def __init__(self, repository, revision):
28 def __init__(self, repository, revision):
28 self._stat_modes = {}
29 self._stat_modes = {}
29 self.repository = repository
30 self.repository = repository
30
31
31 try:
32 try:
32 commit = self.repository._repo.get_object(revision)
33 commit = self.repository._repo.get_object(revision)
33 if isinstance(commit, objects.Tag):
34 if isinstance(commit, objects.Tag):
34 revision = commit.object[1]
35 revision = commit.object[1]
35 commit = self.repository._repo.get_object(commit.object[1])
36 commit = self.repository._repo.get_object(commit.object[1])
36 except KeyError:
37 except KeyError:
37 raise RepositoryError("Cannot get object with id %s" % revision)
38 raise RepositoryError("Cannot get object with id %s" % revision)
38 self.raw_id = revision
39 self.raw_id = revision
39 self.id = self.raw_id
40 self.id = self.raw_id
40 self.short_id = self.raw_id[:12]
41 self.short_id = self.raw_id[:12]
41 self._commit = commit
42 self._commit = commit
42
43
43 self._tree_id = commit.tree
44 self._tree_id = commit.tree
44 self._committer_property = 'committer'
45 self._committer_property = 'committer'
45 self._author_property = 'author'
46 self._author_property = 'author'
46 self._date_property = 'commit_time'
47 self._date_property = 'commit_time'
47 self._date_tz_property = 'commit_timezone'
48 self._date_tz_property = 'commit_timezone'
48 self.revision = repository.revisions.index(revision)
49 self.revision = repository.revisions.index(revision)
49
50
50 self.message = safe_unicode(commit.message)
51 self.message = safe_unicode(commit.message)
51
52
52 self.nodes = {}
53 self.nodes = {}
53 self._paths = {}
54 self._paths = {}
54
55
55 @LazyProperty
56 @LazyProperty
56 def committer(self):
57 def committer(self):
57 return safe_unicode(getattr(self._commit, self._committer_property))
58 return safe_unicode(getattr(self._commit, self._committer_property))
58
59
59 @LazyProperty
60 @LazyProperty
60 def author(self):
61 def author(self):
61 return safe_unicode(getattr(self._commit, self._author_property))
62 return safe_unicode(getattr(self._commit, self._author_property))
62
63
63 @LazyProperty
64 @LazyProperty
64 def date(self):
65 def date(self):
65 return date_fromtimestamp(getattr(self._commit, self._date_property),
66 return date_fromtimestamp(getattr(self._commit, self._date_property),
66 getattr(self._commit, self._date_tz_property))
67 getattr(self._commit, self._date_tz_property))
67
68
68 @LazyProperty
69 @LazyProperty
69 def _timestamp(self):
70 def _timestamp(self):
70 return getattr(self._commit, self._date_property)
71 return getattr(self._commit, self._date_property)
71
72
72 @LazyProperty
73 @LazyProperty
73 def status(self):
74 def status(self):
74 """
75 """
75 Returns modified, added, removed, deleted files for current changeset
76 Returns modified, added, removed, deleted files for current changeset
76 """
77 """
77 return self.changed, self.added, self.removed
78 return self.changed, self.added, self.removed
78
79
79 @LazyProperty
80 @LazyProperty
80 def tags(self):
81 def tags(self):
81 _tags = []
82 _tags = []
82 for tname, tsha in self.repository.tags.iteritems():
83 for tname, tsha in self.repository.tags.iteritems():
83 if tsha == self.raw_id:
84 if tsha == self.raw_id:
84 _tags.append(tname)
85 _tags.append(tname)
85 return _tags
86 return _tags
86
87
87 @LazyProperty
88 @LazyProperty
88 def branch(self):
89 def branch(self):
89
90
90 heads = self.repository._heads(reverse=False)
91 heads = self.repository._heads(reverse=False)
91
92
92 ref = heads.get(self.raw_id)
93 ref = heads.get(self.raw_id)
93 if ref:
94 if ref:
94 return safe_unicode(ref)
95 return safe_unicode(ref)
95
96
96 def _fix_path(self, path):
97 def _fix_path(self, path):
97 """
98 """
98 Paths are stored without trailing slash so we need to get rid off it if
99 Paths are stored without trailing slash so we need to get rid off it if
99 needed.
100 needed.
100 """
101 """
101 if path.endswith('/'):
102 if path.endswith('/'):
102 path = path.rstrip('/')
103 path = path.rstrip('/')
103 return path
104 return path
104
105
105 def _get_id_for_path(self, path):
106 def _get_id_for_path(self, path):
106
107
107 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
108 # FIXME: Please, spare a couple of minutes and make those codes cleaner;
108 if not path in self._paths:
109 if not path in self._paths:
109 path = path.strip('/')
110 path = path.strip('/')
110 # set root tree
111 # set root tree
111 tree = self.repository._repo[self._tree_id]
112 tree = self.repository._repo[self._tree_id]
112 if path == '':
113 if path == '':
113 self._paths[''] = tree.id
114 self._paths[''] = tree.id
114 return tree.id
115 return tree.id
115 splitted = path.split('/')
116 splitted = path.split('/')
116 dirs, name = splitted[:-1], splitted[-1]
117 dirs, name = splitted[:-1], splitted[-1]
117 curdir = ''
118 curdir = ''
118
119
119 # initially extract things from root dir
120 # initially extract things from root dir
120 for item, stat, id in tree.iteritems():
121 for item, stat, id in tree.iteritems():
121 if curdir:
122 if curdir:
122 name = '/'.join((curdir, item))
123 name = '/'.join((curdir, item))
123 else:
124 else:
124 name = item
125 name = item
125 self._paths[name] = id
126 self._paths[name] = id
126 self._stat_modes[name] = stat
127 self._stat_modes[name] = stat
127
128
128 for dir in dirs:
129 for dir in dirs:
129 if curdir:
130 if curdir:
130 curdir = '/'.join((curdir, dir))
131 curdir = '/'.join((curdir, dir))
131 else:
132 else:
132 curdir = dir
133 curdir = dir
133 dir_id = None
134 dir_id = None
134 for item, stat, id in tree.iteritems():
135 for item, stat, id in tree.iteritems():
135 if dir == item:
136 if dir == item:
136 dir_id = id
137 dir_id = id
137 if dir_id:
138 if dir_id:
138 # Update tree
139 # Update tree
139 tree = self.repository._repo[dir_id]
140 tree = self.repository._repo[dir_id]
140 if not isinstance(tree, objects.Tree):
141 if not isinstance(tree, objects.Tree):
141 raise ChangesetError('%s is not a directory' % curdir)
142 raise ChangesetError('%s is not a directory' % curdir)
142 else:
143 else:
143 raise ChangesetError('%s have not been found' % curdir)
144 raise ChangesetError('%s have not been found' % curdir)
144
145
145 # cache all items from the given traversed tree
146 # cache all items from the given traversed tree
146 for item, stat, id in tree.iteritems():
147 for item, stat, id in tree.iteritems():
147 if curdir:
148 if curdir:
148 name = '/'.join((curdir, item))
149 name = '/'.join((curdir, item))
149 else:
150 else:
150 name = item
151 name = item
151 self._paths[name] = id
152 self._paths[name] = id
152 self._stat_modes[name] = stat
153 self._stat_modes[name] = stat
153 if not path in self._paths:
154 if not path in self._paths:
154 raise NodeDoesNotExistError("There is no file nor directory "
155 raise NodeDoesNotExistError("There is no file nor directory "
155 "at the given path %r at revision %r"
156 "at the given path %r at revision %r"
156 % (path, self.short_id))
157 % (path, self.short_id))
157 return self._paths[path]
158 return self._paths[path]
158
159
159 def _get_kind(self, path):
160 def _get_kind(self, path):
160 obj = self.repository._repo[self._get_id_for_path(path)]
161 obj = self.repository._repo[self._get_id_for_path(path)]
161 if isinstance(obj, objects.Blob):
162 if isinstance(obj, objects.Blob):
162 return NodeKind.FILE
163 return NodeKind.FILE
163 elif isinstance(obj, objects.Tree):
164 elif isinstance(obj, objects.Tree):
164 return NodeKind.DIR
165 return NodeKind.DIR
165
166
166 def _get_filectx(self, path):
167 def _get_filectx(self, path):
167 path = self._fix_path(path)
168 path = self._fix_path(path)
168 if self._get_kind(path) != NodeKind.FILE:
169 if self._get_kind(path) != NodeKind.FILE:
169 raise ChangesetError("File does not exist for revision %r at "
170 raise ChangesetError("File does not exist for revision %r at "
170 " %r" % (self.raw_id, path))
171 " %r" % (self.raw_id, path))
171 return path
172 return path
172
173
173 def _get_file_nodes(self):
174 def _get_file_nodes(self):
174 return chain(*(t[2] for t in self.walk()))
175 return chain(*(t[2] for t in self.walk()))
175
176
176 @LazyProperty
177 @LazyProperty
177 def parents(self):
178 def parents(self):
178 """
179 """
179 Returns list of parents changesets.
180 Returns list of parents changesets.
180 """
181 """
181 return [self.repository.get_changeset(parent)
182 return [self.repository.get_changeset(parent)
182 for parent in self._commit.parents]
183 for parent in self._commit.parents]
183
184
184 @LazyProperty
185 @LazyProperty
185 def children(self):
186 def children(self):
186 """
187 """
187 Returns list of children changesets.
188 Returns list of children changesets.
188 """
189 """
189 so, se = self.repository.run_git_command(
190 so, se = self.repository.run_git_command(
190 "rev-list --all --children | grep '^%s'" % self.raw_id
191 "rev-list --all --children | grep '^%s'" % self.raw_id
191 )
192 )
192
193
193 children = []
194 children = []
194 for l in so.splitlines():
195 for l in so.splitlines():
195 childs = l.split(' ')[1:]
196 childs = l.split(' ')[1:]
196 children.extend(childs)
197 children.extend(childs)
197 return [self.repository.get_changeset(cs) for cs in children]
198 return [self.repository.get_changeset(cs) for cs in children]
198
199
199 def next(self, branch=None):
200 def next(self, branch=None):
200
201
201 if branch and self.branch != branch:
202 if branch and self.branch != branch:
202 raise VCSError('Branch option used on changeset not belonging '
203 raise VCSError('Branch option used on changeset not belonging '
203 'to that branch')
204 'to that branch')
204
205
205 def _next(changeset, branch):
206 def _next(changeset, branch):
206 try:
207 try:
207 next_ = changeset.revision + 1
208 next_ = changeset.revision + 1
208 next_rev = changeset.repository.revisions[next_]
209 next_rev = changeset.repository.revisions[next_]
209 except IndexError:
210 except IndexError:
210 raise ChangesetDoesNotExistError
211 raise ChangesetDoesNotExistError
211 cs = changeset.repository.get_changeset(next_rev)
212 cs = changeset.repository.get_changeset(next_rev)
212
213
213 if branch and branch != cs.branch:
214 if branch and branch != cs.branch:
214 return _next(cs, branch)
215 return _next(cs, branch)
215
216
216 return cs
217 return cs
217
218
218 return _next(self, branch)
219 return _next(self, branch)
219
220
220 def prev(self, branch=None):
221 def prev(self, branch=None):
221 if branch and self.branch != branch:
222 if branch and self.branch != branch:
222 raise VCSError('Branch option used on changeset not belonging '
223 raise VCSError('Branch option used on changeset not belonging '
223 'to that branch')
224 'to that branch')
224
225
225 def _prev(changeset, branch):
226 def _prev(changeset, branch):
226 try:
227 try:
227 prev_ = changeset.revision - 1
228 prev_ = changeset.revision - 1
228 if prev_ < 0:
229 if prev_ < 0:
229 raise IndexError
230 raise IndexError
230 prev_rev = changeset.repository.revisions[prev_]
231 prev_rev = changeset.repository.revisions[prev_]
231 except IndexError:
232 except IndexError:
232 raise ChangesetDoesNotExistError
233 raise ChangesetDoesNotExistError
233
234
234 cs = changeset.repository.get_changeset(prev_rev)
235 cs = changeset.repository.get_changeset(prev_rev)
235
236
236 if branch and branch != cs.branch:
237 if branch and branch != cs.branch:
237 return _prev(cs, branch)
238 return _prev(cs, branch)
238
239
239 return cs
240 return cs
240
241
241 return _prev(self, branch)
242 return _prev(self, branch)
242
243
243 def diff(self, ignore_whitespace=True, context=3):
244 def diff(self, ignore_whitespace=True, context=3):
244 rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET
245 rev1 = self.parents[0] if self.parents else self.repository.EMPTY_CHANGESET
245 rev2 = self
246 rev2 = self
246 return ''.join(self.repository.get_diff(rev1, rev2,
247 return ''.join(self.repository.get_diff(rev1, rev2,
247 ignore_whitespace=ignore_whitespace,
248 ignore_whitespace=ignore_whitespace,
248 context=context))
249 context=context))
249
250
250 def get_file_mode(self, path):
251 def get_file_mode(self, path):
251 """
252 """
252 Returns stat mode of the file at the given ``path``.
253 Returns stat mode of the file at the given ``path``.
253 """
254 """
254 # ensure path is traversed
255 # ensure path is traversed
255 self._get_id_for_path(path)
256 self._get_id_for_path(path)
256 return self._stat_modes[path]
257 return self._stat_modes[path]
257
258
258 def get_file_content(self, path):
259 def get_file_content(self, path):
259 """
260 """
260 Returns content of the file at given ``path``.
261 Returns content of the file at given ``path``.
261 """
262 """
262 id = self._get_id_for_path(path)
263 id = self._get_id_for_path(path)
263 blob = self.repository._repo[id]
264 blob = self.repository._repo[id]
264 return blob.as_pretty_string()
265 return blob.as_pretty_string()
265
266
266 def get_file_size(self, path):
267 def get_file_size(self, path):
267 """
268 """
268 Returns size of the file at given ``path``.
269 Returns size of the file at given ``path``.
269 """
270 """
270 id = self._get_id_for_path(path)
271 id = self._get_id_for_path(path)
271 blob = self.repository._repo[id]
272 blob = self.repository._repo[id]
272 return blob.raw_length()
273 return blob.raw_length()
273
274
274 def get_file_changeset(self, path):
275 def get_file_changeset(self, path):
275 """
276 """
276 Returns last commit of the file at the given ``path``.
277 Returns last commit of the file at the given ``path``.
277 """
278 """
278 node = self.get_node(path)
279 return self.get_file_history(path, limit=1)[0]
279 return node.history[0]
280
280
281 def get_file_history(self, path):
281 def get_file_history(self, path, limit=None):
282 """
282 """
283 Returns history of file as reversed list of ``Changeset`` objects for
283 Returns history of file as reversed list of ``Changeset`` objects for
284 which file at given ``path`` has been modified.
284 which file at given ``path`` has been modified.
285
285
286 TODO: This function now uses os underlying 'git' and 'grep' commands
286 TODO: This function now uses os underlying 'git' and 'grep' commands
287 which is generally not good. Should be replaced with algorithm
287 which is generally not good. Should be replaced with algorithm
288 iterating commits.
288 iterating commits.
289 """
289 """
290
290 self._get_filectx(path)
291 self._get_filectx(path)
291
292 if limit:
292 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
293 cmd = 'log -n %s --pretty="format: %%H" -s -p %s -- "%s"' % (
293 self.id, path
294 safe_int(limit, 0), self.id, path
294 )
295 )
296 else:
297 cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
298 self.id, path
299 )
295 so, se = self.repository.run_git_command(cmd)
300 so, se = self.repository.run_git_command(cmd)
296 ids = re.findall(r'[0-9a-fA-F]{40}', so)
301 ids = re.findall(r'[0-9a-fA-F]{40}', so)
297 return [self.repository.get_changeset(id) for id in ids]
302 return [self.repository.get_changeset(id) for id in ids]
298
303
299 def get_file_history_2(self, path):
304 def get_file_history_2(self, path):
300 """
305 """
301 Returns history of file as reversed list of ``Changeset`` objects for
306 Returns history of file as reversed list of ``Changeset`` objects for
302 which file at given ``path`` has been modified.
307 which file at given ``path`` has been modified.
303
308
304 """
309 """
305 self._get_filectx(path)
310 self._get_filectx(path)
306 from dulwich.walk import Walker
311 from dulwich.walk import Walker
307 include = [self.id]
312 include = [self.id]
308 walker = Walker(self.repository._repo.object_store, include,
313 walker = Walker(self.repository._repo.object_store, include,
309 paths=[path], max_entries=1)
314 paths=[path], max_entries=1)
310 return [self.repository.get_changeset(sha)
315 return [self.repository.get_changeset(sha)
311 for sha in (x.commit.id for x in walker)]
316 for sha in (x.commit.id for x in walker)]
312
317
313 def get_file_annotate(self, path):
318 def get_file_annotate(self, path):
314 """
319 """
315 Returns a generator of four element tuples with
320 Returns a generator of four element tuples with
316 lineno, sha, changeset lazy loader and line
321 lineno, sha, changeset lazy loader and line
317
322
318 TODO: This function now uses os underlying 'git' command which is
323 TODO: This function now uses os underlying 'git' command which is
319 generally not good. Should be replaced with algorithm iterating
324 generally not good. Should be replaced with algorithm iterating
320 commits.
325 commits.
321 """
326 """
322 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
327 cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
323 # -l ==> outputs long shas (and we need all 40 characters)
328 # -l ==> outputs long shas (and we need all 40 characters)
324 # --root ==> doesn't put '^' character for bounderies
329 # --root ==> doesn't put '^' character for bounderies
325 # -r sha ==> blames for the given revision
330 # -r sha ==> blames for the given revision
326 so, se = self.repository.run_git_command(cmd)
331 so, se = self.repository.run_git_command(cmd)
327
332
328 for i, blame_line in enumerate(so.split('\n')[:-1]):
333 for i, blame_line in enumerate(so.split('\n')[:-1]):
329 ln_no = i + 1
334 ln_no = i + 1
330 sha, line = re.split(r' ', blame_line, 1)
335 sha, line = re.split(r' ', blame_line, 1)
331 yield (ln_no, sha, lambda: self.repository.get_changeset(sha), line)
336 yield (ln_no, sha, lambda: self.repository.get_changeset(sha), line)
332
337
333 def fill_archive(self, stream=None, kind='tgz', prefix=None,
338 def fill_archive(self, stream=None, kind='tgz', prefix=None,
334 subrepos=False):
339 subrepos=False):
335 """
340 """
336 Fills up given stream.
341 Fills up given stream.
337
342
338 :param stream: file like object.
343 :param stream: file like object.
339 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
344 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
340 Default: ``tgz``.
345 Default: ``tgz``.
341 :param prefix: name of root directory in archive.
346 :param prefix: name of root directory in archive.
342 Default is repository name and changeset's raw_id joined with dash
347 Default is repository name and changeset's raw_id joined with dash
343 (``repo-tip.<KIND>``).
348 (``repo-tip.<KIND>``).
344 :param subrepos: include subrepos in this archive.
349 :param subrepos: include subrepos in this archive.
345
350
346 :raise ImproperArchiveTypeError: If given kind is wrong.
351 :raise ImproperArchiveTypeError: If given kind is wrong.
347 :raise VcsError: If given stream is None
352 :raise VcsError: If given stream is None
348
353
349 """
354 """
350 allowed_kinds = settings.ARCHIVE_SPECS.keys()
355 allowed_kinds = settings.ARCHIVE_SPECS.keys()
351 if kind not in allowed_kinds:
356 if kind not in allowed_kinds:
352 raise ImproperArchiveTypeError('Archive kind not supported use one'
357 raise ImproperArchiveTypeError('Archive kind not supported use one'
353 'of %s', allowed_kinds)
358 'of %s', allowed_kinds)
354
359
355 if prefix is None:
360 if prefix is None:
356 prefix = '%s-%s' % (self.repository.name, self.short_id)
361 prefix = '%s-%s' % (self.repository.name, self.short_id)
357 elif prefix.startswith('/'):
362 elif prefix.startswith('/'):
358 raise VCSError("Prefix cannot start with leading slash")
363 raise VCSError("Prefix cannot start with leading slash")
359 elif prefix.strip() == '':
364 elif prefix.strip() == '':
360 raise VCSError("Prefix cannot be empty")
365 raise VCSError("Prefix cannot be empty")
361
366
362 if kind == 'zip':
367 if kind == 'zip':
363 frmt = 'zip'
368 frmt = 'zip'
364 else:
369 else:
365 frmt = 'tar'
370 frmt = 'tar'
366 _git_path = rhodecode.CONFIG.get('git_path', 'git')
371 _git_path = rhodecode.CONFIG.get('git_path', 'git')
367 cmd = '%s archive --format=%s --prefix=%s/ %s' % (_git_path,
372 cmd = '%s archive --format=%s --prefix=%s/ %s' % (_git_path,
368 frmt, prefix, self.raw_id)
373 frmt, prefix, self.raw_id)
369 if kind == 'tgz':
374 if kind == 'tgz':
370 cmd += ' | gzip -9'
375 cmd += ' | gzip -9'
371 elif kind == 'tbz2':
376 elif kind == 'tbz2':
372 cmd += ' | bzip2 -9'
377 cmd += ' | bzip2 -9'
373
378
374 if stream is None:
379 if stream is None:
375 raise VCSError('You need to pass in a valid stream for filling'
380 raise VCSError('You need to pass in a valid stream for filling'
376 ' with archival data')
381 ' with archival data')
377 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
382 popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
378 cwd=self.repository.path)
383 cwd=self.repository.path)
379
384
380 buffer_size = 1024 * 8
385 buffer_size = 1024 * 8
381 chunk = popen.stdout.read(buffer_size)
386 chunk = popen.stdout.read(buffer_size)
382 while chunk:
387 while chunk:
383 stream.write(chunk)
388 stream.write(chunk)
384 chunk = popen.stdout.read(buffer_size)
389 chunk = popen.stdout.read(buffer_size)
385 # Make sure all descriptors would be read
390 # Make sure all descriptors would be read
386 popen.communicate()
391 popen.communicate()
387
392
388 def get_nodes(self, path):
393 def get_nodes(self, path):
389 if self._get_kind(path) != NodeKind.DIR:
394 if self._get_kind(path) != NodeKind.DIR:
390 raise ChangesetError("Directory does not exist for revision %r at "
395 raise ChangesetError("Directory does not exist for revision %r at "
391 " %r" % (self.revision, path))
396 " %r" % (self.revision, path))
392 path = self._fix_path(path)
397 path = self._fix_path(path)
393 id = self._get_id_for_path(path)
398 id = self._get_id_for_path(path)
394 tree = self.repository._repo[id]
399 tree = self.repository._repo[id]
395 dirnodes = []
400 dirnodes = []
396 filenodes = []
401 filenodes = []
397 als = self.repository.alias
402 als = self.repository.alias
398 for name, stat, id in tree.iteritems():
403 for name, stat, id in tree.iteritems():
399 if objects.S_ISGITLINK(stat):
404 if objects.S_ISGITLINK(stat):
400 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
405 dirnodes.append(SubModuleNode(name, url=None, changeset=id,
401 alias=als))
406 alias=als))
402 continue
407 continue
403
408
404 obj = self.repository._repo.get_object(id)
409 obj = self.repository._repo.get_object(id)
405 if path != '':
410 if path != '':
406 obj_path = '/'.join((path, name))
411 obj_path = '/'.join((path, name))
407 else:
412 else:
408 obj_path = name
413 obj_path = name
409 if obj_path not in self._stat_modes:
414 if obj_path not in self._stat_modes:
410 self._stat_modes[obj_path] = stat
415 self._stat_modes[obj_path] = stat
411 if isinstance(obj, objects.Tree):
416 if isinstance(obj, objects.Tree):
412 dirnodes.append(DirNode(obj_path, changeset=self))
417 dirnodes.append(DirNode(obj_path, changeset=self))
413 elif isinstance(obj, objects.Blob):
418 elif isinstance(obj, objects.Blob):
414 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
419 filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
415 else:
420 else:
416 raise ChangesetError("Requested object should be Tree "
421 raise ChangesetError("Requested object should be Tree "
417 "or Blob, is %r" % type(obj))
422 "or Blob, is %r" % type(obj))
418 nodes = dirnodes + filenodes
423 nodes = dirnodes + filenodes
419 for node in nodes:
424 for node in nodes:
420 if not node.path in self.nodes:
425 if not node.path in self.nodes:
421 self.nodes[node.path] = node
426 self.nodes[node.path] = node
422 nodes.sort()
427 nodes.sort()
423 return nodes
428 return nodes
424
429
425 def get_node(self, path):
430 def get_node(self, path):
426 if isinstance(path, unicode):
431 if isinstance(path, unicode):
427 path = path.encode('utf-8')
432 path = path.encode('utf-8')
428 path = self._fix_path(path)
433 path = self._fix_path(path)
429 if not path in self.nodes:
434 if not path in self.nodes:
430 try:
435 try:
431 id_ = self._get_id_for_path(path)
436 id_ = self._get_id_for_path(path)
432 except ChangesetError:
437 except ChangesetError:
433 raise NodeDoesNotExistError("Cannot find one of parents' "
438 raise NodeDoesNotExistError("Cannot find one of parents' "
434 "directories for a given path: %s" % path)
439 "directories for a given path: %s" % path)
435
440
436 _GL = lambda m: m and objects.S_ISGITLINK(m)
441 _GL = lambda m: m and objects.S_ISGITLINK(m)
437 if _GL(self._stat_modes.get(path)):
442 if _GL(self._stat_modes.get(path)):
438 node = SubModuleNode(path, url=None, changeset=id_,
443 node = SubModuleNode(path, url=None, changeset=id_,
439 alias=self.repository.alias)
444 alias=self.repository.alias)
440 else:
445 else:
441 obj = self.repository._repo.get_object(id_)
446 obj = self.repository._repo.get_object(id_)
442
447
443 if isinstance(obj, objects.Tree):
448 if isinstance(obj, objects.Tree):
444 if path == '':
449 if path == '':
445 node = RootNode(changeset=self)
450 node = RootNode(changeset=self)
446 else:
451 else:
447 node = DirNode(path, changeset=self)
452 node = DirNode(path, changeset=self)
448 node._tree = obj
453 node._tree = obj
449 elif isinstance(obj, objects.Blob):
454 elif isinstance(obj, objects.Blob):
450 node = FileNode(path, changeset=self)
455 node = FileNode(path, changeset=self)
451 node._blob = obj
456 node._blob = obj
452 else:
457 else:
453 raise NodeDoesNotExistError("There is no file nor directory "
458 raise NodeDoesNotExistError("There is no file nor directory "
454 "at the given path %r at revision %r"
459 "at the given path %r at revision %r"
455 % (path, self.short_id))
460 % (path, self.short_id))
456 # cache node
461 # cache node
457 self.nodes[path] = node
462 self.nodes[path] = node
458 return self.nodes[path]
463 return self.nodes[path]
459
464
460 @LazyProperty
465 @LazyProperty
461 def affected_files(self):
466 def affected_files(self):
462 """
467 """
463 Get's a fast accessible file changes for given changeset
468 Get's a fast accessible file changes for given changeset
464 """
469 """
465 a, m, d = self._changes_cache
470 a, m, d = self._changes_cache
466 return list(a.union(m).union(d))
471 return list(a.union(m).union(d))
467
472
468 @LazyProperty
473 @LazyProperty
469 def _diff_name_status(self):
474 def _diff_name_status(self):
470 output = []
475 output = []
471 for parent in self.parents:
476 for parent in self.parents:
472 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id,
477 cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id,
473 self.raw_id)
478 self.raw_id)
474 so, se = self.repository.run_git_command(cmd)
479 so, se = self.repository.run_git_command(cmd)
475 output.append(so.strip())
480 output.append(so.strip())
476 return '\n'.join(output)
481 return '\n'.join(output)
477
482
478 @LazyProperty
483 @LazyProperty
479 def _changes_cache(self):
484 def _changes_cache(self):
480 added = set()
485 added = set()
481 modified = set()
486 modified = set()
482 deleted = set()
487 deleted = set()
483 _r = self.repository._repo
488 _r = self.repository._repo
484
489
485 parents = self.parents
490 parents = self.parents
486 if not self.parents:
491 if not self.parents:
487 parents = [EmptyChangeset()]
492 parents = [EmptyChangeset()]
488 for parent in parents:
493 for parent in parents:
489 if isinstance(parent, EmptyChangeset):
494 if isinstance(parent, EmptyChangeset):
490 oid = None
495 oid = None
491 else:
496 else:
492 oid = _r[parent.raw_id].tree
497 oid = _r[parent.raw_id].tree
493 changes = _r.object_store.tree_changes(oid, _r[self.raw_id].tree)
498 changes = _r.object_store.tree_changes(oid, _r[self.raw_id].tree)
494 for (oldpath, newpath), (_, _), (_, _) in changes:
499 for (oldpath, newpath), (_, _), (_, _) in changes:
495 if newpath and oldpath:
500 if newpath and oldpath:
496 modified.add(newpath)
501 modified.add(newpath)
497 elif newpath and not oldpath:
502 elif newpath and not oldpath:
498 added.add(newpath)
503 added.add(newpath)
499 elif not newpath and oldpath:
504 elif not newpath and oldpath:
500 deleted.add(oldpath)
505 deleted.add(oldpath)
501 return added, modified, deleted
506 return added, modified, deleted
502
507
503 def _get_paths_for_status(self, status):
508 def _get_paths_for_status(self, status):
504 """
509 """
505 Returns sorted list of paths for given ``status``.
510 Returns sorted list of paths for given ``status``.
506
511
507 :param status: one of: *added*, *modified* or *deleted*
512 :param status: one of: *added*, *modified* or *deleted*
508 """
513 """
509 a, m, d = self._changes_cache
514 a, m, d = self._changes_cache
510 return sorted({
515 return sorted({
511 'added': list(a),
516 'added': list(a),
512 'modified': list(m),
517 'modified': list(m),
513 'deleted': list(d)}[status]
518 'deleted': list(d)}[status]
514 )
519 )
515
520
516 @LazyProperty
521 @LazyProperty
517 def added(self):
522 def added(self):
518 """
523 """
519 Returns list of added ``FileNode`` objects.
524 Returns list of added ``FileNode`` objects.
520 """
525 """
521 if not self.parents:
526 if not self.parents:
522 return list(self._get_file_nodes())
527 return list(self._get_file_nodes())
523 return AddedFileNodesGenerator([n for n in
528 return AddedFileNodesGenerator([n for n in
524 self._get_paths_for_status('added')], self)
529 self._get_paths_for_status('added')], self)
525
530
526 @LazyProperty
531 @LazyProperty
527 def changed(self):
532 def changed(self):
528 """
533 """
529 Returns list of modified ``FileNode`` objects.
534 Returns list of modified ``FileNode`` objects.
530 """
535 """
531 if not self.parents:
536 if not self.parents:
532 return []
537 return []
533 return ChangedFileNodesGenerator([n for n in
538 return ChangedFileNodesGenerator([n for n in
534 self._get_paths_for_status('modified')], self)
539 self._get_paths_for_status('modified')], self)
535
540
536 @LazyProperty
541 @LazyProperty
537 def removed(self):
542 def removed(self):
538 """
543 """
539 Returns list of removed ``FileNode`` objects.
544 Returns list of removed ``FileNode`` objects.
540 """
545 """
541 if not self.parents:
546 if not self.parents:
542 return []
547 return []
543 return RemovedFileNodesGenerator([n for n in
548 return RemovedFileNodesGenerator([n for n in
544 self._get_paths_for_status('deleted')], self)
549 self._get_paths_for_status('deleted')], self)
@@ -1,375 +1,379 b''
1 import os
1 import os
2 import posixpath
2 import posixpath
3
3
4 from rhodecode.lib.vcs.backends.base import BaseChangeset
4 from rhodecode.lib.vcs.backends.base import BaseChangeset
5 from rhodecode.lib.vcs.conf import settings
5 from rhodecode.lib.vcs.conf import settings
6 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError, \
6 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError, \
7 ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
7 ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
8 from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \
8 from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \
9 ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \
9 ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \
10 RemovedFileNodesGenerator, RootNode, SubModuleNode
10 RemovedFileNodesGenerator, RootNode, SubModuleNode
11
11
12 from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
12 from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
13 from rhodecode.lib.vcs.utils.lazy import LazyProperty
13 from rhodecode.lib.vcs.utils.lazy import LazyProperty
14 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
14 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
15 from rhodecode.lib.vcs.utils.hgcompat import archival, hex
15 from rhodecode.lib.vcs.utils.hgcompat import archival, hex
16
16
17
17
18 class MercurialChangeset(BaseChangeset):
18 class MercurialChangeset(BaseChangeset):
19 """
19 """
20 Represents state of the repository at the single revision.
20 Represents state of the repository at the single revision.
21 """
21 """
22
22
23 def __init__(self, repository, revision):
23 def __init__(self, repository, revision):
24 self.repository = repository
24 self.repository = repository
25 self.raw_id = revision
25 self.raw_id = revision
26 self._ctx = repository._repo[revision]
26 self._ctx = repository._repo[revision]
27 self.revision = self._ctx._rev
27 self.revision = self._ctx._rev
28 self.nodes = {}
28 self.nodes = {}
29
29
30 @LazyProperty
30 @LazyProperty
31 def tags(self):
31 def tags(self):
32 return map(safe_unicode, self._ctx.tags())
32 return map(safe_unicode, self._ctx.tags())
33
33
34 @LazyProperty
34 @LazyProperty
35 def branch(self):
35 def branch(self):
36 return safe_unicode(self._ctx.branch())
36 return safe_unicode(self._ctx.branch())
37
37
38 @LazyProperty
38 @LazyProperty
39 def bookmarks(self):
39 def bookmarks(self):
40 return map(safe_unicode, self._ctx.bookmarks())
40 return map(safe_unicode, self._ctx.bookmarks())
41
41
42 @LazyProperty
42 @LazyProperty
43 def message(self):
43 def message(self):
44 return safe_unicode(self._ctx.description())
44 return safe_unicode(self._ctx.description())
45
45
46 @LazyProperty
46 @LazyProperty
47 def committer(self):
47 def committer(self):
48 return safe_unicode(self.author)
48 return safe_unicode(self.author)
49
49
50 @LazyProperty
50 @LazyProperty
51 def author(self):
51 def author(self):
52 return safe_unicode(self._ctx.user())
52 return safe_unicode(self._ctx.user())
53
53
54 @LazyProperty
54 @LazyProperty
55 def date(self):
55 def date(self):
56 return date_fromtimestamp(*self._ctx.date())
56 return date_fromtimestamp(*self._ctx.date())
57
57
58 @LazyProperty
58 @LazyProperty
59 def _timestamp(self):
59 def _timestamp(self):
60 return self._ctx.date()[0]
60 return self._ctx.date()[0]
61
61
62 @LazyProperty
62 @LazyProperty
63 def status(self):
63 def status(self):
64 """
64 """
65 Returns modified, added, removed, deleted files for current changeset
65 Returns modified, added, removed, deleted files for current changeset
66 """
66 """
67 return self.repository._repo.status(self._ctx.p1().node(),
67 return self.repository._repo.status(self._ctx.p1().node(),
68 self._ctx.node())
68 self._ctx.node())
69
69
70 @LazyProperty
70 @LazyProperty
71 def _file_paths(self):
71 def _file_paths(self):
72 return list(self._ctx)
72 return list(self._ctx)
73
73
74 @LazyProperty
74 @LazyProperty
75 def _dir_paths(self):
75 def _dir_paths(self):
76 p = list(set(get_dirs_for_path(*self._file_paths)))
76 p = list(set(get_dirs_for_path(*self._file_paths)))
77 p.insert(0, '')
77 p.insert(0, '')
78 return p
78 return p
79
79
80 @LazyProperty
80 @LazyProperty
81 def _paths(self):
81 def _paths(self):
82 return self._dir_paths + self._file_paths
82 return self._dir_paths + self._file_paths
83
83
84 @LazyProperty
84 @LazyProperty
85 def id(self):
85 def id(self):
86 if self.last:
86 if self.last:
87 return u'tip'
87 return u'tip'
88 return self.short_id
88 return self.short_id
89
89
90 @LazyProperty
90 @LazyProperty
91 def short_id(self):
91 def short_id(self):
92 return self.raw_id[:12]
92 return self.raw_id[:12]
93
93
94 @LazyProperty
94 @LazyProperty
95 def parents(self):
95 def parents(self):
96 """
96 """
97 Returns list of parents changesets.
97 Returns list of parents changesets.
98 """
98 """
99 return [self.repository.get_changeset(parent.rev())
99 return [self.repository.get_changeset(parent.rev())
100 for parent in self._ctx.parents() if parent.rev() >= 0]
100 for parent in self._ctx.parents() if parent.rev() >= 0]
101
101
102 @LazyProperty
102 @LazyProperty
103 def children(self):
103 def children(self):
104 """
104 """
105 Returns list of children changesets.
105 Returns list of children changesets.
106 """
106 """
107 return [self.repository.get_changeset(child.rev())
107 return [self.repository.get_changeset(child.rev())
108 for child in self._ctx.children() if child.rev() >= 0]
108 for child in self._ctx.children() if child.rev() >= 0]
109
109
110 def next(self, branch=None):
110 def next(self, branch=None):
111
111
112 if branch and self.branch != branch:
112 if branch and self.branch != branch:
113 raise VCSError('Branch option used on changeset not belonging '
113 raise VCSError('Branch option used on changeset not belonging '
114 'to that branch')
114 'to that branch')
115
115
116 def _next(changeset, branch):
116 def _next(changeset, branch):
117 try:
117 try:
118 next_ = changeset.revision + 1
118 next_ = changeset.revision + 1
119 next_rev = changeset.repository.revisions[next_]
119 next_rev = changeset.repository.revisions[next_]
120 except IndexError:
120 except IndexError:
121 raise ChangesetDoesNotExistError
121 raise ChangesetDoesNotExistError
122 cs = changeset.repository.get_changeset(next_rev)
122 cs = changeset.repository.get_changeset(next_rev)
123
123
124 if branch and branch != cs.branch:
124 if branch and branch != cs.branch:
125 return _next(cs, branch)
125 return _next(cs, branch)
126
126
127 return cs
127 return cs
128
128
129 return _next(self, branch)
129 return _next(self, branch)
130
130
131 def prev(self, branch=None):
131 def prev(self, branch=None):
132 if branch and self.branch != branch:
132 if branch and self.branch != branch:
133 raise VCSError('Branch option used on changeset not belonging '
133 raise VCSError('Branch option used on changeset not belonging '
134 'to that branch')
134 'to that branch')
135
135
136 def _prev(changeset, branch):
136 def _prev(changeset, branch):
137 try:
137 try:
138 prev_ = changeset.revision - 1
138 prev_ = changeset.revision - 1
139 if prev_ < 0:
139 if prev_ < 0:
140 raise IndexError
140 raise IndexError
141 prev_rev = changeset.repository.revisions[prev_]
141 prev_rev = changeset.repository.revisions[prev_]
142 except IndexError:
142 except IndexError:
143 raise ChangesetDoesNotExistError
143 raise ChangesetDoesNotExistError
144
144
145 cs = changeset.repository.get_changeset(prev_rev)
145 cs = changeset.repository.get_changeset(prev_rev)
146
146
147 if branch and branch != cs.branch:
147 if branch and branch != cs.branch:
148 return _prev(cs, branch)
148 return _prev(cs, branch)
149
149
150 return cs
150 return cs
151
151
152 return _prev(self, branch)
152 return _prev(self, branch)
153
153
154 def diff(self, ignore_whitespace=True, context=3):
154 def diff(self, ignore_whitespace=True, context=3):
155 return ''.join(self._ctx.diff(git=True,
155 return ''.join(self._ctx.diff(git=True,
156 ignore_whitespace=ignore_whitespace,
156 ignore_whitespace=ignore_whitespace,
157 context=context))
157 context=context))
158
158
159 def _fix_path(self, path):
159 def _fix_path(self, path):
160 """
160 """
161 Paths are stored without trailing slash so we need to get rid off it if
161 Paths are stored without trailing slash so we need to get rid off it if
162 needed. Also mercurial keeps filenodes as str so we need to decode
162 needed. Also mercurial keeps filenodes as str so we need to decode
163 from unicode to str
163 from unicode to str
164 """
164 """
165 if path.endswith('/'):
165 if path.endswith('/'):
166 path = path.rstrip('/')
166 path = path.rstrip('/')
167
167
168 return safe_str(path)
168 return safe_str(path)
169
169
170 def _get_kind(self, path):
170 def _get_kind(self, path):
171 path = self._fix_path(path)
171 path = self._fix_path(path)
172 if path in self._file_paths:
172 if path in self._file_paths:
173 return NodeKind.FILE
173 return NodeKind.FILE
174 elif path in self._dir_paths:
174 elif path in self._dir_paths:
175 return NodeKind.DIR
175 return NodeKind.DIR
176 else:
176 else:
177 raise ChangesetError("Node does not exist at the given path %r"
177 raise ChangesetError("Node does not exist at the given path %r"
178 % (path))
178 % (path))
179
179
180 def _get_filectx(self, path):
180 def _get_filectx(self, path):
181 path = self._fix_path(path)
181 path = self._fix_path(path)
182 if self._get_kind(path) != NodeKind.FILE:
182 if self._get_kind(path) != NodeKind.FILE:
183 raise ChangesetError("File does not exist for revision %r at "
183 raise ChangesetError("File does not exist for revision %r at "
184 " %r" % (self.raw_id, path))
184 " %r" % (self.raw_id, path))
185 return self._ctx.filectx(path)
185 return self._ctx.filectx(path)
186
186
187 def _extract_submodules(self):
187 def _extract_submodules(self):
188 """
188 """
189 returns a dictionary with submodule information from substate file
189 returns a dictionary with submodule information from substate file
190 of hg repository
190 of hg repository
191 """
191 """
192 return self._ctx.substate
192 return self._ctx.substate
193
193
194 def get_file_mode(self, path):
194 def get_file_mode(self, path):
195 """
195 """
196 Returns stat mode of the file at the given ``path``.
196 Returns stat mode of the file at the given ``path``.
197 """
197 """
198 fctx = self._get_filectx(path)
198 fctx = self._get_filectx(path)
199 if 'x' in fctx.flags():
199 if 'x' in fctx.flags():
200 return 0100755
200 return 0100755
201 else:
201 else:
202 return 0100644
202 return 0100644
203
203
204 def get_file_content(self, path):
204 def get_file_content(self, path):
205 """
205 """
206 Returns content of the file at given ``path``.
206 Returns content of the file at given ``path``.
207 """
207 """
208 fctx = self._get_filectx(path)
208 fctx = self._get_filectx(path)
209 return fctx.data()
209 return fctx.data()
210
210
211 def get_file_size(self, path):
211 def get_file_size(self, path):
212 """
212 """
213 Returns size of the file at given ``path``.
213 Returns size of the file at given ``path``.
214 """
214 """
215 fctx = self._get_filectx(path)
215 fctx = self._get_filectx(path)
216 return fctx.size()
216 return fctx.size()
217
217
218 def get_file_changeset(self, path):
218 def get_file_changeset(self, path):
219 """
219 """
220 Returns last commit of the file at the given ``path``.
220 Returns last commit of the file at the given ``path``.
221 """
221 """
222 node = self.get_node(path)
222 return self.get_file_history(path, limit=1)[0]
223 return node.history[0]
224
223
225 def get_file_history(self, path):
224 def get_file_history(self, path, limit=None):
226 """
225 """
227 Returns history of file as reversed list of ``Changeset`` objects for
226 Returns history of file as reversed list of ``Changeset`` objects for
228 which file at given ``path`` has been modified.
227 which file at given ``path`` has been modified.
229 """
228 """
230 fctx = self._get_filectx(path)
229 fctx = self._get_filectx(path)
231 nodes = [fctx.filectx(x).node() for x in fctx.filelog()]
230 hist = []
232 changesets = [self.repository.get_changeset(hex(node))
231 cnt = 0
233 for node in reversed(nodes)]
232 for cs in reversed([x for x in fctx.filelog()]):
234 return changesets
233 cnt += 1
234 hist.append(hex(fctx.filectx(cs).node()))
235 if limit and cnt == limit:
236 break
237
238 return [self.repository.get_changeset(node) for node in hist]
235
239
236 def get_file_annotate(self, path):
240 def get_file_annotate(self, path):
237 """
241 """
238 Returns a generator of four element tuples with
242 Returns a generator of four element tuples with
239 lineno, sha, changeset lazy loader and line
243 lineno, sha, changeset lazy loader and line
240 """
244 """
241
245
242 fctx = self._get_filectx(path)
246 fctx = self._get_filectx(path)
243 for i, annotate_data in enumerate(fctx.annotate()):
247 for i, annotate_data in enumerate(fctx.annotate()):
244 ln_no = i + 1
248 ln_no = i + 1
245 sha = hex(annotate_data[0].node())
249 sha = hex(annotate_data[0].node())
246 yield (ln_no, sha, lambda: self.repository.get_changeset(sha), annotate_data[1],)
250 yield (ln_no, sha, lambda: self.repository.get_changeset(sha), annotate_data[1],)
247
251
248 def fill_archive(self, stream=None, kind='tgz', prefix=None,
252 def fill_archive(self, stream=None, kind='tgz', prefix=None,
249 subrepos=False):
253 subrepos=False):
250 """
254 """
251 Fills up given stream.
255 Fills up given stream.
252
256
253 :param stream: file like object.
257 :param stream: file like object.
254 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
258 :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
255 Default: ``tgz``.
259 Default: ``tgz``.
256 :param prefix: name of root directory in archive.
260 :param prefix: name of root directory in archive.
257 Default is repository name and changeset's raw_id joined with dash
261 Default is repository name and changeset's raw_id joined with dash
258 (``repo-tip.<KIND>``).
262 (``repo-tip.<KIND>``).
259 :param subrepos: include subrepos in this archive.
263 :param subrepos: include subrepos in this archive.
260
264
261 :raise ImproperArchiveTypeError: If given kind is wrong.
265 :raise ImproperArchiveTypeError: If given kind is wrong.
262 :raise VcsError: If given stream is None
266 :raise VcsError: If given stream is None
263 """
267 """
264
268
265 allowed_kinds = settings.ARCHIVE_SPECS.keys()
269 allowed_kinds = settings.ARCHIVE_SPECS.keys()
266 if kind not in allowed_kinds:
270 if kind not in allowed_kinds:
267 raise ImproperArchiveTypeError('Archive kind not supported use one'
271 raise ImproperArchiveTypeError('Archive kind not supported use one'
268 'of %s', allowed_kinds)
272 'of %s', allowed_kinds)
269
273
270 if stream is None:
274 if stream is None:
271 raise VCSError('You need to pass in a valid stream for filling'
275 raise VCSError('You need to pass in a valid stream for filling'
272 ' with archival data')
276 ' with archival data')
273
277
274 if prefix is None:
278 if prefix is None:
275 prefix = '%s-%s' % (self.repository.name, self.short_id)
279 prefix = '%s-%s' % (self.repository.name, self.short_id)
276 elif prefix.startswith('/'):
280 elif prefix.startswith('/'):
277 raise VCSError("Prefix cannot start with leading slash")
281 raise VCSError("Prefix cannot start with leading slash")
278 elif prefix.strip() == '':
282 elif prefix.strip() == '':
279 raise VCSError("Prefix cannot be empty")
283 raise VCSError("Prefix cannot be empty")
280
284
281 archival.archive(self.repository._repo, stream, self.raw_id,
285 archival.archive(self.repository._repo, stream, self.raw_id,
282 kind, prefix=prefix, subrepos=subrepos)
286 kind, prefix=prefix, subrepos=subrepos)
283
287
284 if stream.closed and hasattr(stream, 'name'):
288 if stream.closed and hasattr(stream, 'name'):
285 stream = open(stream.name, 'rb')
289 stream = open(stream.name, 'rb')
286 elif hasattr(stream, 'mode') and 'r' not in stream.mode:
290 elif hasattr(stream, 'mode') and 'r' not in stream.mode:
287 stream = open(stream.name, 'rb')
291 stream = open(stream.name, 'rb')
288 else:
292 else:
289 stream.seek(0)
293 stream.seek(0)
290
294
291 def get_nodes(self, path):
295 def get_nodes(self, path):
292 """
296 """
293 Returns combined ``DirNode`` and ``FileNode`` objects list representing
297 Returns combined ``DirNode`` and ``FileNode`` objects list representing
294 state of changeset at the given ``path``. If node at the given ``path``
298 state of changeset at the given ``path``. If node at the given ``path``
295 is not instance of ``DirNode``, ChangesetError would be raised.
299 is not instance of ``DirNode``, ChangesetError would be raised.
296 """
300 """
297
301
298 if self._get_kind(path) != NodeKind.DIR:
302 if self._get_kind(path) != NodeKind.DIR:
299 raise ChangesetError("Directory does not exist for revision %r at "
303 raise ChangesetError("Directory does not exist for revision %r at "
300 " %r" % (self.revision, path))
304 " %r" % (self.revision, path))
301 path = self._fix_path(path)
305 path = self._fix_path(path)
302
306
303 filenodes = [FileNode(f, changeset=self) for f in self._file_paths
307 filenodes = [FileNode(f, changeset=self) for f in self._file_paths
304 if os.path.dirname(f) == path]
308 if os.path.dirname(f) == path]
305 dirs = path == '' and '' or [d for d in self._dir_paths
309 dirs = path == '' and '' or [d for d in self._dir_paths
306 if d and posixpath.dirname(d) == path]
310 if d and posixpath.dirname(d) == path]
307 dirnodes = [DirNode(d, changeset=self) for d in dirs
311 dirnodes = [DirNode(d, changeset=self) for d in dirs
308 if os.path.dirname(d) == path]
312 if os.path.dirname(d) == path]
309
313
310 als = self.repository.alias
314 als = self.repository.alias
311 for k, vals in self._extract_submodules().iteritems():
315 for k, vals in self._extract_submodules().iteritems():
312 #vals = url,rev,type
316 #vals = url,rev,type
313 loc = vals[0]
317 loc = vals[0]
314 cs = vals[1]
318 cs = vals[1]
315 dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
319 dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
316 alias=als))
320 alias=als))
317 nodes = dirnodes + filenodes
321 nodes = dirnodes + filenodes
318 # cache nodes
322 # cache nodes
319 for node in nodes:
323 for node in nodes:
320 self.nodes[node.path] = node
324 self.nodes[node.path] = node
321 nodes.sort()
325 nodes.sort()
322
326
323 return nodes
327 return nodes
324
328
325 def get_node(self, path):
329 def get_node(self, path):
326 """
330 """
327 Returns ``Node`` object from the given ``path``. If there is no node at
331 Returns ``Node`` object from the given ``path``. If there is no node at
328 the given ``path``, ``ChangesetError`` would be raised.
332 the given ``path``, ``ChangesetError`` would be raised.
329 """
333 """
330
334
331 path = self._fix_path(path)
335 path = self._fix_path(path)
332
336
333 if not path in self.nodes:
337 if not path in self.nodes:
334 if path in self._file_paths:
338 if path in self._file_paths:
335 node = FileNode(path, changeset=self)
339 node = FileNode(path, changeset=self)
336 elif path in self._dir_paths or path in self._dir_paths:
340 elif path in self._dir_paths or path in self._dir_paths:
337 if path == '':
341 if path == '':
338 node = RootNode(changeset=self)
342 node = RootNode(changeset=self)
339 else:
343 else:
340 node = DirNode(path, changeset=self)
344 node = DirNode(path, changeset=self)
341 else:
345 else:
342 raise NodeDoesNotExistError("There is no file nor directory "
346 raise NodeDoesNotExistError("There is no file nor directory "
343 "at the given path: %r at revision %r"
347 "at the given path: %r at revision %r"
344 % (path, self.short_id))
348 % (path, self.short_id))
345 # cache node
349 # cache node
346 self.nodes[path] = node
350 self.nodes[path] = node
347 return self.nodes[path]
351 return self.nodes[path]
348
352
349 @LazyProperty
353 @LazyProperty
350 def affected_files(self):
354 def affected_files(self):
351 """
355 """
352 Get's a fast accessible file changes for given changeset
356 Get's a fast accessible file changes for given changeset
353 """
357 """
354 return self._ctx.files()
358 return self._ctx.files()
355
359
356 @property
360 @property
357 def added(self):
361 def added(self):
358 """
362 """
359 Returns list of added ``FileNode`` objects.
363 Returns list of added ``FileNode`` objects.
360 """
364 """
361 return AddedFileNodesGenerator([n for n in self.status[1]], self)
365 return AddedFileNodesGenerator([n for n in self.status[1]], self)
362
366
363 @property
367 @property
364 def changed(self):
368 def changed(self):
365 """
369 """
366 Returns list of modified ``FileNode`` objects.
370 Returns list of modified ``FileNode`` objects.
367 """
371 """
368 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
372 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
369
373
370 @property
374 @property
371 def removed(self):
375 def removed(self):
372 """
376 """
373 Returns list of removed ``FileNode`` objects.
377 Returns list of removed ``FileNode`` objects.
374 """
378 """
375 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
379 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
@@ -1,116 +1,116 b''
1 <%def name="file_class(node)">
1 <%def name="file_class(node)">
2 %if node.is_file():
2 %if node.is_file():
3 <%return "browser-file" %>
3 <%return "browser-file" %>
4 %else:
4 %else:
5 <%return "browser-dir"%>
5 <%return "browser-dir"%>
6 %endif
6 %endif
7 </%def>
7 </%def>
8 <div id="body" class="browserblock">
8 <div id="body" class="browserblock">
9 <div class="browser-header">
9 <div class="browser-header">
10 <div class="browser-nav">
10 <div class="browser-nav">
11 ${h.form(h.url.current())}
11 ${h.form(h.url.current())}
12 <div class="info_box">
12 <div class="info_box">
13 <span class="rev">${_('view')}@rev</span>
13 <span class="rev">${_('view')}@rev</span>
14 <a class="ui-btn ypjax-link" href="${c.url_prev}" title="${_('previous revision')}">&laquo;</a>
14 <a class="ui-btn ypjax-link" href="${c.url_prev}" title="${_('previous revision')}">&laquo;</a>
15 ${h.text('at_rev',value=c.changeset.revision,size=5)}
15 ${h.text('at_rev',value=c.changeset.revision,size=5)}
16 <a class="ui-btn ypjax-link" href="${c.url_next}" title="${_('next revision')}">&raquo;</a>
16 <a class="ui-btn ypjax-link" href="${c.url_next}" title="${_('next revision')}">&raquo;</a>
17 ## ${h.submit('view',_('view'),class_="ui-btn")}
17 ## ${h.submit('view',_('view'),class_="ui-btn")}
18 </div>
18 </div>
19 ${h.end_form()}
19 ${h.end_form()}
20 </div>
20 </div>
21 <div class="browser-branch">
21 <div class="browser-branch">
22 ${h.checkbox('stay_at_branch',c.changeset.branch,c.changeset.branch==c.branch)}
22 ${h.checkbox('stay_at_branch',c.changeset.branch,c.changeset.branch==c.branch)}
23 <label>${_('follow current branch')}</label>
23 <label>${_('follow current branch')}</label>
24 </div>
24 </div>
25 <div class="browser-search">
25 <div class="browser-search">
26 <div id="search_activate_id" class="search_activate">
26 <div id="search_activate_id" class="search_activate">
27 <a class="ui-btn" id="filter_activate" href="#">${_('search file list')}</a>
27 <a class="ui-btn" id="filter_activate" href="#">${_('search file list')}</a>
28 </div>
28 </div>
29 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
29 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
30 <div id="add_node_id" class="add_node">
30 <div id="add_node_id" class="add_node">
31 <a class="ui-btn" href="${h.url('files_add_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path)}">${_('add new file')}</a>
31 <a class="ui-btn" href="${h.url('files_add_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path)}">${_('add new file')}</a>
32 </div>
32 </div>
33 % endif
33 % endif
34 <div>
34 <div>
35 <div id="node_filter_box_loading" style="display:none">${_('Loading file list...')}</div>
35 <div id="node_filter_box_loading" style="display:none">${_('Loading file list...')}</div>
36 <div id="node_filter_box" style="display:none">
36 <div id="node_filter_box" style="display:none">
37 ${h.files_breadcrumbs(c.repo_name,c.changeset.raw_id,c.file.path)}/<input class="init" type="text" value="type to search..." name="filter" size="25" id="node_filter" autocomplete="off">
37 ${h.files_breadcrumbs(c.repo_name,c.changeset.raw_id,c.file.path)}/<input class="init" type="text" value="type to search..." name="filter" size="25" id="node_filter" autocomplete="off">
38 </div>
38 </div>
39 </div>
39 </div>
40 </div>
40 </div>
41 </div>
41 </div>
42
42
43 <div class="browser-body">
43 <div class="browser-body">
44 <table class="code-browser">
44 <table class="code-browser">
45 <thead>
45 <thead>
46 <tr>
46 <tr>
47 <th>${_('Name')}</th>
47 <th>${_('Name')}</th>
48 <th>${_('Size')}</th>
48 <th>${_('Size')}</th>
49 <th>${_('Mimetype')}</th>
49 <th>${_('Mimetype')}</th>
50 <th>${_('Last Revision')}</th>
50 <th>${_('Last Revision')}</th>
51 <th>${_('Last modified')}</th>
51 <th>${_('Last modified')}</th>
52 <th>${_('Last committer')}</th>
52 <th>${_('Last committer')}</th>
53 </tr>
53 </tr>
54 </thead>
54 </thead>
55
55
56 <tbody id="tbody">
56 <tbody id="tbody">
57 %if c.file.parent:
57 %if c.file.parent:
58 <tr class="parity0">
58 <tr class="parity0">
59 <td>
59 <td>
60 ${h.link_to('..',h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.file.parent.path),class_="browser-dir ypjax-link")}
60 ${h.link_to('..',h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.file.parent.path),class_="browser-dir ypjax-link")}
61 </td>
61 </td>
62 <td></td>
62 <td></td>
63 <td></td>
63 <td></td>
64 <td></td>
64 <td></td>
65 <td></td>
65 <td></td>
66 <td></td>
66 <td></td>
67 </tr>
67 </tr>
68 %endif
68 %endif
69
69
70 %for cnt,node in enumerate(c.file):
70 %for cnt,node in enumerate(c.file):
71 <tr class="parity${cnt%2}">
71 <tr class="parity${cnt%2}">
72 <td>
72 <td>
73 %if node.is_submodule():
73 %if node.is_submodule():
74 ${h.link_to(node.name,node.url or '#',class_="submodule-dir ypjax-link")}
74 ${h.link_to(node.name,node.url or '#',class_="submodule-dir ypjax-link")}
75 %else:
75 %else:
76 ${h.link_to(node.name, h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=h.safe_unicode(node.path)),class_=file_class(node)+" ypjax-link")}
76 ${h.link_to(node.name, h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=h.safe_unicode(node.path)),class_=file_class(node)+" ypjax-link")}
77 %endif:
77 %endif:
78 </td>
78 </td>
79 <td>
79 <td>
80 %if node.is_file():
80 %if node.is_file():
81 ${h.format_byte_size(node.size,binary=True)}
81 ${h.format_byte_size(node.size,binary=True)}
82 %endif
82 %endif
83 </td>
83 </td>
84 <td>
84 <td>
85 %if node.is_file():
85 %if node.is_file():
86 ${node.mimetype}
86 ${node.mimetype}
87 %endif
87 %endif
88 </td>
88 </td>
89 <td>
89 <td>
90 %if node.is_file():
90 %if node.is_file():
91 <div class="tooltip" title="${h.tooltip(node.last_changeset.message)}">
91 <div class="tooltip" title="${h.tooltip(node.last_changeset.message)}">
92 <pre>${'r%s:%s' % (node.last_changeset.revision,node.last_changeset.short_id)}</pre>
92 <pre>${'r%s:%s' % (node.last_changeset.revision,node.last_changeset.short_id)}</pre>
93 </div>
93 </div>
94 %endif
94 %endif
95 </td>
95 </td>
96 <td>
96 <td>
97 %if node.is_file():
97 %if node.is_file():
98 <span class="tooltip" title="${h.tooltip(h.fmt_date(node.last_changeset.date))}">
98 <span class="tooltip" title="${h.tooltip(h.fmt_date(node.last_changeset.date))}">
99 ${h.age(node.last_changeset.date)}</span>
99 ${h.age(node.last_changeset.date)}</span>
100 %endif
100 %endif
101 </td>
101 </td>
102 <td>
102 <td>
103 %if node.is_file():
103 %if node.is_file():
104 <span title="${node.last_changeset.author}">
104 <span title="${node.last_changeset.author}">
105 ${h.person(node.last_changeset.author)}
105 ${h.person(node.last_changeset.author)}
106 </span>
106 </span>
107 %endif
107 %endif
108 </td>
108 </td>
109 </tr>
109 </tr>
110 %endfor
110 %endfor
111 </tbody>
111 </tbody>
112 <tbody id="tbody_filtered" style="display:none">
112 <tbody id="tbody_filtered" style="display:none">
113 </tbody>
113 </tbody>
114 </table>
114 </table>
115 </div>
115 </div>
116 </div>
116 </div>
General Comments 0
You need to be logged in to leave comments. Login now