##// END OF EJS Templates
api: expose merge message in the merge operation
marcink -
r3458:a818b875 default
parent child Browse files
Show More
@@ -1,157 +1,158 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 import pytest
21 import pytest
22
22
23 from rhodecode.model.db import UserLog, PullRequest
23 from rhodecode.model.db import UserLog, PullRequest
24 from rhodecode.model.meta import Session
24 from rhodecode.model.meta import Session
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
27 build_data, api_call, assert_error, assert_ok)
27 build_data, api_call, assert_error, assert_ok)
28
28
29
29
30 @pytest.mark.usefixtures("testuser_api", "app")
30 @pytest.mark.usefixtures("testuser_api", "app")
31 class TestMergePullRequest(object):
31 class TestMergePullRequest(object):
32
32
33 @pytest.mark.backends("git", "hg")
33 @pytest.mark.backends("git", "hg")
34 def test_api_merge_pull_request_merge_failed(self, pr_util, no_notifications):
34 def test_api_merge_pull_request_merge_failed(self, pr_util, no_notifications):
35 pull_request = pr_util.create_pull_request(mergeable=True)
35 pull_request = pr_util.create_pull_request(mergeable=True)
36 pull_request_id = pull_request.pull_request_id
36 pull_request_id = pull_request.pull_request_id
37 pull_request_repo = pull_request.target_repo.repo_name
37 pull_request_repo = pull_request.target_repo.repo_name
38
38
39 id_, params = build_data(
39 id_, params = build_data(
40 self.apikey, 'merge_pull_request',
40 self.apikey, 'merge_pull_request',
41 repoid=pull_request_repo,
41 repoid=pull_request_repo,
42 pullrequestid=pull_request_id)
42 pullrequestid=pull_request_id)
43
43
44 response = api_call(self.app, params)
44 response = api_call(self.app, params)
45
45
46 # The above api call detaches the pull request DB object from the
46 # The above api call detaches the pull request DB object from the
47 # session because of an unconditional transaction rollback in our
47 # session because of an unconditional transaction rollback in our
48 # middleware. Therefore we need to add it back here if we want to use it.
48 # middleware. Therefore we need to add it back here if we want to use it.
49 Session().add(pull_request)
49 Session().add(pull_request)
50
50
51 expected = 'merge not possible for following reasons: ' \
51 expected = 'merge not possible for following reasons: ' \
52 'Pull request reviewer approval is pending.'
52 'Pull request reviewer approval is pending.'
53 assert_error(id_, expected, given=response.body)
53 assert_error(id_, expected, given=response.body)
54
54
55 @pytest.mark.backends("git", "hg")
55 @pytest.mark.backends("git", "hg")
56 def test_api_merge_pull_request_merge_failed_disallowed_state(
56 def test_api_merge_pull_request_merge_failed_disallowed_state(
57 self, pr_util, no_notifications):
57 self, pr_util, no_notifications):
58 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
58 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
59 pull_request_id = pull_request.pull_request_id
59 pull_request_id = pull_request.pull_request_id
60 pull_request_repo = pull_request.target_repo.repo_name
60 pull_request_repo = pull_request.target_repo.repo_name
61
61
62 pr = PullRequest.get(pull_request_id)
62 pr = PullRequest.get(pull_request_id)
63 pr.pull_request_state = pull_request.STATE_UPDATING
63 pr.pull_request_state = pull_request.STATE_UPDATING
64 Session().add(pr)
64 Session().add(pr)
65 Session().commit()
65 Session().commit()
66
66
67 id_, params = build_data(
67 id_, params = build_data(
68 self.apikey, 'merge_pull_request',
68 self.apikey, 'merge_pull_request',
69 repoid=pull_request_repo,
69 repoid=pull_request_repo,
70 pullrequestid=pull_request_id)
70 pullrequestid=pull_request_id)
71
71
72 response = api_call(self.app, params)
72 response = api_call(self.app, params)
73 expected = 'Operation forbidden because pull request is in state {}, '\
73 expected = 'Operation forbidden because pull request is in state {}, '\
74 'only state {} is allowed.'.format(PullRequest.STATE_UPDATING,
74 'only state {} is allowed.'.format(PullRequest.STATE_UPDATING,
75 PullRequest.STATE_CREATED)
75 PullRequest.STATE_CREATED)
76 assert_error(id_, expected, given=response.body)
76 assert_error(id_, expected, given=response.body)
77
77
78 @pytest.mark.backends("git", "hg")
78 @pytest.mark.backends("git", "hg")
79 def test_api_merge_pull_request(self, pr_util, no_notifications):
79 def test_api_merge_pull_request(self, pr_util, no_notifications):
80 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
80 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
81 author = pull_request.user_id
81 author = pull_request.user_id
82 repo = pull_request.target_repo.repo_id
82 repo = pull_request.target_repo.repo_id
83 pull_request_id = pull_request.pull_request_id
83 pull_request_id = pull_request.pull_request_id
84 pull_request_repo = pull_request.target_repo.repo_name
84 pull_request_repo = pull_request.target_repo.repo_name
85
85
86 id_, params = build_data(
86 id_, params = build_data(
87 self.apikey, 'comment_pull_request',
87 self.apikey, 'comment_pull_request',
88 repoid=pull_request_repo,
88 repoid=pull_request_repo,
89 pullrequestid=pull_request_id,
89 pullrequestid=pull_request_id,
90 status='approved')
90 status='approved')
91
91
92 response = api_call(self.app, params)
92 response = api_call(self.app, params)
93 expected = {
93 expected = {
94 'comment_id': response.json.get('result', {}).get('comment_id'),
94 'comment_id': response.json.get('result', {}).get('comment_id'),
95 'pull_request_id': pull_request_id,
95 'pull_request_id': pull_request_id,
96 'status': {'given': 'approved', 'was_changed': True}
96 'status': {'given': 'approved', 'was_changed': True}
97 }
97 }
98 assert_ok(id_, expected, given=response.body)
98 assert_ok(id_, expected, given=response.body)
99
99
100 id_, params = build_data(
100 id_, params = build_data(
101 self.apikey, 'merge_pull_request',
101 self.apikey, 'merge_pull_request',
102 repoid=pull_request_repo,
102 repoid=pull_request_repo,
103 pullrequestid=pull_request_id)
103 pullrequestid=pull_request_id)
104
104
105 response = api_call(self.app, params)
105 response = api_call(self.app, params)
106
106
107 pull_request = PullRequest.get(pull_request_id)
107 pull_request = PullRequest.get(pull_request_id)
108
108
109 expected = {
109 expected = {
110 'executed': True,
110 'executed': True,
111 'failure_reason': 0,
111 'failure_reason': 0,
112 'merge_status_message': 'This pull request can be automatically merged.',
112 'possible': True,
113 'possible': True,
113 'merge_commit_id': pull_request.shadow_merge_ref.commit_id,
114 'merge_commit_id': pull_request.shadow_merge_ref.commit_id,
114 'merge_ref': pull_request.shadow_merge_ref._asdict()
115 'merge_ref': pull_request.shadow_merge_ref._asdict()
115 }
116 }
116
117
117 assert_ok(id_, expected, response.body)
118 assert_ok(id_, expected, response.body)
118
119
119 journal = UserLog.query()\
120 journal = UserLog.query()\
120 .filter(UserLog.user_id == author)\
121 .filter(UserLog.user_id == author)\
121 .filter(UserLog.repository_id == repo) \
122 .filter(UserLog.repository_id == repo) \
122 .order_by('user_log_id') \
123 .order_by('user_log_id') \
123 .all()
124 .all()
124 assert journal[-2].action == 'repo.pull_request.merge'
125 assert journal[-2].action == 'repo.pull_request.merge'
125 assert journal[-1].action == 'repo.pull_request.close'
126 assert journal[-1].action == 'repo.pull_request.close'
126
127
127 id_, params = build_data(
128 id_, params = build_data(
128 self.apikey, 'merge_pull_request',
129 self.apikey, 'merge_pull_request',
129 repoid=pull_request_repo, pullrequestid=pull_request_id)
130 repoid=pull_request_repo, pullrequestid=pull_request_id)
130 response = api_call(self.app, params)
131 response = api_call(self.app, params)
131
132
132 expected = 'merge not possible for following reasons: This pull request is closed.'
133 expected = 'merge not possible for following reasons: This pull request is closed.'
133 assert_error(id_, expected, given=response.body)
134 assert_error(id_, expected, given=response.body)
134
135
135 @pytest.mark.backends("git", "hg")
136 @pytest.mark.backends("git", "hg")
136 def test_api_merge_pull_request_repo_error(self, pr_util):
137 def test_api_merge_pull_request_repo_error(self, pr_util):
137 pull_request = pr_util.create_pull_request()
138 pull_request = pr_util.create_pull_request()
138 id_, params = build_data(
139 id_, params = build_data(
139 self.apikey, 'merge_pull_request',
140 self.apikey, 'merge_pull_request',
140 repoid=666, pullrequestid=pull_request.pull_request_id)
141 repoid=666, pullrequestid=pull_request.pull_request_id)
141 response = api_call(self.app, params)
142 response = api_call(self.app, params)
142
143
143 expected = 'repository `666` does not exist'
144 expected = 'repository `666` does not exist'
144 assert_error(id_, expected, given=response.body)
145 assert_error(id_, expected, given=response.body)
145
146
146 @pytest.mark.backends("git", "hg")
147 @pytest.mark.backends("git", "hg")
147 def test_api_merge_pull_request_non_admin_with_userid_error(self, pr_util):
148 def test_api_merge_pull_request_non_admin_with_userid_error(self, pr_util):
148 pull_request = pr_util.create_pull_request(mergeable=True)
149 pull_request = pr_util.create_pull_request(mergeable=True)
149 id_, params = build_data(
150 id_, params = build_data(
150 self.apikey_regular, 'merge_pull_request',
151 self.apikey_regular, 'merge_pull_request',
151 repoid=pull_request.target_repo.repo_name,
152 repoid=pull_request.target_repo.repo_name,
152 pullrequestid=pull_request.pull_request_id,
153 pullrequestid=pull_request.pull_request_id,
153 userid=TEST_USER_ADMIN_LOGIN)
154 userid=TEST_USER_ADMIN_LOGIN)
154 response = api_call(self.app, params)
155 response = api_call(self.app, params)
155
156
156 expected = 'userid is not the same as your user'
157 expected = 'userid is not the same as your user'
157 assert_error(id_, expected, given=response.body)
158 assert_error(id_, expected, given=response.body)
@@ -1,986 +1,987 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2019 RhodeCode GmbH
3 # Copyright (C) 2011-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 logging
22 import logging
23
23
24 from rhodecode import events
24 from rhodecode import events
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 from rhodecode.api.utils import (
26 from rhodecode.api.utils import (
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
29 validate_repo_permissions, resolve_ref_or_error)
29 validate_repo_permissions, resolve_ref_or_error)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
31 from rhodecode.lib.base import vcs_operation_context
31 from rhodecode.lib.base import vcs_operation_context
32 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.model.changeset_status import ChangesetStatusModel
33 from rhodecode.model.changeset_status import ChangesetStatusModel
34 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.comment import CommentsModel
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
38 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
40 ReviewerListSchema)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 @jsonrpc_method()
45 @jsonrpc_method()
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
46 def get_pull_request(request, apiuser, pullrequestid, repoid=Optional(None)):
47 """
47 """
48 Get a pull request based on the given ID.
48 Get a pull request based on the given ID.
49
49
50 :param apiuser: This is filled automatically from the |authtoken|.
50 :param apiuser: This is filled automatically from the |authtoken|.
51 :type apiuser: AuthUser
51 :type apiuser: AuthUser
52 :param repoid: Optional, repository name or repository ID from where
52 :param repoid: Optional, repository name or repository ID from where
53 the pull request was opened.
53 the pull request was opened.
54 :type repoid: str or int
54 :type repoid: str or int
55 :param pullrequestid: ID of the requested pull request.
55 :param pullrequestid: ID of the requested pull request.
56 :type pullrequestid: int
56 :type pullrequestid: int
57
57
58 Example output:
58 Example output:
59
59
60 .. code-block:: bash
60 .. code-block:: bash
61
61
62 "id": <id_given_in_input>,
62 "id": <id_given_in_input>,
63 "result":
63 "result":
64 {
64 {
65 "pull_request_id": "<pull_request_id>",
65 "pull_request_id": "<pull_request_id>",
66 "url": "<url>",
66 "url": "<url>",
67 "title": "<title>",
67 "title": "<title>",
68 "description": "<description>",
68 "description": "<description>",
69 "status" : "<status>",
69 "status" : "<status>",
70 "created_on": "<date_time_created>",
70 "created_on": "<date_time_created>",
71 "updated_on": "<date_time_updated>",
71 "updated_on": "<date_time_updated>",
72 "commit_ids": [
72 "commit_ids": [
73 ...
73 ...
74 "<commit_id>",
74 "<commit_id>",
75 "<commit_id>",
75 "<commit_id>",
76 ...
76 ...
77 ],
77 ],
78 "review_status": "<review_status>",
78 "review_status": "<review_status>",
79 "mergeable": {
79 "mergeable": {
80 "status": "<bool>",
80 "status": "<bool>",
81 "message": "<message>",
81 "message": "<message>",
82 },
82 },
83 "source": {
83 "source": {
84 "clone_url": "<clone_url>",
84 "clone_url": "<clone_url>",
85 "repository": "<repository_name>",
85 "repository": "<repository_name>",
86 "reference":
86 "reference":
87 {
87 {
88 "name": "<name>",
88 "name": "<name>",
89 "type": "<type>",
89 "type": "<type>",
90 "commit_id": "<commit_id>",
90 "commit_id": "<commit_id>",
91 }
91 }
92 },
92 },
93 "target": {
93 "target": {
94 "clone_url": "<clone_url>",
94 "clone_url": "<clone_url>",
95 "repository": "<repository_name>",
95 "repository": "<repository_name>",
96 "reference":
96 "reference":
97 {
97 {
98 "name": "<name>",
98 "name": "<name>",
99 "type": "<type>",
99 "type": "<type>",
100 "commit_id": "<commit_id>",
100 "commit_id": "<commit_id>",
101 }
101 }
102 },
102 },
103 "merge": {
103 "merge": {
104 "clone_url": "<clone_url>",
104 "clone_url": "<clone_url>",
105 "reference":
105 "reference":
106 {
106 {
107 "name": "<name>",
107 "name": "<name>",
108 "type": "<type>",
108 "type": "<type>",
109 "commit_id": "<commit_id>",
109 "commit_id": "<commit_id>",
110 }
110 }
111 },
111 },
112 "author": <user_obj>,
112 "author": <user_obj>,
113 "reviewers": [
113 "reviewers": [
114 ...
114 ...
115 {
115 {
116 "user": "<user_obj>",
116 "user": "<user_obj>",
117 "review_status": "<review_status>",
117 "review_status": "<review_status>",
118 }
118 }
119 ...
119 ...
120 ]
120 ]
121 },
121 },
122 "error": null
122 "error": null
123 """
123 """
124
124
125 pull_request = get_pull_request_or_error(pullrequestid)
125 pull_request = get_pull_request_or_error(pullrequestid)
126 if Optional.extract(repoid):
126 if Optional.extract(repoid):
127 repo = get_repo_or_error(repoid)
127 repo = get_repo_or_error(repoid)
128 else:
128 else:
129 repo = pull_request.target_repo
129 repo = pull_request.target_repo
130
130
131 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
131 if not PullRequestModel().check_user_read(pull_request, apiuser, api=True):
132 raise JSONRPCError('repository `%s` or pull request `%s` '
132 raise JSONRPCError('repository `%s` or pull request `%s` '
133 'does not exist' % (repoid, pullrequestid))
133 'does not exist' % (repoid, pullrequestid))
134
134
135 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
135 # NOTE(marcink): only calculate and return merge state if the pr state is 'created'
136 # otherwise we can lock the repo on calculation of merge state while update/merge
136 # otherwise we can lock the repo on calculation of merge state while update/merge
137 # is happening.
137 # is happening.
138 merge_state = pull_request.pull_request_state == pull_request.STATE_CREATED
138 merge_state = pull_request.pull_request_state == pull_request.STATE_CREATED
139 data = pull_request.get_api_data(with_merge_state=merge_state)
139 data = pull_request.get_api_data(with_merge_state=merge_state)
140 return data
140 return data
141
141
142
142
143 @jsonrpc_method()
143 @jsonrpc_method()
144 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
144 def get_pull_requests(request, apiuser, repoid, status=Optional('new'),
145 merge_state=Optional(True)):
145 merge_state=Optional(True)):
146 """
146 """
147 Get all pull requests from the repository specified in `repoid`.
147 Get all pull requests from the repository specified in `repoid`.
148
148
149 :param apiuser: This is filled automatically from the |authtoken|.
149 :param apiuser: This is filled automatically from the |authtoken|.
150 :type apiuser: AuthUser
150 :type apiuser: AuthUser
151 :param repoid: Optional repository name or repository ID.
151 :param repoid: Optional repository name or repository ID.
152 :type repoid: str or int
152 :type repoid: str or int
153 :param status: Only return pull requests with the specified status.
153 :param status: Only return pull requests with the specified status.
154 Valid options are.
154 Valid options are.
155 * ``new`` (default)
155 * ``new`` (default)
156 * ``open``
156 * ``open``
157 * ``closed``
157 * ``closed``
158 :type status: str
158 :type status: str
159 :param merge_state: Optional calculate merge state for each repository.
159 :param merge_state: Optional calculate merge state for each repository.
160 This could result in longer time to fetch the data
160 This could result in longer time to fetch the data
161 :type merge_state: bool
161 :type merge_state: bool
162
162
163 Example output:
163 Example output:
164
164
165 .. code-block:: bash
165 .. code-block:: bash
166
166
167 "id": <id_given_in_input>,
167 "id": <id_given_in_input>,
168 "result":
168 "result":
169 [
169 [
170 ...
170 ...
171 {
171 {
172 "pull_request_id": "<pull_request_id>",
172 "pull_request_id": "<pull_request_id>",
173 "url": "<url>",
173 "url": "<url>",
174 "title" : "<title>",
174 "title" : "<title>",
175 "description": "<description>",
175 "description": "<description>",
176 "status": "<status>",
176 "status": "<status>",
177 "created_on": "<date_time_created>",
177 "created_on": "<date_time_created>",
178 "updated_on": "<date_time_updated>",
178 "updated_on": "<date_time_updated>",
179 "commit_ids": [
179 "commit_ids": [
180 ...
180 ...
181 "<commit_id>",
181 "<commit_id>",
182 "<commit_id>",
182 "<commit_id>",
183 ...
183 ...
184 ],
184 ],
185 "review_status": "<review_status>",
185 "review_status": "<review_status>",
186 "mergeable": {
186 "mergeable": {
187 "status": "<bool>",
187 "status": "<bool>",
188 "message: "<message>",
188 "message: "<message>",
189 },
189 },
190 "source": {
190 "source": {
191 "clone_url": "<clone_url>",
191 "clone_url": "<clone_url>",
192 "reference":
192 "reference":
193 {
193 {
194 "name": "<name>",
194 "name": "<name>",
195 "type": "<type>",
195 "type": "<type>",
196 "commit_id": "<commit_id>",
196 "commit_id": "<commit_id>",
197 }
197 }
198 },
198 },
199 "target": {
199 "target": {
200 "clone_url": "<clone_url>",
200 "clone_url": "<clone_url>",
201 "reference":
201 "reference":
202 {
202 {
203 "name": "<name>",
203 "name": "<name>",
204 "type": "<type>",
204 "type": "<type>",
205 "commit_id": "<commit_id>",
205 "commit_id": "<commit_id>",
206 }
206 }
207 },
207 },
208 "merge": {
208 "merge": {
209 "clone_url": "<clone_url>",
209 "clone_url": "<clone_url>",
210 "reference":
210 "reference":
211 {
211 {
212 "name": "<name>",
212 "name": "<name>",
213 "type": "<type>",
213 "type": "<type>",
214 "commit_id": "<commit_id>",
214 "commit_id": "<commit_id>",
215 }
215 }
216 },
216 },
217 "author": <user_obj>,
217 "author": <user_obj>,
218 "reviewers": [
218 "reviewers": [
219 ...
219 ...
220 {
220 {
221 "user": "<user_obj>",
221 "user": "<user_obj>",
222 "review_status": "<review_status>",
222 "review_status": "<review_status>",
223 }
223 }
224 ...
224 ...
225 ]
225 ]
226 }
226 }
227 ...
227 ...
228 ],
228 ],
229 "error": null
229 "error": null
230
230
231 """
231 """
232 repo = get_repo_or_error(repoid)
232 repo = get_repo_or_error(repoid)
233 if not has_superadmin_permission(apiuser):
233 if not has_superadmin_permission(apiuser):
234 _perms = (
234 _perms = (
235 'repository.admin', 'repository.write', 'repository.read',)
235 'repository.admin', 'repository.write', 'repository.read',)
236 validate_repo_permissions(apiuser, repoid, repo, _perms)
236 validate_repo_permissions(apiuser, repoid, repo, _perms)
237
237
238 status = Optional.extract(status)
238 status = Optional.extract(status)
239 merge_state = Optional.extract(merge_state, binary=True)
239 merge_state = Optional.extract(merge_state, binary=True)
240 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
240 pull_requests = PullRequestModel().get_all(repo, statuses=[status],
241 order_by='id', order_dir='desc')
241 order_by='id', order_dir='desc')
242 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
242 data = [pr.get_api_data(with_merge_state=merge_state) for pr in pull_requests]
243 return data
243 return data
244
244
245
245
246 @jsonrpc_method()
246 @jsonrpc_method()
247 def merge_pull_request(
247 def merge_pull_request(
248 request, apiuser, pullrequestid, repoid=Optional(None),
248 request, apiuser, pullrequestid, repoid=Optional(None),
249 userid=Optional(OAttr('apiuser'))):
249 userid=Optional(OAttr('apiuser'))):
250 """
250 """
251 Merge the pull request specified by `pullrequestid` into its target
251 Merge the pull request specified by `pullrequestid` into its target
252 repository.
252 repository.
253
253
254 :param apiuser: This is filled automatically from the |authtoken|.
254 :param apiuser: This is filled automatically from the |authtoken|.
255 :type apiuser: AuthUser
255 :type apiuser: AuthUser
256 :param repoid: Optional, repository name or repository ID of the
256 :param repoid: Optional, repository name or repository ID of the
257 target repository to which the |pr| is to be merged.
257 target repository to which the |pr| is to be merged.
258 :type repoid: str or int
258 :type repoid: str or int
259 :param pullrequestid: ID of the pull request which shall be merged.
259 :param pullrequestid: ID of the pull request which shall be merged.
260 :type pullrequestid: int
260 :type pullrequestid: int
261 :param userid: Merge the pull request as this user.
261 :param userid: Merge the pull request as this user.
262 :type userid: Optional(str or int)
262 :type userid: Optional(str or int)
263
263
264 Example output:
264 Example output:
265
265
266 .. code-block:: bash
266 .. code-block:: bash
267
267
268 "id": <id_given_in_input>,
268 "id": <id_given_in_input>,
269 "result": {
269 "result": {
270 "executed": "<bool>",
270 "executed": "<bool>",
271 "failure_reason": "<int>",
271 "failure_reason": "<int>",
272 "merge_status_message": "<str>",
272 "merge_commit_id": "<merge_commit_id>",
273 "merge_commit_id": "<merge_commit_id>",
273 "possible": "<bool>",
274 "possible": "<bool>",
274 "merge_ref": {
275 "merge_ref": {
275 "commit_id": "<commit_id>",
276 "commit_id": "<commit_id>",
276 "type": "<type>",
277 "type": "<type>",
277 "name": "<name>"
278 "name": "<name>"
278 }
279 }
279 },
280 },
280 "error": null
281 "error": null
281 """
282 """
282 pull_request = get_pull_request_or_error(pullrequestid)
283 pull_request = get_pull_request_or_error(pullrequestid)
283 if Optional.extract(repoid):
284 if Optional.extract(repoid):
284 repo = get_repo_or_error(repoid)
285 repo = get_repo_or_error(repoid)
285 else:
286 else:
286 repo = pull_request.target_repo
287 repo = pull_request.target_repo
287
288
288 if not isinstance(userid, Optional):
289 if not isinstance(userid, Optional):
289 if (has_superadmin_permission(apiuser) or
290 if (has_superadmin_permission(apiuser) or
290 HasRepoPermissionAnyApi('repository.admin')(
291 HasRepoPermissionAnyApi('repository.admin')(
291 user=apiuser, repo_name=repo.repo_name)):
292 user=apiuser, repo_name=repo.repo_name)):
292 apiuser = get_user_or_error(userid)
293 apiuser = get_user_or_error(userid)
293 else:
294 else:
294 raise JSONRPCError('userid is not the same as your user')
295 raise JSONRPCError('userid is not the same as your user')
295
296
296 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
297 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
297 raise JSONRPCError(
298 raise JSONRPCError(
298 'Operation forbidden because pull request is in state {}, '
299 'Operation forbidden because pull request is in state {}, '
299 'only state {} is allowed.'.format(
300 'only state {} is allowed.'.format(
300 pull_request.pull_request_state, PullRequest.STATE_CREATED))
301 pull_request.pull_request_state, PullRequest.STATE_CREATED))
301
302
302 with pull_request.set_state(PullRequest.STATE_UPDATING):
303 with pull_request.set_state(PullRequest.STATE_UPDATING):
303 check = MergeCheck.validate(
304 check = MergeCheck.validate(
304 pull_request, auth_user=apiuser,
305 pull_request, auth_user=apiuser,
305 translator=request.translate)
306 translator=request.translate)
306 merge_possible = not check.failed
307 merge_possible = not check.failed
307
308
308 if not merge_possible:
309 if not merge_possible:
309 error_messages = []
310 error_messages = []
310 for err_type, error_msg in check.errors:
311 for err_type, error_msg in check.errors:
311 error_msg = request.translate(error_msg)
312 error_msg = request.translate(error_msg)
312 error_messages.append(error_msg)
313 error_messages.append(error_msg)
313
314
314 reasons = ','.join(error_messages)
315 reasons = ','.join(error_messages)
315 raise JSONRPCError(
316 raise JSONRPCError(
316 'merge not possible for following reasons: {}'.format(reasons))
317 'merge not possible for following reasons: {}'.format(reasons))
317
318
318 target_repo = pull_request.target_repo
319 target_repo = pull_request.target_repo
319 extras = vcs_operation_context(
320 extras = vcs_operation_context(
320 request.environ, repo_name=target_repo.repo_name,
321 request.environ, repo_name=target_repo.repo_name,
321 username=apiuser.username, action='push',
322 username=apiuser.username, action='push',
322 scm=target_repo.repo_type)
323 scm=target_repo.repo_type)
323 with pull_request.set_state(PullRequest.STATE_UPDATING):
324 with pull_request.set_state(PullRequest.STATE_UPDATING):
324 merge_response = PullRequestModel().merge_repo(
325 merge_response = PullRequestModel().merge_repo(
325 pull_request, apiuser, extras=extras)
326 pull_request, apiuser, extras=extras)
326 if merge_response.executed:
327 if merge_response.executed:
327 PullRequestModel().close_pull_request(
328 PullRequestModel().close_pull_request(
328 pull_request.pull_request_id, apiuser)
329 pull_request.pull_request_id, apiuser)
329
330
330 Session().commit()
331 Session().commit()
331
332
332 # In previous versions the merge response directly contained the merge
333 # In previous versions the merge response directly contained the merge
333 # commit id. It is now contained in the merge reference object. To be
334 # commit id. It is now contained in the merge reference object. To be
334 # backwards compatible we have to extract it again.
335 # backwards compatible we have to extract it again.
335 merge_response = merge_response.asdict()
336 merge_response = merge_response.asdict()
336 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
337 merge_response['merge_commit_id'] = merge_response['merge_ref'].commit_id
337
338
338 return merge_response
339 return merge_response
339
340
340
341
341 @jsonrpc_method()
342 @jsonrpc_method()
342 def get_pull_request_comments(
343 def get_pull_request_comments(
343 request, apiuser, pullrequestid, repoid=Optional(None)):
344 request, apiuser, pullrequestid, repoid=Optional(None)):
344 """
345 """
345 Get all comments of pull request specified with the `pullrequestid`
346 Get all comments of pull request specified with the `pullrequestid`
346
347
347 :param apiuser: This is filled automatically from the |authtoken|.
348 :param apiuser: This is filled automatically from the |authtoken|.
348 :type apiuser: AuthUser
349 :type apiuser: AuthUser
349 :param repoid: Optional repository name or repository ID.
350 :param repoid: Optional repository name or repository ID.
350 :type repoid: str or int
351 :type repoid: str or int
351 :param pullrequestid: The pull request ID.
352 :param pullrequestid: The pull request ID.
352 :type pullrequestid: int
353 :type pullrequestid: int
353
354
354 Example output:
355 Example output:
355
356
356 .. code-block:: bash
357 .. code-block:: bash
357
358
358 id : <id_given_in_input>
359 id : <id_given_in_input>
359 result : [
360 result : [
360 {
361 {
361 "comment_author": {
362 "comment_author": {
362 "active": true,
363 "active": true,
363 "full_name_or_username": "Tom Gore",
364 "full_name_or_username": "Tom Gore",
364 "username": "admin"
365 "username": "admin"
365 },
366 },
366 "comment_created_on": "2017-01-02T18:43:45.533",
367 "comment_created_on": "2017-01-02T18:43:45.533",
367 "comment_f_path": null,
368 "comment_f_path": null,
368 "comment_id": 25,
369 "comment_id": 25,
369 "comment_lineno": null,
370 "comment_lineno": null,
370 "comment_status": {
371 "comment_status": {
371 "status": "under_review",
372 "status": "under_review",
372 "status_lbl": "Under Review"
373 "status_lbl": "Under Review"
373 },
374 },
374 "comment_text": "Example text",
375 "comment_text": "Example text",
375 "comment_type": null,
376 "comment_type": null,
376 "pull_request_version": null
377 "pull_request_version": null
377 }
378 }
378 ],
379 ],
379 error : null
380 error : null
380 """
381 """
381
382
382 pull_request = get_pull_request_or_error(pullrequestid)
383 pull_request = get_pull_request_or_error(pullrequestid)
383 if Optional.extract(repoid):
384 if Optional.extract(repoid):
384 repo = get_repo_or_error(repoid)
385 repo = get_repo_or_error(repoid)
385 else:
386 else:
386 repo = pull_request.target_repo
387 repo = pull_request.target_repo
387
388
388 if not PullRequestModel().check_user_read(
389 if not PullRequestModel().check_user_read(
389 pull_request, apiuser, api=True):
390 pull_request, apiuser, api=True):
390 raise JSONRPCError('repository `%s` or pull request `%s` '
391 raise JSONRPCError('repository `%s` or pull request `%s` '
391 'does not exist' % (repoid, pullrequestid))
392 'does not exist' % (repoid, pullrequestid))
392
393
393 (pull_request_latest,
394 (pull_request_latest,
394 pull_request_at_ver,
395 pull_request_at_ver,
395 pull_request_display_obj,
396 pull_request_display_obj,
396 at_version) = PullRequestModel().get_pr_version(
397 at_version) = PullRequestModel().get_pr_version(
397 pull_request.pull_request_id, version=None)
398 pull_request.pull_request_id, version=None)
398
399
399 versions = pull_request_display_obj.versions()
400 versions = pull_request_display_obj.versions()
400 ver_map = {
401 ver_map = {
401 ver.pull_request_version_id: cnt
402 ver.pull_request_version_id: cnt
402 for cnt, ver in enumerate(versions, 1)
403 for cnt, ver in enumerate(versions, 1)
403 }
404 }
404
405
405 # GENERAL COMMENTS with versions #
406 # GENERAL COMMENTS with versions #
406 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
407 q = CommentsModel()._all_general_comments_of_pull_request(pull_request)
407 q = q.order_by(ChangesetComment.comment_id.asc())
408 q = q.order_by(ChangesetComment.comment_id.asc())
408 general_comments = q.all()
409 general_comments = q.all()
409
410
410 # INLINE COMMENTS with versions #
411 # INLINE COMMENTS with versions #
411 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
412 q = CommentsModel()._all_inline_comments_of_pull_request(pull_request)
412 q = q.order_by(ChangesetComment.comment_id.asc())
413 q = q.order_by(ChangesetComment.comment_id.asc())
413 inline_comments = q.all()
414 inline_comments = q.all()
414
415
415 data = []
416 data = []
416 for comment in inline_comments + general_comments:
417 for comment in inline_comments + general_comments:
417 full_data = comment.get_api_data()
418 full_data = comment.get_api_data()
418 pr_version_id = None
419 pr_version_id = None
419 if comment.pull_request_version_id:
420 if comment.pull_request_version_id:
420 pr_version_id = 'v{}'.format(
421 pr_version_id = 'v{}'.format(
421 ver_map[comment.pull_request_version_id])
422 ver_map[comment.pull_request_version_id])
422
423
423 # sanitize some entries
424 # sanitize some entries
424
425
425 full_data['pull_request_version'] = pr_version_id
426 full_data['pull_request_version'] = pr_version_id
426 full_data['comment_author'] = {
427 full_data['comment_author'] = {
427 'username': full_data['comment_author'].username,
428 'username': full_data['comment_author'].username,
428 'full_name_or_username': full_data['comment_author'].full_name_or_username,
429 'full_name_or_username': full_data['comment_author'].full_name_or_username,
429 'active': full_data['comment_author'].active,
430 'active': full_data['comment_author'].active,
430 }
431 }
431
432
432 if full_data['comment_status']:
433 if full_data['comment_status']:
433 full_data['comment_status'] = {
434 full_data['comment_status'] = {
434 'status': full_data['comment_status'][0].status,
435 'status': full_data['comment_status'][0].status,
435 'status_lbl': full_data['comment_status'][0].status_lbl,
436 'status_lbl': full_data['comment_status'][0].status_lbl,
436 }
437 }
437 else:
438 else:
438 full_data['comment_status'] = {}
439 full_data['comment_status'] = {}
439
440
440 data.append(full_data)
441 data.append(full_data)
441 return data
442 return data
442
443
443
444
444 @jsonrpc_method()
445 @jsonrpc_method()
445 def comment_pull_request(
446 def comment_pull_request(
446 request, apiuser, pullrequestid, repoid=Optional(None),
447 request, apiuser, pullrequestid, repoid=Optional(None),
447 message=Optional(None), commit_id=Optional(None), status=Optional(None),
448 message=Optional(None), commit_id=Optional(None), status=Optional(None),
448 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
449 comment_type=Optional(ChangesetComment.COMMENT_TYPE_NOTE),
449 resolves_comment_id=Optional(None),
450 resolves_comment_id=Optional(None),
450 userid=Optional(OAttr('apiuser'))):
451 userid=Optional(OAttr('apiuser'))):
451 """
452 """
452 Comment on the pull request specified with the `pullrequestid`,
453 Comment on the pull request specified with the `pullrequestid`,
453 in the |repo| specified by the `repoid`, and optionally change the
454 in the |repo| specified by the `repoid`, and optionally change the
454 review status.
455 review status.
455
456
456 :param apiuser: This is filled automatically from the |authtoken|.
457 :param apiuser: This is filled automatically from the |authtoken|.
457 :type apiuser: AuthUser
458 :type apiuser: AuthUser
458 :param repoid: Optional repository name or repository ID.
459 :param repoid: Optional repository name or repository ID.
459 :type repoid: str or int
460 :type repoid: str or int
460 :param pullrequestid: The pull request ID.
461 :param pullrequestid: The pull request ID.
461 :type pullrequestid: int
462 :type pullrequestid: int
462 :param commit_id: Specify the commit_id for which to set a comment. If
463 :param commit_id: Specify the commit_id for which to set a comment. If
463 given commit_id is different than latest in the PR status
464 given commit_id is different than latest in the PR status
464 change won't be performed.
465 change won't be performed.
465 :type commit_id: str
466 :type commit_id: str
466 :param message: The text content of the comment.
467 :param message: The text content of the comment.
467 :type message: str
468 :type message: str
468 :param status: (**Optional**) Set the approval status of the pull
469 :param status: (**Optional**) Set the approval status of the pull
469 request. One of: 'not_reviewed', 'approved', 'rejected',
470 request. One of: 'not_reviewed', 'approved', 'rejected',
470 'under_review'
471 'under_review'
471 :type status: str
472 :type status: str
472 :param comment_type: Comment type, one of: 'note', 'todo'
473 :param comment_type: Comment type, one of: 'note', 'todo'
473 :type comment_type: Optional(str), default: 'note'
474 :type comment_type: Optional(str), default: 'note'
474 :param userid: Comment on the pull request as this user
475 :param userid: Comment on the pull request as this user
475 :type userid: Optional(str or int)
476 :type userid: Optional(str or int)
476
477
477 Example output:
478 Example output:
478
479
479 .. code-block:: bash
480 .. code-block:: bash
480
481
481 id : <id_given_in_input>
482 id : <id_given_in_input>
482 result : {
483 result : {
483 "pull_request_id": "<Integer>",
484 "pull_request_id": "<Integer>",
484 "comment_id": "<Integer>",
485 "comment_id": "<Integer>",
485 "status": {"given": <given_status>,
486 "status": {"given": <given_status>,
486 "was_changed": <bool status_was_actually_changed> },
487 "was_changed": <bool status_was_actually_changed> },
487 },
488 },
488 error : null
489 error : null
489 """
490 """
490 pull_request = get_pull_request_or_error(pullrequestid)
491 pull_request = get_pull_request_or_error(pullrequestid)
491 if Optional.extract(repoid):
492 if Optional.extract(repoid):
492 repo = get_repo_or_error(repoid)
493 repo = get_repo_or_error(repoid)
493 else:
494 else:
494 repo = pull_request.target_repo
495 repo = pull_request.target_repo
495
496
496 if not isinstance(userid, Optional):
497 if not isinstance(userid, Optional):
497 if (has_superadmin_permission(apiuser) or
498 if (has_superadmin_permission(apiuser) or
498 HasRepoPermissionAnyApi('repository.admin')(
499 HasRepoPermissionAnyApi('repository.admin')(
499 user=apiuser, repo_name=repo.repo_name)):
500 user=apiuser, repo_name=repo.repo_name)):
500 apiuser = get_user_or_error(userid)
501 apiuser = get_user_or_error(userid)
501 else:
502 else:
502 raise JSONRPCError('userid is not the same as your user')
503 raise JSONRPCError('userid is not the same as your user')
503
504
504 if not PullRequestModel().check_user_read(
505 if not PullRequestModel().check_user_read(
505 pull_request, apiuser, api=True):
506 pull_request, apiuser, api=True):
506 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
507 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
507 message = Optional.extract(message)
508 message = Optional.extract(message)
508 status = Optional.extract(status)
509 status = Optional.extract(status)
509 commit_id = Optional.extract(commit_id)
510 commit_id = Optional.extract(commit_id)
510 comment_type = Optional.extract(comment_type)
511 comment_type = Optional.extract(comment_type)
511 resolves_comment_id = Optional.extract(resolves_comment_id)
512 resolves_comment_id = Optional.extract(resolves_comment_id)
512
513
513 if not message and not status:
514 if not message and not status:
514 raise JSONRPCError(
515 raise JSONRPCError(
515 'Both message and status parameters are missing. '
516 'Both message and status parameters are missing. '
516 'At least one is required.')
517 'At least one is required.')
517
518
518 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
519 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
519 status is not None):
520 status is not None):
520 raise JSONRPCError('Unknown comment status: `%s`' % status)
521 raise JSONRPCError('Unknown comment status: `%s`' % status)
521
522
522 if commit_id and commit_id not in pull_request.revisions:
523 if commit_id and commit_id not in pull_request.revisions:
523 raise JSONRPCError(
524 raise JSONRPCError(
524 'Invalid commit_id `%s` for this pull request.' % commit_id)
525 'Invalid commit_id `%s` for this pull request.' % commit_id)
525
526
526 allowed_to_change_status = PullRequestModel().check_user_change_status(
527 allowed_to_change_status = PullRequestModel().check_user_change_status(
527 pull_request, apiuser)
528 pull_request, apiuser)
528
529
529 # if commit_id is passed re-validated if user is allowed to change status
530 # if commit_id is passed re-validated if user is allowed to change status
530 # based on latest commit_id from the PR
531 # based on latest commit_id from the PR
531 if commit_id:
532 if commit_id:
532 commit_idx = pull_request.revisions.index(commit_id)
533 commit_idx = pull_request.revisions.index(commit_id)
533 if commit_idx != 0:
534 if commit_idx != 0:
534 allowed_to_change_status = False
535 allowed_to_change_status = False
535
536
536 if resolves_comment_id:
537 if resolves_comment_id:
537 comment = ChangesetComment.get(resolves_comment_id)
538 comment = ChangesetComment.get(resolves_comment_id)
538 if not comment:
539 if not comment:
539 raise JSONRPCError(
540 raise JSONRPCError(
540 'Invalid resolves_comment_id `%s` for this pull request.'
541 'Invalid resolves_comment_id `%s` for this pull request.'
541 % resolves_comment_id)
542 % resolves_comment_id)
542 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
543 if comment.comment_type != ChangesetComment.COMMENT_TYPE_TODO:
543 raise JSONRPCError(
544 raise JSONRPCError(
544 'Comment `%s` is wrong type for setting status to resolved.'
545 'Comment `%s` is wrong type for setting status to resolved.'
545 % resolves_comment_id)
546 % resolves_comment_id)
546
547
547 text = message
548 text = message
548 status_label = ChangesetStatus.get_status_lbl(status)
549 status_label = ChangesetStatus.get_status_lbl(status)
549 if status and allowed_to_change_status:
550 if status and allowed_to_change_status:
550 st_message = ('Status change %(transition_icon)s %(status)s'
551 st_message = ('Status change %(transition_icon)s %(status)s'
551 % {'transition_icon': '>', 'status': status_label})
552 % {'transition_icon': '>', 'status': status_label})
552 text = message or st_message
553 text = message or st_message
553
554
554 rc_config = SettingsModel().get_all_settings()
555 rc_config = SettingsModel().get_all_settings()
555 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
556 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
556
557
557 status_change = status and allowed_to_change_status
558 status_change = status and allowed_to_change_status
558 comment = CommentsModel().create(
559 comment = CommentsModel().create(
559 text=text,
560 text=text,
560 repo=pull_request.target_repo.repo_id,
561 repo=pull_request.target_repo.repo_id,
561 user=apiuser.user_id,
562 user=apiuser.user_id,
562 pull_request=pull_request.pull_request_id,
563 pull_request=pull_request.pull_request_id,
563 f_path=None,
564 f_path=None,
564 line_no=None,
565 line_no=None,
565 status_change=(status_label if status_change else None),
566 status_change=(status_label if status_change else None),
566 status_change_type=(status if status_change else None),
567 status_change_type=(status if status_change else None),
567 closing_pr=False,
568 closing_pr=False,
568 renderer=renderer,
569 renderer=renderer,
569 comment_type=comment_type,
570 comment_type=comment_type,
570 resolves_comment_id=resolves_comment_id,
571 resolves_comment_id=resolves_comment_id,
571 auth_user=apiuser
572 auth_user=apiuser
572 )
573 )
573
574
574 if allowed_to_change_status and status:
575 if allowed_to_change_status and status:
575 old_calculated_status = pull_request.calculated_review_status()
576 old_calculated_status = pull_request.calculated_review_status()
576 ChangesetStatusModel().set_status(
577 ChangesetStatusModel().set_status(
577 pull_request.target_repo.repo_id,
578 pull_request.target_repo.repo_id,
578 status,
579 status,
579 apiuser.user_id,
580 apiuser.user_id,
580 comment,
581 comment,
581 pull_request=pull_request.pull_request_id
582 pull_request=pull_request.pull_request_id
582 )
583 )
583 Session().flush()
584 Session().flush()
584
585
585 Session().commit()
586 Session().commit()
586
587
587 PullRequestModel().trigger_pull_request_hook(
588 PullRequestModel().trigger_pull_request_hook(
588 pull_request, apiuser, 'comment',
589 pull_request, apiuser, 'comment',
589 data={'comment': comment})
590 data={'comment': comment})
590
591
591 if allowed_to_change_status and status:
592 if allowed_to_change_status and status:
592 # we now calculate the status of pull request, and based on that
593 # we now calculate the status of pull request, and based on that
593 # calculation we set the commits status
594 # calculation we set the commits status
594 calculated_status = pull_request.calculated_review_status()
595 calculated_status = pull_request.calculated_review_status()
595 if old_calculated_status != calculated_status:
596 if old_calculated_status != calculated_status:
596 PullRequestModel().trigger_pull_request_hook(
597 PullRequestModel().trigger_pull_request_hook(
597 pull_request, apiuser, 'review_status_change',
598 pull_request, apiuser, 'review_status_change',
598 data={'status': calculated_status})
599 data={'status': calculated_status})
599
600
600 data = {
601 data = {
601 'pull_request_id': pull_request.pull_request_id,
602 'pull_request_id': pull_request.pull_request_id,
602 'comment_id': comment.comment_id if comment else None,
603 'comment_id': comment.comment_id if comment else None,
603 'status': {'given': status, 'was_changed': status_change},
604 'status': {'given': status, 'was_changed': status_change},
604 }
605 }
605 return data
606 return data
606
607
607
608
608 @jsonrpc_method()
609 @jsonrpc_method()
609 def create_pull_request(
610 def create_pull_request(
610 request, apiuser, source_repo, target_repo, source_ref, target_ref,
611 request, apiuser, source_repo, target_repo, source_ref, target_ref,
611 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
612 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
612 reviewers=Optional(None)):
613 reviewers=Optional(None)):
613 """
614 """
614 Creates a new pull request.
615 Creates a new pull request.
615
616
616 Accepts refs in the following formats:
617 Accepts refs in the following formats:
617
618
618 * branch:<branch_name>:<sha>
619 * branch:<branch_name>:<sha>
619 * branch:<branch_name>
620 * branch:<branch_name>
620 * bookmark:<bookmark_name>:<sha> (Mercurial only)
621 * bookmark:<bookmark_name>:<sha> (Mercurial only)
621 * bookmark:<bookmark_name> (Mercurial only)
622 * bookmark:<bookmark_name> (Mercurial only)
622
623
623 :param apiuser: This is filled automatically from the |authtoken|.
624 :param apiuser: This is filled automatically from the |authtoken|.
624 :type apiuser: AuthUser
625 :type apiuser: AuthUser
625 :param source_repo: Set the source repository name.
626 :param source_repo: Set the source repository name.
626 :type source_repo: str
627 :type source_repo: str
627 :param target_repo: Set the target repository name.
628 :param target_repo: Set the target repository name.
628 :type target_repo: str
629 :type target_repo: str
629 :param source_ref: Set the source ref name.
630 :param source_ref: Set the source ref name.
630 :type source_ref: str
631 :type source_ref: str
631 :param target_ref: Set the target ref name.
632 :param target_ref: Set the target ref name.
632 :type target_ref: str
633 :type target_ref: str
633 :param title: Optionally Set the pull request title, it's generated otherwise
634 :param title: Optionally Set the pull request title, it's generated otherwise
634 :type title: str
635 :type title: str
635 :param description: Set the pull request description.
636 :param description: Set the pull request description.
636 :type description: Optional(str)
637 :type description: Optional(str)
637 :type description_renderer: Optional(str)
638 :type description_renderer: Optional(str)
638 :param description_renderer: Set pull request renderer for the description.
639 :param description_renderer: Set pull request renderer for the description.
639 It should be 'rst', 'markdown' or 'plain'. If not give default
640 It should be 'rst', 'markdown' or 'plain'. If not give default
640 system renderer will be used
641 system renderer will be used
641 :param reviewers: Set the new pull request reviewers list.
642 :param reviewers: Set the new pull request reviewers list.
642 Reviewer defined by review rules will be added automatically to the
643 Reviewer defined by review rules will be added automatically to the
643 defined list.
644 defined list.
644 :type reviewers: Optional(list)
645 :type reviewers: Optional(list)
645 Accepts username strings or objects of the format:
646 Accepts username strings or objects of the format:
646
647
647 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
648 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
648 """
649 """
649
650
650 source_db_repo = get_repo_or_error(source_repo)
651 source_db_repo = get_repo_or_error(source_repo)
651 target_db_repo = get_repo_or_error(target_repo)
652 target_db_repo = get_repo_or_error(target_repo)
652 if not has_superadmin_permission(apiuser):
653 if not has_superadmin_permission(apiuser):
653 _perms = ('repository.admin', 'repository.write', 'repository.read',)
654 _perms = ('repository.admin', 'repository.write', 'repository.read',)
654 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
655 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
655
656
656 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
657 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
657 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
658 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
658
659
659 source_scm = source_db_repo.scm_instance()
660 source_scm = source_db_repo.scm_instance()
660 target_scm = target_db_repo.scm_instance()
661 target_scm = target_db_repo.scm_instance()
661
662
662 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
663 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
663 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
664 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
664
665
665 ancestor = source_scm.get_common_ancestor(
666 ancestor = source_scm.get_common_ancestor(
666 source_commit.raw_id, target_commit.raw_id, target_scm)
667 source_commit.raw_id, target_commit.raw_id, target_scm)
667 if not ancestor:
668 if not ancestor:
668 raise JSONRPCError('no common ancestor found')
669 raise JSONRPCError('no common ancestor found')
669
670
670 # recalculate target ref based on ancestor
671 # recalculate target ref based on ancestor
671 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
672 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
672 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
673 full_target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
673
674
674 commit_ranges = target_scm.compare(
675 commit_ranges = target_scm.compare(
675 target_commit.raw_id, source_commit.raw_id, source_scm,
676 target_commit.raw_id, source_commit.raw_id, source_scm,
676 merge=True, pre_load=[])
677 merge=True, pre_load=[])
677
678
678 if not commit_ranges:
679 if not commit_ranges:
679 raise JSONRPCError('no commits found')
680 raise JSONRPCError('no commits found')
680
681
681 reviewer_objects = Optional.extract(reviewers) or []
682 reviewer_objects = Optional.extract(reviewers) or []
682
683
683 # serialize and validate passed in given reviewers
684 # serialize and validate passed in given reviewers
684 if reviewer_objects:
685 if reviewer_objects:
685 schema = ReviewerListSchema()
686 schema = ReviewerListSchema()
686 try:
687 try:
687 reviewer_objects = schema.deserialize(reviewer_objects)
688 reviewer_objects = schema.deserialize(reviewer_objects)
688 except Invalid as err:
689 except Invalid as err:
689 raise JSONRPCValidationError(colander_exc=err)
690 raise JSONRPCValidationError(colander_exc=err)
690
691
691 # validate users
692 # validate users
692 for reviewer_object in reviewer_objects:
693 for reviewer_object in reviewer_objects:
693 user = get_user_or_error(reviewer_object['username'])
694 user = get_user_or_error(reviewer_object['username'])
694 reviewer_object['user_id'] = user.user_id
695 reviewer_object['user_id'] = user.user_id
695
696
696 get_default_reviewers_data, validate_default_reviewers = \
697 get_default_reviewers_data, validate_default_reviewers = \
697 PullRequestModel().get_reviewer_functions()
698 PullRequestModel().get_reviewer_functions()
698
699
699 # recalculate reviewers logic, to make sure we can validate this
700 # recalculate reviewers logic, to make sure we can validate this
700 reviewer_rules = get_default_reviewers_data(
701 reviewer_rules = get_default_reviewers_data(
701 apiuser.get_instance(), source_db_repo,
702 apiuser.get_instance(), source_db_repo,
702 source_commit, target_db_repo, target_commit)
703 source_commit, target_db_repo, target_commit)
703
704
704 # now MERGE our given with the calculated
705 # now MERGE our given with the calculated
705 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
706 reviewer_objects = reviewer_rules['reviewers'] + reviewer_objects
706
707
707 try:
708 try:
708 reviewers = validate_default_reviewers(
709 reviewers = validate_default_reviewers(
709 reviewer_objects, reviewer_rules)
710 reviewer_objects, reviewer_rules)
710 except ValueError as e:
711 except ValueError as e:
711 raise JSONRPCError('Reviewers Validation: {}'.format(e))
712 raise JSONRPCError('Reviewers Validation: {}'.format(e))
712
713
713 title = Optional.extract(title)
714 title = Optional.extract(title)
714 if not title:
715 if not title:
715 title_source_ref = source_ref.split(':', 2)[1]
716 title_source_ref = source_ref.split(':', 2)[1]
716 title = PullRequestModel().generate_pullrequest_title(
717 title = PullRequestModel().generate_pullrequest_title(
717 source=source_repo,
718 source=source_repo,
718 source_ref=title_source_ref,
719 source_ref=title_source_ref,
719 target=target_repo
720 target=target_repo
720 )
721 )
721 # fetch renderer, if set fallback to plain in case of PR
722 # fetch renderer, if set fallback to plain in case of PR
722 rc_config = SettingsModel().get_all_settings()
723 rc_config = SettingsModel().get_all_settings()
723 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
724 default_system_renderer = rc_config.get('rhodecode_markup_renderer', 'plain')
724 description = Optional.extract(description)
725 description = Optional.extract(description)
725 description_renderer = Optional.extract(description_renderer) or default_system_renderer
726 description_renderer = Optional.extract(description_renderer) or default_system_renderer
726
727
727 pull_request = PullRequestModel().create(
728 pull_request = PullRequestModel().create(
728 created_by=apiuser.user_id,
729 created_by=apiuser.user_id,
729 source_repo=source_repo,
730 source_repo=source_repo,
730 source_ref=full_source_ref,
731 source_ref=full_source_ref,
731 target_repo=target_repo,
732 target_repo=target_repo,
732 target_ref=full_target_ref,
733 target_ref=full_target_ref,
733 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
734 revisions=[commit.raw_id for commit in reversed(commit_ranges)],
734 reviewers=reviewers,
735 reviewers=reviewers,
735 title=title,
736 title=title,
736 description=description,
737 description=description,
737 description_renderer=description_renderer,
738 description_renderer=description_renderer,
738 reviewer_data=reviewer_rules,
739 reviewer_data=reviewer_rules,
739 auth_user=apiuser
740 auth_user=apiuser
740 )
741 )
741
742
742 Session().commit()
743 Session().commit()
743 data = {
744 data = {
744 'msg': 'Created new pull request `{}`'.format(title),
745 'msg': 'Created new pull request `{}`'.format(title),
745 'pull_request_id': pull_request.pull_request_id,
746 'pull_request_id': pull_request.pull_request_id,
746 }
747 }
747 return data
748 return data
748
749
749
750
750 @jsonrpc_method()
751 @jsonrpc_method()
751 def update_pull_request(
752 def update_pull_request(
752 request, apiuser, pullrequestid, repoid=Optional(None),
753 request, apiuser, pullrequestid, repoid=Optional(None),
753 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
754 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
754 reviewers=Optional(None), update_commits=Optional(None)):
755 reviewers=Optional(None), update_commits=Optional(None)):
755 """
756 """
756 Updates a pull request.
757 Updates a pull request.
757
758
758 :param apiuser: This is filled automatically from the |authtoken|.
759 :param apiuser: This is filled automatically from the |authtoken|.
759 :type apiuser: AuthUser
760 :type apiuser: AuthUser
760 :param repoid: Optional repository name or repository ID.
761 :param repoid: Optional repository name or repository ID.
761 :type repoid: str or int
762 :type repoid: str or int
762 :param pullrequestid: The pull request ID.
763 :param pullrequestid: The pull request ID.
763 :type pullrequestid: int
764 :type pullrequestid: int
764 :param title: Set the pull request title.
765 :param title: Set the pull request title.
765 :type title: str
766 :type title: str
766 :param description: Update pull request description.
767 :param description: Update pull request description.
767 :type description: Optional(str)
768 :type description: Optional(str)
768 :type description_renderer: Optional(str)
769 :type description_renderer: Optional(str)
769 :param description_renderer: Update pull request renderer for the description.
770 :param description_renderer: Update pull request renderer for the description.
770 It should be 'rst', 'markdown' or 'plain'
771 It should be 'rst', 'markdown' or 'plain'
771 :param reviewers: Update pull request reviewers list with new value.
772 :param reviewers: Update pull request reviewers list with new value.
772 :type reviewers: Optional(list)
773 :type reviewers: Optional(list)
773 Accepts username strings or objects of the format:
774 Accepts username strings or objects of the format:
774
775
775 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
776 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
776
777
777 :param update_commits: Trigger update of commits for this pull request
778 :param update_commits: Trigger update of commits for this pull request
778 :type: update_commits: Optional(bool)
779 :type: update_commits: Optional(bool)
779
780
780 Example output:
781 Example output:
781
782
782 .. code-block:: bash
783 .. code-block:: bash
783
784
784 id : <id_given_in_input>
785 id : <id_given_in_input>
785 result : {
786 result : {
786 "msg": "Updated pull request `63`",
787 "msg": "Updated pull request `63`",
787 "pull_request": <pull_request_object>,
788 "pull_request": <pull_request_object>,
788 "updated_reviewers": {
789 "updated_reviewers": {
789 "added": [
790 "added": [
790 "username"
791 "username"
791 ],
792 ],
792 "removed": []
793 "removed": []
793 },
794 },
794 "updated_commits": {
795 "updated_commits": {
795 "added": [
796 "added": [
796 "<sha1_hash>"
797 "<sha1_hash>"
797 ],
798 ],
798 "common": [
799 "common": [
799 "<sha1_hash>",
800 "<sha1_hash>",
800 "<sha1_hash>",
801 "<sha1_hash>",
801 ],
802 ],
802 "removed": []
803 "removed": []
803 }
804 }
804 }
805 }
805 error : null
806 error : null
806 """
807 """
807
808
808 pull_request = get_pull_request_or_error(pullrequestid)
809 pull_request = get_pull_request_or_error(pullrequestid)
809 if Optional.extract(repoid):
810 if Optional.extract(repoid):
810 repo = get_repo_or_error(repoid)
811 repo = get_repo_or_error(repoid)
811 else:
812 else:
812 repo = pull_request.target_repo
813 repo = pull_request.target_repo
813
814
814 if not PullRequestModel().check_user_update(
815 if not PullRequestModel().check_user_update(
815 pull_request, apiuser, api=True):
816 pull_request, apiuser, api=True):
816 raise JSONRPCError(
817 raise JSONRPCError(
817 'pull request `%s` update failed, no permission to update.' % (
818 'pull request `%s` update failed, no permission to update.' % (
818 pullrequestid,))
819 pullrequestid,))
819 if pull_request.is_closed():
820 if pull_request.is_closed():
820 raise JSONRPCError(
821 raise JSONRPCError(
821 'pull request `%s` update failed, pull request is closed' % (
822 'pull request `%s` update failed, pull request is closed' % (
822 pullrequestid,))
823 pullrequestid,))
823
824
824 reviewer_objects = Optional.extract(reviewers) or []
825 reviewer_objects = Optional.extract(reviewers) or []
825
826
826 if reviewer_objects:
827 if reviewer_objects:
827 schema = ReviewerListSchema()
828 schema = ReviewerListSchema()
828 try:
829 try:
829 reviewer_objects = schema.deserialize(reviewer_objects)
830 reviewer_objects = schema.deserialize(reviewer_objects)
830 except Invalid as err:
831 except Invalid as err:
831 raise JSONRPCValidationError(colander_exc=err)
832 raise JSONRPCValidationError(colander_exc=err)
832
833
833 # validate users
834 # validate users
834 for reviewer_object in reviewer_objects:
835 for reviewer_object in reviewer_objects:
835 user = get_user_or_error(reviewer_object['username'])
836 user = get_user_or_error(reviewer_object['username'])
836 reviewer_object['user_id'] = user.user_id
837 reviewer_object['user_id'] = user.user_id
837
838
838 get_default_reviewers_data, get_validated_reviewers = \
839 get_default_reviewers_data, get_validated_reviewers = \
839 PullRequestModel().get_reviewer_functions()
840 PullRequestModel().get_reviewer_functions()
840
841
841 # re-use stored rules
842 # re-use stored rules
842 reviewer_rules = pull_request.reviewer_data
843 reviewer_rules = pull_request.reviewer_data
843 try:
844 try:
844 reviewers = get_validated_reviewers(
845 reviewers = get_validated_reviewers(
845 reviewer_objects, reviewer_rules)
846 reviewer_objects, reviewer_rules)
846 except ValueError as e:
847 except ValueError as e:
847 raise JSONRPCError('Reviewers Validation: {}'.format(e))
848 raise JSONRPCError('Reviewers Validation: {}'.format(e))
848 else:
849 else:
849 reviewers = []
850 reviewers = []
850
851
851 title = Optional.extract(title)
852 title = Optional.extract(title)
852 description = Optional.extract(description)
853 description = Optional.extract(description)
853 description_renderer = Optional.extract(description_renderer)
854 description_renderer = Optional.extract(description_renderer)
854
855
855 if title or description:
856 if title or description:
856 PullRequestModel().edit(
857 PullRequestModel().edit(
857 pull_request,
858 pull_request,
858 title or pull_request.title,
859 title or pull_request.title,
859 description or pull_request.description,
860 description or pull_request.description,
860 description_renderer or pull_request.description_renderer,
861 description_renderer or pull_request.description_renderer,
861 apiuser)
862 apiuser)
862 Session().commit()
863 Session().commit()
863
864
864 commit_changes = {"added": [], "common": [], "removed": []}
865 commit_changes = {"added": [], "common": [], "removed": []}
865 if str2bool(Optional.extract(update_commits)):
866 if str2bool(Optional.extract(update_commits)):
866
867
867 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
868 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
868 raise JSONRPCError(
869 raise JSONRPCError(
869 'Operation forbidden because pull request is in state {}, '
870 'Operation forbidden because pull request is in state {}, '
870 'only state {} is allowed.'.format(
871 'only state {} is allowed.'.format(
871 pull_request.pull_request_state, PullRequest.STATE_CREATED))
872 pull_request.pull_request_state, PullRequest.STATE_CREATED))
872
873
873 with pull_request.set_state(PullRequest.STATE_UPDATING):
874 with pull_request.set_state(PullRequest.STATE_UPDATING):
874 if PullRequestModel().has_valid_update_type(pull_request):
875 if PullRequestModel().has_valid_update_type(pull_request):
875 update_response = PullRequestModel().update_commits(pull_request)
876 update_response = PullRequestModel().update_commits(pull_request)
876 commit_changes = update_response.changes or commit_changes
877 commit_changes = update_response.changes or commit_changes
877 Session().commit()
878 Session().commit()
878
879
879 reviewers_changes = {"added": [], "removed": []}
880 reviewers_changes = {"added": [], "removed": []}
880 if reviewers:
881 if reviewers:
881 old_calculated_status = pull_request.calculated_review_status()
882 old_calculated_status = pull_request.calculated_review_status()
882 added_reviewers, removed_reviewers = \
883 added_reviewers, removed_reviewers = \
883 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
884 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
884
885
885 reviewers_changes['added'] = sorted(
886 reviewers_changes['added'] = sorted(
886 [get_user_or_error(n).username for n in added_reviewers])
887 [get_user_or_error(n).username for n in added_reviewers])
887 reviewers_changes['removed'] = sorted(
888 reviewers_changes['removed'] = sorted(
888 [get_user_or_error(n).username for n in removed_reviewers])
889 [get_user_or_error(n).username for n in removed_reviewers])
889 Session().commit()
890 Session().commit()
890
891
891 # trigger status changed if change in reviewers changes the status
892 # trigger status changed if change in reviewers changes the status
892 calculated_status = pull_request.calculated_review_status()
893 calculated_status = pull_request.calculated_review_status()
893 if old_calculated_status != calculated_status:
894 if old_calculated_status != calculated_status:
894 PullRequestModel().trigger_pull_request_hook(
895 PullRequestModel().trigger_pull_request_hook(
895 pull_request, apiuser, 'review_status_change',
896 pull_request, apiuser, 'review_status_change',
896 data={'status': calculated_status})
897 data={'status': calculated_status})
897
898
898 data = {
899 data = {
899 'msg': 'Updated pull request `{}`'.format(
900 'msg': 'Updated pull request `{}`'.format(
900 pull_request.pull_request_id),
901 pull_request.pull_request_id),
901 'pull_request': pull_request.get_api_data(),
902 'pull_request': pull_request.get_api_data(),
902 'updated_commits': commit_changes,
903 'updated_commits': commit_changes,
903 'updated_reviewers': reviewers_changes
904 'updated_reviewers': reviewers_changes
904 }
905 }
905
906
906 return data
907 return data
907
908
908
909
909 @jsonrpc_method()
910 @jsonrpc_method()
910 def close_pull_request(
911 def close_pull_request(
911 request, apiuser, pullrequestid, repoid=Optional(None),
912 request, apiuser, pullrequestid, repoid=Optional(None),
912 userid=Optional(OAttr('apiuser')), message=Optional('')):
913 userid=Optional(OAttr('apiuser')), message=Optional('')):
913 """
914 """
914 Close the pull request specified by `pullrequestid`.
915 Close the pull request specified by `pullrequestid`.
915
916
916 :param apiuser: This is filled automatically from the |authtoken|.
917 :param apiuser: This is filled automatically from the |authtoken|.
917 :type apiuser: AuthUser
918 :type apiuser: AuthUser
918 :param repoid: Repository name or repository ID to which the pull
919 :param repoid: Repository name or repository ID to which the pull
919 request belongs.
920 request belongs.
920 :type repoid: str or int
921 :type repoid: str or int
921 :param pullrequestid: ID of the pull request to be closed.
922 :param pullrequestid: ID of the pull request to be closed.
922 :type pullrequestid: int
923 :type pullrequestid: int
923 :param userid: Close the pull request as this user.
924 :param userid: Close the pull request as this user.
924 :type userid: Optional(str or int)
925 :type userid: Optional(str or int)
925 :param message: Optional message to close the Pull Request with. If not
926 :param message: Optional message to close the Pull Request with. If not
926 specified it will be generated automatically.
927 specified it will be generated automatically.
927 :type message: Optional(str)
928 :type message: Optional(str)
928
929
929 Example output:
930 Example output:
930
931
931 .. code-block:: bash
932 .. code-block:: bash
932
933
933 "id": <id_given_in_input>,
934 "id": <id_given_in_input>,
934 "result": {
935 "result": {
935 "pull_request_id": "<int>",
936 "pull_request_id": "<int>",
936 "close_status": "<str:status_lbl>,
937 "close_status": "<str:status_lbl>,
937 "closed": "<bool>"
938 "closed": "<bool>"
938 },
939 },
939 "error": null
940 "error": null
940
941
941 """
942 """
942 _ = request.translate
943 _ = request.translate
943
944
944 pull_request = get_pull_request_or_error(pullrequestid)
945 pull_request = get_pull_request_or_error(pullrequestid)
945 if Optional.extract(repoid):
946 if Optional.extract(repoid):
946 repo = get_repo_or_error(repoid)
947 repo = get_repo_or_error(repoid)
947 else:
948 else:
948 repo = pull_request.target_repo
949 repo = pull_request.target_repo
949
950
950 if not isinstance(userid, Optional):
951 if not isinstance(userid, Optional):
951 if (has_superadmin_permission(apiuser) or
952 if (has_superadmin_permission(apiuser) or
952 HasRepoPermissionAnyApi('repository.admin')(
953 HasRepoPermissionAnyApi('repository.admin')(
953 user=apiuser, repo_name=repo.repo_name)):
954 user=apiuser, repo_name=repo.repo_name)):
954 apiuser = get_user_or_error(userid)
955 apiuser = get_user_or_error(userid)
955 else:
956 else:
956 raise JSONRPCError('userid is not the same as your user')
957 raise JSONRPCError('userid is not the same as your user')
957
958
958 if pull_request.is_closed():
959 if pull_request.is_closed():
959 raise JSONRPCError(
960 raise JSONRPCError(
960 'pull request `%s` is already closed' % (pullrequestid,))
961 'pull request `%s` is already closed' % (pullrequestid,))
961
962
962 # only owner or admin or person with write permissions
963 # only owner or admin or person with write permissions
963 allowed_to_close = PullRequestModel().check_user_update(
964 allowed_to_close = PullRequestModel().check_user_update(
964 pull_request, apiuser, api=True)
965 pull_request, apiuser, api=True)
965
966
966 if not allowed_to_close:
967 if not allowed_to_close:
967 raise JSONRPCError(
968 raise JSONRPCError(
968 'pull request `%s` close failed, no permission to close.' % (
969 'pull request `%s` close failed, no permission to close.' % (
969 pullrequestid,))
970 pullrequestid,))
970
971
971 # message we're using to close the PR, else it's automatically generated
972 # message we're using to close the PR, else it's automatically generated
972 message = Optional.extract(message)
973 message = Optional.extract(message)
973
974
974 # finally close the PR, with proper message comment
975 # finally close the PR, with proper message comment
975 comment, status = PullRequestModel().close_pull_request_with_comment(
976 comment, status = PullRequestModel().close_pull_request_with_comment(
976 pull_request, apiuser, repo, message=message, auth_user=apiuser)
977 pull_request, apiuser, repo, message=message, auth_user=apiuser)
977 status_lbl = ChangesetStatus.get_status_lbl(status)
978 status_lbl = ChangesetStatus.get_status_lbl(status)
978
979
979 Session().commit()
980 Session().commit()
980
981
981 data = {
982 data = {
982 'pull_request_id': pull_request.pull_request_id,
983 'pull_request_id': pull_request.pull_request_id,
983 'close_status': status_lbl,
984 'close_status': status_lbl,
984 'closed': True,
985 'closed': True,
985 }
986 }
986 return data
987 return data
@@ -1,1841 +1,1842 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 from pyramid import compat
36 from pyramid import compat
37
37
38 from rhodecode.translation import lazy_ugettext
38 from rhodecode.translation import lazy_ugettext
39 from rhodecode.lib.utils2 import safe_str, safe_unicode
39 from rhodecode.lib.utils2 import safe_str, safe_unicode
40 from rhodecode.lib.vcs import connection
40 from rhodecode.lib.vcs import connection
41 from rhodecode.lib.vcs.utils import author_name, author_email
41 from rhodecode.lib.vcs.utils import author_name, author_email
42 from rhodecode.lib.vcs.conf import settings
42 from rhodecode.lib.vcs.conf import settings
43 from rhodecode.lib.vcs.exceptions import (
43 from rhodecode.lib.vcs.exceptions import (
44 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
44 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
45 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
45 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
46 NodeDoesNotExistError, NodeNotChangedError, VCSError,
46 NodeDoesNotExistError, NodeNotChangedError, VCSError,
47 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
47 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
48 RepositoryError)
48 RepositoryError)
49
49
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 FILEMODE_DEFAULT = 0o100644
54 FILEMODE_DEFAULT = 0o100644
55 FILEMODE_EXECUTABLE = 0o100755
55 FILEMODE_EXECUTABLE = 0o100755
56
56
57 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
57 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
58
58
59
59
60 class MergeFailureReason(object):
60 class MergeFailureReason(object):
61 """
61 """
62 Enumeration with all the reasons why the server side merge could fail.
62 Enumeration with all the reasons why the server side merge could fail.
63
63
64 DO NOT change the number of the reasons, as they may be stored in the
64 DO NOT change the number of the reasons, as they may be stored in the
65 database.
65 database.
66
66
67 Changing the name of a reason is acceptable and encouraged to deprecate old
67 Changing the name of a reason is acceptable and encouraged to deprecate old
68 reasons.
68 reasons.
69 """
69 """
70
70
71 # Everything went well.
71 # Everything went well.
72 NONE = 0
72 NONE = 0
73
73
74 # An unexpected exception was raised. Check the logs for more details.
74 # An unexpected exception was raised. Check the logs for more details.
75 UNKNOWN = 1
75 UNKNOWN = 1
76
76
77 # The merge was not successful, there are conflicts.
77 # The merge was not successful, there are conflicts.
78 MERGE_FAILED = 2
78 MERGE_FAILED = 2
79
79
80 # The merge succeeded but we could not push it to the target repository.
80 # The merge succeeded but we could not push it to the target repository.
81 PUSH_FAILED = 3
81 PUSH_FAILED = 3
82
82
83 # The specified target is not a head in the target repository.
83 # The specified target is not a head in the target repository.
84 TARGET_IS_NOT_HEAD = 4
84 TARGET_IS_NOT_HEAD = 4
85
85
86 # The source repository contains more branches than the target. Pushing
86 # The source repository contains more branches than the target. Pushing
87 # the merge will create additional branches in the target.
87 # the merge will create additional branches in the target.
88 HG_SOURCE_HAS_MORE_BRANCHES = 5
88 HG_SOURCE_HAS_MORE_BRANCHES = 5
89
89
90 # The target reference has multiple heads. That does not allow to correctly
90 # The target reference has multiple heads. That does not allow to correctly
91 # identify the target location. This could only happen for mercurial
91 # identify the target location. This could only happen for mercurial
92 # branches.
92 # branches.
93 HG_TARGET_HAS_MULTIPLE_HEADS = 6
93 HG_TARGET_HAS_MULTIPLE_HEADS = 6
94
94
95 # The target repository is locked
95 # The target repository is locked
96 TARGET_IS_LOCKED = 7
96 TARGET_IS_LOCKED = 7
97
97
98 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
98 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
99 # A involved commit could not be found.
99 # A involved commit could not be found.
100 _DEPRECATED_MISSING_COMMIT = 8
100 _DEPRECATED_MISSING_COMMIT = 8
101
101
102 # The target repo reference is missing.
102 # The target repo reference is missing.
103 MISSING_TARGET_REF = 9
103 MISSING_TARGET_REF = 9
104
104
105 # The source repo reference is missing.
105 # The source repo reference is missing.
106 MISSING_SOURCE_REF = 10
106 MISSING_SOURCE_REF = 10
107
107
108 # The merge was not successful, there are conflicts related to sub
108 # The merge was not successful, there are conflicts related to sub
109 # repositories.
109 # repositories.
110 SUBREPO_MERGE_FAILED = 11
110 SUBREPO_MERGE_FAILED = 11
111
111
112
112
113 class UpdateFailureReason(object):
113 class UpdateFailureReason(object):
114 """
114 """
115 Enumeration with all the reasons why the pull request update could fail.
115 Enumeration with all the reasons why the pull request update could fail.
116
116
117 DO NOT change the number of the reasons, as they may be stored in the
117 DO NOT change the number of the reasons, as they may be stored in the
118 database.
118 database.
119
119
120 Changing the name of a reason is acceptable and encouraged to deprecate old
120 Changing the name of a reason is acceptable and encouraged to deprecate old
121 reasons.
121 reasons.
122 """
122 """
123
123
124 # Everything went well.
124 # Everything went well.
125 NONE = 0
125 NONE = 0
126
126
127 # An unexpected exception was raised. Check the logs for more details.
127 # An unexpected exception was raised. Check the logs for more details.
128 UNKNOWN = 1
128 UNKNOWN = 1
129
129
130 # The pull request is up to date.
130 # The pull request is up to date.
131 NO_CHANGE = 2
131 NO_CHANGE = 2
132
132
133 # The pull request has a reference type that is not supported for update.
133 # The pull request has a reference type that is not supported for update.
134 WRONG_REF_TYPE = 3
134 WRONG_REF_TYPE = 3
135
135
136 # Update failed because the target reference is missing.
136 # Update failed because the target reference is missing.
137 MISSING_TARGET_REF = 4
137 MISSING_TARGET_REF = 4
138
138
139 # Update failed because the source reference is missing.
139 # Update failed because the source reference is missing.
140 MISSING_SOURCE_REF = 5
140 MISSING_SOURCE_REF = 5
141
141
142
142
143 class MergeResponse(object):
143 class MergeResponse(object):
144
144
145 # uses .format(**metadata) for variables
145 # uses .format(**metadata) for variables
146 MERGE_STATUS_MESSAGES = {
146 MERGE_STATUS_MESSAGES = {
147 MergeFailureReason.NONE: lazy_ugettext(
147 MergeFailureReason.NONE: lazy_ugettext(
148 u'This pull request can be automatically merged.'),
148 u'This pull request can be automatically merged.'),
149 MergeFailureReason.UNKNOWN: lazy_ugettext(
149 MergeFailureReason.UNKNOWN: lazy_ugettext(
150 u'This pull request cannot be merged because of an unhandled exception. '
150 u'This pull request cannot be merged because of an unhandled exception. '
151 u'{exception}'),
151 u'{exception}'),
152 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
152 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
153 u'This pull request cannot be merged because of merge conflicts.'),
153 u'This pull request cannot be merged because of merge conflicts.'),
154 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
154 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
155 u'This pull request could not be merged because push to '
155 u'This pull request could not be merged because push to '
156 u'target:`{target}@{merge_commit}` failed.'),
156 u'target:`{target}@{merge_commit}` failed.'),
157 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
157 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
158 u'This pull request cannot be merged because the target '
158 u'This pull request cannot be merged because the target '
159 u'`{target_ref.name}` is not a head.'),
159 u'`{target_ref.name}` is not a head.'),
160 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
160 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
161 u'This pull request cannot be merged because the source contains '
161 u'This pull request cannot be merged because the source contains '
162 u'more branches than the target.'),
162 u'more branches than the target.'),
163 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
163 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
164 u'This pull request cannot be merged because the target '
164 u'This pull request cannot be merged because the target '
165 u'has multiple heads: `{heads}`.'),
165 u'has multiple heads: `{heads}`.'),
166 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
166 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
167 u'This pull request cannot be merged because the target repository is '
167 u'This pull request cannot be merged because the target repository is '
168 u'locked by {locked_by}.'),
168 u'locked by {locked_by}.'),
169
169
170 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
170 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
171 u'This pull request cannot be merged because the target '
171 u'This pull request cannot be merged because the target '
172 u'reference `{target_ref.name}` is missing.'),
172 u'reference `{target_ref.name}` is missing.'),
173 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
173 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
174 u'This pull request cannot be merged because the source '
174 u'This pull request cannot be merged because the source '
175 u'reference `{source_ref.name}` is missing.'),
175 u'reference `{source_ref.name}` is missing.'),
176 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
176 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
177 u'This pull request cannot be merged because of conflicts related '
177 u'This pull request cannot be merged because of conflicts related '
178 u'to sub repositories.'),
178 u'to sub repositories.'),
179
179
180 # Deprecations
180 # Deprecations
181 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
181 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
182 u'This pull request cannot be merged because the target or the '
182 u'This pull request cannot be merged because the target or the '
183 u'source reference is missing.'),
183 u'source reference is missing.'),
184
184
185 }
185 }
186
186
187 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
187 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
188 self.possible = possible
188 self.possible = possible
189 self.executed = executed
189 self.executed = executed
190 self.merge_ref = merge_ref
190 self.merge_ref = merge_ref
191 self.failure_reason = failure_reason
191 self.failure_reason = failure_reason
192 self.metadata = metadata or {}
192 self.metadata = metadata or {}
193
193
194 def __repr__(self):
194 def __repr__(self):
195 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
195 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
196
196
197 def __eq__(self, other):
197 def __eq__(self, other):
198 same_instance = isinstance(other, self.__class__)
198 same_instance = isinstance(other, self.__class__)
199 return same_instance \
199 return same_instance \
200 and self.possible == other.possible \
200 and self.possible == other.possible \
201 and self.executed == other.executed \
201 and self.executed == other.executed \
202 and self.failure_reason == other.failure_reason
202 and self.failure_reason == other.failure_reason
203
203
204 @property
204 @property
205 def label(self):
205 def label(self):
206 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
206 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
207 not k.startswith('_'))
207 not k.startswith('_'))
208 return label_dict.get(self.failure_reason)
208 return label_dict.get(self.failure_reason)
209
209
210 @property
210 @property
211 def merge_status_message(self):
211 def merge_status_message(self):
212 """
212 """
213 Return a human friendly error message for the given merge status code.
213 Return a human friendly error message for the given merge status code.
214 """
214 """
215 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
215 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
216 try:
216 try:
217 return msg.format(**self.metadata)
217 return msg.format(**self.metadata)
218 except Exception:
218 except Exception:
219 log.exception('Failed to format %s message', self)
219 log.exception('Failed to format %s message', self)
220 return msg
220 return msg
221
221
222 def asdict(self):
222 def asdict(self):
223 data = {}
223 data = {}
224 for k in ['possible', 'executed', 'merge_ref', 'failure_reason']:
224 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
225 'merge_status_message']:
225 data[k] = getattr(self, k)
226 data[k] = getattr(self, k)
226 return data
227 return data
227
228
228
229
229 class BaseRepository(object):
230 class BaseRepository(object):
230 """
231 """
231 Base Repository for final backends
232 Base Repository for final backends
232
233
233 .. attribute:: DEFAULT_BRANCH_NAME
234 .. attribute:: DEFAULT_BRANCH_NAME
234
235
235 name of default branch (i.e. "trunk" for svn, "master" for git etc.
236 name of default branch (i.e. "trunk" for svn, "master" for git etc.
236
237
237 .. attribute:: commit_ids
238 .. attribute:: commit_ids
238
239
239 list of all available commit ids, in ascending order
240 list of all available commit ids, in ascending order
240
241
241 .. attribute:: path
242 .. attribute:: path
242
243
243 absolute path to the repository
244 absolute path to the repository
244
245
245 .. attribute:: bookmarks
246 .. attribute:: bookmarks
246
247
247 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
248 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
248 there are no bookmarks or the backend implementation does not support
249 there are no bookmarks or the backend implementation does not support
249 bookmarks.
250 bookmarks.
250
251
251 .. attribute:: tags
252 .. attribute:: tags
252
253
253 Mapping from name to :term:`Commit ID` of the tag.
254 Mapping from name to :term:`Commit ID` of the tag.
254
255
255 """
256 """
256
257
257 DEFAULT_BRANCH_NAME = None
258 DEFAULT_BRANCH_NAME = None
258 DEFAULT_CONTACT = u"Unknown"
259 DEFAULT_CONTACT = u"Unknown"
259 DEFAULT_DESCRIPTION = u"unknown"
260 DEFAULT_DESCRIPTION = u"unknown"
260 EMPTY_COMMIT_ID = '0' * 40
261 EMPTY_COMMIT_ID = '0' * 40
261
262
262 path = None
263 path = None
263
264
264 def __init__(self, repo_path, config=None, create=False, **kwargs):
265 def __init__(self, repo_path, config=None, create=False, **kwargs):
265 """
266 """
266 Initializes repository. Raises RepositoryError if repository could
267 Initializes repository. Raises RepositoryError if repository could
267 not be find at the given ``repo_path`` or directory at ``repo_path``
268 not be find at the given ``repo_path`` or directory at ``repo_path``
268 exists and ``create`` is set to True.
269 exists and ``create`` is set to True.
269
270
270 :param repo_path: local path of the repository
271 :param repo_path: local path of the repository
271 :param config: repository configuration
272 :param config: repository configuration
272 :param create=False: if set to True, would try to create repository.
273 :param create=False: if set to True, would try to create repository.
273 :param src_url=None: if set, should be proper url from which repository
274 :param src_url=None: if set, should be proper url from which repository
274 would be cloned; requires ``create`` parameter to be set to True -
275 would be cloned; requires ``create`` parameter to be set to True -
275 raises RepositoryError if src_url is set and create evaluates to
276 raises RepositoryError if src_url is set and create evaluates to
276 False
277 False
277 """
278 """
278 raise NotImplementedError
279 raise NotImplementedError
279
280
280 def __repr__(self):
281 def __repr__(self):
281 return '<%s at %s>' % (self.__class__.__name__, self.path)
282 return '<%s at %s>' % (self.__class__.__name__, self.path)
282
283
283 def __len__(self):
284 def __len__(self):
284 return self.count()
285 return self.count()
285
286
286 def __eq__(self, other):
287 def __eq__(self, other):
287 same_instance = isinstance(other, self.__class__)
288 same_instance = isinstance(other, self.__class__)
288 return same_instance and other.path == self.path
289 return same_instance and other.path == self.path
289
290
290 def __ne__(self, other):
291 def __ne__(self, other):
291 return not self.__eq__(other)
292 return not self.__eq__(other)
292
293
293 def get_create_shadow_cache_pr_path(self, db_repo):
294 def get_create_shadow_cache_pr_path(self, db_repo):
294 path = db_repo.cached_diffs_dir
295 path = db_repo.cached_diffs_dir
295 if not os.path.exists(path):
296 if not os.path.exists(path):
296 os.makedirs(path, 0o755)
297 os.makedirs(path, 0o755)
297 return path
298 return path
298
299
299 @classmethod
300 @classmethod
300 def get_default_config(cls, default=None):
301 def get_default_config(cls, default=None):
301 config = Config()
302 config = Config()
302 if default and isinstance(default, list):
303 if default and isinstance(default, list):
303 for section, key, val in default:
304 for section, key, val in default:
304 config.set(section, key, val)
305 config.set(section, key, val)
305 return config
306 return config
306
307
307 @LazyProperty
308 @LazyProperty
308 def _remote(self):
309 def _remote(self):
309 raise NotImplementedError
310 raise NotImplementedError
310
311
311 @LazyProperty
312 @LazyProperty
312 def EMPTY_COMMIT(self):
313 def EMPTY_COMMIT(self):
313 return EmptyCommit(self.EMPTY_COMMIT_ID)
314 return EmptyCommit(self.EMPTY_COMMIT_ID)
314
315
315 @LazyProperty
316 @LazyProperty
316 def alias(self):
317 def alias(self):
317 for k, v in settings.BACKENDS.items():
318 for k, v in settings.BACKENDS.items():
318 if v.split('.')[-1] == str(self.__class__.__name__):
319 if v.split('.')[-1] == str(self.__class__.__name__):
319 return k
320 return k
320
321
321 @LazyProperty
322 @LazyProperty
322 def name(self):
323 def name(self):
323 return safe_unicode(os.path.basename(self.path))
324 return safe_unicode(os.path.basename(self.path))
324
325
325 @LazyProperty
326 @LazyProperty
326 def description(self):
327 def description(self):
327 raise NotImplementedError
328 raise NotImplementedError
328
329
329 def refs(self):
330 def refs(self):
330 """
331 """
331 returns a `dict` with branches, bookmarks, tags, and closed_branches
332 returns a `dict` with branches, bookmarks, tags, and closed_branches
332 for this repository
333 for this repository
333 """
334 """
334 return dict(
335 return dict(
335 branches=self.branches,
336 branches=self.branches,
336 branches_closed=self.branches_closed,
337 branches_closed=self.branches_closed,
337 tags=self.tags,
338 tags=self.tags,
338 bookmarks=self.bookmarks
339 bookmarks=self.bookmarks
339 )
340 )
340
341
341 @LazyProperty
342 @LazyProperty
342 def branches(self):
343 def branches(self):
343 """
344 """
344 A `dict` which maps branch names to commit ids.
345 A `dict` which maps branch names to commit ids.
345 """
346 """
346 raise NotImplementedError
347 raise NotImplementedError
347
348
348 @LazyProperty
349 @LazyProperty
349 def branches_closed(self):
350 def branches_closed(self):
350 """
351 """
351 A `dict` which maps tags names to commit ids.
352 A `dict` which maps tags names to commit ids.
352 """
353 """
353 raise NotImplementedError
354 raise NotImplementedError
354
355
355 @LazyProperty
356 @LazyProperty
356 def bookmarks(self):
357 def bookmarks(self):
357 """
358 """
358 A `dict` which maps tags names to commit ids.
359 A `dict` which maps tags names to commit ids.
359 """
360 """
360 raise NotImplementedError
361 raise NotImplementedError
361
362
362 @LazyProperty
363 @LazyProperty
363 def tags(self):
364 def tags(self):
364 """
365 """
365 A `dict` which maps tags names to commit ids.
366 A `dict` which maps tags names to commit ids.
366 """
367 """
367 raise NotImplementedError
368 raise NotImplementedError
368
369
369 @LazyProperty
370 @LazyProperty
370 def size(self):
371 def size(self):
371 """
372 """
372 Returns combined size in bytes for all repository files
373 Returns combined size in bytes for all repository files
373 """
374 """
374 tip = self.get_commit()
375 tip = self.get_commit()
375 return tip.size
376 return tip.size
376
377
377 def size_at_commit(self, commit_id):
378 def size_at_commit(self, commit_id):
378 commit = self.get_commit(commit_id)
379 commit = self.get_commit(commit_id)
379 return commit.size
380 return commit.size
380
381
381 def is_empty(self):
382 def is_empty(self):
382 return not bool(self.commit_ids)
383 return not bool(self.commit_ids)
383
384
384 @staticmethod
385 @staticmethod
385 def check_url(url, config):
386 def check_url(url, config):
386 """
387 """
387 Function will check given url and try to verify if it's a valid
388 Function will check given url and try to verify if it's a valid
388 link.
389 link.
389 """
390 """
390 raise NotImplementedError
391 raise NotImplementedError
391
392
392 @staticmethod
393 @staticmethod
393 def is_valid_repository(path):
394 def is_valid_repository(path):
394 """
395 """
395 Check if given `path` contains a valid repository of this backend
396 Check if given `path` contains a valid repository of this backend
396 """
397 """
397 raise NotImplementedError
398 raise NotImplementedError
398
399
399 # ==========================================================================
400 # ==========================================================================
400 # COMMITS
401 # COMMITS
401 # ==========================================================================
402 # ==========================================================================
402
403
403 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
404 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
404 """
405 """
405 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
406 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
406 are both None, most recent commit is returned.
407 are both None, most recent commit is returned.
407
408
408 :param pre_load: Optional. List of commit attributes to load.
409 :param pre_load: Optional. List of commit attributes to load.
409
410
410 :raises ``EmptyRepositoryError``: if there are no commits
411 :raises ``EmptyRepositoryError``: if there are no commits
411 """
412 """
412 raise NotImplementedError
413 raise NotImplementedError
413
414
414 def __iter__(self):
415 def __iter__(self):
415 for commit_id in self.commit_ids:
416 for commit_id in self.commit_ids:
416 yield self.get_commit(commit_id=commit_id)
417 yield self.get_commit(commit_id=commit_id)
417
418
418 def get_commits(
419 def get_commits(
419 self, start_id=None, end_id=None, start_date=None, end_date=None,
420 self, start_id=None, end_id=None, start_date=None, end_date=None,
420 branch_name=None, show_hidden=False, pre_load=None):
421 branch_name=None, show_hidden=False, pre_load=None):
421 """
422 """
422 Returns iterator of `BaseCommit` objects from start to end
423 Returns iterator of `BaseCommit` objects from start to end
423 not inclusive. This should behave just like a list, ie. end is not
424 not inclusive. This should behave just like a list, ie. end is not
424 inclusive.
425 inclusive.
425
426
426 :param start_id: None or str, must be a valid commit id
427 :param start_id: None or str, must be a valid commit id
427 :param end_id: None or str, must be a valid commit id
428 :param end_id: None or str, must be a valid commit id
428 :param start_date:
429 :param start_date:
429 :param end_date:
430 :param end_date:
430 :param branch_name:
431 :param branch_name:
431 :param show_hidden:
432 :param show_hidden:
432 :param pre_load:
433 :param pre_load:
433 """
434 """
434 raise NotImplementedError
435 raise NotImplementedError
435
436
436 def __getitem__(self, key):
437 def __getitem__(self, key):
437 """
438 """
438 Allows index based access to the commit objects of this repository.
439 Allows index based access to the commit objects of this repository.
439 """
440 """
440 pre_load = ["author", "branch", "date", "message", "parents"]
441 pre_load = ["author", "branch", "date", "message", "parents"]
441 if isinstance(key, slice):
442 if isinstance(key, slice):
442 return self._get_range(key, pre_load)
443 return self._get_range(key, pre_load)
443 return self.get_commit(commit_idx=key, pre_load=pre_load)
444 return self.get_commit(commit_idx=key, pre_load=pre_load)
444
445
445 def _get_range(self, slice_obj, pre_load):
446 def _get_range(self, slice_obj, pre_load):
446 for commit_id in self.commit_ids.__getitem__(slice_obj):
447 for commit_id in self.commit_ids.__getitem__(slice_obj):
447 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
448 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
448
449
449 def count(self):
450 def count(self):
450 return len(self.commit_ids)
451 return len(self.commit_ids)
451
452
452 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
453 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
453 """
454 """
454 Creates and returns a tag for the given ``commit_id``.
455 Creates and returns a tag for the given ``commit_id``.
455
456
456 :param name: name for new tag
457 :param name: name for new tag
457 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
458 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
458 :param commit_id: commit id for which new tag would be created
459 :param commit_id: commit id for which new tag would be created
459 :param message: message of the tag's commit
460 :param message: message of the tag's commit
460 :param date: date of tag's commit
461 :param date: date of tag's commit
461
462
462 :raises TagAlreadyExistError: if tag with same name already exists
463 :raises TagAlreadyExistError: if tag with same name already exists
463 """
464 """
464 raise NotImplementedError
465 raise NotImplementedError
465
466
466 def remove_tag(self, name, user, message=None, date=None):
467 def remove_tag(self, name, user, message=None, date=None):
467 """
468 """
468 Removes tag with the given ``name``.
469 Removes tag with the given ``name``.
469
470
470 :param name: name of the tag to be removed
471 :param name: name of the tag to be removed
471 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
472 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
472 :param message: message of the tag's removal commit
473 :param message: message of the tag's removal commit
473 :param date: date of tag's removal commit
474 :param date: date of tag's removal commit
474
475
475 :raises TagDoesNotExistError: if tag with given name does not exists
476 :raises TagDoesNotExistError: if tag with given name does not exists
476 """
477 """
477 raise NotImplementedError
478 raise NotImplementedError
478
479
479 def get_diff(
480 def get_diff(
480 self, commit1, commit2, path=None, ignore_whitespace=False,
481 self, commit1, commit2, path=None, ignore_whitespace=False,
481 context=3, path1=None):
482 context=3, path1=None):
482 """
483 """
483 Returns (git like) *diff*, as plain text. Shows changes introduced by
484 Returns (git like) *diff*, as plain text. Shows changes introduced by
484 `commit2` since `commit1`.
485 `commit2` since `commit1`.
485
486
486 :param commit1: Entry point from which diff is shown. Can be
487 :param commit1: Entry point from which diff is shown. Can be
487 ``self.EMPTY_COMMIT`` - in this case, patch showing all
488 ``self.EMPTY_COMMIT`` - in this case, patch showing all
488 the changes since empty state of the repository until `commit2`
489 the changes since empty state of the repository until `commit2`
489 :param commit2: Until which commit changes should be shown.
490 :param commit2: Until which commit changes should be shown.
490 :param path: Can be set to a path of a file to create a diff of that
491 :param path: Can be set to a path of a file to create a diff of that
491 file. If `path1` is also set, this value is only associated to
492 file. If `path1` is also set, this value is only associated to
492 `commit2`.
493 `commit2`.
493 :param ignore_whitespace: If set to ``True``, would not show whitespace
494 :param ignore_whitespace: If set to ``True``, would not show whitespace
494 changes. Defaults to ``False``.
495 changes. Defaults to ``False``.
495 :param context: How many lines before/after changed lines should be
496 :param context: How many lines before/after changed lines should be
496 shown. Defaults to ``3``.
497 shown. Defaults to ``3``.
497 :param path1: Can be set to a path to associate with `commit1`. This
498 :param path1: Can be set to a path to associate with `commit1`. This
498 parameter works only for backends which support diff generation for
499 parameter works only for backends which support diff generation for
499 different paths. Other backends will raise a `ValueError` if `path1`
500 different paths. Other backends will raise a `ValueError` if `path1`
500 is set and has a different value than `path`.
501 is set and has a different value than `path`.
501 :param file_path: filter this diff by given path pattern
502 :param file_path: filter this diff by given path pattern
502 """
503 """
503 raise NotImplementedError
504 raise NotImplementedError
504
505
505 def strip(self, commit_id, branch=None):
506 def strip(self, commit_id, branch=None):
506 """
507 """
507 Strip given commit_id from the repository
508 Strip given commit_id from the repository
508 """
509 """
509 raise NotImplementedError
510 raise NotImplementedError
510
511
511 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
512 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
512 """
513 """
513 Return a latest common ancestor commit if one exists for this repo
514 Return a latest common ancestor commit if one exists for this repo
514 `commit_id1` vs `commit_id2` from `repo2`.
515 `commit_id1` vs `commit_id2` from `repo2`.
515
516
516 :param commit_id1: Commit it from this repository to use as a
517 :param commit_id1: Commit it from this repository to use as a
517 target for the comparison.
518 target for the comparison.
518 :param commit_id2: Source commit id to use for comparison.
519 :param commit_id2: Source commit id to use for comparison.
519 :param repo2: Source repository to use for comparison.
520 :param repo2: Source repository to use for comparison.
520 """
521 """
521 raise NotImplementedError
522 raise NotImplementedError
522
523
523 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
524 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
524 """
525 """
525 Compare this repository's revision `commit_id1` with `commit_id2`.
526 Compare this repository's revision `commit_id1` with `commit_id2`.
526
527
527 Returns a tuple(commits, ancestor) that would be merged from
528 Returns a tuple(commits, ancestor) that would be merged from
528 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
529 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
529 will be returned as ancestor.
530 will be returned as ancestor.
530
531
531 :param commit_id1: Commit it from this repository to use as a
532 :param commit_id1: Commit it from this repository to use as a
532 target for the comparison.
533 target for the comparison.
533 :param commit_id2: Source commit id to use for comparison.
534 :param commit_id2: Source commit id to use for comparison.
534 :param repo2: Source repository to use for comparison.
535 :param repo2: Source repository to use for comparison.
535 :param merge: If set to ``True`` will do a merge compare which also
536 :param merge: If set to ``True`` will do a merge compare which also
536 returns the common ancestor.
537 returns the common ancestor.
537 :param pre_load: Optional. List of commit attributes to load.
538 :param pre_load: Optional. List of commit attributes to load.
538 """
539 """
539 raise NotImplementedError
540 raise NotImplementedError
540
541
541 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
542 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
542 user_name='', user_email='', message='', dry_run=False,
543 user_name='', user_email='', message='', dry_run=False,
543 use_rebase=False, close_branch=False):
544 use_rebase=False, close_branch=False):
544 """
545 """
545 Merge the revisions specified in `source_ref` from `source_repo`
546 Merge the revisions specified in `source_ref` from `source_repo`
546 onto the `target_ref` of this repository.
547 onto the `target_ref` of this repository.
547
548
548 `source_ref` and `target_ref` are named tupls with the following
549 `source_ref` and `target_ref` are named tupls with the following
549 fields `type`, `name` and `commit_id`.
550 fields `type`, `name` and `commit_id`.
550
551
551 Returns a MergeResponse named tuple with the following fields
552 Returns a MergeResponse named tuple with the following fields
552 'possible', 'executed', 'source_commit', 'target_commit',
553 'possible', 'executed', 'source_commit', 'target_commit',
553 'merge_commit'.
554 'merge_commit'.
554
555
555 :param repo_id: `repo_id` target repo id.
556 :param repo_id: `repo_id` target repo id.
556 :param workspace_id: `workspace_id` unique identifier.
557 :param workspace_id: `workspace_id` unique identifier.
557 :param target_ref: `target_ref` points to the commit on top of which
558 :param target_ref: `target_ref` points to the commit on top of which
558 the `source_ref` should be merged.
559 the `source_ref` should be merged.
559 :param source_repo: The repository that contains the commits to be
560 :param source_repo: The repository that contains the commits to be
560 merged.
561 merged.
561 :param source_ref: `source_ref` points to the topmost commit from
562 :param source_ref: `source_ref` points to the topmost commit from
562 the `source_repo` which should be merged.
563 the `source_repo` which should be merged.
563 :param user_name: Merge commit `user_name`.
564 :param user_name: Merge commit `user_name`.
564 :param user_email: Merge commit `user_email`.
565 :param user_email: Merge commit `user_email`.
565 :param message: Merge commit `message`.
566 :param message: Merge commit `message`.
566 :param dry_run: If `True` the merge will not take place.
567 :param dry_run: If `True` the merge will not take place.
567 :param use_rebase: If `True` commits from the source will be rebased
568 :param use_rebase: If `True` commits from the source will be rebased
568 on top of the target instead of being merged.
569 on top of the target instead of being merged.
569 :param close_branch: If `True` branch will be close before merging it
570 :param close_branch: If `True` branch will be close before merging it
570 """
571 """
571 if dry_run:
572 if dry_run:
572 message = message or settings.MERGE_DRY_RUN_MESSAGE
573 message = message or settings.MERGE_DRY_RUN_MESSAGE
573 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
574 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
574 user_name = user_name or settings.MERGE_DRY_RUN_USER
575 user_name = user_name or settings.MERGE_DRY_RUN_USER
575 else:
576 else:
576 if not user_name:
577 if not user_name:
577 raise ValueError('user_name cannot be empty')
578 raise ValueError('user_name cannot be empty')
578 if not user_email:
579 if not user_email:
579 raise ValueError('user_email cannot be empty')
580 raise ValueError('user_email cannot be empty')
580 if not message:
581 if not message:
581 raise ValueError('message cannot be empty')
582 raise ValueError('message cannot be empty')
582
583
583 try:
584 try:
584 return self._merge_repo(
585 return self._merge_repo(
585 repo_id, workspace_id, target_ref, source_repo,
586 repo_id, workspace_id, target_ref, source_repo,
586 source_ref, message, user_name, user_email, dry_run=dry_run,
587 source_ref, message, user_name, user_email, dry_run=dry_run,
587 use_rebase=use_rebase, close_branch=close_branch)
588 use_rebase=use_rebase, close_branch=close_branch)
588 except RepositoryError as exc:
589 except RepositoryError as exc:
589 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
590 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
590 return MergeResponse(
591 return MergeResponse(
591 False, False, None, MergeFailureReason.UNKNOWN,
592 False, False, None, MergeFailureReason.UNKNOWN,
592 metadata={'exception': str(exc)})
593 metadata={'exception': str(exc)})
593
594
594 def _merge_repo(self, repo_id, workspace_id, target_ref,
595 def _merge_repo(self, repo_id, workspace_id, target_ref,
595 source_repo, source_ref, merge_message,
596 source_repo, source_ref, merge_message,
596 merger_name, merger_email, dry_run=False,
597 merger_name, merger_email, dry_run=False,
597 use_rebase=False, close_branch=False):
598 use_rebase=False, close_branch=False):
598 """Internal implementation of merge."""
599 """Internal implementation of merge."""
599 raise NotImplementedError
600 raise NotImplementedError
600
601
601 def _maybe_prepare_merge_workspace(
602 def _maybe_prepare_merge_workspace(
602 self, repo_id, workspace_id, target_ref, source_ref):
603 self, repo_id, workspace_id, target_ref, source_ref):
603 """
604 """
604 Create the merge workspace.
605 Create the merge workspace.
605
606
606 :param workspace_id: `workspace_id` unique identifier.
607 :param workspace_id: `workspace_id` unique identifier.
607 """
608 """
608 raise NotImplementedError
609 raise NotImplementedError
609
610
610 def _get_legacy_shadow_repository_path(self, workspace_id):
611 def _get_legacy_shadow_repository_path(self, workspace_id):
611 """
612 """
612 Legacy version that was used before. We still need it for
613 Legacy version that was used before. We still need it for
613 backward compat
614 backward compat
614 """
615 """
615 return os.path.join(
616 return os.path.join(
616 os.path.dirname(self.path),
617 os.path.dirname(self.path),
617 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
618 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
618
619
619 def _get_shadow_repository_path(self, repo_id, workspace_id):
620 def _get_shadow_repository_path(self, repo_id, workspace_id):
620 # The name of the shadow repository must start with '.', so it is
621 # The name of the shadow repository must start with '.', so it is
621 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
622 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
622 legacy_repository_path = self._get_legacy_shadow_repository_path(workspace_id)
623 legacy_repository_path = self._get_legacy_shadow_repository_path(workspace_id)
623 if os.path.exists(legacy_repository_path):
624 if os.path.exists(legacy_repository_path):
624 return legacy_repository_path
625 return legacy_repository_path
625 else:
626 else:
626 return os.path.join(
627 return os.path.join(
627 os.path.dirname(self.path),
628 os.path.dirname(self.path),
628 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
629 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
629
630
630 def cleanup_merge_workspace(self, repo_id, workspace_id):
631 def cleanup_merge_workspace(self, repo_id, workspace_id):
631 """
632 """
632 Remove merge workspace.
633 Remove merge workspace.
633
634
634 This function MUST not fail in case there is no workspace associated to
635 This function MUST not fail in case there is no workspace associated to
635 the given `workspace_id`.
636 the given `workspace_id`.
636
637
637 :param workspace_id: `workspace_id` unique identifier.
638 :param workspace_id: `workspace_id` unique identifier.
638 """
639 """
639 shadow_repository_path = self._get_shadow_repository_path(repo_id, workspace_id)
640 shadow_repository_path = self._get_shadow_repository_path(repo_id, workspace_id)
640 shadow_repository_path_del = '{}.{}.delete'.format(
641 shadow_repository_path_del = '{}.{}.delete'.format(
641 shadow_repository_path, time.time())
642 shadow_repository_path, time.time())
642
643
643 # move the shadow repo, so it never conflicts with the one used.
644 # move the shadow repo, so it never conflicts with the one used.
644 # we use this method because shutil.rmtree had some edge case problems
645 # we use this method because shutil.rmtree had some edge case problems
645 # removing symlinked repositories
646 # removing symlinked repositories
646 if not os.path.isdir(shadow_repository_path):
647 if not os.path.isdir(shadow_repository_path):
647 return
648 return
648
649
649 shutil.move(shadow_repository_path, shadow_repository_path_del)
650 shutil.move(shadow_repository_path, shadow_repository_path_del)
650 try:
651 try:
651 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
652 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
652 except Exception:
653 except Exception:
653 log.exception('Failed to gracefully remove shadow repo under %s',
654 log.exception('Failed to gracefully remove shadow repo under %s',
654 shadow_repository_path_del)
655 shadow_repository_path_del)
655 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
656 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
656
657
657 # ========== #
658 # ========== #
658 # COMMIT API #
659 # COMMIT API #
659 # ========== #
660 # ========== #
660
661
661 @LazyProperty
662 @LazyProperty
662 def in_memory_commit(self):
663 def in_memory_commit(self):
663 """
664 """
664 Returns :class:`InMemoryCommit` object for this repository.
665 Returns :class:`InMemoryCommit` object for this repository.
665 """
666 """
666 raise NotImplementedError
667 raise NotImplementedError
667
668
668 # ======================== #
669 # ======================== #
669 # UTILITIES FOR SUBCLASSES #
670 # UTILITIES FOR SUBCLASSES #
670 # ======================== #
671 # ======================== #
671
672
672 def _validate_diff_commits(self, commit1, commit2):
673 def _validate_diff_commits(self, commit1, commit2):
673 """
674 """
674 Validates that the given commits are related to this repository.
675 Validates that the given commits are related to this repository.
675
676
676 Intended as a utility for sub classes to have a consistent validation
677 Intended as a utility for sub classes to have a consistent validation
677 of input parameters in methods like :meth:`get_diff`.
678 of input parameters in methods like :meth:`get_diff`.
678 """
679 """
679 self._validate_commit(commit1)
680 self._validate_commit(commit1)
680 self._validate_commit(commit2)
681 self._validate_commit(commit2)
681 if (isinstance(commit1, EmptyCommit) and
682 if (isinstance(commit1, EmptyCommit) and
682 isinstance(commit2, EmptyCommit)):
683 isinstance(commit2, EmptyCommit)):
683 raise ValueError("Cannot compare two empty commits")
684 raise ValueError("Cannot compare two empty commits")
684
685
685 def _validate_commit(self, commit):
686 def _validate_commit(self, commit):
686 if not isinstance(commit, BaseCommit):
687 if not isinstance(commit, BaseCommit):
687 raise TypeError(
688 raise TypeError(
688 "%s is not of type BaseCommit" % repr(commit))
689 "%s is not of type BaseCommit" % repr(commit))
689 if commit.repository != self and not isinstance(commit, EmptyCommit):
690 if commit.repository != self and not isinstance(commit, EmptyCommit):
690 raise ValueError(
691 raise ValueError(
691 "Commit %s must be a valid commit from this repository %s, "
692 "Commit %s must be a valid commit from this repository %s, "
692 "related to this repository instead %s." %
693 "related to this repository instead %s." %
693 (commit, self, commit.repository))
694 (commit, self, commit.repository))
694
695
695 def _validate_commit_id(self, commit_id):
696 def _validate_commit_id(self, commit_id):
696 if not isinstance(commit_id, compat.string_types):
697 if not isinstance(commit_id, compat.string_types):
697 raise TypeError("commit_id must be a string value")
698 raise TypeError("commit_id must be a string value")
698
699
699 def _validate_commit_idx(self, commit_idx):
700 def _validate_commit_idx(self, commit_idx):
700 if not isinstance(commit_idx, (int, long)):
701 if not isinstance(commit_idx, (int, long)):
701 raise TypeError("commit_idx must be a numeric value")
702 raise TypeError("commit_idx must be a numeric value")
702
703
703 def _validate_branch_name(self, branch_name):
704 def _validate_branch_name(self, branch_name):
704 if branch_name and branch_name not in self.branches_all:
705 if branch_name and branch_name not in self.branches_all:
705 msg = ("Branch %s not found in %s" % (branch_name, self))
706 msg = ("Branch %s not found in %s" % (branch_name, self))
706 raise BranchDoesNotExistError(msg)
707 raise BranchDoesNotExistError(msg)
707
708
708 #
709 #
709 # Supporting deprecated API parts
710 # Supporting deprecated API parts
710 # TODO: johbo: consider to move this into a mixin
711 # TODO: johbo: consider to move this into a mixin
711 #
712 #
712
713
713 @property
714 @property
714 def EMPTY_CHANGESET(self):
715 def EMPTY_CHANGESET(self):
715 warnings.warn(
716 warnings.warn(
716 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
717 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
717 return self.EMPTY_COMMIT_ID
718 return self.EMPTY_COMMIT_ID
718
719
719 @property
720 @property
720 def revisions(self):
721 def revisions(self):
721 warnings.warn("Use commits attribute instead", DeprecationWarning)
722 warnings.warn("Use commits attribute instead", DeprecationWarning)
722 return self.commit_ids
723 return self.commit_ids
723
724
724 @revisions.setter
725 @revisions.setter
725 def revisions(self, value):
726 def revisions(self, value):
726 warnings.warn("Use commits attribute instead", DeprecationWarning)
727 warnings.warn("Use commits attribute instead", DeprecationWarning)
727 self.commit_ids = value
728 self.commit_ids = value
728
729
729 def get_changeset(self, revision=None, pre_load=None):
730 def get_changeset(self, revision=None, pre_load=None):
730 warnings.warn("Use get_commit instead", DeprecationWarning)
731 warnings.warn("Use get_commit instead", DeprecationWarning)
731 commit_id = None
732 commit_id = None
732 commit_idx = None
733 commit_idx = None
733 if isinstance(revision, compat.string_types):
734 if isinstance(revision, compat.string_types):
734 commit_id = revision
735 commit_id = revision
735 else:
736 else:
736 commit_idx = revision
737 commit_idx = revision
737 return self.get_commit(
738 return self.get_commit(
738 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
739 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
739
740
740 def get_changesets(
741 def get_changesets(
741 self, start=None, end=None, start_date=None, end_date=None,
742 self, start=None, end=None, start_date=None, end_date=None,
742 branch_name=None, pre_load=None):
743 branch_name=None, pre_load=None):
743 warnings.warn("Use get_commits instead", DeprecationWarning)
744 warnings.warn("Use get_commits instead", DeprecationWarning)
744 start_id = self._revision_to_commit(start)
745 start_id = self._revision_to_commit(start)
745 end_id = self._revision_to_commit(end)
746 end_id = self._revision_to_commit(end)
746 return self.get_commits(
747 return self.get_commits(
747 start_id=start_id, end_id=end_id, start_date=start_date,
748 start_id=start_id, end_id=end_id, start_date=start_date,
748 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
749 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
749
750
750 def _revision_to_commit(self, revision):
751 def _revision_to_commit(self, revision):
751 """
752 """
752 Translates a revision to a commit_id
753 Translates a revision to a commit_id
753
754
754 Helps to support the old changeset based API which allows to use
755 Helps to support the old changeset based API which allows to use
755 commit ids and commit indices interchangeable.
756 commit ids and commit indices interchangeable.
756 """
757 """
757 if revision is None:
758 if revision is None:
758 return revision
759 return revision
759
760
760 if isinstance(revision, compat.string_types):
761 if isinstance(revision, compat.string_types):
761 commit_id = revision
762 commit_id = revision
762 else:
763 else:
763 commit_id = self.commit_ids[revision]
764 commit_id = self.commit_ids[revision]
764 return commit_id
765 return commit_id
765
766
766 @property
767 @property
767 def in_memory_changeset(self):
768 def in_memory_changeset(self):
768 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
769 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
769 return self.in_memory_commit
770 return self.in_memory_commit
770
771
771 def get_path_permissions(self, username):
772 def get_path_permissions(self, username):
772 """
773 """
773 Returns a path permission checker or None if not supported
774 Returns a path permission checker or None if not supported
774
775
775 :param username: session user name
776 :param username: session user name
776 :return: an instance of BasePathPermissionChecker or None
777 :return: an instance of BasePathPermissionChecker or None
777 """
778 """
778 return None
779 return None
779
780
780 def install_hooks(self, force=False):
781 def install_hooks(self, force=False):
781 return self._remote.install_hooks(force)
782 return self._remote.install_hooks(force)
782
783
783 def get_hooks_info(self):
784 def get_hooks_info(self):
784 return self._remote.get_hooks_info()
785 return self._remote.get_hooks_info()
785
786
786
787
787 class BaseCommit(object):
788 class BaseCommit(object):
788 """
789 """
789 Each backend should implement it's commit representation.
790 Each backend should implement it's commit representation.
790
791
791 **Attributes**
792 **Attributes**
792
793
793 ``repository``
794 ``repository``
794 repository object within which commit exists
795 repository object within which commit exists
795
796
796 ``id``
797 ``id``
797 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
798 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
798 just ``tip``.
799 just ``tip``.
799
800
800 ``raw_id``
801 ``raw_id``
801 raw commit representation (i.e. full 40 length sha for git
802 raw commit representation (i.e. full 40 length sha for git
802 backend)
803 backend)
803
804
804 ``short_id``
805 ``short_id``
805 shortened (if apply) version of ``raw_id``; it would be simple
806 shortened (if apply) version of ``raw_id``; it would be simple
806 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
807 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
807 as ``raw_id`` for subversion
808 as ``raw_id`` for subversion
808
809
809 ``idx``
810 ``idx``
810 commit index
811 commit index
811
812
812 ``files``
813 ``files``
813 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
814 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
814
815
815 ``dirs``
816 ``dirs``
816 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
817 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
817
818
818 ``nodes``
819 ``nodes``
819 combined list of ``Node`` objects
820 combined list of ``Node`` objects
820
821
821 ``author``
822 ``author``
822 author of the commit, as unicode
823 author of the commit, as unicode
823
824
824 ``message``
825 ``message``
825 message of the commit, as unicode
826 message of the commit, as unicode
826
827
827 ``parents``
828 ``parents``
828 list of parent commits
829 list of parent commits
829
830
830 """
831 """
831
832
832 branch = None
833 branch = None
833 """
834 """
834 Depending on the backend this should be set to the branch name of the
835 Depending on the backend this should be set to the branch name of the
835 commit. Backends not supporting branches on commits should leave this
836 commit. Backends not supporting branches on commits should leave this
836 value as ``None``.
837 value as ``None``.
837 """
838 """
838
839
839 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
840 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
840 """
841 """
841 This template is used to generate a default prefix for repository archives
842 This template is used to generate a default prefix for repository archives
842 if no prefix has been specified.
843 if no prefix has been specified.
843 """
844 """
844
845
845 def __str__(self):
846 def __str__(self):
846 return '<%s at %s:%s>' % (
847 return '<%s at %s:%s>' % (
847 self.__class__.__name__, self.idx, self.short_id)
848 self.__class__.__name__, self.idx, self.short_id)
848
849
849 def __repr__(self):
850 def __repr__(self):
850 return self.__str__()
851 return self.__str__()
851
852
852 def __unicode__(self):
853 def __unicode__(self):
853 return u'%s:%s' % (self.idx, self.short_id)
854 return u'%s:%s' % (self.idx, self.short_id)
854
855
855 def __eq__(self, other):
856 def __eq__(self, other):
856 same_instance = isinstance(other, self.__class__)
857 same_instance = isinstance(other, self.__class__)
857 return same_instance and self.raw_id == other.raw_id
858 return same_instance and self.raw_id == other.raw_id
858
859
859 def __json__(self):
860 def __json__(self):
860 parents = []
861 parents = []
861 try:
862 try:
862 for parent in self.parents:
863 for parent in self.parents:
863 parents.append({'raw_id': parent.raw_id})
864 parents.append({'raw_id': parent.raw_id})
864 except NotImplementedError:
865 except NotImplementedError:
865 # empty commit doesn't have parents implemented
866 # empty commit doesn't have parents implemented
866 pass
867 pass
867
868
868 return {
869 return {
869 'short_id': self.short_id,
870 'short_id': self.short_id,
870 'raw_id': self.raw_id,
871 'raw_id': self.raw_id,
871 'revision': self.idx,
872 'revision': self.idx,
872 'message': self.message,
873 'message': self.message,
873 'date': self.date,
874 'date': self.date,
874 'author': self.author,
875 'author': self.author,
875 'parents': parents,
876 'parents': parents,
876 'branch': self.branch
877 'branch': self.branch
877 }
878 }
878
879
879 def __getstate__(self):
880 def __getstate__(self):
880 d = self.__dict__.copy()
881 d = self.__dict__.copy()
881 d.pop('_remote', None)
882 d.pop('_remote', None)
882 d.pop('repository', None)
883 d.pop('repository', None)
883 return d
884 return d
884
885
885 def _get_refs(self):
886 def _get_refs(self):
886 return {
887 return {
887 'branches': [self.branch] if self.branch else [],
888 'branches': [self.branch] if self.branch else [],
888 'bookmarks': getattr(self, 'bookmarks', []),
889 'bookmarks': getattr(self, 'bookmarks', []),
889 'tags': self.tags
890 'tags': self.tags
890 }
891 }
891
892
892 @LazyProperty
893 @LazyProperty
893 def last(self):
894 def last(self):
894 """
895 """
895 ``True`` if this is last commit in repository, ``False``
896 ``True`` if this is last commit in repository, ``False``
896 otherwise; trying to access this attribute while there is no
897 otherwise; trying to access this attribute while there is no
897 commits would raise `EmptyRepositoryError`
898 commits would raise `EmptyRepositoryError`
898 """
899 """
899 if self.repository is None:
900 if self.repository is None:
900 raise CommitError("Cannot check if it's most recent commit")
901 raise CommitError("Cannot check if it's most recent commit")
901 return self.raw_id == self.repository.commit_ids[-1]
902 return self.raw_id == self.repository.commit_ids[-1]
902
903
903 @LazyProperty
904 @LazyProperty
904 def parents(self):
905 def parents(self):
905 """
906 """
906 Returns list of parent commits.
907 Returns list of parent commits.
907 """
908 """
908 raise NotImplementedError
909 raise NotImplementedError
909
910
910 @LazyProperty
911 @LazyProperty
911 def first_parent(self):
912 def first_parent(self):
912 """
913 """
913 Returns list of parent commits.
914 Returns list of parent commits.
914 """
915 """
915 return self.parents[0] if self.parents else EmptyCommit()
916 return self.parents[0] if self.parents else EmptyCommit()
916
917
917 @property
918 @property
918 def merge(self):
919 def merge(self):
919 """
920 """
920 Returns boolean if commit is a merge.
921 Returns boolean if commit is a merge.
921 """
922 """
922 return len(self.parents) > 1
923 return len(self.parents) > 1
923
924
924 @LazyProperty
925 @LazyProperty
925 def children(self):
926 def children(self):
926 """
927 """
927 Returns list of child commits.
928 Returns list of child commits.
928 """
929 """
929 raise NotImplementedError
930 raise NotImplementedError
930
931
931 @LazyProperty
932 @LazyProperty
932 def id(self):
933 def id(self):
933 """
934 """
934 Returns string identifying this commit.
935 Returns string identifying this commit.
935 """
936 """
936 raise NotImplementedError
937 raise NotImplementedError
937
938
938 @LazyProperty
939 @LazyProperty
939 def raw_id(self):
940 def raw_id(self):
940 """
941 """
941 Returns raw string identifying this commit.
942 Returns raw string identifying this commit.
942 """
943 """
943 raise NotImplementedError
944 raise NotImplementedError
944
945
945 @LazyProperty
946 @LazyProperty
946 def short_id(self):
947 def short_id(self):
947 """
948 """
948 Returns shortened version of ``raw_id`` attribute, as string,
949 Returns shortened version of ``raw_id`` attribute, as string,
949 identifying this commit, useful for presentation to users.
950 identifying this commit, useful for presentation to users.
950 """
951 """
951 raise NotImplementedError
952 raise NotImplementedError
952
953
953 @LazyProperty
954 @LazyProperty
954 def idx(self):
955 def idx(self):
955 """
956 """
956 Returns integer identifying this commit.
957 Returns integer identifying this commit.
957 """
958 """
958 raise NotImplementedError
959 raise NotImplementedError
959
960
960 @LazyProperty
961 @LazyProperty
961 def committer(self):
962 def committer(self):
962 """
963 """
963 Returns committer for this commit
964 Returns committer for this commit
964 """
965 """
965 raise NotImplementedError
966 raise NotImplementedError
966
967
967 @LazyProperty
968 @LazyProperty
968 def committer_name(self):
969 def committer_name(self):
969 """
970 """
970 Returns committer name for this commit
971 Returns committer name for this commit
971 """
972 """
972
973
973 return author_name(self.committer)
974 return author_name(self.committer)
974
975
975 @LazyProperty
976 @LazyProperty
976 def committer_email(self):
977 def committer_email(self):
977 """
978 """
978 Returns committer email address for this commit
979 Returns committer email address for this commit
979 """
980 """
980
981
981 return author_email(self.committer)
982 return author_email(self.committer)
982
983
983 @LazyProperty
984 @LazyProperty
984 def author(self):
985 def author(self):
985 """
986 """
986 Returns author for this commit
987 Returns author for this commit
987 """
988 """
988
989
989 raise NotImplementedError
990 raise NotImplementedError
990
991
991 @LazyProperty
992 @LazyProperty
992 def author_name(self):
993 def author_name(self):
993 """
994 """
994 Returns author name for this commit
995 Returns author name for this commit
995 """
996 """
996
997
997 return author_name(self.author)
998 return author_name(self.author)
998
999
999 @LazyProperty
1000 @LazyProperty
1000 def author_email(self):
1001 def author_email(self):
1001 """
1002 """
1002 Returns author email address for this commit
1003 Returns author email address for this commit
1003 """
1004 """
1004
1005
1005 return author_email(self.author)
1006 return author_email(self.author)
1006
1007
1007 def get_file_mode(self, path):
1008 def get_file_mode(self, path):
1008 """
1009 """
1009 Returns stat mode of the file at `path`.
1010 Returns stat mode of the file at `path`.
1010 """
1011 """
1011 raise NotImplementedError
1012 raise NotImplementedError
1012
1013
1013 def is_link(self, path):
1014 def is_link(self, path):
1014 """
1015 """
1015 Returns ``True`` if given `path` is a symlink
1016 Returns ``True`` if given `path` is a symlink
1016 """
1017 """
1017 raise NotImplementedError
1018 raise NotImplementedError
1018
1019
1019 def get_file_content(self, path):
1020 def get_file_content(self, path):
1020 """
1021 """
1021 Returns content of the file at the given `path`.
1022 Returns content of the file at the given `path`.
1022 """
1023 """
1023 raise NotImplementedError
1024 raise NotImplementedError
1024
1025
1025 def get_file_size(self, path):
1026 def get_file_size(self, path):
1026 """
1027 """
1027 Returns size of the file at the given `path`.
1028 Returns size of the file at the given `path`.
1028 """
1029 """
1029 raise NotImplementedError
1030 raise NotImplementedError
1030
1031
1031 def get_path_commit(self, path, pre_load=None):
1032 def get_path_commit(self, path, pre_load=None):
1032 """
1033 """
1033 Returns last commit of the file at the given `path`.
1034 Returns last commit of the file at the given `path`.
1034
1035
1035 :param pre_load: Optional. List of commit attributes to load.
1036 :param pre_load: Optional. List of commit attributes to load.
1036 """
1037 """
1037 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1038 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1038 if not commits:
1039 if not commits:
1039 raise RepositoryError(
1040 raise RepositoryError(
1040 'Failed to fetch history for path {}. '
1041 'Failed to fetch history for path {}. '
1041 'Please check if such path exists in your repository'.format(
1042 'Please check if such path exists in your repository'.format(
1042 path))
1043 path))
1043 return commits[0]
1044 return commits[0]
1044
1045
1045 def get_path_history(self, path, limit=None, pre_load=None):
1046 def get_path_history(self, path, limit=None, pre_load=None):
1046 """
1047 """
1047 Returns history of file as reversed list of :class:`BaseCommit`
1048 Returns history of file as reversed list of :class:`BaseCommit`
1048 objects for which file at given `path` has been modified.
1049 objects for which file at given `path` has been modified.
1049
1050
1050 :param limit: Optional. Allows to limit the size of the returned
1051 :param limit: Optional. Allows to limit the size of the returned
1051 history. This is intended as a hint to the underlying backend, so
1052 history. This is intended as a hint to the underlying backend, so
1052 that it can apply optimizations depending on the limit.
1053 that it can apply optimizations depending on the limit.
1053 :param pre_load: Optional. List of commit attributes to load.
1054 :param pre_load: Optional. List of commit attributes to load.
1054 """
1055 """
1055 raise NotImplementedError
1056 raise NotImplementedError
1056
1057
1057 def get_file_annotate(self, path, pre_load=None):
1058 def get_file_annotate(self, path, pre_load=None):
1058 """
1059 """
1059 Returns a generator of four element tuples with
1060 Returns a generator of four element tuples with
1060 lineno, sha, commit lazy loader and line
1061 lineno, sha, commit lazy loader and line
1061
1062
1062 :param pre_load: Optional. List of commit attributes to load.
1063 :param pre_load: Optional. List of commit attributes to load.
1063 """
1064 """
1064 raise NotImplementedError
1065 raise NotImplementedError
1065
1066
1066 def get_nodes(self, path):
1067 def get_nodes(self, path):
1067 """
1068 """
1068 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1069 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1069 state of commit at the given ``path``.
1070 state of commit at the given ``path``.
1070
1071
1071 :raises ``CommitError``: if node at the given ``path`` is not
1072 :raises ``CommitError``: if node at the given ``path`` is not
1072 instance of ``DirNode``
1073 instance of ``DirNode``
1073 """
1074 """
1074 raise NotImplementedError
1075 raise NotImplementedError
1075
1076
1076 def get_node(self, path):
1077 def get_node(self, path):
1077 """
1078 """
1078 Returns ``Node`` object from the given ``path``.
1079 Returns ``Node`` object from the given ``path``.
1079
1080
1080 :raises ``NodeDoesNotExistError``: if there is no node at the given
1081 :raises ``NodeDoesNotExistError``: if there is no node at the given
1081 ``path``
1082 ``path``
1082 """
1083 """
1083 raise NotImplementedError
1084 raise NotImplementedError
1084
1085
1085 def get_largefile_node(self, path):
1086 def get_largefile_node(self, path):
1086 """
1087 """
1087 Returns the path to largefile from Mercurial/Git-lfs storage.
1088 Returns the path to largefile from Mercurial/Git-lfs storage.
1088 or None if it's not a largefile node
1089 or None if it's not a largefile node
1089 """
1090 """
1090 return None
1091 return None
1091
1092
1092 def archive_repo(self, file_path, kind='tgz', subrepos=None,
1093 def archive_repo(self, file_path, kind='tgz', subrepos=None,
1093 prefix=None, write_metadata=False, mtime=None):
1094 prefix=None, write_metadata=False, mtime=None):
1094 """
1095 """
1095 Creates an archive containing the contents of the repository.
1096 Creates an archive containing the contents of the repository.
1096
1097
1097 :param file_path: path to the file which to create the archive.
1098 :param file_path: path to the file which to create the archive.
1098 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1099 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1099 :param prefix: name of root directory in archive.
1100 :param prefix: name of root directory in archive.
1100 Default is repository name and commit's short_id joined with dash:
1101 Default is repository name and commit's short_id joined with dash:
1101 ``"{repo_name}-{short_id}"``.
1102 ``"{repo_name}-{short_id}"``.
1102 :param write_metadata: write a metadata file into archive.
1103 :param write_metadata: write a metadata file into archive.
1103 :param mtime: custom modification time for archive creation, defaults
1104 :param mtime: custom modification time for archive creation, defaults
1104 to time.time() if not given.
1105 to time.time() if not given.
1105
1106
1106 :raise VCSError: If prefix has a problem.
1107 :raise VCSError: If prefix has a problem.
1107 """
1108 """
1108 allowed_kinds = settings.ARCHIVE_SPECS.keys()
1109 allowed_kinds = settings.ARCHIVE_SPECS.keys()
1109 if kind not in allowed_kinds:
1110 if kind not in allowed_kinds:
1110 raise ImproperArchiveTypeError(
1111 raise ImproperArchiveTypeError(
1111 'Archive kind (%s) not supported use one of %s' %
1112 'Archive kind (%s) not supported use one of %s' %
1112 (kind, allowed_kinds))
1113 (kind, allowed_kinds))
1113
1114
1114 prefix = self._validate_archive_prefix(prefix)
1115 prefix = self._validate_archive_prefix(prefix)
1115
1116
1116 mtime = mtime or time.mktime(self.date.timetuple())
1117 mtime = mtime or time.mktime(self.date.timetuple())
1117
1118
1118 file_info = []
1119 file_info = []
1119 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1120 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1120 for _r, _d, files in cur_rev.walk('/'):
1121 for _r, _d, files in cur_rev.walk('/'):
1121 for f in files:
1122 for f in files:
1122 f_path = os.path.join(prefix, f.path)
1123 f_path = os.path.join(prefix, f.path)
1123 file_info.append(
1124 file_info.append(
1124 (f_path, f.mode, f.is_link(), f.raw_bytes))
1125 (f_path, f.mode, f.is_link(), f.raw_bytes))
1125
1126
1126 if write_metadata:
1127 if write_metadata:
1127 metadata = [
1128 metadata = [
1128 ('repo_name', self.repository.name),
1129 ('repo_name', self.repository.name),
1129 ('rev', self.raw_id),
1130 ('rev', self.raw_id),
1130 ('create_time', mtime),
1131 ('create_time', mtime),
1131 ('branch', self.branch),
1132 ('branch', self.branch),
1132 ('tags', ','.join(self.tags)),
1133 ('tags', ','.join(self.tags)),
1133 ]
1134 ]
1134 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1135 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1135 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1136 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1136
1137
1137 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
1138 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
1138
1139
1139 def _validate_archive_prefix(self, prefix):
1140 def _validate_archive_prefix(self, prefix):
1140 if prefix is None:
1141 if prefix is None:
1141 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1142 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1142 repo_name=safe_str(self.repository.name),
1143 repo_name=safe_str(self.repository.name),
1143 short_id=self.short_id)
1144 short_id=self.short_id)
1144 elif not isinstance(prefix, str):
1145 elif not isinstance(prefix, str):
1145 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1146 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1146 elif prefix.startswith('/'):
1147 elif prefix.startswith('/'):
1147 raise VCSError("Prefix cannot start with leading slash")
1148 raise VCSError("Prefix cannot start with leading slash")
1148 elif prefix.strip() == '':
1149 elif prefix.strip() == '':
1149 raise VCSError("Prefix cannot be empty")
1150 raise VCSError("Prefix cannot be empty")
1150 return prefix
1151 return prefix
1151
1152
1152 @LazyProperty
1153 @LazyProperty
1153 def root(self):
1154 def root(self):
1154 """
1155 """
1155 Returns ``RootNode`` object for this commit.
1156 Returns ``RootNode`` object for this commit.
1156 """
1157 """
1157 return self.get_node('')
1158 return self.get_node('')
1158
1159
1159 def next(self, branch=None):
1160 def next(self, branch=None):
1160 """
1161 """
1161 Returns next commit from current, if branch is gives it will return
1162 Returns next commit from current, if branch is gives it will return
1162 next commit belonging to this branch
1163 next commit belonging to this branch
1163
1164
1164 :param branch: show commits within the given named branch
1165 :param branch: show commits within the given named branch
1165 """
1166 """
1166 indexes = xrange(self.idx + 1, self.repository.count())
1167 indexes = xrange(self.idx + 1, self.repository.count())
1167 return self._find_next(indexes, branch)
1168 return self._find_next(indexes, branch)
1168
1169
1169 def prev(self, branch=None):
1170 def prev(self, branch=None):
1170 """
1171 """
1171 Returns previous commit from current, if branch is gives it will
1172 Returns previous commit from current, if branch is gives it will
1172 return previous commit belonging to this branch
1173 return previous commit belonging to this branch
1173
1174
1174 :param branch: show commit within the given named branch
1175 :param branch: show commit within the given named branch
1175 """
1176 """
1176 indexes = xrange(self.idx - 1, -1, -1)
1177 indexes = xrange(self.idx - 1, -1, -1)
1177 return self._find_next(indexes, branch)
1178 return self._find_next(indexes, branch)
1178
1179
1179 def _find_next(self, indexes, branch=None):
1180 def _find_next(self, indexes, branch=None):
1180 if branch and self.branch != branch:
1181 if branch and self.branch != branch:
1181 raise VCSError('Branch option used on commit not belonging '
1182 raise VCSError('Branch option used on commit not belonging '
1182 'to that branch')
1183 'to that branch')
1183
1184
1184 for next_idx in indexes:
1185 for next_idx in indexes:
1185 commit = self.repository.get_commit(commit_idx=next_idx)
1186 commit = self.repository.get_commit(commit_idx=next_idx)
1186 if branch and branch != commit.branch:
1187 if branch and branch != commit.branch:
1187 continue
1188 continue
1188 return commit
1189 return commit
1189 raise CommitDoesNotExistError
1190 raise CommitDoesNotExistError
1190
1191
1191 def diff(self, ignore_whitespace=True, context=3):
1192 def diff(self, ignore_whitespace=True, context=3):
1192 """
1193 """
1193 Returns a `Diff` object representing the change made by this commit.
1194 Returns a `Diff` object representing the change made by this commit.
1194 """
1195 """
1195 parent = self.first_parent
1196 parent = self.first_parent
1196 diff = self.repository.get_diff(
1197 diff = self.repository.get_diff(
1197 parent, self,
1198 parent, self,
1198 ignore_whitespace=ignore_whitespace,
1199 ignore_whitespace=ignore_whitespace,
1199 context=context)
1200 context=context)
1200 return diff
1201 return diff
1201
1202
1202 @LazyProperty
1203 @LazyProperty
1203 def added(self):
1204 def added(self):
1204 """
1205 """
1205 Returns list of added ``FileNode`` objects.
1206 Returns list of added ``FileNode`` objects.
1206 """
1207 """
1207 raise NotImplementedError
1208 raise NotImplementedError
1208
1209
1209 @LazyProperty
1210 @LazyProperty
1210 def changed(self):
1211 def changed(self):
1211 """
1212 """
1212 Returns list of modified ``FileNode`` objects.
1213 Returns list of modified ``FileNode`` objects.
1213 """
1214 """
1214 raise NotImplementedError
1215 raise NotImplementedError
1215
1216
1216 @LazyProperty
1217 @LazyProperty
1217 def removed(self):
1218 def removed(self):
1218 """
1219 """
1219 Returns list of removed ``FileNode`` objects.
1220 Returns list of removed ``FileNode`` objects.
1220 """
1221 """
1221 raise NotImplementedError
1222 raise NotImplementedError
1222
1223
1223 @LazyProperty
1224 @LazyProperty
1224 def size(self):
1225 def size(self):
1225 """
1226 """
1226 Returns total number of bytes from contents of all filenodes.
1227 Returns total number of bytes from contents of all filenodes.
1227 """
1228 """
1228 return sum((node.size for node in self.get_filenodes_generator()))
1229 return sum((node.size for node in self.get_filenodes_generator()))
1229
1230
1230 def walk(self, topurl=''):
1231 def walk(self, topurl=''):
1231 """
1232 """
1232 Similar to os.walk method. Insted of filesystem it walks through
1233 Similar to os.walk method. Insted of filesystem it walks through
1233 commit starting at given ``topurl``. Returns generator of tuples
1234 commit starting at given ``topurl``. Returns generator of tuples
1234 (topnode, dirnodes, filenodes).
1235 (topnode, dirnodes, filenodes).
1235 """
1236 """
1236 topnode = self.get_node(topurl)
1237 topnode = self.get_node(topurl)
1237 if not topnode.is_dir():
1238 if not topnode.is_dir():
1238 return
1239 return
1239 yield (topnode, topnode.dirs, topnode.files)
1240 yield (topnode, topnode.dirs, topnode.files)
1240 for dirnode in topnode.dirs:
1241 for dirnode in topnode.dirs:
1241 for tup in self.walk(dirnode.path):
1242 for tup in self.walk(dirnode.path):
1242 yield tup
1243 yield tup
1243
1244
1244 def get_filenodes_generator(self):
1245 def get_filenodes_generator(self):
1245 """
1246 """
1246 Returns generator that yields *all* file nodes.
1247 Returns generator that yields *all* file nodes.
1247 """
1248 """
1248 for topnode, dirs, files in self.walk():
1249 for topnode, dirs, files in self.walk():
1249 for node in files:
1250 for node in files:
1250 yield node
1251 yield node
1251
1252
1252 #
1253 #
1253 # Utilities for sub classes to support consistent behavior
1254 # Utilities for sub classes to support consistent behavior
1254 #
1255 #
1255
1256
1256 def no_node_at_path(self, path):
1257 def no_node_at_path(self, path):
1257 return NodeDoesNotExistError(
1258 return NodeDoesNotExistError(
1258 u"There is no file nor directory at the given path: "
1259 u"There is no file nor directory at the given path: "
1259 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1260 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1260
1261
1261 def _fix_path(self, path):
1262 def _fix_path(self, path):
1262 """
1263 """
1263 Paths are stored without trailing slash so we need to get rid off it if
1264 Paths are stored without trailing slash so we need to get rid off it if
1264 needed.
1265 needed.
1265 """
1266 """
1266 return path.rstrip('/')
1267 return path.rstrip('/')
1267
1268
1268 #
1269 #
1269 # Deprecated API based on changesets
1270 # Deprecated API based on changesets
1270 #
1271 #
1271
1272
1272 @property
1273 @property
1273 def revision(self):
1274 def revision(self):
1274 warnings.warn("Use idx instead", DeprecationWarning)
1275 warnings.warn("Use idx instead", DeprecationWarning)
1275 return self.idx
1276 return self.idx
1276
1277
1277 @revision.setter
1278 @revision.setter
1278 def revision(self, value):
1279 def revision(self, value):
1279 warnings.warn("Use idx instead", DeprecationWarning)
1280 warnings.warn("Use idx instead", DeprecationWarning)
1280 self.idx = value
1281 self.idx = value
1281
1282
1282 def get_file_changeset(self, path):
1283 def get_file_changeset(self, path):
1283 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1284 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1284 return self.get_path_commit(path)
1285 return self.get_path_commit(path)
1285
1286
1286
1287
1287 class BaseChangesetClass(type):
1288 class BaseChangesetClass(type):
1288
1289
1289 def __instancecheck__(self, instance):
1290 def __instancecheck__(self, instance):
1290 return isinstance(instance, BaseCommit)
1291 return isinstance(instance, BaseCommit)
1291
1292
1292
1293
1293 class BaseChangeset(BaseCommit):
1294 class BaseChangeset(BaseCommit):
1294
1295
1295 __metaclass__ = BaseChangesetClass
1296 __metaclass__ = BaseChangesetClass
1296
1297
1297 def __new__(cls, *args, **kwargs):
1298 def __new__(cls, *args, **kwargs):
1298 warnings.warn(
1299 warnings.warn(
1299 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1300 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1300 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1301 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1301
1302
1302
1303
1303 class BaseInMemoryCommit(object):
1304 class BaseInMemoryCommit(object):
1304 """
1305 """
1305 Represents differences between repository's state (most recent head) and
1306 Represents differences between repository's state (most recent head) and
1306 changes made *in place*.
1307 changes made *in place*.
1307
1308
1308 **Attributes**
1309 **Attributes**
1309
1310
1310 ``repository``
1311 ``repository``
1311 repository object for this in-memory-commit
1312 repository object for this in-memory-commit
1312
1313
1313 ``added``
1314 ``added``
1314 list of ``FileNode`` objects marked as *added*
1315 list of ``FileNode`` objects marked as *added*
1315
1316
1316 ``changed``
1317 ``changed``
1317 list of ``FileNode`` objects marked as *changed*
1318 list of ``FileNode`` objects marked as *changed*
1318
1319
1319 ``removed``
1320 ``removed``
1320 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1321 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1321 *removed*
1322 *removed*
1322
1323
1323 ``parents``
1324 ``parents``
1324 list of :class:`BaseCommit` instances representing parents of
1325 list of :class:`BaseCommit` instances representing parents of
1325 in-memory commit. Should always be 2-element sequence.
1326 in-memory commit. Should always be 2-element sequence.
1326
1327
1327 """
1328 """
1328
1329
1329 def __init__(self, repository):
1330 def __init__(self, repository):
1330 self.repository = repository
1331 self.repository = repository
1331 self.added = []
1332 self.added = []
1332 self.changed = []
1333 self.changed = []
1333 self.removed = []
1334 self.removed = []
1334 self.parents = []
1335 self.parents = []
1335
1336
1336 def add(self, *filenodes):
1337 def add(self, *filenodes):
1337 """
1338 """
1338 Marks given ``FileNode`` objects as *to be committed*.
1339 Marks given ``FileNode`` objects as *to be committed*.
1339
1340
1340 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1341 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1341 latest commit
1342 latest commit
1342 :raises ``NodeAlreadyAddedError``: if node with same path is already
1343 :raises ``NodeAlreadyAddedError``: if node with same path is already
1343 marked as *added*
1344 marked as *added*
1344 """
1345 """
1345 # Check if not already marked as *added* first
1346 # Check if not already marked as *added* first
1346 for node in filenodes:
1347 for node in filenodes:
1347 if node.path in (n.path for n in self.added):
1348 if node.path in (n.path for n in self.added):
1348 raise NodeAlreadyAddedError(
1349 raise NodeAlreadyAddedError(
1349 "Such FileNode %s is already marked for addition"
1350 "Such FileNode %s is already marked for addition"
1350 % node.path)
1351 % node.path)
1351 for node in filenodes:
1352 for node in filenodes:
1352 self.added.append(node)
1353 self.added.append(node)
1353
1354
1354 def change(self, *filenodes):
1355 def change(self, *filenodes):
1355 """
1356 """
1356 Marks given ``FileNode`` objects to be *changed* in next commit.
1357 Marks given ``FileNode`` objects to be *changed* in next commit.
1357
1358
1358 :raises ``EmptyRepositoryError``: if there are no commits yet
1359 :raises ``EmptyRepositoryError``: if there are no commits yet
1359 :raises ``NodeAlreadyExistsError``: if node with same path is already
1360 :raises ``NodeAlreadyExistsError``: if node with same path is already
1360 marked to be *changed*
1361 marked to be *changed*
1361 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1362 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1362 marked to be *removed*
1363 marked to be *removed*
1363 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1364 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1364 commit
1365 commit
1365 :raises ``NodeNotChangedError``: if node hasn't really be changed
1366 :raises ``NodeNotChangedError``: if node hasn't really be changed
1366 """
1367 """
1367 for node in filenodes:
1368 for node in filenodes:
1368 if node.path in (n.path for n in self.removed):
1369 if node.path in (n.path for n in self.removed):
1369 raise NodeAlreadyRemovedError(
1370 raise NodeAlreadyRemovedError(
1370 "Node at %s is already marked as removed" % node.path)
1371 "Node at %s is already marked as removed" % node.path)
1371 try:
1372 try:
1372 self.repository.get_commit()
1373 self.repository.get_commit()
1373 except EmptyRepositoryError:
1374 except EmptyRepositoryError:
1374 raise EmptyRepositoryError(
1375 raise EmptyRepositoryError(
1375 "Nothing to change - try to *add* new nodes rather than "
1376 "Nothing to change - try to *add* new nodes rather than "
1376 "changing them")
1377 "changing them")
1377 for node in filenodes:
1378 for node in filenodes:
1378 if node.path in (n.path for n in self.changed):
1379 if node.path in (n.path for n in self.changed):
1379 raise NodeAlreadyChangedError(
1380 raise NodeAlreadyChangedError(
1380 "Node at '%s' is already marked as changed" % node.path)
1381 "Node at '%s' is already marked as changed" % node.path)
1381 self.changed.append(node)
1382 self.changed.append(node)
1382
1383
1383 def remove(self, *filenodes):
1384 def remove(self, *filenodes):
1384 """
1385 """
1385 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1386 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1386 *removed* in next commit.
1387 *removed* in next commit.
1387
1388
1388 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1389 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1389 be *removed*
1390 be *removed*
1390 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1391 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1391 be *changed*
1392 be *changed*
1392 """
1393 """
1393 for node in filenodes:
1394 for node in filenodes:
1394 if node.path in (n.path for n in self.removed):
1395 if node.path in (n.path for n in self.removed):
1395 raise NodeAlreadyRemovedError(
1396 raise NodeAlreadyRemovedError(
1396 "Node is already marked to for removal at %s" % node.path)
1397 "Node is already marked to for removal at %s" % node.path)
1397 if node.path in (n.path for n in self.changed):
1398 if node.path in (n.path for n in self.changed):
1398 raise NodeAlreadyChangedError(
1399 raise NodeAlreadyChangedError(
1399 "Node is already marked to be changed at %s" % node.path)
1400 "Node is already marked to be changed at %s" % node.path)
1400 # We only mark node as *removed* - real removal is done by
1401 # We only mark node as *removed* - real removal is done by
1401 # commit method
1402 # commit method
1402 self.removed.append(node)
1403 self.removed.append(node)
1403
1404
1404 def reset(self):
1405 def reset(self):
1405 """
1406 """
1406 Resets this instance to initial state (cleans ``added``, ``changed``
1407 Resets this instance to initial state (cleans ``added``, ``changed``
1407 and ``removed`` lists).
1408 and ``removed`` lists).
1408 """
1409 """
1409 self.added = []
1410 self.added = []
1410 self.changed = []
1411 self.changed = []
1411 self.removed = []
1412 self.removed = []
1412 self.parents = []
1413 self.parents = []
1413
1414
1414 def get_ipaths(self):
1415 def get_ipaths(self):
1415 """
1416 """
1416 Returns generator of paths from nodes marked as added, changed or
1417 Returns generator of paths from nodes marked as added, changed or
1417 removed.
1418 removed.
1418 """
1419 """
1419 for node in itertools.chain(self.added, self.changed, self.removed):
1420 for node in itertools.chain(self.added, self.changed, self.removed):
1420 yield node.path
1421 yield node.path
1421
1422
1422 def get_paths(self):
1423 def get_paths(self):
1423 """
1424 """
1424 Returns list of paths from nodes marked as added, changed or removed.
1425 Returns list of paths from nodes marked as added, changed or removed.
1425 """
1426 """
1426 return list(self.get_ipaths())
1427 return list(self.get_ipaths())
1427
1428
1428 def check_integrity(self, parents=None):
1429 def check_integrity(self, parents=None):
1429 """
1430 """
1430 Checks in-memory commit's integrity. Also, sets parents if not
1431 Checks in-memory commit's integrity. Also, sets parents if not
1431 already set.
1432 already set.
1432
1433
1433 :raises CommitError: if any error occurs (i.e.
1434 :raises CommitError: if any error occurs (i.e.
1434 ``NodeDoesNotExistError``).
1435 ``NodeDoesNotExistError``).
1435 """
1436 """
1436 if not self.parents:
1437 if not self.parents:
1437 parents = parents or []
1438 parents = parents or []
1438 if len(parents) == 0:
1439 if len(parents) == 0:
1439 try:
1440 try:
1440 parents = [self.repository.get_commit(), None]
1441 parents = [self.repository.get_commit(), None]
1441 except EmptyRepositoryError:
1442 except EmptyRepositoryError:
1442 parents = [None, None]
1443 parents = [None, None]
1443 elif len(parents) == 1:
1444 elif len(parents) == 1:
1444 parents += [None]
1445 parents += [None]
1445 self.parents = parents
1446 self.parents = parents
1446
1447
1447 # Local parents, only if not None
1448 # Local parents, only if not None
1448 parents = [p for p in self.parents if p]
1449 parents = [p for p in self.parents if p]
1449
1450
1450 # Check nodes marked as added
1451 # Check nodes marked as added
1451 for p in parents:
1452 for p in parents:
1452 for node in self.added:
1453 for node in self.added:
1453 try:
1454 try:
1454 p.get_node(node.path)
1455 p.get_node(node.path)
1455 except NodeDoesNotExistError:
1456 except NodeDoesNotExistError:
1456 pass
1457 pass
1457 else:
1458 else:
1458 raise NodeAlreadyExistsError(
1459 raise NodeAlreadyExistsError(
1459 "Node `%s` already exists at %s" % (node.path, p))
1460 "Node `%s` already exists at %s" % (node.path, p))
1460
1461
1461 # Check nodes marked as changed
1462 # Check nodes marked as changed
1462 missing = set(self.changed)
1463 missing = set(self.changed)
1463 not_changed = set(self.changed)
1464 not_changed = set(self.changed)
1464 if self.changed and not parents:
1465 if self.changed and not parents:
1465 raise NodeDoesNotExistError(str(self.changed[0].path))
1466 raise NodeDoesNotExistError(str(self.changed[0].path))
1466 for p in parents:
1467 for p in parents:
1467 for node in self.changed:
1468 for node in self.changed:
1468 try:
1469 try:
1469 old = p.get_node(node.path)
1470 old = p.get_node(node.path)
1470 missing.remove(node)
1471 missing.remove(node)
1471 # if content actually changed, remove node from not_changed
1472 # if content actually changed, remove node from not_changed
1472 if old.content != node.content:
1473 if old.content != node.content:
1473 not_changed.remove(node)
1474 not_changed.remove(node)
1474 except NodeDoesNotExistError:
1475 except NodeDoesNotExistError:
1475 pass
1476 pass
1476 if self.changed and missing:
1477 if self.changed and missing:
1477 raise NodeDoesNotExistError(
1478 raise NodeDoesNotExistError(
1478 "Node `%s` marked as modified but missing in parents: %s"
1479 "Node `%s` marked as modified but missing in parents: %s"
1479 % (node.path, parents))
1480 % (node.path, parents))
1480
1481
1481 if self.changed and not_changed:
1482 if self.changed and not_changed:
1482 raise NodeNotChangedError(
1483 raise NodeNotChangedError(
1483 "Node `%s` wasn't actually changed (parents: %s)"
1484 "Node `%s` wasn't actually changed (parents: %s)"
1484 % (not_changed.pop().path, parents))
1485 % (not_changed.pop().path, parents))
1485
1486
1486 # Check nodes marked as removed
1487 # Check nodes marked as removed
1487 if self.removed and not parents:
1488 if self.removed and not parents:
1488 raise NodeDoesNotExistError(
1489 raise NodeDoesNotExistError(
1489 "Cannot remove node at %s as there "
1490 "Cannot remove node at %s as there "
1490 "were no parents specified" % self.removed[0].path)
1491 "were no parents specified" % self.removed[0].path)
1491 really_removed = set()
1492 really_removed = set()
1492 for p in parents:
1493 for p in parents:
1493 for node in self.removed:
1494 for node in self.removed:
1494 try:
1495 try:
1495 p.get_node(node.path)
1496 p.get_node(node.path)
1496 really_removed.add(node)
1497 really_removed.add(node)
1497 except CommitError:
1498 except CommitError:
1498 pass
1499 pass
1499 not_removed = set(self.removed) - really_removed
1500 not_removed = set(self.removed) - really_removed
1500 if not_removed:
1501 if not_removed:
1501 # TODO: johbo: This code branch does not seem to be covered
1502 # TODO: johbo: This code branch does not seem to be covered
1502 raise NodeDoesNotExistError(
1503 raise NodeDoesNotExistError(
1503 "Cannot remove node at %s from "
1504 "Cannot remove node at %s from "
1504 "following parents: %s" % (not_removed, parents))
1505 "following parents: %s" % (not_removed, parents))
1505
1506
1506 def commit(
1507 def commit(
1507 self, message, author, parents=None, branch=None, date=None,
1508 self, message, author, parents=None, branch=None, date=None,
1508 **kwargs):
1509 **kwargs):
1509 """
1510 """
1510 Performs in-memory commit (doesn't check workdir in any way) and
1511 Performs in-memory commit (doesn't check workdir in any way) and
1511 returns newly created :class:`BaseCommit`. Updates repository's
1512 returns newly created :class:`BaseCommit`. Updates repository's
1512 attribute `commits`.
1513 attribute `commits`.
1513
1514
1514 .. note::
1515 .. note::
1515
1516
1516 While overriding this method each backend's should call
1517 While overriding this method each backend's should call
1517 ``self.check_integrity(parents)`` in the first place.
1518 ``self.check_integrity(parents)`` in the first place.
1518
1519
1519 :param message: message of the commit
1520 :param message: message of the commit
1520 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1521 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1521 :param parents: single parent or sequence of parents from which commit
1522 :param parents: single parent or sequence of parents from which commit
1522 would be derived
1523 would be derived
1523 :param date: ``datetime.datetime`` instance. Defaults to
1524 :param date: ``datetime.datetime`` instance. Defaults to
1524 ``datetime.datetime.now()``.
1525 ``datetime.datetime.now()``.
1525 :param branch: branch name, as string. If none given, default backend's
1526 :param branch: branch name, as string. If none given, default backend's
1526 branch would be used.
1527 branch would be used.
1527
1528
1528 :raises ``CommitError``: if any error occurs while committing
1529 :raises ``CommitError``: if any error occurs while committing
1529 """
1530 """
1530 raise NotImplementedError
1531 raise NotImplementedError
1531
1532
1532
1533
1533 class BaseInMemoryChangesetClass(type):
1534 class BaseInMemoryChangesetClass(type):
1534
1535
1535 def __instancecheck__(self, instance):
1536 def __instancecheck__(self, instance):
1536 return isinstance(instance, BaseInMemoryCommit)
1537 return isinstance(instance, BaseInMemoryCommit)
1537
1538
1538
1539
1539 class BaseInMemoryChangeset(BaseInMemoryCommit):
1540 class BaseInMemoryChangeset(BaseInMemoryCommit):
1540
1541
1541 __metaclass__ = BaseInMemoryChangesetClass
1542 __metaclass__ = BaseInMemoryChangesetClass
1542
1543
1543 def __new__(cls, *args, **kwargs):
1544 def __new__(cls, *args, **kwargs):
1544 warnings.warn(
1545 warnings.warn(
1545 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1546 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1546 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1547 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1547
1548
1548
1549
1549 class EmptyCommit(BaseCommit):
1550 class EmptyCommit(BaseCommit):
1550 """
1551 """
1551 An dummy empty commit. It's possible to pass hash when creating
1552 An dummy empty commit. It's possible to pass hash when creating
1552 an EmptyCommit
1553 an EmptyCommit
1553 """
1554 """
1554
1555
1555 def __init__(
1556 def __init__(
1556 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1557 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1557 message='', author='', date=None):
1558 message='', author='', date=None):
1558 self._empty_commit_id = commit_id
1559 self._empty_commit_id = commit_id
1559 # TODO: johbo: Solve idx parameter, default value does not make
1560 # TODO: johbo: Solve idx parameter, default value does not make
1560 # too much sense
1561 # too much sense
1561 self.idx = idx
1562 self.idx = idx
1562 self.message = message
1563 self.message = message
1563 self.author = author
1564 self.author = author
1564 self.date = date or datetime.datetime.fromtimestamp(0)
1565 self.date = date or datetime.datetime.fromtimestamp(0)
1565 self.repository = repo
1566 self.repository = repo
1566 self.alias = alias
1567 self.alias = alias
1567
1568
1568 @LazyProperty
1569 @LazyProperty
1569 def raw_id(self):
1570 def raw_id(self):
1570 """
1571 """
1571 Returns raw string identifying this commit, useful for web
1572 Returns raw string identifying this commit, useful for web
1572 representation.
1573 representation.
1573 """
1574 """
1574
1575
1575 return self._empty_commit_id
1576 return self._empty_commit_id
1576
1577
1577 @LazyProperty
1578 @LazyProperty
1578 def branch(self):
1579 def branch(self):
1579 if self.alias:
1580 if self.alias:
1580 from rhodecode.lib.vcs.backends import get_backend
1581 from rhodecode.lib.vcs.backends import get_backend
1581 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1582 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1582
1583
1583 @LazyProperty
1584 @LazyProperty
1584 def short_id(self):
1585 def short_id(self):
1585 return self.raw_id[:12]
1586 return self.raw_id[:12]
1586
1587
1587 @LazyProperty
1588 @LazyProperty
1588 def id(self):
1589 def id(self):
1589 return self.raw_id
1590 return self.raw_id
1590
1591
1591 def get_path_commit(self, path):
1592 def get_path_commit(self, path):
1592 return self
1593 return self
1593
1594
1594 def get_file_content(self, path):
1595 def get_file_content(self, path):
1595 return u''
1596 return u''
1596
1597
1597 def get_file_size(self, path):
1598 def get_file_size(self, path):
1598 return 0
1599 return 0
1599
1600
1600
1601
1601 class EmptyChangesetClass(type):
1602 class EmptyChangesetClass(type):
1602
1603
1603 def __instancecheck__(self, instance):
1604 def __instancecheck__(self, instance):
1604 return isinstance(instance, EmptyCommit)
1605 return isinstance(instance, EmptyCommit)
1605
1606
1606
1607
1607 class EmptyChangeset(EmptyCommit):
1608 class EmptyChangeset(EmptyCommit):
1608
1609
1609 __metaclass__ = EmptyChangesetClass
1610 __metaclass__ = EmptyChangesetClass
1610
1611
1611 def __new__(cls, *args, **kwargs):
1612 def __new__(cls, *args, **kwargs):
1612 warnings.warn(
1613 warnings.warn(
1613 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1614 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1614 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1615 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1615
1616
1616 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1617 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1617 alias=None, revision=-1, message='', author='', date=None):
1618 alias=None, revision=-1, message='', author='', date=None):
1618 if requested_revision is not None:
1619 if requested_revision is not None:
1619 warnings.warn(
1620 warnings.warn(
1620 "Parameter requested_revision not supported anymore",
1621 "Parameter requested_revision not supported anymore",
1621 DeprecationWarning)
1622 DeprecationWarning)
1622 super(EmptyChangeset, self).__init__(
1623 super(EmptyChangeset, self).__init__(
1623 commit_id=cs, repo=repo, alias=alias, idx=revision,
1624 commit_id=cs, repo=repo, alias=alias, idx=revision,
1624 message=message, author=author, date=date)
1625 message=message, author=author, date=date)
1625
1626
1626 @property
1627 @property
1627 def revision(self):
1628 def revision(self):
1628 warnings.warn("Use idx instead", DeprecationWarning)
1629 warnings.warn("Use idx instead", DeprecationWarning)
1629 return self.idx
1630 return self.idx
1630
1631
1631 @revision.setter
1632 @revision.setter
1632 def revision(self, value):
1633 def revision(self, value):
1633 warnings.warn("Use idx instead", DeprecationWarning)
1634 warnings.warn("Use idx instead", DeprecationWarning)
1634 self.idx = value
1635 self.idx = value
1635
1636
1636
1637
1637 class EmptyRepository(BaseRepository):
1638 class EmptyRepository(BaseRepository):
1638 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1639 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1639 pass
1640 pass
1640
1641
1641 def get_diff(self, *args, **kwargs):
1642 def get_diff(self, *args, **kwargs):
1642 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1643 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1643 return GitDiff('')
1644 return GitDiff('')
1644
1645
1645
1646
1646 class CollectionGenerator(object):
1647 class CollectionGenerator(object):
1647
1648
1648 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1649 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1649 self.repo = repo
1650 self.repo = repo
1650 self.commit_ids = commit_ids
1651 self.commit_ids = commit_ids
1651 # TODO: (oliver) this isn't currently hooked up
1652 # TODO: (oliver) this isn't currently hooked up
1652 self.collection_size = None
1653 self.collection_size = None
1653 self.pre_load = pre_load
1654 self.pre_load = pre_load
1654
1655
1655 def __len__(self):
1656 def __len__(self):
1656 if self.collection_size is not None:
1657 if self.collection_size is not None:
1657 return self.collection_size
1658 return self.collection_size
1658 return self.commit_ids.__len__()
1659 return self.commit_ids.__len__()
1659
1660
1660 def __iter__(self):
1661 def __iter__(self):
1661 for commit_id in self.commit_ids:
1662 for commit_id in self.commit_ids:
1662 # TODO: johbo: Mercurial passes in commit indices or commit ids
1663 # TODO: johbo: Mercurial passes in commit indices or commit ids
1663 yield self._commit_factory(commit_id)
1664 yield self._commit_factory(commit_id)
1664
1665
1665 def _commit_factory(self, commit_id):
1666 def _commit_factory(self, commit_id):
1666 """
1667 """
1667 Allows backends to override the way commits are generated.
1668 Allows backends to override the way commits are generated.
1668 """
1669 """
1669 return self.repo.get_commit(commit_id=commit_id,
1670 return self.repo.get_commit(commit_id=commit_id,
1670 pre_load=self.pre_load)
1671 pre_load=self.pre_load)
1671
1672
1672 def __getslice__(self, i, j):
1673 def __getslice__(self, i, j):
1673 """
1674 """
1674 Returns an iterator of sliced repository
1675 Returns an iterator of sliced repository
1675 """
1676 """
1676 commit_ids = self.commit_ids[i:j]
1677 commit_ids = self.commit_ids[i:j]
1677 return self.__class__(
1678 return self.__class__(
1678 self.repo, commit_ids, pre_load=self.pre_load)
1679 self.repo, commit_ids, pre_load=self.pre_load)
1679
1680
1680 def __repr__(self):
1681 def __repr__(self):
1681 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1682 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1682
1683
1683
1684
1684 class Config(object):
1685 class Config(object):
1685 """
1686 """
1686 Represents the configuration for a repository.
1687 Represents the configuration for a repository.
1687
1688
1688 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1689 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1689 standard library. It implements only the needed subset.
1690 standard library. It implements only the needed subset.
1690 """
1691 """
1691
1692
1692 def __init__(self):
1693 def __init__(self):
1693 self._values = {}
1694 self._values = {}
1694
1695
1695 def copy(self):
1696 def copy(self):
1696 clone = Config()
1697 clone = Config()
1697 for section, values in self._values.items():
1698 for section, values in self._values.items():
1698 clone._values[section] = values.copy()
1699 clone._values[section] = values.copy()
1699 return clone
1700 return clone
1700
1701
1701 def __repr__(self):
1702 def __repr__(self):
1702 return '<Config(%s sections) at %s>' % (
1703 return '<Config(%s sections) at %s>' % (
1703 len(self._values), hex(id(self)))
1704 len(self._values), hex(id(self)))
1704
1705
1705 def items(self, section):
1706 def items(self, section):
1706 return self._values.get(section, {}).iteritems()
1707 return self._values.get(section, {}).iteritems()
1707
1708
1708 def get(self, section, option):
1709 def get(self, section, option):
1709 return self._values.get(section, {}).get(option)
1710 return self._values.get(section, {}).get(option)
1710
1711
1711 def set(self, section, option, value):
1712 def set(self, section, option, value):
1712 section_values = self._values.setdefault(section, {})
1713 section_values = self._values.setdefault(section, {})
1713 section_values[option] = value
1714 section_values[option] = value
1714
1715
1715 def clear_section(self, section):
1716 def clear_section(self, section):
1716 self._values[section] = {}
1717 self._values[section] = {}
1717
1718
1718 def serialize(self):
1719 def serialize(self):
1719 """
1720 """
1720 Creates a list of three tuples (section, key, value) representing
1721 Creates a list of three tuples (section, key, value) representing
1721 this config object.
1722 this config object.
1722 """
1723 """
1723 items = []
1724 items = []
1724 for section in self._values:
1725 for section in self._values:
1725 for option, value in self._values[section].items():
1726 for option, value in self._values[section].items():
1726 items.append(
1727 items.append(
1727 (safe_str(section), safe_str(option), safe_str(value)))
1728 (safe_str(section), safe_str(option), safe_str(value)))
1728 return items
1729 return items
1729
1730
1730
1731
1731 class Diff(object):
1732 class Diff(object):
1732 """
1733 """
1733 Represents a diff result from a repository backend.
1734 Represents a diff result from a repository backend.
1734
1735
1735 Subclasses have to provide a backend specific value for
1736 Subclasses have to provide a backend specific value for
1736 :attr:`_header_re` and :attr:`_meta_re`.
1737 :attr:`_header_re` and :attr:`_meta_re`.
1737 """
1738 """
1738 _meta_re = None
1739 _meta_re = None
1739 _header_re = None
1740 _header_re = None
1740
1741
1741 def __init__(self, raw_diff):
1742 def __init__(self, raw_diff):
1742 self.raw = raw_diff
1743 self.raw = raw_diff
1743
1744
1744 def chunks(self):
1745 def chunks(self):
1745 """
1746 """
1746 split the diff in chunks of separate --git a/file b/file chunks
1747 split the diff in chunks of separate --git a/file b/file chunks
1747 to make diffs consistent we must prepend with \n, and make sure
1748 to make diffs consistent we must prepend with \n, and make sure
1748 we can detect last chunk as this was also has special rule
1749 we can detect last chunk as this was also has special rule
1749 """
1750 """
1750
1751
1751 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1752 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1752 header = diff_parts[0]
1753 header = diff_parts[0]
1753
1754
1754 if self._meta_re:
1755 if self._meta_re:
1755 match = self._meta_re.match(header)
1756 match = self._meta_re.match(header)
1756
1757
1757 chunks = diff_parts[1:]
1758 chunks = diff_parts[1:]
1758 total_chunks = len(chunks)
1759 total_chunks = len(chunks)
1759
1760
1760 return (
1761 return (
1761 DiffChunk(chunk, self, cur_chunk == total_chunks)
1762 DiffChunk(chunk, self, cur_chunk == total_chunks)
1762 for cur_chunk, chunk in enumerate(chunks, start=1))
1763 for cur_chunk, chunk in enumerate(chunks, start=1))
1763
1764
1764
1765
1765 class DiffChunk(object):
1766 class DiffChunk(object):
1766
1767
1767 def __init__(self, chunk, diff, last_chunk):
1768 def __init__(self, chunk, diff, last_chunk):
1768 self._diff = diff
1769 self._diff = diff
1769
1770
1770 # since we split by \ndiff --git that part is lost from original diff
1771 # since we split by \ndiff --git that part is lost from original diff
1771 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1772 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1772 if not last_chunk:
1773 if not last_chunk:
1773 chunk += '\n'
1774 chunk += '\n'
1774
1775
1775 match = self._diff._header_re.match(chunk)
1776 match = self._diff._header_re.match(chunk)
1776 self.header = match.groupdict()
1777 self.header = match.groupdict()
1777 self.diff = chunk[match.end():]
1778 self.diff = chunk[match.end():]
1778 self.raw = chunk
1779 self.raw = chunk
1779
1780
1780
1781
1781 class BasePathPermissionChecker(object):
1782 class BasePathPermissionChecker(object):
1782
1783
1783 @staticmethod
1784 @staticmethod
1784 def create_from_patterns(includes, excludes):
1785 def create_from_patterns(includes, excludes):
1785 if includes and '*' in includes and not excludes:
1786 if includes and '*' in includes and not excludes:
1786 return AllPathPermissionChecker()
1787 return AllPathPermissionChecker()
1787 elif excludes and '*' in excludes:
1788 elif excludes and '*' in excludes:
1788 return NonePathPermissionChecker()
1789 return NonePathPermissionChecker()
1789 else:
1790 else:
1790 return PatternPathPermissionChecker(includes, excludes)
1791 return PatternPathPermissionChecker(includes, excludes)
1791
1792
1792 @property
1793 @property
1793 def has_full_access(self):
1794 def has_full_access(self):
1794 raise NotImplemented()
1795 raise NotImplemented()
1795
1796
1796 def has_access(self, path):
1797 def has_access(self, path):
1797 raise NotImplemented()
1798 raise NotImplemented()
1798
1799
1799
1800
1800 class AllPathPermissionChecker(BasePathPermissionChecker):
1801 class AllPathPermissionChecker(BasePathPermissionChecker):
1801
1802
1802 @property
1803 @property
1803 def has_full_access(self):
1804 def has_full_access(self):
1804 return True
1805 return True
1805
1806
1806 def has_access(self, path):
1807 def has_access(self, path):
1807 return True
1808 return True
1808
1809
1809
1810
1810 class NonePathPermissionChecker(BasePathPermissionChecker):
1811 class NonePathPermissionChecker(BasePathPermissionChecker):
1811
1812
1812 @property
1813 @property
1813 def has_full_access(self):
1814 def has_full_access(self):
1814 return False
1815 return False
1815
1816
1816 def has_access(self, path):
1817 def has_access(self, path):
1817 return False
1818 return False
1818
1819
1819
1820
1820 class PatternPathPermissionChecker(BasePathPermissionChecker):
1821 class PatternPathPermissionChecker(BasePathPermissionChecker):
1821
1822
1822 def __init__(self, includes, excludes):
1823 def __init__(self, includes, excludes):
1823 self.includes = includes
1824 self.includes = includes
1824 self.excludes = excludes
1825 self.excludes = excludes
1825 self.includes_re = [] if not includes else [
1826 self.includes_re = [] if not includes else [
1826 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1827 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1827 self.excludes_re = [] if not excludes else [
1828 self.excludes_re = [] if not excludes else [
1828 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1829 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1829
1830
1830 @property
1831 @property
1831 def has_full_access(self):
1832 def has_full_access(self):
1832 return '*' in self.includes and not self.excludes
1833 return '*' in self.includes and not self.excludes
1833
1834
1834 def has_access(self, path):
1835 def has_access(self, path):
1835 for regex in self.excludes_re:
1836 for regex in self.excludes_re:
1836 if regex.match(path):
1837 if regex.match(path):
1837 return False
1838 return False
1838 for regex in self.includes_re:
1839 for regex in self.includes_re:
1839 if regex.match(path):
1840 if regex.match(path):
1840 return True
1841 return True
1841 return False
1842 return False
General Comments 0
You need to be logged in to leave comments. Login now