##// END OF EJS Templates
release: merge back stable branch into default
marcink -
r3622:cbe7f2f9 merge default
parent child Browse files
Show More
@@ -0,0 +1,41 b''
1 |RCE| 4.16.2 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2019-04-02
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18
19
20 Security
21 ^^^^^^^^
22
23
24
25 Performance
26 ^^^^^^^^^^^
27
28
29
30 Fixes
31 ^^^^^
32
33 - Integrations: fixed missing template variable for fork reference checks.
34 - Permissions: fixed server error when showing permissions for user groups.
35 - Pull requests: fixed a bug in removal of multiple reviewers at once.
36
37
38 Upgrade notes
39 ^^^^^^^^^^^^^
40
41 - Scheduled release addressing problems in 4.16.X releases.
@@ -1,53 +1,54 b''
1 1bd3e92b7e2e2d2024152b34bb88dff1db544a71 v4.0.0
1 1bd3e92b7e2e2d2024152b34bb88dff1db544a71 v4.0.0
2 170c5398320ea6cddd50955e88d408794c21d43a v4.0.1
2 170c5398320ea6cddd50955e88d408794c21d43a v4.0.1
3 c3fe200198f5aa34cf2e4066df2881a9cefe3704 v4.1.0
3 c3fe200198f5aa34cf2e4066df2881a9cefe3704 v4.1.0
4 7fd5c850745e2ea821fb4406af5f4bff9b0a7526 v4.1.1
4 7fd5c850745e2ea821fb4406af5f4bff9b0a7526 v4.1.1
5 41c87da28a179953df86061d817bc35533c66dd2 v4.1.2
5 41c87da28a179953df86061d817bc35533c66dd2 v4.1.2
6 baaf9f5bcea3bae0ef12ae20c8b270482e62abb6 v4.2.0
6 baaf9f5bcea3bae0ef12ae20c8b270482e62abb6 v4.2.0
7 32a70c7e56844a825f61df496ee5eaf8c3c4e189 v4.2.1
7 32a70c7e56844a825f61df496ee5eaf8c3c4e189 v4.2.1
8 fa695cdb411d294679ac081d595ac654e5613b03 v4.3.0
8 fa695cdb411d294679ac081d595ac654e5613b03 v4.3.0
9 0e4dc11b58cad833c513fe17bac39e6850edf959 v4.3.1
9 0e4dc11b58cad833c513fe17bac39e6850edf959 v4.3.1
10 8a876f48f5cb1d018b837db28ff928500cb32cfb v4.4.0
10 8a876f48f5cb1d018b837db28ff928500cb32cfb v4.4.0
11 8dd86b410b1aac086ffdfc524ef300f896af5047 v4.4.1
11 8dd86b410b1aac086ffdfc524ef300f896af5047 v4.4.1
12 d2514226abc8d3b4f6fb57765f47d1b6fb360a05 v4.4.2
12 d2514226abc8d3b4f6fb57765f47d1b6fb360a05 v4.4.2
13 27d783325930af6dad2741476c0d0b1b7c8415c2 v4.5.0
13 27d783325930af6dad2741476c0d0b1b7c8415c2 v4.5.0
14 7f2016f352abcbdba4a19d4039c386e9629449da v4.5.1
14 7f2016f352abcbdba4a19d4039c386e9629449da v4.5.1
15 416fec799314c70a5c780fb28b3357b08869333a v4.5.2
15 416fec799314c70a5c780fb28b3357b08869333a v4.5.2
16 27c3b85fafc83143e6678fbc3da69e1615bcac55 v4.6.0
16 27c3b85fafc83143e6678fbc3da69e1615bcac55 v4.6.0
17 5ad13deb9118c2a5243d4032d4d9cc174e5872db v4.6.1
17 5ad13deb9118c2a5243d4032d4d9cc174e5872db v4.6.1
18 2be921e01fa24bb102696ada596f87464c3666f6 v4.7.0
18 2be921e01fa24bb102696ada596f87464c3666f6 v4.7.0
19 7198bdec29c2872c974431d55200d0398354cdb1 v4.7.1
19 7198bdec29c2872c974431d55200d0398354cdb1 v4.7.1
20 bd1c8d230fe741c2dfd7100a0ef39fd0774fd581 v4.7.2
20 bd1c8d230fe741c2dfd7100a0ef39fd0774fd581 v4.7.2
21 9731914f89765d9628dc4dddc84bc9402aa124c8 v4.8.0
21 9731914f89765d9628dc4dddc84bc9402aa124c8 v4.8.0
22 c5a2b7d0e4bbdebc4a62d7b624befe375207b659 v4.9.0
22 c5a2b7d0e4bbdebc4a62d7b624befe375207b659 v4.9.0
23 d9aa3b27ac9f7e78359775c75fedf7bfece232f1 v4.9.1
23 d9aa3b27ac9f7e78359775c75fedf7bfece232f1 v4.9.1
24 4ba4d74981cec5d6b28b158f875a2540952c2f74 v4.10.0
24 4ba4d74981cec5d6b28b158f875a2540952c2f74 v4.10.0
25 0a6821cbd6b0b3c21503002f88800679fa35ab63 v4.10.1
25 0a6821cbd6b0b3c21503002f88800679fa35ab63 v4.10.1
26 434ad90ec8d621f4416074b84f6e9ce03964defb v4.10.2
26 434ad90ec8d621f4416074b84f6e9ce03964defb v4.10.2
27 68baee10e698da2724c6e0f698c03a6abb993bf2 v4.10.3
27 68baee10e698da2724c6e0f698c03a6abb993bf2 v4.10.3
28 00821d3afd1dce3f4767cc353f84a17f7d5218a1 v4.10.4
28 00821d3afd1dce3f4767cc353f84a17f7d5218a1 v4.10.4
29 22f6744ad8cc274311825f63f953e4dee2ea5cb9 v4.10.5
29 22f6744ad8cc274311825f63f953e4dee2ea5cb9 v4.10.5
30 96eb24bea2f5f9258775245e3f09f6fa0a4dda01 v4.10.6
30 96eb24bea2f5f9258775245e3f09f6fa0a4dda01 v4.10.6
31 3121217a812c956d7dd5a5875821bd73e8002a32 v4.11.0
31 3121217a812c956d7dd5a5875821bd73e8002a32 v4.11.0
32 fa98b454715ac5b912f39e84af54345909a2a805 v4.11.1
32 fa98b454715ac5b912f39e84af54345909a2a805 v4.11.1
33 3982abcfdcc229a723cebe52d3a9bcff10bba08e v4.11.2
33 3982abcfdcc229a723cebe52d3a9bcff10bba08e v4.11.2
34 33195f145db9172f0a8f1487e09207178a6ab065 v4.11.3
34 33195f145db9172f0a8f1487e09207178a6ab065 v4.11.3
35 194c74f33e32bbae6fc4d71ec5a999cff3c13605 v4.11.4
35 194c74f33e32bbae6fc4d71ec5a999cff3c13605 v4.11.4
36 8fbd8b0c3ddc2fa4ac9e4ca16942a03eb593df2d v4.11.5
36 8fbd8b0c3ddc2fa4ac9e4ca16942a03eb593df2d v4.11.5
37 f0609aa5d5d05a1ca2f97c3995542236131c9d8a v4.11.6
37 f0609aa5d5d05a1ca2f97c3995542236131c9d8a v4.11.6
38 b5b30547d90d2e088472a70c84878f429ffbf40d v4.12.0
38 b5b30547d90d2e088472a70c84878f429ffbf40d v4.12.0
39 9072253aa8894d20c00b4a43dc61c2168c1eff94 v4.12.1
39 9072253aa8894d20c00b4a43dc61c2168c1eff94 v4.12.1
40 6a517543ea9ef9987d74371bd2a315eb0b232dc9 v4.12.2
40 6a517543ea9ef9987d74371bd2a315eb0b232dc9 v4.12.2
41 7fc0731b024c3114be87865eda7ab621cc957e32 v4.12.3
41 7fc0731b024c3114be87865eda7ab621cc957e32 v4.12.3
42 6d531c0b068c6eda62dddceedc9f845ecb6feb6f v4.12.4
42 6d531c0b068c6eda62dddceedc9f845ecb6feb6f v4.12.4
43 3d6bf2d81b1564830eb5e83396110d2a9a93eb1e v4.13.0
43 3d6bf2d81b1564830eb5e83396110d2a9a93eb1e v4.13.0
44 5468fc89e708bd90e413cd0d54350017abbdbc0e v4.13.1
44 5468fc89e708bd90e413cd0d54350017abbdbc0e v4.13.1
45 610d621550521c314ee97b3d43473ac0bcf06fb8 v4.13.2
45 610d621550521c314ee97b3d43473ac0bcf06fb8 v4.13.2
46 7dc62c090881fb5d03268141e71e0940d7c3295d v4.13.3
46 7dc62c090881fb5d03268141e71e0940d7c3295d v4.13.3
47 9151328c1c46b72ba6f00d7640d9141e75aa1ca2 v4.14.0
47 9151328c1c46b72ba6f00d7640d9141e75aa1ca2 v4.14.0
48 a47eeac5dfa41fa6779d90452affba4091c3ade8 v4.14.1
48 a47eeac5dfa41fa6779d90452affba4091c3ade8 v4.14.1
49 4b34ce0d2c3c10510626b3b65044939bb7a2cddf v4.15.0
49 4b34ce0d2c3c10510626b3b65044939bb7a2cddf v4.15.0
50 14502561d22e6b70613674cd675ae9a604b7989f v4.15.1
50 14502561d22e6b70613674cd675ae9a604b7989f v4.15.1
51 4aaa40b605b01af78a9f6882eca561c54b525ef0 v4.15.2
51 4aaa40b605b01af78a9f6882eca561c54b525ef0 v4.15.2
52 797744642eca86640ed20bef2cd77445780abaec v4.16.0
52 797744642eca86640ed20bef2cd77445780abaec v4.16.0
53 6c3452c7c25ed35ff269690929e11960ed6ad7d3 v4.16.1
53 6c3452c7c25ed35ff269690929e11960ed6ad7d3 v4.16.1
54 5d8057df561c4b6b81b6401aed7d2f911e6e77f7 v4.16.2
@@ -1,130 +1,131 b''
1 .. _rhodecode-release-notes-ref:
1 .. _rhodecode-release-notes-ref:
2
2
3 Release Notes
3 Release Notes
4 =============
4 =============
5
5
6 |RCE| 4.x Versions
6 |RCE| 4.x Versions
7 ------------------
7 ------------------
8
8
9 .. toctree::
9 .. toctree::
10 :maxdepth: 1
10 :maxdepth: 1
11
11
12 release-notes-4.16.2.rst
12 release-notes-4.16.1.rst
13 release-notes-4.16.1.rst
13 release-notes-4.16.0.rst
14 release-notes-4.16.0.rst
14 release-notes-4.15.2.rst
15 release-notes-4.15.2.rst
15 release-notes-4.15.1.rst
16 release-notes-4.15.1.rst
16 release-notes-4.15.0.rst
17 release-notes-4.15.0.rst
17 release-notes-4.14.1.rst
18 release-notes-4.14.1.rst
18 release-notes-4.14.0.rst
19 release-notes-4.14.0.rst
19 release-notes-4.13.3.rst
20 release-notes-4.13.3.rst
20 release-notes-4.13.2.rst
21 release-notes-4.13.2.rst
21 release-notes-4.13.1.rst
22 release-notes-4.13.1.rst
22 release-notes-4.13.0.rst
23 release-notes-4.13.0.rst
23 release-notes-4.12.4.rst
24 release-notes-4.12.4.rst
24 release-notes-4.12.3.rst
25 release-notes-4.12.3.rst
25 release-notes-4.12.2.rst
26 release-notes-4.12.2.rst
26 release-notes-4.12.1.rst
27 release-notes-4.12.1.rst
27 release-notes-4.12.0.rst
28 release-notes-4.12.0.rst
28 release-notes-4.11.6.rst
29 release-notes-4.11.6.rst
29 release-notes-4.11.5.rst
30 release-notes-4.11.5.rst
30 release-notes-4.11.4.rst
31 release-notes-4.11.4.rst
31 release-notes-4.11.3.rst
32 release-notes-4.11.3.rst
32 release-notes-4.11.2.rst
33 release-notes-4.11.2.rst
33 release-notes-4.11.1.rst
34 release-notes-4.11.1.rst
34 release-notes-4.11.0.rst
35 release-notes-4.11.0.rst
35 release-notes-4.10.6.rst
36 release-notes-4.10.6.rst
36 release-notes-4.10.5.rst
37 release-notes-4.10.5.rst
37 release-notes-4.10.4.rst
38 release-notes-4.10.4.rst
38 release-notes-4.10.3.rst
39 release-notes-4.10.3.rst
39 release-notes-4.10.2.rst
40 release-notes-4.10.2.rst
40 release-notes-4.10.1.rst
41 release-notes-4.10.1.rst
41 release-notes-4.10.0.rst
42 release-notes-4.10.0.rst
42 release-notes-4.9.1.rst
43 release-notes-4.9.1.rst
43 release-notes-4.9.0.rst
44 release-notes-4.9.0.rst
44 release-notes-4.8.0.rst
45 release-notes-4.8.0.rst
45 release-notes-4.7.2.rst
46 release-notes-4.7.2.rst
46 release-notes-4.7.1.rst
47 release-notes-4.7.1.rst
47 release-notes-4.7.0.rst
48 release-notes-4.7.0.rst
48 release-notes-4.6.1.rst
49 release-notes-4.6.1.rst
49 release-notes-4.6.0.rst
50 release-notes-4.6.0.rst
50 release-notes-4.5.2.rst
51 release-notes-4.5.2.rst
51 release-notes-4.5.1.rst
52 release-notes-4.5.1.rst
52 release-notes-4.5.0.rst
53 release-notes-4.5.0.rst
53 release-notes-4.4.2.rst
54 release-notes-4.4.2.rst
54 release-notes-4.4.1.rst
55 release-notes-4.4.1.rst
55 release-notes-4.4.0.rst
56 release-notes-4.4.0.rst
56 release-notes-4.3.1.rst
57 release-notes-4.3.1.rst
57 release-notes-4.3.0.rst
58 release-notes-4.3.0.rst
58 release-notes-4.2.1.rst
59 release-notes-4.2.1.rst
59 release-notes-4.2.0.rst
60 release-notes-4.2.0.rst
60 release-notes-4.1.2.rst
61 release-notes-4.1.2.rst
61 release-notes-4.1.1.rst
62 release-notes-4.1.1.rst
62 release-notes-4.1.0.rst
63 release-notes-4.1.0.rst
63 release-notes-4.0.1.rst
64 release-notes-4.0.1.rst
64 release-notes-4.0.0.rst
65 release-notes-4.0.0.rst
65
66
66 |RCE| 3.x Versions
67 |RCE| 3.x Versions
67 ------------------
68 ------------------
68
69
69 .. toctree::
70 .. toctree::
70 :maxdepth: 1
71 :maxdepth: 1
71
72
72 release-notes-3.8.4.rst
73 release-notes-3.8.4.rst
73 release-notes-3.8.3.rst
74 release-notes-3.8.3.rst
74 release-notes-3.8.2.rst
75 release-notes-3.8.2.rst
75 release-notes-3.8.1.rst
76 release-notes-3.8.1.rst
76 release-notes-3.8.0.rst
77 release-notes-3.8.0.rst
77 release-notes-3.7.1.rst
78 release-notes-3.7.1.rst
78 release-notes-3.7.0.rst
79 release-notes-3.7.0.rst
79 release-notes-3.6.1.rst
80 release-notes-3.6.1.rst
80 release-notes-3.6.0.rst
81 release-notes-3.6.0.rst
81 release-notes-3.5.2.rst
82 release-notes-3.5.2.rst
82 release-notes-3.5.1.rst
83 release-notes-3.5.1.rst
83 release-notes-3.5.0.rst
84 release-notes-3.5.0.rst
84 release-notes-3.4.1.rst
85 release-notes-3.4.1.rst
85 release-notes-3.4.0.rst
86 release-notes-3.4.0.rst
86 release-notes-3.3.4.rst
87 release-notes-3.3.4.rst
87 release-notes-3.3.3.rst
88 release-notes-3.3.3.rst
88 release-notes-3.3.2.rst
89 release-notes-3.3.2.rst
89 release-notes-3.3.1.rst
90 release-notes-3.3.1.rst
90 release-notes-3.3.0.rst
91 release-notes-3.3.0.rst
91 release-notes-3.2.3.rst
92 release-notes-3.2.3.rst
92 release-notes-3.2.2.rst
93 release-notes-3.2.2.rst
93 release-notes-3.2.1.rst
94 release-notes-3.2.1.rst
94 release-notes-3.2.0.rst
95 release-notes-3.2.0.rst
95 release-notes-3.1.1.rst
96 release-notes-3.1.1.rst
96 release-notes-3.1.0.rst
97 release-notes-3.1.0.rst
97 release-notes-3.0.2.rst
98 release-notes-3.0.2.rst
98 release-notes-3.0.1.rst
99 release-notes-3.0.1.rst
99 release-notes-3.0.0.rst
100 release-notes-3.0.0.rst
100
101
101 |RCE| 2.x Versions
102 |RCE| 2.x Versions
102 ------------------
103 ------------------
103
104
104 .. toctree::
105 .. toctree::
105 :maxdepth: 1
106 :maxdepth: 1
106
107
107 release-notes-2.2.8.rst
108 release-notes-2.2.8.rst
108 release-notes-2.2.7.rst
109 release-notes-2.2.7.rst
109 release-notes-2.2.6.rst
110 release-notes-2.2.6.rst
110 release-notes-2.2.5.rst
111 release-notes-2.2.5.rst
111 release-notes-2.2.4.rst
112 release-notes-2.2.4.rst
112 release-notes-2.2.3.rst
113 release-notes-2.2.3.rst
113 release-notes-2.2.2.rst
114 release-notes-2.2.2.rst
114 release-notes-2.2.1.rst
115 release-notes-2.2.1.rst
115 release-notes-2.2.0.rst
116 release-notes-2.2.0.rst
116 release-notes-2.1.0.rst
117 release-notes-2.1.0.rst
117 release-notes-2.0.2.rst
118 release-notes-2.0.2.rst
118 release-notes-2.0.1.rst
119 release-notes-2.0.1.rst
119 release-notes-2.0.0.rst
120 release-notes-2.0.0.rst
120
121
121 |RCE| 1.x Versions
122 |RCE| 1.x Versions
122 ------------------
123 ------------------
123
124
124 .. toctree::
125 .. toctree::
125 :maxdepth: 1
126 :maxdepth: 1
126
127
127 release-notes-1.7.2.rst
128 release-notes-1.7.2.rst
128 release-notes-1.7.1.rst
129 release-notes-1.7.1.rst
129 release-notes-1.7.0.rst
130 release-notes-1.7.0.rst
130 release-notes-1.6.0.rst
131 release-notes-1.6.0.rst
@@ -1,316 +1,310 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 import logging
21 import logging
22
22
23 from pyramid.view import view_config
23 from pyramid.view import view_config
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25
25
26 from rhodecode import events
26 from rhodecode import events
27 from rhodecode.apps._base import RepoAppView
27 from rhodecode.apps._base import RepoAppView
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib import audit_logger
29 from rhodecode.lib import audit_logger
30 from rhodecode.lib.auth import (
30 from rhodecode.lib.auth import (
31 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired,
31 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired,
32 HasRepoPermissionAny)
32 HasRepoPermissionAny)
33 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
33 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
34 from rhodecode.lib.utils2 import safe_int
34 from rhodecode.lib.utils2 import safe_int
35 from rhodecode.lib.vcs import RepositoryError
35 from rhodecode.lib.vcs import RepositoryError
36 from rhodecode.model.db import Session, UserFollowing, User, Repository
36 from rhodecode.model.db import Session, UserFollowing, User, Repository
37 from rhodecode.model.repo import RepoModel
37 from rhodecode.model.repo import RepoModel
38 from rhodecode.model.scm import ScmModel
38 from rhodecode.model.scm import ScmModel
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 class RepoSettingsView(RepoAppView):
43 class RepoSettingsView(RepoAppView):
44
44
45 def load_default_context(self):
45 def load_default_context(self):
46 c = self._get_local_tmpl_context()
46 c = self._get_local_tmpl_context()
47 return c
47 return c
48
48
49 def _get_users_with_permissions(self):
49 def _get_users_with_permissions(self):
50 user_permissions = {}
50 user_permissions = {}
51 for perm in self.db_repo.permissions():
51 for perm in self.db_repo.permissions():
52 user_permissions[perm.user_id] = perm
52 user_permissions[perm.user_id] = perm
53
53
54 return user_permissions
54 return user_permissions
55
55
56 @LoginRequired()
56 @LoginRequired()
57 @HasRepoPermissionAnyDecorator('repository.admin')
57 @HasRepoPermissionAnyDecorator('repository.admin')
58 @view_config(
58 @view_config(
59 route_name='edit_repo_advanced', request_method='GET',
59 route_name='edit_repo_advanced', request_method='GET',
60 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
60 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
61 def edit_advanced(self):
61 def edit_advanced(self):
62 c = self.load_default_context()
62 c = self.load_default_context()
63 c.active = 'advanced'
63 c.active = 'advanced'
64
64
65 c.default_user_id = User.get_default_user().user_id
65 c.default_user_id = User.get_default_user().user_id
66 c.in_public_journal = UserFollowing.query() \
66 c.in_public_journal = UserFollowing.query() \
67 .filter(UserFollowing.user_id == c.default_user_id) \
67 .filter(UserFollowing.user_id == c.default_user_id) \
68 .filter(UserFollowing.follows_repository == self.db_repo).scalar()
68 .filter(UserFollowing.follows_repository == self.db_repo).scalar()
69
69
70 c.has_origin_repo_read_perm = False
71 if self.db_repo.fork:
72 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
73 'repository.write', 'repository.read', 'repository.admin')(
74 self.db_repo.fork.repo_name, 'repo set as fork page')
75
76 c.ver_info_dict = self.rhodecode_vcs_repo.get_hooks_info()
70 c.ver_info_dict = self.rhodecode_vcs_repo.get_hooks_info()
77
71
78 return self._get_template_context(c)
72 return self._get_template_context(c)
79
73
80 @LoginRequired()
74 @LoginRequired()
81 @HasRepoPermissionAnyDecorator('repository.admin')
75 @HasRepoPermissionAnyDecorator('repository.admin')
82 @CSRFRequired()
76 @CSRFRequired()
83 @view_config(
77 @view_config(
84 route_name='edit_repo_advanced_archive', request_method='POST',
78 route_name='edit_repo_advanced_archive', request_method='POST',
85 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
79 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
86 def edit_advanced_archive(self):
80 def edit_advanced_archive(self):
87 """
81 """
88 Archives the repository. It will become read-only, and not visible in search
82 Archives the repository. It will become read-only, and not visible in search
89 or other queries. But still visible for super-admins.
83 or other queries. But still visible for super-admins.
90 """
84 """
91
85
92 _ = self.request.translate
86 _ = self.request.translate
93
87
94 try:
88 try:
95 old_data = self.db_repo.get_api_data()
89 old_data = self.db_repo.get_api_data()
96 RepoModel().archive(self.db_repo)
90 RepoModel().archive(self.db_repo)
97
91
98 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
92 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
99 audit_logger.store_web(
93 audit_logger.store_web(
100 'repo.archive', action_data={'old_data': old_data},
94 'repo.archive', action_data={'old_data': old_data},
101 user=self._rhodecode_user, repo=repo)
95 user=self._rhodecode_user, repo=repo)
102
96
103 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
97 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
104 h.flash(
98 h.flash(
105 _('Archived repository `%s`') % self.db_repo_name,
99 _('Archived repository `%s`') % self.db_repo_name,
106 category='success')
100 category='success')
107 Session().commit()
101 Session().commit()
108 except Exception:
102 except Exception:
109 log.exception("Exception during archiving of repository")
103 log.exception("Exception during archiving of repository")
110 h.flash(_('An error occurred during archiving of `%s`')
104 h.flash(_('An error occurred during archiving of `%s`')
111 % self.db_repo_name, category='error')
105 % self.db_repo_name, category='error')
112 # redirect to advanced for more deletion options
106 # redirect to advanced for more deletion options
113 raise HTTPFound(
107 raise HTTPFound(
114 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
108 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
115 _anchor='advanced-archive'))
109 _anchor='advanced-archive'))
116
110
117 # flush permissions for all users defined in permissions
111 # flush permissions for all users defined in permissions
118 affected_user_ids = self._get_users_with_permissions().keys()
112 affected_user_ids = self._get_users_with_permissions().keys()
119 events.trigger(events.UserPermissionsChange(affected_user_ids))
113 events.trigger(events.UserPermissionsChange(affected_user_ids))
120
114
121 raise HTTPFound(h.route_path('home'))
115 raise HTTPFound(h.route_path('home'))
122
116
123 @LoginRequired()
117 @LoginRequired()
124 @HasRepoPermissionAnyDecorator('repository.admin')
118 @HasRepoPermissionAnyDecorator('repository.admin')
125 @CSRFRequired()
119 @CSRFRequired()
126 @view_config(
120 @view_config(
127 route_name='edit_repo_advanced_delete', request_method='POST',
121 route_name='edit_repo_advanced_delete', request_method='POST',
128 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
122 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
129 def edit_advanced_delete(self):
123 def edit_advanced_delete(self):
130 """
124 """
131 Deletes the repository, or shows warnings if deletion is not possible
125 Deletes the repository, or shows warnings if deletion is not possible
132 because of attached forks or other errors.
126 because of attached forks or other errors.
133 """
127 """
134 _ = self.request.translate
128 _ = self.request.translate
135 handle_forks = self.request.POST.get('forks', None)
129 handle_forks = self.request.POST.get('forks', None)
136 if handle_forks == 'detach_forks':
130 if handle_forks == 'detach_forks':
137 handle_forks = 'detach'
131 handle_forks = 'detach'
138 elif handle_forks == 'delete_forks':
132 elif handle_forks == 'delete_forks':
139 handle_forks = 'delete'
133 handle_forks = 'delete'
140
134
141 try:
135 try:
142 old_data = self.db_repo.get_api_data()
136 old_data = self.db_repo.get_api_data()
143 RepoModel().delete(self.db_repo, forks=handle_forks)
137 RepoModel().delete(self.db_repo, forks=handle_forks)
144
138
145 _forks = self.db_repo.forks.count()
139 _forks = self.db_repo.forks.count()
146 if _forks and handle_forks:
140 if _forks and handle_forks:
147 if handle_forks == 'detach_forks':
141 if handle_forks == 'detach_forks':
148 h.flash(_('Detached %s forks') % _forks, category='success')
142 h.flash(_('Detached %s forks') % _forks, category='success')
149 elif handle_forks == 'delete_forks':
143 elif handle_forks == 'delete_forks':
150 h.flash(_('Deleted %s forks') % _forks, category='success')
144 h.flash(_('Deleted %s forks') % _forks, category='success')
151
145
152 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
146 repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name)
153 audit_logger.store_web(
147 audit_logger.store_web(
154 'repo.delete', action_data={'old_data': old_data},
148 'repo.delete', action_data={'old_data': old_data},
155 user=self._rhodecode_user, repo=repo)
149 user=self._rhodecode_user, repo=repo)
156
150
157 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
151 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
158 h.flash(
152 h.flash(
159 _('Deleted repository `%s`') % self.db_repo_name,
153 _('Deleted repository `%s`') % self.db_repo_name,
160 category='success')
154 category='success')
161 Session().commit()
155 Session().commit()
162 except AttachedForksError:
156 except AttachedForksError:
163 repo_advanced_url = h.route_path(
157 repo_advanced_url = h.route_path(
164 'edit_repo_advanced', repo_name=self.db_repo_name,
158 'edit_repo_advanced', repo_name=self.db_repo_name,
165 _anchor='advanced-delete')
159 _anchor='advanced-delete')
166 delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url)
160 delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url)
167 h.flash(_('Cannot delete `{repo}` it still contains attached forks. '
161 h.flash(_('Cannot delete `{repo}` it still contains attached forks. '
168 'Try using {delete_or_detach} option.')
162 'Try using {delete_or_detach} option.')
169 .format(repo=self.db_repo_name, delete_or_detach=delete_anchor),
163 .format(repo=self.db_repo_name, delete_or_detach=delete_anchor),
170 category='warning')
164 category='warning')
171
165
172 # redirect to advanced for forks handle action ?
166 # redirect to advanced for forks handle action ?
173 raise HTTPFound(repo_advanced_url)
167 raise HTTPFound(repo_advanced_url)
174
168
175 except AttachedPullRequestsError:
169 except AttachedPullRequestsError:
176 repo_advanced_url = h.route_path(
170 repo_advanced_url = h.route_path(
177 'edit_repo_advanced', repo_name=self.db_repo_name,
171 'edit_repo_advanced', repo_name=self.db_repo_name,
178 _anchor='advanced-delete')
172 _anchor='advanced-delete')
179 attached_prs = len(self.db_repo.pull_requests_source +
173 attached_prs = len(self.db_repo.pull_requests_source +
180 self.db_repo.pull_requests_target)
174 self.db_repo.pull_requests_target)
181 h.flash(
175 h.flash(
182 _('Cannot delete `{repo}` it still contains {num} attached pull requests. '
176 _('Cannot delete `{repo}` it still contains {num} attached pull requests. '
183 'Consider archiving the repository instead.').format(
177 'Consider archiving the repository instead.').format(
184 repo=self.db_repo_name, num=attached_prs), category='warning')
178 repo=self.db_repo_name, num=attached_prs), category='warning')
185
179
186 # redirect to advanced for forks handle action ?
180 # redirect to advanced for forks handle action ?
187 raise HTTPFound(repo_advanced_url)
181 raise HTTPFound(repo_advanced_url)
188
182
189 except Exception:
183 except Exception:
190 log.exception("Exception during deletion of repository")
184 log.exception("Exception during deletion of repository")
191 h.flash(_('An error occurred during deletion of `%s`')
185 h.flash(_('An error occurred during deletion of `%s`')
192 % self.db_repo_name, category='error')
186 % self.db_repo_name, category='error')
193 # redirect to advanced for more deletion options
187 # redirect to advanced for more deletion options
194 raise HTTPFound(
188 raise HTTPFound(
195 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
189 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name,
196 _anchor='advanced-delete'))
190 _anchor='advanced-delete'))
197
191
198 raise HTTPFound(h.route_path('home'))
192 raise HTTPFound(h.route_path('home'))
199
193
200 @LoginRequired()
194 @LoginRequired()
201 @HasRepoPermissionAnyDecorator('repository.admin')
195 @HasRepoPermissionAnyDecorator('repository.admin')
202 @CSRFRequired()
196 @CSRFRequired()
203 @view_config(
197 @view_config(
204 route_name='edit_repo_advanced_journal', request_method='POST',
198 route_name='edit_repo_advanced_journal', request_method='POST',
205 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
199 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
206 def edit_advanced_journal(self):
200 def edit_advanced_journal(self):
207 """
201 """
208 Set's this repository to be visible in public journal,
202 Set's this repository to be visible in public journal,
209 in other words making default user to follow this repo
203 in other words making default user to follow this repo
210 """
204 """
211 _ = self.request.translate
205 _ = self.request.translate
212
206
213 try:
207 try:
214 user_id = User.get_default_user().user_id
208 user_id = User.get_default_user().user_id
215 ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id)
209 ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id)
216 h.flash(_('Updated repository visibility in public journal'),
210 h.flash(_('Updated repository visibility in public journal'),
217 category='success')
211 category='success')
218 Session().commit()
212 Session().commit()
219 except Exception:
213 except Exception:
220 h.flash(_('An error occurred during setting this '
214 h.flash(_('An error occurred during setting this '
221 'repository in public journal'),
215 'repository in public journal'),
222 category='error')
216 category='error')
223
217
224 raise HTTPFound(
218 raise HTTPFound(
225 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
219 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
226
220
227 @LoginRequired()
221 @LoginRequired()
228 @HasRepoPermissionAnyDecorator('repository.admin')
222 @HasRepoPermissionAnyDecorator('repository.admin')
229 @CSRFRequired()
223 @CSRFRequired()
230 @view_config(
224 @view_config(
231 route_name='edit_repo_advanced_fork', request_method='POST',
225 route_name='edit_repo_advanced_fork', request_method='POST',
232 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
226 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
233 def edit_advanced_fork(self):
227 def edit_advanced_fork(self):
234 """
228 """
235 Mark given repository as a fork of another
229 Mark given repository as a fork of another
236 """
230 """
237 _ = self.request.translate
231 _ = self.request.translate
238
232
239 new_fork_id = safe_int(self.request.POST.get('id_fork_of'))
233 new_fork_id = safe_int(self.request.POST.get('id_fork_of'))
240
234
241 # valid repo, re-check permissions
235 # valid repo, re-check permissions
242 if new_fork_id:
236 if new_fork_id:
243 repo = Repository.get(new_fork_id)
237 repo = Repository.get(new_fork_id)
244 # ensure we have at least read access to the repo we mark
238 # ensure we have at least read access to the repo we mark
245 perm_check = HasRepoPermissionAny(
239 perm_check = HasRepoPermissionAny(
246 'repository.read', 'repository.write', 'repository.admin')
240 'repository.read', 'repository.write', 'repository.admin')
247
241
248 if repo and perm_check(repo_name=repo.repo_name):
242 if repo and perm_check(repo_name=repo.repo_name):
249 new_fork_id = repo.repo_id
243 new_fork_id = repo.repo_id
250 else:
244 else:
251 new_fork_id = None
245 new_fork_id = None
252
246
253 try:
247 try:
254 repo = ScmModel().mark_as_fork(
248 repo = ScmModel().mark_as_fork(
255 self.db_repo_name, new_fork_id, self._rhodecode_user.user_id)
249 self.db_repo_name, new_fork_id, self._rhodecode_user.user_id)
256 fork = repo.fork.repo_name if repo.fork else _('Nothing')
250 fork = repo.fork.repo_name if repo.fork else _('Nothing')
257 Session().commit()
251 Session().commit()
258 h.flash(
252 h.flash(
259 _('Marked repo %s as fork of %s') % (self.db_repo_name, fork),
253 _('Marked repo %s as fork of %s') % (self.db_repo_name, fork),
260 category='success')
254 category='success')
261 except RepositoryError as e:
255 except RepositoryError as e:
262 log.exception("Repository Error occurred")
256 log.exception("Repository Error occurred")
263 h.flash(str(e), category='error')
257 h.flash(str(e), category='error')
264 except Exception:
258 except Exception:
265 log.exception("Exception while editing fork")
259 log.exception("Exception while editing fork")
266 h.flash(_('An error occurred during this operation'),
260 h.flash(_('An error occurred during this operation'),
267 category='error')
261 category='error')
268
262
269 raise HTTPFound(
263 raise HTTPFound(
270 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
264 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
271
265
272 @LoginRequired()
266 @LoginRequired()
273 @HasRepoPermissionAnyDecorator('repository.admin')
267 @HasRepoPermissionAnyDecorator('repository.admin')
274 @CSRFRequired()
268 @CSRFRequired()
275 @view_config(
269 @view_config(
276 route_name='edit_repo_advanced_locking', request_method='POST',
270 route_name='edit_repo_advanced_locking', request_method='POST',
277 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
271 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
278 def edit_advanced_locking(self):
272 def edit_advanced_locking(self):
279 """
273 """
280 Toggle locking of repository
274 Toggle locking of repository
281 """
275 """
282 _ = self.request.translate
276 _ = self.request.translate
283 set_lock = self.request.POST.get('set_lock')
277 set_lock = self.request.POST.get('set_lock')
284 set_unlock = self.request.POST.get('set_unlock')
278 set_unlock = self.request.POST.get('set_unlock')
285
279
286 try:
280 try:
287 if set_lock:
281 if set_lock:
288 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
282 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
289 lock_reason=Repository.LOCK_WEB)
283 lock_reason=Repository.LOCK_WEB)
290 h.flash(_('Locked repository'), category='success')
284 h.flash(_('Locked repository'), category='success')
291 elif set_unlock:
285 elif set_unlock:
292 Repository.unlock(self.db_repo)
286 Repository.unlock(self.db_repo)
293 h.flash(_('Unlocked repository'), category='success')
287 h.flash(_('Unlocked repository'), category='success')
294 except Exception as e:
288 except Exception as e:
295 log.exception("Exception during unlocking")
289 log.exception("Exception during unlocking")
296 h.flash(_('An error occurred during unlocking'), category='error')
290 h.flash(_('An error occurred during unlocking'), category='error')
297
291
298 raise HTTPFound(
292 raise HTTPFound(
299 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
293 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
300
294
301 @LoginRequired()
295 @LoginRequired()
302 @HasRepoPermissionAnyDecorator('repository.admin')
296 @HasRepoPermissionAnyDecorator('repository.admin')
303 @view_config(
297 @view_config(
304 route_name='edit_repo_advanced_hooks', request_method='GET',
298 route_name='edit_repo_advanced_hooks', request_method='GET',
305 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
299 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
306 def edit_advanced_install_hooks(self):
300 def edit_advanced_install_hooks(self):
307 """
301 """
308 Install Hooks for repository
302 Install Hooks for repository
309 """
303 """
310 _ = self.request.translate
304 _ = self.request.translate
311 self.load_default_context()
305 self.load_default_context()
312 self.rhodecode_vcs_repo.install_hooks(force=True)
306 self.rhodecode_vcs_repo.install_hooks(force=True)
313 h.flash(_('installed updated hooks into this repository'),
307 h.flash(_('installed updated hooks into this repository'),
314 category='success')
308 category='success')
315 raise HTTPFound(
309 raise HTTPFound(
316 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
310 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
@@ -1,459 +1,463 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
3 # Copyright (C) 2012-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 deform
21 import deform
22 import logging
22 import logging
23 import peppercorn
23 import peppercorn
24 import webhelpers.paginate
24 import webhelpers.paginate
25
25
26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound
26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound
27
27
28 from rhodecode.integrations import integration_type_registry
28 from rhodecode.integrations import integration_type_registry
29 from rhodecode.apps._base import BaseAppView
29 from rhodecode.apps._base import BaseAppView
30 from rhodecode.apps._base.navigation import navigation_list
30 from rhodecode.apps._base.navigation import navigation_list
31 from rhodecode.lib.auth import (
31 from rhodecode.lib.auth import (
32 LoginRequired, CSRFRequired, HasPermissionAnyDecorator,
32 LoginRequired, CSRFRequired, HasPermissionAnyDecorator,
33 HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
33 HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator)
34 from rhodecode.lib.utils2 import safe_int
34 from rhodecode.lib.utils2 import safe_int
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
36 from rhodecode.model.db import Repository, RepoGroup, Session, Integration
36 from rhodecode.model.db import Repository, RepoGroup, Session, Integration
37 from rhodecode.model.scm import ScmModel
37 from rhodecode.model.scm import ScmModel
38 from rhodecode.model.integration import IntegrationModel
38 from rhodecode.model.integration import IntegrationModel
39 from rhodecode.model.validation_schema.schemas.integration_schema import (
39 from rhodecode.model.validation_schema.schemas.integration_schema import (
40 make_integration_schema, IntegrationScopeType)
40 make_integration_schema, IntegrationScopeType)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 class IntegrationSettingsViewBase(BaseAppView):
45 class IntegrationSettingsViewBase(BaseAppView):
46 """
46 """
47 Base Integration settings view used by both repo / global settings
47 Base Integration settings view used by both repo / global settings
48 """
48 """
49
49
50 def __init__(self, context, request):
50 def __init__(self, context, request):
51 super(IntegrationSettingsViewBase, self).__init__(context, request)
51 super(IntegrationSettingsViewBase, self).__init__(context, request)
52 self._load_view_context()
52 self._load_view_context()
53
53
54 def _load_view_context(self):
54 def _load_view_context(self):
55 """
55 """
56 This avoids boilerplate for repo/global+list/edit+views/templates
56 This avoids boilerplate for repo/global+list/edit+views/templates
57 by doing all possible contexts at the same time however it should
57 by doing all possible contexts at the same time however it should
58 be split up into separate functions once more "contexts" exist
58 be split up into separate functions once more "contexts" exist
59 """
59 """
60
60
61 self.IntegrationType = None
61 self.IntegrationType = None
62 self.repo = None
62 self.repo = None
63 self.repo_group = None
63 self.repo_group = None
64 self.integration = None
64 self.integration = None
65 self.integrations = {}
65 self.integrations = {}
66
66
67 request = self.request
67 request = self.request
68
68
69 if 'repo_name' in request.matchdict: # in repo settings context
69 if 'repo_name' in request.matchdict: # in repo settings context
70 repo_name = request.matchdict['repo_name']
70 repo_name = request.matchdict['repo_name']
71 self.repo = Repository.get_by_repo_name(repo_name)
71 self.repo = Repository.get_by_repo_name(repo_name)
72
72
73 if 'repo_group_name' in request.matchdict: # in group settings context
73 if 'repo_group_name' in request.matchdict: # in group settings context
74 repo_group_name = request.matchdict['repo_group_name']
74 repo_group_name = request.matchdict['repo_group_name']
75 self.repo_group = RepoGroup.get_by_group_name(repo_group_name)
75 self.repo_group = RepoGroup.get_by_group_name(repo_group_name)
76
76
77 if 'integration' in request.matchdict: # integration type context
77 if 'integration' in request.matchdict: # integration type context
78 integration_type = request.matchdict['integration']
78 integration_type = request.matchdict['integration']
79 if integration_type not in integration_type_registry:
79 if integration_type not in integration_type_registry:
80 raise HTTPNotFound()
80 raise HTTPNotFound()
81
81
82 self.IntegrationType = integration_type_registry[integration_type]
82 self.IntegrationType = integration_type_registry[integration_type]
83 if self.IntegrationType.is_dummy:
83 if self.IntegrationType.is_dummy:
84 raise HTTPNotFound()
84 raise HTTPNotFound()
85
85
86 if 'integration_id' in request.matchdict: # single integration context
86 if 'integration_id' in request.matchdict: # single integration context
87 integration_id = request.matchdict['integration_id']
87 integration_id = request.matchdict['integration_id']
88 self.integration = Integration.get(integration_id)
88 self.integration = Integration.get(integration_id)
89
89
90 # extra perms check just in case
90 # extra perms check just in case
91 if not self._has_perms_for_integration(self.integration):
91 if not self._has_perms_for_integration(self.integration):
92 raise HTTPForbidden()
92 raise HTTPForbidden()
93
93
94 self.settings = self.integration and self.integration.settings or {}
94 self.settings = self.integration and self.integration.settings or {}
95 self.admin_view = not (self.repo or self.repo_group)
95 self.admin_view = not (self.repo or self.repo_group)
96
96
97 def _has_perms_for_integration(self, integration):
97 def _has_perms_for_integration(self, integration):
98 perms = self.request.user.permissions
98 perms = self.request.user.permissions
99
99
100 if 'hg.admin' in perms['global']:
100 if 'hg.admin' in perms['global']:
101 return True
101 return True
102
102
103 if integration.repo:
103 if integration.repo:
104 return perms['repositories'].get(
104 return perms['repositories'].get(
105 integration.repo.repo_name) == 'repository.admin'
105 integration.repo.repo_name) == 'repository.admin'
106
106
107 if integration.repo_group:
107 if integration.repo_group:
108 return perms['repositories_groups'].get(
108 return perms['repositories_groups'].get(
109 integration.repo_group.group_name) == 'group.admin'
109 integration.repo_group.group_name) == 'group.admin'
110
110
111 return False
111 return False
112
112
113 def _get_local_tmpl_context(self, include_app_defaults=True):
113 def _get_local_tmpl_context(self, include_app_defaults=True):
114 _ = self.request.translate
114 _ = self.request.translate
115 c = super(IntegrationSettingsViewBase, self)._get_local_tmpl_context(
115 c = super(IntegrationSettingsViewBase, self)._get_local_tmpl_context(
116 include_app_defaults=include_app_defaults)
116 include_app_defaults=include_app_defaults)
117
118 c.active = 'integrations'
117 c.active = 'integrations'
119
118
120 return c
119 return c
121
120
122 def _form_schema(self):
121 def _form_schema(self):
123 schema = make_integration_schema(IntegrationType=self.IntegrationType,
122 schema = make_integration_schema(IntegrationType=self.IntegrationType,
124 settings=self.settings)
123 settings=self.settings)
125
124
126 # returns a clone, important if mutating the schema later
125 # returns a clone, important if mutating the schema later
127 return schema.bind(
126 return schema.bind(
128 permissions=self.request.user.permissions,
127 permissions=self.request.user.permissions,
129 no_scope=not self.admin_view)
128 no_scope=not self.admin_view)
130
129
131 def _form_defaults(self):
130 def _form_defaults(self):
132 _ = self.request.translate
131 _ = self.request.translate
133 defaults = {}
132 defaults = {}
134
133
135 if self.integration:
134 if self.integration:
136 defaults['settings'] = self.integration.settings or {}
135 defaults['settings'] = self.integration.settings or {}
137 defaults['options'] = {
136 defaults['options'] = {
138 'name': self.integration.name,
137 'name': self.integration.name,
139 'enabled': self.integration.enabled,
138 'enabled': self.integration.enabled,
140 'scope': {
139 'scope': {
141 'repo': self.integration.repo,
140 'repo': self.integration.repo,
142 'repo_group': self.integration.repo_group,
141 'repo_group': self.integration.repo_group,
143 'child_repos_only': self.integration.child_repos_only,
142 'child_repos_only': self.integration.child_repos_only,
144 },
143 },
145 }
144 }
146 else:
145 else:
147 if self.repo:
146 if self.repo:
148 scope = _('{repo_name} repository').format(
147 scope = _('{repo_name} repository').format(
149 repo_name=self.repo.repo_name)
148 repo_name=self.repo.repo_name)
150 elif self.repo_group:
149 elif self.repo_group:
151 scope = _('{repo_group_name} repo group').format(
150 scope = _('{repo_group_name} repo group').format(
152 repo_group_name=self.repo_group.group_name)
151 repo_group_name=self.repo_group.group_name)
153 else:
152 else:
154 scope = _('Global')
153 scope = _('Global')
155
154
156 defaults['options'] = {
155 defaults['options'] = {
157 'enabled': True,
156 'enabled': True,
158 'name': _('{name} integration').format(
157 'name': _('{name} integration').format(
159 name=self.IntegrationType.display_name),
158 name=self.IntegrationType.display_name),
160 }
159 }
161 defaults['options']['scope'] = {
160 defaults['options']['scope'] = {
162 'repo': self.repo,
161 'repo': self.repo,
163 'repo_group': self.repo_group,
162 'repo_group': self.repo_group,
164 }
163 }
165
164
166 return defaults
165 return defaults
167
166
168 def _delete_integration(self, integration):
167 def _delete_integration(self, integration):
169 _ = self.request.translate
168 _ = self.request.translate
170 Session().delete(integration)
169 Session().delete(integration)
171 Session().commit()
170 Session().commit()
172 h.flash(
171 h.flash(
173 _('Integration {integration_name} deleted successfully.').format(
172 _('Integration {integration_name} deleted successfully.').format(
174 integration_name=integration.name),
173 integration_name=integration.name),
175 category='success')
174 category='success')
176
175
177 if self.repo:
176 if self.repo:
178 redirect_to = self.request.route_path(
177 redirect_to = self.request.route_path(
179 'repo_integrations_home', repo_name=self.repo.repo_name)
178 'repo_integrations_home', repo_name=self.repo.repo_name)
180 elif self.repo_group:
179 elif self.repo_group:
181 redirect_to = self.request.route_path(
180 redirect_to = self.request.route_path(
182 'repo_group_integrations_home',
181 'repo_group_integrations_home',
183 repo_group_name=self.repo_group.group_name)
182 repo_group_name=self.repo_group.group_name)
184 else:
183 else:
185 redirect_to = self.request.route_path('global_integrations_home')
184 redirect_to = self.request.route_path('global_integrations_home')
186 raise HTTPFound(redirect_to)
185 raise HTTPFound(redirect_to)
187
186
188 def _integration_list(self):
187 def _integration_list(self):
189 """ List integrations """
188 """ List integrations """
190
189
191 c = self.load_default_context()
190 c = self.load_default_context()
192 if self.repo:
191 if self.repo:
193 scope = self.repo
192 scope = self.repo
194 elif self.repo_group:
193 elif self.repo_group:
195 scope = self.repo_group
194 scope = self.repo_group
196 else:
195 else:
197 scope = 'all'
196 scope = 'all'
198
197
199 integrations = []
198 integrations = []
200
199
201 for IntType, integration in IntegrationModel().get_integrations(
200 for IntType, integration in IntegrationModel().get_integrations(
202 scope=scope, IntegrationType=self.IntegrationType):
201 scope=scope, IntegrationType=self.IntegrationType):
203
202
204 # extra permissions check *just in case*
203 # extra permissions check *just in case*
205 if not self._has_perms_for_integration(integration):
204 if not self._has_perms_for_integration(integration):
206 continue
205 continue
207
206
208 integrations.append((IntType, integration))
207 integrations.append((IntType, integration))
209
208
210 sort_arg = self.request.GET.get('sort', 'name:asc')
209 sort_arg = self.request.GET.get('sort', 'name:asc')
211 sort_dir = 'asc'
210 sort_dir = 'asc'
212 if ':' in sort_arg:
211 if ':' in sort_arg:
213 sort_field, sort_dir = sort_arg.split(':')
212 sort_field, sort_dir = sort_arg.split(':')
214 else:
213 else:
215 sort_field = sort_arg, 'asc'
214 sort_field = sort_arg, 'asc'
216
215
217 assert sort_field in ('name', 'integration_type', 'enabled', 'scope')
216 assert sort_field in ('name', 'integration_type', 'enabled', 'scope')
218
217
219 integrations.sort(
218 integrations.sort(
220 key=lambda x: getattr(x[1], sort_field),
219 key=lambda x: getattr(x[1], sort_field),
221 reverse=(sort_dir == 'desc'))
220 reverse=(sort_dir == 'desc'))
222
221
223 page_url = webhelpers.paginate.PageURL(
222 page_url = webhelpers.paginate.PageURL(
224 self.request.path, self.request.GET)
223 self.request.path, self.request.GET)
225 page = safe_int(self.request.GET.get('page', 1), 1)
224 page = safe_int(self.request.GET.get('page', 1), 1)
226
225
227 integrations = h.Page(
226 integrations = h.Page(
228 integrations, page=page, items_per_page=10, url=page_url)
227 integrations, page=page, items_per_page=10, url=page_url)
229
228
230 c.rev_sort_dir = sort_dir != 'desc' and 'desc' or 'asc'
229 c.rev_sort_dir = sort_dir != 'desc' and 'desc' or 'asc'
231
230
232 c.current_IntegrationType = self.IntegrationType
231 c.current_IntegrationType = self.IntegrationType
233 c.integrations_list = integrations
232 c.integrations_list = integrations
234 c.available_integrations = integration_type_registry
233 c.available_integrations = integration_type_registry
235
234
236 return self._get_template_context(c)
235 return self._get_template_context(c)
237
236
238 def _settings_get(self, defaults=None, form=None):
237 def _settings_get(self, defaults=None, form=None):
239 """
238 """
240 View that displays the integration settings as a form.
239 View that displays the integration settings as a form.
241 """
240 """
242 c = self.load_default_context()
241 c = self.load_default_context()
243
242
244 defaults = defaults or self._form_defaults()
243 defaults = defaults or self._form_defaults()
245 schema = self._form_schema()
244 schema = self._form_schema()
246
245
247 if self.integration:
246 if self.integration:
248 buttons = ('submit', 'delete')
247 buttons = ('submit', 'delete')
249 else:
248 else:
250 buttons = ('submit',)
249 buttons = ('submit',)
251
250
252 form = form or deform.Form(schema, appstruct=defaults, buttons=buttons)
251 form = form or deform.Form(schema, appstruct=defaults, buttons=buttons)
253
252
254 c.form = form
253 c.form = form
255 c.current_IntegrationType = self.IntegrationType
254 c.current_IntegrationType = self.IntegrationType
256 c.integration = self.integration
255 c.integration = self.integration
257
256
258 return self._get_template_context(c)
257 return self._get_template_context(c)
259
258
260 def _settings_post(self):
259 def _settings_post(self):
261 """
260 """
262 View that validates and stores the integration settings.
261 View that validates and stores the integration settings.
263 """
262 """
264 _ = self.request.translate
263 _ = self.request.translate
265
264
266 controls = self.request.POST.items()
265 controls = self.request.POST.items()
267 pstruct = peppercorn.parse(controls)
266 pstruct = peppercorn.parse(controls)
268
267
269 if self.integration and pstruct.get('delete'):
268 if self.integration and pstruct.get('delete'):
270 return self._delete_integration(self.integration)
269 return self._delete_integration(self.integration)
271
270
272 schema = self._form_schema()
271 schema = self._form_schema()
273
272
274 skip_settings_validation = False
273 skip_settings_validation = False
275 if self.integration and 'enabled' not in pstruct.get('options', {}):
274 if self.integration and 'enabled' not in pstruct.get('options', {}):
276 skip_settings_validation = True
275 skip_settings_validation = True
277 schema['settings'].validator = None
276 schema['settings'].validator = None
278 for field in schema['settings'].children:
277 for field in schema['settings'].children:
279 field.validator = None
278 field.validator = None
280 field.missing = ''
279 field.missing = ''
281
280
282 if self.integration:
281 if self.integration:
283 buttons = ('submit', 'delete')
282 buttons = ('submit', 'delete')
284 else:
283 else:
285 buttons = ('submit',)
284 buttons = ('submit',)
286
285
287 form = deform.Form(schema, buttons=buttons)
286 form = deform.Form(schema, buttons=buttons)
288
287
289 if not self.admin_view:
288 if not self.admin_view:
290 # scope is read only field in these cases, and has to be added
289 # scope is read only field in these cases, and has to be added
291 options = pstruct.setdefault('options', {})
290 options = pstruct.setdefault('options', {})
292 if 'scope' not in options:
291 if 'scope' not in options:
293 options['scope'] = IntegrationScopeType().serialize(None, {
292 options['scope'] = IntegrationScopeType().serialize(None, {
294 'repo': self.repo,
293 'repo': self.repo,
295 'repo_group': self.repo_group,
294 'repo_group': self.repo_group,
296 })
295 })
297
296
298 try:
297 try:
299 valid_data = form.validate_pstruct(pstruct)
298 valid_data = form.validate_pstruct(pstruct)
300 except deform.ValidationFailure as e:
299 except deform.ValidationFailure as e:
301 h.flash(
300 h.flash(
302 _('Errors exist when saving integration settings. '
301 _('Errors exist when saving integration settings. '
303 'Please check the form inputs.'),
302 'Please check the form inputs.'),
304 category='error')
303 category='error')
305 return self._settings_get(form=e)
304 return self._settings_get(form=e)
306
305
307 if not self.integration:
306 if not self.integration:
308 self.integration = Integration()
307 self.integration = Integration()
309 self.integration.integration_type = self.IntegrationType.key
308 self.integration.integration_type = self.IntegrationType.key
310 Session().add(self.integration)
309 Session().add(self.integration)
311
310
312 scope = valid_data['options']['scope']
311 scope = valid_data['options']['scope']
313
312
314 IntegrationModel().update_integration(self.integration,
313 IntegrationModel().update_integration(self.integration,
315 name=valid_data['options']['name'],
314 name=valid_data['options']['name'],
316 enabled=valid_data['options']['enabled'],
315 enabled=valid_data['options']['enabled'],
317 settings=valid_data['settings'],
316 settings=valid_data['settings'],
318 repo=scope['repo'],
317 repo=scope['repo'],
319 repo_group=scope['repo_group'],
318 repo_group=scope['repo_group'],
320 child_repos_only=scope['child_repos_only'],
319 child_repos_only=scope['child_repos_only'],
321 )
320 )
322
321
323 self.integration.settings = valid_data['settings']
322 self.integration.settings = valid_data['settings']
324 Session().commit()
323 Session().commit()
325 # Display success message and redirect.
324 # Display success message and redirect.
326 h.flash(
325 h.flash(
327 _('Integration {integration_name} updated successfully.').format(
326 _('Integration {integration_name} updated successfully.').format(
328 integration_name=self.IntegrationType.display_name),
327 integration_name=self.IntegrationType.display_name),
329 category='success')
328 category='success')
330
329
331 # if integration scope changes, we must redirect to the right place
330 # if integration scope changes, we must redirect to the right place
332 # keeping in mind if the original view was for /repo/ or /_admin/
331 # keeping in mind if the original view was for /repo/ or /_admin/
333 admin_view = not (self.repo or self.repo_group)
332 admin_view = not (self.repo or self.repo_group)
334
333
335 if self.integration.repo and not admin_view:
334 if self.integration.repo and not admin_view:
336 redirect_to = self.request.route_path(
335 redirect_to = self.request.route_path(
337 'repo_integrations_edit',
336 'repo_integrations_edit',
338 repo_name=self.integration.repo.repo_name,
337 repo_name=self.integration.repo.repo_name,
339 integration=self.integration.integration_type,
338 integration=self.integration.integration_type,
340 integration_id=self.integration.integration_id)
339 integration_id=self.integration.integration_id)
341 elif self.integration.repo_group and not admin_view:
340 elif self.integration.repo_group and not admin_view:
342 redirect_to = self.request.route_path(
341 redirect_to = self.request.route_path(
343 'repo_group_integrations_edit',
342 'repo_group_integrations_edit',
344 repo_group_name=self.integration.repo_group.group_name,
343 repo_group_name=self.integration.repo_group.group_name,
345 integration=self.integration.integration_type,
344 integration=self.integration.integration_type,
346 integration_id=self.integration.integration_id)
345 integration_id=self.integration.integration_id)
347 else:
346 else:
348 redirect_to = self.request.route_path(
347 redirect_to = self.request.route_path(
349 'global_integrations_edit',
348 'global_integrations_edit',
350 integration=self.integration.integration_type,
349 integration=self.integration.integration_type,
351 integration_id=self.integration.integration_id)
350 integration_id=self.integration.integration_id)
352
351
353 return HTTPFound(redirect_to)
352 return HTTPFound(redirect_to)
354
353
355 def _new_integration(self):
354 def _new_integration(self):
356 c = self.load_default_context()
355 c = self.load_default_context()
357 c.available_integrations = integration_type_registry
356 c.available_integrations = integration_type_registry
358 return self._get_template_context(c)
357 return self._get_template_context(c)
359
358
360 def load_default_context(self):
359 def load_default_context(self):
361 raise NotImplementedError()
360 raise NotImplementedError()
362
361
363
362
364 class GlobalIntegrationsView(IntegrationSettingsViewBase):
363 class GlobalIntegrationsView(IntegrationSettingsViewBase):
365 def load_default_context(self):
364 def load_default_context(self):
366 c = self._get_local_tmpl_context()
365 c = self._get_local_tmpl_context()
367 c.repo = self.repo
366 c.repo = self.repo
368 c.repo_group = self.repo_group
367 c.repo_group = self.repo_group
369 c.navlist = navigation_list(self.request)
368 c.navlist = navigation_list(self.request)
370
369
371 return c
370 return c
372
371
373 @LoginRequired()
372 @LoginRequired()
374 @HasPermissionAnyDecorator('hg.admin')
373 @HasPermissionAnyDecorator('hg.admin')
375 def integration_list(self):
374 def integration_list(self):
376 return self._integration_list()
375 return self._integration_list()
377
376
378 @LoginRequired()
377 @LoginRequired()
379 @HasPermissionAnyDecorator('hg.admin')
378 @HasPermissionAnyDecorator('hg.admin')
380 def settings_get(self):
379 def settings_get(self):
381 return self._settings_get()
380 return self._settings_get()
382
381
383 @LoginRequired()
382 @LoginRequired()
384 @HasPermissionAnyDecorator('hg.admin')
383 @HasPermissionAnyDecorator('hg.admin')
385 @CSRFRequired()
384 @CSRFRequired()
386 def settings_post(self):
385 def settings_post(self):
387 return self._settings_post()
386 return self._settings_post()
388
387
389 @LoginRequired()
388 @LoginRequired()
390 @HasPermissionAnyDecorator('hg.admin')
389 @HasPermissionAnyDecorator('hg.admin')
391 def new_integration(self):
390 def new_integration(self):
392 return self._new_integration()
391 return self._new_integration()
393
392
394
393
395 class RepoIntegrationsView(IntegrationSettingsViewBase):
394 class RepoIntegrationsView(IntegrationSettingsViewBase):
396 def load_default_context(self):
395 def load_default_context(self):
397 c = self._get_local_tmpl_context()
396 c = self._get_local_tmpl_context()
398
397
399 c.repo = self.repo
398 c.repo = self.repo
400 c.repo_group = self.repo_group
399 c.repo_group = self.repo_group
401
400
402 self.db_repo = self.repo
401 self.db_repo = self.repo
403 c.rhodecode_db_repo = self.repo
402 c.rhodecode_db_repo = self.repo
404 c.repo_name = self.db_repo.repo_name
403 c.repo_name = self.db_repo.repo_name
405 c.repository_pull_requests = ScmModel().get_pull_requests(self.repo)
404 c.repository_pull_requests = ScmModel().get_pull_requests(self.repo)
406
405
406 c.has_origin_repo_read_perm = False
407 if self.db_repo.fork:
408 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
409 'repository.write', 'repository.read', 'repository.admin')(
410 self.db_repo.fork.repo_name, 'summary fork link')
407 return c
411 return c
408
412
409 @LoginRequired()
413 @LoginRequired()
410 @HasRepoPermissionAnyDecorator('repository.admin')
414 @HasRepoPermissionAnyDecorator('repository.admin')
411 def integration_list(self):
415 def integration_list(self):
412 return self._integration_list()
416 return self._integration_list()
413
417
414 @LoginRequired()
418 @LoginRequired()
415 @HasRepoPermissionAnyDecorator('repository.admin')
419 @HasRepoPermissionAnyDecorator('repository.admin')
416 def settings_get(self):
420 def settings_get(self):
417 return self._settings_get()
421 return self._settings_get()
418
422
419 @LoginRequired()
423 @LoginRequired()
420 @HasRepoPermissionAnyDecorator('repository.admin')
424 @HasRepoPermissionAnyDecorator('repository.admin')
421 @CSRFRequired()
425 @CSRFRequired()
422 def settings_post(self):
426 def settings_post(self):
423 return self._settings_post()
427 return self._settings_post()
424
428
425 @LoginRequired()
429 @LoginRequired()
426 @HasRepoPermissionAnyDecorator('repository.admin')
430 @HasRepoPermissionAnyDecorator('repository.admin')
427 def new_integration(self):
431 def new_integration(self):
428 return self._new_integration()
432 return self._new_integration()
429
433
430
434
431 class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
435 class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
432 def load_default_context(self):
436 def load_default_context(self):
433 c = self._get_local_tmpl_context()
437 c = self._get_local_tmpl_context()
434 c.repo = self.repo
438 c.repo = self.repo
435 c.repo_group = self.repo_group
439 c.repo_group = self.repo_group
436 c.navlist = navigation_list(self.request)
440 c.navlist = navigation_list(self.request)
437
441
438 return c
442 return c
439
443
440 @LoginRequired()
444 @LoginRequired()
441 @HasRepoGroupPermissionAnyDecorator('group.admin')
445 @HasRepoGroupPermissionAnyDecorator('group.admin')
442 def integration_list(self):
446 def integration_list(self):
443 return self._integration_list()
447 return self._integration_list()
444
448
445 @LoginRequired()
449 @LoginRequired()
446 @HasRepoGroupPermissionAnyDecorator('group.admin')
450 @HasRepoGroupPermissionAnyDecorator('group.admin')
447 def settings_get(self):
451 def settings_get(self):
448 return self._settings_get()
452 return self._settings_get()
449
453
450 @LoginRequired()
454 @LoginRequired()
451 @HasRepoGroupPermissionAnyDecorator('group.admin')
455 @HasRepoGroupPermissionAnyDecorator('group.admin')
452 @CSRFRequired()
456 @CSRFRequired()
453 def settings_post(self):
457 def settings_post(self):
454 return self._settings_post()
458 return self._settings_post()
455
459
456 @LoginRequired()
460 @LoginRequired()
457 @HasRepoGroupPermissionAnyDecorator('group.admin')
461 @HasRepoGroupPermissionAnyDecorator('group.admin')
458 def new_integration(self):
462 def new_integration(self):
459 return self._new_integration()
463 return self._new_integration()
@@ -1,1720 +1,1731 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
3 # Copyright (C) 2012-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 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import datetime
29 import datetime
30 import urllib
30 import urllib
31 import collections
31 import collections
32
32
33 from pyramid import compat
33 from pyramid import compat
34 from pyramid.threadlocal import get_current_request
34 from pyramid.threadlocal import get_current_request
35
35
36 from rhodecode import events
36 from rhodecode import events
37 from rhodecode.translation import lazy_ugettext
37 from rhodecode.translation import lazy_ugettext
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
40 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.compat import OrderedDict
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.markup_renderer import (
42 from rhodecode.lib.markup_renderer import (
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 from rhodecode.lib.vcs.backends.base import (
45 from rhodecode.lib.vcs.backends.base import (
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.lib.vcs.exceptions import (
48 from rhodecode.lib.vcs.exceptions import (
49 CommitDoesNotExistError, EmptyRepositoryError)
49 CommitDoesNotExistError, EmptyRepositoryError)
50 from rhodecode.model import BaseModel
50 from rhodecode.model import BaseModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.comment import CommentsModel
53 from rhodecode.model.db import (
53 from rhodecode.model.db import (
54 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
55 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
55 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
56 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
57 from rhodecode.model.notification import NotificationModel, \
57 from rhodecode.model.notification import NotificationModel, \
58 EmailNotificationModel
58 EmailNotificationModel
59 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.settings import VcsSettingsModel
60 from rhodecode.model.settings import VcsSettingsModel
61
61
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 # Data structure to hold the response data when updating commits during a pull
66 # Data structure to hold the response data when updating commits during a pull
67 # request update.
67 # request update.
68 UpdateResponse = collections.namedtuple('UpdateResponse', [
68 UpdateResponse = collections.namedtuple('UpdateResponse', [
69 'executed', 'reason', 'new', 'old', 'changes',
69 'executed', 'reason', 'new', 'old', 'changes',
70 'source_changed', 'target_changed'])
70 'source_changed', 'target_changed'])
71
71
72
72
73 class PullRequestModel(BaseModel):
73 class PullRequestModel(BaseModel):
74
74
75 cls = PullRequest
75 cls = PullRequest
76
76
77 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
77 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
78
78
79 UPDATE_STATUS_MESSAGES = {
79 UPDATE_STATUS_MESSAGES = {
80 UpdateFailureReason.NONE: lazy_ugettext(
80 UpdateFailureReason.NONE: lazy_ugettext(
81 'Pull request update successful.'),
81 'Pull request update successful.'),
82 UpdateFailureReason.UNKNOWN: lazy_ugettext(
82 UpdateFailureReason.UNKNOWN: lazy_ugettext(
83 'Pull request update failed because of an unknown error.'),
83 'Pull request update failed because of an unknown error.'),
84 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
84 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
85 'No update needed because the source and target have not changed.'),
85 'No update needed because the source and target have not changed.'),
86 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
86 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
87 'Pull request cannot be updated because the reference type is '
87 'Pull request cannot be updated because the reference type is '
88 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
88 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
89 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
89 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
90 'This pull request cannot be updated because the target '
90 'This pull request cannot be updated because the target '
91 'reference is missing.'),
91 'reference is missing.'),
92 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
92 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
93 'This pull request cannot be updated because the source '
93 'This pull request cannot be updated because the source '
94 'reference is missing.'),
94 'reference is missing.'),
95 }
95 }
96 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
96 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
97 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
97 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
98
98
99 def __get_pull_request(self, pull_request):
99 def __get_pull_request(self, pull_request):
100 return self._get_instance((
100 return self._get_instance((
101 PullRequest, PullRequestVersion), pull_request)
101 PullRequest, PullRequestVersion), pull_request)
102
102
103 def _check_perms(self, perms, pull_request, user, api=False):
103 def _check_perms(self, perms, pull_request, user, api=False):
104 if not api:
104 if not api:
105 return h.HasRepoPermissionAny(*perms)(
105 return h.HasRepoPermissionAny(*perms)(
106 user=user, repo_name=pull_request.target_repo.repo_name)
106 user=user, repo_name=pull_request.target_repo.repo_name)
107 else:
107 else:
108 return h.HasRepoPermissionAnyApi(*perms)(
108 return h.HasRepoPermissionAnyApi(*perms)(
109 user=user, repo_name=pull_request.target_repo.repo_name)
109 user=user, repo_name=pull_request.target_repo.repo_name)
110
110
111 def check_user_read(self, pull_request, user, api=False):
111 def check_user_read(self, pull_request, user, api=False):
112 _perms = ('repository.admin', 'repository.write', 'repository.read',)
112 _perms = ('repository.admin', 'repository.write', 'repository.read',)
113 return self._check_perms(_perms, pull_request, user, api)
113 return self._check_perms(_perms, pull_request, user, api)
114
114
115 def check_user_merge(self, pull_request, user, api=False):
115 def check_user_merge(self, pull_request, user, api=False):
116 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
116 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
117 return self._check_perms(_perms, pull_request, user, api)
117 return self._check_perms(_perms, pull_request, user, api)
118
118
119 def check_user_update(self, pull_request, user, api=False):
119 def check_user_update(self, pull_request, user, api=False):
120 owner = user.user_id == pull_request.user_id
120 owner = user.user_id == pull_request.user_id
121 return self.check_user_merge(pull_request, user, api) or owner
121 return self.check_user_merge(pull_request, user, api) or owner
122
122
123 def check_user_delete(self, pull_request, user):
123 def check_user_delete(self, pull_request, user):
124 owner = user.user_id == pull_request.user_id
124 owner = user.user_id == pull_request.user_id
125 _perms = ('repository.admin',)
125 _perms = ('repository.admin',)
126 return self._check_perms(_perms, pull_request, user) or owner
126 return self._check_perms(_perms, pull_request, user) or owner
127
127
128 def check_user_change_status(self, pull_request, user, api=False):
128 def check_user_change_status(self, pull_request, user, api=False):
129 reviewer = user.user_id in [x.user_id for x in
129 reviewer = user.user_id in [x.user_id for x in
130 pull_request.reviewers]
130 pull_request.reviewers]
131 return self.check_user_update(pull_request, user, api) or reviewer
131 return self.check_user_update(pull_request, user, api) or reviewer
132
132
133 def check_user_comment(self, pull_request, user):
133 def check_user_comment(self, pull_request, user):
134 owner = user.user_id == pull_request.user_id
134 owner = user.user_id == pull_request.user_id
135 return self.check_user_read(pull_request, user) or owner
135 return self.check_user_read(pull_request, user) or owner
136
136
137 def get(self, pull_request):
137 def get(self, pull_request):
138 return self.__get_pull_request(pull_request)
138 return self.__get_pull_request(pull_request)
139
139
140 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
140 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
141 opened_by=None, order_by=None,
141 opened_by=None, order_by=None,
142 order_dir='desc', only_created=True):
142 order_dir='desc', only_created=True):
143 repo = None
143 repo = None
144 if repo_name:
144 if repo_name:
145 repo = self._get_repo(repo_name)
145 repo = self._get_repo(repo_name)
146
146
147 q = PullRequest.query()
147 q = PullRequest.query()
148
148
149 # source or target
149 # source or target
150 if repo and source:
150 if repo and source:
151 q = q.filter(PullRequest.source_repo == repo)
151 q = q.filter(PullRequest.source_repo == repo)
152 elif repo:
152 elif repo:
153 q = q.filter(PullRequest.target_repo == repo)
153 q = q.filter(PullRequest.target_repo == repo)
154
154
155 # closed,opened
155 # closed,opened
156 if statuses:
156 if statuses:
157 q = q.filter(PullRequest.status.in_(statuses))
157 q = q.filter(PullRequest.status.in_(statuses))
158
158
159 # opened by filter
159 # opened by filter
160 if opened_by:
160 if opened_by:
161 q = q.filter(PullRequest.user_id.in_(opened_by))
161 q = q.filter(PullRequest.user_id.in_(opened_by))
162
162
163 # only get those that are in "created" state
163 # only get those that are in "created" state
164 if only_created:
164 if only_created:
165 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
165 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
166
166
167 if order_by:
167 if order_by:
168 order_map = {
168 order_map = {
169 'name_raw': PullRequest.pull_request_id,
169 'name_raw': PullRequest.pull_request_id,
170 'id': PullRequest.pull_request_id,
170 'id': PullRequest.pull_request_id,
171 'title': PullRequest.title,
171 'title': PullRequest.title,
172 'updated_on_raw': PullRequest.updated_on,
172 'updated_on_raw': PullRequest.updated_on,
173 'target_repo': PullRequest.target_repo_id
173 'target_repo': PullRequest.target_repo_id
174 }
174 }
175 if order_dir == 'asc':
175 if order_dir == 'asc':
176 q = q.order_by(order_map[order_by].asc())
176 q = q.order_by(order_map[order_by].asc())
177 else:
177 else:
178 q = q.order_by(order_map[order_by].desc())
178 q = q.order_by(order_map[order_by].desc())
179
179
180 return q
180 return q
181
181
182 def count_all(self, repo_name, source=False, statuses=None,
182 def count_all(self, repo_name, source=False, statuses=None,
183 opened_by=None):
183 opened_by=None):
184 """
184 """
185 Count the number of pull requests for a specific repository.
185 Count the number of pull requests for a specific repository.
186
186
187 :param repo_name: target or source repo
187 :param repo_name: target or source repo
188 :param source: boolean flag to specify if repo_name refers to source
188 :param source: boolean flag to specify if repo_name refers to source
189 :param statuses: list of pull request statuses
189 :param statuses: list of pull request statuses
190 :param opened_by: author user of the pull request
190 :param opened_by: author user of the pull request
191 :returns: int number of pull requests
191 :returns: int number of pull requests
192 """
192 """
193 q = self._prepare_get_all_query(
193 q = self._prepare_get_all_query(
194 repo_name, source=source, statuses=statuses, opened_by=opened_by)
194 repo_name, source=source, statuses=statuses, opened_by=opened_by)
195
195
196 return q.count()
196 return q.count()
197
197
198 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
198 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
199 offset=0, length=None, order_by=None, order_dir='desc'):
199 offset=0, length=None, order_by=None, order_dir='desc'):
200 """
200 """
201 Get all pull requests for a specific repository.
201 Get all pull requests for a specific repository.
202
202
203 :param repo_name: target or source repo
203 :param repo_name: target or source repo
204 :param source: boolean flag to specify if repo_name refers to source
204 :param source: boolean flag to specify if repo_name refers to source
205 :param statuses: list of pull request statuses
205 :param statuses: list of pull request statuses
206 :param opened_by: author user of the pull request
206 :param opened_by: author user of the pull request
207 :param offset: pagination offset
207 :param offset: pagination offset
208 :param length: length of returned list
208 :param length: length of returned list
209 :param order_by: order of the returned list
209 :param order_by: order of the returned list
210 :param order_dir: 'asc' or 'desc' ordering direction
210 :param order_dir: 'asc' or 'desc' ordering direction
211 :returns: list of pull requests
211 :returns: list of pull requests
212 """
212 """
213 q = self._prepare_get_all_query(
213 q = self._prepare_get_all_query(
214 repo_name, source=source, statuses=statuses, opened_by=opened_by,
214 repo_name, source=source, statuses=statuses, opened_by=opened_by,
215 order_by=order_by, order_dir=order_dir)
215 order_by=order_by, order_dir=order_dir)
216
216
217 if length:
217 if length:
218 pull_requests = q.limit(length).offset(offset).all()
218 pull_requests = q.limit(length).offset(offset).all()
219 else:
219 else:
220 pull_requests = q.all()
220 pull_requests = q.all()
221
221
222 return pull_requests
222 return pull_requests
223
223
224 def count_awaiting_review(self, repo_name, source=False, statuses=None,
224 def count_awaiting_review(self, repo_name, source=False, statuses=None,
225 opened_by=None):
225 opened_by=None):
226 """
226 """
227 Count the number of pull requests for a specific repository that are
227 Count the number of pull requests for a specific repository that are
228 awaiting review.
228 awaiting review.
229
229
230 :param repo_name: target or source repo
230 :param repo_name: target or source repo
231 :param source: boolean flag to specify if repo_name refers to source
231 :param source: boolean flag to specify if repo_name refers to source
232 :param statuses: list of pull request statuses
232 :param statuses: list of pull request statuses
233 :param opened_by: author user of the pull request
233 :param opened_by: author user of the pull request
234 :returns: int number of pull requests
234 :returns: int number of pull requests
235 """
235 """
236 pull_requests = self.get_awaiting_review(
236 pull_requests = self.get_awaiting_review(
237 repo_name, source=source, statuses=statuses, opened_by=opened_by)
237 repo_name, source=source, statuses=statuses, opened_by=opened_by)
238
238
239 return len(pull_requests)
239 return len(pull_requests)
240
240
241 def get_awaiting_review(self, repo_name, source=False, statuses=None,
241 def get_awaiting_review(self, repo_name, source=False, statuses=None,
242 opened_by=None, offset=0, length=None,
242 opened_by=None, offset=0, length=None,
243 order_by=None, order_dir='desc'):
243 order_by=None, order_dir='desc'):
244 """
244 """
245 Get all pull requests for a specific repository that are awaiting
245 Get all pull requests for a specific repository that are awaiting
246 review.
246 review.
247
247
248 :param repo_name: target or source repo
248 :param repo_name: target or source repo
249 :param source: boolean flag to specify if repo_name refers to source
249 :param source: boolean flag to specify if repo_name refers to source
250 :param statuses: list of pull request statuses
250 :param statuses: list of pull request statuses
251 :param opened_by: author user of the pull request
251 :param opened_by: author user of the pull request
252 :param offset: pagination offset
252 :param offset: pagination offset
253 :param length: length of returned list
253 :param length: length of returned list
254 :param order_by: order of the returned list
254 :param order_by: order of the returned list
255 :param order_dir: 'asc' or 'desc' ordering direction
255 :param order_dir: 'asc' or 'desc' ordering direction
256 :returns: list of pull requests
256 :returns: list of pull requests
257 """
257 """
258 pull_requests = self.get_all(
258 pull_requests = self.get_all(
259 repo_name, source=source, statuses=statuses, opened_by=opened_by,
259 repo_name, source=source, statuses=statuses, opened_by=opened_by,
260 order_by=order_by, order_dir=order_dir)
260 order_by=order_by, order_dir=order_dir)
261
261
262 _filtered_pull_requests = []
262 _filtered_pull_requests = []
263 for pr in pull_requests:
263 for pr in pull_requests:
264 status = pr.calculated_review_status()
264 status = pr.calculated_review_status()
265 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
265 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
266 ChangesetStatus.STATUS_UNDER_REVIEW]:
266 ChangesetStatus.STATUS_UNDER_REVIEW]:
267 _filtered_pull_requests.append(pr)
267 _filtered_pull_requests.append(pr)
268 if length:
268 if length:
269 return _filtered_pull_requests[offset:offset+length]
269 return _filtered_pull_requests[offset:offset+length]
270 else:
270 else:
271 return _filtered_pull_requests
271 return _filtered_pull_requests
272
272
273 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
273 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
274 opened_by=None, user_id=None):
274 opened_by=None, user_id=None):
275 """
275 """
276 Count the number of pull requests for a specific repository that are
276 Count the number of pull requests for a specific repository that are
277 awaiting review from a specific user.
277 awaiting review from a specific user.
278
278
279 :param repo_name: target or source repo
279 :param repo_name: target or source repo
280 :param source: boolean flag to specify if repo_name refers to source
280 :param source: boolean flag to specify if repo_name refers to source
281 :param statuses: list of pull request statuses
281 :param statuses: list of pull request statuses
282 :param opened_by: author user of the pull request
282 :param opened_by: author user of the pull request
283 :param user_id: reviewer user of the pull request
283 :param user_id: reviewer user of the pull request
284 :returns: int number of pull requests
284 :returns: int number of pull requests
285 """
285 """
286 pull_requests = self.get_awaiting_my_review(
286 pull_requests = self.get_awaiting_my_review(
287 repo_name, source=source, statuses=statuses, opened_by=opened_by,
287 repo_name, source=source, statuses=statuses, opened_by=opened_by,
288 user_id=user_id)
288 user_id=user_id)
289
289
290 return len(pull_requests)
290 return len(pull_requests)
291
291
292 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
292 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
293 opened_by=None, user_id=None, offset=0,
293 opened_by=None, user_id=None, offset=0,
294 length=None, order_by=None, order_dir='desc'):
294 length=None, order_by=None, order_dir='desc'):
295 """
295 """
296 Get all pull requests for a specific repository that are awaiting
296 Get all pull requests for a specific repository that are awaiting
297 review from a specific user.
297 review from a specific user.
298
298
299 :param repo_name: target or source repo
299 :param repo_name: target or source repo
300 :param source: boolean flag to specify if repo_name refers to source
300 :param source: boolean flag to specify if repo_name refers to source
301 :param statuses: list of pull request statuses
301 :param statuses: list of pull request statuses
302 :param opened_by: author user of the pull request
302 :param opened_by: author user of the pull request
303 :param user_id: reviewer user of the pull request
303 :param user_id: reviewer user of the pull request
304 :param offset: pagination offset
304 :param offset: pagination offset
305 :param length: length of returned list
305 :param length: length of returned list
306 :param order_by: order of the returned list
306 :param order_by: order of the returned list
307 :param order_dir: 'asc' or 'desc' ordering direction
307 :param order_dir: 'asc' or 'desc' ordering direction
308 :returns: list of pull requests
308 :returns: list of pull requests
309 """
309 """
310 pull_requests = self.get_all(
310 pull_requests = self.get_all(
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
311 repo_name, source=source, statuses=statuses, opened_by=opened_by,
312 order_by=order_by, order_dir=order_dir)
312 order_by=order_by, order_dir=order_dir)
313
313
314 _my = PullRequestModel().get_not_reviewed(user_id)
314 _my = PullRequestModel().get_not_reviewed(user_id)
315 my_participation = []
315 my_participation = []
316 for pr in pull_requests:
316 for pr in pull_requests:
317 if pr in _my:
317 if pr in _my:
318 my_participation.append(pr)
318 my_participation.append(pr)
319 _filtered_pull_requests = my_participation
319 _filtered_pull_requests = my_participation
320 if length:
320 if length:
321 return _filtered_pull_requests[offset:offset+length]
321 return _filtered_pull_requests[offset:offset+length]
322 else:
322 else:
323 return _filtered_pull_requests
323 return _filtered_pull_requests
324
324
325 def get_not_reviewed(self, user_id):
325 def get_not_reviewed(self, user_id):
326 return [
326 return [
327 x.pull_request for x in PullRequestReviewers.query().filter(
327 x.pull_request for x in PullRequestReviewers.query().filter(
328 PullRequestReviewers.user_id == user_id).all()
328 PullRequestReviewers.user_id == user_id).all()
329 ]
329 ]
330
330
331 def _prepare_participating_query(self, user_id=None, statuses=None,
331 def _prepare_participating_query(self, user_id=None, statuses=None,
332 order_by=None, order_dir='desc'):
332 order_by=None, order_dir='desc'):
333 q = PullRequest.query()
333 q = PullRequest.query()
334 if user_id:
334 if user_id:
335 reviewers_subquery = Session().query(
335 reviewers_subquery = Session().query(
336 PullRequestReviewers.pull_request_id).filter(
336 PullRequestReviewers.pull_request_id).filter(
337 PullRequestReviewers.user_id == user_id).subquery()
337 PullRequestReviewers.user_id == user_id).subquery()
338 user_filter = or_(
338 user_filter = or_(
339 PullRequest.user_id == user_id,
339 PullRequest.user_id == user_id,
340 PullRequest.pull_request_id.in_(reviewers_subquery)
340 PullRequest.pull_request_id.in_(reviewers_subquery)
341 )
341 )
342 q = PullRequest.query().filter(user_filter)
342 q = PullRequest.query().filter(user_filter)
343
343
344 # closed,opened
344 # closed,opened
345 if statuses:
345 if statuses:
346 q = q.filter(PullRequest.status.in_(statuses))
346 q = q.filter(PullRequest.status.in_(statuses))
347
347
348 if order_by:
348 if order_by:
349 order_map = {
349 order_map = {
350 'name_raw': PullRequest.pull_request_id,
350 'name_raw': PullRequest.pull_request_id,
351 'title': PullRequest.title,
351 'title': PullRequest.title,
352 'updated_on_raw': PullRequest.updated_on,
352 'updated_on_raw': PullRequest.updated_on,
353 'target_repo': PullRequest.target_repo_id
353 'target_repo': PullRequest.target_repo_id
354 }
354 }
355 if order_dir == 'asc':
355 if order_dir == 'asc':
356 q = q.order_by(order_map[order_by].asc())
356 q = q.order_by(order_map[order_by].asc())
357 else:
357 else:
358 q = q.order_by(order_map[order_by].desc())
358 q = q.order_by(order_map[order_by].desc())
359
359
360 return q
360 return q
361
361
362 def count_im_participating_in(self, user_id=None, statuses=None):
362 def count_im_participating_in(self, user_id=None, statuses=None):
363 q = self._prepare_participating_query(user_id, statuses=statuses)
363 q = self._prepare_participating_query(user_id, statuses=statuses)
364 return q.count()
364 return q.count()
365
365
366 def get_im_participating_in(
366 def get_im_participating_in(
367 self, user_id=None, statuses=None, offset=0,
367 self, user_id=None, statuses=None, offset=0,
368 length=None, order_by=None, order_dir='desc'):
368 length=None, order_by=None, order_dir='desc'):
369 """
369 """
370 Get all Pull requests that i'm participating in, or i have opened
370 Get all Pull requests that i'm participating in, or i have opened
371 """
371 """
372
372
373 q = self._prepare_participating_query(
373 q = self._prepare_participating_query(
374 user_id, statuses=statuses, order_by=order_by,
374 user_id, statuses=statuses, order_by=order_by,
375 order_dir=order_dir)
375 order_dir=order_dir)
376
376
377 if length:
377 if length:
378 pull_requests = q.limit(length).offset(offset).all()
378 pull_requests = q.limit(length).offset(offset).all()
379 else:
379 else:
380 pull_requests = q.all()
380 pull_requests = q.all()
381
381
382 return pull_requests
382 return pull_requests
383
383
384 def get_versions(self, pull_request):
384 def get_versions(self, pull_request):
385 """
385 """
386 returns version of pull request sorted by ID descending
386 returns version of pull request sorted by ID descending
387 """
387 """
388 return PullRequestVersion.query()\
388 return PullRequestVersion.query()\
389 .filter(PullRequestVersion.pull_request == pull_request)\
389 .filter(PullRequestVersion.pull_request == pull_request)\
390 .order_by(PullRequestVersion.pull_request_version_id.asc())\
390 .order_by(PullRequestVersion.pull_request_version_id.asc())\
391 .all()
391 .all()
392
392
393 def get_pr_version(self, pull_request_id, version=None):
393 def get_pr_version(self, pull_request_id, version=None):
394 at_version = None
394 at_version = None
395
395
396 if version and version == 'latest':
396 if version and version == 'latest':
397 pull_request_ver = PullRequest.get(pull_request_id)
397 pull_request_ver = PullRequest.get(pull_request_id)
398 pull_request_obj = pull_request_ver
398 pull_request_obj = pull_request_ver
399 _org_pull_request_obj = pull_request_obj
399 _org_pull_request_obj = pull_request_obj
400 at_version = 'latest'
400 at_version = 'latest'
401 elif version:
401 elif version:
402 pull_request_ver = PullRequestVersion.get_or_404(version)
402 pull_request_ver = PullRequestVersion.get_or_404(version)
403 pull_request_obj = pull_request_ver
403 pull_request_obj = pull_request_ver
404 _org_pull_request_obj = pull_request_ver.pull_request
404 _org_pull_request_obj = pull_request_ver.pull_request
405 at_version = pull_request_ver.pull_request_version_id
405 at_version = pull_request_ver.pull_request_version_id
406 else:
406 else:
407 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
407 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
408 pull_request_id)
408 pull_request_id)
409
409
410 pull_request_display_obj = PullRequest.get_pr_display_object(
410 pull_request_display_obj = PullRequest.get_pr_display_object(
411 pull_request_obj, _org_pull_request_obj)
411 pull_request_obj, _org_pull_request_obj)
412
412
413 return _org_pull_request_obj, pull_request_obj, \
413 return _org_pull_request_obj, pull_request_obj, \
414 pull_request_display_obj, at_version
414 pull_request_display_obj, at_version
415
415
416 def create(self, created_by, source_repo, source_ref, target_repo,
416 def create(self, created_by, source_repo, source_ref, target_repo,
417 target_ref, revisions, reviewers, title, description=None,
417 target_ref, revisions, reviewers, title, description=None,
418 description_renderer=None,
418 description_renderer=None,
419 reviewer_data=None, translator=None, auth_user=None):
419 reviewer_data=None, translator=None, auth_user=None):
420 translator = translator or get_current_request().translate
420 translator = translator or get_current_request().translate
421
421
422 created_by_user = self._get_user(created_by)
422 created_by_user = self._get_user(created_by)
423 auth_user = auth_user or created_by_user.AuthUser()
423 auth_user = auth_user or created_by_user.AuthUser()
424 source_repo = self._get_repo(source_repo)
424 source_repo = self._get_repo(source_repo)
425 target_repo = self._get_repo(target_repo)
425 target_repo = self._get_repo(target_repo)
426
426
427 pull_request = PullRequest()
427 pull_request = PullRequest()
428 pull_request.source_repo = source_repo
428 pull_request.source_repo = source_repo
429 pull_request.source_ref = source_ref
429 pull_request.source_ref = source_ref
430 pull_request.target_repo = target_repo
430 pull_request.target_repo = target_repo
431 pull_request.target_ref = target_ref
431 pull_request.target_ref = target_ref
432 pull_request.revisions = revisions
432 pull_request.revisions = revisions
433 pull_request.title = title
433 pull_request.title = title
434 pull_request.description = description
434 pull_request.description = description
435 pull_request.description_renderer = description_renderer
435 pull_request.description_renderer = description_renderer
436 pull_request.author = created_by_user
436 pull_request.author = created_by_user
437 pull_request.reviewer_data = reviewer_data
437 pull_request.reviewer_data = reviewer_data
438 pull_request.pull_request_state = pull_request.STATE_CREATING
438 pull_request.pull_request_state = pull_request.STATE_CREATING
439 Session().add(pull_request)
439 Session().add(pull_request)
440 Session().flush()
440 Session().flush()
441
441
442 reviewer_ids = set()
442 reviewer_ids = set()
443 # members / reviewers
443 # members / reviewers
444 for reviewer_object in reviewers:
444 for reviewer_object in reviewers:
445 user_id, reasons, mandatory, rules = reviewer_object
445 user_id, reasons, mandatory, rules = reviewer_object
446 user = self._get_user(user_id)
446 user = self._get_user(user_id)
447
447
448 # skip duplicates
448 # skip duplicates
449 if user.user_id in reviewer_ids:
449 if user.user_id in reviewer_ids:
450 continue
450 continue
451
451
452 reviewer_ids.add(user.user_id)
452 reviewer_ids.add(user.user_id)
453
453
454 reviewer = PullRequestReviewers()
454 reviewer = PullRequestReviewers()
455 reviewer.user = user
455 reviewer.user = user
456 reviewer.pull_request = pull_request
456 reviewer.pull_request = pull_request
457 reviewer.reasons = reasons
457 reviewer.reasons = reasons
458 reviewer.mandatory = mandatory
458 reviewer.mandatory = mandatory
459
459
460 # NOTE(marcink): pick only first rule for now
460 # NOTE(marcink): pick only first rule for now
461 rule_id = list(rules)[0] if rules else None
461 rule_id = list(rules)[0] if rules else None
462 rule = RepoReviewRule.get(rule_id) if rule_id else None
462 rule = RepoReviewRule.get(rule_id) if rule_id else None
463 if rule:
463 if rule:
464 review_group = rule.user_group_vote_rule(user_id)
464 review_group = rule.user_group_vote_rule(user_id)
465 # we check if this particular reviewer is member of a voting group
465 # we check if this particular reviewer is member of a voting group
466 if review_group:
466 if review_group:
467 # NOTE(marcink):
467 # NOTE(marcink):
468 # can be that user is member of more but we pick the first same,
468 # can be that user is member of more but we pick the first same,
469 # same as default reviewers algo
469 # same as default reviewers algo
470 review_group = review_group[0]
470 review_group = review_group[0]
471
471
472 rule_data = {
472 rule_data = {
473 'rule_name':
473 'rule_name':
474 rule.review_rule_name,
474 rule.review_rule_name,
475 'rule_user_group_entry_id':
475 'rule_user_group_entry_id':
476 review_group.repo_review_rule_users_group_id,
476 review_group.repo_review_rule_users_group_id,
477 'rule_user_group_name':
477 'rule_user_group_name':
478 review_group.users_group.users_group_name,
478 review_group.users_group.users_group_name,
479 'rule_user_group_members':
479 'rule_user_group_members':
480 [x.user.username for x in review_group.users_group.members],
480 [x.user.username for x in review_group.users_group.members],
481 'rule_user_group_members_id':
481 'rule_user_group_members_id':
482 [x.user.user_id for x in review_group.users_group.members],
482 [x.user.user_id for x in review_group.users_group.members],
483 }
483 }
484 # e.g {'vote_rule': -1, 'mandatory': True}
484 # e.g {'vote_rule': -1, 'mandatory': True}
485 rule_data.update(review_group.rule_data())
485 rule_data.update(review_group.rule_data())
486
486
487 reviewer.rule_data = rule_data
487 reviewer.rule_data = rule_data
488
488
489 Session().add(reviewer)
489 Session().add(reviewer)
490 Session().flush()
490 Session().flush()
491
491
492 # Set approval status to "Under Review" for all commits which are
492 # Set approval status to "Under Review" for all commits which are
493 # part of this pull request.
493 # part of this pull request.
494 ChangesetStatusModel().set_status(
494 ChangesetStatusModel().set_status(
495 repo=target_repo,
495 repo=target_repo,
496 status=ChangesetStatus.STATUS_UNDER_REVIEW,
496 status=ChangesetStatus.STATUS_UNDER_REVIEW,
497 user=created_by_user,
497 user=created_by_user,
498 pull_request=pull_request
498 pull_request=pull_request
499 )
499 )
500 # we commit early at this point. This has to do with a fact
500 # we commit early at this point. This has to do with a fact
501 # that before queries do some row-locking. And because of that
501 # that before queries do some row-locking. And because of that
502 # we need to commit and finish transaction before below validate call
502 # we need to commit and finish transaction before below validate call
503 # that for large repos could be long resulting in long row locks
503 # that for large repos could be long resulting in long row locks
504 Session().commit()
504 Session().commit()
505
505
506 # prepare workspace, and run initial merge simulation. Set state during that
506 # prepare workspace, and run initial merge simulation. Set state during that
507 # operation
507 # operation
508 pull_request = PullRequest.get(pull_request.pull_request_id)
508 pull_request = PullRequest.get(pull_request.pull_request_id)
509
509
510 # set as merging, for simulation, and if finished to created so we mark
510 # set as merging, for simulation, and if finished to created so we mark
511 # simulation is working fine
511 # simulation is working fine
512 with pull_request.set_state(PullRequest.STATE_MERGING,
512 with pull_request.set_state(PullRequest.STATE_MERGING,
513 final_state=PullRequest.STATE_CREATED):
513 final_state=PullRequest.STATE_CREATED):
514 MergeCheck.validate(
514 MergeCheck.validate(
515 pull_request, auth_user=auth_user, translator=translator)
515 pull_request, auth_user=auth_user, translator=translator)
516
516
517 self.notify_reviewers(pull_request, reviewer_ids)
517 self.notify_reviewers(pull_request, reviewer_ids)
518 self.trigger_pull_request_hook(
518 self.trigger_pull_request_hook(
519 pull_request, created_by_user, 'create')
519 pull_request, created_by_user, 'create')
520
520
521 creation_data = pull_request.get_api_data(with_merge_state=False)
521 creation_data = pull_request.get_api_data(with_merge_state=False)
522 self._log_audit_action(
522 self._log_audit_action(
523 'repo.pull_request.create', {'data': creation_data},
523 'repo.pull_request.create', {'data': creation_data},
524 auth_user, pull_request)
524 auth_user, pull_request)
525
525
526 return pull_request
526 return pull_request
527
527
528 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
528 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
529 pull_request = self.__get_pull_request(pull_request)
529 pull_request = self.__get_pull_request(pull_request)
530 target_scm = pull_request.target_repo.scm_instance()
530 target_scm = pull_request.target_repo.scm_instance()
531 if action == 'create':
531 if action == 'create':
532 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
532 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
533 elif action == 'merge':
533 elif action == 'merge':
534 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
534 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
535 elif action == 'close':
535 elif action == 'close':
536 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
536 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
537 elif action == 'review_status_change':
537 elif action == 'review_status_change':
538 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
538 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
539 elif action == 'update':
539 elif action == 'update':
540 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
540 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
541 elif action == 'comment':
541 elif action == 'comment':
542 # dummy hook ! for comment. We want this function to handle all cases
542 # dummy hook ! for comment. We want this function to handle all cases
543 def trigger_hook(*args, **kwargs):
543 def trigger_hook(*args, **kwargs):
544 pass
544 pass
545 comment = data['comment']
545 comment = data['comment']
546 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
546 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
547 else:
547 else:
548 return
548 return
549
549
550 trigger_hook(
550 trigger_hook(
551 username=user.username,
551 username=user.username,
552 repo_name=pull_request.target_repo.repo_name,
552 repo_name=pull_request.target_repo.repo_name,
553 repo_alias=target_scm.alias,
553 repo_alias=target_scm.alias,
554 pull_request=pull_request,
554 pull_request=pull_request,
555 data=data)
555 data=data)
556
556
557 def _get_commit_ids(self, pull_request):
557 def _get_commit_ids(self, pull_request):
558 """
558 """
559 Return the commit ids of the merged pull request.
559 Return the commit ids of the merged pull request.
560
560
561 This method is not dealing correctly yet with the lack of autoupdates
561 This method is not dealing correctly yet with the lack of autoupdates
562 nor with the implicit target updates.
562 nor with the implicit target updates.
563 For example: if a commit in the source repo is already in the target it
563 For example: if a commit in the source repo is already in the target it
564 will be reported anyways.
564 will be reported anyways.
565 """
565 """
566 merge_rev = pull_request.merge_rev
566 merge_rev = pull_request.merge_rev
567 if merge_rev is None:
567 if merge_rev is None:
568 raise ValueError('This pull request was not merged yet')
568 raise ValueError('This pull request was not merged yet')
569
569
570 commit_ids = list(pull_request.revisions)
570 commit_ids = list(pull_request.revisions)
571 if merge_rev not in commit_ids:
571 if merge_rev not in commit_ids:
572 commit_ids.append(merge_rev)
572 commit_ids.append(merge_rev)
573
573
574 return commit_ids
574 return commit_ids
575
575
576 def merge_repo(self, pull_request, user, extras):
576 def merge_repo(self, pull_request, user, extras):
577 log.debug("Merging pull request %s", pull_request.pull_request_id)
577 log.debug("Merging pull request %s", pull_request.pull_request_id)
578 extras['user_agent'] = 'internal-merge'
578 extras['user_agent'] = 'internal-merge'
579 merge_state = self._merge_pull_request(pull_request, user, extras)
579 merge_state = self._merge_pull_request(pull_request, user, extras)
580 if merge_state.executed:
580 if merge_state.executed:
581 log.debug("Merge was successful, updating the pull request comments.")
581 log.debug("Merge was successful, updating the pull request comments.")
582 self._comment_and_close_pr(pull_request, user, merge_state)
582 self._comment_and_close_pr(pull_request, user, merge_state)
583
583
584 self._log_audit_action(
584 self._log_audit_action(
585 'repo.pull_request.merge',
585 'repo.pull_request.merge',
586 {'merge_state': merge_state.__dict__},
586 {'merge_state': merge_state.__dict__},
587 user, pull_request)
587 user, pull_request)
588
588
589 else:
589 else:
590 log.warn("Merge failed, not updating the pull request.")
590 log.warn("Merge failed, not updating the pull request.")
591 return merge_state
591 return merge_state
592
592
593 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
593 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
594 target_vcs = pull_request.target_repo.scm_instance()
594 target_vcs = pull_request.target_repo.scm_instance()
595 source_vcs = pull_request.source_repo.scm_instance()
595 source_vcs = pull_request.source_repo.scm_instance()
596
596
597 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
597 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
598 pr_id=pull_request.pull_request_id,
598 pr_id=pull_request.pull_request_id,
599 pr_title=pull_request.title,
599 pr_title=pull_request.title,
600 source_repo=source_vcs.name,
600 source_repo=source_vcs.name,
601 source_ref_name=pull_request.source_ref_parts.name,
601 source_ref_name=pull_request.source_ref_parts.name,
602 target_repo=target_vcs.name,
602 target_repo=target_vcs.name,
603 target_ref_name=pull_request.target_ref_parts.name,
603 target_ref_name=pull_request.target_ref_parts.name,
604 )
604 )
605
605
606 workspace_id = self._workspace_id(pull_request)
606 workspace_id = self._workspace_id(pull_request)
607 repo_id = pull_request.target_repo.repo_id
607 repo_id = pull_request.target_repo.repo_id
608 use_rebase = self._use_rebase_for_merging(pull_request)
608 use_rebase = self._use_rebase_for_merging(pull_request)
609 close_branch = self._close_branch_before_merging(pull_request)
609 close_branch = self._close_branch_before_merging(pull_request)
610
610
611 target_ref = self._refresh_reference(
611 target_ref = self._refresh_reference(
612 pull_request.target_ref_parts, target_vcs)
612 pull_request.target_ref_parts, target_vcs)
613
613
614 callback_daemon, extras = prepare_callback_daemon(
614 callback_daemon, extras = prepare_callback_daemon(
615 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
615 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
616 host=vcs_settings.HOOKS_HOST,
616 host=vcs_settings.HOOKS_HOST,
617 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
617 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
618
618
619 with callback_daemon:
619 with callback_daemon:
620 # TODO: johbo: Implement a clean way to run a config_override
620 # TODO: johbo: Implement a clean way to run a config_override
621 # for a single call.
621 # for a single call.
622 target_vcs.config.set(
622 target_vcs.config.set(
623 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
623 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
624
624
625 user_name = user.short_contact
625 user_name = user.short_contact
626 merge_state = target_vcs.merge(
626 merge_state = target_vcs.merge(
627 repo_id, workspace_id, target_ref, source_vcs,
627 repo_id, workspace_id, target_ref, source_vcs,
628 pull_request.source_ref_parts,
628 pull_request.source_ref_parts,
629 user_name=user_name, user_email=user.email,
629 user_name=user_name, user_email=user.email,
630 message=message, use_rebase=use_rebase,
630 message=message, use_rebase=use_rebase,
631 close_branch=close_branch)
631 close_branch=close_branch)
632 return merge_state
632 return merge_state
633
633
634 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
634 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
635 pull_request.merge_rev = merge_state.merge_ref.commit_id
635 pull_request.merge_rev = merge_state.merge_ref.commit_id
636 pull_request.updated_on = datetime.datetime.now()
636 pull_request.updated_on = datetime.datetime.now()
637 close_msg = close_msg or 'Pull request merged and closed'
637 close_msg = close_msg or 'Pull request merged and closed'
638
638
639 CommentsModel().create(
639 CommentsModel().create(
640 text=safe_unicode(close_msg),
640 text=safe_unicode(close_msg),
641 repo=pull_request.target_repo.repo_id,
641 repo=pull_request.target_repo.repo_id,
642 user=user.user_id,
642 user=user.user_id,
643 pull_request=pull_request.pull_request_id,
643 pull_request=pull_request.pull_request_id,
644 f_path=None,
644 f_path=None,
645 line_no=None,
645 line_no=None,
646 closing_pr=True
646 closing_pr=True
647 )
647 )
648
648
649 Session().add(pull_request)
649 Session().add(pull_request)
650 Session().flush()
650 Session().flush()
651 # TODO: paris: replace invalidation with less radical solution
651 # TODO: paris: replace invalidation with less radical solution
652 ScmModel().mark_for_invalidation(
652 ScmModel().mark_for_invalidation(
653 pull_request.target_repo.repo_name)
653 pull_request.target_repo.repo_name)
654 self.trigger_pull_request_hook(pull_request, user, 'merge')
654 self.trigger_pull_request_hook(pull_request, user, 'merge')
655
655
656 def has_valid_update_type(self, pull_request):
656 def has_valid_update_type(self, pull_request):
657 source_ref_type = pull_request.source_ref_parts.type
657 source_ref_type = pull_request.source_ref_parts.type
658 return source_ref_type in self.REF_TYPES
658 return source_ref_type in self.REF_TYPES
659
659
660 def update_commits(self, pull_request):
660 def update_commits(self, pull_request):
661 """
661 """
662 Get the updated list of commits for the pull request
662 Get the updated list of commits for the pull request
663 and return the new pull request version and the list
663 and return the new pull request version and the list
664 of commits processed by this update action
664 of commits processed by this update action
665 """
665 """
666 pull_request = self.__get_pull_request(pull_request)
666 pull_request = self.__get_pull_request(pull_request)
667 source_ref_type = pull_request.source_ref_parts.type
667 source_ref_type = pull_request.source_ref_parts.type
668 source_ref_name = pull_request.source_ref_parts.name
668 source_ref_name = pull_request.source_ref_parts.name
669 source_ref_id = pull_request.source_ref_parts.commit_id
669 source_ref_id = pull_request.source_ref_parts.commit_id
670
670
671 target_ref_type = pull_request.target_ref_parts.type
671 target_ref_type = pull_request.target_ref_parts.type
672 target_ref_name = pull_request.target_ref_parts.name
672 target_ref_name = pull_request.target_ref_parts.name
673 target_ref_id = pull_request.target_ref_parts.commit_id
673 target_ref_id = pull_request.target_ref_parts.commit_id
674
674
675 if not self.has_valid_update_type(pull_request):
675 if not self.has_valid_update_type(pull_request):
676 log.debug("Skipping update of pull request %s due to ref type: %s",
676 log.debug("Skipping update of pull request %s due to ref type: %s",
677 pull_request, source_ref_type)
677 pull_request, source_ref_type)
678 return UpdateResponse(
678 return UpdateResponse(
679 executed=False,
679 executed=False,
680 reason=UpdateFailureReason.WRONG_REF_TYPE,
680 reason=UpdateFailureReason.WRONG_REF_TYPE,
681 old=pull_request, new=None, changes=None,
681 old=pull_request, new=None, changes=None,
682 source_changed=False, target_changed=False)
682 source_changed=False, target_changed=False)
683
683
684 # source repo
684 # source repo
685 source_repo = pull_request.source_repo.scm_instance()
685 source_repo = pull_request.source_repo.scm_instance()
686 try:
686 try:
687 source_commit = source_repo.get_commit(commit_id=source_ref_name)
687 source_commit = source_repo.get_commit(commit_id=source_ref_name)
688 except CommitDoesNotExistError:
688 except CommitDoesNotExistError:
689 return UpdateResponse(
689 return UpdateResponse(
690 executed=False,
690 executed=False,
691 reason=UpdateFailureReason.MISSING_SOURCE_REF,
691 reason=UpdateFailureReason.MISSING_SOURCE_REF,
692 old=pull_request, new=None, changes=None,
692 old=pull_request, new=None, changes=None,
693 source_changed=False, target_changed=False)
693 source_changed=False, target_changed=False)
694
694
695 source_changed = source_ref_id != source_commit.raw_id
695 source_changed = source_ref_id != source_commit.raw_id
696
696
697 # target repo
697 # target repo
698 target_repo = pull_request.target_repo.scm_instance()
698 target_repo = pull_request.target_repo.scm_instance()
699 try:
699 try:
700 target_commit = target_repo.get_commit(commit_id=target_ref_name)
700 target_commit = target_repo.get_commit(commit_id=target_ref_name)
701 except CommitDoesNotExistError:
701 except CommitDoesNotExistError:
702 return UpdateResponse(
702 return UpdateResponse(
703 executed=False,
703 executed=False,
704 reason=UpdateFailureReason.MISSING_TARGET_REF,
704 reason=UpdateFailureReason.MISSING_TARGET_REF,
705 old=pull_request, new=None, changes=None,
705 old=pull_request, new=None, changes=None,
706 source_changed=False, target_changed=False)
706 source_changed=False, target_changed=False)
707 target_changed = target_ref_id != target_commit.raw_id
707 target_changed = target_ref_id != target_commit.raw_id
708
708
709 if not (source_changed or target_changed):
709 if not (source_changed or target_changed):
710 log.debug("Nothing changed in pull request %s", pull_request)
710 log.debug("Nothing changed in pull request %s", pull_request)
711 return UpdateResponse(
711 return UpdateResponse(
712 executed=False,
712 executed=False,
713 reason=UpdateFailureReason.NO_CHANGE,
713 reason=UpdateFailureReason.NO_CHANGE,
714 old=pull_request, new=None, changes=None,
714 old=pull_request, new=None, changes=None,
715 source_changed=target_changed, target_changed=source_changed)
715 source_changed=target_changed, target_changed=source_changed)
716
716
717 change_in_found = 'target repo' if target_changed else 'source repo'
717 change_in_found = 'target repo' if target_changed else 'source repo'
718 log.debug('Updating pull request because of change in %s detected',
718 log.debug('Updating pull request because of change in %s detected',
719 change_in_found)
719 change_in_found)
720
720
721 # Finally there is a need for an update, in case of source change
721 # Finally there is a need for an update, in case of source change
722 # we create a new version, else just an update
722 # we create a new version, else just an update
723 if source_changed:
723 if source_changed:
724 pull_request_version = self._create_version_from_snapshot(pull_request)
724 pull_request_version = self._create_version_from_snapshot(pull_request)
725 self._link_comments_to_version(pull_request_version)
725 self._link_comments_to_version(pull_request_version)
726 else:
726 else:
727 try:
727 try:
728 ver = pull_request.versions[-1]
728 ver = pull_request.versions[-1]
729 except IndexError:
729 except IndexError:
730 ver = None
730 ver = None
731
731
732 pull_request.pull_request_version_id = \
732 pull_request.pull_request_version_id = \
733 ver.pull_request_version_id if ver else None
733 ver.pull_request_version_id if ver else None
734 pull_request_version = pull_request
734 pull_request_version = pull_request
735
735
736 try:
736 try:
737 if target_ref_type in self.REF_TYPES:
737 if target_ref_type in self.REF_TYPES:
738 target_commit = target_repo.get_commit(target_ref_name)
738 target_commit = target_repo.get_commit(target_ref_name)
739 else:
739 else:
740 target_commit = target_repo.get_commit(target_ref_id)
740 target_commit = target_repo.get_commit(target_ref_id)
741 except CommitDoesNotExistError:
741 except CommitDoesNotExistError:
742 return UpdateResponse(
742 return UpdateResponse(
743 executed=False,
743 executed=False,
744 reason=UpdateFailureReason.MISSING_TARGET_REF,
744 reason=UpdateFailureReason.MISSING_TARGET_REF,
745 old=pull_request, new=None, changes=None,
745 old=pull_request, new=None, changes=None,
746 source_changed=source_changed, target_changed=target_changed)
746 source_changed=source_changed, target_changed=target_changed)
747
747
748 # re-compute commit ids
748 # re-compute commit ids
749 old_commit_ids = pull_request.revisions
749 old_commit_ids = pull_request.revisions
750 pre_load = ["author", "branch", "date", "message"]
750 pre_load = ["author", "branch", "date", "message"]
751 commit_ranges = target_repo.compare(
751 commit_ranges = target_repo.compare(
752 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
752 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
753 pre_load=pre_load)
753 pre_load=pre_load)
754
754
755 ancestor = target_repo.get_common_ancestor(
755 ancestor = target_repo.get_common_ancestor(
756 target_commit.raw_id, source_commit.raw_id, source_repo)
756 target_commit.raw_id, source_commit.raw_id, source_repo)
757
757
758 pull_request.source_ref = '%s:%s:%s' % (
758 pull_request.source_ref = '%s:%s:%s' % (
759 source_ref_type, source_ref_name, source_commit.raw_id)
759 source_ref_type, source_ref_name, source_commit.raw_id)
760 pull_request.target_ref = '%s:%s:%s' % (
760 pull_request.target_ref = '%s:%s:%s' % (
761 target_ref_type, target_ref_name, ancestor)
761 target_ref_type, target_ref_name, ancestor)
762
762
763 pull_request.revisions = [
763 pull_request.revisions = [
764 commit.raw_id for commit in reversed(commit_ranges)]
764 commit.raw_id for commit in reversed(commit_ranges)]
765 pull_request.updated_on = datetime.datetime.now()
765 pull_request.updated_on = datetime.datetime.now()
766 Session().add(pull_request)
766 Session().add(pull_request)
767 new_commit_ids = pull_request.revisions
767 new_commit_ids = pull_request.revisions
768
768
769 old_diff_data, new_diff_data = self._generate_update_diffs(
769 old_diff_data, new_diff_data = self._generate_update_diffs(
770 pull_request, pull_request_version)
770 pull_request, pull_request_version)
771
771
772 # calculate commit and file changes
772 # calculate commit and file changes
773 changes = self._calculate_commit_id_changes(
773 changes = self._calculate_commit_id_changes(
774 old_commit_ids, new_commit_ids)
774 old_commit_ids, new_commit_ids)
775 file_changes = self._calculate_file_changes(
775 file_changes = self._calculate_file_changes(
776 old_diff_data, new_diff_data)
776 old_diff_data, new_diff_data)
777
777
778 # set comments as outdated if DIFFS changed
778 # set comments as outdated if DIFFS changed
779 CommentsModel().outdate_comments(
779 CommentsModel().outdate_comments(
780 pull_request, old_diff_data=old_diff_data,
780 pull_request, old_diff_data=old_diff_data,
781 new_diff_data=new_diff_data)
781 new_diff_data=new_diff_data)
782
782
783 commit_changes = (changes.added or changes.removed)
783 commit_changes = (changes.added or changes.removed)
784 file_node_changes = (
784 file_node_changes = (
785 file_changes.added or file_changes.modified or file_changes.removed)
785 file_changes.added or file_changes.modified or file_changes.removed)
786 pr_has_changes = commit_changes or file_node_changes
786 pr_has_changes = commit_changes or file_node_changes
787
787
788 # Add an automatic comment to the pull request, in case
788 # Add an automatic comment to the pull request, in case
789 # anything has changed
789 # anything has changed
790 if pr_has_changes:
790 if pr_has_changes:
791 update_comment = CommentsModel().create(
791 update_comment = CommentsModel().create(
792 text=self._render_update_message(changes, file_changes),
792 text=self._render_update_message(changes, file_changes),
793 repo=pull_request.target_repo,
793 repo=pull_request.target_repo,
794 user=pull_request.author,
794 user=pull_request.author,
795 pull_request=pull_request,
795 pull_request=pull_request,
796 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
796 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
797
797
798 # Update status to "Under Review" for added commits
798 # Update status to "Under Review" for added commits
799 for commit_id in changes.added:
799 for commit_id in changes.added:
800 ChangesetStatusModel().set_status(
800 ChangesetStatusModel().set_status(
801 repo=pull_request.source_repo,
801 repo=pull_request.source_repo,
802 status=ChangesetStatus.STATUS_UNDER_REVIEW,
802 status=ChangesetStatus.STATUS_UNDER_REVIEW,
803 comment=update_comment,
803 comment=update_comment,
804 user=pull_request.author,
804 user=pull_request.author,
805 pull_request=pull_request,
805 pull_request=pull_request,
806 revision=commit_id)
806 revision=commit_id)
807
807
808 log.debug(
808 log.debug(
809 'Updated pull request %s, added_ids: %s, common_ids: %s, '
809 'Updated pull request %s, added_ids: %s, common_ids: %s, '
810 'removed_ids: %s', pull_request.pull_request_id,
810 'removed_ids: %s', pull_request.pull_request_id,
811 changes.added, changes.common, changes.removed)
811 changes.added, changes.common, changes.removed)
812 log.debug(
812 log.debug(
813 'Updated pull request with the following file changes: %s',
813 'Updated pull request with the following file changes: %s',
814 file_changes)
814 file_changes)
815
815
816 log.info(
816 log.info(
817 "Updated pull request %s from commit %s to commit %s, "
817 "Updated pull request %s from commit %s to commit %s, "
818 "stored new version %s of this pull request.",
818 "stored new version %s of this pull request.",
819 pull_request.pull_request_id, source_ref_id,
819 pull_request.pull_request_id, source_ref_id,
820 pull_request.source_ref_parts.commit_id,
820 pull_request.source_ref_parts.commit_id,
821 pull_request_version.pull_request_version_id)
821 pull_request_version.pull_request_version_id)
822 Session().commit()
822 Session().commit()
823 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
823 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
824
824
825 return UpdateResponse(
825 return UpdateResponse(
826 executed=True, reason=UpdateFailureReason.NONE,
826 executed=True, reason=UpdateFailureReason.NONE,
827 old=pull_request, new=pull_request_version, changes=changes,
827 old=pull_request, new=pull_request_version, changes=changes,
828 source_changed=source_changed, target_changed=target_changed)
828 source_changed=source_changed, target_changed=target_changed)
829
829
830 def _create_version_from_snapshot(self, pull_request):
830 def _create_version_from_snapshot(self, pull_request):
831 version = PullRequestVersion()
831 version = PullRequestVersion()
832 version.title = pull_request.title
832 version.title = pull_request.title
833 version.description = pull_request.description
833 version.description = pull_request.description
834 version.status = pull_request.status
834 version.status = pull_request.status
835 version.pull_request_state = pull_request.pull_request_state
835 version.pull_request_state = pull_request.pull_request_state
836 version.created_on = datetime.datetime.now()
836 version.created_on = datetime.datetime.now()
837 version.updated_on = pull_request.updated_on
837 version.updated_on = pull_request.updated_on
838 version.user_id = pull_request.user_id
838 version.user_id = pull_request.user_id
839 version.source_repo = pull_request.source_repo
839 version.source_repo = pull_request.source_repo
840 version.source_ref = pull_request.source_ref
840 version.source_ref = pull_request.source_ref
841 version.target_repo = pull_request.target_repo
841 version.target_repo = pull_request.target_repo
842 version.target_ref = pull_request.target_ref
842 version.target_ref = pull_request.target_ref
843
843
844 version._last_merge_source_rev = pull_request._last_merge_source_rev
844 version._last_merge_source_rev = pull_request._last_merge_source_rev
845 version._last_merge_target_rev = pull_request._last_merge_target_rev
845 version._last_merge_target_rev = pull_request._last_merge_target_rev
846 version.last_merge_status = pull_request.last_merge_status
846 version.last_merge_status = pull_request.last_merge_status
847 version.shadow_merge_ref = pull_request.shadow_merge_ref
847 version.shadow_merge_ref = pull_request.shadow_merge_ref
848 version.merge_rev = pull_request.merge_rev
848 version.merge_rev = pull_request.merge_rev
849 version.reviewer_data = pull_request.reviewer_data
849 version.reviewer_data = pull_request.reviewer_data
850
850
851 version.revisions = pull_request.revisions
851 version.revisions = pull_request.revisions
852 version.pull_request = pull_request
852 version.pull_request = pull_request
853 Session().add(version)
853 Session().add(version)
854 Session().flush()
854 Session().flush()
855
855
856 return version
856 return version
857
857
858 def _generate_update_diffs(self, pull_request, pull_request_version):
858 def _generate_update_diffs(self, pull_request, pull_request_version):
859
859
860 diff_context = (
860 diff_context = (
861 self.DIFF_CONTEXT +
861 self.DIFF_CONTEXT +
862 CommentsModel.needed_extra_diff_context())
862 CommentsModel.needed_extra_diff_context())
863 hide_whitespace_changes = False
863 hide_whitespace_changes = False
864 source_repo = pull_request_version.source_repo
864 source_repo = pull_request_version.source_repo
865 source_ref_id = pull_request_version.source_ref_parts.commit_id
865 source_ref_id = pull_request_version.source_ref_parts.commit_id
866 target_ref_id = pull_request_version.target_ref_parts.commit_id
866 target_ref_id = pull_request_version.target_ref_parts.commit_id
867 old_diff = self._get_diff_from_pr_or_version(
867 old_diff = self._get_diff_from_pr_or_version(
868 source_repo, source_ref_id, target_ref_id,
868 source_repo, source_ref_id, target_ref_id,
869 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
869 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
870
870
871 source_repo = pull_request.source_repo
871 source_repo = pull_request.source_repo
872 source_ref_id = pull_request.source_ref_parts.commit_id
872 source_ref_id = pull_request.source_ref_parts.commit_id
873 target_ref_id = pull_request.target_ref_parts.commit_id
873 target_ref_id = pull_request.target_ref_parts.commit_id
874
874
875 new_diff = self._get_diff_from_pr_or_version(
875 new_diff = self._get_diff_from_pr_or_version(
876 source_repo, source_ref_id, target_ref_id,
876 source_repo, source_ref_id, target_ref_id,
877 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
877 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
878
878
879 old_diff_data = diffs.DiffProcessor(old_diff)
879 old_diff_data = diffs.DiffProcessor(old_diff)
880 old_diff_data.prepare()
880 old_diff_data.prepare()
881 new_diff_data = diffs.DiffProcessor(new_diff)
881 new_diff_data = diffs.DiffProcessor(new_diff)
882 new_diff_data.prepare()
882 new_diff_data.prepare()
883
883
884 return old_diff_data, new_diff_data
884 return old_diff_data, new_diff_data
885
885
886 def _link_comments_to_version(self, pull_request_version):
886 def _link_comments_to_version(self, pull_request_version):
887 """
887 """
888 Link all unlinked comments of this pull request to the given version.
888 Link all unlinked comments of this pull request to the given version.
889
889
890 :param pull_request_version: The `PullRequestVersion` to which
890 :param pull_request_version: The `PullRequestVersion` to which
891 the comments shall be linked.
891 the comments shall be linked.
892
892
893 """
893 """
894 pull_request = pull_request_version.pull_request
894 pull_request = pull_request_version.pull_request
895 comments = ChangesetComment.query()\
895 comments = ChangesetComment.query()\
896 .filter(
896 .filter(
897 # TODO: johbo: Should we query for the repo at all here?
897 # TODO: johbo: Should we query for the repo at all here?
898 # Pending decision on how comments of PRs are to be related
898 # Pending decision on how comments of PRs are to be related
899 # to either the source repo, the target repo or no repo at all.
899 # to either the source repo, the target repo or no repo at all.
900 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
900 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
901 ChangesetComment.pull_request == pull_request,
901 ChangesetComment.pull_request == pull_request,
902 ChangesetComment.pull_request_version == None)\
902 ChangesetComment.pull_request_version == None)\
903 .order_by(ChangesetComment.comment_id.asc())
903 .order_by(ChangesetComment.comment_id.asc())
904
904
905 # TODO: johbo: Find out why this breaks if it is done in a bulk
905 # TODO: johbo: Find out why this breaks if it is done in a bulk
906 # operation.
906 # operation.
907 for comment in comments:
907 for comment in comments:
908 comment.pull_request_version_id = (
908 comment.pull_request_version_id = (
909 pull_request_version.pull_request_version_id)
909 pull_request_version.pull_request_version_id)
910 Session().add(comment)
910 Session().add(comment)
911
911
912 def _calculate_commit_id_changes(self, old_ids, new_ids):
912 def _calculate_commit_id_changes(self, old_ids, new_ids):
913 added = [x for x in new_ids if x not in old_ids]
913 added = [x for x in new_ids if x not in old_ids]
914 common = [x for x in new_ids if x in old_ids]
914 common = [x for x in new_ids if x in old_ids]
915 removed = [x for x in old_ids if x not in new_ids]
915 removed = [x for x in old_ids if x not in new_ids]
916 total = new_ids
916 total = new_ids
917 return ChangeTuple(added, common, removed, total)
917 return ChangeTuple(added, common, removed, total)
918
918
919 def _calculate_file_changes(self, old_diff_data, new_diff_data):
919 def _calculate_file_changes(self, old_diff_data, new_diff_data):
920
920
921 old_files = OrderedDict()
921 old_files = OrderedDict()
922 for diff_data in old_diff_data.parsed_diff:
922 for diff_data in old_diff_data.parsed_diff:
923 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
923 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
924
924
925 added_files = []
925 added_files = []
926 modified_files = []
926 modified_files = []
927 removed_files = []
927 removed_files = []
928 for diff_data in new_diff_data.parsed_diff:
928 for diff_data in new_diff_data.parsed_diff:
929 new_filename = diff_data['filename']
929 new_filename = diff_data['filename']
930 new_hash = md5_safe(diff_data['raw_diff'])
930 new_hash = md5_safe(diff_data['raw_diff'])
931
931
932 old_hash = old_files.get(new_filename)
932 old_hash = old_files.get(new_filename)
933 if not old_hash:
933 if not old_hash:
934 # file is not present in old diff, means it's added
934 # file is not present in old diff, means it's added
935 added_files.append(new_filename)
935 added_files.append(new_filename)
936 else:
936 else:
937 if new_hash != old_hash:
937 if new_hash != old_hash:
938 modified_files.append(new_filename)
938 modified_files.append(new_filename)
939 # now remove a file from old, since we have seen it already
939 # now remove a file from old, since we have seen it already
940 del old_files[new_filename]
940 del old_files[new_filename]
941
941
942 # removed files is when there are present in old, but not in NEW,
942 # removed files is when there are present in old, but not in NEW,
943 # since we remove old files that are present in new diff, left-overs
943 # since we remove old files that are present in new diff, left-overs
944 # if any should be the removed files
944 # if any should be the removed files
945 removed_files.extend(old_files.keys())
945 removed_files.extend(old_files.keys())
946
946
947 return FileChangeTuple(added_files, modified_files, removed_files)
947 return FileChangeTuple(added_files, modified_files, removed_files)
948
948
949 def _render_update_message(self, changes, file_changes):
949 def _render_update_message(self, changes, file_changes):
950 """
950 """
951 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
951 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
952 so it's always looking the same disregarding on which default
952 so it's always looking the same disregarding on which default
953 renderer system is using.
953 renderer system is using.
954
954
955 :param changes: changes named tuple
955 :param changes: changes named tuple
956 :param file_changes: file changes named tuple
956 :param file_changes: file changes named tuple
957
957
958 """
958 """
959 new_status = ChangesetStatus.get_status_lbl(
959 new_status = ChangesetStatus.get_status_lbl(
960 ChangesetStatus.STATUS_UNDER_REVIEW)
960 ChangesetStatus.STATUS_UNDER_REVIEW)
961
961
962 changed_files = (
962 changed_files = (
963 file_changes.added + file_changes.modified + file_changes.removed)
963 file_changes.added + file_changes.modified + file_changes.removed)
964
964
965 params = {
965 params = {
966 'under_review_label': new_status,
966 'under_review_label': new_status,
967 'added_commits': changes.added,
967 'added_commits': changes.added,
968 'removed_commits': changes.removed,
968 'removed_commits': changes.removed,
969 'changed_files': changed_files,
969 'changed_files': changed_files,
970 'added_files': file_changes.added,
970 'added_files': file_changes.added,
971 'modified_files': file_changes.modified,
971 'modified_files': file_changes.modified,
972 'removed_files': file_changes.removed,
972 'removed_files': file_changes.removed,
973 }
973 }
974 renderer = RstTemplateRenderer()
974 renderer = RstTemplateRenderer()
975 return renderer.render('pull_request_update.mako', **params)
975 return renderer.render('pull_request_update.mako', **params)
976
976
977 def edit(self, pull_request, title, description, description_renderer, user):
977 def edit(self, pull_request, title, description, description_renderer, user):
978 pull_request = self.__get_pull_request(pull_request)
978 pull_request = self.__get_pull_request(pull_request)
979 old_data = pull_request.get_api_data(with_merge_state=False)
979 old_data = pull_request.get_api_data(with_merge_state=False)
980 if pull_request.is_closed():
980 if pull_request.is_closed():
981 raise ValueError('This pull request is closed')
981 raise ValueError('This pull request is closed')
982 if title:
982 if title:
983 pull_request.title = title
983 pull_request.title = title
984 pull_request.description = description
984 pull_request.description = description
985 pull_request.updated_on = datetime.datetime.now()
985 pull_request.updated_on = datetime.datetime.now()
986 pull_request.description_renderer = description_renderer
986 pull_request.description_renderer = description_renderer
987 Session().add(pull_request)
987 Session().add(pull_request)
988 self._log_audit_action(
988 self._log_audit_action(
989 'repo.pull_request.edit', {'old_data': old_data},
989 'repo.pull_request.edit', {'old_data': old_data},
990 user, pull_request)
990 user, pull_request)
991
991
992 def update_reviewers(self, pull_request, reviewer_data, user):
992 def update_reviewers(self, pull_request, reviewer_data, user):
993 """
993 """
994 Update the reviewers in the pull request
994 Update the reviewers in the pull request
995
995
996 :param pull_request: the pr to update
996 :param pull_request: the pr to update
997 :param reviewer_data: list of tuples
997 :param reviewer_data: list of tuples
998 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
998 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
999 """
999 """
1000 pull_request = self.__get_pull_request(pull_request)
1000 pull_request = self.__get_pull_request(pull_request)
1001 if pull_request.is_closed():
1001 if pull_request.is_closed():
1002 raise ValueError('This pull request is closed')
1002 raise ValueError('This pull request is closed')
1003
1003
1004 reviewers = {}
1004 reviewers = {}
1005 for user_id, reasons, mandatory, rules in reviewer_data:
1005 for user_id, reasons, mandatory, rules in reviewer_data:
1006 if isinstance(user_id, (int, compat.string_types)):
1006 if isinstance(user_id, (int, compat.string_types)):
1007 user_id = self._get_user(user_id).user_id
1007 user_id = self._get_user(user_id).user_id
1008 reviewers[user_id] = {
1008 reviewers[user_id] = {
1009 'reasons': reasons, 'mandatory': mandatory}
1009 'reasons': reasons, 'mandatory': mandatory}
1010
1010
1011 reviewers_ids = set(reviewers.keys())
1011 reviewers_ids = set(reviewers.keys())
1012 current_reviewers = PullRequestReviewers.query()\
1012 current_reviewers = PullRequestReviewers.query()\
1013 .filter(PullRequestReviewers.pull_request ==
1013 .filter(PullRequestReviewers.pull_request ==
1014 pull_request).all()
1014 pull_request).all()
1015 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1015 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1016
1016
1017 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1017 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1018 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1018 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1019
1019
1020 log.debug("Adding %s reviewers", ids_to_add)
1020 log.debug("Adding %s reviewers", ids_to_add)
1021 log.debug("Removing %s reviewers", ids_to_remove)
1021 log.debug("Removing %s reviewers", ids_to_remove)
1022 changed = False
1022 changed = False
1023 added_audit_reviewers = []
1024 removed_audit_reviewers = []
1025
1023 for uid in ids_to_add:
1026 for uid in ids_to_add:
1024 changed = True
1027 changed = True
1025 _usr = self._get_user(uid)
1028 _usr = self._get_user(uid)
1026 reviewer = PullRequestReviewers()
1029 reviewer = PullRequestReviewers()
1027 reviewer.user = _usr
1030 reviewer.user = _usr
1028 reviewer.pull_request = pull_request
1031 reviewer.pull_request = pull_request
1029 reviewer.reasons = reviewers[uid]['reasons']
1032 reviewer.reasons = reviewers[uid]['reasons']
1030 # NOTE(marcink): mandatory shouldn't be changed now
1033 # NOTE(marcink): mandatory shouldn't be changed now
1031 # reviewer.mandatory = reviewers[uid]['reasons']
1034 # reviewer.mandatory = reviewers[uid]['reasons']
1032 Session().add(reviewer)
1035 Session().add(reviewer)
1033 self._log_audit_action(
1036 added_audit_reviewers.append(reviewer.get_dict())
1034 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1035 user, pull_request)
1036
1037
1037 for uid in ids_to_remove:
1038 for uid in ids_to_remove:
1038 changed = True
1039 changed = True
1040 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1041 # that prevents and fixes cases that we added the same reviewer twice.
1042 # this CAN happen due to the lack of DB checks
1039 reviewers = PullRequestReviewers.query()\
1043 reviewers = PullRequestReviewers.query()\
1040 .filter(PullRequestReviewers.user_id == uid,
1044 .filter(PullRequestReviewers.user_id == uid,
1041 PullRequestReviewers.pull_request == pull_request)\
1045 PullRequestReviewers.pull_request == pull_request)\
1042 .all()
1046 .all()
1043 # use .all() in case we accidentally added the same person twice
1047
1044 # this CAN happen due to the lack of DB checks
1045 for obj in reviewers:
1048 for obj in reviewers:
1046 old_data = obj.get_dict()
1049 added_audit_reviewers.append(obj.get_dict())
1047 Session().delete(obj)
1050 Session().delete(obj)
1048 self._log_audit_action(
1049 'repo.pull_request.reviewer.delete',
1050 {'old_data': old_data}, user, pull_request)
1051
1051
1052 if changed:
1052 if changed:
1053 Session().expire_all()
1053 pull_request.updated_on = datetime.datetime.now()
1054 pull_request.updated_on = datetime.datetime.now()
1054 Session().add(pull_request)
1055 Session().add(pull_request)
1055
1056
1057 # finally store audit logs
1058 for user_data in added_audit_reviewers:
1059 self._log_audit_action(
1060 'repo.pull_request.reviewer.add', {'data': user_data},
1061 user, pull_request)
1062 for user_data in removed_audit_reviewers:
1063 self._log_audit_action(
1064 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1065 user, pull_request)
1066
1056 self.notify_reviewers(pull_request, ids_to_add)
1067 self.notify_reviewers(pull_request, ids_to_add)
1057 return ids_to_add, ids_to_remove
1068 return ids_to_add, ids_to_remove
1058
1069
1059 def get_url(self, pull_request, request=None, permalink=False):
1070 def get_url(self, pull_request, request=None, permalink=False):
1060 if not request:
1071 if not request:
1061 request = get_current_request()
1072 request = get_current_request()
1062
1073
1063 if permalink:
1074 if permalink:
1064 return request.route_url(
1075 return request.route_url(
1065 'pull_requests_global',
1076 'pull_requests_global',
1066 pull_request_id=pull_request.pull_request_id,)
1077 pull_request_id=pull_request.pull_request_id,)
1067 else:
1078 else:
1068 return request.route_url('pullrequest_show',
1079 return request.route_url('pullrequest_show',
1069 repo_name=safe_str(pull_request.target_repo.repo_name),
1080 repo_name=safe_str(pull_request.target_repo.repo_name),
1070 pull_request_id=pull_request.pull_request_id,)
1081 pull_request_id=pull_request.pull_request_id,)
1071
1082
1072 def get_shadow_clone_url(self, pull_request, request=None):
1083 def get_shadow_clone_url(self, pull_request, request=None):
1073 """
1084 """
1074 Returns qualified url pointing to the shadow repository. If this pull
1085 Returns qualified url pointing to the shadow repository. If this pull
1075 request is closed there is no shadow repository and ``None`` will be
1086 request is closed there is no shadow repository and ``None`` will be
1076 returned.
1087 returned.
1077 """
1088 """
1078 if pull_request.is_closed():
1089 if pull_request.is_closed():
1079 return None
1090 return None
1080 else:
1091 else:
1081 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1092 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1082 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1093 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1083
1094
1084 def notify_reviewers(self, pull_request, reviewers_ids):
1095 def notify_reviewers(self, pull_request, reviewers_ids):
1085 # notification to reviewers
1096 # notification to reviewers
1086 if not reviewers_ids:
1097 if not reviewers_ids:
1087 return
1098 return
1088
1099
1089 pull_request_obj = pull_request
1100 pull_request_obj = pull_request
1090 # get the current participants of this pull request
1101 # get the current participants of this pull request
1091 recipients = reviewers_ids
1102 recipients = reviewers_ids
1092 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1103 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1093
1104
1094 pr_source_repo = pull_request_obj.source_repo
1105 pr_source_repo = pull_request_obj.source_repo
1095 pr_target_repo = pull_request_obj.target_repo
1106 pr_target_repo = pull_request_obj.target_repo
1096
1107
1097 pr_url = h.route_url('pullrequest_show',
1108 pr_url = h.route_url('pullrequest_show',
1098 repo_name=pr_target_repo.repo_name,
1109 repo_name=pr_target_repo.repo_name,
1099 pull_request_id=pull_request_obj.pull_request_id,)
1110 pull_request_id=pull_request_obj.pull_request_id,)
1100
1111
1101 # set some variables for email notification
1112 # set some variables for email notification
1102 pr_target_repo_url = h.route_url(
1113 pr_target_repo_url = h.route_url(
1103 'repo_summary', repo_name=pr_target_repo.repo_name)
1114 'repo_summary', repo_name=pr_target_repo.repo_name)
1104
1115
1105 pr_source_repo_url = h.route_url(
1116 pr_source_repo_url = h.route_url(
1106 'repo_summary', repo_name=pr_source_repo.repo_name)
1117 'repo_summary', repo_name=pr_source_repo.repo_name)
1107
1118
1108 # pull request specifics
1119 # pull request specifics
1109 pull_request_commits = [
1120 pull_request_commits = [
1110 (x.raw_id, x.message)
1121 (x.raw_id, x.message)
1111 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1122 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1112
1123
1113 kwargs = {
1124 kwargs = {
1114 'user': pull_request.author,
1125 'user': pull_request.author,
1115 'pull_request': pull_request_obj,
1126 'pull_request': pull_request_obj,
1116 'pull_request_commits': pull_request_commits,
1127 'pull_request_commits': pull_request_commits,
1117
1128
1118 'pull_request_target_repo': pr_target_repo,
1129 'pull_request_target_repo': pr_target_repo,
1119 'pull_request_target_repo_url': pr_target_repo_url,
1130 'pull_request_target_repo_url': pr_target_repo_url,
1120
1131
1121 'pull_request_source_repo': pr_source_repo,
1132 'pull_request_source_repo': pr_source_repo,
1122 'pull_request_source_repo_url': pr_source_repo_url,
1133 'pull_request_source_repo_url': pr_source_repo_url,
1123
1134
1124 'pull_request_url': pr_url,
1135 'pull_request_url': pr_url,
1125 }
1136 }
1126
1137
1127 # pre-generate the subject for notification itself
1138 # pre-generate the subject for notification itself
1128 (subject,
1139 (subject,
1129 _h, _e, # we don't care about those
1140 _h, _e, # we don't care about those
1130 body_plaintext) = EmailNotificationModel().render_email(
1141 body_plaintext) = EmailNotificationModel().render_email(
1131 notification_type, **kwargs)
1142 notification_type, **kwargs)
1132
1143
1133 # create notification objects, and emails
1144 # create notification objects, and emails
1134 NotificationModel().create(
1145 NotificationModel().create(
1135 created_by=pull_request.author,
1146 created_by=pull_request.author,
1136 notification_subject=subject,
1147 notification_subject=subject,
1137 notification_body=body_plaintext,
1148 notification_body=body_plaintext,
1138 notification_type=notification_type,
1149 notification_type=notification_type,
1139 recipients=recipients,
1150 recipients=recipients,
1140 email_kwargs=kwargs,
1151 email_kwargs=kwargs,
1141 )
1152 )
1142
1153
1143 def delete(self, pull_request, user):
1154 def delete(self, pull_request, user):
1144 pull_request = self.__get_pull_request(pull_request)
1155 pull_request = self.__get_pull_request(pull_request)
1145 old_data = pull_request.get_api_data(with_merge_state=False)
1156 old_data = pull_request.get_api_data(with_merge_state=False)
1146 self._cleanup_merge_workspace(pull_request)
1157 self._cleanup_merge_workspace(pull_request)
1147 self._log_audit_action(
1158 self._log_audit_action(
1148 'repo.pull_request.delete', {'old_data': old_data},
1159 'repo.pull_request.delete', {'old_data': old_data},
1149 user, pull_request)
1160 user, pull_request)
1150 Session().delete(pull_request)
1161 Session().delete(pull_request)
1151
1162
1152 def close_pull_request(self, pull_request, user):
1163 def close_pull_request(self, pull_request, user):
1153 pull_request = self.__get_pull_request(pull_request)
1164 pull_request = self.__get_pull_request(pull_request)
1154 self._cleanup_merge_workspace(pull_request)
1165 self._cleanup_merge_workspace(pull_request)
1155 pull_request.status = PullRequest.STATUS_CLOSED
1166 pull_request.status = PullRequest.STATUS_CLOSED
1156 pull_request.updated_on = datetime.datetime.now()
1167 pull_request.updated_on = datetime.datetime.now()
1157 Session().add(pull_request)
1168 Session().add(pull_request)
1158 self.trigger_pull_request_hook(
1169 self.trigger_pull_request_hook(
1159 pull_request, pull_request.author, 'close')
1170 pull_request, pull_request.author, 'close')
1160
1171
1161 pr_data = pull_request.get_api_data(with_merge_state=False)
1172 pr_data = pull_request.get_api_data(with_merge_state=False)
1162 self._log_audit_action(
1173 self._log_audit_action(
1163 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1174 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1164
1175
1165 def close_pull_request_with_comment(
1176 def close_pull_request_with_comment(
1166 self, pull_request, user, repo, message=None, auth_user=None):
1177 self, pull_request, user, repo, message=None, auth_user=None):
1167
1178
1168 pull_request_review_status = pull_request.calculated_review_status()
1179 pull_request_review_status = pull_request.calculated_review_status()
1169
1180
1170 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1181 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1171 # approved only if we have voting consent
1182 # approved only if we have voting consent
1172 status = ChangesetStatus.STATUS_APPROVED
1183 status = ChangesetStatus.STATUS_APPROVED
1173 else:
1184 else:
1174 status = ChangesetStatus.STATUS_REJECTED
1185 status = ChangesetStatus.STATUS_REJECTED
1175 status_lbl = ChangesetStatus.get_status_lbl(status)
1186 status_lbl = ChangesetStatus.get_status_lbl(status)
1176
1187
1177 default_message = (
1188 default_message = (
1178 'Closing with status change {transition_icon} {status}.'
1189 'Closing with status change {transition_icon} {status}.'
1179 ).format(transition_icon='>', status=status_lbl)
1190 ).format(transition_icon='>', status=status_lbl)
1180 text = message or default_message
1191 text = message or default_message
1181
1192
1182 # create a comment, and link it to new status
1193 # create a comment, and link it to new status
1183 comment = CommentsModel().create(
1194 comment = CommentsModel().create(
1184 text=text,
1195 text=text,
1185 repo=repo.repo_id,
1196 repo=repo.repo_id,
1186 user=user.user_id,
1197 user=user.user_id,
1187 pull_request=pull_request.pull_request_id,
1198 pull_request=pull_request.pull_request_id,
1188 status_change=status_lbl,
1199 status_change=status_lbl,
1189 status_change_type=status,
1200 status_change_type=status,
1190 closing_pr=True,
1201 closing_pr=True,
1191 auth_user=auth_user,
1202 auth_user=auth_user,
1192 )
1203 )
1193
1204
1194 # calculate old status before we change it
1205 # calculate old status before we change it
1195 old_calculated_status = pull_request.calculated_review_status()
1206 old_calculated_status = pull_request.calculated_review_status()
1196 ChangesetStatusModel().set_status(
1207 ChangesetStatusModel().set_status(
1197 repo.repo_id,
1208 repo.repo_id,
1198 status,
1209 status,
1199 user.user_id,
1210 user.user_id,
1200 comment=comment,
1211 comment=comment,
1201 pull_request=pull_request.pull_request_id
1212 pull_request=pull_request.pull_request_id
1202 )
1213 )
1203
1214
1204 Session().flush()
1215 Session().flush()
1205 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1216 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1206 # we now calculate the status of pull request again, and based on that
1217 # we now calculate the status of pull request again, and based on that
1207 # calculation trigger status change. This might happen in cases
1218 # calculation trigger status change. This might happen in cases
1208 # that non-reviewer admin closes a pr, which means his vote doesn't
1219 # that non-reviewer admin closes a pr, which means his vote doesn't
1209 # change the status, while if he's a reviewer this might change it.
1220 # change the status, while if he's a reviewer this might change it.
1210 calculated_status = pull_request.calculated_review_status()
1221 calculated_status = pull_request.calculated_review_status()
1211 if old_calculated_status != calculated_status:
1222 if old_calculated_status != calculated_status:
1212 self.trigger_pull_request_hook(
1223 self.trigger_pull_request_hook(
1213 pull_request, user, 'review_status_change',
1224 pull_request, user, 'review_status_change',
1214 data={'status': calculated_status})
1225 data={'status': calculated_status})
1215
1226
1216 # finally close the PR
1227 # finally close the PR
1217 PullRequestModel().close_pull_request(
1228 PullRequestModel().close_pull_request(
1218 pull_request.pull_request_id, user)
1229 pull_request.pull_request_id, user)
1219
1230
1220 return comment, status
1231 return comment, status
1221
1232
1222 def merge_status(self, pull_request, translator=None,
1233 def merge_status(self, pull_request, translator=None,
1223 force_shadow_repo_refresh=False):
1234 force_shadow_repo_refresh=False):
1224 _ = translator or get_current_request().translate
1235 _ = translator or get_current_request().translate
1225
1236
1226 if not self._is_merge_enabled(pull_request):
1237 if not self._is_merge_enabled(pull_request):
1227 return False, _('Server-side pull request merging is disabled.')
1238 return False, _('Server-side pull request merging is disabled.')
1228 if pull_request.is_closed():
1239 if pull_request.is_closed():
1229 return False, _('This pull request is closed.')
1240 return False, _('This pull request is closed.')
1230 merge_possible, msg = self._check_repo_requirements(
1241 merge_possible, msg = self._check_repo_requirements(
1231 target=pull_request.target_repo, source=pull_request.source_repo,
1242 target=pull_request.target_repo, source=pull_request.source_repo,
1232 translator=_)
1243 translator=_)
1233 if not merge_possible:
1244 if not merge_possible:
1234 return merge_possible, msg
1245 return merge_possible, msg
1235
1246
1236 try:
1247 try:
1237 resp = self._try_merge(
1248 resp = self._try_merge(
1238 pull_request,
1249 pull_request,
1239 force_shadow_repo_refresh=force_shadow_repo_refresh)
1250 force_shadow_repo_refresh=force_shadow_repo_refresh)
1240 log.debug("Merge response: %s", resp)
1251 log.debug("Merge response: %s", resp)
1241 status = resp.possible, resp.merge_status_message
1252 status = resp.possible, resp.merge_status_message
1242 except NotImplementedError:
1253 except NotImplementedError:
1243 status = False, _('Pull request merging is not supported.')
1254 status = False, _('Pull request merging is not supported.')
1244
1255
1245 return status
1256 return status
1246
1257
1247 def _check_repo_requirements(self, target, source, translator):
1258 def _check_repo_requirements(self, target, source, translator):
1248 """
1259 """
1249 Check if `target` and `source` have compatible requirements.
1260 Check if `target` and `source` have compatible requirements.
1250
1261
1251 Currently this is just checking for largefiles.
1262 Currently this is just checking for largefiles.
1252 """
1263 """
1253 _ = translator
1264 _ = translator
1254 target_has_largefiles = self._has_largefiles(target)
1265 target_has_largefiles = self._has_largefiles(target)
1255 source_has_largefiles = self._has_largefiles(source)
1266 source_has_largefiles = self._has_largefiles(source)
1256 merge_possible = True
1267 merge_possible = True
1257 message = u''
1268 message = u''
1258
1269
1259 if target_has_largefiles != source_has_largefiles:
1270 if target_has_largefiles != source_has_largefiles:
1260 merge_possible = False
1271 merge_possible = False
1261 if source_has_largefiles:
1272 if source_has_largefiles:
1262 message = _(
1273 message = _(
1263 'Target repository large files support is disabled.')
1274 'Target repository large files support is disabled.')
1264 else:
1275 else:
1265 message = _(
1276 message = _(
1266 'Source repository large files support is disabled.')
1277 'Source repository large files support is disabled.')
1267
1278
1268 return merge_possible, message
1279 return merge_possible, message
1269
1280
1270 def _has_largefiles(self, repo):
1281 def _has_largefiles(self, repo):
1271 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1282 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1272 'extensions', 'largefiles')
1283 'extensions', 'largefiles')
1273 return largefiles_ui and largefiles_ui[0].active
1284 return largefiles_ui and largefiles_ui[0].active
1274
1285
1275 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1286 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1276 """
1287 """
1277 Try to merge the pull request and return the merge status.
1288 Try to merge the pull request and return the merge status.
1278 """
1289 """
1279 log.debug(
1290 log.debug(
1280 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1291 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1281 pull_request.pull_request_id, force_shadow_repo_refresh)
1292 pull_request.pull_request_id, force_shadow_repo_refresh)
1282 target_vcs = pull_request.target_repo.scm_instance()
1293 target_vcs = pull_request.target_repo.scm_instance()
1283 # Refresh the target reference.
1294 # Refresh the target reference.
1284 try:
1295 try:
1285 target_ref = self._refresh_reference(
1296 target_ref = self._refresh_reference(
1286 pull_request.target_ref_parts, target_vcs)
1297 pull_request.target_ref_parts, target_vcs)
1287 except CommitDoesNotExistError:
1298 except CommitDoesNotExistError:
1288 merge_state = MergeResponse(
1299 merge_state = MergeResponse(
1289 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1300 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1290 metadata={'target_ref': pull_request.target_ref_parts})
1301 metadata={'target_ref': pull_request.target_ref_parts})
1291 return merge_state
1302 return merge_state
1292
1303
1293 target_locked = pull_request.target_repo.locked
1304 target_locked = pull_request.target_repo.locked
1294 if target_locked and target_locked[0]:
1305 if target_locked and target_locked[0]:
1295 locked_by = 'user:{}'.format(target_locked[0])
1306 locked_by = 'user:{}'.format(target_locked[0])
1296 log.debug("The target repository is locked by %s.", locked_by)
1307 log.debug("The target repository is locked by %s.", locked_by)
1297 merge_state = MergeResponse(
1308 merge_state = MergeResponse(
1298 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1309 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1299 metadata={'locked_by': locked_by})
1310 metadata={'locked_by': locked_by})
1300 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1311 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1301 pull_request, target_ref):
1312 pull_request, target_ref):
1302 log.debug("Refreshing the merge status of the repository.")
1313 log.debug("Refreshing the merge status of the repository.")
1303 merge_state = self._refresh_merge_state(
1314 merge_state = self._refresh_merge_state(
1304 pull_request, target_vcs, target_ref)
1315 pull_request, target_vcs, target_ref)
1305 else:
1316 else:
1306 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1317 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1307 metadata = {
1318 metadata = {
1308 'target_ref': pull_request.target_ref_parts,
1319 'target_ref': pull_request.target_ref_parts,
1309 'source_ref': pull_request.source_ref_parts
1320 'source_ref': pull_request.source_ref_parts
1310 }
1321 }
1311 merge_state = MergeResponse(
1322 merge_state = MergeResponse(
1312 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1323 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1313
1324
1314 return merge_state
1325 return merge_state
1315
1326
1316 def _refresh_reference(self, reference, vcs_repository):
1327 def _refresh_reference(self, reference, vcs_repository):
1317 if reference.type in self.UPDATABLE_REF_TYPES:
1328 if reference.type in self.UPDATABLE_REF_TYPES:
1318 name_or_id = reference.name
1329 name_or_id = reference.name
1319 else:
1330 else:
1320 name_or_id = reference.commit_id
1331 name_or_id = reference.commit_id
1321 refreshed_commit = vcs_repository.get_commit(name_or_id)
1332 refreshed_commit = vcs_repository.get_commit(name_or_id)
1322 refreshed_reference = Reference(
1333 refreshed_reference = Reference(
1323 reference.type, reference.name, refreshed_commit.raw_id)
1334 reference.type, reference.name, refreshed_commit.raw_id)
1324 return refreshed_reference
1335 return refreshed_reference
1325
1336
1326 def _needs_merge_state_refresh(self, pull_request, target_reference):
1337 def _needs_merge_state_refresh(self, pull_request, target_reference):
1327 return not(
1338 return not(
1328 pull_request.revisions and
1339 pull_request.revisions and
1329 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1340 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1330 target_reference.commit_id == pull_request._last_merge_target_rev)
1341 target_reference.commit_id == pull_request._last_merge_target_rev)
1331
1342
1332 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1343 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1333 workspace_id = self._workspace_id(pull_request)
1344 workspace_id = self._workspace_id(pull_request)
1334 source_vcs = pull_request.source_repo.scm_instance()
1345 source_vcs = pull_request.source_repo.scm_instance()
1335 repo_id = pull_request.target_repo.repo_id
1346 repo_id = pull_request.target_repo.repo_id
1336 use_rebase = self._use_rebase_for_merging(pull_request)
1347 use_rebase = self._use_rebase_for_merging(pull_request)
1337 close_branch = self._close_branch_before_merging(pull_request)
1348 close_branch = self._close_branch_before_merging(pull_request)
1338 merge_state = target_vcs.merge(
1349 merge_state = target_vcs.merge(
1339 repo_id, workspace_id,
1350 repo_id, workspace_id,
1340 target_reference, source_vcs, pull_request.source_ref_parts,
1351 target_reference, source_vcs, pull_request.source_ref_parts,
1341 dry_run=True, use_rebase=use_rebase,
1352 dry_run=True, use_rebase=use_rebase,
1342 close_branch=close_branch)
1353 close_branch=close_branch)
1343
1354
1344 # Do not store the response if there was an unknown error.
1355 # Do not store the response if there was an unknown error.
1345 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1356 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1346 pull_request._last_merge_source_rev = \
1357 pull_request._last_merge_source_rev = \
1347 pull_request.source_ref_parts.commit_id
1358 pull_request.source_ref_parts.commit_id
1348 pull_request._last_merge_target_rev = target_reference.commit_id
1359 pull_request._last_merge_target_rev = target_reference.commit_id
1349 pull_request.last_merge_status = merge_state.failure_reason
1360 pull_request.last_merge_status = merge_state.failure_reason
1350 pull_request.shadow_merge_ref = merge_state.merge_ref
1361 pull_request.shadow_merge_ref = merge_state.merge_ref
1351 Session().add(pull_request)
1362 Session().add(pull_request)
1352 Session().commit()
1363 Session().commit()
1353
1364
1354 return merge_state
1365 return merge_state
1355
1366
1356 def _workspace_id(self, pull_request):
1367 def _workspace_id(self, pull_request):
1357 workspace_id = 'pr-%s' % pull_request.pull_request_id
1368 workspace_id = 'pr-%s' % pull_request.pull_request_id
1358 return workspace_id
1369 return workspace_id
1359
1370
1360 def generate_repo_data(self, repo, commit_id=None, branch=None,
1371 def generate_repo_data(self, repo, commit_id=None, branch=None,
1361 bookmark=None, translator=None):
1372 bookmark=None, translator=None):
1362 from rhodecode.model.repo import RepoModel
1373 from rhodecode.model.repo import RepoModel
1363
1374
1364 all_refs, selected_ref = \
1375 all_refs, selected_ref = \
1365 self._get_repo_pullrequest_sources(
1376 self._get_repo_pullrequest_sources(
1366 repo.scm_instance(), commit_id=commit_id,
1377 repo.scm_instance(), commit_id=commit_id,
1367 branch=branch, bookmark=bookmark, translator=translator)
1378 branch=branch, bookmark=bookmark, translator=translator)
1368
1379
1369 refs_select2 = []
1380 refs_select2 = []
1370 for element in all_refs:
1381 for element in all_refs:
1371 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1382 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1372 refs_select2.append({'text': element[1], 'children': children})
1383 refs_select2.append({'text': element[1], 'children': children})
1373
1384
1374 return {
1385 return {
1375 'user': {
1386 'user': {
1376 'user_id': repo.user.user_id,
1387 'user_id': repo.user.user_id,
1377 'username': repo.user.username,
1388 'username': repo.user.username,
1378 'firstname': repo.user.first_name,
1389 'firstname': repo.user.first_name,
1379 'lastname': repo.user.last_name,
1390 'lastname': repo.user.last_name,
1380 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1391 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1381 },
1392 },
1382 'name': repo.repo_name,
1393 'name': repo.repo_name,
1383 'link': RepoModel().get_url(repo),
1394 'link': RepoModel().get_url(repo),
1384 'description': h.chop_at_smart(repo.description_safe, '\n'),
1395 'description': h.chop_at_smart(repo.description_safe, '\n'),
1385 'refs': {
1396 'refs': {
1386 'all_refs': all_refs,
1397 'all_refs': all_refs,
1387 'selected_ref': selected_ref,
1398 'selected_ref': selected_ref,
1388 'select2_refs': refs_select2
1399 'select2_refs': refs_select2
1389 }
1400 }
1390 }
1401 }
1391
1402
1392 def generate_pullrequest_title(self, source, source_ref, target):
1403 def generate_pullrequest_title(self, source, source_ref, target):
1393 return u'{source}#{at_ref} to {target}'.format(
1404 return u'{source}#{at_ref} to {target}'.format(
1394 source=source,
1405 source=source,
1395 at_ref=source_ref,
1406 at_ref=source_ref,
1396 target=target,
1407 target=target,
1397 )
1408 )
1398
1409
1399 def _cleanup_merge_workspace(self, pull_request):
1410 def _cleanup_merge_workspace(self, pull_request):
1400 # Merging related cleanup
1411 # Merging related cleanup
1401 repo_id = pull_request.target_repo.repo_id
1412 repo_id = pull_request.target_repo.repo_id
1402 target_scm = pull_request.target_repo.scm_instance()
1413 target_scm = pull_request.target_repo.scm_instance()
1403 workspace_id = self._workspace_id(pull_request)
1414 workspace_id = self._workspace_id(pull_request)
1404
1415
1405 try:
1416 try:
1406 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1417 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1407 except NotImplementedError:
1418 except NotImplementedError:
1408 pass
1419 pass
1409
1420
1410 def _get_repo_pullrequest_sources(
1421 def _get_repo_pullrequest_sources(
1411 self, repo, commit_id=None, branch=None, bookmark=None,
1422 self, repo, commit_id=None, branch=None, bookmark=None,
1412 translator=None):
1423 translator=None):
1413 """
1424 """
1414 Return a structure with repo's interesting commits, suitable for
1425 Return a structure with repo's interesting commits, suitable for
1415 the selectors in pullrequest controller
1426 the selectors in pullrequest controller
1416
1427
1417 :param commit_id: a commit that must be in the list somehow
1428 :param commit_id: a commit that must be in the list somehow
1418 and selected by default
1429 and selected by default
1419 :param branch: a branch that must be in the list and selected
1430 :param branch: a branch that must be in the list and selected
1420 by default - even if closed
1431 by default - even if closed
1421 :param bookmark: a bookmark that must be in the list and selected
1432 :param bookmark: a bookmark that must be in the list and selected
1422 """
1433 """
1423 _ = translator or get_current_request().translate
1434 _ = translator or get_current_request().translate
1424
1435
1425 commit_id = safe_str(commit_id) if commit_id else None
1436 commit_id = safe_str(commit_id) if commit_id else None
1426 branch = safe_unicode(branch) if branch else None
1437 branch = safe_unicode(branch) if branch else None
1427 bookmark = safe_unicode(bookmark) if bookmark else None
1438 bookmark = safe_unicode(bookmark) if bookmark else None
1428
1439
1429 selected = None
1440 selected = None
1430
1441
1431 # order matters: first source that has commit_id in it will be selected
1442 # order matters: first source that has commit_id in it will be selected
1432 sources = []
1443 sources = []
1433 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1444 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1434 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1445 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1435
1446
1436 if commit_id:
1447 if commit_id:
1437 ref_commit = (h.short_id(commit_id), commit_id)
1448 ref_commit = (h.short_id(commit_id), commit_id)
1438 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1449 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1439
1450
1440 sources.append(
1451 sources.append(
1441 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1452 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1442 )
1453 )
1443
1454
1444 groups = []
1455 groups = []
1445
1456
1446 for group_key, ref_list, group_name, match in sources:
1457 for group_key, ref_list, group_name, match in sources:
1447 group_refs = []
1458 group_refs = []
1448 for ref_name, ref_id in ref_list:
1459 for ref_name, ref_id in ref_list:
1449 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1460 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1450 group_refs.append((ref_key, ref_name))
1461 group_refs.append((ref_key, ref_name))
1451
1462
1452 if not selected:
1463 if not selected:
1453 if set([commit_id, match]) & set([ref_id, ref_name]):
1464 if set([commit_id, match]) & set([ref_id, ref_name]):
1454 selected = ref_key
1465 selected = ref_key
1455
1466
1456 if group_refs:
1467 if group_refs:
1457 groups.append((group_refs, group_name))
1468 groups.append((group_refs, group_name))
1458
1469
1459 if not selected:
1470 if not selected:
1460 ref = commit_id or branch or bookmark
1471 ref = commit_id or branch or bookmark
1461 if ref:
1472 if ref:
1462 raise CommitDoesNotExistError(
1473 raise CommitDoesNotExistError(
1463 u'No commit refs could be found matching: {}'.format(ref))
1474 u'No commit refs could be found matching: {}'.format(ref))
1464 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1475 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1465 selected = u'branch:{}:{}'.format(
1476 selected = u'branch:{}:{}'.format(
1466 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1477 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1467 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1478 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1468 )
1479 )
1469 elif repo.commit_ids:
1480 elif repo.commit_ids:
1470 # make the user select in this case
1481 # make the user select in this case
1471 selected = None
1482 selected = None
1472 else:
1483 else:
1473 raise EmptyRepositoryError()
1484 raise EmptyRepositoryError()
1474 return groups, selected
1485 return groups, selected
1475
1486
1476 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1487 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1477 hide_whitespace_changes, diff_context):
1488 hide_whitespace_changes, diff_context):
1478
1489
1479 return self._get_diff_from_pr_or_version(
1490 return self._get_diff_from_pr_or_version(
1480 source_repo, source_ref_id, target_ref_id,
1491 source_repo, source_ref_id, target_ref_id,
1481 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1492 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1482
1493
1483 def _get_diff_from_pr_or_version(
1494 def _get_diff_from_pr_or_version(
1484 self, source_repo, source_ref_id, target_ref_id,
1495 self, source_repo, source_ref_id, target_ref_id,
1485 hide_whitespace_changes, diff_context):
1496 hide_whitespace_changes, diff_context):
1486
1497
1487 target_commit = source_repo.get_commit(
1498 target_commit = source_repo.get_commit(
1488 commit_id=safe_str(target_ref_id))
1499 commit_id=safe_str(target_ref_id))
1489 source_commit = source_repo.get_commit(
1500 source_commit = source_repo.get_commit(
1490 commit_id=safe_str(source_ref_id))
1501 commit_id=safe_str(source_ref_id))
1491 if isinstance(source_repo, Repository):
1502 if isinstance(source_repo, Repository):
1492 vcs_repo = source_repo.scm_instance()
1503 vcs_repo = source_repo.scm_instance()
1493 else:
1504 else:
1494 vcs_repo = source_repo
1505 vcs_repo = source_repo
1495
1506
1496 # TODO: johbo: In the context of an update, we cannot reach
1507 # TODO: johbo: In the context of an update, we cannot reach
1497 # the old commit anymore with our normal mechanisms. It needs
1508 # the old commit anymore with our normal mechanisms. It needs
1498 # some sort of special support in the vcs layer to avoid this
1509 # some sort of special support in the vcs layer to avoid this
1499 # workaround.
1510 # workaround.
1500 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1511 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1501 vcs_repo.alias == 'git'):
1512 vcs_repo.alias == 'git'):
1502 source_commit.raw_id = safe_str(source_ref_id)
1513 source_commit.raw_id = safe_str(source_ref_id)
1503
1514
1504 log.debug('calculating diff between '
1515 log.debug('calculating diff between '
1505 'source_ref:%s and target_ref:%s for repo `%s`',
1516 'source_ref:%s and target_ref:%s for repo `%s`',
1506 target_ref_id, source_ref_id,
1517 target_ref_id, source_ref_id,
1507 safe_unicode(vcs_repo.path))
1518 safe_unicode(vcs_repo.path))
1508
1519
1509 vcs_diff = vcs_repo.get_diff(
1520 vcs_diff = vcs_repo.get_diff(
1510 commit1=target_commit, commit2=source_commit,
1521 commit1=target_commit, commit2=source_commit,
1511 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1522 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1512 return vcs_diff
1523 return vcs_diff
1513
1524
1514 def _is_merge_enabled(self, pull_request):
1525 def _is_merge_enabled(self, pull_request):
1515 return self._get_general_setting(
1526 return self._get_general_setting(
1516 pull_request, 'rhodecode_pr_merge_enabled')
1527 pull_request, 'rhodecode_pr_merge_enabled')
1517
1528
1518 def _use_rebase_for_merging(self, pull_request):
1529 def _use_rebase_for_merging(self, pull_request):
1519 repo_type = pull_request.target_repo.repo_type
1530 repo_type = pull_request.target_repo.repo_type
1520 if repo_type == 'hg':
1531 if repo_type == 'hg':
1521 return self._get_general_setting(
1532 return self._get_general_setting(
1522 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1533 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1523 elif repo_type == 'git':
1534 elif repo_type == 'git':
1524 return self._get_general_setting(
1535 return self._get_general_setting(
1525 pull_request, 'rhodecode_git_use_rebase_for_merging')
1536 pull_request, 'rhodecode_git_use_rebase_for_merging')
1526
1537
1527 return False
1538 return False
1528
1539
1529 def _close_branch_before_merging(self, pull_request):
1540 def _close_branch_before_merging(self, pull_request):
1530 repo_type = pull_request.target_repo.repo_type
1541 repo_type = pull_request.target_repo.repo_type
1531 if repo_type == 'hg':
1542 if repo_type == 'hg':
1532 return self._get_general_setting(
1543 return self._get_general_setting(
1533 pull_request, 'rhodecode_hg_close_branch_before_merging')
1544 pull_request, 'rhodecode_hg_close_branch_before_merging')
1534 elif repo_type == 'git':
1545 elif repo_type == 'git':
1535 return self._get_general_setting(
1546 return self._get_general_setting(
1536 pull_request, 'rhodecode_git_close_branch_before_merging')
1547 pull_request, 'rhodecode_git_close_branch_before_merging')
1537
1548
1538 return False
1549 return False
1539
1550
1540 def _get_general_setting(self, pull_request, settings_key, default=False):
1551 def _get_general_setting(self, pull_request, settings_key, default=False):
1541 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1552 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1542 settings = settings_model.get_general_settings()
1553 settings = settings_model.get_general_settings()
1543 return settings.get(settings_key, default)
1554 return settings.get(settings_key, default)
1544
1555
1545 def _log_audit_action(self, action, action_data, user, pull_request):
1556 def _log_audit_action(self, action, action_data, user, pull_request):
1546 audit_logger.store(
1557 audit_logger.store(
1547 action=action,
1558 action=action,
1548 action_data=action_data,
1559 action_data=action_data,
1549 user=user,
1560 user=user,
1550 repo=pull_request.target_repo)
1561 repo=pull_request.target_repo)
1551
1562
1552 def get_reviewer_functions(self):
1563 def get_reviewer_functions(self):
1553 """
1564 """
1554 Fetches functions for validation and fetching default reviewers.
1565 Fetches functions for validation and fetching default reviewers.
1555 If available we use the EE package, else we fallback to CE
1566 If available we use the EE package, else we fallback to CE
1556 package functions
1567 package functions
1557 """
1568 """
1558 try:
1569 try:
1559 from rc_reviewers.utils import get_default_reviewers_data
1570 from rc_reviewers.utils import get_default_reviewers_data
1560 from rc_reviewers.utils import validate_default_reviewers
1571 from rc_reviewers.utils import validate_default_reviewers
1561 except ImportError:
1572 except ImportError:
1562 from rhodecode.apps.repository.utils import get_default_reviewers_data
1573 from rhodecode.apps.repository.utils import get_default_reviewers_data
1563 from rhodecode.apps.repository.utils import validate_default_reviewers
1574 from rhodecode.apps.repository.utils import validate_default_reviewers
1564
1575
1565 return get_default_reviewers_data, validate_default_reviewers
1576 return get_default_reviewers_data, validate_default_reviewers
1566
1577
1567
1578
1568 class MergeCheck(object):
1579 class MergeCheck(object):
1569 """
1580 """
1570 Perform Merge Checks and returns a check object which stores information
1581 Perform Merge Checks and returns a check object which stores information
1571 about merge errors, and merge conditions
1582 about merge errors, and merge conditions
1572 """
1583 """
1573 TODO_CHECK = 'todo'
1584 TODO_CHECK = 'todo'
1574 PERM_CHECK = 'perm'
1585 PERM_CHECK = 'perm'
1575 REVIEW_CHECK = 'review'
1586 REVIEW_CHECK = 'review'
1576 MERGE_CHECK = 'merge'
1587 MERGE_CHECK = 'merge'
1577
1588
1578 def __init__(self):
1589 def __init__(self):
1579 self.review_status = None
1590 self.review_status = None
1580 self.merge_possible = None
1591 self.merge_possible = None
1581 self.merge_msg = ''
1592 self.merge_msg = ''
1582 self.failed = None
1593 self.failed = None
1583 self.errors = []
1594 self.errors = []
1584 self.error_details = OrderedDict()
1595 self.error_details = OrderedDict()
1585
1596
1586 def push_error(self, error_type, message, error_key, details):
1597 def push_error(self, error_type, message, error_key, details):
1587 self.failed = True
1598 self.failed = True
1588 self.errors.append([error_type, message])
1599 self.errors.append([error_type, message])
1589 self.error_details[error_key] = dict(
1600 self.error_details[error_key] = dict(
1590 details=details,
1601 details=details,
1591 error_type=error_type,
1602 error_type=error_type,
1592 message=message
1603 message=message
1593 )
1604 )
1594
1605
1595 @classmethod
1606 @classmethod
1596 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1607 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1597 force_shadow_repo_refresh=False):
1608 force_shadow_repo_refresh=False):
1598 _ = translator
1609 _ = translator
1599 merge_check = cls()
1610 merge_check = cls()
1600
1611
1601 # permissions to merge
1612 # permissions to merge
1602 user_allowed_to_merge = PullRequestModel().check_user_merge(
1613 user_allowed_to_merge = PullRequestModel().check_user_merge(
1603 pull_request, auth_user)
1614 pull_request, auth_user)
1604 if not user_allowed_to_merge:
1615 if not user_allowed_to_merge:
1605 log.debug("MergeCheck: cannot merge, approval is pending.")
1616 log.debug("MergeCheck: cannot merge, approval is pending.")
1606
1617
1607 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1618 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1608 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1619 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1609 if fail_early:
1620 if fail_early:
1610 return merge_check
1621 return merge_check
1611
1622
1612 # permission to merge into the target branch
1623 # permission to merge into the target branch
1613 target_commit_id = pull_request.target_ref_parts.commit_id
1624 target_commit_id = pull_request.target_ref_parts.commit_id
1614 if pull_request.target_ref_parts.type == 'branch':
1625 if pull_request.target_ref_parts.type == 'branch':
1615 branch_name = pull_request.target_ref_parts.name
1626 branch_name = pull_request.target_ref_parts.name
1616 else:
1627 else:
1617 # for mercurial we can always figure out the branch from the commit
1628 # for mercurial we can always figure out the branch from the commit
1618 # in case of bookmark
1629 # in case of bookmark
1619 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1630 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1620 branch_name = target_commit.branch
1631 branch_name = target_commit.branch
1621
1632
1622 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1633 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1623 pull_request.target_repo.repo_name, branch_name)
1634 pull_request.target_repo.repo_name, branch_name)
1624 if branch_perm and branch_perm == 'branch.none':
1635 if branch_perm and branch_perm == 'branch.none':
1625 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1636 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1626 branch_name, rule)
1637 branch_name, rule)
1627 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1638 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1628 if fail_early:
1639 if fail_early:
1629 return merge_check
1640 return merge_check
1630
1641
1631 # review status, must be always present
1642 # review status, must be always present
1632 review_status = pull_request.calculated_review_status()
1643 review_status = pull_request.calculated_review_status()
1633 merge_check.review_status = review_status
1644 merge_check.review_status = review_status
1634
1645
1635 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1646 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1636 if not status_approved:
1647 if not status_approved:
1637 log.debug("MergeCheck: cannot merge, approval is pending.")
1648 log.debug("MergeCheck: cannot merge, approval is pending.")
1638
1649
1639 msg = _('Pull request reviewer approval is pending.')
1650 msg = _('Pull request reviewer approval is pending.')
1640
1651
1641 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1652 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1642
1653
1643 if fail_early:
1654 if fail_early:
1644 return merge_check
1655 return merge_check
1645
1656
1646 # left over TODOs
1657 # left over TODOs
1647 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1658 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1648 if todos:
1659 if todos:
1649 log.debug("MergeCheck: cannot merge, {} "
1660 log.debug("MergeCheck: cannot merge, {} "
1650 "unresolved TODOs left.".format(len(todos)))
1661 "unresolved TODOs left.".format(len(todos)))
1651
1662
1652 if len(todos) == 1:
1663 if len(todos) == 1:
1653 msg = _('Cannot merge, {} TODO still not resolved.').format(
1664 msg = _('Cannot merge, {} TODO still not resolved.').format(
1654 len(todos))
1665 len(todos))
1655 else:
1666 else:
1656 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1667 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1657 len(todos))
1668 len(todos))
1658
1669
1659 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1670 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1660
1671
1661 if fail_early:
1672 if fail_early:
1662 return merge_check
1673 return merge_check
1663
1674
1664 # merge possible, here is the filesystem simulation + shadow repo
1675 # merge possible, here is the filesystem simulation + shadow repo
1665 merge_status, msg = PullRequestModel().merge_status(
1676 merge_status, msg = PullRequestModel().merge_status(
1666 pull_request, translator=translator,
1677 pull_request, translator=translator,
1667 force_shadow_repo_refresh=force_shadow_repo_refresh)
1678 force_shadow_repo_refresh=force_shadow_repo_refresh)
1668 merge_check.merge_possible = merge_status
1679 merge_check.merge_possible = merge_status
1669 merge_check.merge_msg = msg
1680 merge_check.merge_msg = msg
1670 if not merge_status:
1681 if not merge_status:
1671 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1682 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
1672 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1683 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1673
1684
1674 if fail_early:
1685 if fail_early:
1675 return merge_check
1686 return merge_check
1676
1687
1677 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1688 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1678 return merge_check
1689 return merge_check
1679
1690
1680 @classmethod
1691 @classmethod
1681 def get_merge_conditions(cls, pull_request, translator):
1692 def get_merge_conditions(cls, pull_request, translator):
1682 _ = translator
1693 _ = translator
1683 merge_details = {}
1694 merge_details = {}
1684
1695
1685 model = PullRequestModel()
1696 model = PullRequestModel()
1686 use_rebase = model._use_rebase_for_merging(pull_request)
1697 use_rebase = model._use_rebase_for_merging(pull_request)
1687
1698
1688 if use_rebase:
1699 if use_rebase:
1689 merge_details['merge_strategy'] = dict(
1700 merge_details['merge_strategy'] = dict(
1690 details={},
1701 details={},
1691 message=_('Merge strategy: rebase')
1702 message=_('Merge strategy: rebase')
1692 )
1703 )
1693 else:
1704 else:
1694 merge_details['merge_strategy'] = dict(
1705 merge_details['merge_strategy'] = dict(
1695 details={},
1706 details={},
1696 message=_('Merge strategy: explicit merge commit')
1707 message=_('Merge strategy: explicit merge commit')
1697 )
1708 )
1698
1709
1699 close_branch = model._close_branch_before_merging(pull_request)
1710 close_branch = model._close_branch_before_merging(pull_request)
1700 if close_branch:
1711 if close_branch:
1701 repo_type = pull_request.target_repo.repo_type
1712 repo_type = pull_request.target_repo.repo_type
1702 close_msg = ''
1713 close_msg = ''
1703 if repo_type == 'hg':
1714 if repo_type == 'hg':
1704 close_msg = _('Source branch will be closed after merge.')
1715 close_msg = _('Source branch will be closed after merge.')
1705 elif repo_type == 'git':
1716 elif repo_type == 'git':
1706 close_msg = _('Source branch will be deleted after merge.')
1717 close_msg = _('Source branch will be deleted after merge.')
1707
1718
1708 merge_details['close_branch'] = dict(
1719 merge_details['close_branch'] = dict(
1709 details={},
1720 details={},
1710 message=close_msg
1721 message=close_msg
1711 )
1722 )
1712
1723
1713 return merge_details
1724 return merge_details
1714
1725
1715
1726
1716 ChangeTuple = collections.namedtuple(
1727 ChangeTuple = collections.namedtuple(
1717 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1728 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1718
1729
1719 FileChangeTuple = collections.namedtuple(
1730 FileChangeTuple = collections.namedtuple(
1720 'FileChangeTuple', ['added', 'modified', 'removed'])
1731 'FileChangeTuple', ['added', 'modified', 'removed'])
General Comments 0
You need to be logged in to leave comments. Login now