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