##// END OF EJS Templates
diffs: fixed outdated files in pull-requests re-using the filediff raw_id for anchor generation....
marcink -
r3937:cc125265 default
parent child Browse files
Show More
@@ -1,141 +1,141 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import pytest
23 23
24 24 from rhodecode.api.tests.utils import build_data, api_call, assert_error
25 25
26 26
27 27 @pytest.mark.usefixtures("testuser_api", "app")
28 28 class TestGetRepoChangeset(object):
29 29 @pytest.mark.parametrize("details", ['basic', 'extended', 'full'])
30 30 def test_get_repo_changeset(self, details, backend):
31 31 commit = backend.repo.get_commit(commit_idx=0)
32 32 __, params = build_data(
33 33 self.apikey, 'get_repo_changeset',
34 34 repoid=backend.repo_name, revision=commit.raw_id,
35 35 details=details,
36 36 )
37 37 response = api_call(self.app, params)
38 38 result = response.json['result']
39 39 assert result['revision'] == 0
40 40 assert result['raw_id'] == commit.raw_id
41 41
42 42 if details == 'full':
43 43 assert result['refs']['bookmarks'] == getattr(
44 44 commit, 'bookmarks', [])
45 45 branches = [commit.branch] if commit.branch else []
46 46 assert result['refs']['branches'] == branches
47 47 assert result['refs']['tags'] == commit.tags
48 48
49 49 @pytest.mark.parametrize("details", ['basic', 'extended', 'full'])
50 50 def test_get_repo_changeset_bad_type(self, details, backend):
51 51 id_, params = build_data(
52 52 self.apikey, 'get_repo_changeset',
53 53 repoid=backend.repo_name, revision=0,
54 54 details=details,
55 55 )
56 56 response = api_call(self.app, params)
57 expected = 'commit_id must be a string value'
57 expected = "commit_id must be a string value got <type 'int'> instead"
58 58 assert_error(id_, expected, given=response.body)
59 59
60 60 @pytest.mark.parametrize("details", ['basic', 'extended', 'full'])
61 61 def test_get_repo_changesets(self, details, backend):
62 62 limit = 2
63 63 commit = backend.repo.get_commit(commit_idx=0)
64 64 __, params = build_data(
65 65 self.apikey, 'get_repo_changesets',
66 66 repoid=backend.repo_name, start_rev=commit.raw_id, limit=limit,
67 67 details=details,
68 68 )
69 69 response = api_call(self.app, params)
70 70 result = response.json['result']
71 71 assert result
72 72 assert len(result) == limit
73 73 for x in xrange(limit):
74 74 assert result[x]['revision'] == x
75 75
76 76 if details == 'full':
77 77 for x in xrange(limit):
78 78 assert 'bookmarks' in result[x]['refs']
79 79 assert 'branches' in result[x]['refs']
80 80 assert 'tags' in result[x]['refs']
81 81
82 82 @pytest.mark.parametrize("details", ['basic', 'extended', 'full'])
83 83 @pytest.mark.parametrize("start_rev, expected_revision", [
84 84 ("0", 0),
85 85 ("10", 10),
86 86 ("20", 20),
87 87 ])
88 88 @pytest.mark.backends("hg", "git")
89 89 def test_get_repo_changesets_commit_range(
90 90 self, details, backend, start_rev, expected_revision):
91 91 limit = 10
92 92 __, params = build_data(
93 93 self.apikey, 'get_repo_changesets',
94 94 repoid=backend.repo_name, start_rev=start_rev, limit=limit,
95 95 details=details,
96 96 )
97 97 response = api_call(self.app, params)
98 98 result = response.json['result']
99 99 assert result
100 100 assert len(result) == limit
101 101 for i in xrange(limit):
102 102 assert result[i]['revision'] == int(expected_revision) + i
103 103
104 104 @pytest.mark.parametrize("details", ['basic', 'extended', 'full'])
105 105 @pytest.mark.parametrize("start_rev, expected_revision", [
106 106 ("0", 0),
107 107 ("10", 9),
108 108 ("20", 19),
109 109 ])
110 110 def test_get_repo_changesets_commit_range_svn(
111 111 self, details, backend_svn, start_rev, expected_revision):
112 112
113 113 # TODO: johbo: SVN showed a problem here: The parameter "start_rev"
114 114 # in our API allows to pass in a "Commit ID" as well as a
115 115 # "Commit Index". In the case of Subversion it is not possible to
116 116 # distinguish these cases. As a workaround we implemented this
117 117 # behavior which gives a preference to see it as a "Commit ID".
118 118
119 119 limit = 10
120 120 __, params = build_data(
121 121 self.apikey, 'get_repo_changesets',
122 122 repoid=backend_svn.repo_name, start_rev=start_rev, limit=limit,
123 123 details=details,
124 124 )
125 125 response = api_call(self.app, params)
126 126 result = response.json['result']
127 127 assert result
128 128 assert len(result) == limit
129 129 for i in xrange(limit):
130 130 assert result[i]['revision'] == int(expected_revision) + i
131 131
132 132 @pytest.mark.parametrize("details", ['basic', 'extended', 'full'])
133 133 def test_get_repo_changesets_bad_type(self, details, backend):
134 134 id_, params = build_data(
135 135 self.apikey, 'get_repo_changesets',
136 136 repoid=backend.repo_name, start_rev=0, limit=2,
137 137 details=details,
138 138 )
139 139 response = api_call(self.app, params)
140 expected = 'commit_id must be a string value'
140 expected = "commit_id must be a string value got <type 'int'> instead"
141 141 assert_error(id_, expected, given=response.body)
@@ -1,1899 +1,1899 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Base module for all VCS systems
23 23 """
24 24 import os
25 25 import re
26 26 import time
27 27 import shutil
28 28 import datetime
29 29 import fnmatch
30 30 import itertools
31 31 import logging
32 32 import collections
33 33 import warnings
34 34
35 35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 36
37 37 from pyramid import compat
38 38
39 39 import rhodecode
40 40 from rhodecode.translation import lazy_ugettext
41 41 from rhodecode.lib.utils2 import safe_str, safe_unicode, CachedProperty
42 42 from rhodecode.lib.vcs import connection
43 43 from rhodecode.lib.vcs.utils import author_name, author_email
44 44 from rhodecode.lib.vcs.conf import settings
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
47 47 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
48 48 NodeDoesNotExistError, NodeNotChangedError, VCSError,
49 49 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
50 50 RepositoryError)
51 51
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 FILEMODE_DEFAULT = 0o100644
57 57 FILEMODE_EXECUTABLE = 0o100755
58 58 EMPTY_COMMIT_ID = '0' * 40
59 59
60 60 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
61 61
62 62
63 63 class MergeFailureReason(object):
64 64 """
65 65 Enumeration with all the reasons why the server side merge could fail.
66 66
67 67 DO NOT change the number of the reasons, as they may be stored in the
68 68 database.
69 69
70 70 Changing the name of a reason is acceptable and encouraged to deprecate old
71 71 reasons.
72 72 """
73 73
74 74 # Everything went well.
75 75 NONE = 0
76 76
77 77 # An unexpected exception was raised. Check the logs for more details.
78 78 UNKNOWN = 1
79 79
80 80 # The merge was not successful, there are conflicts.
81 81 MERGE_FAILED = 2
82 82
83 83 # The merge succeeded but we could not push it to the target repository.
84 84 PUSH_FAILED = 3
85 85
86 86 # The specified target is not a head in the target repository.
87 87 TARGET_IS_NOT_HEAD = 4
88 88
89 89 # The source repository contains more branches than the target. Pushing
90 90 # the merge will create additional branches in the target.
91 91 HG_SOURCE_HAS_MORE_BRANCHES = 5
92 92
93 93 # The target reference has multiple heads. That does not allow to correctly
94 94 # identify the target location. This could only happen for mercurial
95 95 # branches.
96 96 HG_TARGET_HAS_MULTIPLE_HEADS = 6
97 97
98 98 # The target repository is locked
99 99 TARGET_IS_LOCKED = 7
100 100
101 101 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
102 102 # A involved commit could not be found.
103 103 _DEPRECATED_MISSING_COMMIT = 8
104 104
105 105 # The target repo reference is missing.
106 106 MISSING_TARGET_REF = 9
107 107
108 108 # The source repo reference is missing.
109 109 MISSING_SOURCE_REF = 10
110 110
111 111 # The merge was not successful, there are conflicts related to sub
112 112 # repositories.
113 113 SUBREPO_MERGE_FAILED = 11
114 114
115 115
116 116 class UpdateFailureReason(object):
117 117 """
118 118 Enumeration with all the reasons why the pull request update could fail.
119 119
120 120 DO NOT change the number of the reasons, as they may be stored in the
121 121 database.
122 122
123 123 Changing the name of a reason is acceptable and encouraged to deprecate old
124 124 reasons.
125 125 """
126 126
127 127 # Everything went well.
128 128 NONE = 0
129 129
130 130 # An unexpected exception was raised. Check the logs for more details.
131 131 UNKNOWN = 1
132 132
133 133 # The pull request is up to date.
134 134 NO_CHANGE = 2
135 135
136 136 # The pull request has a reference type that is not supported for update.
137 137 WRONG_REF_TYPE = 3
138 138
139 139 # Update failed because the target reference is missing.
140 140 MISSING_TARGET_REF = 4
141 141
142 142 # Update failed because the source reference is missing.
143 143 MISSING_SOURCE_REF = 5
144 144
145 145
146 146 class MergeResponse(object):
147 147
148 148 # uses .format(**metadata) for variables
149 149 MERGE_STATUS_MESSAGES = {
150 150 MergeFailureReason.NONE: lazy_ugettext(
151 151 u'This pull request can be automatically merged.'),
152 152 MergeFailureReason.UNKNOWN: lazy_ugettext(
153 153 u'This pull request cannot be merged because of an unhandled exception. '
154 154 u'{exception}'),
155 155 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
156 156 u'This pull request cannot be merged because of merge conflicts.'),
157 157 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
158 158 u'This pull request could not be merged because push to '
159 159 u'target:`{target}@{merge_commit}` failed.'),
160 160 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
161 161 u'This pull request cannot be merged because the target '
162 162 u'`{target_ref.name}` is not a head.'),
163 163 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
164 164 u'This pull request cannot be merged because the source contains '
165 165 u'more branches than the target.'),
166 166 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
167 167 u'This pull request cannot be merged because the target `{target_ref.name}` '
168 168 u'has multiple heads: `{heads}`.'),
169 169 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
170 170 u'This pull request cannot be merged because the target repository is '
171 171 u'locked by {locked_by}.'),
172 172
173 173 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
174 174 u'This pull request cannot be merged because the target '
175 175 u'reference `{target_ref.name}` is missing.'),
176 176 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
177 177 u'This pull request cannot be merged because the source '
178 178 u'reference `{source_ref.name}` is missing.'),
179 179 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
180 180 u'This pull request cannot be merged because of conflicts related '
181 181 u'to sub repositories.'),
182 182
183 183 # Deprecations
184 184 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
185 185 u'This pull request cannot be merged because the target or the '
186 186 u'source reference is missing.'),
187 187
188 188 }
189 189
190 190 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
191 191 self.possible = possible
192 192 self.executed = executed
193 193 self.merge_ref = merge_ref
194 194 self.failure_reason = failure_reason
195 195 self.metadata = metadata or {}
196 196
197 197 def __repr__(self):
198 198 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
199 199
200 200 def __eq__(self, other):
201 201 same_instance = isinstance(other, self.__class__)
202 202 return same_instance \
203 203 and self.possible == other.possible \
204 204 and self.executed == other.executed \
205 205 and self.failure_reason == other.failure_reason
206 206
207 207 @property
208 208 def label(self):
209 209 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
210 210 not k.startswith('_'))
211 211 return label_dict.get(self.failure_reason)
212 212
213 213 @property
214 214 def merge_status_message(self):
215 215 """
216 216 Return a human friendly error message for the given merge status code.
217 217 """
218 218 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
219 219 try:
220 220 return msg.format(**self.metadata)
221 221 except Exception:
222 222 log.exception('Failed to format %s message', self)
223 223 return msg
224 224
225 225 def asdict(self):
226 226 data = {}
227 227 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
228 228 'merge_status_message']:
229 229 data[k] = getattr(self, k)
230 230 return data
231 231
232 232
233 233 class BaseRepository(object):
234 234 """
235 235 Base Repository for final backends
236 236
237 237 .. attribute:: DEFAULT_BRANCH_NAME
238 238
239 239 name of default branch (i.e. "trunk" for svn, "master" for git etc.
240 240
241 241 .. attribute:: commit_ids
242 242
243 243 list of all available commit ids, in ascending order
244 244
245 245 .. attribute:: path
246 246
247 247 absolute path to the repository
248 248
249 249 .. attribute:: bookmarks
250 250
251 251 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
252 252 there are no bookmarks or the backend implementation does not support
253 253 bookmarks.
254 254
255 255 .. attribute:: tags
256 256
257 257 Mapping from name to :term:`Commit ID` of the tag.
258 258
259 259 """
260 260
261 261 DEFAULT_BRANCH_NAME = None
262 262 DEFAULT_CONTACT = u"Unknown"
263 263 DEFAULT_DESCRIPTION = u"unknown"
264 264 EMPTY_COMMIT_ID = '0' * 40
265 265
266 266 path = None
267 267
268 268 _is_empty = None
269 269 _commit_ids = {}
270 270
271 271 def __init__(self, repo_path, config=None, create=False, **kwargs):
272 272 """
273 273 Initializes repository. Raises RepositoryError if repository could
274 274 not be find at the given ``repo_path`` or directory at ``repo_path``
275 275 exists and ``create`` is set to True.
276 276
277 277 :param repo_path: local path of the repository
278 278 :param config: repository configuration
279 279 :param create=False: if set to True, would try to create repository.
280 280 :param src_url=None: if set, should be proper url from which repository
281 281 would be cloned; requires ``create`` parameter to be set to True -
282 282 raises RepositoryError if src_url is set and create evaluates to
283 283 False
284 284 """
285 285 raise NotImplementedError
286 286
287 287 def __repr__(self):
288 288 return '<%s at %s>' % (self.__class__.__name__, self.path)
289 289
290 290 def __len__(self):
291 291 return self.count()
292 292
293 293 def __eq__(self, other):
294 294 same_instance = isinstance(other, self.__class__)
295 295 return same_instance and other.path == self.path
296 296
297 297 def __ne__(self, other):
298 298 return not self.__eq__(other)
299 299
300 300 def get_create_shadow_cache_pr_path(self, db_repo):
301 301 path = db_repo.cached_diffs_dir
302 302 if not os.path.exists(path):
303 303 os.makedirs(path, 0o755)
304 304 return path
305 305
306 306 @classmethod
307 307 def get_default_config(cls, default=None):
308 308 config = Config()
309 309 if default and isinstance(default, list):
310 310 for section, key, val in default:
311 311 config.set(section, key, val)
312 312 return config
313 313
314 314 @LazyProperty
315 315 def _remote(self):
316 316 raise NotImplementedError
317 317
318 318 def _heads(self, branch=None):
319 319 return []
320 320
321 321 @LazyProperty
322 322 def EMPTY_COMMIT(self):
323 323 return EmptyCommit(self.EMPTY_COMMIT_ID)
324 324
325 325 @LazyProperty
326 326 def alias(self):
327 327 for k, v in settings.BACKENDS.items():
328 328 if v.split('.')[-1] == str(self.__class__.__name__):
329 329 return k
330 330
331 331 @LazyProperty
332 332 def name(self):
333 333 return safe_unicode(os.path.basename(self.path))
334 334
335 335 @LazyProperty
336 336 def description(self):
337 337 raise NotImplementedError
338 338
339 339 def refs(self):
340 340 """
341 341 returns a `dict` with branches, bookmarks, tags, and closed_branches
342 342 for this repository
343 343 """
344 344 return dict(
345 345 branches=self.branches,
346 346 branches_closed=self.branches_closed,
347 347 tags=self.tags,
348 348 bookmarks=self.bookmarks
349 349 )
350 350
351 351 @LazyProperty
352 352 def branches(self):
353 353 """
354 354 A `dict` which maps branch names to commit ids.
355 355 """
356 356 raise NotImplementedError
357 357
358 358 @LazyProperty
359 359 def branches_closed(self):
360 360 """
361 361 A `dict` which maps tags names to commit ids.
362 362 """
363 363 raise NotImplementedError
364 364
365 365 @LazyProperty
366 366 def bookmarks(self):
367 367 """
368 368 A `dict` which maps tags names to commit ids.
369 369 """
370 370 raise NotImplementedError
371 371
372 372 @LazyProperty
373 373 def tags(self):
374 374 """
375 375 A `dict` which maps tags names to commit ids.
376 376 """
377 377 raise NotImplementedError
378 378
379 379 @LazyProperty
380 380 def size(self):
381 381 """
382 382 Returns combined size in bytes for all repository files
383 383 """
384 384 tip = self.get_commit()
385 385 return tip.size
386 386
387 387 def size_at_commit(self, commit_id):
388 388 commit = self.get_commit(commit_id)
389 389 return commit.size
390 390
391 391 def _check_for_empty(self):
392 392 no_commits = len(self._commit_ids) == 0
393 393 if no_commits:
394 394 # check on remote to be sure
395 395 return self._remote.is_empty()
396 396 else:
397 397 return False
398 398
399 399 def is_empty(self):
400 400 if rhodecode.is_test:
401 401 return self._check_for_empty()
402 402
403 403 if self._is_empty is None:
404 404 # cache empty for production, but not tests
405 405 self._is_empty = self._check_for_empty()
406 406
407 407 return self._is_empty
408 408
409 409 @staticmethod
410 410 def check_url(url, config):
411 411 """
412 412 Function will check given url and try to verify if it's a valid
413 413 link.
414 414 """
415 415 raise NotImplementedError
416 416
417 417 @staticmethod
418 418 def is_valid_repository(path):
419 419 """
420 420 Check if given `path` contains a valid repository of this backend
421 421 """
422 422 raise NotImplementedError
423 423
424 424 # ==========================================================================
425 425 # COMMITS
426 426 # ==========================================================================
427 427
428 428 @CachedProperty
429 429 def commit_ids(self):
430 430 raise NotImplementedError
431 431
432 432 def append_commit_id(self, commit_id):
433 433 if commit_id not in self.commit_ids:
434 434 self._rebuild_cache(self.commit_ids + [commit_id])
435 435
436 436 # clear cache
437 437 self._invalidate_prop_cache('commit_ids')
438 438 self._is_empty = False
439 439
440 440 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=None):
441 441 """
442 442 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
443 443 are both None, most recent commit is returned.
444 444
445 445 :param pre_load: Optional. List of commit attributes to load.
446 446
447 447 :raises ``EmptyRepositoryError``: if there are no commits
448 448 """
449 449 raise NotImplementedError
450 450
451 451 def __iter__(self):
452 452 for commit_id in self.commit_ids:
453 453 yield self.get_commit(commit_id=commit_id)
454 454
455 455 def get_commits(
456 456 self, start_id=None, end_id=None, start_date=None, end_date=None,
457 457 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
458 458 """
459 459 Returns iterator of `BaseCommit` objects from start to end
460 460 not inclusive. This should behave just like a list, ie. end is not
461 461 inclusive.
462 462
463 463 :param start_id: None or str, must be a valid commit id
464 464 :param end_id: None or str, must be a valid commit id
465 465 :param start_date:
466 466 :param end_date:
467 467 :param branch_name:
468 468 :param show_hidden:
469 469 :param pre_load:
470 470 :param translate_tags:
471 471 """
472 472 raise NotImplementedError
473 473
474 474 def __getitem__(self, key):
475 475 """
476 476 Allows index based access to the commit objects of this repository.
477 477 """
478 478 pre_load = ["author", "branch", "date", "message", "parents"]
479 479 if isinstance(key, slice):
480 480 return self._get_range(key, pre_load)
481 481 return self.get_commit(commit_idx=key, pre_load=pre_load)
482 482
483 483 def _get_range(self, slice_obj, pre_load):
484 484 for commit_id in self.commit_ids.__getitem__(slice_obj):
485 485 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
486 486
487 487 def count(self):
488 488 return len(self.commit_ids)
489 489
490 490 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
491 491 """
492 492 Creates and returns a tag for the given ``commit_id``.
493 493
494 494 :param name: name for new tag
495 495 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
496 496 :param commit_id: commit id for which new tag would be created
497 497 :param message: message of the tag's commit
498 498 :param date: date of tag's commit
499 499
500 500 :raises TagAlreadyExistError: if tag with same name already exists
501 501 """
502 502 raise NotImplementedError
503 503
504 504 def remove_tag(self, name, user, message=None, date=None):
505 505 """
506 506 Removes tag with the given ``name``.
507 507
508 508 :param name: name of the tag to be removed
509 509 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
510 510 :param message: message of the tag's removal commit
511 511 :param date: date of tag's removal commit
512 512
513 513 :raises TagDoesNotExistError: if tag with given name does not exists
514 514 """
515 515 raise NotImplementedError
516 516
517 517 def get_diff(
518 518 self, commit1, commit2, path=None, ignore_whitespace=False,
519 519 context=3, path1=None):
520 520 """
521 521 Returns (git like) *diff*, as plain text. Shows changes introduced by
522 522 `commit2` since `commit1`.
523 523
524 524 :param commit1: Entry point from which diff is shown. Can be
525 525 ``self.EMPTY_COMMIT`` - in this case, patch showing all
526 526 the changes since empty state of the repository until `commit2`
527 527 :param commit2: Until which commit changes should be shown.
528 528 :param path: Can be set to a path of a file to create a diff of that
529 529 file. If `path1` is also set, this value is only associated to
530 530 `commit2`.
531 531 :param ignore_whitespace: If set to ``True``, would not show whitespace
532 532 changes. Defaults to ``False``.
533 533 :param context: How many lines before/after changed lines should be
534 534 shown. Defaults to ``3``.
535 535 :param path1: Can be set to a path to associate with `commit1`. This
536 536 parameter works only for backends which support diff generation for
537 537 different paths. Other backends will raise a `ValueError` if `path1`
538 538 is set and has a different value than `path`.
539 539 :param file_path: filter this diff by given path pattern
540 540 """
541 541 raise NotImplementedError
542 542
543 543 def strip(self, commit_id, branch=None):
544 544 """
545 545 Strip given commit_id from the repository
546 546 """
547 547 raise NotImplementedError
548 548
549 549 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
550 550 """
551 551 Return a latest common ancestor commit if one exists for this repo
552 552 `commit_id1` vs `commit_id2` from `repo2`.
553 553
554 554 :param commit_id1: Commit it from this repository to use as a
555 555 target for the comparison.
556 556 :param commit_id2: Source commit id to use for comparison.
557 557 :param repo2: Source repository to use for comparison.
558 558 """
559 559 raise NotImplementedError
560 560
561 561 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
562 562 """
563 563 Compare this repository's revision `commit_id1` with `commit_id2`.
564 564
565 565 Returns a tuple(commits, ancestor) that would be merged from
566 566 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
567 567 will be returned as ancestor.
568 568
569 569 :param commit_id1: Commit it from this repository to use as a
570 570 target for the comparison.
571 571 :param commit_id2: Source commit id to use for comparison.
572 572 :param repo2: Source repository to use for comparison.
573 573 :param merge: If set to ``True`` will do a merge compare which also
574 574 returns the common ancestor.
575 575 :param pre_load: Optional. List of commit attributes to load.
576 576 """
577 577 raise NotImplementedError
578 578
579 579 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
580 580 user_name='', user_email='', message='', dry_run=False,
581 581 use_rebase=False, close_branch=False):
582 582 """
583 583 Merge the revisions specified in `source_ref` from `source_repo`
584 584 onto the `target_ref` of this repository.
585 585
586 586 `source_ref` and `target_ref` are named tupls with the following
587 587 fields `type`, `name` and `commit_id`.
588 588
589 589 Returns a MergeResponse named tuple with the following fields
590 590 'possible', 'executed', 'source_commit', 'target_commit',
591 591 'merge_commit'.
592 592
593 593 :param repo_id: `repo_id` target repo id.
594 594 :param workspace_id: `workspace_id` unique identifier.
595 595 :param target_ref: `target_ref` points to the commit on top of which
596 596 the `source_ref` should be merged.
597 597 :param source_repo: The repository that contains the commits to be
598 598 merged.
599 599 :param source_ref: `source_ref` points to the topmost commit from
600 600 the `source_repo` which should be merged.
601 601 :param user_name: Merge commit `user_name`.
602 602 :param user_email: Merge commit `user_email`.
603 603 :param message: Merge commit `message`.
604 604 :param dry_run: If `True` the merge will not take place.
605 605 :param use_rebase: If `True` commits from the source will be rebased
606 606 on top of the target instead of being merged.
607 607 :param close_branch: If `True` branch will be close before merging it
608 608 """
609 609 if dry_run:
610 610 message = message or settings.MERGE_DRY_RUN_MESSAGE
611 611 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
612 612 user_name = user_name or settings.MERGE_DRY_RUN_USER
613 613 else:
614 614 if not user_name:
615 615 raise ValueError('user_name cannot be empty')
616 616 if not user_email:
617 617 raise ValueError('user_email cannot be empty')
618 618 if not message:
619 619 raise ValueError('message cannot be empty')
620 620
621 621 try:
622 622 return self._merge_repo(
623 623 repo_id, workspace_id, target_ref, source_repo,
624 624 source_ref, message, user_name, user_email, dry_run=dry_run,
625 625 use_rebase=use_rebase, close_branch=close_branch)
626 626 except RepositoryError as exc:
627 627 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
628 628 return MergeResponse(
629 629 False, False, None, MergeFailureReason.UNKNOWN,
630 630 metadata={'exception': str(exc)})
631 631
632 632 def _merge_repo(self, repo_id, workspace_id, target_ref,
633 633 source_repo, source_ref, merge_message,
634 634 merger_name, merger_email, dry_run=False,
635 635 use_rebase=False, close_branch=False):
636 636 """Internal implementation of merge."""
637 637 raise NotImplementedError
638 638
639 639 def _maybe_prepare_merge_workspace(
640 640 self, repo_id, workspace_id, target_ref, source_ref):
641 641 """
642 642 Create the merge workspace.
643 643
644 644 :param workspace_id: `workspace_id` unique identifier.
645 645 """
646 646 raise NotImplementedError
647 647
648 648 @classmethod
649 649 def _get_legacy_shadow_repository_path(cls, repo_path, workspace_id):
650 650 """
651 651 Legacy version that was used before. We still need it for
652 652 backward compat
653 653 """
654 654 return os.path.join(
655 655 os.path.dirname(repo_path),
656 656 '.__shadow_%s_%s' % (os.path.basename(repo_path), workspace_id))
657 657
658 658 @classmethod
659 659 def _get_shadow_repository_path(cls, repo_path, repo_id, workspace_id):
660 660 # The name of the shadow repository must start with '.', so it is
661 661 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
662 662 legacy_repository_path = cls._get_legacy_shadow_repository_path(repo_path, workspace_id)
663 663 if os.path.exists(legacy_repository_path):
664 664 return legacy_repository_path
665 665 else:
666 666 return os.path.join(
667 667 os.path.dirname(repo_path),
668 668 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
669 669
670 670 def cleanup_merge_workspace(self, repo_id, workspace_id):
671 671 """
672 672 Remove merge workspace.
673 673
674 674 This function MUST not fail in case there is no workspace associated to
675 675 the given `workspace_id`.
676 676
677 677 :param workspace_id: `workspace_id` unique identifier.
678 678 """
679 679 shadow_repository_path = self._get_shadow_repository_path(
680 680 self.path, repo_id, workspace_id)
681 681 shadow_repository_path_del = '{}.{}.delete'.format(
682 682 shadow_repository_path, time.time())
683 683
684 684 # move the shadow repo, so it never conflicts with the one used.
685 685 # we use this method because shutil.rmtree had some edge case problems
686 686 # removing symlinked repositories
687 687 if not os.path.isdir(shadow_repository_path):
688 688 return
689 689
690 690 shutil.move(shadow_repository_path, shadow_repository_path_del)
691 691 try:
692 692 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
693 693 except Exception:
694 694 log.exception('Failed to gracefully remove shadow repo under %s',
695 695 shadow_repository_path_del)
696 696 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
697 697
698 698 # ========== #
699 699 # COMMIT API #
700 700 # ========== #
701 701
702 702 @LazyProperty
703 703 def in_memory_commit(self):
704 704 """
705 705 Returns :class:`InMemoryCommit` object for this repository.
706 706 """
707 707 raise NotImplementedError
708 708
709 709 # ======================== #
710 710 # UTILITIES FOR SUBCLASSES #
711 711 # ======================== #
712 712
713 713 def _validate_diff_commits(self, commit1, commit2):
714 714 """
715 715 Validates that the given commits are related to this repository.
716 716
717 717 Intended as a utility for sub classes to have a consistent validation
718 718 of input parameters in methods like :meth:`get_diff`.
719 719 """
720 720 self._validate_commit(commit1)
721 721 self._validate_commit(commit2)
722 722 if (isinstance(commit1, EmptyCommit) and
723 723 isinstance(commit2, EmptyCommit)):
724 724 raise ValueError("Cannot compare two empty commits")
725 725
726 726 def _validate_commit(self, commit):
727 727 if not isinstance(commit, BaseCommit):
728 728 raise TypeError(
729 729 "%s is not of type BaseCommit" % repr(commit))
730 730 if commit.repository != self and not isinstance(commit, EmptyCommit):
731 731 raise ValueError(
732 732 "Commit %s must be a valid commit from this repository %s, "
733 733 "related to this repository instead %s." %
734 734 (commit, self, commit.repository))
735 735
736 736 def _validate_commit_id(self, commit_id):
737 737 if not isinstance(commit_id, compat.string_types):
738 raise TypeError("commit_id must be a string value")
738 raise TypeError("commit_id must be a string value got {} instead".format(type(commit_id)))
739 739
740 740 def _validate_commit_idx(self, commit_idx):
741 741 if not isinstance(commit_idx, (int, long)):
742 742 raise TypeError("commit_idx must be a numeric value")
743 743
744 744 def _validate_branch_name(self, branch_name):
745 745 if branch_name and branch_name not in self.branches_all:
746 746 msg = ("Branch %s not found in %s" % (branch_name, self))
747 747 raise BranchDoesNotExistError(msg)
748 748
749 749 #
750 750 # Supporting deprecated API parts
751 751 # TODO: johbo: consider to move this into a mixin
752 752 #
753 753
754 754 @property
755 755 def EMPTY_CHANGESET(self):
756 756 warnings.warn(
757 757 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
758 758 return self.EMPTY_COMMIT_ID
759 759
760 760 @property
761 761 def revisions(self):
762 762 warnings.warn("Use commits attribute instead", DeprecationWarning)
763 763 return self.commit_ids
764 764
765 765 @revisions.setter
766 766 def revisions(self, value):
767 767 warnings.warn("Use commits attribute instead", DeprecationWarning)
768 768 self.commit_ids = value
769 769
770 770 def get_changeset(self, revision=None, pre_load=None):
771 771 warnings.warn("Use get_commit instead", DeprecationWarning)
772 772 commit_id = None
773 773 commit_idx = None
774 774 if isinstance(revision, compat.string_types):
775 775 commit_id = revision
776 776 else:
777 777 commit_idx = revision
778 778 return self.get_commit(
779 779 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
780 780
781 781 def get_changesets(
782 782 self, start=None, end=None, start_date=None, end_date=None,
783 783 branch_name=None, pre_load=None):
784 784 warnings.warn("Use get_commits instead", DeprecationWarning)
785 785 start_id = self._revision_to_commit(start)
786 786 end_id = self._revision_to_commit(end)
787 787 return self.get_commits(
788 788 start_id=start_id, end_id=end_id, start_date=start_date,
789 789 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
790 790
791 791 def _revision_to_commit(self, revision):
792 792 """
793 793 Translates a revision to a commit_id
794 794
795 795 Helps to support the old changeset based API which allows to use
796 796 commit ids and commit indices interchangeable.
797 797 """
798 798 if revision is None:
799 799 return revision
800 800
801 801 if isinstance(revision, compat.string_types):
802 802 commit_id = revision
803 803 else:
804 804 commit_id = self.commit_ids[revision]
805 805 return commit_id
806 806
807 807 @property
808 808 def in_memory_changeset(self):
809 809 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
810 810 return self.in_memory_commit
811 811
812 812 def get_path_permissions(self, username):
813 813 """
814 814 Returns a path permission checker or None if not supported
815 815
816 816 :param username: session user name
817 817 :return: an instance of BasePathPermissionChecker or None
818 818 """
819 819 return None
820 820
821 821 def install_hooks(self, force=False):
822 822 return self._remote.install_hooks(force)
823 823
824 824 def get_hooks_info(self):
825 825 return self._remote.get_hooks_info()
826 826
827 827
828 828 class BaseCommit(object):
829 829 """
830 830 Each backend should implement it's commit representation.
831 831
832 832 **Attributes**
833 833
834 834 ``repository``
835 835 repository object within which commit exists
836 836
837 837 ``id``
838 838 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
839 839 just ``tip``.
840 840
841 841 ``raw_id``
842 842 raw commit representation (i.e. full 40 length sha for git
843 843 backend)
844 844
845 845 ``short_id``
846 846 shortened (if apply) version of ``raw_id``; it would be simple
847 847 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
848 848 as ``raw_id`` for subversion
849 849
850 850 ``idx``
851 851 commit index
852 852
853 853 ``files``
854 854 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
855 855
856 856 ``dirs``
857 857 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
858 858
859 859 ``nodes``
860 860 combined list of ``Node`` objects
861 861
862 862 ``author``
863 863 author of the commit, as unicode
864 864
865 865 ``message``
866 866 message of the commit, as unicode
867 867
868 868 ``parents``
869 869 list of parent commits
870 870
871 871 """
872 872
873 873 branch = None
874 874 """
875 875 Depending on the backend this should be set to the branch name of the
876 876 commit. Backends not supporting branches on commits should leave this
877 877 value as ``None``.
878 878 """
879 879
880 880 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
881 881 """
882 882 This template is used to generate a default prefix for repository archives
883 883 if no prefix has been specified.
884 884 """
885 885
886 886 def __str__(self):
887 887 return '<%s at %s:%s>' % (
888 888 self.__class__.__name__, self.idx, self.short_id)
889 889
890 890 def __repr__(self):
891 891 return self.__str__()
892 892
893 893 def __unicode__(self):
894 894 return u'%s:%s' % (self.idx, self.short_id)
895 895
896 896 def __eq__(self, other):
897 897 same_instance = isinstance(other, self.__class__)
898 898 return same_instance and self.raw_id == other.raw_id
899 899
900 900 def __json__(self):
901 901 parents = []
902 902 try:
903 903 for parent in self.parents:
904 904 parents.append({'raw_id': parent.raw_id})
905 905 except NotImplementedError:
906 906 # empty commit doesn't have parents implemented
907 907 pass
908 908
909 909 return {
910 910 'short_id': self.short_id,
911 911 'raw_id': self.raw_id,
912 912 'revision': self.idx,
913 913 'message': self.message,
914 914 'date': self.date,
915 915 'author': self.author,
916 916 'parents': parents,
917 917 'branch': self.branch
918 918 }
919 919
920 920 def __getstate__(self):
921 921 d = self.__dict__.copy()
922 922 d.pop('_remote', None)
923 923 d.pop('repository', None)
924 924 return d
925 925
926 926 def _get_refs(self):
927 927 return {
928 928 'branches': [self.branch] if self.branch else [],
929 929 'bookmarks': getattr(self, 'bookmarks', []),
930 930 'tags': self.tags
931 931 }
932 932
933 933 @LazyProperty
934 934 def last(self):
935 935 """
936 936 ``True`` if this is last commit in repository, ``False``
937 937 otherwise; trying to access this attribute while there is no
938 938 commits would raise `EmptyRepositoryError`
939 939 """
940 940 if self.repository is None:
941 941 raise CommitError("Cannot check if it's most recent commit")
942 942 return self.raw_id == self.repository.commit_ids[-1]
943 943
944 944 @LazyProperty
945 945 def parents(self):
946 946 """
947 947 Returns list of parent commits.
948 948 """
949 949 raise NotImplementedError
950 950
951 951 @LazyProperty
952 952 def first_parent(self):
953 953 """
954 954 Returns list of parent commits.
955 955 """
956 956 return self.parents[0] if self.parents else EmptyCommit()
957 957
958 958 @property
959 959 def merge(self):
960 960 """
961 961 Returns boolean if commit is a merge.
962 962 """
963 963 return len(self.parents) > 1
964 964
965 965 @LazyProperty
966 966 def children(self):
967 967 """
968 968 Returns list of child commits.
969 969 """
970 970 raise NotImplementedError
971 971
972 972 @LazyProperty
973 973 def id(self):
974 974 """
975 975 Returns string identifying this commit.
976 976 """
977 977 raise NotImplementedError
978 978
979 979 @LazyProperty
980 980 def raw_id(self):
981 981 """
982 982 Returns raw string identifying this commit.
983 983 """
984 984 raise NotImplementedError
985 985
986 986 @LazyProperty
987 987 def short_id(self):
988 988 """
989 989 Returns shortened version of ``raw_id`` attribute, as string,
990 990 identifying this commit, useful for presentation to users.
991 991 """
992 992 raise NotImplementedError
993 993
994 994 @LazyProperty
995 995 def idx(self):
996 996 """
997 997 Returns integer identifying this commit.
998 998 """
999 999 raise NotImplementedError
1000 1000
1001 1001 @LazyProperty
1002 1002 def committer(self):
1003 1003 """
1004 1004 Returns committer for this commit
1005 1005 """
1006 1006 raise NotImplementedError
1007 1007
1008 1008 @LazyProperty
1009 1009 def committer_name(self):
1010 1010 """
1011 1011 Returns committer name for this commit
1012 1012 """
1013 1013
1014 1014 return author_name(self.committer)
1015 1015
1016 1016 @LazyProperty
1017 1017 def committer_email(self):
1018 1018 """
1019 1019 Returns committer email address for this commit
1020 1020 """
1021 1021
1022 1022 return author_email(self.committer)
1023 1023
1024 1024 @LazyProperty
1025 1025 def author(self):
1026 1026 """
1027 1027 Returns author for this commit
1028 1028 """
1029 1029
1030 1030 raise NotImplementedError
1031 1031
1032 1032 @LazyProperty
1033 1033 def author_name(self):
1034 1034 """
1035 1035 Returns author name for this commit
1036 1036 """
1037 1037
1038 1038 return author_name(self.author)
1039 1039
1040 1040 @LazyProperty
1041 1041 def author_email(self):
1042 1042 """
1043 1043 Returns author email address for this commit
1044 1044 """
1045 1045
1046 1046 return author_email(self.author)
1047 1047
1048 1048 def get_file_mode(self, path):
1049 1049 """
1050 1050 Returns stat mode of the file at `path`.
1051 1051 """
1052 1052 raise NotImplementedError
1053 1053
1054 1054 def is_link(self, path):
1055 1055 """
1056 1056 Returns ``True`` if given `path` is a symlink
1057 1057 """
1058 1058 raise NotImplementedError
1059 1059
1060 1060 def is_node_binary(self, path):
1061 1061 """
1062 1062 Returns ``True`` is given path is a binary file
1063 1063 """
1064 1064 raise NotImplementedError
1065 1065
1066 1066 def get_file_content(self, path):
1067 1067 """
1068 1068 Returns content of the file at the given `path`.
1069 1069 """
1070 1070 raise NotImplementedError
1071 1071
1072 1072 def get_file_content_streamed(self, path):
1073 1073 """
1074 1074 returns a streaming response from vcsserver with file content
1075 1075 """
1076 1076 raise NotImplementedError
1077 1077
1078 1078 def get_file_size(self, path):
1079 1079 """
1080 1080 Returns size of the file at the given `path`.
1081 1081 """
1082 1082 raise NotImplementedError
1083 1083
1084 1084 def get_path_commit(self, path, pre_load=None):
1085 1085 """
1086 1086 Returns last commit of the file at the given `path`.
1087 1087
1088 1088 :param pre_load: Optional. List of commit attributes to load.
1089 1089 """
1090 1090 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1091 1091 if not commits:
1092 1092 raise RepositoryError(
1093 1093 'Failed to fetch history for path {}. '
1094 1094 'Please check if such path exists in your repository'.format(
1095 1095 path))
1096 1096 return commits[0]
1097 1097
1098 1098 def get_path_history(self, path, limit=None, pre_load=None):
1099 1099 """
1100 1100 Returns history of file as reversed list of :class:`BaseCommit`
1101 1101 objects for which file at given `path` has been modified.
1102 1102
1103 1103 :param limit: Optional. Allows to limit the size of the returned
1104 1104 history. This is intended as a hint to the underlying backend, so
1105 1105 that it can apply optimizations depending on the limit.
1106 1106 :param pre_load: Optional. List of commit attributes to load.
1107 1107 """
1108 1108 raise NotImplementedError
1109 1109
1110 1110 def get_file_annotate(self, path, pre_load=None):
1111 1111 """
1112 1112 Returns a generator of four element tuples with
1113 1113 lineno, sha, commit lazy loader and line
1114 1114
1115 1115 :param pre_load: Optional. List of commit attributes to load.
1116 1116 """
1117 1117 raise NotImplementedError
1118 1118
1119 1119 def get_nodes(self, path):
1120 1120 """
1121 1121 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1122 1122 state of commit at the given ``path``.
1123 1123
1124 1124 :raises ``CommitError``: if node at the given ``path`` is not
1125 1125 instance of ``DirNode``
1126 1126 """
1127 1127 raise NotImplementedError
1128 1128
1129 1129 def get_node(self, path):
1130 1130 """
1131 1131 Returns ``Node`` object from the given ``path``.
1132 1132
1133 1133 :raises ``NodeDoesNotExistError``: if there is no node at the given
1134 1134 ``path``
1135 1135 """
1136 1136 raise NotImplementedError
1137 1137
1138 1138 def get_largefile_node(self, path):
1139 1139 """
1140 1140 Returns the path to largefile from Mercurial/Git-lfs storage.
1141 1141 or None if it's not a largefile node
1142 1142 """
1143 1143 return None
1144 1144
1145 1145 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1146 1146 prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
1147 1147 """
1148 1148 Creates an archive containing the contents of the repository.
1149 1149
1150 1150 :param archive_dest_path: path to the file which to create the archive.
1151 1151 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1152 1152 :param prefix: name of root directory in archive.
1153 1153 Default is repository name and commit's short_id joined with dash:
1154 1154 ``"{repo_name}-{short_id}"``.
1155 1155 :param write_metadata: write a metadata file into archive.
1156 1156 :param mtime: custom modification time for archive creation, defaults
1157 1157 to time.time() if not given.
1158 1158 :param archive_at_path: pack files at this path (default '/')
1159 1159
1160 1160 :raise VCSError: If prefix has a problem.
1161 1161 """
1162 1162 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1163 1163 if kind not in allowed_kinds:
1164 1164 raise ImproperArchiveTypeError(
1165 1165 'Archive kind (%s) not supported use one of %s' %
1166 1166 (kind, allowed_kinds))
1167 1167
1168 1168 prefix = self._validate_archive_prefix(prefix)
1169 1169
1170 1170 mtime = mtime is not None or time.mktime(self.date.timetuple())
1171 1171
1172 1172 file_info = []
1173 1173 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1174 1174 for _r, _d, files in cur_rev.walk(archive_at_path):
1175 1175 for f in files:
1176 1176 f_path = os.path.join(prefix, f.path)
1177 1177 file_info.append(
1178 1178 (f_path, f.mode, f.is_link(), f.raw_bytes))
1179 1179
1180 1180 if write_metadata:
1181 1181 metadata = [
1182 1182 ('repo_name', self.repository.name),
1183 1183 ('commit_id', self.raw_id),
1184 1184 ('mtime', mtime),
1185 1185 ('branch', self.branch),
1186 1186 ('tags', ','.join(self.tags)),
1187 1187 ]
1188 1188 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1189 1189 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1190 1190
1191 1191 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1192 1192
1193 1193 def _validate_archive_prefix(self, prefix):
1194 1194 if prefix is None:
1195 1195 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1196 1196 repo_name=safe_str(self.repository.name),
1197 1197 short_id=self.short_id)
1198 1198 elif not isinstance(prefix, str):
1199 1199 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1200 1200 elif prefix.startswith('/'):
1201 1201 raise VCSError("Prefix cannot start with leading slash")
1202 1202 elif prefix.strip() == '':
1203 1203 raise VCSError("Prefix cannot be empty")
1204 1204 return prefix
1205 1205
1206 1206 @LazyProperty
1207 1207 def root(self):
1208 1208 """
1209 1209 Returns ``RootNode`` object for this commit.
1210 1210 """
1211 1211 return self.get_node('')
1212 1212
1213 1213 def next(self, branch=None):
1214 1214 """
1215 1215 Returns next commit from current, if branch is gives it will return
1216 1216 next commit belonging to this branch
1217 1217
1218 1218 :param branch: show commits within the given named branch
1219 1219 """
1220 1220 indexes = xrange(self.idx + 1, self.repository.count())
1221 1221 return self._find_next(indexes, branch)
1222 1222
1223 1223 def prev(self, branch=None):
1224 1224 """
1225 1225 Returns previous commit from current, if branch is gives it will
1226 1226 return previous commit belonging to this branch
1227 1227
1228 1228 :param branch: show commit within the given named branch
1229 1229 """
1230 1230 indexes = xrange(self.idx - 1, -1, -1)
1231 1231 return self._find_next(indexes, branch)
1232 1232
1233 1233 def _find_next(self, indexes, branch=None):
1234 1234 if branch and self.branch != branch:
1235 1235 raise VCSError('Branch option used on commit not belonging '
1236 1236 'to that branch')
1237 1237
1238 1238 for next_idx in indexes:
1239 1239 commit = self.repository.get_commit(commit_idx=next_idx)
1240 1240 if branch and branch != commit.branch:
1241 1241 continue
1242 1242 return commit
1243 1243 raise CommitDoesNotExistError
1244 1244
1245 1245 def diff(self, ignore_whitespace=True, context=3):
1246 1246 """
1247 1247 Returns a `Diff` object representing the change made by this commit.
1248 1248 """
1249 1249 parent = self.first_parent
1250 1250 diff = self.repository.get_diff(
1251 1251 parent, self,
1252 1252 ignore_whitespace=ignore_whitespace,
1253 1253 context=context)
1254 1254 return diff
1255 1255
1256 1256 @LazyProperty
1257 1257 def added(self):
1258 1258 """
1259 1259 Returns list of added ``FileNode`` objects.
1260 1260 """
1261 1261 raise NotImplementedError
1262 1262
1263 1263 @LazyProperty
1264 1264 def changed(self):
1265 1265 """
1266 1266 Returns list of modified ``FileNode`` objects.
1267 1267 """
1268 1268 raise NotImplementedError
1269 1269
1270 1270 @LazyProperty
1271 1271 def removed(self):
1272 1272 """
1273 1273 Returns list of removed ``FileNode`` objects.
1274 1274 """
1275 1275 raise NotImplementedError
1276 1276
1277 1277 @LazyProperty
1278 1278 def size(self):
1279 1279 """
1280 1280 Returns total number of bytes from contents of all filenodes.
1281 1281 """
1282 1282 return sum((node.size for node in self.get_filenodes_generator()))
1283 1283
1284 1284 def walk(self, topurl=''):
1285 1285 """
1286 1286 Similar to os.walk method. Insted of filesystem it walks through
1287 1287 commit starting at given ``topurl``. Returns generator of tuples
1288 1288 (topnode, dirnodes, filenodes).
1289 1289 """
1290 1290 topnode = self.get_node(topurl)
1291 1291 if not topnode.is_dir():
1292 1292 return
1293 1293 yield (topnode, topnode.dirs, topnode.files)
1294 1294 for dirnode in topnode.dirs:
1295 1295 for tup in self.walk(dirnode.path):
1296 1296 yield tup
1297 1297
1298 1298 def get_filenodes_generator(self):
1299 1299 """
1300 1300 Returns generator that yields *all* file nodes.
1301 1301 """
1302 1302 for topnode, dirs, files in self.walk():
1303 1303 for node in files:
1304 1304 yield node
1305 1305
1306 1306 #
1307 1307 # Utilities for sub classes to support consistent behavior
1308 1308 #
1309 1309
1310 1310 def no_node_at_path(self, path):
1311 1311 return NodeDoesNotExistError(
1312 1312 u"There is no file nor directory at the given path: "
1313 1313 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1314 1314
1315 1315 def _fix_path(self, path):
1316 1316 """
1317 1317 Paths are stored without trailing slash so we need to get rid off it if
1318 1318 needed.
1319 1319 """
1320 1320 return path.rstrip('/')
1321 1321
1322 1322 #
1323 1323 # Deprecated API based on changesets
1324 1324 #
1325 1325
1326 1326 @property
1327 1327 def revision(self):
1328 1328 warnings.warn("Use idx instead", DeprecationWarning)
1329 1329 return self.idx
1330 1330
1331 1331 @revision.setter
1332 1332 def revision(self, value):
1333 1333 warnings.warn("Use idx instead", DeprecationWarning)
1334 1334 self.idx = value
1335 1335
1336 1336 def get_file_changeset(self, path):
1337 1337 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1338 1338 return self.get_path_commit(path)
1339 1339
1340 1340
1341 1341 class BaseChangesetClass(type):
1342 1342
1343 1343 def __instancecheck__(self, instance):
1344 1344 return isinstance(instance, BaseCommit)
1345 1345
1346 1346
1347 1347 class BaseChangeset(BaseCommit):
1348 1348
1349 1349 __metaclass__ = BaseChangesetClass
1350 1350
1351 1351 def __new__(cls, *args, **kwargs):
1352 1352 warnings.warn(
1353 1353 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1354 1354 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1355 1355
1356 1356
1357 1357 class BaseInMemoryCommit(object):
1358 1358 """
1359 1359 Represents differences between repository's state (most recent head) and
1360 1360 changes made *in place*.
1361 1361
1362 1362 **Attributes**
1363 1363
1364 1364 ``repository``
1365 1365 repository object for this in-memory-commit
1366 1366
1367 1367 ``added``
1368 1368 list of ``FileNode`` objects marked as *added*
1369 1369
1370 1370 ``changed``
1371 1371 list of ``FileNode`` objects marked as *changed*
1372 1372
1373 1373 ``removed``
1374 1374 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1375 1375 *removed*
1376 1376
1377 1377 ``parents``
1378 1378 list of :class:`BaseCommit` instances representing parents of
1379 1379 in-memory commit. Should always be 2-element sequence.
1380 1380
1381 1381 """
1382 1382
1383 1383 def __init__(self, repository):
1384 1384 self.repository = repository
1385 1385 self.added = []
1386 1386 self.changed = []
1387 1387 self.removed = []
1388 1388 self.parents = []
1389 1389
1390 1390 def add(self, *filenodes):
1391 1391 """
1392 1392 Marks given ``FileNode`` objects as *to be committed*.
1393 1393
1394 1394 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1395 1395 latest commit
1396 1396 :raises ``NodeAlreadyAddedError``: if node with same path is already
1397 1397 marked as *added*
1398 1398 """
1399 1399 # Check if not already marked as *added* first
1400 1400 for node in filenodes:
1401 1401 if node.path in (n.path for n in self.added):
1402 1402 raise NodeAlreadyAddedError(
1403 1403 "Such FileNode %s is already marked for addition"
1404 1404 % node.path)
1405 1405 for node in filenodes:
1406 1406 self.added.append(node)
1407 1407
1408 1408 def change(self, *filenodes):
1409 1409 """
1410 1410 Marks given ``FileNode`` objects to be *changed* in next commit.
1411 1411
1412 1412 :raises ``EmptyRepositoryError``: if there are no commits yet
1413 1413 :raises ``NodeAlreadyExistsError``: if node with same path is already
1414 1414 marked to be *changed*
1415 1415 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1416 1416 marked to be *removed*
1417 1417 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1418 1418 commit
1419 1419 :raises ``NodeNotChangedError``: if node hasn't really be changed
1420 1420 """
1421 1421 for node in filenodes:
1422 1422 if node.path in (n.path for n in self.removed):
1423 1423 raise NodeAlreadyRemovedError(
1424 1424 "Node at %s is already marked as removed" % node.path)
1425 1425 try:
1426 1426 self.repository.get_commit()
1427 1427 except EmptyRepositoryError:
1428 1428 raise EmptyRepositoryError(
1429 1429 "Nothing to change - try to *add* new nodes rather than "
1430 1430 "changing them")
1431 1431 for node in filenodes:
1432 1432 if node.path in (n.path for n in self.changed):
1433 1433 raise NodeAlreadyChangedError(
1434 1434 "Node at '%s' is already marked as changed" % node.path)
1435 1435 self.changed.append(node)
1436 1436
1437 1437 def remove(self, *filenodes):
1438 1438 """
1439 1439 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1440 1440 *removed* in next commit.
1441 1441
1442 1442 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1443 1443 be *removed*
1444 1444 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1445 1445 be *changed*
1446 1446 """
1447 1447 for node in filenodes:
1448 1448 if node.path in (n.path for n in self.removed):
1449 1449 raise NodeAlreadyRemovedError(
1450 1450 "Node is already marked to for removal at %s" % node.path)
1451 1451 if node.path in (n.path for n in self.changed):
1452 1452 raise NodeAlreadyChangedError(
1453 1453 "Node is already marked to be changed at %s" % node.path)
1454 1454 # We only mark node as *removed* - real removal is done by
1455 1455 # commit method
1456 1456 self.removed.append(node)
1457 1457
1458 1458 def reset(self):
1459 1459 """
1460 1460 Resets this instance to initial state (cleans ``added``, ``changed``
1461 1461 and ``removed`` lists).
1462 1462 """
1463 1463 self.added = []
1464 1464 self.changed = []
1465 1465 self.removed = []
1466 1466 self.parents = []
1467 1467
1468 1468 def get_ipaths(self):
1469 1469 """
1470 1470 Returns generator of paths from nodes marked as added, changed or
1471 1471 removed.
1472 1472 """
1473 1473 for node in itertools.chain(self.added, self.changed, self.removed):
1474 1474 yield node.path
1475 1475
1476 1476 def get_paths(self):
1477 1477 """
1478 1478 Returns list of paths from nodes marked as added, changed or removed.
1479 1479 """
1480 1480 return list(self.get_ipaths())
1481 1481
1482 1482 def check_integrity(self, parents=None):
1483 1483 """
1484 1484 Checks in-memory commit's integrity. Also, sets parents if not
1485 1485 already set.
1486 1486
1487 1487 :raises CommitError: if any error occurs (i.e.
1488 1488 ``NodeDoesNotExistError``).
1489 1489 """
1490 1490 if not self.parents:
1491 1491 parents = parents or []
1492 1492 if len(parents) == 0:
1493 1493 try:
1494 1494 parents = [self.repository.get_commit(), None]
1495 1495 except EmptyRepositoryError:
1496 1496 parents = [None, None]
1497 1497 elif len(parents) == 1:
1498 1498 parents += [None]
1499 1499 self.parents = parents
1500 1500
1501 1501 # Local parents, only if not None
1502 1502 parents = [p for p in self.parents if p]
1503 1503
1504 1504 # Check nodes marked as added
1505 1505 for p in parents:
1506 1506 for node in self.added:
1507 1507 try:
1508 1508 p.get_node(node.path)
1509 1509 except NodeDoesNotExistError:
1510 1510 pass
1511 1511 else:
1512 1512 raise NodeAlreadyExistsError(
1513 1513 "Node `%s` already exists at %s" % (node.path, p))
1514 1514
1515 1515 # Check nodes marked as changed
1516 1516 missing = set(self.changed)
1517 1517 not_changed = set(self.changed)
1518 1518 if self.changed and not parents:
1519 1519 raise NodeDoesNotExistError(str(self.changed[0].path))
1520 1520 for p in parents:
1521 1521 for node in self.changed:
1522 1522 try:
1523 1523 old = p.get_node(node.path)
1524 1524 missing.remove(node)
1525 1525 # if content actually changed, remove node from not_changed
1526 1526 if old.content != node.content:
1527 1527 not_changed.remove(node)
1528 1528 except NodeDoesNotExistError:
1529 1529 pass
1530 1530 if self.changed and missing:
1531 1531 raise NodeDoesNotExistError(
1532 1532 "Node `%s` marked as modified but missing in parents: %s"
1533 1533 % (node.path, parents))
1534 1534
1535 1535 if self.changed and not_changed:
1536 1536 raise NodeNotChangedError(
1537 1537 "Node `%s` wasn't actually changed (parents: %s)"
1538 1538 % (not_changed.pop().path, parents))
1539 1539
1540 1540 # Check nodes marked as removed
1541 1541 if self.removed and not parents:
1542 1542 raise NodeDoesNotExistError(
1543 1543 "Cannot remove node at %s as there "
1544 1544 "were no parents specified" % self.removed[0].path)
1545 1545 really_removed = set()
1546 1546 for p in parents:
1547 1547 for node in self.removed:
1548 1548 try:
1549 1549 p.get_node(node.path)
1550 1550 really_removed.add(node)
1551 1551 except CommitError:
1552 1552 pass
1553 1553 not_removed = set(self.removed) - really_removed
1554 1554 if not_removed:
1555 1555 # TODO: johbo: This code branch does not seem to be covered
1556 1556 raise NodeDoesNotExistError(
1557 1557 "Cannot remove node at %s from "
1558 1558 "following parents: %s" % (not_removed, parents))
1559 1559
1560 1560 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1561 1561 """
1562 1562 Performs in-memory commit (doesn't check workdir in any way) and
1563 1563 returns newly created :class:`BaseCommit`. Updates repository's
1564 1564 attribute `commits`.
1565 1565
1566 1566 .. note::
1567 1567
1568 1568 While overriding this method each backend's should call
1569 1569 ``self.check_integrity(parents)`` in the first place.
1570 1570
1571 1571 :param message: message of the commit
1572 1572 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1573 1573 :param parents: single parent or sequence of parents from which commit
1574 1574 would be derived
1575 1575 :param date: ``datetime.datetime`` instance. Defaults to
1576 1576 ``datetime.datetime.now()``.
1577 1577 :param branch: branch name, as string. If none given, default backend's
1578 1578 branch would be used.
1579 1579
1580 1580 :raises ``CommitError``: if any error occurs while committing
1581 1581 """
1582 1582 raise NotImplementedError
1583 1583
1584 1584
1585 1585 class BaseInMemoryChangesetClass(type):
1586 1586
1587 1587 def __instancecheck__(self, instance):
1588 1588 return isinstance(instance, BaseInMemoryCommit)
1589 1589
1590 1590
1591 1591 class BaseInMemoryChangeset(BaseInMemoryCommit):
1592 1592
1593 1593 __metaclass__ = BaseInMemoryChangesetClass
1594 1594
1595 1595 def __new__(cls, *args, **kwargs):
1596 1596 warnings.warn(
1597 1597 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1598 1598 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1599 1599
1600 1600
1601 1601 class EmptyCommit(BaseCommit):
1602 1602 """
1603 1603 An dummy empty commit. It's possible to pass hash when creating
1604 1604 an EmptyCommit
1605 1605 """
1606 1606
1607 1607 def __init__(
1608 1608 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1609 1609 message='', author='', date=None):
1610 1610 self._empty_commit_id = commit_id
1611 1611 # TODO: johbo: Solve idx parameter, default value does not make
1612 1612 # too much sense
1613 1613 self.idx = idx
1614 1614 self.message = message
1615 1615 self.author = author
1616 1616 self.date = date or datetime.datetime.fromtimestamp(0)
1617 1617 self.repository = repo
1618 1618 self.alias = alias
1619 1619
1620 1620 @LazyProperty
1621 1621 def raw_id(self):
1622 1622 """
1623 1623 Returns raw string identifying this commit, useful for web
1624 1624 representation.
1625 1625 """
1626 1626
1627 1627 return self._empty_commit_id
1628 1628
1629 1629 @LazyProperty
1630 1630 def branch(self):
1631 1631 if self.alias:
1632 1632 from rhodecode.lib.vcs.backends import get_backend
1633 1633 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1634 1634
1635 1635 @LazyProperty
1636 1636 def short_id(self):
1637 1637 return self.raw_id[:12]
1638 1638
1639 1639 @LazyProperty
1640 1640 def id(self):
1641 1641 return self.raw_id
1642 1642
1643 1643 def get_path_commit(self, path):
1644 1644 return self
1645 1645
1646 1646 def get_file_content(self, path):
1647 1647 return u''
1648 1648
1649 1649 def get_file_content_streamed(self, path):
1650 1650 yield self.get_file_content()
1651 1651
1652 1652 def get_file_size(self, path):
1653 1653 return 0
1654 1654
1655 1655
1656 1656 class EmptyChangesetClass(type):
1657 1657
1658 1658 def __instancecheck__(self, instance):
1659 1659 return isinstance(instance, EmptyCommit)
1660 1660
1661 1661
1662 1662 class EmptyChangeset(EmptyCommit):
1663 1663
1664 1664 __metaclass__ = EmptyChangesetClass
1665 1665
1666 1666 def __new__(cls, *args, **kwargs):
1667 1667 warnings.warn(
1668 1668 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1669 1669 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1670 1670
1671 1671 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1672 1672 alias=None, revision=-1, message='', author='', date=None):
1673 1673 if requested_revision is not None:
1674 1674 warnings.warn(
1675 1675 "Parameter requested_revision not supported anymore",
1676 1676 DeprecationWarning)
1677 1677 super(EmptyChangeset, self).__init__(
1678 1678 commit_id=cs, repo=repo, alias=alias, idx=revision,
1679 1679 message=message, author=author, date=date)
1680 1680
1681 1681 @property
1682 1682 def revision(self):
1683 1683 warnings.warn("Use idx instead", DeprecationWarning)
1684 1684 return self.idx
1685 1685
1686 1686 @revision.setter
1687 1687 def revision(self, value):
1688 1688 warnings.warn("Use idx instead", DeprecationWarning)
1689 1689 self.idx = value
1690 1690
1691 1691
1692 1692 class EmptyRepository(BaseRepository):
1693 1693 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1694 1694 pass
1695 1695
1696 1696 def get_diff(self, *args, **kwargs):
1697 1697 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1698 1698 return GitDiff('')
1699 1699
1700 1700
1701 1701 class CollectionGenerator(object):
1702 1702
1703 1703 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1704 1704 self.repo = repo
1705 1705 self.commit_ids = commit_ids
1706 1706 # TODO: (oliver) this isn't currently hooked up
1707 1707 self.collection_size = None
1708 1708 self.pre_load = pre_load
1709 1709 self.translate_tag = translate_tag
1710 1710
1711 1711 def __len__(self):
1712 1712 if self.collection_size is not None:
1713 1713 return self.collection_size
1714 1714 return self.commit_ids.__len__()
1715 1715
1716 1716 def __iter__(self):
1717 1717 for commit_id in self.commit_ids:
1718 1718 # TODO: johbo: Mercurial passes in commit indices or commit ids
1719 1719 yield self._commit_factory(commit_id)
1720 1720
1721 1721 def _commit_factory(self, commit_id):
1722 1722 """
1723 1723 Allows backends to override the way commits are generated.
1724 1724 """
1725 1725 return self.repo.get_commit(
1726 1726 commit_id=commit_id, pre_load=self.pre_load,
1727 1727 translate_tag=self.translate_tag)
1728 1728
1729 1729 def __getslice__(self, i, j):
1730 1730 """
1731 1731 Returns an iterator of sliced repository
1732 1732 """
1733 1733 commit_ids = self.commit_ids[i:j]
1734 1734 return self.__class__(
1735 1735 self.repo, commit_ids, pre_load=self.pre_load,
1736 1736 translate_tag=self.translate_tag)
1737 1737
1738 1738 def __repr__(self):
1739 1739 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1740 1740
1741 1741
1742 1742 class Config(object):
1743 1743 """
1744 1744 Represents the configuration for a repository.
1745 1745
1746 1746 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1747 1747 standard library. It implements only the needed subset.
1748 1748 """
1749 1749
1750 1750 def __init__(self):
1751 1751 self._values = {}
1752 1752
1753 1753 def copy(self):
1754 1754 clone = Config()
1755 1755 for section, values in self._values.items():
1756 1756 clone._values[section] = values.copy()
1757 1757 return clone
1758 1758
1759 1759 def __repr__(self):
1760 1760 return '<Config(%s sections) at %s>' % (
1761 1761 len(self._values), hex(id(self)))
1762 1762
1763 1763 def items(self, section):
1764 1764 return self._values.get(section, {}).iteritems()
1765 1765
1766 1766 def get(self, section, option):
1767 1767 return self._values.get(section, {}).get(option)
1768 1768
1769 1769 def set(self, section, option, value):
1770 1770 section_values = self._values.setdefault(section, {})
1771 1771 section_values[option] = value
1772 1772
1773 1773 def clear_section(self, section):
1774 1774 self._values[section] = {}
1775 1775
1776 1776 def serialize(self):
1777 1777 """
1778 1778 Creates a list of three tuples (section, key, value) representing
1779 1779 this config object.
1780 1780 """
1781 1781 items = []
1782 1782 for section in self._values:
1783 1783 for option, value in self._values[section].items():
1784 1784 items.append(
1785 1785 (safe_str(section), safe_str(option), safe_str(value)))
1786 1786 return items
1787 1787
1788 1788
1789 1789 class Diff(object):
1790 1790 """
1791 1791 Represents a diff result from a repository backend.
1792 1792
1793 1793 Subclasses have to provide a backend specific value for
1794 1794 :attr:`_header_re` and :attr:`_meta_re`.
1795 1795 """
1796 1796 _meta_re = None
1797 1797 _header_re = None
1798 1798
1799 1799 def __init__(self, raw_diff):
1800 1800 self.raw = raw_diff
1801 1801
1802 1802 def chunks(self):
1803 1803 """
1804 1804 split the diff in chunks of separate --git a/file b/file chunks
1805 1805 to make diffs consistent we must prepend with \n, and make sure
1806 1806 we can detect last chunk as this was also has special rule
1807 1807 """
1808 1808
1809 1809 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1810 1810 header = diff_parts[0]
1811 1811
1812 1812 if self._meta_re:
1813 1813 match = self._meta_re.match(header)
1814 1814
1815 1815 chunks = diff_parts[1:]
1816 1816 total_chunks = len(chunks)
1817 1817
1818 1818 return (
1819 1819 DiffChunk(chunk, self, cur_chunk == total_chunks)
1820 1820 for cur_chunk, chunk in enumerate(chunks, start=1))
1821 1821
1822 1822
1823 1823 class DiffChunk(object):
1824 1824
1825 1825 def __init__(self, chunk, diff, last_chunk):
1826 1826 self._diff = diff
1827 1827
1828 1828 # since we split by \ndiff --git that part is lost from original diff
1829 1829 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1830 1830 if not last_chunk:
1831 1831 chunk += '\n'
1832 1832
1833 1833 match = self._diff._header_re.match(chunk)
1834 1834 self.header = match.groupdict()
1835 1835 self.diff = chunk[match.end():]
1836 1836 self.raw = chunk
1837 1837
1838 1838
1839 1839 class BasePathPermissionChecker(object):
1840 1840
1841 1841 @staticmethod
1842 1842 def create_from_patterns(includes, excludes):
1843 1843 if includes and '*' in includes and not excludes:
1844 1844 return AllPathPermissionChecker()
1845 1845 elif excludes and '*' in excludes:
1846 1846 return NonePathPermissionChecker()
1847 1847 else:
1848 1848 return PatternPathPermissionChecker(includes, excludes)
1849 1849
1850 1850 @property
1851 1851 def has_full_access(self):
1852 1852 raise NotImplemented()
1853 1853
1854 1854 def has_access(self, path):
1855 1855 raise NotImplemented()
1856 1856
1857 1857
1858 1858 class AllPathPermissionChecker(BasePathPermissionChecker):
1859 1859
1860 1860 @property
1861 1861 def has_full_access(self):
1862 1862 return True
1863 1863
1864 1864 def has_access(self, path):
1865 1865 return True
1866 1866
1867 1867
1868 1868 class NonePathPermissionChecker(BasePathPermissionChecker):
1869 1869
1870 1870 @property
1871 1871 def has_full_access(self):
1872 1872 return False
1873 1873
1874 1874 def has_access(self, path):
1875 1875 return False
1876 1876
1877 1877
1878 1878 class PatternPathPermissionChecker(BasePathPermissionChecker):
1879 1879
1880 1880 def __init__(self, includes, excludes):
1881 1881 self.includes = includes
1882 1882 self.excludes = excludes
1883 1883 self.includes_re = [] if not includes else [
1884 1884 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1885 1885 self.excludes_re = [] if not excludes else [
1886 1886 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1887 1887
1888 1888 @property
1889 1889 def has_full_access(self):
1890 1890 return '*' in self.includes and not self.excludes
1891 1891
1892 1892 def has_access(self, path):
1893 1893 for regex in self.excludes_re:
1894 1894 if regex.match(path):
1895 1895 return False
1896 1896 for regex in self.includes_re:
1897 1897 if regex.match(path):
1898 1898 return True
1899 1899 return False
@@ -1,1149 +1,1151 b''
1 1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 2
3 3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 5 %></%def>
6 6
7 7 <%def name="action_class(action)">
8 8 <%
9 9 return {
10 10 '-': 'cb-deletion',
11 11 '+': 'cb-addition',
12 12 ' ': 'cb-context',
13 13 }.get(action, 'cb-empty')
14 14 %>
15 15 </%def>
16 16
17 17 <%def name="op_class(op_id)">
18 18 <%
19 19 return {
20 20 DEL_FILENODE: 'deletion', # file deleted
21 21 BIN_FILENODE: 'warning' # binary diff hidden
22 22 }.get(op_id, 'addition')
23 23 %>
24 24 </%def>
25 25
26 26
27 27
28 28 <%def name="render_diffset(diffset, commit=None,
29 29
30 30 # collapse all file diff entries when there are more than this amount of files in the diff
31 31 collapse_when_files_over=20,
32 32
33 33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 34 lines_changed_limit=500,
35 35
36 36 # add a ruler at to the output
37 37 ruler_at_chars=0,
38 38
39 39 # show inline comments
40 40 use_comments=False,
41 41
42 42 # disable new comments
43 43 disable_new_comments=False,
44 44
45 45 # special file-comments that were deleted in previous versions
46 46 # it's used for showing outdated comments for deleted files in a PR
47 47 deleted_files_comments=None,
48 48
49 49 # for cache purpose
50 50 inline_comments=None,
51 51
52 52 # additional menu for PRs
53 53 pull_request_menu=None
54 54
55 55 )">
56 56
57 57 <%
58 58 diffset_container_id = h.md5(diffset.target_ref)
59 59 collapse_all = len(diffset.files) > collapse_when_files_over
60 60 %>
61 61
62 62 %if use_comments:
63 63 <div id="cb-comments-inline-container-template" class="js-template">
64 64 ${inline_comments_container([], inline_comments)}
65 65 </div>
66 66 <div class="js-template" id="cb-comment-inline-form-template">
67 67 <div class="comment-inline-form ac">
68 68
69 69 %if c.rhodecode_user.username != h.DEFAULT_USER:
70 70 ## render template for inline comments
71 71 ${commentblock.comment_form(form_type='inline')}
72 72 %else:
73 73 ${h.form('', class_='inline-form comment-form-login', method='get')}
74 74 <div class="pull-left">
75 75 <div class="comment-help pull-right">
76 76 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
77 77 </div>
78 78 </div>
79 79 <div class="comment-button pull-right">
80 80 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
81 81 ${_('Cancel')}
82 82 </button>
83 83 </div>
84 84 <div class="clearfix"></div>
85 85 ${h.end_form()}
86 86 %endif
87 87 </div>
88 88 </div>
89 89
90 90 %endif
91 91
92 92 %if c.user_session_attrs["diffmode"] == 'sideside':
93 93 <style>
94 94 .wrapper {
95 95 max-width: 1600px !important;
96 96 }
97 97 </style>
98 98 %endif
99 99
100 100 %if ruler_at_chars:
101 101 <style>
102 102 .diff table.cb .cb-content:after {
103 103 content: "";
104 104 border-left: 1px solid blue;
105 105 position: absolute;
106 106 top: 0;
107 107 height: 18px;
108 108 opacity: .2;
109 109 z-index: 10;
110 110 //## +5 to account for diff action (+/-)
111 111 left: ${ruler_at_chars + 5}ch;
112 112 </style>
113 113 %endif
114 114
115 115 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
116 116
117 117 <div style="height: 20px; line-height: 20px">
118 118 ## expand/collapse action
119 119 <div class="pull-left">
120 120 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
121 121 % if collapse_all:
122 122 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
123 123 % else:
124 124 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
125 125 % endif
126 126 </a>
127 127
128 128 </div>
129 129
130 130 ## todos
131 131 % if getattr(c, 'at_version', None):
132 132 <div class="pull-right">
133 133 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
134 134 ${_('not available in this view')}
135 135 </div>
136 136 % else:
137 137 <div class="pull-right">
138 138 <div class="comments-number" style="padding-left: 10px">
139 139 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
140 140 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
141 141 % if c.unresolved_comments:
142 142 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
143 143 ${_('{} unresolved').format(len(c.unresolved_comments))}
144 144 </a>
145 145 % else:
146 146 ${_('0 unresolved')}
147 147 % endif
148 148
149 149 ${_('{} Resolved').format(len(c.resolved_comments))}
150 150 % endif
151 151 </div>
152 152 </div>
153 153 % endif
154 154
155 155 ## comments
156 156 <div class="pull-right">
157 157 <div class="comments-number" style="padding-left: 10px">
158 158 % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
159 159 <i class="icon-comment" style="color: #949494">COMMENTS:</i>
160 160 % if c.comments:
161 161 <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
162 162 % else:
163 163 ${_('0 General')}
164 164 % endif
165 165
166 166 % if c.inline_cnt:
167 167 <a href="#" onclick="return Rhodecode.comments.nextComment();"
168 168 id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
169 169 </a>
170 170 % else:
171 171 ${_('0 Inline')}
172 172 % endif
173 173 % endif
174 174
175 175 % if pull_request_menu:
176 176 <%
177 177 outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
178 178 %>
179 179
180 180 % if outdated_comm_count_ver:
181 181 <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
182 182 (${_("{} Outdated").format(outdated_comm_count_ver)})
183 183 </a>
184 184 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
185 185 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
186 186 % else:
187 187 (${_("{} Outdated").format(outdated_comm_count_ver)})
188 188 % endif
189 189
190 190 % endif
191 191
192 192 </div>
193 193 </div>
194 194
195 195 </div>
196 196
197 197 % if diffset.limited_diff:
198 198 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
199 199 <h2 class="clearinner">
200 200 ${_('The requested changes are too big and content was truncated.')}
201 201 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
202 202 </h2>
203 203 </div>
204 204 ## commit range header for each individual diff
205 205 % elif commit and hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
206 206 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
207 207 <div class="clearinner">
208 208 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a>
209 209 </div>
210 210 </div>
211 211 % endif
212 212
213 213 <div id="todo-box">
214 214 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
215 215 % for co in c.unresolved_comments:
216 216 <a class="permalink" href="#comment-${co.comment_id}"
217 217 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
218 218 <i class="icon-flag-filled-red"></i>
219 219 ${co.comment_id}</a>${('' if loop.last else ',')}
220 220 % endfor
221 221 % endif
222 222 </div>
223 223 %if diffset.has_hidden_changes:
224 224 <p class="empty_data">${_('Some changes may be hidden')}</p>
225 225 %elif not diffset.files:
226 226 <p class="empty_data">${_('No files')}</p>
227 227 %endif
228 228
229 229 <div class="filediffs">
230 230
231 231 ## initial value could be marked as False later on
232 232 <% over_lines_changed_limit = False %>
233 233 %for i, filediff in enumerate(diffset.files):
234 234
235 235 <%
236 236 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
237 237 over_lines_changed_limit = lines_changed > lines_changed_limit
238 238 %>
239 239 ## anchor with support of sticky header
240 240 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
241 241
242 242 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
243 243 <div
244 244 class="filediff"
245 245 data-f-path="${filediff.patch['filename']}"
246 246 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
247 247 >
248 248 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
249 249 <div class="filediff-collapse-indicator icon-"></div>
250 250 ${diff_ops(filediff)}
251 251 </label>
252 252
253 253 ${diff_menu(filediff, use_comments=use_comments)}
254 254 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
255 255
256 256 ## new/deleted/empty content case
257 257 % if not filediff.hunks:
258 258 ## Comment container, on "fakes" hunk that contains all data to render comments
259 259 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
260 260 % endif
261 261
262 262 %if filediff.limited_diff:
263 263 <tr class="cb-warning cb-collapser">
264 264 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
265 265 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
266 266 </td>
267 267 </tr>
268 268 %else:
269 269 %if over_lines_changed_limit:
270 270 <tr class="cb-warning cb-collapser">
271 271 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
272 272 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
273 273 <a href="#" class="cb-expand"
274 274 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
275 275 </a>
276 276 <a href="#" class="cb-collapse"
277 277 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
278 278 </a>
279 279 </td>
280 280 </tr>
281 281 %endif
282 282 %endif
283 283
284 284 % for hunk in filediff.hunks:
285 285 <tr class="cb-hunk">
286 286 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
287 287 ## TODO: dan: add ajax loading of more context here
288 288 ## <a href="#">
289 289 <i class="icon-more"></i>
290 290 ## </a>
291 291 </td>
292 292 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
293 293 @@
294 294 -${hunk.source_start},${hunk.source_length}
295 295 +${hunk.target_start},${hunk.target_length}
296 296 ${hunk.section_header}
297 297 </td>
298 298 </tr>
299 299 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
300 300 % endfor
301 301
302 302 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
303 303
304 304 ## outdated comments that do not fit into currently displayed lines
305 305 % for lineno, comments in unmatched_comments.items():
306 306
307 307 %if c.user_session_attrs["diffmode"] == 'unified':
308 308 % if loop.index == 0:
309 309 <tr class="cb-hunk">
310 310 <td colspan="3"></td>
311 311 <td>
312 312 <div>
313 313 ${_('Unmatched inline comments below')}
314 314 </div>
315 315 </td>
316 316 </tr>
317 317 % endif
318 318 <tr class="cb-line">
319 319 <td class="cb-data cb-context"></td>
320 320 <td class="cb-lineno cb-context"></td>
321 321 <td class="cb-lineno cb-context"></td>
322 322 <td class="cb-content cb-context">
323 323 ${inline_comments_container(comments, inline_comments)}
324 324 </td>
325 325 </tr>
326 326 %elif c.user_session_attrs["diffmode"] == 'sideside':
327 327 % if loop.index == 0:
328 328 <tr class="cb-comment-info">
329 329 <td colspan="2"></td>
330 330 <td class="cb-line">
331 331 <div>
332 332 ${_('Unmatched inline comments below')}
333 333 </div>
334 334 </td>
335 335 <td colspan="2"></td>
336 336 <td class="cb-line">
337 337 <div>
338 338 ${_('Unmatched comments below')}
339 339 </div>
340 340 </td>
341 341 </tr>
342 342 % endif
343 343 <tr class="cb-line">
344 344 <td class="cb-data cb-context"></td>
345 345 <td class="cb-lineno cb-context"></td>
346 346 <td class="cb-content cb-context">
347 347 % if lineno.startswith('o'):
348 348 ${inline_comments_container(comments, inline_comments)}
349 349 % endif
350 350 </td>
351 351
352 352 <td class="cb-data cb-context"></td>
353 353 <td class="cb-lineno cb-context"></td>
354 354 <td class="cb-content cb-context">
355 355 % if lineno.startswith('n'):
356 356 ${inline_comments_container(comments, inline_comments)}
357 357 % endif
358 358 </td>
359 359 </tr>
360 360 %endif
361 361
362 362 % endfor
363 363
364 364 </table>
365 365 </div>
366 366 %endfor
367 367
368 368 ## outdated comments that are made for a file that has been deleted
369 369 % for filename, comments_dict in (deleted_files_comments or {}).items():
370
370 371 <%
371 372 display_state = 'display: none'
372 373 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
373 374 if open_comments_in_file:
374 375 display_state = ''
376 fid = str(id(filename))
375 377 %>
376 378 <div class="filediffs filediff-outdated" style="${display_state}">
377 379 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
378 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(filediff.raw_id, filename)}">
380 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
379 381 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
380 382 <div class="filediff-collapse-indicator icon-"></div>
381 383
382 384 <span class="pill">
383 385 ## file was deleted
384 386 ${filename}
385 387 </span>
386 388 <span class="pill-group pull-left" >
387 389 ## file op, doesn't need translation
388 390 <span class="pill" op="removed">removed in this version</span>
389 391 </span>
390 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filename)}">ΒΆ</a>
392 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}">ΒΆ</a>
391 393 <span class="pill-group pull-right">
392 394 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
393 395 </span>
394 396 </label>
395 397
396 398 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
397 399 <tr>
398 400 % if c.user_session_attrs["diffmode"] == 'unified':
399 401 <td></td>
400 402 %endif
401 403
402 404 <td></td>
403 405 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
404 406 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
405 407 </td>
406 408 </tr>
407 409 %if c.user_session_attrs["diffmode"] == 'unified':
408 410 <tr class="cb-line">
409 411 <td class="cb-data cb-context"></td>
410 412 <td class="cb-lineno cb-context"></td>
411 413 <td class="cb-lineno cb-context"></td>
412 414 <td class="cb-content cb-context">
413 415 ${inline_comments_container(comments_dict['comments'], inline_comments)}
414 416 </td>
415 417 </tr>
416 418 %elif c.user_session_attrs["diffmode"] == 'sideside':
417 419 <tr class="cb-line">
418 420 <td class="cb-data cb-context"></td>
419 421 <td class="cb-lineno cb-context"></td>
420 422 <td class="cb-content cb-context"></td>
421 423
422 424 <td class="cb-data cb-context"></td>
423 425 <td class="cb-lineno cb-context"></td>
424 426 <td class="cb-content cb-context">
425 427 ${inline_comments_container(comments_dict['comments'], inline_comments)}
426 428 </td>
427 429 </tr>
428 430 %endif
429 431 </table>
430 432 </div>
431 433 </div>
432 434 % endfor
433 435
434 436 </div>
435 437 </div>
436 438 </%def>
437 439
438 440 <%def name="diff_ops(filediff)">
439 441 <%
440 442 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
441 443 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
442 444 %>
443 445 <span class="pill">
444 446 <i class="icon-file-text"></i>
445 447 %if filediff.source_file_path and filediff.target_file_path:
446 448 %if filediff.source_file_path != filediff.target_file_path:
447 449 ## file was renamed, or copied
448 450 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
449 451 ${filediff.target_file_path} β¬… <del>${filediff.source_file_path}</del>
450 452 <% final_path = filediff.target_file_path %>
451 453 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
452 454 ${filediff.target_file_path} β¬… ${filediff.source_file_path}
453 455 <% final_path = filediff.target_file_path %>
454 456 %endif
455 457 %else:
456 458 ## file was modified
457 459 ${filediff.source_file_path}
458 460 <% final_path = filediff.source_file_path %>
459 461 %endif
460 462 %else:
461 463 %if filediff.source_file_path:
462 464 ## file was deleted
463 465 ${filediff.source_file_path}
464 466 <% final_path = filediff.source_file_path %>
465 467 %else:
466 468 ## file was added
467 469 ${filediff.target_file_path}
468 470 <% final_path = filediff.target_file_path %>
469 471 %endif
470 472 %endif
471 473 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
472 474 </span>
473 475 ## anchor link
474 476 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
475 477
476 478 <span class="pill-group pull-right">
477 479
478 480 ## ops pills
479 481 %if filediff.limited_diff:
480 482 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
481 483 %endif
482 484
483 485 %if NEW_FILENODE in filediff.patch['stats']['ops']:
484 486 <span class="pill" op="created">created</span>
485 487 %if filediff['target_mode'].startswith('120'):
486 488 <span class="pill" op="symlink">symlink</span>
487 489 %else:
488 490 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
489 491 %endif
490 492 %endif
491 493
492 494 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
493 495 <span class="pill" op="renamed">renamed</span>
494 496 %endif
495 497
496 498 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
497 499 <span class="pill" op="copied">copied</span>
498 500 %endif
499 501
500 502 %if DEL_FILENODE in filediff.patch['stats']['ops']:
501 503 <span class="pill" op="removed">removed</span>
502 504 %endif
503 505
504 506 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
505 507 <span class="pill" op="mode">
506 508 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
507 509 </span>
508 510 %endif
509 511
510 512 %if BIN_FILENODE in filediff.patch['stats']['ops']:
511 513 <span class="pill" op="binary">binary</span>
512 514 %if MOD_FILENODE in filediff.patch['stats']['ops']:
513 515 <span class="pill" op="modified">modified</span>
514 516 %endif
515 517 %endif
516 518
517 519 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
518 520 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
519 521
520 522 </span>
521 523
522 524 </%def>
523 525
524 526 <%def name="nice_mode(filemode)">
525 527 ${(filemode.startswith('100') and filemode[3:] or filemode)}
526 528 </%def>
527 529
528 530 <%def name="diff_menu(filediff, use_comments=False)">
529 531 <div class="filediff-menu">
530 532
531 533 %if filediff.diffset.source_ref:
532 534
533 535 ## FILE BEFORE CHANGES
534 536 %if filediff.operation in ['D', 'M']:
535 537 <a
536 538 class="tooltip"
537 539 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
538 540 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
539 541 >
540 542 ${_('Show file before')}
541 543 </a> |
542 544 %else:
543 545 <span
544 546 class="tooltip"
545 547 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
546 548 >
547 549 ${_('Show file before')}
548 550 </span> |
549 551 %endif
550 552
551 553 ## FILE AFTER CHANGES
552 554 %if filediff.operation in ['A', 'M']:
553 555 <a
554 556 class="tooltip"
555 557 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
556 558 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
557 559 >
558 560 ${_('Show file after')}
559 561 </a>
560 562 %else:
561 563 <span
562 564 class="tooltip"
563 565 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
564 566 >
565 567 ${_('Show file after')}
566 568 </span>
567 569 %endif
568 570
569 571 % if use_comments:
570 572 |
571 573 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
572 574 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
573 575 </a>
574 576 % endif
575 577
576 578 %endif
577 579
578 580 </div>
579 581 </%def>
580 582
581 583
582 584 <%def name="inline_comments_container(comments, inline_comments)">
583 585 <div class="inline-comments">
584 586 %for comment in comments:
585 587 ${commentblock.comment_block(comment, inline=True)}
586 588 %endfor
587 589 % if comments and comments[-1].outdated:
588 590 <span class="btn btn-secondary cb-comment-add-button comment-outdated}" style="display: none;}">
589 591 ${_('Add another comment')}
590 592 </span>
591 593 % else:
592 594 <span onclick="return Rhodecode.comments.createComment(this)" class="btn btn-secondary cb-comment-add-button">
593 595 ${_('Add another comment')}
594 596 </span>
595 597 % endif
596 598
597 599 </div>
598 600 </%def>
599 601
600 602 <%!
601 603 def get_comments_for(diff_type, comments, filename, line_version, line_number):
602 604 if hasattr(filename, 'unicode_path'):
603 605 filename = filename.unicode_path
604 606
605 607 if not isinstance(filename, (unicode, str)):
606 608 return None
607 609
608 610 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
609 611
610 612 if comments and filename in comments:
611 613 file_comments = comments[filename]
612 614 if line_key in file_comments:
613 615 data = file_comments.pop(line_key)
614 616 return data
615 617 %>
616 618
617 619 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
618 620 %for i, line in enumerate(hunk.sideside):
619 621 <%
620 622 old_line_anchor, new_line_anchor = None, None
621 623
622 624 if line.original.lineno:
623 625 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
624 626 if line.modified.lineno:
625 627 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
626 628 %>
627 629
628 630 <tr class="cb-line">
629 631 <td class="cb-data ${action_class(line.original.action)}"
630 632 data-line-no="${line.original.lineno}"
631 633 >
632 634 <div>
633 635
634 636 <% line_old_comments = None %>
635 637 %if line.original.get_comment_args:
636 638 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
637 639 %endif
638 640 %if line_old_comments:
639 641 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
640 642 % if has_outdated:
641 643 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
642 644 % else:
643 645 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
644 646 % endif
645 647 %endif
646 648 </div>
647 649 </td>
648 650 <td class="cb-lineno ${action_class(line.original.action)}"
649 651 data-line-no="${line.original.lineno}"
650 652 %if old_line_anchor:
651 653 id="${old_line_anchor}"
652 654 %endif
653 655 >
654 656 %if line.original.lineno:
655 657 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
656 658 %endif
657 659 </td>
658 660 <td class="cb-content ${action_class(line.original.action)}"
659 661 data-line-no="o${line.original.lineno}"
660 662 >
661 663 %if use_comments and line.original.lineno:
662 664 ${render_add_comment_button()}
663 665 %endif
664 666 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
665 667
666 668 %if use_comments and line.original.lineno and line_old_comments:
667 669 ${inline_comments_container(line_old_comments, inline_comments)}
668 670 %endif
669 671
670 672 </td>
671 673 <td class="cb-data ${action_class(line.modified.action)}"
672 674 data-line-no="${line.modified.lineno}"
673 675 >
674 676 <div>
675 677
676 678 %if line.modified.get_comment_args:
677 679 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
678 680 %else:
679 681 <% line_new_comments = None%>
680 682 %endif
681 683 %if line_new_comments:
682 684 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
683 685 % if has_outdated:
684 686 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
685 687 % else:
686 688 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
687 689 % endif
688 690 %endif
689 691 </div>
690 692 </td>
691 693 <td class="cb-lineno ${action_class(line.modified.action)}"
692 694 data-line-no="${line.modified.lineno}"
693 695 %if new_line_anchor:
694 696 id="${new_line_anchor}"
695 697 %endif
696 698 >
697 699 %if line.modified.lineno:
698 700 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
699 701 %endif
700 702 </td>
701 703 <td class="cb-content ${action_class(line.modified.action)}"
702 704 data-line-no="n${line.modified.lineno}"
703 705 >
704 706 %if use_comments and line.modified.lineno:
705 707 ${render_add_comment_button()}
706 708 %endif
707 709 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
708 710 %if use_comments and line.modified.lineno and line_new_comments:
709 711 ${inline_comments_container(line_new_comments, inline_comments)}
710 712 %endif
711 713 </td>
712 714 </tr>
713 715 %endfor
714 716 </%def>
715 717
716 718
717 719 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
718 720 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
719 721
720 722 <%
721 723 old_line_anchor, new_line_anchor = None, None
722 724 if old_line_no:
723 725 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
724 726 if new_line_no:
725 727 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
726 728 %>
727 729 <tr class="cb-line">
728 730 <td class="cb-data ${action_class(action)}">
729 731 <div>
730 732
731 733 %if comments_args:
732 734 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
733 735 %else:
734 736 <% comments = None %>
735 737 %endif
736 738
737 739 % if comments:
738 740 <% has_outdated = any([x.outdated for x in comments]) %>
739 741 % if has_outdated:
740 742 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
741 743 % else:
742 744 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
743 745 % endif
744 746 % endif
745 747 </div>
746 748 </td>
747 749 <td class="cb-lineno ${action_class(action)}"
748 750 data-line-no="${old_line_no}"
749 751 %if old_line_anchor:
750 752 id="${old_line_anchor}"
751 753 %endif
752 754 >
753 755 %if old_line_anchor:
754 756 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
755 757 %endif
756 758 </td>
757 759 <td class="cb-lineno ${action_class(action)}"
758 760 data-line-no="${new_line_no}"
759 761 %if new_line_anchor:
760 762 id="${new_line_anchor}"
761 763 %endif
762 764 >
763 765 %if new_line_anchor:
764 766 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
765 767 %endif
766 768 </td>
767 769 <td class="cb-content ${action_class(action)}"
768 770 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
769 771 >
770 772 %if use_comments:
771 773 ${render_add_comment_button()}
772 774 %endif
773 775 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
774 776 %if use_comments and comments:
775 777 ${inline_comments_container(comments, inline_comments)}
776 778 %endif
777 779 </td>
778 780 </tr>
779 781 %endfor
780 782 </%def>
781 783
782 784
783 785 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
784 786 % if diff_mode == 'unified':
785 787 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
786 788 % elif diff_mode == 'sideside':
787 789 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
788 790 % else:
789 791 <tr class="cb-line">
790 792 <td>unknown diff mode</td>
791 793 </tr>
792 794 % endif
793 795 </%def>file changes
794 796
795 797
796 798 <%def name="render_add_comment_button()">
797 799 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
798 800 <span><i class="icon-comment"></i></span>
799 801 </button>
800 802 </%def>
801 803
802 804 <%def name="render_diffset_menu(diffset, range_diff_on=None)">
803 805 <% diffset_container_id = h.md5(diffset.target_ref) %>
804 806
805 807 <div id="diff-file-sticky" class="diffset-menu clearinner">
806 808 ## auto adjustable
807 809 <div class="sidebar__inner">
808 810 <div class="sidebar__bar">
809 811 <div class="pull-right">
810 812 <div class="btn-group">
811 813 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
812 814 <i class="icon-wide-mode"></i>
813 815 </a>
814 816 </div>
815 817 <div class="btn-group">
816 818
817 819 <a
818 820 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
819 821 title="${h.tooltip(_('View diff as side by side'))}"
820 822 href="${h.current_route_path(request, diffmode='sideside')}">
821 823 <span>${_('Side by Side')}</span>
822 824 </a>
823 825
824 826 <a
825 827 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
826 828 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
827 829 <span>${_('Unified')}</span>
828 830 </a>
829 831
830 832 % if range_diff_on is True:
831 833 <a
832 834 title="${_('Turn off: Show the diff as commit range')}"
833 835 class="btn btn-primary"
834 836 href="${h.current_route_path(request, **{"range-diff":"0"})}">
835 837 <span>${_('Range Diff')}</span>
836 838 </a>
837 839 % elif range_diff_on is False:
838 840 <a
839 841 title="${_('Show the diff as commit range')}"
840 842 class="btn"
841 843 href="${h.current_route_path(request, **{"range-diff":"1"})}">
842 844 <span>${_('Range Diff')}</span>
843 845 </a>
844 846 % endif
845 847 </div>
846 848 <div class="btn-group">
847 849
848 850 <div class="pull-left">
849 851 ${h.hidden('diff_menu_{}'.format(diffset_container_id))}
850 852 </div>
851 853
852 854 </div>
853 855 </div>
854 856 <div class="pull-left">
855 857 <div class="btn-group">
856 858 <div class="pull-left">
857 859 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
858 860 </div>
859 861
860 862 </div>
861 863 </div>
862 864 </div>
863 865 <div class="fpath-placeholder">
864 866 <i class="icon-file-text"></i>
865 867 <strong class="fpath-placeholder-text">
866 868 Context file:
867 869 </strong>
868 870 </div>
869 871 <div class="sidebar_inner_shadow"></div>
870 872 </div>
871 873 </div>
872 874
873 875 % if diffset:
874 876 %if diffset.limited_diff:
875 877 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
876 878 %else:
877 879 <% file_placeholder = h.literal(_ungettext('%(num)s file changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>', '%(num)s files changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>',
878 880 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
879 881
880 882 %endif
881 883 ## case on range-diff placeholder needs to be updated
882 884 % if range_diff_on is True:
883 885 <% file_placeholder = _('Disabled on range diff') %>
884 886 % endif
885 887
886 888 <script type="text/javascript">
887 889 var feedFilesOptions = function (query, initialData) {
888 890 var data = {results: []};
889 891 var isQuery = typeof query.term !== 'undefined';
890 892
891 893 var section = _gettext('Changed files');
892 894 var filteredData = [];
893 895
894 896 //filter results
895 897 $.each(initialData.results, function (idx, value) {
896 898
897 899 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
898 900 filteredData.push({
899 901 'id': this.id,
900 902 'text': this.text,
901 903 "ops": this.ops,
902 904 })
903 905 }
904 906
905 907 });
906 908
907 909 data.results = filteredData;
908 910
909 911 query.callback(data);
910 912 };
911 913
912 914 var selectionFormatter = function(data, escapeMarkup) {
913 915 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
914 916 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
915 917 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
916 918 '<span class="pill" op="added">{0}</span>' +
917 919 '<span class="pill" op="deleted">{1}</span>' +
918 920 '</div>'
919 921 ;
920 922 var added = data['ops']['added'];
921 923 if (added === 0) {
922 924 // don't show +0
923 925 added = 0;
924 926 } else {
925 927 added = '+' + added;
926 928 }
927 929
928 930 var deleted = -1*data['ops']['deleted'];
929 931
930 932 tmpl += pill.format(added, deleted);
931 933 return container.format(tmpl);
932 934 };
933 935 var formatFileResult = function(result, container, query, escapeMarkup) {
934 936 return selectionFormatter(result, escapeMarkup);
935 937 };
936 938
937 939 var formatSelection = function (data, container) {
938 940 return '${file_placeholder}'
939 941 };
940 942
941 943 if (window.preloadFileFilterData === undefined) {
942 944 window.preloadFileFilterData = {}
943 945 }
944 946
945 947 preloadFileFilterData["${diffset_container_id}"] = {
946 948 results: [
947 949 % for filediff in diffset.files:
948 950 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
949 951 text:"${filediff.patch['filename']}",
950 952 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
951 953 % endfor
952 954 ]
953 955 };
954 956
955 957 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
956 958 var diffFileFilter = $(diffFileFilterId).select2({
957 959 'dropdownAutoWidth': true,
958 960 'width': 'auto',
959 961
960 962 containerCssClass: "drop-menu",
961 963 dropdownCssClass: "drop-menu-dropdown",
962 964 data: preloadFileFilterData["${diffset_container_id}"],
963 965 query: function(query) {
964 966 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
965 967 },
966 968 initSelection: function(element, callback) {
967 969 callback({'init': true});
968 970 },
969 971 formatResult: formatFileResult,
970 972 formatSelection: formatSelection
971 973 });
972 974
973 975 % if range_diff_on is True:
974 976 diffFileFilter.select2("enable", false);
975 977 % endif
976 978
977 979 $(diffFileFilterId).on('select2-selecting', function (e) {
978 980 var idSelector = e.choice.id;
979 981
980 982 // expand the container if we quick-select the field
981 983 $('#'+idSelector).next().prop('checked', false);
982 984 // hide the mast as we later do preventDefault()
983 985 $("#select2-drop-mask").click();
984 986
985 987 window.location.hash = '#'+idSelector;
986 988 updateSticky();
987 989
988 990 e.preventDefault();
989 991 });
990 992
991 993 </script>
992 994 % endif
993 995
994 996 <script type="text/javascript">
995 997 $(document).ready(function () {
996 998
997 999 var contextPrefix = _gettext('Context file: ');
998 1000 ## sticky sidebar
999 1001 var sidebarElement = document.getElementById('diff-file-sticky');
1000 1002 sidebar = new StickySidebar(sidebarElement, {
1001 1003 topSpacing: 0,
1002 1004 bottomSpacing: 0,
1003 1005 innerWrapperSelector: '.sidebar__inner'
1004 1006 });
1005 1007 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1006 1008 // reset our file so it's not holding new value
1007 1009 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1008 1010 });
1009 1011
1010 1012 updateSticky = function () {
1011 1013 sidebar.updateSticky();
1012 1014 Waypoint.refreshAll();
1013 1015 };
1014 1016
1015 1017 var animateText = function (fPath, anchorId) {
1016 1018 fPath = Select2.util.escapeMarkup(fPath);
1017 1019 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1018 1020 };
1019 1021
1020 1022 ## dynamic file waypoints
1021 1023 var setFPathInfo = function(fPath, anchorId){
1022 1024 animateText(fPath, anchorId)
1023 1025 };
1024 1026
1025 1027 var codeBlock = $('.filediff');
1026 1028
1027 1029 // forward waypoint
1028 1030 codeBlock.waypoint(
1029 1031 function(direction) {
1030 1032 if (direction === "down"){
1031 1033 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1032 1034 }
1033 1035 }, {
1034 1036 offset: function () {
1035 1037 return 70;
1036 1038 },
1037 1039 context: '.fpath-placeholder'
1038 1040 }
1039 1041 );
1040 1042
1041 1043 // backward waypoint
1042 1044 codeBlock.waypoint(
1043 1045 function(direction) {
1044 1046 if (direction === "up"){
1045 1047 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1046 1048 }
1047 1049 }, {
1048 1050 offset: function () {
1049 1051 return -this.element.clientHeight + 90;
1050 1052 },
1051 1053 context: '.fpath-placeholder'
1052 1054 }
1053 1055 );
1054 1056
1055 1057 toggleWideDiff = function (el) {
1056 1058 updateSticky();
1057 1059 var wide = Rhodecode.comments.toggleWideMode(this);
1058 1060 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1059 1061 if (wide === true) {
1060 1062 $(el).addClass('btn-active');
1061 1063 } else {
1062 1064 $(el).removeClass('btn-active');
1063 1065 }
1064 1066 return null;
1065 1067 };
1066 1068
1067 1069 var preloadDiffMenuData = {
1068 1070 results: [
1069 1071
1070 1072 ## Whitespace change
1071 1073 % if request.GET.get('ignorews', '') == '1':
1072 1074 {
1073 1075 id: 2,
1074 1076 text: _gettext('Show whitespace changes'),
1075 1077 action: function () {},
1076 1078 url: "${h.current_route_path(request, ignorews=0)|n}"
1077 1079 },
1078 1080 % else:
1079 1081 {
1080 1082 id: 2,
1081 1083 text: _gettext('Hide whitespace changes'),
1082 1084 action: function () {},
1083 1085 url: "${h.current_route_path(request, ignorews=1)|n}"
1084 1086 },
1085 1087 % endif
1086 1088
1087 1089 ## FULL CONTEXT
1088 1090 % if request.GET.get('fullcontext', '') == '1':
1089 1091 {
1090 1092 id: 3,
1091 1093 text: _gettext('Hide full context diff'),
1092 1094 action: function () {},
1093 1095 url: "${h.current_route_path(request, fullcontext=0)|n}"
1094 1096 },
1095 1097 % else:
1096 1098 {
1097 1099 id: 3,
1098 1100 text: _gettext('Show full context diff'),
1099 1101 action: function () {},
1100 1102 url: "${h.current_route_path(request, fullcontext=1)|n}"
1101 1103 },
1102 1104 % endif
1103 1105
1104 1106 ]
1105 1107 };
1106 1108
1107 1109 // get stored diff mode and pre-enable it
1108 1110 if (templateContext.session_attrs.wide_diff_mode === "true") {
1109 1111 Rhodecode.comments.toggleWideMode(null);
1110 1112 $('.toggle-wide-diff').addClass('btn-active');
1111 1113 }
1112 1114
1113 1115 var diffMenuId = "#diff_menu_" + "${diffset_container_id}";
1114 1116 $(diffMenuId).select2({
1115 1117 minimumResultsForSearch: -1,
1116 1118 containerCssClass: "drop-menu-no-width",
1117 1119 dropdownCssClass: "drop-menu-dropdown",
1118 1120 dropdownAutoWidth: true,
1119 1121 data: preloadDiffMenuData,
1120 1122 placeholder: "${_('...')}",
1121 1123 });
1122 1124 $(diffMenuId).on('select2-selecting', function (e) {
1123 1125 e.choice.action();
1124 1126 if (e.choice.url !== null) {
1125 1127 window.location = e.choice.url
1126 1128 }
1127 1129 });
1128 1130 toggleExpand = function (el, diffsetEl) {
1129 1131 var el = $(el);
1130 1132 if (el.hasClass('collapsed')) {
1131 1133 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1132 1134 el.removeClass('collapsed');
1133 1135 el.html(
1134 1136 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1135 1137 _gettext('Collapse all files'));
1136 1138 }
1137 1139 else {
1138 1140 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1139 1141 el.addClass('collapsed');
1140 1142 el.html(
1141 1143 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1142 1144 _gettext('Expand all files'));
1143 1145 }
1144 1146 updateSticky()
1145 1147 }
1146 1148 });
1147 1149 </script>
1148 1150
1149 1151 </%def>
General Comments 0
You need to be logged in to leave comments. Login now