##// END OF EJS Templates
pull-requests: fixed problems with unicode characters in branches.
milka -
r4681:7eba96a2 stable
parent child Browse files
Show More
@@ -1,1937 +1,1938 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2020 RhodeCode GmbH
3 # Copyright (C) 2014-2020 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 import os
24 import os
25 import re
25 import re
26 import time
26 import time
27 import shutil
27 import shutil
28 import datetime
28 import datetime
29 import fnmatch
29 import fnmatch
30 import itertools
30 import itertools
31 import logging
31 import logging
32 import collections
32 import collections
33 import warnings
33 import warnings
34
34
35 from zope.cachedescriptors.property import Lazy as LazyProperty
35 from zope.cachedescriptors.property import Lazy as LazyProperty
36
36
37 from pyramid import compat
37 from pyramid import compat
38
38
39 import rhodecode
39 import rhodecode
40 from rhodecode.translation import lazy_ugettext
40 from rhodecode.translation import lazy_ugettext
41 from rhodecode.lib.utils2 import safe_str, safe_unicode, CachedProperty
41 from rhodecode.lib.utils2 import safe_str, safe_unicode, CachedProperty
42 from rhodecode.lib.vcs import connection
42 from rhodecode.lib.vcs import connection
43 from rhodecode.lib.vcs.utils import author_name, author_email
43 from rhodecode.lib.vcs.utils import author_name, author_email
44 from rhodecode.lib.vcs.conf import settings
44 from rhodecode.lib.vcs.conf import settings
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
46 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
47 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
47 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
48 NodeDoesNotExistError, NodeNotChangedError, VCSError,
48 NodeDoesNotExistError, NodeNotChangedError, VCSError,
49 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
49 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
50 RepositoryError)
50 RepositoryError)
51
51
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 FILEMODE_DEFAULT = 0o100644
56 FILEMODE_DEFAULT = 0o100644
57 FILEMODE_EXECUTABLE = 0o100755
57 FILEMODE_EXECUTABLE = 0o100755
58 EMPTY_COMMIT_ID = '0' * 40
58 EMPTY_COMMIT_ID = '0' * 40
59
59
60 _Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
60 _Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
61
61
62
62
63 class Reference(_Reference):
63 class Reference(_Reference):
64
64
65 @property
65 @property
66 def branch(self):
66 def branch(self):
67 if self.type == 'branch':
67 if self.type == 'branch':
68 return self.name
68 return self.name
69
69
70 @property
70 @property
71 def bookmark(self):
71 def bookmark(self):
72 if self.type == 'book':
72 if self.type == 'book':
73 return self.name
73 return self.name
74
74
75 @property
75 @property
76 def to_unicode(self):
76 def to_unicode(self):
77 return reference_to_unicode(self)
77 return reference_to_unicode(self)
78
78
79
79
80 def unicode_to_reference(raw):
80 def unicode_to_reference(raw):
81 """
81 """
82 Convert a unicode (or string) to a reference object.
82 Convert a unicode (or string) to a reference object.
83 If unicode evaluates to False it returns None.
83 If unicode evaluates to False it returns None.
84 """
84 """
85 if raw:
85 if raw:
86 refs = raw.split(':')
86 refs = raw.split(':')
87 return Reference(*refs)
87 return Reference(*refs)
88 else:
88 else:
89 return None
89 return None
90
90
91
91
92 def reference_to_unicode(ref):
92 def reference_to_unicode(ref):
93 """
93 """
94 Convert a reference object to unicode.
94 Convert a reference object to unicode.
95 If reference is None it returns None.
95 If reference is None it returns None.
96 """
96 """
97 if ref:
97 if ref:
98 return u':'.join(ref)
98 return u':'.join(ref)
99 else:
99 else:
100 return None
100 return None
101
101
102
102
103 class MergeFailureReason(object):
103 class MergeFailureReason(object):
104 """
104 """
105 Enumeration with all the reasons why the server side merge could fail.
105 Enumeration with all the reasons why the server side merge could fail.
106
106
107 DO NOT change the number of the reasons, as they may be stored in the
107 DO NOT change the number of the reasons, as they may be stored in the
108 database.
108 database.
109
109
110 Changing the name of a reason is acceptable and encouraged to deprecate old
110 Changing the name of a reason is acceptable and encouraged to deprecate old
111 reasons.
111 reasons.
112 """
112 """
113
113
114 # Everything went well.
114 # Everything went well.
115 NONE = 0
115 NONE = 0
116
116
117 # An unexpected exception was raised. Check the logs for more details.
117 # An unexpected exception was raised. Check the logs for more details.
118 UNKNOWN = 1
118 UNKNOWN = 1
119
119
120 # The merge was not successful, there are conflicts.
120 # The merge was not successful, there are conflicts.
121 MERGE_FAILED = 2
121 MERGE_FAILED = 2
122
122
123 # The merge succeeded but we could not push it to the target repository.
123 # The merge succeeded but we could not push it to the target repository.
124 PUSH_FAILED = 3
124 PUSH_FAILED = 3
125
125
126 # The specified target is not a head in the target repository.
126 # The specified target is not a head in the target repository.
127 TARGET_IS_NOT_HEAD = 4
127 TARGET_IS_NOT_HEAD = 4
128
128
129 # The source repository contains more branches than the target. Pushing
129 # The source repository contains more branches than the target. Pushing
130 # the merge will create additional branches in the target.
130 # the merge will create additional branches in the target.
131 HG_SOURCE_HAS_MORE_BRANCHES = 5
131 HG_SOURCE_HAS_MORE_BRANCHES = 5
132
132
133 # The target reference has multiple heads. That does not allow to correctly
133 # The target reference has multiple heads. That does not allow to correctly
134 # identify the target location. This could only happen for mercurial
134 # identify the target location. This could only happen for mercurial
135 # branches.
135 # branches.
136 HG_TARGET_HAS_MULTIPLE_HEADS = 6
136 HG_TARGET_HAS_MULTIPLE_HEADS = 6
137
137
138 # The target repository is locked
138 # The target repository is locked
139 TARGET_IS_LOCKED = 7
139 TARGET_IS_LOCKED = 7
140
140
141 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
141 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
142 # A involved commit could not be found.
142 # A involved commit could not be found.
143 _DEPRECATED_MISSING_COMMIT = 8
143 _DEPRECATED_MISSING_COMMIT = 8
144
144
145 # The target repo reference is missing.
145 # The target repo reference is missing.
146 MISSING_TARGET_REF = 9
146 MISSING_TARGET_REF = 9
147
147
148 # The source repo reference is missing.
148 # The source repo reference is missing.
149 MISSING_SOURCE_REF = 10
149 MISSING_SOURCE_REF = 10
150
150
151 # The merge was not successful, there are conflicts related to sub
151 # The merge was not successful, there are conflicts related to sub
152 # repositories.
152 # repositories.
153 SUBREPO_MERGE_FAILED = 11
153 SUBREPO_MERGE_FAILED = 11
154
154
155
155
156 class UpdateFailureReason(object):
156 class UpdateFailureReason(object):
157 """
157 """
158 Enumeration with all the reasons why the pull request update could fail.
158 Enumeration with all the reasons why the pull request update could fail.
159
159
160 DO NOT change the number of the reasons, as they may be stored in the
160 DO NOT change the number of the reasons, as they may be stored in the
161 database.
161 database.
162
162
163 Changing the name of a reason is acceptable and encouraged to deprecate old
163 Changing the name of a reason is acceptable and encouraged to deprecate old
164 reasons.
164 reasons.
165 """
165 """
166
166
167 # Everything went well.
167 # Everything went well.
168 NONE = 0
168 NONE = 0
169
169
170 # An unexpected exception was raised. Check the logs for more details.
170 # An unexpected exception was raised. Check the logs for more details.
171 UNKNOWN = 1
171 UNKNOWN = 1
172
172
173 # The pull request is up to date.
173 # The pull request is up to date.
174 NO_CHANGE = 2
174 NO_CHANGE = 2
175
175
176 # The pull request has a reference type that is not supported for update.
176 # The pull request has a reference type that is not supported for update.
177 WRONG_REF_TYPE = 3
177 WRONG_REF_TYPE = 3
178
178
179 # Update failed because the target reference is missing.
179 # Update failed because the target reference is missing.
180 MISSING_TARGET_REF = 4
180 MISSING_TARGET_REF = 4
181
181
182 # Update failed because the source reference is missing.
182 # Update failed because the source reference is missing.
183 MISSING_SOURCE_REF = 5
183 MISSING_SOURCE_REF = 5
184
184
185
185
186 class MergeResponse(object):
186 class MergeResponse(object):
187
187
188 # uses .format(**metadata) for variables
188 # uses .format(**metadata) for variables
189 MERGE_STATUS_MESSAGES = {
189 MERGE_STATUS_MESSAGES = {
190 MergeFailureReason.NONE: lazy_ugettext(
190 MergeFailureReason.NONE: lazy_ugettext(
191 u'This pull request can be automatically merged.'),
191 u'This pull request can be automatically merged.'),
192 MergeFailureReason.UNKNOWN: lazy_ugettext(
192 MergeFailureReason.UNKNOWN: lazy_ugettext(
193 u'This pull request cannot be merged because of an unhandled exception. '
193 u'This pull request cannot be merged because of an unhandled exception. '
194 u'{exception}'),
194 u'{exception}'),
195 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
195 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
196 u'This pull request cannot be merged because of merge conflicts. {unresolved_files}'),
196 u'This pull request cannot be merged because of merge conflicts. {unresolved_files}'),
197 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
197 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
198 u'This pull request could not be merged because push to '
198 u'This pull request could not be merged because push to '
199 u'target:`{target}@{merge_commit}` failed.'),
199 u'target:`{target}@{merge_commit}` failed.'),
200 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
200 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
201 u'This pull request cannot be merged because the target '
201 u'This pull request cannot be merged because the target '
202 u'`{target_ref.name}` is not a head.'),
202 u'`{target_ref.name}` is not a head.'),
203 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
203 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
204 u'This pull request cannot be merged because the source contains '
204 u'This pull request cannot be merged because the source contains '
205 u'more branches than the target.'),
205 u'more branches than the target.'),
206 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
206 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
207 u'This pull request cannot be merged because the target `{target_ref.name}` '
207 u'This pull request cannot be merged because the target `{target_ref.name}` '
208 u'has multiple heads: `{heads}`.'),
208 u'has multiple heads: `{heads}`.'),
209 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
209 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
210 u'This pull request cannot be merged because the target repository is '
210 u'This pull request cannot be merged because the target repository is '
211 u'locked by {locked_by}.'),
211 u'locked by {locked_by}.'),
212
212
213 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
213 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
214 u'This pull request cannot be merged because the target '
214 u'This pull request cannot be merged because the target '
215 u'reference `{target_ref.name}` is missing.'),
215 u'reference `{target_ref.name}` is missing.'),
216 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
216 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
217 u'This pull request cannot be merged because the source '
217 u'This pull request cannot be merged because the source '
218 u'reference `{source_ref.name}` is missing.'),
218 u'reference `{source_ref.name}` is missing.'),
219 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
219 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
220 u'This pull request cannot be merged because of conflicts related '
220 u'This pull request cannot be merged because of conflicts related '
221 u'to sub repositories.'),
221 u'to sub repositories.'),
222
222
223 # Deprecations
223 # Deprecations
224 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
224 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
225 u'This pull request cannot be merged because the target or the '
225 u'This pull request cannot be merged because the target or the '
226 u'source reference is missing.'),
226 u'source reference is missing.'),
227
227
228 }
228 }
229
229
230 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
230 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
231 self.possible = possible
231 self.possible = possible
232 self.executed = executed
232 self.executed = executed
233 self.merge_ref = merge_ref
233 self.merge_ref = merge_ref
234 self.failure_reason = failure_reason
234 self.failure_reason = failure_reason
235 self.metadata = metadata or {}
235 self.metadata = metadata or {}
236
236
237 def __repr__(self):
237 def __repr__(self):
238 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
238 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
239
239
240 def __eq__(self, other):
240 def __eq__(self, other):
241 same_instance = isinstance(other, self.__class__)
241 same_instance = isinstance(other, self.__class__)
242 return same_instance \
242 return same_instance \
243 and self.possible == other.possible \
243 and self.possible == other.possible \
244 and self.executed == other.executed \
244 and self.executed == other.executed \
245 and self.failure_reason == other.failure_reason
245 and self.failure_reason == other.failure_reason
246
246
247 @property
247 @property
248 def label(self):
248 def label(self):
249 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
249 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
250 not k.startswith('_'))
250 not k.startswith('_'))
251 return label_dict.get(self.failure_reason)
251 return label_dict.get(self.failure_reason)
252
252
253 @property
253 @property
254 def merge_status_message(self):
254 def merge_status_message(self):
255 """
255 """
256 Return a human friendly error message for the given merge status code.
256 Return a human friendly error message for the given merge status code.
257 """
257 """
258 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
258 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
259
259
260 try:
260 try:
261 return msg.format(**self.metadata)
261 return msg.format(**self.metadata)
262 except Exception:
262 except Exception:
263 log.exception('Failed to format %s message', self)
263 log.exception('Failed to format %s message', self)
264 return msg
264 return msg
265
265
266 def asdict(self):
266 def asdict(self):
267 data = {}
267 data = {}
268 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
268 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
269 'merge_status_message']:
269 'merge_status_message']:
270 data[k] = getattr(self, k)
270 data[k] = getattr(self, k)
271 return data
271 return data
272
272
273
273
274 class TargetRefMissing(ValueError):
274 class TargetRefMissing(ValueError):
275 pass
275 pass
276
276
277
277
278 class SourceRefMissing(ValueError):
278 class SourceRefMissing(ValueError):
279 pass
279 pass
280
280
281
281
282 class BaseRepository(object):
282 class BaseRepository(object):
283 """
283 """
284 Base Repository for final backends
284 Base Repository for final backends
285
285
286 .. attribute:: DEFAULT_BRANCH_NAME
286 .. attribute:: DEFAULT_BRANCH_NAME
287
287
288 name of default branch (i.e. "trunk" for svn, "master" for git etc.
288 name of default branch (i.e. "trunk" for svn, "master" for git etc.
289
289
290 .. attribute:: commit_ids
290 .. attribute:: commit_ids
291
291
292 list of all available commit ids, in ascending order
292 list of all available commit ids, in ascending order
293
293
294 .. attribute:: path
294 .. attribute:: path
295
295
296 absolute path to the repository
296 absolute path to the repository
297
297
298 .. attribute:: bookmarks
298 .. attribute:: bookmarks
299
299
300 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
300 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
301 there are no bookmarks or the backend implementation does not support
301 there are no bookmarks or the backend implementation does not support
302 bookmarks.
302 bookmarks.
303
303
304 .. attribute:: tags
304 .. attribute:: tags
305
305
306 Mapping from name to :term:`Commit ID` of the tag.
306 Mapping from name to :term:`Commit ID` of the tag.
307
307
308 """
308 """
309
309
310 DEFAULT_BRANCH_NAME = None
310 DEFAULT_BRANCH_NAME = None
311 DEFAULT_CONTACT = u"Unknown"
311 DEFAULT_CONTACT = u"Unknown"
312 DEFAULT_DESCRIPTION = u"unknown"
312 DEFAULT_DESCRIPTION = u"unknown"
313 EMPTY_COMMIT_ID = '0' * 40
313 EMPTY_COMMIT_ID = '0' * 40
314 COMMIT_ID_PAT = re.compile(r'[0-9a-fA-F]{40}')
314
315
315 path = None
316 path = None
316
317
317 _is_empty = None
318 _is_empty = None
318 _commit_ids = {}
319 _commit_ids = {}
319
320
320 def __init__(self, repo_path, config=None, create=False, **kwargs):
321 def __init__(self, repo_path, config=None, create=False, **kwargs):
321 """
322 """
322 Initializes repository. Raises RepositoryError if repository could
323 Initializes repository. Raises RepositoryError if repository could
323 not be find at the given ``repo_path`` or directory at ``repo_path``
324 not be find at the given ``repo_path`` or directory at ``repo_path``
324 exists and ``create`` is set to True.
325 exists and ``create`` is set to True.
325
326
326 :param repo_path: local path of the repository
327 :param repo_path: local path of the repository
327 :param config: repository configuration
328 :param config: repository configuration
328 :param create=False: if set to True, would try to create repository.
329 :param create=False: if set to True, would try to create repository.
329 :param src_url=None: if set, should be proper url from which repository
330 :param src_url=None: if set, should be proper url from which repository
330 would be cloned; requires ``create`` parameter to be set to True -
331 would be cloned; requires ``create`` parameter to be set to True -
331 raises RepositoryError if src_url is set and create evaluates to
332 raises RepositoryError if src_url is set and create evaluates to
332 False
333 False
333 """
334 """
334 raise NotImplementedError
335 raise NotImplementedError
335
336
336 def __repr__(self):
337 def __repr__(self):
337 return '<%s at %s>' % (self.__class__.__name__, self.path)
338 return '<%s at %s>' % (self.__class__.__name__, self.path)
338
339
339 def __len__(self):
340 def __len__(self):
340 return self.count()
341 return self.count()
341
342
342 def __eq__(self, other):
343 def __eq__(self, other):
343 same_instance = isinstance(other, self.__class__)
344 same_instance = isinstance(other, self.__class__)
344 return same_instance and other.path == self.path
345 return same_instance and other.path == self.path
345
346
346 def __ne__(self, other):
347 def __ne__(self, other):
347 return not self.__eq__(other)
348 return not self.__eq__(other)
348
349
349 def get_create_shadow_cache_pr_path(self, db_repo):
350 def get_create_shadow_cache_pr_path(self, db_repo):
350 path = db_repo.cached_diffs_dir
351 path = db_repo.cached_diffs_dir
351 if not os.path.exists(path):
352 if not os.path.exists(path):
352 os.makedirs(path, 0o755)
353 os.makedirs(path, 0o755)
353 return path
354 return path
354
355
355 @classmethod
356 @classmethod
356 def get_default_config(cls, default=None):
357 def get_default_config(cls, default=None):
357 config = Config()
358 config = Config()
358 if default and isinstance(default, list):
359 if default and isinstance(default, list):
359 for section, key, val in default:
360 for section, key, val in default:
360 config.set(section, key, val)
361 config.set(section, key, val)
361 return config
362 return config
362
363
363 @LazyProperty
364 @LazyProperty
364 def _remote(self):
365 def _remote(self):
365 raise NotImplementedError
366 raise NotImplementedError
366
367
367 def _heads(self, branch=None):
368 def _heads(self, branch=None):
368 return []
369 return []
369
370
370 @LazyProperty
371 @LazyProperty
371 def EMPTY_COMMIT(self):
372 def EMPTY_COMMIT(self):
372 return EmptyCommit(self.EMPTY_COMMIT_ID)
373 return EmptyCommit(self.EMPTY_COMMIT_ID)
373
374
374 @LazyProperty
375 @LazyProperty
375 def alias(self):
376 def alias(self):
376 for k, v in settings.BACKENDS.items():
377 for k, v in settings.BACKENDS.items():
377 if v.split('.')[-1] == str(self.__class__.__name__):
378 if v.split('.')[-1] == str(self.__class__.__name__):
378 return k
379 return k
379
380
380 @LazyProperty
381 @LazyProperty
381 def name(self):
382 def name(self):
382 return safe_unicode(os.path.basename(self.path))
383 return safe_unicode(os.path.basename(self.path))
383
384
384 @LazyProperty
385 @LazyProperty
385 def description(self):
386 def description(self):
386 raise NotImplementedError
387 raise NotImplementedError
387
388
388 def refs(self):
389 def refs(self):
389 """
390 """
390 returns a `dict` with branches, bookmarks, tags, and closed_branches
391 returns a `dict` with branches, bookmarks, tags, and closed_branches
391 for this repository
392 for this repository
392 """
393 """
393 return dict(
394 return dict(
394 branches=self.branches,
395 branches=self.branches,
395 branches_closed=self.branches_closed,
396 branches_closed=self.branches_closed,
396 tags=self.tags,
397 tags=self.tags,
397 bookmarks=self.bookmarks
398 bookmarks=self.bookmarks
398 )
399 )
399
400
400 @LazyProperty
401 @LazyProperty
401 def branches(self):
402 def branches(self):
402 """
403 """
403 A `dict` which maps branch names to commit ids.
404 A `dict` which maps branch names to commit ids.
404 """
405 """
405 raise NotImplementedError
406 raise NotImplementedError
406
407
407 @LazyProperty
408 @LazyProperty
408 def branches_closed(self):
409 def branches_closed(self):
409 """
410 """
410 A `dict` which maps tags names to commit ids.
411 A `dict` which maps tags names to commit ids.
411 """
412 """
412 raise NotImplementedError
413 raise NotImplementedError
413
414
414 @LazyProperty
415 @LazyProperty
415 def bookmarks(self):
416 def bookmarks(self):
416 """
417 """
417 A `dict` which maps tags names to commit ids.
418 A `dict` which maps tags names to commit ids.
418 """
419 """
419 raise NotImplementedError
420 raise NotImplementedError
420
421
421 @LazyProperty
422 @LazyProperty
422 def tags(self):
423 def tags(self):
423 """
424 """
424 A `dict` which maps tags names to commit ids.
425 A `dict` which maps tags names to commit ids.
425 """
426 """
426 raise NotImplementedError
427 raise NotImplementedError
427
428
428 @LazyProperty
429 @LazyProperty
429 def size(self):
430 def size(self):
430 """
431 """
431 Returns combined size in bytes for all repository files
432 Returns combined size in bytes for all repository files
432 """
433 """
433 tip = self.get_commit()
434 tip = self.get_commit()
434 return tip.size
435 return tip.size
435
436
436 def size_at_commit(self, commit_id):
437 def size_at_commit(self, commit_id):
437 commit = self.get_commit(commit_id)
438 commit = self.get_commit(commit_id)
438 return commit.size
439 return commit.size
439
440
440 def _check_for_empty(self):
441 def _check_for_empty(self):
441 no_commits = len(self._commit_ids) == 0
442 no_commits = len(self._commit_ids) == 0
442 if no_commits:
443 if no_commits:
443 # check on remote to be sure
444 # check on remote to be sure
444 return self._remote.is_empty()
445 return self._remote.is_empty()
445 else:
446 else:
446 return False
447 return False
447
448
448 def is_empty(self):
449 def is_empty(self):
449 if rhodecode.is_test:
450 if rhodecode.is_test:
450 return self._check_for_empty()
451 return self._check_for_empty()
451
452
452 if self._is_empty is None:
453 if self._is_empty is None:
453 # cache empty for production, but not tests
454 # cache empty for production, but not tests
454 self._is_empty = self._check_for_empty()
455 self._is_empty = self._check_for_empty()
455
456
456 return self._is_empty
457 return self._is_empty
457
458
458 @staticmethod
459 @staticmethod
459 def check_url(url, config):
460 def check_url(url, config):
460 """
461 """
461 Function will check given url and try to verify if it's a valid
462 Function will check given url and try to verify if it's a valid
462 link.
463 link.
463 """
464 """
464 raise NotImplementedError
465 raise NotImplementedError
465
466
466 @staticmethod
467 @staticmethod
467 def is_valid_repository(path):
468 def is_valid_repository(path):
468 """
469 """
469 Check if given `path` contains a valid repository of this backend
470 Check if given `path` contains a valid repository of this backend
470 """
471 """
471 raise NotImplementedError
472 raise NotImplementedError
472
473
473 # ==========================================================================
474 # ==========================================================================
474 # COMMITS
475 # COMMITS
475 # ==========================================================================
476 # ==========================================================================
476
477
477 @CachedProperty
478 @CachedProperty
478 def commit_ids(self):
479 def commit_ids(self):
479 raise NotImplementedError
480 raise NotImplementedError
480
481
481 def append_commit_id(self, commit_id):
482 def append_commit_id(self, commit_id):
482 if commit_id not in self.commit_ids:
483 if commit_id not in self.commit_ids:
483 self._rebuild_cache(self.commit_ids + [commit_id])
484 self._rebuild_cache(self.commit_ids + [commit_id])
484
485
485 # clear cache
486 # clear cache
486 self._invalidate_prop_cache('commit_ids')
487 self._invalidate_prop_cache('commit_ids')
487 self._is_empty = False
488 self._is_empty = False
488
489
489 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
490 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
490 translate_tag=None, maybe_unreachable=False, reference_obj=None):
491 translate_tag=None, maybe_unreachable=False, reference_obj=None):
491 """
492 """
492 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
493 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
493 are both None, most recent commit is returned.
494 are both None, most recent commit is returned.
494
495
495 :param pre_load: Optional. List of commit attributes to load.
496 :param pre_load: Optional. List of commit attributes to load.
496
497
497 :raises ``EmptyRepositoryError``: if there are no commits
498 :raises ``EmptyRepositoryError``: if there are no commits
498 """
499 """
499 raise NotImplementedError
500 raise NotImplementedError
500
501
501 def __iter__(self):
502 def __iter__(self):
502 for commit_id in self.commit_ids:
503 for commit_id in self.commit_ids:
503 yield self.get_commit(commit_id=commit_id)
504 yield self.get_commit(commit_id=commit_id)
504
505
505 def get_commits(
506 def get_commits(
506 self, start_id=None, end_id=None, start_date=None, end_date=None,
507 self, start_id=None, end_id=None, start_date=None, end_date=None,
507 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
508 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
508 """
509 """
509 Returns iterator of `BaseCommit` objects from start to end
510 Returns iterator of `BaseCommit` objects from start to end
510 not inclusive. This should behave just like a list, ie. end is not
511 not inclusive. This should behave just like a list, ie. end is not
511 inclusive.
512 inclusive.
512
513
513 :param start_id: None or str, must be a valid commit id
514 :param start_id: None or str, must be a valid commit id
514 :param end_id: None or str, must be a valid commit id
515 :param end_id: None or str, must be a valid commit id
515 :param start_date:
516 :param start_date:
516 :param end_date:
517 :param end_date:
517 :param branch_name:
518 :param branch_name:
518 :param show_hidden:
519 :param show_hidden:
519 :param pre_load:
520 :param pre_load:
520 :param translate_tags:
521 :param translate_tags:
521 """
522 """
522 raise NotImplementedError
523 raise NotImplementedError
523
524
524 def __getitem__(self, key):
525 def __getitem__(self, key):
525 """
526 """
526 Allows index based access to the commit objects of this repository.
527 Allows index based access to the commit objects of this repository.
527 """
528 """
528 pre_load = ["author", "branch", "date", "message", "parents"]
529 pre_load = ["author", "branch", "date", "message", "parents"]
529 if isinstance(key, slice):
530 if isinstance(key, slice):
530 return self._get_range(key, pre_load)
531 return self._get_range(key, pre_load)
531 return self.get_commit(commit_idx=key, pre_load=pre_load)
532 return self.get_commit(commit_idx=key, pre_load=pre_load)
532
533
533 def _get_range(self, slice_obj, pre_load):
534 def _get_range(self, slice_obj, pre_load):
534 for commit_id in self.commit_ids.__getitem__(slice_obj):
535 for commit_id in self.commit_ids.__getitem__(slice_obj):
535 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
536 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
536
537
537 def count(self):
538 def count(self):
538 return len(self.commit_ids)
539 return len(self.commit_ids)
539
540
540 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
541 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
541 """
542 """
542 Creates and returns a tag for the given ``commit_id``.
543 Creates and returns a tag for the given ``commit_id``.
543
544
544 :param name: name for new tag
545 :param name: name for new tag
545 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
546 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
546 :param commit_id: commit id for which new tag would be created
547 :param commit_id: commit id for which new tag would be created
547 :param message: message of the tag's commit
548 :param message: message of the tag's commit
548 :param date: date of tag's commit
549 :param date: date of tag's commit
549
550
550 :raises TagAlreadyExistError: if tag with same name already exists
551 :raises TagAlreadyExistError: if tag with same name already exists
551 """
552 """
552 raise NotImplementedError
553 raise NotImplementedError
553
554
554 def remove_tag(self, name, user, message=None, date=None):
555 def remove_tag(self, name, user, message=None, date=None):
555 """
556 """
556 Removes tag with the given ``name``.
557 Removes tag with the given ``name``.
557
558
558 :param name: name of the tag to be removed
559 :param name: name of the tag to be removed
559 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
560 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
560 :param message: message of the tag's removal commit
561 :param message: message of the tag's removal commit
561 :param date: date of tag's removal commit
562 :param date: date of tag's removal commit
562
563
563 :raises TagDoesNotExistError: if tag with given name does not exists
564 :raises TagDoesNotExistError: if tag with given name does not exists
564 """
565 """
565 raise NotImplementedError
566 raise NotImplementedError
566
567
567 def get_diff(
568 def get_diff(
568 self, commit1, commit2, path=None, ignore_whitespace=False,
569 self, commit1, commit2, path=None, ignore_whitespace=False,
569 context=3, path1=None):
570 context=3, path1=None):
570 """
571 """
571 Returns (git like) *diff*, as plain text. Shows changes introduced by
572 Returns (git like) *diff*, as plain text. Shows changes introduced by
572 `commit2` since `commit1`.
573 `commit2` since `commit1`.
573
574
574 :param commit1: Entry point from which diff is shown. Can be
575 :param commit1: Entry point from which diff is shown. Can be
575 ``self.EMPTY_COMMIT`` - in this case, patch showing all
576 ``self.EMPTY_COMMIT`` - in this case, patch showing all
576 the changes since empty state of the repository until `commit2`
577 the changes since empty state of the repository until `commit2`
577 :param commit2: Until which commit changes should be shown.
578 :param commit2: Until which commit changes should be shown.
578 :param path: Can be set to a path of a file to create a diff of that
579 :param path: Can be set to a path of a file to create a diff of that
579 file. If `path1` is also set, this value is only associated to
580 file. If `path1` is also set, this value is only associated to
580 `commit2`.
581 `commit2`.
581 :param ignore_whitespace: If set to ``True``, would not show whitespace
582 :param ignore_whitespace: If set to ``True``, would not show whitespace
582 changes. Defaults to ``False``.
583 changes. Defaults to ``False``.
583 :param context: How many lines before/after changed lines should be
584 :param context: How many lines before/after changed lines should be
584 shown. Defaults to ``3``.
585 shown. Defaults to ``3``.
585 :param path1: Can be set to a path to associate with `commit1`. This
586 :param path1: Can be set to a path to associate with `commit1`. This
586 parameter works only for backends which support diff generation for
587 parameter works only for backends which support diff generation for
587 different paths. Other backends will raise a `ValueError` if `path1`
588 different paths. Other backends will raise a `ValueError` if `path1`
588 is set and has a different value than `path`.
589 is set and has a different value than `path`.
589 :param file_path: filter this diff by given path pattern
590 :param file_path: filter this diff by given path pattern
590 """
591 """
591 raise NotImplementedError
592 raise NotImplementedError
592
593
593 def strip(self, commit_id, branch=None):
594 def strip(self, commit_id, branch=None):
594 """
595 """
595 Strip given commit_id from the repository
596 Strip given commit_id from the repository
596 """
597 """
597 raise NotImplementedError
598 raise NotImplementedError
598
599
599 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
600 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
600 """
601 """
601 Return a latest common ancestor commit if one exists for this repo
602 Return a latest common ancestor commit if one exists for this repo
602 `commit_id1` vs `commit_id2` from `repo2`.
603 `commit_id1` vs `commit_id2` from `repo2`.
603
604
604 :param commit_id1: Commit it from this repository to use as a
605 :param commit_id1: Commit it from this repository to use as a
605 target for the comparison.
606 target for the comparison.
606 :param commit_id2: Source commit id to use for comparison.
607 :param commit_id2: Source commit id to use for comparison.
607 :param repo2: Source repository to use for comparison.
608 :param repo2: Source repository to use for comparison.
608 """
609 """
609 raise NotImplementedError
610 raise NotImplementedError
610
611
611 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
612 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
612 """
613 """
613 Compare this repository's revision `commit_id1` with `commit_id2`.
614 Compare this repository's revision `commit_id1` with `commit_id2`.
614
615
615 Returns a tuple(commits, ancestor) that would be merged from
616 Returns a tuple(commits, ancestor) that would be merged from
616 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
617 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
617 will be returned as ancestor.
618 will be returned as ancestor.
618
619
619 :param commit_id1: Commit it from this repository to use as a
620 :param commit_id1: Commit it from this repository to use as a
620 target for the comparison.
621 target for the comparison.
621 :param commit_id2: Source commit id to use for comparison.
622 :param commit_id2: Source commit id to use for comparison.
622 :param repo2: Source repository to use for comparison.
623 :param repo2: Source repository to use for comparison.
623 :param merge: If set to ``True`` will do a merge compare which also
624 :param merge: If set to ``True`` will do a merge compare which also
624 returns the common ancestor.
625 returns the common ancestor.
625 :param pre_load: Optional. List of commit attributes to load.
626 :param pre_load: Optional. List of commit attributes to load.
626 """
627 """
627 raise NotImplementedError
628 raise NotImplementedError
628
629
629 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
630 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
630 user_name='', user_email='', message='', dry_run=False,
631 user_name='', user_email='', message='', dry_run=False,
631 use_rebase=False, close_branch=False):
632 use_rebase=False, close_branch=False):
632 """
633 """
633 Merge the revisions specified in `source_ref` from `source_repo`
634 Merge the revisions specified in `source_ref` from `source_repo`
634 onto the `target_ref` of this repository.
635 onto the `target_ref` of this repository.
635
636
636 `source_ref` and `target_ref` are named tupls with the following
637 `source_ref` and `target_ref` are named tupls with the following
637 fields `type`, `name` and `commit_id`.
638 fields `type`, `name` and `commit_id`.
638
639
639 Returns a MergeResponse named tuple with the following fields
640 Returns a MergeResponse named tuple with the following fields
640 'possible', 'executed', 'source_commit', 'target_commit',
641 'possible', 'executed', 'source_commit', 'target_commit',
641 'merge_commit'.
642 'merge_commit'.
642
643
643 :param repo_id: `repo_id` target repo id.
644 :param repo_id: `repo_id` target repo id.
644 :param workspace_id: `workspace_id` unique identifier.
645 :param workspace_id: `workspace_id` unique identifier.
645 :param target_ref: `target_ref` points to the commit on top of which
646 :param target_ref: `target_ref` points to the commit on top of which
646 the `source_ref` should be merged.
647 the `source_ref` should be merged.
647 :param source_repo: The repository that contains the commits to be
648 :param source_repo: The repository that contains the commits to be
648 merged.
649 merged.
649 :param source_ref: `source_ref` points to the topmost commit from
650 :param source_ref: `source_ref` points to the topmost commit from
650 the `source_repo` which should be merged.
651 the `source_repo` which should be merged.
651 :param user_name: Merge commit `user_name`.
652 :param user_name: Merge commit `user_name`.
652 :param user_email: Merge commit `user_email`.
653 :param user_email: Merge commit `user_email`.
653 :param message: Merge commit `message`.
654 :param message: Merge commit `message`.
654 :param dry_run: If `True` the merge will not take place.
655 :param dry_run: If `True` the merge will not take place.
655 :param use_rebase: If `True` commits from the source will be rebased
656 :param use_rebase: If `True` commits from the source will be rebased
656 on top of the target instead of being merged.
657 on top of the target instead of being merged.
657 :param close_branch: If `True` branch will be close before merging it
658 :param close_branch: If `True` branch will be close before merging it
658 """
659 """
659 if dry_run:
660 if dry_run:
660 message = message or settings.MERGE_DRY_RUN_MESSAGE
661 message = message or settings.MERGE_DRY_RUN_MESSAGE
661 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
662 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
662 user_name = user_name or settings.MERGE_DRY_RUN_USER
663 user_name = user_name or settings.MERGE_DRY_RUN_USER
663 else:
664 else:
664 if not user_name:
665 if not user_name:
665 raise ValueError('user_name cannot be empty')
666 raise ValueError('user_name cannot be empty')
666 if not user_email:
667 if not user_email:
667 raise ValueError('user_email cannot be empty')
668 raise ValueError('user_email cannot be empty')
668 if not message:
669 if not message:
669 raise ValueError('message cannot be empty')
670 raise ValueError('message cannot be empty')
670
671
671 try:
672 try:
672 return self._merge_repo(
673 return self._merge_repo(
673 repo_id, workspace_id, target_ref, source_repo,
674 repo_id, workspace_id, target_ref, source_repo,
674 source_ref, message, user_name, user_email, dry_run=dry_run,
675 source_ref, message, user_name, user_email, dry_run=dry_run,
675 use_rebase=use_rebase, close_branch=close_branch)
676 use_rebase=use_rebase, close_branch=close_branch)
676 except RepositoryError as exc:
677 except RepositoryError as exc:
677 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
678 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
678 return MergeResponse(
679 return MergeResponse(
679 False, False, None, MergeFailureReason.UNKNOWN,
680 False, False, None, MergeFailureReason.UNKNOWN,
680 metadata={'exception': str(exc)})
681 metadata={'exception': str(exc)})
681
682
682 def _merge_repo(self, repo_id, workspace_id, target_ref,
683 def _merge_repo(self, repo_id, workspace_id, target_ref,
683 source_repo, source_ref, merge_message,
684 source_repo, source_ref, merge_message,
684 merger_name, merger_email, dry_run=False,
685 merger_name, merger_email, dry_run=False,
685 use_rebase=False, close_branch=False):
686 use_rebase=False, close_branch=False):
686 """Internal implementation of merge."""
687 """Internal implementation of merge."""
687 raise NotImplementedError
688 raise NotImplementedError
688
689
689 def _maybe_prepare_merge_workspace(
690 def _maybe_prepare_merge_workspace(
690 self, repo_id, workspace_id, target_ref, source_ref):
691 self, repo_id, workspace_id, target_ref, source_ref):
691 """
692 """
692 Create the merge workspace.
693 Create the merge workspace.
693
694
694 :param workspace_id: `workspace_id` unique identifier.
695 :param workspace_id: `workspace_id` unique identifier.
695 """
696 """
696 raise NotImplementedError
697 raise NotImplementedError
697
698
698 @classmethod
699 @classmethod
699 def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id):
700 def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id):
700 """
701 """
701 Legacy version that was used before. We still need it for
702 Legacy version that was used before. We still need it for
702 backward compat
703 backward compat
703 """
704 """
704 return os.path.join(
705 return os.path.join(
705 os.path.dirname(repo_path),
706 os.path.dirname(repo_path),
706 '.__shadow_%s_%s' % (os.path.basename(repo_path), workspace_id))
707 '.__shadow_%s_%s' % (os.path.basename(repo_path), workspace_id))
707
708
708 @classmethod
709 @classmethod
709 def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id):
710 def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id):
710 # The name of the shadow repository must start with '.', so it is
711 # The name of the shadow repository must start with '.', so it is
711 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
712 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
712 legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id)
713 legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id)
713 if os.path.exists(legacy_repository_path):
714 if os.path.exists(legacy_repository_path):
714 return legacy_repository_path
715 return legacy_repository_path
715 else:
716 else:
716 return os.path.join(
717 return os.path.join(
717 os.path.dirname(repo_path),
718 os.path.dirname(repo_path),
718 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
719 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
719
720
720 def cleanup_merge_workspace(self, repo_id, workspace_id):
721 def cleanup_merge_workspace(self, repo_id, workspace_id):
721 """
722 """
722 Remove merge workspace.
723 Remove merge workspace.
723
724
724 This function MUST not fail in case there is no workspace associated to
725 This function MUST not fail in case there is no workspace associated to
725 the given `workspace_id`.
726 the given `workspace_id`.
726
727
727 :param workspace_id: `workspace_id` unique identifier.
728 :param workspace_id: `workspace_id` unique identifier.
728 """
729 """
729 shadow_repository_path = self._get_shadow_repository_path(
730 shadow_repository_path = self._get_shadow_repository_path(
730 self.path, repo_id, workspace_id)
731 self.path, repo_id, workspace_id)
731 shadow_repository_path_del = '{}.{}.delete'.format(
732 shadow_repository_path_del = '{}.{}.delete'.format(
732 shadow_repository_path, time.time())
733 shadow_repository_path, time.time())
733
734
734 # move the shadow repo, so it never conflicts with the one used.
735 # move the shadow repo, so it never conflicts with the one used.
735 # we use this method because shutil.rmtree had some edge case problems
736 # we use this method because shutil.rmtree had some edge case problems
736 # removing symlinked repositories
737 # removing symlinked repositories
737 if not os.path.isdir(shadow_repository_path):
738 if not os.path.isdir(shadow_repository_path):
738 return
739 return
739
740
740 shutil.move(shadow_repository_path, shadow_repository_path_del)
741 shutil.move(shadow_repository_path, shadow_repository_path_del)
741 try:
742 try:
742 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
743 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
743 except Exception:
744 except Exception:
744 log.exception('Failed to gracefully remove shadow repo under %s',
745 log.exception('Failed to gracefully remove shadow repo under %s',
745 shadow_repository_path_del)
746 shadow_repository_path_del)
746 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
747 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
747
748
748 # ========== #
749 # ========== #
749 # COMMIT API #
750 # COMMIT API #
750 # ========== #
751 # ========== #
751
752
752 @LazyProperty
753 @LazyProperty
753 def in_memory_commit(self):
754 def in_memory_commit(self):
754 """
755 """
755 Returns :class:`InMemoryCommit` object for this repository.
756 Returns :class:`InMemoryCommit` object for this repository.
756 """
757 """
757 raise NotImplementedError
758 raise NotImplementedError
758
759
759 # ======================== #
760 # ======================== #
760 # UTILITIES FOR SUBCLASSES #
761 # UTILITIES FOR SUBCLASSES #
761 # ======================== #
762 # ======================== #
762
763
763 def _validate_diff_commits(self, commit1, commit2):
764 def _validate_diff_commits(self, commit1, commit2):
764 """
765 """
765 Validates that the given commits are related to this repository.
766 Validates that the given commits are related to this repository.
766
767
767 Intended as a utility for sub classes to have a consistent validation
768 Intended as a utility for sub classes to have a consistent validation
768 of input parameters in methods like :meth:`get_diff`.
769 of input parameters in methods like :meth:`get_diff`.
769 """
770 """
770 self._validate_commit(commit1)
771 self._validate_commit(commit1)
771 self._validate_commit(commit2)
772 self._validate_commit(commit2)
772 if (isinstance(commit1, EmptyCommit) and
773 if (isinstance(commit1, EmptyCommit) and
773 isinstance(commit2, EmptyCommit)):
774 isinstance(commit2, EmptyCommit)):
774 raise ValueError("Cannot compare two empty commits")
775 raise ValueError("Cannot compare two empty commits")
775
776
776 def _validate_commit(self, commit):
777 def _validate_commit(self, commit):
777 if not isinstance(commit, BaseCommit):
778 if not isinstance(commit, BaseCommit):
778 raise TypeError(
779 raise TypeError(
779 "%s is not of type BaseCommit" % repr(commit))
780 "%s is not of type BaseCommit" % repr(commit))
780 if commit.repository != self and not isinstance(commit, EmptyCommit):
781 if commit.repository != self and not isinstance(commit, EmptyCommit):
781 raise ValueError(
782 raise ValueError(
782 "Commit %s must be a valid commit from this repository %s, "
783 "Commit %s must be a valid commit from this repository %s, "
783 "related to this repository instead %s." %
784 "related to this repository instead %s." %
784 (commit, self, commit.repository))
785 (commit, self, commit.repository))
785
786
786 def _validate_commit_id(self, commit_id):
787 def _validate_commit_id(self, commit_id):
787 if not isinstance(commit_id, compat.string_types):
788 if not isinstance(commit_id, compat.string_types):
788 raise TypeError("commit_id must be a string value got {} instead".format(type(commit_id)))
789 raise TypeError("commit_id must be a string value got {} instead".format(type(commit_id)))
789
790
790 def _validate_commit_idx(self, commit_idx):
791 def _validate_commit_idx(self, commit_idx):
791 if not isinstance(commit_idx, (int, long)):
792 if not isinstance(commit_idx, (int, long)):
792 raise TypeError("commit_idx must be a numeric value")
793 raise TypeError("commit_idx must be a numeric value")
793
794
794 def _validate_branch_name(self, branch_name):
795 def _validate_branch_name(self, branch_name):
795 if branch_name and branch_name not in self.branches_all:
796 if branch_name and branch_name not in self.branches_all:
796 msg = ("Branch %s not found in %s" % (branch_name, self))
797 msg = ("Branch %s not found in %s" % (branch_name, self))
797 raise BranchDoesNotExistError(msg)
798 raise BranchDoesNotExistError(msg)
798
799
799 #
800 #
800 # Supporting deprecated API parts
801 # Supporting deprecated API parts
801 # TODO: johbo: consider to move this into a mixin
802 # TODO: johbo: consider to move this into a mixin
802 #
803 #
803
804
804 @property
805 @property
805 def EMPTY_CHANGESET(self):
806 def EMPTY_CHANGESET(self):
806 warnings.warn(
807 warnings.warn(
807 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
808 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
808 return self.EMPTY_COMMIT_ID
809 return self.EMPTY_COMMIT_ID
809
810
810 @property
811 @property
811 def revisions(self):
812 def revisions(self):
812 warnings.warn("Use commits attribute instead", DeprecationWarning)
813 warnings.warn("Use commits attribute instead", DeprecationWarning)
813 return self.commit_ids
814 return self.commit_ids
814
815
815 @revisions.setter
816 @revisions.setter
816 def revisions(self, value):
817 def revisions(self, value):
817 warnings.warn("Use commits attribute instead", DeprecationWarning)
818 warnings.warn("Use commits attribute instead", DeprecationWarning)
818 self.commit_ids = value
819 self.commit_ids = value
819
820
820 def get_changeset(self, revision=None, pre_load=None):
821 def get_changeset(self, revision=None, pre_load=None):
821 warnings.warn("Use get_commit instead", DeprecationWarning)
822 warnings.warn("Use get_commit instead", DeprecationWarning)
822 commit_id = None
823 commit_id = None
823 commit_idx = None
824 commit_idx = None
824 if isinstance(revision, compat.string_types):
825 if isinstance(revision, compat.string_types):
825 commit_id = revision
826 commit_id = revision
826 else:
827 else:
827 commit_idx = revision
828 commit_idx = revision
828 return self.get_commit(
829 return self.get_commit(
829 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
830 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
830
831
831 def get_changesets(
832 def get_changesets(
832 self, start=None, end=None, start_date=None, end_date=None,
833 self, start=None, end=None, start_date=None, end_date=None,
833 branch_name=None, pre_load=None):
834 branch_name=None, pre_load=None):
834 warnings.warn("Use get_commits instead", DeprecationWarning)
835 warnings.warn("Use get_commits instead", DeprecationWarning)
835 start_id = self._revision_to_commit(start)
836 start_id = self._revision_to_commit(start)
836 end_id = self._revision_to_commit(end)
837 end_id = self._revision_to_commit(end)
837 return self.get_commits(
838 return self.get_commits(
838 start_id=start_id, end_id=end_id, start_date=start_date,
839 start_id=start_id, end_id=end_id, start_date=start_date,
839 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
840 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
840
841
841 def _revision_to_commit(self, revision):
842 def _revision_to_commit(self, revision):
842 """
843 """
843 Translates a revision to a commit_id
844 Translates a revision to a commit_id
844
845
845 Helps to support the old changeset based API which allows to use
846 Helps to support the old changeset based API which allows to use
846 commit ids and commit indices interchangeable.
847 commit ids and commit indices interchangeable.
847 """
848 """
848 if revision is None:
849 if revision is None:
849 return revision
850 return revision
850
851
851 if isinstance(revision, compat.string_types):
852 if isinstance(revision, compat.string_types):
852 commit_id = revision
853 commit_id = revision
853 else:
854 else:
854 commit_id = self.commit_ids[revision]
855 commit_id = self.commit_ids[revision]
855 return commit_id
856 return commit_id
856
857
857 @property
858 @property
858 def in_memory_changeset(self):
859 def in_memory_changeset(self):
859 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
860 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
860 return self.in_memory_commit
861 return self.in_memory_commit
861
862
862 def get_path_permissions(self, username):
863 def get_path_permissions(self, username):
863 """
864 """
864 Returns a path permission checker or None if not supported
865 Returns a path permission checker or None if not supported
865
866
866 :param username: session user name
867 :param username: session user name
867 :return: an instance of BasePathPermissionChecker or None
868 :return: an instance of BasePathPermissionChecker or None
868 """
869 """
869 return None
870 return None
870
871
871 def install_hooks(self, force=False):
872 def install_hooks(self, force=False):
872 return self._remote.install_hooks(force)
873 return self._remote.install_hooks(force)
873
874
874 def get_hooks_info(self):
875 def get_hooks_info(self):
875 return self._remote.get_hooks_info()
876 return self._remote.get_hooks_info()
876
877
877
878
878 class BaseCommit(object):
879 class BaseCommit(object):
879 """
880 """
880 Each backend should implement it's commit representation.
881 Each backend should implement it's commit representation.
881
882
882 **Attributes**
883 **Attributes**
883
884
884 ``repository``
885 ``repository``
885 repository object within which commit exists
886 repository object within which commit exists
886
887
887 ``id``
888 ``id``
888 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
889 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
889 just ``tip``.
890 just ``tip``.
890
891
891 ``raw_id``
892 ``raw_id``
892 raw commit representation (i.e. full 40 length sha for git
893 raw commit representation (i.e. full 40 length sha for git
893 backend)
894 backend)
894
895
895 ``short_id``
896 ``short_id``
896 shortened (if apply) version of ``raw_id``; it would be simple
897 shortened (if apply) version of ``raw_id``; it would be simple
897 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
898 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
898 as ``raw_id`` for subversion
899 as ``raw_id`` for subversion
899
900
900 ``idx``
901 ``idx``
901 commit index
902 commit index
902
903
903 ``files``
904 ``files``
904 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
905 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
905
906
906 ``dirs``
907 ``dirs``
907 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
908 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
908
909
909 ``nodes``
910 ``nodes``
910 combined list of ``Node`` objects
911 combined list of ``Node`` objects
911
912
912 ``author``
913 ``author``
913 author of the commit, as unicode
914 author of the commit, as unicode
914
915
915 ``message``
916 ``message``
916 message of the commit, as unicode
917 message of the commit, as unicode
917
918
918 ``parents``
919 ``parents``
919 list of parent commits
920 list of parent commits
920
921
921 """
922 """
922 repository = None
923 repository = None
923 branch = None
924 branch = None
924
925
925 """
926 """
926 Depending on the backend this should be set to the branch name of the
927 Depending on the backend this should be set to the branch name of the
927 commit. Backends not supporting branches on commits should leave this
928 commit. Backends not supporting branches on commits should leave this
928 value as ``None``.
929 value as ``None``.
929 """
930 """
930
931
931 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
932 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
932 """
933 """
933 This template is used to generate a default prefix for repository archives
934 This template is used to generate a default prefix for repository archives
934 if no prefix has been specified.
935 if no prefix has been specified.
935 """
936 """
936
937
937 def __str__(self):
938 def __str__(self):
938 return '<%s at %s:%s>' % (
939 return '<%s at %s:%s>' % (
939 self.__class__.__name__, self.idx, self.short_id)
940 self.__class__.__name__, self.idx, self.short_id)
940
941
941 def __repr__(self):
942 def __repr__(self):
942 return self.__str__()
943 return self.__str__()
943
944
944 def __unicode__(self):
945 def __unicode__(self):
945 return u'%s:%s' % (self.idx, self.short_id)
946 return u'%s:%s' % (self.idx, self.short_id)
946
947
947 def __eq__(self, other):
948 def __eq__(self, other):
948 same_instance = isinstance(other, self.__class__)
949 same_instance = isinstance(other, self.__class__)
949 return same_instance and self.raw_id == other.raw_id
950 return same_instance and self.raw_id == other.raw_id
950
951
951 def __json__(self):
952 def __json__(self):
952 parents = []
953 parents = []
953 try:
954 try:
954 for parent in self.parents:
955 for parent in self.parents:
955 parents.append({'raw_id': parent.raw_id})
956 parents.append({'raw_id': parent.raw_id})
956 except NotImplementedError:
957 except NotImplementedError:
957 # empty commit doesn't have parents implemented
958 # empty commit doesn't have parents implemented
958 pass
959 pass
959
960
960 return {
961 return {
961 'short_id': self.short_id,
962 'short_id': self.short_id,
962 'raw_id': self.raw_id,
963 'raw_id': self.raw_id,
963 'revision': self.idx,
964 'revision': self.idx,
964 'message': self.message,
965 'message': self.message,
965 'date': self.date,
966 'date': self.date,
966 'author': self.author,
967 'author': self.author,
967 'parents': parents,
968 'parents': parents,
968 'branch': self.branch
969 'branch': self.branch
969 }
970 }
970
971
971 def __getstate__(self):
972 def __getstate__(self):
972 d = self.__dict__.copy()
973 d = self.__dict__.copy()
973 d.pop('_remote', None)
974 d.pop('_remote', None)
974 d.pop('repository', None)
975 d.pop('repository', None)
975 return d
976 return d
976
977
977 def serialize(self):
978 def serialize(self):
978 return self.__json__()
979 return self.__json__()
979
980
980 def _get_refs(self):
981 def _get_refs(self):
981 return {
982 return {
982 'branches': [self.branch] if self.branch else [],
983 'branches': [self.branch] if self.branch else [],
983 'bookmarks': getattr(self, 'bookmarks', []),
984 'bookmarks': getattr(self, 'bookmarks', []),
984 'tags': self.tags
985 'tags': self.tags
985 }
986 }
986
987
987 @LazyProperty
988 @LazyProperty
988 def last(self):
989 def last(self):
989 """
990 """
990 ``True`` if this is last commit in repository, ``False``
991 ``True`` if this is last commit in repository, ``False``
991 otherwise; trying to access this attribute while there is no
992 otherwise; trying to access this attribute while there is no
992 commits would raise `EmptyRepositoryError`
993 commits would raise `EmptyRepositoryError`
993 """
994 """
994 if self.repository is None:
995 if self.repository is None:
995 raise CommitError("Cannot check if it's most recent commit")
996 raise CommitError("Cannot check if it's most recent commit")
996 return self.raw_id == self.repository.commit_ids[-1]
997 return self.raw_id == self.repository.commit_ids[-1]
997
998
998 @LazyProperty
999 @LazyProperty
999 def parents(self):
1000 def parents(self):
1000 """
1001 """
1001 Returns list of parent commits.
1002 Returns list of parent commits.
1002 """
1003 """
1003 raise NotImplementedError
1004 raise NotImplementedError
1004
1005
1005 @LazyProperty
1006 @LazyProperty
1006 def first_parent(self):
1007 def first_parent(self):
1007 """
1008 """
1008 Returns list of parent commits.
1009 Returns list of parent commits.
1009 """
1010 """
1010 return self.parents[0] if self.parents else EmptyCommit()
1011 return self.parents[0] if self.parents else EmptyCommit()
1011
1012
1012 @property
1013 @property
1013 def merge(self):
1014 def merge(self):
1014 """
1015 """
1015 Returns boolean if commit is a merge.
1016 Returns boolean if commit is a merge.
1016 """
1017 """
1017 return len(self.parents) > 1
1018 return len(self.parents) > 1
1018
1019
1019 @LazyProperty
1020 @LazyProperty
1020 def children(self):
1021 def children(self):
1021 """
1022 """
1022 Returns list of child commits.
1023 Returns list of child commits.
1023 """
1024 """
1024 raise NotImplementedError
1025 raise NotImplementedError
1025
1026
1026 @LazyProperty
1027 @LazyProperty
1027 def id(self):
1028 def id(self):
1028 """
1029 """
1029 Returns string identifying this commit.
1030 Returns string identifying this commit.
1030 """
1031 """
1031 raise NotImplementedError
1032 raise NotImplementedError
1032
1033
1033 @LazyProperty
1034 @LazyProperty
1034 def raw_id(self):
1035 def raw_id(self):
1035 """
1036 """
1036 Returns raw string identifying this commit.
1037 Returns raw string identifying this commit.
1037 """
1038 """
1038 raise NotImplementedError
1039 raise NotImplementedError
1039
1040
1040 @LazyProperty
1041 @LazyProperty
1041 def short_id(self):
1042 def short_id(self):
1042 """
1043 """
1043 Returns shortened version of ``raw_id`` attribute, as string,
1044 Returns shortened version of ``raw_id`` attribute, as string,
1044 identifying this commit, useful for presentation to users.
1045 identifying this commit, useful for presentation to users.
1045 """
1046 """
1046 raise NotImplementedError
1047 raise NotImplementedError
1047
1048
1048 @LazyProperty
1049 @LazyProperty
1049 def idx(self):
1050 def idx(self):
1050 """
1051 """
1051 Returns integer identifying this commit.
1052 Returns integer identifying this commit.
1052 """
1053 """
1053 raise NotImplementedError
1054 raise NotImplementedError
1054
1055
1055 @LazyProperty
1056 @LazyProperty
1056 def committer(self):
1057 def committer(self):
1057 """
1058 """
1058 Returns committer for this commit
1059 Returns committer for this commit
1059 """
1060 """
1060 raise NotImplementedError
1061 raise NotImplementedError
1061
1062
1062 @LazyProperty
1063 @LazyProperty
1063 def committer_name(self):
1064 def committer_name(self):
1064 """
1065 """
1065 Returns committer name for this commit
1066 Returns committer name for this commit
1066 """
1067 """
1067
1068
1068 return author_name(self.committer)
1069 return author_name(self.committer)
1069
1070
1070 @LazyProperty
1071 @LazyProperty
1071 def committer_email(self):
1072 def committer_email(self):
1072 """
1073 """
1073 Returns committer email address for this commit
1074 Returns committer email address for this commit
1074 """
1075 """
1075
1076
1076 return author_email(self.committer)
1077 return author_email(self.committer)
1077
1078
1078 @LazyProperty
1079 @LazyProperty
1079 def author(self):
1080 def author(self):
1080 """
1081 """
1081 Returns author for this commit
1082 Returns author for this commit
1082 """
1083 """
1083
1084
1084 raise NotImplementedError
1085 raise NotImplementedError
1085
1086
1086 @LazyProperty
1087 @LazyProperty
1087 def author_name(self):
1088 def author_name(self):
1088 """
1089 """
1089 Returns author name for this commit
1090 Returns author name for this commit
1090 """
1091 """
1091
1092
1092 return author_name(self.author)
1093 return author_name(self.author)
1093
1094
1094 @LazyProperty
1095 @LazyProperty
1095 def author_email(self):
1096 def author_email(self):
1096 """
1097 """
1097 Returns author email address for this commit
1098 Returns author email address for this commit
1098 """
1099 """
1099
1100
1100 return author_email(self.author)
1101 return author_email(self.author)
1101
1102
1102 def get_file_mode(self, path):
1103 def get_file_mode(self, path):
1103 """
1104 """
1104 Returns stat mode of the file at `path`.
1105 Returns stat mode of the file at `path`.
1105 """
1106 """
1106 raise NotImplementedError
1107 raise NotImplementedError
1107
1108
1108 def is_link(self, path):
1109 def is_link(self, path):
1109 """
1110 """
1110 Returns ``True`` if given `path` is a symlink
1111 Returns ``True`` if given `path` is a symlink
1111 """
1112 """
1112 raise NotImplementedError
1113 raise NotImplementedError
1113
1114
1114 def is_node_binary(self, path):
1115 def is_node_binary(self, path):
1115 """
1116 """
1116 Returns ``True`` is given path is a binary file
1117 Returns ``True`` is given path is a binary file
1117 """
1118 """
1118 raise NotImplementedError
1119 raise NotImplementedError
1119
1120
1120 def get_file_content(self, path):
1121 def get_file_content(self, path):
1121 """
1122 """
1122 Returns content of the file at the given `path`.
1123 Returns content of the file at the given `path`.
1123 """
1124 """
1124 raise NotImplementedError
1125 raise NotImplementedError
1125
1126
1126 def get_file_content_streamed(self, path):
1127 def get_file_content_streamed(self, path):
1127 """
1128 """
1128 returns a streaming response from vcsserver with file content
1129 returns a streaming response from vcsserver with file content
1129 """
1130 """
1130 raise NotImplementedError
1131 raise NotImplementedError
1131
1132
1132 def get_file_size(self, path):
1133 def get_file_size(self, path):
1133 """
1134 """
1134 Returns size of the file at the given `path`.
1135 Returns size of the file at the given `path`.
1135 """
1136 """
1136 raise NotImplementedError
1137 raise NotImplementedError
1137
1138
1138 def get_path_commit(self, path, pre_load=None):
1139 def get_path_commit(self, path, pre_load=None):
1139 """
1140 """
1140 Returns last commit of the file at the given `path`.
1141 Returns last commit of the file at the given `path`.
1141
1142
1142 :param pre_load: Optional. List of commit attributes to load.
1143 :param pre_load: Optional. List of commit attributes to load.
1143 """
1144 """
1144 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1145 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1145 if not commits:
1146 if not commits:
1146 raise RepositoryError(
1147 raise RepositoryError(
1147 'Failed to fetch history for path {}. '
1148 'Failed to fetch history for path {}. '
1148 'Please check if such path exists in your repository'.format(
1149 'Please check if such path exists in your repository'.format(
1149 path))
1150 path))
1150 return commits[0]
1151 return commits[0]
1151
1152
1152 def get_path_history(self, path, limit=None, pre_load=None):
1153 def get_path_history(self, path, limit=None, pre_load=None):
1153 """
1154 """
1154 Returns history of file as reversed list of :class:`BaseCommit`
1155 Returns history of file as reversed list of :class:`BaseCommit`
1155 objects for which file at given `path` has been modified.
1156 objects for which file at given `path` has been modified.
1156
1157
1157 :param limit: Optional. Allows to limit the size of the returned
1158 :param limit: Optional. Allows to limit the size of the returned
1158 history. This is intended as a hint to the underlying backend, so
1159 history. This is intended as a hint to the underlying backend, so
1159 that it can apply optimizations depending on the limit.
1160 that it can apply optimizations depending on the limit.
1160 :param pre_load: Optional. List of commit attributes to load.
1161 :param pre_load: Optional. List of commit attributes to load.
1161 """
1162 """
1162 raise NotImplementedError
1163 raise NotImplementedError
1163
1164
1164 def get_file_annotate(self, path, pre_load=None):
1165 def get_file_annotate(self, path, pre_load=None):
1165 """
1166 """
1166 Returns a generator of four element tuples with
1167 Returns a generator of four element tuples with
1167 lineno, sha, commit lazy loader and line
1168 lineno, sha, commit lazy loader and line
1168
1169
1169 :param pre_load: Optional. List of commit attributes to load.
1170 :param pre_load: Optional. List of commit attributes to load.
1170 """
1171 """
1171 raise NotImplementedError
1172 raise NotImplementedError
1172
1173
1173 def get_nodes(self, path):
1174 def get_nodes(self, path):
1174 """
1175 """
1175 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1176 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1176 state of commit at the given ``path``.
1177 state of commit at the given ``path``.
1177
1178
1178 :raises ``CommitError``: if node at the given ``path`` is not
1179 :raises ``CommitError``: if node at the given ``path`` is not
1179 instance of ``DirNode``
1180 instance of ``DirNode``
1180 """
1181 """
1181 raise NotImplementedError
1182 raise NotImplementedError
1182
1183
1183 def get_node(self, path):
1184 def get_node(self, path):
1184 """
1185 """
1185 Returns ``Node`` object from the given ``path``.
1186 Returns ``Node`` object from the given ``path``.
1186
1187
1187 :raises ``NodeDoesNotExistError``: if there is no node at the given
1188 :raises ``NodeDoesNotExistError``: if there is no node at the given
1188 ``path``
1189 ``path``
1189 """
1190 """
1190 raise NotImplementedError
1191 raise NotImplementedError
1191
1192
1192 def get_largefile_node(self, path):
1193 def get_largefile_node(self, path):
1193 """
1194 """
1194 Returns the path to largefile from Mercurial/Git-lfs storage.
1195 Returns the path to largefile from Mercurial/Git-lfs storage.
1195 or None if it's not a largefile node
1196 or None if it's not a largefile node
1196 """
1197 """
1197 return None
1198 return None
1198
1199
1199 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1200 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1200 archive_dir_name=None, write_metadata=False, mtime=None,
1201 archive_dir_name=None, write_metadata=False, mtime=None,
1201 archive_at_path='/'):
1202 archive_at_path='/'):
1202 """
1203 """
1203 Creates an archive containing the contents of the repository.
1204 Creates an archive containing the contents of the repository.
1204
1205
1205 :param archive_dest_path: path to the file which to create the archive.
1206 :param archive_dest_path: path to the file which to create the archive.
1206 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1207 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1207 :param archive_dir_name: name of root directory in archive.
1208 :param archive_dir_name: name of root directory in archive.
1208 Default is repository name and commit's short_id joined with dash:
1209 Default is repository name and commit's short_id joined with dash:
1209 ``"{repo_name}-{short_id}"``.
1210 ``"{repo_name}-{short_id}"``.
1210 :param write_metadata: write a metadata file into archive.
1211 :param write_metadata: write a metadata file into archive.
1211 :param mtime: custom modification time for archive creation, defaults
1212 :param mtime: custom modification time for archive creation, defaults
1212 to time.time() if not given.
1213 to time.time() if not given.
1213 :param archive_at_path: pack files at this path (default '/')
1214 :param archive_at_path: pack files at this path (default '/')
1214
1215
1215 :raise VCSError: If prefix has a problem.
1216 :raise VCSError: If prefix has a problem.
1216 """
1217 """
1217 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1218 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1218 if kind not in allowed_kinds:
1219 if kind not in allowed_kinds:
1219 raise ImproperArchiveTypeError(
1220 raise ImproperArchiveTypeError(
1220 'Archive kind (%s) not supported use one of %s' %
1221 'Archive kind (%s) not supported use one of %s' %
1221 (kind, allowed_kinds))
1222 (kind, allowed_kinds))
1222
1223
1223 archive_dir_name = self._validate_archive_prefix(archive_dir_name)
1224 archive_dir_name = self._validate_archive_prefix(archive_dir_name)
1224 mtime = mtime is not None or time.mktime(self.date.timetuple())
1225 mtime = mtime is not None or time.mktime(self.date.timetuple())
1225 commit_id = self.raw_id
1226 commit_id = self.raw_id
1226
1227
1227 return self.repository._remote.archive_repo(
1228 return self.repository._remote.archive_repo(
1228 archive_dest_path, kind, mtime, archive_at_path,
1229 archive_dest_path, kind, mtime, archive_at_path,
1229 archive_dir_name, commit_id)
1230 archive_dir_name, commit_id)
1230
1231
1231 def _validate_archive_prefix(self, archive_dir_name):
1232 def _validate_archive_prefix(self, archive_dir_name):
1232 if archive_dir_name is None:
1233 if archive_dir_name is None:
1233 archive_dir_name = self._ARCHIVE_PREFIX_TEMPLATE.format(
1234 archive_dir_name = self._ARCHIVE_PREFIX_TEMPLATE.format(
1234 repo_name=safe_str(self.repository.name),
1235 repo_name=safe_str(self.repository.name),
1235 short_id=self.short_id)
1236 short_id=self.short_id)
1236 elif not isinstance(archive_dir_name, str):
1237 elif not isinstance(archive_dir_name, str):
1237 raise ValueError("prefix not a bytes object: %s" % repr(archive_dir_name))
1238 raise ValueError("prefix not a bytes object: %s" % repr(archive_dir_name))
1238 elif archive_dir_name.startswith('/'):
1239 elif archive_dir_name.startswith('/'):
1239 raise VCSError("Prefix cannot start with leading slash")
1240 raise VCSError("Prefix cannot start with leading slash")
1240 elif archive_dir_name.strip() == '':
1241 elif archive_dir_name.strip() == '':
1241 raise VCSError("Prefix cannot be empty")
1242 raise VCSError("Prefix cannot be empty")
1242 return archive_dir_name
1243 return archive_dir_name
1243
1244
1244 @LazyProperty
1245 @LazyProperty
1245 def root(self):
1246 def root(self):
1246 """
1247 """
1247 Returns ``RootNode`` object for this commit.
1248 Returns ``RootNode`` object for this commit.
1248 """
1249 """
1249 return self.get_node('')
1250 return self.get_node('')
1250
1251
1251 def next(self, branch=None):
1252 def next(self, branch=None):
1252 """
1253 """
1253 Returns next commit from current, if branch is gives it will return
1254 Returns next commit from current, if branch is gives it will return
1254 next commit belonging to this branch
1255 next commit belonging to this branch
1255
1256
1256 :param branch: show commits within the given named branch
1257 :param branch: show commits within the given named branch
1257 """
1258 """
1258 indexes = xrange(self.idx + 1, self.repository.count())
1259 indexes = xrange(self.idx + 1, self.repository.count())
1259 return self._find_next(indexes, branch)
1260 return self._find_next(indexes, branch)
1260
1261
1261 def prev(self, branch=None):
1262 def prev(self, branch=None):
1262 """
1263 """
1263 Returns previous commit from current, if branch is gives it will
1264 Returns previous commit from current, if branch is gives it will
1264 return previous commit belonging to this branch
1265 return previous commit belonging to this branch
1265
1266
1266 :param branch: show commit within the given named branch
1267 :param branch: show commit within the given named branch
1267 """
1268 """
1268 indexes = xrange(self.idx - 1, -1, -1)
1269 indexes = xrange(self.idx - 1, -1, -1)
1269 return self._find_next(indexes, branch)
1270 return self._find_next(indexes, branch)
1270
1271
1271 def _find_next(self, indexes, branch=None):
1272 def _find_next(self, indexes, branch=None):
1272 if branch and self.branch != branch:
1273 if branch and self.branch != branch:
1273 raise VCSError('Branch option used on commit not belonging '
1274 raise VCSError('Branch option used on commit not belonging '
1274 'to that branch')
1275 'to that branch')
1275
1276
1276 for next_idx in indexes:
1277 for next_idx in indexes:
1277 commit = self.repository.get_commit(commit_idx=next_idx)
1278 commit = self.repository.get_commit(commit_idx=next_idx)
1278 if branch and branch != commit.branch:
1279 if branch and branch != commit.branch:
1279 continue
1280 continue
1280 return commit
1281 return commit
1281 raise CommitDoesNotExistError
1282 raise CommitDoesNotExistError
1282
1283
1283 def diff(self, ignore_whitespace=True, context=3):
1284 def diff(self, ignore_whitespace=True, context=3):
1284 """
1285 """
1285 Returns a `Diff` object representing the change made by this commit.
1286 Returns a `Diff` object representing the change made by this commit.
1286 """
1287 """
1287 parent = self.first_parent
1288 parent = self.first_parent
1288 diff = self.repository.get_diff(
1289 diff = self.repository.get_diff(
1289 parent, self,
1290 parent, self,
1290 ignore_whitespace=ignore_whitespace,
1291 ignore_whitespace=ignore_whitespace,
1291 context=context)
1292 context=context)
1292 return diff
1293 return diff
1293
1294
1294 @LazyProperty
1295 @LazyProperty
1295 def added(self):
1296 def added(self):
1296 """
1297 """
1297 Returns list of added ``FileNode`` objects.
1298 Returns list of added ``FileNode`` objects.
1298 """
1299 """
1299 raise NotImplementedError
1300 raise NotImplementedError
1300
1301
1301 @LazyProperty
1302 @LazyProperty
1302 def changed(self):
1303 def changed(self):
1303 """
1304 """
1304 Returns list of modified ``FileNode`` objects.
1305 Returns list of modified ``FileNode`` objects.
1305 """
1306 """
1306 raise NotImplementedError
1307 raise NotImplementedError
1307
1308
1308 @LazyProperty
1309 @LazyProperty
1309 def removed(self):
1310 def removed(self):
1310 """
1311 """
1311 Returns list of removed ``FileNode`` objects.
1312 Returns list of removed ``FileNode`` objects.
1312 """
1313 """
1313 raise NotImplementedError
1314 raise NotImplementedError
1314
1315
1315 @LazyProperty
1316 @LazyProperty
1316 def size(self):
1317 def size(self):
1317 """
1318 """
1318 Returns total number of bytes from contents of all filenodes.
1319 Returns total number of bytes from contents of all filenodes.
1319 """
1320 """
1320 return sum((node.size for node in self.get_filenodes_generator()))
1321 return sum((node.size for node in self.get_filenodes_generator()))
1321
1322
1322 def walk(self, topurl=''):
1323 def walk(self, topurl=''):
1323 """
1324 """
1324 Similar to os.walk method. Insted of filesystem it walks through
1325 Similar to os.walk method. Insted of filesystem it walks through
1325 commit starting at given ``topurl``. Returns generator of tuples
1326 commit starting at given ``topurl``. Returns generator of tuples
1326 (topnode, dirnodes, filenodes).
1327 (topnode, dirnodes, filenodes).
1327 """
1328 """
1328 topnode = self.get_node(topurl)
1329 topnode = self.get_node(topurl)
1329 if not topnode.is_dir():
1330 if not topnode.is_dir():
1330 return
1331 return
1331 yield (topnode, topnode.dirs, topnode.files)
1332 yield (topnode, topnode.dirs, topnode.files)
1332 for dirnode in topnode.dirs:
1333 for dirnode in topnode.dirs:
1333 for tup in self.walk(dirnode.path):
1334 for tup in self.walk(dirnode.path):
1334 yield tup
1335 yield tup
1335
1336
1336 def get_filenodes_generator(self):
1337 def get_filenodes_generator(self):
1337 """
1338 """
1338 Returns generator that yields *all* file nodes.
1339 Returns generator that yields *all* file nodes.
1339 """
1340 """
1340 for topnode, dirs, files in self.walk():
1341 for topnode, dirs, files in self.walk():
1341 for node in files:
1342 for node in files:
1342 yield node
1343 yield node
1343
1344
1344 #
1345 #
1345 # Utilities for sub classes to support consistent behavior
1346 # Utilities for sub classes to support consistent behavior
1346 #
1347 #
1347
1348
1348 def no_node_at_path(self, path):
1349 def no_node_at_path(self, path):
1349 return NodeDoesNotExistError(
1350 return NodeDoesNotExistError(
1350 u"There is no file nor directory at the given path: "
1351 u"There is no file nor directory at the given path: "
1351 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1352 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1352
1353
1353 def _fix_path(self, path):
1354 def _fix_path(self, path):
1354 """
1355 """
1355 Paths are stored without trailing slash so we need to get rid off it if
1356 Paths are stored without trailing slash so we need to get rid off it if
1356 needed.
1357 needed.
1357 """
1358 """
1358 return path.rstrip('/')
1359 return path.rstrip('/')
1359
1360
1360 #
1361 #
1361 # Deprecated API based on changesets
1362 # Deprecated API based on changesets
1362 #
1363 #
1363
1364
1364 @property
1365 @property
1365 def revision(self):
1366 def revision(self):
1366 warnings.warn("Use idx instead", DeprecationWarning)
1367 warnings.warn("Use idx instead", DeprecationWarning)
1367 return self.idx
1368 return self.idx
1368
1369
1369 @revision.setter
1370 @revision.setter
1370 def revision(self, value):
1371 def revision(self, value):
1371 warnings.warn("Use idx instead", DeprecationWarning)
1372 warnings.warn("Use idx instead", DeprecationWarning)
1372 self.idx = value
1373 self.idx = value
1373
1374
1374 def get_file_changeset(self, path):
1375 def get_file_changeset(self, path):
1375 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1376 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1376 return self.get_path_commit(path)
1377 return self.get_path_commit(path)
1377
1378
1378
1379
1379 class BaseChangesetClass(type):
1380 class BaseChangesetClass(type):
1380
1381
1381 def __instancecheck__(self, instance):
1382 def __instancecheck__(self, instance):
1382 return isinstance(instance, BaseCommit)
1383 return isinstance(instance, BaseCommit)
1383
1384
1384
1385
1385 class BaseChangeset(BaseCommit):
1386 class BaseChangeset(BaseCommit):
1386
1387
1387 __metaclass__ = BaseChangesetClass
1388 __metaclass__ = BaseChangesetClass
1388
1389
1389 def __new__(cls, *args, **kwargs):
1390 def __new__(cls, *args, **kwargs):
1390 warnings.warn(
1391 warnings.warn(
1391 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1392 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1392 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1393 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1393
1394
1394
1395
1395 class BaseInMemoryCommit(object):
1396 class BaseInMemoryCommit(object):
1396 """
1397 """
1397 Represents differences between repository's state (most recent head) and
1398 Represents differences between repository's state (most recent head) and
1398 changes made *in place*.
1399 changes made *in place*.
1399
1400
1400 **Attributes**
1401 **Attributes**
1401
1402
1402 ``repository``
1403 ``repository``
1403 repository object for this in-memory-commit
1404 repository object for this in-memory-commit
1404
1405
1405 ``added``
1406 ``added``
1406 list of ``FileNode`` objects marked as *added*
1407 list of ``FileNode`` objects marked as *added*
1407
1408
1408 ``changed``
1409 ``changed``
1409 list of ``FileNode`` objects marked as *changed*
1410 list of ``FileNode`` objects marked as *changed*
1410
1411
1411 ``removed``
1412 ``removed``
1412 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1413 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1413 *removed*
1414 *removed*
1414
1415
1415 ``parents``
1416 ``parents``
1416 list of :class:`BaseCommit` instances representing parents of
1417 list of :class:`BaseCommit` instances representing parents of
1417 in-memory commit. Should always be 2-element sequence.
1418 in-memory commit. Should always be 2-element sequence.
1418
1419
1419 """
1420 """
1420
1421
1421 def __init__(self, repository):
1422 def __init__(self, repository):
1422 self.repository = repository
1423 self.repository = repository
1423 self.added = []
1424 self.added = []
1424 self.changed = []
1425 self.changed = []
1425 self.removed = []
1426 self.removed = []
1426 self.parents = []
1427 self.parents = []
1427
1428
1428 def add(self, *filenodes):
1429 def add(self, *filenodes):
1429 """
1430 """
1430 Marks given ``FileNode`` objects as *to be committed*.
1431 Marks given ``FileNode`` objects as *to be committed*.
1431
1432
1432 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1433 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1433 latest commit
1434 latest commit
1434 :raises ``NodeAlreadyAddedError``: if node with same path is already
1435 :raises ``NodeAlreadyAddedError``: if node with same path is already
1435 marked as *added*
1436 marked as *added*
1436 """
1437 """
1437 # Check if not already marked as *added* first
1438 # Check if not already marked as *added* first
1438 for node in filenodes:
1439 for node in filenodes:
1439 if node.path in (n.path for n in self.added):
1440 if node.path in (n.path for n in self.added):
1440 raise NodeAlreadyAddedError(
1441 raise NodeAlreadyAddedError(
1441 "Such FileNode %s is already marked for addition"
1442 "Such FileNode %s is already marked for addition"
1442 % node.path)
1443 % node.path)
1443 for node in filenodes:
1444 for node in filenodes:
1444 self.added.append(node)
1445 self.added.append(node)
1445
1446
1446 def change(self, *filenodes):
1447 def change(self, *filenodes):
1447 """
1448 """
1448 Marks given ``FileNode`` objects to be *changed* in next commit.
1449 Marks given ``FileNode`` objects to be *changed* in next commit.
1449
1450
1450 :raises ``EmptyRepositoryError``: if there are no commits yet
1451 :raises ``EmptyRepositoryError``: if there are no commits yet
1451 :raises ``NodeAlreadyExistsError``: if node with same path is already
1452 :raises ``NodeAlreadyExistsError``: if node with same path is already
1452 marked to be *changed*
1453 marked to be *changed*
1453 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1454 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1454 marked to be *removed*
1455 marked to be *removed*
1455 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1456 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1456 commit
1457 commit
1457 :raises ``NodeNotChangedError``: if node hasn't really be changed
1458 :raises ``NodeNotChangedError``: if node hasn't really be changed
1458 """
1459 """
1459 for node in filenodes:
1460 for node in filenodes:
1460 if node.path in (n.path for n in self.removed):
1461 if node.path in (n.path for n in self.removed):
1461 raise NodeAlreadyRemovedError(
1462 raise NodeAlreadyRemovedError(
1462 "Node at %s is already marked as removed" % node.path)
1463 "Node at %s is already marked as removed" % node.path)
1463 try:
1464 try:
1464 self.repository.get_commit()
1465 self.repository.get_commit()
1465 except EmptyRepositoryError:
1466 except EmptyRepositoryError:
1466 raise EmptyRepositoryError(
1467 raise EmptyRepositoryError(
1467 "Nothing to change - try to *add* new nodes rather than "
1468 "Nothing to change - try to *add* new nodes rather than "
1468 "changing them")
1469 "changing them")
1469 for node in filenodes:
1470 for node in filenodes:
1470 if node.path in (n.path for n in self.changed):
1471 if node.path in (n.path for n in self.changed):
1471 raise NodeAlreadyChangedError(
1472 raise NodeAlreadyChangedError(
1472 "Node at '%s' is already marked as changed" % node.path)
1473 "Node at '%s' is already marked as changed" % node.path)
1473 self.changed.append(node)
1474 self.changed.append(node)
1474
1475
1475 def remove(self, *filenodes):
1476 def remove(self, *filenodes):
1476 """
1477 """
1477 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1478 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1478 *removed* in next commit.
1479 *removed* in next commit.
1479
1480
1480 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1481 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1481 be *removed*
1482 be *removed*
1482 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1483 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1483 be *changed*
1484 be *changed*
1484 """
1485 """
1485 for node in filenodes:
1486 for node in filenodes:
1486 if node.path in (n.path for n in self.removed):
1487 if node.path in (n.path for n in self.removed):
1487 raise NodeAlreadyRemovedError(
1488 raise NodeAlreadyRemovedError(
1488 "Node is already marked to for removal at %s" % node.path)
1489 "Node is already marked to for removal at %s" % node.path)
1489 if node.path in (n.path for n in self.changed):
1490 if node.path in (n.path for n in self.changed):
1490 raise NodeAlreadyChangedError(
1491 raise NodeAlreadyChangedError(
1491 "Node is already marked to be changed at %s" % node.path)
1492 "Node is already marked to be changed at %s" % node.path)
1492 # We only mark node as *removed* - real removal is done by
1493 # We only mark node as *removed* - real removal is done by
1493 # commit method
1494 # commit method
1494 self.removed.append(node)
1495 self.removed.append(node)
1495
1496
1496 def reset(self):
1497 def reset(self):
1497 """
1498 """
1498 Resets this instance to initial state (cleans ``added``, ``changed``
1499 Resets this instance to initial state (cleans ``added``, ``changed``
1499 and ``removed`` lists).
1500 and ``removed`` lists).
1500 """
1501 """
1501 self.added = []
1502 self.added = []
1502 self.changed = []
1503 self.changed = []
1503 self.removed = []
1504 self.removed = []
1504 self.parents = []
1505 self.parents = []
1505
1506
1506 def get_ipaths(self):
1507 def get_ipaths(self):
1507 """
1508 """
1508 Returns generator of paths from nodes marked as added, changed or
1509 Returns generator of paths from nodes marked as added, changed or
1509 removed.
1510 removed.
1510 """
1511 """
1511 for node in itertools.chain(self.added, self.changed, self.removed):
1512 for node in itertools.chain(self.added, self.changed, self.removed):
1512 yield node.path
1513 yield node.path
1513
1514
1514 def get_paths(self):
1515 def get_paths(self):
1515 """
1516 """
1516 Returns list of paths from nodes marked as added, changed or removed.
1517 Returns list of paths from nodes marked as added, changed or removed.
1517 """
1518 """
1518 return list(self.get_ipaths())
1519 return list(self.get_ipaths())
1519
1520
1520 def check_integrity(self, parents=None):
1521 def check_integrity(self, parents=None):
1521 """
1522 """
1522 Checks in-memory commit's integrity. Also, sets parents if not
1523 Checks in-memory commit's integrity. Also, sets parents if not
1523 already set.
1524 already set.
1524
1525
1525 :raises CommitError: if any error occurs (i.e.
1526 :raises CommitError: if any error occurs (i.e.
1526 ``NodeDoesNotExistError``).
1527 ``NodeDoesNotExistError``).
1527 """
1528 """
1528 if not self.parents:
1529 if not self.parents:
1529 parents = parents or []
1530 parents = parents or []
1530 if len(parents) == 0:
1531 if len(parents) == 0:
1531 try:
1532 try:
1532 parents = [self.repository.get_commit(), None]
1533 parents = [self.repository.get_commit(), None]
1533 except EmptyRepositoryError:
1534 except EmptyRepositoryError:
1534 parents = [None, None]
1535 parents = [None, None]
1535 elif len(parents) == 1:
1536 elif len(parents) == 1:
1536 parents += [None]
1537 parents += [None]
1537 self.parents = parents
1538 self.parents = parents
1538
1539
1539 # Local parents, only if not None
1540 # Local parents, only if not None
1540 parents = [p for p in self.parents if p]
1541 parents = [p for p in self.parents if p]
1541
1542
1542 # Check nodes marked as added
1543 # Check nodes marked as added
1543 for p in parents:
1544 for p in parents:
1544 for node in self.added:
1545 for node in self.added:
1545 try:
1546 try:
1546 p.get_node(node.path)
1547 p.get_node(node.path)
1547 except NodeDoesNotExistError:
1548 except NodeDoesNotExistError:
1548 pass
1549 pass
1549 else:
1550 else:
1550 raise NodeAlreadyExistsError(
1551 raise NodeAlreadyExistsError(
1551 "Node `%s` already exists at %s" % (node.path, p))
1552 "Node `%s` already exists at %s" % (node.path, p))
1552
1553
1553 # Check nodes marked as changed
1554 # Check nodes marked as changed
1554 missing = set(self.changed)
1555 missing = set(self.changed)
1555 not_changed = set(self.changed)
1556 not_changed = set(self.changed)
1556 if self.changed and not parents:
1557 if self.changed and not parents:
1557 raise NodeDoesNotExistError(str(self.changed[0].path))
1558 raise NodeDoesNotExistError(str(self.changed[0].path))
1558 for p in parents:
1559 for p in parents:
1559 for node in self.changed:
1560 for node in self.changed:
1560 try:
1561 try:
1561 old = p.get_node(node.path)
1562 old = p.get_node(node.path)
1562 missing.remove(node)
1563 missing.remove(node)
1563 # if content actually changed, remove node from not_changed
1564 # if content actually changed, remove node from not_changed
1564 if old.content != node.content:
1565 if old.content != node.content:
1565 not_changed.remove(node)
1566 not_changed.remove(node)
1566 except NodeDoesNotExistError:
1567 except NodeDoesNotExistError:
1567 pass
1568 pass
1568 if self.changed and missing:
1569 if self.changed and missing:
1569 raise NodeDoesNotExistError(
1570 raise NodeDoesNotExistError(
1570 "Node `%s` marked as modified but missing in parents: %s"
1571 "Node `%s` marked as modified but missing in parents: %s"
1571 % (node.path, parents))
1572 % (node.path, parents))
1572
1573
1573 if self.changed and not_changed:
1574 if self.changed and not_changed:
1574 raise NodeNotChangedError(
1575 raise NodeNotChangedError(
1575 "Node `%s` wasn't actually changed (parents: %s)"
1576 "Node `%s` wasn't actually changed (parents: %s)"
1576 % (not_changed.pop().path, parents))
1577 % (not_changed.pop().path, parents))
1577
1578
1578 # Check nodes marked as removed
1579 # Check nodes marked as removed
1579 if self.removed and not parents:
1580 if self.removed and not parents:
1580 raise NodeDoesNotExistError(
1581 raise NodeDoesNotExistError(
1581 "Cannot remove node at %s as there "
1582 "Cannot remove node at %s as there "
1582 "were no parents specified" % self.removed[0].path)
1583 "were no parents specified" % self.removed[0].path)
1583 really_removed = set()
1584 really_removed = set()
1584 for p in parents:
1585 for p in parents:
1585 for node in self.removed:
1586 for node in self.removed:
1586 try:
1587 try:
1587 p.get_node(node.path)
1588 p.get_node(node.path)
1588 really_removed.add(node)
1589 really_removed.add(node)
1589 except CommitError:
1590 except CommitError:
1590 pass
1591 pass
1591 not_removed = set(self.removed) - really_removed
1592 not_removed = set(self.removed) - really_removed
1592 if not_removed:
1593 if not_removed:
1593 # TODO: johbo: This code branch does not seem to be covered
1594 # TODO: johbo: This code branch does not seem to be covered
1594 raise NodeDoesNotExistError(
1595 raise NodeDoesNotExistError(
1595 "Cannot remove node at %s from "
1596 "Cannot remove node at %s from "
1596 "following parents: %s" % (not_removed, parents))
1597 "following parents: %s" % (not_removed, parents))
1597
1598
1598 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1599 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1599 """
1600 """
1600 Performs in-memory commit (doesn't check workdir in any way) and
1601 Performs in-memory commit (doesn't check workdir in any way) and
1601 returns newly created :class:`BaseCommit`. Updates repository's
1602 returns newly created :class:`BaseCommit`. Updates repository's
1602 attribute `commits`.
1603 attribute `commits`.
1603
1604
1604 .. note::
1605 .. note::
1605
1606
1606 While overriding this method each backend's should call
1607 While overriding this method each backend's should call
1607 ``self.check_integrity(parents)`` in the first place.
1608 ``self.check_integrity(parents)`` in the first place.
1608
1609
1609 :param message: message of the commit
1610 :param message: message of the commit
1610 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1611 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1611 :param parents: single parent or sequence of parents from which commit
1612 :param parents: single parent or sequence of parents from which commit
1612 would be derived
1613 would be derived
1613 :param date: ``datetime.datetime`` instance. Defaults to
1614 :param date: ``datetime.datetime`` instance. Defaults to
1614 ``datetime.datetime.now()``.
1615 ``datetime.datetime.now()``.
1615 :param branch: branch name, as string. If none given, default backend's
1616 :param branch: branch name, as string. If none given, default backend's
1616 branch would be used.
1617 branch would be used.
1617
1618
1618 :raises ``CommitError``: if any error occurs while committing
1619 :raises ``CommitError``: if any error occurs while committing
1619 """
1620 """
1620 raise NotImplementedError
1621 raise NotImplementedError
1621
1622
1622
1623
1623 class BaseInMemoryChangesetClass(type):
1624 class BaseInMemoryChangesetClass(type):
1624
1625
1625 def __instancecheck__(self, instance):
1626 def __instancecheck__(self, instance):
1626 return isinstance(instance, BaseInMemoryCommit)
1627 return isinstance(instance, BaseInMemoryCommit)
1627
1628
1628
1629
1629 class BaseInMemoryChangeset(BaseInMemoryCommit):
1630 class BaseInMemoryChangeset(BaseInMemoryCommit):
1630
1631
1631 __metaclass__ = BaseInMemoryChangesetClass
1632 __metaclass__ = BaseInMemoryChangesetClass
1632
1633
1633 def __new__(cls, *args, **kwargs):
1634 def __new__(cls, *args, **kwargs):
1634 warnings.warn(
1635 warnings.warn(
1635 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1636 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1636 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1637 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1637
1638
1638
1639
1639 class EmptyCommit(BaseCommit):
1640 class EmptyCommit(BaseCommit):
1640 """
1641 """
1641 An dummy empty commit. It's possible to pass hash when creating
1642 An dummy empty commit. It's possible to pass hash when creating
1642 an EmptyCommit
1643 an EmptyCommit
1643 """
1644 """
1644
1645
1645 def __init__(
1646 def __init__(
1646 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1647 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1647 message='', author='', date=None):
1648 message='', author='', date=None):
1648 self._empty_commit_id = commit_id
1649 self._empty_commit_id = commit_id
1649 # TODO: johbo: Solve idx parameter, default value does not make
1650 # TODO: johbo: Solve idx parameter, default value does not make
1650 # too much sense
1651 # too much sense
1651 self.idx = idx
1652 self.idx = idx
1652 self.message = message
1653 self.message = message
1653 self.author = author
1654 self.author = author
1654 self.date = date or datetime.datetime.fromtimestamp(0)
1655 self.date = date or datetime.datetime.fromtimestamp(0)
1655 self.repository = repo
1656 self.repository = repo
1656 self.alias = alias
1657 self.alias = alias
1657
1658
1658 @LazyProperty
1659 @LazyProperty
1659 def raw_id(self):
1660 def raw_id(self):
1660 """
1661 """
1661 Returns raw string identifying this commit, useful for web
1662 Returns raw string identifying this commit, useful for web
1662 representation.
1663 representation.
1663 """
1664 """
1664
1665
1665 return self._empty_commit_id
1666 return self._empty_commit_id
1666
1667
1667 @LazyProperty
1668 @LazyProperty
1668 def branch(self):
1669 def branch(self):
1669 if self.alias:
1670 if self.alias:
1670 from rhodecode.lib.vcs.backends import get_backend
1671 from rhodecode.lib.vcs.backends import get_backend
1671 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1672 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1672
1673
1673 @LazyProperty
1674 @LazyProperty
1674 def short_id(self):
1675 def short_id(self):
1675 return self.raw_id[:12]
1676 return self.raw_id[:12]
1676
1677
1677 @LazyProperty
1678 @LazyProperty
1678 def id(self):
1679 def id(self):
1679 return self.raw_id
1680 return self.raw_id
1680
1681
1681 def get_path_commit(self, path):
1682 def get_path_commit(self, path):
1682 return self
1683 return self
1683
1684
1684 def get_file_content(self, path):
1685 def get_file_content(self, path):
1685 return u''
1686 return u''
1686
1687
1687 def get_file_content_streamed(self, path):
1688 def get_file_content_streamed(self, path):
1688 yield self.get_file_content()
1689 yield self.get_file_content()
1689
1690
1690 def get_file_size(self, path):
1691 def get_file_size(self, path):
1691 return 0
1692 return 0
1692
1693
1693
1694
1694 class EmptyChangesetClass(type):
1695 class EmptyChangesetClass(type):
1695
1696
1696 def __instancecheck__(self, instance):
1697 def __instancecheck__(self, instance):
1697 return isinstance(instance, EmptyCommit)
1698 return isinstance(instance, EmptyCommit)
1698
1699
1699
1700
1700 class EmptyChangeset(EmptyCommit):
1701 class EmptyChangeset(EmptyCommit):
1701
1702
1702 __metaclass__ = EmptyChangesetClass
1703 __metaclass__ = EmptyChangesetClass
1703
1704
1704 def __new__(cls, *args, **kwargs):
1705 def __new__(cls, *args, **kwargs):
1705 warnings.warn(
1706 warnings.warn(
1706 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1707 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1707 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1708 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1708
1709
1709 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1710 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1710 alias=None, revision=-1, message='', author='', date=None):
1711 alias=None, revision=-1, message='', author='', date=None):
1711 if requested_revision is not None:
1712 if requested_revision is not None:
1712 warnings.warn(
1713 warnings.warn(
1713 "Parameter requested_revision not supported anymore",
1714 "Parameter requested_revision not supported anymore",
1714 DeprecationWarning)
1715 DeprecationWarning)
1715 super(EmptyChangeset, self).__init__(
1716 super(EmptyChangeset, self).__init__(
1716 commit_id=cs, repo=repo, alias=alias, idx=revision,
1717 commit_id=cs, repo=repo, alias=alias, idx=revision,
1717 message=message, author=author, date=date)
1718 message=message, author=author, date=date)
1718
1719
1719 @property
1720 @property
1720 def revision(self):
1721 def revision(self):
1721 warnings.warn("Use idx instead", DeprecationWarning)
1722 warnings.warn("Use idx instead", DeprecationWarning)
1722 return self.idx
1723 return self.idx
1723
1724
1724 @revision.setter
1725 @revision.setter
1725 def revision(self, value):
1726 def revision(self, value):
1726 warnings.warn("Use idx instead", DeprecationWarning)
1727 warnings.warn("Use idx instead", DeprecationWarning)
1727 self.idx = value
1728 self.idx = value
1728
1729
1729
1730
1730 class EmptyRepository(BaseRepository):
1731 class EmptyRepository(BaseRepository):
1731 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1732 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1732 pass
1733 pass
1733
1734
1734 def get_diff(self, *args, **kwargs):
1735 def get_diff(self, *args, **kwargs):
1735 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1736 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1736 return GitDiff('')
1737 return GitDiff('')
1737
1738
1738
1739
1739 class CollectionGenerator(object):
1740 class CollectionGenerator(object):
1740
1741
1741 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1742 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1742 self.repo = repo
1743 self.repo = repo
1743 self.commit_ids = commit_ids
1744 self.commit_ids = commit_ids
1744 # TODO: (oliver) this isn't currently hooked up
1745 # TODO: (oliver) this isn't currently hooked up
1745 self.collection_size = None
1746 self.collection_size = None
1746 self.pre_load = pre_load
1747 self.pre_load = pre_load
1747 self.translate_tag = translate_tag
1748 self.translate_tag = translate_tag
1748
1749
1749 def __len__(self):
1750 def __len__(self):
1750 if self.collection_size is not None:
1751 if self.collection_size is not None:
1751 return self.collection_size
1752 return self.collection_size
1752 return self.commit_ids.__len__()
1753 return self.commit_ids.__len__()
1753
1754
1754 def __iter__(self):
1755 def __iter__(self):
1755 for commit_id in self.commit_ids:
1756 for commit_id in self.commit_ids:
1756 # TODO: johbo: Mercurial passes in commit indices or commit ids
1757 # TODO: johbo: Mercurial passes in commit indices or commit ids
1757 yield self._commit_factory(commit_id)
1758 yield self._commit_factory(commit_id)
1758
1759
1759 def _commit_factory(self, commit_id):
1760 def _commit_factory(self, commit_id):
1760 """
1761 """
1761 Allows backends to override the way commits are generated.
1762 Allows backends to override the way commits are generated.
1762 """
1763 """
1763 return self.repo.get_commit(
1764 return self.repo.get_commit(
1764 commit_id=commit_id, pre_load=self.pre_load,
1765 commit_id=commit_id, pre_load=self.pre_load,
1765 translate_tag=self.translate_tag)
1766 translate_tag=self.translate_tag)
1766
1767
1767 def __getslice__(self, i, j):
1768 def __getslice__(self, i, j):
1768 """
1769 """
1769 Returns an iterator of sliced repository
1770 Returns an iterator of sliced repository
1770 """
1771 """
1771 commit_ids = self.commit_ids[i:j]
1772 commit_ids = self.commit_ids[i:j]
1772 return self.__class__(
1773 return self.__class__(
1773 self.repo, commit_ids, pre_load=self.pre_load,
1774 self.repo, commit_ids, pre_load=self.pre_load,
1774 translate_tag=self.translate_tag)
1775 translate_tag=self.translate_tag)
1775
1776
1776 def __repr__(self):
1777 def __repr__(self):
1777 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1778 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1778
1779
1779
1780
1780 class Config(object):
1781 class Config(object):
1781 """
1782 """
1782 Represents the configuration for a repository.
1783 Represents the configuration for a repository.
1783
1784
1784 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1785 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1785 standard library. It implements only the needed subset.
1786 standard library. It implements only the needed subset.
1786 """
1787 """
1787
1788
1788 def __init__(self):
1789 def __init__(self):
1789 self._values = {}
1790 self._values = {}
1790
1791
1791 def copy(self):
1792 def copy(self):
1792 clone = Config()
1793 clone = Config()
1793 for section, values in self._values.items():
1794 for section, values in self._values.items():
1794 clone._values[section] = values.copy()
1795 clone._values[section] = values.copy()
1795 return clone
1796 return clone
1796
1797
1797 def __repr__(self):
1798 def __repr__(self):
1798 return '<Config(%s sections) at %s>' % (
1799 return '<Config(%s sections) at %s>' % (
1799 len(self._values), hex(id(self)))
1800 len(self._values), hex(id(self)))
1800
1801
1801 def items(self, section):
1802 def items(self, section):
1802 return self._values.get(section, {}).iteritems()
1803 return self._values.get(section, {}).iteritems()
1803
1804
1804 def get(self, section, option):
1805 def get(self, section, option):
1805 return self._values.get(section, {}).get(option)
1806 return self._values.get(section, {}).get(option)
1806
1807
1807 def set(self, section, option, value):
1808 def set(self, section, option, value):
1808 section_values = self._values.setdefault(section, {})
1809 section_values = self._values.setdefault(section, {})
1809 section_values[option] = value
1810 section_values[option] = value
1810
1811
1811 def clear_section(self, section):
1812 def clear_section(self, section):
1812 self._values[section] = {}
1813 self._values[section] = {}
1813
1814
1814 def serialize(self):
1815 def serialize(self):
1815 """
1816 """
1816 Creates a list of three tuples (section, key, value) representing
1817 Creates a list of three tuples (section, key, value) representing
1817 this config object.
1818 this config object.
1818 """
1819 """
1819 items = []
1820 items = []
1820 for section in self._values:
1821 for section in self._values:
1821 for option, value in self._values[section].items():
1822 for option, value in self._values[section].items():
1822 items.append(
1823 items.append(
1823 (safe_str(section), safe_str(option), safe_str(value)))
1824 (safe_str(section), safe_str(option), safe_str(value)))
1824 return items
1825 return items
1825
1826
1826
1827
1827 class Diff(object):
1828 class Diff(object):
1828 """
1829 """
1829 Represents a diff result from a repository backend.
1830 Represents a diff result from a repository backend.
1830
1831
1831 Subclasses have to provide a backend specific value for
1832 Subclasses have to provide a backend specific value for
1832 :attr:`_header_re` and :attr:`_meta_re`.
1833 :attr:`_header_re` and :attr:`_meta_re`.
1833 """
1834 """
1834 _meta_re = None
1835 _meta_re = None
1835 _header_re = None
1836 _header_re = None
1836
1837
1837 def __init__(self, raw_diff):
1838 def __init__(self, raw_diff):
1838 self.raw = raw_diff
1839 self.raw = raw_diff
1839
1840
1840 def chunks(self):
1841 def chunks(self):
1841 """
1842 """
1842 split the diff in chunks of separate --git a/file b/file chunks
1843 split the diff in chunks of separate --git a/file b/file chunks
1843 to make diffs consistent we must prepend with \n, and make sure
1844 to make diffs consistent we must prepend with \n, and make sure
1844 we can detect last chunk as this was also has special rule
1845 we can detect last chunk as this was also has special rule
1845 """
1846 """
1846
1847
1847 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1848 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1848 header = diff_parts[0]
1849 header = diff_parts[0]
1849
1850
1850 if self._meta_re:
1851 if self._meta_re:
1851 match = self._meta_re.match(header)
1852 match = self._meta_re.match(header)
1852
1853
1853 chunks = diff_parts[1:]
1854 chunks = diff_parts[1:]
1854 total_chunks = len(chunks)
1855 total_chunks = len(chunks)
1855
1856
1856 return (
1857 return (
1857 DiffChunk(chunk, self, cur_chunk == total_chunks)
1858 DiffChunk(chunk, self, cur_chunk == total_chunks)
1858 for cur_chunk, chunk in enumerate(chunks, start=1))
1859 for cur_chunk, chunk in enumerate(chunks, start=1))
1859
1860
1860
1861
1861 class DiffChunk(object):
1862 class DiffChunk(object):
1862
1863
1863 def __init__(self, chunk, diff, last_chunk):
1864 def __init__(self, chunk, diff, last_chunk):
1864 self._diff = diff
1865 self._diff = diff
1865
1866
1866 # since we split by \ndiff --git that part is lost from original diff
1867 # since we split by \ndiff --git that part is lost from original diff
1867 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1868 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1868 if not last_chunk:
1869 if not last_chunk:
1869 chunk += '\n'
1870 chunk += '\n'
1870
1871
1871 match = self._diff._header_re.match(chunk)
1872 match = self._diff._header_re.match(chunk)
1872 self.header = match.groupdict()
1873 self.header = match.groupdict()
1873 self.diff = chunk[match.end():]
1874 self.diff = chunk[match.end():]
1874 self.raw = chunk
1875 self.raw = chunk
1875
1876
1876
1877
1877 class BasePathPermissionChecker(object):
1878 class BasePathPermissionChecker(object):
1878
1879
1879 @staticmethod
1880 @staticmethod
1880 def create_from_patterns(includes, excludes):
1881 def create_from_patterns(includes, excludes):
1881 if includes and '*' in includes and not excludes:
1882 if includes and '*' in includes and not excludes:
1882 return AllPathPermissionChecker()
1883 return AllPathPermissionChecker()
1883 elif excludes and '*' in excludes:
1884 elif excludes and '*' in excludes:
1884 return NonePathPermissionChecker()
1885 return NonePathPermissionChecker()
1885 else:
1886 else:
1886 return PatternPathPermissionChecker(includes, excludes)
1887 return PatternPathPermissionChecker(includes, excludes)
1887
1888
1888 @property
1889 @property
1889 def has_full_access(self):
1890 def has_full_access(self):
1890 raise NotImplemented()
1891 raise NotImplemented()
1891
1892
1892 def has_access(self, path):
1893 def has_access(self, path):
1893 raise NotImplemented()
1894 raise NotImplemented()
1894
1895
1895
1896
1896 class AllPathPermissionChecker(BasePathPermissionChecker):
1897 class AllPathPermissionChecker(BasePathPermissionChecker):
1897
1898
1898 @property
1899 @property
1899 def has_full_access(self):
1900 def has_full_access(self):
1900 return True
1901 return True
1901
1902
1902 def has_access(self, path):
1903 def has_access(self, path):
1903 return True
1904 return True
1904
1905
1905
1906
1906 class NonePathPermissionChecker(BasePathPermissionChecker):
1907 class NonePathPermissionChecker(BasePathPermissionChecker):
1907
1908
1908 @property
1909 @property
1909 def has_full_access(self):
1910 def has_full_access(self):
1910 return False
1911 return False
1911
1912
1912 def has_access(self, path):
1913 def has_access(self, path):
1913 return False
1914 return False
1914
1915
1915
1916
1916 class PatternPathPermissionChecker(BasePathPermissionChecker):
1917 class PatternPathPermissionChecker(BasePathPermissionChecker):
1917
1918
1918 def __init__(self, includes, excludes):
1919 def __init__(self, includes, excludes):
1919 self.includes = includes
1920 self.includes = includes
1920 self.excludes = excludes
1921 self.excludes = excludes
1921 self.includes_re = [] if not includes else [
1922 self.includes_re = [] if not includes else [
1922 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1923 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1923 self.excludes_re = [] if not excludes else [
1924 self.excludes_re = [] if not excludes else [
1924 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1925 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1925
1926
1926 @property
1927 @property
1927 def has_full_access(self):
1928 def has_full_access(self):
1928 return '*' in self.includes and not self.excludes
1929 return '*' in self.includes and not self.excludes
1929
1930
1930 def has_access(self, path):
1931 def has_access(self, path):
1931 for regex in self.excludes_re:
1932 for regex in self.excludes_re:
1932 if regex.match(path):
1933 if regex.match(path):
1933 return False
1934 return False
1934 for regex in self.includes_re:
1935 for regex in self.includes_re:
1935 if regex.match(path):
1936 if regex.match(path):
1936 return True
1937 return True
1937 return False
1938 return False
@@ -1,1051 +1,1051 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2020 RhodeCode GmbH
3 # Copyright (C) 2014-2020 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
28
29 from zope.cachedescriptors.property import Lazy as LazyProperty
29 from zope.cachedescriptors.property import Lazy as LazyProperty
30
30
31 from rhodecode.lib.compat import OrderedDict
31 from rhodecode.lib.compat import OrderedDict
32 from rhodecode.lib.datelib import (
32 from rhodecode.lib.datelib import (
33 utcdate_fromtimestamp, makedate, date_astimestamp)
33 utcdate_fromtimestamp, makedate, date_astimestamp)
34 from rhodecode.lib.utils import safe_unicode, safe_str
34 from rhodecode.lib.utils import safe_unicode, safe_str
35 from rhodecode.lib.utils2 import CachedProperty
35 from rhodecode.lib.utils2 import CachedProperty
36 from rhodecode.lib.vcs import connection, path as vcspath
36 from rhodecode.lib.vcs import connection, path as vcspath
37 from rhodecode.lib.vcs.backends.base import (
37 from rhodecode.lib.vcs.backends.base import (
38 BaseRepository, CollectionGenerator, Config, MergeResponse,
38 BaseRepository, CollectionGenerator, Config, MergeResponse,
39 MergeFailureReason, Reference)
39 MergeFailureReason, Reference)
40 from rhodecode.lib.vcs.backends.git.commit import GitCommit
40 from rhodecode.lib.vcs.backends.git.commit import GitCommit
41 from rhodecode.lib.vcs.backends.git.diff import GitDiff
41 from rhodecode.lib.vcs.backends.git.diff import GitDiff
42 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
42 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
43 from rhodecode.lib.vcs.exceptions import (
43 from rhodecode.lib.vcs.exceptions import (
44 CommitDoesNotExistError, EmptyRepositoryError,
44 CommitDoesNotExistError, EmptyRepositoryError,
45 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError, UnresolvedFilesInRepo)
45 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError, UnresolvedFilesInRepo)
46
46
47
47
48 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
48 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class GitRepository(BaseRepository):
53 class GitRepository(BaseRepository):
54 """
54 """
55 Git repository backend.
55 Git repository backend.
56 """
56 """
57 DEFAULT_BRANCH_NAME = 'master'
57 DEFAULT_BRANCH_NAME = 'master'
58
58
59 contact = BaseRepository.DEFAULT_CONTACT
59 contact = BaseRepository.DEFAULT_CONTACT
60
60
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
62 do_workspace_checkout=False, with_wire=None, bare=False):
62 do_workspace_checkout=False, with_wire=None, bare=False):
63
63
64 self.path = safe_str(os.path.abspath(repo_path))
64 self.path = safe_str(os.path.abspath(repo_path))
65 self.config = config if config else self.get_default_config()
65 self.config = config if config else self.get_default_config()
66 self.with_wire = with_wire or {"cache": False} # default should not use cache
66 self.with_wire = with_wire or {"cache": False} # default should not use cache
67
67
68 self._init_repo(create, src_url, do_workspace_checkout, bare)
68 self._init_repo(create, src_url, do_workspace_checkout, bare)
69
69
70 # caches
70 # caches
71 self._commit_ids = {}
71 self._commit_ids = {}
72
72
73 @LazyProperty
73 @LazyProperty
74 def _remote(self):
74 def _remote(self):
75 repo_id = self.path
75 repo_id = self.path
76 return connection.Git(self.path, repo_id, self.config, with_wire=self.with_wire)
76 return connection.Git(self.path, repo_id, self.config, with_wire=self.with_wire)
77
77
78 @LazyProperty
78 @LazyProperty
79 def bare(self):
79 def bare(self):
80 return self._remote.bare()
80 return self._remote.bare()
81
81
82 @LazyProperty
82 @LazyProperty
83 def head(self):
83 def head(self):
84 return self._remote.head()
84 return self._remote.head()
85
85
86 @CachedProperty
86 @CachedProperty
87 def commit_ids(self):
87 def commit_ids(self):
88 """
88 """
89 Returns list of commit ids, in ascending order. Being lazy
89 Returns list of commit ids, in ascending order. Being lazy
90 attribute allows external tools to inject commit ids from cache.
90 attribute allows external tools to inject commit ids from cache.
91 """
91 """
92 commit_ids = self._get_all_commit_ids()
92 commit_ids = self._get_all_commit_ids()
93 self._rebuild_cache(commit_ids)
93 self._rebuild_cache(commit_ids)
94 return commit_ids
94 return commit_ids
95
95
96 def _rebuild_cache(self, commit_ids):
96 def _rebuild_cache(self, commit_ids):
97 self._commit_ids = dict((commit_id, index)
97 self._commit_ids = dict((commit_id, index)
98 for index, commit_id in enumerate(commit_ids))
98 for index, commit_id in enumerate(commit_ids))
99
99
100 def run_git_command(self, cmd, **opts):
100 def run_git_command(self, cmd, **opts):
101 """
101 """
102 Runs given ``cmd`` as git command and returns tuple
102 Runs given ``cmd`` as git command and returns tuple
103 (stdout, stderr).
103 (stdout, stderr).
104
104
105 :param cmd: git command to be executed
105 :param cmd: git command to be executed
106 :param opts: env options to pass into Subprocess command
106 :param opts: env options to pass into Subprocess command
107 """
107 """
108 if not isinstance(cmd, list):
108 if not isinstance(cmd, list):
109 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
109 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
110
110
111 skip_stderr_log = opts.pop('skip_stderr_log', False)
111 skip_stderr_log = opts.pop('skip_stderr_log', False)
112 out, err = self._remote.run_git_command(cmd, **opts)
112 out, err = self._remote.run_git_command(cmd, **opts)
113 if err and not skip_stderr_log:
113 if err and not skip_stderr_log:
114 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
114 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
115 return out, err
115 return out, err
116
116
117 @staticmethod
117 @staticmethod
118 def check_url(url, config):
118 def check_url(url, config):
119 """
119 """
120 Function will check given url and try to verify if it's a valid
120 Function will check given url and try to verify if it's a valid
121 link. Sometimes it may happened that git will issue basic
121 link. Sometimes it may happened that git will issue basic
122 auth request that can cause whole API to hang when used from python
122 auth request that can cause whole API to hang when used from python
123 or other external calls.
123 or other external calls.
124
124
125 On failures it'll raise urllib2.HTTPError, exception is also thrown
125 On failures it'll raise urllib2.HTTPError, exception is also thrown
126 when the return code is non 200
126 when the return code is non 200
127 """
127 """
128 # check first if it's not an url
128 # check first if it's not an url
129 if os.path.isdir(url) or url.startswith('file:'):
129 if os.path.isdir(url) or url.startswith('file:'):
130 return True
130 return True
131
131
132 if '+' in url.split('://', 1)[0]:
132 if '+' in url.split('://', 1)[0]:
133 url = url.split('+', 1)[1]
133 url = url.split('+', 1)[1]
134
134
135 # Request the _remote to verify the url
135 # Request the _remote to verify the url
136 return connection.Git.check_url(url, config.serialize())
136 return connection.Git.check_url(url, config.serialize())
137
137
138 @staticmethod
138 @staticmethod
139 def is_valid_repository(path):
139 def is_valid_repository(path):
140 if os.path.isdir(os.path.join(path, '.git')):
140 if os.path.isdir(os.path.join(path, '.git')):
141 return True
141 return True
142 # check case of bare repository
142 # check case of bare repository
143 try:
143 try:
144 GitRepository(path)
144 GitRepository(path)
145 return True
145 return True
146 except VCSError:
146 except VCSError:
147 pass
147 pass
148 return False
148 return False
149
149
150 def _init_repo(self, create, src_url=None, do_workspace_checkout=False,
150 def _init_repo(self, create, src_url=None, do_workspace_checkout=False,
151 bare=False):
151 bare=False):
152 if create and os.path.exists(self.path):
152 if create and os.path.exists(self.path):
153 raise RepositoryError(
153 raise RepositoryError(
154 "Cannot create repository at %s, location already exist"
154 "Cannot create repository at %s, location already exist"
155 % self.path)
155 % self.path)
156
156
157 if bare and do_workspace_checkout:
157 if bare and do_workspace_checkout:
158 raise RepositoryError("Cannot update a bare repository")
158 raise RepositoryError("Cannot update a bare repository")
159 try:
159 try:
160
160
161 if src_url:
161 if src_url:
162 # check URL before any actions
162 # check URL before any actions
163 GitRepository.check_url(src_url, self.config)
163 GitRepository.check_url(src_url, self.config)
164
164
165 if create:
165 if create:
166 os.makedirs(self.path, mode=0o755)
166 os.makedirs(self.path, mode=0o755)
167
167
168 if bare:
168 if bare:
169 self._remote.init_bare()
169 self._remote.init_bare()
170 else:
170 else:
171 self._remote.init()
171 self._remote.init()
172
172
173 if src_url and bare:
173 if src_url and bare:
174 # bare repository only allows a fetch and checkout is not allowed
174 # bare repository only allows a fetch and checkout is not allowed
175 self.fetch(src_url, commit_ids=None)
175 self.fetch(src_url, commit_ids=None)
176 elif src_url:
176 elif src_url:
177 self.pull(src_url, commit_ids=None,
177 self.pull(src_url, commit_ids=None,
178 update_after=do_workspace_checkout)
178 update_after=do_workspace_checkout)
179
179
180 else:
180 else:
181 if not self._remote.assert_correct_path():
181 if not self._remote.assert_correct_path():
182 raise RepositoryError(
182 raise RepositoryError(
183 'Path "%s" does not contain a Git repository' %
183 'Path "%s" does not contain a Git repository' %
184 (self.path,))
184 (self.path,))
185
185
186 # TODO: johbo: check if we have to translate the OSError here
186 # TODO: johbo: check if we have to translate the OSError here
187 except OSError as err:
187 except OSError as err:
188 raise RepositoryError(err)
188 raise RepositoryError(err)
189
189
190 def _get_all_commit_ids(self):
190 def _get_all_commit_ids(self):
191 return self._remote.get_all_commit_ids()
191 return self._remote.get_all_commit_ids()
192
192
193 def _get_commit_ids(self, filters=None):
193 def _get_commit_ids(self, filters=None):
194 # we must check if this repo is not empty, since later command
194 # we must check if this repo is not empty, since later command
195 # fails if it is. And it's cheaper to ask than throw the subprocess
195 # fails if it is. And it's cheaper to ask than throw the subprocess
196 # errors
196 # errors
197
197
198 head = self._remote.head(show_exc=False)
198 head = self._remote.head(show_exc=False)
199
199
200 if not head:
200 if not head:
201 return []
201 return []
202
202
203 rev_filter = ['--branches', '--tags']
203 rev_filter = ['--branches', '--tags']
204 extra_filter = []
204 extra_filter = []
205
205
206 if filters:
206 if filters:
207 if filters.get('since'):
207 if filters.get('since'):
208 extra_filter.append('--since=%s' % (filters['since']))
208 extra_filter.append('--since=%s' % (filters['since']))
209 if filters.get('until'):
209 if filters.get('until'):
210 extra_filter.append('--until=%s' % (filters['until']))
210 extra_filter.append('--until=%s' % (filters['until']))
211 if filters.get('branch_name'):
211 if filters.get('branch_name'):
212 rev_filter = []
212 rev_filter = []
213 extra_filter.append(filters['branch_name'])
213 extra_filter.append(filters['branch_name'])
214 rev_filter.extend(extra_filter)
214 rev_filter.extend(extra_filter)
215
215
216 # if filters.get('start') or filters.get('end'):
216 # if filters.get('start') or filters.get('end'):
217 # # skip is offset, max-count is limit
217 # # skip is offset, max-count is limit
218 # if filters.get('start'):
218 # if filters.get('start'):
219 # extra_filter += ' --skip=%s' % filters['start']
219 # extra_filter += ' --skip=%s' % filters['start']
220 # if filters.get('end'):
220 # if filters.get('end'):
221 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
221 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
222
222
223 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
223 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
224 try:
224 try:
225 output, __ = self.run_git_command(cmd)
225 output, __ = self.run_git_command(cmd)
226 except RepositoryError:
226 except RepositoryError:
227 # Can be raised for empty repositories
227 # Can be raised for empty repositories
228 return []
228 return []
229 return output.splitlines()
229 return output.splitlines()
230
230
231 def _lookup_commit(self, commit_id_or_idx, translate_tag=True, maybe_unreachable=False, reference_obj=None):
231 def _lookup_commit(self, commit_id_or_idx, translate_tag=True, maybe_unreachable=False, reference_obj=None):
232
232
233 def is_null(value):
233 def is_null(value):
234 return len(value) == commit_id_or_idx.count('0')
234 return len(value) == commit_id_or_idx.count('0')
235
235
236 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
236 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
237 return self.commit_ids[-1]
237 return self.commit_ids[-1]
238
238
239 commit_missing_err = "Commit {} does not exist for `{}`".format(
239 commit_missing_err = "Commit {} does not exist for `{}`".format(
240 *map(safe_str, [commit_id_or_idx, self.name]))
240 *map(safe_str, [commit_id_or_idx, self.name]))
241
241
242 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
242 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
243 is_branch = reference_obj and reference_obj.branch
243 is_branch = reference_obj and reference_obj.branch
244
244
245 lookup_ok = False
245 lookup_ok = False
246 if is_bstr:
246 if is_bstr:
247 # Need to call remote to translate id for tagging scenarios,
247 # Need to call remote to translate id for tagging scenarios,
248 # or branch that are numeric
248 # or branch that are numeric
249 try:
249 try:
250 remote_data = self._remote.get_object(commit_id_or_idx,
250 remote_data = self._remote.get_object(commit_id_or_idx,
251 maybe_unreachable=maybe_unreachable)
251 maybe_unreachable=maybe_unreachable)
252 commit_id_or_idx = remote_data["commit_id"]
252 commit_id_or_idx = remote_data["commit_id"]
253 lookup_ok = True
253 lookup_ok = True
254 except (CommitDoesNotExistError,):
254 except (CommitDoesNotExistError,):
255 lookup_ok = False
255 lookup_ok = False
256
256
257 if lookup_ok is False:
257 if lookup_ok is False:
258 is_numeric_idx = \
258 is_numeric_idx = \
259 (is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12) \
259 (is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12) \
260 or isinstance(commit_id_or_idx, int)
260 or isinstance(commit_id_or_idx, int)
261 if not is_branch and (is_numeric_idx or is_null(commit_id_or_idx)):
261 if not is_branch and (is_numeric_idx or is_null(commit_id_or_idx)):
262 try:
262 try:
263 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
263 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
264 lookup_ok = True
264 lookup_ok = True
265 except Exception:
265 except Exception:
266 raise CommitDoesNotExistError(commit_missing_err)
266 raise CommitDoesNotExistError(commit_missing_err)
267
267
268 # we failed regular lookup, and by integer number lookup
268 # we failed regular lookup, and by integer number lookup
269 if lookup_ok is False:
269 if lookup_ok is False:
270 raise CommitDoesNotExistError(commit_missing_err)
270 raise CommitDoesNotExistError(commit_missing_err)
271
271
272 # Ensure we return full id
272 # Ensure we return full id
273 if not SHA_PATTERN.match(str(commit_id_or_idx)):
273 if not SHA_PATTERN.match(str(commit_id_or_idx)):
274 raise CommitDoesNotExistError(
274 raise CommitDoesNotExistError(
275 "Given commit id %s not recognized" % commit_id_or_idx)
275 "Given commit id %s not recognized" % commit_id_or_idx)
276 return commit_id_or_idx
276 return commit_id_or_idx
277
277
278 def get_hook_location(self):
278 def get_hook_location(self):
279 """
279 """
280 returns absolute path to location where hooks are stored
280 returns absolute path to location where hooks are stored
281 """
281 """
282 loc = os.path.join(self.path, 'hooks')
282 loc = os.path.join(self.path, 'hooks')
283 if not self.bare:
283 if not self.bare:
284 loc = os.path.join(self.path, '.git', 'hooks')
284 loc = os.path.join(self.path, '.git', 'hooks')
285 return loc
285 return loc
286
286
287 @LazyProperty
287 @LazyProperty
288 def last_change(self):
288 def last_change(self):
289 """
289 """
290 Returns last change made on this repository as
290 Returns last change made on this repository as
291 `datetime.datetime` object.
291 `datetime.datetime` object.
292 """
292 """
293 try:
293 try:
294 return self.get_commit().date
294 return self.get_commit().date
295 except RepositoryError:
295 except RepositoryError:
296 tzoffset = makedate()[1]
296 tzoffset = makedate()[1]
297 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
297 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
298
298
299 def _get_fs_mtime(self):
299 def _get_fs_mtime(self):
300 idx_loc = '' if self.bare else '.git'
300 idx_loc = '' if self.bare else '.git'
301 # fallback to filesystem
301 # fallback to filesystem
302 in_path = os.path.join(self.path, idx_loc, "index")
302 in_path = os.path.join(self.path, idx_loc, "index")
303 he_path = os.path.join(self.path, idx_loc, "HEAD")
303 he_path = os.path.join(self.path, idx_loc, "HEAD")
304 if os.path.exists(in_path):
304 if os.path.exists(in_path):
305 return os.stat(in_path).st_mtime
305 return os.stat(in_path).st_mtime
306 else:
306 else:
307 return os.stat(he_path).st_mtime
307 return os.stat(he_path).st_mtime
308
308
309 @LazyProperty
309 @LazyProperty
310 def description(self):
310 def description(self):
311 description = self._remote.get_description()
311 description = self._remote.get_description()
312 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
312 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
313
313
314 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
314 def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
315 if self.is_empty():
315 if self.is_empty():
316 return OrderedDict()
316 return OrderedDict()
317
317
318 result = []
318 result = []
319 for ref, sha in self._refs.iteritems():
319 for ref, sha in self._refs.iteritems():
320 if ref.startswith(prefix):
320 if ref.startswith(prefix):
321 ref_name = ref
321 ref_name = ref
322 if strip_prefix:
322 if strip_prefix:
323 ref_name = ref[len(prefix):]
323 ref_name = ref[len(prefix):]
324 result.append((safe_unicode(ref_name), sha))
324 result.append((safe_unicode(ref_name), sha))
325
325
326 def get_name(entry):
326 def get_name(entry):
327 return entry[0]
327 return entry[0]
328
328
329 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
329 return OrderedDict(sorted(result, key=get_name, reverse=reverse))
330
330
331 def _get_branches(self):
331 def _get_branches(self):
332 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
332 return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
333
333
334 @CachedProperty
334 @CachedProperty
335 def branches(self):
335 def branches(self):
336 return self._get_branches()
336 return self._get_branches()
337
337
338 @CachedProperty
338 @CachedProperty
339 def branches_closed(self):
339 def branches_closed(self):
340 return {}
340 return {}
341
341
342 @CachedProperty
342 @CachedProperty
343 def bookmarks(self):
343 def bookmarks(self):
344 return {}
344 return {}
345
345
346 @CachedProperty
346 @CachedProperty
347 def branches_all(self):
347 def branches_all(self):
348 all_branches = {}
348 all_branches = {}
349 all_branches.update(self.branches)
349 all_branches.update(self.branches)
350 all_branches.update(self.branches_closed)
350 all_branches.update(self.branches_closed)
351 return all_branches
351 return all_branches
352
352
353 @CachedProperty
353 @CachedProperty
354 def tags(self):
354 def tags(self):
355 return self._get_tags()
355 return self._get_tags()
356
356
357 def _get_tags(self):
357 def _get_tags(self):
358 return self._get_refs_entries(prefix='refs/tags/', strip_prefix=True, reverse=True)
358 return self._get_refs_entries(prefix='refs/tags/', strip_prefix=True, reverse=True)
359
359
360 def tag(self, name, user, commit_id=None, message=None, date=None,
360 def tag(self, name, user, commit_id=None, message=None, date=None,
361 **kwargs):
361 **kwargs):
362 # TODO: fix this method to apply annotated tags correct with message
362 # TODO: fix this method to apply annotated tags correct with message
363 """
363 """
364 Creates and returns a tag for the given ``commit_id``.
364 Creates and returns a tag for the given ``commit_id``.
365
365
366 :param name: name for new tag
366 :param name: name for new tag
367 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
367 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
368 :param commit_id: commit id for which new tag would be created
368 :param commit_id: commit id for which new tag would be created
369 :param message: message of the tag's commit
369 :param message: message of the tag's commit
370 :param date: date of tag's commit
370 :param date: date of tag's commit
371
371
372 :raises TagAlreadyExistError: if tag with same name already exists
372 :raises TagAlreadyExistError: if tag with same name already exists
373 """
373 """
374 if name in self.tags:
374 if name in self.tags:
375 raise TagAlreadyExistError("Tag %s already exists" % name)
375 raise TagAlreadyExistError("Tag %s already exists" % name)
376 commit = self.get_commit(commit_id=commit_id)
376 commit = self.get_commit(commit_id=commit_id)
377 message = message or "Added tag %s for commit %s" % (name, commit.raw_id)
377 message = message or "Added tag %s for commit %s" % (name, commit.raw_id)
378
378
379 self._remote.set_refs('refs/tags/%s' % name, commit.raw_id)
379 self._remote.set_refs('refs/tags/%s' % name, commit.raw_id)
380
380
381 self._invalidate_prop_cache('tags')
381 self._invalidate_prop_cache('tags')
382 self._invalidate_prop_cache('_refs')
382 self._invalidate_prop_cache('_refs')
383
383
384 return commit
384 return commit
385
385
386 def remove_tag(self, name, user, message=None, date=None):
386 def remove_tag(self, name, user, message=None, date=None):
387 """
387 """
388 Removes tag with the given ``name``.
388 Removes tag with the given ``name``.
389
389
390 :param name: name of the tag to be removed
390 :param name: name of the tag to be removed
391 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
391 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
392 :param message: message of the tag's removal commit
392 :param message: message of the tag's removal commit
393 :param date: date of tag's removal commit
393 :param date: date of tag's removal commit
394
394
395 :raises TagDoesNotExistError: if tag with given name does not exists
395 :raises TagDoesNotExistError: if tag with given name does not exists
396 """
396 """
397 if name not in self.tags:
397 if name not in self.tags:
398 raise TagDoesNotExistError("Tag %s does not exist" % name)
398 raise TagDoesNotExistError("Tag %s does not exist" % name)
399
399
400 self._remote.tag_remove(name)
400 self._remote.tag_remove(name)
401 self._invalidate_prop_cache('tags')
401 self._invalidate_prop_cache('tags')
402 self._invalidate_prop_cache('_refs')
402 self._invalidate_prop_cache('_refs')
403
403
404 def _get_refs(self):
404 def _get_refs(self):
405 return self._remote.get_refs()
405 return self._remote.get_refs()
406
406
407 @CachedProperty
407 @CachedProperty
408 def _refs(self):
408 def _refs(self):
409 return self._get_refs()
409 return self._get_refs()
410
410
411 @property
411 @property
412 def _ref_tree(self):
412 def _ref_tree(self):
413 node = tree = {}
413 node = tree = {}
414 for ref, sha in self._refs.iteritems():
414 for ref, sha in self._refs.iteritems():
415 path = ref.split('/')
415 path = ref.split('/')
416 for bit in path[:-1]:
416 for bit in path[:-1]:
417 node = node.setdefault(bit, {})
417 node = node.setdefault(bit, {})
418 node[path[-1]] = sha
418 node[path[-1]] = sha
419 node = tree
419 node = tree
420 return tree
420 return tree
421
421
422 def get_remote_ref(self, ref_name):
422 def get_remote_ref(self, ref_name):
423 ref_key = 'refs/remotes/origin/{}'.format(safe_str(ref_name))
423 ref_key = 'refs/remotes/origin/{}'.format(safe_str(ref_name))
424 try:
424 try:
425 return self._refs[ref_key]
425 return self._refs[ref_key]
426 except Exception:
426 except Exception:
427 return
427 return
428
428
429 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
429 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None,
430 translate_tag=True, maybe_unreachable=False, reference_obj=None):
430 translate_tag=True, maybe_unreachable=False, reference_obj=None):
431 """
431 """
432 Returns `GitCommit` object representing commit from git repository
432 Returns `GitCommit` object representing commit from git repository
433 at the given `commit_id` or head (most recent commit) if None given.
433 at the given `commit_id` or head (most recent commit) if None given.
434 """
434 """
435
435
436 if self.is_empty():
436 if self.is_empty():
437 raise EmptyRepositoryError("There are no commits yet")
437 raise EmptyRepositoryError("There are no commits yet")
438
438
439 if commit_id is not None:
439 if commit_id is not None:
440 self._validate_commit_id(commit_id)
440 self._validate_commit_id(commit_id)
441 try:
441 try:
442 # we have cached idx, use it without contacting the remote
442 # we have cached idx, use it without contacting the remote
443 idx = self._commit_ids[commit_id]
443 idx = self._commit_ids[commit_id]
444 return GitCommit(self, commit_id, idx, pre_load=pre_load)
444 return GitCommit(self, commit_id, idx, pre_load=pre_load)
445 except KeyError:
445 except KeyError:
446 pass
446 pass
447
447
448 elif commit_idx is not None:
448 elif commit_idx is not None:
449 self._validate_commit_idx(commit_idx)
449 self._validate_commit_idx(commit_idx)
450 try:
450 try:
451 _commit_id = self.commit_ids[commit_idx]
451 _commit_id = self.commit_ids[commit_idx]
452 if commit_idx < 0:
452 if commit_idx < 0:
453 commit_idx = self.commit_ids.index(_commit_id)
453 commit_idx = self.commit_ids.index(_commit_id)
454 return GitCommit(self, _commit_id, commit_idx, pre_load=pre_load)
454 return GitCommit(self, _commit_id, commit_idx, pre_load=pre_load)
455 except IndexError:
455 except IndexError:
456 commit_id = commit_idx
456 commit_id = commit_idx
457 else:
457 else:
458 commit_id = "tip"
458 commit_id = "tip"
459
459
460 if translate_tag:
460 if translate_tag:
461 commit_id = self._lookup_commit(
461 commit_id = self._lookup_commit(
462 commit_id, maybe_unreachable=maybe_unreachable,
462 commit_id, maybe_unreachable=maybe_unreachable,
463 reference_obj=reference_obj)
463 reference_obj=reference_obj)
464
464
465 try:
465 try:
466 idx = self._commit_ids[commit_id]
466 idx = self._commit_ids[commit_id]
467 except KeyError:
467 except KeyError:
468 idx = -1
468 idx = -1
469
469
470 return GitCommit(self, commit_id, idx, pre_load=pre_load)
470 return GitCommit(self, commit_id, idx, pre_load=pre_load)
471
471
472 def get_commits(
472 def get_commits(
473 self, start_id=None, end_id=None, start_date=None, end_date=None,
473 self, start_id=None, end_id=None, start_date=None, end_date=None,
474 branch_name=None, show_hidden=False, pre_load=None, translate_tags=True):
474 branch_name=None, show_hidden=False, pre_load=None, translate_tags=True):
475 """
475 """
476 Returns generator of `GitCommit` objects from start to end (both
476 Returns generator of `GitCommit` objects from start to end (both
477 are inclusive), in ascending date order.
477 are inclusive), in ascending date order.
478
478
479 :param start_id: None, str(commit_id)
479 :param start_id: None, str(commit_id)
480 :param end_id: None, str(commit_id)
480 :param end_id: None, str(commit_id)
481 :param start_date: if specified, commits with commit date less than
481 :param start_date: if specified, commits with commit date less than
482 ``start_date`` would be filtered out from returned set
482 ``start_date`` would be filtered out from returned set
483 :param end_date: if specified, commits with commit date greater than
483 :param end_date: if specified, commits with commit date greater than
484 ``end_date`` would be filtered out from returned set
484 ``end_date`` would be filtered out from returned set
485 :param branch_name: if specified, commits not reachable from given
485 :param branch_name: if specified, commits not reachable from given
486 branch would be filtered out from returned set
486 branch would be filtered out from returned set
487 :param show_hidden: Show hidden commits such as obsolete or hidden from
487 :param show_hidden: Show hidden commits such as obsolete or hidden from
488 Mercurial evolve
488 Mercurial evolve
489 :raise BranchDoesNotExistError: If given `branch_name` does not
489 :raise BranchDoesNotExistError: If given `branch_name` does not
490 exist.
490 exist.
491 :raise CommitDoesNotExistError: If commits for given `start` or
491 :raise CommitDoesNotExistError: If commits for given `start` or
492 `end` could not be found.
492 `end` could not be found.
493
493
494 """
494 """
495 if self.is_empty():
495 if self.is_empty():
496 raise EmptyRepositoryError("There are no commits yet")
496 raise EmptyRepositoryError("There are no commits yet")
497
497
498 self._validate_branch_name(branch_name)
498 self._validate_branch_name(branch_name)
499
499
500 if start_id is not None:
500 if start_id is not None:
501 self._validate_commit_id(start_id)
501 self._validate_commit_id(start_id)
502 if end_id is not None:
502 if end_id is not None:
503 self._validate_commit_id(end_id)
503 self._validate_commit_id(end_id)
504
504
505 start_raw_id = self._lookup_commit(start_id)
505 start_raw_id = self._lookup_commit(start_id)
506 start_pos = self._commit_ids[start_raw_id] if start_id else None
506 start_pos = self._commit_ids[start_raw_id] if start_id else None
507 end_raw_id = self._lookup_commit(end_id)
507 end_raw_id = self._lookup_commit(end_id)
508 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
508 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
509
509
510 if None not in [start_id, end_id] and start_pos > end_pos:
510 if None not in [start_id, end_id] and start_pos > end_pos:
511 raise RepositoryError(
511 raise RepositoryError(
512 "Start commit '%s' cannot be after end commit '%s'" %
512 "Start commit '%s' cannot be after end commit '%s'" %
513 (start_id, end_id))
513 (start_id, end_id))
514
514
515 if end_pos is not None:
515 if end_pos is not None:
516 end_pos += 1
516 end_pos += 1
517
517
518 filter_ = []
518 filter_ = []
519 if branch_name:
519 if branch_name:
520 filter_.append({'branch_name': branch_name})
520 filter_.append({'branch_name': branch_name})
521 if start_date and not end_date:
521 if start_date and not end_date:
522 filter_.append({'since': start_date})
522 filter_.append({'since': start_date})
523 if end_date and not start_date:
523 if end_date and not start_date:
524 filter_.append({'until': end_date})
524 filter_.append({'until': end_date})
525 if start_date and end_date:
525 if start_date and end_date:
526 filter_.append({'since': start_date})
526 filter_.append({'since': start_date})
527 filter_.append({'until': end_date})
527 filter_.append({'until': end_date})
528
528
529 # if start_pos or end_pos:
529 # if start_pos or end_pos:
530 # filter_.append({'start': start_pos})
530 # filter_.append({'start': start_pos})
531 # filter_.append({'end': end_pos})
531 # filter_.append({'end': end_pos})
532
532
533 if filter_:
533 if filter_:
534 revfilters = {
534 revfilters = {
535 'branch_name': branch_name,
535 'branch_name': branch_name,
536 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
536 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
537 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
537 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
538 'start': start_pos,
538 'start': start_pos,
539 'end': end_pos,
539 'end': end_pos,
540 }
540 }
541 commit_ids = self._get_commit_ids(filters=revfilters)
541 commit_ids = self._get_commit_ids(filters=revfilters)
542
542
543 else:
543 else:
544 commit_ids = self.commit_ids
544 commit_ids = self.commit_ids
545
545
546 if start_pos or end_pos:
546 if start_pos or end_pos:
547 commit_ids = commit_ids[start_pos: end_pos]
547 commit_ids = commit_ids[start_pos: end_pos]
548
548
549 return CollectionGenerator(self, commit_ids, pre_load=pre_load,
549 return CollectionGenerator(self, commit_ids, pre_load=pre_load,
550 translate_tag=translate_tags)
550 translate_tag=translate_tags)
551
551
552 def get_diff(
552 def get_diff(
553 self, commit1, commit2, path='', ignore_whitespace=False,
553 self, commit1, commit2, path='', ignore_whitespace=False,
554 context=3, path1=None):
554 context=3, path1=None):
555 """
555 """
556 Returns (git like) *diff*, as plain text. Shows changes introduced by
556 Returns (git like) *diff*, as plain text. Shows changes introduced by
557 ``commit2`` since ``commit1``.
557 ``commit2`` since ``commit1``.
558
558
559 :param commit1: Entry point from which diff is shown. Can be
559 :param commit1: Entry point from which diff is shown. Can be
560 ``self.EMPTY_COMMIT`` - in this case, patch showing all
560 ``self.EMPTY_COMMIT`` - in this case, patch showing all
561 the changes since empty state of the repository until ``commit2``
561 the changes since empty state of the repository until ``commit2``
562 :param commit2: Until which commits changes should be shown.
562 :param commit2: Until which commits changes should be shown.
563 :param ignore_whitespace: If set to ``True``, would not show whitespace
563 :param ignore_whitespace: If set to ``True``, would not show whitespace
564 changes. Defaults to ``False``.
564 changes. Defaults to ``False``.
565 :param context: How many lines before/after changed lines should be
565 :param context: How many lines before/after changed lines should be
566 shown. Defaults to ``3``.
566 shown. Defaults to ``3``.
567 """
567 """
568 self._validate_diff_commits(commit1, commit2)
568 self._validate_diff_commits(commit1, commit2)
569 if path1 is not None and path1 != path:
569 if path1 is not None and path1 != path:
570 raise ValueError("Diff of two different paths not supported.")
570 raise ValueError("Diff of two different paths not supported.")
571
571
572 if path:
572 if path:
573 file_filter = path
573 file_filter = path
574 else:
574 else:
575 file_filter = None
575 file_filter = None
576
576
577 diff = self._remote.diff(
577 diff = self._remote.diff(
578 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
578 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
579 opt_ignorews=ignore_whitespace,
579 opt_ignorews=ignore_whitespace,
580 context=context)
580 context=context)
581 return GitDiff(diff)
581 return GitDiff(diff)
582
582
583 def strip(self, commit_id, branch_name):
583 def strip(self, commit_id, branch_name):
584 commit = self.get_commit(commit_id=commit_id)
584 commit = self.get_commit(commit_id=commit_id)
585 if commit.merge:
585 if commit.merge:
586 raise Exception('Cannot reset to merge commit')
586 raise Exception('Cannot reset to merge commit')
587
587
588 # parent is going to be the new head now
588 # parent is going to be the new head now
589 commit = commit.parents[0]
589 commit = commit.parents[0]
590 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
590 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
591
591
592 # clear cached properties
592 # clear cached properties
593 self._invalidate_prop_cache('commit_ids')
593 self._invalidate_prop_cache('commit_ids')
594 self._invalidate_prop_cache('_refs')
594 self._invalidate_prop_cache('_refs')
595 self._invalidate_prop_cache('branches')
595 self._invalidate_prop_cache('branches')
596
596
597 return len(self.commit_ids)
597 return len(self.commit_ids)
598
598
599 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
599 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
600 log.debug('Calculating common ancestor between %sc1:%s and %sc2:%s',
600 log.debug('Calculating common ancestor between %sc1:%s and %sc2:%s',
601 self, commit_id1, repo2, commit_id2)
601 self, commit_id1, repo2, commit_id2)
602
602
603 if commit_id1 == commit_id2:
603 if commit_id1 == commit_id2:
604 return commit_id1
604 return commit_id1
605
605
606 if self != repo2:
606 if self != repo2:
607 commits = self._remote.get_missing_revs(
607 commits = self._remote.get_missing_revs(
608 commit_id1, commit_id2, repo2.path)
608 commit_id1, commit_id2, repo2.path)
609 if commits:
609 if commits:
610 commit = repo2.get_commit(commits[-1])
610 commit = repo2.get_commit(commits[-1])
611 if commit.parents:
611 if commit.parents:
612 ancestor_id = commit.parents[0].raw_id
612 ancestor_id = commit.parents[0].raw_id
613 else:
613 else:
614 ancestor_id = None
614 ancestor_id = None
615 else:
615 else:
616 # no commits from other repo, ancestor_id is the commit_id2
616 # no commits from other repo, ancestor_id is the commit_id2
617 ancestor_id = commit_id2
617 ancestor_id = commit_id2
618 else:
618 else:
619 output, __ = self.run_git_command(
619 output, __ = self.run_git_command(
620 ['merge-base', commit_id1, commit_id2])
620 ['merge-base', commit_id1, commit_id2])
621 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
621 ancestor_id = self.COMMIT_ID_PAT.findall(output)[0]
622
622
623 log.debug('Found common ancestor with sha: %s', ancestor_id)
623 log.debug('Found common ancestor with sha: %s', ancestor_id)
624
624
625 return ancestor_id
625 return ancestor_id
626
626
627 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
627 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
628 repo1 = self
628 repo1 = self
629 ancestor_id = None
629 ancestor_id = None
630
630
631 if commit_id1 == commit_id2:
631 if commit_id1 == commit_id2:
632 commits = []
632 commits = []
633 elif repo1 != repo2:
633 elif repo1 != repo2:
634 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
634 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
635 repo2.path)
635 repo2.path)
636 commits = [
636 commits = [
637 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
637 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
638 for commit_id in reversed(missing_ids)]
638 for commit_id in reversed(missing_ids)]
639 else:
639 else:
640 output, __ = repo1.run_git_command(
640 output, __ = repo1.run_git_command(
641 ['log', '--reverse', '--pretty=format: %H', '-s',
641 ['log', '--reverse', '--pretty=format: %H', '-s',
642 '%s..%s' % (commit_id1, commit_id2)])
642 '%s..%s' % (commit_id1, commit_id2)])
643 commits = [
643 commits = [
644 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
644 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
645 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
645 for commit_id in self.COMMIT_ID_PAT.findall(output)]
646
646
647 return commits
647 return commits
648
648
649 @LazyProperty
649 @LazyProperty
650 def in_memory_commit(self):
650 def in_memory_commit(self):
651 """
651 """
652 Returns ``GitInMemoryCommit`` object for this repository.
652 Returns ``GitInMemoryCommit`` object for this repository.
653 """
653 """
654 return GitInMemoryCommit(self)
654 return GitInMemoryCommit(self)
655
655
656 def pull(self, url, commit_ids=None, update_after=False):
656 def pull(self, url, commit_ids=None, update_after=False):
657 """
657 """
658 Pull changes from external location. Pull is different in GIT
658 Pull changes from external location. Pull is different in GIT
659 that fetch since it's doing a checkout
659 that fetch since it's doing a checkout
660
660
661 :param commit_ids: Optional. Can be set to a list of commit ids
661 :param commit_ids: Optional. Can be set to a list of commit ids
662 which shall be pulled from the other repository.
662 which shall be pulled from the other repository.
663 """
663 """
664 refs = None
664 refs = None
665 if commit_ids is not None:
665 if commit_ids is not None:
666 remote_refs = self._remote.get_remote_refs(url)
666 remote_refs = self._remote.get_remote_refs(url)
667 refs = [ref for ref in remote_refs if remote_refs[ref] in commit_ids]
667 refs = [ref for ref in remote_refs if remote_refs[ref] in commit_ids]
668 self._remote.pull(url, refs=refs, update_after=update_after)
668 self._remote.pull(url, refs=refs, update_after=update_after)
669 self._remote.invalidate_vcs_cache()
669 self._remote.invalidate_vcs_cache()
670
670
671 def fetch(self, url, commit_ids=None):
671 def fetch(self, url, commit_ids=None):
672 """
672 """
673 Fetch all git objects from external location.
673 Fetch all git objects from external location.
674 """
674 """
675 self._remote.sync_fetch(url, refs=commit_ids)
675 self._remote.sync_fetch(url, refs=commit_ids)
676 self._remote.invalidate_vcs_cache()
676 self._remote.invalidate_vcs_cache()
677
677
678 def push(self, url):
678 def push(self, url):
679 refs = None
679 refs = None
680 self._remote.sync_push(url, refs=refs)
680 self._remote.sync_push(url, refs=refs)
681
681
682 def set_refs(self, ref_name, commit_id):
682 def set_refs(self, ref_name, commit_id):
683 self._remote.set_refs(ref_name, commit_id)
683 self._remote.set_refs(ref_name, commit_id)
684 self._invalidate_prop_cache('_refs')
684 self._invalidate_prop_cache('_refs')
685
685
686 def remove_ref(self, ref_name):
686 def remove_ref(self, ref_name):
687 self._remote.remove_ref(ref_name)
687 self._remote.remove_ref(ref_name)
688 self._invalidate_prop_cache('_refs')
688 self._invalidate_prop_cache('_refs')
689
689
690 def run_gc(self, prune=True):
690 def run_gc(self, prune=True):
691 cmd = ['gc', '--aggressive']
691 cmd = ['gc', '--aggressive']
692 if prune:
692 if prune:
693 cmd += ['--prune=now']
693 cmd += ['--prune=now']
694 _stdout, stderr = self.run_git_command(cmd, fail_on_stderr=False)
694 _stdout, stderr = self.run_git_command(cmd, fail_on_stderr=False)
695 return stderr
695 return stderr
696
696
697 def _update_server_info(self):
697 def _update_server_info(self):
698 """
698 """
699 runs gits update-server-info command in this repo instance
699 runs gits update-server-info command in this repo instance
700 """
700 """
701 self._remote.update_server_info()
701 self._remote.update_server_info()
702
702
703 def _current_branch(self):
703 def _current_branch(self):
704 """
704 """
705 Return the name of the current branch.
705 Return the name of the current branch.
706
706
707 It only works for non bare repositories (i.e. repositories with a
707 It only works for non bare repositories (i.e. repositories with a
708 working copy)
708 working copy)
709 """
709 """
710 if self.bare:
710 if self.bare:
711 raise RepositoryError('Bare git repos do not have active branches')
711 raise RepositoryError('Bare git repos do not have active branches')
712
712
713 if self.is_empty():
713 if self.is_empty():
714 return None
714 return None
715
715
716 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
716 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
717 return stdout.strip()
717 return stdout.strip()
718
718
719 def _checkout(self, branch_name, create=False, force=False):
719 def _checkout(self, branch_name, create=False, force=False):
720 """
720 """
721 Checkout a branch in the working directory.
721 Checkout a branch in the working directory.
722
722
723 It tries to create the branch if create is True, failing if the branch
723 It tries to create the branch if create is True, failing if the branch
724 already exists.
724 already exists.
725
725
726 It only works for non bare repositories (i.e. repositories with a
726 It only works for non bare repositories (i.e. repositories with a
727 working copy)
727 working copy)
728 """
728 """
729 if self.bare:
729 if self.bare:
730 raise RepositoryError('Cannot checkout branches in a bare git repo')
730 raise RepositoryError('Cannot checkout branches in a bare git repo')
731
731
732 cmd = ['checkout']
732 cmd = ['checkout']
733 if force:
733 if force:
734 cmd.append('-f')
734 cmd.append('-f')
735 if create:
735 if create:
736 cmd.append('-b')
736 cmd.append('-b')
737 cmd.append(branch_name)
737 cmd.append(branch_name)
738 self.run_git_command(cmd, fail_on_stderr=False)
738 self.run_git_command(cmd, fail_on_stderr=False)
739
739
740 def _create_branch(self, branch_name, commit_id):
740 def _create_branch(self, branch_name, commit_id):
741 """
741 """
742 creates a branch in a GIT repo
742 creates a branch in a GIT repo
743 """
743 """
744 self._remote.create_branch(branch_name, commit_id)
744 self._remote.create_branch(branch_name, commit_id)
745
745
746 def _identify(self):
746 def _identify(self):
747 """
747 """
748 Return the current state of the working directory.
748 Return the current state of the working directory.
749 """
749 """
750 if self.bare:
750 if self.bare:
751 raise RepositoryError('Bare git repos do not have active branches')
751 raise RepositoryError('Bare git repos do not have active branches')
752
752
753 if self.is_empty():
753 if self.is_empty():
754 return None
754 return None
755
755
756 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
756 stdout, _ = self.run_git_command(['rev-parse', 'HEAD'])
757 return stdout.strip()
757 return stdout.strip()
758
758
759 def _local_clone(self, clone_path, branch_name, source_branch=None):
759 def _local_clone(self, clone_path, branch_name, source_branch=None):
760 """
760 """
761 Create a local clone of the current repo.
761 Create a local clone of the current repo.
762 """
762 """
763 # N.B.(skreft): the --branch option is required as otherwise the shallow
763 # N.B.(skreft): the --branch option is required as otherwise the shallow
764 # clone will only fetch the active branch.
764 # clone will only fetch the active branch.
765 cmd = ['clone', '--branch', branch_name,
765 cmd = ['clone', '--branch', branch_name,
766 self.path, os.path.abspath(clone_path)]
766 self.path, os.path.abspath(clone_path)]
767
767
768 self.run_git_command(cmd, fail_on_stderr=False)
768 self.run_git_command(cmd, fail_on_stderr=False)
769
769
770 # if we get the different source branch, make sure we also fetch it for
770 # if we get the different source branch, make sure we also fetch it for
771 # merge conditions
771 # merge conditions
772 if source_branch and source_branch != branch_name:
772 if source_branch and source_branch != branch_name:
773 # check if the ref exists.
773 # check if the ref exists.
774 shadow_repo = GitRepository(os.path.abspath(clone_path))
774 shadow_repo = GitRepository(os.path.abspath(clone_path))
775 if shadow_repo.get_remote_ref(source_branch):
775 if shadow_repo.get_remote_ref(source_branch):
776 cmd = ['fetch', self.path, source_branch]
776 cmd = ['fetch', self.path, source_branch]
777 self.run_git_command(cmd, fail_on_stderr=False)
777 self.run_git_command(cmd, fail_on_stderr=False)
778
778
779 def _local_fetch(self, repository_path, branch_name, use_origin=False):
779 def _local_fetch(self, repository_path, branch_name, use_origin=False):
780 """
780 """
781 Fetch a branch from a local repository.
781 Fetch a branch from a local repository.
782 """
782 """
783 repository_path = os.path.abspath(repository_path)
783 repository_path = os.path.abspath(repository_path)
784 if repository_path == self.path:
784 if repository_path == self.path:
785 raise ValueError('Cannot fetch from the same repository')
785 raise ValueError('Cannot fetch from the same repository')
786
786
787 if use_origin:
787 if use_origin:
788 branch_name = '+{branch}:refs/heads/{branch}'.format(
788 branch_name = '+{branch}:refs/heads/{branch}'.format(
789 branch=branch_name)
789 branch=branch_name)
790
790
791 cmd = ['fetch', '--no-tags', '--update-head-ok',
791 cmd = ['fetch', '--no-tags', '--update-head-ok',
792 repository_path, branch_name]
792 repository_path, branch_name]
793 self.run_git_command(cmd, fail_on_stderr=False)
793 self.run_git_command(cmd, fail_on_stderr=False)
794
794
795 def _local_reset(self, branch_name):
795 def _local_reset(self, branch_name):
796 branch_name = '{}'.format(branch_name)
796 branch_name = '{}'.format(branch_name)
797 cmd = ['reset', '--hard', branch_name, '--']
797 cmd = ['reset', '--hard', branch_name, '--']
798 self.run_git_command(cmd, fail_on_stderr=False)
798 self.run_git_command(cmd, fail_on_stderr=False)
799
799
800 def _last_fetch_heads(self):
800 def _last_fetch_heads(self):
801 """
801 """
802 Return the last fetched heads that need merging.
802 Return the last fetched heads that need merging.
803
803
804 The algorithm is defined at
804 The algorithm is defined at
805 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
805 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
806 """
806 """
807 if not self.bare:
807 if not self.bare:
808 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
808 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
809 else:
809 else:
810 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
810 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
811
811
812 heads = []
812 heads = []
813 with open(fetch_heads_path) as f:
813 with open(fetch_heads_path) as f:
814 for line in f:
814 for line in f:
815 if ' not-for-merge ' in line:
815 if ' not-for-merge ' in line:
816 continue
816 continue
817 line = re.sub('\t.*', '', line, flags=re.DOTALL)
817 line = re.sub('\t.*', '', line, flags=re.DOTALL)
818 heads.append(line)
818 heads.append(line)
819
819
820 return heads
820 return heads
821
821
822 def get_shadow_instance(self, shadow_repository_path, enable_hooks=False, cache=False):
822 def get_shadow_instance(self, shadow_repository_path, enable_hooks=False, cache=False):
823 return GitRepository(shadow_repository_path, with_wire={"cache": cache})
823 return GitRepository(shadow_repository_path, with_wire={"cache": cache})
824
824
825 def _local_pull(self, repository_path, branch_name, ff_only=True):
825 def _local_pull(self, repository_path, branch_name, ff_only=True):
826 """
826 """
827 Pull a branch from a local repository.
827 Pull a branch from a local repository.
828 """
828 """
829 if self.bare:
829 if self.bare:
830 raise RepositoryError('Cannot pull into a bare git repository')
830 raise RepositoryError('Cannot pull into a bare git repository')
831 # N.B.(skreft): The --ff-only option is to make sure this is a
831 # N.B.(skreft): The --ff-only option is to make sure this is a
832 # fast-forward (i.e., we are only pulling new changes and there are no
832 # fast-forward (i.e., we are only pulling new changes and there are no
833 # conflicts with our current branch)
833 # conflicts with our current branch)
834 # Additionally, that option needs to go before --no-tags, otherwise git
834 # Additionally, that option needs to go before --no-tags, otherwise git
835 # pull complains about it being an unknown flag.
835 # pull complains about it being an unknown flag.
836 cmd = ['pull']
836 cmd = ['pull']
837 if ff_only:
837 if ff_only:
838 cmd.append('--ff-only')
838 cmd.append('--ff-only')
839 cmd.extend(['--no-tags', repository_path, branch_name])
839 cmd.extend(['--no-tags', repository_path, branch_name])
840 self.run_git_command(cmd, fail_on_stderr=False)
840 self.run_git_command(cmd, fail_on_stderr=False)
841
841
842 def _local_merge(self, merge_message, user_name, user_email, heads):
842 def _local_merge(self, merge_message, user_name, user_email, heads):
843 """
843 """
844 Merge the given head into the checked out branch.
844 Merge the given head into the checked out branch.
845
845
846 It will force a merge commit.
846 It will force a merge commit.
847
847
848 Currently it raises an error if the repo is empty, as it is not possible
848 Currently it raises an error if the repo is empty, as it is not possible
849 to create a merge commit in an empty repo.
849 to create a merge commit in an empty repo.
850
850
851 :param merge_message: The message to use for the merge commit.
851 :param merge_message: The message to use for the merge commit.
852 :param heads: the heads to merge.
852 :param heads: the heads to merge.
853 """
853 """
854 if self.bare:
854 if self.bare:
855 raise RepositoryError('Cannot merge into a bare git repository')
855 raise RepositoryError('Cannot merge into a bare git repository')
856
856
857 if not heads:
857 if not heads:
858 return
858 return
859
859
860 if self.is_empty():
860 if self.is_empty():
861 # TODO(skreft): do something more robust in this case.
861 # TODO(skreft): do something more robust in this case.
862 raise RepositoryError('Do not know how to merge into empty repositories yet')
862 raise RepositoryError('Do not know how to merge into empty repositories yet')
863 unresolved = None
863 unresolved = None
864
864
865 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
865 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
866 # commit message. We also specify the user who is doing the merge.
866 # commit message. We also specify the user who is doing the merge.
867 cmd = ['-c', 'user.name="%s"' % safe_str(user_name),
867 cmd = ['-c', 'user.name="%s"' % safe_str(user_name),
868 '-c', 'user.email=%s' % safe_str(user_email),
868 '-c', 'user.email=%s' % safe_str(user_email),
869 'merge', '--no-ff', '-m', safe_str(merge_message)]
869 'merge', '--no-ff', '-m', safe_str(merge_message)]
870
870
871 merge_cmd = cmd + heads
871 merge_cmd = cmd + heads
872
872
873 try:
873 try:
874 self.run_git_command(merge_cmd, fail_on_stderr=False)
874 self.run_git_command(merge_cmd, fail_on_stderr=False)
875 except RepositoryError:
875 except RepositoryError:
876 files = self.run_git_command(['diff', '--name-only', '--diff-filter', 'U'],
876 files = self.run_git_command(['diff', '--name-only', '--diff-filter', 'U'],
877 fail_on_stderr=False)[0].splitlines()
877 fail_on_stderr=False)[0].splitlines()
878 # NOTE(marcink): we add U notation for consistent with HG backend output
878 # NOTE(marcink): we add U notation for consistent with HG backend output
879 unresolved = ['U {}'.format(f) for f in files]
879 unresolved = ['U {}'.format(f) for f in files]
880
880
881 # Cleanup any merge leftovers
881 # Cleanup any merge leftovers
882 self._remote.invalidate_vcs_cache()
882 self._remote.invalidate_vcs_cache()
883 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
883 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
884
884
885 if unresolved:
885 if unresolved:
886 raise UnresolvedFilesInRepo(unresolved)
886 raise UnresolvedFilesInRepo(unresolved)
887 else:
887 else:
888 raise
888 raise
889
889
890 def _local_push(
890 def _local_push(
891 self, source_branch, repository_path, target_branch,
891 self, source_branch, repository_path, target_branch,
892 enable_hooks=False, rc_scm_data=None):
892 enable_hooks=False, rc_scm_data=None):
893 """
893 """
894 Push the source_branch to the given repository and target_branch.
894 Push the source_branch to the given repository and target_branch.
895
895
896 Currently it if the target_branch is not master and the target repo is
896 Currently it if the target_branch is not master and the target repo is
897 empty, the push will work, but then GitRepository won't be able to find
897 empty, the push will work, but then GitRepository won't be able to find
898 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
898 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
899 pointing to master, which does not exist).
899 pointing to master, which does not exist).
900
900
901 It does not run the hooks in the target repo.
901 It does not run the hooks in the target repo.
902 """
902 """
903 # TODO(skreft): deal with the case in which the target repo is empty,
903 # TODO(skreft): deal with the case in which the target repo is empty,
904 # and the target_branch is not master.
904 # and the target_branch is not master.
905 target_repo = GitRepository(repository_path)
905 target_repo = GitRepository(repository_path)
906 if (not target_repo.bare and
906 if (not target_repo.bare and
907 target_repo._current_branch() == target_branch):
907 target_repo._current_branch() == target_branch):
908 # Git prevents pushing to the checked out branch, so simulate it by
908 # Git prevents pushing to the checked out branch, so simulate it by
909 # pulling into the target repository.
909 # pulling into the target repository.
910 target_repo._local_pull(self.path, source_branch)
910 target_repo._local_pull(self.path, source_branch)
911 else:
911 else:
912 cmd = ['push', os.path.abspath(repository_path),
912 cmd = ['push', os.path.abspath(repository_path),
913 '%s:%s' % (source_branch, target_branch)]
913 '%s:%s' % (source_branch, target_branch)]
914 gitenv = {}
914 gitenv = {}
915 if rc_scm_data:
915 if rc_scm_data:
916 gitenv.update({'RC_SCM_DATA': rc_scm_data})
916 gitenv.update({'RC_SCM_DATA': rc_scm_data})
917
917
918 if not enable_hooks:
918 if not enable_hooks:
919 gitenv['RC_SKIP_HOOKS'] = '1'
919 gitenv['RC_SKIP_HOOKS'] = '1'
920 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
920 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
921
921
922 def _get_new_pr_branch(self, source_branch, target_branch):
922 def _get_new_pr_branch(self, source_branch, target_branch):
923 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
923 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
924 pr_branches = []
924 pr_branches = []
925 for branch in self.branches:
925 for branch in self.branches:
926 if branch.startswith(prefix):
926 if branch.startswith(prefix):
927 pr_branches.append(int(branch[len(prefix):]))
927 pr_branches.append(int(branch[len(prefix):]))
928
928
929 if not pr_branches:
929 if not pr_branches:
930 branch_id = 0
930 branch_id = 0
931 else:
931 else:
932 branch_id = max(pr_branches) + 1
932 branch_id = max(pr_branches) + 1
933
933
934 return '%s%d' % (prefix, branch_id)
934 return '%s%d' % (prefix, branch_id)
935
935
936 def _maybe_prepare_merge_workspace(
936 def _maybe_prepare_merge_workspace(
937 self, repo_id, workspace_id, target_ref, source_ref):
937 self, repo_id, workspace_id, target_ref, source_ref):
938 shadow_repository_path = self._get_shadow_repository_path(
938 shadow_repository_path = self._get_shadow_repository_path(
939 self.path, repo_id, workspace_id)
939 self.path, repo_id, workspace_id)
940 if not os.path.exists(shadow_repository_path):
940 if not os.path.exists(shadow_repository_path):
941 self._local_clone(
941 self._local_clone(
942 shadow_repository_path, target_ref.name, source_ref.name)
942 shadow_repository_path, target_ref.name, source_ref.name)
943 log.debug('Prepared %s shadow repository in %s',
943 log.debug('Prepared %s shadow repository in %s',
944 self.alias, shadow_repository_path)
944 self.alias, shadow_repository_path)
945
945
946 return shadow_repository_path
946 return shadow_repository_path
947
947
948 def _merge_repo(self, repo_id, workspace_id, target_ref,
948 def _merge_repo(self, repo_id, workspace_id, target_ref,
949 source_repo, source_ref, merge_message,
949 source_repo, source_ref, merge_message,
950 merger_name, merger_email, dry_run=False,
950 merger_name, merger_email, dry_run=False,
951 use_rebase=False, close_branch=False):
951 use_rebase=False, close_branch=False):
952
952
953 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
953 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
954 'rebase' if use_rebase else 'merge', dry_run)
954 'rebase' if use_rebase else 'merge', dry_run)
955 if target_ref.commit_id != self.branches[target_ref.name]:
955 if target_ref.commit_id != self.branches[target_ref.name]:
956 log.warning('Target ref %s commit mismatch %s vs %s', target_ref,
956 log.warning('Target ref %s commit mismatch %s vs %s', target_ref,
957 target_ref.commit_id, self.branches[target_ref.name])
957 target_ref.commit_id, self.branches[target_ref.name])
958 return MergeResponse(
958 return MergeResponse(
959 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
959 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
960 metadata={'target_ref': target_ref})
960 metadata={'target_ref': target_ref})
961
961
962 shadow_repository_path = self._maybe_prepare_merge_workspace(
962 shadow_repository_path = self._maybe_prepare_merge_workspace(
963 repo_id, workspace_id, target_ref, source_ref)
963 repo_id, workspace_id, target_ref, source_ref)
964 shadow_repo = self.get_shadow_instance(shadow_repository_path)
964 shadow_repo = self.get_shadow_instance(shadow_repository_path)
965
965
966 # checkout source, if it's different. Otherwise we could not
966 # checkout source, if it's different. Otherwise we could not
967 # fetch proper commits for merge testing
967 # fetch proper commits for merge testing
968 if source_ref.name != target_ref.name:
968 if source_ref.name != target_ref.name:
969 if shadow_repo.get_remote_ref(source_ref.name):
969 if shadow_repo.get_remote_ref(source_ref.name):
970 shadow_repo._checkout(source_ref.name, force=True)
970 shadow_repo._checkout(source_ref.name, force=True)
971
971
972 # checkout target, and fetch changes
972 # checkout target, and fetch changes
973 shadow_repo._checkout(target_ref.name, force=True)
973 shadow_repo._checkout(target_ref.name, force=True)
974
974
975 # fetch/reset pull the target, in case it is changed
975 # fetch/reset pull the target, in case it is changed
976 # this handles even force changes
976 # this handles even force changes
977 shadow_repo._local_fetch(self.path, target_ref.name, use_origin=True)
977 shadow_repo._local_fetch(self.path, target_ref.name, use_origin=True)
978 shadow_repo._local_reset(target_ref.name)
978 shadow_repo._local_reset(target_ref.name)
979
979
980 # Need to reload repo to invalidate the cache, or otherwise we cannot
980 # Need to reload repo to invalidate the cache, or otherwise we cannot
981 # retrieve the last target commit.
981 # retrieve the last target commit.
982 shadow_repo = self.get_shadow_instance(shadow_repository_path)
982 shadow_repo = self.get_shadow_instance(shadow_repository_path)
983 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
983 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
984 log.warning('Shadow Target ref %s commit mismatch %s vs %s',
984 log.warning('Shadow Target ref %s commit mismatch %s vs %s',
985 target_ref, target_ref.commit_id,
985 target_ref, target_ref.commit_id,
986 shadow_repo.branches[target_ref.name])
986 shadow_repo.branches[target_ref.name])
987 return MergeResponse(
987 return MergeResponse(
988 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
988 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD,
989 metadata={'target_ref': target_ref})
989 metadata={'target_ref': target_ref})
990
990
991 # calculate new branch
991 # calculate new branch
992 pr_branch = shadow_repo._get_new_pr_branch(
992 pr_branch = shadow_repo._get_new_pr_branch(
993 source_ref.name, target_ref.name)
993 source_ref.name, target_ref.name)
994 log.debug('using pull-request merge branch: `%s`', pr_branch)
994 log.debug('using pull-request merge branch: `%s`', pr_branch)
995 # checkout to temp branch, and fetch changes
995 # checkout to temp branch, and fetch changes
996 shadow_repo._checkout(pr_branch, create=True)
996 shadow_repo._checkout(pr_branch, create=True)
997 try:
997 try:
998 shadow_repo._local_fetch(source_repo.path, source_ref.name)
998 shadow_repo._local_fetch(source_repo.path, source_ref.name)
999 except RepositoryError:
999 except RepositoryError:
1000 log.exception('Failure when doing local fetch on '
1000 log.exception('Failure when doing local fetch on '
1001 'shadow repo: %s', shadow_repo)
1001 'shadow repo: %s', shadow_repo)
1002 return MergeResponse(
1002 return MergeResponse(
1003 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
1003 False, False, None, MergeFailureReason.MISSING_SOURCE_REF,
1004 metadata={'source_ref': source_ref})
1004 metadata={'source_ref': source_ref})
1005
1005
1006 merge_ref = None
1006 merge_ref = None
1007 merge_failure_reason = MergeFailureReason.NONE
1007 merge_failure_reason = MergeFailureReason.NONE
1008 metadata = {}
1008 metadata = {}
1009 try:
1009 try:
1010 shadow_repo._local_merge(merge_message, merger_name, merger_email,
1010 shadow_repo._local_merge(merge_message, merger_name, merger_email,
1011 [source_ref.commit_id])
1011 [source_ref.commit_id])
1012 merge_possible = True
1012 merge_possible = True
1013
1013
1014 # Need to invalidate the cache, or otherwise we
1014 # Need to invalidate the cache, or otherwise we
1015 # cannot retrieve the merge commit.
1015 # cannot retrieve the merge commit.
1016 shadow_repo = shadow_repo.get_shadow_instance(shadow_repository_path)
1016 shadow_repo = shadow_repo.get_shadow_instance(shadow_repository_path)
1017 merge_commit_id = shadow_repo.branches[pr_branch]
1017 merge_commit_id = shadow_repo.branches[pr_branch]
1018
1018
1019 # Set a reference pointing to the merge commit. This reference may
1019 # Set a reference pointing to the merge commit. This reference may
1020 # be used to easily identify the last successful merge commit in
1020 # be used to easily identify the last successful merge commit in
1021 # the shadow repository.
1021 # the shadow repository.
1022 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
1022 shadow_repo.set_refs('refs/heads/pr-merge', merge_commit_id)
1023 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
1023 merge_ref = Reference('branch', 'pr-merge', merge_commit_id)
1024 except RepositoryError as e:
1024 except RepositoryError as e:
1025 log.exception('Failure when doing local merge on git shadow repo')
1025 log.exception('Failure when doing local merge on git shadow repo')
1026 if isinstance(e, UnresolvedFilesInRepo):
1026 if isinstance(e, UnresolvedFilesInRepo):
1027 metadata['unresolved_files'] = '\n* conflict: ' + ('\n * conflict: '.join(e.args[0]))
1027 metadata['unresolved_files'] = '\n* conflict: ' + ('\n * conflict: '.join(e.args[0]))
1028
1028
1029 merge_possible = False
1029 merge_possible = False
1030 merge_failure_reason = MergeFailureReason.MERGE_FAILED
1030 merge_failure_reason = MergeFailureReason.MERGE_FAILED
1031
1031
1032 if merge_possible and not dry_run:
1032 if merge_possible and not dry_run:
1033 try:
1033 try:
1034 shadow_repo._local_push(
1034 shadow_repo._local_push(
1035 pr_branch, self.path, target_ref.name, enable_hooks=True,
1035 pr_branch, self.path, target_ref.name, enable_hooks=True,
1036 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
1036 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
1037 merge_succeeded = True
1037 merge_succeeded = True
1038 except RepositoryError:
1038 except RepositoryError:
1039 log.exception(
1039 log.exception(
1040 'Failure when doing local push from the shadow '
1040 'Failure when doing local push from the shadow '
1041 'repository to the target repository at %s.', self.path)
1041 'repository to the target repository at %s.', self.path)
1042 merge_succeeded = False
1042 merge_succeeded = False
1043 merge_failure_reason = MergeFailureReason.PUSH_FAILED
1043 merge_failure_reason = MergeFailureReason.PUSH_FAILED
1044 metadata['target'] = 'git shadow repo'
1044 metadata['target'] = 'git shadow repo'
1045 metadata['merge_commit'] = pr_branch
1045 metadata['merge_commit'] = pr_branch
1046 else:
1046 else:
1047 merge_succeeded = False
1047 merge_succeeded = False
1048
1048
1049 return MergeResponse(
1049 return MergeResponse(
1050 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
1050 merge_possible, merge_succeeded, merge_ref, merge_failure_reason,
1051 metadata=metadata)
1051 metadata=metadata)
@@ -1,205 +1,205 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'user': '@'+h.person(user),
9 'user': '@'+h.person(user),
10 'repo_name': repo_name,
10 'repo_name': repo_name,
11 'status': status_change,
11 'status': status_change,
12 'comment_file': comment_file,
12 'comment_file': comment_file,
13 'comment_line': comment_line,
13 'comment_line': comment_line,
14 'comment_type': comment_type,
14 'comment_type': comment_type,
15 'comment_id': comment_id,
15 'comment_id': comment_id,
16
16
17 'pr_title': pull_request.title_safe,
17 'pr_title': pull_request.title_safe,
18 'pr_id': pull_request.pull_request_id,
18 'pr_id': pull_request.pull_request_id,
19 'mention_prefix': '[mention] ' if mention else '',
19 'mention_prefix': '[mention] ' if mention else '',
20 }
20 }
21
21
22 if comment_file:
22 if comment_file:
23 subject_template = email_pr_comment_file_subject_template or \
23 subject_template = email_pr_comment_file_subject_template or \
24 _('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data)
24 _('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data)
25 else:
25 else:
26 if status_change:
26 if status_change:
27 subject_template = email_pr_comment_status_change_subject_template or \
27 subject_template = email_pr_comment_status_change_subject_template or \
28 _('{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
28 _('{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
29 else:
29 else:
30 subject_template = email_pr_comment_subject_template or \
30 subject_template = email_pr_comment_subject_template or \
31 _('{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
31 _('{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
32 %>
32 %>
33
33
34 ${subject_template.format(**data) |n}
34 ${subject_template.format(**data) |n}
35 </%def>
35 </%def>
36
36
37 ## PLAINTEXT VERSION OF BODY
37 ## PLAINTEXT VERSION OF BODY
38 <%def name="body_plaintext()" filter="n,trim">
38 <%def name="body_plaintext()" filter="n,trim">
39 <%
39 <%
40 data = {
40 data = {
41 'user': h.person(user),
41 'user': h.person(user),
42 'repo_name': repo_name,
42 'repo_name': repo_name,
43 'status': status_change,
43 'status': status_change,
44 'comment_file': comment_file,
44 'comment_file': comment_file,
45 'comment_line': comment_line,
45 'comment_line': comment_line,
46 'comment_type': comment_type,
46 'comment_type': comment_type,
47 'comment_id': comment_id,
47 'comment_id': comment_id,
48
48
49 'pr_title': pull_request.title_safe,
49 'pr_title': pull_request.title_safe,
50 'pr_id': pull_request.pull_request_id,
50 'pr_id': pull_request.pull_request_id,
51 'source_ref_type': pull_request.source_ref_parts.type,
51 'source_ref_type': pull_request.source_ref_parts.type,
52 'source_ref_name': pull_request.source_ref_parts.name,
52 'source_ref_name': pull_request.source_ref_parts.name,
53 'target_ref_type': pull_request.target_ref_parts.type,
53 'target_ref_type': pull_request.target_ref_parts.type,
54 'target_ref_name': pull_request.target_ref_parts.name,
54 'target_ref_name': pull_request.target_ref_parts.name,
55 'source_repo': pull_request_source_repo.repo_name,
55 'source_repo': pull_request_source_repo.repo_name,
56 'target_repo': pull_request_target_repo.repo_name,
56 'target_repo': pull_request_target_repo.repo_name,
57 'source_repo_url': pull_request_source_repo_url,
57 'source_repo_url': pull_request_source_repo_url,
58 'target_repo_url': pull_request_target_repo_url,
58 'target_repo_url': pull_request_target_repo_url,
59 }
59 }
60 %>
60 %>
61
61
62 * ${_('Comment link')}: ${pr_comment_url}
62 * ${_('Comment link')}: ${pr_comment_url}
63
63
64 * ${_('Pull Request')}: !${pull_request.pull_request_id}
64 * ${_('Pull Request')}: !${pull_request.pull_request_id}
65
65
66 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
66 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
67
67
68 %if status_change and not closing_pr:
68 %if status_change and not closing_pr:
69 * ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
69 * ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
70
70
71 %elif status_change and closing_pr:
71 %elif status_change and closing_pr:
72 * ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
72 * ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
73
73
74 %endif
74 %endif
75 %if comment_file:
75 %if comment_file:
76 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
76 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
77
77
78 %endif
78 %endif
79 % if comment_type == 'todo':
79 % if comment_type == 'todo':
80 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
80 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
81 % else:
81 % else:
82 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
82 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
83 % endif
83 % endif
84
84
85 ${comment_body |n, trim}
85 ${comment_body |n, trim}
86
86
87 ---
87 ---
88 ${self.plaintext_footer()}
88 ${self.plaintext_footer()}
89 </%def>
89 </%def>
90
90
91
91
92 <%
92 <%
93 data = {
93 data = {
94 'user': h.person(user),
94 'user': h.person(user),
95 'comment_file': comment_file,
95 'comment_file': comment_file,
96 'comment_line': comment_line,
96 'comment_line': comment_line,
97 'comment_type': comment_type,
97 'comment_type': comment_type,
98 'comment_id': comment_id,
98 'comment_id': comment_id,
99 'renderer_type': renderer_type or 'plain',
99 'renderer_type': renderer_type or 'plain',
100
100
101 'pr_title': pull_request.title_safe,
101 'pr_title': pull_request.title_safe,
102 'pr_id': pull_request.pull_request_id,
102 'pr_id': pull_request.pull_request_id,
103 'status': status_change,
103 'status': status_change,
104 'source_ref_type': pull_request.source_ref_parts.type,
104 'source_ref_type': pull_request.source_ref_parts.type,
105 'source_ref_name': pull_request.source_ref_parts.name,
105 'source_ref_name': pull_request.source_ref_parts.name,
106 'target_ref_type': pull_request.target_ref_parts.type,
106 'target_ref_type': pull_request.target_ref_parts.type,
107 'target_ref_name': pull_request.target_ref_parts.name,
107 'target_ref_name': pull_request.target_ref_parts.name,
108 'source_repo': pull_request_source_repo.repo_name,
108 'source_repo': pull_request_source_repo.repo_name,
109 'target_repo': pull_request_target_repo.repo_name,
109 'target_repo': pull_request_target_repo.repo_name,
110 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
110 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
111 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
111 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
112 }
112 }
113 %>
113 %>
114
114
115 ## header
115 ## header
116 <table style="text-align:left;vertical-align:middle;width: 100%">
116 <table style="text-align:left;vertical-align:middle;width: 100%">
117 <tr>
117 <tr>
118 <td style="width:100%;border-bottom:1px solid #dbd9da;">
118 <td style="width:100%;border-bottom:1px solid #dbd9da;">
119
119
120 <div style="margin: 0; font-weight: bold">
120 <div style="margin: 0; font-weight: bold">
121 <div class="clear-both" style="margin-bottom: 4px">
121 <div class="clear-both" style="margin-bottom: 4px">
122 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
122 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
123 ${_('left a')}
123 ${_('left a')}
124 <a href="${pr_comment_url}" style="${base.link_css()}">
124 <a href="${pr_comment_url}" style="${base.link_css()}">
125 % if comment_file:
125 % if comment_file:
126 ${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
126 ${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
127 % else:
127 % else:
128 ${_('{comment_type} on pull request.').format(**data) |n}
128 ${_('{comment_type} on pull request.').format(**data) |n}
129 % endif
129 % endif
130 </a>
130 </a>
131 </div>
131 </div>
132 <div style="margin-top: 10px"></div>
132 <div style="margin-top: 10px"></div>
133 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
133 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
134 </div>
134 </div>
135
135
136 </td>
136 </td>
137 </tr>
137 </tr>
138
138
139 </table>
139 </table>
140 <div class="clear-both"></div>
140 <div class="clear-both"></div>
141 ## main body
141 ## main body
142 <table style="text-align:left;vertical-align:middle;width: 100%">
142 <table style="text-align:left;vertical-align:middle;width: 100%">
143
143
144 ## spacing def
144 ## spacing def
145 <tr>
145 <tr>
146 <td style="width: 130px"></td>
146 <td style="width: 130px"></td>
147 <td></td>
147 <td></td>
148 </tr>
148 </tr>
149
149
150 % if status_change:
150 % if status_change:
151 <tr>
151 <tr>
152 <td style="padding-right:20px;">${_('Review Status')}:</td>
152 <td style="padding-right:20px;">${_('Review Status')}:</td>
153 <td>
153 <td>
154 % if closing_pr:
154 % if closing_pr:
155 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
155 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
156 % else:
156 % else:
157 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
157 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
158 % endif
158 % endif
159 </td>
159 </td>
160 </tr>
160 </tr>
161 % endif
161 % endif
162 <tr>
162 <tr>
163 <td style="padding-right:20px;">${_('Pull request')}:</td>
163 <td style="padding-right:20px;">${_('Pull request')}:</td>
164 <td>
164 <td>
165 <a href="${pull_request_url}" style="${base.link_css()}">
165 <a href="${pull_request_url}" style="${base.link_css()}">
166 !${pull_request.pull_request_id}
166 !${pull_request.pull_request_id}
167 </a>
167 </a>
168 </td>
168 </td>
169 </tr>
169 </tr>
170
170
171 <tr>
171 <tr>
172 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
172 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
173 <td style="line-height:20px;">
173 <td style="line-height:20px;">
174 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
174 <code>${data['source_ref_type']}:${pull_request.source_ref_parts.name}</code> ${_('of')} ${data['source_repo_url']}
175 &rarr;
175 &rarr;
176 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
176 <code>${data['target_ref_type']}:${pull_request.target_ref_parts.name}</code> ${_('of')} ${data['target_repo_url']}
177 </td>
177 </td>
178 </tr>
178 </tr>
179
179
180 % if comment_file:
180 % if comment_file:
181 <tr>
181 <tr>
182 <td style="padding-right:20px;">${_('File')}:</td>
182 <td style="padding-right:20px;">${_('File')}:</td>
183 <td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
183 <td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
184 </tr>
184 </tr>
185 % endif
185 % endif
186
186
187 <tr style="border-bottom:1px solid #dbd9da;">
187 <tr style="border-bottom:1px solid #dbd9da;">
188 <td colspan="2" style="padding-right:20px;">
188 <td colspan="2" style="padding-right:20px;">
189 % if comment_type == 'todo':
189 % if comment_type == 'todo':
190 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
190 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
191 % else:
191 % else:
192 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
192 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
193 % endif
193 % endif
194 </td>
194 </td>
195 </tr>
195 </tr>
196
196
197 <tr>
197 <tr>
198 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
198 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
199 </tr>
199 </tr>
200
200
201 <tr>
201 <tr>
202 <td><a href="${pr_comment_reply_url}">${_('Reply')}</a></td>
202 <td><a href="${pr_comment_reply_url}">${_('Reply')}</a></td>
203 <td></td>
203 <td></td>
204 </tr>
204 </tr>
205 </table>
205 </table>
@@ -1,154 +1,154 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'user': '@'+h.person(user),
9 'user': '@'+h.person(user),
10 'pr_id': pull_request.pull_request_id,
10 'pr_id': pull_request.pull_request_id,
11 'pr_title': pull_request.title_safe,
11 'pr_title': pull_request.title_safe,
12 }
12 }
13
13
14 if user_role == 'observer':
14 if user_role == 'observer':
15 subject_template = email_pr_review_subject_template or _('{user} added you as observer to pull request. !{pr_id}: "{pr_title}"')
15 subject_template = email_pr_review_subject_template or _('{user} added you as observer to pull request. !{pr_id}: "{pr_title}"')
16 else:
16 else:
17 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
17 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
18 %>
18 %>
19
19
20 ${subject_template.format(**data) |n}
20 ${subject_template.format(**data) |n}
21 </%def>
21 </%def>
22
22
23 ## PLAINTEXT VERSION OF BODY
23 ## PLAINTEXT VERSION OF BODY
24 <%def name="body_plaintext()" filter="n,trim">
24 <%def name="body_plaintext()" filter="n,trim">
25 <%
25 <%
26 data = {
26 data = {
27 'user': h.person(user),
27 'user': h.person(user),
28 'pr_id': pull_request.pull_request_id,
28 'pr_id': pull_request.pull_request_id,
29 'pr_title': pull_request.title_safe,
29 'pr_title': pull_request.title_safe,
30 'source_ref_type': pull_request.source_ref_parts.type,
30 'source_ref_type': pull_request.source_ref_parts.type,
31 'source_ref_name': pull_request.source_ref_parts.name,
31 'source_ref_name': pull_request.source_ref_parts.name,
32 'target_ref_type': pull_request.target_ref_parts.type,
32 'target_ref_type': pull_request.target_ref_parts.type,
33 'target_ref_name': pull_request.target_ref_parts.name,
33 'target_ref_name': pull_request.target_ref_parts.name,
34 'repo_url': pull_request_source_repo_url,
34 'repo_url': pull_request_source_repo_url,
35 'source_repo': pull_request_source_repo.repo_name,
35 'source_repo': pull_request_source_repo.repo_name,
36 'target_repo': pull_request_target_repo.repo_name,
36 'target_repo': pull_request_target_repo.repo_name,
37 'source_repo_url': pull_request_source_repo_url,
37 'source_repo_url': pull_request_source_repo_url,
38 'target_repo_url': pull_request_target_repo_url,
38 'target_repo_url': pull_request_target_repo_url,
39 }
39 }
40
40
41 %>
41 %>
42
42
43 * ${_('Pull Request link')}: ${pull_request_url}
43 * ${_('Pull Request link')}: ${pull_request_url}
44
44
45 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
45 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
46
46
47 * ${_('Title')}: ${pull_request.title}
47 * ${_('Title')}: ${pull_request.title}
48
48
49 * ${_('Description')}:
49 * ${_('Description')}:
50
50
51 ${pull_request.description | trim}
51 ${pull_request.description | trim}
52
52
53
53
54 * ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
54 * ${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
55
55
56 % for commit_id, message in pull_request_commits:
56 % for commit_id, message in pull_request_commits:
57 - ${h.short_id(commit_id)}
57 - ${h.short_id(commit_id)}
58 ${h.chop_at_smart(message.lstrip(), '\n', suffix_if_chopped='...')}
58 ${h.chop_at_smart(message.lstrip(), '\n', suffix_if_chopped='...')}
59
59
60 % endfor
60 % endfor
61
61
62 ---
62 ---
63 ${self.plaintext_footer()}
63 ${self.plaintext_footer()}
64 </%def>
64 </%def>
65 <%
65 <%
66 data = {
66 data = {
67 'user': h.person(user),
67 'user': h.person(user),
68 'pr_id': pull_request.pull_request_id,
68 'pr_id': pull_request.pull_request_id,
69 'pr_title': pull_request.title_safe,
69 'pr_title': pull_request.title_safe,
70 'source_ref_type': pull_request.source_ref_parts.type,
70 'source_ref_type': pull_request.source_ref_parts.type,
71 'source_ref_name': pull_request.source_ref_parts.name,
71 'source_ref_name': pull_request.source_ref_parts.name,
72 'target_ref_type': pull_request.target_ref_parts.type,
72 'target_ref_type': pull_request.target_ref_parts.type,
73 'target_ref_name': pull_request.target_ref_parts.name,
73 'target_ref_name': pull_request.target_ref_parts.name,
74 'repo_url': pull_request_source_repo_url,
74 'repo_url': pull_request_source_repo_url,
75 'source_repo': pull_request_source_repo.repo_name,
75 'source_repo': pull_request_source_repo.repo_name,
76 'target_repo': pull_request_target_repo.repo_name,
76 'target_repo': pull_request_target_repo.repo_name,
77 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
77 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
78 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
78 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
79 }
79 }
80 %>
80 %>
81 ## header
81 ## header
82 <table style="text-align:left;vertical-align:middle;width: 100%">
82 <table style="text-align:left;vertical-align:middle;width: 100%">
83 <tr>
83 <tr>
84 <td style="width:100%;border-bottom:1px solid #dbd9da;">
84 <td style="width:100%;border-bottom:1px solid #dbd9da;">
85 <div style="margin: 0; font-weight: bold">
85 <div style="margin: 0; font-weight: bold">
86 % if user_role == 'observer':
86 % if user_role == 'observer':
87 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
87 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
88 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
88 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
89 ${_('added you as observer to')}
89 ${_('added you as observer to')}
90 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a>.
90 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a>.
91 </div>
91 </div>
92 % else:
92 % else:
93 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
93 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
94 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
94 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
95 ${_('requested a')}
95 ${_('requested a')}
96 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a> review.
96 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a> review.
97 </div>
97 </div>
98 % endif
98 % endif
99 <div style="margin-top: 10px"></div>
99 <div style="margin-top: 10px"></div>
100 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
100 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
101 </div>
101 </div>
102 </td>
102 </td>
103 </tr>
103 </tr>
104
104
105 </table>
105 </table>
106 <div class="clear-both"></div>
106 <div class="clear-both"></div>
107 ## main body
107 ## main body
108 <table style="text-align:left;vertical-align:middle;width: 100%">
108 <table style="text-align:left;vertical-align:middle;width: 100%">
109 ## spacing def
109 ## spacing def
110 <tr>
110 <tr>
111 <td style="width: 130px"></td>
111 <td style="width: 130px"></td>
112 <td></td>
112 <td></td>
113 </tr>
113 </tr>
114
114
115 <tr>
115 <tr>
116 <td style="padding-right:20px;">${_('Pull request')}:</td>
116 <td style="padding-right:20px;">${_('Pull request')}:</td>
117 <td>
117 <td>
118 <a href="${pull_request_url}" style="${base.link_css()}">
118 <a href="${pull_request_url}" style="${base.link_css()}">
119 !${pull_request.pull_request_id}
119 !${pull_request.pull_request_id}
120 </a>
120 </a>
121 </td>
121 </td>
122 </tr>
122 </tr>
123
123
124 <tr>
124 <tr>
125 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
125 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
126 <td style="line-height:20px;">
126 <td style="line-height:20px;">
127 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
127 <code>${data['source_ref_type']}:${pull_request.source_ref_parts.name}</code> ${_('of')} ${data['source_repo_url']}
128 &rarr;
128 &rarr;
129 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
129 <code>${data['target_ref_type']}:${pull_request.target_ref_parts.name}</code> ${_('of')} ${data['target_repo_url']}
130 </td>
130 </td>
131 </tr>
131 </tr>
132
132
133 <tr>
133 <tr>
134 <td style="padding-right:20px;">${_('Description')}:</td>
134 <td style="padding-right:20px;">${_('Description')}:</td>
135 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
135 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
136 </tr>
136 </tr>
137 <tr>
137 <tr>
138 <td style="padding-right:20px;">${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits)) % {'num': len(pull_request_commits)}}:</td>
138 <td style="padding-right:20px;">${_ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits)) % {'num': len(pull_request_commits)}}:</td>
139 <td></td>
139 <td></td>
140 </tr>
140 </tr>
141
141
142 <tr>
142 <tr>
143 <td colspan="2">
143 <td colspan="2">
144 <ol style="margin:0 0 0 1em;padding:0;text-align:left;">
144 <ol style="margin:0 0 0 1em;padding:0;text-align:left;">
145 % for commit_id, message in pull_request_commits:
145 % for commit_id, message in pull_request_commits:
146 <li style="margin:0 0 1em;">
146 <li style="margin:0 0 1em;">
147 <pre style="margin:0 0 .5em"><a href="${h.route_path('repo_commit', repo_name=pull_request_source_repo.repo_name, commit_id=commit_id)}" style="${base.link_css()}">${h.short_id(commit_id)}</a></pre>
147 <pre style="margin:0 0 .5em"><a href="${h.route_path('repo_commit', repo_name=pull_request_source_repo.repo_name, commit_id=commit_id)}" style="${base.link_css()}">${h.short_id(commit_id)}</a></pre>
148 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
148 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
149 </li>
149 </li>
150 % endfor
150 % endfor
151 </ol>
151 </ol>
152 </td>
152 </td>
153 </tr>
153 </tr>
154 </table>
154 </table>
@@ -1,172 +1,172 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'updating_user': '@'+h.person(updating_user),
9 'updating_user': '@'+h.person(updating_user),
10 'pr_id': pull_request.pull_request_id,
10 'pr_id': pull_request.pull_request_id,
11 'pr_title': pull_request.title_safe,
11 'pr_title': pull_request.title_safe,
12 }
12 }
13
13
14 subject_template = email_pr_update_subject_template or _('{updating_user} updated pull request. !{pr_id}: "{pr_title}"')
14 subject_template = email_pr_update_subject_template or _('{updating_user} updated pull request. !{pr_id}: "{pr_title}"')
15 %>
15 %>
16
16
17 ${subject_template.format(**data) |n}
17 ${subject_template.format(**data) |n}
18 </%def>
18 </%def>
19
19
20 ## PLAINTEXT VERSION OF BODY
20 ## PLAINTEXT VERSION OF BODY
21 <%def name="body_plaintext()" filter="n,trim">
21 <%def name="body_plaintext()" filter="n,trim">
22 <%
22 <%
23 data = {
23 data = {
24 'updating_user': h.person(updating_user),
24 'updating_user': h.person(updating_user),
25 'pr_id': pull_request.pull_request_id,
25 'pr_id': pull_request.pull_request_id,
26 'pr_title': pull_request.title_safe,
26 'pr_title': pull_request.title_safe,
27 'source_ref_type': pull_request.source_ref_parts.type,
27 'source_ref_type': pull_request.source_ref_parts.type,
28 'source_ref_name': pull_request.source_ref_parts.name,
28 'source_ref_name': pull_request.source_ref_parts.name,
29 'target_ref_type': pull_request.target_ref_parts.type,
29 'target_ref_type': pull_request.target_ref_parts.type,
30 'target_ref_name': pull_request.target_ref_parts.name,
30 'target_ref_name': pull_request.target_ref_parts.name,
31 'repo_url': pull_request_source_repo_url,
31 'repo_url': pull_request_source_repo_url,
32 'source_repo': pull_request_source_repo.repo_name,
32 'source_repo': pull_request_source_repo.repo_name,
33 'target_repo': pull_request_target_repo.repo_name,
33 'target_repo': pull_request_target_repo.repo_name,
34 'source_repo_url': pull_request_source_repo_url,
34 'source_repo_url': pull_request_source_repo_url,
35 'target_repo_url': pull_request_target_repo_url,
35 'target_repo_url': pull_request_target_repo_url,
36 }
36 }
37 %>
37 %>
38
38
39 * ${_('Pull Request link')}: ${pull_request_url}
39 * ${_('Pull Request link')}: ${pull_request_url}
40
40
41 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
41 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
42
42
43 * ${_('Title')}: ${pull_request.title}
43 * ${_('Title')}: ${pull_request.title}
44
44
45 * ${_('Description')}:
45 * ${_('Description')}:
46
46
47 ${pull_request.description | trim}
47 ${pull_request.description | trim}
48
48
49 * Changed commits:
49 * Changed commits:
50
50
51 - Added: ${len(added_commits)}
51 - Added: ${len(added_commits)}
52 - Removed: ${len(removed_commits)}
52 - Removed: ${len(removed_commits)}
53
53
54 * Changed files:
54 * Changed files:
55
55
56 %if not changed_files:
56 %if not changed_files:
57 No file changes found
57 No file changes found
58 %else:
58 %else:
59 %for file_name in added_files:
59 %for file_name in added_files:
60 - A `${file_name}`
60 - A `${file_name}`
61 %endfor
61 %endfor
62 %for file_name in modified_files:
62 %for file_name in modified_files:
63 - M `${file_name}`
63 - M `${file_name}`
64 %endfor
64 %endfor
65 %for file_name in removed_files:
65 %for file_name in removed_files:
66 - R `${file_name}`
66 - R `${file_name}`
67 %endfor
67 %endfor
68 %endif
68 %endif
69
69
70 ---
70 ---
71 ${self.plaintext_footer()}
71 ${self.plaintext_footer()}
72 </%def>
72 </%def>
73 <%
73 <%
74 data = {
74 data = {
75 'updating_user': h.person(updating_user),
75 'updating_user': h.person(updating_user),
76 'pr_id': pull_request.pull_request_id,
76 'pr_id': pull_request.pull_request_id,
77 'pr_title': pull_request.title_safe,
77 'pr_title': pull_request.title_safe,
78 'source_ref_type': pull_request.source_ref_parts.type,
78 'source_ref_type': pull_request.source_ref_parts.type,
79 'source_ref_name': pull_request.source_ref_parts.name,
79 'source_ref_name': pull_request.source_ref_parts.name,
80 'target_ref_type': pull_request.target_ref_parts.type,
80 'target_ref_type': pull_request.target_ref_parts.type,
81 'target_ref_name': pull_request.target_ref_parts.name,
81 'target_ref_name': pull_request.target_ref_parts.name,
82 'repo_url': pull_request_source_repo_url,
82 'repo_url': pull_request_source_repo_url,
83 'source_repo': pull_request_source_repo.repo_name,
83 'source_repo': pull_request_source_repo.repo_name,
84 'target_repo': pull_request_target_repo.repo_name,
84 'target_repo': pull_request_target_repo.repo_name,
85 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
85 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
86 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
86 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
87 }
87 }
88 %>
88 %>
89
89
90 ## header
90 ## header
91 <table style="text-align:left;vertical-align:middle;width: 100%">
91 <table style="text-align:left;vertical-align:middle;width: 100%">
92 <tr>
92 <tr>
93 <td style="width:100%;border-bottom:1px solid #dbd9da;">
93 <td style="width:100%;border-bottom:1px solid #dbd9da;">
94
94
95 <div style="margin: 0; font-weight: bold">
95 <div style="margin: 0; font-weight: bold">
96 <div class="clear-both" style="margin-bottom: 4px">
96 <div class="clear-both" style="margin-bottom: 4px">
97 <span style="color:#7E7F7F">@${h.person(updating_user.username)}</span>
97 <span style="color:#7E7F7F">@${h.person(updating_user.username)}</span>
98 ${_('updated')}
98 ${_('updated')}
99 <a href="${pull_request_url}" style="${base.link_css()}">
99 <a href="${pull_request_url}" style="${base.link_css()}">
100 ${_('pull request.').format(**data) }
100 ${_('pull request.').format(**data) }
101 </a>
101 </a>
102 </div>
102 </div>
103 <div style="margin-top: 10px"></div>
103 <div style="margin-top: 10px"></div>
104 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
104 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
105 </div>
105 </div>
106
106
107 </td>
107 </td>
108 </tr>
108 </tr>
109
109
110 </table>
110 </table>
111 <div class="clear-both"></div>
111 <div class="clear-both"></div>
112 ## main body
112 ## main body
113 <table style="text-align:left;vertical-align:middle;width: 100%">
113 <table style="text-align:left;vertical-align:middle;width: 100%">
114 ## spacing def
114 ## spacing def
115 <tr>
115 <tr>
116 <td style="width: 130px"></td>
116 <td style="width: 130px"></td>
117 <td></td>
117 <td></td>
118 </tr>
118 </tr>
119
119
120 <tr>
120 <tr>
121 <td style="padding-right:20px;">${_('Pull request')}:</td>
121 <td style="padding-right:20px;">${_('Pull request')}:</td>
122 <td>
122 <td>
123 <a href="${pull_request_url}" style="${base.link_css()}">
123 <a href="${pull_request_url}" style="${base.link_css()}">
124 !${pull_request.pull_request_id}
124 !${pull_request.pull_request_id}
125 </a>
125 </a>
126 </td>
126 </td>
127 </tr>
127 </tr>
128
128
129 <tr>
129 <tr>
130 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
130 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
131 <td style="line-height:20px;">
131 <td style="line-height:20px;">
132 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
132 <code>${data['source_ref_type']}:${pull_request.source_ref_parts.name}</code> ${_('of')} ${data['source_repo_url']}
133 &rarr;
133 &rarr;
134 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
134 <code>${data['target_ref_type']}:${pull_request.target_ref_parts.name}</code> ${_('of')} ${data['target_repo_url']}
135 </td>
135 </td>
136 </tr>
136 </tr>
137
137
138 <tr>
138 <tr>
139 <td style="padding-right:20px;">${_('Description')}:</td>
139 <td style="padding-right:20px;">${_('Description')}:</td>
140 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
140 <td style="white-space:pre-wrap"><code>${pull_request.description | trim}</code></td>
141 </tr>
141 </tr>
142 <tr>
142 <tr>
143 <td style="padding-right:20px;">${_('Changes')}:</td>
143 <td style="padding-right:20px;">${_('Changes')}:</td>
144 <td>
144 <td>
145 <strong>Changed commits:</strong>
145 <strong>Changed commits:</strong>
146 <ul class="changes-ul">
146 <ul class="changes-ul">
147 <li>- Added: ${len(added_commits)}</li>
147 <li>- Added: ${len(added_commits)}</li>
148 <li>- Removed: ${len(removed_commits)}</li>
148 <li>- Removed: ${len(removed_commits)}</li>
149 </ul>
149 </ul>
150
150
151 <strong>Changed files:</strong>
151 <strong>Changed files:</strong>
152 <ul class="changes-ul">
152 <ul class="changes-ul">
153
153
154 %if not changed_files:
154 %if not changed_files:
155 <li>No file changes found</li>
155 <li>No file changes found</li>
156 %else:
156 %else:
157 %for file_name in added_files:
157 %for file_name in added_files:
158 <li>- A <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
158 <li>- A <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
159 %endfor
159 %endfor
160 %for file_name in modified_files:
160 %for file_name in modified_files:
161 <li>- M <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
161 <li>- M <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
162 %endfor
162 %endfor
163 %for file_name in removed_files:
163 %for file_name in removed_files:
164 <li>- R <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
164 <li>- R <a href="${pull_request_url + '#a_' + h.FID(ancestor_commit_id, file_name)}">${file_name}</a></li>
165 %endfor
165 %endfor
166 %endif
166 %endif
167
167
168 </ul>
168 </ul>
169 </td>
169 </td>
170 </tr>
170 </tr>
171
171
172 </table>
172 </table>
@@ -1,1055 +1,1055 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 <%namespace name="sidebar" file="/base/sidebar.mako"/>
4 <%namespace name="sidebar" file="/base/sidebar.mako"/>
5
5
6
6
7 <%def name="title()">
7 <%def name="title()">
8 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
8 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
9 %if c.rhodecode_name:
9 %if c.rhodecode_name:
10 &middot; ${h.branding(c.rhodecode_name)}
10 &middot; ${h.branding(c.rhodecode_name)}
11 %endif
11 %endif
12 </%def>
12 </%def>
13
13
14 <%def name="breadcrumbs_links()">
14 <%def name="breadcrumbs_links()">
15
15
16 </%def>
16 </%def>
17
17
18 <%def name="menu_bar_nav()">
18 <%def name="menu_bar_nav()">
19 ${self.menu_items(active='repositories')}
19 ${self.menu_items(active='repositories')}
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_subnav()">
22 <%def name="menu_bar_subnav()">
23 ${self.repo_menu(active='showpullrequest')}
23 ${self.repo_menu(active='showpullrequest')}
24 </%def>
24 </%def>
25
25
26
26
27 <%def name="main()">
27 <%def name="main()">
28 ## Container to gather extracted Tickets
28 ## Container to gather extracted Tickets
29 <%
29 <%
30 c.referenced_commit_issues = h.IssuesRegistry()
30 c.referenced_commit_issues = h.IssuesRegistry()
31 c.referenced_desc_issues = h.IssuesRegistry()
31 c.referenced_desc_issues = h.IssuesRegistry()
32 %>
32 %>
33
33
34 <script type="text/javascript">
34 <script type="text/javascript">
35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
36 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
37 </script>
37 </script>
38
38
39 <div class="box">
39 <div class="box">
40
40
41 <div class="box pr-summary">
41 <div class="box pr-summary">
42
42
43 <div class="summary-details block-left">
43 <div class="summary-details block-left">
44 <div id="pr-title">
44 <div id="pr-title">
45 % if c.pull_request.is_closed():
45 % if c.pull_request.is_closed():
46 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
46 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
47 % endif
47 % endif
48 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
48 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
49 </div>
49 </div>
50 <div id="pr-title-edit" class="input" style="display: none;">
50 <div id="pr-title-edit" class="input" style="display: none;">
51 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
51 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
52 </div>
52 </div>
53
53
54 <% summary = lambda n:{False:'summary-short'}.get(n) %>
54 <% summary = lambda n:{False:'summary-short'}.get(n) %>
55 <div class="pr-details-title">
55 <div class="pr-details-title">
56 <div class="pull-left">
56 <div class="pull-left">
57 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a>
57 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a>
58 ${_('Created on')}
58 ${_('Created on')}
59 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
59 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
60 <span class="pr-details-title-author-pref">${_('by')}</span>
60 <span class="pr-details-title-author-pref">${_('by')}</span>
61 </div>
61 </div>
62
62
63 <div class="pull-left">
63 <div class="pull-left">
64 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
64 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
65 </div>
65 </div>
66
66
67 %if c.allowed_to_update:
67 %if c.allowed_to_update:
68 <div class="pull-right">
68 <div class="pull-right">
69 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
69 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
70 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
70 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
71 % if c.allowed_to_delete:
71 % if c.allowed_to_delete:
72 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
72 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
73 <input class="btn btn-link btn-danger no-margin" id="remove_${c.pull_request.pull_request_id}" name="remove_${c.pull_request.pull_request_id}"
73 <input class="btn btn-link btn-danger no-margin" id="remove_${c.pull_request.pull_request_id}" name="remove_${c.pull_request.pull_request_id}"
74 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
74 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
75 type="submit" value="${_('Delete pull request')}">
75 type="submit" value="${_('Delete pull request')}">
76 ${h.end_form()}
76 ${h.end_form()}
77 % else:
77 % else:
78 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
78 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
79 % endif
79 % endif
80 </div>
80 </div>
81 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
81 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
82 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
82 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
83 </div>
83 </div>
84
84
85 %endif
85 %endif
86 </div>
86 </div>
87
87
88 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
88 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
89 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container_callback=c.referenced_desc_issues())}
89 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container_callback=c.referenced_desc_issues())}
90 </div>
90 </div>
91
91
92 <div id="pr-desc-edit" class="input textarea" style="display: none;">
92 <div id="pr-desc-edit" class="input textarea" style="display: none;">
93 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
93 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
94 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
94 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
95 </div>
95 </div>
96
96
97 <div id="summary" class="fields pr-details-content">
97 <div id="summary" class="fields pr-details-content">
98
98
99 ## source
99 ## source
100 <div class="field">
100 <div class="field">
101 <div class="label-pr-detail">
101 <div class="label-pr-detail">
102 <label>${_('Commit flow')}:</label>
102 <label>${_('Commit flow')}:</label>
103 </div>
103 </div>
104 <div class="input">
104 <div class="input">
105 <div class="pr-commit-flow">
105 <div class="pr-commit-flow">
106 ## Source
106 ## Source
107 %if c.pull_request.source_ref_parts.type == 'branch':
107 %if c.pull_request.source_ref_parts.type == 'branch':
108 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}</code></a>
108 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}</code></a>
109 %else:
109 %else:
110 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
110 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
111 %endif
111 %endif
112 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.repo_name}</a>
112 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.repo_name}</a>
113 &rarr;
113 &rarr;
114 ## Target
114 ## Target
115 %if c.pull_request.target_ref_parts.type == 'branch':
115 %if c.pull_request.target_ref_parts.type == 'branch':
116 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}"><code class="pr-target-info">${c.pull_request.target_ref_parts.type}:${c.pull_request.target_ref_parts.name}</code></a>
116 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}"><code class="pr-target-info">${c.pull_request.target_ref_parts.type}:${c.pull_request.target_ref_parts.name}</code></a>
117 %else:
117 %else:
118 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
118 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
119 %endif
119 %endif
120
120
121 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
121 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
122
122
123 <a class="source-details-action" href="#expand-source-details" onclick="return toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
123 <a class="source-details-action" href="#expand-source-details" onclick="return toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
124 <i class="icon-angle-down">more details</i>
124 <i class="icon-angle-down">more details</i>
125 </a>
125 </a>
126
126
127 </div>
127 </div>
128
128
129 <div class="source-details" style="display: none">
129 <div class="source-details" style="display: none">
130
130
131 <ul>
131 <ul>
132
132
133 ## common ancestor
133 ## common ancestor
134 <li>
134 <li>
135 ${_('Common ancestor')}:
135 ${_('Common ancestor')}:
136 % if c.ancestor_commit:
136 % if c.ancestor_commit:
137 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a>
137 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a>
138 % else:
138 % else:
139 ${_('not available')}
139 ${_('not available')}
140 % endif
140 % endif
141 </li>
141 </li>
142
142
143 ## pull url
143 ## pull url
144 <li>
144 <li>
145 %if h.is_hg(c.pull_request.source_repo):
145 %if h.is_hg(c.pull_request.source_repo):
146 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
146 <% clone_url = u'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
147 %elif h.is_git(c.pull_request.source_repo):
147 %elif h.is_git(c.pull_request.source_repo):
148 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
148 <% clone_url = u'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
149 %endif
149 %endif
150
150
151 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
151 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
152 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
152 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
153 </li>
153 </li>
154
154
155 ## Shadow repo
155 ## Shadow repo
156 <li>
156 <li>
157 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
157 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
158 %if h.is_hg(c.pull_request.target_repo):
158 %if h.is_hg(c.pull_request.target_repo):
159 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
159 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
160 %elif h.is_git(c.pull_request.target_repo):
160 %elif h.is_git(c.pull_request.target_repo):
161 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
161 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
162 %endif
162 %endif
163
163
164 <span class="tooltip" title="${_('Clone repository in its merged state using shadow repository')}">${_('Clone from shadow repository')}</span>: <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
164 <span class="tooltip" title="${_('Clone repository in its merged state using shadow repository')}">${_('Clone from shadow repository')}</span>: <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
165 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
165 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
166
166
167 % else:
167 % else:
168 <div class="">
168 <div class="">
169 ${_('Shadow repository data not available')}.
169 ${_('Shadow repository data not available')}.
170 </div>
170 </div>
171 % endif
171 % endif
172 </li>
172 </li>
173
173
174 </ul>
174 </ul>
175
175
176 </div>
176 </div>
177
177
178 </div>
178 </div>
179
179
180 </div>
180 </div>
181
181
182 ## versions
182 ## versions
183 <div class="field">
183 <div class="field">
184 <div class="label-pr-detail">
184 <div class="label-pr-detail">
185 <label>${_('Versions')}:</label>
185 <label>${_('Versions')}:</label>
186 </div>
186 </div>
187
187
188 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
188 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
189 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
189 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
190
190
191 <div class="pr-versions">
191 <div class="pr-versions">
192 % if c.show_version_changes:
192 % if c.show_version_changes:
193 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
193 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
194 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
194 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
195 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
195 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
196 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
196 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
197 data-toggle-on="${_('show versions')}."
197 data-toggle-on="${_('show versions')}."
198 data-toggle-off="${_('hide versions')}.">
198 data-toggle-off="${_('hide versions')}.">
199 ${_('show versions')}.
199 ${_('show versions')}.
200 </a>
200 </a>
201 <table>
201 <table>
202 ## SHOW ALL VERSIONS OF PR
202 ## SHOW ALL VERSIONS OF PR
203 <% ver_pr = None %>
203 <% ver_pr = None %>
204
204
205 % for data in reversed(list(enumerate(c.versions, 1))):
205 % for data in reversed(list(enumerate(c.versions, 1))):
206 <% ver_pos = data[0] %>
206 <% ver_pos = data[0] %>
207 <% ver = data[1] %>
207 <% ver = data[1] %>
208 <% ver_pr = ver.pull_request_version_id %>
208 <% ver_pr = ver.pull_request_version_id %>
209 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
209 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
210
210
211 <tr class="version-pr" style="display: ${display_row}">
211 <tr class="version-pr" style="display: ${display_row}">
212 <td>
212 <td>
213 <code>
213 <code>
214 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
214 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
215 </code>
215 </code>
216 </td>
216 </td>
217 <td>
217 <td>
218 <input ${('checked="checked"' if c.from_version_index == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
218 <input ${('checked="checked"' if c.from_version_index == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
219 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
219 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
220 </td>
220 </td>
221 <td>
221 <td>
222 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
222 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
223 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
223 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
224
224
225 </td>
225 </td>
226 <td>
226 <td>
227 % if c.at_version_num != ver_pr:
227 % if c.at_version_num != ver_pr:
228 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
228 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
229 <code>
229 <code>
230 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
230 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
231 </code>
231 </code>
232 % endif
232 % endif
233 </td>
233 </td>
234 <td>
234 <td>
235 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
235 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
236 </td>
236 </td>
237 <td>
237 <td>
238 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
238 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
239 </td>
239 </td>
240 </tr>
240 </tr>
241 % endfor
241 % endfor
242
242
243 <tr>
243 <tr>
244 <td colspan="6">
244 <td colspan="6">
245 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
245 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
246 data-label-text-locked="${_('select versions to show changes')}"
246 data-label-text-locked="${_('select versions to show changes')}"
247 data-label-text-diff="${_('show changes between versions')}"
247 data-label-text-diff="${_('show changes between versions')}"
248 data-label-text-show="${_('show pull request for this version')}"
248 data-label-text-show="${_('show pull request for this version')}"
249 >
249 >
250 ${_('select versions to show changes')}
250 ${_('select versions to show changes')}
251 </button>
251 </button>
252 </td>
252 </td>
253 </tr>
253 </tr>
254 </table>
254 </table>
255 % else:
255 % else:
256 <div>
256 <div>
257 ${_('Pull request versions not available')}.
257 ${_('Pull request versions not available')}.
258 </div>
258 </div>
259 % endif
259 % endif
260 </div>
260 </div>
261 </div>
261 </div>
262
262
263 </div>
263 </div>
264
264
265 </div>
265 </div>
266
266
267
267
268 </div>
268 </div>
269
269
270 </div>
270 </div>
271
271
272 <div class="box">
272 <div class="box">
273
273
274 % if c.state_progressing:
274 % if c.state_progressing:
275
275
276 <h2 style="text-align: center">
276 <h2 style="text-align: center">
277 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
277 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
278
278
279 % if c.is_super_admin:
279 % if c.is_super_admin:
280 <br/>
280 <br/>
281 If you think this is an error try <a href="${h.current_route_path(request, force_state='created')}">forced state reset</a> to <span class="tag tag-merge-state-created">created</span> state.
281 If you think this is an error try <a href="${h.current_route_path(request, force_state='created')}">forced state reset</a> to <span class="tag tag-merge-state-created">created</span> state.
282 % endif
282 % endif
283 </h2>
283 </h2>
284
284
285 % else:
285 % else:
286
286
287 ## Diffs rendered here
287 ## Diffs rendered here
288 <div class="table" >
288 <div class="table" >
289 <div id="changeset_compare_view_content">
289 <div id="changeset_compare_view_content">
290 ##CS
290 ##CS
291 % if c.missing_requirements:
291 % if c.missing_requirements:
292 <div class="box">
292 <div class="box">
293 <div class="alert alert-warning">
293 <div class="alert alert-warning">
294 <div>
294 <div>
295 <strong>${_('Missing requirements:')}</strong>
295 <strong>${_('Missing requirements:')}</strong>
296 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
296 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
297 </div>
297 </div>
298 </div>
298 </div>
299 </div>
299 </div>
300 % elif c.missing_commits:
300 % elif c.missing_commits:
301 <div class="box">
301 <div class="box">
302 <div class="alert alert-warning">
302 <div class="alert alert-warning">
303 <div>
303 <div>
304 <strong>${_('Missing commits')}:</strong>
304 <strong>${_('Missing commits')}:</strong>
305 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
305 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
306 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
306 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
307 ${_('Consider doing a `force update commits` in case you think this is an error.')}
307 ${_('Consider doing a `force update commits` in case you think this is an error.')}
308 </div>
308 </div>
309 </div>
309 </div>
310 </div>
310 </div>
311 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
311 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
312 <div class="box">
312 <div class="box">
313 <div class="alert alert-info">
313 <div class="alert alert-info">
314 <div>
314 <div>
315 <strong>${_('There are new changes for `{}:{}` in source repository, please consider updating this pull request.').format(c.pr_merge_source_commit.ref_spec.type, c.pr_merge_source_commit.ref_spec.name)}</strong>
315 <strong>${_('There are new changes for `{}:{}` in source repository, please consider updating this pull request.').format(c.pr_merge_source_commit.ref_spec.type, c.pr_merge_source_commit.ref_spec.name)}</strong>
316 </div>
316 </div>
317 </div>
317 </div>
318 </div>
318 </div>
319 % endif
319 % endif
320
320
321 <div class="compare_view_commits_title">
321 <div class="compare_view_commits_title">
322 % if not c.compare_mode:
322 % if not c.compare_mode:
323
323
324 % if c.at_version_index:
324 % if c.at_version_index:
325 <h4>
325 <h4>
326 ${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
326 ${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
327 </h4>
327 </h4>
328 % endif
328 % endif
329
329
330 <div class="pull-left">
330 <div class="pull-left">
331 <div class="btn-group">
331 <div class="btn-group">
332 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
332 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
333 % if c.collapse_all_commits:
333 % if c.collapse_all_commits:
334 <i class="icon-plus-squared-alt icon-no-margin"></i>
334 <i class="icon-plus-squared-alt icon-no-margin"></i>
335 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
335 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
336 % else:
336 % else:
337 <i class="icon-minus-squared-alt icon-no-margin"></i>
337 <i class="icon-minus-squared-alt icon-no-margin"></i>
338 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
338 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
339 % endif
339 % endif
340 </a>
340 </a>
341 </div>
341 </div>
342 </div>
342 </div>
343
343
344 <div class="pull-right">
344 <div class="pull-right">
345 % if c.allowed_to_update and not c.pull_request.is_closed():
345 % if c.allowed_to_update and not c.pull_request.is_closed():
346
346
347 <div class="btn-group btn-group-actions">
347 <div class="btn-group btn-group-actions">
348 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
348 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
349 ${_('Update commits')}
349 ${_('Update commits')}
350 </a>
350 </a>
351
351
352 <a id="update_commits_switcher" class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
352 <a id="update_commits_switcher" class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
353 <i class="icon-down"></i>
353 <i class="icon-down"></i>
354 </a>
354 </a>
355
355
356 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
356 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
357 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
357 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
358 <li>
358 <li>
359 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
359 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
360 ${_('Force update commits')}
360 ${_('Force update commits')}
361 </a>
361 </a>
362 <div class="action-help-block">
362 <div class="action-help-block">
363 ${_('Update commits and force refresh this pull request.')}
363 ${_('Update commits and force refresh this pull request.')}
364 </div>
364 </div>
365 </li>
365 </li>
366 </ul>
366 </ul>
367 </div>
367 </div>
368 </div>
368 </div>
369
369
370 % else:
370 % else:
371 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
371 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
372 % endif
372 % endif
373
373
374 </div>
374 </div>
375 % endif
375 % endif
376 </div>
376 </div>
377
377
378 % if not c.missing_commits:
378 % if not c.missing_commits:
379 ## COMPARE RANGE DIFF MODE
379 ## COMPARE RANGE DIFF MODE
380 % if c.compare_mode:
380 % if c.compare_mode:
381 % if c.at_version:
381 % if c.at_version:
382 <h4>
382 <h4>
383 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_index, ver_to=c.at_version_index if c.at_version_index else 'latest')}:
383 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_index, ver_to=c.at_version_index if c.at_version_index else 'latest')}:
384 </h4>
384 </h4>
385
385
386 <div class="subtitle-compare">
386 <div class="subtitle-compare">
387 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
387 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
388 </div>
388 </div>
389
389
390 <div class="container">
390 <div class="container">
391 <table class="rctable compare_view_commits">
391 <table class="rctable compare_view_commits">
392 <tr>
392 <tr>
393 <th></th>
393 <th></th>
394 <th>${_('Time')}</th>
394 <th>${_('Time')}</th>
395 <th>${_('Author')}</th>
395 <th>${_('Author')}</th>
396 <th>${_('Commit')}</th>
396 <th>${_('Commit')}</th>
397 <th></th>
397 <th></th>
398 <th>${_('Description')}</th>
398 <th>${_('Description')}</th>
399 </tr>
399 </tr>
400
400
401 % for c_type, commit in c.commit_changes:
401 % for c_type, commit in c.commit_changes:
402 % if c_type in ['a', 'r']:
402 % if c_type in ['a', 'r']:
403 <%
403 <%
404 if c_type == 'a':
404 if c_type == 'a':
405 cc_title = _('Commit added in displayed changes')
405 cc_title = _('Commit added in displayed changes')
406 elif c_type == 'r':
406 elif c_type == 'r':
407 cc_title = _('Commit removed in displayed changes')
407 cc_title = _('Commit removed in displayed changes')
408 else:
408 else:
409 cc_title = ''
409 cc_title = ''
410 %>
410 %>
411 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
411 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
412 <td>
412 <td>
413 <div class="commit-change-indicator color-${c_type}-border">
413 <div class="commit-change-indicator color-${c_type}-border">
414 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
414 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
415 ${c_type.upper()}
415 ${c_type.upper()}
416 </div>
416 </div>
417 </div>
417 </div>
418 </td>
418 </td>
419 <td class="td-time">
419 <td class="td-time">
420 ${h.age_component(commit.date)}
420 ${h.age_component(commit.date)}
421 </td>
421 </td>
422 <td class="td-user">
422 <td class="td-user">
423 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
423 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
424 </td>
424 </td>
425 <td class="td-hash">
425 <td class="td-hash">
426 <code>
426 <code>
427 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
427 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
428 r${commit.idx}:${h.short_id(commit.raw_id)}
428 r${commit.idx}:${h.short_id(commit.raw_id)}
429 </a>
429 </a>
430 ${h.hidden('revisions', commit.raw_id)}
430 ${h.hidden('revisions', commit.raw_id)}
431 </code>
431 </code>
432 </td>
432 </td>
433 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
433 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
434 <i class="icon-expand-linked"></i>
434 <i class="icon-expand-linked"></i>
435 </td>
435 </td>
436 <td class="mid td-description">
436 <td class="mid td-description">
437 <div class="log-container truncate-wrap">
437 <div class="log-container truncate-wrap">
438 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name, issues_container_callback=c.referenced_commit_issues(commit.serialize()))}</div>
438 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name, issues_container_callback=c.referenced_commit_issues(commit.serialize()))}</div>
439 </div>
439 </div>
440 </td>
440 </td>
441 </tr>
441 </tr>
442 % endif
442 % endif
443 % endfor
443 % endfor
444 </table>
444 </table>
445 </div>
445 </div>
446
446
447 % endif
447 % endif
448
448
449 ## Regular DIFF
449 ## Regular DIFF
450 % else:
450 % else:
451 <%include file="/compare/compare_commits.mako" />
451 <%include file="/compare/compare_commits.mako" />
452 % endif
452 % endif
453
453
454 <div class="cs_files">
454 <div class="cs_files">
455 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
455 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
456
456
457 <%
457 <%
458 pr_menu_data = {
458 pr_menu_data = {
459 'outdated_comm_count_ver': outdated_comm_count_ver,
459 'outdated_comm_count_ver': outdated_comm_count_ver,
460 'pull_request': c.pull_request
460 'pull_request': c.pull_request
461 }
461 }
462 %>
462 %>
463
463
464 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
464 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
465
465
466 % if c.range_diff_on:
466 % if c.range_diff_on:
467 % for commit in c.commit_ranges:
467 % for commit in c.commit_ranges:
468 ${cbdiffs.render_diffset(
468 ${cbdiffs.render_diffset(
469 c.changes[commit.raw_id],
469 c.changes[commit.raw_id],
470 commit=commit, use_comments=True,
470 commit=commit, use_comments=True,
471 collapse_when_files_over=5,
471 collapse_when_files_over=5,
472 disable_new_comments=True,
472 disable_new_comments=True,
473 deleted_files_comments=c.deleted_files_comments,
473 deleted_files_comments=c.deleted_files_comments,
474 inline_comments=c.inline_comments,
474 inline_comments=c.inline_comments,
475 pull_request_menu=pr_menu_data, show_todos=False)}
475 pull_request_menu=pr_menu_data, show_todos=False)}
476 % endfor
476 % endfor
477 % else:
477 % else:
478 ${cbdiffs.render_diffset(
478 ${cbdiffs.render_diffset(
479 c.diffset, use_comments=True,
479 c.diffset, use_comments=True,
480 collapse_when_files_over=30,
480 collapse_when_files_over=30,
481 disable_new_comments=not c.allowed_to_comment,
481 disable_new_comments=not c.allowed_to_comment,
482 deleted_files_comments=c.deleted_files_comments,
482 deleted_files_comments=c.deleted_files_comments,
483 inline_comments=c.inline_comments,
483 inline_comments=c.inline_comments,
484 pull_request_menu=pr_menu_data, show_todos=False)}
484 pull_request_menu=pr_menu_data, show_todos=False)}
485 % endif
485 % endif
486
486
487 </div>
487 </div>
488 % else:
488 % else:
489 ## skipping commits we need to clear the view for missing commits
489 ## skipping commits we need to clear the view for missing commits
490 <div style="clear:both;"></div>
490 <div style="clear:both;"></div>
491 % endif
491 % endif
492
492
493 </div>
493 </div>
494 </div>
494 </div>
495
495
496 ## template for inline comment form
496 ## template for inline comment form
497 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
497 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
498
498
499 ## comments heading with count
499 ## comments heading with count
500 <div class="comments-heading">
500 <div class="comments-heading">
501 <i class="icon-comment"></i>
501 <i class="icon-comment"></i>
502 ${_('General Comments')} ${len(c.comments)}
502 ${_('General Comments')} ${len(c.comments)}
503 </div>
503 </div>
504
504
505 ## render general comments
505 ## render general comments
506 <div id="comment-tr-show">
506 <div id="comment-tr-show">
507 % if general_outdated_comm_count_ver:
507 % if general_outdated_comm_count_ver:
508 <div class="info-box">
508 <div class="info-box">
509 % if general_outdated_comm_count_ver == 1:
509 % if general_outdated_comm_count_ver == 1:
510 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
510 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
511 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
511 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
512 % else:
512 % else:
513 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
513 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
514 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
514 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
515 % endif
515 % endif
516 </div>
516 </div>
517 % endif
517 % endif
518 </div>
518 </div>
519
519
520 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
520 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
521
521
522 % if not c.pull_request.is_closed():
522 % if not c.pull_request.is_closed():
523 ## main comment form and it status
523 ## main comment form and it status
524 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
524 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
525 pull_request_id=c.pull_request.pull_request_id),
525 pull_request_id=c.pull_request.pull_request_id),
526 c.pull_request_review_status,
526 c.pull_request_review_status,
527 is_pull_request=True, change_status=c.allowed_to_change_status)}
527 is_pull_request=True, change_status=c.allowed_to_change_status)}
528
528
529 ## merge status, and merge action
529 ## merge status, and merge action
530 <div class="pull-request-merge">
530 <div class="pull-request-merge">
531 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
531 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
532 </div>
532 </div>
533
533
534 %endif
534 %endif
535
535
536 % endif
536 % endif
537 </div>
537 </div>
538
538
539
539
540 ### NAV SIDEBAR
540 ### NAV SIDEBAR
541 <aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
541 <aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
542 <div class="sidenav navbar__inner" >
542 <div class="sidenav navbar__inner" >
543 ## TOGGLE
543 ## TOGGLE
544 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
544 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
545 <a href="#toggleSidebar" class="grey-link-action">
545 <a href="#toggleSidebar" class="grey-link-action">
546
546
547 </a>
547 </a>
548 </div>
548 </div>
549
549
550 ## CONTENT
550 ## CONTENT
551 <div class="sidebar-content">
551 <div class="sidebar-content">
552
552
553 ## Drafts
553 ## Drafts
554 % if c.rhodecode_edition_id == 'EE':
554 % if c.rhodecode_edition_id == 'EE':
555 <div id="draftsTable" class="sidebar-element clear-both" style="display: ${'block' if c.draft_comments else 'none'}">
555 <div id="draftsTable" class="sidebar-element clear-both" style="display: ${'block' if c.draft_comments else 'none'}">
556 <div class="tooltip right-sidebar-collapsed-state" style="display: none;" onclick="toggleSidebar(); return false" title="${_('Drafts')}">
556 <div class="tooltip right-sidebar-collapsed-state" style="display: none;" onclick="toggleSidebar(); return false" title="${_('Drafts')}">
557 <i class="icon-comment icon-draft"></i>
557 <i class="icon-comment icon-draft"></i>
558 <span id="drafts-count">${len(c.draft_comments)}</span>
558 <span id="drafts-count">${len(c.draft_comments)}</span>
559 </div>
559 </div>
560
560
561 <div class="right-sidebar-expanded-state pr-details-title">
561 <div class="right-sidebar-expanded-state pr-details-title">
562 <span style="padding-left: 2px">
562 <span style="padding-left: 2px">
563 <input name="select_all_drafts" type="checkbox" onclick="$('[name=submit_draft]').prop('checked', !$('[name=submit_draft]').prop('checked'))">
563 <input name="select_all_drafts" type="checkbox" onclick="$('[name=submit_draft]').prop('checked', !$('[name=submit_draft]').prop('checked'))">
564 </span>
564 </span>
565 <span class="sidebar-heading noselect" onclick="refreshDraftComments(); return false">
565 <span class="sidebar-heading noselect" onclick="refreshDraftComments(); return false">
566 <i class="icon-comment icon-draft"></i>
566 <i class="icon-comment icon-draft"></i>
567 ${_('Drafts')}
567 ${_('Drafts')}
568 </span>
568 </span>
569 <span class="block-right action_button last-item" onclick="submitDrafts(event)">${_('Submit')}</span>
569 <span class="block-right action_button last-item" onclick="submitDrafts(event)">${_('Submit')}</span>
570 </div>
570 </div>
571
571
572 <div id="drafts" class="right-sidebar-expanded-state pr-details-content reviewers">
572 <div id="drafts" class="right-sidebar-expanded-state pr-details-content reviewers">
573 % if c.draft_comments:
573 % if c.draft_comments:
574 ${sidebar.comments_table(c.draft_comments, len(c.draft_comments), draft_comments=True)}
574 ${sidebar.comments_table(c.draft_comments, len(c.draft_comments), draft_comments=True)}
575 % else:
575 % else:
576 <table class="drafts-content-table">
576 <table class="drafts-content-table">
577 <tr>
577 <tr>
578 <td>
578 <td>
579 ${_('No TODOs yet')}
579 ${_('No TODOs yet')}
580 </td>
580 </td>
581 </tr>
581 </tr>
582 </table>
582 </table>
583 % endif
583 % endif
584 </div>
584 </div>
585
585
586 </div>
586 </div>
587 % endif
587 % endif
588
588
589 ## RULES SUMMARY/RULES
589 ## RULES SUMMARY/RULES
590 <div class="sidebar-element clear-both">
590 <div class="sidebar-element clear-both">
591 <% vote_title = _ungettext(
591 <% vote_title = _ungettext(
592 'Status calculated based on votes from {} reviewer',
592 'Status calculated based on votes from {} reviewer',
593 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
593 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
594 %>
594 %>
595
595
596 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
596 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
597 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
597 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
598 ${c.reviewers_count}
598 ${c.reviewers_count}
599 </div>
599 </div>
600
600
601 ## REVIEWERS
601 ## REVIEWERS
602 <div class="right-sidebar-expanded-state pr-details-title">
602 <div class="right-sidebar-expanded-state pr-details-title">
603 <span class="tooltip sidebar-heading" title="${vote_title}">
603 <span class="tooltip sidebar-heading" title="${vote_title}">
604 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
604 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
605 ${_('Reviewers')}
605 ${_('Reviewers')}
606 </span>
606 </span>
607
607
608 %if c.allowed_to_update:
608 %if c.allowed_to_update:
609 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
609 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
610 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
610 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
611 %else:
611 %else:
612 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
612 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
613 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
613 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
614 %endif
614 %endif
615 </div>
615 </div>
616
616
617 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
617 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
618
618
619 <div id="review_rules" style="display: none" class="">
619 <div id="review_rules" style="display: none" class="">
620
620
621 <strong>${_('Reviewer rules')}</strong>
621 <strong>${_('Reviewer rules')}</strong>
622 <div class="pr-reviewer-rules">
622 <div class="pr-reviewer-rules">
623 ## review rules will be appended here, by default reviewers logic
623 ## review rules will be appended here, by default reviewers logic
624 </div>
624 </div>
625 <input id="review_data" type="hidden" name="review_data" value="">
625 <input id="review_data" type="hidden" name="review_data" value="">
626 </div>
626 </div>
627
627
628 ## members redering block
628 ## members redering block
629 <input type="hidden" name="__start__" value="review_members:sequence">
629 <input type="hidden" name="__start__" value="review_members:sequence">
630
630
631 <table id="review_members" class="group_members">
631 <table id="review_members" class="group_members">
632 ## This content is loaded via JS and ReviewersPanel
632 ## This content is loaded via JS and ReviewersPanel
633 </table>
633 </table>
634
634
635 <input type="hidden" name="__end__" value="review_members:sequence">
635 <input type="hidden" name="__end__" value="review_members:sequence">
636 ## end members redering block
636 ## end members redering block
637
637
638 %if not c.pull_request.is_closed():
638 %if not c.pull_request.is_closed():
639 <div id="add_reviewer" class="ac" style="display: none;">
639 <div id="add_reviewer" class="ac" style="display: none;">
640 %if c.allowed_to_update:
640 %if c.allowed_to_update:
641 % if not c.forbid_adding_reviewers:
641 % if not c.forbid_adding_reviewers:
642 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
642 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
643 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
643 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
644 <div id="reviewers_container"></div>
644 <div id="reviewers_container"></div>
645 </div>
645 </div>
646 % endif
646 % endif
647 <div class="pull-right" style="margin-bottom: 15px">
647 <div class="pull-right" style="margin-bottom: 15px">
648 <button data-role="reviewer" id="update_reviewers" class="btn btn-sm no-margin">${_('Save Changes')}</button>
648 <button data-role="reviewer" id="update_reviewers" class="btn btn-sm no-margin">${_('Save Changes')}</button>
649 </div>
649 </div>
650 %endif
650 %endif
651 </div>
651 </div>
652 %endif
652 %endif
653 </div>
653 </div>
654 </div>
654 </div>
655
655
656 ## OBSERVERS
656 ## OBSERVERS
657 % if c.rhodecode_edition_id == 'EE':
657 % if c.rhodecode_edition_id == 'EE':
658 <div class="sidebar-element clear-both">
658 <div class="sidebar-element clear-both">
659 <% vote_title = _ungettext(
659 <% vote_title = _ungettext(
660 '{} observer without voting right.',
660 '{} observer without voting right.',
661 '{} observers without voting right.', c.observers_count).format(c.observers_count)
661 '{} observers without voting right.', c.observers_count).format(c.observers_count)
662 %>
662 %>
663
663
664 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
664 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
665 <i class="icon-circle-thin"></i>
665 <i class="icon-circle-thin"></i>
666 ${c.observers_count}
666 ${c.observers_count}
667 </div>
667 </div>
668
668
669 <div class="right-sidebar-expanded-state pr-details-title">
669 <div class="right-sidebar-expanded-state pr-details-title">
670 <span class="tooltip sidebar-heading" title="${vote_title}">
670 <span class="tooltip sidebar-heading" title="${vote_title}">
671 <i class="icon-circle-thin"></i>
671 <i class="icon-circle-thin"></i>
672 ${_('Observers')}
672 ${_('Observers')}
673 </span>
673 </span>
674 %if c.allowed_to_update:
674 %if c.allowed_to_update:
675 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
675 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
676 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
676 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
677 %endif
677 %endif
678 </div>
678 </div>
679
679
680 <div id="observers" class="right-sidebar-expanded-state pr-details-content reviewers">
680 <div id="observers" class="right-sidebar-expanded-state pr-details-content reviewers">
681 ## members redering block
681 ## members redering block
682 <input type="hidden" name="__start__" value="observer_members:sequence">
682 <input type="hidden" name="__start__" value="observer_members:sequence">
683
683
684 <table id="observer_members" class="group_members">
684 <table id="observer_members" class="group_members">
685 ## This content is loaded via JS and ReviewersPanel
685 ## This content is loaded via JS and ReviewersPanel
686 </table>
686 </table>
687
687
688 <input type="hidden" name="__end__" value="observer_members:sequence">
688 <input type="hidden" name="__end__" value="observer_members:sequence">
689 ## end members redering block
689 ## end members redering block
690
690
691 %if not c.pull_request.is_closed():
691 %if not c.pull_request.is_closed():
692 <div id="add_observer" class="ac" style="display: none;">
692 <div id="add_observer" class="ac" style="display: none;">
693 %if c.allowed_to_update:
693 %if c.allowed_to_update:
694 % if not c.forbid_adding_reviewers or 1:
694 % if not c.forbid_adding_reviewers or 1:
695 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px" >
695 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px" >
696 <input class="ac-input" id="observer" name="observer" placeholder="${_('Add observer or observer group')}" type="text" autocomplete="off">
696 <input class="ac-input" id="observer" name="observer" placeholder="${_('Add observer or observer group')}" type="text" autocomplete="off">
697 <div id="observers_container"></div>
697 <div id="observers_container"></div>
698 </div>
698 </div>
699 % endif
699 % endif
700 <div class="pull-right" style="margin-bottom: 15px">
700 <div class="pull-right" style="margin-bottom: 15px">
701 <button data-role="observer" id="update_observers" class="btn btn-sm no-margin">${_('Save Changes')}</button>
701 <button data-role="observer" id="update_observers" class="btn btn-sm no-margin">${_('Save Changes')}</button>
702 </div>
702 </div>
703 %endif
703 %endif
704 </div>
704 </div>
705 %endif
705 %endif
706 </div>
706 </div>
707 </div>
707 </div>
708 % endif
708 % endif
709
709
710 ## TODOs
710 ## TODOs
711 <div id="todosTable" class="sidebar-element clear-both">
711 <div id="todosTable" class="sidebar-element clear-both">
712 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
712 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
713 <i class="icon-flag-filled"></i>
713 <i class="icon-flag-filled"></i>
714 <span id="todos-count">${len(c.unresolved_comments)}</span>
714 <span id="todos-count">${len(c.unresolved_comments)}</span>
715 </div>
715 </div>
716
716
717 <div class="right-sidebar-expanded-state pr-details-title">
717 <div class="right-sidebar-expanded-state pr-details-title">
718 ## Only show unresolved, that is only what matters
718 ## Only show unresolved, that is only what matters
719 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
719 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
720 <i class="icon-flag-filled"></i>
720 <i class="icon-flag-filled"></i>
721 TODOs
721 TODOs
722 </span>
722 </span>
723
723
724 % if not c.at_version:
724 % if not c.at_version:
725 % if c.resolved_comments:
725 % if c.resolved_comments:
726 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
726 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
727 % else:
727 % else:
728 <span class="block-right last-item noselect">Show resolved</span>
728 <span class="block-right last-item noselect">Show resolved</span>
729 % endif
729 % endif
730 % endif
730 % endif
731 </div>
731 </div>
732
732
733 <div class="right-sidebar-expanded-state pr-details-content">
733 <div class="right-sidebar-expanded-state pr-details-content">
734
734
735 % if c.at_version:
735 % if c.at_version:
736 <table>
736 <table>
737 <tr>
737 <tr>
738 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
738 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
739 </tr>
739 </tr>
740 </table>
740 </table>
741 % else:
741 % else:
742 % if c.unresolved_comments + c.resolved_comments:
742 % if c.unresolved_comments + c.resolved_comments:
743 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
743 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
744 % else:
744 % else:
745 <table class="todos-content-table">
745 <table class="todos-content-table">
746 <tr>
746 <tr>
747 <td>
747 <td>
748 ${_('No TODOs yet')}
748 ${_('No TODOs yet')}
749 </td>
749 </td>
750 </tr>
750 </tr>
751 </table>
751 </table>
752 % endif
752 % endif
753 % endif
753 % endif
754 </div>
754 </div>
755 </div>
755 </div>
756
756
757 ## COMMENTS
757 ## COMMENTS
758 <div id="commentsTable" class="sidebar-element clear-both">
758 <div id="commentsTable" class="sidebar-element clear-both">
759 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
759 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
760 <i class="icon-comment" style="color: #949494"></i>
760 <i class="icon-comment" style="color: #949494"></i>
761 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
761 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
762 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
762 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
763 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
763 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
764 </div>
764 </div>
765
765
766 <div class="right-sidebar-expanded-state pr-details-title">
766 <div class="right-sidebar-expanded-state pr-details-title">
767 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
767 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
768 <i class="icon-comment" style="color: #949494"></i>
768 <i class="icon-comment" style="color: #949494"></i>
769 ${_('Comments')}
769 ${_('Comments')}
770
770
771 ## % if outdated_comm_count_ver:
771 ## % if outdated_comm_count_ver:
772 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
772 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
773 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
773 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
774 ## </a>
774 ## </a>
775 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
775 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
776 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
776 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
777
777
778 ## % else:
778 ## % else:
779 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
779 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
780 ## % endif
780 ## % endif
781
781
782 </span>
782 </span>
783
783
784 % if outdated_comm_count_ver:
784 % if outdated_comm_count_ver:
785 <span class="block-right action_button last-item noselect" onclick="return toggleElement(this, '.hidden-comment');" data-toggle-on="Show outdated" data-toggle-off="Hide outdated">Show outdated</span>
785 <span class="block-right action_button last-item noselect" onclick="return toggleElement(this, '.hidden-comment');" data-toggle-on="Show outdated" data-toggle-off="Hide outdated">Show outdated</span>
786 % else:
786 % else:
787 <span class="block-right last-item noselect">Show hidden</span>
787 <span class="block-right last-item noselect">Show hidden</span>
788 % endif
788 % endif
789
789
790 </div>
790 </div>
791
791
792 <div class="right-sidebar-expanded-state pr-details-content">
792 <div class="right-sidebar-expanded-state pr-details-content">
793 % if c.inline_comments_flat + c.comments:
793 % if c.inline_comments_flat + c.comments:
794 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
794 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
795 % else:
795 % else:
796 <table class="comments-content-table">
796 <table class="comments-content-table">
797 <tr>
797 <tr>
798 <td>
798 <td>
799 ${_('No Comments yet')}
799 ${_('No Comments yet')}
800 </td>
800 </td>
801 </tr>
801 </tr>
802 </table>
802 </table>
803 % endif
803 % endif
804 </div>
804 </div>
805
805
806 </div>
806 </div>
807
807
808 ## Referenced Tickets
808 ## Referenced Tickets
809 <div class="sidebar-element clear-both">
809 <div class="sidebar-element clear-both">
810 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
810 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
811 <i class="icon-info-circled"></i>
811 <i class="icon-info-circled"></i>
812 ${(c.referenced_desc_issues.issues_unique_count + c.referenced_commit_issues.issues_unique_count)}
812 ${(c.referenced_desc_issues.issues_unique_count + c.referenced_commit_issues.issues_unique_count)}
813 </div>
813 </div>
814
814
815 <div class="right-sidebar-expanded-state pr-details-title">
815 <div class="right-sidebar-expanded-state pr-details-title">
816 <span class="sidebar-heading">
816 <span class="sidebar-heading">
817 <i class="icon-info-circled"></i>
817 <i class="icon-info-circled"></i>
818 ${_('Referenced Tickets')}
818 ${_('Referenced Tickets')}
819 </span>
819 </span>
820 </div>
820 </div>
821 <div class="right-sidebar-expanded-state pr-details-content">
821 <div class="right-sidebar-expanded-state pr-details-content">
822 <table>
822 <table>
823
823
824 <tr><td><code>${_('In pull request description')}:</code></td></tr>
824 <tr><td><code>${_('In pull request description')}:</code></td></tr>
825 % if c.referenced_desc_issues.issues:
825 % if c.referenced_desc_issues.issues:
826
826
827 % for ticket_id, ticket_dict in c.referenced_desc_issues.unique_issues.items():
827 % for ticket_id, ticket_dict in c.referenced_desc_issues.unique_issues.items():
828 <tr>
828 <tr>
829 <td>
829 <td>
830 <a href="${ticket_dict[0].get('url')}">
830 <a href="${ticket_dict[0].get('url')}">
831 ${ticket_id}
831 ${ticket_id}
832 </a>
832 </a>
833 </td>
833 </td>
834 </tr>
834 </tr>
835
835
836 % endfor
836 % endfor
837 % else:
837 % else:
838 <tr>
838 <tr>
839 <td>
839 <td>
840 ${_('No Ticket data found.')}
840 ${_('No Ticket data found.')}
841 </td>
841 </td>
842 </tr>
842 </tr>
843 % endif
843 % endif
844
844
845 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
845 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
846 % if c.referenced_commit_issues.issues:
846 % if c.referenced_commit_issues.issues:
847 % for ticket_id, ticket_dict in c.referenced_commit_issues.unique_issues.items():
847 % for ticket_id, ticket_dict in c.referenced_commit_issues.unique_issues.items():
848 <tr>
848 <tr>
849 <td>
849 <td>
850 <a href="${ticket_dict[0].get('url')}">
850 <a href="${ticket_dict[0].get('url')}">
851 ${ticket_id}
851 ${ticket_id}
852 </a>
852 </a>
853 - ${_ungettext('in %s commit', 'in %s commits', len(ticket_dict)) % (len(ticket_dict))}
853 - ${_ungettext('in %s commit', 'in %s commits', len(ticket_dict)) % (len(ticket_dict))}
854 </td>
854 </td>
855 </tr>
855 </tr>
856 % endfor
856 % endfor
857 % else:
857 % else:
858 <tr>
858 <tr>
859 <td>
859 <td>
860 ${_('No Ticket data found.')}
860 ${_('No Ticket data found.')}
861 </td>
861 </td>
862 </tr>
862 </tr>
863 % endif
863 % endif
864 </table>
864 </table>
865
865
866 </div>
866 </div>
867 </div>
867 </div>
868
868
869 </div>
869 </div>
870
870
871 </div>
871 </div>
872 </aside>
872 </aside>
873
873
874 ## This JS needs to be at the end
874 ## This JS needs to be at the end
875 <script type="text/javascript">
875 <script type="text/javascript">
876
876
877 versionController = new VersionController();
877 versionController = new VersionController();
878 versionController.init();
878 versionController.init();
879
879
880 reviewersController = new ReviewersController();
880 reviewersController = new ReviewersController();
881 commitsController = new CommitsController();
881 commitsController = new CommitsController();
882 commentsController = new CommentsController();
882 commentsController = new CommentsController();
883
883
884 updateController = new UpdatePrController();
884 updateController = new UpdatePrController();
885
885
886 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
886 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
887 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
887 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
888 window.setObserversData = ${c.pull_request_set_observers_data_json | n};
888 window.setObserversData = ${c.pull_request_set_observers_data_json | n};
889
889
890 (function () {
890 (function () {
891 "use strict";
891 "use strict";
892
892
893 // custom code mirror
893 // custom code mirror
894 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
894 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
895
895
896 PRDetails.init();
896 PRDetails.init();
897 ReviewersPanel.init(reviewersController, reviewerRulesData, setReviewersData);
897 ReviewersPanel.init(reviewersController, reviewerRulesData, setReviewersData);
898 ObserversPanel.init(reviewersController, reviewerRulesData, setObserversData);
898 ObserversPanel.init(reviewersController, reviewerRulesData, setObserversData);
899
899
900 window.showOutdated = function (self) {
900 window.showOutdated = function (self) {
901 $('.comment-inline.comment-outdated').show();
901 $('.comment-inline.comment-outdated').show();
902 $('.filediff-outdated').show();
902 $('.filediff-outdated').show();
903 $('.showOutdatedComments').hide();
903 $('.showOutdatedComments').hide();
904 $('.hideOutdatedComments').show();
904 $('.hideOutdatedComments').show();
905 };
905 };
906
906
907 window.hideOutdated = function (self) {
907 window.hideOutdated = function (self) {
908 $('.comment-inline.comment-outdated').hide();
908 $('.comment-inline.comment-outdated').hide();
909 $('.filediff-outdated').hide();
909 $('.filediff-outdated').hide();
910 $('.hideOutdatedComments').hide();
910 $('.hideOutdatedComments').hide();
911 $('.showOutdatedComments').show();
911 $('.showOutdatedComments').show();
912 };
912 };
913
913
914 window.refreshMergeChecks = function () {
914 window.refreshMergeChecks = function () {
915 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
915 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
916 $('.pull-request-merge').css('opacity', 0.3);
916 $('.pull-request-merge').css('opacity', 0.3);
917 $('.action-buttons-extra').css('opacity', 0.3);
917 $('.action-buttons-extra').css('opacity', 0.3);
918
918
919 $('.pull-request-merge').load(
919 $('.pull-request-merge').load(
920 loadUrl, function () {
920 loadUrl, function () {
921 $('.pull-request-merge').css('opacity', 1);
921 $('.pull-request-merge').css('opacity', 1);
922
922
923 $('.action-buttons-extra').css('opacity', 1);
923 $('.action-buttons-extra').css('opacity', 1);
924 }
924 }
925 );
925 );
926 };
926 };
927
927
928 window.submitDrafts = function (event) {
928 window.submitDrafts = function (event) {
929 var target = $(event.currentTarget);
929 var target = $(event.currentTarget);
930 var callback = function (result) {
930 var callback = function (result) {
931 target.removeAttr('onclick').html('saving...');
931 target.removeAttr('onclick').html('saving...');
932 }
932 }
933 var draftIds = [];
933 var draftIds = [];
934 $.each($('[name=submit_draft]:checked'), function (idx, val) {
934 $.each($('[name=submit_draft]:checked'), function (idx, val) {
935 draftIds.push(parseInt($(val).val()));
935 draftIds.push(parseInt($(val).val()));
936 })
936 })
937 if (draftIds.length > 0) {
937 if (draftIds.length > 0) {
938 Rhodecode.comments.finalizeDrafts(draftIds, callback);
938 Rhodecode.comments.finalizeDrafts(draftIds, callback);
939 }
939 }
940 else {
940 else {
941
941
942 }
942 }
943 }
943 }
944
944
945 window.closePullRequest = function (status) {
945 window.closePullRequest = function (status) {
946 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
946 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
947 return false;
947 return false;
948 }
948 }
949 // inject closing flag
949 // inject closing flag
950 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
950 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
951 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
951 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
952 $(generalCommentForm.submitForm).submit();
952 $(generalCommentForm.submitForm).submit();
953 };
953 };
954
954
955 //TODO this functionality is now missing
955 //TODO this functionality is now missing
956 $('#show-outdated-comments').on('click', function (e) {
956 $('#show-outdated-comments').on('click', function (e) {
957 var button = $(this);
957 var button = $(this);
958 var outdated = $('.comment-outdated');
958 var outdated = $('.comment-outdated');
959
959
960 if (button.html() === "(Show)") {
960 if (button.html() === "(Show)") {
961 button.html("(Hide)");
961 button.html("(Hide)");
962 outdated.show();
962 outdated.show();
963 } else {
963 } else {
964 button.html("(Show)");
964 button.html("(Show)");
965 outdated.hide();
965 outdated.hide();
966 }
966 }
967 });
967 });
968
968
969 $('#merge_pull_request_form').submit(function () {
969 $('#merge_pull_request_form').submit(function () {
970 if (!$('#merge_pull_request').attr('disabled')) {
970 if (!$('#merge_pull_request').attr('disabled')) {
971 $('#merge_pull_request').attr('disabled', 'disabled');
971 $('#merge_pull_request').attr('disabled', 'disabled');
972 }
972 }
973 return true;
973 return true;
974 });
974 });
975
975
976 $('#edit_pull_request').on('click', function (e) {
976 $('#edit_pull_request').on('click', function (e) {
977 var title = $('#pr-title-input').val();
977 var title = $('#pr-title-input').val();
978 var description = codeMirrorInstance.getValue();
978 var description = codeMirrorInstance.getValue();
979 var renderer = $('#pr-renderer-input').val();
979 var renderer = $('#pr-renderer-input').val();
980 editPullRequest(
980 editPullRequest(
981 "${c.repo_name}", "${c.pull_request.pull_request_id}",
981 "${c.repo_name}", "${c.pull_request.pull_request_id}",
982 title, description, renderer);
982 title, description, renderer);
983 });
983 });
984
984
985 var $updateButtons = $('#update_reviewers,#update_observers');
985 var $updateButtons = $('#update_reviewers,#update_observers');
986 $updateButtons.on('click', function (e) {
986 $updateButtons.on('click', function (e) {
987 var role = $(this).data('role');
987 var role = $(this).data('role');
988 $updateButtons.attr('disabled', 'disabled');
988 $updateButtons.attr('disabled', 'disabled');
989 $updateButtons.addClass('disabled');
989 $updateButtons.addClass('disabled');
990 $updateButtons.html(_gettext('Saving...'));
990 $updateButtons.html(_gettext('Saving...'));
991 reviewersController.updateReviewers(
991 reviewersController.updateReviewers(
992 templateContext.repo_name,
992 templateContext.repo_name,
993 templateContext.pull_request_data.pull_request_id,
993 templateContext.pull_request_data.pull_request_id,
994 role
994 role
995 );
995 );
996 });
996 });
997
997
998 // fixing issue with caches on firefox
998 // fixing issue with caches on firefox
999 $('#update_commits').removeAttr("disabled");
999 $('#update_commits').removeAttr("disabled");
1000
1000
1001 $('.show-inline-comments').on('click', function (e) {
1001 $('.show-inline-comments').on('click', function (e) {
1002 var boxid = $(this).attr('data-comment-id');
1002 var boxid = $(this).attr('data-comment-id');
1003 var button = $(this);
1003 var button = $(this);
1004
1004
1005 if (button.hasClass("comments-visible")) {
1005 if (button.hasClass("comments-visible")) {
1006 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
1006 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
1007 $(this).hide();
1007 $(this).hide();
1008 });
1008 });
1009 button.removeClass("comments-visible");
1009 button.removeClass("comments-visible");
1010 } else {
1010 } else {
1011 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
1011 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
1012 $(this).show();
1012 $(this).show();
1013 });
1013 });
1014 button.addClass("comments-visible");
1014 button.addClass("comments-visible");
1015 }
1015 }
1016 });
1016 });
1017
1017
1018 $('.show-inline-comments').on('change', function (e) {
1018 $('.show-inline-comments').on('change', function (e) {
1019 var show = 'none';
1019 var show = 'none';
1020 var target = e.currentTarget;
1020 var target = e.currentTarget;
1021 if (target.checked) {
1021 if (target.checked) {
1022 show = ''
1022 show = ''
1023 }
1023 }
1024 var boxid = $(target).attr('id_for');
1024 var boxid = $(target).attr('id_for');
1025 var comments = $('#{0} .inline-comments'.format(boxid));
1025 var comments = $('#{0} .inline-comments'.format(boxid));
1026 var fn_display = function (idx) {
1026 var fn_display = function (idx) {
1027 $(this).css('display', show);
1027 $(this).css('display', show);
1028 };
1028 };
1029 $(comments).each(fn_display);
1029 $(comments).each(fn_display);
1030 var btns = $('#{0} .inline-comments-button'.format(boxid));
1030 var btns = $('#{0} .inline-comments-button'.format(boxid));
1031 $(btns).each(fn_display);
1031 $(btns).each(fn_display);
1032 });
1032 });
1033
1033
1034 // register submit callback on commentForm form to track TODOs, and refresh mergeChecks conditions
1034 // register submit callback on commentForm form to track TODOs, and refresh mergeChecks conditions
1035 window.commentFormGlobalSubmitSuccessCallback = function (comment) {
1035 window.commentFormGlobalSubmitSuccessCallback = function (comment) {
1036 if (!comment.draft) {
1036 if (!comment.draft) {
1037 refreshMergeChecks();
1037 refreshMergeChecks();
1038 }
1038 }
1039 };
1039 };
1040
1040
1041 ReviewerAutoComplete('#user', reviewersController);
1041 ReviewerAutoComplete('#user', reviewersController);
1042 ObserverAutoComplete('#observer', reviewersController);
1042 ObserverAutoComplete('#observer', reviewersController);
1043
1043
1044 })();
1044 })();
1045
1045
1046 $(document).ready(function () {
1046 $(document).ready(function () {
1047
1047
1048 var channel = '${c.pr_broadcast_channel}';
1048 var channel = '${c.pr_broadcast_channel}';
1049 new ReviewerPresenceController(channel)
1049 new ReviewerPresenceController(channel)
1050 // register globally so inject comment logic can re-use it.
1050 // register globally so inject comment logic can re-use it.
1051 window.commentsController = commentsController;
1051 window.commentsController = commentsController;
1052 })
1052 })
1053 </script>
1053 </script>
1054
1054
1055 </%def>
1055 </%def>
General Comments 1
Under Review
author

Auto status change to "Under Review"

You need to be logged in to leave comments. Login now