##// END OF EJS Templates
shadow-repos: use safer way to destroy shadow repositories....
marcink -
r2777:f1cc2e3d default
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,923 +1,918 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 _get_url(self, url):
397 def _get_url(self, url):
399 """
398 """
400 Returns normalized url. If schema is not given, would fall
399 Returns normalized url. If schema is not given, would fall
401 to filesystem
400 to filesystem
402 (``file:///``) schema.
401 (``file:///``) schema.
403 """
402 """
404 url = url.encode('utf8')
403 url = url.encode('utf8')
405 if url != 'default' and '://' not in url:
404 if url != 'default' and '://' not in url:
406 url = "file:" + urllib.pathname2url(url)
405 url = "file:" + urllib.pathname2url(url)
407 return url
406 return url
408
407
409 def get_hook_location(self):
408 def get_hook_location(self):
410 """
409 """
411 returns absolute path to location where hooks are stored
410 returns absolute path to location where hooks are stored
412 """
411 """
413 return os.path.join(self.path, '.hg', '.hgrc')
412 return os.path.join(self.path, '.hg', '.hgrc')
414
413
415 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
414 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
416 """
415 """
417 Returns ``MercurialCommit`` object representing repository's
416 Returns ``MercurialCommit`` object representing repository's
418 commit at the given `commit_id` or `commit_idx`.
417 commit at the given `commit_id` or `commit_idx`.
419 """
418 """
420 if self.is_empty():
419 if self.is_empty():
421 raise EmptyRepositoryError("There are no commits yet")
420 raise EmptyRepositoryError("There are no commits yet")
422
421
423 if commit_id is not None:
422 if commit_id is not None:
424 self._validate_commit_id(commit_id)
423 self._validate_commit_id(commit_id)
425 try:
424 try:
426 idx = self._commit_ids[commit_id]
425 idx = self._commit_ids[commit_id]
427 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
426 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
428 except KeyError:
427 except KeyError:
429 pass
428 pass
430 elif commit_idx is not None:
429 elif commit_idx is not None:
431 self._validate_commit_idx(commit_idx)
430 self._validate_commit_idx(commit_idx)
432 try:
431 try:
433 id_ = self.commit_ids[commit_idx]
432 id_ = self.commit_ids[commit_idx]
434 if commit_idx < 0:
433 if commit_idx < 0:
435 commit_idx += len(self.commit_ids)
434 commit_idx += len(self.commit_ids)
436 return MercurialCommit(
435 return MercurialCommit(
437 self, id_, commit_idx, pre_load=pre_load)
436 self, id_, commit_idx, pre_load=pre_load)
438 except IndexError:
437 except IndexError:
439 commit_id = commit_idx
438 commit_id = commit_idx
440 else:
439 else:
441 commit_id = "tip"
440 commit_id = "tip"
442
441
443 if isinstance(commit_id, unicode):
442 if isinstance(commit_id, unicode):
444 commit_id = safe_str(commit_id)
443 commit_id = safe_str(commit_id)
445
444
446 try:
445 try:
447 raw_id, idx = self._remote.lookup(commit_id, both=True)
446 raw_id, idx = self._remote.lookup(commit_id, both=True)
448 except CommitDoesNotExistError:
447 except CommitDoesNotExistError:
449 msg = "Commit %s does not exist for %s" % (
448 msg = "Commit %s does not exist for %s" % (
450 commit_id, self)
449 commit_id, self)
451 raise CommitDoesNotExistError(msg)
450 raise CommitDoesNotExistError(msg)
452
451
453 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
452 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
454
453
455 def get_commits(
454 def get_commits(
456 self, start_id=None, end_id=None, start_date=None, end_date=None,
455 self, start_id=None, end_id=None, start_date=None, end_date=None,
457 branch_name=None, show_hidden=False, pre_load=None):
456 branch_name=None, show_hidden=False, pre_load=None):
458 """
457 """
459 Returns generator of ``MercurialCommit`` objects from start to end
458 Returns generator of ``MercurialCommit`` objects from start to end
460 (both are inclusive)
459 (both are inclusive)
461
460
462 :param start_id: None, str(commit_id)
461 :param start_id: None, str(commit_id)
463 :param end_id: None, str(commit_id)
462 :param end_id: None, str(commit_id)
464 :param start_date: if specified, commits with commit date less than
463 :param start_date: if specified, commits with commit date less than
465 ``start_date`` would be filtered out from returned set
464 ``start_date`` would be filtered out from returned set
466 :param end_date: if specified, commits with commit date greater than
465 :param end_date: if specified, commits with commit date greater than
467 ``end_date`` would be filtered out from returned set
466 ``end_date`` would be filtered out from returned set
468 :param branch_name: if specified, commits not reachable from given
467 :param branch_name: if specified, commits not reachable from given
469 branch would be filtered out from returned set
468 branch would be filtered out from returned set
470 :param show_hidden: Show hidden commits such as obsolete or hidden from
469 :param show_hidden: Show hidden commits such as obsolete or hidden from
471 Mercurial evolve
470 Mercurial evolve
472 :raise BranchDoesNotExistError: If given ``branch_name`` does not
471 :raise BranchDoesNotExistError: If given ``branch_name`` does not
473 exist.
472 exist.
474 :raise CommitDoesNotExistError: If commit for given ``start`` or
473 :raise CommitDoesNotExistError: If commit for given ``start`` or
475 ``end`` could not be found.
474 ``end`` could not be found.
476 """
475 """
477 # actually we should check now if it's not an empty repo
476 # actually we should check now if it's not an empty repo
478 branch_ancestors = False
477 branch_ancestors = False
479 if self.is_empty():
478 if self.is_empty():
480 raise EmptyRepositoryError("There are no commits yet")
479 raise EmptyRepositoryError("There are no commits yet")
481 self._validate_branch_name(branch_name)
480 self._validate_branch_name(branch_name)
482
481
483 if start_id is not None:
482 if start_id is not None:
484 self._validate_commit_id(start_id)
483 self._validate_commit_id(start_id)
485 c_start = self.get_commit(commit_id=start_id)
484 c_start = self.get_commit(commit_id=start_id)
486 start_pos = self._commit_ids[c_start.raw_id]
485 start_pos = self._commit_ids[c_start.raw_id]
487 else:
486 else:
488 start_pos = None
487 start_pos = None
489
488
490 if end_id is not None:
489 if end_id is not None:
491 self._validate_commit_id(end_id)
490 self._validate_commit_id(end_id)
492 c_end = self.get_commit(commit_id=end_id)
491 c_end = self.get_commit(commit_id=end_id)
493 end_pos = max(0, self._commit_ids[c_end.raw_id])
492 end_pos = max(0, self._commit_ids[c_end.raw_id])
494 else:
493 else:
495 end_pos = None
494 end_pos = None
496
495
497 if None not in [start_id, end_id] and start_pos > end_pos:
496 if None not in [start_id, end_id] and start_pos > end_pos:
498 raise RepositoryError(
497 raise RepositoryError(
499 "Start commit '%s' cannot be after end commit '%s'" %
498 "Start commit '%s' cannot be after end commit '%s'" %
500 (start_id, end_id))
499 (start_id, end_id))
501
500
502 if end_pos is not None:
501 if end_pos is not None:
503 end_pos += 1
502 end_pos += 1
504
503
505 commit_filter = []
504 commit_filter = []
506
505
507 if branch_name and not branch_ancestors:
506 if branch_name and not branch_ancestors:
508 commit_filter.append('branch("%s")' % (branch_name,))
507 commit_filter.append('branch("%s")' % (branch_name,))
509 elif branch_name and branch_ancestors:
508 elif branch_name and branch_ancestors:
510 commit_filter.append('ancestors(branch("%s"))' % (branch_name,))
509 commit_filter.append('ancestors(branch("%s"))' % (branch_name,))
511
510
512 if start_date and not end_date:
511 if start_date and not end_date:
513 commit_filter.append('date(">%s")' % (start_date,))
512 commit_filter.append('date(">%s")' % (start_date,))
514 if end_date and not start_date:
513 if end_date and not start_date:
515 commit_filter.append('date("<%s")' % (end_date,))
514 commit_filter.append('date("<%s")' % (end_date,))
516 if start_date and end_date:
515 if start_date and end_date:
517 commit_filter.append(
516 commit_filter.append(
518 'date(">%s") and date("<%s")' % (start_date, end_date))
517 'date(">%s") and date("<%s")' % (start_date, end_date))
519
518
520 if not show_hidden:
519 if not show_hidden:
521 commit_filter.append('not obsolete()')
520 commit_filter.append('not obsolete()')
522 commit_filter.append('not hidden()')
521 commit_filter.append('not hidden()')
523
522
524 # TODO: johbo: Figure out a simpler way for this solution
523 # TODO: johbo: Figure out a simpler way for this solution
525 collection_generator = CollectionGenerator
524 collection_generator = CollectionGenerator
526 if commit_filter:
525 if commit_filter:
527 commit_filter = ' and '.join(map(safe_str, commit_filter))
526 commit_filter = ' and '.join(map(safe_str, commit_filter))
528 revisions = self._remote.rev_range([commit_filter])
527 revisions = self._remote.rev_range([commit_filter])
529 collection_generator = MercurialIndexBasedCollectionGenerator
528 collection_generator = MercurialIndexBasedCollectionGenerator
530 else:
529 else:
531 revisions = self.commit_ids
530 revisions = self.commit_ids
532
531
533 if start_pos or end_pos:
532 if start_pos or end_pos:
534 revisions = revisions[start_pos:end_pos]
533 revisions = revisions[start_pos:end_pos]
535
534
536 return collection_generator(self, revisions, pre_load=pre_load)
535 return collection_generator(self, revisions, pre_load=pre_load)
537
536
538 def pull(self, url, commit_ids=None):
537 def pull(self, url, commit_ids=None):
539 """
538 """
540 Tries to pull changes from external location.
539 Tries to pull changes from external location.
541
540
542 :param commit_ids: Optional. Can be set to a list of commit ids
541 :param commit_ids: Optional. Can be set to a list of commit ids
543 which shall be pulled from the other repository.
542 which shall be pulled from the other repository.
544 """
543 """
545 url = self._get_url(url)
544 url = self._get_url(url)
546 self._remote.pull(url, commit_ids=commit_ids)
545 self._remote.pull(url, commit_ids=commit_ids)
547 self._remote.invalidate_vcs_cache()
546 self._remote.invalidate_vcs_cache()
548
547
549 def push(self, url):
548 def push(self, url):
550 url = self._get_url(url)
549 url = self._get_url(url)
551 self._remote.sync_push(url)
550 self._remote.sync_push(url)
552
551
553 def _local_clone(self, clone_path):
552 def _local_clone(self, clone_path):
554 """
553 """
555 Create a local clone of the current repo.
554 Create a local clone of the current repo.
556 """
555 """
557 self._remote.clone(self.path, clone_path, update_after_clone=True,
556 self._remote.clone(self.path, clone_path, update_after_clone=True,
558 hooks=False)
557 hooks=False)
559
558
560 def _update(self, revision, clean=False):
559 def _update(self, revision, clean=False):
561 """
560 """
562 Update the working copy to the specified revision.
561 Update the working copy to the specified revision.
563 """
562 """
564 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
563 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
565 self._remote.update(revision, clean=clean)
564 self._remote.update(revision, clean=clean)
566
565
567 def _identify(self):
566 def _identify(self):
568 """
567 """
569 Return the current state of the working directory.
568 Return the current state of the working directory.
570 """
569 """
571 return self._remote.identify().strip().rstrip('+')
570 return self._remote.identify().strip().rstrip('+')
572
571
573 def _heads(self, branch=None):
572 def _heads(self, branch=None):
574 """
573 """
575 Return the commit ids of the repository heads.
574 Return the commit ids of the repository heads.
576 """
575 """
577 return self._remote.heads(branch=branch).strip().split(' ')
576 return self._remote.heads(branch=branch).strip().split(' ')
578
577
579 def _ancestor(self, revision1, revision2):
578 def _ancestor(self, revision1, revision2):
580 """
579 """
581 Return the common ancestor of the two revisions.
580 Return the common ancestor of the two revisions.
582 """
581 """
583 return self._remote.ancestor(revision1, revision2)
582 return self._remote.ancestor(revision1, revision2)
584
583
585 def _local_push(
584 def _local_push(
586 self, revision, repository_path, push_branches=False,
585 self, revision, repository_path, push_branches=False,
587 enable_hooks=False):
586 enable_hooks=False):
588 """
587 """
589 Push the given revision to the specified repository.
588 Push the given revision to the specified repository.
590
589
591 :param push_branches: allow to create branches in the target repo.
590 :param push_branches: allow to create branches in the target repo.
592 """
591 """
593 self._remote.push(
592 self._remote.push(
594 [revision], repository_path, hooks=enable_hooks,
593 [revision], repository_path, hooks=enable_hooks,
595 push_branches=push_branches)
594 push_branches=push_branches)
596
595
597 def _local_merge(self, target_ref, merge_message, user_name, user_email,
596 def _local_merge(self, target_ref, merge_message, user_name, user_email,
598 source_ref, use_rebase=False, dry_run=False):
597 source_ref, use_rebase=False, dry_run=False):
599 """
598 """
600 Merge the given source_revision into the checked out revision.
599 Merge the given source_revision into the checked out revision.
601
600
602 Returns the commit id of the merge and a boolean indicating if the
601 Returns the commit id of the merge and a boolean indicating if the
603 commit needs to be pushed.
602 commit needs to be pushed.
604 """
603 """
605 self._update(target_ref.commit_id)
604 self._update(target_ref.commit_id)
606
605
607 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
606 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
608 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
607 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
609
608
610 if ancestor == source_ref.commit_id:
609 if ancestor == source_ref.commit_id:
611 # Nothing to do, the changes were already integrated
610 # Nothing to do, the changes were already integrated
612 return target_ref.commit_id, False
611 return target_ref.commit_id, False
613
612
614 elif ancestor == target_ref.commit_id and is_the_same_branch:
613 elif ancestor == target_ref.commit_id and is_the_same_branch:
615 # In this case we should force a commit message
614 # In this case we should force a commit message
616 return source_ref.commit_id, True
615 return source_ref.commit_id, True
617
616
618 if use_rebase:
617 if use_rebase:
619 try:
618 try:
620 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
619 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
621 target_ref.commit_id)
620 target_ref.commit_id)
622 self.bookmark(bookmark_name, revision=source_ref.commit_id)
621 self.bookmark(bookmark_name, revision=source_ref.commit_id)
623 self._remote.rebase(
622 self._remote.rebase(
624 source=source_ref.commit_id, dest=target_ref.commit_id)
623 source=source_ref.commit_id, dest=target_ref.commit_id)
625 self._remote.invalidate_vcs_cache()
624 self._remote.invalidate_vcs_cache()
626 self._update(bookmark_name)
625 self._update(bookmark_name)
627 return self._identify(), True
626 return self._identify(), True
628 except RepositoryError:
627 except RepositoryError:
629 # The rebase-abort may raise another exception which 'hides'
628 # The rebase-abort may raise another exception which 'hides'
630 # the original one, therefore we log it here.
629 # the original one, therefore we log it here.
631 log.exception('Error while rebasing shadow repo during merge.')
630 log.exception('Error while rebasing shadow repo during merge.')
632
631
633 # Cleanup any rebase leftovers
632 # Cleanup any rebase leftovers
634 self._remote.invalidate_vcs_cache()
633 self._remote.invalidate_vcs_cache()
635 self._remote.rebase(abort=True)
634 self._remote.rebase(abort=True)
636 self._remote.invalidate_vcs_cache()
635 self._remote.invalidate_vcs_cache()
637 self._remote.update(clean=True)
636 self._remote.update(clean=True)
638 raise
637 raise
639 else:
638 else:
640 try:
639 try:
641 self._remote.merge(source_ref.commit_id)
640 self._remote.merge(source_ref.commit_id)
642 self._remote.invalidate_vcs_cache()
641 self._remote.invalidate_vcs_cache()
643 self._remote.commit(
642 self._remote.commit(
644 message=safe_str(merge_message),
643 message=safe_str(merge_message),
645 username=safe_str('%s <%s>' % (user_name, user_email)))
644 username=safe_str('%s <%s>' % (user_name, user_email)))
646 self._remote.invalidate_vcs_cache()
645 self._remote.invalidate_vcs_cache()
647 return self._identify(), True
646 return self._identify(), True
648 except RepositoryError:
647 except RepositoryError:
649 # Cleanup any merge leftovers
648 # Cleanup any merge leftovers
650 self._remote.update(clean=True)
649 self._remote.update(clean=True)
651 raise
650 raise
652
651
653 def _local_close(self, target_ref, user_name, user_email,
652 def _local_close(self, target_ref, user_name, user_email,
654 source_ref, close_message=''):
653 source_ref, close_message=''):
655 """
654 """
656 Close the branch of the given source_revision
655 Close the branch of the given source_revision
657
656
658 Returns the commit id of the close and a boolean indicating if the
657 Returns the commit id of the close and a boolean indicating if the
659 commit needs to be pushed.
658 commit needs to be pushed.
660 """
659 """
661 self._update(source_ref.commit_id)
660 self._update(source_ref.commit_id)
662 message = close_message or "Closing branch: `{}`".format(source_ref.name)
661 message = close_message or "Closing branch: `{}`".format(source_ref.name)
663 try:
662 try:
664 self._remote.commit(
663 self._remote.commit(
665 message=safe_str(message),
664 message=safe_str(message),
666 username=safe_str('%s <%s>' % (user_name, user_email)),
665 username=safe_str('%s <%s>' % (user_name, user_email)),
667 close_branch=True)
666 close_branch=True)
668 self._remote.invalidate_vcs_cache()
667 self._remote.invalidate_vcs_cache()
669 return self._identify(), True
668 return self._identify(), True
670 except RepositoryError:
669 except RepositoryError:
671 # Cleanup any commit leftovers
670 # Cleanup any commit leftovers
672 self._remote.update(clean=True)
671 self._remote.update(clean=True)
673 raise
672 raise
674
673
675 def _is_the_same_branch(self, target_ref, source_ref):
674 def _is_the_same_branch(self, target_ref, source_ref):
676 return (
675 return (
677 self._get_branch_name(target_ref) ==
676 self._get_branch_name(target_ref) ==
678 self._get_branch_name(source_ref))
677 self._get_branch_name(source_ref))
679
678
680 def _get_branch_name(self, ref):
679 def _get_branch_name(self, ref):
681 if ref.type == 'branch':
680 if ref.type == 'branch':
682 return ref.name
681 return ref.name
683 return self._remote.ctx_branch(ref.commit_id)
682 return self._remote.ctx_branch(ref.commit_id)
684
683
685 def _get_shadow_repository_path(self, workspace_id):
684 def _get_shadow_repository_path(self, workspace_id):
686 # The name of the shadow repository must start with '.', so it is
685 # The name of the shadow repository must start with '.', so it is
687 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
686 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
688 return os.path.join(
687 return os.path.join(
689 os.path.dirname(self.path),
688 os.path.dirname(self.path),
690 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
689 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
691
690
692 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref, unused_source_ref):
691 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref, unused_source_ref):
693 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
692 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
694 if not os.path.exists(shadow_repository_path):
693 if not os.path.exists(shadow_repository_path):
695 self._local_clone(shadow_repository_path)
694 self._local_clone(shadow_repository_path)
696 log.debug(
695 log.debug(
697 'Prepared shadow repository in %s', shadow_repository_path)
696 'Prepared shadow repository in %s', shadow_repository_path)
698
697
699 return shadow_repository_path
698 return shadow_repository_path
700
699
701 def cleanup_merge_workspace(self, workspace_id):
702 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
703 shutil.rmtree(shadow_repository_path, ignore_errors=True)
704
705 def _merge_repo(self, shadow_repository_path, target_ref,
700 def _merge_repo(self, shadow_repository_path, target_ref,
706 source_repo, source_ref, merge_message,
701 source_repo, source_ref, merge_message,
707 merger_name, merger_email, dry_run=False,
702 merger_name, merger_email, dry_run=False,
708 use_rebase=False, close_branch=False):
703 use_rebase=False, close_branch=False):
709
704
710 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
705 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
711 'rebase' if use_rebase else 'merge', dry_run)
706 'rebase' if use_rebase else 'merge', dry_run)
712 if target_ref.commit_id not in self._heads():
707 if target_ref.commit_id not in self._heads():
713 return MergeResponse(
708 return MergeResponse(
714 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
709 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
715
710
716 try:
711 try:
717 if (target_ref.type == 'branch' and
712 if (target_ref.type == 'branch' and
718 len(self._heads(target_ref.name)) != 1):
713 len(self._heads(target_ref.name)) != 1):
719 return MergeResponse(
714 return MergeResponse(
720 False, False, None,
715 False, False, None,
721 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
716 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
722 except CommitDoesNotExistError:
717 except CommitDoesNotExistError:
723 log.exception('Failure when looking up branch heads on hg target')
718 log.exception('Failure when looking up branch heads on hg target')
724 return MergeResponse(
719 return MergeResponse(
725 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
720 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
726
721
727 shadow_repo = self._get_shadow_instance(shadow_repository_path)
722 shadow_repo = self._get_shadow_instance(shadow_repository_path)
728
723
729 log.debug('Pulling in target reference %s', target_ref)
724 log.debug('Pulling in target reference %s', target_ref)
730 self._validate_pull_reference(target_ref)
725 self._validate_pull_reference(target_ref)
731 shadow_repo._local_pull(self.path, target_ref)
726 shadow_repo._local_pull(self.path, target_ref)
732 try:
727 try:
733 log.debug('Pulling in source reference %s', source_ref)
728 log.debug('Pulling in source reference %s', source_ref)
734 source_repo._validate_pull_reference(source_ref)
729 source_repo._validate_pull_reference(source_ref)
735 shadow_repo._local_pull(source_repo.path, source_ref)
730 shadow_repo._local_pull(source_repo.path, source_ref)
736 except CommitDoesNotExistError:
731 except CommitDoesNotExistError:
737 log.exception('Failure when doing local pull on hg shadow repo')
732 log.exception('Failure when doing local pull on hg shadow repo')
738 return MergeResponse(
733 return MergeResponse(
739 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
734 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
740
735
741 merge_ref = None
736 merge_ref = None
742 merge_commit_id = None
737 merge_commit_id = None
743 close_commit_id = None
738 close_commit_id = None
744 merge_failure_reason = MergeFailureReason.NONE
739 merge_failure_reason = MergeFailureReason.NONE
745
740
746 # enforce that close branch should be used only in case we source from
741 # enforce that close branch should be used only in case we source from
747 # an actual Branch
742 # an actual Branch
748 close_branch = close_branch and source_ref.type == 'branch'
743 close_branch = close_branch and source_ref.type == 'branch'
749
744
750 # don't allow to close branch if source and target are the same
745 # don't allow to close branch if source and target are the same
751 close_branch = close_branch and source_ref.name != target_ref.name
746 close_branch = close_branch and source_ref.name != target_ref.name
752
747
753 needs_push_on_close = False
748 needs_push_on_close = False
754 if close_branch and not use_rebase and not dry_run:
749 if close_branch and not use_rebase and not dry_run:
755 try:
750 try:
756 close_commit_id, needs_push_on_close = shadow_repo._local_close(
751 close_commit_id, needs_push_on_close = shadow_repo._local_close(
757 target_ref, merger_name, merger_email, source_ref)
752 target_ref, merger_name, merger_email, source_ref)
758 merge_possible = True
753 merge_possible = True
759 except RepositoryError:
754 except RepositoryError:
760 log.exception(
755 log.exception(
761 'Failure when doing close branch on hg shadow repo')
756 'Failure when doing close branch on hg shadow repo')
762 merge_possible = False
757 merge_possible = False
763 merge_failure_reason = MergeFailureReason.MERGE_FAILED
758 merge_failure_reason = MergeFailureReason.MERGE_FAILED
764 else:
759 else:
765 merge_possible = True
760 merge_possible = True
766
761
767 needs_push = False
762 needs_push = False
768 if merge_possible:
763 if merge_possible:
769 try:
764 try:
770 merge_commit_id, needs_push = shadow_repo._local_merge(
765 merge_commit_id, needs_push = shadow_repo._local_merge(
771 target_ref, merge_message, merger_name, merger_email,
766 target_ref, merge_message, merger_name, merger_email,
772 source_ref, use_rebase=use_rebase, dry_run=dry_run)
767 source_ref, use_rebase=use_rebase, dry_run=dry_run)
773 merge_possible = True
768 merge_possible = True
774
769
775 # read the state of the close action, if it
770 # read the state of the close action, if it
776 # maybe required a push
771 # maybe required a push
777 needs_push = needs_push or needs_push_on_close
772 needs_push = needs_push or needs_push_on_close
778
773
779 # Set a bookmark pointing to the merge commit. This bookmark
774 # Set a bookmark pointing to the merge commit. This bookmark
780 # may be used to easily identify the last successful merge
775 # may be used to easily identify the last successful merge
781 # commit in the shadow repository.
776 # commit in the shadow repository.
782 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
777 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
783 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
778 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
784 except SubrepoMergeError:
779 except SubrepoMergeError:
785 log.exception(
780 log.exception(
786 'Subrepo merge error during local merge on hg shadow repo.')
781 'Subrepo merge error during local merge on hg shadow repo.')
787 merge_possible = False
782 merge_possible = False
788 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
783 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
789 needs_push = False
784 needs_push = False
790 except RepositoryError:
785 except RepositoryError:
791 log.exception('Failure when doing local merge on hg shadow repo')
786 log.exception('Failure when doing local merge on hg shadow repo')
792 merge_possible = False
787 merge_possible = False
793 merge_failure_reason = MergeFailureReason.MERGE_FAILED
788 merge_failure_reason = MergeFailureReason.MERGE_FAILED
794 needs_push = False
789 needs_push = False
795
790
796 if merge_possible and not dry_run:
791 if merge_possible and not dry_run:
797 if needs_push:
792 if needs_push:
798 # In case the target is a bookmark, update it, so after pushing
793 # In case the target is a bookmark, update it, so after pushing
799 # the bookmarks is also updated in the target.
794 # the bookmarks is also updated in the target.
800 if target_ref.type == 'book':
795 if target_ref.type == 'book':
801 shadow_repo.bookmark(
796 shadow_repo.bookmark(
802 target_ref.name, revision=merge_commit_id)
797 target_ref.name, revision=merge_commit_id)
803 try:
798 try:
804 shadow_repo_with_hooks = self._get_shadow_instance(
799 shadow_repo_with_hooks = self._get_shadow_instance(
805 shadow_repository_path,
800 shadow_repository_path,
806 enable_hooks=True)
801 enable_hooks=True)
807 # This is the actual merge action, we push from shadow
802 # This is the actual merge action, we push from shadow
808 # into origin.
803 # into origin.
809 # Note: the push_branches option will push any new branch
804 # Note: the push_branches option will push any new branch
810 # defined in the source repository to the target. This may
805 # defined in the source repository to the target. This may
811 # be dangerous as branches are permanent in Mercurial.
806 # be dangerous as branches are permanent in Mercurial.
812 # This feature was requested in issue #441.
807 # This feature was requested in issue #441.
813 shadow_repo_with_hooks._local_push(
808 shadow_repo_with_hooks._local_push(
814 merge_commit_id, self.path, push_branches=True,
809 merge_commit_id, self.path, push_branches=True,
815 enable_hooks=True)
810 enable_hooks=True)
816
811
817 # maybe we also need to push the close_commit_id
812 # maybe we also need to push the close_commit_id
818 if close_commit_id:
813 if close_commit_id:
819 shadow_repo_with_hooks._local_push(
814 shadow_repo_with_hooks._local_push(
820 close_commit_id, self.path, push_branches=True,
815 close_commit_id, self.path, push_branches=True,
821 enable_hooks=True)
816 enable_hooks=True)
822 merge_succeeded = True
817 merge_succeeded = True
823 except RepositoryError:
818 except RepositoryError:
824 log.exception(
819 log.exception(
825 'Failure when doing local push from the shadow '
820 'Failure when doing local push from the shadow '
826 'repository to the target repository.')
821 'repository to the target repository.')
827 merge_succeeded = False
822 merge_succeeded = False
828 merge_failure_reason = MergeFailureReason.PUSH_FAILED
823 merge_failure_reason = MergeFailureReason.PUSH_FAILED
829 else:
824 else:
830 merge_succeeded = True
825 merge_succeeded = True
831 else:
826 else:
832 merge_succeeded = False
827 merge_succeeded = False
833
828
834 return MergeResponse(
829 return MergeResponse(
835 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
830 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
836
831
837 def _get_shadow_instance(
832 def _get_shadow_instance(
838 self, shadow_repository_path, enable_hooks=False):
833 self, shadow_repository_path, enable_hooks=False):
839 config = self.config.copy()
834 config = self.config.copy()
840 if not enable_hooks:
835 if not enable_hooks:
841 config.clear_section('hooks')
836 config.clear_section('hooks')
842 return MercurialRepository(shadow_repository_path, config)
837 return MercurialRepository(shadow_repository_path, config)
843
838
844 def _validate_pull_reference(self, reference):
839 def _validate_pull_reference(self, reference):
845 if not (reference.name in self.bookmarks or
840 if not (reference.name in self.bookmarks or
846 reference.name in self.branches or
841 reference.name in self.branches or
847 self.get_commit(reference.commit_id)):
842 self.get_commit(reference.commit_id)):
848 raise CommitDoesNotExistError(
843 raise CommitDoesNotExistError(
849 'Unknown branch, bookmark or commit id')
844 'Unknown branch, bookmark or commit id')
850
845
851 def _local_pull(self, repository_path, reference):
846 def _local_pull(self, repository_path, reference):
852 """
847 """
853 Fetch a branch, bookmark or commit from a local repository.
848 Fetch a branch, bookmark or commit from a local repository.
854 """
849 """
855 repository_path = os.path.abspath(repository_path)
850 repository_path = os.path.abspath(repository_path)
856 if repository_path == self.path:
851 if repository_path == self.path:
857 raise ValueError('Cannot pull from the same repository')
852 raise ValueError('Cannot pull from the same repository')
858
853
859 reference_type_to_option_name = {
854 reference_type_to_option_name = {
860 'book': 'bookmark',
855 'book': 'bookmark',
861 'branch': 'branch',
856 'branch': 'branch',
862 }
857 }
863 option_name = reference_type_to_option_name.get(
858 option_name = reference_type_to_option_name.get(
864 reference.type, 'revision')
859 reference.type, 'revision')
865
860
866 if option_name == 'revision':
861 if option_name == 'revision':
867 ref = reference.commit_id
862 ref = reference.commit_id
868 else:
863 else:
869 ref = reference.name
864 ref = reference.name
870
865
871 options = {option_name: [ref]}
866 options = {option_name: [ref]}
872 self._remote.pull_cmd(repository_path, hooks=False, **options)
867 self._remote.pull_cmd(repository_path, hooks=False, **options)
873 self._remote.invalidate_vcs_cache()
868 self._remote.invalidate_vcs_cache()
874
869
875 def bookmark(self, bookmark, revision=None):
870 def bookmark(self, bookmark, revision=None):
876 if isinstance(bookmark, unicode):
871 if isinstance(bookmark, unicode):
877 bookmark = safe_str(bookmark)
872 bookmark = safe_str(bookmark)
878 self._remote.bookmark(bookmark, revision=revision)
873 self._remote.bookmark(bookmark, revision=revision)
879 self._remote.invalidate_vcs_cache()
874 self._remote.invalidate_vcs_cache()
880
875
881 def get_path_permissions(self, username):
876 def get_path_permissions(self, username):
882 hgacl_file = os.path.join(self.path, '.hg/hgacl')
877 hgacl_file = os.path.join(self.path, '.hg/hgacl')
883
878
884 def read_patterns(suffix):
879 def read_patterns(suffix):
885 svalue = None
880 svalue = None
886 try:
881 try:
887 svalue = hgacl.get('narrowhgacl', username + suffix)
882 svalue = hgacl.get('narrowhgacl', username + suffix)
888 except configparser.NoOptionError:
883 except configparser.NoOptionError:
889 try:
884 try:
890 svalue = hgacl.get('narrowhgacl', 'default' + suffix)
885 svalue = hgacl.get('narrowhgacl', 'default' + suffix)
891 except configparser.NoOptionError:
886 except configparser.NoOptionError:
892 pass
887 pass
893 if not svalue:
888 if not svalue:
894 return None
889 return None
895 result = ['/']
890 result = ['/']
896 for pattern in svalue.split():
891 for pattern in svalue.split():
897 result.append(pattern)
892 result.append(pattern)
898 if '*' not in pattern and '?' not in pattern:
893 if '*' not in pattern and '?' not in pattern:
899 result.append(pattern + '/*')
894 result.append(pattern + '/*')
900 return result
895 return result
901
896
902 if os.path.exists(hgacl_file):
897 if os.path.exists(hgacl_file):
903 try:
898 try:
904 hgacl = configparser.RawConfigParser()
899 hgacl = configparser.RawConfigParser()
905 hgacl.read(hgacl_file)
900 hgacl.read(hgacl_file)
906
901
907 includes = read_patterns('.includes')
902 includes = read_patterns('.includes')
908 excludes = read_patterns('.excludes')
903 excludes = read_patterns('.excludes')
909 return BasePathPermissionChecker.create_from_patterns(
904 return BasePathPermissionChecker.create_from_patterns(
910 includes, excludes)
905 includes, excludes)
911 except BaseException as e:
906 except BaseException as e:
912 msg = 'Cannot read ACL settings from {} on {}: {}'.format(
907 msg = 'Cannot read ACL settings from {} on {}: {}'.format(
913 hgacl_file, self.name, e)
908 hgacl_file, self.name, e)
914 raise exceptions.RepositoryRequirementError(msg)
909 raise exceptions.RepositoryRequirementError(msg)
915 else:
910 else:
916 return None
911 return None
917
912
918
913
919 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
914 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
920
915
921 def _commit_factory(self, commit_id):
916 def _commit_factory(self, commit_id):
922 return self.repo.get_commit(
917 return self.repo.get_commit(
923 commit_idx=commit_id, pre_load=self.pre_load)
918 commit_idx=commit_id, pre_load=self.pre_load)
General Comments 0
You need to be logged in to leave comments. Login now