##// END OF EJS Templates
vcs: use proper attributes/inits on base and child classes.
marcink -
r2617:61a47f51 default
parent child Browse files
Show More
@@ -1,1606 +1,1620 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Base module for all VCS systems
22 Base module for all VCS systems
23 """
23 """
24
24
25 import collections
25 import collections
26 import datetime
26 import datetime
27 import itertools
27 import itertools
28 import logging
28 import logging
29 import os
29 import os
30 import time
30 import time
31 import warnings
31 import warnings
32
32
33 from zope.cachedescriptors.property import Lazy as LazyProperty
33 from zope.cachedescriptors.property import Lazy as LazyProperty
34
34
35 from rhodecode.lib.utils2 import safe_str, safe_unicode
35 from rhodecode.lib.utils2 import safe_str, safe_unicode
36 from rhodecode.lib.vcs import connection
36 from rhodecode.lib.vcs import connection
37 from rhodecode.lib.vcs.utils import author_name, author_email
37 from rhodecode.lib.vcs.utils import author_name, author_email
38 from rhodecode.lib.vcs.conf import settings
38 from rhodecode.lib.vcs.conf import settings
39 from rhodecode.lib.vcs.exceptions import (
39 from rhodecode.lib.vcs.exceptions import (
40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
44 RepositoryError)
44 RepositoryError)
45
45
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 FILEMODE_DEFAULT = 0100644
50 FILEMODE_DEFAULT = 0100644
51 FILEMODE_EXECUTABLE = 0100755
51 FILEMODE_EXECUTABLE = 0100755
52
52
53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
54 MergeResponse = collections.namedtuple(
54 MergeResponse = collections.namedtuple(
55 'MergeResponse',
55 'MergeResponse',
56 ('possible', 'executed', 'merge_ref', 'failure_reason'))
56 ('possible', 'executed', 'merge_ref', 'failure_reason'))
57
57
58
58
59 class MergeFailureReason(object):
59 class MergeFailureReason(object):
60 """
60 """
61 Enumeration with all the reasons why the server side merge could fail.
61 Enumeration with all the reasons why the server side merge could fail.
62
62
63 DO NOT change the number of the reasons, as they may be stored in the
63 DO NOT change the number of the reasons, as they may be stored in the
64 database.
64 database.
65
65
66 Changing the name of a reason is acceptable and encouraged to deprecate old
66 Changing the name of a reason is acceptable and encouraged to deprecate old
67 reasons.
67 reasons.
68 """
68 """
69
69
70 # Everything went well.
70 # Everything went well.
71 NONE = 0
71 NONE = 0
72
72
73 # An unexpected exception was raised. Check the logs for more details.
73 # An unexpected exception was raised. Check the logs for more details.
74 UNKNOWN = 1
74 UNKNOWN = 1
75
75
76 # The merge was not successful, there are conflicts.
76 # The merge was not successful, there are conflicts.
77 MERGE_FAILED = 2
77 MERGE_FAILED = 2
78
78
79 # The merge succeeded but we could not push it to the target repository.
79 # The merge succeeded but we could not push it to the target repository.
80 PUSH_FAILED = 3
80 PUSH_FAILED = 3
81
81
82 # The specified target is not a head in the target repository.
82 # The specified target is not a head in the target repository.
83 TARGET_IS_NOT_HEAD = 4
83 TARGET_IS_NOT_HEAD = 4
84
84
85 # The source repository contains more branches than the target. Pushing
85 # The source repository contains more branches than the target. Pushing
86 # the merge will create additional branches in the target.
86 # the merge will create additional branches in the target.
87 HG_SOURCE_HAS_MORE_BRANCHES = 5
87 HG_SOURCE_HAS_MORE_BRANCHES = 5
88
88
89 # The target reference has multiple heads. That does not allow to correctly
89 # The target reference has multiple heads. That does not allow to correctly
90 # identify the target location. This could only happen for mercurial
90 # identify the target location. This could only happen for mercurial
91 # branches.
91 # branches.
92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
93
93
94 # The target repository is locked
94 # The target repository is locked
95 TARGET_IS_LOCKED = 7
95 TARGET_IS_LOCKED = 7
96
96
97 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
97 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
98 # A involved commit could not be found.
98 # A involved commit could not be found.
99 _DEPRECATED_MISSING_COMMIT = 8
99 _DEPRECATED_MISSING_COMMIT = 8
100
100
101 # The target repo reference is missing.
101 # The target repo reference is missing.
102 MISSING_TARGET_REF = 9
102 MISSING_TARGET_REF = 9
103
103
104 # The source repo reference is missing.
104 # The source repo reference is missing.
105 MISSING_SOURCE_REF = 10
105 MISSING_SOURCE_REF = 10
106
106
107 # The merge was not successful, there are conflicts related to sub
107 # The merge was not successful, there are conflicts related to sub
108 # repositories.
108 # repositories.
109 SUBREPO_MERGE_FAILED = 11
109 SUBREPO_MERGE_FAILED = 11
110
110
111
111
112 class UpdateFailureReason(object):
112 class UpdateFailureReason(object):
113 """
113 """
114 Enumeration with all the reasons why the pull request update could fail.
114 Enumeration with all the reasons why the pull request update could fail.
115
115
116 DO NOT change the number of the reasons, as they may be stored in the
116 DO NOT change the number of the reasons, as they may be stored in the
117 database.
117 database.
118
118
119 Changing the name of a reason is acceptable and encouraged to deprecate old
119 Changing the name of a reason is acceptable and encouraged to deprecate old
120 reasons.
120 reasons.
121 """
121 """
122
122
123 # Everything went well.
123 # Everything went well.
124 NONE = 0
124 NONE = 0
125
125
126 # An unexpected exception was raised. Check the logs for more details.
126 # An unexpected exception was raised. Check the logs for more details.
127 UNKNOWN = 1
127 UNKNOWN = 1
128
128
129 # The pull request is up to date.
129 # The pull request is up to date.
130 NO_CHANGE = 2
130 NO_CHANGE = 2
131
131
132 # The pull request has a reference type that is not supported for update.
132 # The pull request has a reference type that is not supported for update.
133 WRONG_REF_TYPE = 3
133 WRONG_REF_TYPE = 3
134
134
135 # Update failed because the target reference is missing.
135 # Update failed because the target reference is missing.
136 MISSING_TARGET_REF = 4
136 MISSING_TARGET_REF = 4
137
137
138 # Update failed because the source reference is missing.
138 # Update failed because the source reference is missing.
139 MISSING_SOURCE_REF = 5
139 MISSING_SOURCE_REF = 5
140
140
141
141
142 class BaseRepository(object):
142 class BaseRepository(object):
143 """
143 """
144 Base Repository for final backends
144 Base Repository for final backends
145
145
146 .. attribute:: DEFAULT_BRANCH_NAME
146 .. attribute:: DEFAULT_BRANCH_NAME
147
147
148 name of default branch (i.e. "trunk" for svn, "master" for git etc.
148 name of default branch (i.e. "trunk" for svn, "master" for git etc.
149
149
150 .. attribute:: commit_ids
150 .. attribute:: commit_ids
151
151
152 list of all available commit ids, in ascending order
152 list of all available commit ids, in ascending order
153
153
154 .. attribute:: path
154 .. attribute:: path
155
155
156 absolute path to the repository
156 absolute path to the repository
157
157
158 .. attribute:: bookmarks
158 .. attribute:: bookmarks
159
159
160 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
160 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
161 there are no bookmarks or the backend implementation does not support
161 there are no bookmarks or the backend implementation does not support
162 bookmarks.
162 bookmarks.
163
163
164 .. attribute:: tags
164 .. attribute:: tags
165
165
166 Mapping from name to :term:`Commit ID` of the tag.
166 Mapping from name to :term:`Commit ID` of the tag.
167
167
168 """
168 """
169
169
170 DEFAULT_BRANCH_NAME = None
170 DEFAULT_BRANCH_NAME = None
171 DEFAULT_CONTACT = u"Unknown"
171 DEFAULT_CONTACT = u"Unknown"
172 DEFAULT_DESCRIPTION = u"unknown"
172 DEFAULT_DESCRIPTION = u"unknown"
173 EMPTY_COMMIT_ID = '0' * 40
173 EMPTY_COMMIT_ID = '0' * 40
174
174
175 path = None
175 path = None
176
176
177 def __init__(self, repo_path, config=None, create=False, **kwargs):
177 def __init__(self, repo_path, config=None, create=False, **kwargs):
178 """
178 """
179 Initializes repository. Raises RepositoryError if repository could
179 Initializes repository. Raises RepositoryError if repository could
180 not be find at the given ``repo_path`` or directory at ``repo_path``
180 not be find at the given ``repo_path`` or directory at ``repo_path``
181 exists and ``create`` is set to True.
181 exists and ``create`` is set to True.
182
182
183 :param repo_path: local path of the repository
183 :param repo_path: local path of the repository
184 :param config: repository configuration
184 :param config: repository configuration
185 :param create=False: if set to True, would try to create repository.
185 :param create=False: if set to True, would try to create repository.
186 :param src_url=None: if set, should be proper url from which repository
186 :param src_url=None: if set, should be proper url from which repository
187 would be cloned; requires ``create`` parameter to be set to True -
187 would be cloned; requires ``create`` parameter to be set to True -
188 raises RepositoryError if src_url is set and create evaluates to
188 raises RepositoryError if src_url is set and create evaluates to
189 False
189 False
190 """
190 """
191 raise NotImplementedError
191 raise NotImplementedError
192
192
193 def __repr__(self):
193 def __repr__(self):
194 return '<%s at %s>' % (self.__class__.__name__, self.path)
194 return '<%s at %s>' % (self.__class__.__name__, self.path)
195
195
196 def __len__(self):
196 def __len__(self):
197 return self.count()
197 return self.count()
198
198
199 def __eq__(self, other):
199 def __eq__(self, other):
200 same_instance = isinstance(other, self.__class__)
200 same_instance = isinstance(other, self.__class__)
201 return same_instance and other.path == self.path
201 return same_instance and other.path == self.path
202
202
203 def __ne__(self, other):
203 def __ne__(self, other):
204 return not self.__eq__(other)
204 return not self.__eq__(other)
205
205
206 @classmethod
206 @classmethod
207 def get_default_config(cls, default=None):
207 def get_default_config(cls, default=None):
208 config = Config()
208 config = Config()
209 if default and isinstance(default, list):
209 if default and isinstance(default, list):
210 for section, key, val in default:
210 for section, key, val in default:
211 config.set(section, key, val)
211 config.set(section, key, val)
212 return config
212 return config
213
213
214 @LazyProperty
214 @LazyProperty
215 def EMPTY_COMMIT(self):
215 def EMPTY_COMMIT(self):
216 return EmptyCommit(self.EMPTY_COMMIT_ID)
216 return EmptyCommit(self.EMPTY_COMMIT_ID)
217
217
218 @LazyProperty
218 @LazyProperty
219 def alias(self):
219 def alias(self):
220 for k, v in settings.BACKENDS.items():
220 for k, v in settings.BACKENDS.items():
221 if v.split('.')[-1] == str(self.__class__.__name__):
221 if v.split('.')[-1] == str(self.__class__.__name__):
222 return k
222 return k
223
223
224 @LazyProperty
224 @LazyProperty
225 def name(self):
225 def name(self):
226 return safe_unicode(os.path.basename(self.path))
226 return safe_unicode(os.path.basename(self.path))
227
227
228 @LazyProperty
228 @LazyProperty
229 def description(self):
229 def description(self):
230 raise NotImplementedError
230 raise NotImplementedError
231
231
232 def refs(self):
232 def refs(self):
233 """
233 """
234 returns a `dict` with branches, bookmarks, tags, and closed_branches
234 returns a `dict` with branches, bookmarks, tags, and closed_branches
235 for this repository
235 for this repository
236 """
236 """
237 return dict(
237 return dict(
238 branches=self.branches,
238 branches=self.branches,
239 branches_closed=self.branches_closed,
239 branches_closed=self.branches_closed,
240 tags=self.tags,
240 tags=self.tags,
241 bookmarks=self.bookmarks
241 bookmarks=self.bookmarks
242 )
242 )
243
243
244 @LazyProperty
244 @LazyProperty
245 def branches(self):
245 def branches(self):
246 """
246 """
247 A `dict` which maps branch names to commit ids.
247 A `dict` which maps branch names to commit ids.
248 """
248 """
249 raise NotImplementedError
249 raise NotImplementedError
250
250
251 @LazyProperty
251 @LazyProperty
252 def branches_closed(self):
253 """
254 A `dict` which maps tags names to commit ids.
255 """
256 raise NotImplementedError
257
258 @LazyProperty
259 def bookmarks(self):
260 """
261 A `dict` which maps tags names to commit ids.
262 """
263 raise NotImplementedError
264
265 @LazyProperty
252 def tags(self):
266 def tags(self):
253 """
267 """
254 A `dict` which maps tags names to commit ids.
268 A `dict` which maps tags names to commit ids.
255 """
269 """
256 raise NotImplementedError
270 raise NotImplementedError
257
271
258 @LazyProperty
272 @LazyProperty
259 def size(self):
273 def size(self):
260 """
274 """
261 Returns combined size in bytes for all repository files
275 Returns combined size in bytes for all repository files
262 """
276 """
263 tip = self.get_commit()
277 tip = self.get_commit()
264 return tip.size
278 return tip.size
265
279
266 def size_at_commit(self, commit_id):
280 def size_at_commit(self, commit_id):
267 commit = self.get_commit(commit_id)
281 commit = self.get_commit(commit_id)
268 return commit.size
282 return commit.size
269
283
270 def is_empty(self):
284 def is_empty(self):
271 return not bool(self.commit_ids)
285 return not bool(self.commit_ids)
272
286
273 @staticmethod
287 @staticmethod
274 def check_url(url, config):
288 def check_url(url, config):
275 """
289 """
276 Function will check given url and try to verify if it's a valid
290 Function will check given url and try to verify if it's a valid
277 link.
291 link.
278 """
292 """
279 raise NotImplementedError
293 raise NotImplementedError
280
294
281 @staticmethod
295 @staticmethod
282 def is_valid_repository(path):
296 def is_valid_repository(path):
283 """
297 """
284 Check if given `path` contains a valid repository of this backend
298 Check if given `path` contains a valid repository of this backend
285 """
299 """
286 raise NotImplementedError
300 raise NotImplementedError
287
301
288 # ==========================================================================
302 # ==========================================================================
289 # COMMITS
303 # COMMITS
290 # ==========================================================================
304 # ==========================================================================
291
305
292 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
306 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
293 """
307 """
294 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
308 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
295 are both None, most recent commit is returned.
309 are both None, most recent commit is returned.
296
310
297 :param pre_load: Optional. List of commit attributes to load.
311 :param pre_load: Optional. List of commit attributes to load.
298
312
299 :raises ``EmptyRepositoryError``: if there are no commits
313 :raises ``EmptyRepositoryError``: if there are no commits
300 """
314 """
301 raise NotImplementedError
315 raise NotImplementedError
302
316
303 def __iter__(self):
317 def __iter__(self):
304 for commit_id in self.commit_ids:
318 for commit_id in self.commit_ids:
305 yield self.get_commit(commit_id=commit_id)
319 yield self.get_commit(commit_id=commit_id)
306
320
307 def get_commits(
321 def get_commits(
308 self, start_id=None, end_id=None, start_date=None, end_date=None,
322 self, start_id=None, end_id=None, start_date=None, end_date=None,
309 branch_name=None, show_hidden=False, pre_load=None):
323 branch_name=None, show_hidden=False, pre_load=None):
310 """
324 """
311 Returns iterator of `BaseCommit` objects from start to end
325 Returns iterator of `BaseCommit` objects from start to end
312 not inclusive. This should behave just like a list, ie. end is not
326 not inclusive. This should behave just like a list, ie. end is not
313 inclusive.
327 inclusive.
314
328
315 :param start_id: None or str, must be a valid commit id
329 :param start_id: None or str, must be a valid commit id
316 :param end_id: None or str, must be a valid commit id
330 :param end_id: None or str, must be a valid commit id
317 :param start_date:
331 :param start_date:
318 :param end_date:
332 :param end_date:
319 :param branch_name:
333 :param branch_name:
320 :param show_hidden:
334 :param show_hidden:
321 :param pre_load:
335 :param pre_load:
322 """
336 """
323 raise NotImplementedError
337 raise NotImplementedError
324
338
325 def __getitem__(self, key):
339 def __getitem__(self, key):
326 """
340 """
327 Allows index based access to the commit objects of this repository.
341 Allows index based access to the commit objects of this repository.
328 """
342 """
329 pre_load = ["author", "branch", "date", "message", "parents"]
343 pre_load = ["author", "branch", "date", "message", "parents"]
330 if isinstance(key, slice):
344 if isinstance(key, slice):
331 return self._get_range(key, pre_load)
345 return self._get_range(key, pre_load)
332 return self.get_commit(commit_idx=key, pre_load=pre_load)
346 return self.get_commit(commit_idx=key, pre_load=pre_load)
333
347
334 def _get_range(self, slice_obj, pre_load):
348 def _get_range(self, slice_obj, pre_load):
335 for commit_id in self.commit_ids.__getitem__(slice_obj):
349 for commit_id in self.commit_ids.__getitem__(slice_obj):
336 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
350 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
337
351
338 def count(self):
352 def count(self):
339 return len(self.commit_ids)
353 return len(self.commit_ids)
340
354
341 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
355 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
342 """
356 """
343 Creates and returns a tag for the given ``commit_id``.
357 Creates and returns a tag for the given ``commit_id``.
344
358
345 :param name: name for new tag
359 :param name: name for new tag
346 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
360 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
347 :param commit_id: commit id for which new tag would be created
361 :param commit_id: commit id for which new tag would be created
348 :param message: message of the tag's commit
362 :param message: message of the tag's commit
349 :param date: date of tag's commit
363 :param date: date of tag's commit
350
364
351 :raises TagAlreadyExistError: if tag with same name already exists
365 :raises TagAlreadyExistError: if tag with same name already exists
352 """
366 """
353 raise NotImplementedError
367 raise NotImplementedError
354
368
355 def remove_tag(self, name, user, message=None, date=None):
369 def remove_tag(self, name, user, message=None, date=None):
356 """
370 """
357 Removes tag with the given ``name``.
371 Removes tag with the given ``name``.
358
372
359 :param name: name of the tag to be removed
373 :param name: name of the tag to be removed
360 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
374 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
361 :param message: message of the tag's removal commit
375 :param message: message of the tag's removal commit
362 :param date: date of tag's removal commit
376 :param date: date of tag's removal commit
363
377
364 :raises TagDoesNotExistError: if tag with given name does not exists
378 :raises TagDoesNotExistError: if tag with given name does not exists
365 """
379 """
366 raise NotImplementedError
380 raise NotImplementedError
367
381
368 def get_diff(
382 def get_diff(
369 self, commit1, commit2, path=None, ignore_whitespace=False,
383 self, commit1, commit2, path=None, ignore_whitespace=False,
370 context=3, path1=None):
384 context=3, path1=None):
371 """
385 """
372 Returns (git like) *diff*, as plain text. Shows changes introduced by
386 Returns (git like) *diff*, as plain text. Shows changes introduced by
373 `commit2` since `commit1`.
387 `commit2` since `commit1`.
374
388
375 :param commit1: Entry point from which diff is shown. Can be
389 :param commit1: Entry point from which diff is shown. Can be
376 ``self.EMPTY_COMMIT`` - in this case, patch showing all
390 ``self.EMPTY_COMMIT`` - in this case, patch showing all
377 the changes since empty state of the repository until `commit2`
391 the changes since empty state of the repository until `commit2`
378 :param commit2: Until which commit changes should be shown.
392 :param commit2: Until which commit changes should be shown.
379 :param path: Can be set to a path of a file to create a diff of that
393 :param path: Can be set to a path of a file to create a diff of that
380 file. If `path1` is also set, this value is only associated to
394 file. If `path1` is also set, this value is only associated to
381 `commit2`.
395 `commit2`.
382 :param ignore_whitespace: If set to ``True``, would not show whitespace
396 :param ignore_whitespace: If set to ``True``, would not show whitespace
383 changes. Defaults to ``False``.
397 changes. Defaults to ``False``.
384 :param context: How many lines before/after changed lines should be
398 :param context: How many lines before/after changed lines should be
385 shown. Defaults to ``3``.
399 shown. Defaults to ``3``.
386 :param path1: Can be set to a path to associate with `commit1`. This
400 :param path1: Can be set to a path to associate with `commit1`. This
387 parameter works only for backends which support diff generation for
401 parameter works only for backends which support diff generation for
388 different paths. Other backends will raise a `ValueError` if `path1`
402 different paths. Other backends will raise a `ValueError` if `path1`
389 is set and has a different value than `path`.
403 is set and has a different value than `path`.
390 :param file_path: filter this diff by given path pattern
404 :param file_path: filter this diff by given path pattern
391 """
405 """
392 raise NotImplementedError
406 raise NotImplementedError
393
407
394 def strip(self, commit_id, branch=None):
408 def strip(self, commit_id, branch=None):
395 """
409 """
396 Strip given commit_id from the repository
410 Strip given commit_id from the repository
397 """
411 """
398 raise NotImplementedError
412 raise NotImplementedError
399
413
400 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
414 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
401 """
415 """
402 Return a latest common ancestor commit if one exists for this repo
416 Return a latest common ancestor commit if one exists for this repo
403 `commit_id1` vs `commit_id2` from `repo2`.
417 `commit_id1` vs `commit_id2` from `repo2`.
404
418
405 :param commit_id1: Commit it from this repository to use as a
419 :param commit_id1: Commit it from this repository to use as a
406 target for the comparison.
420 target for the comparison.
407 :param commit_id2: Source commit id to use for comparison.
421 :param commit_id2: Source commit id to use for comparison.
408 :param repo2: Source repository to use for comparison.
422 :param repo2: Source repository to use for comparison.
409 """
423 """
410 raise NotImplementedError
424 raise NotImplementedError
411
425
412 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
426 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
413 """
427 """
414 Compare this repository's revision `commit_id1` with `commit_id2`.
428 Compare this repository's revision `commit_id1` with `commit_id2`.
415
429
416 Returns a tuple(commits, ancestor) that would be merged from
430 Returns a tuple(commits, ancestor) that would be merged from
417 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
431 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
418 will be returned as ancestor.
432 will be returned as ancestor.
419
433
420 :param commit_id1: Commit it from this repository to use as a
434 :param commit_id1: Commit it from this repository to use as a
421 target for the comparison.
435 target for the comparison.
422 :param commit_id2: Source commit id to use for comparison.
436 :param commit_id2: Source commit id to use for comparison.
423 :param repo2: Source repository to use for comparison.
437 :param repo2: Source repository to use for comparison.
424 :param merge: If set to ``True`` will do a merge compare which also
438 :param merge: If set to ``True`` will do a merge compare which also
425 returns the common ancestor.
439 returns the common ancestor.
426 :param pre_load: Optional. List of commit attributes to load.
440 :param pre_load: Optional. List of commit attributes to load.
427 """
441 """
428 raise NotImplementedError
442 raise NotImplementedError
429
443
430 def merge(self, target_ref, source_repo, source_ref, workspace_id,
444 def merge(self, target_ref, source_repo, source_ref, workspace_id,
431 user_name='', user_email='', message='', dry_run=False,
445 user_name='', user_email='', message='', dry_run=False,
432 use_rebase=False, close_branch=False):
446 use_rebase=False, close_branch=False):
433 """
447 """
434 Merge the revisions specified in `source_ref` from `source_repo`
448 Merge the revisions specified in `source_ref` from `source_repo`
435 onto the `target_ref` of this repository.
449 onto the `target_ref` of this repository.
436
450
437 `source_ref` and `target_ref` are named tupls with the following
451 `source_ref` and `target_ref` are named tupls with the following
438 fields `type`, `name` and `commit_id`.
452 fields `type`, `name` and `commit_id`.
439
453
440 Returns a MergeResponse named tuple with the following fields
454 Returns a MergeResponse named tuple with the following fields
441 'possible', 'executed', 'source_commit', 'target_commit',
455 'possible', 'executed', 'source_commit', 'target_commit',
442 'merge_commit'.
456 'merge_commit'.
443
457
444 :param target_ref: `target_ref` points to the commit on top of which
458 :param target_ref: `target_ref` points to the commit on top of which
445 the `source_ref` should be merged.
459 the `source_ref` should be merged.
446 :param source_repo: The repository that contains the commits to be
460 :param source_repo: The repository that contains the commits to be
447 merged.
461 merged.
448 :param source_ref: `source_ref` points to the topmost commit from
462 :param source_ref: `source_ref` points to the topmost commit from
449 the `source_repo` which should be merged.
463 the `source_repo` which should be merged.
450 :param workspace_id: `workspace_id` unique identifier.
464 :param workspace_id: `workspace_id` unique identifier.
451 :param user_name: Merge commit `user_name`.
465 :param user_name: Merge commit `user_name`.
452 :param user_email: Merge commit `user_email`.
466 :param user_email: Merge commit `user_email`.
453 :param message: Merge commit `message`.
467 :param message: Merge commit `message`.
454 :param dry_run: If `True` the merge will not take place.
468 :param dry_run: If `True` the merge will not take place.
455 :param use_rebase: If `True` commits from the source will be rebased
469 :param use_rebase: If `True` commits from the source will be rebased
456 on top of the target instead of being merged.
470 on top of the target instead of being merged.
457 :param close_branch: If `True` branch will be close before merging it
471 :param close_branch: If `True` branch will be close before merging it
458 """
472 """
459 if dry_run:
473 if dry_run:
460 message = message or 'dry_run_merge_message'
474 message = message or 'dry_run_merge_message'
461 user_email = user_email or 'dry-run-merge@rhodecode.com'
475 user_email = user_email or 'dry-run-merge@rhodecode.com'
462 user_name = user_name or 'Dry-Run User'
476 user_name = user_name or 'Dry-Run User'
463 else:
477 else:
464 if not user_name:
478 if not user_name:
465 raise ValueError('user_name cannot be empty')
479 raise ValueError('user_name cannot be empty')
466 if not user_email:
480 if not user_email:
467 raise ValueError('user_email cannot be empty')
481 raise ValueError('user_email cannot be empty')
468 if not message:
482 if not message:
469 raise ValueError('message cannot be empty')
483 raise ValueError('message cannot be empty')
470
484
471 shadow_repository_path = self._maybe_prepare_merge_workspace(
485 shadow_repository_path = self._maybe_prepare_merge_workspace(
472 workspace_id, target_ref, source_ref)
486 workspace_id, target_ref, source_ref)
473
487
474 try:
488 try:
475 return self._merge_repo(
489 return self._merge_repo(
476 shadow_repository_path, target_ref, source_repo,
490 shadow_repository_path, target_ref, source_repo,
477 source_ref, message, user_name, user_email, dry_run=dry_run,
491 source_ref, message, user_name, user_email, dry_run=dry_run,
478 use_rebase=use_rebase, close_branch=close_branch)
492 use_rebase=use_rebase, close_branch=close_branch)
479 except RepositoryError:
493 except RepositoryError:
480 log.exception(
494 log.exception(
481 'Unexpected failure when running merge, dry-run=%s',
495 'Unexpected failure when running merge, dry-run=%s',
482 dry_run)
496 dry_run)
483 return MergeResponse(
497 return MergeResponse(
484 False, False, None, MergeFailureReason.UNKNOWN)
498 False, False, None, MergeFailureReason.UNKNOWN)
485
499
486 def _merge_repo(self, shadow_repository_path, target_ref,
500 def _merge_repo(self, shadow_repository_path, target_ref,
487 source_repo, source_ref, merge_message,
501 source_repo, source_ref, merge_message,
488 merger_name, merger_email, dry_run=False,
502 merger_name, merger_email, dry_run=False,
489 use_rebase=False, close_branch=False):
503 use_rebase=False, close_branch=False):
490 """Internal implementation of merge."""
504 """Internal implementation of merge."""
491 raise NotImplementedError
505 raise NotImplementedError
492
506
493 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref, source_ref):
507 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref, source_ref):
494 """
508 """
495 Create the merge workspace.
509 Create the merge workspace.
496
510
497 :param workspace_id: `workspace_id` unique identifier.
511 :param workspace_id: `workspace_id` unique identifier.
498 """
512 """
499 raise NotImplementedError
513 raise NotImplementedError
500
514
501 def cleanup_merge_workspace(self, workspace_id):
515 def cleanup_merge_workspace(self, workspace_id):
502 """
516 """
503 Remove merge workspace.
517 Remove merge workspace.
504
518
505 This function MUST not fail in case there is no workspace associated to
519 This function MUST not fail in case there is no workspace associated to
506 the given `workspace_id`.
520 the given `workspace_id`.
507
521
508 :param workspace_id: `workspace_id` unique identifier.
522 :param workspace_id: `workspace_id` unique identifier.
509 """
523 """
510 raise NotImplementedError
524 raise NotImplementedError
511
525
512 # ========== #
526 # ========== #
513 # COMMIT API #
527 # COMMIT API #
514 # ========== #
528 # ========== #
515
529
516 @LazyProperty
530 @LazyProperty
517 def in_memory_commit(self):
531 def in_memory_commit(self):
518 """
532 """
519 Returns :class:`InMemoryCommit` object for this repository.
533 Returns :class:`InMemoryCommit` object for this repository.
520 """
534 """
521 raise NotImplementedError
535 raise NotImplementedError
522
536
523 # ======================== #
537 # ======================== #
524 # UTILITIES FOR SUBCLASSES #
538 # UTILITIES FOR SUBCLASSES #
525 # ======================== #
539 # ======================== #
526
540
527 def _validate_diff_commits(self, commit1, commit2):
541 def _validate_diff_commits(self, commit1, commit2):
528 """
542 """
529 Validates that the given commits are related to this repository.
543 Validates that the given commits are related to this repository.
530
544
531 Intended as a utility for sub classes to have a consistent validation
545 Intended as a utility for sub classes to have a consistent validation
532 of input parameters in methods like :meth:`get_diff`.
546 of input parameters in methods like :meth:`get_diff`.
533 """
547 """
534 self._validate_commit(commit1)
548 self._validate_commit(commit1)
535 self._validate_commit(commit2)
549 self._validate_commit(commit2)
536 if (isinstance(commit1, EmptyCommit) and
550 if (isinstance(commit1, EmptyCommit) and
537 isinstance(commit2, EmptyCommit)):
551 isinstance(commit2, EmptyCommit)):
538 raise ValueError("Cannot compare two empty commits")
552 raise ValueError("Cannot compare two empty commits")
539
553
540 def _validate_commit(self, commit):
554 def _validate_commit(self, commit):
541 if not isinstance(commit, BaseCommit):
555 if not isinstance(commit, BaseCommit):
542 raise TypeError(
556 raise TypeError(
543 "%s is not of type BaseCommit" % repr(commit))
557 "%s is not of type BaseCommit" % repr(commit))
544 if commit.repository != self and not isinstance(commit, EmptyCommit):
558 if commit.repository != self and not isinstance(commit, EmptyCommit):
545 raise ValueError(
559 raise ValueError(
546 "Commit %s must be a valid commit from this repository %s, "
560 "Commit %s must be a valid commit from this repository %s, "
547 "related to this repository instead %s." %
561 "related to this repository instead %s." %
548 (commit, self, commit.repository))
562 (commit, self, commit.repository))
549
563
550 def _validate_commit_id(self, commit_id):
564 def _validate_commit_id(self, commit_id):
551 if not isinstance(commit_id, basestring):
565 if not isinstance(commit_id, basestring):
552 raise TypeError("commit_id must be a string value")
566 raise TypeError("commit_id must be a string value")
553
567
554 def _validate_commit_idx(self, commit_idx):
568 def _validate_commit_idx(self, commit_idx):
555 if not isinstance(commit_idx, (int, long)):
569 if not isinstance(commit_idx, (int, long)):
556 raise TypeError("commit_idx must be a numeric value")
570 raise TypeError("commit_idx must be a numeric value")
557
571
558 def _validate_branch_name(self, branch_name):
572 def _validate_branch_name(self, branch_name):
559 if branch_name and branch_name not in self.branches_all:
573 if branch_name and branch_name not in self.branches_all:
560 msg = ("Branch %s not found in %s" % (branch_name, self))
574 msg = ("Branch %s not found in %s" % (branch_name, self))
561 raise BranchDoesNotExistError(msg)
575 raise BranchDoesNotExistError(msg)
562
576
563 #
577 #
564 # Supporting deprecated API parts
578 # Supporting deprecated API parts
565 # TODO: johbo: consider to move this into a mixin
579 # TODO: johbo: consider to move this into a mixin
566 #
580 #
567
581
568 @property
582 @property
569 def EMPTY_CHANGESET(self):
583 def EMPTY_CHANGESET(self):
570 warnings.warn(
584 warnings.warn(
571 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
585 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
572 return self.EMPTY_COMMIT_ID
586 return self.EMPTY_COMMIT_ID
573
587
574 @property
588 @property
575 def revisions(self):
589 def revisions(self):
576 warnings.warn("Use commits attribute instead", DeprecationWarning)
590 warnings.warn("Use commits attribute instead", DeprecationWarning)
577 return self.commit_ids
591 return self.commit_ids
578
592
579 @revisions.setter
593 @revisions.setter
580 def revisions(self, value):
594 def revisions(self, value):
581 warnings.warn("Use commits attribute instead", DeprecationWarning)
595 warnings.warn("Use commits attribute instead", DeprecationWarning)
582 self.commit_ids = value
596 self.commit_ids = value
583
597
584 def get_changeset(self, revision=None, pre_load=None):
598 def get_changeset(self, revision=None, pre_load=None):
585 warnings.warn("Use get_commit instead", DeprecationWarning)
599 warnings.warn("Use get_commit instead", DeprecationWarning)
586 commit_id = None
600 commit_id = None
587 commit_idx = None
601 commit_idx = None
588 if isinstance(revision, basestring):
602 if isinstance(revision, basestring):
589 commit_id = revision
603 commit_id = revision
590 else:
604 else:
591 commit_idx = revision
605 commit_idx = revision
592 return self.get_commit(
606 return self.get_commit(
593 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
607 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
594
608
595 def get_changesets(
609 def get_changesets(
596 self, start=None, end=None, start_date=None, end_date=None,
610 self, start=None, end=None, start_date=None, end_date=None,
597 branch_name=None, pre_load=None):
611 branch_name=None, pre_load=None):
598 warnings.warn("Use get_commits instead", DeprecationWarning)
612 warnings.warn("Use get_commits instead", DeprecationWarning)
599 start_id = self._revision_to_commit(start)
613 start_id = self._revision_to_commit(start)
600 end_id = self._revision_to_commit(end)
614 end_id = self._revision_to_commit(end)
601 return self.get_commits(
615 return self.get_commits(
602 start_id=start_id, end_id=end_id, start_date=start_date,
616 start_id=start_id, end_id=end_id, start_date=start_date,
603 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
617 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
604
618
605 def _revision_to_commit(self, revision):
619 def _revision_to_commit(self, revision):
606 """
620 """
607 Translates a revision to a commit_id
621 Translates a revision to a commit_id
608
622
609 Helps to support the old changeset based API which allows to use
623 Helps to support the old changeset based API which allows to use
610 commit ids and commit indices interchangeable.
624 commit ids and commit indices interchangeable.
611 """
625 """
612 if revision is None:
626 if revision is None:
613 return revision
627 return revision
614
628
615 if isinstance(revision, basestring):
629 if isinstance(revision, basestring):
616 commit_id = revision
630 commit_id = revision
617 else:
631 else:
618 commit_id = self.commit_ids[revision]
632 commit_id = self.commit_ids[revision]
619 return commit_id
633 return commit_id
620
634
621 @property
635 @property
622 def in_memory_changeset(self):
636 def in_memory_changeset(self):
623 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
637 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
624 return self.in_memory_commit
638 return self.in_memory_commit
625
639
626
640
627 class BaseCommit(object):
641 class BaseCommit(object):
628 """
642 """
629 Each backend should implement it's commit representation.
643 Each backend should implement it's commit representation.
630
644
631 **Attributes**
645 **Attributes**
632
646
633 ``repository``
647 ``repository``
634 repository object within which commit exists
648 repository object within which commit exists
635
649
636 ``id``
650 ``id``
637 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
651 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
638 just ``tip``.
652 just ``tip``.
639
653
640 ``raw_id``
654 ``raw_id``
641 raw commit representation (i.e. full 40 length sha for git
655 raw commit representation (i.e. full 40 length sha for git
642 backend)
656 backend)
643
657
644 ``short_id``
658 ``short_id``
645 shortened (if apply) version of ``raw_id``; it would be simple
659 shortened (if apply) version of ``raw_id``; it would be simple
646 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
660 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
647 as ``raw_id`` for subversion
661 as ``raw_id`` for subversion
648
662
649 ``idx``
663 ``idx``
650 commit index
664 commit index
651
665
652 ``files``
666 ``files``
653 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
667 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
654
668
655 ``dirs``
669 ``dirs``
656 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
670 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
657
671
658 ``nodes``
672 ``nodes``
659 combined list of ``Node`` objects
673 combined list of ``Node`` objects
660
674
661 ``author``
675 ``author``
662 author of the commit, as unicode
676 author of the commit, as unicode
663
677
664 ``message``
678 ``message``
665 message of the commit, as unicode
679 message of the commit, as unicode
666
680
667 ``parents``
681 ``parents``
668 list of parent commits
682 list of parent commits
669
683
670 """
684 """
671
685
672 branch = None
686 branch = None
673 """
687 """
674 Depending on the backend this should be set to the branch name of the
688 Depending on the backend this should be set to the branch name of the
675 commit. Backends not supporting branches on commits should leave this
689 commit. Backends not supporting branches on commits should leave this
676 value as ``None``.
690 value as ``None``.
677 """
691 """
678
692
679 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
693 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
680 """
694 """
681 This template is used to generate a default prefix for repository archives
695 This template is used to generate a default prefix for repository archives
682 if no prefix has been specified.
696 if no prefix has been specified.
683 """
697 """
684
698
685 def __str__(self):
699 def __str__(self):
686 return '<%s at %s:%s>' % (
700 return '<%s at %s:%s>' % (
687 self.__class__.__name__, self.idx, self.short_id)
701 self.__class__.__name__, self.idx, self.short_id)
688
702
689 def __repr__(self):
703 def __repr__(self):
690 return self.__str__()
704 return self.__str__()
691
705
692 def __unicode__(self):
706 def __unicode__(self):
693 return u'%s:%s' % (self.idx, self.short_id)
707 return u'%s:%s' % (self.idx, self.short_id)
694
708
695 def __eq__(self, other):
709 def __eq__(self, other):
696 same_instance = isinstance(other, self.__class__)
710 same_instance = isinstance(other, self.__class__)
697 return same_instance and self.raw_id == other.raw_id
711 return same_instance and self.raw_id == other.raw_id
698
712
699 def __json__(self):
713 def __json__(self):
700 parents = []
714 parents = []
701 try:
715 try:
702 for parent in self.parents:
716 for parent in self.parents:
703 parents.append({'raw_id': parent.raw_id})
717 parents.append({'raw_id': parent.raw_id})
704 except NotImplementedError:
718 except NotImplementedError:
705 # empty commit doesn't have parents implemented
719 # empty commit doesn't have parents implemented
706 pass
720 pass
707
721
708 return {
722 return {
709 'short_id': self.short_id,
723 'short_id': self.short_id,
710 'raw_id': self.raw_id,
724 'raw_id': self.raw_id,
711 'revision': self.idx,
725 'revision': self.idx,
712 'message': self.message,
726 'message': self.message,
713 'date': self.date,
727 'date': self.date,
714 'author': self.author,
728 'author': self.author,
715 'parents': parents,
729 'parents': parents,
716 'branch': self.branch
730 'branch': self.branch
717 }
731 }
718
732
719 def _get_refs(self):
733 def _get_refs(self):
720 return {
734 return {
721 'branches': [self.branch],
735 'branches': [self.branch],
722 'bookmarks': getattr(self, 'bookmarks', []),
736 'bookmarks': getattr(self, 'bookmarks', []),
723 'tags': self.tags
737 'tags': self.tags
724 }
738 }
725
739
726 @LazyProperty
740 @LazyProperty
727 def last(self):
741 def last(self):
728 """
742 """
729 ``True`` if this is last commit in repository, ``False``
743 ``True`` if this is last commit in repository, ``False``
730 otherwise; trying to access this attribute while there is no
744 otherwise; trying to access this attribute while there is no
731 commits would raise `EmptyRepositoryError`
745 commits would raise `EmptyRepositoryError`
732 """
746 """
733 if self.repository is None:
747 if self.repository is None:
734 raise CommitError("Cannot check if it's most recent commit")
748 raise CommitError("Cannot check if it's most recent commit")
735 return self.raw_id == self.repository.commit_ids[-1]
749 return self.raw_id == self.repository.commit_ids[-1]
736
750
737 @LazyProperty
751 @LazyProperty
738 def parents(self):
752 def parents(self):
739 """
753 """
740 Returns list of parent commits.
754 Returns list of parent commits.
741 """
755 """
742 raise NotImplementedError
756 raise NotImplementedError
743
757
744 @property
758 @property
745 def merge(self):
759 def merge(self):
746 """
760 """
747 Returns boolean if commit is a merge.
761 Returns boolean if commit is a merge.
748 """
762 """
749 return len(self.parents) > 1
763 return len(self.parents) > 1
750
764
751 @LazyProperty
765 @LazyProperty
752 def children(self):
766 def children(self):
753 """
767 """
754 Returns list of child commits.
768 Returns list of child commits.
755 """
769 """
756 raise NotImplementedError
770 raise NotImplementedError
757
771
758 @LazyProperty
772 @LazyProperty
759 def id(self):
773 def id(self):
760 """
774 """
761 Returns string identifying this commit.
775 Returns string identifying this commit.
762 """
776 """
763 raise NotImplementedError
777 raise NotImplementedError
764
778
765 @LazyProperty
779 @LazyProperty
766 def raw_id(self):
780 def raw_id(self):
767 """
781 """
768 Returns raw string identifying this commit.
782 Returns raw string identifying this commit.
769 """
783 """
770 raise NotImplementedError
784 raise NotImplementedError
771
785
772 @LazyProperty
786 @LazyProperty
773 def short_id(self):
787 def short_id(self):
774 """
788 """
775 Returns shortened version of ``raw_id`` attribute, as string,
789 Returns shortened version of ``raw_id`` attribute, as string,
776 identifying this commit, useful for presentation to users.
790 identifying this commit, useful for presentation to users.
777 """
791 """
778 raise NotImplementedError
792 raise NotImplementedError
779
793
780 @LazyProperty
794 @LazyProperty
781 def idx(self):
795 def idx(self):
782 """
796 """
783 Returns integer identifying this commit.
797 Returns integer identifying this commit.
784 """
798 """
785 raise NotImplementedError
799 raise NotImplementedError
786
800
787 @LazyProperty
801 @LazyProperty
788 def committer(self):
802 def committer(self):
789 """
803 """
790 Returns committer for this commit
804 Returns committer for this commit
791 """
805 """
792 raise NotImplementedError
806 raise NotImplementedError
793
807
794 @LazyProperty
808 @LazyProperty
795 def committer_name(self):
809 def committer_name(self):
796 """
810 """
797 Returns committer name for this commit
811 Returns committer name for this commit
798 """
812 """
799
813
800 return author_name(self.committer)
814 return author_name(self.committer)
801
815
802 @LazyProperty
816 @LazyProperty
803 def committer_email(self):
817 def committer_email(self):
804 """
818 """
805 Returns committer email address for this commit
819 Returns committer email address for this commit
806 """
820 """
807
821
808 return author_email(self.committer)
822 return author_email(self.committer)
809
823
810 @LazyProperty
824 @LazyProperty
811 def author(self):
825 def author(self):
812 """
826 """
813 Returns author for this commit
827 Returns author for this commit
814 """
828 """
815
829
816 raise NotImplementedError
830 raise NotImplementedError
817
831
818 @LazyProperty
832 @LazyProperty
819 def author_name(self):
833 def author_name(self):
820 """
834 """
821 Returns author name for this commit
835 Returns author name for this commit
822 """
836 """
823
837
824 return author_name(self.author)
838 return author_name(self.author)
825
839
826 @LazyProperty
840 @LazyProperty
827 def author_email(self):
841 def author_email(self):
828 """
842 """
829 Returns author email address for this commit
843 Returns author email address for this commit
830 """
844 """
831
845
832 return author_email(self.author)
846 return author_email(self.author)
833
847
834 def get_file_mode(self, path):
848 def get_file_mode(self, path):
835 """
849 """
836 Returns stat mode of the file at `path`.
850 Returns stat mode of the file at `path`.
837 """
851 """
838 raise NotImplementedError
852 raise NotImplementedError
839
853
840 def is_link(self, path):
854 def is_link(self, path):
841 """
855 """
842 Returns ``True`` if given `path` is a symlink
856 Returns ``True`` if given `path` is a symlink
843 """
857 """
844 raise NotImplementedError
858 raise NotImplementedError
845
859
846 def get_file_content(self, path):
860 def get_file_content(self, path):
847 """
861 """
848 Returns content of the file at the given `path`.
862 Returns content of the file at the given `path`.
849 """
863 """
850 raise NotImplementedError
864 raise NotImplementedError
851
865
852 def get_file_size(self, path):
866 def get_file_size(self, path):
853 """
867 """
854 Returns size of the file at the given `path`.
868 Returns size of the file at the given `path`.
855 """
869 """
856 raise NotImplementedError
870 raise NotImplementedError
857
871
858 def get_file_commit(self, path, pre_load=None):
872 def get_file_commit(self, path, pre_load=None):
859 """
873 """
860 Returns last commit of the file at the given `path`.
874 Returns last commit of the file at the given `path`.
861
875
862 :param pre_load: Optional. List of commit attributes to load.
876 :param pre_load: Optional. List of commit attributes to load.
863 """
877 """
864 commits = self.get_file_history(path, limit=1, pre_load=pre_load)
878 commits = self.get_file_history(path, limit=1, pre_load=pre_load)
865 if not commits:
879 if not commits:
866 raise RepositoryError(
880 raise RepositoryError(
867 'Failed to fetch history for path {}. '
881 'Failed to fetch history for path {}. '
868 'Please check if such path exists in your repository'.format(
882 'Please check if such path exists in your repository'.format(
869 path))
883 path))
870 return commits[0]
884 return commits[0]
871
885
872 def get_file_history(self, path, limit=None, pre_load=None):
886 def get_file_history(self, path, limit=None, pre_load=None):
873 """
887 """
874 Returns history of file as reversed list of :class:`BaseCommit`
888 Returns history of file as reversed list of :class:`BaseCommit`
875 objects for which file at given `path` has been modified.
889 objects for which file at given `path` has been modified.
876
890
877 :param limit: Optional. Allows to limit the size of the returned
891 :param limit: Optional. Allows to limit the size of the returned
878 history. This is intended as a hint to the underlying backend, so
892 history. This is intended as a hint to the underlying backend, so
879 that it can apply optimizations depending on the limit.
893 that it can apply optimizations depending on the limit.
880 :param pre_load: Optional. List of commit attributes to load.
894 :param pre_load: Optional. List of commit attributes to load.
881 """
895 """
882 raise NotImplementedError
896 raise NotImplementedError
883
897
884 def get_file_annotate(self, path, pre_load=None):
898 def get_file_annotate(self, path, pre_load=None):
885 """
899 """
886 Returns a generator of four element tuples with
900 Returns a generator of four element tuples with
887 lineno, sha, commit lazy loader and line
901 lineno, sha, commit lazy loader and line
888
902
889 :param pre_load: Optional. List of commit attributes to load.
903 :param pre_load: Optional. List of commit attributes to load.
890 """
904 """
891 raise NotImplementedError
905 raise NotImplementedError
892
906
893 def get_nodes(self, path):
907 def get_nodes(self, path):
894 """
908 """
895 Returns combined ``DirNode`` and ``FileNode`` objects list representing
909 Returns combined ``DirNode`` and ``FileNode`` objects list representing
896 state of commit at the given ``path``.
910 state of commit at the given ``path``.
897
911
898 :raises ``CommitError``: if node at the given ``path`` is not
912 :raises ``CommitError``: if node at the given ``path`` is not
899 instance of ``DirNode``
913 instance of ``DirNode``
900 """
914 """
901 raise NotImplementedError
915 raise NotImplementedError
902
916
903 def get_node(self, path):
917 def get_node(self, path):
904 """
918 """
905 Returns ``Node`` object from the given ``path``.
919 Returns ``Node`` object from the given ``path``.
906
920
907 :raises ``NodeDoesNotExistError``: if there is no node at the given
921 :raises ``NodeDoesNotExistError``: if there is no node at the given
908 ``path``
922 ``path``
909 """
923 """
910 raise NotImplementedError
924 raise NotImplementedError
911
925
912 def get_largefile_node(self, path):
926 def get_largefile_node(self, path):
913 """
927 """
914 Returns the path to largefile from Mercurial/Git-lfs storage.
928 Returns the path to largefile from Mercurial/Git-lfs storage.
915 or None if it's not a largefile node
929 or None if it's not a largefile node
916 """
930 """
917 return None
931 return None
918
932
919 def archive_repo(self, file_path, kind='tgz', subrepos=None,
933 def archive_repo(self, file_path, kind='tgz', subrepos=None,
920 prefix=None, write_metadata=False, mtime=None):
934 prefix=None, write_metadata=False, mtime=None):
921 """
935 """
922 Creates an archive containing the contents of the repository.
936 Creates an archive containing the contents of the repository.
923
937
924 :param file_path: path to the file which to create the archive.
938 :param file_path: path to the file which to create the archive.
925 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
939 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
926 :param prefix: name of root directory in archive.
940 :param prefix: name of root directory in archive.
927 Default is repository name and commit's short_id joined with dash:
941 Default is repository name and commit's short_id joined with dash:
928 ``"{repo_name}-{short_id}"``.
942 ``"{repo_name}-{short_id}"``.
929 :param write_metadata: write a metadata file into archive.
943 :param write_metadata: write a metadata file into archive.
930 :param mtime: custom modification time for archive creation, defaults
944 :param mtime: custom modification time for archive creation, defaults
931 to time.time() if not given.
945 to time.time() if not given.
932
946
933 :raise VCSError: If prefix has a problem.
947 :raise VCSError: If prefix has a problem.
934 """
948 """
935 allowed_kinds = settings.ARCHIVE_SPECS.keys()
949 allowed_kinds = settings.ARCHIVE_SPECS.keys()
936 if kind not in allowed_kinds:
950 if kind not in allowed_kinds:
937 raise ImproperArchiveTypeError(
951 raise ImproperArchiveTypeError(
938 'Archive kind (%s) not supported use one of %s' %
952 'Archive kind (%s) not supported use one of %s' %
939 (kind, allowed_kinds))
953 (kind, allowed_kinds))
940
954
941 prefix = self._validate_archive_prefix(prefix)
955 prefix = self._validate_archive_prefix(prefix)
942
956
943 mtime = mtime or time.mktime(self.date.timetuple())
957 mtime = mtime or time.mktime(self.date.timetuple())
944
958
945 file_info = []
959 file_info = []
946 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
960 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
947 for _r, _d, files in cur_rev.walk('/'):
961 for _r, _d, files in cur_rev.walk('/'):
948 for f in files:
962 for f in files:
949 f_path = os.path.join(prefix, f.path)
963 f_path = os.path.join(prefix, f.path)
950 file_info.append(
964 file_info.append(
951 (f_path, f.mode, f.is_link(), f.raw_bytes))
965 (f_path, f.mode, f.is_link(), f.raw_bytes))
952
966
953 if write_metadata:
967 if write_metadata:
954 metadata = [
968 metadata = [
955 ('repo_name', self.repository.name),
969 ('repo_name', self.repository.name),
956 ('rev', self.raw_id),
970 ('rev', self.raw_id),
957 ('create_time', mtime),
971 ('create_time', mtime),
958 ('branch', self.branch),
972 ('branch', self.branch),
959 ('tags', ','.join(self.tags)),
973 ('tags', ','.join(self.tags)),
960 ]
974 ]
961 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
975 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
962 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
976 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
963
977
964 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
978 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
965
979
966 def _validate_archive_prefix(self, prefix):
980 def _validate_archive_prefix(self, prefix):
967 if prefix is None:
981 if prefix is None:
968 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
982 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
969 repo_name=safe_str(self.repository.name),
983 repo_name=safe_str(self.repository.name),
970 short_id=self.short_id)
984 short_id=self.short_id)
971 elif not isinstance(prefix, str):
985 elif not isinstance(prefix, str):
972 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
986 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
973 elif prefix.startswith('/'):
987 elif prefix.startswith('/'):
974 raise VCSError("Prefix cannot start with leading slash")
988 raise VCSError("Prefix cannot start with leading slash")
975 elif prefix.strip() == '':
989 elif prefix.strip() == '':
976 raise VCSError("Prefix cannot be empty")
990 raise VCSError("Prefix cannot be empty")
977 return prefix
991 return prefix
978
992
979 @LazyProperty
993 @LazyProperty
980 def root(self):
994 def root(self):
981 """
995 """
982 Returns ``RootNode`` object for this commit.
996 Returns ``RootNode`` object for this commit.
983 """
997 """
984 return self.get_node('')
998 return self.get_node('')
985
999
986 def next(self, branch=None):
1000 def next(self, branch=None):
987 """
1001 """
988 Returns next commit from current, if branch is gives it will return
1002 Returns next commit from current, if branch is gives it will return
989 next commit belonging to this branch
1003 next commit belonging to this branch
990
1004
991 :param branch: show commits within the given named branch
1005 :param branch: show commits within the given named branch
992 """
1006 """
993 indexes = xrange(self.idx + 1, self.repository.count())
1007 indexes = xrange(self.idx + 1, self.repository.count())
994 return self._find_next(indexes, branch)
1008 return self._find_next(indexes, branch)
995
1009
996 def prev(self, branch=None):
1010 def prev(self, branch=None):
997 """
1011 """
998 Returns previous commit from current, if branch is gives it will
1012 Returns previous commit from current, if branch is gives it will
999 return previous commit belonging to this branch
1013 return previous commit belonging to this branch
1000
1014
1001 :param branch: show commit within the given named branch
1015 :param branch: show commit within the given named branch
1002 """
1016 """
1003 indexes = xrange(self.idx - 1, -1, -1)
1017 indexes = xrange(self.idx - 1, -1, -1)
1004 return self._find_next(indexes, branch)
1018 return self._find_next(indexes, branch)
1005
1019
1006 def _find_next(self, indexes, branch=None):
1020 def _find_next(self, indexes, branch=None):
1007 if branch and self.branch != branch:
1021 if branch and self.branch != branch:
1008 raise VCSError('Branch option used on commit not belonging '
1022 raise VCSError('Branch option used on commit not belonging '
1009 'to that branch')
1023 'to that branch')
1010
1024
1011 for next_idx in indexes:
1025 for next_idx in indexes:
1012 commit = self.repository.get_commit(commit_idx=next_idx)
1026 commit = self.repository.get_commit(commit_idx=next_idx)
1013 if branch and branch != commit.branch:
1027 if branch and branch != commit.branch:
1014 continue
1028 continue
1015 return commit
1029 return commit
1016 raise CommitDoesNotExistError
1030 raise CommitDoesNotExistError
1017
1031
1018 def diff(self, ignore_whitespace=True, context=3):
1032 def diff(self, ignore_whitespace=True, context=3):
1019 """
1033 """
1020 Returns a `Diff` object representing the change made by this commit.
1034 Returns a `Diff` object representing the change made by this commit.
1021 """
1035 """
1022 parent = (
1036 parent = (
1023 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
1037 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
1024 diff = self.repository.get_diff(
1038 diff = self.repository.get_diff(
1025 parent, self,
1039 parent, self,
1026 ignore_whitespace=ignore_whitespace,
1040 ignore_whitespace=ignore_whitespace,
1027 context=context)
1041 context=context)
1028 return diff
1042 return diff
1029
1043
1030 @LazyProperty
1044 @LazyProperty
1031 def added(self):
1045 def added(self):
1032 """
1046 """
1033 Returns list of added ``FileNode`` objects.
1047 Returns list of added ``FileNode`` objects.
1034 """
1048 """
1035 raise NotImplementedError
1049 raise NotImplementedError
1036
1050
1037 @LazyProperty
1051 @LazyProperty
1038 def changed(self):
1052 def changed(self):
1039 """
1053 """
1040 Returns list of modified ``FileNode`` objects.
1054 Returns list of modified ``FileNode`` objects.
1041 """
1055 """
1042 raise NotImplementedError
1056 raise NotImplementedError
1043
1057
1044 @LazyProperty
1058 @LazyProperty
1045 def removed(self):
1059 def removed(self):
1046 """
1060 """
1047 Returns list of removed ``FileNode`` objects.
1061 Returns list of removed ``FileNode`` objects.
1048 """
1062 """
1049 raise NotImplementedError
1063 raise NotImplementedError
1050
1064
1051 @LazyProperty
1065 @LazyProperty
1052 def size(self):
1066 def size(self):
1053 """
1067 """
1054 Returns total number of bytes from contents of all filenodes.
1068 Returns total number of bytes from contents of all filenodes.
1055 """
1069 """
1056 return sum((node.size for node in self.get_filenodes_generator()))
1070 return sum((node.size for node in self.get_filenodes_generator()))
1057
1071
1058 def walk(self, topurl=''):
1072 def walk(self, topurl=''):
1059 """
1073 """
1060 Similar to os.walk method. Insted of filesystem it walks through
1074 Similar to os.walk method. Insted of filesystem it walks through
1061 commit starting at given ``topurl``. Returns generator of tuples
1075 commit starting at given ``topurl``. Returns generator of tuples
1062 (topnode, dirnodes, filenodes).
1076 (topnode, dirnodes, filenodes).
1063 """
1077 """
1064 topnode = self.get_node(topurl)
1078 topnode = self.get_node(topurl)
1065 if not topnode.is_dir():
1079 if not topnode.is_dir():
1066 return
1080 return
1067 yield (topnode, topnode.dirs, topnode.files)
1081 yield (topnode, topnode.dirs, topnode.files)
1068 for dirnode in topnode.dirs:
1082 for dirnode in topnode.dirs:
1069 for tup in self.walk(dirnode.path):
1083 for tup in self.walk(dirnode.path):
1070 yield tup
1084 yield tup
1071
1085
1072 def get_filenodes_generator(self):
1086 def get_filenodes_generator(self):
1073 """
1087 """
1074 Returns generator that yields *all* file nodes.
1088 Returns generator that yields *all* file nodes.
1075 """
1089 """
1076 for topnode, dirs, files in self.walk():
1090 for topnode, dirs, files in self.walk():
1077 for node in files:
1091 for node in files:
1078 yield node
1092 yield node
1079
1093
1080 #
1094 #
1081 # Utilities for sub classes to support consistent behavior
1095 # Utilities for sub classes to support consistent behavior
1082 #
1096 #
1083
1097
1084 def no_node_at_path(self, path):
1098 def no_node_at_path(self, path):
1085 return NodeDoesNotExistError(
1099 return NodeDoesNotExistError(
1086 u"There is no file nor directory at the given path: "
1100 u"There is no file nor directory at the given path: "
1087 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1101 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1088
1102
1089 def _fix_path(self, path):
1103 def _fix_path(self, path):
1090 """
1104 """
1091 Paths are stored without trailing slash so we need to get rid off it if
1105 Paths are stored without trailing slash so we need to get rid off it if
1092 needed.
1106 needed.
1093 """
1107 """
1094 return path.rstrip('/')
1108 return path.rstrip('/')
1095
1109
1096 #
1110 #
1097 # Deprecated API based on changesets
1111 # Deprecated API based on changesets
1098 #
1112 #
1099
1113
1100 @property
1114 @property
1101 def revision(self):
1115 def revision(self):
1102 warnings.warn("Use idx instead", DeprecationWarning)
1116 warnings.warn("Use idx instead", DeprecationWarning)
1103 return self.idx
1117 return self.idx
1104
1118
1105 @revision.setter
1119 @revision.setter
1106 def revision(self, value):
1120 def revision(self, value):
1107 warnings.warn("Use idx instead", DeprecationWarning)
1121 warnings.warn("Use idx instead", DeprecationWarning)
1108 self.idx = value
1122 self.idx = value
1109
1123
1110 def get_file_changeset(self, path):
1124 def get_file_changeset(self, path):
1111 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1125 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1112 return self.get_file_commit(path)
1126 return self.get_file_commit(path)
1113
1127
1114
1128
1115 class BaseChangesetClass(type):
1129 class BaseChangesetClass(type):
1116
1130
1117 def __instancecheck__(self, instance):
1131 def __instancecheck__(self, instance):
1118 return isinstance(instance, BaseCommit)
1132 return isinstance(instance, BaseCommit)
1119
1133
1120
1134
1121 class BaseChangeset(BaseCommit):
1135 class BaseChangeset(BaseCommit):
1122
1136
1123 __metaclass__ = BaseChangesetClass
1137 __metaclass__ = BaseChangesetClass
1124
1138
1125 def __new__(cls, *args, **kwargs):
1139 def __new__(cls, *args, **kwargs):
1126 warnings.warn(
1140 warnings.warn(
1127 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1141 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1128 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1142 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1129
1143
1130
1144
1131 class BaseInMemoryCommit(object):
1145 class BaseInMemoryCommit(object):
1132 """
1146 """
1133 Represents differences between repository's state (most recent head) and
1147 Represents differences between repository's state (most recent head) and
1134 changes made *in place*.
1148 changes made *in place*.
1135
1149
1136 **Attributes**
1150 **Attributes**
1137
1151
1138 ``repository``
1152 ``repository``
1139 repository object for this in-memory-commit
1153 repository object for this in-memory-commit
1140
1154
1141 ``added``
1155 ``added``
1142 list of ``FileNode`` objects marked as *added*
1156 list of ``FileNode`` objects marked as *added*
1143
1157
1144 ``changed``
1158 ``changed``
1145 list of ``FileNode`` objects marked as *changed*
1159 list of ``FileNode`` objects marked as *changed*
1146
1160
1147 ``removed``
1161 ``removed``
1148 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1162 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1149 *removed*
1163 *removed*
1150
1164
1151 ``parents``
1165 ``parents``
1152 list of :class:`BaseCommit` instances representing parents of
1166 list of :class:`BaseCommit` instances representing parents of
1153 in-memory commit. Should always be 2-element sequence.
1167 in-memory commit. Should always be 2-element sequence.
1154
1168
1155 """
1169 """
1156
1170
1157 def __init__(self, repository):
1171 def __init__(self, repository):
1158 self.repository = repository
1172 self.repository = repository
1159 self.added = []
1173 self.added = []
1160 self.changed = []
1174 self.changed = []
1161 self.removed = []
1175 self.removed = []
1162 self.parents = []
1176 self.parents = []
1163
1177
1164 def add(self, *filenodes):
1178 def add(self, *filenodes):
1165 """
1179 """
1166 Marks given ``FileNode`` objects as *to be committed*.
1180 Marks given ``FileNode`` objects as *to be committed*.
1167
1181
1168 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1182 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1169 latest commit
1183 latest commit
1170 :raises ``NodeAlreadyAddedError``: if node with same path is already
1184 :raises ``NodeAlreadyAddedError``: if node with same path is already
1171 marked as *added*
1185 marked as *added*
1172 """
1186 """
1173 # Check if not already marked as *added* first
1187 # Check if not already marked as *added* first
1174 for node in filenodes:
1188 for node in filenodes:
1175 if node.path in (n.path for n in self.added):
1189 if node.path in (n.path for n in self.added):
1176 raise NodeAlreadyAddedError(
1190 raise NodeAlreadyAddedError(
1177 "Such FileNode %s is already marked for addition"
1191 "Such FileNode %s is already marked for addition"
1178 % node.path)
1192 % node.path)
1179 for node in filenodes:
1193 for node in filenodes:
1180 self.added.append(node)
1194 self.added.append(node)
1181
1195
1182 def change(self, *filenodes):
1196 def change(self, *filenodes):
1183 """
1197 """
1184 Marks given ``FileNode`` objects to be *changed* in next commit.
1198 Marks given ``FileNode`` objects to be *changed* in next commit.
1185
1199
1186 :raises ``EmptyRepositoryError``: if there are no commits yet
1200 :raises ``EmptyRepositoryError``: if there are no commits yet
1187 :raises ``NodeAlreadyExistsError``: if node with same path is already
1201 :raises ``NodeAlreadyExistsError``: if node with same path is already
1188 marked to be *changed*
1202 marked to be *changed*
1189 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1203 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1190 marked to be *removed*
1204 marked to be *removed*
1191 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1205 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1192 commit
1206 commit
1193 :raises ``NodeNotChangedError``: if node hasn't really be changed
1207 :raises ``NodeNotChangedError``: if node hasn't really be changed
1194 """
1208 """
1195 for node in filenodes:
1209 for node in filenodes:
1196 if node.path in (n.path for n in self.removed):
1210 if node.path in (n.path for n in self.removed):
1197 raise NodeAlreadyRemovedError(
1211 raise NodeAlreadyRemovedError(
1198 "Node at %s is already marked as removed" % node.path)
1212 "Node at %s is already marked as removed" % node.path)
1199 try:
1213 try:
1200 self.repository.get_commit()
1214 self.repository.get_commit()
1201 except EmptyRepositoryError:
1215 except EmptyRepositoryError:
1202 raise EmptyRepositoryError(
1216 raise EmptyRepositoryError(
1203 "Nothing to change - try to *add* new nodes rather than "
1217 "Nothing to change - try to *add* new nodes rather than "
1204 "changing them")
1218 "changing them")
1205 for node in filenodes:
1219 for node in filenodes:
1206 if node.path in (n.path for n in self.changed):
1220 if node.path in (n.path for n in self.changed):
1207 raise NodeAlreadyChangedError(
1221 raise NodeAlreadyChangedError(
1208 "Node at '%s' is already marked as changed" % node.path)
1222 "Node at '%s' is already marked as changed" % node.path)
1209 self.changed.append(node)
1223 self.changed.append(node)
1210
1224
1211 def remove(self, *filenodes):
1225 def remove(self, *filenodes):
1212 """
1226 """
1213 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1227 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1214 *removed* in next commit.
1228 *removed* in next commit.
1215
1229
1216 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1230 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1217 be *removed*
1231 be *removed*
1218 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1232 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1219 be *changed*
1233 be *changed*
1220 """
1234 """
1221 for node in filenodes:
1235 for node in filenodes:
1222 if node.path in (n.path for n in self.removed):
1236 if node.path in (n.path for n in self.removed):
1223 raise NodeAlreadyRemovedError(
1237 raise NodeAlreadyRemovedError(
1224 "Node is already marked to for removal at %s" % node.path)
1238 "Node is already marked to for removal at %s" % node.path)
1225 if node.path in (n.path for n in self.changed):
1239 if node.path in (n.path for n in self.changed):
1226 raise NodeAlreadyChangedError(
1240 raise NodeAlreadyChangedError(
1227 "Node is already marked to be changed at %s" % node.path)
1241 "Node is already marked to be changed at %s" % node.path)
1228 # We only mark node as *removed* - real removal is done by
1242 # We only mark node as *removed* - real removal is done by
1229 # commit method
1243 # commit method
1230 self.removed.append(node)
1244 self.removed.append(node)
1231
1245
1232 def reset(self):
1246 def reset(self):
1233 """
1247 """
1234 Resets this instance to initial state (cleans ``added``, ``changed``
1248 Resets this instance to initial state (cleans ``added``, ``changed``
1235 and ``removed`` lists).
1249 and ``removed`` lists).
1236 """
1250 """
1237 self.added = []
1251 self.added = []
1238 self.changed = []
1252 self.changed = []
1239 self.removed = []
1253 self.removed = []
1240 self.parents = []
1254 self.parents = []
1241
1255
1242 def get_ipaths(self):
1256 def get_ipaths(self):
1243 """
1257 """
1244 Returns generator of paths from nodes marked as added, changed or
1258 Returns generator of paths from nodes marked as added, changed or
1245 removed.
1259 removed.
1246 """
1260 """
1247 for node in itertools.chain(self.added, self.changed, self.removed):
1261 for node in itertools.chain(self.added, self.changed, self.removed):
1248 yield node.path
1262 yield node.path
1249
1263
1250 def get_paths(self):
1264 def get_paths(self):
1251 """
1265 """
1252 Returns list of paths from nodes marked as added, changed or removed.
1266 Returns list of paths from nodes marked as added, changed or removed.
1253 """
1267 """
1254 return list(self.get_ipaths())
1268 return list(self.get_ipaths())
1255
1269
1256 def check_integrity(self, parents=None):
1270 def check_integrity(self, parents=None):
1257 """
1271 """
1258 Checks in-memory commit's integrity. Also, sets parents if not
1272 Checks in-memory commit's integrity. Also, sets parents if not
1259 already set.
1273 already set.
1260
1274
1261 :raises CommitError: if any error occurs (i.e.
1275 :raises CommitError: if any error occurs (i.e.
1262 ``NodeDoesNotExistError``).
1276 ``NodeDoesNotExistError``).
1263 """
1277 """
1264 if not self.parents:
1278 if not self.parents:
1265 parents = parents or []
1279 parents = parents or []
1266 if len(parents) == 0:
1280 if len(parents) == 0:
1267 try:
1281 try:
1268 parents = [self.repository.get_commit(), None]
1282 parents = [self.repository.get_commit(), None]
1269 except EmptyRepositoryError:
1283 except EmptyRepositoryError:
1270 parents = [None, None]
1284 parents = [None, None]
1271 elif len(parents) == 1:
1285 elif len(parents) == 1:
1272 parents += [None]
1286 parents += [None]
1273 self.parents = parents
1287 self.parents = parents
1274
1288
1275 # Local parents, only if not None
1289 # Local parents, only if not None
1276 parents = [p for p in self.parents if p]
1290 parents = [p for p in self.parents if p]
1277
1291
1278 # Check nodes marked as added
1292 # Check nodes marked as added
1279 for p in parents:
1293 for p in parents:
1280 for node in self.added:
1294 for node in self.added:
1281 try:
1295 try:
1282 p.get_node(node.path)
1296 p.get_node(node.path)
1283 except NodeDoesNotExistError:
1297 except NodeDoesNotExistError:
1284 pass
1298 pass
1285 else:
1299 else:
1286 raise NodeAlreadyExistsError(
1300 raise NodeAlreadyExistsError(
1287 "Node `%s` already exists at %s" % (node.path, p))
1301 "Node `%s` already exists at %s" % (node.path, p))
1288
1302
1289 # Check nodes marked as changed
1303 # Check nodes marked as changed
1290 missing = set(self.changed)
1304 missing = set(self.changed)
1291 not_changed = set(self.changed)
1305 not_changed = set(self.changed)
1292 if self.changed and not parents:
1306 if self.changed and not parents:
1293 raise NodeDoesNotExistError(str(self.changed[0].path))
1307 raise NodeDoesNotExistError(str(self.changed[0].path))
1294 for p in parents:
1308 for p in parents:
1295 for node in self.changed:
1309 for node in self.changed:
1296 try:
1310 try:
1297 old = p.get_node(node.path)
1311 old = p.get_node(node.path)
1298 missing.remove(node)
1312 missing.remove(node)
1299 # if content actually changed, remove node from not_changed
1313 # if content actually changed, remove node from not_changed
1300 if old.content != node.content:
1314 if old.content != node.content:
1301 not_changed.remove(node)
1315 not_changed.remove(node)
1302 except NodeDoesNotExistError:
1316 except NodeDoesNotExistError:
1303 pass
1317 pass
1304 if self.changed and missing:
1318 if self.changed and missing:
1305 raise NodeDoesNotExistError(
1319 raise NodeDoesNotExistError(
1306 "Node `%s` marked as modified but missing in parents: %s"
1320 "Node `%s` marked as modified but missing in parents: %s"
1307 % (node.path, parents))
1321 % (node.path, parents))
1308
1322
1309 if self.changed and not_changed:
1323 if self.changed and not_changed:
1310 raise NodeNotChangedError(
1324 raise NodeNotChangedError(
1311 "Node `%s` wasn't actually changed (parents: %s)"
1325 "Node `%s` wasn't actually changed (parents: %s)"
1312 % (not_changed.pop().path, parents))
1326 % (not_changed.pop().path, parents))
1313
1327
1314 # Check nodes marked as removed
1328 # Check nodes marked as removed
1315 if self.removed and not parents:
1329 if self.removed and not parents:
1316 raise NodeDoesNotExistError(
1330 raise NodeDoesNotExistError(
1317 "Cannot remove node at %s as there "
1331 "Cannot remove node at %s as there "
1318 "were no parents specified" % self.removed[0].path)
1332 "were no parents specified" % self.removed[0].path)
1319 really_removed = set()
1333 really_removed = set()
1320 for p in parents:
1334 for p in parents:
1321 for node in self.removed:
1335 for node in self.removed:
1322 try:
1336 try:
1323 p.get_node(node.path)
1337 p.get_node(node.path)
1324 really_removed.add(node)
1338 really_removed.add(node)
1325 except CommitError:
1339 except CommitError:
1326 pass
1340 pass
1327 not_removed = set(self.removed) - really_removed
1341 not_removed = set(self.removed) - really_removed
1328 if not_removed:
1342 if not_removed:
1329 # TODO: johbo: This code branch does not seem to be covered
1343 # TODO: johbo: This code branch does not seem to be covered
1330 raise NodeDoesNotExistError(
1344 raise NodeDoesNotExistError(
1331 "Cannot remove node at %s from "
1345 "Cannot remove node at %s from "
1332 "following parents: %s" % (not_removed, parents))
1346 "following parents: %s" % (not_removed, parents))
1333
1347
1334 def commit(
1348 def commit(
1335 self, message, author, parents=None, branch=None, date=None,
1349 self, message, author, parents=None, branch=None, date=None,
1336 **kwargs):
1350 **kwargs):
1337 """
1351 """
1338 Performs in-memory commit (doesn't check workdir in any way) and
1352 Performs in-memory commit (doesn't check workdir in any way) and
1339 returns newly created :class:`BaseCommit`. Updates repository's
1353 returns newly created :class:`BaseCommit`. Updates repository's
1340 attribute `commits`.
1354 attribute `commits`.
1341
1355
1342 .. note::
1356 .. note::
1343
1357
1344 While overriding this method each backend's should call
1358 While overriding this method each backend's should call
1345 ``self.check_integrity(parents)`` in the first place.
1359 ``self.check_integrity(parents)`` in the first place.
1346
1360
1347 :param message: message of the commit
1361 :param message: message of the commit
1348 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1362 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1349 :param parents: single parent or sequence of parents from which commit
1363 :param parents: single parent or sequence of parents from which commit
1350 would be derived
1364 would be derived
1351 :param date: ``datetime.datetime`` instance. Defaults to
1365 :param date: ``datetime.datetime`` instance. Defaults to
1352 ``datetime.datetime.now()``.
1366 ``datetime.datetime.now()``.
1353 :param branch: branch name, as string. If none given, default backend's
1367 :param branch: branch name, as string. If none given, default backend's
1354 branch would be used.
1368 branch would be used.
1355
1369
1356 :raises ``CommitError``: if any error occurs while committing
1370 :raises ``CommitError``: if any error occurs while committing
1357 """
1371 """
1358 raise NotImplementedError
1372 raise NotImplementedError
1359
1373
1360
1374
1361 class BaseInMemoryChangesetClass(type):
1375 class BaseInMemoryChangesetClass(type):
1362
1376
1363 def __instancecheck__(self, instance):
1377 def __instancecheck__(self, instance):
1364 return isinstance(instance, BaseInMemoryCommit)
1378 return isinstance(instance, BaseInMemoryCommit)
1365
1379
1366
1380
1367 class BaseInMemoryChangeset(BaseInMemoryCommit):
1381 class BaseInMemoryChangeset(BaseInMemoryCommit):
1368
1382
1369 __metaclass__ = BaseInMemoryChangesetClass
1383 __metaclass__ = BaseInMemoryChangesetClass
1370
1384
1371 def __new__(cls, *args, **kwargs):
1385 def __new__(cls, *args, **kwargs):
1372 warnings.warn(
1386 warnings.warn(
1373 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1387 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1374 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1388 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1375
1389
1376
1390
1377 class EmptyCommit(BaseCommit):
1391 class EmptyCommit(BaseCommit):
1378 """
1392 """
1379 An dummy empty commit. It's possible to pass hash when creating
1393 An dummy empty commit. It's possible to pass hash when creating
1380 an EmptyCommit
1394 an EmptyCommit
1381 """
1395 """
1382
1396
1383 def __init__(
1397 def __init__(
1384 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1398 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1385 message='', author='', date=None):
1399 message='', author='', date=None):
1386 self._empty_commit_id = commit_id
1400 self._empty_commit_id = commit_id
1387 # TODO: johbo: Solve idx parameter, default value does not make
1401 # TODO: johbo: Solve idx parameter, default value does not make
1388 # too much sense
1402 # too much sense
1389 self.idx = idx
1403 self.idx = idx
1390 self.message = message
1404 self.message = message
1391 self.author = author
1405 self.author = author
1392 self.date = date or datetime.datetime.fromtimestamp(0)
1406 self.date = date or datetime.datetime.fromtimestamp(0)
1393 self.repository = repo
1407 self.repository = repo
1394 self.alias = alias
1408 self.alias = alias
1395
1409
1396 @LazyProperty
1410 @LazyProperty
1397 def raw_id(self):
1411 def raw_id(self):
1398 """
1412 """
1399 Returns raw string identifying this commit, useful for web
1413 Returns raw string identifying this commit, useful for web
1400 representation.
1414 representation.
1401 """
1415 """
1402
1416
1403 return self._empty_commit_id
1417 return self._empty_commit_id
1404
1418
1405 @LazyProperty
1419 @LazyProperty
1406 def branch(self):
1420 def branch(self):
1407 if self.alias:
1421 if self.alias:
1408 from rhodecode.lib.vcs.backends import get_backend
1422 from rhodecode.lib.vcs.backends import get_backend
1409 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1423 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1410
1424
1411 @LazyProperty
1425 @LazyProperty
1412 def short_id(self):
1426 def short_id(self):
1413 return self.raw_id[:12]
1427 return self.raw_id[:12]
1414
1428
1415 @LazyProperty
1429 @LazyProperty
1416 def id(self):
1430 def id(self):
1417 return self.raw_id
1431 return self.raw_id
1418
1432
1419 def get_file_commit(self, path):
1433 def get_file_commit(self, path):
1420 return self
1434 return self
1421
1435
1422 def get_file_content(self, path):
1436 def get_file_content(self, path):
1423 return u''
1437 return u''
1424
1438
1425 def get_file_size(self, path):
1439 def get_file_size(self, path):
1426 return 0
1440 return 0
1427
1441
1428
1442
1429 class EmptyChangesetClass(type):
1443 class EmptyChangesetClass(type):
1430
1444
1431 def __instancecheck__(self, instance):
1445 def __instancecheck__(self, instance):
1432 return isinstance(instance, EmptyCommit)
1446 return isinstance(instance, EmptyCommit)
1433
1447
1434
1448
1435 class EmptyChangeset(EmptyCommit):
1449 class EmptyChangeset(EmptyCommit):
1436
1450
1437 __metaclass__ = EmptyChangesetClass
1451 __metaclass__ = EmptyChangesetClass
1438
1452
1439 def __new__(cls, *args, **kwargs):
1453 def __new__(cls, *args, **kwargs):
1440 warnings.warn(
1454 warnings.warn(
1441 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1455 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1442 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1456 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1443
1457
1444 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1458 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1445 alias=None, revision=-1, message='', author='', date=None):
1459 alias=None, revision=-1, message='', author='', date=None):
1446 if requested_revision is not None:
1460 if requested_revision is not None:
1447 warnings.warn(
1461 warnings.warn(
1448 "Parameter requested_revision not supported anymore",
1462 "Parameter requested_revision not supported anymore",
1449 DeprecationWarning)
1463 DeprecationWarning)
1450 super(EmptyChangeset, self).__init__(
1464 super(EmptyChangeset, self).__init__(
1451 commit_id=cs, repo=repo, alias=alias, idx=revision,
1465 commit_id=cs, repo=repo, alias=alias, idx=revision,
1452 message=message, author=author, date=date)
1466 message=message, author=author, date=date)
1453
1467
1454 @property
1468 @property
1455 def revision(self):
1469 def revision(self):
1456 warnings.warn("Use idx instead", DeprecationWarning)
1470 warnings.warn("Use idx instead", DeprecationWarning)
1457 return self.idx
1471 return self.idx
1458
1472
1459 @revision.setter
1473 @revision.setter
1460 def revision(self, value):
1474 def revision(self, value):
1461 warnings.warn("Use idx instead", DeprecationWarning)
1475 warnings.warn("Use idx instead", DeprecationWarning)
1462 self.idx = value
1476 self.idx = value
1463
1477
1464
1478
1465 class EmptyRepository(BaseRepository):
1479 class EmptyRepository(BaseRepository):
1466 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1480 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1467 pass
1481 pass
1468
1482
1469 def get_diff(self, *args, **kwargs):
1483 def get_diff(self, *args, **kwargs):
1470 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1484 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1471 return GitDiff('')
1485 return GitDiff('')
1472
1486
1473
1487
1474 class CollectionGenerator(object):
1488 class CollectionGenerator(object):
1475
1489
1476 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1490 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1477 self.repo = repo
1491 self.repo = repo
1478 self.commit_ids = commit_ids
1492 self.commit_ids = commit_ids
1479 # TODO: (oliver) this isn't currently hooked up
1493 # TODO: (oliver) this isn't currently hooked up
1480 self.collection_size = None
1494 self.collection_size = None
1481 self.pre_load = pre_load
1495 self.pre_load = pre_load
1482
1496
1483 def __len__(self):
1497 def __len__(self):
1484 if self.collection_size is not None:
1498 if self.collection_size is not None:
1485 return self.collection_size
1499 return self.collection_size
1486 return self.commit_ids.__len__()
1500 return self.commit_ids.__len__()
1487
1501
1488 def __iter__(self):
1502 def __iter__(self):
1489 for commit_id in self.commit_ids:
1503 for commit_id in self.commit_ids:
1490 # TODO: johbo: Mercurial passes in commit indices or commit ids
1504 # TODO: johbo: Mercurial passes in commit indices or commit ids
1491 yield self._commit_factory(commit_id)
1505 yield self._commit_factory(commit_id)
1492
1506
1493 def _commit_factory(self, commit_id):
1507 def _commit_factory(self, commit_id):
1494 """
1508 """
1495 Allows backends to override the way commits are generated.
1509 Allows backends to override the way commits are generated.
1496 """
1510 """
1497 return self.repo.get_commit(commit_id=commit_id,
1511 return self.repo.get_commit(commit_id=commit_id,
1498 pre_load=self.pre_load)
1512 pre_load=self.pre_load)
1499
1513
1500 def __getslice__(self, i, j):
1514 def __getslice__(self, i, j):
1501 """
1515 """
1502 Returns an iterator of sliced repository
1516 Returns an iterator of sliced repository
1503 """
1517 """
1504 commit_ids = self.commit_ids[i:j]
1518 commit_ids = self.commit_ids[i:j]
1505 return self.__class__(
1519 return self.__class__(
1506 self.repo, commit_ids, pre_load=self.pre_load)
1520 self.repo, commit_ids, pre_load=self.pre_load)
1507
1521
1508 def __repr__(self):
1522 def __repr__(self):
1509 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1523 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1510
1524
1511
1525
1512 class Config(object):
1526 class Config(object):
1513 """
1527 """
1514 Represents the configuration for a repository.
1528 Represents the configuration for a repository.
1515
1529
1516 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1530 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1517 standard library. It implements only the needed subset.
1531 standard library. It implements only the needed subset.
1518 """
1532 """
1519
1533
1520 def __init__(self):
1534 def __init__(self):
1521 self._values = {}
1535 self._values = {}
1522
1536
1523 def copy(self):
1537 def copy(self):
1524 clone = Config()
1538 clone = Config()
1525 for section, values in self._values.items():
1539 for section, values in self._values.items():
1526 clone._values[section] = values.copy()
1540 clone._values[section] = values.copy()
1527 return clone
1541 return clone
1528
1542
1529 def __repr__(self):
1543 def __repr__(self):
1530 return '<Config(%s sections) at %s>' % (
1544 return '<Config(%s sections) at %s>' % (
1531 len(self._values), hex(id(self)))
1545 len(self._values), hex(id(self)))
1532
1546
1533 def items(self, section):
1547 def items(self, section):
1534 return self._values.get(section, {}).iteritems()
1548 return self._values.get(section, {}).iteritems()
1535
1549
1536 def get(self, section, option):
1550 def get(self, section, option):
1537 return self._values.get(section, {}).get(option)
1551 return self._values.get(section, {}).get(option)
1538
1552
1539 def set(self, section, option, value):
1553 def set(self, section, option, value):
1540 section_values = self._values.setdefault(section, {})
1554 section_values = self._values.setdefault(section, {})
1541 section_values[option] = value
1555 section_values[option] = value
1542
1556
1543 def clear_section(self, section):
1557 def clear_section(self, section):
1544 self._values[section] = {}
1558 self._values[section] = {}
1545
1559
1546 def serialize(self):
1560 def serialize(self):
1547 """
1561 """
1548 Creates a list of three tuples (section, key, value) representing
1562 Creates a list of three tuples (section, key, value) representing
1549 this config object.
1563 this config object.
1550 """
1564 """
1551 items = []
1565 items = []
1552 for section in self._values:
1566 for section in self._values:
1553 for option, value in self._values[section].items():
1567 for option, value in self._values[section].items():
1554 items.append(
1568 items.append(
1555 (safe_str(section), safe_str(option), safe_str(value)))
1569 (safe_str(section), safe_str(option), safe_str(value)))
1556 return items
1570 return items
1557
1571
1558
1572
1559 class Diff(object):
1573 class Diff(object):
1560 """
1574 """
1561 Represents a diff result from a repository backend.
1575 Represents a diff result from a repository backend.
1562
1576
1563 Subclasses have to provide a backend specific value for
1577 Subclasses have to provide a backend specific value for
1564 :attr:`_header_re` and :attr:`_meta_re`.
1578 :attr:`_header_re` and :attr:`_meta_re`.
1565 """
1579 """
1566 _meta_re = None
1580 _meta_re = None
1567 _header_re = None
1581 _header_re = None
1568
1582
1569 def __init__(self, raw_diff):
1583 def __init__(self, raw_diff):
1570 self.raw = raw_diff
1584 self.raw = raw_diff
1571
1585
1572 def chunks(self):
1586 def chunks(self):
1573 """
1587 """
1574 split the diff in chunks of separate --git a/file b/file chunks
1588 split the diff in chunks of separate --git a/file b/file chunks
1575 to make diffs consistent we must prepend with \n, and make sure
1589 to make diffs consistent we must prepend with \n, and make sure
1576 we can detect last chunk as this was also has special rule
1590 we can detect last chunk as this was also has special rule
1577 """
1591 """
1578
1592
1579 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1593 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1580 header = diff_parts[0]
1594 header = diff_parts[0]
1581
1595
1582 if self._meta_re:
1596 if self._meta_re:
1583 match = self._meta_re.match(header)
1597 match = self._meta_re.match(header)
1584
1598
1585 chunks = diff_parts[1:]
1599 chunks = diff_parts[1:]
1586 total_chunks = len(chunks)
1600 total_chunks = len(chunks)
1587
1601
1588 return (
1602 return (
1589 DiffChunk(chunk, self, cur_chunk == total_chunks)
1603 DiffChunk(chunk, self, cur_chunk == total_chunks)
1590 for cur_chunk, chunk in enumerate(chunks, start=1))
1604 for cur_chunk, chunk in enumerate(chunks, start=1))
1591
1605
1592
1606
1593 class DiffChunk(object):
1607 class DiffChunk(object):
1594
1608
1595 def __init__(self, chunk, diff, last_chunk):
1609 def __init__(self, chunk, diff, last_chunk):
1596 self._diff = diff
1610 self._diff = diff
1597
1611
1598 # since we split by \ndiff --git that part is lost from original diff
1612 # since we split by \ndiff --git that part is lost from original diff
1599 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1613 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1600 if not last_chunk:
1614 if not last_chunk:
1601 chunk += '\n'
1615 chunk += '\n'
1602
1616
1603 match = self._diff._header_re.match(chunk)
1617 match = self._diff._header_re.match(chunk)
1604 self.header = match.groupdict()
1618 self.header = match.groupdict()
1605 self.diff = chunk[match.end():]
1619 self.diff = chunk[match.end():]
1606 self.raw = chunk
1620 self.raw = chunk
@@ -1,987 +1,989 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 GIT repository module
22 GIT repository module
23 """
23 """
24
24
25 import logging
25 import logging
26 import os
26 import os
27 import re
27 import re
28 import shutil
28 import shutil
29
29
30 from zope.cachedescriptors.property import Lazy as LazyProperty
30 from zope.cachedescriptors.property import Lazy as LazyProperty
31
31
32 from rhodecode.lib.compat import OrderedDict
32 from rhodecode.lib.compat import OrderedDict
33 from rhodecode.lib.datelib import (
33 from rhodecode.lib.datelib import (
34 utcdate_fromtimestamp, makedate, date_astimestamp)
34 utcdate_fromtimestamp, makedate, date_astimestamp)
35 from rhodecode.lib.utils import safe_unicode, safe_str
35 from rhodecode.lib.utils import safe_unicode, safe_str
36 from rhodecode.lib.vcs import connection, path as vcspath
36 from rhodecode.lib.vcs import connection, path as vcspath
37 from rhodecode.lib.vcs.backends.base import (
37 from rhodecode.lib.vcs.backends.base import (
38 BaseRepository, CollectionGenerator, Config, MergeResponse,
38 BaseRepository, CollectionGenerator, Config, MergeResponse,
39 MergeFailureReason, Reference)
39 MergeFailureReason, Reference)
40 from rhodecode.lib.vcs.backends.git.commit import GitCommit
40 from rhodecode.lib.vcs.backends.git.commit import GitCommit
41 from rhodecode.lib.vcs.backends.git.diff import GitDiff
41 from rhodecode.lib.vcs.backends.git.diff import GitDiff
42 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
42 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
43 from rhodecode.lib.vcs.exceptions import (
43 from rhodecode.lib.vcs.exceptions import (
44 CommitDoesNotExistError, EmptyRepositoryError,
44 CommitDoesNotExistError, EmptyRepositoryError,
45 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
45 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
46
46
47
47
48 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
48 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class GitRepository(BaseRepository):
53 class GitRepository(BaseRepository):
54 """
54 """
55 Git repository backend.
55 Git repository backend.
56 """
56 """
57 DEFAULT_BRANCH_NAME = 'master'
57 DEFAULT_BRANCH_NAME = 'master'
58
58
59 contact = BaseRepository.DEFAULT_CONTACT
59 contact = BaseRepository.DEFAULT_CONTACT
60
60
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
62 update_after_clone=False, with_wire=None, bare=False):
62 update_after_clone=False, with_wire=None, bare=False):
63
63
64 self.path = safe_str(os.path.abspath(repo_path))
64 self.path = safe_str(os.path.abspath(repo_path))
65 self.config = config if config else self.get_default_config()
65 self.config = config if config else self.get_default_config()
66 self._remote = connection.Git(
66 self._remote = connection.Git(
67 self.path, self.config, with_wire=with_wire)
67 self.path, self.config, with_wire=with_wire)
68
68
69 self._init_repo(create, src_url, update_after_clone, bare)
69 self._init_repo(create, src_url, update_after_clone, bare)
70
70
71 # caches
71 # caches
72 self._commit_ids = {}
72 self._commit_ids = {}
73
73
74 self.bookmarks = {}
75
76 @LazyProperty
74 @LazyProperty
77 def bare(self):
75 def bare(self):
78 return self._remote.bare()
76 return self._remote.bare()
79
77
80 @LazyProperty
78 @LazyProperty
81 def head(self):
79 def head(self):
82 return self._remote.head()
80 return self._remote.head()
83
81
84 @LazyProperty
82 @LazyProperty
85 def commit_ids(self):
83 def commit_ids(self):
86 """
84 """
87 Returns list of commit ids, in ascending order. Being lazy
85 Returns list of commit ids, in ascending order. Being lazy
88 attribute allows external tools to inject commit ids from cache.
86 attribute allows external tools to inject commit ids from cache.
89 """
87 """
90 commit_ids = self._get_all_commit_ids()
88 commit_ids = self._get_all_commit_ids()
91 self._rebuild_cache(commit_ids)
89 self._rebuild_cache(commit_ids)
92 return commit_ids
90 return commit_ids
93
91
94 def _rebuild_cache(self, commit_ids):
92 def _rebuild_cache(self, commit_ids):
95 self._commit_ids = dict((commit_id, index)
93 self._commit_ids = dict((commit_id, index)
96 for index, commit_id in enumerate(commit_ids))
94 for index, commit_id in enumerate(commit_ids))
97
95
98 def run_git_command(self, cmd, **opts):
96 def run_git_command(self, cmd, **opts):
99 """
97 """
100 Runs given ``cmd`` as git command and returns tuple
98 Runs given ``cmd`` as git command and returns tuple
101 (stdout, stderr).
99 (stdout, stderr).
102
100
103 :param cmd: git command to be executed
101 :param cmd: git command to be executed
104 :param opts: env options to pass into Subprocess command
102 :param opts: env options to pass into Subprocess command
105 """
103 """
106 if not isinstance(cmd, list):
104 if not isinstance(cmd, list):
107 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
105 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
108
106
109 out, err = self._remote.run_git_command(cmd, **opts)
107 out, err = self._remote.run_git_command(cmd, **opts)
110 if err:
108 if err:
111 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
109 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
112 return out, err
110 return out, err
113
111
114 @staticmethod
112 @staticmethod
115 def check_url(url, config):
113 def check_url(url, config):
116 """
114 """
117 Function will check given url and try to verify if it's a valid
115 Function will check given url and try to verify if it's a valid
118 link. Sometimes it may happened that git will issue basic
116 link. Sometimes it may happened that git will issue basic
119 auth request that can cause whole API to hang when used from python
117 auth request that can cause whole API to hang when used from python
120 or other external calls.
118 or other external calls.
121
119
122 On failures it'll raise urllib2.HTTPError, exception is also thrown
120 On failures it'll raise urllib2.HTTPError, exception is also thrown
123 when the return code is non 200
121 when the return code is non 200
124 """
122 """
125 # check first if it's not an url
123 # check first if it's not an url
126 if os.path.isdir(url) or url.startswith('file:'):
124 if os.path.isdir(url) or url.startswith('file:'):
127 return True
125 return True
128
126
129 if '+' in url.split('://', 1)[0]:
127 if '+' in url.split('://', 1)[0]:
130 url = url.split('+', 1)[1]
128 url = url.split('+', 1)[1]
131
129
132 # Request the _remote to verify the url
130 # Request the _remote to verify the url
133 return connection.Git.check_url(url, config.serialize())
131 return connection.Git.check_url(url, config.serialize())
134
132
135 @staticmethod
133 @staticmethod
136 def is_valid_repository(path):
134 def is_valid_repository(path):
137 if os.path.isdir(os.path.join(path, '.git')):
135 if os.path.isdir(os.path.join(path, '.git')):
138 return True
136 return True
139 # check case of bare repository
137 # check case of bare repository
140 try:
138 try:
141 GitRepository(path)
139 GitRepository(path)
142 return True
140 return True
143 except VCSError:
141 except VCSError:
144 pass
142 pass
145 return False
143 return False
146
144
147 def _init_repo(self, create, src_url=None, update_after_clone=False,
145 def _init_repo(self, create, src_url=None, update_after_clone=False,
148 bare=False):
146 bare=False):
149 if create and os.path.exists(self.path):
147 if create and os.path.exists(self.path):
150 raise RepositoryError(
148 raise RepositoryError(
151 "Cannot create repository at %s, location already exist"
149 "Cannot create repository at %s, location already exist"
152 % self.path)
150 % self.path)
153
151
154 try:
152 try:
155 if create and src_url:
153 if create and src_url:
156 GitRepository.check_url(src_url, self.config)
154 GitRepository.check_url(src_url, self.config)
157 self.clone(src_url, update_after_clone, bare)
155 self.clone(src_url, update_after_clone, bare)
158 elif create:
156 elif create:
159 os.makedirs(self.path, mode=0755)
157 os.makedirs(self.path, mode=0755)
160
158
161 if bare:
159 if bare:
162 self._remote.init_bare()
160 self._remote.init_bare()
163 else:
161 else:
164 self._remote.init()
162 self._remote.init()
165 else:
163 else:
166 if not self._remote.assert_correct_path():
164 if not self._remote.assert_correct_path():
167 raise RepositoryError(
165 raise RepositoryError(
168 'Path "%s" does not contain a Git repository' %
166 'Path "%s" does not contain a Git repository' %
169 (self.path,))
167 (self.path,))
170
168
171 # TODO: johbo: check if we have to translate the OSError here
169 # TODO: johbo: check if we have to translate the OSError here
172 except OSError as err:
170 except OSError as err:
173 raise RepositoryError(err)
171 raise RepositoryError(err)
174
172
175 def _get_all_commit_ids(self, filters=None):
173 def _get_all_commit_ids(self, filters=None):
176 # we must check if this repo is not empty, since later command
174 # we must check if this repo is not empty, since later command
177 # fails if it is. And it's cheaper to ask than throw the subprocess
175 # fails if it is. And it's cheaper to ask than throw the subprocess
178 # errors
176 # errors
179 try:
177 try:
180 self._remote.head()
178 self._remote.head()
181 except KeyError:
179 except KeyError:
182 return []
180 return []
183
181
184 rev_filter = ['--branches', '--tags']
182 rev_filter = ['--branches', '--tags']
185 extra_filter = []
183 extra_filter = []
186
184
187 if filters:
185 if filters:
188 if filters.get('since'):
186 if filters.get('since'):
189 extra_filter.append('--since=%s' % (filters['since']))
187 extra_filter.append('--since=%s' % (filters['since']))
190 if filters.get('until'):
188 if filters.get('until'):
191 extra_filter.append('--until=%s' % (filters['until']))
189 extra_filter.append('--until=%s' % (filters['until']))
192 if filters.get('branch_name'):
190 if filters.get('branch_name'):
193 rev_filter = ['--tags']
191 rev_filter = ['--tags']
194 extra_filter.append(filters['branch_name'])
192 extra_filter.append(filters['branch_name'])
195 rev_filter.extend(extra_filter)
193 rev_filter.extend(extra_filter)
196
194
197 # if filters.get('start') or filters.get('end'):
195 # if filters.get('start') or filters.get('end'):
198 # # skip is offset, max-count is limit
196 # # skip is offset, max-count is limit
199 # if filters.get('start'):
197 # if filters.get('start'):
200 # extra_filter += ' --skip=%s' % filters['start']
198 # extra_filter += ' --skip=%s' % filters['start']
201 # if filters.get('end'):
199 # if filters.get('end'):
202 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
200 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
203
201
204 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
202 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
205 try:
203 try:
206 output, __ = self.run_git_command(cmd)
204 output, __ = self.run_git_command(cmd)
207 except RepositoryError:
205 except RepositoryError:
208 # Can be raised for empty repositories
206 # Can be raised for empty repositories
209 return []
207 return []
210 return output.splitlines()
208 return output.splitlines()
211
209
212 def _get_commit_id(self, commit_id_or_idx):
210 def _get_commit_id(self, commit_id_or_idx):
213 def is_null(value):
211 def is_null(value):
214 return len(value) == commit_id_or_idx.count('0')
212 return len(value) == commit_id_or_idx.count('0')
215
213
216 if self.is_empty():
214 if self.is_empty():
217 raise EmptyRepositoryError("There are no commits yet")
215 raise EmptyRepositoryError("There are no commits yet")
218
216
219 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
217 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
220 return self.commit_ids[-1]
218 return self.commit_ids[-1]
221
219
222 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
220 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
223 if ((is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12)
221 if ((is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12)
224 or isinstance(commit_id_or_idx, int) or is_null(commit_id_or_idx)):
222 or isinstance(commit_id_or_idx, int) or is_null(commit_id_or_idx)):
225 try:
223 try:
226 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
224 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
227 except Exception:
225 except Exception:
228 msg = "Commit %s does not exist for %s" % (
226 msg = "Commit %s does not exist for %s" % (
229 commit_id_or_idx, self)
227 commit_id_or_idx, self)
230 raise CommitDoesNotExistError(msg)
228 raise CommitDoesNotExistError(msg)
231
229
232 elif is_bstr:
230 elif is_bstr:
233 # check full path ref, eg. refs/heads/master
231 # check full path ref, eg. refs/heads/master
234 ref_id = self._refs.get(commit_id_or_idx)
232 ref_id = self._refs.get(commit_id_or_idx)
235 if ref_id:
233 if ref_id:
236 return ref_id
234 return ref_id
237
235
238 # check branch name
236 # check branch name
239 branch_ids = self.branches.values()
237 branch_ids = self.branches.values()
240 ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx)
238 ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx)
241 if ref_id:
239 if ref_id:
242 return ref_id
240 return ref_id
243
241
244 # check tag name
242 # check tag name
245 ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx)
243 ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx)
246 if ref_id:
244 if ref_id:
247 return ref_id
245 return ref_id
248
246
249 if (not SHA_PATTERN.match(commit_id_or_idx) or
247 if (not SHA_PATTERN.match(commit_id_or_idx) or
250 commit_id_or_idx not in self.commit_ids):
248 commit_id_or_idx not in self.commit_ids):
251 msg = "Commit %s does not exist for %s" % (
249 msg = "Commit %s does not exist for %s" % (
252 commit_id_or_idx, self)
250 commit_id_or_idx, self)
253 raise CommitDoesNotExistError(msg)
251 raise CommitDoesNotExistError(msg)
254
252
255 # Ensure we return full id
253 # Ensure we return full id
256 if not SHA_PATTERN.match(str(commit_id_or_idx)):
254 if not SHA_PATTERN.match(str(commit_id_or_idx)):
257 raise CommitDoesNotExistError(
255 raise CommitDoesNotExistError(
258 "Given commit id %s not recognized" % commit_id_or_idx)
256 "Given commit id %s not recognized" % commit_id_or_idx)
259 return commit_id_or_idx
257 return commit_id_or_idx
260
258
261 def get_hook_location(self):
259 def get_hook_location(self):
262 """
260 """
263 returns absolute path to location where hooks are stored
261 returns absolute path to location where hooks are stored
264 """
262 """
265 loc = os.path.join(self.path, 'hooks')
263 loc = os.path.join(self.path, 'hooks')
266 if not self.bare:
264 if not self.bare:
267 loc = os.path.join(self.path, '.git', 'hooks')
265 loc = os.path.join(self.path, '.git', 'hooks')
268 return loc
266 return loc
269
267
270 @LazyProperty
268 @LazyProperty
271 def last_change(self):
269 def last_change(self):
272 """
270 """
273 Returns last change made on this repository as
271 Returns last change made on this repository as
274 `datetime.datetime` object.
272 `datetime.datetime` object.
275 """
273 """
276 try:
274 try:
277 return self.get_commit().date
275 return self.get_commit().date
278 except RepositoryError:
276 except RepositoryError:
279 tzoffset = makedate()[1]
277 tzoffset = makedate()[1]
280 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
278 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
281
279
282 def _get_fs_mtime(self):
280 def _get_fs_mtime(self):
283 idx_loc = '' if self.bare else '.git'
281 idx_loc = '' if self.bare else '.git'
284 # fallback to filesystem
282 # fallback to filesystem
285 in_path = os.path.join(self.path, idx_loc, "index")
283 in_path = os.path.join(self.path, idx_loc, "index")
286 he_path = os.path.join(self.path, idx_loc, "HEAD")
284 he_path = os.path.join(self.path, idx_loc, "HEAD")
287 if os.path.exists(in_path):
285 if os.path.exists(in_path):
288 return os.stat(in_path).st_mtime
286 return os.stat(in_path).st_mtime
289 else:
287 else:
290 return os.stat(he_path).st_mtime
288 return os.stat(he_path).st_mtime
291
289
292 @LazyProperty
290 @LazyProperty
293 def description(self):
291 def description(self):
294 description = self._remote.get_description()
292 description = self._remote.get_description()
295 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
293 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
296
294
297 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
295 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
298 if self.is_empty():
296 if self.is_empty():
299 return OrderedDict()
297 return OrderedDict()
300
298
301 result = []
299 result = []
302 for ref, sha in self._refs.iteritems():
300 for ref, sha in self._refs.iteritems():
303 if ref.startswith(prefix):
301 if ref.startswith(prefix):
304 ref_name = ref
302 ref_name = ref
305 if strip_prefix:
303 if strip_prefix:
306 ref_name = ref[len(prefix):]
304 ref_name = ref[len(prefix):]
307 result.append((safe_unicode(ref_name), sha))
305 result.append((safe_unicode(ref_name), sha))
308
306
309 def get_name(entry):
307 def get_name(entry):
310 return entry[0]
308 return entry[0]
311
309
312 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
310 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
313
311
314 def _get_branches(self):
312 def _get_branches(self):
315 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
313 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
316
314
317 @LazyProperty
315 @LazyProperty
318 def branches(self):
316 def branches(self):
319 return self._get_branches()
317 return self._get_branches()
320
318
321 @LazyProperty
319 @LazyProperty
322 def branches_closed(self):
320 def branches_closed(self):
323 return {}
321 return {}
324
322
325 @LazyProperty
323 @LazyProperty
324 def bookmarks(self):
325 return {}
326
327 @LazyProperty
326 def branches_all(self):
328 def branches_all(self):
327 all_branches = {}
329 all_branches = {}
328 all_branches.update(self.branches)
330 all_branches.update(self.branches)
329 all_branches.update(self.branches_closed)
331 all_branches.update(self.branches_closed)
330 return all_branches
332 return all_branches
331
333
332 @LazyProperty
334 @LazyProperty
333 def tags(self):
335 def tags(self):
334 return self._get_tags()
336 return self._get_tags()
335
337
336 def _get_tags(self):
338 def _get_tags(self):
337 return self._get_refs_entries(
339 return self._get_refs_entries(
338 prefix='refs/tags/', strip_prefix=True, reverse=True)
340 prefix='refs/tags/', strip_prefix=True, reverse=True)
339
341
340 def tag(self, name, user, commit_id=None, message=None, date=None,
342 def tag(self, name, user, commit_id=None, message=None, date=None,
341 **kwargs):
343 **kwargs):
342 # TODO: fix this method to apply annotated tags correct with message
344 # TODO: fix this method to apply annotated tags correct with message
343 """
345 """
344 Creates and returns a tag for the given ``commit_id``.
346 Creates and returns a tag for the given ``commit_id``.
345
347
346 :param name: name for new tag
348 :param name: name for new tag
347 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
349 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
348 :param commit_id: commit id for which new tag would be created
350 :param commit_id: commit id for which new tag would be created
349 :param message: message of the tag's commit
351 :param message: message of the tag's commit
350 :param date: date of tag's commit
352 :param date: date of tag's commit
351
353
352 :raises TagAlreadyExistError: if tag with same name already exists
354 :raises TagAlreadyExistError: if tag with same name already exists
353 """
355 """
354 if name in self.tags:
356 if name in self.tags:
355 raise TagAlreadyExistError("Tag %s already exists" % name)
357 raise TagAlreadyExistError("Tag %s already exists" % name)
356 commit = self.get_commit(commit_id=commit_id)
358 commit = self.get_commit(commit_id=commit_id)
357 message = message or "Added tag %s for commit %s" % (
359 message = message or "Added tag %s for commit %s" % (
358 name, commit.raw_id)
360 name, commit.raw_id)
359 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
361 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
360
362
361 self._refs = self._get_refs()
363 self._refs = self._get_refs()
362 self.tags = self._get_tags()
364 self.tags = self._get_tags()
363 return commit
365 return commit
364
366
365 def remove_tag(self, name, user, message=None, date=None):
367 def remove_tag(self, name, user, message=None, date=None):
366 """
368 """
367 Removes tag with the given ``name``.
369 Removes tag with the given ``name``.
368
370
369 :param name: name of the tag to be removed
371 :param name: name of the tag to be removed
370 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
372 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
371 :param message: message of the tag's removal commit
373 :param message: message of the tag's removal commit
372 :param date: date of tag's removal commit
374 :param date: date of tag's removal commit
373
375
374 :raises TagDoesNotExistError: if tag with given name does not exists
376 :raises TagDoesNotExistError: if tag with given name does not exists
375 """
377 """
376 if name not in self.tags:
378 if name not in self.tags:
377 raise TagDoesNotExistError("Tag %s does not exist" % name)
379 raise TagDoesNotExistError("Tag %s does not exist" % name)
378 tagpath = vcspath.join(
380 tagpath = vcspath.join(
379 self._remote.get_refs_path(), 'refs', 'tags', name)
381 self._remote.get_refs_path(), 'refs', 'tags', name)
380 try:
382 try:
381 os.remove(tagpath)
383 os.remove(tagpath)
382 self._refs = self._get_refs()
384 self._refs = self._get_refs()
383 self.tags = self._get_tags()
385 self.tags = self._get_tags()
384 except OSError as e:
386 except OSError as e:
385 raise RepositoryError(e.strerror)
387 raise RepositoryError(e.strerror)
386
388
387 def _get_refs(self):
389 def _get_refs(self):
388 return self._remote.get_refs()
390 return self._remote.get_refs()
389
391
390 @LazyProperty
392 @LazyProperty
391 def _refs(self):
393 def _refs(self):
392 return self._get_refs()
394 return self._get_refs()
393
395
394 @property
396 @property
395 def _ref_tree(self):
397 def _ref_tree(self):
396 node = tree = {}
398 node = tree = {}
397 for ref, sha in self._refs.iteritems():
399 for ref, sha in self._refs.iteritems():
398 path = ref.split('/')
400 path = ref.split('/')
399 for bit in path[:-1]:
401 for bit in path[:-1]:
400 node = node.setdefault(bit, {})
402 node = node.setdefault(bit, {})
401 node[path[-1]] = sha
403 node[path[-1]] = sha
402 node = tree
404 node = tree
403 return tree
405 return tree
404
406
405 def get_remote_ref(self, ref_name):
407 def get_remote_ref(self, ref_name):
406 ref_key = 'refs/remotes/origin/{}'.format(safe_str(ref_name))
408 ref_key = 'refs/remotes/origin/{}'.format(safe_str(ref_name))
407 try:
409 try:
408 return self._refs[ref_key]
410 return self._refs[ref_key]
409 except Exception:
411 except Exception:
410 return
412 return
411
413
412 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
414 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
413 """
415 """
414 Returns `GitCommit` object representing commit from git repository
416 Returns `GitCommit` object representing commit from git repository
415 at the given `commit_id` or head (most recent commit) if None given.
417 at the given `commit_id` or head (most recent commit) if None given.
416 """
418 """
417 if commit_id is not None:
419 if commit_id is not None:
418 self._validate_commit_id(commit_id)
420 self._validate_commit_id(commit_id)
419 elif commit_idx is not None:
421 elif commit_idx is not None:
420 self._validate_commit_idx(commit_idx)
422 self._validate_commit_idx(commit_idx)
421 commit_id = commit_idx
423 commit_id = commit_idx
422 commit_id = self._get_commit_id(commit_id)
424 commit_id = self._get_commit_id(commit_id)
423 try:
425 try:
424 # Need to call remote to translate id for tagging scenario
426 # Need to call remote to translate id for tagging scenario
425 commit_id = self._remote.get_object(commit_id)["commit_id"]
427 commit_id = self._remote.get_object(commit_id)["commit_id"]
426 idx = self._commit_ids[commit_id]
428 idx = self._commit_ids[commit_id]
427 except KeyError:
429 except KeyError:
428 raise RepositoryError("Cannot get object with id %s" % commit_id)
430 raise RepositoryError("Cannot get object with id %s" % commit_id)
429
431
430 return GitCommit(self, commit_id, idx, pre_load=pre_load)
432 return GitCommit(self, commit_id, idx, pre_load=pre_load)
431
433
432 def get_commits(
434 def get_commits(
433 self, start_id=None, end_id=None, start_date=None, end_date=None,
435 self, start_id=None, end_id=None, start_date=None, end_date=None,
434 branch_name=None, show_hidden=False, pre_load=None):
436 branch_name=None, show_hidden=False, pre_load=None):
435 """
437 """
436 Returns generator of `GitCommit` objects from start to end (both
438 Returns generator of `GitCommit` objects from start to end (both
437 are inclusive), in ascending date order.
439 are inclusive), in ascending date order.
438
440
439 :param start_id: None, str(commit_id)
441 :param start_id: None, str(commit_id)
440 :param end_id: None, str(commit_id)
442 :param end_id: None, str(commit_id)
441 :param start_date: if specified, commits with commit date less than
443 :param start_date: if specified, commits with commit date less than
442 ``start_date`` would be filtered out from returned set
444 ``start_date`` would be filtered out from returned set
443 :param end_date: if specified, commits with commit date greater than
445 :param end_date: if specified, commits with commit date greater than
444 ``end_date`` would be filtered out from returned set
446 ``end_date`` would be filtered out from returned set
445 :param branch_name: if specified, commits not reachable from given
447 :param branch_name: if specified, commits not reachable from given
446 branch would be filtered out from returned set
448 branch would be filtered out from returned set
447 :param show_hidden: Show hidden commits such as obsolete or hidden from
449 :param show_hidden: Show hidden commits such as obsolete or hidden from
448 Mercurial evolve
450 Mercurial evolve
449 :raise BranchDoesNotExistError: If given `branch_name` does not
451 :raise BranchDoesNotExistError: If given `branch_name` does not
450 exist.
452 exist.
451 :raise CommitDoesNotExistError: If commits for given `start` or
453 :raise CommitDoesNotExistError: If commits for given `start` or
452 `end` could not be found.
454 `end` could not be found.
453
455
454 """
456 """
455 if self.is_empty():
457 if self.is_empty():
456 raise EmptyRepositoryError("There are no commits yet")
458 raise EmptyRepositoryError("There are no commits yet")
457 self._validate_branch_name(branch_name)
459 self._validate_branch_name(branch_name)
458
460
459 if start_id is not None:
461 if start_id is not None:
460 self._validate_commit_id(start_id)
462 self._validate_commit_id(start_id)
461 if end_id is not None:
463 if end_id is not None:
462 self._validate_commit_id(end_id)
464 self._validate_commit_id(end_id)
463
465
464 start_raw_id = self._get_commit_id(start_id)
466 start_raw_id = self._get_commit_id(start_id)
465 start_pos = self._commit_ids[start_raw_id] if start_id else None
467 start_pos = self._commit_ids[start_raw_id] if start_id else None
466 end_raw_id = self._get_commit_id(end_id)
468 end_raw_id = self._get_commit_id(end_id)
467 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
469 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
468
470
469 if None not in [start_id, end_id] and start_pos > end_pos:
471 if None not in [start_id, end_id] and start_pos > end_pos:
470 raise RepositoryError(
472 raise RepositoryError(
471 "Start commit '%s' cannot be after end commit '%s'" %
473 "Start commit '%s' cannot be after end commit '%s'" %
472 (start_id, end_id))
474 (start_id, end_id))
473
475
474 if end_pos is not None:
476 if end_pos is not None:
475 end_pos += 1
477 end_pos += 1
476
478
477 filter_ = []
479 filter_ = []
478 if branch_name:
480 if branch_name:
479 filter_.append({'branch_name': branch_name})
481 filter_.append({'branch_name': branch_name})
480 if start_date and not end_date:
482 if start_date and not end_date:
481 filter_.append({'since': start_date})
483 filter_.append({'since': start_date})
482 if end_date and not start_date:
484 if end_date and not start_date:
483 filter_.append({'until': end_date})
485 filter_.append({'until': end_date})
484 if start_date and end_date:
486 if start_date and end_date:
485 filter_.append({'since': start_date})
487 filter_.append({'since': start_date})
486 filter_.append({'until': end_date})
488 filter_.append({'until': end_date})
487
489
488 # if start_pos or end_pos:
490 # if start_pos or end_pos:
489 # filter_.append({'start': start_pos})
491 # filter_.append({'start': start_pos})
490 # filter_.append({'end': end_pos})
492 # filter_.append({'end': end_pos})
491
493
492 if filter_:
494 if filter_:
493 revfilters = {
495 revfilters = {
494 'branch_name': branch_name,
496 'branch_name': branch_name,
495 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
497 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
496 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
498 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
497 'start': start_pos,
499 'start': start_pos,
498 'end': end_pos,
500 'end': end_pos,
499 }
501 }
500 commit_ids = self._get_all_commit_ids(filters=revfilters)
502 commit_ids = self._get_all_commit_ids(filters=revfilters)
501
503
502 # pure python stuff, it's slow due to walker walking whole repo
504 # pure python stuff, it's slow due to walker walking whole repo
503 # def get_revs(walker):
505 # def get_revs(walker):
504 # for walker_entry in walker:
506 # for walker_entry in walker:
505 # yield walker_entry.commit.id
507 # yield walker_entry.commit.id
506 # revfilters = {}
508 # revfilters = {}
507 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
509 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
508 else:
510 else:
509 commit_ids = self.commit_ids
511 commit_ids = self.commit_ids
510
512
511 if start_pos or end_pos:
513 if start_pos or end_pos:
512 commit_ids = commit_ids[start_pos: end_pos]
514 commit_ids = commit_ids[start_pos: end_pos]
513
515
514 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
516 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
515
517
516 def get_diff(
518 def get_diff(
517 self, commit1, commit2, path='', ignore_whitespace=False,
519 self, commit1, commit2, path='', ignore_whitespace=False,
518 context=3, path1=None):
520 context=3, path1=None):
519 """
521 """
520 Returns (git like) *diff*, as plain text. Shows changes introduced by
522 Returns (git like) *diff*, as plain text. Shows changes introduced by
521 ``commit2`` since ``commit1``.
523 ``commit2`` since ``commit1``.
522
524
523 :param commit1: Entry point from which diff is shown. Can be
525 :param commit1: Entry point from which diff is shown. Can be
524 ``self.EMPTY_COMMIT`` - in this case, patch showing all
526 ``self.EMPTY_COMMIT`` - in this case, patch showing all
525 the changes since empty state of the repository until ``commit2``
527 the changes since empty state of the repository until ``commit2``
526 :param commit2: Until which commits changes should be shown.
528 :param commit2: Until which commits changes should be shown.
527 :param ignore_whitespace: If set to ``True``, would not show whitespace
529 :param ignore_whitespace: If set to ``True``, would not show whitespace
528 changes. Defaults to ``False``.
530 changes. Defaults to ``False``.
529 :param context: How many lines before/after changed lines should be
531 :param context: How many lines before/after changed lines should be
530 shown. Defaults to ``3``.
532 shown. Defaults to ``3``.
531 """
533 """
532 self._validate_diff_commits(commit1, commit2)
534 self._validate_diff_commits(commit1, commit2)
533 if path1 is not None and path1 != path:
535 if path1 is not None and path1 != path:
534 raise ValueError("Diff of two different paths not supported.")
536 raise ValueError("Diff of two different paths not supported.")
535
537
536 flags = [
538 flags = [
537 '-U%s' % context, '--full-index', '--binary', '-p',
539 '-U%s' % context, '--full-index', '--binary', '-p',
538 '-M', '--abbrev=40']
540 '-M', '--abbrev=40']
539 if ignore_whitespace:
541 if ignore_whitespace:
540 flags.append('-w')
542 flags.append('-w')
541
543
542 if commit1 == self.EMPTY_COMMIT:
544 if commit1 == self.EMPTY_COMMIT:
543 cmd = ['show'] + flags + [commit2.raw_id]
545 cmd = ['show'] + flags + [commit2.raw_id]
544 else:
546 else:
545 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
547 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
546
548
547 if path:
549 if path:
548 cmd.extend(['--', path])
550 cmd.extend(['--', path])
549
551
550 stdout, __ = self.run_git_command(cmd)
552 stdout, __ = self.run_git_command(cmd)
551 # If we used 'show' command, strip first few lines (until actual diff
553 # If we used 'show' command, strip first few lines (until actual diff
552 # starts)
554 # starts)
553 if commit1 == self.EMPTY_COMMIT:
555 if commit1 == self.EMPTY_COMMIT:
554 lines = stdout.splitlines()
556 lines = stdout.splitlines()
555 x = 0
557 x = 0
556 for line in lines:
558 for line in lines:
557 if line.startswith('diff'):
559 if line.startswith('diff'):
558 break
560 break
559 x += 1
561 x += 1
560 # Append new line just like 'diff' command do
562 # Append new line just like 'diff' command do
561 stdout = '\n'.join(lines[x:]) + '\n'
563 stdout = '\n'.join(lines[x:]) + '\n'
562 return GitDiff(stdout)
564 return GitDiff(stdout)
563
565
564 def strip(self, commit_id, branch_name):
566 def strip(self, commit_id, branch_name):
565 commit = self.get_commit(commit_id=commit_id)
567 commit = self.get_commit(commit_id=commit_id)
566 if commit.merge:
568 if commit.merge:
567 raise Exception('Cannot reset to merge commit')
569 raise Exception('Cannot reset to merge commit')
568
570
569 # parent is going to be the new head now
571 # parent is going to be the new head now
570 commit = commit.parents[0]
572 commit = commit.parents[0]
571 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
573 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
572
574
573 self.commit_ids = self._get_all_commit_ids()
575 self.commit_ids = self._get_all_commit_ids()
574 self._rebuild_cache(self.commit_ids)
576 self._rebuild_cache(self.commit_ids)
575
577
576 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
578 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
577 if commit_id1 == commit_id2:
579 if commit_id1 == commit_id2:
578 return commit_id1
580 return commit_id1
579
581
580 if self != repo2:
582 if self != repo2:
581 commits = self._remote.get_missing_revs(
583 commits = self._remote.get_missing_revs(
582 commit_id1, commit_id2, repo2.path)
584 commit_id1, commit_id2, repo2.path)
583 if commits:
585 if commits:
584 commit = repo2.get_commit(commits[-1])
586 commit = repo2.get_commit(commits[-1])
585 if commit.parents:
587 if commit.parents:
586 ancestor_id = commit.parents[0].raw_id
588 ancestor_id = commit.parents[0].raw_id
587 else:
589 else:
588 ancestor_id = None
590 ancestor_id = None
589 else:
591 else:
590 # no commits from other repo, ancestor_id is the commit_id2
592 # no commits from other repo, ancestor_id is the commit_id2
591 ancestor_id = commit_id2
593 ancestor_id = commit_id2
592 else:
594 else:
593 output, __ = self.run_git_command(
595 output, __ = self.run_git_command(
594 ['merge-base', commit_id1, commit_id2])
596 ['merge-base', commit_id1, commit_id2])
595 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
597 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
596
598
597 return ancestor_id
599 return ancestor_id
598
600
599 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
601 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
600 repo1 = self
602 repo1 = self
601 ancestor_id = None
603 ancestor_id = None
602
604
603 if commit_id1 == commit_id2:
605 if commit_id1 == commit_id2:
604 commits = []
606 commits = []
605 elif repo1 != repo2:
607 elif repo1 != repo2:
606 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
608 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
607 repo2.path)
609 repo2.path)
608 commits = [
610 commits = [
609 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
611 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
610 for commit_id in reversed(missing_ids)]
612 for commit_id in reversed(missing_ids)]
611 else:
613 else:
612 output, __ = repo1.run_git_command(
614 output, __ = repo1.run_git_command(
613 ['log', '--reverse', '--pretty=format: %H', '-s',
615 ['log', '--reverse', '--pretty=format: %H', '-s',
614 '%s..%s' % (commit_id1, commit_id2)])
616 '%s..%s' % (commit_id1, commit_id2)])
615 commits = [
617 commits = [
616 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
618 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
617 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
619 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
618
620
619 return commits
621 return commits
620
622
621 @LazyProperty
623 @LazyProperty
622 def in_memory_commit(self):
624 def in_memory_commit(self):
623 """
625 """
624 Returns ``GitInMemoryCommit`` object for this repository.
626 Returns ``GitInMemoryCommit`` object for this repository.
625 """
627 """
626 return GitInMemoryCommit(self)
628 return GitInMemoryCommit(self)
627
629
628 def clone(self, url, update_after_clone=True, bare=False):
630 def clone(self, url, update_after_clone=True, bare=False):
629 """
631 """
630 Tries to clone commits from external location.
632 Tries to clone commits from external location.
631
633
632 :param update_after_clone: If set to ``False``, git won't checkout
634 :param update_after_clone: If set to ``False``, git won't checkout
633 working directory
635 working directory
634 :param bare: If set to ``True``, repository would be cloned into
636 :param bare: If set to ``True``, repository would be cloned into
635 *bare* git repository (no working directory at all).
637 *bare* git repository (no working directory at all).
636 """
638 """
637 # init_bare and init expect empty dir created to proceed
639 # init_bare and init expect empty dir created to proceed
638 if not os.path.exists(self.path):
640 if not os.path.exists(self.path):
639 os.mkdir(self.path)
641 os.mkdir(self.path)
640
642
641 if bare:
643 if bare:
642 self._remote.init_bare()
644 self._remote.init_bare()
643 else:
645 else:
644 self._remote.init()
646 self._remote.init()
645
647
646 deferred = '^{}'
648 deferred = '^{}'
647 valid_refs = ('refs/heads', 'refs/tags', 'HEAD')
649 valid_refs = ('refs/heads', 'refs/tags', 'HEAD')
648
650
649 return self._remote.clone(
651 return self._remote.clone(
650 url, deferred, valid_refs, update_after_clone)
652 url, deferred, valid_refs, update_after_clone)
651
653
652 def pull(self, url, commit_ids=None):
654 def pull(self, url, commit_ids=None):
653 """
655 """
654 Tries to pull changes from external location. We use fetch here since
656 Tries to pull changes from external location. We use fetch here since
655 pull in get does merges and we want to be compatible with hg backend so
657 pull in get does merges and we want to be compatible with hg backend so
656 pull == fetch in this case
658 pull == fetch in this case
657 """
659 """
658 self.fetch(url, commit_ids=commit_ids)
660 self.fetch(url, commit_ids=commit_ids)
659
661
660 def fetch(self, url, commit_ids=None):
662 def fetch(self, url, commit_ids=None):
661 """
663 """
662 Tries to fetch changes from external location.
664 Tries to fetch changes from external location.
663 """
665 """
664 refs = None
666 refs = None
665
667
666 if commit_ids is not None:
668 if commit_ids is not None:
667 remote_refs = self._remote.get_remote_refs(url)
669 remote_refs = self._remote.get_remote_refs(url)
668 refs = [
670 refs = [
669 ref for ref in remote_refs if remote_refs[ref] in commit_ids]
671 ref for ref in remote_refs if remote_refs[ref] in commit_ids]
670 self._remote.fetch(url, refs=refs)
672 self._remote.fetch(url, refs=refs)
671
673
672 def push(self, url):
674 def push(self, url):
673 refs = None
675 refs = None
674 self._remote.sync_push(url, refs=refs)
676 self._remote.sync_push(url, refs=refs)
675
677
676 def set_refs(self, ref_name, commit_id):
678 def set_refs(self, ref_name, commit_id):
677 self._remote.set_refs(ref_name, commit_id)
679 self._remote.set_refs(ref_name, commit_id)
678
680
679 def remove_ref(self, ref_name):
681 def remove_ref(self, ref_name):
680 self._remote.remove_ref(ref_name)
682 self._remote.remove_ref(ref_name)
681
683
682 def _update_server_info(self):
684 def _update_server_info(self):
683 """
685 """
684 runs gits update-server-info command in this repo instance
686 runs gits update-server-info command in this repo instance
685 """
687 """
686 self._remote.update_server_info()
688 self._remote.update_server_info()
687
689
688 def _current_branch(self):
690 def _current_branch(self):
689 """
691 """
690 Return the name of the current branch.
692 Return the name of the current branch.
691
693
692 It only works for non bare repositories (i.e. repositories with a
694 It only works for non bare repositories (i.e. repositories with a
693 working copy)
695 working copy)
694 """
696 """
695 if self.bare:
697 if self.bare:
696 raise RepositoryError('Bare git repos do not have active branches')
698 raise RepositoryError('Bare git repos do not have active branches')
697
699
698 if self.is_empty():
700 if self.is_empty():
699 return None
701 return None
700
702
701 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
703 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
702 return stdout.strip()
704 return stdout.strip()
703
705
704 def _checkout(self, branch_name, create=False, force=False):
706 def _checkout(self, branch_name, create=False, force=False):
705 """
707 """
706 Checkout a branch in the working directory.
708 Checkout a branch in the working directory.
707
709
708 It tries to create the branch if create is True, failing if the branch
710 It tries to create the branch if create is True, failing if the branch
709 already exists.
711 already exists.
710
712
711 It only works for non bare repositories (i.e. repositories with a
713 It only works for non bare repositories (i.e. repositories with a
712 working copy)
714 working copy)
713 """
715 """
714 if self.bare:
716 if self.bare:
715 raise RepositoryError('Cannot checkout branches in a bare git repo')
717 raise RepositoryError('Cannot checkout branches in a bare git repo')
716
718
717 cmd = ['checkout']
719 cmd = ['checkout']
718 if force:
720 if force:
719 cmd.append('-f')
721 cmd.append('-f')
720 if create:
722 if create:
721 cmd.append('-b')
723 cmd.append('-b')
722 cmd.append(branch_name)
724 cmd.append(branch_name)
723 self.run_git_command(cmd, fail_on_stderr=False)
725 self.run_git_command(cmd, fail_on_stderr=False)
724
726
725 def _identify(self):
727 def _identify(self):
726 """
728 """
727 Return the current state of the working directory.
729 Return the current state of the working directory.
728 """
730 """
729 if self.bare:
731 if self.bare:
730 raise RepositoryError('Bare git repos do not have active branches')
732 raise RepositoryError('Bare git repos do not have active branches')
731
733
732 if self.is_empty():
734 if self.is_empty():
733 return None
735 return None
734
736
735 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
737 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
736 return stdout.strip()
738 return stdout.strip()
737
739
738 def _local_clone(self, clone_path, branch_name, source_branch=None):
740 def _local_clone(self, clone_path, branch_name, source_branch=None):
739 """
741 """
740 Create a local clone of the current repo.
742 Create a local clone of the current repo.
741 """
743 """
742 # N.B.(skreft): the --branch option is required as otherwise the shallow
744 # N.B.(skreft): the --branch option is required as otherwise the shallow
743 # clone will only fetch the active branch.
745 # clone will only fetch the active branch.
744 cmd = ['clone', '--branch', branch_name,
746 cmd = ['clone', '--branch', branch_name,
745 self.path, os.path.abspath(clone_path)]
747 self.path, os.path.abspath(clone_path)]
746
748
747 self.run_git_command(cmd, fail_on_stderr=False)
749 self.run_git_command(cmd, fail_on_stderr=False)
748
750
749 # if we get the different source branch, make sure we also fetch it for
751 # if we get the different source branch, make sure we also fetch it for
750 # merge conditions
752 # merge conditions
751 if source_branch and source_branch != branch_name:
753 if source_branch and source_branch != branch_name:
752 # check if the ref exists.
754 # check if the ref exists.
753 shadow_repo = GitRepository(os.path.abspath(clone_path))
755 shadow_repo = GitRepository(os.path.abspath(clone_path))
754 if shadow_repo.get_remote_ref(source_branch):
756 if shadow_repo.get_remote_ref(source_branch):
755 cmd = ['fetch', self.path, source_branch]
757 cmd = ['fetch', self.path, source_branch]
756 self.run_git_command(cmd, fail_on_stderr=False)
758 self.run_git_command(cmd, fail_on_stderr=False)
757
759
758 def _local_fetch(self, repository_path, branch_name):
760 def _local_fetch(self, repository_path, branch_name):
759 """
761 """
760 Fetch a branch from a local repository.
762 Fetch a branch from a local repository.
761 """
763 """
762 repository_path = os.path.abspath(repository_path)
764 repository_path = os.path.abspath(repository_path)
763 if repository_path == self.path:
765 if repository_path == self.path:
764 raise ValueError('Cannot fetch from the same repository')
766 raise ValueError('Cannot fetch from the same repository')
765
767
766 cmd = ['fetch', '--no-tags', repository_path, branch_name]
768 cmd = ['fetch', '--no-tags', repository_path, branch_name]
767 self.run_git_command(cmd, fail_on_stderr=False)
769 self.run_git_command(cmd, fail_on_stderr=False)
768
770
769 def _last_fetch_heads(self):
771 def _last_fetch_heads(self):
770 """
772 """
771 Return the last fetched heads that need merging.
773 Return the last fetched heads that need merging.
772
774
773 The algorithm is defined at
775 The algorithm is defined at
774 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
776 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
775 """
777 """
776 if not self.bare:
778 if not self.bare:
777 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
779 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
778 else:
780 else:
779 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
781 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
780
782
781 heads = []
783 heads = []
782 with open(fetch_heads_path) as f:
784 with open(fetch_heads_path) as f:
783 for line in f:
785 for line in f:
784 if ' not-for-merge ' in line:
786 if ' not-for-merge ' in line:
785 continue
787 continue
786 line = re.sub('\t.*', '', line, flags=re.DOTALL)
788 line = re.sub('\t.*', '', line, flags=re.DOTALL)
787 heads.append(line)
789 heads.append(line)
788
790
789 return heads
791 return heads
790
792
791 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
793 def _get_shadow_instance(self, shadow_repository_path, enable_hooks=False):
792 return GitRepository(shadow_repository_path)
794 return GitRepository(shadow_repository_path)
793
795
794 def _local_pull(self, repository_path, branch_name, ff_only=True):
796 def _local_pull(self, repository_path, branch_name, ff_only=True):
795 """
797 """
796 Pull a branch from a local repository.
798 Pull a branch from a local repository.
797 """
799 """
798 if self.bare:
800 if self.bare:
799 raise RepositoryError('Cannot pull into a bare git repository')
801 raise RepositoryError('Cannot pull into a bare git repository')
800 # N.B.(skreft): The --ff-only option is to make sure this is a
802 # N.B.(skreft): The --ff-only option is to make sure this is a
801 # fast-forward (i.e., we are only pulling new changes and there are no
803 # fast-forward (i.e., we are only pulling new changes and there are no
802 # conflicts with our current branch)
804 # conflicts with our current branch)
803 # Additionally, that option needs to go before --no-tags, otherwise git
805 # Additionally, that option needs to go before --no-tags, otherwise git
804 # pull complains about it being an unknown flag.
806 # pull complains about it being an unknown flag.
805 cmd = ['pull']
807 cmd = ['pull']
806 if ff_only:
808 if ff_only:
807 cmd.append('--ff-only')
809 cmd.append('--ff-only')
808 cmd.extend(['--no-tags', repository_path, branch_name])
810 cmd.extend(['--no-tags', repository_path, branch_name])
809 self.run_git_command(cmd, fail_on_stderr=False)
811 self.run_git_command(cmd, fail_on_stderr=False)
810
812
811 def _local_merge(self, merge_message, user_name, user_email, heads):
813 def _local_merge(self, merge_message, user_name, user_email, heads):
812 """
814 """
813 Merge the given head into the checked out branch.
815 Merge the given head into the checked out branch.
814
816
815 It will force a merge commit.
817 It will force a merge commit.
816
818
817 Currently it raises an error if the repo is empty, as it is not possible
819 Currently it raises an error if the repo is empty, as it is not possible
818 to create a merge commit in an empty repo.
820 to create a merge commit in an empty repo.
819
821
820 :param merge_message: The message to use for the merge commit.
822 :param merge_message: The message to use for the merge commit.
821 :param heads: the heads to merge.
823 :param heads: the heads to merge.
822 """
824 """
823 if self.bare:
825 if self.bare:
824 raise RepositoryError('Cannot merge into a bare git repository')
826 raise RepositoryError('Cannot merge into a bare git repository')
825
827
826 if not heads:
828 if not heads:
827 return
829 return
828
830
829 if self.is_empty():
831 if self.is_empty():
830 # TODO(skreft): do somehting more robust in this case.
832 # TODO(skreft): do somehting more robust in this case.
831 raise RepositoryError(
833 raise RepositoryError(
832 'Do not know how to merge into empty repositories yet')
834 'Do not know how to merge into empty repositories yet')
833
835
834 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
836 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
835 # commit message. We also specify the user who is doing the merge.
837 # commit message. We also specify the user who is doing the merge.
836 cmd = ['-c', 'user.name=%s' % safe_str(user_name),
838 cmd = ['-c', 'user.name=%s' % safe_str(user_name),
837 '-c', 'user.email=%s' % safe_str(user_email),
839 '-c', 'user.email=%s' % safe_str(user_email),
838 'merge', '--no-ff', '-m', safe_str(merge_message)]
840 'merge', '--no-ff', '-m', safe_str(merge_message)]
839 cmd.extend(heads)
841 cmd.extend(heads)
840 try:
842 try:
841 self.run_git_command(cmd, fail_on_stderr=False)
843 self.run_git_command(cmd, fail_on_stderr=False)
842 except RepositoryError:
844 except RepositoryError:
843 # Cleanup any merge leftovers
845 # Cleanup any merge leftovers
844 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
846 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
845 raise
847 raise
846
848
847 def _local_push(
849 def _local_push(
848 self, source_branch, repository_path, target_branch,
850 self, source_branch, repository_path, target_branch,
849 enable_hooks=False, rc_scm_data=None):
851 enable_hooks=False, rc_scm_data=None):
850 """
852 """
851 Push the source_branch to the given repository and target_branch.
853 Push the source_branch to the given repository and target_branch.
852
854
853 Currently it if the target_branch is not master and the target repo is
855 Currently it if the target_branch is not master and the target repo is
854 empty, the push will work, but then GitRepository won't be able to find
856 empty, the push will work, but then GitRepository won't be able to find
855 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
857 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
856 pointing to master, which does not exist).
858 pointing to master, which does not exist).
857
859
858 It does not run the hooks in the target repo.
860 It does not run the hooks in the target repo.
859 """
861 """
860 # TODO(skreft): deal with the case in which the target repo is empty,
862 # TODO(skreft): deal with the case in which the target repo is empty,
861 # and the target_branch is not master.
863 # and the target_branch is not master.
862 target_repo = GitRepository(repository_path)
864 target_repo = GitRepository(repository_path)
863 if (not target_repo.bare and
865 if (not target_repo.bare and
864 target_repo._current_branch() == target_branch):
866 target_repo._current_branch() == target_branch):
865 # Git prevents pushing to the checked out branch, so simulate it by
867 # Git prevents pushing to the checked out branch, so simulate it by
866 # pulling into the target repository.
868 # pulling into the target repository.
867 target_repo._local_pull(self.path, source_branch)
869 target_repo._local_pull(self.path, source_branch)
868 else:
870 else:
869 cmd = ['push', os.path.abspath(repository_path),
871 cmd = ['push', os.path.abspath(repository_path),
870 '%s:%s' % (source_branch, target_branch)]
872 '%s:%s' % (source_branch, target_branch)]
871 gitenv = {}
873 gitenv = {}
872 if rc_scm_data:
874 if rc_scm_data:
873 gitenv.update({'RC_SCM_DATA': rc_scm_data})
875 gitenv.update({'RC_SCM_DATA': rc_scm_data})
874
876
875 if not enable_hooks:
877 if not enable_hooks:
876 gitenv['RC_SKIP_HOOKS'] = '1'
878 gitenv['RC_SKIP_HOOKS'] = '1'
877 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
879 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
878
880
879 def _get_new_pr_branch(self, source_branch, target_branch):
881 def _get_new_pr_branch(self, source_branch, target_branch):
880 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
882 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
881 pr_branches = []
883 pr_branches = []
882 for branch in self.branches:
884 for branch in self.branches:
883 if branch.startswith(prefix):
885 if branch.startswith(prefix):
884 pr_branches.append(int(branch[len(prefix):]))
886 pr_branches.append(int(branch[len(prefix):]))
885
887
886 if not pr_branches:
888 if not pr_branches:
887 branch_id = 0
889 branch_id = 0
888 else:
890 else:
889 branch_id = max(pr_branches) + 1
891 branch_id = max(pr_branches) + 1
890
892
891 return '%s%d' % (prefix, branch_id)
893 return '%s%d' % (prefix, branch_id)
892
894
893 def _merge_repo(self, shadow_repository_path, target_ref,
895 def _merge_repo(self, shadow_repository_path, target_ref,
894 source_repo, source_ref, merge_message,
896 source_repo, source_ref, merge_message,
895 merger_name, merger_email, dry_run=False,
897 merger_name, merger_email, dry_run=False,
896 use_rebase=False, close_branch=False):
898 use_rebase=False, close_branch=False):
897 if target_ref.commit_id != self.branches[target_ref.name]:
899 if target_ref.commit_id != self.branches[target_ref.name]:
898 return MergeResponse(
900 return MergeResponse(
899 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
901 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
900
902
901 shadow_repo = GitRepository(shadow_repository_path)
903 shadow_repo = GitRepository(shadow_repository_path)
902 # checkout source, if it's different. Otherwise we could not
904 # checkout source, if it's different. Otherwise we could not
903 # fetch proper commits for merge testing
905 # fetch proper commits for merge testing
904 if source_ref.name != target_ref.name:
906 if source_ref.name != target_ref.name:
905 if shadow_repo.get_remote_ref(source_ref.name):
907 if shadow_repo.get_remote_ref(source_ref.name):
906 shadow_repo._checkout(source_ref.name, force=True)
908 shadow_repo._checkout(source_ref.name, force=True)
907
909
908 # checkout target
910 # checkout target
909 shadow_repo._checkout(target_ref.name, force=True)
911 shadow_repo._checkout(target_ref.name, force=True)
910 shadow_repo._local_pull(self.path, target_ref.name)
912 shadow_repo._local_pull(self.path, target_ref.name)
911
913
912 # Need to reload repo to invalidate the cache, or otherwise we cannot
914 # Need to reload repo to invalidate the cache, or otherwise we cannot
913 # retrieve the last target commit.
915 # retrieve the last target commit.
914 shadow_repo = GitRepository(shadow_repository_path)
916 shadow_repo = GitRepository(shadow_repository_path)
915 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
917 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
916 return MergeResponse(
918 return MergeResponse(
917 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
919 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
918
920
919 pr_branch = shadow_repo._get_new_pr_branch(
921 pr_branch = shadow_repo._get_new_pr_branch(
920 source_ref.name, target_ref.name)
922 source_ref.name, target_ref.name)
921 log.debug('using pull-request merge branch: `%s`', pr_branch)
923 log.debug('using pull-request merge branch: `%s`', pr_branch)
922 shadow_repo._checkout(pr_branch, create=True)
924 shadow_repo._checkout(pr_branch, create=True)
923 try:
925 try:
924 shadow_repo._local_fetch(source_repo.path, source_ref.name)
926 shadow_repo._local_fetch(source_repo.path, source_ref.name)
925 except RepositoryError:
927 except RepositoryError:
926 log.exception('Failure when doing local fetch on git shadow repo')
928 log.exception('Failure when doing local fetch on git shadow repo')
927 return MergeResponse(
929 return MergeResponse(
928 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
930 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
929
931
930 merge_ref = None
932 merge_ref = None
931 merge_failure_reason = MergeFailureReason.NONE
933 merge_failure_reason = MergeFailureReason.NONE
932 try:
934 try:
933 shadow_repo._local_merge(merge_message, merger_name, merger_email,
935 shadow_repo._local_merge(merge_message, merger_name, merger_email,
934 [source_ref.commit_id])
936 [source_ref.commit_id])
935 merge_possible = True
937 merge_possible = True
936
938
937 # Need to reload repo to invalidate the cache, or otherwise we
939 # Need to reload repo to invalidate the cache, or otherwise we
938 # cannot retrieve the merge commit.
940 # cannot retrieve the merge commit.
939 shadow_repo = GitRepository(shadow_repository_path)
941 shadow_repo = GitRepository(shadow_repository_path)
940 merge_commit_id = shadow_repo.branches[pr_branch]
942 merge_commit_id = shadow_repo.branches[pr_branch]
941
943
942 # Set a reference pointing to the merge commit. This reference may
944 # Set a reference pointing to the merge commit. This reference may
943 # be used to easily identify the last successful merge commit in
945 # be used to easily identify the last successful merge commit in
944 # the shadow repository.
946 # the shadow repository.
945 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
947 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
946 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
948 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
947 except RepositoryError:
949 except RepositoryError:
948 log.exception('Failure when doing local merge on git shadow repo')
950 log.exception('Failure when doing local merge on git shadow repo')
949 merge_possible = False
951 merge_possible = False
950 merge_failure_reason = MergeFailureReason.MERGE_FAILED
952 merge_failure_reason = MergeFailureReason.MERGE_FAILED
951
953
952 if merge_possible and not dry_run:
954 if merge_possible and not dry_run:
953 try:
955 try:
954 shadow_repo._local_push(
956 shadow_repo._local_push(
955 pr_branch, self.path, target_ref.name, enable_hooks=True,
957 pr_branch, self.path, target_ref.name, enable_hooks=True,
956 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
958 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
957 merge_succeeded = True
959 merge_succeeded = True
958 except RepositoryError:
960 except RepositoryError:
959 log.exception(
961 log.exception(
960 'Failure when doing local push on git shadow repo')
962 'Failure when doing local push on git shadow repo')
961 merge_succeeded = False
963 merge_succeeded = False
962 merge_failure_reason = MergeFailureReason.PUSH_FAILED
964 merge_failure_reason = MergeFailureReason.PUSH_FAILED
963 else:
965 else:
964 merge_succeeded = False
966 merge_succeeded = False
965
967
966 return MergeResponse(
968 return MergeResponse(
967 merge_possible, merge_succeeded, merge_ref,
969 merge_possible, merge_succeeded, merge_ref,
968 merge_failure_reason)
970 merge_failure_reason)
969
971
970 def _get_shadow_repository_path(self, workspace_id):
972 def _get_shadow_repository_path(self, workspace_id):
971 # The name of the shadow repository must start with '.', so it is
973 # The name of the shadow repository must start with '.', so it is
972 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
974 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
973 return os.path.join(
975 return os.path.join(
974 os.path.dirname(self.path),
976 os.path.dirname(self.path),
975 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
977 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
976
978
977 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref, source_ref):
979 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref, source_ref):
978 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
980 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
979 if not os.path.exists(shadow_repository_path):
981 if not os.path.exists(shadow_repository_path):
980 self._local_clone(
982 self._local_clone(
981 shadow_repository_path, target_ref.name, source_ref.name)
983 shadow_repository_path, target_ref.name, source_ref.name)
982
984
983 return shadow_repository_path
985 return shadow_repository_path
984
986
985 def cleanup_merge_workspace(self, workspace_id):
987 def cleanup_merge_workspace(self, workspace_id):
986 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
988 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
987 shutil.rmtree(shadow_repository_path, ignore_errors=True)
989 shutil.rmtree(shadow_repository_path, ignore_errors=True)
@@ -1,339 +1,341 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 SVN repository module
22 SVN repository module
23 """
23 """
24
24
25 import logging
25 import logging
26 import os
26 import os
27 import urllib
27 import urllib
28
28
29 from zope.cachedescriptors.property import Lazy as LazyProperty
29 from zope.cachedescriptors.property import Lazy as LazyProperty
30
30
31 from rhodecode.lib.compat import OrderedDict
31 from rhodecode.lib.compat import OrderedDict
32 from rhodecode.lib.datelib import date_astimestamp
32 from rhodecode.lib.datelib import date_astimestamp
33 from rhodecode.lib.utils import safe_str, safe_unicode
33 from rhodecode.lib.utils import safe_str, safe_unicode
34 from rhodecode.lib.vcs import connection, path as vcspath
34 from rhodecode.lib.vcs import connection, path as vcspath
35 from rhodecode.lib.vcs.backends import base
35 from rhodecode.lib.vcs.backends import base
36 from rhodecode.lib.vcs.backends.svn.commit import (
36 from rhodecode.lib.vcs.backends.svn.commit import (
37 SubversionCommit, _date_from_svn_properties)
37 SubversionCommit, _date_from_svn_properties)
38 from rhodecode.lib.vcs.backends.svn.diff import SubversionDiff
38 from rhodecode.lib.vcs.backends.svn.diff import SubversionDiff
39 from rhodecode.lib.vcs.backends.svn.inmemory import SubversionInMemoryCommit
39 from rhodecode.lib.vcs.backends.svn.inmemory import SubversionInMemoryCommit
40 from rhodecode.lib.vcs.conf import settings
40 from rhodecode.lib.vcs.conf import settings
41 from rhodecode.lib.vcs.exceptions import (
41 from rhodecode.lib.vcs.exceptions import (
42 CommitDoesNotExistError, EmptyRepositoryError, RepositoryError,
42 CommitDoesNotExistError, EmptyRepositoryError, RepositoryError,
43 VCSError, NodeDoesNotExistError)
43 VCSError, NodeDoesNotExistError)
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class SubversionRepository(base.BaseRepository):
49 class SubversionRepository(base.BaseRepository):
50 """
50 """
51 Subversion backend implementation
51 Subversion backend implementation
52
52
53 .. important::
53 .. important::
54
54
55 It is very important to distinguish the commit index and the commit id
55 It is very important to distinguish the commit index and the commit id
56 which is assigned by Subversion. The first one is always handled as an
56 which is assigned by Subversion. The first one is always handled as an
57 `int` by this implementation. The commit id assigned by Subversion on
57 `int` by this implementation. The commit id assigned by Subversion on
58 the other side will always be a `str`.
58 the other side will always be a `str`.
59
59
60 There is a specific trap since the first commit will have the index
60 There is a specific trap since the first commit will have the index
61 ``0`` but the svn id will be ``"1"``.
61 ``0`` but the svn id will be ``"1"``.
62
62
63 """
63 """
64
64
65 # Note: Subversion does not really have a default branch name.
65 # Note: Subversion does not really have a default branch name.
66 DEFAULT_BRANCH_NAME = None
66 DEFAULT_BRANCH_NAME = None
67
67
68 contact = base.BaseRepository.DEFAULT_CONTACT
68 contact = base.BaseRepository.DEFAULT_CONTACT
69 description = base.BaseRepository.DEFAULT_DESCRIPTION
69 description = base.BaseRepository.DEFAULT_DESCRIPTION
70
70
71 def __init__(self, repo_path, config=None, create=False, src_url=None,
71 def __init__(self, repo_path, config=None, create=False, src_url=None,
72 **kwargs):
72 **kwargs):
73 self.path = safe_str(os.path.abspath(repo_path))
73 self.path = safe_str(os.path.abspath(repo_path))
74 self.config = config if config else self.get_default_config()
74 self.config = config if config else self.get_default_config()
75 self._remote = connection.Svn(
75 self._remote = connection.Svn(
76 self.path, self.config)
76 self.path, self.config)
77
77
78 self._init_repo(create, src_url)
78 self._init_repo(create, src_url)
79
79
80 self.bookmarks = {}
81
82 def _init_repo(self, create, src_url):
80 def _init_repo(self, create, src_url):
83 if create and os.path.exists(self.path):
81 if create and os.path.exists(self.path):
84 raise RepositoryError(
82 raise RepositoryError(
85 "Cannot create repository at %s, location already exist"
83 "Cannot create repository at %s, location already exist"
86 % self.path)
84 % self.path)
87
85
88 if create:
86 if create:
89 self._remote.create_repository(settings.SVN_COMPATIBLE_VERSION)
87 self._remote.create_repository(settings.SVN_COMPATIBLE_VERSION)
90 if src_url:
88 if src_url:
91 src_url = _sanitize_url(src_url)
89 src_url = _sanitize_url(src_url)
92 self._remote.import_remote_repository(src_url)
90 self._remote.import_remote_repository(src_url)
93 else:
91 else:
94 self._check_path()
92 self._check_path()
95
93
96 @LazyProperty
94 @LazyProperty
97 def commit_ids(self):
95 def commit_ids(self):
98 head = self._remote.lookup(None)
96 head = self._remote.lookup(None)
99 return [str(r) for r in xrange(1, head + 1)]
97 return [str(r) for r in xrange(1, head + 1)]
100
98
101 @LazyProperty
99 @LazyProperty
102 def branches(self):
100 def branches(self):
103 return self._tags_or_branches('vcs_svn_branch')
101 return self._tags_or_branches('vcs_svn_branch')
104
102
105 @LazyProperty
103 @LazyProperty
106 def branches_closed(self):
104 def branches_closed(self):
107 return {}
105 return {}
108
106
109 @LazyProperty
107 @LazyProperty
108 def bookmarks(self):
109 return {}
110
111 @LazyProperty
110 def branches_all(self):
112 def branches_all(self):
111 # TODO: johbo: Implement proper branch support
113 # TODO: johbo: Implement proper branch support
112 all_branches = {}
114 all_branches = {}
113 all_branches.update(self.branches)
115 all_branches.update(self.branches)
114 all_branches.update(self.branches_closed)
116 all_branches.update(self.branches_closed)
115 return all_branches
117 return all_branches
116
118
117 @LazyProperty
119 @LazyProperty
118 def tags(self):
120 def tags(self):
119 return self._tags_or_branches('vcs_svn_tag')
121 return self._tags_or_branches('vcs_svn_tag')
120
122
121 def _tags_or_branches(self, config_section):
123 def _tags_or_branches(self, config_section):
122 found_items = {}
124 found_items = {}
123
125
124 if self.is_empty():
126 if self.is_empty():
125 return {}
127 return {}
126
128
127 for pattern in self._patterns_from_section(config_section):
129 for pattern in self._patterns_from_section(config_section):
128 pattern = vcspath.sanitize(pattern)
130 pattern = vcspath.sanitize(pattern)
129 tip = self.get_commit()
131 tip = self.get_commit()
130 try:
132 try:
131 if pattern.endswith('*'):
133 if pattern.endswith('*'):
132 basedir = tip.get_node(vcspath.dirname(pattern))
134 basedir = tip.get_node(vcspath.dirname(pattern))
133 directories = basedir.dirs
135 directories = basedir.dirs
134 else:
136 else:
135 directories = (tip.get_node(pattern), )
137 directories = (tip.get_node(pattern), )
136 except NodeDoesNotExistError:
138 except NodeDoesNotExistError:
137 continue
139 continue
138 found_items.update(
140 found_items.update(
139 (safe_unicode(n.path),
141 (safe_unicode(n.path),
140 self.commit_ids[-1])
142 self.commit_ids[-1])
141 for n in directories)
143 for n in directories)
142
144
143 def get_name(item):
145 def get_name(item):
144 return item[0]
146 return item[0]
145
147
146 return OrderedDict(sorted(found_items.items(), key=get_name))
148 return OrderedDict(sorted(found_items.items(), key=get_name))
147
149
148 def _patterns_from_section(self, section):
150 def _patterns_from_section(self, section):
149 return (pattern for key, pattern in self.config.items(section))
151 return (pattern for key, pattern in self.config.items(section))
150
152
151 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
153 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
152 if self != repo2:
154 if self != repo2:
153 raise ValueError(
155 raise ValueError(
154 "Subversion does not support getting common ancestor of"
156 "Subversion does not support getting common ancestor of"
155 " different repositories.")
157 " different repositories.")
156
158
157 if int(commit_id1) < int(commit_id2):
159 if int(commit_id1) < int(commit_id2):
158 return commit_id1
160 return commit_id1
159 return commit_id2
161 return commit_id2
160
162
161 def verify(self):
163 def verify(self):
162 verify = self._remote.verify()
164 verify = self._remote.verify()
163
165
164 self._remote.invalidate_vcs_cache()
166 self._remote.invalidate_vcs_cache()
165 return verify
167 return verify
166
168
167 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
169 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
168 # TODO: johbo: Implement better comparison, this is a very naive
170 # TODO: johbo: Implement better comparison, this is a very naive
169 # version which does not allow to compare branches, tags or folders
171 # version which does not allow to compare branches, tags or folders
170 # at all.
172 # at all.
171 if repo2 != self:
173 if repo2 != self:
172 raise ValueError(
174 raise ValueError(
173 "Subversion does not support comparison of of different "
175 "Subversion does not support comparison of of different "
174 "repositories.")
176 "repositories.")
175
177
176 if commit_id1 == commit_id2:
178 if commit_id1 == commit_id2:
177 return []
179 return []
178
180
179 commit_idx1 = self._get_commit_idx(commit_id1)
181 commit_idx1 = self._get_commit_idx(commit_id1)
180 commit_idx2 = self._get_commit_idx(commit_id2)
182 commit_idx2 = self._get_commit_idx(commit_id2)
181
183
182 commits = [
184 commits = [
183 self.get_commit(commit_idx=idx)
185 self.get_commit(commit_idx=idx)
184 for idx in range(commit_idx1 + 1, commit_idx2 + 1)]
186 for idx in range(commit_idx1 + 1, commit_idx2 + 1)]
185
187
186 return commits
188 return commits
187
189
188 def _get_commit_idx(self, commit_id):
190 def _get_commit_idx(self, commit_id):
189 try:
191 try:
190 svn_rev = int(commit_id)
192 svn_rev = int(commit_id)
191 except:
193 except:
192 # TODO: johbo: this might be only one case, HEAD, check this
194 # TODO: johbo: this might be only one case, HEAD, check this
193 svn_rev = self._remote.lookup(commit_id)
195 svn_rev = self._remote.lookup(commit_id)
194 commit_idx = svn_rev - 1
196 commit_idx = svn_rev - 1
195 if commit_idx >= len(self.commit_ids):
197 if commit_idx >= len(self.commit_ids):
196 raise CommitDoesNotExistError(
198 raise CommitDoesNotExistError(
197 "Commit at index %s does not exist." % (commit_idx, ))
199 "Commit at index %s does not exist." % (commit_idx, ))
198 return commit_idx
200 return commit_idx
199
201
200 @staticmethod
202 @staticmethod
201 def check_url(url, config):
203 def check_url(url, config):
202 """
204 """
203 Check if `url` is a valid source to import a Subversion repository.
205 Check if `url` is a valid source to import a Subversion repository.
204 """
206 """
205 # convert to URL if it's a local directory
207 # convert to URL if it's a local directory
206 if os.path.isdir(url):
208 if os.path.isdir(url):
207 url = 'file://' + urllib.pathname2url(url)
209 url = 'file://' + urllib.pathname2url(url)
208 return connection.Svn.check_url(url, config.serialize())
210 return connection.Svn.check_url(url, config.serialize())
209
211
210 @staticmethod
212 @staticmethod
211 def is_valid_repository(path):
213 def is_valid_repository(path):
212 try:
214 try:
213 SubversionRepository(path)
215 SubversionRepository(path)
214 return True
216 return True
215 except VCSError:
217 except VCSError:
216 pass
218 pass
217 return False
219 return False
218
220
219 def _check_path(self):
221 def _check_path(self):
220 if not os.path.exists(self.path):
222 if not os.path.exists(self.path):
221 raise VCSError('Path "%s" does not exist!' % (self.path, ))
223 raise VCSError('Path "%s" does not exist!' % (self.path, ))
222 if not self._remote.is_path_valid_repository(self.path):
224 if not self._remote.is_path_valid_repository(self.path):
223 raise VCSError(
225 raise VCSError(
224 'Path "%s" does not contain a Subversion repository' %
226 'Path "%s" does not contain a Subversion repository' %
225 (self.path, ))
227 (self.path, ))
226
228
227 @LazyProperty
229 @LazyProperty
228 def last_change(self):
230 def last_change(self):
229 """
231 """
230 Returns last change made on this repository as
232 Returns last change made on this repository as
231 `datetime.datetime` object.
233 `datetime.datetime` object.
232 """
234 """
233 # Subversion always has a first commit which has id "0" and contains
235 # Subversion always has a first commit which has id "0" and contains
234 # what we are looking for.
236 # what we are looking for.
235 last_id = len(self.commit_ids)
237 last_id = len(self.commit_ids)
236 properties = self._remote.revision_properties(last_id)
238 properties = self._remote.revision_properties(last_id)
237 return _date_from_svn_properties(properties)
239 return _date_from_svn_properties(properties)
238
240
239 @LazyProperty
241 @LazyProperty
240 def in_memory_commit(self):
242 def in_memory_commit(self):
241 return SubversionInMemoryCommit(self)
243 return SubversionInMemoryCommit(self)
242
244
243 def get_hook_location(self):
245 def get_hook_location(self):
244 """
246 """
245 returns absolute path to location where hooks are stored
247 returns absolute path to location where hooks are stored
246 """
248 """
247 return os.path.join(self.path, 'hooks')
249 return os.path.join(self.path, 'hooks')
248
250
249 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
251 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
250 if self.is_empty():
252 if self.is_empty():
251 raise EmptyRepositoryError("There are no commits yet")
253 raise EmptyRepositoryError("There are no commits yet")
252 if commit_id is not None:
254 if commit_id is not None:
253 self._validate_commit_id(commit_id)
255 self._validate_commit_id(commit_id)
254 elif commit_idx is not None:
256 elif commit_idx is not None:
255 self._validate_commit_idx(commit_idx)
257 self._validate_commit_idx(commit_idx)
256 try:
258 try:
257 commit_id = self.commit_ids[commit_idx]
259 commit_id = self.commit_ids[commit_idx]
258 except IndexError:
260 except IndexError:
259 raise CommitDoesNotExistError
261 raise CommitDoesNotExistError
260
262
261 commit_id = self._sanitize_commit_id(commit_id)
263 commit_id = self._sanitize_commit_id(commit_id)
262 commit = SubversionCommit(repository=self, commit_id=commit_id)
264 commit = SubversionCommit(repository=self, commit_id=commit_id)
263 return commit
265 return commit
264
266
265 def get_commits(
267 def get_commits(
266 self, start_id=None, end_id=None, start_date=None, end_date=None,
268 self, start_id=None, end_id=None, start_date=None, end_date=None,
267 branch_name=None, show_hidden=False, pre_load=None):
269 branch_name=None, show_hidden=False, pre_load=None):
268 if self.is_empty():
270 if self.is_empty():
269 raise EmptyRepositoryError("There are no commit_ids yet")
271 raise EmptyRepositoryError("There are no commit_ids yet")
270 self._validate_branch_name(branch_name)
272 self._validate_branch_name(branch_name)
271
273
272 if start_id is not None:
274 if start_id is not None:
273 self._validate_commit_id(start_id)
275 self._validate_commit_id(start_id)
274 if end_id is not None:
276 if end_id is not None:
275 self._validate_commit_id(end_id)
277 self._validate_commit_id(end_id)
276
278
277 start_raw_id = self._sanitize_commit_id(start_id)
279 start_raw_id = self._sanitize_commit_id(start_id)
278 start_pos = self.commit_ids.index(start_raw_id) if start_id else None
280 start_pos = self.commit_ids.index(start_raw_id) if start_id else None
279 end_raw_id = self._sanitize_commit_id(end_id)
281 end_raw_id = self._sanitize_commit_id(end_id)
280 end_pos = max(0, self.commit_ids.index(end_raw_id)) if end_id else None
282 end_pos = max(0, self.commit_ids.index(end_raw_id)) if end_id else None
281
283
282 if None not in [start_id, end_id] and start_pos > end_pos:
284 if None not in [start_id, end_id] and start_pos > end_pos:
283 raise RepositoryError(
285 raise RepositoryError(
284 "Start commit '%s' cannot be after end commit '%s'" %
286 "Start commit '%s' cannot be after end commit '%s'" %
285 (start_id, end_id))
287 (start_id, end_id))
286 if end_pos is not None:
288 if end_pos is not None:
287 end_pos += 1
289 end_pos += 1
288
290
289 # Date based filtering
291 # Date based filtering
290 if start_date or end_date:
292 if start_date or end_date:
291 start_raw_id, end_raw_id = self._remote.lookup_interval(
293 start_raw_id, end_raw_id = self._remote.lookup_interval(
292 date_astimestamp(start_date) if start_date else None,
294 date_astimestamp(start_date) if start_date else None,
293 date_astimestamp(end_date) if end_date else None)
295 date_astimestamp(end_date) if end_date else None)
294 start_pos = start_raw_id - 1
296 start_pos = start_raw_id - 1
295 end_pos = end_raw_id
297 end_pos = end_raw_id
296
298
297 commit_ids = self.commit_ids
299 commit_ids = self.commit_ids
298
300
299 # TODO: johbo: Reconsider impact of DEFAULT_BRANCH_NAME here
301 # TODO: johbo: Reconsider impact of DEFAULT_BRANCH_NAME here
300 if branch_name not in [None, self.DEFAULT_BRANCH_NAME]:
302 if branch_name not in [None, self.DEFAULT_BRANCH_NAME]:
301 svn_rev = long(self.commit_ids[-1])
303 svn_rev = long(self.commit_ids[-1])
302 commit_ids = self._remote.node_history(
304 commit_ids = self._remote.node_history(
303 path=branch_name, revision=svn_rev, limit=None)
305 path=branch_name, revision=svn_rev, limit=None)
304 commit_ids = [str(i) for i in reversed(commit_ids)]
306 commit_ids = [str(i) for i in reversed(commit_ids)]
305
307
306 if start_pos or end_pos:
308 if start_pos or end_pos:
307 commit_ids = commit_ids[start_pos:end_pos]
309 commit_ids = commit_ids[start_pos:end_pos]
308 return base.CollectionGenerator(self, commit_ids, pre_load=pre_load)
310 return base.CollectionGenerator(self, commit_ids, pre_load=pre_load)
309
311
310 def _sanitize_commit_id(self, commit_id):
312 def _sanitize_commit_id(self, commit_id):
311 if commit_id and commit_id.isdigit():
313 if commit_id and commit_id.isdigit():
312 if int(commit_id) <= len(self.commit_ids):
314 if int(commit_id) <= len(self.commit_ids):
313 return commit_id
315 return commit_id
314 else:
316 else:
315 raise CommitDoesNotExistError(
317 raise CommitDoesNotExistError(
316 "Commit %s does not exist." % (commit_id, ))
318 "Commit %s does not exist." % (commit_id, ))
317 if commit_id not in [
319 if commit_id not in [
318 None, 'HEAD', 'tip', self.DEFAULT_BRANCH_NAME]:
320 None, 'HEAD', 'tip', self.DEFAULT_BRANCH_NAME]:
319 raise CommitDoesNotExistError(
321 raise CommitDoesNotExistError(
320 "Commit id %s not understood." % (commit_id, ))
322 "Commit id %s not understood." % (commit_id, ))
321 svn_rev = self._remote.lookup('HEAD')
323 svn_rev = self._remote.lookup('HEAD')
322 return str(svn_rev)
324 return str(svn_rev)
323
325
324 def get_diff(
326 def get_diff(
325 self, commit1, commit2, path=None, ignore_whitespace=False,
327 self, commit1, commit2, path=None, ignore_whitespace=False,
326 context=3, path1=None):
328 context=3, path1=None):
327 self._validate_diff_commits(commit1, commit2)
329 self._validate_diff_commits(commit1, commit2)
328 svn_rev1 = long(commit1.raw_id)
330 svn_rev1 = long(commit1.raw_id)
329 svn_rev2 = long(commit2.raw_id)
331 svn_rev2 = long(commit2.raw_id)
330 diff = self._remote.diff(
332 diff = self._remote.diff(
331 svn_rev1, svn_rev2, path1=path1, path2=path,
333 svn_rev1, svn_rev2, path1=path1, path2=path,
332 ignore_whitespace=ignore_whitespace, context=context)
334 ignore_whitespace=ignore_whitespace, context=context)
333 return SubversionDiff(diff)
335 return SubversionDiff(diff)
334
336
335
337
336 def _sanitize_url(url):
338 def _sanitize_url(url):
337 if '://' not in url:
339 if '://' not in url:
338 url = 'file://' + urllib.pathname2url(url)
340 url = 'file://' + urllib.pathname2url(url)
339 return url
341 return url
General Comments 0
You need to be logged in to leave comments. Login now