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