##// END OF EJS Templates
vcs: use correct name of parameter “reverse” in docstring of get_changesets()
Manuel Jacob -
r8707:586ce8c2 stable
parent child Browse files
Show More
@@ -1,1075 +1,1075 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.base
3 vcs.backends.base
4 ~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~
5
5
6 Base for all available scm backends
6 Base for all available scm backends
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import datetime
12 import datetime
13 import itertools
13 import itertools
14 from typing import Sequence
14 from typing import Sequence
15
15
16 from kallithea.lib.vcs.backends import get_backend
16 from kallithea.lib.vcs.backends import get_backend
17 from kallithea.lib.vcs.conf import settings
17 from kallithea.lib.vcs.conf import settings
18 from kallithea.lib.vcs.exceptions import (ChangesetError, EmptyRepositoryError, NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError,
18 from kallithea.lib.vcs.exceptions import (ChangesetError, EmptyRepositoryError, NodeAlreadyAddedError, NodeAlreadyChangedError, NodeAlreadyExistsError,
19 NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, RepositoryError)
19 NodeAlreadyRemovedError, NodeDoesNotExistError, NodeNotChangedError, RepositoryError)
20 from kallithea.lib.vcs.utils import author_email, author_name
20 from kallithea.lib.vcs.utils import author_email, author_name
21 from kallithea.lib.vcs.utils.helpers import get_dict_for_attrs
21 from kallithea.lib.vcs.utils.helpers import get_dict_for_attrs
22 from kallithea.lib.vcs.utils.lazy import LazyProperty
22 from kallithea.lib.vcs.utils.lazy import LazyProperty
23
23
24
24
25 class BaseRepository(object):
25 class BaseRepository(object):
26 """
26 """
27 Base Repository for final backends
27 Base Repository for final backends
28
28
29 **Attributes**
29 **Attributes**
30
30
31 ``DEFAULT_BRANCH_NAME``
31 ``DEFAULT_BRANCH_NAME``
32 name of default branch (i.e. "master" for git etc.
32 name of default branch (i.e. "master" for git etc.
33
33
34 ``scm``
34 ``scm``
35 alias of scm, i.e. *git* or *hg*
35 alias of scm, i.e. *git* or *hg*
36
36
37 ``repo``
37 ``repo``
38 object from external api
38 object from external api
39
39
40 ``revisions``
40 ``revisions``
41 list of all available revisions' ids, in ascending order
41 list of all available revisions' ids, in ascending order
42
42
43 ``changesets``
43 ``changesets``
44 storage dict caching returned changesets
44 storage dict caching returned changesets
45
45
46 ``path``
46 ``path``
47 absolute path to the repository
47 absolute path to the repository
48
48
49 ``branches``
49 ``branches``
50 branches as list of changesets
50 branches as list of changesets
51
51
52 ``tags``
52 ``tags``
53 tags as list of changesets
53 tags as list of changesets
54 """
54 """
55 DEFAULT_BRANCH_NAME: str # assigned in subclass
55 DEFAULT_BRANCH_NAME: str # assigned in subclass
56 scm: str # assigned in subclass
56 scm: str # assigned in subclass
57 path: str # assigned in subclass __init__
57 path: str # assigned in subclass __init__
58 revisions: Sequence[str] # LazyProperty in subclass
58 revisions: Sequence[str] # LazyProperty in subclass
59 _empty: bool # property in subclass
59 _empty: bool # property in subclass
60
60
61 EMPTY_CHANGESET = '0' * 40
61 EMPTY_CHANGESET = '0' * 40
62
62
63 def __init__(self, repo_path, create=False, **kwargs):
63 def __init__(self, repo_path, create=False, **kwargs):
64 """
64 """
65 Initializes repository. Raises RepositoryError if repository could
65 Initializes repository. Raises RepositoryError if repository could
66 not be find at the given ``repo_path`` or directory at ``repo_path``
66 not be find at the given ``repo_path`` or directory at ``repo_path``
67 exists and ``create`` is set to True.
67 exists and ``create`` is set to True.
68
68
69 :param repo_path: local path of the repository
69 :param repo_path: local path of the repository
70 :param create=False: if set to True, would try to create repository.
70 :param create=False: if set to True, would try to create repository.
71 :param src_url=None: if set, should be proper url from which repository
71 :param src_url=None: if set, should be proper url from which repository
72 would be cloned; requires ``create`` parameter to be set to True -
72 would be cloned; requires ``create`` parameter to be set to True -
73 raises RepositoryError if src_url is set and create evaluates to
73 raises RepositoryError if src_url is set and create evaluates to
74 False
74 False
75 """
75 """
76 raise NotImplementedError
76 raise NotImplementedError
77
77
78 def __str__(self):
78 def __str__(self):
79 return '<%s at %s>' % (self.__class__.__name__, self.path)
79 return '<%s at %s>' % (self.__class__.__name__, self.path)
80
80
81 def __repr__(self):
81 def __repr__(self):
82 return self.__str__()
82 return self.__str__()
83
83
84 def __len__(self):
84 def __len__(self):
85 return self.count()
85 return self.count()
86
86
87 def __eq__(self, other):
87 def __eq__(self, other):
88 same_instance = isinstance(other, self.__class__)
88 same_instance = isinstance(other, self.__class__)
89 return same_instance and getattr(other, 'path', None) == self.path
89 return same_instance and getattr(other, 'path', None) == self.path
90
90
91 def __ne__(self, other):
91 def __ne__(self, other):
92 return not self.__eq__(other)
92 return not self.__eq__(other)
93
93
94 @LazyProperty
94 @LazyProperty
95 def alias(self):
95 def alias(self):
96 for k, v in settings.BACKENDS.items():
96 for k, v in settings.BACKENDS.items():
97 if v.split('.')[-1] == str(self.__class__.__name__):
97 if v.split('.')[-1] == str(self.__class__.__name__):
98 return k
98 return k
99
99
100 @LazyProperty
100 @LazyProperty
101 def name(self):
101 def name(self):
102 """
102 """
103 Return repository name (without group name)
103 Return repository name (without group name)
104 """
104 """
105 raise NotImplementedError
105 raise NotImplementedError
106
106
107 @LazyProperty
107 @LazyProperty
108 def owner(self):
108 def owner(self):
109 raise NotImplementedError
109 raise NotImplementedError
110
110
111 @LazyProperty
111 @LazyProperty
112 def description(self):
112 def description(self):
113 raise NotImplementedError
113 raise NotImplementedError
114
114
115 @LazyProperty
115 @LazyProperty
116 def size(self):
116 def size(self):
117 """
117 """
118 Returns combined size in bytes for all repository files
118 Returns combined size in bytes for all repository files
119 """
119 """
120
120
121 size = 0
121 size = 0
122 try:
122 try:
123 tip = self.get_changeset()
123 tip = self.get_changeset()
124 for topnode, dirs, files in tip.walk('/'):
124 for topnode, dirs, files in tip.walk('/'):
125 for f in files:
125 for f in files:
126 size += tip.get_file_size(f.path)
126 size += tip.get_file_size(f.path)
127
127
128 except RepositoryError as e:
128 except RepositoryError as e:
129 pass
129 pass
130 return size
130 return size
131
131
132 def is_valid(self):
132 def is_valid(self):
133 """
133 """
134 Validates repository.
134 Validates repository.
135 """
135 """
136 raise NotImplementedError
136 raise NotImplementedError
137
137
138 def is_empty(self):
138 def is_empty(self):
139 return self._empty
139 return self._empty
140
140
141 #==========================================================================
141 #==========================================================================
142 # CHANGESETS
142 # CHANGESETS
143 #==========================================================================
143 #==========================================================================
144
144
145 def get_changeset(self, revision=None):
145 def get_changeset(self, revision=None):
146 """
146 """
147 Returns instance of ``Changeset`` class. If ``revision`` is None, most
147 Returns instance of ``Changeset`` class. If ``revision`` is None, most
148 recent changeset is returned.
148 recent changeset is returned.
149
149
150 :raises ``EmptyRepositoryError``: if there are no revisions
150 :raises ``EmptyRepositoryError``: if there are no revisions
151 """
151 """
152 raise NotImplementedError
152 raise NotImplementedError
153
153
154 def __iter__(self):
154 def __iter__(self):
155 """
155 """
156 Allows Repository objects to be iterated.
156 Allows Repository objects to be iterated.
157
157
158 *Requires* implementation of ``__getitem__`` method.
158 *Requires* implementation of ``__getitem__`` method.
159 """
159 """
160 for revision in self.revisions:
160 for revision in self.revisions:
161 yield self.get_changeset(revision)
161 yield self.get_changeset(revision)
162
162
163 def get_changesets(self, start=None, end=None, start_date=None,
163 def get_changesets(self, start=None, end=None, start_date=None,
164 end_date=None, branch_name=None, reverse=False, max_revisions=None):
164 end_date=None, branch_name=None, reverse=False, max_revisions=None):
165 """
165 """
166 Returns iterator of ``BaseChangeset`` objects from start to end,
166 Returns iterator of ``BaseChangeset`` objects from start to end,
167 both inclusive.
167 both inclusive.
168
168
169 :param start: None or str
169 :param start: None or str
170 :param end: None or str
170 :param end: None or str
171 :param start_date:
171 :param start_date:
172 :param end_date:
172 :param end_date:
173 :param branch_name:
173 :param branch_name:
174 :param reversed:
174 :param reverse:
175 """
175 """
176 raise NotImplementedError
176 raise NotImplementedError
177
177
178 def get_diff_changesets(self, org_rev, other_repo, other_rev):
178 def get_diff_changesets(self, org_rev, other_repo, other_rev):
179 """
179 """
180 Returns lists of changesets that can be merged from this repo @org_rev
180 Returns lists of changesets that can be merged from this repo @org_rev
181 to other_repo @other_rev
181 to other_repo @other_rev
182 ... and the other way
182 ... and the other way
183 ... and the ancestors that would be used for merge
183 ... and the ancestors that would be used for merge
184
184
185 :param org_rev: the revision we want our compare to be made
185 :param org_rev: the revision we want our compare to be made
186 :param other_repo: repo object, most likely the fork of org_repo. It has
186 :param other_repo: repo object, most likely the fork of org_repo. It has
187 all changesets that we need to obtain
187 all changesets that we need to obtain
188 :param other_rev: revision we want out compare to be made on other_repo
188 :param other_rev: revision we want out compare to be made on other_repo
189 """
189 """
190 raise NotImplementedError
190 raise NotImplementedError
191
191
192 def __getitem__(self, key):
192 def __getitem__(self, key):
193 if isinstance(key, slice):
193 if isinstance(key, slice):
194 return (self.get_changeset(rev) for rev in self.revisions[key])
194 return (self.get_changeset(rev) for rev in self.revisions[key])
195 return self.get_changeset(key)
195 return self.get_changeset(key)
196
196
197 def count(self):
197 def count(self):
198 return len(self.revisions)
198 return len(self.revisions)
199
199
200 def tag(self, name, user, revision=None, message=None, date=None, **opts):
200 def tag(self, name, user, revision=None, message=None, date=None, **opts):
201 """
201 """
202 Creates and returns a tag for the given ``revision``.
202 Creates and returns a tag for the given ``revision``.
203
203
204 :param name: name for new tag
204 :param name: name for new tag
205 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
205 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
206 :param revision: changeset id for which new tag would be created
206 :param revision: changeset id for which new tag would be created
207 :param message: message of the tag's commit
207 :param message: message of the tag's commit
208 :param date: date of tag's commit
208 :param date: date of tag's commit
209
209
210 :raises TagAlreadyExistError: if tag with same name already exists
210 :raises TagAlreadyExistError: if tag with same name already exists
211 """
211 """
212 raise NotImplementedError
212 raise NotImplementedError
213
213
214 def remove_tag(self, name, user, message=None, date=None):
214 def remove_tag(self, name, user, message=None, date=None):
215 """
215 """
216 Removes tag with the given ``name``.
216 Removes tag with the given ``name``.
217
217
218 :param name: name of the tag to be removed
218 :param name: name of the tag to be removed
219 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
219 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
220 :param message: message of the tag's removal commit
220 :param message: message of the tag's removal commit
221 :param date: date of tag's removal commit
221 :param date: date of tag's removal commit
222
222
223 :raises TagDoesNotExistError: if tag with given name does not exists
223 :raises TagDoesNotExistError: if tag with given name does not exists
224 """
224 """
225 raise NotImplementedError
225 raise NotImplementedError
226
226
227 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
227 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
228 context=3):
228 context=3):
229 """
229 """
230 Returns (git like) *diff*, as plain text. Shows changes introduced by
230 Returns (git like) *diff*, as plain text. Shows changes introduced by
231 ``rev2`` since ``rev1``.
231 ``rev2`` since ``rev1``.
232
232
233 :param rev1: Entry point from which diff is shown. Can be
233 :param rev1: Entry point from which diff is shown. Can be
234 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
234 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
235 the changes since empty state of the repository until ``rev2``
235 the changes since empty state of the repository until ``rev2``
236 :param rev2: Until which revision changes should be shown.
236 :param rev2: Until which revision changes should be shown.
237 :param ignore_whitespace: If set to ``True``, would not show whitespace
237 :param ignore_whitespace: If set to ``True``, would not show whitespace
238 changes. Defaults to ``False``.
238 changes. Defaults to ``False``.
239 :param context: How many lines before/after changed lines should be
239 :param context: How many lines before/after changed lines should be
240 shown. Defaults to ``3``.
240 shown. Defaults to ``3``.
241 """
241 """
242 raise NotImplementedError
242 raise NotImplementedError
243
243
244 # ========== #
244 # ========== #
245 # COMMIT API #
245 # COMMIT API #
246 # ========== #
246 # ========== #
247
247
248 @LazyProperty
248 @LazyProperty
249 def in_memory_changeset(self):
249 def in_memory_changeset(self):
250 """
250 """
251 Returns ``InMemoryChangeset`` object for this repository.
251 Returns ``InMemoryChangeset`` object for this repository.
252 """
252 """
253 raise NotImplementedError
253 raise NotImplementedError
254
254
255 def add(self, filenode, **kwargs):
255 def add(self, filenode, **kwargs):
256 """
256 """
257 Commit api function that will add given ``FileNode`` into this
257 Commit api function that will add given ``FileNode`` into this
258 repository.
258 repository.
259
259
260 :raises ``NodeAlreadyExistsError``: if there is a file with same path
260 :raises ``NodeAlreadyExistsError``: if there is a file with same path
261 already in repository
261 already in repository
262 :raises ``NodeAlreadyAddedError``: if given node is already marked as
262 :raises ``NodeAlreadyAddedError``: if given node is already marked as
263 *added*
263 *added*
264 """
264 """
265 raise NotImplementedError
265 raise NotImplementedError
266
266
267 def remove(self, filenode, **kwargs):
267 def remove(self, filenode, **kwargs):
268 """
268 """
269 Commit api function that will remove given ``FileNode`` into this
269 Commit api function that will remove given ``FileNode`` into this
270 repository.
270 repository.
271
271
272 :raises ``EmptyRepositoryError``: if there are no changesets yet
272 :raises ``EmptyRepositoryError``: if there are no changesets yet
273 :raises ``NodeDoesNotExistError``: if there is no file with given path
273 :raises ``NodeDoesNotExistError``: if there is no file with given path
274 """
274 """
275 raise NotImplementedError
275 raise NotImplementedError
276
276
277 def commit(self, message, **kwargs):
277 def commit(self, message, **kwargs):
278 """
278 """
279 Persists current changes made on this repository and returns newly
279 Persists current changes made on this repository and returns newly
280 created changeset.
280 created changeset.
281 """
281 """
282 raise NotImplementedError
282 raise NotImplementedError
283
283
284 def get_state(self):
284 def get_state(self):
285 """
285 """
286 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
286 Returns dictionary with ``added``, ``changed`` and ``removed`` lists
287 containing ``FileNode`` objects.
287 containing ``FileNode`` objects.
288 """
288 """
289 raise NotImplementedError
289 raise NotImplementedError
290
290
291 def get_config_value(self, section, name, config_file=None):
291 def get_config_value(self, section, name, config_file=None):
292 """
292 """
293 Returns configuration value for a given [``section``] and ``name``.
293 Returns configuration value for a given [``section``] and ``name``.
294
294
295 :param section: Section we want to retrieve value from
295 :param section: Section we want to retrieve value from
296 :param name: Name of configuration we want to retrieve
296 :param name: Name of configuration we want to retrieve
297 :param config_file: A path to file which should be used to retrieve
297 :param config_file: A path to file which should be used to retrieve
298 configuration from (might also be a list of file paths)
298 configuration from (might also be a list of file paths)
299 """
299 """
300 raise NotImplementedError
300 raise NotImplementedError
301
301
302 def get_user_name(self, config_file=None):
302 def get_user_name(self, config_file=None):
303 """
303 """
304 Returns user's name from global configuration file.
304 Returns user's name from global configuration file.
305
305
306 :param config_file: A path to file which should be used to retrieve
306 :param config_file: A path to file which should be used to retrieve
307 configuration from (might also be a list of file paths)
307 configuration from (might also be a list of file paths)
308 """
308 """
309 raise NotImplementedError
309 raise NotImplementedError
310
310
311 def get_user_email(self, config_file=None):
311 def get_user_email(self, config_file=None):
312 """
312 """
313 Returns user's email from global configuration file.
313 Returns user's email from global configuration file.
314
314
315 :param config_file: A path to file which should be used to retrieve
315 :param config_file: A path to file which should be used to retrieve
316 configuration from (might also be a list of file paths)
316 configuration from (might also be a list of file paths)
317 """
317 """
318 raise NotImplementedError
318 raise NotImplementedError
319
319
320 # =========== #
320 # =========== #
321 # WORKDIR API #
321 # WORKDIR API #
322 # =========== #
322 # =========== #
323
323
324 @LazyProperty
324 @LazyProperty
325 def workdir(self):
325 def workdir(self):
326 """
326 """
327 Returns ``Workdir`` instance for this repository.
327 Returns ``Workdir`` instance for this repository.
328 """
328 """
329 raise NotImplementedError
329 raise NotImplementedError
330
330
331
331
332 class BaseChangeset(object):
332 class BaseChangeset(object):
333 """
333 """
334 Each backend should implement it's changeset representation.
334 Each backend should implement it's changeset representation.
335
335
336 **Attributes**
336 **Attributes**
337
337
338 ``repository``
338 ``repository``
339 repository object within which changeset exists
339 repository object within which changeset exists
340
340
341 ``raw_id``
341 ``raw_id``
342 raw changeset representation (i.e. full 40 length sha for git
342 raw changeset representation (i.e. full 40 length sha for git
343 backend)
343 backend)
344
344
345 ``short_id``
345 ``short_id``
346 shortened (if apply) version of ``raw_id``; it would be simple
346 shortened (if apply) version of ``raw_id``; it would be simple
347 shortcut for ``raw_id[:12]`` for git/mercurial backends
347 shortcut for ``raw_id[:12]`` for git/mercurial backends
348
348
349 ``revision``
349 ``revision``
350 revision number as integer
350 revision number as integer
351
351
352 ``files``
352 ``files``
353 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
353 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
354
354
355 ``dirs``
355 ``dirs``
356 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
356 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
357
357
358 ``nodes``
358 ``nodes``
359 combined list of ``Node`` objects
359 combined list of ``Node`` objects
360
360
361 ``author``
361 ``author``
362 author of the changeset, as str
362 author of the changeset, as str
363
363
364 ``message``
364 ``message``
365 message of the changeset, as str
365 message of the changeset, as str
366
366
367 ``parents``
367 ``parents``
368 list of parent changesets
368 list of parent changesets
369
369
370 ``last``
370 ``last``
371 ``True`` if this is last changeset in repository, ``False``
371 ``True`` if this is last changeset in repository, ``False``
372 otherwise; trying to access this attribute while there is no
372 otherwise; trying to access this attribute while there is no
373 changesets would raise ``EmptyRepositoryError``
373 changesets would raise ``EmptyRepositoryError``
374 """
374 """
375 message: str # LazyProperty in subclass
375 message: str # LazyProperty in subclass
376 date: datetime.datetime # LazyProperty in subclass
376 date: datetime.datetime # LazyProperty in subclass
377
377
378 def __str__(self):
378 def __str__(self):
379 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
379 return '<%s at %s:%s>' % (self.__class__.__name__, self.revision,
380 self.short_id)
380 self.short_id)
381
381
382 def __repr__(self):
382 def __repr__(self):
383 return self.__str__()
383 return self.__str__()
384
384
385 def __eq__(self, other):
385 def __eq__(self, other):
386 if type(self) is not type(other):
386 if type(self) is not type(other):
387 return False
387 return False
388 return self.raw_id == other.raw_id
388 return self.raw_id == other.raw_id
389
389
390 def __json__(self, with_file_list=False):
390 def __json__(self, with_file_list=False):
391 if with_file_list:
391 if with_file_list:
392 return dict(
392 return dict(
393 short_id=self.short_id,
393 short_id=self.short_id,
394 raw_id=self.raw_id,
394 raw_id=self.raw_id,
395 revision=self.revision,
395 revision=self.revision,
396 message=self.message,
396 message=self.message,
397 date=self.date,
397 date=self.date,
398 author=self.author,
398 author=self.author,
399 added=[el.path for el in self.added],
399 added=[el.path for el in self.added],
400 changed=[el.path for el in self.changed],
400 changed=[el.path for el in self.changed],
401 removed=[el.path for el in self.removed],
401 removed=[el.path for el in self.removed],
402 )
402 )
403 else:
403 else:
404 return dict(
404 return dict(
405 short_id=self.short_id,
405 short_id=self.short_id,
406 raw_id=self.raw_id,
406 raw_id=self.raw_id,
407 revision=self.revision,
407 revision=self.revision,
408 message=self.message,
408 message=self.message,
409 date=self.date,
409 date=self.date,
410 author=self.author,
410 author=self.author,
411 )
411 )
412
412
413 @LazyProperty
413 @LazyProperty
414 def last(self):
414 def last(self):
415 if self.repository is None:
415 if self.repository is None:
416 raise ChangesetError("Cannot check if it's most recent revision")
416 raise ChangesetError("Cannot check if it's most recent revision")
417 return self.raw_id == self.repository.revisions[-1]
417 return self.raw_id == self.repository.revisions[-1]
418
418
419 @LazyProperty
419 @LazyProperty
420 def parents(self):
420 def parents(self):
421 """
421 """
422 Returns list of parents changesets.
422 Returns list of parents changesets.
423 """
423 """
424 raise NotImplementedError
424 raise NotImplementedError
425
425
426 @LazyProperty
426 @LazyProperty
427 def children(self):
427 def children(self):
428 """
428 """
429 Returns list of children changesets.
429 Returns list of children changesets.
430 """
430 """
431 raise NotImplementedError
431 raise NotImplementedError
432
432
433 @LazyProperty
433 @LazyProperty
434 def raw_id(self):
434 def raw_id(self):
435 """
435 """
436 Returns raw string identifying this changeset.
436 Returns raw string identifying this changeset.
437 """
437 """
438 raise NotImplementedError
438 raise NotImplementedError
439
439
440 @LazyProperty
440 @LazyProperty
441 def short_id(self):
441 def short_id(self):
442 """
442 """
443 Returns shortened version of ``raw_id`` attribute, as string,
443 Returns shortened version of ``raw_id`` attribute, as string,
444 identifying this changeset, useful for web representation.
444 identifying this changeset, useful for web representation.
445 """
445 """
446 raise NotImplementedError
446 raise NotImplementedError
447
447
448 @LazyProperty
448 @LazyProperty
449 def revision(self):
449 def revision(self):
450 """
450 """
451 Returns integer identifying this changeset.
451 Returns integer identifying this changeset.
452
452
453 """
453 """
454 raise NotImplementedError
454 raise NotImplementedError
455
455
456 @LazyProperty
456 @LazyProperty
457 def committer(self):
457 def committer(self):
458 """
458 """
459 Returns Committer for given commit
459 Returns Committer for given commit
460 """
460 """
461
461
462 raise NotImplementedError
462 raise NotImplementedError
463
463
464 @LazyProperty
464 @LazyProperty
465 def committer_name(self):
465 def committer_name(self):
466 """
466 """
467 Returns Author name for given commit
467 Returns Author name for given commit
468 """
468 """
469
469
470 return author_name(self.committer)
470 return author_name(self.committer)
471
471
472 @LazyProperty
472 @LazyProperty
473 def committer_email(self):
473 def committer_email(self):
474 """
474 """
475 Returns Author email address for given commit
475 Returns Author email address for given commit
476 """
476 """
477
477
478 return author_email(self.committer)
478 return author_email(self.committer)
479
479
480 @LazyProperty
480 @LazyProperty
481 def author(self):
481 def author(self):
482 """
482 """
483 Returns Author for given commit
483 Returns Author for given commit
484 """
484 """
485
485
486 raise NotImplementedError
486 raise NotImplementedError
487
487
488 @LazyProperty
488 @LazyProperty
489 def author_name(self):
489 def author_name(self):
490 """
490 """
491 Returns Author name for given commit
491 Returns Author name for given commit
492 """
492 """
493
493
494 return author_name(self.author)
494 return author_name(self.author)
495
495
496 @LazyProperty
496 @LazyProperty
497 def author_email(self):
497 def author_email(self):
498 """
498 """
499 Returns Author email address for given commit
499 Returns Author email address for given commit
500 """
500 """
501
501
502 return author_email(self.author)
502 return author_email(self.author)
503
503
504 def get_file_mode(self, path):
504 def get_file_mode(self, path):
505 """
505 """
506 Returns stat mode of the file at the given ``path``.
506 Returns stat mode of the file at the given ``path``.
507 """
507 """
508 raise NotImplementedError
508 raise NotImplementedError
509
509
510 def get_file_content(self, path):
510 def get_file_content(self, path):
511 """
511 """
512 Returns content of the file at the given ``path``.
512 Returns content of the file at the given ``path``.
513 """
513 """
514 raise NotImplementedError
514 raise NotImplementedError
515
515
516 def get_file_size(self, path):
516 def get_file_size(self, path):
517 """
517 """
518 Returns size of the file at the given ``path``.
518 Returns size of the file at the given ``path``.
519 """
519 """
520 raise NotImplementedError
520 raise NotImplementedError
521
521
522 def get_file_changeset(self, path):
522 def get_file_changeset(self, path):
523 """
523 """
524 Returns last commit of the file at the given ``path``.
524 Returns last commit of the file at the given ``path``.
525 """
525 """
526 raise NotImplementedError
526 raise NotImplementedError
527
527
528 def get_file_history(self, path):
528 def get_file_history(self, path):
529 """
529 """
530 Returns history of file as reversed list of ``Changeset`` objects for
530 Returns history of file as reversed list of ``Changeset`` objects for
531 which file at given ``path`` has been modified.
531 which file at given ``path`` has been modified.
532 """
532 """
533 raise NotImplementedError
533 raise NotImplementedError
534
534
535 def get_nodes(self, path):
535 def get_nodes(self, path):
536 """
536 """
537 Returns combined ``DirNode`` and ``FileNode`` objects list representing
537 Returns combined ``DirNode`` and ``FileNode`` objects list representing
538 state of changeset at the given ``path``.
538 state of changeset at the given ``path``.
539
539
540 :raises ``ChangesetError``: if node at the given ``path`` is not
540 :raises ``ChangesetError``: if node at the given ``path`` is not
541 instance of ``DirNode``
541 instance of ``DirNode``
542 """
542 """
543 raise NotImplementedError
543 raise NotImplementedError
544
544
545 def get_node(self, path):
545 def get_node(self, path):
546 """
546 """
547 Returns ``Node`` object from the given ``path``.
547 Returns ``Node`` object from the given ``path``.
548
548
549 :raises ``NodeDoesNotExistError``: if there is no node at the given
549 :raises ``NodeDoesNotExistError``: if there is no node at the given
550 ``path``
550 ``path``
551 """
551 """
552 raise NotImplementedError
552 raise NotImplementedError
553
553
554 def fill_archive(self, stream=None, kind='tgz', prefix=None):
554 def fill_archive(self, stream=None, kind='tgz', prefix=None):
555 """
555 """
556 Fills up given stream.
556 Fills up given stream.
557
557
558 :param stream: file like object.
558 :param stream: file like object.
559 :param kind: one of following: ``zip``, ``tar``, ``tgz``
559 :param kind: one of following: ``zip``, ``tar``, ``tgz``
560 or ``tbz2``. Default: ``tgz``.
560 or ``tbz2``. Default: ``tgz``.
561 :param prefix: name of root directory in archive.
561 :param prefix: name of root directory in archive.
562 Default is repository name and changeset's raw_id joined with dash.
562 Default is repository name and changeset's raw_id joined with dash.
563
563
564 repo-tip.<kind>
564 repo-tip.<kind>
565 """
565 """
566
566
567 raise NotImplementedError
567 raise NotImplementedError
568
568
569 def get_chunked_archive(self, **kwargs):
569 def get_chunked_archive(self, **kwargs):
570 """
570 """
571 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
571 Returns iterable archive. Tiny wrapper around ``fill_archive`` method.
572
572
573 :param chunk_size: extra parameter which controls size of returned
573 :param chunk_size: extra parameter which controls size of returned
574 chunks. Default:8k.
574 chunks. Default:8k.
575 """
575 """
576
576
577 chunk_size = kwargs.pop('chunk_size', 8192)
577 chunk_size = kwargs.pop('chunk_size', 8192)
578 stream = kwargs.get('stream')
578 stream = kwargs.get('stream')
579 self.fill_archive(**kwargs)
579 self.fill_archive(**kwargs)
580 while True:
580 while True:
581 data = stream.read(chunk_size)
581 data = stream.read(chunk_size)
582 if not data:
582 if not data:
583 break
583 break
584 yield data
584 yield data
585
585
586 @LazyProperty
586 @LazyProperty
587 def root(self):
587 def root(self):
588 """
588 """
589 Returns ``RootNode`` object for this changeset.
589 Returns ``RootNode`` object for this changeset.
590 """
590 """
591 return self.get_node('')
591 return self.get_node('')
592
592
593 def next(self, branch=None):
593 def next(self, branch=None):
594 """
594 """
595 Returns next changeset from current, if branch is gives it will return
595 Returns next changeset from current, if branch is gives it will return
596 next changeset belonging to this branch
596 next changeset belonging to this branch
597
597
598 :param branch: show changesets within the given named branch
598 :param branch: show changesets within the given named branch
599 """
599 """
600 raise NotImplementedError
600 raise NotImplementedError
601
601
602 def prev(self, branch=None):
602 def prev(self, branch=None):
603 """
603 """
604 Returns previous changeset from current, if branch is gives it will
604 Returns previous changeset from current, if branch is gives it will
605 return previous changeset belonging to this branch
605 return previous changeset belonging to this branch
606
606
607 :param branch: show changesets within the given named branch
607 :param branch: show changesets within the given named branch
608 """
608 """
609 raise NotImplementedError
609 raise NotImplementedError
610
610
611 @LazyProperty
611 @LazyProperty
612 def added(self):
612 def added(self):
613 """
613 """
614 Returns list of added ``FileNode`` objects.
614 Returns list of added ``FileNode`` objects.
615 """
615 """
616 raise NotImplementedError
616 raise NotImplementedError
617
617
618 @LazyProperty
618 @LazyProperty
619 def changed(self):
619 def changed(self):
620 """
620 """
621 Returns list of modified ``FileNode`` objects.
621 Returns list of modified ``FileNode`` objects.
622 """
622 """
623 raise NotImplementedError
623 raise NotImplementedError
624
624
625 @LazyProperty
625 @LazyProperty
626 def removed(self):
626 def removed(self):
627 """
627 """
628 Returns list of removed ``FileNode`` objects.
628 Returns list of removed ``FileNode`` objects.
629 """
629 """
630 raise NotImplementedError
630 raise NotImplementedError
631
631
632 @LazyProperty
632 @LazyProperty
633 def size(self):
633 def size(self):
634 """
634 """
635 Returns total number of bytes from contents of all filenodes.
635 Returns total number of bytes from contents of all filenodes.
636 """
636 """
637 return sum((node.size for node in self.get_filenodes_generator()))
637 return sum((node.size for node in self.get_filenodes_generator()))
638
638
639 def walk(self, topurl=''):
639 def walk(self, topurl=''):
640 """
640 """
641 Similar to os.walk method. Instead of filesystem it walks through
641 Similar to os.walk method. Instead of filesystem it walks through
642 changeset starting at given ``topurl``. Returns generator of tuples
642 changeset starting at given ``topurl``. Returns generator of tuples
643 (topnode, dirnodes, filenodes).
643 (topnode, dirnodes, filenodes).
644 """
644 """
645 topnode = self.get_node(topurl)
645 topnode = self.get_node(topurl)
646 yield (topnode, topnode.dirs, topnode.files)
646 yield (topnode, topnode.dirs, topnode.files)
647 for dirnode in topnode.dirs:
647 for dirnode in topnode.dirs:
648 for tup in self.walk(dirnode.path):
648 for tup in self.walk(dirnode.path):
649 yield tup
649 yield tup
650
650
651 def get_filenodes_generator(self):
651 def get_filenodes_generator(self):
652 """
652 """
653 Returns generator that yields *all* file nodes.
653 Returns generator that yields *all* file nodes.
654 """
654 """
655 for topnode, dirs, files in self.walk():
655 for topnode, dirs, files in self.walk():
656 for node in files:
656 for node in files:
657 yield node
657 yield node
658
658
659 def as_dict(self):
659 def as_dict(self):
660 """
660 """
661 Returns dictionary with changeset's attributes and their values.
661 Returns dictionary with changeset's attributes and their values.
662 """
662 """
663 data = get_dict_for_attrs(self, ['raw_id', 'short_id',
663 data = get_dict_for_attrs(self, ['raw_id', 'short_id',
664 'revision', 'date', 'message'])
664 'revision', 'date', 'message'])
665 data['author'] = {'name': self.author_name, 'email': self.author_email}
665 data['author'] = {'name': self.author_name, 'email': self.author_email}
666 data['added'] = [node.path for node in self.added]
666 data['added'] = [node.path for node in self.added]
667 data['changed'] = [node.path for node in self.changed]
667 data['changed'] = [node.path for node in self.changed]
668 data['removed'] = [node.path for node in self.removed]
668 data['removed'] = [node.path for node in self.removed]
669 return data
669 return data
670
670
671 @LazyProperty
671 @LazyProperty
672 def closesbranch(self):
672 def closesbranch(self):
673 return False
673 return False
674
674
675 @LazyProperty
675 @LazyProperty
676 def obsolete(self):
676 def obsolete(self):
677 return False
677 return False
678
678
679 @LazyProperty
679 @LazyProperty
680 def bumped(self):
680 def bumped(self):
681 return False
681 return False
682
682
683 @LazyProperty
683 @LazyProperty
684 def divergent(self):
684 def divergent(self):
685 return False
685 return False
686
686
687 @LazyProperty
687 @LazyProperty
688 def extinct(self):
688 def extinct(self):
689 return False
689 return False
690
690
691 @LazyProperty
691 @LazyProperty
692 def unstable(self):
692 def unstable(self):
693 return False
693 return False
694
694
695 @LazyProperty
695 @LazyProperty
696 def phase(self):
696 def phase(self):
697 return ''
697 return ''
698
698
699
699
700 class BaseWorkdir(object):
700 class BaseWorkdir(object):
701 """
701 """
702 Working directory representation of single repository.
702 Working directory representation of single repository.
703
703
704 :attribute: repository: repository object of working directory
704 :attribute: repository: repository object of working directory
705 """
705 """
706
706
707 def __init__(self, repository):
707 def __init__(self, repository):
708 self.repository = repository
708 self.repository = repository
709
709
710 def get_branch(self):
710 def get_branch(self):
711 """
711 """
712 Returns name of current branch.
712 Returns name of current branch.
713 """
713 """
714 raise NotImplementedError
714 raise NotImplementedError
715
715
716 def get_changeset(self):
716 def get_changeset(self):
717 """
717 """
718 Returns current changeset.
718 Returns current changeset.
719 """
719 """
720 raise NotImplementedError
720 raise NotImplementedError
721
721
722 def get_added(self):
722 def get_added(self):
723 """
723 """
724 Returns list of ``FileNode`` objects marked as *new* in working
724 Returns list of ``FileNode`` objects marked as *new* in working
725 directory.
725 directory.
726 """
726 """
727 raise NotImplementedError
727 raise NotImplementedError
728
728
729 def get_changed(self):
729 def get_changed(self):
730 """
730 """
731 Returns list of ``FileNode`` objects *changed* in working directory.
731 Returns list of ``FileNode`` objects *changed* in working directory.
732 """
732 """
733 raise NotImplementedError
733 raise NotImplementedError
734
734
735 def get_removed(self):
735 def get_removed(self):
736 """
736 """
737 Returns list of ``RemovedFileNode`` objects marked as *removed* in
737 Returns list of ``RemovedFileNode`` objects marked as *removed* in
738 working directory.
738 working directory.
739 """
739 """
740 raise NotImplementedError
740 raise NotImplementedError
741
741
742 def get_untracked(self):
742 def get_untracked(self):
743 """
743 """
744 Returns list of ``FileNode`` objects which are present within working
744 Returns list of ``FileNode`` objects which are present within working
745 directory however are not tracked by repository.
745 directory however are not tracked by repository.
746 """
746 """
747 raise NotImplementedError
747 raise NotImplementedError
748
748
749 def get_status(self):
749 def get_status(self):
750 """
750 """
751 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
751 Returns dict with ``added``, ``changed``, ``removed`` and ``untracked``
752 lists.
752 lists.
753 """
753 """
754 raise NotImplementedError
754 raise NotImplementedError
755
755
756 def commit(self, message, **kwargs):
756 def commit(self, message, **kwargs):
757 """
757 """
758 Commits local (from working directory) changes and returns newly
758 Commits local (from working directory) changes and returns newly
759 created
759 created
760 ``Changeset``. Updates repository's ``revisions`` list.
760 ``Changeset``. Updates repository's ``revisions`` list.
761
761
762 :raises ``CommitError``: if any error occurs while committing
762 :raises ``CommitError``: if any error occurs while committing
763 """
763 """
764 raise NotImplementedError
764 raise NotImplementedError
765
765
766 def update(self, revision=None):
766 def update(self, revision=None):
767 """
767 """
768 Fetches content of the given revision and populates it within working
768 Fetches content of the given revision and populates it within working
769 directory.
769 directory.
770 """
770 """
771 raise NotImplementedError
771 raise NotImplementedError
772
772
773 def checkout_branch(self, branch=None):
773 def checkout_branch(self, branch=None):
774 """
774 """
775 Checks out ``branch`` or the backend's default branch.
775 Checks out ``branch`` or the backend's default branch.
776
776
777 Raises ``BranchDoesNotExistError`` if the branch does not exist.
777 Raises ``BranchDoesNotExistError`` if the branch does not exist.
778 """
778 """
779 raise NotImplementedError
779 raise NotImplementedError
780
780
781
781
782 class BaseInMemoryChangeset(object):
782 class BaseInMemoryChangeset(object):
783 """
783 """
784 Represents differences between repository's state (most recent head) and
784 Represents differences between repository's state (most recent head) and
785 changes made *in place*.
785 changes made *in place*.
786
786
787 **Attributes**
787 **Attributes**
788
788
789 ``repository``
789 ``repository``
790 repository object for this in-memory-changeset
790 repository object for this in-memory-changeset
791
791
792 ``added``
792 ``added``
793 list of ``FileNode`` objects marked as *added*
793 list of ``FileNode`` objects marked as *added*
794
794
795 ``changed``
795 ``changed``
796 list of ``FileNode`` objects marked as *changed*
796 list of ``FileNode`` objects marked as *changed*
797
797
798 ``removed``
798 ``removed``
799 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
799 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
800 *removed*
800 *removed*
801
801
802 ``parents``
802 ``parents``
803 list of ``Changeset`` representing parents of in-memory changeset.
803 list of ``Changeset`` representing parents of in-memory changeset.
804 Should always be 2-element sequence.
804 Should always be 2-element sequence.
805
805
806 """
806 """
807
807
808 def __init__(self, repository):
808 def __init__(self, repository):
809 self.repository = repository
809 self.repository = repository
810 self.added = []
810 self.added = []
811 self.changed = []
811 self.changed = []
812 self.removed = []
812 self.removed = []
813 self.parents = []
813 self.parents = []
814
814
815 def add(self, *filenodes):
815 def add(self, *filenodes):
816 """
816 """
817 Marks given ``FileNode`` objects as *to be committed*.
817 Marks given ``FileNode`` objects as *to be committed*.
818
818
819 :raises ``NodeAlreadyExistsError``: if node with same path exists at
819 :raises ``NodeAlreadyExistsError``: if node with same path exists at
820 latest changeset
820 latest changeset
821 :raises ``NodeAlreadyAddedError``: if node with same path is already
821 :raises ``NodeAlreadyAddedError``: if node with same path is already
822 marked as *added*
822 marked as *added*
823 """
823 """
824 # Check if not already marked as *added* first
824 # Check if not already marked as *added* first
825 for node in filenodes:
825 for node in filenodes:
826 if node.path in (n.path for n in self.added):
826 if node.path in (n.path for n in self.added):
827 raise NodeAlreadyAddedError("Such FileNode %s is already "
827 raise NodeAlreadyAddedError("Such FileNode %s is already "
828 "marked for addition" % node.path)
828 "marked for addition" % node.path)
829 for node in filenodes:
829 for node in filenodes:
830 self.added.append(node)
830 self.added.append(node)
831
831
832 def change(self, *filenodes):
832 def change(self, *filenodes):
833 """
833 """
834 Marks given ``FileNode`` objects to be *changed* in next commit.
834 Marks given ``FileNode`` objects to be *changed* in next commit.
835
835
836 :raises ``EmptyRepositoryError``: if there are no changesets yet
836 :raises ``EmptyRepositoryError``: if there are no changesets yet
837 :raises ``NodeAlreadyExistsError``: if node with same path is already
837 :raises ``NodeAlreadyExistsError``: if node with same path is already
838 marked to be *changed*
838 marked to be *changed*
839 :raises ``NodeAlreadyRemovedError``: if node with same path is already
839 :raises ``NodeAlreadyRemovedError``: if node with same path is already
840 marked to be *removed*
840 marked to be *removed*
841 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
841 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
842 changeset
842 changeset
843 :raises ``NodeNotChangedError``: if node hasn't really be changed
843 :raises ``NodeNotChangedError``: if node hasn't really be changed
844 """
844 """
845 for node in filenodes:
845 for node in filenodes:
846 if node.path in (n.path for n in self.removed):
846 if node.path in (n.path for n in self.removed):
847 raise NodeAlreadyRemovedError("Node at %s is already marked "
847 raise NodeAlreadyRemovedError("Node at %s is already marked "
848 "as removed" % node.path)
848 "as removed" % node.path)
849 try:
849 try:
850 self.repository.get_changeset()
850 self.repository.get_changeset()
851 except EmptyRepositoryError:
851 except EmptyRepositoryError:
852 raise EmptyRepositoryError("Nothing to change - try to *add* new "
852 raise EmptyRepositoryError("Nothing to change - try to *add* new "
853 "nodes rather than changing them")
853 "nodes rather than changing them")
854 for node in filenodes:
854 for node in filenodes:
855 if node.path in (n.path for n in self.changed):
855 if node.path in (n.path for n in self.changed):
856 raise NodeAlreadyChangedError("Node at '%s' is already "
856 raise NodeAlreadyChangedError("Node at '%s' is already "
857 "marked as changed" % node.path)
857 "marked as changed" % node.path)
858 self.changed.append(node)
858 self.changed.append(node)
859
859
860 def remove(self, *filenodes):
860 def remove(self, *filenodes):
861 """
861 """
862 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
862 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
863 *removed* in next commit.
863 *removed* in next commit.
864
864
865 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
865 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
866 be *removed*
866 be *removed*
867 :raises ``NodeAlreadyChangedError``: if node has been already marked to
867 :raises ``NodeAlreadyChangedError``: if node has been already marked to
868 be *changed*
868 be *changed*
869 """
869 """
870 for node in filenodes:
870 for node in filenodes:
871 if node.path in (n.path for n in self.removed):
871 if node.path in (n.path for n in self.removed):
872 raise NodeAlreadyRemovedError("Node is already marked to "
872 raise NodeAlreadyRemovedError("Node is already marked to "
873 "for removal at %s" % node.path)
873 "for removal at %s" % node.path)
874 if node.path in (n.path for n in self.changed):
874 if node.path in (n.path for n in self.changed):
875 raise NodeAlreadyChangedError("Node is already marked to "
875 raise NodeAlreadyChangedError("Node is already marked to "
876 "be changed at %s" % node.path)
876 "be changed at %s" % node.path)
877 # We only mark node as *removed* - real removal is done by
877 # We only mark node as *removed* - real removal is done by
878 # commit method
878 # commit method
879 self.removed.append(node)
879 self.removed.append(node)
880
880
881 def reset(self):
881 def reset(self):
882 """
882 """
883 Resets this instance to initial state (cleans ``added``, ``changed``
883 Resets this instance to initial state (cleans ``added``, ``changed``
884 and ``removed`` lists).
884 and ``removed`` lists).
885 """
885 """
886 self.added = []
886 self.added = []
887 self.changed = []
887 self.changed = []
888 self.removed = []
888 self.removed = []
889 self.parents = []
889 self.parents = []
890
890
891 def get_ipaths(self):
891 def get_ipaths(self):
892 """
892 """
893 Returns generator of paths from nodes marked as added, changed or
893 Returns generator of paths from nodes marked as added, changed or
894 removed.
894 removed.
895 """
895 """
896 for node in itertools.chain(self.added, self.changed, self.removed):
896 for node in itertools.chain(self.added, self.changed, self.removed):
897 yield node.path
897 yield node.path
898
898
899 def get_paths(self):
899 def get_paths(self):
900 """
900 """
901 Returns list of paths from nodes marked as added, changed or removed.
901 Returns list of paths from nodes marked as added, changed or removed.
902 """
902 """
903 return list(self.get_ipaths())
903 return list(self.get_ipaths())
904
904
905 def check_integrity(self, parents=None):
905 def check_integrity(self, parents=None):
906 """
906 """
907 Checks in-memory changeset's integrity. Also, sets parents if not
907 Checks in-memory changeset's integrity. Also, sets parents if not
908 already set.
908 already set.
909
909
910 :raises CommitError: if any error occurs (i.e.
910 :raises CommitError: if any error occurs (i.e.
911 ``NodeDoesNotExistError``).
911 ``NodeDoesNotExistError``).
912 """
912 """
913 if not self.parents:
913 if not self.parents:
914 parents = parents or []
914 parents = parents or []
915 if len(parents) == 0:
915 if len(parents) == 0:
916 try:
916 try:
917 parents = [self.repository.get_changeset(), None]
917 parents = [self.repository.get_changeset(), None]
918 except EmptyRepositoryError:
918 except EmptyRepositoryError:
919 parents = [None, None]
919 parents = [None, None]
920 elif len(parents) == 1:
920 elif len(parents) == 1:
921 parents += [None]
921 parents += [None]
922 self.parents = parents
922 self.parents = parents
923
923
924 # Local parents, only if not None
924 # Local parents, only if not None
925 parents = [p for p in self.parents if p]
925 parents = [p for p in self.parents if p]
926
926
927 # Check nodes marked as added
927 # Check nodes marked as added
928 for p in parents:
928 for p in parents:
929 for node in self.added:
929 for node in self.added:
930 try:
930 try:
931 p.get_node(node.path)
931 p.get_node(node.path)
932 except NodeDoesNotExistError:
932 except NodeDoesNotExistError:
933 pass
933 pass
934 else:
934 else:
935 raise NodeAlreadyExistsError("Node at %s already exists "
935 raise NodeAlreadyExistsError("Node at %s already exists "
936 "at %s" % (node.path, p))
936 "at %s" % (node.path, p))
937
937
938 # Check nodes marked as changed
938 # Check nodes marked as changed
939 missing = set(node.path for node in self.changed)
939 missing = set(node.path for node in self.changed)
940 not_changed = set(node.path for node in self.changed)
940 not_changed = set(node.path for node in self.changed)
941 if self.changed and not parents:
941 if self.changed and not parents:
942 raise NodeDoesNotExistError(self.changed[0].path)
942 raise NodeDoesNotExistError(self.changed[0].path)
943 for p in parents:
943 for p in parents:
944 for node in self.changed:
944 for node in self.changed:
945 try:
945 try:
946 old = p.get_node(node.path)
946 old = p.get_node(node.path)
947 missing.remove(node.path)
947 missing.remove(node.path)
948 # if content actually changed, remove node from unchanged
948 # if content actually changed, remove node from unchanged
949 if old.content != node.content:
949 if old.content != node.content:
950 not_changed.remove(node.path)
950 not_changed.remove(node.path)
951 except NodeDoesNotExistError:
951 except NodeDoesNotExistError:
952 pass
952 pass
953 if self.changed and missing:
953 if self.changed and missing:
954 raise NodeDoesNotExistError("Node at %s is missing "
954 raise NodeDoesNotExistError("Node at %s is missing "
955 "(parents: %s)" % (node.path, parents))
955 "(parents: %s)" % (node.path, parents))
956
956
957 if self.changed and not_changed:
957 if self.changed and not_changed:
958 raise NodeNotChangedError("Node at %s wasn't actually changed "
958 raise NodeNotChangedError("Node at %s wasn't actually changed "
959 "since parents' changesets: %s" % (not_changed.pop(),
959 "since parents' changesets: %s" % (not_changed.pop(),
960 parents)
960 parents)
961 )
961 )
962
962
963 # Check nodes marked as removed
963 # Check nodes marked as removed
964 if self.removed and not parents:
964 if self.removed and not parents:
965 raise NodeDoesNotExistError("Cannot remove node at %s as there "
965 raise NodeDoesNotExistError("Cannot remove node at %s as there "
966 "were no parents specified" % self.removed[0].path)
966 "were no parents specified" % self.removed[0].path)
967 really_removed = set()
967 really_removed = set()
968 for p in parents:
968 for p in parents:
969 for node in self.removed:
969 for node in self.removed:
970 try:
970 try:
971 p.get_node(node.path)
971 p.get_node(node.path)
972 really_removed.add(node.path)
972 really_removed.add(node.path)
973 except ChangesetError:
973 except ChangesetError:
974 pass
974 pass
975 not_removed = list(set(node.path for node in self.removed) - really_removed)
975 not_removed = list(set(node.path for node in self.removed) - really_removed)
976 if not_removed:
976 if not_removed:
977 raise NodeDoesNotExistError("Cannot remove node at %s from "
977 raise NodeDoesNotExistError("Cannot remove node at %s from "
978 "following parents: %s" % (not_removed[0], parents))
978 "following parents: %s" % (not_removed[0], parents))
979
979
980 def commit(self, message, author, parents=None, branch=None, date=None,
980 def commit(self, message, author, parents=None, branch=None, date=None,
981 **kwargs):
981 **kwargs):
982 """
982 """
983 Performs in-memory commit (doesn't check workdir in any way) and
983 Performs in-memory commit (doesn't check workdir in any way) and
984 returns newly created ``Changeset``. Updates repository's
984 returns newly created ``Changeset``. Updates repository's
985 ``revisions``.
985 ``revisions``.
986
986
987 .. note::
987 .. note::
988 While overriding this method each backend's should call
988 While overriding this method each backend's should call
989 ``self.check_integrity(parents)`` in the first place.
989 ``self.check_integrity(parents)`` in the first place.
990
990
991 :param message: message of the commit
991 :param message: message of the commit
992 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
992 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
993 :param parents: single parent or sequence of parents from which commit
993 :param parents: single parent or sequence of parents from which commit
994 would be derived
994 would be derived
995 :param date: ``datetime.datetime`` instance. Defaults to
995 :param date: ``datetime.datetime`` instance. Defaults to
996 ``datetime.datetime.now()``.
996 ``datetime.datetime.now()``.
997 :param branch: branch name, as string. If none given, default backend's
997 :param branch: branch name, as string. If none given, default backend's
998 branch would be used.
998 branch would be used.
999
999
1000 :raises ``CommitError``: if any error occurs while committing
1000 :raises ``CommitError``: if any error occurs while committing
1001 """
1001 """
1002 raise NotImplementedError
1002 raise NotImplementedError
1003
1003
1004
1004
1005 class EmptyChangeset(BaseChangeset):
1005 class EmptyChangeset(BaseChangeset):
1006 """
1006 """
1007 An dummy empty changeset. It's possible to pass hash when creating
1007 An dummy empty changeset. It's possible to pass hash when creating
1008 an EmptyChangeset
1008 an EmptyChangeset
1009 """
1009 """
1010
1010
1011 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1011 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1012 alias=None, revision=-1, message='', author='', date=None):
1012 alias=None, revision=-1, message='', author='', date=None):
1013 self._empty_cs = cs
1013 self._empty_cs = cs
1014 self.revision = revision
1014 self.revision = revision
1015 self.message = message
1015 self.message = message
1016 self.author = author
1016 self.author = author
1017 self.date = date or datetime.datetime.fromtimestamp(0)
1017 self.date = date or datetime.datetime.fromtimestamp(0)
1018 self.repository = repo
1018 self.repository = repo
1019 self.requested_revision = requested_revision
1019 self.requested_revision = requested_revision
1020 self.alias = alias
1020 self.alias = alias
1021
1021
1022 @LazyProperty
1022 @LazyProperty
1023 def raw_id(self):
1023 def raw_id(self):
1024 """
1024 """
1025 Returns raw string identifying this changeset, useful for web
1025 Returns raw string identifying this changeset, useful for web
1026 representation.
1026 representation.
1027 """
1027 """
1028
1028
1029 return self._empty_cs
1029 return self._empty_cs
1030
1030
1031 @LazyProperty
1031 @LazyProperty
1032 def branch(self):
1032 def branch(self):
1033 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1033 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1034
1034
1035 @LazyProperty
1035 @LazyProperty
1036 def branches(self):
1036 def branches(self):
1037 return [self.branch]
1037 return [self.branch]
1038
1038
1039 @LazyProperty
1039 @LazyProperty
1040 def short_id(self):
1040 def short_id(self):
1041 return self.raw_id[:12]
1041 return self.raw_id[:12]
1042
1042
1043 def get_file_changeset(self, path):
1043 def get_file_changeset(self, path):
1044 return self
1044 return self
1045
1045
1046 def get_file_content(self, path):
1046 def get_file_content(self, path):
1047 return b''
1047 return b''
1048
1048
1049 def get_file_size(self, path):
1049 def get_file_size(self, path):
1050 return 0
1050 return 0
1051
1051
1052
1052
1053 class CollectionGenerator(object):
1053 class CollectionGenerator(object):
1054
1054
1055 def __init__(self, repo, revs):
1055 def __init__(self, repo, revs):
1056 self.repo = repo
1056 self.repo = repo
1057 self.revs = revs
1057 self.revs = revs
1058
1058
1059 def __len__(self):
1059 def __len__(self):
1060 return len(self.revs)
1060 return len(self.revs)
1061
1061
1062 def __iter__(self):
1062 def __iter__(self):
1063 for rev in self.revs:
1063 for rev in self.revs:
1064 yield self.repo.get_changeset(rev)
1064 yield self.repo.get_changeset(rev)
1065
1065
1066 def __getitem__(self, what):
1066 def __getitem__(self, what):
1067 """Return either a single element by index, or a sliced collection."""
1067 """Return either a single element by index, or a sliced collection."""
1068 if isinstance(what, slice):
1068 if isinstance(what, slice):
1069 return CollectionGenerator(self.repo, self.revs[what])
1069 return CollectionGenerator(self.repo, self.revs[what])
1070 else:
1070 else:
1071 # single item
1071 # single item
1072 return self.repo.get_changeset(self.revs[what])
1072 return self.repo.get_changeset(self.revs[what])
1073
1073
1074 def __repr__(self):
1074 def __repr__(self):
1075 return '<CollectionGenerator[len:%s]>' % (len(self))
1075 return '<CollectionGenerator[len:%s]>' % (len(self))
@@ -1,690 +1,690 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.hg.repository
3 vcs.backends.hg.repository
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Mercurial repository implementation.
6 Mercurial repository implementation.
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import datetime
12 import datetime
13 import logging
13 import logging
14 import os
14 import os
15 import time
15 import time
16 import urllib.error
16 import urllib.error
17 import urllib.parse
17 import urllib.parse
18 import urllib.request
18 import urllib.request
19 from collections import OrderedDict
19 from collections import OrderedDict
20
20
21 import mercurial.commands
21 import mercurial.commands
22 import mercurial.error
22 import mercurial.error
23 import mercurial.exchange
23 import mercurial.exchange
24 import mercurial.hg
24 import mercurial.hg
25 import mercurial.hgweb
25 import mercurial.hgweb
26 import mercurial.httppeer
26 import mercurial.httppeer
27 import mercurial.localrepo
27 import mercurial.localrepo
28 import mercurial.match
28 import mercurial.match
29 import mercurial.mdiff
29 import mercurial.mdiff
30 import mercurial.node
30 import mercurial.node
31 import mercurial.patch
31 import mercurial.patch
32 import mercurial.scmutil
32 import mercurial.scmutil
33 import mercurial.sshpeer
33 import mercurial.sshpeer
34 import mercurial.tags
34 import mercurial.tags
35 import mercurial.ui
35 import mercurial.ui
36 import mercurial.unionrepo
36 import mercurial.unionrepo
37
37
38
38
39 try:
39 try:
40 from mercurial.utils.urlutil import url as hg_url
40 from mercurial.utils.urlutil import url as hg_url
41 except ImportError: # urlutil was introduced in Mercurial 5.8
41 except ImportError: # urlutil was introduced in Mercurial 5.8
42 from mercurial.util import url as hg_url
42 from mercurial.util import url as hg_url
43
43
44 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
44 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
45 from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
45 from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
46 TagDoesNotExistError, VCSError)
46 TagDoesNotExistError, VCSError)
47 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, author_email, author_name, date_fromtimestamp, makedate, safe_bytes, safe_str
47 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, author_email, author_name, date_fromtimestamp, makedate, safe_bytes, safe_str
48 from kallithea.lib.vcs.utils.helpers import get_urllib_request_handlers
48 from kallithea.lib.vcs.utils.helpers import get_urllib_request_handlers
49 from kallithea.lib.vcs.utils.lazy import LazyProperty
49 from kallithea.lib.vcs.utils.lazy import LazyProperty
50 from kallithea.lib.vcs.utils.paths import abspath
50 from kallithea.lib.vcs.utils.paths import abspath
51
51
52 from . import changeset, inmemory, workdir
52 from . import changeset, inmemory, workdir
53
53
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class MercurialRepository(BaseRepository):
58 class MercurialRepository(BaseRepository):
59 """
59 """
60 Mercurial repository backend
60 Mercurial repository backend
61 """
61 """
62 DEFAULT_BRANCH_NAME = 'default'
62 DEFAULT_BRANCH_NAME = 'default'
63 scm = 'hg'
63 scm = 'hg'
64
64
65 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
65 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
66 update_after_clone=False):
66 update_after_clone=False):
67 """
67 """
68 Raises RepositoryError if repository could not be find at the given
68 Raises RepositoryError if repository could not be find at the given
69 ``repo_path``.
69 ``repo_path``.
70
70
71 :param repo_path: local path of the repository
71 :param repo_path: local path of the repository
72 :param create=False: if set to True, would try to create repository if
72 :param create=False: if set to True, would try to create repository if
73 it does not exist rather than raising exception
73 it does not exist rather than raising exception
74 :param baseui=None: user data
74 :param baseui=None: user data
75 :param src_url=None: would try to clone repository from given location
75 :param src_url=None: would try to clone repository from given location
76 :param update_after_clone=False: sets update of working copy after
76 :param update_after_clone=False: sets update of working copy after
77 making a clone
77 making a clone
78 """
78 """
79
79
80 if not isinstance(repo_path, str):
80 if not isinstance(repo_path, str):
81 raise VCSError('Mercurial backend requires repository path to '
81 raise VCSError('Mercurial backend requires repository path to '
82 'be instance of <str> got %s instead' %
82 'be instance of <str> got %s instead' %
83 type(repo_path))
83 type(repo_path))
84 self.path = abspath(repo_path)
84 self.path = abspath(repo_path)
85 self.baseui = baseui or mercurial.ui.ui()
85 self.baseui = baseui or mercurial.ui.ui()
86 # We've set path and ui, now we can set _repo itself
86 # We've set path and ui, now we can set _repo itself
87 self._repo = self._get_repo(create, src_url, update_after_clone)
87 self._repo = self._get_repo(create, src_url, update_after_clone)
88
88
89 @property
89 @property
90 def _empty(self):
90 def _empty(self):
91 """
91 """
92 Checks if repository is empty ie. without any changesets
92 Checks if repository is empty ie. without any changesets
93 """
93 """
94 # TODO: Following raises errors when using InMemoryChangeset...
94 # TODO: Following raises errors when using InMemoryChangeset...
95 # return len(self._repo.changelog) == 0
95 # return len(self._repo.changelog) == 0
96 return len(self.revisions) == 0
96 return len(self.revisions) == 0
97
97
98 @LazyProperty
98 @LazyProperty
99 def revisions(self):
99 def revisions(self):
100 """
100 """
101 Returns list of revisions' ids, in ascending order. Being lazy
101 Returns list of revisions' ids, in ascending order. Being lazy
102 attribute allows external tools to inject shas from cache.
102 attribute allows external tools to inject shas from cache.
103 """
103 """
104 return self._get_all_revisions()
104 return self._get_all_revisions()
105
105
106 @LazyProperty
106 @LazyProperty
107 def name(self):
107 def name(self):
108 return os.path.basename(self.path)
108 return os.path.basename(self.path)
109
109
110 @LazyProperty
110 @LazyProperty
111 def branches(self):
111 def branches(self):
112 return self._get_branches()
112 return self._get_branches()
113
113
114 @LazyProperty
114 @LazyProperty
115 def closed_branches(self):
115 def closed_branches(self):
116 return self._get_branches(normal=False, closed=True)
116 return self._get_branches(normal=False, closed=True)
117
117
118 @LazyProperty
118 @LazyProperty
119 def allbranches(self):
119 def allbranches(self):
120 """
120 """
121 List all branches, including closed branches.
121 List all branches, including closed branches.
122 """
122 """
123 return self._get_branches(closed=True)
123 return self._get_branches(closed=True)
124
124
125 def _get_branches(self, normal=True, closed=False):
125 def _get_branches(self, normal=True, closed=False):
126 """
126 """
127 Gets branches for this repository
127 Gets branches for this repository
128 Returns only not closed branches by default
128 Returns only not closed branches by default
129
129
130 :param closed: return also closed branches for mercurial
130 :param closed: return also closed branches for mercurial
131 :param normal: return also normal branches
131 :param normal: return also normal branches
132 """
132 """
133
133
134 if self._empty:
134 if self._empty:
135 return {}
135 return {}
136
136
137 bt = OrderedDict()
137 bt = OrderedDict()
138 for bn, _heads, node, isclosed in sorted(self._repo.branchmap().iterbranches()):
138 for bn, _heads, node, isclosed in sorted(self._repo.branchmap().iterbranches()):
139 if isclosed:
139 if isclosed:
140 if closed:
140 if closed:
141 bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
141 bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
142 else:
142 else:
143 if normal:
143 if normal:
144 bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
144 bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
145 return bt
145 return bt
146
146
147 @LazyProperty
147 @LazyProperty
148 def tags(self):
148 def tags(self):
149 """
149 """
150 Gets tags for this repository
150 Gets tags for this repository
151 """
151 """
152 return self._get_tags()
152 return self._get_tags()
153
153
154 def _get_tags(self):
154 def _get_tags(self):
155 if self._empty:
155 if self._empty:
156 return {}
156 return {}
157
157
158 return OrderedDict(sorted(
158 return OrderedDict(sorted(
159 ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo.tags().items()),
159 ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo.tags().items()),
160 reverse=True,
160 reverse=True,
161 key=lambda x: x[0], # sort by name
161 key=lambda x: x[0], # sort by name
162 ))
162 ))
163
163
164 def tag(self, name, user, revision=None, message=None, date=None,
164 def tag(self, name, user, revision=None, message=None, date=None,
165 **kwargs):
165 **kwargs):
166 """
166 """
167 Creates and returns a tag for the given ``revision``.
167 Creates and returns a tag for the given ``revision``.
168
168
169 :param name: name for new tag
169 :param name: name for new tag
170 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
170 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
171 :param revision: changeset id for which new tag would be created
171 :param revision: changeset id for which new tag would be created
172 :param message: message of the tag's commit
172 :param message: message of the tag's commit
173 :param date: date of tag's commit
173 :param date: date of tag's commit
174
174
175 :raises TagAlreadyExistError: if tag with same name already exists
175 :raises TagAlreadyExistError: if tag with same name already exists
176 """
176 """
177 if name in self.tags:
177 if name in self.tags:
178 raise TagAlreadyExistError("Tag %s already exists" % name)
178 raise TagAlreadyExistError("Tag %s already exists" % name)
179 changeset = self.get_changeset(revision)
179 changeset = self.get_changeset(revision)
180 local = kwargs.setdefault('local', False)
180 local = kwargs.setdefault('local', False)
181
181
182 if message is None:
182 if message is None:
183 message = "Added tag %s for changeset %s" % (name,
183 message = "Added tag %s for changeset %s" % (name,
184 changeset.short_id)
184 changeset.short_id)
185
185
186 if date is None:
186 if date is None:
187 date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
187 date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
188
188
189 try:
189 try:
190 mercurial.tags.tag(self._repo, safe_bytes(name), changeset._ctx.node(), safe_bytes(message), local, safe_bytes(user), date)
190 mercurial.tags.tag(self._repo, safe_bytes(name), changeset._ctx.node(), safe_bytes(message), local, safe_bytes(user), date)
191 except mercurial.error.Abort as e:
191 except mercurial.error.Abort as e:
192 raise RepositoryError(e.args[0])
192 raise RepositoryError(e.args[0])
193
193
194 # Reinitialize tags
194 # Reinitialize tags
195 self.tags = self._get_tags()
195 self.tags = self._get_tags()
196 tag_id = self.tags[name]
196 tag_id = self.tags[name]
197
197
198 return self.get_changeset(revision=tag_id)
198 return self.get_changeset(revision=tag_id)
199
199
200 def remove_tag(self, name, user, message=None, date=None):
200 def remove_tag(self, name, user, message=None, date=None):
201 """
201 """
202 Removes tag with the given ``name``.
202 Removes tag with the given ``name``.
203
203
204 :param name: name of the tag to be removed
204 :param name: name of the tag to be removed
205 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
205 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
206 :param message: message of the tag's removal commit
206 :param message: message of the tag's removal commit
207 :param date: date of tag's removal commit
207 :param date: date of tag's removal commit
208
208
209 :raises TagDoesNotExistError: if tag with given name does not exists
209 :raises TagDoesNotExistError: if tag with given name does not exists
210 """
210 """
211 if name not in self.tags:
211 if name not in self.tags:
212 raise TagDoesNotExistError("Tag %s does not exist" % name)
212 raise TagDoesNotExistError("Tag %s does not exist" % name)
213 if message is None:
213 if message is None:
214 message = "Removed tag %s" % name
214 message = "Removed tag %s" % name
215 if date is None:
215 if date is None:
216 date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
216 date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
217 local = False
217 local = False
218
218
219 try:
219 try:
220 mercurial.tags.tag(self._repo, safe_bytes(name), mercurial.node.nullid, safe_bytes(message), local, safe_bytes(user), date)
220 mercurial.tags.tag(self._repo, safe_bytes(name), mercurial.node.nullid, safe_bytes(message), local, safe_bytes(user), date)
221 self.tags = self._get_tags()
221 self.tags = self._get_tags()
222 except mercurial.error.Abort as e:
222 except mercurial.error.Abort as e:
223 raise RepositoryError(e.args[0])
223 raise RepositoryError(e.args[0])
224
224
225 @LazyProperty
225 @LazyProperty
226 def bookmarks(self):
226 def bookmarks(self):
227 """
227 """
228 Gets bookmarks for this repository
228 Gets bookmarks for this repository
229 """
229 """
230 return self._get_bookmarks()
230 return self._get_bookmarks()
231
231
232 def _get_bookmarks(self):
232 def _get_bookmarks(self):
233 if self._empty:
233 if self._empty:
234 return {}
234 return {}
235
235
236 return OrderedDict(sorted(
236 return OrderedDict(sorted(
237 ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo._bookmarks.items()),
237 ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo._bookmarks.items()),
238 reverse=True,
238 reverse=True,
239 key=lambda x: x[0], # sort by name
239 key=lambda x: x[0], # sort by name
240 ))
240 ))
241
241
242 def _get_all_revisions(self):
242 def _get_all_revisions(self):
243 return [ascii_str(self._repo[x].hex()) for x in self._repo.filtered(b'visible').changelog.revs()]
243 return [ascii_str(self._repo[x].hex()) for x in self._repo.filtered(b'visible').changelog.revs()]
244
244
245 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
245 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
246 context=3):
246 context=3):
247 """
247 """
248 Returns (git like) *diff*, as plain text. Shows changes introduced by
248 Returns (git like) *diff*, as plain text. Shows changes introduced by
249 ``rev2`` since ``rev1``.
249 ``rev2`` since ``rev1``.
250
250
251 :param rev1: Entry point from which diff is shown. Can be
251 :param rev1: Entry point from which diff is shown. Can be
252 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
252 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
253 the changes since empty state of the repository until ``rev2``
253 the changes since empty state of the repository until ``rev2``
254 :param rev2: Until which revision changes should be shown.
254 :param rev2: Until which revision changes should be shown.
255 :param ignore_whitespace: If set to ``True``, would not show whitespace
255 :param ignore_whitespace: If set to ``True``, would not show whitespace
256 changes. Defaults to ``False``.
256 changes. Defaults to ``False``.
257 :param context: How many lines before/after changed lines should be
257 :param context: How many lines before/after changed lines should be
258 shown. Defaults to ``3``. If negative value is passed-in, it will be
258 shown. Defaults to ``3``. If negative value is passed-in, it will be
259 set to ``0`` instead.
259 set to ``0`` instead.
260 """
260 """
261
261
262 # Negative context values make no sense, and will result in
262 # Negative context values make no sense, and will result in
263 # errors. Ensure this does not happen.
263 # errors. Ensure this does not happen.
264 if context < 0:
264 if context < 0:
265 context = 0
265 context = 0
266
266
267 if hasattr(rev1, 'raw_id'):
267 if hasattr(rev1, 'raw_id'):
268 rev1 = getattr(rev1, 'raw_id')
268 rev1 = getattr(rev1, 'raw_id')
269
269
270 if hasattr(rev2, 'raw_id'):
270 if hasattr(rev2, 'raw_id'):
271 rev2 = getattr(rev2, 'raw_id')
271 rev2 = getattr(rev2, 'raw_id')
272
272
273 # Check if given revisions are present at repository (may raise
273 # Check if given revisions are present at repository (may raise
274 # ChangesetDoesNotExistError)
274 # ChangesetDoesNotExistError)
275 if rev1 != self.EMPTY_CHANGESET:
275 if rev1 != self.EMPTY_CHANGESET:
276 self.get_changeset(rev1)
276 self.get_changeset(rev1)
277 self.get_changeset(rev2)
277 self.get_changeset(rev2)
278 if path:
278 if path:
279 file_filter = mercurial.match.exact([safe_bytes(path)])
279 file_filter = mercurial.match.exact([safe_bytes(path)])
280 else:
280 else:
281 file_filter = None
281 file_filter = None
282
282
283 return b''.join(mercurial.patch.diff(self._repo, rev1, rev2, match=file_filter,
283 return b''.join(mercurial.patch.diff(self._repo, rev1, rev2, match=file_filter,
284 opts=mercurial.mdiff.diffopts(git=True,
284 opts=mercurial.mdiff.diffopts(git=True,
285 showfunc=True,
285 showfunc=True,
286 ignorews=ignore_whitespace,
286 ignorews=ignore_whitespace,
287 context=context)))
287 context=context)))
288
288
289 @staticmethod
289 @staticmethod
290 def _check_url(url, repoui=None):
290 def _check_url(url, repoui=None):
291 r"""
291 r"""
292 Raise URLError if url doesn't seem like a valid safe Hg URL. We
292 Raise URLError if url doesn't seem like a valid safe Hg URL. We
293 only allow http, https, ssh, and hg-git URLs.
293 only allow http, https, ssh, and hg-git URLs.
294
294
295 For http, https and git URLs, make a connection and probe to see if it is valid.
295 For http, https and git URLs, make a connection and probe to see if it is valid.
296
296
297 On failures it'll raise urllib2.HTTPError, exception is also thrown
297 On failures it'll raise urllib2.HTTPError, exception is also thrown
298 when the return code is non 200
298 when the return code is non 200
299
299
300 >>> MercurialRepository._check_url('file:///repo')
300 >>> MercurialRepository._check_url('file:///repo')
301
301
302 >>> MercurialRepository._check_url('http://example.com:65537/repo')
302 >>> MercurialRepository._check_url('http://example.com:65537/repo')
303 Traceback (most recent call last):
303 Traceback (most recent call last):
304 ...
304 ...
305 urllib.error.URLError: <urlopen error Error parsing URL: 'http://example.com:65537/repo'>
305 urllib.error.URLError: <urlopen error Error parsing URL: 'http://example.com:65537/repo'>
306 >>> MercurialRepository._check_url('foo')
306 >>> MercurialRepository._check_url('foo')
307 Traceback (most recent call last):
307 Traceback (most recent call last):
308 ...
308 ...
309 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'foo'>
309 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'foo'>
310 >>> MercurialRepository._check_url('git+ssh://example.com/my%20fine repo')
310 >>> MercurialRepository._check_url('git+ssh://example.com/my%20fine repo')
311 Traceback (most recent call last):
311 Traceback (most recent call last):
312 ...
312 ...
313 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'git+ssh://example.com/my%20fine repo'>
313 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'git+ssh://example.com/my%20fine repo'>
314 >>> MercurialRepository._check_url('svn+http://example.com/repo')
314 >>> MercurialRepository._check_url('svn+http://example.com/repo')
315 Traceback (most recent call last):
315 Traceback (most recent call last):
316 ...
316 ...
317 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'svn+http://example.com/repo'>
317 urllib.error.URLError: <urlopen error Unsupported protocol in URL 'svn+http://example.com/repo'>
318 """
318 """
319 try:
319 try:
320 parsed_url = urllib.parse.urlparse(url)
320 parsed_url = urllib.parse.urlparse(url)
321 parsed_url.port # trigger netloc parsing which might raise ValueError
321 parsed_url.port # trigger netloc parsing which might raise ValueError
322 except ValueError:
322 except ValueError:
323 raise urllib.error.URLError("Error parsing URL: %r" % url)
323 raise urllib.error.URLError("Error parsing URL: %r" % url)
324
324
325 # check first if it's not an local url
325 # check first if it's not an local url
326 if os.path.isabs(url) and os.path.isdir(url) or parsed_url.scheme == 'file':
326 if os.path.isabs(url) and os.path.isdir(url) or parsed_url.scheme == 'file':
327 # When creating repos, _get_url will use file protocol for local paths
327 # When creating repos, _get_url will use file protocol for local paths
328 return
328 return
329
329
330 if parsed_url.scheme not in ['http', 'https', 'ssh', 'git+http', 'git+https']:
330 if parsed_url.scheme not in ['http', 'https', 'ssh', 'git+http', 'git+https']:
331 raise urllib.error.URLError("Unsupported protocol in URL %r" % url)
331 raise urllib.error.URLError("Unsupported protocol in URL %r" % url)
332
332
333 url = safe_bytes(url)
333 url = safe_bytes(url)
334
334
335 if parsed_url.scheme == 'ssh':
335 if parsed_url.scheme == 'ssh':
336 # in case of invalid uri or authentication issues, sshpeer will
336 # in case of invalid uri or authentication issues, sshpeer will
337 # throw an exception.
337 # throw an exception.
338 mercurial.sshpeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
338 mercurial.sshpeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
339 return
339 return
340
340
341 if '+' in parsed_url.scheme: # strip 'git+' for hg-git URLs
341 if '+' in parsed_url.scheme: # strip 'git+' for hg-git URLs
342 url = url.split(b'+', 1)[1]
342 url = url.split(b'+', 1)[1]
343
343
344 url_obj = hg_url(url)
344 url_obj = hg_url(url)
345 test_uri, handlers = get_urllib_request_handlers(url_obj)
345 test_uri, handlers = get_urllib_request_handlers(url_obj)
346
346
347 url_obj.passwd = b'*****'
347 url_obj.passwd = b'*****'
348 cleaned_uri = str(url_obj)
348 cleaned_uri = str(url_obj)
349
349
350 o = urllib.request.build_opener(*handlers)
350 o = urllib.request.build_opener(*handlers)
351 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
351 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
352 ('Accept', 'application/mercurial-0.1')]
352 ('Accept', 'application/mercurial-0.1')]
353
353
354 req = urllib.request.Request(
354 req = urllib.request.Request(
355 "%s?%s" % (
355 "%s?%s" % (
356 safe_str(test_uri),
356 safe_str(test_uri),
357 urllib.parse.urlencode({
357 urllib.parse.urlencode({
358 'cmd': 'between',
358 'cmd': 'between',
359 'pairs': "%s-%s" % ('0' * 40, '0' * 40),
359 'pairs': "%s-%s" % ('0' * 40, '0' * 40),
360 })
360 })
361 ))
361 ))
362
362
363 try:
363 try:
364 resp = o.open(req)
364 resp = o.open(req)
365 if resp.code != 200:
365 if resp.code != 200:
366 raise Exception('Return Code is not 200')
366 raise Exception('Return Code is not 200')
367 except Exception as e:
367 except Exception as e:
368 # means it cannot be cloned
368 # means it cannot be cloned
369 raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
369 raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
370
370
371 if parsed_url.scheme in ['http', 'https']: # skip git+http://... etc
371 if parsed_url.scheme in ['http', 'https']: # skip git+http://... etc
372 # now check if it's a proper hg repo
372 # now check if it's a proper hg repo
373 try:
373 try:
374 mercurial.httppeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
374 mercurial.httppeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
375 except Exception as e:
375 except Exception as e:
376 raise urllib.error.URLError(
376 raise urllib.error.URLError(
377 "url [%s] does not look like an hg repo org_exc: %s"
377 "url [%s] does not look like an hg repo org_exc: %s"
378 % (cleaned_uri, e))
378 % (cleaned_uri, e))
379
379
380 def _get_repo(self, create, src_url=None, update_after_clone=False):
380 def _get_repo(self, create, src_url=None, update_after_clone=False):
381 """
381 """
382 Function will check for mercurial repository in given path and return
382 Function will check for mercurial repository in given path and return
383 a localrepo object. If there is no repository in that path it will
383 a localrepo object. If there is no repository in that path it will
384 raise an exception unless ``create`` parameter is set to True - in
384 raise an exception unless ``create`` parameter is set to True - in
385 that case repository would be created and returned.
385 that case repository would be created and returned.
386 If ``src_url`` is given, would try to clone repository from the
386 If ``src_url`` is given, would try to clone repository from the
387 location at given clone_point. Additionally it'll make update to
387 location at given clone_point. Additionally it'll make update to
388 working copy accordingly to ``update_after_clone`` flag
388 working copy accordingly to ``update_after_clone`` flag
389 """
389 """
390 try:
390 try:
391 if src_url:
391 if src_url:
392 url = self._get_url(src_url)
392 url = self._get_url(src_url)
393 opts = {}
393 opts = {}
394 if not update_after_clone:
394 if not update_after_clone:
395 opts.update({'noupdate': True})
395 opts.update({'noupdate': True})
396 MercurialRepository._check_url(url, self.baseui)
396 MercurialRepository._check_url(url, self.baseui)
397 mercurial.commands.clone(self.baseui, safe_bytes(url), safe_bytes(self.path), **opts)
397 mercurial.commands.clone(self.baseui, safe_bytes(url), safe_bytes(self.path), **opts)
398
398
399 # Don't try to create if we've already cloned repo
399 # Don't try to create if we've already cloned repo
400 create = False
400 create = False
401 return mercurial.localrepo.instance(self.baseui, safe_bytes(self.path), create=create)
401 return mercurial.localrepo.instance(self.baseui, safe_bytes(self.path), create=create)
402 except (mercurial.error.Abort, mercurial.error.RepoError) as err:
402 except (mercurial.error.Abort, mercurial.error.RepoError) as err:
403 if create:
403 if create:
404 msg = "Cannot create repository at %s. Original error was %s" \
404 msg = "Cannot create repository at %s. Original error was %s" \
405 % (self.name, err)
405 % (self.name, err)
406 else:
406 else:
407 msg = "Not valid repository at %s. Original error was %s" \
407 msg = "Not valid repository at %s. Original error was %s" \
408 % (self.name, err)
408 % (self.name, err)
409 raise RepositoryError(msg)
409 raise RepositoryError(msg)
410
410
411 @LazyProperty
411 @LazyProperty
412 def in_memory_changeset(self):
412 def in_memory_changeset(self):
413 return inmemory.MercurialInMemoryChangeset(self)
413 return inmemory.MercurialInMemoryChangeset(self)
414
414
415 @LazyProperty
415 @LazyProperty
416 def description(self):
416 def description(self):
417 _desc = self._repo.ui.config(b'web', b'description', None, untrusted=True)
417 _desc = self._repo.ui.config(b'web', b'description', None, untrusted=True)
418 return safe_str(_desc or b'unknown')
418 return safe_str(_desc or b'unknown')
419
419
420 @LazyProperty
420 @LazyProperty
421 def last_change(self):
421 def last_change(self):
422 """
422 """
423 Returns last change made on this repository as datetime object
423 Returns last change made on this repository as datetime object
424 """
424 """
425 return date_fromtimestamp(self._get_mtime(), makedate()[1])
425 return date_fromtimestamp(self._get_mtime(), makedate()[1])
426
426
427 def _get_mtime(self):
427 def _get_mtime(self):
428 try:
428 try:
429 return time.mktime(self.get_changeset().date.timetuple())
429 return time.mktime(self.get_changeset().date.timetuple())
430 except RepositoryError:
430 except RepositoryError:
431 # fallback to filesystem
431 # fallback to filesystem
432 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
432 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
433 st_path = os.path.join(self.path, '.hg', "store")
433 st_path = os.path.join(self.path, '.hg', "store")
434 if os.path.exists(cl_path):
434 if os.path.exists(cl_path):
435 return os.stat(cl_path).st_mtime
435 return os.stat(cl_path).st_mtime
436 else:
436 else:
437 return os.stat(st_path).st_mtime
437 return os.stat(st_path).st_mtime
438
438
439 def _get_revision(self, revision):
439 def _get_revision(self, revision):
440 """
440 """
441 Given any revision identifier, returns a 40 char string with revision hash.
441 Given any revision identifier, returns a 40 char string with revision hash.
442
442
443 :param revision: str or int or None
443 :param revision: str or int or None
444 """
444 """
445 if self._empty:
445 if self._empty:
446 raise EmptyRepositoryError("There are no changesets yet")
446 raise EmptyRepositoryError("There are no changesets yet")
447
447
448 if revision in [-1, None]:
448 if revision in [-1, None]:
449 revision = b'tip'
449 revision = b'tip'
450 elif isinstance(revision, str):
450 elif isinstance(revision, str):
451 revision = safe_bytes(revision)
451 revision = safe_bytes(revision)
452
452
453 try:
453 try:
454 if isinstance(revision, int):
454 if isinstance(revision, int):
455 return ascii_str(self._repo[revision].hex())
455 return ascii_str(self._repo[revision].hex())
456 return ascii_str(mercurial.scmutil.revsymbol(self._repo, revision).hex())
456 return ascii_str(mercurial.scmutil.revsymbol(self._repo, revision).hex())
457 except (IndexError, ValueError, mercurial.error.RepoLookupError, TypeError):
457 except (IndexError, ValueError, mercurial.error.RepoLookupError, TypeError):
458 msg = "Revision %r does not exist for %s" % (safe_str(revision), self.name)
458 msg = "Revision %r does not exist for %s" % (safe_str(revision), self.name)
459 raise ChangesetDoesNotExistError(msg)
459 raise ChangesetDoesNotExistError(msg)
460 except (LookupError, ):
460 except (LookupError, ):
461 msg = "Ambiguous identifier `%s` for %s" % (safe_str(revision), self.name)
461 msg = "Ambiguous identifier `%s` for %s" % (safe_str(revision), self.name)
462 raise ChangesetDoesNotExistError(msg)
462 raise ChangesetDoesNotExistError(msg)
463
463
464 def get_ref_revision(self, ref_type, ref_name):
464 def get_ref_revision(self, ref_type, ref_name):
465 """
465 """
466 Returns revision number for the given reference.
466 Returns revision number for the given reference.
467 """
467 """
468 if ref_type == 'rev' and not ref_name.strip('0'):
468 if ref_type == 'rev' and not ref_name.strip('0'):
469 return self.EMPTY_CHANGESET
469 return self.EMPTY_CHANGESET
470 # lookup up the exact node id
470 # lookup up the exact node id
471 _revset_predicates = {
471 _revset_predicates = {
472 'branch': 'branch',
472 'branch': 'branch',
473 'book': 'bookmark',
473 'book': 'bookmark',
474 'tag': 'tag',
474 'tag': 'tag',
475 'rev': 'id',
475 'rev': 'id',
476 }
476 }
477 # avoid expensive branch(x) iteration over whole repo
477 # avoid expensive branch(x) iteration over whole repo
478 rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type]
478 rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type]
479 try:
479 try:
480 revs = self._repo.revs(rev_spec, ref_name, ref_name)
480 revs = self._repo.revs(rev_spec, ref_name, ref_name)
481 except LookupError:
481 except LookupError:
482 msg = "Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name)
482 msg = "Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name)
483 raise ChangesetDoesNotExistError(msg)
483 raise ChangesetDoesNotExistError(msg)
484 except mercurial.error.RepoLookupError:
484 except mercurial.error.RepoLookupError:
485 msg = "Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name)
485 msg = "Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name)
486 raise ChangesetDoesNotExistError(msg)
486 raise ChangesetDoesNotExistError(msg)
487 if revs:
487 if revs:
488 revision = revs.last()
488 revision = revs.last()
489 else:
489 else:
490 # TODO: just report 'not found'?
490 # TODO: just report 'not found'?
491 revision = ref_name
491 revision = ref_name
492
492
493 return self._get_revision(revision)
493 return self._get_revision(revision)
494
494
495 def _get_archives(self, archive_name='tip'):
495 def _get_archives(self, archive_name='tip'):
496 allowed = self.baseui.configlist(b"web", b"allow_archive",
496 allowed = self.baseui.configlist(b"web", b"allow_archive",
497 untrusted=True)
497 untrusted=True)
498 for name, ext in [(b'zip', '.zip'), (b'gz', '.tar.gz'), (b'bz2', '.tar.bz2')]:
498 for name, ext in [(b'zip', '.zip'), (b'gz', '.tar.gz'), (b'bz2', '.tar.bz2')]:
499 if name in allowed or self._repo.ui.configbool(b"web",
499 if name in allowed or self._repo.ui.configbool(b"web",
500 b"allow" + name,
500 b"allow" + name,
501 untrusted=True):
501 untrusted=True):
502 yield {"type": safe_str(name), "extension": ext, "node": archive_name}
502 yield {"type": safe_str(name), "extension": ext, "node": archive_name}
503
503
504 def _get_url(self, url):
504 def _get_url(self, url):
505 """
505 """
506 Returns normalized url. If schema is not given, fall back to
506 Returns normalized url. If schema is not given, fall back to
507 filesystem (``file:///``) schema.
507 filesystem (``file:///``) schema.
508 """
508 """
509 if url != 'default' and '://' not in url:
509 if url != 'default' and '://' not in url:
510 url = "file:" + urllib.request.pathname2url(url)
510 url = "file:" + urllib.request.pathname2url(url)
511 return url
511 return url
512
512
513 def get_changeset(self, revision=None):
513 def get_changeset(self, revision=None):
514 """
514 """
515 Returns ``MercurialChangeset`` object representing repository's
515 Returns ``MercurialChangeset`` object representing repository's
516 changeset at the given ``revision``.
516 changeset at the given ``revision``.
517 """
517 """
518 return changeset.MercurialChangeset(repository=self, revision=self._get_revision(revision))
518 return changeset.MercurialChangeset(repository=self, revision=self._get_revision(revision))
519
519
520 def get_changesets(self, start=None, end=None, start_date=None,
520 def get_changesets(self, start=None, end=None, start_date=None,
521 end_date=None, branch_name=None, reverse=False, max_revisions=None):
521 end_date=None, branch_name=None, reverse=False, max_revisions=None):
522 """
522 """
523 Returns iterator of ``MercurialChangeset`` objects from start to end
523 Returns iterator of ``MercurialChangeset`` objects from start to end
524 (both are inclusive)
524 (both are inclusive)
525
525
526 :param start: None, str, int or mercurial lookup format
526 :param start: None, str, int or mercurial lookup format
527 :param end: None, str, int or mercurial lookup format
527 :param end: None, str, int or mercurial lookup format
528 :param start_date:
528 :param start_date:
529 :param end_date:
529 :param end_date:
530 :param branch_name:
530 :param branch_name:
531 :param reversed: return changesets in reversed order
531 :param reverse: return changesets in reversed order
532 """
532 """
533 start_raw_id = self._get_revision(start)
533 start_raw_id = self._get_revision(start)
534 start_pos = None if start is None else self.revisions.index(start_raw_id)
534 start_pos = None if start is None else self.revisions.index(start_raw_id)
535 end_raw_id = self._get_revision(end)
535 end_raw_id = self._get_revision(end)
536 end_pos = None if end is None else self.revisions.index(end_raw_id)
536 end_pos = None if end is None else self.revisions.index(end_raw_id)
537
537
538 if start_pos is not None and end_pos is not None and start_pos > end_pos:
538 if start_pos is not None and end_pos is not None and start_pos > end_pos:
539 raise RepositoryError("Start revision '%s' cannot be "
539 raise RepositoryError("Start revision '%s' cannot be "
540 "after end revision '%s'" % (start, end))
540 "after end revision '%s'" % (start, end))
541
541
542 if branch_name and branch_name not in self.allbranches:
542 if branch_name and branch_name not in self.allbranches:
543 msg = "Branch %r not found in %s" % (branch_name, self.name)
543 msg = "Branch %r not found in %s" % (branch_name, self.name)
544 raise BranchDoesNotExistError(msg)
544 raise BranchDoesNotExistError(msg)
545 if end_pos is not None:
545 if end_pos is not None:
546 end_pos += 1
546 end_pos += 1
547 # filter branches
547 # filter branches
548 filter_ = []
548 filter_ = []
549 if branch_name:
549 if branch_name:
550 filter_.append(b'branch("%s")' % safe_bytes(branch_name))
550 filter_.append(b'branch("%s")' % safe_bytes(branch_name))
551 if start_date:
551 if start_date:
552 filter_.append(b'date(">%s")' % safe_bytes(str(start_date)))
552 filter_.append(b'date(">%s")' % safe_bytes(str(start_date)))
553 if end_date:
553 if end_date:
554 filter_.append(b'date("<%s")' % safe_bytes(str(end_date)))
554 filter_.append(b'date("<%s")' % safe_bytes(str(end_date)))
555 if filter_ or max_revisions:
555 if filter_ or max_revisions:
556 if filter_:
556 if filter_:
557 revspec = b' and '.join(filter_)
557 revspec = b' and '.join(filter_)
558 else:
558 else:
559 revspec = b'all()'
559 revspec = b'all()'
560 if max_revisions:
560 if max_revisions:
561 revspec = b'limit(%s, %d)' % (revspec, max_revisions)
561 revspec = b'limit(%s, %d)' % (revspec, max_revisions)
562 revisions = mercurial.scmutil.revrange(self._repo, [revspec])
562 revisions = mercurial.scmutil.revrange(self._repo, [revspec])
563 else:
563 else:
564 revisions = self.revisions
564 revisions = self.revisions
565
565
566 # this is very much a hack to turn this into a list; a better solution
566 # this is very much a hack to turn this into a list; a better solution
567 # would be to get rid of this function entirely and use revsets
567 # would be to get rid of this function entirely and use revsets
568 revs = list(revisions)[start_pos:end_pos]
568 revs = list(revisions)[start_pos:end_pos]
569 if reverse:
569 if reverse:
570 revs.reverse()
570 revs.reverse()
571
571
572 return CollectionGenerator(self, revs)
572 return CollectionGenerator(self, revs)
573
573
574 def get_diff_changesets(self, org_rev, other_repo, other_rev):
574 def get_diff_changesets(self, org_rev, other_repo, other_rev):
575 """
575 """
576 Returns lists of changesets that can be merged from this repo @org_rev
576 Returns lists of changesets that can be merged from this repo @org_rev
577 to other_repo @other_rev
577 to other_repo @other_rev
578 ... and the other way
578 ... and the other way
579 ... and the ancestors that would be used for merge
579 ... and the ancestors that would be used for merge
580
580
581 :param org_rev: the revision we want our compare to be made
581 :param org_rev: the revision we want our compare to be made
582 :param other_repo: repo object, most likely the fork of org_repo. It has
582 :param other_repo: repo object, most likely the fork of org_repo. It has
583 all changesets that we need to obtain
583 all changesets that we need to obtain
584 :param other_rev: revision we want out compare to be made on other_repo
584 :param other_rev: revision we want out compare to be made on other_repo
585 """
585 """
586 ancestors = None
586 ancestors = None
587 if org_rev == other_rev:
587 if org_rev == other_rev:
588 org_changesets = []
588 org_changesets = []
589 other_changesets = []
589 other_changesets = []
590
590
591 else:
591 else:
592 # case two independent repos
592 # case two independent repos
593 if self != other_repo:
593 if self != other_repo:
594 hgrepo = mercurial.unionrepo.makeunionrepository(other_repo.baseui,
594 hgrepo = mercurial.unionrepo.makeunionrepository(other_repo.baseui,
595 safe_bytes(other_repo.path),
595 safe_bytes(other_repo.path),
596 safe_bytes(self.path))
596 safe_bytes(self.path))
597 # all ancestors of other_rev will be in other_repo and
597 # all ancestors of other_rev will be in other_repo and
598 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
598 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
599
599
600 # no remote compare do it on the same repository
600 # no remote compare do it on the same repository
601 else:
601 else:
602 hgrepo = other_repo._repo
602 hgrepo = other_repo._repo
603
603
604 ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
604 ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
605 hgrepo.revs(b"id(%s) & ::id(%s)", ascii_bytes(other_rev), ascii_bytes(org_rev))]
605 hgrepo.revs(b"id(%s) & ::id(%s)", ascii_bytes(other_rev), ascii_bytes(org_rev))]
606 if ancestors:
606 if ancestors:
607 log.debug("shortcut found: %s is already an ancestor of %s", other_rev, org_rev)
607 log.debug("shortcut found: %s is already an ancestor of %s", other_rev, org_rev)
608 else:
608 else:
609 log.debug("no shortcut found: %s is not an ancestor of %s", other_rev, org_rev)
609 log.debug("no shortcut found: %s is not an ancestor of %s", other_rev, org_rev)
610 ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
610 ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
611 hgrepo.revs(b"heads(::id(%s) & ::id(%s))", ascii_bytes(org_rev), ascii_bytes(other_rev))] # FIXME: expensive!
611 hgrepo.revs(b"heads(::id(%s) & ::id(%s))", ascii_bytes(org_rev), ascii_bytes(other_rev))] # FIXME: expensive!
612
612
613 other_changesets = [
613 other_changesets = [
614 other_repo.get_changeset(rev)
614 other_repo.get_changeset(rev)
615 for rev in hgrepo.revs(
615 for rev in hgrepo.revs(
616 b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
616 b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
617 ascii_bytes(other_rev), ascii_bytes(org_rev), ascii_bytes(org_rev))
617 ascii_bytes(other_rev), ascii_bytes(org_rev), ascii_bytes(org_rev))
618 ]
618 ]
619 org_changesets = [
619 org_changesets = [
620 self.get_changeset(ascii_str(hgrepo[rev].hex()))
620 self.get_changeset(ascii_str(hgrepo[rev].hex()))
621 for rev in hgrepo.revs(
621 for rev in hgrepo.revs(
622 b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
622 b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
623 ascii_bytes(org_rev), ascii_bytes(other_rev), ascii_bytes(other_rev))
623 ascii_bytes(org_rev), ascii_bytes(other_rev), ascii_bytes(other_rev))
624 ]
624 ]
625
625
626 return other_changesets, org_changesets, ancestors
626 return other_changesets, org_changesets, ancestors
627
627
628 def pull(self, url):
628 def pull(self, url):
629 """
629 """
630 Tries to pull changes from external location.
630 Tries to pull changes from external location.
631 """
631 """
632 other = mercurial.hg.peer(self._repo, {}, safe_bytes(self._get_url(url)))
632 other = mercurial.hg.peer(self._repo, {}, safe_bytes(self._get_url(url)))
633 try:
633 try:
634 mercurial.exchange.pull(self._repo, other, heads=None, force=None)
634 mercurial.exchange.pull(self._repo, other, heads=None, force=None)
635 except mercurial.error.Abort as err:
635 except mercurial.error.Abort as err:
636 # Propagate error but with vcs's type
636 # Propagate error but with vcs's type
637 raise RepositoryError(str(err))
637 raise RepositoryError(str(err))
638
638
639 @LazyProperty
639 @LazyProperty
640 def workdir(self):
640 def workdir(self):
641 """
641 """
642 Returns ``Workdir`` instance for this repository.
642 Returns ``Workdir`` instance for this repository.
643 """
643 """
644 return workdir.MercurialWorkdir(self)
644 return workdir.MercurialWorkdir(self)
645
645
646 def get_config_value(self, section, name=None, config_file=None):
646 def get_config_value(self, section, name=None, config_file=None):
647 """
647 """
648 Returns configuration value for a given [``section``] and ``name``.
648 Returns configuration value for a given [``section``] and ``name``.
649
649
650 :param section: Section we want to retrieve value from
650 :param section: Section we want to retrieve value from
651 :param name: Name of configuration we want to retrieve
651 :param name: Name of configuration we want to retrieve
652 :param config_file: A path to file which should be used to retrieve
652 :param config_file: A path to file which should be used to retrieve
653 configuration from (might also be a list of file paths)
653 configuration from (might also be a list of file paths)
654 """
654 """
655 if config_file is None:
655 if config_file is None:
656 config_file = []
656 config_file = []
657 elif isinstance(config_file, str):
657 elif isinstance(config_file, str):
658 config_file = [config_file]
658 config_file = [config_file]
659
659
660 config = self._repo.ui
660 config = self._repo.ui
661 if config_file:
661 if config_file:
662 config = mercurial.ui.ui()
662 config = mercurial.ui.ui()
663 for path in config_file:
663 for path in config_file:
664 config.readconfig(safe_bytes(path))
664 config.readconfig(safe_bytes(path))
665 value = config.config(safe_bytes(section), safe_bytes(name))
665 value = config.config(safe_bytes(section), safe_bytes(name))
666 return value if value is None else safe_str(value)
666 return value if value is None else safe_str(value)
667
667
668 def get_user_name(self, config_file=None):
668 def get_user_name(self, config_file=None):
669 """
669 """
670 Returns user's name from global configuration file.
670 Returns user's name from global configuration file.
671
671
672 :param config_file: A path to file which should be used to retrieve
672 :param config_file: A path to file which should be used to retrieve
673 configuration from (might also be a list of file paths)
673 configuration from (might also be a list of file paths)
674 """
674 """
675 username = self.get_config_value('ui', 'username', config_file=config_file)
675 username = self.get_config_value('ui', 'username', config_file=config_file)
676 if username:
676 if username:
677 return author_name(username)
677 return author_name(username)
678 return None
678 return None
679
679
680 def get_user_email(self, config_file=None):
680 def get_user_email(self, config_file=None):
681 """
681 """
682 Returns user's email from global configuration file.
682 Returns user's email from global configuration file.
683
683
684 :param config_file: A path to file which should be used to retrieve
684 :param config_file: A path to file which should be used to retrieve
685 configuration from (might also be a list of file paths)
685 configuration from (might also be a list of file paths)
686 """
686 """
687 username = self.get_config_value('ui', 'username', config_file=config_file)
687 username = self.get_config_value('ui', 'username', config_file=config_file)
688 if username:
688 if username:
689 return author_email(username)
689 return author_email(username)
690 return None
690 return None
General Comments 0
You need to be logged in to leave comments. Login now