##// END OF EJS Templates
release: merge back stable branch into default
marcink -
r3352:ac6efde5 merge default
parent child Browse files
Show More
@@ -0,0 +1,50 b''
1 |RCE| 4.15.1 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2019-01-01
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18 - Downloads: properly encode " in the filenames, and add RFC 5987 header for non-ascii files.
19 - Documentation: updated configuration for Nginx and reverse proxy.
20 - VCS: streaming will use now 100kb chunks for faster network throughput.
21
22
23 Security
24 ^^^^^^^^
25
26 - Diffs: fixed xss in context diff menu.
27 - Downloads: properly encode " in the filenames, prevents from hiding executable
28 files disguised in another type of file using crafted file names.
29
30 Performance
31 ^^^^^^^^^^^
32
33
34
35 Fixes
36 ^^^^^
37
38 - VCS: handle excessive slashes in from of the repo name path, fixes #5522.
39 This prevents 500 errors when excessive slashes are used
40 - SVN: support proxy-prefix properly, fixes #5521.
41 - Pull requests: validate ref types on API calls for pull request so users cannot
42 provide wrongs ones.
43 - Scheduler: fix url generation with proxy prefix.
44 - Celery: add DB connection ping to validate DB connection is working at worker startup.
45
46
47 Upgrade notes
48 ^^^^^^^^^^^^^
49
50 - Scheduled release addressing reported problems in 4.15.X releases.
@@ -1,49 +1,50 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
@@ -1,126 +1,127 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.15.1.rst
12 release-notes-4.15.0.rst
13 release-notes-4.15.0.rst
13 release-notes-4.14.1.rst
14 release-notes-4.14.1.rst
14 release-notes-4.14.0.rst
15 release-notes-4.14.0.rst
15 release-notes-4.13.3.rst
16 release-notes-4.13.3.rst
16 release-notes-4.13.2.rst
17 release-notes-4.13.2.rst
17 release-notes-4.13.1.rst
18 release-notes-4.13.1.rst
18 release-notes-4.13.0.rst
19 release-notes-4.13.0.rst
19 release-notes-4.12.4.rst
20 release-notes-4.12.4.rst
20 release-notes-4.12.3.rst
21 release-notes-4.12.3.rst
21 release-notes-4.12.2.rst
22 release-notes-4.12.2.rst
22 release-notes-4.12.1.rst
23 release-notes-4.12.1.rst
23 release-notes-4.12.0.rst
24 release-notes-4.12.0.rst
24 release-notes-4.11.6.rst
25 release-notes-4.11.6.rst
25 release-notes-4.11.5.rst
26 release-notes-4.11.5.rst
26 release-notes-4.11.4.rst
27 release-notes-4.11.4.rst
27 release-notes-4.11.3.rst
28 release-notes-4.11.3.rst
28 release-notes-4.11.2.rst
29 release-notes-4.11.2.rst
29 release-notes-4.11.1.rst
30 release-notes-4.11.1.rst
30 release-notes-4.11.0.rst
31 release-notes-4.11.0.rst
31 release-notes-4.10.6.rst
32 release-notes-4.10.6.rst
32 release-notes-4.10.5.rst
33 release-notes-4.10.5.rst
33 release-notes-4.10.4.rst
34 release-notes-4.10.4.rst
34 release-notes-4.10.3.rst
35 release-notes-4.10.3.rst
35 release-notes-4.10.2.rst
36 release-notes-4.10.2.rst
36 release-notes-4.10.1.rst
37 release-notes-4.10.1.rst
37 release-notes-4.10.0.rst
38 release-notes-4.10.0.rst
38 release-notes-4.9.1.rst
39 release-notes-4.9.1.rst
39 release-notes-4.9.0.rst
40 release-notes-4.9.0.rst
40 release-notes-4.8.0.rst
41 release-notes-4.8.0.rst
41 release-notes-4.7.2.rst
42 release-notes-4.7.2.rst
42 release-notes-4.7.1.rst
43 release-notes-4.7.1.rst
43 release-notes-4.7.0.rst
44 release-notes-4.7.0.rst
44 release-notes-4.6.1.rst
45 release-notes-4.6.1.rst
45 release-notes-4.6.0.rst
46 release-notes-4.6.0.rst
46 release-notes-4.5.2.rst
47 release-notes-4.5.2.rst
47 release-notes-4.5.1.rst
48 release-notes-4.5.1.rst
48 release-notes-4.5.0.rst
49 release-notes-4.5.0.rst
49 release-notes-4.4.2.rst
50 release-notes-4.4.2.rst
50 release-notes-4.4.1.rst
51 release-notes-4.4.1.rst
51 release-notes-4.4.0.rst
52 release-notes-4.4.0.rst
52 release-notes-4.3.1.rst
53 release-notes-4.3.1.rst
53 release-notes-4.3.0.rst
54 release-notes-4.3.0.rst
54 release-notes-4.2.1.rst
55 release-notes-4.2.1.rst
55 release-notes-4.2.0.rst
56 release-notes-4.2.0.rst
56 release-notes-4.1.2.rst
57 release-notes-4.1.2.rst
57 release-notes-4.1.1.rst
58 release-notes-4.1.1.rst
58 release-notes-4.1.0.rst
59 release-notes-4.1.0.rst
59 release-notes-4.0.1.rst
60 release-notes-4.0.1.rst
60 release-notes-4.0.0.rst
61 release-notes-4.0.0.rst
61
62
62 |RCE| 3.x Versions
63 |RCE| 3.x Versions
63 ------------------
64 ------------------
64
65
65 .. toctree::
66 .. toctree::
66 :maxdepth: 1
67 :maxdepth: 1
67
68
68 release-notes-3.8.4.rst
69 release-notes-3.8.4.rst
69 release-notes-3.8.3.rst
70 release-notes-3.8.3.rst
70 release-notes-3.8.2.rst
71 release-notes-3.8.2.rst
71 release-notes-3.8.1.rst
72 release-notes-3.8.1.rst
72 release-notes-3.8.0.rst
73 release-notes-3.8.0.rst
73 release-notes-3.7.1.rst
74 release-notes-3.7.1.rst
74 release-notes-3.7.0.rst
75 release-notes-3.7.0.rst
75 release-notes-3.6.1.rst
76 release-notes-3.6.1.rst
76 release-notes-3.6.0.rst
77 release-notes-3.6.0.rst
77 release-notes-3.5.2.rst
78 release-notes-3.5.2.rst
78 release-notes-3.5.1.rst
79 release-notes-3.5.1.rst
79 release-notes-3.5.0.rst
80 release-notes-3.5.0.rst
80 release-notes-3.4.1.rst
81 release-notes-3.4.1.rst
81 release-notes-3.4.0.rst
82 release-notes-3.4.0.rst
82 release-notes-3.3.4.rst
83 release-notes-3.3.4.rst
83 release-notes-3.3.3.rst
84 release-notes-3.3.3.rst
84 release-notes-3.3.2.rst
85 release-notes-3.3.2.rst
85 release-notes-3.3.1.rst
86 release-notes-3.3.1.rst
86 release-notes-3.3.0.rst
87 release-notes-3.3.0.rst
87 release-notes-3.2.3.rst
88 release-notes-3.2.3.rst
88 release-notes-3.2.2.rst
89 release-notes-3.2.2.rst
89 release-notes-3.2.1.rst
90 release-notes-3.2.1.rst
90 release-notes-3.2.0.rst
91 release-notes-3.2.0.rst
91 release-notes-3.1.1.rst
92 release-notes-3.1.1.rst
92 release-notes-3.1.0.rst
93 release-notes-3.1.0.rst
93 release-notes-3.0.2.rst
94 release-notes-3.0.2.rst
94 release-notes-3.0.1.rst
95 release-notes-3.0.1.rst
95 release-notes-3.0.0.rst
96 release-notes-3.0.0.rst
96
97
97 |RCE| 2.x Versions
98 |RCE| 2.x Versions
98 ------------------
99 ------------------
99
100
100 .. toctree::
101 .. toctree::
101 :maxdepth: 1
102 :maxdepth: 1
102
103
103 release-notes-2.2.8.rst
104 release-notes-2.2.8.rst
104 release-notes-2.2.7.rst
105 release-notes-2.2.7.rst
105 release-notes-2.2.6.rst
106 release-notes-2.2.6.rst
106 release-notes-2.2.5.rst
107 release-notes-2.2.5.rst
107 release-notes-2.2.4.rst
108 release-notes-2.2.4.rst
108 release-notes-2.2.3.rst
109 release-notes-2.2.3.rst
109 release-notes-2.2.2.rst
110 release-notes-2.2.2.rst
110 release-notes-2.2.1.rst
111 release-notes-2.2.1.rst
111 release-notes-2.2.0.rst
112 release-notes-2.2.0.rst
112 release-notes-2.1.0.rst
113 release-notes-2.1.0.rst
113 release-notes-2.0.2.rst
114 release-notes-2.0.2.rst
114 release-notes-2.0.1.rst
115 release-notes-2.0.1.rst
115 release-notes-2.0.0.rst
116 release-notes-2.0.0.rst
116
117
117 |RCE| 1.x Versions
118 |RCE| 1.x Versions
118 ------------------
119 ------------------
119
120
120 .. toctree::
121 .. toctree::
121 :maxdepth: 1
122 :maxdepth: 1
122
123
123 release-notes-1.7.2.rst
124 release-notes-1.7.2.rst
124 release-notes-1.7.1.rst
125 release-notes-1.7.1.rst
125 release-notes-1.7.0.rst
126 release-notes-1.7.0.rst
126 release-notes-1.6.0.rst
127 release-notes-1.6.0.rst
@@ -1,349 +1,368 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.model.db import User
23 from rhodecode.model.db import User
24 from rhodecode.model.pull_request import PullRequestModel
24 from rhodecode.model.pull_request import PullRequestModel
25 from rhodecode.model.repo import RepoModel
25 from rhodecode.model.repo import RepoModel
26 from rhodecode.model.user import UserModel
26 from rhodecode.model.user import UserModel
27 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
27 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN
28 from rhodecode.api.tests.utils import build_data, api_call, assert_error
28 from rhodecode.api.tests.utils import build_data, api_call, assert_error
29
29
30
30
31 @pytest.mark.usefixtures("testuser_api", "app")
31 @pytest.mark.usefixtures("testuser_api", "app")
32 class TestCreatePullRequestApi(object):
32 class TestCreatePullRequestApi(object):
33 finalizers = []
33 finalizers = []
34
34
35 def teardown_method(self, method):
35 def teardown_method(self, method):
36 if self.finalizers:
36 if self.finalizers:
37 for finalizer in self.finalizers:
37 for finalizer in self.finalizers:
38 finalizer()
38 finalizer()
39 self.finalizers = []
39 self.finalizers = []
40
40
41 def test_create_with_wrong_data(self):
41 def test_create_with_wrong_data(self):
42 required_data = {
42 required_data = {
43 'source_repo': 'tests/source_repo',
43 'source_repo': 'tests/source_repo',
44 'target_repo': 'tests/target_repo',
44 'target_repo': 'tests/target_repo',
45 'source_ref': 'branch:default:initial',
45 'source_ref': 'branch:default:initial',
46 'target_ref': 'branch:default:new-feature',
46 'target_ref': 'branch:default:new-feature',
47 }
47 }
48 for key in required_data:
48 for key in required_data:
49 data = required_data.copy()
49 data = required_data.copy()
50 data.pop(key)
50 data.pop(key)
51 id_, params = build_data(
51 id_, params = build_data(
52 self.apikey, 'create_pull_request', **data)
52 self.apikey, 'create_pull_request', **data)
53 response = api_call(self.app, params)
53 response = api_call(self.app, params)
54
54
55 expected = 'Missing non optional `{}` arg in JSON DATA'.format(key)
55 expected = 'Missing non optional `{}` arg in JSON DATA'.format(key)
56 assert_error(id_, expected, given=response.body)
56 assert_error(id_, expected, given=response.body)
57
57
58 @pytest.mark.backends("git", "hg")
58 @pytest.mark.backends("git", "hg")
59 @pytest.mark.parametrize('source_ref', [
60 'bookmarg:default:initial'
61 ])
62 def test_create_with_wrong_refs_data(self, backend, source_ref):
63
64 data = self._prepare_data(backend)
65 data['source_ref'] = source_ref
66
67 id_, params = build_data(
68 self.apikey_regular, 'create_pull_request', **data)
69
70 response = api_call(self.app, params)
71
72 expected = "Ref `{}` type is not allowed. " \
73 "Only:['bookmark', 'book', 'tag', 'branch'] " \
74 "are possible.".format(source_ref)
75 assert_error(id_, expected, given=response.body)
76
77 @pytest.mark.backends("git", "hg")
59 def test_create_with_correct_data(self, backend):
78 def test_create_with_correct_data(self, backend):
60 data = self._prepare_data(backend)
79 data = self._prepare_data(backend)
61 RepoModel().revoke_user_permission(
80 RepoModel().revoke_user_permission(
62 self.source.repo_name, User.DEFAULT_USER)
81 self.source.repo_name, User.DEFAULT_USER)
63 id_, params = build_data(
82 id_, params = build_data(
64 self.apikey_regular, 'create_pull_request', **data)
83 self.apikey_regular, 'create_pull_request', **data)
65 response = api_call(self.app, params)
84 response = api_call(self.app, params)
66 expected_message = "Created new pull request `{title}`".format(
85 expected_message = "Created new pull request `{title}`".format(
67 title=data['title'])
86 title=data['title'])
68 result = response.json
87 result = response.json
69 assert result['error'] is None
88 assert result['error'] is None
70 assert result['result']['msg'] == expected_message
89 assert result['result']['msg'] == expected_message
71 pull_request_id = result['result']['pull_request_id']
90 pull_request_id = result['result']['pull_request_id']
72 pull_request = PullRequestModel().get(pull_request_id)
91 pull_request = PullRequestModel().get(pull_request_id)
73 assert pull_request.title == data['title']
92 assert pull_request.title == data['title']
74 assert pull_request.description == data['description']
93 assert pull_request.description == data['description']
75 assert pull_request.source_ref == data['source_ref']
94 assert pull_request.source_ref == data['source_ref']
76 assert pull_request.target_ref == data['target_ref']
95 assert pull_request.target_ref == data['target_ref']
77 assert pull_request.source_repo.repo_name == data['source_repo']
96 assert pull_request.source_repo.repo_name == data['source_repo']
78 assert pull_request.target_repo.repo_name == data['target_repo']
97 assert pull_request.target_repo.repo_name == data['target_repo']
79 assert pull_request.revisions == [self.commit_ids['change']]
98 assert pull_request.revisions == [self.commit_ids['change']]
80 assert len(pull_request.reviewers) == 1
99 assert len(pull_request.reviewers) == 1
81
100
82 @pytest.mark.backends("git", "hg")
101 @pytest.mark.backends("git", "hg")
83 def test_create_with_empty_description(self, backend):
102 def test_create_with_empty_description(self, backend):
84 data = self._prepare_data(backend)
103 data = self._prepare_data(backend)
85 data.pop('description')
104 data.pop('description')
86 id_, params = build_data(
105 id_, params = build_data(
87 self.apikey_regular, 'create_pull_request', **data)
106 self.apikey_regular, 'create_pull_request', **data)
88 response = api_call(self.app, params)
107 response = api_call(self.app, params)
89 expected_message = "Created new pull request `{title}`".format(
108 expected_message = "Created new pull request `{title}`".format(
90 title=data['title'])
109 title=data['title'])
91 result = response.json
110 result = response.json
92 assert result['error'] is None
111 assert result['error'] is None
93 assert result['result']['msg'] == expected_message
112 assert result['result']['msg'] == expected_message
94 pull_request_id = result['result']['pull_request_id']
113 pull_request_id = result['result']['pull_request_id']
95 pull_request = PullRequestModel().get(pull_request_id)
114 pull_request = PullRequestModel().get(pull_request_id)
96 assert pull_request.description == ''
115 assert pull_request.description == ''
97
116
98 @pytest.mark.backends("git", "hg")
117 @pytest.mark.backends("git", "hg")
99 def test_create_with_empty_title(self, backend):
118 def test_create_with_empty_title(self, backend):
100 data = self._prepare_data(backend)
119 data = self._prepare_data(backend)
101 data.pop('title')
120 data.pop('title')
102 id_, params = build_data(
121 id_, params = build_data(
103 self.apikey_regular, 'create_pull_request', **data)
122 self.apikey_regular, 'create_pull_request', **data)
104 response = api_call(self.app, params)
123 response = api_call(self.app, params)
105 result = response.json
124 result = response.json
106 pull_request_id = result['result']['pull_request_id']
125 pull_request_id = result['result']['pull_request_id']
107 pull_request = PullRequestModel().get(pull_request_id)
126 pull_request = PullRequestModel().get(pull_request_id)
108 data['ref'] = backend.default_branch_name
127 data['ref'] = backend.default_branch_name
109 title = '{source_repo}#{ref} to {target_repo}'.format(**data)
128 title = '{source_repo}#{ref} to {target_repo}'.format(**data)
110 assert pull_request.title == title
129 assert pull_request.title == title
111
130
112 @pytest.mark.backends("git", "hg")
131 @pytest.mark.backends("git", "hg")
113 def test_create_with_reviewers_specified_by_names(
132 def test_create_with_reviewers_specified_by_names(
114 self, backend, no_notifications):
133 self, backend, no_notifications):
115 data = self._prepare_data(backend)
134 data = self._prepare_data(backend)
116 reviewers = [
135 reviewers = [
117 {'username': TEST_USER_REGULAR_LOGIN,
136 {'username': TEST_USER_REGULAR_LOGIN,
118 'reasons': ['{} added manually'.format(TEST_USER_REGULAR_LOGIN)]},
137 'reasons': ['{} added manually'.format(TEST_USER_REGULAR_LOGIN)]},
119 {'username': TEST_USER_ADMIN_LOGIN,
138 {'username': TEST_USER_ADMIN_LOGIN,
120 'reasons': ['{} added manually'.format(TEST_USER_ADMIN_LOGIN)],
139 'reasons': ['{} added manually'.format(TEST_USER_ADMIN_LOGIN)],
121 'mandatory': True},
140 'mandatory': True},
122 ]
141 ]
123 data['reviewers'] = reviewers
142 data['reviewers'] = reviewers
124
143
125 id_, params = build_data(
144 id_, params = build_data(
126 self.apikey_regular, 'create_pull_request', **data)
145 self.apikey_regular, 'create_pull_request', **data)
127 response = api_call(self.app, params)
146 response = api_call(self.app, params)
128
147
129 expected_message = "Created new pull request `{title}`".format(
148 expected_message = "Created new pull request `{title}`".format(
130 title=data['title'])
149 title=data['title'])
131 result = response.json
150 result = response.json
132 assert result['error'] is None
151 assert result['error'] is None
133 assert result['result']['msg'] == expected_message
152 assert result['result']['msg'] == expected_message
134 pull_request_id = result['result']['pull_request_id']
153 pull_request_id = result['result']['pull_request_id']
135 pull_request = PullRequestModel().get(pull_request_id)
154 pull_request = PullRequestModel().get(pull_request_id)
136
155
137 actual_reviewers = []
156 actual_reviewers = []
138 for rev in pull_request.reviewers:
157 for rev in pull_request.reviewers:
139 entry = {
158 entry = {
140 'username': rev.user.username,
159 'username': rev.user.username,
141 'reasons': rev.reasons,
160 'reasons': rev.reasons,
142 }
161 }
143 if rev.mandatory:
162 if rev.mandatory:
144 entry['mandatory'] = rev.mandatory
163 entry['mandatory'] = rev.mandatory
145 actual_reviewers.append(entry)
164 actual_reviewers.append(entry)
146
165
147 owner_username = pull_request.target_repo.user.username
166 owner_username = pull_request.target_repo.user.username
148 for spec_reviewer in reviewers[::]:
167 for spec_reviewer in reviewers[::]:
149 # default reviewer will be added who is an owner of the repo
168 # default reviewer will be added who is an owner of the repo
150 # this get's overridden by a add owner to reviewers rule
169 # this get's overridden by a add owner to reviewers rule
151 if spec_reviewer['username'] == owner_username:
170 if spec_reviewer['username'] == owner_username:
152 spec_reviewer['reasons'] = [u'Default reviewer', u'Repository owner']
171 spec_reviewer['reasons'] = [u'Default reviewer', u'Repository owner']
153 # since owner is more important, we don't inherit mandatory flag
172 # since owner is more important, we don't inherit mandatory flag
154 del spec_reviewer['mandatory']
173 del spec_reviewer['mandatory']
155
174
156 assert sorted(actual_reviewers, key=lambda e: e['username']) \
175 assert sorted(actual_reviewers, key=lambda e: e['username']) \
157 == sorted(reviewers, key=lambda e: e['username'])
176 == sorted(reviewers, key=lambda e: e['username'])
158
177
159 @pytest.mark.backends("git", "hg")
178 @pytest.mark.backends("git", "hg")
160 def test_create_with_reviewers_specified_by_ids(
179 def test_create_with_reviewers_specified_by_ids(
161 self, backend, no_notifications):
180 self, backend, no_notifications):
162 data = self._prepare_data(backend)
181 data = self._prepare_data(backend)
163 reviewers = [
182 reviewers = [
164 {'username': UserModel().get_by_username(
183 {'username': UserModel().get_by_username(
165 TEST_USER_REGULAR_LOGIN).user_id,
184 TEST_USER_REGULAR_LOGIN).user_id,
166 'reasons': ['added manually']},
185 'reasons': ['added manually']},
167 {'username': UserModel().get_by_username(
186 {'username': UserModel().get_by_username(
168 TEST_USER_ADMIN_LOGIN).user_id,
187 TEST_USER_ADMIN_LOGIN).user_id,
169 'reasons': ['added manually']},
188 'reasons': ['added manually']},
170 ]
189 ]
171
190
172 data['reviewers'] = reviewers
191 data['reviewers'] = reviewers
173 id_, params = build_data(
192 id_, params = build_data(
174 self.apikey_regular, 'create_pull_request', **data)
193 self.apikey_regular, 'create_pull_request', **data)
175 response = api_call(self.app, params)
194 response = api_call(self.app, params)
176
195
177 expected_message = "Created new pull request `{title}`".format(
196 expected_message = "Created new pull request `{title}`".format(
178 title=data['title'])
197 title=data['title'])
179 result = response.json
198 result = response.json
180 assert result['error'] is None
199 assert result['error'] is None
181 assert result['result']['msg'] == expected_message
200 assert result['result']['msg'] == expected_message
182 pull_request_id = result['result']['pull_request_id']
201 pull_request_id = result['result']['pull_request_id']
183 pull_request = PullRequestModel().get(pull_request_id)
202 pull_request = PullRequestModel().get(pull_request_id)
184
203
185 actual_reviewers = []
204 actual_reviewers = []
186 for rev in pull_request.reviewers:
205 for rev in pull_request.reviewers:
187 entry = {
206 entry = {
188 'username': rev.user.user_id,
207 'username': rev.user.user_id,
189 'reasons': rev.reasons,
208 'reasons': rev.reasons,
190 }
209 }
191 if rev.mandatory:
210 if rev.mandatory:
192 entry['mandatory'] = rev.mandatory
211 entry['mandatory'] = rev.mandatory
193 actual_reviewers.append(entry)
212 actual_reviewers.append(entry)
194
213
195 owner_user_id = pull_request.target_repo.user.user_id
214 owner_user_id = pull_request.target_repo.user.user_id
196 for spec_reviewer in reviewers[::]:
215 for spec_reviewer in reviewers[::]:
197 # default reviewer will be added who is an owner of the repo
216 # default reviewer will be added who is an owner of the repo
198 # this get's overridden by a add owner to reviewers rule
217 # this get's overridden by a add owner to reviewers rule
199 if spec_reviewer['username'] == owner_user_id:
218 if spec_reviewer['username'] == owner_user_id:
200 spec_reviewer['reasons'] = [u'Default reviewer', u'Repository owner']
219 spec_reviewer['reasons'] = [u'Default reviewer', u'Repository owner']
201
220
202 assert sorted(actual_reviewers, key=lambda e: e['username']) \
221 assert sorted(actual_reviewers, key=lambda e: e['username']) \
203 == sorted(reviewers, key=lambda e: e['username'])
222 == sorted(reviewers, key=lambda e: e['username'])
204
223
205 @pytest.mark.backends("git", "hg")
224 @pytest.mark.backends("git", "hg")
206 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
225 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
207 data = self._prepare_data(backend)
226 data = self._prepare_data(backend)
208 data['reviewers'] = [{'username': 'somebody'}]
227 data['reviewers'] = [{'username': 'somebody'}]
209 id_, params = build_data(
228 id_, params = build_data(
210 self.apikey_regular, 'create_pull_request', **data)
229 self.apikey_regular, 'create_pull_request', **data)
211 response = api_call(self.app, params)
230 response = api_call(self.app, params)
212 expected_message = 'user `somebody` does not exist'
231 expected_message = 'user `somebody` does not exist'
213 assert_error(id_, expected_message, given=response.body)
232 assert_error(id_, expected_message, given=response.body)
214
233
215 @pytest.mark.backends("git", "hg")
234 @pytest.mark.backends("git", "hg")
216 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
235 def test_cannot_create_with_reviewers_in_wrong_format(self, backend):
217 data = self._prepare_data(backend)
236 data = self._prepare_data(backend)
218 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
237 reviewers = ','.join([TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN])
219 data['reviewers'] = reviewers
238 data['reviewers'] = reviewers
220 id_, params = build_data(
239 id_, params = build_data(
221 self.apikey_regular, 'create_pull_request', **data)
240 self.apikey_regular, 'create_pull_request', **data)
222 response = api_call(self.app, params)
241 response = api_call(self.app, params)
223 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
242 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
224 assert_error(id_, expected_message, given=response.body)
243 assert_error(id_, expected_message, given=response.body)
225
244
226 @pytest.mark.backends("git", "hg")
245 @pytest.mark.backends("git", "hg")
227 def test_create_with_no_commit_hashes(self, backend):
246 def test_create_with_no_commit_hashes(self, backend):
228 data = self._prepare_data(backend)
247 data = self._prepare_data(backend)
229 expected_source_ref = data['source_ref']
248 expected_source_ref = data['source_ref']
230 expected_target_ref = data['target_ref']
249 expected_target_ref = data['target_ref']
231 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
250 data['source_ref'] = 'branch:{}'.format(backend.default_branch_name)
232 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
251 data['target_ref'] = 'branch:{}'.format(backend.default_branch_name)
233 id_, params = build_data(
252 id_, params = build_data(
234 self.apikey_regular, 'create_pull_request', **data)
253 self.apikey_regular, 'create_pull_request', **data)
235 response = api_call(self.app, params)
254 response = api_call(self.app, params)
236 expected_message = "Created new pull request `{title}`".format(
255 expected_message = "Created new pull request `{title}`".format(
237 title=data['title'])
256 title=data['title'])
238 result = response.json
257 result = response.json
239 assert result['result']['msg'] == expected_message
258 assert result['result']['msg'] == expected_message
240 pull_request_id = result['result']['pull_request_id']
259 pull_request_id = result['result']['pull_request_id']
241 pull_request = PullRequestModel().get(pull_request_id)
260 pull_request = PullRequestModel().get(pull_request_id)
242 assert pull_request.source_ref == expected_source_ref
261 assert pull_request.source_ref == expected_source_ref
243 assert pull_request.target_ref == expected_target_ref
262 assert pull_request.target_ref == expected_target_ref
244
263
245 @pytest.mark.backends("git", "hg")
264 @pytest.mark.backends("git", "hg")
246 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
265 @pytest.mark.parametrize("data_key", ["source_repo", "target_repo"])
247 def test_create_fails_with_wrong_repo(self, backend, data_key):
266 def test_create_fails_with_wrong_repo(self, backend, data_key):
248 repo_name = 'fake-repo'
267 repo_name = 'fake-repo'
249 data = self._prepare_data(backend)
268 data = self._prepare_data(backend)
250 data[data_key] = repo_name
269 data[data_key] = repo_name
251 id_, params = build_data(
270 id_, params = build_data(
252 self.apikey_regular, 'create_pull_request', **data)
271 self.apikey_regular, 'create_pull_request', **data)
253 response = api_call(self.app, params)
272 response = api_call(self.app, params)
254 expected_message = 'repository `{}` does not exist'.format(repo_name)
273 expected_message = 'repository `{}` does not exist'.format(repo_name)
255 assert_error(id_, expected_message, given=response.body)
274 assert_error(id_, expected_message, given=response.body)
256
275
257 @pytest.mark.backends("git", "hg")
276 @pytest.mark.backends("git", "hg")
258 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
277 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
259 def test_create_fails_with_non_existing_branch(self, backend, data_key):
278 def test_create_fails_with_non_existing_branch(self, backend, data_key):
260 branch_name = 'test-branch'
279 branch_name = 'test-branch'
261 data = self._prepare_data(backend)
280 data = self._prepare_data(backend)
262 data[data_key] = "branch:{}".format(branch_name)
281 data[data_key] = "branch:{}".format(branch_name)
263 id_, params = build_data(
282 id_, params = build_data(
264 self.apikey_regular, 'create_pull_request', **data)
283 self.apikey_regular, 'create_pull_request', **data)
265 response = api_call(self.app, params)
284 response = api_call(self.app, params)
266 expected_message = 'The specified value:{type}:`{name}` ' \
285 expected_message = 'The specified value:{type}:`{name}` ' \
267 'does not exist, or is not allowed.'.format(type='branch',
286 'does not exist, or is not allowed.'.format(type='branch',
268 name=branch_name)
287 name=branch_name)
269 assert_error(id_, expected_message, given=response.body)
288 assert_error(id_, expected_message, given=response.body)
270
289
271 @pytest.mark.backends("git", "hg")
290 @pytest.mark.backends("git", "hg")
272 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
291 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
273 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
292 def test_create_fails_with_ref_in_a_wrong_format(self, backend, data_key):
274 data = self._prepare_data(backend)
293 data = self._prepare_data(backend)
275 ref = 'stange-ref'
294 ref = 'stange-ref'
276 data[data_key] = ref
295 data[data_key] = ref
277 id_, params = build_data(
296 id_, params = build_data(
278 self.apikey_regular, 'create_pull_request', **data)
297 self.apikey_regular, 'create_pull_request', **data)
279 response = api_call(self.app, params)
298 response = api_call(self.app, params)
280 expected_message = (
299 expected_message = (
281 'Ref `{ref}` given in a wrong format. Please check the API'
300 'Ref `{ref}` given in a wrong format. Please check the API'
282 ' documentation for more details'.format(ref=ref))
301 ' documentation for more details'.format(ref=ref))
283 assert_error(id_, expected_message, given=response.body)
302 assert_error(id_, expected_message, given=response.body)
284
303
285 @pytest.mark.backends("git", "hg")
304 @pytest.mark.backends("git", "hg")
286 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
305 @pytest.mark.parametrize("data_key", ["source_ref", "target_ref"])
287 def test_create_fails_with_non_existing_ref(self, backend, data_key):
306 def test_create_fails_with_non_existing_ref(self, backend, data_key):
288 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
307 commit_id = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
289 ref = self._get_full_ref(backend, commit_id)
308 ref = self._get_full_ref(backend, commit_id)
290 data = self._prepare_data(backend)
309 data = self._prepare_data(backend)
291 data[data_key] = ref
310 data[data_key] = ref
292 id_, params = build_data(
311 id_, params = build_data(
293 self.apikey_regular, 'create_pull_request', **data)
312 self.apikey_regular, 'create_pull_request', **data)
294 response = api_call(self.app, params)
313 response = api_call(self.app, params)
295 expected_message = 'Ref `{}` does not exist'.format(ref)
314 expected_message = 'Ref `{}` does not exist'.format(ref)
296 assert_error(id_, expected_message, given=response.body)
315 assert_error(id_, expected_message, given=response.body)
297
316
298 @pytest.mark.backends("git", "hg")
317 @pytest.mark.backends("git", "hg")
299 def test_create_fails_when_no_revisions(self, backend):
318 def test_create_fails_when_no_revisions(self, backend):
300 data = self._prepare_data(backend, source_head='initial')
319 data = self._prepare_data(backend, source_head='initial')
301 id_, params = build_data(
320 id_, params = build_data(
302 self.apikey_regular, 'create_pull_request', **data)
321 self.apikey_regular, 'create_pull_request', **data)
303 response = api_call(self.app, params)
322 response = api_call(self.app, params)
304 expected_message = 'no commits found'
323 expected_message = 'no commits found'
305 assert_error(id_, expected_message, given=response.body)
324 assert_error(id_, expected_message, given=response.body)
306
325
307 @pytest.mark.backends("git", "hg")
326 @pytest.mark.backends("git", "hg")
308 def test_create_fails_when_no_permissions(self, backend):
327 def test_create_fails_when_no_permissions(self, backend):
309 data = self._prepare_data(backend)
328 data = self._prepare_data(backend)
310 RepoModel().revoke_user_permission(
329 RepoModel().revoke_user_permission(
311 self.source.repo_name, self.test_user)
330 self.source.repo_name, self.test_user)
312 RepoModel().revoke_user_permission(
331 RepoModel().revoke_user_permission(
313 self.source.repo_name, User.DEFAULT_USER)
332 self.source.repo_name, User.DEFAULT_USER)
314
333
315 id_, params = build_data(
334 id_, params = build_data(
316 self.apikey_regular, 'create_pull_request', **data)
335 self.apikey_regular, 'create_pull_request', **data)
317 response = api_call(self.app, params)
336 response = api_call(self.app, params)
318 expected_message = 'repository `{}` does not exist'.format(
337 expected_message = 'repository `{}` does not exist'.format(
319 self.source.repo_name)
338 self.source.repo_name)
320 assert_error(id_, expected_message, given=response.body)
339 assert_error(id_, expected_message, given=response.body)
321
340
322 def _prepare_data(
341 def _prepare_data(
323 self, backend, source_head='change', target_head='initial'):
342 self, backend, source_head='change', target_head='initial'):
324 commits = [
343 commits = [
325 {'message': 'initial'},
344 {'message': 'initial'},
326 {'message': 'change'},
345 {'message': 'change'},
327 {'message': 'new-feature', 'parents': ['initial']},
346 {'message': 'new-feature', 'parents': ['initial']},
328 ]
347 ]
329 self.commit_ids = backend.create_master_repo(commits)
348 self.commit_ids = backend.create_master_repo(commits)
330 self.source = backend.create_repo(heads=[source_head])
349 self.source = backend.create_repo(heads=[source_head])
331 self.target = backend.create_repo(heads=[target_head])
350 self.target = backend.create_repo(heads=[target_head])
332
351
333 data = {
352 data = {
334 'source_repo': self.source.repo_name,
353 'source_repo': self.source.repo_name,
335 'target_repo': self.target.repo_name,
354 'target_repo': self.target.repo_name,
336 'source_ref': self._get_full_ref(
355 'source_ref': self._get_full_ref(
337 backend, self.commit_ids[source_head]),
356 backend, self.commit_ids[source_head]),
338 'target_ref': self._get_full_ref(
357 'target_ref': self._get_full_ref(
339 backend, self.commit_ids[target_head]),
358 backend, self.commit_ids[target_head]),
340 'title': 'Test PR 1',
359 'title': 'Test PR 1',
341 'description': 'Test'
360 'description': 'Test'
342 }
361 }
343 RepoModel().grant_user_permission(
362 RepoModel().grant_user_permission(
344 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
363 self.source.repo_name, self.TEST_USER_LOGIN, 'repository.read')
345 return data
364 return data
346
365
347 def _get_full_ref(self, backend, commit_id):
366 def _get_full_ref(self, backend, commit_id):
348 return 'branch:{branch}:{commit_id}'.format(
367 return 'branch:{branch}:{commit_id}'.format(
349 branch=backend.default_branch_name, commit_id=commit_id)
368 branch=backend.default_branch_name, commit_id=commit_id)
@@ -1,295 +1,295 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import pytest
22 import pytest
23 from mock import Mock, patch
23 from mock import Mock, patch
24
24
25 from rhodecode.api import utils
25 from rhodecode.api import utils
26 from rhodecode.api import JSONRPCError
26 from rhodecode.api import JSONRPCError
27 from rhodecode.lib.vcs.exceptions import RepositoryError
27 from rhodecode.lib.vcs.exceptions import RepositoryError
28
28
29
29
30 class TestGetCommitOrError(object):
30 class TestGetCommitOrError(object):
31 def setup(self):
31 def setup(self):
32 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
32 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
33
33
34 @pytest.mark.parametrize("ref", ['ref', '12345', 'a:b:c:d', 'branch:name'])
34 @pytest.mark.parametrize("ref", ['ref', '12345', 'a:b:c:d', 'branch:name'])
35 def test_ref_cannot_be_parsed(self, ref):
35 def test_ref_cannot_be_parsed(self, ref):
36 repo = Mock()
36 repo = Mock()
37 with pytest.raises(JSONRPCError) as excinfo:
37 with pytest.raises(JSONRPCError) as excinfo:
38 utils.get_commit_or_error(ref, repo)
38 utils.get_commit_or_error(ref, repo)
39 expected_message = (
39 expected_message = (
40 'Ref `{ref}` given in a wrong format. Please check the API'
40 'Ref `{ref}` given in a wrong format. Please check the API'
41 ' documentation for more details'.format(ref=ref)
41 ' documentation for more details'.format(ref=ref)
42 )
42 )
43 assert excinfo.value.message == expected_message
43 assert excinfo.value.message == expected_message
44
44
45 def test_success_with_hash_specified(self):
45 def test_success_with_hash_specified(self):
46 repo = Mock()
46 repo = Mock()
47 ref_type = 'branch'
47 ref_type = 'branch'
48 ref = '{}:master:{}'.format(ref_type, self.commit_hash)
48 ref = '{}:master:{}'.format(ref_type, self.commit_hash)
49
49
50 with patch('rhodecode.api.utils.get_commit_from_ref_name') as get_commit:
50 with patch('rhodecode.api.utils.get_commit_from_ref_name') as get_commit:
51 result = utils.get_commit_or_error(ref, repo)
51 result = utils.get_commit_or_error(ref, repo)
52 get_commit.assert_called_once_with(
52 get_commit.assert_called_once_with(
53 repo, self.commit_hash)
53 repo, self.commit_hash)
54 assert result == get_commit()
54 assert result == get_commit()
55
55
56 def test_raises_an_error_when_commit_not_found(self):
56 def test_raises_an_error_when_commit_not_found(self):
57 repo = Mock()
57 repo = Mock()
58 ref = 'branch:master:{}'.format(self.commit_hash)
58 ref = 'branch:master:{}'.format(self.commit_hash)
59
59
60 with patch('rhodecode.api.utils.get_commit_from_ref_name') as get_commit:
60 with patch('rhodecode.api.utils.get_commit_from_ref_name') as get_commit:
61 get_commit.side_effect = RepositoryError('Commit not found')
61 get_commit.side_effect = RepositoryError('Commit not found')
62 with pytest.raises(JSONRPCError) as excinfo:
62 with pytest.raises(JSONRPCError) as excinfo:
63 utils.get_commit_or_error(ref, repo)
63 utils.get_commit_or_error(ref, repo)
64 expected_message = 'Ref `{}` does not exist'.format(ref)
64 expected_message = 'Ref `{}` does not exist'.format(ref)
65 assert excinfo.value.message == expected_message
65 assert excinfo.value.message == expected_message
66
66
67
67
68 class TestResolveRefOrError(object):
68 class TestResolveRefOrError(object):
69 def setup(self):
69 def setup(self):
70 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
70 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
71
71
72 def test_success_with_no_hash_specified(self):
72 def test_success_with_no_hash_specified(self):
73 repo = Mock()
73 repo = Mock()
74 ref_type = 'branch'
74 ref_type = 'branch'
75 ref_name = 'master'
75 ref_name = 'master'
76 ref = '{}:{}'.format(ref_type, ref_name)
76 ref = '{}:{}'.format(ref_type, ref_name)
77
77
78 with patch('rhodecode.api.utils._get_ref_hash') \
78 with patch('rhodecode.api.utils._get_ref_hash') \
79 as _get_ref_hash:
79 as _get_ref_hash:
80 _get_ref_hash.return_value = self.commit_hash
80 _get_ref_hash.return_value = self.commit_hash
81 result = utils.resolve_ref_or_error(ref, repo)
81 result = utils.resolve_ref_or_error(ref, repo)
82 _get_ref_hash.assert_called_once_with(repo, ref_type, ref_name)
82 _get_ref_hash.assert_called_once_with(repo, ref_type, ref_name)
83 assert result == '{}:{}'.format(ref, self.commit_hash)
83 assert result == '{}:{}'.format(ref, self.commit_hash)
84
84
85 def test_non_supported_refs(self):
85 def test_non_supported_refs(self):
86 repo = Mock()
86 repo = Mock()
87 ref = 'ancestor:ref'
87 ref = 'bookmark:ref'
88 with pytest.raises(JSONRPCError) as excinfo:
88 with pytest.raises(JSONRPCError) as excinfo:
89 utils.resolve_ref_or_error(ref, repo)
89 utils.resolve_ref_or_error(ref, repo)
90 expected_message = (
90 expected_message = (
91 'The specified value:ancestor:`ref` does not exist, or is not allowed.')
91 'The specified value:bookmark:`ref` does not exist, or is not allowed.')
92 assert excinfo.value.message == expected_message
92 assert excinfo.value.message == expected_message
93
93
94 def test_branch_is_not_found(self):
94 def test_branch_is_not_found(self):
95 repo = Mock()
95 repo = Mock()
96 ref = 'branch:non-existing-one'
96 ref = 'branch:non-existing-one'
97 with patch('rhodecode.api.utils._get_ref_hash')\
97 with patch('rhodecode.api.utils._get_ref_hash')\
98 as _get_ref_hash:
98 as _get_ref_hash:
99 _get_ref_hash.side_effect = KeyError()
99 _get_ref_hash.side_effect = KeyError()
100 with pytest.raises(JSONRPCError) as excinfo:
100 with pytest.raises(JSONRPCError) as excinfo:
101 utils.resolve_ref_or_error(ref, repo)
101 utils.resolve_ref_or_error(ref, repo)
102 expected_message = (
102 expected_message = (
103 'The specified value:branch:`non-existing-one` does not exist, or is not allowed.')
103 'The specified value:branch:`non-existing-one` does not exist, or is not allowed.')
104 assert excinfo.value.message == expected_message
104 assert excinfo.value.message == expected_message
105
105
106 def test_bookmark_is_not_found(self):
106 def test_bookmark_is_not_found(self):
107 repo = Mock()
107 repo = Mock()
108 ref = 'bookmark:non-existing-one'
108 ref = 'bookmark:non-existing-one'
109 with patch('rhodecode.api.utils._get_ref_hash')\
109 with patch('rhodecode.api.utils._get_ref_hash')\
110 as _get_ref_hash:
110 as _get_ref_hash:
111 _get_ref_hash.side_effect = KeyError()
111 _get_ref_hash.side_effect = KeyError()
112 with pytest.raises(JSONRPCError) as excinfo:
112 with pytest.raises(JSONRPCError) as excinfo:
113 utils.resolve_ref_or_error(ref, repo)
113 utils.resolve_ref_or_error(ref, repo)
114 expected_message = (
114 expected_message = (
115 'The specified value:bookmark:`non-existing-one` does not exist, or is not allowed.')
115 'The specified value:bookmark:`non-existing-one` does not exist, or is not allowed.')
116 assert excinfo.value.message == expected_message
116 assert excinfo.value.message == expected_message
117
117
118 @pytest.mark.parametrize("ref", ['ref', '12345', 'a:b:c:d'])
118 @pytest.mark.parametrize("ref", ['ref', '12345', 'a:b:c:d'])
119 def test_ref_cannot_be_parsed(self, ref):
119 def test_ref_cannot_be_parsed(self, ref):
120 repo = Mock()
120 repo = Mock()
121 with pytest.raises(JSONRPCError) as excinfo:
121 with pytest.raises(JSONRPCError) as excinfo:
122 utils.resolve_ref_or_error(ref, repo)
122 utils.resolve_ref_or_error(ref, repo)
123 expected_message = (
123 expected_message = (
124 'Ref `{ref}` given in a wrong format. Please check the API'
124 'Ref `{ref}` given in a wrong format. Please check the API'
125 ' documentation for more details'.format(ref=ref)
125 ' documentation for more details'.format(ref=ref)
126 )
126 )
127 assert excinfo.value.message == expected_message
127 assert excinfo.value.message == expected_message
128
128
129
129
130 class TestGetRefHash(object):
130 class TestGetRefHash(object):
131 def setup(self):
131 def setup(self):
132 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
132 self.commit_hash = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa10'
133 self.bookmark_name = 'test-bookmark'
133 self.bookmark_name = 'test-bookmark'
134
134
135 @pytest.mark.parametrize("alias, branch_name", [
135 @pytest.mark.parametrize("alias, branch_name", [
136 ("git", "master"),
136 ("git", "master"),
137 ("hg", "default")
137 ("hg", "default")
138 ])
138 ])
139 def test_returns_hash_by_branch_name(self, alias, branch_name):
139 def test_returns_hash_by_branch_name(self, alias, branch_name):
140 with patch('rhodecode.model.db.Repository') as repo:
140 with patch('rhodecode.model.db.Repository') as repo:
141 repo.scm_instance().alias = alias
141 repo.scm_instance().alias = alias
142 repo.scm_instance().branches = {branch_name: self.commit_hash}
142 repo.scm_instance().branches = {branch_name: self.commit_hash}
143 result_hash = utils._get_ref_hash(repo, 'branch', branch_name)
143 result_hash = utils._get_ref_hash(repo, 'branch', branch_name)
144 assert result_hash == self.commit_hash
144 assert result_hash == self.commit_hash
145
145
146 @pytest.mark.parametrize("alias, branch_name", [
146 @pytest.mark.parametrize("alias, branch_name", [
147 ("git", "master"),
147 ("git", "master"),
148 ("hg", "default")
148 ("hg", "default")
149 ])
149 ])
150 def test_raises_error_when_branch_is_not_found(self, alias, branch_name):
150 def test_raises_error_when_branch_is_not_found(self, alias, branch_name):
151 with patch('rhodecode.model.db.Repository') as repo:
151 with patch('rhodecode.model.db.Repository') as repo:
152 repo.scm_instance().alias = alias
152 repo.scm_instance().alias = alias
153 repo.scm_instance().branches = {}
153 repo.scm_instance().branches = {}
154 with pytest.raises(KeyError):
154 with pytest.raises(KeyError):
155 utils._get_ref_hash(repo, 'branch', branch_name)
155 utils._get_ref_hash(repo, 'branch', branch_name)
156
156
157 def test_returns_hash_when_bookmark_is_specified_for_hg(self):
157 def test_returns_hash_when_bookmark_is_specified_for_hg(self):
158 with patch('rhodecode.model.db.Repository') as repo:
158 with patch('rhodecode.model.db.Repository') as repo:
159 repo.scm_instance().alias = 'hg'
159 repo.scm_instance().alias = 'hg'
160 repo.scm_instance().bookmarks = {
160 repo.scm_instance().bookmarks = {
161 self.bookmark_name: self.commit_hash}
161 self.bookmark_name: self.commit_hash}
162 result_hash = utils._get_ref_hash(
162 result_hash = utils._get_ref_hash(
163 repo, 'bookmark', self.bookmark_name)
163 repo, 'bookmark', self.bookmark_name)
164 assert result_hash == self.commit_hash
164 assert result_hash == self.commit_hash
165
165
166 def test_raises_error_when_bookmark_is_not_found_in_hg_repo(self):
166 def test_raises_error_when_bookmark_is_not_found_in_hg_repo(self):
167 with patch('rhodecode.model.db.Repository') as repo:
167 with patch('rhodecode.model.db.Repository') as repo:
168 repo.scm_instance().alias = 'hg'
168 repo.scm_instance().alias = 'hg'
169 repo.scm_instance().bookmarks = {}
169 repo.scm_instance().bookmarks = {}
170 with pytest.raises(KeyError):
170 with pytest.raises(KeyError):
171 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
171 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
172
172
173 def test_raises_error_when_bookmark_is_specified_for_git(self):
173 def test_raises_error_when_bookmark_is_specified_for_git(self):
174 with patch('rhodecode.model.db.Repository') as repo:
174 with patch('rhodecode.model.db.Repository') as repo:
175 repo.scm_instance().alias = 'git'
175 repo.scm_instance().alias = 'git'
176 repo.scm_instance().bookmarks = {
176 repo.scm_instance().bookmarks = {
177 self.bookmark_name: self.commit_hash}
177 self.bookmark_name: self.commit_hash}
178 with pytest.raises(ValueError):
178 with pytest.raises(ValueError):
179 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
179 utils._get_ref_hash(repo, 'bookmark', self.bookmark_name)
180
180
181
181
182 class TestUserByNameOrError(object):
182 class TestUserByNameOrError(object):
183 def test_user_found_by_id(self):
183 def test_user_found_by_id(self):
184 fake_user = Mock(id=123)
184 fake_user = Mock(id=123)
185
185
186 patcher = patch('rhodecode.model.user.UserModel.get_user')
186 patcher = patch('rhodecode.model.user.UserModel.get_user')
187 with patcher as get_user:
187 with patcher as get_user:
188 get_user.return_value = fake_user
188 get_user.return_value = fake_user
189
189
190 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
190 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
191 with patcher as get_by_username:
191 with patcher as get_by_username:
192 result = utils.get_user_or_error(123)
192 result = utils.get_user_or_error(123)
193 assert result == fake_user
193 assert result == fake_user
194
194
195 def test_user_not_found_by_id_as_str(self):
195 def test_user_not_found_by_id_as_str(self):
196 fake_user = Mock(id=123)
196 fake_user = Mock(id=123)
197
197
198 patcher = patch('rhodecode.model.user.UserModel.get_user')
198 patcher = patch('rhodecode.model.user.UserModel.get_user')
199 with patcher as get_user:
199 with patcher as get_user:
200 get_user.return_value = fake_user
200 get_user.return_value = fake_user
201 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
201 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
202 with patcher as get_by_username:
202 with patcher as get_by_username:
203 get_by_username.return_value = None
203 get_by_username.return_value = None
204
204
205 with pytest.raises(JSONRPCError):
205 with pytest.raises(JSONRPCError):
206 utils.get_user_or_error('123')
206 utils.get_user_or_error('123')
207
207
208 def test_user_found_by_name(self):
208 def test_user_found_by_name(self):
209 fake_user = Mock(id=123)
209 fake_user = Mock(id=123)
210
210
211 patcher = patch('rhodecode.model.user.UserModel.get_user')
211 patcher = patch('rhodecode.model.user.UserModel.get_user')
212 with patcher as get_user:
212 with patcher as get_user:
213 get_user.return_value = None
213 get_user.return_value = None
214
214
215 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
215 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
216 with patcher as get_by_username:
216 with patcher as get_by_username:
217 get_by_username.return_value = fake_user
217 get_by_username.return_value = fake_user
218
218
219 result = utils.get_user_or_error('test')
219 result = utils.get_user_or_error('test')
220 assert result == fake_user
220 assert result == fake_user
221
221
222 def test_user_not_found_by_id(self):
222 def test_user_not_found_by_id(self):
223 patcher = patch('rhodecode.model.user.UserModel.get_user')
223 patcher = patch('rhodecode.model.user.UserModel.get_user')
224 with patcher as get_user:
224 with patcher as get_user:
225 get_user.return_value = None
225 get_user.return_value = None
226 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
226 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
227 with patcher as get_by_username:
227 with patcher as get_by_username:
228 get_by_username.return_value = None
228 get_by_username.return_value = None
229
229
230 with pytest.raises(JSONRPCError) as excinfo:
230 with pytest.raises(JSONRPCError) as excinfo:
231 utils.get_user_or_error(123)
231 utils.get_user_or_error(123)
232
232
233 expected_message = 'user `123` does not exist'
233 expected_message = 'user `123` does not exist'
234 assert excinfo.value.message == expected_message
234 assert excinfo.value.message == expected_message
235
235
236 def test_user_not_found_by_name(self):
236 def test_user_not_found_by_name(self):
237 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
237 patcher = patch('rhodecode.model.user.UserModel.get_by_username')
238 with patcher as get_by_username:
238 with patcher as get_by_username:
239 get_by_username.return_value = None
239 get_by_username.return_value = None
240 with pytest.raises(JSONRPCError) as excinfo:
240 with pytest.raises(JSONRPCError) as excinfo:
241 utils.get_user_or_error('test')
241 utils.get_user_or_error('test')
242
242
243 expected_message = 'user `test` does not exist'
243 expected_message = 'user `test` does not exist'
244 assert excinfo.value.message == expected_message
244 assert excinfo.value.message == expected_message
245
245
246
246
247 class TestGetCommitDict(object):
247 class TestGetCommitDict(object):
248 @pytest.mark.parametrize('filename, expected', [
248 @pytest.mark.parametrize('filename, expected', [
249 (b'sp\xc3\xa4cial', u'sp\xe4cial'),
249 (b'sp\xc3\xa4cial', u'sp\xe4cial'),
250 (b'sp\xa4cial', u'sp\ufffdcial'),
250 (b'sp\xa4cial', u'sp\ufffdcial'),
251 ])
251 ])
252 def test_decodes_filenames_to_unicode(self, filename, expected):
252 def test_decodes_filenames_to_unicode(self, filename, expected):
253 result = utils._get_commit_dict(filename=filename, op='A')
253 result = utils._get_commit_dict(filename=filename, op='A')
254 assert result['filename'] == expected
254 assert result['filename'] == expected
255
255
256
256
257 class TestRepoAccess(object):
257 class TestRepoAccess(object):
258 def setup_method(self, method):
258 def setup_method(self, method):
259
259
260 self.admin_perm_patch = patch(
260 self.admin_perm_patch = patch(
261 'rhodecode.api.utils.HasPermissionAnyApi')
261 'rhodecode.api.utils.HasPermissionAnyApi')
262 self.repo_perm_patch = patch(
262 self.repo_perm_patch = patch(
263 'rhodecode.api.utils.HasRepoPermissionAnyApi')
263 'rhodecode.api.utils.HasRepoPermissionAnyApi')
264
264
265 def test_has_superadmin_permission_checks_for_admin(self):
265 def test_has_superadmin_permission_checks_for_admin(self):
266 admin_mock = Mock()
266 admin_mock = Mock()
267 with self.admin_perm_patch as amock:
267 with self.admin_perm_patch as amock:
268 amock.return_value = admin_mock
268 amock.return_value = admin_mock
269 assert utils.has_superadmin_permission('fake_user')
269 assert utils.has_superadmin_permission('fake_user')
270 amock.assert_called_once_with('hg.admin')
270 amock.assert_called_once_with('hg.admin')
271
271
272 admin_mock.assert_called_once_with(user='fake_user')
272 admin_mock.assert_called_once_with(user='fake_user')
273
273
274 def test_has_repo_permissions_checks_for_repo_access(self):
274 def test_has_repo_permissions_checks_for_repo_access(self):
275 repo_mock = Mock()
275 repo_mock = Mock()
276 fake_repo = Mock()
276 fake_repo = Mock()
277 with self.repo_perm_patch as rmock:
277 with self.repo_perm_patch as rmock:
278 rmock.return_value = repo_mock
278 rmock.return_value = repo_mock
279 assert utils.validate_repo_permissions(
279 assert utils.validate_repo_permissions(
280 'fake_user', 'fake_repo_id', fake_repo,
280 'fake_user', 'fake_repo_id', fake_repo,
281 ['perm1', 'perm2'])
281 ['perm1', 'perm2'])
282 rmock.assert_called_once_with(*['perm1', 'perm2'])
282 rmock.assert_called_once_with(*['perm1', 'perm2'])
283
283
284 repo_mock.assert_called_once_with(
284 repo_mock.assert_called_once_with(
285 user='fake_user', repo_name=fake_repo.repo_name)
285 user='fake_user', repo_name=fake_repo.repo_name)
286
286
287 def test_has_repo_permissions_raises_not_found(self):
287 def test_has_repo_permissions_raises_not_found(self):
288 repo_mock = Mock(return_value=False)
288 repo_mock = Mock(return_value=False)
289 fake_repo = Mock()
289 fake_repo = Mock()
290 with self.repo_perm_patch as rmock:
290 with self.repo_perm_patch as rmock:
291 rmock.return_value = repo_mock
291 rmock.return_value = repo_mock
292 with pytest.raises(JSONRPCError) as excinfo:
292 with pytest.raises(JSONRPCError) as excinfo:
293 utils.validate_repo_permissions(
293 utils.validate_repo_permissions(
294 'fake_user', 'fake_repo_id', fake_repo, 'perms')
294 'fake_user', 'fake_repo_id', fake_repo, 'perms')
295 assert 'fake_repo_id' in excinfo
295 assert 'fake_repo_id' in excinfo
@@ -1,441 +1,449 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 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 JSON RPC utils
22 JSON RPC utils
23 """
23 """
24
24
25 import collections
25 import collections
26 import logging
26 import logging
27
27
28 from rhodecode.api.exc import JSONRPCError
28 from rhodecode.api.exc import JSONRPCError
29 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
30 HasPermissionAnyApi, HasRepoPermissionAnyApi, HasRepoGroupPermissionAnyApi)
30 HasPermissionAnyApi, HasRepoPermissionAnyApi, HasRepoGroupPermissionAnyApi)
31 from rhodecode.lib.utils import safe_unicode
31 from rhodecode.lib.utils import safe_unicode
32 from rhodecode.lib.vcs.exceptions import RepositoryError
32 from rhodecode.lib.vcs.exceptions import RepositoryError
33 from rhodecode.lib.view_utils import get_commit_from_ref_name
33 from rhodecode.lib.view_utils import get_commit_from_ref_name
34 from rhodecode.lib.utils2 import str2bool
34 from rhodecode.lib.utils2 import str2bool
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38
38
39 class OAttr(object):
39 class OAttr(object):
40 """
40 """
41 Special Option that defines other attribute, and can default to them
41 Special Option that defines other attribute, and can default to them
42
42
43 Example::
43 Example::
44
44
45 def test(apiuser, userid=Optional(OAttr('apiuser')):
45 def test(apiuser, userid=Optional(OAttr('apiuser')):
46 user = Optional.extract(userid, evaluate_locals=local())
46 user = Optional.extract(userid, evaluate_locals=local())
47 #if we pass in userid, we get it, else it will default to apiuser
47 #if we pass in userid, we get it, else it will default to apiuser
48 #attribute
48 #attribute
49 """
49 """
50
50
51 def __init__(self, attr_name):
51 def __init__(self, attr_name):
52 self.attr_name = attr_name
52 self.attr_name = attr_name
53
53
54 def __repr__(self):
54 def __repr__(self):
55 return '<OptionalAttr:%s>' % self.attr_name
55 return '<OptionalAttr:%s>' % self.attr_name
56
56
57 def __call__(self):
57 def __call__(self):
58 return self
58 return self
59
59
60
60
61 class Optional(object):
61 class Optional(object):
62 """
62 """
63 Defines an optional parameter::
63 Defines an optional parameter::
64
64
65 param = param.getval() if isinstance(param, Optional) else param
65 param = param.getval() if isinstance(param, Optional) else param
66 param = param() if isinstance(param, Optional) else param
66 param = param() if isinstance(param, Optional) else param
67
67
68 is equivalent of::
68 is equivalent of::
69
69
70 param = Optional.extract(param)
70 param = Optional.extract(param)
71
71
72 """
72 """
73
73
74 def __init__(self, type_):
74 def __init__(self, type_):
75 self.type_ = type_
75 self.type_ = type_
76
76
77 def __repr__(self):
77 def __repr__(self):
78 return '<Optional:%s>' % self.type_.__repr__()
78 return '<Optional:%s>' % self.type_.__repr__()
79
79
80 def __call__(self):
80 def __call__(self):
81 return self.getval()
81 return self.getval()
82
82
83 def getval(self, evaluate_locals=None):
83 def getval(self, evaluate_locals=None):
84 """
84 """
85 returns value from this Optional instance
85 returns value from this Optional instance
86 """
86 """
87 if isinstance(self.type_, OAttr):
87 if isinstance(self.type_, OAttr):
88 param_name = self.type_.attr_name
88 param_name = self.type_.attr_name
89 if evaluate_locals:
89 if evaluate_locals:
90 return evaluate_locals[param_name]
90 return evaluate_locals[param_name]
91 # use params name
91 # use params name
92 return param_name
92 return param_name
93 return self.type_
93 return self.type_
94
94
95 @classmethod
95 @classmethod
96 def extract(cls, val, evaluate_locals=None, binary=None):
96 def extract(cls, val, evaluate_locals=None, binary=None):
97 """
97 """
98 Extracts value from Optional() instance
98 Extracts value from Optional() instance
99
99
100 :param val:
100 :param val:
101 :return: original value if it's not Optional instance else
101 :return: original value if it's not Optional instance else
102 value of instance
102 value of instance
103 """
103 """
104 if isinstance(val, cls):
104 if isinstance(val, cls):
105 val = val.getval(evaluate_locals)
105 val = val.getval(evaluate_locals)
106
106
107 if binary:
107 if binary:
108 val = str2bool(val)
108 val = str2bool(val)
109
109
110 return val
110 return val
111
111
112
112
113 def parse_args(cli_args, key_prefix=''):
113 def parse_args(cli_args, key_prefix=''):
114 from rhodecode.lib.utils2 import (escape_split)
114 from rhodecode.lib.utils2 import (escape_split)
115 kwargs = collections.defaultdict(dict)
115 kwargs = collections.defaultdict(dict)
116 for el in escape_split(cli_args, ','):
116 for el in escape_split(cli_args, ','):
117 kv = escape_split(el, '=', 1)
117 kv = escape_split(el, '=', 1)
118 if len(kv) == 2:
118 if len(kv) == 2:
119 k, v = kv
119 k, v = kv
120 kwargs[key_prefix + k] = v
120 kwargs[key_prefix + k] = v
121 return kwargs
121 return kwargs
122
122
123
123
124 def get_origin(obj):
124 def get_origin(obj):
125 """
125 """
126 Get origin of permission from object.
126 Get origin of permission from object.
127
127
128 :param obj:
128 :param obj:
129 """
129 """
130 origin = 'permission'
130 origin = 'permission'
131
131
132 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
132 if getattr(obj, 'owner_row', '') and getattr(obj, 'admin_row', ''):
133 # admin and owner case, maybe we should use dual string ?
133 # admin and owner case, maybe we should use dual string ?
134 origin = 'owner'
134 origin = 'owner'
135 elif getattr(obj, 'owner_row', ''):
135 elif getattr(obj, 'owner_row', ''):
136 origin = 'owner'
136 origin = 'owner'
137 elif getattr(obj, 'admin_row', ''):
137 elif getattr(obj, 'admin_row', ''):
138 origin = 'super-admin'
138 origin = 'super-admin'
139 return origin
139 return origin
140
140
141
141
142 def store_update(updates, attr, name):
142 def store_update(updates, attr, name):
143 """
143 """
144 Stores param in updates dict if it's not instance of Optional
144 Stores param in updates dict if it's not instance of Optional
145 allows easy updates of passed in params
145 allows easy updates of passed in params
146 """
146 """
147 if not isinstance(attr, Optional):
147 if not isinstance(attr, Optional):
148 updates[name] = attr
148 updates[name] = attr
149
149
150
150
151 def has_superadmin_permission(apiuser):
151 def has_superadmin_permission(apiuser):
152 """
152 """
153 Return True if apiuser is admin or return False
153 Return True if apiuser is admin or return False
154
154
155 :param apiuser:
155 :param apiuser:
156 """
156 """
157 if HasPermissionAnyApi('hg.admin')(user=apiuser):
157 if HasPermissionAnyApi('hg.admin')(user=apiuser):
158 return True
158 return True
159 return False
159 return False
160
160
161
161
162 def validate_repo_permissions(apiuser, repoid, repo, perms):
162 def validate_repo_permissions(apiuser, repoid, repo, perms):
163 """
163 """
164 Raise JsonRPCError if apiuser is not authorized or return True
164 Raise JsonRPCError if apiuser is not authorized or return True
165
165
166 :param apiuser:
166 :param apiuser:
167 :param repoid:
167 :param repoid:
168 :param repo:
168 :param repo:
169 :param perms:
169 :param perms:
170 """
170 """
171 if not HasRepoPermissionAnyApi(*perms)(
171 if not HasRepoPermissionAnyApi(*perms)(
172 user=apiuser, repo_name=repo.repo_name):
172 user=apiuser, repo_name=repo.repo_name):
173 raise JSONRPCError(
173 raise JSONRPCError(
174 'repository `%s` does not exist' % repoid)
174 'repository `%s` does not exist' % repoid)
175
175
176 return True
176 return True
177
177
178
178
179 def validate_repo_group_permissions(apiuser, repogroupid, repo_group, perms):
179 def validate_repo_group_permissions(apiuser, repogroupid, repo_group, perms):
180 """
180 """
181 Raise JsonRPCError if apiuser is not authorized or return True
181 Raise JsonRPCError if apiuser is not authorized or return True
182
182
183 :param apiuser:
183 :param apiuser:
184 :param repogroupid: just the id of repository group
184 :param repogroupid: just the id of repository group
185 :param repo_group: instance of repo_group
185 :param repo_group: instance of repo_group
186 :param perms:
186 :param perms:
187 """
187 """
188 if not HasRepoGroupPermissionAnyApi(*perms)(
188 if not HasRepoGroupPermissionAnyApi(*perms)(
189 user=apiuser, group_name=repo_group.group_name):
189 user=apiuser, group_name=repo_group.group_name):
190 raise JSONRPCError(
190 raise JSONRPCError(
191 'repository group `%s` does not exist' % repogroupid)
191 'repository group `%s` does not exist' % repogroupid)
192
192
193 return True
193 return True
194
194
195
195
196 def validate_set_owner_permissions(apiuser, owner):
196 def validate_set_owner_permissions(apiuser, owner):
197 if isinstance(owner, Optional):
197 if isinstance(owner, Optional):
198 owner = get_user_or_error(apiuser.user_id)
198 owner = get_user_or_error(apiuser.user_id)
199 else:
199 else:
200 if has_superadmin_permission(apiuser):
200 if has_superadmin_permission(apiuser):
201 owner = get_user_or_error(owner)
201 owner = get_user_or_error(owner)
202 else:
202 else:
203 # forbid setting owner for non-admins
203 # forbid setting owner for non-admins
204 raise JSONRPCError(
204 raise JSONRPCError(
205 'Only RhodeCode super-admin can specify `owner` param')
205 'Only RhodeCode super-admin can specify `owner` param')
206 return owner
206 return owner
207
207
208
208
209 def get_user_or_error(userid):
209 def get_user_or_error(userid):
210 """
210 """
211 Get user by id or name or return JsonRPCError if not found
211 Get user by id or name or return JsonRPCError if not found
212
212
213 :param userid:
213 :param userid:
214 """
214 """
215 from rhodecode.model.user import UserModel
215 from rhodecode.model.user import UserModel
216 user_model = UserModel()
216 user_model = UserModel()
217
217
218 if isinstance(userid, (int, long)):
218 if isinstance(userid, (int, long)):
219 try:
219 try:
220 user = user_model.get_user(userid)
220 user = user_model.get_user(userid)
221 except ValueError:
221 except ValueError:
222 user = None
222 user = None
223 else:
223 else:
224 user = user_model.get_by_username(userid)
224 user = user_model.get_by_username(userid)
225
225
226 if user is None:
226 if user is None:
227 raise JSONRPCError(
227 raise JSONRPCError(
228 'user `%s` does not exist' % (userid,))
228 'user `%s` does not exist' % (userid,))
229 return user
229 return user
230
230
231
231
232 def get_repo_or_error(repoid):
232 def get_repo_or_error(repoid):
233 """
233 """
234 Get repo by id or name or return JsonRPCError if not found
234 Get repo by id or name or return JsonRPCError if not found
235
235
236 :param repoid:
236 :param repoid:
237 """
237 """
238 from rhodecode.model.repo import RepoModel
238 from rhodecode.model.repo import RepoModel
239 repo_model = RepoModel()
239 repo_model = RepoModel()
240
240
241 if isinstance(repoid, (int, long)):
241 if isinstance(repoid, (int, long)):
242 try:
242 try:
243 repo = repo_model.get_repo(repoid)
243 repo = repo_model.get_repo(repoid)
244 except ValueError:
244 except ValueError:
245 repo = None
245 repo = None
246 else:
246 else:
247 repo = repo_model.get_by_repo_name(repoid)
247 repo = repo_model.get_by_repo_name(repoid)
248
248
249 if repo is None:
249 if repo is None:
250 raise JSONRPCError(
250 raise JSONRPCError(
251 'repository `%s` does not exist' % (repoid,))
251 'repository `%s` does not exist' % (repoid,))
252 return repo
252 return repo
253
253
254
254
255 def get_repo_group_or_error(repogroupid):
255 def get_repo_group_or_error(repogroupid):
256 """
256 """
257 Get repo group by id or name or return JsonRPCError if not found
257 Get repo group by id or name or return JsonRPCError if not found
258
258
259 :param repogroupid:
259 :param repogroupid:
260 """
260 """
261 from rhodecode.model.repo_group import RepoGroupModel
261 from rhodecode.model.repo_group import RepoGroupModel
262 repo_group_model = RepoGroupModel()
262 repo_group_model = RepoGroupModel()
263
263
264 if isinstance(repogroupid, (int, long)):
264 if isinstance(repogroupid, (int, long)):
265 try:
265 try:
266 repo_group = repo_group_model._get_repo_group(repogroupid)
266 repo_group = repo_group_model._get_repo_group(repogroupid)
267 except ValueError:
267 except ValueError:
268 repo_group = None
268 repo_group = None
269 else:
269 else:
270 repo_group = repo_group_model.get_by_group_name(repogroupid)
270 repo_group = repo_group_model.get_by_group_name(repogroupid)
271
271
272 if repo_group is None:
272 if repo_group is None:
273 raise JSONRPCError(
273 raise JSONRPCError(
274 'repository group `%s` does not exist' % (repogroupid,))
274 'repository group `%s` does not exist' % (repogroupid,))
275 return repo_group
275 return repo_group
276
276
277
277
278 def get_user_group_or_error(usergroupid):
278 def get_user_group_or_error(usergroupid):
279 """
279 """
280 Get user group by id or name or return JsonRPCError if not found
280 Get user group by id or name or return JsonRPCError if not found
281
281
282 :param usergroupid:
282 :param usergroupid:
283 """
283 """
284 from rhodecode.model.user_group import UserGroupModel
284 from rhodecode.model.user_group import UserGroupModel
285 user_group_model = UserGroupModel()
285 user_group_model = UserGroupModel()
286
286
287 if isinstance(usergroupid, (int, long)):
287 if isinstance(usergroupid, (int, long)):
288 try:
288 try:
289 user_group = user_group_model.get_group(usergroupid)
289 user_group = user_group_model.get_group(usergroupid)
290 except ValueError:
290 except ValueError:
291 user_group = None
291 user_group = None
292 else:
292 else:
293 user_group = user_group_model.get_by_name(usergroupid)
293 user_group = user_group_model.get_by_name(usergroupid)
294
294
295 if user_group is None:
295 if user_group is None:
296 raise JSONRPCError(
296 raise JSONRPCError(
297 'user group `%s` does not exist' % (usergroupid,))
297 'user group `%s` does not exist' % (usergroupid,))
298 return user_group
298 return user_group
299
299
300
300
301 def get_perm_or_error(permid, prefix=None):
301 def get_perm_or_error(permid, prefix=None):
302 """
302 """
303 Get permission by id or name or return JsonRPCError if not found
303 Get permission by id or name or return JsonRPCError if not found
304
304
305 :param permid:
305 :param permid:
306 """
306 """
307 from rhodecode.model.permission import PermissionModel
307 from rhodecode.model.permission import PermissionModel
308
308
309 perm = PermissionModel.cls.get_by_key(permid)
309 perm = PermissionModel.cls.get_by_key(permid)
310 if perm is None:
310 if perm is None:
311 raise JSONRPCError('permission `%s` does not exist' % (permid,))
311 raise JSONRPCError('permission `%s` does not exist' % (permid,))
312 if prefix:
312 if prefix:
313 if not perm.permission_name.startswith(prefix):
313 if not perm.permission_name.startswith(prefix):
314 raise JSONRPCError('permission `%s` is invalid, '
314 raise JSONRPCError('permission `%s` is invalid, '
315 'should start with %s' % (permid, prefix))
315 'should start with %s' % (permid, prefix))
316 return perm
316 return perm
317
317
318
318
319 def get_gist_or_error(gistid):
319 def get_gist_or_error(gistid):
320 """
320 """
321 Get gist by id or gist_access_id or return JsonRPCError if not found
321 Get gist by id or gist_access_id or return JsonRPCError if not found
322
322
323 :param gistid:
323 :param gistid:
324 """
324 """
325 from rhodecode.model.gist import GistModel
325 from rhodecode.model.gist import GistModel
326
326
327 gist = GistModel.cls.get_by_access_id(gistid)
327 gist = GistModel.cls.get_by_access_id(gistid)
328 if gist is None:
328 if gist is None:
329 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
329 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
330 return gist
330 return gist
331
331
332
332
333 def get_pull_request_or_error(pullrequestid):
333 def get_pull_request_or_error(pullrequestid):
334 """
334 """
335 Get pull request by id or return JsonRPCError if not found
335 Get pull request by id or return JsonRPCError if not found
336
336
337 :param pullrequestid:
337 :param pullrequestid:
338 """
338 """
339 from rhodecode.model.pull_request import PullRequestModel
339 from rhodecode.model.pull_request import PullRequestModel
340
340
341 try:
341 try:
342 pull_request = PullRequestModel().get(int(pullrequestid))
342 pull_request = PullRequestModel().get(int(pullrequestid))
343 except ValueError:
343 except ValueError:
344 raise JSONRPCError('pullrequestid must be an integer')
344 raise JSONRPCError('pullrequestid must be an integer')
345 if not pull_request:
345 if not pull_request:
346 raise JSONRPCError('pull request `%s` does not exist' % (
346 raise JSONRPCError('pull request `%s` does not exist' % (
347 pullrequestid,))
347 pullrequestid,))
348 return pull_request
348 return pull_request
349
349
350
350
351 def build_commit_data(commit, detail_level):
351 def build_commit_data(commit, detail_level):
352 parsed_diff = []
352 parsed_diff = []
353 if detail_level == 'extended':
353 if detail_level == 'extended':
354 for f in commit.added:
354 for f in commit.added:
355 parsed_diff.append(_get_commit_dict(filename=f.path, op='A'))
355 parsed_diff.append(_get_commit_dict(filename=f.path, op='A'))
356 for f in commit.changed:
356 for f in commit.changed:
357 parsed_diff.append(_get_commit_dict(filename=f.path, op='M'))
357 parsed_diff.append(_get_commit_dict(filename=f.path, op='M'))
358 for f in commit.removed:
358 for f in commit.removed:
359 parsed_diff.append(_get_commit_dict(filename=f.path, op='D'))
359 parsed_diff.append(_get_commit_dict(filename=f.path, op='D'))
360
360
361 elif detail_level == 'full':
361 elif detail_level == 'full':
362 from rhodecode.lib.diffs import DiffProcessor
362 from rhodecode.lib.diffs import DiffProcessor
363 diff_processor = DiffProcessor(commit.diff())
363 diff_processor = DiffProcessor(commit.diff())
364 for dp in diff_processor.prepare():
364 for dp in diff_processor.prepare():
365 del dp['stats']['ops']
365 del dp['stats']['ops']
366 _stats = dp['stats']
366 _stats = dp['stats']
367 parsed_diff.append(_get_commit_dict(
367 parsed_diff.append(_get_commit_dict(
368 filename=dp['filename'], op=dp['operation'],
368 filename=dp['filename'], op=dp['operation'],
369 new_revision=dp['new_revision'],
369 new_revision=dp['new_revision'],
370 old_revision=dp['old_revision'],
370 old_revision=dp['old_revision'],
371 raw_diff=dp['raw_diff'], stats=_stats))
371 raw_diff=dp['raw_diff'], stats=_stats))
372
372
373 return parsed_diff
373 return parsed_diff
374
374
375
375
376 def get_commit_or_error(ref, repo):
376 def get_commit_or_error(ref, repo):
377 try:
377 try:
378 ref_type, _, ref_hash = ref.split(':')
378 ref_type, _, ref_hash = ref.split(':')
379 except ValueError:
379 except ValueError:
380 raise JSONRPCError(
380 raise JSONRPCError(
381 'Ref `{ref}` given in a wrong format. Please check the API'
381 'Ref `{ref}` given in a wrong format. Please check the API'
382 ' documentation for more details'.format(ref=ref))
382 ' documentation for more details'.format(ref=ref))
383 try:
383 try:
384 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
384 # TODO: dan: refactor this to use repo.scm_instance().get_commit()
385 # once get_commit supports ref_types
385 # once get_commit supports ref_types
386 return get_commit_from_ref_name(repo, ref_hash)
386 return get_commit_from_ref_name(repo, ref_hash)
387 except RepositoryError:
387 except RepositoryError:
388 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
388 raise JSONRPCError('Ref `{ref}` does not exist'.format(ref=ref))
389
389
390
390
391 def resolve_ref_or_error(ref, repo):
391 def _get_ref_hash(repo, type_, name):
392 vcs_repo = repo.scm_instance()
393 if type_ in ['branch'] and vcs_repo.alias in ('hg', 'git'):
394 return vcs_repo.branches[name]
395 elif type_ in ['bookmark', 'book'] and vcs_repo.alias == 'hg':
396 return vcs_repo.bookmarks[name]
397 else:
398 raise ValueError()
399
400
401 def resolve_ref_or_error(ref, repo, allowed_ref_types=None):
402 allowed_ref_types = allowed_ref_types or ['bookmark', 'book', 'tag', 'branch']
403
392 def _parse_ref(type_, name, hash_=None):
404 def _parse_ref(type_, name, hash_=None):
393 return type_, name, hash_
405 return type_, name, hash_
394
406
395 try:
407 try:
396 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
408 ref_type, ref_name, ref_hash = _parse_ref(*ref.split(':'))
397 except TypeError:
409 except TypeError:
398 raise JSONRPCError(
410 raise JSONRPCError(
399 'Ref `{ref}` given in a wrong format. Please check the API'
411 'Ref `{ref}` given in a wrong format. Please check the API'
400 ' documentation for more details'.format(ref=ref))
412 ' documentation for more details'.format(ref=ref))
401
413
414 if ref_type not in allowed_ref_types:
415 raise JSONRPCError(
416 'Ref `{ref}` type is not allowed. '
417 'Only:{allowed_refs} are possible.'.format(
418 ref=ref, allowed_refs=allowed_ref_types))
419
402 try:
420 try:
403 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
421 ref_hash = ref_hash or _get_ref_hash(repo, ref_type, ref_name)
404 except (KeyError, ValueError):
422 except (KeyError, ValueError):
405 raise JSONRPCError(
423 raise JSONRPCError(
406 'The specified value:{type}:`{name}` does not exist, or is not allowed.'.format(
424 'The specified value:{type}:`{name}` does not exist, or is not allowed.'.format(
407 type=ref_type, name=ref_name))
425 type=ref_type, name=ref_name))
408
426
409 return ':'.join([ref_type, ref_name, ref_hash])
427 return ':'.join([ref_type, ref_name, ref_hash])
410
428
411
429
412 def _get_commit_dict(
430 def _get_commit_dict(
413 filename, op, new_revision=None, old_revision=None,
431 filename, op, new_revision=None, old_revision=None,
414 raw_diff=None, stats=None):
432 raw_diff=None, stats=None):
415 if stats is None:
433 if stats is None:
416 stats = {
434 stats = {
417 "added": None,
435 "added": None,
418 "binary": None,
436 "binary": None,
419 "deleted": None
437 "deleted": None
420 }
438 }
421 return {
439 return {
422 "filename": safe_unicode(filename),
440 "filename": safe_unicode(filename),
423 "op": op,
441 "op": op,
424
442
425 # extra details
443 # extra details
426 "new_revision": new_revision,
444 "new_revision": new_revision,
427 "old_revision": old_revision,
445 "old_revision": old_revision,
428
446
429 "raw_diff": raw_diff,
447 "raw_diff": raw_diff,
430 "stats": stats
448 "stats": stats
431 }
449 }
432
433
434 def _get_ref_hash(repo, type_, name):
435 vcs_repo = repo.scm_instance()
436 if type_ == 'branch' and vcs_repo.alias in ('hg', 'git'):
437 return vcs_repo.branches[name]
438 elif type_ == 'bookmark' and vcs_repo.alias == 'hg':
439 return vcs_repo.bookmarks[name]
440 else:
441 raise ValueError()
@@ -1,1069 +1,1069 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 os
21 import os
22
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.apps.repository.views.repo_files import RepoFilesView
26 from rhodecode.apps.repository.views.repo_files import RepoFilesView
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.compat import OrderedDict
28 from rhodecode.lib.compat import OrderedDict
29 from rhodecode.lib.ext_json import json
29 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.vcs import nodes
30 from rhodecode.lib.vcs import nodes
31
31
32 from rhodecode.lib.vcs.conf import settings
32 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.tests import assert_session_flash
33 from rhodecode.tests import assert_session_flash
34 from rhodecode.tests.fixture import Fixture
34 from rhodecode.tests.fixture import Fixture
35 from rhodecode.model.db import Session
35 from rhodecode.model.db import Session
36
36
37 fixture = Fixture()
37 fixture = Fixture()
38
38
39
39
40 def get_node_history(backend_type):
40 def get_node_history(backend_type):
41 return {
41 return {
42 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
42 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
43 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
43 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
44 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
44 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
45 }[backend_type]
45 }[backend_type]
46
46
47
47
48 def route_path(name, params=None, **kwargs):
48 def route_path(name, params=None, **kwargs):
49 import urllib
49 import urllib
50
50
51 base_url = {
51 base_url = {
52 'repo_summary': '/{repo_name}',
52 'repo_summary': '/{repo_name}',
53 'repo_archivefile': '/{repo_name}/archive/{fname}',
53 'repo_archivefile': '/{repo_name}/archive/{fname}',
54 'repo_files_diff': '/{repo_name}/diff/{f_path}',
54 'repo_files_diff': '/{repo_name}/diff/{f_path}',
55 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
55 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
56 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
56 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
57 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
57 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
58 'repo_files:default_commit': '/{repo_name}/files',
58 'repo_files:default_commit': '/{repo_name}/files',
59 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
59 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
60 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
60 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
61 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
61 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
62 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
62 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
63 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
63 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
64 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
64 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
65 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
65 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
66 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
66 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
67 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
67 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
68 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
68 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
69 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
69 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
70 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
70 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
71 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
71 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
72 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
72 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
73 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
73 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
74 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
74 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
75 }[name].format(**kwargs)
75 }[name].format(**kwargs)
76
76
77 if params:
77 if params:
78 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
78 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
79 return base_url
79 return base_url
80
80
81
81
82 def assert_files_in_response(response, files, params):
82 def assert_files_in_response(response, files, params):
83 template = (
83 template = (
84 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
84 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
85 _assert_items_in_response(response, files, template, params)
85 _assert_items_in_response(response, files, template, params)
86
86
87
87
88 def assert_dirs_in_response(response, dirs, params):
88 def assert_dirs_in_response(response, dirs, params):
89 template = (
89 template = (
90 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
90 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
91 _assert_items_in_response(response, dirs, template, params)
91 _assert_items_in_response(response, dirs, template, params)
92
92
93
93
94 def _assert_items_in_response(response, items, template, params):
94 def _assert_items_in_response(response, items, template, params):
95 for item in items:
95 for item in items:
96 item_params = {'name': item}
96 item_params = {'name': item}
97 item_params.update(params)
97 item_params.update(params)
98 response.mustcontain(template % item_params)
98 response.mustcontain(template % item_params)
99
99
100
100
101 def assert_timeago_in_response(response, items, params):
101 def assert_timeago_in_response(response, items, params):
102 for item in items:
102 for item in items:
103 response.mustcontain(h.age_component(params['date']))
103 response.mustcontain(h.age_component(params['date']))
104
104
105
105
106 @pytest.mark.usefixtures("app")
106 @pytest.mark.usefixtures("app")
107 class TestFilesViews(object):
107 class TestFilesViews(object):
108
108
109 def test_show_files(self, backend):
109 def test_show_files(self, backend):
110 response = self.app.get(
110 response = self.app.get(
111 route_path('repo_files',
111 route_path('repo_files',
112 repo_name=backend.repo_name,
112 repo_name=backend.repo_name,
113 commit_id='tip', f_path='/'))
113 commit_id='tip', f_path='/'))
114 commit = backend.repo.get_commit()
114 commit = backend.repo.get_commit()
115
115
116 params = {
116 params = {
117 'repo_name': backend.repo_name,
117 'repo_name': backend.repo_name,
118 'commit_id': commit.raw_id,
118 'commit_id': commit.raw_id,
119 'date': commit.date
119 'date': commit.date
120 }
120 }
121 assert_dirs_in_response(response, ['docs', 'vcs'], params)
121 assert_dirs_in_response(response, ['docs', 'vcs'], params)
122 files = [
122 files = [
123 '.gitignore',
123 '.gitignore',
124 '.hgignore',
124 '.hgignore',
125 '.hgtags',
125 '.hgtags',
126 # TODO: missing in Git
126 # TODO: missing in Git
127 # '.travis.yml',
127 # '.travis.yml',
128 'MANIFEST.in',
128 'MANIFEST.in',
129 'README.rst',
129 'README.rst',
130 # TODO: File is missing in svn repository
130 # TODO: File is missing in svn repository
131 # 'run_test_and_report.sh',
131 # 'run_test_and_report.sh',
132 'setup.cfg',
132 'setup.cfg',
133 'setup.py',
133 'setup.py',
134 'test_and_report.sh',
134 'test_and_report.sh',
135 'tox.ini',
135 'tox.ini',
136 ]
136 ]
137 assert_files_in_response(response, files, params)
137 assert_files_in_response(response, files, params)
138 assert_timeago_in_response(response, files, params)
138 assert_timeago_in_response(response, files, params)
139
139
140 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
140 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
141 repo = backend_hg['subrepos']
141 repo = backend_hg['subrepos']
142 response = self.app.get(
142 response = self.app.get(
143 route_path('repo_files',
143 route_path('repo_files',
144 repo_name=repo.repo_name,
144 repo_name=repo.repo_name,
145 commit_id='tip', f_path='/'))
145 commit_id='tip', f_path='/'))
146 assert_response = response.assert_response()
146 assert_response = response.assert_response()
147 assert_response.contains_one_link(
147 assert_response.contains_one_link(
148 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
148 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
149
149
150 def test_show_files_links_submodules_with_absolute_url_subpaths(
150 def test_show_files_links_submodules_with_absolute_url_subpaths(
151 self, backend_hg):
151 self, backend_hg):
152 repo = backend_hg['subrepos']
152 repo = backend_hg['subrepos']
153 response = self.app.get(
153 response = self.app.get(
154 route_path('repo_files',
154 route_path('repo_files',
155 repo_name=repo.repo_name,
155 repo_name=repo.repo_name,
156 commit_id='tip', f_path='/'))
156 commit_id='tip', f_path='/'))
157 assert_response = response.assert_response()
157 assert_response = response.assert_response()
158 assert_response.contains_one_link(
158 assert_response.contains_one_link(
159 'subpaths-path @ 000000000000',
159 'subpaths-path @ 000000000000',
160 'http://sub-base.example.com/subpaths-path')
160 'http://sub-base.example.com/subpaths-path')
161
161
162 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
162 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
163 def test_files_menu(self, backend):
163 def test_files_menu(self, backend):
164 new_branch = "temp_branch_name"
164 new_branch = "temp_branch_name"
165 commits = [
165 commits = [
166 {'message': 'a'},
166 {'message': 'a'},
167 {'message': 'b', 'branch': new_branch}
167 {'message': 'b', 'branch': new_branch}
168 ]
168 ]
169 backend.create_repo(commits)
169 backend.create_repo(commits)
170 backend.repo.landing_rev = "branch:%s" % new_branch
170 backend.repo.landing_rev = "branch:%s" % new_branch
171 Session().commit()
171 Session().commit()
172
172
173 # get response based on tip and not new commit
173 # get response based on tip and not new commit
174 response = self.app.get(
174 response = self.app.get(
175 route_path('repo_files',
175 route_path('repo_files',
176 repo_name=backend.repo_name,
176 repo_name=backend.repo_name,
177 commit_id='tip', f_path='/'))
177 commit_id='tip', f_path='/'))
178
178
179 # make sure Files menu url is not tip but new commit
179 # make sure Files menu url is not tip but new commit
180 landing_rev = backend.repo.landing_rev[1]
180 landing_rev = backend.repo.landing_rev[1]
181 files_url = route_path('repo_files:default_path',
181 files_url = route_path('repo_files:default_path',
182 repo_name=backend.repo_name,
182 repo_name=backend.repo_name,
183 commit_id=landing_rev)
183 commit_id=landing_rev)
184
184
185 assert landing_rev != 'tip'
185 assert landing_rev != 'tip'
186 response.mustcontain(
186 response.mustcontain(
187 '<li class="active"><a class="menulink" href="%s">' % files_url)
187 '<li class="active"><a class="menulink" href="%s">' % files_url)
188
188
189 def test_show_files_commit(self, backend):
189 def test_show_files_commit(self, backend):
190 commit = backend.repo.get_commit(commit_idx=32)
190 commit = backend.repo.get_commit(commit_idx=32)
191
191
192 response = self.app.get(
192 response = self.app.get(
193 route_path('repo_files',
193 route_path('repo_files',
194 repo_name=backend.repo_name,
194 repo_name=backend.repo_name,
195 commit_id=commit.raw_id, f_path='/'))
195 commit_id=commit.raw_id, f_path='/'))
196
196
197 dirs = ['docs', 'tests']
197 dirs = ['docs', 'tests']
198 files = ['README.rst']
198 files = ['README.rst']
199 params = {
199 params = {
200 'repo_name': backend.repo_name,
200 'repo_name': backend.repo_name,
201 'commit_id': commit.raw_id,
201 'commit_id': commit.raw_id,
202 }
202 }
203 assert_dirs_in_response(response, dirs, params)
203 assert_dirs_in_response(response, dirs, params)
204 assert_files_in_response(response, files, params)
204 assert_files_in_response(response, files, params)
205
205
206 def test_show_files_different_branch(self, backend):
206 def test_show_files_different_branch(self, backend):
207 branches = dict(
207 branches = dict(
208 hg=(150, ['git']),
208 hg=(150, ['git']),
209 # TODO: Git test repository does not contain other branches
209 # TODO: Git test repository does not contain other branches
210 git=(633, ['master']),
210 git=(633, ['master']),
211 # TODO: Branch support in Subversion
211 # TODO: Branch support in Subversion
212 svn=(150, [])
212 svn=(150, [])
213 )
213 )
214 idx, branches = branches[backend.alias]
214 idx, branches = branches[backend.alias]
215 commit = backend.repo.get_commit(commit_idx=idx)
215 commit = backend.repo.get_commit(commit_idx=idx)
216 response = self.app.get(
216 response = self.app.get(
217 route_path('repo_files',
217 route_path('repo_files',
218 repo_name=backend.repo_name,
218 repo_name=backend.repo_name,
219 commit_id=commit.raw_id, f_path='/'))
219 commit_id=commit.raw_id, f_path='/'))
220
220
221 assert_response = response.assert_response()
221 assert_response = response.assert_response()
222 for branch in branches:
222 for branch in branches:
223 assert_response.element_contains('.tags .branchtag', branch)
223 assert_response.element_contains('.tags .branchtag', branch)
224
224
225 def test_show_files_paging(self, backend):
225 def test_show_files_paging(self, backend):
226 repo = backend.repo
226 repo = backend.repo
227 indexes = [73, 92, 109, 1, 0]
227 indexes = [73, 92, 109, 1, 0]
228 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
228 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
229 for rev in indexes]
229 for rev in indexes]
230
230
231 for idx in idx_map:
231 for idx in idx_map:
232 response = self.app.get(
232 response = self.app.get(
233 route_path('repo_files',
233 route_path('repo_files',
234 repo_name=backend.repo_name,
234 repo_name=backend.repo_name,
235 commit_id=idx[1], f_path='/'))
235 commit_id=idx[1], f_path='/'))
236
236
237 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
237 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
238
238
239 def test_file_source(self, backend):
239 def test_file_source(self, backend):
240 commit = backend.repo.get_commit(commit_idx=167)
240 commit = backend.repo.get_commit(commit_idx=167)
241 response = self.app.get(
241 response = self.app.get(
242 route_path('repo_files',
242 route_path('repo_files',
243 repo_name=backend.repo_name,
243 repo_name=backend.repo_name,
244 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
244 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
245
245
246 msgbox = """<div class="commit right-content">%s</div>"""
246 msgbox = """<div class="commit right-content">%s</div>"""
247 response.mustcontain(msgbox % (commit.message, ))
247 response.mustcontain(msgbox % (commit.message, ))
248
248
249 assert_response = response.assert_response()
249 assert_response = response.assert_response()
250 if commit.branch:
250 if commit.branch:
251 assert_response.element_contains(
251 assert_response.element_contains(
252 '.tags.tags-main .branchtag', commit.branch)
252 '.tags.tags-main .branchtag', commit.branch)
253 if commit.tags:
253 if commit.tags:
254 for tag in commit.tags:
254 for tag in commit.tags:
255 assert_response.element_contains('.tags.tags-main .tagtag', tag)
255 assert_response.element_contains('.tags.tags-main .tagtag', tag)
256
256
257 def test_file_source_annotated(self, backend):
257 def test_file_source_annotated(self, backend):
258 response = self.app.get(
258 response = self.app.get(
259 route_path('repo_files:annotated',
259 route_path('repo_files:annotated',
260 repo_name=backend.repo_name,
260 repo_name=backend.repo_name,
261 commit_id='tip', f_path='vcs/nodes.py'))
261 commit_id='tip', f_path='vcs/nodes.py'))
262 expected_commits = {
262 expected_commits = {
263 'hg': 'r356',
263 'hg': 'r356',
264 'git': 'r345',
264 'git': 'r345',
265 'svn': 'r208',
265 'svn': 'r208',
266 }
266 }
267 response.mustcontain(expected_commits[backend.alias])
267 response.mustcontain(expected_commits[backend.alias])
268
268
269 def test_file_source_authors(self, backend):
269 def test_file_source_authors(self, backend):
270 response = self.app.get(
270 response = self.app.get(
271 route_path('repo_file_authors',
271 route_path('repo_file_authors',
272 repo_name=backend.repo_name,
272 repo_name=backend.repo_name,
273 commit_id='tip', f_path='vcs/nodes.py'))
273 commit_id='tip', f_path='vcs/nodes.py'))
274 expected_authors = {
274 expected_authors = {
275 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
275 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
276 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
276 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 'svn': ('marcin', 'lukasz'),
277 'svn': ('marcin', 'lukasz'),
278 }
278 }
279
279
280 for author in expected_authors[backend.alias]:
280 for author in expected_authors[backend.alias]:
281 response.mustcontain(author)
281 response.mustcontain(author)
282
282
283 def test_file_source_authors_with_annotation(self, backend):
283 def test_file_source_authors_with_annotation(self, backend):
284 response = self.app.get(
284 response = self.app.get(
285 route_path('repo_file_authors',
285 route_path('repo_file_authors',
286 repo_name=backend.repo_name,
286 repo_name=backend.repo_name,
287 commit_id='tip', f_path='vcs/nodes.py',
287 commit_id='tip', f_path='vcs/nodes.py',
288 params=dict(annotate=1)))
288 params=dict(annotate=1)))
289 expected_authors = {
289 expected_authors = {
290 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
290 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
291 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
291 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 'svn': ('marcin', 'lukasz'),
292 'svn': ('marcin', 'lukasz'),
293 }
293 }
294
294
295 for author in expected_authors[backend.alias]:
295 for author in expected_authors[backend.alias]:
296 response.mustcontain(author)
296 response.mustcontain(author)
297
297
298 def test_file_source_history(self, backend, xhr_header):
298 def test_file_source_history(self, backend, xhr_header):
299 response = self.app.get(
299 response = self.app.get(
300 route_path('repo_file_history',
300 route_path('repo_file_history',
301 repo_name=backend.repo_name,
301 repo_name=backend.repo_name,
302 commit_id='tip', f_path='vcs/nodes.py'),
302 commit_id='tip', f_path='vcs/nodes.py'),
303 extra_environ=xhr_header)
303 extra_environ=xhr_header)
304 assert get_node_history(backend.alias) == json.loads(response.body)
304 assert get_node_history(backend.alias) == json.loads(response.body)
305
305
306 def test_file_source_history_svn(self, backend_svn, xhr_header):
306 def test_file_source_history_svn(self, backend_svn, xhr_header):
307 simple_repo = backend_svn['svn-simple-layout']
307 simple_repo = backend_svn['svn-simple-layout']
308 response = self.app.get(
308 response = self.app.get(
309 route_path('repo_file_history',
309 route_path('repo_file_history',
310 repo_name=simple_repo.repo_name,
310 repo_name=simple_repo.repo_name,
311 commit_id='tip', f_path='trunk/example.py'),
311 commit_id='tip', f_path='trunk/example.py'),
312 extra_environ=xhr_header)
312 extra_environ=xhr_header)
313
313
314 expected_data = json.loads(
314 expected_data = json.loads(
315 fixture.load_resource('svn_node_history_branches.json'))
315 fixture.load_resource('svn_node_history_branches.json'))
316 assert expected_data == response.json
316 assert expected_data == response.json
317
317
318 def test_file_source_history_with_annotation(self, backend, xhr_header):
318 def test_file_source_history_with_annotation(self, backend, xhr_header):
319 response = self.app.get(
319 response = self.app.get(
320 route_path('repo_file_history',
320 route_path('repo_file_history',
321 repo_name=backend.repo_name,
321 repo_name=backend.repo_name,
322 commit_id='tip', f_path='vcs/nodes.py',
322 commit_id='tip', f_path='vcs/nodes.py',
323 params=dict(annotate=1)),
323 params=dict(annotate=1)),
324
324
325 extra_environ=xhr_header)
325 extra_environ=xhr_header)
326 assert get_node_history(backend.alias) == json.loads(response.body)
326 assert get_node_history(backend.alias) == json.loads(response.body)
327
327
328 def test_tree_search_top_level(self, backend, xhr_header):
328 def test_tree_search_top_level(self, backend, xhr_header):
329 commit = backend.repo.get_commit(commit_idx=173)
329 commit = backend.repo.get_commit(commit_idx=173)
330 response = self.app.get(
330 response = self.app.get(
331 route_path('repo_files_nodelist',
331 route_path('repo_files_nodelist',
332 repo_name=backend.repo_name,
332 repo_name=backend.repo_name,
333 commit_id=commit.raw_id, f_path='/'),
333 commit_id=commit.raw_id, f_path='/'),
334 extra_environ=xhr_header)
334 extra_environ=xhr_header)
335 assert 'nodes' in response.json
335 assert 'nodes' in response.json
336 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
336 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
337
337
338 def test_tree_search_missing_xhr(self, backend):
338 def test_tree_search_missing_xhr(self, backend):
339 self.app.get(
339 self.app.get(
340 route_path('repo_files_nodelist',
340 route_path('repo_files_nodelist',
341 repo_name=backend.repo_name,
341 repo_name=backend.repo_name,
342 commit_id='tip', f_path='/'),
342 commit_id='tip', f_path='/'),
343 status=404)
343 status=404)
344
344
345 def test_tree_search_at_path(self, backend, xhr_header):
345 def test_tree_search_at_path(self, backend, xhr_header):
346 commit = backend.repo.get_commit(commit_idx=173)
346 commit = backend.repo.get_commit(commit_idx=173)
347 response = self.app.get(
347 response = self.app.get(
348 route_path('repo_files_nodelist',
348 route_path('repo_files_nodelist',
349 repo_name=backend.repo_name,
349 repo_name=backend.repo_name,
350 commit_id=commit.raw_id, f_path='/docs'),
350 commit_id=commit.raw_id, f_path='/docs'),
351 extra_environ=xhr_header)
351 extra_environ=xhr_header)
352 assert 'nodes' in response.json
352 assert 'nodes' in response.json
353 nodes = response.json['nodes']
353 nodes = response.json['nodes']
354 assert {'name': 'docs/api', 'type': 'dir'} in nodes
354 assert {'name': 'docs/api', 'type': 'dir'} in nodes
355 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
355 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
356
356
357 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
357 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
358 commit = backend.repo.get_commit(commit_idx=173)
358 commit = backend.repo.get_commit(commit_idx=173)
359 response = self.app.get(
359 response = self.app.get(
360 route_path('repo_files_nodelist',
360 route_path('repo_files_nodelist',
361 repo_name=backend.repo_name,
361 repo_name=backend.repo_name,
362 commit_id=commit.raw_id, f_path='/docs/api'),
362 commit_id=commit.raw_id, f_path='/docs/api'),
363 extra_environ=xhr_header)
363 extra_environ=xhr_header)
364 assert 'nodes' in response.json
364 assert 'nodes' in response.json
365 nodes = response.json['nodes']
365 nodes = response.json['nodes']
366 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
366 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
367
367
368 def test_tree_search_at_path_missing_xhr(self, backend):
368 def test_tree_search_at_path_missing_xhr(self, backend):
369 self.app.get(
369 self.app.get(
370 route_path('repo_files_nodelist',
370 route_path('repo_files_nodelist',
371 repo_name=backend.repo_name,
371 repo_name=backend.repo_name,
372 commit_id='tip', f_path='/docs'),
372 commit_id='tip', f_path='/docs'),
373 status=404)
373 status=404)
374
374
375 def test_nodetree(self, backend, xhr_header):
375 def test_nodetree(self, backend, xhr_header):
376 commit = backend.repo.get_commit(commit_idx=173)
376 commit = backend.repo.get_commit(commit_idx=173)
377 response = self.app.get(
377 response = self.app.get(
378 route_path('repo_nodetree_full',
378 route_path('repo_nodetree_full',
379 repo_name=backend.repo_name,
379 repo_name=backend.repo_name,
380 commit_id=commit.raw_id, f_path='/'),
380 commit_id=commit.raw_id, f_path='/'),
381 extra_environ=xhr_header)
381 extra_environ=xhr_header)
382
382
383 assert_response = response.assert_response()
383 assert_response = response.assert_response()
384
384
385 for attr in ['data-commit-id', 'data-date', 'data-author']:
385 for attr in ['data-commit-id', 'data-date', 'data-author']:
386 elements = assert_response.get_elements('[{}]'.format(attr))
386 elements = assert_response.get_elements('[{}]'.format(attr))
387 assert len(elements) > 1
387 assert len(elements) > 1
388
388
389 for element in elements:
389 for element in elements:
390 assert element.get(attr)
390 assert element.get(attr)
391
391
392 def test_nodetree_if_file(self, backend, xhr_header):
392 def test_nodetree_if_file(self, backend, xhr_header):
393 commit = backend.repo.get_commit(commit_idx=173)
393 commit = backend.repo.get_commit(commit_idx=173)
394 response = self.app.get(
394 response = self.app.get(
395 route_path('repo_nodetree_full',
395 route_path('repo_nodetree_full',
396 repo_name=backend.repo_name,
396 repo_name=backend.repo_name,
397 commit_id=commit.raw_id, f_path='README.rst'),
397 commit_id=commit.raw_id, f_path='README.rst'),
398 extra_environ=xhr_header)
398 extra_environ=xhr_header)
399 assert response.body == ''
399 assert response.body == ''
400
400
401 def test_nodetree_wrong_path(self, backend, xhr_header):
401 def test_nodetree_wrong_path(self, backend, xhr_header):
402 commit = backend.repo.get_commit(commit_idx=173)
402 commit = backend.repo.get_commit(commit_idx=173)
403 response = self.app.get(
403 response = self.app.get(
404 route_path('repo_nodetree_full',
404 route_path('repo_nodetree_full',
405 repo_name=backend.repo_name,
405 repo_name=backend.repo_name,
406 commit_id=commit.raw_id, f_path='/dont-exist'),
406 commit_id=commit.raw_id, f_path='/dont-exist'),
407 extra_environ=xhr_header)
407 extra_environ=xhr_header)
408
408
409 err = 'error: There is no file nor ' \
409 err = 'error: There is no file nor ' \
410 'directory at the given path'
410 'directory at the given path'
411 assert err in response.body
411 assert err in response.body
412
412
413 def test_nodetree_missing_xhr(self, backend):
413 def test_nodetree_missing_xhr(self, backend):
414 self.app.get(
414 self.app.get(
415 route_path('repo_nodetree_full',
415 route_path('repo_nodetree_full',
416 repo_name=backend.repo_name,
416 repo_name=backend.repo_name,
417 commit_id='tip', f_path='/'),
417 commit_id='tip', f_path='/'),
418 status=404)
418 status=404)
419
419
420
420
421 @pytest.mark.usefixtures("app", "autologin_user")
421 @pytest.mark.usefixtures("app", "autologin_user")
422 class TestRawFileHandling(object):
422 class TestRawFileHandling(object):
423
423
424 def test_download_file(self, backend):
424 def test_download_file(self, backend):
425 commit = backend.repo.get_commit(commit_idx=173)
425 commit = backend.repo.get_commit(commit_idx=173)
426 response = self.app.get(
426 response = self.app.get(
427 route_path('repo_file_download',
427 route_path('repo_file_download',
428 repo_name=backend.repo_name,
428 repo_name=backend.repo_name,
429 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
429 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
430
430
431 assert response.content_disposition == "attachment; filename=nodes.py"
431 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
432 assert response.content_type == "text/x-python"
432 assert response.content_type == "text/x-python"
433
433
434 def test_download_file_wrong_cs(self, backend):
434 def test_download_file_wrong_cs(self, backend):
435 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
435 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
436
436
437 response = self.app.get(
437 response = self.app.get(
438 route_path('repo_file_download',
438 route_path('repo_file_download',
439 repo_name=backend.repo_name,
439 repo_name=backend.repo_name,
440 commit_id=raw_id, f_path='vcs/nodes.svg'),
440 commit_id=raw_id, f_path='vcs/nodes.svg'),
441 status=404)
441 status=404)
442
442
443 msg = """No such commit exists for this repository"""
443 msg = """No such commit exists for this repository"""
444 response.mustcontain(msg)
444 response.mustcontain(msg)
445
445
446 def test_download_file_wrong_f_path(self, backend):
446 def test_download_file_wrong_f_path(self, backend):
447 commit = backend.repo.get_commit(commit_idx=173)
447 commit = backend.repo.get_commit(commit_idx=173)
448 f_path = 'vcs/ERRORnodes.py'
448 f_path = 'vcs/ERRORnodes.py'
449
449
450 response = self.app.get(
450 response = self.app.get(
451 route_path('repo_file_download',
451 route_path('repo_file_download',
452 repo_name=backend.repo_name,
452 repo_name=backend.repo_name,
453 commit_id=commit.raw_id, f_path=f_path),
453 commit_id=commit.raw_id, f_path=f_path),
454 status=404)
454 status=404)
455
455
456 msg = (
456 msg = (
457 "There is no file nor directory at the given path: "
457 "There is no file nor directory at the given path: "
458 "`%s` at commit %s" % (f_path, commit.short_id))
458 "`%s` at commit %s" % (f_path, commit.short_id))
459 response.mustcontain(msg)
459 response.mustcontain(msg)
460
460
461 def test_file_raw(self, backend):
461 def test_file_raw(self, backend):
462 commit = backend.repo.get_commit(commit_idx=173)
462 commit = backend.repo.get_commit(commit_idx=173)
463 response = self.app.get(
463 response = self.app.get(
464 route_path('repo_file_raw',
464 route_path('repo_file_raw',
465 repo_name=backend.repo_name,
465 repo_name=backend.repo_name,
466 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
466 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
467
467
468 assert response.content_type == "text/plain"
468 assert response.content_type == "text/plain"
469
469
470 def test_file_raw_binary(self, backend):
470 def test_file_raw_binary(self, backend):
471 commit = backend.repo.get_commit()
471 commit = backend.repo.get_commit()
472 response = self.app.get(
472 response = self.app.get(
473 route_path('repo_file_raw',
473 route_path('repo_file_raw',
474 repo_name=backend.repo_name,
474 repo_name=backend.repo_name,
475 commit_id=commit.raw_id,
475 commit_id=commit.raw_id,
476 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
476 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
477
477
478 assert response.content_disposition == 'inline'
478 assert response.content_disposition == 'inline'
479
479
480 def test_raw_file_wrong_cs(self, backend):
480 def test_raw_file_wrong_cs(self, backend):
481 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
481 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
482
482
483 response = self.app.get(
483 response = self.app.get(
484 route_path('repo_file_raw',
484 route_path('repo_file_raw',
485 repo_name=backend.repo_name,
485 repo_name=backend.repo_name,
486 commit_id=raw_id, f_path='vcs/nodes.svg'),
486 commit_id=raw_id, f_path='vcs/nodes.svg'),
487 status=404)
487 status=404)
488
488
489 msg = """No such commit exists for this repository"""
489 msg = """No such commit exists for this repository"""
490 response.mustcontain(msg)
490 response.mustcontain(msg)
491
491
492 def test_raw_wrong_f_path(self, backend):
492 def test_raw_wrong_f_path(self, backend):
493 commit = backend.repo.get_commit(commit_idx=173)
493 commit = backend.repo.get_commit(commit_idx=173)
494 f_path = 'vcs/ERRORnodes.py'
494 f_path = 'vcs/ERRORnodes.py'
495 response = self.app.get(
495 response = self.app.get(
496 route_path('repo_file_raw',
496 route_path('repo_file_raw',
497 repo_name=backend.repo_name,
497 repo_name=backend.repo_name,
498 commit_id=commit.raw_id, f_path=f_path),
498 commit_id=commit.raw_id, f_path=f_path),
499 status=404)
499 status=404)
500
500
501 msg = (
501 msg = (
502 "There is no file nor directory at the given path: "
502 "There is no file nor directory at the given path: "
503 "`%s` at commit %s" % (f_path, commit.short_id))
503 "`%s` at commit %s" % (f_path, commit.short_id))
504 response.mustcontain(msg)
504 response.mustcontain(msg)
505
505
506 def test_raw_svg_should_not_be_rendered(self, backend):
506 def test_raw_svg_should_not_be_rendered(self, backend):
507 backend.create_repo()
507 backend.create_repo()
508 backend.ensure_file("xss.svg")
508 backend.ensure_file("xss.svg")
509 response = self.app.get(
509 response = self.app.get(
510 route_path('repo_file_raw',
510 route_path('repo_file_raw',
511 repo_name=backend.repo_name,
511 repo_name=backend.repo_name,
512 commit_id='tip', f_path='xss.svg'),)
512 commit_id='tip', f_path='xss.svg'),)
513 # If the content type is image/svg+xml then it allows to render HTML
513 # If the content type is image/svg+xml then it allows to render HTML
514 # and malicious SVG.
514 # and malicious SVG.
515 assert response.content_type == "text/plain"
515 assert response.content_type == "text/plain"
516
516
517
517
518 @pytest.mark.usefixtures("app")
518 @pytest.mark.usefixtures("app")
519 class TestRepositoryArchival(object):
519 class TestRepositoryArchival(object):
520
520
521 def test_archival(self, backend):
521 def test_archival(self, backend):
522 backend.enable_downloads()
522 backend.enable_downloads()
523 commit = backend.repo.get_commit(commit_idx=173)
523 commit = backend.repo.get_commit(commit_idx=173)
524 for archive, info in settings.ARCHIVE_SPECS.items():
524 for archive, info in settings.ARCHIVE_SPECS.items():
525 mime_type, arch_ext = info
525 mime_type, arch_ext = info
526 short = commit.short_id + arch_ext
526 short = commit.short_id + arch_ext
527 fname = commit.raw_id + arch_ext
527 fname = commit.raw_id + arch_ext
528 filename = '%s-%s' % (backend.repo_name, short)
528 filename = '%s-%s' % (backend.repo_name, short)
529 response = self.app.get(
529 response = self.app.get(
530 route_path('repo_archivefile',
530 route_path('repo_archivefile',
531 repo_name=backend.repo_name,
531 repo_name=backend.repo_name,
532 fname=fname))
532 fname=fname))
533
533
534 assert response.status == '200 OK'
534 assert response.status == '200 OK'
535 headers = [
535 headers = [
536 ('Content-Disposition', 'attachment; filename=%s' % filename),
536 ('Content-Disposition', 'attachment; filename=%s' % filename),
537 ('Content-Type', '%s' % mime_type),
537 ('Content-Type', '%s' % mime_type),
538 ]
538 ]
539
539
540 for header in headers:
540 for header in headers:
541 assert header in response.headers.items()
541 assert header in response.headers.items()
542
542
543 @pytest.mark.parametrize('arch_ext',[
543 @pytest.mark.parametrize('arch_ext',[
544 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
544 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
545 def test_archival_wrong_ext(self, backend, arch_ext):
545 def test_archival_wrong_ext(self, backend, arch_ext):
546 backend.enable_downloads()
546 backend.enable_downloads()
547 commit = backend.repo.get_commit(commit_idx=173)
547 commit = backend.repo.get_commit(commit_idx=173)
548
548
549 fname = commit.raw_id + '.' + arch_ext
549 fname = commit.raw_id + '.' + arch_ext
550
550
551 response = self.app.get(
551 response = self.app.get(
552 route_path('repo_archivefile',
552 route_path('repo_archivefile',
553 repo_name=backend.repo_name,
553 repo_name=backend.repo_name,
554 fname=fname))
554 fname=fname))
555 response.mustcontain(
555 response.mustcontain(
556 'Unknown archive type for: `{}`'.format(fname))
556 'Unknown archive type for: `{}`'.format(fname))
557
557
558 @pytest.mark.parametrize('commit_id', [
558 @pytest.mark.parametrize('commit_id', [
559 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
559 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
560 def test_archival_wrong_commit_id(self, backend, commit_id):
560 def test_archival_wrong_commit_id(self, backend, commit_id):
561 backend.enable_downloads()
561 backend.enable_downloads()
562 fname = '%s.zip' % commit_id
562 fname = '%s.zip' % commit_id
563
563
564 response = self.app.get(
564 response = self.app.get(
565 route_path('repo_archivefile',
565 route_path('repo_archivefile',
566 repo_name=backend.repo_name,
566 repo_name=backend.repo_name,
567 fname=fname))
567 fname=fname))
568 response.mustcontain('Unknown commit_id')
568 response.mustcontain('Unknown commit_id')
569
569
570
570
571 @pytest.mark.usefixtures("app")
571 @pytest.mark.usefixtures("app")
572 class TestFilesDiff(object):
572 class TestFilesDiff(object):
573
573
574 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
574 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
575 def test_file_full_diff(self, backend, diff):
575 def test_file_full_diff(self, backend, diff):
576 commit1 = backend.repo.get_commit(commit_idx=-1)
576 commit1 = backend.repo.get_commit(commit_idx=-1)
577 commit2 = backend.repo.get_commit(commit_idx=-2)
577 commit2 = backend.repo.get_commit(commit_idx=-2)
578
578
579 response = self.app.get(
579 response = self.app.get(
580 route_path('repo_files_diff',
580 route_path('repo_files_diff',
581 repo_name=backend.repo_name,
581 repo_name=backend.repo_name,
582 f_path='README'),
582 f_path='README'),
583 params={
583 params={
584 'diff1': commit2.raw_id,
584 'diff1': commit2.raw_id,
585 'diff2': commit1.raw_id,
585 'diff2': commit1.raw_id,
586 'fulldiff': '1',
586 'fulldiff': '1',
587 'diff': diff,
587 'diff': diff,
588 })
588 })
589
589
590 if diff == 'diff':
590 if diff == 'diff':
591 # use redirect since this is OLD view redirecting to compare page
591 # use redirect since this is OLD view redirecting to compare page
592 response = response.follow()
592 response = response.follow()
593
593
594 # It's a symlink to README.rst
594 # It's a symlink to README.rst
595 response.mustcontain('README.rst')
595 response.mustcontain('README.rst')
596 response.mustcontain('No newline at end of file')
596 response.mustcontain('No newline at end of file')
597
597
598 def test_file_binary_diff(self, backend):
598 def test_file_binary_diff(self, backend):
599 commits = [
599 commits = [
600 {'message': 'First commit'},
600 {'message': 'First commit'},
601 {'message': 'Commit with binary',
601 {'message': 'Commit with binary',
602 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
602 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
603 ]
603 ]
604 repo = backend.create_repo(commits=commits)
604 repo = backend.create_repo(commits=commits)
605
605
606 response = self.app.get(
606 response = self.app.get(
607 route_path('repo_files_diff',
607 route_path('repo_files_diff',
608 repo_name=backend.repo_name,
608 repo_name=backend.repo_name,
609 f_path='file.bin'),
609 f_path='file.bin'),
610 params={
610 params={
611 'diff1': repo.get_commit(commit_idx=0).raw_id,
611 'diff1': repo.get_commit(commit_idx=0).raw_id,
612 'diff2': repo.get_commit(commit_idx=1).raw_id,
612 'diff2': repo.get_commit(commit_idx=1).raw_id,
613 'fulldiff': '1',
613 'fulldiff': '1',
614 'diff': 'diff',
614 'diff': 'diff',
615 })
615 })
616 # use redirect since this is OLD view redirecting to compare page
616 # use redirect since this is OLD view redirecting to compare page
617 response = response.follow()
617 response = response.follow()
618 response.mustcontain('Expand 1 commit')
618 response.mustcontain('Expand 1 commit')
619 response.mustcontain('1 file changed: 0 inserted, 0 deleted')
619 response.mustcontain('1 file changed: 0 inserted, 0 deleted')
620
620
621 if backend.alias == 'svn':
621 if backend.alias == 'svn':
622 response.mustcontain('new file 10644')
622 response.mustcontain('new file 10644')
623 # TODO(marcink): SVN doesn't yet detect binary changes
623 # TODO(marcink): SVN doesn't yet detect binary changes
624 else:
624 else:
625 response.mustcontain('new file 100644')
625 response.mustcontain('new file 100644')
626 response.mustcontain('binary diff hidden')
626 response.mustcontain('binary diff hidden')
627
627
628 def test_diff_2way(self, backend):
628 def test_diff_2way(self, backend):
629 commit1 = backend.repo.get_commit(commit_idx=-1)
629 commit1 = backend.repo.get_commit(commit_idx=-1)
630 commit2 = backend.repo.get_commit(commit_idx=-2)
630 commit2 = backend.repo.get_commit(commit_idx=-2)
631 response = self.app.get(
631 response = self.app.get(
632 route_path('repo_files_diff_2way_redirect',
632 route_path('repo_files_diff_2way_redirect',
633 repo_name=backend.repo_name,
633 repo_name=backend.repo_name,
634 f_path='README'),
634 f_path='README'),
635 params={
635 params={
636 'diff1': commit2.raw_id,
636 'diff1': commit2.raw_id,
637 'diff2': commit1.raw_id,
637 'diff2': commit1.raw_id,
638 })
638 })
639 # use redirect since this is OLD view redirecting to compare page
639 # use redirect since this is OLD view redirecting to compare page
640 response = response.follow()
640 response = response.follow()
641
641
642 # It's a symlink to README.rst
642 # It's a symlink to README.rst
643 response.mustcontain('README.rst')
643 response.mustcontain('README.rst')
644 response.mustcontain('No newline at end of file')
644 response.mustcontain('No newline at end of file')
645
645
646 def test_requires_one_commit_id(self, backend, autologin_user):
646 def test_requires_one_commit_id(self, backend, autologin_user):
647 response = self.app.get(
647 response = self.app.get(
648 route_path('repo_files_diff',
648 route_path('repo_files_diff',
649 repo_name=backend.repo_name,
649 repo_name=backend.repo_name,
650 f_path='README.rst'),
650 f_path='README.rst'),
651 status=400)
651 status=400)
652 response.mustcontain(
652 response.mustcontain(
653 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
653 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
654
654
655 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
655 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
656 repo = vcsbackend.repo
656 repo = vcsbackend.repo
657 response = self.app.get(
657 response = self.app.get(
658 route_path('repo_files_diff',
658 route_path('repo_files_diff',
659 repo_name=repo.name,
659 repo_name=repo.name,
660 f_path='does-not-exist-in-any-commit'),
660 f_path='does-not-exist-in-any-commit'),
661 params={
661 params={
662 'diff1': repo[0].raw_id,
662 'diff1': repo[0].raw_id,
663 'diff2': repo[1].raw_id
663 'diff2': repo[1].raw_id
664 })
664 })
665
665
666 response = response.follow()
666 response = response.follow()
667 response.mustcontain('No files')
667 response.mustcontain('No files')
668
668
669 def test_returns_redirect_if_file_not_changed(self, backend):
669 def test_returns_redirect_if_file_not_changed(self, backend):
670 commit = backend.repo.get_commit(commit_idx=-1)
670 commit = backend.repo.get_commit(commit_idx=-1)
671 response = self.app.get(
671 response = self.app.get(
672 route_path('repo_files_diff_2way_redirect',
672 route_path('repo_files_diff_2way_redirect',
673 repo_name=backend.repo_name,
673 repo_name=backend.repo_name,
674 f_path='README'),
674 f_path='README'),
675 params={
675 params={
676 'diff1': commit.raw_id,
676 'diff1': commit.raw_id,
677 'diff2': commit.raw_id,
677 'diff2': commit.raw_id,
678 })
678 })
679
679
680 response = response.follow()
680 response = response.follow()
681 response.mustcontain('No files')
681 response.mustcontain('No files')
682 response.mustcontain('No commits in this compare')
682 response.mustcontain('No commits in this compare')
683
683
684 def test_supports_diff_to_different_path_svn(self, backend_svn):
684 def test_supports_diff_to_different_path_svn(self, backend_svn):
685 #TODO: check this case
685 #TODO: check this case
686 return
686 return
687
687
688 repo = backend_svn['svn-simple-layout'].scm_instance()
688 repo = backend_svn['svn-simple-layout'].scm_instance()
689 commit_id_1 = '24'
689 commit_id_1 = '24'
690 commit_id_2 = '26'
690 commit_id_2 = '26'
691
691
692 response = self.app.get(
692 response = self.app.get(
693 route_path('repo_files_diff',
693 route_path('repo_files_diff',
694 repo_name=backend_svn.repo_name,
694 repo_name=backend_svn.repo_name,
695 f_path='trunk/example.py'),
695 f_path='trunk/example.py'),
696 params={
696 params={
697 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
697 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
698 'diff2': commit_id_2,
698 'diff2': commit_id_2,
699 })
699 })
700
700
701 response = response.follow()
701 response = response.follow()
702 response.mustcontain(
702 response.mustcontain(
703 # diff contains this
703 # diff contains this
704 "Will print out a useful message on invocation.")
704 "Will print out a useful message on invocation.")
705
705
706 # Note: Expecting that we indicate the user what's being compared
706 # Note: Expecting that we indicate the user what's being compared
707 response.mustcontain("trunk/example.py")
707 response.mustcontain("trunk/example.py")
708 response.mustcontain("tags/v0.2/example.py")
708 response.mustcontain("tags/v0.2/example.py")
709
709
710 def test_show_rev_redirects_to_svn_path(self, backend_svn):
710 def test_show_rev_redirects_to_svn_path(self, backend_svn):
711 #TODO: check this case
711 #TODO: check this case
712 return
712 return
713
713
714 repo = backend_svn['svn-simple-layout'].scm_instance()
714 repo = backend_svn['svn-simple-layout'].scm_instance()
715 commit_id = repo[-1].raw_id
715 commit_id = repo[-1].raw_id
716
716
717 response = self.app.get(
717 response = self.app.get(
718 route_path('repo_files_diff',
718 route_path('repo_files_diff',
719 repo_name=backend_svn.repo_name,
719 repo_name=backend_svn.repo_name,
720 f_path='trunk/example.py'),
720 f_path='trunk/example.py'),
721 params={
721 params={
722 'diff1': 'branches/argparse/example.py@' + commit_id,
722 'diff1': 'branches/argparse/example.py@' + commit_id,
723 'diff2': commit_id,
723 'diff2': commit_id,
724 },
724 },
725 status=302)
725 status=302)
726 response = response.follow()
726 response = response.follow()
727 assert response.headers['Location'].endswith(
727 assert response.headers['Location'].endswith(
728 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
728 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
729
729
730 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
730 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
731 #TODO: check this case
731 #TODO: check this case
732 return
732 return
733
733
734 repo = backend_svn['svn-simple-layout'].scm_instance()
734 repo = backend_svn['svn-simple-layout'].scm_instance()
735 commit_id = repo[-1].raw_id
735 commit_id = repo[-1].raw_id
736 response = self.app.get(
736 response = self.app.get(
737 route_path('repo_files_diff',
737 route_path('repo_files_diff',
738 repo_name=backend_svn.repo_name,
738 repo_name=backend_svn.repo_name,
739 f_path='trunk/example.py'),
739 f_path='trunk/example.py'),
740 params={
740 params={
741 'diff1': 'branches/argparse/example.py@' + commit_id,
741 'diff1': 'branches/argparse/example.py@' + commit_id,
742 'diff2': commit_id,
742 'diff2': commit_id,
743 'show_rev': 'Show at Revision',
743 'show_rev': 'Show at Revision',
744 'annotate': 'true',
744 'annotate': 'true',
745 },
745 },
746 status=302)
746 status=302)
747 response = response.follow()
747 response = response.follow()
748 assert response.headers['Location'].endswith(
748 assert response.headers['Location'].endswith(
749 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
749 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
750
750
751
751
752 @pytest.mark.usefixtures("app", "autologin_user")
752 @pytest.mark.usefixtures("app", "autologin_user")
753 class TestModifyFilesWithWebInterface(object):
753 class TestModifyFilesWithWebInterface(object):
754
754
755 def test_add_file_view(self, backend):
755 def test_add_file_view(self, backend):
756 self.app.get(
756 self.app.get(
757 route_path('repo_files_add_file',
757 route_path('repo_files_add_file',
758 repo_name=backend.repo_name,
758 repo_name=backend.repo_name,
759 commit_id='tip', f_path='/')
759 commit_id='tip', f_path='/')
760 )
760 )
761
761
762 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
762 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
763 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
763 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
764 repo = backend.create_repo()
764 repo = backend.create_repo()
765 filename = 'init.py'
765 filename = 'init.py'
766 response = self.app.post(
766 response = self.app.post(
767 route_path('repo_files_create_file',
767 route_path('repo_files_create_file',
768 repo_name=backend.repo_name,
768 repo_name=backend.repo_name,
769 commit_id='tip', f_path='/'),
769 commit_id='tip', f_path='/'),
770 params={
770 params={
771 'content': "",
771 'content': "",
772 'filename': filename,
772 'filename': filename,
773 'location': "",
773 'location': "",
774 'csrf_token': csrf_token,
774 'csrf_token': csrf_token,
775 },
775 },
776 status=302)
776 status=302)
777 assert_session_flash(response,
777 assert_session_flash(response,
778 'Successfully committed new file `{}`'.format(
778 'Successfully committed new file `{}`'.format(
779 os.path.join(filename)))
779 os.path.join(filename)))
780
780
781 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
781 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
782 response = self.app.post(
782 response = self.app.post(
783 route_path('repo_files_create_file',
783 route_path('repo_files_create_file',
784 repo_name=backend.repo_name,
784 repo_name=backend.repo_name,
785 commit_id='tip', f_path='/'),
785 commit_id='tip', f_path='/'),
786 params={
786 params={
787 'content': "foo",
787 'content': "foo",
788 'csrf_token': csrf_token,
788 'csrf_token': csrf_token,
789 },
789 },
790 status=302)
790 status=302)
791
791
792 assert_session_flash(response, 'No filename')
792 assert_session_flash(response, 'No filename')
793
793
794 def test_add_file_into_repo_errors_and_no_commits(
794 def test_add_file_into_repo_errors_and_no_commits(
795 self, backend, csrf_token):
795 self, backend, csrf_token):
796 repo = backend.create_repo()
796 repo = backend.create_repo()
797 # Create a file with no filename, it will display an error but
797 # Create a file with no filename, it will display an error but
798 # the repo has no commits yet
798 # the repo has no commits yet
799 response = self.app.post(
799 response = self.app.post(
800 route_path('repo_files_create_file',
800 route_path('repo_files_create_file',
801 repo_name=repo.repo_name,
801 repo_name=repo.repo_name,
802 commit_id='tip', f_path='/'),
802 commit_id='tip', f_path='/'),
803 params={
803 params={
804 'content': "foo",
804 'content': "foo",
805 'csrf_token': csrf_token,
805 'csrf_token': csrf_token,
806 },
806 },
807 status=302)
807 status=302)
808
808
809 assert_session_flash(response, 'No filename')
809 assert_session_flash(response, 'No filename')
810
810
811 # Not allowed, redirect to the summary
811 # Not allowed, redirect to the summary
812 redirected = response.follow()
812 redirected = response.follow()
813 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
813 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
814
814
815 # As there are no commits, displays the summary page with the error of
815 # As there are no commits, displays the summary page with the error of
816 # creating a file with no filename
816 # creating a file with no filename
817
817
818 assert redirected.request.path == summary_url
818 assert redirected.request.path == summary_url
819
819
820 @pytest.mark.parametrize("location, filename", [
820 @pytest.mark.parametrize("location, filename", [
821 ('/abs', 'foo'),
821 ('/abs', 'foo'),
822 ('../rel', 'foo'),
822 ('../rel', 'foo'),
823 ('file/../foo', 'foo'),
823 ('file/../foo', 'foo'),
824 ])
824 ])
825 def test_add_file_into_repo_bad_filenames(
825 def test_add_file_into_repo_bad_filenames(
826 self, location, filename, backend, csrf_token):
826 self, location, filename, backend, csrf_token):
827 response = self.app.post(
827 response = self.app.post(
828 route_path('repo_files_create_file',
828 route_path('repo_files_create_file',
829 repo_name=backend.repo_name,
829 repo_name=backend.repo_name,
830 commit_id='tip', f_path='/'),
830 commit_id='tip', f_path='/'),
831 params={
831 params={
832 'content': "foo",
832 'content': "foo",
833 'filename': filename,
833 'filename': filename,
834 'location': location,
834 'location': location,
835 'csrf_token': csrf_token,
835 'csrf_token': csrf_token,
836 },
836 },
837 status=302)
837 status=302)
838
838
839 assert_session_flash(
839 assert_session_flash(
840 response,
840 response,
841 'The location specified must be a relative path and must not '
841 'The location specified must be a relative path and must not '
842 'contain .. in the path')
842 'contain .. in the path')
843
843
844 @pytest.mark.parametrize("cnt, location, filename", [
844 @pytest.mark.parametrize("cnt, location, filename", [
845 (1, '', 'foo.txt'),
845 (1, '', 'foo.txt'),
846 (2, 'dir', 'foo.rst'),
846 (2, 'dir', 'foo.rst'),
847 (3, 'rel/dir', 'foo.bar'),
847 (3, 'rel/dir', 'foo.bar'),
848 ])
848 ])
849 def test_add_file_into_repo(self, cnt, location, filename, backend,
849 def test_add_file_into_repo(self, cnt, location, filename, backend,
850 csrf_token):
850 csrf_token):
851 repo = backend.create_repo()
851 repo = backend.create_repo()
852 response = self.app.post(
852 response = self.app.post(
853 route_path('repo_files_create_file',
853 route_path('repo_files_create_file',
854 repo_name=repo.repo_name,
854 repo_name=repo.repo_name,
855 commit_id='tip', f_path='/'),
855 commit_id='tip', f_path='/'),
856 params={
856 params={
857 'content': "foo",
857 'content': "foo",
858 'filename': filename,
858 'filename': filename,
859 'location': location,
859 'location': location,
860 'csrf_token': csrf_token,
860 'csrf_token': csrf_token,
861 },
861 },
862 status=302)
862 status=302)
863 assert_session_flash(response,
863 assert_session_flash(response,
864 'Successfully committed new file `{}`'.format(
864 'Successfully committed new file `{}`'.format(
865 os.path.join(location, filename)))
865 os.path.join(location, filename)))
866
866
867 def test_edit_file_view(self, backend):
867 def test_edit_file_view(self, backend):
868 response = self.app.get(
868 response = self.app.get(
869 route_path('repo_files_edit_file',
869 route_path('repo_files_edit_file',
870 repo_name=backend.repo_name,
870 repo_name=backend.repo_name,
871 commit_id=backend.default_head_id,
871 commit_id=backend.default_head_id,
872 f_path='vcs/nodes.py'),
872 f_path='vcs/nodes.py'),
873 status=200)
873 status=200)
874 response.mustcontain("Module holding everything related to vcs nodes.")
874 response.mustcontain("Module holding everything related to vcs nodes.")
875
875
876 def test_edit_file_view_not_on_branch(self, backend):
876 def test_edit_file_view_not_on_branch(self, backend):
877 repo = backend.create_repo()
877 repo = backend.create_repo()
878 backend.ensure_file("vcs/nodes.py")
878 backend.ensure_file("vcs/nodes.py")
879
879
880 response = self.app.get(
880 response = self.app.get(
881 route_path('repo_files_edit_file',
881 route_path('repo_files_edit_file',
882 repo_name=repo.repo_name,
882 repo_name=repo.repo_name,
883 commit_id='tip',
883 commit_id='tip',
884 f_path='vcs/nodes.py'),
884 f_path='vcs/nodes.py'),
885 status=302)
885 status=302)
886 assert_session_flash(
886 assert_session_flash(
887 response,
887 response,
888 'You can only edit files with commit being a valid branch')
888 'You can only edit files with commit being a valid branch')
889
889
890 def test_edit_file_view_commit_changes(self, backend, csrf_token):
890 def test_edit_file_view_commit_changes(self, backend, csrf_token):
891 repo = backend.create_repo()
891 repo = backend.create_repo()
892 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
892 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
893
893
894 response = self.app.post(
894 response = self.app.post(
895 route_path('repo_files_update_file',
895 route_path('repo_files_update_file',
896 repo_name=repo.repo_name,
896 repo_name=repo.repo_name,
897 commit_id=backend.default_head_id,
897 commit_id=backend.default_head_id,
898 f_path='vcs/nodes.py'),
898 f_path='vcs/nodes.py'),
899 params={
899 params={
900 'content': "print 'hello world'",
900 'content': "print 'hello world'",
901 'message': 'I committed',
901 'message': 'I committed',
902 'filename': "vcs/nodes.py",
902 'filename': "vcs/nodes.py",
903 'csrf_token': csrf_token,
903 'csrf_token': csrf_token,
904 },
904 },
905 status=302)
905 status=302)
906 assert_session_flash(
906 assert_session_flash(
907 response, 'Successfully committed changes to file `vcs/nodes.py`')
907 response, 'Successfully committed changes to file `vcs/nodes.py`')
908 tip = repo.get_commit(commit_idx=-1)
908 tip = repo.get_commit(commit_idx=-1)
909 assert tip.message == 'I committed'
909 assert tip.message == 'I committed'
910
910
911 def test_edit_file_view_commit_changes_default_message(self, backend,
911 def test_edit_file_view_commit_changes_default_message(self, backend,
912 csrf_token):
912 csrf_token):
913 repo = backend.create_repo()
913 repo = backend.create_repo()
914 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
914 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
915
915
916 commit_id = (
916 commit_id = (
917 backend.default_branch_name or
917 backend.default_branch_name or
918 backend.repo.scm_instance().commit_ids[-1])
918 backend.repo.scm_instance().commit_ids[-1])
919
919
920 response = self.app.post(
920 response = self.app.post(
921 route_path('repo_files_update_file',
921 route_path('repo_files_update_file',
922 repo_name=repo.repo_name,
922 repo_name=repo.repo_name,
923 commit_id=commit_id,
923 commit_id=commit_id,
924 f_path='vcs/nodes.py'),
924 f_path='vcs/nodes.py'),
925 params={
925 params={
926 'content': "print 'hello world'",
926 'content': "print 'hello world'",
927 'message': '',
927 'message': '',
928 'filename': "vcs/nodes.py",
928 'filename': "vcs/nodes.py",
929 'csrf_token': csrf_token,
929 'csrf_token': csrf_token,
930 },
930 },
931 status=302)
931 status=302)
932 assert_session_flash(
932 assert_session_flash(
933 response, 'Successfully committed changes to file `vcs/nodes.py`')
933 response, 'Successfully committed changes to file `vcs/nodes.py`')
934 tip = repo.get_commit(commit_idx=-1)
934 tip = repo.get_commit(commit_idx=-1)
935 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
935 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
936
936
937 def test_delete_file_view(self, backend):
937 def test_delete_file_view(self, backend):
938 self.app.get(
938 self.app.get(
939 route_path('repo_files_remove_file',
939 route_path('repo_files_remove_file',
940 repo_name=backend.repo_name,
940 repo_name=backend.repo_name,
941 commit_id=backend.default_head_id,
941 commit_id=backend.default_head_id,
942 f_path='vcs/nodes.py'),
942 f_path='vcs/nodes.py'),
943 status=200)
943 status=200)
944
944
945 def test_delete_file_view_not_on_branch(self, backend):
945 def test_delete_file_view_not_on_branch(self, backend):
946 repo = backend.create_repo()
946 repo = backend.create_repo()
947 backend.ensure_file('vcs/nodes.py')
947 backend.ensure_file('vcs/nodes.py')
948
948
949 response = self.app.get(
949 response = self.app.get(
950 route_path('repo_files_remove_file',
950 route_path('repo_files_remove_file',
951 repo_name=repo.repo_name,
951 repo_name=repo.repo_name,
952 commit_id='tip',
952 commit_id='tip',
953 f_path='vcs/nodes.py'),
953 f_path='vcs/nodes.py'),
954 status=302)
954 status=302)
955 assert_session_flash(
955 assert_session_flash(
956 response,
956 response,
957 'You can only delete files with commit being a valid branch')
957 'You can only delete files with commit being a valid branch')
958
958
959 def test_delete_file_view_commit_changes(self, backend, csrf_token):
959 def test_delete_file_view_commit_changes(self, backend, csrf_token):
960 repo = backend.create_repo()
960 repo = backend.create_repo()
961 backend.ensure_file("vcs/nodes.py")
961 backend.ensure_file("vcs/nodes.py")
962
962
963 response = self.app.post(
963 response = self.app.post(
964 route_path('repo_files_delete_file',
964 route_path('repo_files_delete_file',
965 repo_name=repo.repo_name,
965 repo_name=repo.repo_name,
966 commit_id=backend.default_head_id,
966 commit_id=backend.default_head_id,
967 f_path='vcs/nodes.py'),
967 f_path='vcs/nodes.py'),
968 params={
968 params={
969 'message': 'i commited',
969 'message': 'i commited',
970 'csrf_token': csrf_token,
970 'csrf_token': csrf_token,
971 },
971 },
972 status=302)
972 status=302)
973 assert_session_flash(
973 assert_session_flash(
974 response, 'Successfully deleted file `vcs/nodes.py`')
974 response, 'Successfully deleted file `vcs/nodes.py`')
975
975
976
976
977 @pytest.mark.usefixtures("app")
977 @pytest.mark.usefixtures("app")
978 class TestFilesViewOtherCases(object):
978 class TestFilesViewOtherCases(object):
979
979
980 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
980 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
981 self, backend_stub, autologin_regular_user, user_regular,
981 self, backend_stub, autologin_regular_user, user_regular,
982 user_util):
982 user_util):
983
983
984 repo = backend_stub.create_repo()
984 repo = backend_stub.create_repo()
985 user_util.grant_user_permission_to_repo(
985 user_util.grant_user_permission_to_repo(
986 repo, user_regular, 'repository.write')
986 repo, user_regular, 'repository.write')
987 response = self.app.get(
987 response = self.app.get(
988 route_path('repo_files',
988 route_path('repo_files',
989 repo_name=repo.repo_name,
989 repo_name=repo.repo_name,
990 commit_id='tip', f_path='/'))
990 commit_id='tip', f_path='/'))
991
991
992 repo_file_add_url = route_path(
992 repo_file_add_url = route_path(
993 'repo_files_add_file',
993 'repo_files_add_file',
994 repo_name=repo.repo_name,
994 repo_name=repo.repo_name,
995 commit_id=0, f_path='') + '#edit'
995 commit_id=0, f_path='') + '#edit'
996
996
997 assert_session_flash(
997 assert_session_flash(
998 response,
998 response,
999 'There are no files yet. <a class="alert-link" '
999 'There are no files yet. <a class="alert-link" '
1000 'href="{}">Click here to add a new file.</a>'
1000 'href="{}">Click here to add a new file.</a>'
1001 .format(repo_file_add_url))
1001 .format(repo_file_add_url))
1002
1002
1003 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1003 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1004 self, backend_stub, autologin_regular_user):
1004 self, backend_stub, autologin_regular_user):
1005 repo = backend_stub.create_repo()
1005 repo = backend_stub.create_repo()
1006 # init session for anon user
1006 # init session for anon user
1007 route_path('repo_summary', repo_name=repo.repo_name)
1007 route_path('repo_summary', repo_name=repo.repo_name)
1008
1008
1009 repo_file_add_url = route_path(
1009 repo_file_add_url = route_path(
1010 'repo_files_add_file',
1010 'repo_files_add_file',
1011 repo_name=repo.repo_name,
1011 repo_name=repo.repo_name,
1012 commit_id=0, f_path='') + '#edit'
1012 commit_id=0, f_path='') + '#edit'
1013
1013
1014 response = self.app.get(
1014 response = self.app.get(
1015 route_path('repo_files',
1015 route_path('repo_files',
1016 repo_name=repo.repo_name,
1016 repo_name=repo.repo_name,
1017 commit_id='tip', f_path='/'))
1017 commit_id='tip', f_path='/'))
1018
1018
1019 assert_session_flash(response, no_=repo_file_add_url)
1019 assert_session_flash(response, no_=repo_file_add_url)
1020
1020
1021 @pytest.mark.parametrize('file_node', [
1021 @pytest.mark.parametrize('file_node', [
1022 'archive/file.zip',
1022 'archive/file.zip',
1023 'diff/my-file.txt',
1023 'diff/my-file.txt',
1024 'render.py',
1024 'render.py',
1025 'render',
1025 'render',
1026 'remove_file',
1026 'remove_file',
1027 'remove_file/to-delete.txt',
1027 'remove_file/to-delete.txt',
1028 ])
1028 ])
1029 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1029 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1030 backend.create_repo()
1030 backend.create_repo()
1031 backend.ensure_file(file_node)
1031 backend.ensure_file(file_node)
1032
1032
1033 self.app.get(
1033 self.app.get(
1034 route_path('repo_files',
1034 route_path('repo_files',
1035 repo_name=backend.repo_name,
1035 repo_name=backend.repo_name,
1036 commit_id='tip', f_path=file_node),
1036 commit_id='tip', f_path=file_node),
1037 status=200)
1037 status=200)
1038
1038
1039
1039
1040 class TestAdjustFilePathForSvn(object):
1040 class TestAdjustFilePathForSvn(object):
1041 """
1041 """
1042 SVN specific adjustments of node history in RepoFilesView.
1042 SVN specific adjustments of node history in RepoFilesView.
1043 """
1043 """
1044
1044
1045 def test_returns_path_relative_to_matched_reference(self):
1045 def test_returns_path_relative_to_matched_reference(self):
1046 repo = self._repo(branches=['trunk'])
1046 repo = self._repo(branches=['trunk'])
1047 self.assert_file_adjustment('trunk/file', 'file', repo)
1047 self.assert_file_adjustment('trunk/file', 'file', repo)
1048
1048
1049 def test_does_not_modify_file_if_no_reference_matches(self):
1049 def test_does_not_modify_file_if_no_reference_matches(self):
1050 repo = self._repo(branches=['trunk'])
1050 repo = self._repo(branches=['trunk'])
1051 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1051 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1052
1052
1053 def test_does_not_adjust_partial_directory_names(self):
1053 def test_does_not_adjust_partial_directory_names(self):
1054 repo = self._repo(branches=['trun'])
1054 repo = self._repo(branches=['trun'])
1055 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1055 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1056
1056
1057 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1057 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1058 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1058 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1059 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1059 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1060
1060
1061 def assert_file_adjustment(self, f_path, expected, repo):
1061 def assert_file_adjustment(self, f_path, expected, repo):
1062 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1062 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1063 assert result == expected
1063 assert result == expected
1064
1064
1065 def _repo(self, branches=None):
1065 def _repo(self, branches=None):
1066 repo = mock.Mock()
1066 repo = mock.Mock()
1067 repo.branches = OrderedDict((name, '0') for name in branches or [])
1067 repo.branches = OrderedDict((name, '0') for name in branches or [])
1068 repo.tags = {}
1068 repo.tags = {}
1069 return repo
1069 return repo
@@ -1,1386 +1,1392 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 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 itertools
21 import itertools
22 import logging
22 import logging
23 import os
23 import os
24 import shutil
24 import shutil
25 import tempfile
25 import tempfile
26 import collections
26 import collections
27 import urllib
27
28
28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 from pyramid.view import view_config
30 from pyramid.view import view_config
30 from pyramid.renderers import render
31 from pyramid.renderers import render
31 from pyramid.response import Response
32 from pyramid.response import Response
32
33
33 import rhodecode
34 import rhodecode
34 from rhodecode.apps._base import RepoAppView
35 from rhodecode.apps._base import RepoAppView
35
36
36
37
37 from rhodecode.lib import diffs, helpers as h, rc_cache
38 from rhodecode.lib import diffs, helpers as h, rc_cache
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib.view_utils import parse_path_ref
40 from rhodecode.lib.view_utils import parse_path_ref
40 from rhodecode.lib.exceptions import NonRelativePathError
41 from rhodecode.lib.exceptions import NonRelativePathError
41 from rhodecode.lib.codeblocks import (
42 from rhodecode.lib.codeblocks import (
42 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
43 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
43 from rhodecode.lib.utils2 import (
44 from rhodecode.lib.utils2 import (
44 convert_line_endings, detect_mode, safe_str, str2bool, safe_int)
45 convert_line_endings, detect_mode, safe_str, str2bool, safe_int)
45 from rhodecode.lib.auth import (
46 from rhodecode.lib.auth import (
46 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
47 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
47 from rhodecode.lib.vcs import path as vcspath
48 from rhodecode.lib.vcs import path as vcspath
48 from rhodecode.lib.vcs.backends.base import EmptyCommit
49 from rhodecode.lib.vcs.backends.base import EmptyCommit
49 from rhodecode.lib.vcs.conf import settings
50 from rhodecode.lib.vcs.conf import settings
50 from rhodecode.lib.vcs.nodes import FileNode
51 from rhodecode.lib.vcs.nodes import FileNode
51 from rhodecode.lib.vcs.exceptions import (
52 from rhodecode.lib.vcs.exceptions import (
52 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
53 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
53 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
54 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
54 NodeDoesNotExistError, CommitError, NodeError)
55 NodeDoesNotExistError, CommitError, NodeError)
55
56
56 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.db import Repository
58 from rhodecode.model.db import Repository
58
59
59 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
60
61
61
62
62 class RepoFilesView(RepoAppView):
63 class RepoFilesView(RepoAppView):
63
64
64 @staticmethod
65 @staticmethod
65 def adjust_file_path_for_svn(f_path, repo):
66 def adjust_file_path_for_svn(f_path, repo):
66 """
67 """
67 Computes the relative path of `f_path`.
68 Computes the relative path of `f_path`.
68
69
69 This is mainly based on prefix matching of the recognized tags and
70 This is mainly based on prefix matching of the recognized tags and
70 branches in the underlying repository.
71 branches in the underlying repository.
71 """
72 """
72 tags_and_branches = itertools.chain(
73 tags_and_branches = itertools.chain(
73 repo.branches.iterkeys(),
74 repo.branches.iterkeys(),
74 repo.tags.iterkeys())
75 repo.tags.iterkeys())
75 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
76 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
76
77
77 for name in tags_and_branches:
78 for name in tags_and_branches:
78 if f_path.startswith('{}/'.format(name)):
79 if f_path.startswith('{}/'.format(name)):
79 f_path = vcspath.relpath(f_path, name)
80 f_path = vcspath.relpath(f_path, name)
80 break
81 break
81 return f_path
82 return f_path
82
83
83 def load_default_context(self):
84 def load_default_context(self):
84 c = self._get_local_tmpl_context(include_app_defaults=True)
85 c = self._get_local_tmpl_context(include_app_defaults=True)
85 c.rhodecode_repo = self.rhodecode_vcs_repo
86 c.rhodecode_repo = self.rhodecode_vcs_repo
86 return c
87 return c
87
88
88 def _ensure_not_locked(self):
89 def _ensure_not_locked(self):
89 _ = self.request.translate
90 _ = self.request.translate
90
91
91 repo = self.db_repo
92 repo = self.db_repo
92 if repo.enable_locking and repo.locked[0]:
93 if repo.enable_locking and repo.locked[0]:
93 h.flash(_('This repository has been locked by %s on %s')
94 h.flash(_('This repository has been locked by %s on %s')
94 % (h.person_by_id(repo.locked[0]),
95 % (h.person_by_id(repo.locked[0]),
95 h.format_date(h.time_to_datetime(repo.locked[1]))),
96 h.format_date(h.time_to_datetime(repo.locked[1]))),
96 'warning')
97 'warning')
97 files_url = h.route_path(
98 files_url = h.route_path(
98 'repo_files:default_path',
99 'repo_files:default_path',
99 repo_name=self.db_repo_name, commit_id='tip')
100 repo_name=self.db_repo_name, commit_id='tip')
100 raise HTTPFound(files_url)
101 raise HTTPFound(files_url)
101
102
102 def check_branch_permission(self, branch_name):
103 def check_branch_permission(self, branch_name):
103 _ = self.request.translate
104 _ = self.request.translate
104
105
105 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
106 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
106 self.db_repo_name, branch_name)
107 self.db_repo_name, branch_name)
107 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
108 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
108 h.flash(
109 h.flash(
109 _('Branch `{}` changes forbidden by rule {}.').format(branch_name, rule),
110 _('Branch `{}` changes forbidden by rule {}.').format(branch_name, rule),
110 'warning')
111 'warning')
111 files_url = h.route_path(
112 files_url = h.route_path(
112 'repo_files:default_path',
113 'repo_files:default_path',
113 repo_name=self.db_repo_name, commit_id='tip')
114 repo_name=self.db_repo_name, commit_id='tip')
114 raise HTTPFound(files_url)
115 raise HTTPFound(files_url)
115
116
116 def _get_commit_and_path(self):
117 def _get_commit_and_path(self):
117 default_commit_id = self.db_repo.landing_rev[1]
118 default_commit_id = self.db_repo.landing_rev[1]
118 default_f_path = '/'
119 default_f_path = '/'
119
120
120 commit_id = self.request.matchdict.get(
121 commit_id = self.request.matchdict.get(
121 'commit_id', default_commit_id)
122 'commit_id', default_commit_id)
122 f_path = self._get_f_path(self.request.matchdict, default_f_path)
123 f_path = self._get_f_path(self.request.matchdict, default_f_path)
123 return commit_id, f_path
124 return commit_id, f_path
124
125
125 def _get_default_encoding(self, c):
126 def _get_default_encoding(self, c):
126 enc_list = getattr(c, 'default_encodings', [])
127 enc_list = getattr(c, 'default_encodings', [])
127 return enc_list[0] if enc_list else 'UTF-8'
128 return enc_list[0] if enc_list else 'UTF-8'
128
129
129 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
130 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
130 """
131 """
131 This is a safe way to get commit. If an error occurs it redirects to
132 This is a safe way to get commit. If an error occurs it redirects to
132 tip with proper message
133 tip with proper message
133
134
134 :param commit_id: id of commit to fetch
135 :param commit_id: id of commit to fetch
135 :param redirect_after: toggle redirection
136 :param redirect_after: toggle redirection
136 """
137 """
137 _ = self.request.translate
138 _ = self.request.translate
138
139
139 try:
140 try:
140 return self.rhodecode_vcs_repo.get_commit(commit_id)
141 return self.rhodecode_vcs_repo.get_commit(commit_id)
141 except EmptyRepositoryError:
142 except EmptyRepositoryError:
142 if not redirect_after:
143 if not redirect_after:
143 return None
144 return None
144
145
145 _url = h.route_path(
146 _url = h.route_path(
146 'repo_files_add_file',
147 'repo_files_add_file',
147 repo_name=self.db_repo_name, commit_id=0, f_path='',
148 repo_name=self.db_repo_name, commit_id=0, f_path='',
148 _anchor='edit')
149 _anchor='edit')
149
150
150 if h.HasRepoPermissionAny(
151 if h.HasRepoPermissionAny(
151 'repository.write', 'repository.admin')(self.db_repo_name):
152 'repository.write', 'repository.admin')(self.db_repo_name):
152 add_new = h.link_to(
153 add_new = h.link_to(
153 _('Click here to add a new file.'), _url, class_="alert-link")
154 _('Click here to add a new file.'), _url, class_="alert-link")
154 else:
155 else:
155 add_new = ""
156 add_new = ""
156
157
157 h.flash(h.literal(
158 h.flash(h.literal(
158 _('There are no files yet. %s') % add_new), category='warning')
159 _('There are no files yet. %s') % add_new), category='warning')
159 raise HTTPFound(
160 raise HTTPFound(
160 h.route_path('repo_summary', repo_name=self.db_repo_name))
161 h.route_path('repo_summary', repo_name=self.db_repo_name))
161
162
162 except (CommitDoesNotExistError, LookupError):
163 except (CommitDoesNotExistError, LookupError):
163 msg = _('No such commit exists for this repository')
164 msg = _('No such commit exists for this repository')
164 h.flash(msg, category='error')
165 h.flash(msg, category='error')
165 raise HTTPNotFound()
166 raise HTTPNotFound()
166 except RepositoryError as e:
167 except RepositoryError as e:
167 h.flash(safe_str(h.escape(e)), category='error')
168 h.flash(safe_str(h.escape(e)), category='error')
168 raise HTTPNotFound()
169 raise HTTPNotFound()
169
170
170 def _get_filenode_or_redirect(self, commit_obj, path):
171 def _get_filenode_or_redirect(self, commit_obj, path):
171 """
172 """
172 Returns file_node, if error occurs or given path is directory,
173 Returns file_node, if error occurs or given path is directory,
173 it'll redirect to top level path
174 it'll redirect to top level path
174 """
175 """
175 _ = self.request.translate
176 _ = self.request.translate
176
177
177 try:
178 try:
178 file_node = commit_obj.get_node(path)
179 file_node = commit_obj.get_node(path)
179 if file_node.is_dir():
180 if file_node.is_dir():
180 raise RepositoryError('The given path is a directory')
181 raise RepositoryError('The given path is a directory')
181 except CommitDoesNotExistError:
182 except CommitDoesNotExistError:
182 log.exception('No such commit exists for this repository')
183 log.exception('No such commit exists for this repository')
183 h.flash(_('No such commit exists for this repository'), category='error')
184 h.flash(_('No such commit exists for this repository'), category='error')
184 raise HTTPNotFound()
185 raise HTTPNotFound()
185 except RepositoryError as e:
186 except RepositoryError as e:
186 log.warning('Repository error while fetching '
187 log.warning('Repository error while fetching '
187 'filenode `%s`. Err:%s', path, e)
188 'filenode `%s`. Err:%s', path, e)
188 h.flash(safe_str(h.escape(e)), category='error')
189 h.flash(safe_str(h.escape(e)), category='error')
189 raise HTTPNotFound()
190 raise HTTPNotFound()
190
191
191 return file_node
192 return file_node
192
193
193 def _is_valid_head(self, commit_id, repo):
194 def _is_valid_head(self, commit_id, repo):
194 branch_name = sha_commit_id = ''
195 branch_name = sha_commit_id = ''
195 is_head = False
196 is_head = False
196
197
197 if h.is_svn(repo) and not repo.is_empty():
198 if h.is_svn(repo) and not repo.is_empty():
198 # Note: Subversion only has one head.
199 # Note: Subversion only has one head.
199 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
200 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
200 is_head = True
201 is_head = True
201 return branch_name, sha_commit_id, is_head
202 return branch_name, sha_commit_id, is_head
202
203
203 for _branch_name, branch_commit_id in repo.branches.items():
204 for _branch_name, branch_commit_id in repo.branches.items():
204 # simple case we pass in branch name, it's a HEAD
205 # simple case we pass in branch name, it's a HEAD
205 if commit_id == _branch_name:
206 if commit_id == _branch_name:
206 is_head = True
207 is_head = True
207 branch_name = _branch_name
208 branch_name = _branch_name
208 sha_commit_id = branch_commit_id
209 sha_commit_id = branch_commit_id
209 break
210 break
210 # case when we pass in full sha commit_id, which is a head
211 # case when we pass in full sha commit_id, which is a head
211 elif commit_id == branch_commit_id:
212 elif commit_id == branch_commit_id:
212 is_head = True
213 is_head = True
213 branch_name = _branch_name
214 branch_name = _branch_name
214 sha_commit_id = branch_commit_id
215 sha_commit_id = branch_commit_id
215 break
216 break
216
217
217 # checked branches, means we only need to try to get the branch/commit_sha
218 # checked branches, means we only need to try to get the branch/commit_sha
218 if not repo.is_empty:
219 if not repo.is_empty:
219 commit = repo.get_commit(commit_id=commit_id)
220 commit = repo.get_commit(commit_id=commit_id)
220 if commit:
221 if commit:
221 branch_name = commit.branch
222 branch_name = commit.branch
222 sha_commit_id = commit.raw_id
223 sha_commit_id = commit.raw_id
223
224
224 return branch_name, sha_commit_id, is_head
225 return branch_name, sha_commit_id, is_head
225
226
226 def _get_tree_at_commit(
227 def _get_tree_at_commit(
227 self, c, commit_id, f_path, full_load=False):
228 self, c, commit_id, f_path, full_load=False):
228
229
229 repo_id = self.db_repo.repo_id
230 repo_id = self.db_repo.repo_id
230
231
231 cache_seconds = safe_int(
232 cache_seconds = safe_int(
232 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
233 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
233 cache_on = cache_seconds > 0
234 cache_on = cache_seconds > 0
234 log.debug(
235 log.debug(
235 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
236 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
236 'with caching: %s[TTL: %ss]' % (
237 'with caching: %s[TTL: %ss]' % (
237 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
238 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
238
239
239 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
240 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
240 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
241 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
241
242
242 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
243 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
243 condition=cache_on)
244 condition=cache_on)
244 def compute_file_tree(repo_id, commit_id, f_path, full_load):
245 def compute_file_tree(repo_id, commit_id, f_path, full_load):
245 log.debug('Generating cached file tree for repo_id: %s, %s, %s',
246 log.debug('Generating cached file tree for repo_id: %s, %s, %s',
246 repo_id, commit_id, f_path)
247 repo_id, commit_id, f_path)
247
248
248 c.full_load = full_load
249 c.full_load = full_load
249 return render(
250 return render(
250 'rhodecode:templates/files/files_browser_tree.mako',
251 'rhodecode:templates/files/files_browser_tree.mako',
251 self._get_template_context(c), self.request)
252 self._get_template_context(c), self.request)
252
253
253 return compute_file_tree(self.db_repo.repo_id, commit_id, f_path, full_load)
254 return compute_file_tree(self.db_repo.repo_id, commit_id, f_path, full_load)
254
255
255 def _get_archive_spec(self, fname):
256 def _get_archive_spec(self, fname):
256 log.debug('Detecting archive spec for: `%s`', fname)
257 log.debug('Detecting archive spec for: `%s`', fname)
257
258
258 fileformat = None
259 fileformat = None
259 ext = None
260 ext = None
260 content_type = None
261 content_type = None
261 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
262 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
262 content_type, extension = ext_data
263 content_type, extension = ext_data
263
264
264 if fname.endswith(extension):
265 if fname.endswith(extension):
265 fileformat = a_type
266 fileformat = a_type
266 log.debug('archive is of type: %s', fileformat)
267 log.debug('archive is of type: %s', fileformat)
267 ext = extension
268 ext = extension
268 break
269 break
269
270
270 if not fileformat:
271 if not fileformat:
271 raise ValueError()
272 raise ValueError()
272
273
273 # left over part of whole fname is the commit
274 # left over part of whole fname is the commit
274 commit_id = fname[:-len(ext)]
275 commit_id = fname[:-len(ext)]
275
276
276 return commit_id, ext, fileformat, content_type
277 return commit_id, ext, fileformat, content_type
277
278
278 @LoginRequired()
279 @LoginRequired()
279 @HasRepoPermissionAnyDecorator(
280 @HasRepoPermissionAnyDecorator(
280 'repository.read', 'repository.write', 'repository.admin')
281 'repository.read', 'repository.write', 'repository.admin')
281 @view_config(
282 @view_config(
282 route_name='repo_archivefile', request_method='GET',
283 route_name='repo_archivefile', request_method='GET',
283 renderer=None)
284 renderer=None)
284 def repo_archivefile(self):
285 def repo_archivefile(self):
285 # archive cache config
286 # archive cache config
286 from rhodecode import CONFIG
287 from rhodecode import CONFIG
287 _ = self.request.translate
288 _ = self.request.translate
288 self.load_default_context()
289 self.load_default_context()
289
290
290 fname = self.request.matchdict['fname']
291 fname = self.request.matchdict['fname']
291 subrepos = self.request.GET.get('subrepos') == 'true'
292 subrepos = self.request.GET.get('subrepos') == 'true'
292
293
293 if not self.db_repo.enable_downloads:
294 if not self.db_repo.enable_downloads:
294 return Response(_('Downloads disabled'))
295 return Response(_('Downloads disabled'))
295
296
296 try:
297 try:
297 commit_id, ext, fileformat, content_type = \
298 commit_id, ext, fileformat, content_type = \
298 self._get_archive_spec(fname)
299 self._get_archive_spec(fname)
299 except ValueError:
300 except ValueError:
300 return Response(_('Unknown archive type for: `{}`').format(
301 return Response(_('Unknown archive type for: `{}`').format(
301 h.escape(fname)))
302 h.escape(fname)))
302
303
303 try:
304 try:
304 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
305 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
305 except CommitDoesNotExistError:
306 except CommitDoesNotExistError:
306 return Response(_('Unknown commit_id {}').format(
307 return Response(_('Unknown commit_id {}').format(
307 h.escape(commit_id)))
308 h.escape(commit_id)))
308 except EmptyRepositoryError:
309 except EmptyRepositoryError:
309 return Response(_('Empty repository'))
310 return Response(_('Empty repository'))
310
311
311 archive_name = '%s-%s%s%s' % (
312 archive_name = '%s-%s%s%s' % (
312 safe_str(self.db_repo_name.replace('/', '_')),
313 safe_str(self.db_repo_name.replace('/', '_')),
313 '-sub' if subrepos else '',
314 '-sub' if subrepos else '',
314 safe_str(commit.short_id), ext)
315 safe_str(commit.short_id), ext)
315
316
316 use_cached_archive = False
317 use_cached_archive = False
317 archive_cache_enabled = CONFIG.get(
318 archive_cache_enabled = CONFIG.get(
318 'archive_cache_dir') and not self.request.GET.get('no_cache')
319 'archive_cache_dir') and not self.request.GET.get('no_cache')
319 cached_archive_path = None
320 cached_archive_path = None
320
321
321 if archive_cache_enabled:
322 if archive_cache_enabled:
322 # check if we it's ok to write
323 # check if we it's ok to write
323 if not os.path.isdir(CONFIG['archive_cache_dir']):
324 if not os.path.isdir(CONFIG['archive_cache_dir']):
324 os.makedirs(CONFIG['archive_cache_dir'])
325 os.makedirs(CONFIG['archive_cache_dir'])
325 cached_archive_path = os.path.join(
326 cached_archive_path = os.path.join(
326 CONFIG['archive_cache_dir'], archive_name)
327 CONFIG['archive_cache_dir'], archive_name)
327 if os.path.isfile(cached_archive_path):
328 if os.path.isfile(cached_archive_path):
328 log.debug('Found cached archive in %s', cached_archive_path)
329 log.debug('Found cached archive in %s', cached_archive_path)
329 fd, archive = None, cached_archive_path
330 fd, archive = None, cached_archive_path
330 use_cached_archive = True
331 use_cached_archive = True
331 else:
332 else:
332 log.debug('Archive %s is not yet cached', archive_name)
333 log.debug('Archive %s is not yet cached', archive_name)
333
334
334 if not use_cached_archive:
335 if not use_cached_archive:
335 # generate new archive
336 # generate new archive
336 fd, archive = tempfile.mkstemp()
337 fd, archive = tempfile.mkstemp()
337 log.debug('Creating new temp archive in %s', archive)
338 log.debug('Creating new temp archive in %s', archive)
338 try:
339 try:
339 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
340 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
340 except ImproperArchiveTypeError:
341 except ImproperArchiveTypeError:
341 return _('Unknown archive type')
342 return _('Unknown archive type')
342 if archive_cache_enabled:
343 if archive_cache_enabled:
343 # if we generated the archive and we have cache enabled
344 # if we generated the archive and we have cache enabled
344 # let's use this for future
345 # let's use this for future
345 log.debug('Storing new archive in %s', cached_archive_path)
346 log.debug('Storing new archive in %s', cached_archive_path)
346 shutil.move(archive, cached_archive_path)
347 shutil.move(archive, cached_archive_path)
347 archive = cached_archive_path
348 archive = cached_archive_path
348
349
349 # store download action
350 # store download action
350 audit_logger.store_web(
351 audit_logger.store_web(
351 'repo.archive.download', action_data={
352 'repo.archive.download', action_data={
352 'user_agent': self.request.user_agent,
353 'user_agent': self.request.user_agent,
353 'archive_name': archive_name,
354 'archive_name': archive_name,
354 'archive_spec': fname,
355 'archive_spec': fname,
355 'archive_cached': use_cached_archive},
356 'archive_cached': use_cached_archive},
356 user=self._rhodecode_user,
357 user=self._rhodecode_user,
357 repo=self.db_repo,
358 repo=self.db_repo,
358 commit=True
359 commit=True
359 )
360 )
360
361
361 def get_chunked_archive(archive_path):
362 def get_chunked_archive(archive_path):
362 with open(archive_path, 'rb') as stream:
363 with open(archive_path, 'rb') as stream:
363 while True:
364 while True:
364 data = stream.read(16 * 1024)
365 data = stream.read(16 * 1024)
365 if not data:
366 if not data:
366 if fd: # fd means we used temporary file
367 if fd: # fd means we used temporary file
367 os.close(fd)
368 os.close(fd)
368 if not archive_cache_enabled:
369 if not archive_cache_enabled:
369 log.debug('Destroying temp archive %s', archive_path)
370 log.debug('Destroying temp archive %s', archive_path)
370 os.remove(archive_path)
371 os.remove(archive_path)
371 break
372 break
372 yield data
373 yield data
373
374
374 response = Response(app_iter=get_chunked_archive(archive))
375 response = Response(app_iter=get_chunked_archive(archive))
375 response.content_disposition = str(
376 response.content_disposition = str(
376 'attachment; filename=%s' % archive_name)
377 'attachment; filename=%s' % archive_name)
377 response.content_type = str(content_type)
378 response.content_type = str(content_type)
378
379
379 return response
380 return response
380
381
381 def _get_file_node(self, commit_id, f_path):
382 def _get_file_node(self, commit_id, f_path):
382 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
383 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
383 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
384 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
384 try:
385 try:
385 node = commit.get_node(f_path)
386 node = commit.get_node(f_path)
386 if node.is_dir():
387 if node.is_dir():
387 raise NodeError('%s path is a %s not a file'
388 raise NodeError('%s path is a %s not a file'
388 % (node, type(node)))
389 % (node, type(node)))
389 except NodeDoesNotExistError:
390 except NodeDoesNotExistError:
390 commit = EmptyCommit(
391 commit = EmptyCommit(
391 commit_id=commit_id,
392 commit_id=commit_id,
392 idx=commit.idx,
393 idx=commit.idx,
393 repo=commit.repository,
394 repo=commit.repository,
394 alias=commit.repository.alias,
395 alias=commit.repository.alias,
395 message=commit.message,
396 message=commit.message,
396 author=commit.author,
397 author=commit.author,
397 date=commit.date)
398 date=commit.date)
398 node = FileNode(f_path, '', commit=commit)
399 node = FileNode(f_path, '', commit=commit)
399 else:
400 else:
400 commit = EmptyCommit(
401 commit = EmptyCommit(
401 repo=self.rhodecode_vcs_repo,
402 repo=self.rhodecode_vcs_repo,
402 alias=self.rhodecode_vcs_repo.alias)
403 alias=self.rhodecode_vcs_repo.alias)
403 node = FileNode(f_path, '', commit=commit)
404 node = FileNode(f_path, '', commit=commit)
404 return node
405 return node
405
406
406 @LoginRequired()
407 @LoginRequired()
407 @HasRepoPermissionAnyDecorator(
408 @HasRepoPermissionAnyDecorator(
408 'repository.read', 'repository.write', 'repository.admin')
409 'repository.read', 'repository.write', 'repository.admin')
409 @view_config(
410 @view_config(
410 route_name='repo_files_diff', request_method='GET',
411 route_name='repo_files_diff', request_method='GET',
411 renderer=None)
412 renderer=None)
412 def repo_files_diff(self):
413 def repo_files_diff(self):
413 c = self.load_default_context()
414 c = self.load_default_context()
414 f_path = self._get_f_path(self.request.matchdict)
415 f_path = self._get_f_path(self.request.matchdict)
415 diff1 = self.request.GET.get('diff1', '')
416 diff1 = self.request.GET.get('diff1', '')
416 diff2 = self.request.GET.get('diff2', '')
417 diff2 = self.request.GET.get('diff2', '')
417
418
418 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
419 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
419
420
420 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
421 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
421 line_context = self.request.GET.get('context', 3)
422 line_context = self.request.GET.get('context', 3)
422
423
423 if not any((diff1, diff2)):
424 if not any((diff1, diff2)):
424 h.flash(
425 h.flash(
425 'Need query parameter "diff1" or "diff2" to generate a diff.',
426 'Need query parameter "diff1" or "diff2" to generate a diff.',
426 category='error')
427 category='error')
427 raise HTTPBadRequest()
428 raise HTTPBadRequest()
428
429
429 c.action = self.request.GET.get('diff')
430 c.action = self.request.GET.get('diff')
430 if c.action not in ['download', 'raw']:
431 if c.action not in ['download', 'raw']:
431 compare_url = h.route_path(
432 compare_url = h.route_path(
432 'repo_compare',
433 'repo_compare',
433 repo_name=self.db_repo_name,
434 repo_name=self.db_repo_name,
434 source_ref_type='rev',
435 source_ref_type='rev',
435 source_ref=diff1,
436 source_ref=diff1,
436 target_repo=self.db_repo_name,
437 target_repo=self.db_repo_name,
437 target_ref_type='rev',
438 target_ref_type='rev',
438 target_ref=diff2,
439 target_ref=diff2,
439 _query=dict(f_path=f_path))
440 _query=dict(f_path=f_path))
440 # redirect to new view if we render diff
441 # redirect to new view if we render diff
441 raise HTTPFound(compare_url)
442 raise HTTPFound(compare_url)
442
443
443 try:
444 try:
444 node1 = self._get_file_node(diff1, path1)
445 node1 = self._get_file_node(diff1, path1)
445 node2 = self._get_file_node(diff2, f_path)
446 node2 = self._get_file_node(diff2, f_path)
446 except (RepositoryError, NodeError):
447 except (RepositoryError, NodeError):
447 log.exception("Exception while trying to get node from repository")
448 log.exception("Exception while trying to get node from repository")
448 raise HTTPFound(
449 raise HTTPFound(
449 h.route_path('repo_files', repo_name=self.db_repo_name,
450 h.route_path('repo_files', repo_name=self.db_repo_name,
450 commit_id='tip', f_path=f_path))
451 commit_id='tip', f_path=f_path))
451
452
452 if all(isinstance(node.commit, EmptyCommit)
453 if all(isinstance(node.commit, EmptyCommit)
453 for node in (node1, node2)):
454 for node in (node1, node2)):
454 raise HTTPNotFound()
455 raise HTTPNotFound()
455
456
456 c.commit_1 = node1.commit
457 c.commit_1 = node1.commit
457 c.commit_2 = node2.commit
458 c.commit_2 = node2.commit
458
459
459 if c.action == 'download':
460 if c.action == 'download':
460 _diff = diffs.get_gitdiff(node1, node2,
461 _diff = diffs.get_gitdiff(node1, node2,
461 ignore_whitespace=ignore_whitespace,
462 ignore_whitespace=ignore_whitespace,
462 context=line_context)
463 context=line_context)
463 diff = diffs.DiffProcessor(_diff, format='gitdiff')
464 diff = diffs.DiffProcessor(_diff, format='gitdiff')
464
465
465 response = Response(self.path_filter.get_raw_patch(diff))
466 response = Response(self.path_filter.get_raw_patch(diff))
466 response.content_type = 'text/plain'
467 response.content_type = 'text/plain'
467 response.content_disposition = (
468 response.content_disposition = (
468 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
469 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
469 )
470 )
470 charset = self._get_default_encoding(c)
471 charset = self._get_default_encoding(c)
471 if charset:
472 if charset:
472 response.charset = charset
473 response.charset = charset
473 return response
474 return response
474
475
475 elif c.action == 'raw':
476 elif c.action == 'raw':
476 _diff = diffs.get_gitdiff(node1, node2,
477 _diff = diffs.get_gitdiff(node1, node2,
477 ignore_whitespace=ignore_whitespace,
478 ignore_whitespace=ignore_whitespace,
478 context=line_context)
479 context=line_context)
479 diff = diffs.DiffProcessor(_diff, format='gitdiff')
480 diff = diffs.DiffProcessor(_diff, format='gitdiff')
480
481
481 response = Response(self.path_filter.get_raw_patch(diff))
482 response = Response(self.path_filter.get_raw_patch(diff))
482 response.content_type = 'text/plain'
483 response.content_type = 'text/plain'
483 charset = self._get_default_encoding(c)
484 charset = self._get_default_encoding(c)
484 if charset:
485 if charset:
485 response.charset = charset
486 response.charset = charset
486 return response
487 return response
487
488
488 # in case we ever end up here
489 # in case we ever end up here
489 raise HTTPNotFound()
490 raise HTTPNotFound()
490
491
491 @LoginRequired()
492 @LoginRequired()
492 @HasRepoPermissionAnyDecorator(
493 @HasRepoPermissionAnyDecorator(
493 'repository.read', 'repository.write', 'repository.admin')
494 'repository.read', 'repository.write', 'repository.admin')
494 @view_config(
495 @view_config(
495 route_name='repo_files_diff_2way_redirect', request_method='GET',
496 route_name='repo_files_diff_2way_redirect', request_method='GET',
496 renderer=None)
497 renderer=None)
497 def repo_files_diff_2way_redirect(self):
498 def repo_files_diff_2way_redirect(self):
498 """
499 """
499 Kept only to make OLD links work
500 Kept only to make OLD links work
500 """
501 """
501 f_path = self._get_f_path_unchecked(self.request.matchdict)
502 f_path = self._get_f_path_unchecked(self.request.matchdict)
502 diff1 = self.request.GET.get('diff1', '')
503 diff1 = self.request.GET.get('diff1', '')
503 diff2 = self.request.GET.get('diff2', '')
504 diff2 = self.request.GET.get('diff2', '')
504
505
505 if not any((diff1, diff2)):
506 if not any((diff1, diff2)):
506 h.flash(
507 h.flash(
507 'Need query parameter "diff1" or "diff2" to generate a diff.',
508 'Need query parameter "diff1" or "diff2" to generate a diff.',
508 category='error')
509 category='error')
509 raise HTTPBadRequest()
510 raise HTTPBadRequest()
510
511
511 compare_url = h.route_path(
512 compare_url = h.route_path(
512 'repo_compare',
513 'repo_compare',
513 repo_name=self.db_repo_name,
514 repo_name=self.db_repo_name,
514 source_ref_type='rev',
515 source_ref_type='rev',
515 source_ref=diff1,
516 source_ref=diff1,
516 target_ref_type='rev',
517 target_ref_type='rev',
517 target_ref=diff2,
518 target_ref=diff2,
518 _query=dict(f_path=f_path, diffmode='sideside',
519 _query=dict(f_path=f_path, diffmode='sideside',
519 target_repo=self.db_repo_name,))
520 target_repo=self.db_repo_name,))
520 raise HTTPFound(compare_url)
521 raise HTTPFound(compare_url)
521
522
522 @LoginRequired()
523 @LoginRequired()
523 @HasRepoPermissionAnyDecorator(
524 @HasRepoPermissionAnyDecorator(
524 'repository.read', 'repository.write', 'repository.admin')
525 'repository.read', 'repository.write', 'repository.admin')
525 @view_config(
526 @view_config(
526 route_name='repo_files', request_method='GET',
527 route_name='repo_files', request_method='GET',
527 renderer=None)
528 renderer=None)
528 @view_config(
529 @view_config(
529 route_name='repo_files:default_path', request_method='GET',
530 route_name='repo_files:default_path', request_method='GET',
530 renderer=None)
531 renderer=None)
531 @view_config(
532 @view_config(
532 route_name='repo_files:default_commit', request_method='GET',
533 route_name='repo_files:default_commit', request_method='GET',
533 renderer=None)
534 renderer=None)
534 @view_config(
535 @view_config(
535 route_name='repo_files:rendered', request_method='GET',
536 route_name='repo_files:rendered', request_method='GET',
536 renderer=None)
537 renderer=None)
537 @view_config(
538 @view_config(
538 route_name='repo_files:annotated', request_method='GET',
539 route_name='repo_files:annotated', request_method='GET',
539 renderer=None)
540 renderer=None)
540 def repo_files(self):
541 def repo_files(self):
541 c = self.load_default_context()
542 c = self.load_default_context()
542
543
543 view_name = getattr(self.request.matched_route, 'name', None)
544 view_name = getattr(self.request.matched_route, 'name', None)
544
545
545 c.annotate = view_name == 'repo_files:annotated'
546 c.annotate = view_name == 'repo_files:annotated'
546 # default is false, but .rst/.md files later are auto rendered, we can
547 # default is false, but .rst/.md files later are auto rendered, we can
547 # overwrite auto rendering by setting this GET flag
548 # overwrite auto rendering by setting this GET flag
548 c.renderer = view_name == 'repo_files:rendered' or \
549 c.renderer = view_name == 'repo_files:rendered' or \
549 not self.request.GET.get('no-render', False)
550 not self.request.GET.get('no-render', False)
550
551
551 # redirect to given commit_id from form if given
552 # redirect to given commit_id from form if given
552 get_commit_id = self.request.GET.get('at_rev', None)
553 get_commit_id = self.request.GET.get('at_rev', None)
553 if get_commit_id:
554 if get_commit_id:
554 self._get_commit_or_redirect(get_commit_id)
555 self._get_commit_or_redirect(get_commit_id)
555
556
556 commit_id, f_path = self._get_commit_and_path()
557 commit_id, f_path = self._get_commit_and_path()
557 c.commit = self._get_commit_or_redirect(commit_id)
558 c.commit = self._get_commit_or_redirect(commit_id)
558 c.branch = self.request.GET.get('branch', None)
559 c.branch = self.request.GET.get('branch', None)
559 c.f_path = f_path
560 c.f_path = f_path
560
561
561 # prev link
562 # prev link
562 try:
563 try:
563 prev_commit = c.commit.prev(c.branch)
564 prev_commit = c.commit.prev(c.branch)
564 c.prev_commit = prev_commit
565 c.prev_commit = prev_commit
565 c.url_prev = h.route_path(
566 c.url_prev = h.route_path(
566 'repo_files', repo_name=self.db_repo_name,
567 'repo_files', repo_name=self.db_repo_name,
567 commit_id=prev_commit.raw_id, f_path=f_path)
568 commit_id=prev_commit.raw_id, f_path=f_path)
568 if c.branch:
569 if c.branch:
569 c.url_prev += '?branch=%s' % c.branch
570 c.url_prev += '?branch=%s' % c.branch
570 except (CommitDoesNotExistError, VCSError):
571 except (CommitDoesNotExistError, VCSError):
571 c.url_prev = '#'
572 c.url_prev = '#'
572 c.prev_commit = EmptyCommit()
573 c.prev_commit = EmptyCommit()
573
574
574 # next link
575 # next link
575 try:
576 try:
576 next_commit = c.commit.next(c.branch)
577 next_commit = c.commit.next(c.branch)
577 c.next_commit = next_commit
578 c.next_commit = next_commit
578 c.url_next = h.route_path(
579 c.url_next = h.route_path(
579 'repo_files', repo_name=self.db_repo_name,
580 'repo_files', repo_name=self.db_repo_name,
580 commit_id=next_commit.raw_id, f_path=f_path)
581 commit_id=next_commit.raw_id, f_path=f_path)
581 if c.branch:
582 if c.branch:
582 c.url_next += '?branch=%s' % c.branch
583 c.url_next += '?branch=%s' % c.branch
583 except (CommitDoesNotExistError, VCSError):
584 except (CommitDoesNotExistError, VCSError):
584 c.url_next = '#'
585 c.url_next = '#'
585 c.next_commit = EmptyCommit()
586 c.next_commit = EmptyCommit()
586
587
587 # files or dirs
588 # files or dirs
588 try:
589 try:
589 c.file = c.commit.get_node(f_path)
590 c.file = c.commit.get_node(f_path)
590 c.file_author = True
591 c.file_author = True
591 c.file_tree = ''
592 c.file_tree = ''
592
593
593 # load file content
594 # load file content
594 if c.file.is_file():
595 if c.file.is_file():
595 c.lf_node = c.file.get_largefile_node()
596 c.lf_node = c.file.get_largefile_node()
596
597
597 c.file_source_page = 'true'
598 c.file_source_page = 'true'
598 c.file_last_commit = c.file.last_commit
599 c.file_last_commit = c.file.last_commit
599 if c.file.size < c.visual.cut_off_limit_diff:
600 if c.file.size < c.visual.cut_off_limit_diff:
600 if c.annotate: # annotation has precedence over renderer
601 if c.annotate: # annotation has precedence over renderer
601 c.annotated_lines = filenode_as_annotated_lines_tokens(
602 c.annotated_lines = filenode_as_annotated_lines_tokens(
602 c.file
603 c.file
603 )
604 )
604 else:
605 else:
605 c.renderer = (
606 c.renderer = (
606 c.renderer and h.renderer_from_filename(c.file.path)
607 c.renderer and h.renderer_from_filename(c.file.path)
607 )
608 )
608 if not c.renderer:
609 if not c.renderer:
609 c.lines = filenode_as_lines_tokens(c.file)
610 c.lines = filenode_as_lines_tokens(c.file)
610
611
611 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
612 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
612 commit_id, self.rhodecode_vcs_repo)
613 commit_id, self.rhodecode_vcs_repo)
613 c.on_branch_head = is_head
614 c.on_branch_head = is_head
614
615
615 branch = c.commit.branch if (
616 branch = c.commit.branch if (
616 c.commit.branch and '/' not in c.commit.branch) else None
617 c.commit.branch and '/' not in c.commit.branch) else None
617 c.branch_or_raw_id = branch or c.commit.raw_id
618 c.branch_or_raw_id = branch or c.commit.raw_id
618 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
619 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
619
620
620 author = c.file_last_commit.author
621 author = c.file_last_commit.author
621 c.authors = [[
622 c.authors = [[
622 h.email(author),
623 h.email(author),
623 h.person(author, 'username_or_name_or_email'),
624 h.person(author, 'username_or_name_or_email'),
624 1
625 1
625 ]]
626 ]]
626
627
627 else: # load tree content at path
628 else: # load tree content at path
628 c.file_source_page = 'false'
629 c.file_source_page = 'false'
629 c.authors = []
630 c.authors = []
630 # this loads a simple tree without metadata to speed things up
631 # this loads a simple tree without metadata to speed things up
631 # later via ajax we call repo_nodetree_full and fetch whole
632 # later via ajax we call repo_nodetree_full and fetch whole
632 c.file_tree = self._get_tree_at_commit(
633 c.file_tree = self._get_tree_at_commit(
633 c, c.commit.raw_id, f_path)
634 c, c.commit.raw_id, f_path)
634
635
635 except RepositoryError as e:
636 except RepositoryError as e:
636 h.flash(safe_str(h.escape(e)), category='error')
637 h.flash(safe_str(h.escape(e)), category='error')
637 raise HTTPNotFound()
638 raise HTTPNotFound()
638
639
639 if self.request.environ.get('HTTP_X_PJAX'):
640 if self.request.environ.get('HTTP_X_PJAX'):
640 html = render('rhodecode:templates/files/files_pjax.mako',
641 html = render('rhodecode:templates/files/files_pjax.mako',
641 self._get_template_context(c), self.request)
642 self._get_template_context(c), self.request)
642 else:
643 else:
643 html = render('rhodecode:templates/files/files.mako',
644 html = render('rhodecode:templates/files/files.mako',
644 self._get_template_context(c), self.request)
645 self._get_template_context(c), self.request)
645 return Response(html)
646 return Response(html)
646
647
647 @HasRepoPermissionAnyDecorator(
648 @HasRepoPermissionAnyDecorator(
648 'repository.read', 'repository.write', 'repository.admin')
649 'repository.read', 'repository.write', 'repository.admin')
649 @view_config(
650 @view_config(
650 route_name='repo_files:annotated_previous', request_method='GET',
651 route_name='repo_files:annotated_previous', request_method='GET',
651 renderer=None)
652 renderer=None)
652 def repo_files_annotated_previous(self):
653 def repo_files_annotated_previous(self):
653 self.load_default_context()
654 self.load_default_context()
654
655
655 commit_id, f_path = self._get_commit_and_path()
656 commit_id, f_path = self._get_commit_and_path()
656 commit = self._get_commit_or_redirect(commit_id)
657 commit = self._get_commit_or_redirect(commit_id)
657 prev_commit_id = commit.raw_id
658 prev_commit_id = commit.raw_id
658 line_anchor = self.request.GET.get('line_anchor')
659 line_anchor = self.request.GET.get('line_anchor')
659 is_file = False
660 is_file = False
660 try:
661 try:
661 _file = commit.get_node(f_path)
662 _file = commit.get_node(f_path)
662 is_file = _file.is_file()
663 is_file = _file.is_file()
663 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
664 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
664 pass
665 pass
665
666
666 if is_file:
667 if is_file:
667 history = commit.get_path_history(f_path)
668 history = commit.get_path_history(f_path)
668 prev_commit_id = history[1].raw_id \
669 prev_commit_id = history[1].raw_id \
669 if len(history) > 1 else prev_commit_id
670 if len(history) > 1 else prev_commit_id
670 prev_url = h.route_path(
671 prev_url = h.route_path(
671 'repo_files:annotated', repo_name=self.db_repo_name,
672 'repo_files:annotated', repo_name=self.db_repo_name,
672 commit_id=prev_commit_id, f_path=f_path,
673 commit_id=prev_commit_id, f_path=f_path,
673 _anchor='L{}'.format(line_anchor))
674 _anchor='L{}'.format(line_anchor))
674
675
675 raise HTTPFound(prev_url)
676 raise HTTPFound(prev_url)
676
677
677 @LoginRequired()
678 @LoginRequired()
678 @HasRepoPermissionAnyDecorator(
679 @HasRepoPermissionAnyDecorator(
679 'repository.read', 'repository.write', 'repository.admin')
680 'repository.read', 'repository.write', 'repository.admin')
680 @view_config(
681 @view_config(
681 route_name='repo_nodetree_full', request_method='GET',
682 route_name='repo_nodetree_full', request_method='GET',
682 renderer=None, xhr=True)
683 renderer=None, xhr=True)
683 @view_config(
684 @view_config(
684 route_name='repo_nodetree_full:default_path', request_method='GET',
685 route_name='repo_nodetree_full:default_path', request_method='GET',
685 renderer=None, xhr=True)
686 renderer=None, xhr=True)
686 def repo_nodetree_full(self):
687 def repo_nodetree_full(self):
687 """
688 """
688 Returns rendered html of file tree that contains commit date,
689 Returns rendered html of file tree that contains commit date,
689 author, commit_id for the specified combination of
690 author, commit_id for the specified combination of
690 repo, commit_id and file path
691 repo, commit_id and file path
691 """
692 """
692 c = self.load_default_context()
693 c = self.load_default_context()
693
694
694 commit_id, f_path = self._get_commit_and_path()
695 commit_id, f_path = self._get_commit_and_path()
695 commit = self._get_commit_or_redirect(commit_id)
696 commit = self._get_commit_or_redirect(commit_id)
696 try:
697 try:
697 dir_node = commit.get_node(f_path)
698 dir_node = commit.get_node(f_path)
698 except RepositoryError as e:
699 except RepositoryError as e:
699 return Response('error: {}'.format(h.escape(safe_str(e))))
700 return Response('error: {}'.format(h.escape(safe_str(e))))
700
701
701 if dir_node.is_file():
702 if dir_node.is_file():
702 return Response('')
703 return Response('')
703
704
704 c.file = dir_node
705 c.file = dir_node
705 c.commit = commit
706 c.commit = commit
706
707
707 html = self._get_tree_at_commit(
708 html = self._get_tree_at_commit(
708 c, commit.raw_id, dir_node.path, full_load=True)
709 c, commit.raw_id, dir_node.path, full_load=True)
709
710
710 return Response(html)
711 return Response(html)
711
712
712 def _get_attachement_disposition(self, f_path):
713 def _get_attachement_headers(self, f_path):
713 return 'attachment; filename=%s' % \
714 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
714 safe_str(f_path.split(Repository.NAME_SEP)[-1])
715 safe_path = f_name.replace('"', '\\"')
716 encoded_path = urllib.quote(f_name)
717
718 return "attachment; " \
719 "filename=\"{}\"; " \
720 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
715
721
716 @LoginRequired()
722 @LoginRequired()
717 @HasRepoPermissionAnyDecorator(
723 @HasRepoPermissionAnyDecorator(
718 'repository.read', 'repository.write', 'repository.admin')
724 'repository.read', 'repository.write', 'repository.admin')
719 @view_config(
725 @view_config(
720 route_name='repo_file_raw', request_method='GET',
726 route_name='repo_file_raw', request_method='GET',
721 renderer=None)
727 renderer=None)
722 def repo_file_raw(self):
728 def repo_file_raw(self):
723 """
729 """
724 Action for show as raw, some mimetypes are "rendered",
730 Action for show as raw, some mimetypes are "rendered",
725 those include images, icons.
731 those include images, icons.
726 """
732 """
727 c = self.load_default_context()
733 c = self.load_default_context()
728
734
729 commit_id, f_path = self._get_commit_and_path()
735 commit_id, f_path = self._get_commit_and_path()
730 commit = self._get_commit_or_redirect(commit_id)
736 commit = self._get_commit_or_redirect(commit_id)
731 file_node = self._get_filenode_or_redirect(commit, f_path)
737 file_node = self._get_filenode_or_redirect(commit, f_path)
732
738
733 raw_mimetype_mapping = {
739 raw_mimetype_mapping = {
734 # map original mimetype to a mimetype used for "show as raw"
740 # map original mimetype to a mimetype used for "show as raw"
735 # you can also provide a content-disposition to override the
741 # you can also provide a content-disposition to override the
736 # default "attachment" disposition.
742 # default "attachment" disposition.
737 # orig_type: (new_type, new_dispo)
743 # orig_type: (new_type, new_dispo)
738
744
739 # show images inline:
745 # show images inline:
740 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
746 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
741 # for example render an SVG with javascript inside or even render
747 # for example render an SVG with javascript inside or even render
742 # HTML.
748 # HTML.
743 'image/x-icon': ('image/x-icon', 'inline'),
749 'image/x-icon': ('image/x-icon', 'inline'),
744 'image/png': ('image/png', 'inline'),
750 'image/png': ('image/png', 'inline'),
745 'image/gif': ('image/gif', 'inline'),
751 'image/gif': ('image/gif', 'inline'),
746 'image/jpeg': ('image/jpeg', 'inline'),
752 'image/jpeg': ('image/jpeg', 'inline'),
747 'application/pdf': ('application/pdf', 'inline'),
753 'application/pdf': ('application/pdf', 'inline'),
748 }
754 }
749
755
750 mimetype = file_node.mimetype
756 mimetype = file_node.mimetype
751 try:
757 try:
752 mimetype, disposition = raw_mimetype_mapping[mimetype]
758 mimetype, disposition = raw_mimetype_mapping[mimetype]
753 except KeyError:
759 except KeyError:
754 # we don't know anything special about this, handle it safely
760 # we don't know anything special about this, handle it safely
755 if file_node.is_binary:
761 if file_node.is_binary:
756 # do same as download raw for binary files
762 # do same as download raw for binary files
757 mimetype, disposition = 'application/octet-stream', 'attachment'
763 mimetype, disposition = 'application/octet-stream', 'attachment'
758 else:
764 else:
759 # do not just use the original mimetype, but force text/plain,
765 # do not just use the original mimetype, but force text/plain,
760 # otherwise it would serve text/html and that might be unsafe.
766 # otherwise it would serve text/html and that might be unsafe.
761 # Note: underlying vcs library fakes text/plain mimetype if the
767 # Note: underlying vcs library fakes text/plain mimetype if the
762 # mimetype can not be determined and it thinks it is not
768 # mimetype can not be determined and it thinks it is not
763 # binary.This might lead to erroneous text display in some
769 # binary.This might lead to erroneous text display in some
764 # cases, but helps in other cases, like with text files
770 # cases, but helps in other cases, like with text files
765 # without extension.
771 # without extension.
766 mimetype, disposition = 'text/plain', 'inline'
772 mimetype, disposition = 'text/plain', 'inline'
767
773
768 if disposition == 'attachment':
774 if disposition == 'attachment':
769 disposition = self._get_attachement_disposition(f_path)
775 disposition = self._get_attachement_headers(f_path)
770
776
771 def stream_node():
777 def stream_node():
772 yield file_node.raw_bytes
778 yield file_node.raw_bytes
773
779
774 response = Response(app_iter=stream_node())
780 response = Response(app_iter=stream_node())
775 response.content_disposition = disposition
781 response.content_disposition = disposition
776 response.content_type = mimetype
782 response.content_type = mimetype
777
783
778 charset = self._get_default_encoding(c)
784 charset = self._get_default_encoding(c)
779 if charset:
785 if charset:
780 response.charset = charset
786 response.charset = charset
781
787
782 return response
788 return response
783
789
784 @LoginRequired()
790 @LoginRequired()
785 @HasRepoPermissionAnyDecorator(
791 @HasRepoPermissionAnyDecorator(
786 'repository.read', 'repository.write', 'repository.admin')
792 'repository.read', 'repository.write', 'repository.admin')
787 @view_config(
793 @view_config(
788 route_name='repo_file_download', request_method='GET',
794 route_name='repo_file_download', request_method='GET',
789 renderer=None)
795 renderer=None)
790 @view_config(
796 @view_config(
791 route_name='repo_file_download:legacy', request_method='GET',
797 route_name='repo_file_download:legacy', request_method='GET',
792 renderer=None)
798 renderer=None)
793 def repo_file_download(self):
799 def repo_file_download(self):
794 c = self.load_default_context()
800 c = self.load_default_context()
795
801
796 commit_id, f_path = self._get_commit_and_path()
802 commit_id, f_path = self._get_commit_and_path()
797 commit = self._get_commit_or_redirect(commit_id)
803 commit = self._get_commit_or_redirect(commit_id)
798 file_node = self._get_filenode_or_redirect(commit, f_path)
804 file_node = self._get_filenode_or_redirect(commit, f_path)
799
805
800 if self.request.GET.get('lf'):
806 if self.request.GET.get('lf'):
801 # only if lf get flag is passed, we download this file
807 # only if lf get flag is passed, we download this file
802 # as LFS/Largefile
808 # as LFS/Largefile
803 lf_node = file_node.get_largefile_node()
809 lf_node = file_node.get_largefile_node()
804 if lf_node:
810 if lf_node:
805 # overwrite our pointer with the REAL large-file
811 # overwrite our pointer with the REAL large-file
806 file_node = lf_node
812 file_node = lf_node
807
813
808 disposition = self._get_attachement_disposition(f_path)
814 disposition = self._get_attachement_headers(f_path)
809
815
810 def stream_node():
816 def stream_node():
811 yield file_node.raw_bytes
817 yield file_node.raw_bytes
812
818
813 response = Response(app_iter=stream_node())
819 response = Response(app_iter=stream_node())
814 response.content_disposition = disposition
820 response.content_disposition = disposition
815 response.content_type = file_node.mimetype
821 response.content_type = file_node.mimetype
816
822
817 charset = self._get_default_encoding(c)
823 charset = self._get_default_encoding(c)
818 if charset:
824 if charset:
819 response.charset = charset
825 response.charset = charset
820
826
821 return response
827 return response
822
828
823 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
829 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
824
830
825 cache_seconds = safe_int(
831 cache_seconds = safe_int(
826 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
832 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
827 cache_on = cache_seconds > 0
833 cache_on = cache_seconds > 0
828 log.debug(
834 log.debug(
829 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
835 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
830 'with caching: %s[TTL: %ss]' % (
836 'with caching: %s[TTL: %ss]' % (
831 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
837 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
832
838
833 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
839 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
834 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
840 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
835
841
836 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
842 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
837 condition=cache_on)
843 condition=cache_on)
838 def compute_file_search(repo_id, commit_id, f_path):
844 def compute_file_search(repo_id, commit_id, f_path):
839 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
845 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
840 repo_id, commit_id, f_path)
846 repo_id, commit_id, f_path)
841 try:
847 try:
842 _d, _f = ScmModel().get_nodes(
848 _d, _f = ScmModel().get_nodes(
843 repo_name, commit_id, f_path, flat=False)
849 repo_name, commit_id, f_path, flat=False)
844 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
850 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
845 log.exception(safe_str(e))
851 log.exception(safe_str(e))
846 h.flash(safe_str(h.escape(e)), category='error')
852 h.flash(safe_str(h.escape(e)), category='error')
847 raise HTTPFound(h.route_path(
853 raise HTTPFound(h.route_path(
848 'repo_files', repo_name=self.db_repo_name,
854 'repo_files', repo_name=self.db_repo_name,
849 commit_id='tip', f_path='/'))
855 commit_id='tip', f_path='/'))
850 return _d + _f
856 return _d + _f
851
857
852 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
858 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
853
859
854 @LoginRequired()
860 @LoginRequired()
855 @HasRepoPermissionAnyDecorator(
861 @HasRepoPermissionAnyDecorator(
856 'repository.read', 'repository.write', 'repository.admin')
862 'repository.read', 'repository.write', 'repository.admin')
857 @view_config(
863 @view_config(
858 route_name='repo_files_nodelist', request_method='GET',
864 route_name='repo_files_nodelist', request_method='GET',
859 renderer='json_ext', xhr=True)
865 renderer='json_ext', xhr=True)
860 def repo_nodelist(self):
866 def repo_nodelist(self):
861 self.load_default_context()
867 self.load_default_context()
862
868
863 commit_id, f_path = self._get_commit_and_path()
869 commit_id, f_path = self._get_commit_and_path()
864 commit = self._get_commit_or_redirect(commit_id)
870 commit = self._get_commit_or_redirect(commit_id)
865
871
866 metadata = self._get_nodelist_at_commit(
872 metadata = self._get_nodelist_at_commit(
867 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
873 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
868 return {'nodes': metadata}
874 return {'nodes': metadata}
869
875
870 def _create_references(
876 def _create_references(
871 self, branches_or_tags, symbolic_reference, f_path):
877 self, branches_or_tags, symbolic_reference, f_path):
872 items = []
878 items = []
873 for name, commit_id in branches_or_tags.items():
879 for name, commit_id in branches_or_tags.items():
874 sym_ref = symbolic_reference(commit_id, name, f_path)
880 sym_ref = symbolic_reference(commit_id, name, f_path)
875 items.append((sym_ref, name))
881 items.append((sym_ref, name))
876 return items
882 return items
877
883
878 def _symbolic_reference(self, commit_id, name, f_path):
884 def _symbolic_reference(self, commit_id, name, f_path):
879 return commit_id
885 return commit_id
880
886
881 def _symbolic_reference_svn(self, commit_id, name, f_path):
887 def _symbolic_reference_svn(self, commit_id, name, f_path):
882 new_f_path = vcspath.join(name, f_path)
888 new_f_path = vcspath.join(name, f_path)
883 return u'%s@%s' % (new_f_path, commit_id)
889 return u'%s@%s' % (new_f_path, commit_id)
884
890
885 def _get_node_history(self, commit_obj, f_path, commits=None):
891 def _get_node_history(self, commit_obj, f_path, commits=None):
886 """
892 """
887 get commit history for given node
893 get commit history for given node
888
894
889 :param commit_obj: commit to calculate history
895 :param commit_obj: commit to calculate history
890 :param f_path: path for node to calculate history for
896 :param f_path: path for node to calculate history for
891 :param commits: if passed don't calculate history and take
897 :param commits: if passed don't calculate history and take
892 commits defined in this list
898 commits defined in this list
893 """
899 """
894 _ = self.request.translate
900 _ = self.request.translate
895
901
896 # calculate history based on tip
902 # calculate history based on tip
897 tip = self.rhodecode_vcs_repo.get_commit()
903 tip = self.rhodecode_vcs_repo.get_commit()
898 if commits is None:
904 if commits is None:
899 pre_load = ["author", "branch"]
905 pre_load = ["author", "branch"]
900 try:
906 try:
901 commits = tip.get_path_history(f_path, pre_load=pre_load)
907 commits = tip.get_path_history(f_path, pre_load=pre_load)
902 except (NodeDoesNotExistError, CommitError):
908 except (NodeDoesNotExistError, CommitError):
903 # this node is not present at tip!
909 # this node is not present at tip!
904 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
910 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
905
911
906 history = []
912 history = []
907 commits_group = ([], _("Changesets"))
913 commits_group = ([], _("Changesets"))
908 for commit in commits:
914 for commit in commits:
909 branch = ' (%s)' % commit.branch if commit.branch else ''
915 branch = ' (%s)' % commit.branch if commit.branch else ''
910 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
916 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
911 commits_group[0].append((commit.raw_id, n_desc,))
917 commits_group[0].append((commit.raw_id, n_desc,))
912 history.append(commits_group)
918 history.append(commits_group)
913
919
914 symbolic_reference = self._symbolic_reference
920 symbolic_reference = self._symbolic_reference
915
921
916 if self.rhodecode_vcs_repo.alias == 'svn':
922 if self.rhodecode_vcs_repo.alias == 'svn':
917 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
923 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
918 f_path, self.rhodecode_vcs_repo)
924 f_path, self.rhodecode_vcs_repo)
919 if adjusted_f_path != f_path:
925 if adjusted_f_path != f_path:
920 log.debug(
926 log.debug(
921 'Recognized svn tag or branch in file "%s", using svn '
927 'Recognized svn tag or branch in file "%s", using svn '
922 'specific symbolic references', f_path)
928 'specific symbolic references', f_path)
923 f_path = adjusted_f_path
929 f_path = adjusted_f_path
924 symbolic_reference = self._symbolic_reference_svn
930 symbolic_reference = self._symbolic_reference_svn
925
931
926 branches = self._create_references(
932 branches = self._create_references(
927 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
933 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
928 branches_group = (branches, _("Branches"))
934 branches_group = (branches, _("Branches"))
929
935
930 tags = self._create_references(
936 tags = self._create_references(
931 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
937 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
932 tags_group = (tags, _("Tags"))
938 tags_group = (tags, _("Tags"))
933
939
934 history.append(branches_group)
940 history.append(branches_group)
935 history.append(tags_group)
941 history.append(tags_group)
936
942
937 return history, commits
943 return history, commits
938
944
939 @LoginRequired()
945 @LoginRequired()
940 @HasRepoPermissionAnyDecorator(
946 @HasRepoPermissionAnyDecorator(
941 'repository.read', 'repository.write', 'repository.admin')
947 'repository.read', 'repository.write', 'repository.admin')
942 @view_config(
948 @view_config(
943 route_name='repo_file_history', request_method='GET',
949 route_name='repo_file_history', request_method='GET',
944 renderer='json_ext')
950 renderer='json_ext')
945 def repo_file_history(self):
951 def repo_file_history(self):
946 self.load_default_context()
952 self.load_default_context()
947
953
948 commit_id, f_path = self._get_commit_and_path()
954 commit_id, f_path = self._get_commit_and_path()
949 commit = self._get_commit_or_redirect(commit_id)
955 commit = self._get_commit_or_redirect(commit_id)
950 file_node = self._get_filenode_or_redirect(commit, f_path)
956 file_node = self._get_filenode_or_redirect(commit, f_path)
951
957
952 if file_node.is_file():
958 if file_node.is_file():
953 file_history, _hist = self._get_node_history(commit, f_path)
959 file_history, _hist = self._get_node_history(commit, f_path)
954
960
955 res = []
961 res = []
956 for obj in file_history:
962 for obj in file_history:
957 res.append({
963 res.append({
958 'text': obj[1],
964 'text': obj[1],
959 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
965 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
960 })
966 })
961
967
962 data = {
968 data = {
963 'more': False,
969 'more': False,
964 'results': res
970 'results': res
965 }
971 }
966 return data
972 return data
967
973
968 log.warning('Cannot fetch history for directory')
974 log.warning('Cannot fetch history for directory')
969 raise HTTPBadRequest()
975 raise HTTPBadRequest()
970
976
971 @LoginRequired()
977 @LoginRequired()
972 @HasRepoPermissionAnyDecorator(
978 @HasRepoPermissionAnyDecorator(
973 'repository.read', 'repository.write', 'repository.admin')
979 'repository.read', 'repository.write', 'repository.admin')
974 @view_config(
980 @view_config(
975 route_name='repo_file_authors', request_method='GET',
981 route_name='repo_file_authors', request_method='GET',
976 renderer='rhodecode:templates/files/file_authors_box.mako')
982 renderer='rhodecode:templates/files/file_authors_box.mako')
977 def repo_file_authors(self):
983 def repo_file_authors(self):
978 c = self.load_default_context()
984 c = self.load_default_context()
979
985
980 commit_id, f_path = self._get_commit_and_path()
986 commit_id, f_path = self._get_commit_and_path()
981 commit = self._get_commit_or_redirect(commit_id)
987 commit = self._get_commit_or_redirect(commit_id)
982 file_node = self._get_filenode_or_redirect(commit, f_path)
988 file_node = self._get_filenode_or_redirect(commit, f_path)
983
989
984 if not file_node.is_file():
990 if not file_node.is_file():
985 raise HTTPBadRequest()
991 raise HTTPBadRequest()
986
992
987 c.file_last_commit = file_node.last_commit
993 c.file_last_commit = file_node.last_commit
988 if self.request.GET.get('annotate') == '1':
994 if self.request.GET.get('annotate') == '1':
989 # use _hist from annotation if annotation mode is on
995 # use _hist from annotation if annotation mode is on
990 commit_ids = set(x[1] for x in file_node.annotate)
996 commit_ids = set(x[1] for x in file_node.annotate)
991 _hist = (
997 _hist = (
992 self.rhodecode_vcs_repo.get_commit(commit_id)
998 self.rhodecode_vcs_repo.get_commit(commit_id)
993 for commit_id in commit_ids)
999 for commit_id in commit_ids)
994 else:
1000 else:
995 _f_history, _hist = self._get_node_history(commit, f_path)
1001 _f_history, _hist = self._get_node_history(commit, f_path)
996 c.file_author = False
1002 c.file_author = False
997
1003
998 unique = collections.OrderedDict()
1004 unique = collections.OrderedDict()
999 for commit in _hist:
1005 for commit in _hist:
1000 author = commit.author
1006 author = commit.author
1001 if author not in unique:
1007 if author not in unique:
1002 unique[commit.author] = [
1008 unique[commit.author] = [
1003 h.email(author),
1009 h.email(author),
1004 h.person(author, 'username_or_name_or_email'),
1010 h.person(author, 'username_or_name_or_email'),
1005 1 # counter
1011 1 # counter
1006 ]
1012 ]
1007
1013
1008 else:
1014 else:
1009 # increase counter
1015 # increase counter
1010 unique[commit.author][2] += 1
1016 unique[commit.author][2] += 1
1011
1017
1012 c.authors = [val for val in unique.values()]
1018 c.authors = [val for val in unique.values()]
1013
1019
1014 return self._get_template_context(c)
1020 return self._get_template_context(c)
1015
1021
1016 @LoginRequired()
1022 @LoginRequired()
1017 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1023 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1018 @view_config(
1024 @view_config(
1019 route_name='repo_files_remove_file', request_method='GET',
1025 route_name='repo_files_remove_file', request_method='GET',
1020 renderer='rhodecode:templates/files/files_delete.mako')
1026 renderer='rhodecode:templates/files/files_delete.mako')
1021 def repo_files_remove_file(self):
1027 def repo_files_remove_file(self):
1022 _ = self.request.translate
1028 _ = self.request.translate
1023 c = self.load_default_context()
1029 c = self.load_default_context()
1024 commit_id, f_path = self._get_commit_and_path()
1030 commit_id, f_path = self._get_commit_and_path()
1025
1031
1026 self._ensure_not_locked()
1032 self._ensure_not_locked()
1027 _branch_name, _sha_commit_id, is_head = \
1033 _branch_name, _sha_commit_id, is_head = \
1028 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1034 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1029
1035
1030 if not is_head:
1036 if not is_head:
1031 h.flash(_('You can only delete files with commit '
1037 h.flash(_('You can only delete files with commit '
1032 'being a valid branch head.'), category='warning')
1038 'being a valid branch head.'), category='warning')
1033 raise HTTPFound(
1039 raise HTTPFound(
1034 h.route_path('repo_files',
1040 h.route_path('repo_files',
1035 repo_name=self.db_repo_name, commit_id='tip',
1041 repo_name=self.db_repo_name, commit_id='tip',
1036 f_path=f_path))
1042 f_path=f_path))
1037
1043
1038 self.check_branch_permission(_branch_name)
1044 self.check_branch_permission(_branch_name)
1039 c.commit = self._get_commit_or_redirect(commit_id)
1045 c.commit = self._get_commit_or_redirect(commit_id)
1040 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1046 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1041
1047
1042 c.default_message = _(
1048 c.default_message = _(
1043 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1049 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1044 c.f_path = f_path
1050 c.f_path = f_path
1045
1051
1046 return self._get_template_context(c)
1052 return self._get_template_context(c)
1047
1053
1048 @LoginRequired()
1054 @LoginRequired()
1049 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1055 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1050 @CSRFRequired()
1056 @CSRFRequired()
1051 @view_config(
1057 @view_config(
1052 route_name='repo_files_delete_file', request_method='POST',
1058 route_name='repo_files_delete_file', request_method='POST',
1053 renderer=None)
1059 renderer=None)
1054 def repo_files_delete_file(self):
1060 def repo_files_delete_file(self):
1055 _ = self.request.translate
1061 _ = self.request.translate
1056
1062
1057 c = self.load_default_context()
1063 c = self.load_default_context()
1058 commit_id, f_path = self._get_commit_and_path()
1064 commit_id, f_path = self._get_commit_and_path()
1059
1065
1060 self._ensure_not_locked()
1066 self._ensure_not_locked()
1061 _branch_name, _sha_commit_id, is_head = \
1067 _branch_name, _sha_commit_id, is_head = \
1062 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1068 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1063
1069
1064 if not is_head:
1070 if not is_head:
1065 h.flash(_('You can only delete files with commit '
1071 h.flash(_('You can only delete files with commit '
1066 'being a valid branch head.'), category='warning')
1072 'being a valid branch head.'), category='warning')
1067 raise HTTPFound(
1073 raise HTTPFound(
1068 h.route_path('repo_files',
1074 h.route_path('repo_files',
1069 repo_name=self.db_repo_name, commit_id='tip',
1075 repo_name=self.db_repo_name, commit_id='tip',
1070 f_path=f_path))
1076 f_path=f_path))
1071 self.check_branch_permission(_branch_name)
1077 self.check_branch_permission(_branch_name)
1072
1078
1073 c.commit = self._get_commit_or_redirect(commit_id)
1079 c.commit = self._get_commit_or_redirect(commit_id)
1074 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1080 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1075
1081
1076 c.default_message = _(
1082 c.default_message = _(
1077 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1083 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1078 c.f_path = f_path
1084 c.f_path = f_path
1079 node_path = f_path
1085 node_path = f_path
1080 author = self._rhodecode_db_user.full_contact
1086 author = self._rhodecode_db_user.full_contact
1081 message = self.request.POST.get('message') or c.default_message
1087 message = self.request.POST.get('message') or c.default_message
1082 try:
1088 try:
1083 nodes = {
1089 nodes = {
1084 node_path: {
1090 node_path: {
1085 'content': ''
1091 'content': ''
1086 }
1092 }
1087 }
1093 }
1088 ScmModel().delete_nodes(
1094 ScmModel().delete_nodes(
1089 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1095 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1090 message=message,
1096 message=message,
1091 nodes=nodes,
1097 nodes=nodes,
1092 parent_commit=c.commit,
1098 parent_commit=c.commit,
1093 author=author,
1099 author=author,
1094 )
1100 )
1095
1101
1096 h.flash(
1102 h.flash(
1097 _('Successfully deleted file `{}`').format(
1103 _('Successfully deleted file `{}`').format(
1098 h.escape(f_path)), category='success')
1104 h.escape(f_path)), category='success')
1099 except Exception:
1105 except Exception:
1100 log.exception('Error during commit operation')
1106 log.exception('Error during commit operation')
1101 h.flash(_('Error occurred during commit'), category='error')
1107 h.flash(_('Error occurred during commit'), category='error')
1102 raise HTTPFound(
1108 raise HTTPFound(
1103 h.route_path('repo_commit', repo_name=self.db_repo_name,
1109 h.route_path('repo_commit', repo_name=self.db_repo_name,
1104 commit_id='tip'))
1110 commit_id='tip'))
1105
1111
1106 @LoginRequired()
1112 @LoginRequired()
1107 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1113 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1108 @view_config(
1114 @view_config(
1109 route_name='repo_files_edit_file', request_method='GET',
1115 route_name='repo_files_edit_file', request_method='GET',
1110 renderer='rhodecode:templates/files/files_edit.mako')
1116 renderer='rhodecode:templates/files/files_edit.mako')
1111 def repo_files_edit_file(self):
1117 def repo_files_edit_file(self):
1112 _ = self.request.translate
1118 _ = self.request.translate
1113 c = self.load_default_context()
1119 c = self.load_default_context()
1114 commit_id, f_path = self._get_commit_and_path()
1120 commit_id, f_path = self._get_commit_and_path()
1115
1121
1116 self._ensure_not_locked()
1122 self._ensure_not_locked()
1117 _branch_name, _sha_commit_id, is_head = \
1123 _branch_name, _sha_commit_id, is_head = \
1118 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1124 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1119
1125
1120 if not is_head:
1126 if not is_head:
1121 h.flash(_('You can only edit files with commit '
1127 h.flash(_('You can only edit files with commit '
1122 'being a valid branch head.'), category='warning')
1128 'being a valid branch head.'), category='warning')
1123 raise HTTPFound(
1129 raise HTTPFound(
1124 h.route_path('repo_files',
1130 h.route_path('repo_files',
1125 repo_name=self.db_repo_name, commit_id='tip',
1131 repo_name=self.db_repo_name, commit_id='tip',
1126 f_path=f_path))
1132 f_path=f_path))
1127 self.check_branch_permission(_branch_name)
1133 self.check_branch_permission(_branch_name)
1128
1134
1129 c.commit = self._get_commit_or_redirect(commit_id)
1135 c.commit = self._get_commit_or_redirect(commit_id)
1130 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1136 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1131
1137
1132 if c.file.is_binary:
1138 if c.file.is_binary:
1133 files_url = h.route_path(
1139 files_url = h.route_path(
1134 'repo_files',
1140 'repo_files',
1135 repo_name=self.db_repo_name,
1141 repo_name=self.db_repo_name,
1136 commit_id=c.commit.raw_id, f_path=f_path)
1142 commit_id=c.commit.raw_id, f_path=f_path)
1137 raise HTTPFound(files_url)
1143 raise HTTPFound(files_url)
1138
1144
1139 c.default_message = _(
1145 c.default_message = _(
1140 'Edited file {} via RhodeCode Enterprise').format(f_path)
1146 'Edited file {} via RhodeCode Enterprise').format(f_path)
1141 c.f_path = f_path
1147 c.f_path = f_path
1142
1148
1143 return self._get_template_context(c)
1149 return self._get_template_context(c)
1144
1150
1145 @LoginRequired()
1151 @LoginRequired()
1146 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1152 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1147 @CSRFRequired()
1153 @CSRFRequired()
1148 @view_config(
1154 @view_config(
1149 route_name='repo_files_update_file', request_method='POST',
1155 route_name='repo_files_update_file', request_method='POST',
1150 renderer=None)
1156 renderer=None)
1151 def repo_files_update_file(self):
1157 def repo_files_update_file(self):
1152 _ = self.request.translate
1158 _ = self.request.translate
1153 c = self.load_default_context()
1159 c = self.load_default_context()
1154 commit_id, f_path = self._get_commit_and_path()
1160 commit_id, f_path = self._get_commit_and_path()
1155
1161
1156 self._ensure_not_locked()
1162 self._ensure_not_locked()
1157 _branch_name, _sha_commit_id, is_head = \
1163 _branch_name, _sha_commit_id, is_head = \
1158 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1164 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1159
1165
1160 if not is_head:
1166 if not is_head:
1161 h.flash(_('You can only edit files with commit '
1167 h.flash(_('You can only edit files with commit '
1162 'being a valid branch head.'), category='warning')
1168 'being a valid branch head.'), category='warning')
1163 raise HTTPFound(
1169 raise HTTPFound(
1164 h.route_path('repo_files',
1170 h.route_path('repo_files',
1165 repo_name=self.db_repo_name, commit_id='tip',
1171 repo_name=self.db_repo_name, commit_id='tip',
1166 f_path=f_path))
1172 f_path=f_path))
1167
1173
1168 self.check_branch_permission(_branch_name)
1174 self.check_branch_permission(_branch_name)
1169
1175
1170 c.commit = self._get_commit_or_redirect(commit_id)
1176 c.commit = self._get_commit_or_redirect(commit_id)
1171 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1177 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1172
1178
1173 if c.file.is_binary:
1179 if c.file.is_binary:
1174 raise HTTPFound(
1180 raise HTTPFound(
1175 h.route_path('repo_files',
1181 h.route_path('repo_files',
1176 repo_name=self.db_repo_name,
1182 repo_name=self.db_repo_name,
1177 commit_id=c.commit.raw_id,
1183 commit_id=c.commit.raw_id,
1178 f_path=f_path))
1184 f_path=f_path))
1179
1185
1180 c.default_message = _(
1186 c.default_message = _(
1181 'Edited file {} via RhodeCode Enterprise').format(f_path)
1187 'Edited file {} via RhodeCode Enterprise').format(f_path)
1182 c.f_path = f_path
1188 c.f_path = f_path
1183 old_content = c.file.content
1189 old_content = c.file.content
1184 sl = old_content.splitlines(1)
1190 sl = old_content.splitlines(1)
1185 first_line = sl[0] if sl else ''
1191 first_line = sl[0] if sl else ''
1186
1192
1187 r_post = self.request.POST
1193 r_post = self.request.POST
1188 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1194 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1189 mode = detect_mode(first_line, 0)
1195 mode = detect_mode(first_line, 0)
1190 content = convert_line_endings(r_post.get('content', ''), mode)
1196 content = convert_line_endings(r_post.get('content', ''), mode)
1191
1197
1192 message = r_post.get('message') or c.default_message
1198 message = r_post.get('message') or c.default_message
1193 org_f_path = c.file.unicode_path
1199 org_f_path = c.file.unicode_path
1194 filename = r_post['filename']
1200 filename = r_post['filename']
1195 org_filename = c.file.name
1201 org_filename = c.file.name
1196
1202
1197 if content == old_content and filename == org_filename:
1203 if content == old_content and filename == org_filename:
1198 h.flash(_('No changes'), category='warning')
1204 h.flash(_('No changes'), category='warning')
1199 raise HTTPFound(
1205 raise HTTPFound(
1200 h.route_path('repo_commit', repo_name=self.db_repo_name,
1206 h.route_path('repo_commit', repo_name=self.db_repo_name,
1201 commit_id='tip'))
1207 commit_id='tip'))
1202 try:
1208 try:
1203 mapping = {
1209 mapping = {
1204 org_f_path: {
1210 org_f_path: {
1205 'org_filename': org_f_path,
1211 'org_filename': org_f_path,
1206 'filename': os.path.join(c.file.dir_path, filename),
1212 'filename': os.path.join(c.file.dir_path, filename),
1207 'content': content,
1213 'content': content,
1208 'lexer': '',
1214 'lexer': '',
1209 'op': 'mod',
1215 'op': 'mod',
1210 }
1216 }
1211 }
1217 }
1212
1218
1213 ScmModel().update_nodes(
1219 ScmModel().update_nodes(
1214 user=self._rhodecode_db_user.user_id,
1220 user=self._rhodecode_db_user.user_id,
1215 repo=self.db_repo,
1221 repo=self.db_repo,
1216 message=message,
1222 message=message,
1217 nodes=mapping,
1223 nodes=mapping,
1218 parent_commit=c.commit,
1224 parent_commit=c.commit,
1219 )
1225 )
1220
1226
1221 h.flash(
1227 h.flash(
1222 _('Successfully committed changes to file `{}`').format(
1228 _('Successfully committed changes to file `{}`').format(
1223 h.escape(f_path)), category='success')
1229 h.escape(f_path)), category='success')
1224 except Exception:
1230 except Exception:
1225 log.exception('Error occurred during commit')
1231 log.exception('Error occurred during commit')
1226 h.flash(_('Error occurred during commit'), category='error')
1232 h.flash(_('Error occurred during commit'), category='error')
1227 raise HTTPFound(
1233 raise HTTPFound(
1228 h.route_path('repo_commit', repo_name=self.db_repo_name,
1234 h.route_path('repo_commit', repo_name=self.db_repo_name,
1229 commit_id='tip'))
1235 commit_id='tip'))
1230
1236
1231 @LoginRequired()
1237 @LoginRequired()
1232 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1238 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1233 @view_config(
1239 @view_config(
1234 route_name='repo_files_add_file', request_method='GET',
1240 route_name='repo_files_add_file', request_method='GET',
1235 renderer='rhodecode:templates/files/files_add.mako')
1241 renderer='rhodecode:templates/files/files_add.mako')
1236 def repo_files_add_file(self):
1242 def repo_files_add_file(self):
1237 _ = self.request.translate
1243 _ = self.request.translate
1238 c = self.load_default_context()
1244 c = self.load_default_context()
1239 commit_id, f_path = self._get_commit_and_path()
1245 commit_id, f_path = self._get_commit_and_path()
1240
1246
1241 self._ensure_not_locked()
1247 self._ensure_not_locked()
1242
1248
1243 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1249 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1244 if c.commit is None:
1250 if c.commit is None:
1245 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1251 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1246 c.default_message = (_('Added file via RhodeCode Enterprise'))
1252 c.default_message = (_('Added file via RhodeCode Enterprise'))
1247 c.f_path = f_path.lstrip('/') # ensure not relative path
1253 c.f_path = f_path.lstrip('/') # ensure not relative path
1248
1254
1249 if self.rhodecode_vcs_repo.is_empty:
1255 if self.rhodecode_vcs_repo.is_empty:
1250 # for empty repository we cannot check for current branch, we rely on
1256 # for empty repository we cannot check for current branch, we rely on
1251 # c.commit.branch instead
1257 # c.commit.branch instead
1252 _branch_name = c.commit.branch
1258 _branch_name = c.commit.branch
1253 is_head = True
1259 is_head = True
1254 else:
1260 else:
1255 _branch_name, _sha_commit_id, is_head = \
1261 _branch_name, _sha_commit_id, is_head = \
1256 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1262 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1257
1263
1258 if not is_head:
1264 if not is_head:
1259 h.flash(_('You can only add files with commit '
1265 h.flash(_('You can only add files with commit '
1260 'being a valid branch head.'), category='warning')
1266 'being a valid branch head.'), category='warning')
1261 raise HTTPFound(
1267 raise HTTPFound(
1262 h.route_path('repo_files',
1268 h.route_path('repo_files',
1263 repo_name=self.db_repo_name, commit_id='tip',
1269 repo_name=self.db_repo_name, commit_id='tip',
1264 f_path=f_path))
1270 f_path=f_path))
1265
1271
1266 self.check_branch_permission(_branch_name)
1272 self.check_branch_permission(_branch_name)
1267
1273
1268 return self._get_template_context(c)
1274 return self._get_template_context(c)
1269
1275
1270 @LoginRequired()
1276 @LoginRequired()
1271 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1277 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1272 @CSRFRequired()
1278 @CSRFRequired()
1273 @view_config(
1279 @view_config(
1274 route_name='repo_files_create_file', request_method='POST',
1280 route_name='repo_files_create_file', request_method='POST',
1275 renderer=None)
1281 renderer=None)
1276 def repo_files_create_file(self):
1282 def repo_files_create_file(self):
1277 _ = self.request.translate
1283 _ = self.request.translate
1278 c = self.load_default_context()
1284 c = self.load_default_context()
1279 commit_id, f_path = self._get_commit_and_path()
1285 commit_id, f_path = self._get_commit_and_path()
1280
1286
1281 self._ensure_not_locked()
1287 self._ensure_not_locked()
1282
1288
1283 r_post = self.request.POST
1289 r_post = self.request.POST
1284
1290
1285 c.commit = self._get_commit_or_redirect(
1291 c.commit = self._get_commit_or_redirect(
1286 commit_id, redirect_after=False)
1292 commit_id, redirect_after=False)
1287 if c.commit is None:
1293 if c.commit is None:
1288 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1294 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1289
1295
1290 if self.rhodecode_vcs_repo.is_empty:
1296 if self.rhodecode_vcs_repo.is_empty:
1291 # for empty repository we cannot check for current branch, we rely on
1297 # for empty repository we cannot check for current branch, we rely on
1292 # c.commit.branch instead
1298 # c.commit.branch instead
1293 _branch_name = c.commit.branch
1299 _branch_name = c.commit.branch
1294 is_head = True
1300 is_head = True
1295 else:
1301 else:
1296 _branch_name, _sha_commit_id, is_head = \
1302 _branch_name, _sha_commit_id, is_head = \
1297 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1303 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1298
1304
1299 if not is_head:
1305 if not is_head:
1300 h.flash(_('You can only add files with commit '
1306 h.flash(_('You can only add files with commit '
1301 'being a valid branch head.'), category='warning')
1307 'being a valid branch head.'), category='warning')
1302 raise HTTPFound(
1308 raise HTTPFound(
1303 h.route_path('repo_files',
1309 h.route_path('repo_files',
1304 repo_name=self.db_repo_name, commit_id='tip',
1310 repo_name=self.db_repo_name, commit_id='tip',
1305 f_path=f_path))
1311 f_path=f_path))
1306
1312
1307 self.check_branch_permission(_branch_name)
1313 self.check_branch_permission(_branch_name)
1308
1314
1309 c.default_message = (_('Added file via RhodeCode Enterprise'))
1315 c.default_message = (_('Added file via RhodeCode Enterprise'))
1310 c.f_path = f_path
1316 c.f_path = f_path
1311 unix_mode = 0
1317 unix_mode = 0
1312 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1318 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1313
1319
1314 message = r_post.get('message') or c.default_message
1320 message = r_post.get('message') or c.default_message
1315 filename = r_post.get('filename')
1321 filename = r_post.get('filename')
1316 location = r_post.get('location', '') # dir location
1322 location = r_post.get('location', '') # dir location
1317 file_obj = r_post.get('upload_file', None)
1323 file_obj = r_post.get('upload_file', None)
1318
1324
1319 if file_obj is not None and hasattr(file_obj, 'filename'):
1325 if file_obj is not None and hasattr(file_obj, 'filename'):
1320 filename = r_post.get('filename_upload')
1326 filename = r_post.get('filename_upload')
1321 content = file_obj.file
1327 content = file_obj.file
1322
1328
1323 if hasattr(content, 'file'):
1329 if hasattr(content, 'file'):
1324 # non posix systems store real file under file attr
1330 # non posix systems store real file under file attr
1325 content = content.file
1331 content = content.file
1326
1332
1327 if self.rhodecode_vcs_repo.is_empty:
1333 if self.rhodecode_vcs_repo.is_empty:
1328 default_redirect_url = h.route_path(
1334 default_redirect_url = h.route_path(
1329 'repo_summary', repo_name=self.db_repo_name)
1335 'repo_summary', repo_name=self.db_repo_name)
1330 else:
1336 else:
1331 default_redirect_url = h.route_path(
1337 default_redirect_url = h.route_path(
1332 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1338 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1333
1339
1334 # If there's no commit, redirect to repo summary
1340 # If there's no commit, redirect to repo summary
1335 if type(c.commit) is EmptyCommit:
1341 if type(c.commit) is EmptyCommit:
1336 redirect_url = h.route_path(
1342 redirect_url = h.route_path(
1337 'repo_summary', repo_name=self.db_repo_name)
1343 'repo_summary', repo_name=self.db_repo_name)
1338 else:
1344 else:
1339 redirect_url = default_redirect_url
1345 redirect_url = default_redirect_url
1340
1346
1341 if not filename:
1347 if not filename:
1342 h.flash(_('No filename'), category='warning')
1348 h.flash(_('No filename'), category='warning')
1343 raise HTTPFound(redirect_url)
1349 raise HTTPFound(redirect_url)
1344
1350
1345 # extract the location from filename,
1351 # extract the location from filename,
1346 # allows using foo/bar.txt syntax to create subdirectories
1352 # allows using foo/bar.txt syntax to create subdirectories
1347 subdir_loc = filename.rsplit('/', 1)
1353 subdir_loc = filename.rsplit('/', 1)
1348 if len(subdir_loc) == 2:
1354 if len(subdir_loc) == 2:
1349 location = os.path.join(location, subdir_loc[0])
1355 location = os.path.join(location, subdir_loc[0])
1350
1356
1351 # strip all crap out of file, just leave the basename
1357 # strip all crap out of file, just leave the basename
1352 filename = os.path.basename(filename)
1358 filename = os.path.basename(filename)
1353 node_path = os.path.join(location, filename)
1359 node_path = os.path.join(location, filename)
1354 author = self._rhodecode_db_user.full_contact
1360 author = self._rhodecode_db_user.full_contact
1355
1361
1356 try:
1362 try:
1357 nodes = {
1363 nodes = {
1358 node_path: {
1364 node_path: {
1359 'content': content
1365 'content': content
1360 }
1366 }
1361 }
1367 }
1362 ScmModel().create_nodes(
1368 ScmModel().create_nodes(
1363 user=self._rhodecode_db_user.user_id,
1369 user=self._rhodecode_db_user.user_id,
1364 repo=self.db_repo,
1370 repo=self.db_repo,
1365 message=message,
1371 message=message,
1366 nodes=nodes,
1372 nodes=nodes,
1367 parent_commit=c.commit,
1373 parent_commit=c.commit,
1368 author=author,
1374 author=author,
1369 )
1375 )
1370
1376
1371 h.flash(
1377 h.flash(
1372 _('Successfully committed new file `{}`').format(
1378 _('Successfully committed new file `{}`').format(
1373 h.escape(node_path)), category='success')
1379 h.escape(node_path)), category='success')
1374 except NonRelativePathError:
1380 except NonRelativePathError:
1375 log.exception('Non Relative path found')
1381 log.exception('Non Relative path found')
1376 h.flash(_(
1382 h.flash(_(
1377 'The location specified must be a relative path and must not '
1383 'The location specified must be a relative path and must not '
1378 'contain .. in the path'), category='warning')
1384 'contain .. in the path'), category='warning')
1379 raise HTTPFound(default_redirect_url)
1385 raise HTTPFound(default_redirect_url)
1380 except (NodeError, NodeAlreadyExistsError) as e:
1386 except (NodeError, NodeAlreadyExistsError) as e:
1381 h.flash(_(h.escape(e)), category='error')
1387 h.flash(_(h.escape(e)), category='error')
1382 except Exception:
1388 except Exception:
1383 log.exception('Error occurred during commit')
1389 log.exception('Error occurred during commit')
1384 h.flash(_('Error occurred during commit'), category='error')
1390 h.flash(_('Error occurred during commit'), category='error')
1385
1391
1386 raise HTTPFound(default_redirect_url)
1392 raise HTTPFound(default_redirect_url)
@@ -1,225 +1,226 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 base64
21 import base64
22 import logging
22 import logging
23 import urllib
23 import urllib
24 import urlparse
24 import urlparse
25
25
26 import requests
26 import requests
27 from pyramid.httpexceptions import HTTPNotAcceptable
27 from pyramid.httpexceptions import HTTPNotAcceptable
28
28
29 from rhodecode.lib import rc_cache
29 from rhodecode.lib import rc_cache
30 from rhodecode.lib.middleware import simplevcs
30 from rhodecode.lib.middleware import simplevcs
31 from rhodecode.lib.utils import is_valid_repo
31 from rhodecode.lib.utils import is_valid_repo
32 from rhodecode.lib.utils2 import str2bool, safe_int
32 from rhodecode.lib.utils2 import str2bool, safe_int
33 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.hooks_daemon import store_txn_id_data
34 from rhodecode.lib.hooks_daemon import store_txn_id_data
35
35
36
36
37 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
38
38
39
39
40 class SimpleSvnApp(object):
40 class SimpleSvnApp(object):
41 IGNORED_HEADERS = [
41 IGNORED_HEADERS = [
42 'connection', 'keep-alive', 'content-encoding',
42 'connection', 'keep-alive', 'content-encoding',
43 'transfer-encoding', 'content-length']
43 'transfer-encoding', 'content-length']
44 rc_extras = {}
44 rc_extras = {}
45
45
46 def __init__(self, config):
46 def __init__(self, config):
47 self.config = config
47 self.config = config
48
48
49 def __call__(self, environ, start_response):
49 def __call__(self, environ, start_response):
50 request_headers = self._get_request_headers(environ)
50 request_headers = self._get_request_headers(environ)
51 data = environ['wsgi.input']
51 data = environ['wsgi.input']
52 req_method = environ['REQUEST_METHOD']
52 req_method = environ['REQUEST_METHOD']
53 has_content_length = 'CONTENT_LENGTH' in environ
53 has_content_length = 'CONTENT_LENGTH' in environ
54 path_info = self._get_url(environ['PATH_INFO'])
54 path_info = self._get_url(
55 self.config.get('subversion_http_server_url', ''), environ['PATH_INFO'])
55 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
56 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
56 log.debug('Handling: %s method via `%s`', req_method, path_info)
57 log.debug('Handling: %s method via `%s`', req_method, path_info)
57
58
58 # stream control flag, based on request and content type...
59 # stream control flag, based on request and content type...
59 stream = False
60 stream = False
60
61
61 if req_method in ['MKCOL'] or has_content_length:
62 if req_method in ['MKCOL'] or has_content_length:
62 data_processed = False
63 data_processed = False
63 # read chunk to check if we have txn-with-props
64 # read chunk to check if we have txn-with-props
64 initial_data = data.read(1024)
65 initial_data = data.read(1024)
65 if initial_data.startswith('(create-txn-with-props'):
66 if initial_data.startswith('(create-txn-with-props'):
66 data = initial_data + data.read()
67 data = initial_data + data.read()
67 # store on-the-fly our rc_extra using svn revision properties
68 # store on-the-fly our rc_extra using svn revision properties
68 # those can be read later on in hooks executed so we have a way
69 # those can be read later on in hooks executed so we have a way
69 # to pass in the data into svn hooks
70 # to pass in the data into svn hooks
70 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
71 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
71 rc_data_len = len(rc_data)
72 rc_data_len = len(rc_data)
72 # header defines data length, and serialized data
73 # header defines data length, and serialized data
73 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
74 skel = ' rc-scm-extras {} {}'.format(rc_data_len, rc_data)
74 data = data[:-2] + skel + '))'
75 data = data[:-2] + skel + '))'
75 data_processed = True
76 data_processed = True
76
77
77 if not data_processed:
78 if not data_processed:
78 # NOTE(johbo): Avoid that we end up with sending the request in chunked
79 # NOTE(johbo): Avoid that we end up with sending the request in chunked
79 # transfer encoding (mainly on Gunicorn). If we know the content
80 # transfer encoding (mainly on Gunicorn). If we know the content
80 # length, then we should transfer the payload in one request.
81 # length, then we should transfer the payload in one request.
81 data = initial_data + data.read()
82 data = initial_data + data.read()
82
83
83 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
84 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
84 # NOTE(marcink): when getting/uploading files we want to STREAM content
85 # NOTE(marcink): when getting/uploading files we want to STREAM content
85 # back to the client/proxy instead of buffering it here...
86 # back to the client/proxy instead of buffering it here...
86 stream = True
87 stream = True
87
88
88 stream = stream
89 stream = stream
89 log.debug(
90 log.debug(
90 'Calling SVN PROXY: method:%s via `%s`, Stream: %s',
91 'Calling SVN PROXY: method:%s via `%s`, Stream: %s',
91 req_method, path_info, stream)
92 req_method, path_info, stream)
92 response = requests.request(
93 response = requests.request(
93 req_method, path_info,
94 req_method, path_info,
94 data=data, headers=request_headers, stream=stream)
95 data=data, headers=request_headers, stream=stream)
95
96
96 if response.status_code not in [200, 401]:
97 if response.status_code not in [200, 401]:
97 if response.status_code >= 500:
98 if response.status_code >= 500:
98 log.error('Got SVN response:%s with text:\n`%s`',
99 log.error('Got SVN response:%s with text:\n`%s`',
99 response, response.text)
100 response, response.text)
100 else:
101 else:
101 log.debug('Got SVN response:%s with text:\n`%s`',
102 log.debug('Got SVN response:%s with text:\n`%s`',
102 response, response.text)
103 response, response.text)
103 else:
104 else:
104 log.debug('got response code: %s', response.status_code)
105 log.debug('got response code: %s', response.status_code)
105
106
106 response_headers = self._get_response_headers(response.headers)
107 response_headers = self._get_response_headers(response.headers)
107
108
108 if response.headers.get('SVN-Txn-name'):
109 if response.headers.get('SVN-Txn-name'):
109 svn_tx_id = response.headers.get('SVN-Txn-name')
110 svn_tx_id = response.headers.get('SVN-Txn-name')
110 txn_id = rc_cache.utils.compute_key_from_params(
111 txn_id = rc_cache.utils.compute_key_from_params(
111 self.config['repository'], svn_tx_id)
112 self.config['repository'], svn_tx_id)
112 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
113 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
113 store_txn_id_data(txn_id, {'port': port})
114 store_txn_id_data(txn_id, {'port': port})
114
115
115 start_response(
116 start_response(
116 '{} {}'.format(response.status_code, response.reason),
117 '{} {}'.format(response.status_code, response.reason),
117 response_headers)
118 response_headers)
118 return response.iter_content(chunk_size=1024)
119 return response.iter_content(chunk_size=1024)
119
120
120 def _get_url(self, path):
121 def _get_url(self, svn_http_server, path):
121 url_path = urlparse.urljoin(
122 svn_http_server_url = (svn_http_server or '').rstrip('/')
122 self.config.get('subversion_http_server_url', ''), path)
123 url_path = urlparse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
123 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
124 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
124 return url_path
125 return url_path
125
126
126 def _get_request_headers(self, environ):
127 def _get_request_headers(self, environ):
127 headers = {}
128 headers = {}
128
129
129 for key in environ:
130 for key in environ:
130 if not key.startswith('HTTP_'):
131 if not key.startswith('HTTP_'):
131 continue
132 continue
132 new_key = key.split('_')
133 new_key = key.split('_')
133 new_key = [k.capitalize() for k in new_key[1:]]
134 new_key = [k.capitalize() for k in new_key[1:]]
134 new_key = '-'.join(new_key)
135 new_key = '-'.join(new_key)
135 headers[new_key] = environ[key]
136 headers[new_key] = environ[key]
136
137
137 if 'CONTENT_TYPE' in environ:
138 if 'CONTENT_TYPE' in environ:
138 headers['Content-Type'] = environ['CONTENT_TYPE']
139 headers['Content-Type'] = environ['CONTENT_TYPE']
139
140
140 if 'CONTENT_LENGTH' in environ:
141 if 'CONTENT_LENGTH' in environ:
141 headers['Content-Length'] = environ['CONTENT_LENGTH']
142 headers['Content-Length'] = environ['CONTENT_LENGTH']
142
143
143 return headers
144 return headers
144
145
145 def _get_response_headers(self, headers):
146 def _get_response_headers(self, headers):
146 headers = [
147 headers = [
147 (h, headers[h])
148 (h, headers[h])
148 for h in headers
149 for h in headers
149 if h.lower() not in self.IGNORED_HEADERS
150 if h.lower() not in self.IGNORED_HEADERS
150 ]
151 ]
151
152
152 return headers
153 return headers
153
154
154
155
155 class DisabledSimpleSvnApp(object):
156 class DisabledSimpleSvnApp(object):
156 def __init__(self, config):
157 def __init__(self, config):
157 self.config = config
158 self.config = config
158
159
159 def __call__(self, environ, start_response):
160 def __call__(self, environ, start_response):
160 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
161 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
161 log.warning(reason)
162 log.warning(reason)
162 return HTTPNotAcceptable(reason)(environ, start_response)
163 return HTTPNotAcceptable(reason)(environ, start_response)
163
164
164
165
165 class SimpleSvn(simplevcs.SimpleVCS):
166 class SimpleSvn(simplevcs.SimpleVCS):
166
167
167 SCM = 'svn'
168 SCM = 'svn'
168 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
169 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
169 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
170 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
170
171
171 def _get_repository_name(self, environ):
172 def _get_repository_name(self, environ):
172 """
173 """
173 Gets repository name out of PATH_INFO header
174 Gets repository name out of PATH_INFO header
174
175
175 :param environ: environ where PATH_INFO is stored
176 :param environ: environ where PATH_INFO is stored
176 """
177 """
177 path = environ['PATH_INFO'].split('!')
178 path = environ['PATH_INFO'].split('!')
178 repo_name = path[0].strip('/')
179 repo_name = path[0].strip('/')
179
180
180 # SVN includes the whole path in it's requests, including
181 # SVN includes the whole path in it's requests, including
181 # subdirectories inside the repo. Therefore we have to search for
182 # subdirectories inside the repo. Therefore we have to search for
182 # the repo root directory.
183 # the repo root directory.
183 if not is_valid_repo(
184 if not is_valid_repo(
184 repo_name, self.base_path, explicit_scm=self.SCM):
185 repo_name, self.base_path, explicit_scm=self.SCM):
185 current_path = ''
186 current_path = ''
186 for component in repo_name.split('/'):
187 for component in repo_name.split('/'):
187 current_path += component
188 current_path += component
188 if is_valid_repo(
189 if is_valid_repo(
189 current_path, self.base_path, explicit_scm=self.SCM):
190 current_path, self.base_path, explicit_scm=self.SCM):
190 return current_path
191 return current_path
191 current_path += '/'
192 current_path += '/'
192
193
193 return repo_name
194 return repo_name
194
195
195 def _get_action(self, environ):
196 def _get_action(self, environ):
196 return (
197 return (
197 'pull'
198 'pull'
198 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
199 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
199 else 'push')
200 else 'push')
200
201
201 def _should_use_callback_daemon(self, extras, environ, action):
202 def _should_use_callback_daemon(self, extras, environ, action):
202 # only MERGE command triggers hooks, so we don't want to start
203 # only MERGE command triggers hooks, so we don't want to start
203 # hooks server too many times. POST however starts the svn transaction
204 # hooks server too many times. POST however starts the svn transaction
204 # so we also need to run the init of callback daemon of POST
205 # so we also need to run the init of callback daemon of POST
205 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
206 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
206 return True
207 return True
207 return False
208 return False
208
209
209 def _create_wsgi_app(self, repo_path, repo_name, config):
210 def _create_wsgi_app(self, repo_path, repo_name, config):
210 if self._is_svn_enabled():
211 if self._is_svn_enabled():
211 return SimpleSvnApp(config)
212 return SimpleSvnApp(config)
212 # we don't have http proxy enabled return dummy request handler
213 # we don't have http proxy enabled return dummy request handler
213 return DisabledSimpleSvnApp(config)
214 return DisabledSimpleSvnApp(config)
214
215
215 def _is_svn_enabled(self):
216 def _is_svn_enabled(self):
216 conf = self.repo_vcs_config
217 conf = self.repo_vcs_config
217 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
218 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
218
219
219 def _create_config(self, extras, repo_name):
220 def _create_config(self, extras, repo_name):
220 conf = self.repo_vcs_config
221 conf = self.repo_vcs_config
221 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
222 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
222 server_url = server_url or self.DEFAULT_HTTP_SERVER
223 server_url = server_url or self.DEFAULT_HTTP_SERVER
223
224
224 extras['subversion_http_server_url'] = server_url
225 extras['subversion_http_server_url'] = server_url
225 return extras
226 return extras
@@ -1,1022 +1,1022 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 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 Some simple helper functions
23 Some simple helper functions
24 """
24 """
25
25
26 import collections
26 import collections
27 import datetime
27 import datetime
28 import dateutil.relativedelta
28 import dateutil.relativedelta
29 import hashlib
29 import hashlib
30 import logging
30 import logging
31 import re
31 import re
32 import sys
32 import sys
33 import time
33 import time
34 import urllib
34 import urllib
35 import urlobject
35 import urlobject
36 import uuid
36 import uuid
37 import getpass
37 import getpass
38
38
39 import pygments.lexers
39 import pygments.lexers
40 import sqlalchemy
40 import sqlalchemy
41 import sqlalchemy.engine.url
41 import sqlalchemy.engine.url
42 import sqlalchemy.exc
42 import sqlalchemy.exc
43 import sqlalchemy.sql
43 import sqlalchemy.sql
44 import webob
44 import webob
45 import pyramid.threadlocal
45 import pyramid.threadlocal
46 from pyramid.settings import asbool
46
47
47 import rhodecode
48 import rhodecode
48 from rhodecode.translation import _, _pluralize
49 from rhodecode.translation import _, _pluralize
49
50
50
51
51 def md5(s):
52 def md5(s):
52 return hashlib.md5(s).hexdigest()
53 return hashlib.md5(s).hexdigest()
53
54
54
55
55 def md5_safe(s):
56 def md5_safe(s):
56 return md5(safe_str(s))
57 return md5(safe_str(s))
57
58
58
59
59 def sha1(s):
60 def sha1(s):
60 return hashlib.sha1(s).hexdigest()
61 return hashlib.sha1(s).hexdigest()
61
62
62
63
63 def sha1_safe(s):
64 def sha1_safe(s):
64 return sha1(safe_str(s))
65 return sha1(safe_str(s))
65
66
66
67
67 def __get_lem(extra_mapping=None):
68 def __get_lem(extra_mapping=None):
68 """
69 """
69 Get language extension map based on what's inside pygments lexers
70 Get language extension map based on what's inside pygments lexers
70 """
71 """
71 d = collections.defaultdict(lambda: [])
72 d = collections.defaultdict(lambda: [])
72
73
73 def __clean(s):
74 def __clean(s):
74 s = s.lstrip('*')
75 s = s.lstrip('*')
75 s = s.lstrip('.')
76 s = s.lstrip('.')
76
77
77 if s.find('[') != -1:
78 if s.find('[') != -1:
78 exts = []
79 exts = []
79 start, stop = s.find('['), s.find(']')
80 start, stop = s.find('['), s.find(']')
80
81
81 for suffix in s[start + 1:stop]:
82 for suffix in s[start + 1:stop]:
82 exts.append(s[:s.find('[')] + suffix)
83 exts.append(s[:s.find('[')] + suffix)
83 return [e.lower() for e in exts]
84 return [e.lower() for e in exts]
84 else:
85 else:
85 return [s.lower()]
86 return [s.lower()]
86
87
87 for lx, t in sorted(pygments.lexers.LEXERS.items()):
88 for lx, t in sorted(pygments.lexers.LEXERS.items()):
88 m = map(__clean, t[-2])
89 m = map(__clean, t[-2])
89 if m:
90 if m:
90 m = reduce(lambda x, y: x + y, m)
91 m = reduce(lambda x, y: x + y, m)
91 for ext in m:
92 for ext in m:
92 desc = lx.replace('Lexer', '')
93 desc = lx.replace('Lexer', '')
93 d[ext].append(desc)
94 d[ext].append(desc)
94
95
95 data = dict(d)
96 data = dict(d)
96
97
97 extra_mapping = extra_mapping or {}
98 extra_mapping = extra_mapping or {}
98 if extra_mapping:
99 if extra_mapping:
99 for k, v in extra_mapping.items():
100 for k, v in extra_mapping.items():
100 if k not in data:
101 if k not in data:
101 # register new mapping2lexer
102 # register new mapping2lexer
102 data[k] = [v]
103 data[k] = [v]
103
104
104 return data
105 return data
105
106
106
107
107 def str2bool(_str):
108 def str2bool(_str):
108 """
109 """
109 returns True/False value from given string, it tries to translate the
110 returns True/False value from given string, it tries to translate the
110 string into boolean
111 string into boolean
111
112
112 :param _str: string value to translate into boolean
113 :param _str: string value to translate into boolean
113 :rtype: boolean
114 :rtype: boolean
114 :returns: boolean from given string
115 :returns: boolean from given string
115 """
116 """
116 if _str is None:
117 if _str is None:
117 return False
118 return False
118 if _str in (True, False):
119 if _str in (True, False):
119 return _str
120 return _str
120 _str = str(_str).strip().lower()
121 _str = str(_str).strip().lower()
121 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
122 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
122
123
123
124
124 def aslist(obj, sep=None, strip=True):
125 def aslist(obj, sep=None, strip=True):
125 """
126 """
126 Returns given string separated by sep as list
127 Returns given string separated by sep as list
127
128
128 :param obj:
129 :param obj:
129 :param sep:
130 :param sep:
130 :param strip:
131 :param strip:
131 """
132 """
132 if isinstance(obj, (basestring,)):
133 if isinstance(obj, (basestring,)):
133 lst = obj.split(sep)
134 lst = obj.split(sep)
134 if strip:
135 if strip:
135 lst = [v.strip() for v in lst]
136 lst = [v.strip() for v in lst]
136 return lst
137 return lst
137 elif isinstance(obj, (list, tuple)):
138 elif isinstance(obj, (list, tuple)):
138 return obj
139 return obj
139 elif obj is None:
140 elif obj is None:
140 return []
141 return []
141 else:
142 else:
142 return [obj]
143 return [obj]
143
144
144
145
145 def convert_line_endings(line, mode):
146 def convert_line_endings(line, mode):
146 """
147 """
147 Converts a given line "line end" accordingly to given mode
148 Converts a given line "line end" accordingly to given mode
148
149
149 Available modes are::
150 Available modes are::
150 0 - Unix
151 0 - Unix
151 1 - Mac
152 1 - Mac
152 2 - DOS
153 2 - DOS
153
154
154 :param line: given line to convert
155 :param line: given line to convert
155 :param mode: mode to convert to
156 :param mode: mode to convert to
156 :rtype: str
157 :rtype: str
157 :return: converted line according to mode
158 :return: converted line according to mode
158 """
159 """
159 if mode == 0:
160 if mode == 0:
160 line = line.replace('\r\n', '\n')
161 line = line.replace('\r\n', '\n')
161 line = line.replace('\r', '\n')
162 line = line.replace('\r', '\n')
162 elif mode == 1:
163 elif mode == 1:
163 line = line.replace('\r\n', '\r')
164 line = line.replace('\r\n', '\r')
164 line = line.replace('\n', '\r')
165 line = line.replace('\n', '\r')
165 elif mode == 2:
166 elif mode == 2:
166 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
167 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
167 return line
168 return line
168
169
169
170
170 def detect_mode(line, default):
171 def detect_mode(line, default):
171 """
172 """
172 Detects line break for given line, if line break couldn't be found
173 Detects line break for given line, if line break couldn't be found
173 given default value is returned
174 given default value is returned
174
175
175 :param line: str line
176 :param line: str line
176 :param default: default
177 :param default: default
177 :rtype: int
178 :rtype: int
178 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
179 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
179 """
180 """
180 if line.endswith('\r\n'):
181 if line.endswith('\r\n'):
181 return 2
182 return 2
182 elif line.endswith('\n'):
183 elif line.endswith('\n'):
183 return 0
184 return 0
184 elif line.endswith('\r'):
185 elif line.endswith('\r'):
185 return 1
186 return 1
186 else:
187 else:
187 return default
188 return default
188
189
189
190
190 def safe_int(val, default=None):
191 def safe_int(val, default=None):
191 """
192 """
192 Returns int() of val if val is not convertable to int use default
193 Returns int() of val if val is not convertable to int use default
193 instead
194 instead
194
195
195 :param val:
196 :param val:
196 :param default:
197 :param default:
197 """
198 """
198
199
199 try:
200 try:
200 val = int(val)
201 val = int(val)
201 except (ValueError, TypeError):
202 except (ValueError, TypeError):
202 val = default
203 val = default
203
204
204 return val
205 return val
205
206
206
207
207 def safe_unicode(str_, from_encoding=None):
208 def safe_unicode(str_, from_encoding=None):
208 """
209 """
209 safe unicode function. Does few trick to turn str_ into unicode
210 safe unicode function. Does few trick to turn str_ into unicode
210
211
211 In case of UnicodeDecode error, we try to return it with encoding detected
212 In case of UnicodeDecode error, we try to return it with encoding detected
212 by chardet library if it fails fallback to unicode with errors replaced
213 by chardet library if it fails fallback to unicode with errors replaced
213
214
214 :param str_: string to decode
215 :param str_: string to decode
215 :rtype: unicode
216 :rtype: unicode
216 :returns: unicode object
217 :returns: unicode object
217 """
218 """
218 if isinstance(str_, unicode):
219 if isinstance(str_, unicode):
219 return str_
220 return str_
220
221
221 if not from_encoding:
222 if not from_encoding:
222 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
223 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
223 'utf8'), sep=',')
224 'utf8'), sep=',')
224 from_encoding = DEFAULT_ENCODINGS
225 from_encoding = DEFAULT_ENCODINGS
225
226
226 if not isinstance(from_encoding, (list, tuple)):
227 if not isinstance(from_encoding, (list, tuple)):
227 from_encoding = [from_encoding]
228 from_encoding = [from_encoding]
228
229
229 try:
230 try:
230 return unicode(str_)
231 return unicode(str_)
231 except UnicodeDecodeError:
232 except UnicodeDecodeError:
232 pass
233 pass
233
234
234 for enc in from_encoding:
235 for enc in from_encoding:
235 try:
236 try:
236 return unicode(str_, enc)
237 return unicode(str_, enc)
237 except UnicodeDecodeError:
238 except UnicodeDecodeError:
238 pass
239 pass
239
240
240 try:
241 try:
241 import chardet
242 import chardet
242 encoding = chardet.detect(str_)['encoding']
243 encoding = chardet.detect(str_)['encoding']
243 if encoding is None:
244 if encoding is None:
244 raise Exception()
245 raise Exception()
245 return str_.decode(encoding)
246 return str_.decode(encoding)
246 except (ImportError, UnicodeDecodeError, Exception):
247 except (ImportError, UnicodeDecodeError, Exception):
247 return unicode(str_, from_encoding[0], 'replace')
248 return unicode(str_, from_encoding[0], 'replace')
248
249
249
250
250 def safe_str(unicode_, to_encoding=None):
251 def safe_str(unicode_, to_encoding=None):
251 """
252 """
252 safe str function. Does few trick to turn unicode_ into string
253 safe str function. Does few trick to turn unicode_ into string
253
254
254 In case of UnicodeEncodeError, we try to return it with encoding detected
255 In case of UnicodeEncodeError, we try to return it with encoding detected
255 by chardet library if it fails fallback to string with errors replaced
256 by chardet library if it fails fallback to string with errors replaced
256
257
257 :param unicode_: unicode to encode
258 :param unicode_: unicode to encode
258 :rtype: str
259 :rtype: str
259 :returns: str object
260 :returns: str object
260 """
261 """
261
262
262 # if it's not basestr cast to str
263 # if it's not basestr cast to str
263 if not isinstance(unicode_, basestring):
264 if not isinstance(unicode_, basestring):
264 return str(unicode_)
265 return str(unicode_)
265
266
266 if isinstance(unicode_, str):
267 if isinstance(unicode_, str):
267 return unicode_
268 return unicode_
268
269
269 if not to_encoding:
270 if not to_encoding:
270 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
271 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
271 'utf8'), sep=',')
272 'utf8'), sep=',')
272 to_encoding = DEFAULT_ENCODINGS
273 to_encoding = DEFAULT_ENCODINGS
273
274
274 if not isinstance(to_encoding, (list, tuple)):
275 if not isinstance(to_encoding, (list, tuple)):
275 to_encoding = [to_encoding]
276 to_encoding = [to_encoding]
276
277
277 for enc in to_encoding:
278 for enc in to_encoding:
278 try:
279 try:
279 return unicode_.encode(enc)
280 return unicode_.encode(enc)
280 except UnicodeEncodeError:
281 except UnicodeEncodeError:
281 pass
282 pass
282
283
283 try:
284 try:
284 import chardet
285 import chardet
285 encoding = chardet.detect(unicode_)['encoding']
286 encoding = chardet.detect(unicode_)['encoding']
286 if encoding is None:
287 if encoding is None:
287 raise UnicodeEncodeError()
288 raise UnicodeEncodeError()
288
289
289 return unicode_.encode(encoding)
290 return unicode_.encode(encoding)
290 except (ImportError, UnicodeEncodeError):
291 except (ImportError, UnicodeEncodeError):
291 return unicode_.encode(to_encoding[0], 'replace')
292 return unicode_.encode(to_encoding[0], 'replace')
292
293
293
294
294 def remove_suffix(s, suffix):
295 def remove_suffix(s, suffix):
295 if s.endswith(suffix):
296 if s.endswith(suffix):
296 s = s[:-1 * len(suffix)]
297 s = s[:-1 * len(suffix)]
297 return s
298 return s
298
299
299
300
300 def remove_prefix(s, prefix):
301 def remove_prefix(s, prefix):
301 if s.startswith(prefix):
302 if s.startswith(prefix):
302 s = s[len(prefix):]
303 s = s[len(prefix):]
303 return s
304 return s
304
305
305
306
306 def find_calling_context(ignore_modules=None):
307 def find_calling_context(ignore_modules=None):
307 """
308 """
308 Look through the calling stack and return the frame which called
309 Look through the calling stack and return the frame which called
309 this function and is part of core module ( ie. rhodecode.* )
310 this function and is part of core module ( ie. rhodecode.* )
310
311
311 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
312 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
312 """
313 """
313
314
314 ignore_modules = ignore_modules or []
315 ignore_modules = ignore_modules or []
315
316
316 f = sys._getframe(2)
317 f = sys._getframe(2)
317 while f.f_back is not None:
318 while f.f_back is not None:
318 name = f.f_globals.get('__name__')
319 name = f.f_globals.get('__name__')
319 if name and name.startswith(__name__.split('.')[0]):
320 if name and name.startswith(__name__.split('.')[0]):
320 if name not in ignore_modules:
321 if name not in ignore_modules:
321 return f
322 return f
322 f = f.f_back
323 f = f.f_back
323 return None
324 return None
324
325
325
326
326 def ping_connection(connection, branch):
327 def ping_connection(connection, branch):
327 if branch:
328 if branch:
328 # "branch" refers to a sub-connection of a connection,
329 # "branch" refers to a sub-connection of a connection,
329 # we don't want to bother pinging on these.
330 # we don't want to bother pinging on these.
330 return
331 return
331
332
332 # turn off "close with result". This flag is only used with
333 # turn off "close with result". This flag is only used with
333 # "connectionless" execution, otherwise will be False in any case
334 # "connectionless" execution, otherwise will be False in any case
334 save_should_close_with_result = connection.should_close_with_result
335 save_should_close_with_result = connection.should_close_with_result
335 connection.should_close_with_result = False
336 connection.should_close_with_result = False
336
337
337 try:
338 try:
338 # run a SELECT 1. use a core select() so that
339 # run a SELECT 1. use a core select() so that
339 # the SELECT of a scalar value without a table is
340 # the SELECT of a scalar value without a table is
340 # appropriately formatted for the backend
341 # appropriately formatted for the backend
341 connection.scalar(sqlalchemy.sql.select([1]))
342 connection.scalar(sqlalchemy.sql.select([1]))
342 except sqlalchemy.exc.DBAPIError as err:
343 except sqlalchemy.exc.DBAPIError as err:
343 # catch SQLAlchemy's DBAPIError, which is a wrapper
344 # catch SQLAlchemy's DBAPIError, which is a wrapper
344 # for the DBAPI's exception. It includes a .connection_invalidated
345 # for the DBAPI's exception. It includes a .connection_invalidated
345 # attribute which specifies if this connection is a "disconnect"
346 # attribute which specifies if this connection is a "disconnect"
346 # condition, which is based on inspection of the original exception
347 # condition, which is based on inspection of the original exception
347 # by the dialect in use.
348 # by the dialect in use.
348 if err.connection_invalidated:
349 if err.connection_invalidated:
349 # run the same SELECT again - the connection will re-validate
350 # run the same SELECT again - the connection will re-validate
350 # itself and establish a new connection. The disconnect detection
351 # itself and establish a new connection. The disconnect detection
351 # here also causes the whole connection pool to be invalidated
352 # here also causes the whole connection pool to be invalidated
352 # so that all stale connections are discarded.
353 # so that all stale connections are discarded.
353 connection.scalar(sqlalchemy.sql.select([1]))
354 connection.scalar(sqlalchemy.sql.select([1]))
354 else:
355 else:
355 raise
356 raise
356 finally:
357 finally:
357 # restore "close with result"
358 # restore "close with result"
358 connection.should_close_with_result = save_should_close_with_result
359 connection.should_close_with_result = save_should_close_with_result
359
360
360
361
361 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
362 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
362 """Custom engine_from_config functions."""
363 """Custom engine_from_config functions."""
363 log = logging.getLogger('sqlalchemy.engine')
364 log = logging.getLogger('sqlalchemy.engine')
364 _ping_connection = configuration.pop('sqlalchemy.db1.ping_connection', None)
365 use_ping_connection = asbool(configuration.pop('sqlalchemy.db1.ping_connection', None))
366 debug = asbool(configuration.get('debug'))
365
367
366 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
368 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
367
369
368 def color_sql(sql):
370 def color_sql(sql):
369 color_seq = '\033[1;33m' # This is yellow: code 33
371 color_seq = '\033[1;33m' # This is yellow: code 33
370 normal = '\x1b[0m'
372 normal = '\x1b[0m'
371 return ''.join([color_seq, sql, normal])
373 return ''.join([color_seq, sql, normal])
372
374
373 if configuration['debug'] or _ping_connection:
375 if use_ping_connection:
376 log.debug('Adding ping_connection on the engine config.')
374 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
377 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
375
378
376 if configuration['debug']:
379 if debug:
377 # attach events only for debug configuration
380 # attach events only for debug configuration
378
379 def before_cursor_execute(conn, cursor, statement,
381 def before_cursor_execute(conn, cursor, statement,
380 parameters, context, executemany):
382 parameters, context, executemany):
381 setattr(conn, 'query_start_time', time.time())
383 setattr(conn, 'query_start_time', time.time())
382 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
384 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
383 calling_context = find_calling_context(ignore_modules=[
385 calling_context = find_calling_context(ignore_modules=[
384 'rhodecode.lib.caching_query',
386 'rhodecode.lib.caching_query',
385 'rhodecode.model.settings',
387 'rhodecode.model.settings',
386 ])
388 ])
387 if calling_context:
389 if calling_context:
388 log.info(color_sql('call context %s:%s' % (
390 log.info(color_sql('call context %s:%s' % (
389 calling_context.f_code.co_filename,
391 calling_context.f_code.co_filename,
390 calling_context.f_lineno,
392 calling_context.f_lineno,
391 )))
393 )))
392
394
393 def after_cursor_execute(conn, cursor, statement,
395 def after_cursor_execute(conn, cursor, statement,
394 parameters, context, executemany):
396 parameters, context, executemany):
395 delattr(conn, 'query_start_time')
397 delattr(conn, 'query_start_time')
396
398
397 sqlalchemy.event.listen(engine, "before_cursor_execute",
399 sqlalchemy.event.listen(engine, "before_cursor_execute", before_cursor_execute)
398 before_cursor_execute)
400 sqlalchemy.event.listen(engine, "after_cursor_execute", after_cursor_execute)
399 sqlalchemy.event.listen(engine, "after_cursor_execute",
400 after_cursor_execute)
401
401
402 return engine
402 return engine
403
403
404
404
405 def get_encryption_key(config):
405 def get_encryption_key(config):
406 secret = config.get('rhodecode.encrypted_values.secret')
406 secret = config.get('rhodecode.encrypted_values.secret')
407 default = config['beaker.session.secret']
407 default = config['beaker.session.secret']
408 return secret or default
408 return secret or default
409
409
410
410
411 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
411 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
412 short_format=False):
412 short_format=False):
413 """
413 """
414 Turns a datetime into an age string.
414 Turns a datetime into an age string.
415 If show_short_version is True, this generates a shorter string with
415 If show_short_version is True, this generates a shorter string with
416 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
416 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
417
417
418 * IMPORTANT*
418 * IMPORTANT*
419 Code of this function is written in special way so it's easier to
419 Code of this function is written in special way so it's easier to
420 backport it to javascript. If you mean to update it, please also update
420 backport it to javascript. If you mean to update it, please also update
421 `jquery.timeago-extension.js` file
421 `jquery.timeago-extension.js` file
422
422
423 :param prevdate: datetime object
423 :param prevdate: datetime object
424 :param now: get current time, if not define we use
424 :param now: get current time, if not define we use
425 `datetime.datetime.now()`
425 `datetime.datetime.now()`
426 :param show_short_version: if it should approximate the date and
426 :param show_short_version: if it should approximate the date and
427 return a shorter string
427 return a shorter string
428 :param show_suffix:
428 :param show_suffix:
429 :param short_format: show short format, eg 2D instead of 2 days
429 :param short_format: show short format, eg 2D instead of 2 days
430 :rtype: unicode
430 :rtype: unicode
431 :returns: unicode words describing age
431 :returns: unicode words describing age
432 """
432 """
433
433
434 def _get_relative_delta(now, prevdate):
434 def _get_relative_delta(now, prevdate):
435 base = dateutil.relativedelta.relativedelta(now, prevdate)
435 base = dateutil.relativedelta.relativedelta(now, prevdate)
436 return {
436 return {
437 'year': base.years,
437 'year': base.years,
438 'month': base.months,
438 'month': base.months,
439 'day': base.days,
439 'day': base.days,
440 'hour': base.hours,
440 'hour': base.hours,
441 'minute': base.minutes,
441 'minute': base.minutes,
442 'second': base.seconds,
442 'second': base.seconds,
443 }
443 }
444
444
445 def _is_leap_year(year):
445 def _is_leap_year(year):
446 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
446 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
447
447
448 def get_month(prevdate):
448 def get_month(prevdate):
449 return prevdate.month
449 return prevdate.month
450
450
451 def get_year(prevdate):
451 def get_year(prevdate):
452 return prevdate.year
452 return prevdate.year
453
453
454 now = now or datetime.datetime.now()
454 now = now or datetime.datetime.now()
455 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
455 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
456 deltas = {}
456 deltas = {}
457 future = False
457 future = False
458
458
459 if prevdate > now:
459 if prevdate > now:
460 now_old = now
460 now_old = now
461 now = prevdate
461 now = prevdate
462 prevdate = now_old
462 prevdate = now_old
463 future = True
463 future = True
464 if future:
464 if future:
465 prevdate = prevdate.replace(microsecond=0)
465 prevdate = prevdate.replace(microsecond=0)
466 # Get date parts deltas
466 # Get date parts deltas
467 for part in order:
467 for part in order:
468 rel_delta = _get_relative_delta(now, prevdate)
468 rel_delta = _get_relative_delta(now, prevdate)
469 deltas[part] = rel_delta[part]
469 deltas[part] = rel_delta[part]
470
470
471 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
471 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
472 # not 1 hour, -59 minutes and -59 seconds)
472 # not 1 hour, -59 minutes and -59 seconds)
473 offsets = [[5, 60], [4, 60], [3, 24]]
473 offsets = [[5, 60], [4, 60], [3, 24]]
474 for element in offsets: # seconds, minutes, hours
474 for element in offsets: # seconds, minutes, hours
475 num = element[0]
475 num = element[0]
476 length = element[1]
476 length = element[1]
477
477
478 part = order[num]
478 part = order[num]
479 carry_part = order[num - 1]
479 carry_part = order[num - 1]
480
480
481 if deltas[part] < 0:
481 if deltas[part] < 0:
482 deltas[part] += length
482 deltas[part] += length
483 deltas[carry_part] -= 1
483 deltas[carry_part] -= 1
484
484
485 # Same thing for days except that the increment depends on the (variable)
485 # Same thing for days except that the increment depends on the (variable)
486 # number of days in the month
486 # number of days in the month
487 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
487 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
488 if deltas['day'] < 0:
488 if deltas['day'] < 0:
489 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
489 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
490 deltas['day'] += 29
490 deltas['day'] += 29
491 else:
491 else:
492 deltas['day'] += month_lengths[get_month(prevdate) - 1]
492 deltas['day'] += month_lengths[get_month(prevdate) - 1]
493
493
494 deltas['month'] -= 1
494 deltas['month'] -= 1
495
495
496 if deltas['month'] < 0:
496 if deltas['month'] < 0:
497 deltas['month'] += 12
497 deltas['month'] += 12
498 deltas['year'] -= 1
498 deltas['year'] -= 1
499
499
500 # Format the result
500 # Format the result
501 if short_format:
501 if short_format:
502 fmt_funcs = {
502 fmt_funcs = {
503 'year': lambda d: u'%dy' % d,
503 'year': lambda d: u'%dy' % d,
504 'month': lambda d: u'%dm' % d,
504 'month': lambda d: u'%dm' % d,
505 'day': lambda d: u'%dd' % d,
505 'day': lambda d: u'%dd' % d,
506 'hour': lambda d: u'%dh' % d,
506 'hour': lambda d: u'%dh' % d,
507 'minute': lambda d: u'%dmin' % d,
507 'minute': lambda d: u'%dmin' % d,
508 'second': lambda d: u'%dsec' % d,
508 'second': lambda d: u'%dsec' % d,
509 }
509 }
510 else:
510 else:
511 fmt_funcs = {
511 fmt_funcs = {
512 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
512 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
513 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
513 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
514 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
514 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
515 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
515 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
516 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
516 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
517 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
517 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
518 }
518 }
519
519
520 i = 0
520 i = 0
521 for part in order:
521 for part in order:
522 value = deltas[part]
522 value = deltas[part]
523 if value != 0:
523 if value != 0:
524
524
525 if i < 5:
525 if i < 5:
526 sub_part = order[i + 1]
526 sub_part = order[i + 1]
527 sub_value = deltas[sub_part]
527 sub_value = deltas[sub_part]
528 else:
528 else:
529 sub_value = 0
529 sub_value = 0
530
530
531 if sub_value == 0 or show_short_version:
531 if sub_value == 0 or show_short_version:
532 _val = fmt_funcs[part](value)
532 _val = fmt_funcs[part](value)
533 if future:
533 if future:
534 if show_suffix:
534 if show_suffix:
535 return _(u'in ${ago}', mapping={'ago': _val})
535 return _(u'in ${ago}', mapping={'ago': _val})
536 else:
536 else:
537 return _(_val)
537 return _(_val)
538
538
539 else:
539 else:
540 if show_suffix:
540 if show_suffix:
541 return _(u'${ago} ago', mapping={'ago': _val})
541 return _(u'${ago} ago', mapping={'ago': _val})
542 else:
542 else:
543 return _(_val)
543 return _(_val)
544
544
545 val = fmt_funcs[part](value)
545 val = fmt_funcs[part](value)
546 val_detail = fmt_funcs[sub_part](sub_value)
546 val_detail = fmt_funcs[sub_part](sub_value)
547 mapping = {'val': val, 'detail': val_detail}
547 mapping = {'val': val, 'detail': val_detail}
548
548
549 if short_format:
549 if short_format:
550 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
550 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
551 if show_suffix:
551 if show_suffix:
552 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
552 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
553 if future:
553 if future:
554 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
554 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
555 else:
555 else:
556 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
556 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
557 if show_suffix:
557 if show_suffix:
558 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
558 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
559 if future:
559 if future:
560 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
560 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
561
561
562 return datetime_tmpl
562 return datetime_tmpl
563 i += 1
563 i += 1
564 return _(u'just now')
564 return _(u'just now')
565
565
566
566
567 def cleaned_uri(uri):
567 def cleaned_uri(uri):
568 """
568 """
569 Quotes '[' and ']' from uri if there is only one of them.
569 Quotes '[' and ']' from uri if there is only one of them.
570 according to RFC3986 we cannot use such chars in uri
570 according to RFC3986 we cannot use such chars in uri
571 :param uri:
571 :param uri:
572 :return: uri without this chars
572 :return: uri without this chars
573 """
573 """
574 return urllib.quote(uri, safe='@$:/')
574 return urllib.quote(uri, safe='@$:/')
575
575
576
576
577 def uri_filter(uri):
577 def uri_filter(uri):
578 """
578 """
579 Removes user:password from given url string
579 Removes user:password from given url string
580
580
581 :param uri:
581 :param uri:
582 :rtype: unicode
582 :rtype: unicode
583 :returns: filtered list of strings
583 :returns: filtered list of strings
584 """
584 """
585 if not uri:
585 if not uri:
586 return ''
586 return ''
587
587
588 proto = ''
588 proto = ''
589
589
590 for pat in ('https://', 'http://'):
590 for pat in ('https://', 'http://'):
591 if uri.startswith(pat):
591 if uri.startswith(pat):
592 uri = uri[len(pat):]
592 uri = uri[len(pat):]
593 proto = pat
593 proto = pat
594 break
594 break
595
595
596 # remove passwords and username
596 # remove passwords and username
597 uri = uri[uri.find('@') + 1:]
597 uri = uri[uri.find('@') + 1:]
598
598
599 # get the port
599 # get the port
600 cred_pos = uri.find(':')
600 cred_pos = uri.find(':')
601 if cred_pos == -1:
601 if cred_pos == -1:
602 host, port = uri, None
602 host, port = uri, None
603 else:
603 else:
604 host, port = uri[:cred_pos], uri[cred_pos + 1:]
604 host, port = uri[:cred_pos], uri[cred_pos + 1:]
605
605
606 return filter(None, [proto, host, port])
606 return filter(None, [proto, host, port])
607
607
608
608
609 def credentials_filter(uri):
609 def credentials_filter(uri):
610 """
610 """
611 Returns a url with removed credentials
611 Returns a url with removed credentials
612
612
613 :param uri:
613 :param uri:
614 """
614 """
615
615
616 uri = uri_filter(uri)
616 uri = uri_filter(uri)
617 # check if we have port
617 # check if we have port
618 if len(uri) > 2 and uri[2]:
618 if len(uri) > 2 and uri[2]:
619 uri[2] = ':' + uri[2]
619 uri[2] = ':' + uri[2]
620
620
621 return ''.join(uri)
621 return ''.join(uri)
622
622
623
623
624 def get_clone_url(request, uri_tmpl, repo_name, repo_id, **override):
624 def get_clone_url(request, uri_tmpl, repo_name, repo_id, **override):
625 qualifed_home_url = request.route_url('home')
625 qualifed_home_url = request.route_url('home')
626 parsed_url = urlobject.URLObject(qualifed_home_url)
626 parsed_url = urlobject.URLObject(qualifed_home_url)
627 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
627 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
628
628
629 args = {
629 args = {
630 'scheme': parsed_url.scheme,
630 'scheme': parsed_url.scheme,
631 'user': '',
631 'user': '',
632 'sys_user': getpass.getuser(),
632 'sys_user': getpass.getuser(),
633 # path if we use proxy-prefix
633 # path if we use proxy-prefix
634 'netloc': parsed_url.netloc+decoded_path,
634 'netloc': parsed_url.netloc+decoded_path,
635 'hostname': parsed_url.hostname,
635 'hostname': parsed_url.hostname,
636 'prefix': decoded_path,
636 'prefix': decoded_path,
637 'repo': repo_name,
637 'repo': repo_name,
638 'repoid': str(repo_id)
638 'repoid': str(repo_id)
639 }
639 }
640 args.update(override)
640 args.update(override)
641 args['user'] = urllib.quote(safe_str(args['user']))
641 args['user'] = urllib.quote(safe_str(args['user']))
642
642
643 for k, v in args.items():
643 for k, v in args.items():
644 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
644 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
645
645
646 # remove leading @ sign if it's present. Case of empty user
646 # remove leading @ sign if it's present. Case of empty user
647 url_obj = urlobject.URLObject(uri_tmpl)
647 url_obj = urlobject.URLObject(uri_tmpl)
648 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
648 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
649
649
650 return safe_unicode(url)
650 return safe_unicode(url)
651
651
652
652
653 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None):
653 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None):
654 """
654 """
655 Safe version of get_commit if this commit doesn't exists for a
655 Safe version of get_commit if this commit doesn't exists for a
656 repository it returns a Dummy one instead
656 repository it returns a Dummy one instead
657
657
658 :param repo: repository instance
658 :param repo: repository instance
659 :param commit_id: commit id as str
659 :param commit_id: commit id as str
660 :param pre_load: optional list of commit attributes to load
660 :param pre_load: optional list of commit attributes to load
661 """
661 """
662 # TODO(skreft): remove these circular imports
662 # TODO(skreft): remove these circular imports
663 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
663 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
664 from rhodecode.lib.vcs.exceptions import RepositoryError
664 from rhodecode.lib.vcs.exceptions import RepositoryError
665 if not isinstance(repo, BaseRepository):
665 if not isinstance(repo, BaseRepository):
666 raise Exception('You must pass an Repository '
666 raise Exception('You must pass an Repository '
667 'object as first argument got %s', type(repo))
667 'object as first argument got %s', type(repo))
668
668
669 try:
669 try:
670 commit = repo.get_commit(
670 commit = repo.get_commit(
671 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
671 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
672 except (RepositoryError, LookupError):
672 except (RepositoryError, LookupError):
673 commit = EmptyCommit()
673 commit = EmptyCommit()
674 return commit
674 return commit
675
675
676
676
677 def datetime_to_time(dt):
677 def datetime_to_time(dt):
678 if dt:
678 if dt:
679 return time.mktime(dt.timetuple())
679 return time.mktime(dt.timetuple())
680
680
681
681
682 def time_to_datetime(tm):
682 def time_to_datetime(tm):
683 if tm:
683 if tm:
684 if isinstance(tm, basestring):
684 if isinstance(tm, basestring):
685 try:
685 try:
686 tm = float(tm)
686 tm = float(tm)
687 except ValueError:
687 except ValueError:
688 return
688 return
689 return datetime.datetime.fromtimestamp(tm)
689 return datetime.datetime.fromtimestamp(tm)
690
690
691
691
692 def time_to_utcdatetime(tm):
692 def time_to_utcdatetime(tm):
693 if tm:
693 if tm:
694 if isinstance(tm, basestring):
694 if isinstance(tm, basestring):
695 try:
695 try:
696 tm = float(tm)
696 tm = float(tm)
697 except ValueError:
697 except ValueError:
698 return
698 return
699 return datetime.datetime.utcfromtimestamp(tm)
699 return datetime.datetime.utcfromtimestamp(tm)
700
700
701
701
702 MENTIONS_REGEX = re.compile(
702 MENTIONS_REGEX = re.compile(
703 # ^@ or @ without any special chars in front
703 # ^@ or @ without any special chars in front
704 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
704 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
705 # main body starts with letter, then can be . - _
705 # main body starts with letter, then can be . - _
706 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
706 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
707 re.VERBOSE | re.MULTILINE)
707 re.VERBOSE | re.MULTILINE)
708
708
709
709
710 def extract_mentioned_users(s):
710 def extract_mentioned_users(s):
711 """
711 """
712 Returns unique usernames from given string s that have @mention
712 Returns unique usernames from given string s that have @mention
713
713
714 :param s: string to get mentions
714 :param s: string to get mentions
715 """
715 """
716 usrs = set()
716 usrs = set()
717 for username in MENTIONS_REGEX.findall(s):
717 for username in MENTIONS_REGEX.findall(s):
718 usrs.add(username)
718 usrs.add(username)
719
719
720 return sorted(list(usrs), key=lambda k: k.lower())
720 return sorted(list(usrs), key=lambda k: k.lower())
721
721
722
722
723 class AttributeDictBase(dict):
723 class AttributeDictBase(dict):
724 def __getstate__(self):
724 def __getstate__(self):
725 odict = self.__dict__ # get attribute dictionary
725 odict = self.__dict__ # get attribute dictionary
726 return odict
726 return odict
727
727
728 def __setstate__(self, dict):
728 def __setstate__(self, dict):
729 self.__dict__ = dict
729 self.__dict__ = dict
730
730
731 __setattr__ = dict.__setitem__
731 __setattr__ = dict.__setitem__
732 __delattr__ = dict.__delitem__
732 __delattr__ = dict.__delitem__
733
733
734
734
735 class StrictAttributeDict(AttributeDictBase):
735 class StrictAttributeDict(AttributeDictBase):
736 """
736 """
737 Strict Version of Attribute dict which raises an Attribute error when
737 Strict Version of Attribute dict which raises an Attribute error when
738 requested attribute is not set
738 requested attribute is not set
739 """
739 """
740 def __getattr__(self, attr):
740 def __getattr__(self, attr):
741 try:
741 try:
742 return self[attr]
742 return self[attr]
743 except KeyError:
743 except KeyError:
744 raise AttributeError('%s object has no attribute %s' % (
744 raise AttributeError('%s object has no attribute %s' % (
745 self.__class__, attr))
745 self.__class__, attr))
746
746
747
747
748 class AttributeDict(AttributeDictBase):
748 class AttributeDict(AttributeDictBase):
749 def __getattr__(self, attr):
749 def __getattr__(self, attr):
750 return self.get(attr, None)
750 return self.get(attr, None)
751
751
752
752
753
753
754 class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict):
754 class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict):
755 def __init__(self, default_factory=None, *args, **kwargs):
755 def __init__(self, default_factory=None, *args, **kwargs):
756 # in python3 you can omit the args to super
756 # in python3 you can omit the args to super
757 super(OrderedDefaultDict, self).__init__(*args, **kwargs)
757 super(OrderedDefaultDict, self).__init__(*args, **kwargs)
758 self.default_factory = default_factory
758 self.default_factory = default_factory
759
759
760
760
761 def fix_PATH(os_=None):
761 def fix_PATH(os_=None):
762 """
762 """
763 Get current active python path, and append it to PATH variable to fix
763 Get current active python path, and append it to PATH variable to fix
764 issues of subprocess calls and different python versions
764 issues of subprocess calls and different python versions
765 """
765 """
766 if os_ is None:
766 if os_ is None:
767 import os
767 import os
768 else:
768 else:
769 os = os_
769 os = os_
770
770
771 cur_path = os.path.split(sys.executable)[0]
771 cur_path = os.path.split(sys.executable)[0]
772 if not os.environ['PATH'].startswith(cur_path):
772 if not os.environ['PATH'].startswith(cur_path):
773 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
773 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
774
774
775
775
776 def obfuscate_url_pw(engine):
776 def obfuscate_url_pw(engine):
777 _url = engine or ''
777 _url = engine or ''
778 try:
778 try:
779 _url = sqlalchemy.engine.url.make_url(engine)
779 _url = sqlalchemy.engine.url.make_url(engine)
780 if _url.password:
780 if _url.password:
781 _url.password = 'XXXXX'
781 _url.password = 'XXXXX'
782 except Exception:
782 except Exception:
783 pass
783 pass
784 return unicode(_url)
784 return unicode(_url)
785
785
786
786
787 def get_server_url(environ):
787 def get_server_url(environ):
788 req = webob.Request(environ)
788 req = webob.Request(environ)
789 return req.host_url + req.script_name
789 return req.host_url + req.script_name
790
790
791
791
792 def unique_id(hexlen=32):
792 def unique_id(hexlen=32):
793 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
793 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
794 return suuid(truncate_to=hexlen, alphabet=alphabet)
794 return suuid(truncate_to=hexlen, alphabet=alphabet)
795
795
796
796
797 def suuid(url=None, truncate_to=22, alphabet=None):
797 def suuid(url=None, truncate_to=22, alphabet=None):
798 """
798 """
799 Generate and return a short URL safe UUID.
799 Generate and return a short URL safe UUID.
800
800
801 If the url parameter is provided, set the namespace to the provided
801 If the url parameter is provided, set the namespace to the provided
802 URL and generate a UUID.
802 URL and generate a UUID.
803
803
804 :param url to get the uuid for
804 :param url to get the uuid for
805 :truncate_to: truncate the basic 22 UUID to shorter version
805 :truncate_to: truncate the basic 22 UUID to shorter version
806
806
807 The IDs won't be universally unique any longer, but the probability of
807 The IDs won't be universally unique any longer, but the probability of
808 a collision will still be very low.
808 a collision will still be very low.
809 """
809 """
810 # Define our alphabet.
810 # Define our alphabet.
811 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
811 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
812
812
813 # If no URL is given, generate a random UUID.
813 # If no URL is given, generate a random UUID.
814 if url is None:
814 if url is None:
815 unique_id = uuid.uuid4().int
815 unique_id = uuid.uuid4().int
816 else:
816 else:
817 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
817 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
818
818
819 alphabet_length = len(_ALPHABET)
819 alphabet_length = len(_ALPHABET)
820 output = []
820 output = []
821 while unique_id > 0:
821 while unique_id > 0:
822 digit = unique_id % alphabet_length
822 digit = unique_id % alphabet_length
823 output.append(_ALPHABET[digit])
823 output.append(_ALPHABET[digit])
824 unique_id = int(unique_id / alphabet_length)
824 unique_id = int(unique_id / alphabet_length)
825 return "".join(output)[:truncate_to]
825 return "".join(output)[:truncate_to]
826
826
827
827
828 def get_current_rhodecode_user(request=None):
828 def get_current_rhodecode_user(request=None):
829 """
829 """
830 Gets rhodecode user from request
830 Gets rhodecode user from request
831 """
831 """
832 pyramid_request = request or pyramid.threadlocal.get_current_request()
832 pyramid_request = request or pyramid.threadlocal.get_current_request()
833
833
834 # web case
834 # web case
835 if pyramid_request and hasattr(pyramid_request, 'user'):
835 if pyramid_request and hasattr(pyramid_request, 'user'):
836 return pyramid_request.user
836 return pyramid_request.user
837
837
838 # api case
838 # api case
839 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
839 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
840 return pyramid_request.rpc_user
840 return pyramid_request.rpc_user
841
841
842 return None
842 return None
843
843
844
844
845 def action_logger_generic(action, namespace=''):
845 def action_logger_generic(action, namespace=''):
846 """
846 """
847 A generic logger for actions useful to the system overview, tries to find
847 A generic logger for actions useful to the system overview, tries to find
848 an acting user for the context of the call otherwise reports unknown user
848 an acting user for the context of the call otherwise reports unknown user
849
849
850 :param action: logging message eg 'comment 5 deleted'
850 :param action: logging message eg 'comment 5 deleted'
851 :param type: string
851 :param type: string
852
852
853 :param namespace: namespace of the logging message eg. 'repo.comments'
853 :param namespace: namespace of the logging message eg. 'repo.comments'
854 :param type: string
854 :param type: string
855
855
856 """
856 """
857
857
858 logger_name = 'rhodecode.actions'
858 logger_name = 'rhodecode.actions'
859
859
860 if namespace:
860 if namespace:
861 logger_name += '.' + namespace
861 logger_name += '.' + namespace
862
862
863 log = logging.getLogger(logger_name)
863 log = logging.getLogger(logger_name)
864
864
865 # get a user if we can
865 # get a user if we can
866 user = get_current_rhodecode_user()
866 user = get_current_rhodecode_user()
867
867
868 logfunc = log.info
868 logfunc = log.info
869
869
870 if not user:
870 if not user:
871 user = '<unknown user>'
871 user = '<unknown user>'
872 logfunc = log.warning
872 logfunc = log.warning
873
873
874 logfunc('Logging action by {}: {}'.format(user, action))
874 logfunc('Logging action by {}: {}'.format(user, action))
875
875
876
876
877 def escape_split(text, sep=',', maxsplit=-1):
877 def escape_split(text, sep=',', maxsplit=-1):
878 r"""
878 r"""
879 Allows for escaping of the separator: e.g. arg='foo\, bar'
879 Allows for escaping of the separator: e.g. arg='foo\, bar'
880
880
881 It should be noted that the way bash et. al. do command line parsing, those
881 It should be noted that the way bash et. al. do command line parsing, those
882 single quotes are required.
882 single quotes are required.
883 """
883 """
884 escaped_sep = r'\%s' % sep
884 escaped_sep = r'\%s' % sep
885
885
886 if escaped_sep not in text:
886 if escaped_sep not in text:
887 return text.split(sep, maxsplit)
887 return text.split(sep, maxsplit)
888
888
889 before, _mid, after = text.partition(escaped_sep)
889 before, _mid, after = text.partition(escaped_sep)
890 startlist = before.split(sep, maxsplit) # a regular split is fine here
890 startlist = before.split(sep, maxsplit) # a regular split is fine here
891 unfinished = startlist[-1]
891 unfinished = startlist[-1]
892 startlist = startlist[:-1]
892 startlist = startlist[:-1]
893
893
894 # recurse because there may be more escaped separators
894 # recurse because there may be more escaped separators
895 endlist = escape_split(after, sep, maxsplit)
895 endlist = escape_split(after, sep, maxsplit)
896
896
897 # finish building the escaped value. we use endlist[0] becaue the first
897 # finish building the escaped value. we use endlist[0] becaue the first
898 # part of the string sent in recursion is the rest of the escaped value.
898 # part of the string sent in recursion is the rest of the escaped value.
899 unfinished += sep + endlist[0]
899 unfinished += sep + endlist[0]
900
900
901 return startlist + [unfinished] + endlist[1:] # put together all the parts
901 return startlist + [unfinished] + endlist[1:] # put together all the parts
902
902
903
903
904 class OptionalAttr(object):
904 class OptionalAttr(object):
905 """
905 """
906 Special Optional Option that defines other attribute. Example::
906 Special Optional Option that defines other attribute. Example::
907
907
908 def test(apiuser, userid=Optional(OAttr('apiuser')):
908 def test(apiuser, userid=Optional(OAttr('apiuser')):
909 user = Optional.extract(userid)
909 user = Optional.extract(userid)
910 # calls
910 # calls
911
911
912 """
912 """
913
913
914 def __init__(self, attr_name):
914 def __init__(self, attr_name):
915 self.attr_name = attr_name
915 self.attr_name = attr_name
916
916
917 def __repr__(self):
917 def __repr__(self):
918 return '<OptionalAttr:%s>' % self.attr_name
918 return '<OptionalAttr:%s>' % self.attr_name
919
919
920 def __call__(self):
920 def __call__(self):
921 return self
921 return self
922
922
923
923
924 # alias
924 # alias
925 OAttr = OptionalAttr
925 OAttr = OptionalAttr
926
926
927
927
928 class Optional(object):
928 class Optional(object):
929 """
929 """
930 Defines an optional parameter::
930 Defines an optional parameter::
931
931
932 param = param.getval() if isinstance(param, Optional) else param
932 param = param.getval() if isinstance(param, Optional) else param
933 param = param() if isinstance(param, Optional) else param
933 param = param() if isinstance(param, Optional) else param
934
934
935 is equivalent of::
935 is equivalent of::
936
936
937 param = Optional.extract(param)
937 param = Optional.extract(param)
938
938
939 """
939 """
940
940
941 def __init__(self, type_):
941 def __init__(self, type_):
942 self.type_ = type_
942 self.type_ = type_
943
943
944 def __repr__(self):
944 def __repr__(self):
945 return '<Optional:%s>' % self.type_.__repr__()
945 return '<Optional:%s>' % self.type_.__repr__()
946
946
947 def __call__(self):
947 def __call__(self):
948 return self.getval()
948 return self.getval()
949
949
950 def getval(self):
950 def getval(self):
951 """
951 """
952 returns value from this Optional instance
952 returns value from this Optional instance
953 """
953 """
954 if isinstance(self.type_, OAttr):
954 if isinstance(self.type_, OAttr):
955 # use params name
955 # use params name
956 return self.type_.attr_name
956 return self.type_.attr_name
957 return self.type_
957 return self.type_
958
958
959 @classmethod
959 @classmethod
960 def extract(cls, val):
960 def extract(cls, val):
961 """
961 """
962 Extracts value from Optional() instance
962 Extracts value from Optional() instance
963
963
964 :param val:
964 :param val:
965 :return: original value if it's not Optional instance else
965 :return: original value if it's not Optional instance else
966 value of instance
966 value of instance
967 """
967 """
968 if isinstance(val, cls):
968 if isinstance(val, cls):
969 return val.getval()
969 return val.getval()
970 return val
970 return val
971
971
972
972
973 def glob2re(pat):
973 def glob2re(pat):
974 """
974 """
975 Translate a shell PATTERN to a regular expression.
975 Translate a shell PATTERN to a regular expression.
976
976
977 There is no way to quote meta-characters.
977 There is no way to quote meta-characters.
978 """
978 """
979
979
980 i, n = 0, len(pat)
980 i, n = 0, len(pat)
981 res = ''
981 res = ''
982 while i < n:
982 while i < n:
983 c = pat[i]
983 c = pat[i]
984 i = i+1
984 i = i+1
985 if c == '*':
985 if c == '*':
986 #res = res + '.*'
986 #res = res + '.*'
987 res = res + '[^/]*'
987 res = res + '[^/]*'
988 elif c == '?':
988 elif c == '?':
989 #res = res + '.'
989 #res = res + '.'
990 res = res + '[^/]'
990 res = res + '[^/]'
991 elif c == '[':
991 elif c == '[':
992 j = i
992 j = i
993 if j < n and pat[j] == '!':
993 if j < n and pat[j] == '!':
994 j = j+1
994 j = j+1
995 if j < n and pat[j] == ']':
995 if j < n and pat[j] == ']':
996 j = j+1
996 j = j+1
997 while j < n and pat[j] != ']':
997 while j < n and pat[j] != ']':
998 j = j+1
998 j = j+1
999 if j >= n:
999 if j >= n:
1000 res = res + '\\['
1000 res = res + '\\['
1001 else:
1001 else:
1002 stuff = pat[i:j].replace('\\','\\\\')
1002 stuff = pat[i:j].replace('\\','\\\\')
1003 i = j+1
1003 i = j+1
1004 if stuff[0] == '!':
1004 if stuff[0] == '!':
1005 stuff = '^' + stuff[1:]
1005 stuff = '^' + stuff[1:]
1006 elif stuff[0] == '^':
1006 elif stuff[0] == '^':
1007 stuff = '\\' + stuff
1007 stuff = '\\' + stuff
1008 res = '%s[%s]' % (res, stuff)
1008 res = '%s[%s]' % (res, stuff)
1009 else:
1009 else:
1010 res = res + re.escape(c)
1010 res = res + re.escape(c)
1011 return res + '\Z(?ms)'
1011 return res + '\Z(?ms)'
1012
1012
1013
1013
1014 def parse_byte_string(size_str):
1014 def parse_byte_string(size_str):
1015 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1015 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1016 if not match:
1016 if not match:
1017 raise ValueError('Given size:%s is invalid, please make sure '
1017 raise ValueError('Given size:%s is invalid, please make sure '
1018 'to use format of <num>(MB|KB)' % size_str)
1018 'to use format of <num>(MB|KB)' % size_str)
1019
1019
1020 _parts = match.groups()
1020 _parts = match.groups()
1021 num, type_ = _parts
1021 num, type_ = _parts
1022 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
1022 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
@@ -1,1694 +1,1696 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 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.threadlocal import get_current_request
33 from pyramid.threadlocal import get_current_request
34
34
35 from rhodecode import events
35 from rhodecode import events
36 from rhodecode.translation import lazy_ugettext
36 from rhodecode.translation import lazy_ugettext
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
37 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.markup_renderer import (
41 from rhodecode.lib.markup_renderer import (
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
43 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.vcs.backends.base import (
44 from rhodecode.lib.vcs.backends.base import (
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
45 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
46 from rhodecode.lib.vcs.conf import settings as vcs_settings
47 from rhodecode.lib.vcs.exceptions import (
47 from rhodecode.lib.vcs.exceptions import (
48 CommitDoesNotExistError, EmptyRepositoryError)
48 CommitDoesNotExistError, EmptyRepositoryError)
49 from rhodecode.model import BaseModel
49 from rhodecode.model import BaseModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.db import (
52 from rhodecode.model.db import (
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
53 or_, PullRequest, PullRequestReviewers, ChangesetStatus,
54 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
54 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule)
55 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
56 from rhodecode.model.notification import NotificationModel, \
56 from rhodecode.model.notification import NotificationModel, \
57 EmailNotificationModel
57 EmailNotificationModel
58 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.settings import VcsSettingsModel
59 from rhodecode.model.settings import VcsSettingsModel
60
60
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 # Data structure to hold the response data when updating commits during a pull
65 # Data structure to hold the response data when updating commits during a pull
66 # request update.
66 # request update.
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
67 UpdateResponse = collections.namedtuple('UpdateResponse', [
68 'executed', 'reason', 'new', 'old', 'changes',
68 'executed', 'reason', 'new', 'old', 'changes',
69 'source_changed', 'target_changed'])
69 'source_changed', 'target_changed'])
70
70
71
71
72 class PullRequestModel(BaseModel):
72 class PullRequestModel(BaseModel):
73
73
74 cls = PullRequest
74 cls = PullRequest
75
75
76 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
76 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
77
77
78 UPDATE_STATUS_MESSAGES = {
78 UPDATE_STATUS_MESSAGES = {
79 UpdateFailureReason.NONE: lazy_ugettext(
79 UpdateFailureReason.NONE: lazy_ugettext(
80 'Pull request update successful.'),
80 'Pull request update successful.'),
81 UpdateFailureReason.UNKNOWN: lazy_ugettext(
81 UpdateFailureReason.UNKNOWN: lazy_ugettext(
82 'Pull request update failed because of an unknown error.'),
82 'Pull request update failed because of an unknown error.'),
83 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
83 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
84 'No update needed because the source and target have not changed.'),
84 'No update needed because the source and target have not changed.'),
85 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
85 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
86 'Pull request cannot be updated because the reference type is '
86 'Pull request cannot be updated because the reference type is '
87 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
87 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
88 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
88 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
89 'This pull request cannot be updated because the target '
89 'This pull request cannot be updated because the target '
90 'reference is missing.'),
90 'reference is missing.'),
91 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
91 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
92 'This pull request cannot be updated because the source '
92 'This pull request cannot be updated because the source '
93 'reference is missing.'),
93 'reference is missing.'),
94 }
94 }
95 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
96 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
95
97
96 def __get_pull_request(self, pull_request):
98 def __get_pull_request(self, pull_request):
97 return self._get_instance((
99 return self._get_instance((
98 PullRequest, PullRequestVersion), pull_request)
100 PullRequest, PullRequestVersion), pull_request)
99
101
100 def _check_perms(self, perms, pull_request, user, api=False):
102 def _check_perms(self, perms, pull_request, user, api=False):
101 if not api:
103 if not api:
102 return h.HasRepoPermissionAny(*perms)(
104 return h.HasRepoPermissionAny(*perms)(
103 user=user, repo_name=pull_request.target_repo.repo_name)
105 user=user, repo_name=pull_request.target_repo.repo_name)
104 else:
106 else:
105 return h.HasRepoPermissionAnyApi(*perms)(
107 return h.HasRepoPermissionAnyApi(*perms)(
106 user=user, repo_name=pull_request.target_repo.repo_name)
108 user=user, repo_name=pull_request.target_repo.repo_name)
107
109
108 def check_user_read(self, pull_request, user, api=False):
110 def check_user_read(self, pull_request, user, api=False):
109 _perms = ('repository.admin', 'repository.write', 'repository.read',)
111 _perms = ('repository.admin', 'repository.write', 'repository.read',)
110 return self._check_perms(_perms, pull_request, user, api)
112 return self._check_perms(_perms, pull_request, user, api)
111
113
112 def check_user_merge(self, pull_request, user, api=False):
114 def check_user_merge(self, pull_request, user, api=False):
113 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
115 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
114 return self._check_perms(_perms, pull_request, user, api)
116 return self._check_perms(_perms, pull_request, user, api)
115
117
116 def check_user_update(self, pull_request, user, api=False):
118 def check_user_update(self, pull_request, user, api=False):
117 owner = user.user_id == pull_request.user_id
119 owner = user.user_id == pull_request.user_id
118 return self.check_user_merge(pull_request, user, api) or owner
120 return self.check_user_merge(pull_request, user, api) or owner
119
121
120 def check_user_delete(self, pull_request, user):
122 def check_user_delete(self, pull_request, user):
121 owner = user.user_id == pull_request.user_id
123 owner = user.user_id == pull_request.user_id
122 _perms = ('repository.admin',)
124 _perms = ('repository.admin',)
123 return self._check_perms(_perms, pull_request, user) or owner
125 return self._check_perms(_perms, pull_request, user) or owner
124
126
125 def check_user_change_status(self, pull_request, user, api=False):
127 def check_user_change_status(self, pull_request, user, api=False):
126 reviewer = user.user_id in [x.user_id for x in
128 reviewer = user.user_id in [x.user_id for x in
127 pull_request.reviewers]
129 pull_request.reviewers]
128 return self.check_user_update(pull_request, user, api) or reviewer
130 return self.check_user_update(pull_request, user, api) or reviewer
129
131
130 def check_user_comment(self, pull_request, user):
132 def check_user_comment(self, pull_request, user):
131 owner = user.user_id == pull_request.user_id
133 owner = user.user_id == pull_request.user_id
132 return self.check_user_read(pull_request, user) or owner
134 return self.check_user_read(pull_request, user) or owner
133
135
134 def get(self, pull_request):
136 def get(self, pull_request):
135 return self.__get_pull_request(pull_request)
137 return self.__get_pull_request(pull_request)
136
138
137 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
139 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
138 opened_by=None, order_by=None,
140 opened_by=None, order_by=None,
139 order_dir='desc'):
141 order_dir='desc'):
140 repo = None
142 repo = None
141 if repo_name:
143 if repo_name:
142 repo = self._get_repo(repo_name)
144 repo = self._get_repo(repo_name)
143
145
144 q = PullRequest.query()
146 q = PullRequest.query()
145
147
146 # source or target
148 # source or target
147 if repo and source:
149 if repo and source:
148 q = q.filter(PullRequest.source_repo == repo)
150 q = q.filter(PullRequest.source_repo == repo)
149 elif repo:
151 elif repo:
150 q = q.filter(PullRequest.target_repo == repo)
152 q = q.filter(PullRequest.target_repo == repo)
151
153
152 # closed,opened
154 # closed,opened
153 if statuses:
155 if statuses:
154 q = q.filter(PullRequest.status.in_(statuses))
156 q = q.filter(PullRequest.status.in_(statuses))
155
157
156 # opened by filter
158 # opened by filter
157 if opened_by:
159 if opened_by:
158 q = q.filter(PullRequest.user_id.in_(opened_by))
160 q = q.filter(PullRequest.user_id.in_(opened_by))
159
161
160 if order_by:
162 if order_by:
161 order_map = {
163 order_map = {
162 'name_raw': PullRequest.pull_request_id,
164 'name_raw': PullRequest.pull_request_id,
163 'title': PullRequest.title,
165 'title': PullRequest.title,
164 'updated_on_raw': PullRequest.updated_on,
166 'updated_on_raw': PullRequest.updated_on,
165 'target_repo': PullRequest.target_repo_id
167 'target_repo': PullRequest.target_repo_id
166 }
168 }
167 if order_dir == 'asc':
169 if order_dir == 'asc':
168 q = q.order_by(order_map[order_by].asc())
170 q = q.order_by(order_map[order_by].asc())
169 else:
171 else:
170 q = q.order_by(order_map[order_by].desc())
172 q = q.order_by(order_map[order_by].desc())
171
173
172 return q
174 return q
173
175
174 def count_all(self, repo_name, source=False, statuses=None,
176 def count_all(self, repo_name, source=False, statuses=None,
175 opened_by=None):
177 opened_by=None):
176 """
178 """
177 Count the number of pull requests for a specific repository.
179 Count the number of pull requests for a specific repository.
178
180
179 :param repo_name: target or source repo
181 :param repo_name: target or source repo
180 :param source: boolean flag to specify if repo_name refers to source
182 :param source: boolean flag to specify if repo_name refers to source
181 :param statuses: list of pull request statuses
183 :param statuses: list of pull request statuses
182 :param opened_by: author user of the pull request
184 :param opened_by: author user of the pull request
183 :returns: int number of pull requests
185 :returns: int number of pull requests
184 """
186 """
185 q = self._prepare_get_all_query(
187 q = self._prepare_get_all_query(
186 repo_name, source=source, statuses=statuses, opened_by=opened_by)
188 repo_name, source=source, statuses=statuses, opened_by=opened_by)
187
189
188 return q.count()
190 return q.count()
189
191
190 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
192 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
191 offset=0, length=None, order_by=None, order_dir='desc'):
193 offset=0, length=None, order_by=None, order_dir='desc'):
192 """
194 """
193 Get all pull requests for a specific repository.
195 Get all pull requests for a specific repository.
194
196
195 :param repo_name: target or source repo
197 :param repo_name: target or source repo
196 :param source: boolean flag to specify if repo_name refers to source
198 :param source: boolean flag to specify if repo_name refers to source
197 :param statuses: list of pull request statuses
199 :param statuses: list of pull request statuses
198 :param opened_by: author user of the pull request
200 :param opened_by: author user of the pull request
199 :param offset: pagination offset
201 :param offset: pagination offset
200 :param length: length of returned list
202 :param length: length of returned list
201 :param order_by: order of the returned list
203 :param order_by: order of the returned list
202 :param order_dir: 'asc' or 'desc' ordering direction
204 :param order_dir: 'asc' or 'desc' ordering direction
203 :returns: list of pull requests
205 :returns: list of pull requests
204 """
206 """
205 q = self._prepare_get_all_query(
207 q = self._prepare_get_all_query(
206 repo_name, source=source, statuses=statuses, opened_by=opened_by,
208 repo_name, source=source, statuses=statuses, opened_by=opened_by,
207 order_by=order_by, order_dir=order_dir)
209 order_by=order_by, order_dir=order_dir)
208
210
209 if length:
211 if length:
210 pull_requests = q.limit(length).offset(offset).all()
212 pull_requests = q.limit(length).offset(offset).all()
211 else:
213 else:
212 pull_requests = q.all()
214 pull_requests = q.all()
213
215
214 return pull_requests
216 return pull_requests
215
217
216 def count_awaiting_review(self, repo_name, source=False, statuses=None,
218 def count_awaiting_review(self, repo_name, source=False, statuses=None,
217 opened_by=None):
219 opened_by=None):
218 """
220 """
219 Count the number of pull requests for a specific repository that are
221 Count the number of pull requests for a specific repository that are
220 awaiting review.
222 awaiting review.
221
223
222 :param repo_name: target or source repo
224 :param repo_name: target or source repo
223 :param source: boolean flag to specify if repo_name refers to source
225 :param source: boolean flag to specify if repo_name refers to source
224 :param statuses: list of pull request statuses
226 :param statuses: list of pull request statuses
225 :param opened_by: author user of the pull request
227 :param opened_by: author user of the pull request
226 :returns: int number of pull requests
228 :returns: int number of pull requests
227 """
229 """
228 pull_requests = self.get_awaiting_review(
230 pull_requests = self.get_awaiting_review(
229 repo_name, source=source, statuses=statuses, opened_by=opened_by)
231 repo_name, source=source, statuses=statuses, opened_by=opened_by)
230
232
231 return len(pull_requests)
233 return len(pull_requests)
232
234
233 def get_awaiting_review(self, repo_name, source=False, statuses=None,
235 def get_awaiting_review(self, repo_name, source=False, statuses=None,
234 opened_by=None, offset=0, length=None,
236 opened_by=None, offset=0, length=None,
235 order_by=None, order_dir='desc'):
237 order_by=None, order_dir='desc'):
236 """
238 """
237 Get all pull requests for a specific repository that are awaiting
239 Get all pull requests for a specific repository that are awaiting
238 review.
240 review.
239
241
240 :param repo_name: target or source repo
242 :param repo_name: target or source repo
241 :param source: boolean flag to specify if repo_name refers to source
243 :param source: boolean flag to specify if repo_name refers to source
242 :param statuses: list of pull request statuses
244 :param statuses: list of pull request statuses
243 :param opened_by: author user of the pull request
245 :param opened_by: author user of the pull request
244 :param offset: pagination offset
246 :param offset: pagination offset
245 :param length: length of returned list
247 :param length: length of returned list
246 :param order_by: order of the returned list
248 :param order_by: order of the returned list
247 :param order_dir: 'asc' or 'desc' ordering direction
249 :param order_dir: 'asc' or 'desc' ordering direction
248 :returns: list of pull requests
250 :returns: list of pull requests
249 """
251 """
250 pull_requests = self.get_all(
252 pull_requests = self.get_all(
251 repo_name, source=source, statuses=statuses, opened_by=opened_by,
253 repo_name, source=source, statuses=statuses, opened_by=opened_by,
252 order_by=order_by, order_dir=order_dir)
254 order_by=order_by, order_dir=order_dir)
253
255
254 _filtered_pull_requests = []
256 _filtered_pull_requests = []
255 for pr in pull_requests:
257 for pr in pull_requests:
256 status = pr.calculated_review_status()
258 status = pr.calculated_review_status()
257 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
259 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
258 ChangesetStatus.STATUS_UNDER_REVIEW]:
260 ChangesetStatus.STATUS_UNDER_REVIEW]:
259 _filtered_pull_requests.append(pr)
261 _filtered_pull_requests.append(pr)
260 if length:
262 if length:
261 return _filtered_pull_requests[offset:offset+length]
263 return _filtered_pull_requests[offset:offset+length]
262 else:
264 else:
263 return _filtered_pull_requests
265 return _filtered_pull_requests
264
266
265 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
267 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
266 opened_by=None, user_id=None):
268 opened_by=None, user_id=None):
267 """
269 """
268 Count the number of pull requests for a specific repository that are
270 Count the number of pull requests for a specific repository that are
269 awaiting review from a specific user.
271 awaiting review from a specific user.
270
272
271 :param repo_name: target or source repo
273 :param repo_name: target or source repo
272 :param source: boolean flag to specify if repo_name refers to source
274 :param source: boolean flag to specify if repo_name refers to source
273 :param statuses: list of pull request statuses
275 :param statuses: list of pull request statuses
274 :param opened_by: author user of the pull request
276 :param opened_by: author user of the pull request
275 :param user_id: reviewer user of the pull request
277 :param user_id: reviewer user of the pull request
276 :returns: int number of pull requests
278 :returns: int number of pull requests
277 """
279 """
278 pull_requests = self.get_awaiting_my_review(
280 pull_requests = self.get_awaiting_my_review(
279 repo_name, source=source, statuses=statuses, opened_by=opened_by,
281 repo_name, source=source, statuses=statuses, opened_by=opened_by,
280 user_id=user_id)
282 user_id=user_id)
281
283
282 return len(pull_requests)
284 return len(pull_requests)
283
285
284 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
286 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
285 opened_by=None, user_id=None, offset=0,
287 opened_by=None, user_id=None, offset=0,
286 length=None, order_by=None, order_dir='desc'):
288 length=None, order_by=None, order_dir='desc'):
287 """
289 """
288 Get all pull requests for a specific repository that are awaiting
290 Get all pull requests for a specific repository that are awaiting
289 review from a specific user.
291 review from a specific user.
290
292
291 :param repo_name: target or source repo
293 :param repo_name: target or source repo
292 :param source: boolean flag to specify if repo_name refers to source
294 :param source: boolean flag to specify if repo_name refers to source
293 :param statuses: list of pull request statuses
295 :param statuses: list of pull request statuses
294 :param opened_by: author user of the pull request
296 :param opened_by: author user of the pull request
295 :param user_id: reviewer user of the pull request
297 :param user_id: reviewer user of the pull request
296 :param offset: pagination offset
298 :param offset: pagination offset
297 :param length: length of returned list
299 :param length: length of returned list
298 :param order_by: order of the returned list
300 :param order_by: order of the returned list
299 :param order_dir: 'asc' or 'desc' ordering direction
301 :param order_dir: 'asc' or 'desc' ordering direction
300 :returns: list of pull requests
302 :returns: list of pull requests
301 """
303 """
302 pull_requests = self.get_all(
304 pull_requests = self.get_all(
303 repo_name, source=source, statuses=statuses, opened_by=opened_by,
305 repo_name, source=source, statuses=statuses, opened_by=opened_by,
304 order_by=order_by, order_dir=order_dir)
306 order_by=order_by, order_dir=order_dir)
305
307
306 _my = PullRequestModel().get_not_reviewed(user_id)
308 _my = PullRequestModel().get_not_reviewed(user_id)
307 my_participation = []
309 my_participation = []
308 for pr in pull_requests:
310 for pr in pull_requests:
309 if pr in _my:
311 if pr in _my:
310 my_participation.append(pr)
312 my_participation.append(pr)
311 _filtered_pull_requests = my_participation
313 _filtered_pull_requests = my_participation
312 if length:
314 if length:
313 return _filtered_pull_requests[offset:offset+length]
315 return _filtered_pull_requests[offset:offset+length]
314 else:
316 else:
315 return _filtered_pull_requests
317 return _filtered_pull_requests
316
318
317 def get_not_reviewed(self, user_id):
319 def get_not_reviewed(self, user_id):
318 return [
320 return [
319 x.pull_request for x in PullRequestReviewers.query().filter(
321 x.pull_request for x in PullRequestReviewers.query().filter(
320 PullRequestReviewers.user_id == user_id).all()
322 PullRequestReviewers.user_id == user_id).all()
321 ]
323 ]
322
324
323 def _prepare_participating_query(self, user_id=None, statuses=None,
325 def _prepare_participating_query(self, user_id=None, statuses=None,
324 order_by=None, order_dir='desc'):
326 order_by=None, order_dir='desc'):
325 q = PullRequest.query()
327 q = PullRequest.query()
326 if user_id:
328 if user_id:
327 reviewers_subquery = Session().query(
329 reviewers_subquery = Session().query(
328 PullRequestReviewers.pull_request_id).filter(
330 PullRequestReviewers.pull_request_id).filter(
329 PullRequestReviewers.user_id == user_id).subquery()
331 PullRequestReviewers.user_id == user_id).subquery()
330 user_filter = or_(
332 user_filter = or_(
331 PullRequest.user_id == user_id,
333 PullRequest.user_id == user_id,
332 PullRequest.pull_request_id.in_(reviewers_subquery)
334 PullRequest.pull_request_id.in_(reviewers_subquery)
333 )
335 )
334 q = PullRequest.query().filter(user_filter)
336 q = PullRequest.query().filter(user_filter)
335
337
336 # closed,opened
338 # closed,opened
337 if statuses:
339 if statuses:
338 q = q.filter(PullRequest.status.in_(statuses))
340 q = q.filter(PullRequest.status.in_(statuses))
339
341
340 if order_by:
342 if order_by:
341 order_map = {
343 order_map = {
342 'name_raw': PullRequest.pull_request_id,
344 'name_raw': PullRequest.pull_request_id,
343 'title': PullRequest.title,
345 'title': PullRequest.title,
344 'updated_on_raw': PullRequest.updated_on,
346 'updated_on_raw': PullRequest.updated_on,
345 'target_repo': PullRequest.target_repo_id
347 'target_repo': PullRequest.target_repo_id
346 }
348 }
347 if order_dir == 'asc':
349 if order_dir == 'asc':
348 q = q.order_by(order_map[order_by].asc())
350 q = q.order_by(order_map[order_by].asc())
349 else:
351 else:
350 q = q.order_by(order_map[order_by].desc())
352 q = q.order_by(order_map[order_by].desc())
351
353
352 return q
354 return q
353
355
354 def count_im_participating_in(self, user_id=None, statuses=None):
356 def count_im_participating_in(self, user_id=None, statuses=None):
355 q = self._prepare_participating_query(user_id, statuses=statuses)
357 q = self._prepare_participating_query(user_id, statuses=statuses)
356 return q.count()
358 return q.count()
357
359
358 def get_im_participating_in(
360 def get_im_participating_in(
359 self, user_id=None, statuses=None, offset=0,
361 self, user_id=None, statuses=None, offset=0,
360 length=None, order_by=None, order_dir='desc'):
362 length=None, order_by=None, order_dir='desc'):
361 """
363 """
362 Get all Pull requests that i'm participating in, or i have opened
364 Get all Pull requests that i'm participating in, or i have opened
363 """
365 """
364
366
365 q = self._prepare_participating_query(
367 q = self._prepare_participating_query(
366 user_id, statuses=statuses, order_by=order_by,
368 user_id, statuses=statuses, order_by=order_by,
367 order_dir=order_dir)
369 order_dir=order_dir)
368
370
369 if length:
371 if length:
370 pull_requests = q.limit(length).offset(offset).all()
372 pull_requests = q.limit(length).offset(offset).all()
371 else:
373 else:
372 pull_requests = q.all()
374 pull_requests = q.all()
373
375
374 return pull_requests
376 return pull_requests
375
377
376 def get_versions(self, pull_request):
378 def get_versions(self, pull_request):
377 """
379 """
378 returns version of pull request sorted by ID descending
380 returns version of pull request sorted by ID descending
379 """
381 """
380 return PullRequestVersion.query()\
382 return PullRequestVersion.query()\
381 .filter(PullRequestVersion.pull_request == pull_request)\
383 .filter(PullRequestVersion.pull_request == pull_request)\
382 .order_by(PullRequestVersion.pull_request_version_id.asc())\
384 .order_by(PullRequestVersion.pull_request_version_id.asc())\
383 .all()
385 .all()
384
386
385 def get_pr_version(self, pull_request_id, version=None):
387 def get_pr_version(self, pull_request_id, version=None):
386 at_version = None
388 at_version = None
387
389
388 if version and version == 'latest':
390 if version and version == 'latest':
389 pull_request_ver = PullRequest.get(pull_request_id)
391 pull_request_ver = PullRequest.get(pull_request_id)
390 pull_request_obj = pull_request_ver
392 pull_request_obj = pull_request_ver
391 _org_pull_request_obj = pull_request_obj
393 _org_pull_request_obj = pull_request_obj
392 at_version = 'latest'
394 at_version = 'latest'
393 elif version:
395 elif version:
394 pull_request_ver = PullRequestVersion.get_or_404(version)
396 pull_request_ver = PullRequestVersion.get_or_404(version)
395 pull_request_obj = pull_request_ver
397 pull_request_obj = pull_request_ver
396 _org_pull_request_obj = pull_request_ver.pull_request
398 _org_pull_request_obj = pull_request_ver.pull_request
397 at_version = pull_request_ver.pull_request_version_id
399 at_version = pull_request_ver.pull_request_version_id
398 else:
400 else:
399 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
401 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
400 pull_request_id)
402 pull_request_id)
401
403
402 pull_request_display_obj = PullRequest.get_pr_display_object(
404 pull_request_display_obj = PullRequest.get_pr_display_object(
403 pull_request_obj, _org_pull_request_obj)
405 pull_request_obj, _org_pull_request_obj)
404
406
405 return _org_pull_request_obj, pull_request_obj, \
407 return _org_pull_request_obj, pull_request_obj, \
406 pull_request_display_obj, at_version
408 pull_request_display_obj, at_version
407
409
408 def create(self, created_by, source_repo, source_ref, target_repo,
410 def create(self, created_by, source_repo, source_ref, target_repo,
409 target_ref, revisions, reviewers, title, description=None,
411 target_ref, revisions, reviewers, title, description=None,
410 description_renderer=None,
412 description_renderer=None,
411 reviewer_data=None, translator=None, auth_user=None):
413 reviewer_data=None, translator=None, auth_user=None):
412 translator = translator or get_current_request().translate
414 translator = translator or get_current_request().translate
413
415
414 created_by_user = self._get_user(created_by)
416 created_by_user = self._get_user(created_by)
415 auth_user = auth_user or created_by_user.AuthUser()
417 auth_user = auth_user or created_by_user.AuthUser()
416 source_repo = self._get_repo(source_repo)
418 source_repo = self._get_repo(source_repo)
417 target_repo = self._get_repo(target_repo)
419 target_repo = self._get_repo(target_repo)
418
420
419 pull_request = PullRequest()
421 pull_request = PullRequest()
420 pull_request.source_repo = source_repo
422 pull_request.source_repo = source_repo
421 pull_request.source_ref = source_ref
423 pull_request.source_ref = source_ref
422 pull_request.target_repo = target_repo
424 pull_request.target_repo = target_repo
423 pull_request.target_ref = target_ref
425 pull_request.target_ref = target_ref
424 pull_request.revisions = revisions
426 pull_request.revisions = revisions
425 pull_request.title = title
427 pull_request.title = title
426 pull_request.description = description
428 pull_request.description = description
427 pull_request.description_renderer = description_renderer
429 pull_request.description_renderer = description_renderer
428 pull_request.author = created_by_user
430 pull_request.author = created_by_user
429 pull_request.reviewer_data = reviewer_data
431 pull_request.reviewer_data = reviewer_data
430
432
431 Session().add(pull_request)
433 Session().add(pull_request)
432 Session().flush()
434 Session().flush()
433
435
434 reviewer_ids = set()
436 reviewer_ids = set()
435 # members / reviewers
437 # members / reviewers
436 for reviewer_object in reviewers:
438 for reviewer_object in reviewers:
437 user_id, reasons, mandatory, rules = reviewer_object
439 user_id, reasons, mandatory, rules = reviewer_object
438 user = self._get_user(user_id)
440 user = self._get_user(user_id)
439
441
440 # skip duplicates
442 # skip duplicates
441 if user.user_id in reviewer_ids:
443 if user.user_id in reviewer_ids:
442 continue
444 continue
443
445
444 reviewer_ids.add(user.user_id)
446 reviewer_ids.add(user.user_id)
445
447
446 reviewer = PullRequestReviewers()
448 reviewer = PullRequestReviewers()
447 reviewer.user = user
449 reviewer.user = user
448 reviewer.pull_request = pull_request
450 reviewer.pull_request = pull_request
449 reviewer.reasons = reasons
451 reviewer.reasons = reasons
450 reviewer.mandatory = mandatory
452 reviewer.mandatory = mandatory
451
453
452 # NOTE(marcink): pick only first rule for now
454 # NOTE(marcink): pick only first rule for now
453 rule_id = list(rules)[0] if rules else None
455 rule_id = list(rules)[0] if rules else None
454 rule = RepoReviewRule.get(rule_id) if rule_id else None
456 rule = RepoReviewRule.get(rule_id) if rule_id else None
455 if rule:
457 if rule:
456 review_group = rule.user_group_vote_rule(user_id)
458 review_group = rule.user_group_vote_rule(user_id)
457 # we check if this particular reviewer is member of a voting group
459 # we check if this particular reviewer is member of a voting group
458 if review_group:
460 if review_group:
459 # NOTE(marcink):
461 # NOTE(marcink):
460 # can be that user is member of more but we pick the first same,
462 # can be that user is member of more but we pick the first same,
461 # same as default reviewers algo
463 # same as default reviewers algo
462 review_group = review_group[0]
464 review_group = review_group[0]
463
465
464 rule_data = {
466 rule_data = {
465 'rule_name':
467 'rule_name':
466 rule.review_rule_name,
468 rule.review_rule_name,
467 'rule_user_group_entry_id':
469 'rule_user_group_entry_id':
468 review_group.repo_review_rule_users_group_id,
470 review_group.repo_review_rule_users_group_id,
469 'rule_user_group_name':
471 'rule_user_group_name':
470 review_group.users_group.users_group_name,
472 review_group.users_group.users_group_name,
471 'rule_user_group_members':
473 'rule_user_group_members':
472 [x.user.username for x in review_group.users_group.members],
474 [x.user.username for x in review_group.users_group.members],
473 'rule_user_group_members_id':
475 'rule_user_group_members_id':
474 [x.user.user_id for x in review_group.users_group.members],
476 [x.user.user_id for x in review_group.users_group.members],
475 }
477 }
476 # e.g {'vote_rule': -1, 'mandatory': True}
478 # e.g {'vote_rule': -1, 'mandatory': True}
477 rule_data.update(review_group.rule_data())
479 rule_data.update(review_group.rule_data())
478
480
479 reviewer.rule_data = rule_data
481 reviewer.rule_data = rule_data
480
482
481 Session().add(reviewer)
483 Session().add(reviewer)
482 Session().flush()
484 Session().flush()
483
485
484 # Set approval status to "Under Review" for all commits which are
486 # Set approval status to "Under Review" for all commits which are
485 # part of this pull request.
487 # part of this pull request.
486 ChangesetStatusModel().set_status(
488 ChangesetStatusModel().set_status(
487 repo=target_repo,
489 repo=target_repo,
488 status=ChangesetStatus.STATUS_UNDER_REVIEW,
490 status=ChangesetStatus.STATUS_UNDER_REVIEW,
489 user=created_by_user,
491 user=created_by_user,
490 pull_request=pull_request
492 pull_request=pull_request
491 )
493 )
492 # we commit early at this point. This has to do with a fact
494 # we commit early at this point. This has to do with a fact
493 # that before queries do some row-locking. And because of that
495 # that before queries do some row-locking. And because of that
494 # we need to commit and finish transation before below validate call
496 # we need to commit and finish transation before below validate call
495 # that for large repos could be long resulting in long row locks
497 # that for large repos could be long resulting in long row locks
496 Session().commit()
498 Session().commit()
497
499
498 # prepare workspace, and run initial merge simulation
500 # prepare workspace, and run initial merge simulation
499 MergeCheck.validate(
501 MergeCheck.validate(
500 pull_request, auth_user=auth_user, translator=translator)
502 pull_request, auth_user=auth_user, translator=translator)
501
503
502 self.notify_reviewers(pull_request, reviewer_ids)
504 self.notify_reviewers(pull_request, reviewer_ids)
503 self._trigger_pull_request_hook(
505 self._trigger_pull_request_hook(
504 pull_request, created_by_user, 'create')
506 pull_request, created_by_user, 'create')
505
507
506 creation_data = pull_request.get_api_data(with_merge_state=False)
508 creation_data = pull_request.get_api_data(with_merge_state=False)
507 self._log_audit_action(
509 self._log_audit_action(
508 'repo.pull_request.create', {'data': creation_data},
510 'repo.pull_request.create', {'data': creation_data},
509 auth_user, pull_request)
511 auth_user, pull_request)
510
512
511 return pull_request
513 return pull_request
512
514
513 def _trigger_pull_request_hook(self, pull_request, user, action):
515 def _trigger_pull_request_hook(self, pull_request, user, action):
514 pull_request = self.__get_pull_request(pull_request)
516 pull_request = self.__get_pull_request(pull_request)
515 target_scm = pull_request.target_repo.scm_instance()
517 target_scm = pull_request.target_repo.scm_instance()
516 if action == 'create':
518 if action == 'create':
517 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
519 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
518 elif action == 'merge':
520 elif action == 'merge':
519 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
521 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
520 elif action == 'close':
522 elif action == 'close':
521 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
523 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
522 elif action == 'review_status_change':
524 elif action == 'review_status_change':
523 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
525 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
524 elif action == 'update':
526 elif action == 'update':
525 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
527 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
526 else:
528 else:
527 return
529 return
528
530
529 trigger_hook(
531 trigger_hook(
530 username=user.username,
532 username=user.username,
531 repo_name=pull_request.target_repo.repo_name,
533 repo_name=pull_request.target_repo.repo_name,
532 repo_alias=target_scm.alias,
534 repo_alias=target_scm.alias,
533 pull_request=pull_request)
535 pull_request=pull_request)
534
536
535 def _get_commit_ids(self, pull_request):
537 def _get_commit_ids(self, pull_request):
536 """
538 """
537 Return the commit ids of the merged pull request.
539 Return the commit ids of the merged pull request.
538
540
539 This method is not dealing correctly yet with the lack of autoupdates
541 This method is not dealing correctly yet with the lack of autoupdates
540 nor with the implicit target updates.
542 nor with the implicit target updates.
541 For example: if a commit in the source repo is already in the target it
543 For example: if a commit in the source repo is already in the target it
542 will be reported anyways.
544 will be reported anyways.
543 """
545 """
544 merge_rev = pull_request.merge_rev
546 merge_rev = pull_request.merge_rev
545 if merge_rev is None:
547 if merge_rev is None:
546 raise ValueError('This pull request was not merged yet')
548 raise ValueError('This pull request was not merged yet')
547
549
548 commit_ids = list(pull_request.revisions)
550 commit_ids = list(pull_request.revisions)
549 if merge_rev not in commit_ids:
551 if merge_rev not in commit_ids:
550 commit_ids.append(merge_rev)
552 commit_ids.append(merge_rev)
551
553
552 return commit_ids
554 return commit_ids
553
555
554 def merge_repo(self, pull_request, user, extras):
556 def merge_repo(self, pull_request, user, extras):
555 log.debug("Merging pull request %s", pull_request.pull_request_id)
557 log.debug("Merging pull request %s", pull_request.pull_request_id)
556 extras['user_agent'] = 'internal-merge'
558 extras['user_agent'] = 'internal-merge'
557 merge_state = self._merge_pull_request(pull_request, user, extras)
559 merge_state = self._merge_pull_request(pull_request, user, extras)
558 if merge_state.executed:
560 if merge_state.executed:
559 log.debug("Merge was successful, updating the pull request comments.")
561 log.debug("Merge was successful, updating the pull request comments.")
560 self._comment_and_close_pr(pull_request, user, merge_state)
562 self._comment_and_close_pr(pull_request, user, merge_state)
561
563
562 self._log_audit_action(
564 self._log_audit_action(
563 'repo.pull_request.merge',
565 'repo.pull_request.merge',
564 {'merge_state': merge_state.__dict__},
566 {'merge_state': merge_state.__dict__},
565 user, pull_request)
567 user, pull_request)
566
568
567 else:
569 else:
568 log.warn("Merge failed, not updating the pull request.")
570 log.warn("Merge failed, not updating the pull request.")
569 return merge_state
571 return merge_state
570
572
571 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
573 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
572 target_vcs = pull_request.target_repo.scm_instance()
574 target_vcs = pull_request.target_repo.scm_instance()
573 source_vcs = pull_request.source_repo.scm_instance()
575 source_vcs = pull_request.source_repo.scm_instance()
574
576
575 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
577 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
576 pr_id=pull_request.pull_request_id,
578 pr_id=pull_request.pull_request_id,
577 pr_title=pull_request.title,
579 pr_title=pull_request.title,
578 source_repo=source_vcs.name,
580 source_repo=source_vcs.name,
579 source_ref_name=pull_request.source_ref_parts.name,
581 source_ref_name=pull_request.source_ref_parts.name,
580 target_repo=target_vcs.name,
582 target_repo=target_vcs.name,
581 target_ref_name=pull_request.target_ref_parts.name,
583 target_ref_name=pull_request.target_ref_parts.name,
582 )
584 )
583
585
584 workspace_id = self._workspace_id(pull_request)
586 workspace_id = self._workspace_id(pull_request)
585 repo_id = pull_request.target_repo.repo_id
587 repo_id = pull_request.target_repo.repo_id
586 use_rebase = self._use_rebase_for_merging(pull_request)
588 use_rebase = self._use_rebase_for_merging(pull_request)
587 close_branch = self._close_branch_before_merging(pull_request)
589 close_branch = self._close_branch_before_merging(pull_request)
588
590
589 target_ref = self._refresh_reference(
591 target_ref = self._refresh_reference(
590 pull_request.target_ref_parts, target_vcs)
592 pull_request.target_ref_parts, target_vcs)
591
593
592 callback_daemon, extras = prepare_callback_daemon(
594 callback_daemon, extras = prepare_callback_daemon(
593 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
595 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
594 host=vcs_settings.HOOKS_HOST,
596 host=vcs_settings.HOOKS_HOST,
595 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
597 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
596
598
597 with callback_daemon:
599 with callback_daemon:
598 # TODO: johbo: Implement a clean way to run a config_override
600 # TODO: johbo: Implement a clean way to run a config_override
599 # for a single call.
601 # for a single call.
600 target_vcs.config.set(
602 target_vcs.config.set(
601 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
603 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
602
604
603 user_name = user.short_contact
605 user_name = user.short_contact
604 merge_state = target_vcs.merge(
606 merge_state = target_vcs.merge(
605 repo_id, workspace_id, target_ref, source_vcs,
607 repo_id, workspace_id, target_ref, source_vcs,
606 pull_request.source_ref_parts,
608 pull_request.source_ref_parts,
607 user_name=user_name, user_email=user.email,
609 user_name=user_name, user_email=user.email,
608 message=message, use_rebase=use_rebase,
610 message=message, use_rebase=use_rebase,
609 close_branch=close_branch)
611 close_branch=close_branch)
610 return merge_state
612 return merge_state
611
613
612 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
614 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
613 pull_request.merge_rev = merge_state.merge_ref.commit_id
615 pull_request.merge_rev = merge_state.merge_ref.commit_id
614 pull_request.updated_on = datetime.datetime.now()
616 pull_request.updated_on = datetime.datetime.now()
615 close_msg = close_msg or 'Pull request merged and closed'
617 close_msg = close_msg or 'Pull request merged and closed'
616
618
617 CommentsModel().create(
619 CommentsModel().create(
618 text=safe_unicode(close_msg),
620 text=safe_unicode(close_msg),
619 repo=pull_request.target_repo.repo_id,
621 repo=pull_request.target_repo.repo_id,
620 user=user.user_id,
622 user=user.user_id,
621 pull_request=pull_request.pull_request_id,
623 pull_request=pull_request.pull_request_id,
622 f_path=None,
624 f_path=None,
623 line_no=None,
625 line_no=None,
624 closing_pr=True
626 closing_pr=True
625 )
627 )
626
628
627 Session().add(pull_request)
629 Session().add(pull_request)
628 Session().flush()
630 Session().flush()
629 # TODO: paris: replace invalidation with less radical solution
631 # TODO: paris: replace invalidation with less radical solution
630 ScmModel().mark_for_invalidation(
632 ScmModel().mark_for_invalidation(
631 pull_request.target_repo.repo_name)
633 pull_request.target_repo.repo_name)
632 self._trigger_pull_request_hook(pull_request, user, 'merge')
634 self._trigger_pull_request_hook(pull_request, user, 'merge')
633
635
634 def has_valid_update_type(self, pull_request):
636 def has_valid_update_type(self, pull_request):
635 source_ref_type = pull_request.source_ref_parts.type
637 source_ref_type = pull_request.source_ref_parts.type
636 return source_ref_type in ['book', 'branch', 'tag']
638 return source_ref_type in self.REF_TYPES
637
639
638 def update_commits(self, pull_request):
640 def update_commits(self, pull_request):
639 """
641 """
640 Get the updated list of commits for the pull request
642 Get the updated list of commits for the pull request
641 and return the new pull request version and the list
643 and return the new pull request version and the list
642 of commits processed by this update action
644 of commits processed by this update action
643 """
645 """
644 pull_request = self.__get_pull_request(pull_request)
646 pull_request = self.__get_pull_request(pull_request)
645 source_ref_type = pull_request.source_ref_parts.type
647 source_ref_type = pull_request.source_ref_parts.type
646 source_ref_name = pull_request.source_ref_parts.name
648 source_ref_name = pull_request.source_ref_parts.name
647 source_ref_id = pull_request.source_ref_parts.commit_id
649 source_ref_id = pull_request.source_ref_parts.commit_id
648
650
649 target_ref_type = pull_request.target_ref_parts.type
651 target_ref_type = pull_request.target_ref_parts.type
650 target_ref_name = pull_request.target_ref_parts.name
652 target_ref_name = pull_request.target_ref_parts.name
651 target_ref_id = pull_request.target_ref_parts.commit_id
653 target_ref_id = pull_request.target_ref_parts.commit_id
652
654
653 if not self.has_valid_update_type(pull_request):
655 if not self.has_valid_update_type(pull_request):
654 log.debug(
656 log.debug(
655 "Skipping update of pull request %s due to ref type: %s",
657 "Skipping update of pull request %s due to ref type: %s",
656 pull_request, source_ref_type)
658 pull_request, source_ref_type)
657 return UpdateResponse(
659 return UpdateResponse(
658 executed=False,
660 executed=False,
659 reason=UpdateFailureReason.WRONG_REF_TYPE,
661 reason=UpdateFailureReason.WRONG_REF_TYPE,
660 old=pull_request, new=None, changes=None,
662 old=pull_request, new=None, changes=None,
661 source_changed=False, target_changed=False)
663 source_changed=False, target_changed=False)
662
664
663 # source repo
665 # source repo
664 source_repo = pull_request.source_repo.scm_instance()
666 source_repo = pull_request.source_repo.scm_instance()
665 try:
667 try:
666 source_commit = source_repo.get_commit(commit_id=source_ref_name)
668 source_commit = source_repo.get_commit(commit_id=source_ref_name)
667 except CommitDoesNotExistError:
669 except CommitDoesNotExistError:
668 return UpdateResponse(
670 return UpdateResponse(
669 executed=False,
671 executed=False,
670 reason=UpdateFailureReason.MISSING_SOURCE_REF,
672 reason=UpdateFailureReason.MISSING_SOURCE_REF,
671 old=pull_request, new=None, changes=None,
673 old=pull_request, new=None, changes=None,
672 source_changed=False, target_changed=False)
674 source_changed=False, target_changed=False)
673
675
674 source_changed = source_ref_id != source_commit.raw_id
676 source_changed = source_ref_id != source_commit.raw_id
675
677
676 # target repo
678 # target repo
677 target_repo = pull_request.target_repo.scm_instance()
679 target_repo = pull_request.target_repo.scm_instance()
678 try:
680 try:
679 target_commit = target_repo.get_commit(commit_id=target_ref_name)
681 target_commit = target_repo.get_commit(commit_id=target_ref_name)
680 except CommitDoesNotExistError:
682 except CommitDoesNotExistError:
681 return UpdateResponse(
683 return UpdateResponse(
682 executed=False,
684 executed=False,
683 reason=UpdateFailureReason.MISSING_TARGET_REF,
685 reason=UpdateFailureReason.MISSING_TARGET_REF,
684 old=pull_request, new=None, changes=None,
686 old=pull_request, new=None, changes=None,
685 source_changed=False, target_changed=False)
687 source_changed=False, target_changed=False)
686 target_changed = target_ref_id != target_commit.raw_id
688 target_changed = target_ref_id != target_commit.raw_id
687
689
688 if not (source_changed or target_changed):
690 if not (source_changed or target_changed):
689 log.debug("Nothing changed in pull request %s", pull_request)
691 log.debug("Nothing changed in pull request %s", pull_request)
690 return UpdateResponse(
692 return UpdateResponse(
691 executed=False,
693 executed=False,
692 reason=UpdateFailureReason.NO_CHANGE,
694 reason=UpdateFailureReason.NO_CHANGE,
693 old=pull_request, new=None, changes=None,
695 old=pull_request, new=None, changes=None,
694 source_changed=target_changed, target_changed=source_changed)
696 source_changed=target_changed, target_changed=source_changed)
695
697
696 change_in_found = 'target repo' if target_changed else 'source repo'
698 change_in_found = 'target repo' if target_changed else 'source repo'
697 log.debug('Updating pull request because of change in %s detected',
699 log.debug('Updating pull request because of change in %s detected',
698 change_in_found)
700 change_in_found)
699
701
700 # Finally there is a need for an update, in case of source change
702 # Finally there is a need for an update, in case of source change
701 # we create a new version, else just an update
703 # we create a new version, else just an update
702 if source_changed:
704 if source_changed:
703 pull_request_version = self._create_version_from_snapshot(pull_request)
705 pull_request_version = self._create_version_from_snapshot(pull_request)
704 self._link_comments_to_version(pull_request_version)
706 self._link_comments_to_version(pull_request_version)
705 else:
707 else:
706 try:
708 try:
707 ver = pull_request.versions[-1]
709 ver = pull_request.versions[-1]
708 except IndexError:
710 except IndexError:
709 ver = None
711 ver = None
710
712
711 pull_request.pull_request_version_id = \
713 pull_request.pull_request_version_id = \
712 ver.pull_request_version_id if ver else None
714 ver.pull_request_version_id if ver else None
713 pull_request_version = pull_request
715 pull_request_version = pull_request
714
716
715 try:
717 try:
716 if target_ref_type in ('tag', 'branch', 'book'):
718 if target_ref_type in self.REF_TYPES:
717 target_commit = target_repo.get_commit(target_ref_name)
719 target_commit = target_repo.get_commit(target_ref_name)
718 else:
720 else:
719 target_commit = target_repo.get_commit(target_ref_id)
721 target_commit = target_repo.get_commit(target_ref_id)
720 except CommitDoesNotExistError:
722 except CommitDoesNotExistError:
721 return UpdateResponse(
723 return UpdateResponse(
722 executed=False,
724 executed=False,
723 reason=UpdateFailureReason.MISSING_TARGET_REF,
725 reason=UpdateFailureReason.MISSING_TARGET_REF,
724 old=pull_request, new=None, changes=None,
726 old=pull_request, new=None, changes=None,
725 source_changed=source_changed, target_changed=target_changed)
727 source_changed=source_changed, target_changed=target_changed)
726
728
727 # re-compute commit ids
729 # re-compute commit ids
728 old_commit_ids = pull_request.revisions
730 old_commit_ids = pull_request.revisions
729 pre_load = ["author", "branch", "date", "message"]
731 pre_load = ["author", "branch", "date", "message"]
730 commit_ranges = target_repo.compare(
732 commit_ranges = target_repo.compare(
731 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
733 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
732 pre_load=pre_load)
734 pre_load=pre_load)
733
735
734 ancestor = target_repo.get_common_ancestor(
736 ancestor = target_repo.get_common_ancestor(
735 target_commit.raw_id, source_commit.raw_id, source_repo)
737 target_commit.raw_id, source_commit.raw_id, source_repo)
736
738
737 pull_request.source_ref = '%s:%s:%s' % (
739 pull_request.source_ref = '%s:%s:%s' % (
738 source_ref_type, source_ref_name, source_commit.raw_id)
740 source_ref_type, source_ref_name, source_commit.raw_id)
739 pull_request.target_ref = '%s:%s:%s' % (
741 pull_request.target_ref = '%s:%s:%s' % (
740 target_ref_type, target_ref_name, ancestor)
742 target_ref_type, target_ref_name, ancestor)
741
743
742 pull_request.revisions = [
744 pull_request.revisions = [
743 commit.raw_id for commit in reversed(commit_ranges)]
745 commit.raw_id for commit in reversed(commit_ranges)]
744 pull_request.updated_on = datetime.datetime.now()
746 pull_request.updated_on = datetime.datetime.now()
745 Session().add(pull_request)
747 Session().add(pull_request)
746 new_commit_ids = pull_request.revisions
748 new_commit_ids = pull_request.revisions
747
749
748 old_diff_data, new_diff_data = self._generate_update_diffs(
750 old_diff_data, new_diff_data = self._generate_update_diffs(
749 pull_request, pull_request_version)
751 pull_request, pull_request_version)
750
752
751 # calculate commit and file changes
753 # calculate commit and file changes
752 changes = self._calculate_commit_id_changes(
754 changes = self._calculate_commit_id_changes(
753 old_commit_ids, new_commit_ids)
755 old_commit_ids, new_commit_ids)
754 file_changes = self._calculate_file_changes(
756 file_changes = self._calculate_file_changes(
755 old_diff_data, new_diff_data)
757 old_diff_data, new_diff_data)
756
758
757 # set comments as outdated if DIFFS changed
759 # set comments as outdated if DIFFS changed
758 CommentsModel().outdate_comments(
760 CommentsModel().outdate_comments(
759 pull_request, old_diff_data=old_diff_data,
761 pull_request, old_diff_data=old_diff_data,
760 new_diff_data=new_diff_data)
762 new_diff_data=new_diff_data)
761
763
762 commit_changes = (changes.added or changes.removed)
764 commit_changes = (changes.added or changes.removed)
763 file_node_changes = (
765 file_node_changes = (
764 file_changes.added or file_changes.modified or file_changes.removed)
766 file_changes.added or file_changes.modified or file_changes.removed)
765 pr_has_changes = commit_changes or file_node_changes
767 pr_has_changes = commit_changes or file_node_changes
766
768
767 # Add an automatic comment to the pull request, in case
769 # Add an automatic comment to the pull request, in case
768 # anything has changed
770 # anything has changed
769 if pr_has_changes:
771 if pr_has_changes:
770 update_comment = CommentsModel().create(
772 update_comment = CommentsModel().create(
771 text=self._render_update_message(changes, file_changes),
773 text=self._render_update_message(changes, file_changes),
772 repo=pull_request.target_repo,
774 repo=pull_request.target_repo,
773 user=pull_request.author,
775 user=pull_request.author,
774 pull_request=pull_request,
776 pull_request=pull_request,
775 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
777 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
776
778
777 # Update status to "Under Review" for added commits
779 # Update status to "Under Review" for added commits
778 for commit_id in changes.added:
780 for commit_id in changes.added:
779 ChangesetStatusModel().set_status(
781 ChangesetStatusModel().set_status(
780 repo=pull_request.source_repo,
782 repo=pull_request.source_repo,
781 status=ChangesetStatus.STATUS_UNDER_REVIEW,
783 status=ChangesetStatus.STATUS_UNDER_REVIEW,
782 comment=update_comment,
784 comment=update_comment,
783 user=pull_request.author,
785 user=pull_request.author,
784 pull_request=pull_request,
786 pull_request=pull_request,
785 revision=commit_id)
787 revision=commit_id)
786
788
787 log.debug(
789 log.debug(
788 'Updated pull request %s, added_ids: %s, common_ids: %s, '
790 'Updated pull request %s, added_ids: %s, common_ids: %s, '
789 'removed_ids: %s', pull_request.pull_request_id,
791 'removed_ids: %s', pull_request.pull_request_id,
790 changes.added, changes.common, changes.removed)
792 changes.added, changes.common, changes.removed)
791 log.debug(
793 log.debug(
792 'Updated pull request with the following file changes: %s',
794 'Updated pull request with the following file changes: %s',
793 file_changes)
795 file_changes)
794
796
795 log.info(
797 log.info(
796 "Updated pull request %s from commit %s to commit %s, "
798 "Updated pull request %s from commit %s to commit %s, "
797 "stored new version %s of this pull request.",
799 "stored new version %s of this pull request.",
798 pull_request.pull_request_id, source_ref_id,
800 pull_request.pull_request_id, source_ref_id,
799 pull_request.source_ref_parts.commit_id,
801 pull_request.source_ref_parts.commit_id,
800 pull_request_version.pull_request_version_id)
802 pull_request_version.pull_request_version_id)
801 Session().commit()
803 Session().commit()
802 self._trigger_pull_request_hook(
804 self._trigger_pull_request_hook(
803 pull_request, pull_request.author, 'update')
805 pull_request, pull_request.author, 'update')
804
806
805 return UpdateResponse(
807 return UpdateResponse(
806 executed=True, reason=UpdateFailureReason.NONE,
808 executed=True, reason=UpdateFailureReason.NONE,
807 old=pull_request, new=pull_request_version, changes=changes,
809 old=pull_request, new=pull_request_version, changes=changes,
808 source_changed=source_changed, target_changed=target_changed)
810 source_changed=source_changed, target_changed=target_changed)
809
811
810 def _create_version_from_snapshot(self, pull_request):
812 def _create_version_from_snapshot(self, pull_request):
811 version = PullRequestVersion()
813 version = PullRequestVersion()
812 version.title = pull_request.title
814 version.title = pull_request.title
813 version.description = pull_request.description
815 version.description = pull_request.description
814 version.status = pull_request.status
816 version.status = pull_request.status
815 version.created_on = datetime.datetime.now()
817 version.created_on = datetime.datetime.now()
816 version.updated_on = pull_request.updated_on
818 version.updated_on = pull_request.updated_on
817 version.user_id = pull_request.user_id
819 version.user_id = pull_request.user_id
818 version.source_repo = pull_request.source_repo
820 version.source_repo = pull_request.source_repo
819 version.source_ref = pull_request.source_ref
821 version.source_ref = pull_request.source_ref
820 version.target_repo = pull_request.target_repo
822 version.target_repo = pull_request.target_repo
821 version.target_ref = pull_request.target_ref
823 version.target_ref = pull_request.target_ref
822
824
823 version._last_merge_source_rev = pull_request._last_merge_source_rev
825 version._last_merge_source_rev = pull_request._last_merge_source_rev
824 version._last_merge_target_rev = pull_request._last_merge_target_rev
826 version._last_merge_target_rev = pull_request._last_merge_target_rev
825 version.last_merge_status = pull_request.last_merge_status
827 version.last_merge_status = pull_request.last_merge_status
826 version.shadow_merge_ref = pull_request.shadow_merge_ref
828 version.shadow_merge_ref = pull_request.shadow_merge_ref
827 version.merge_rev = pull_request.merge_rev
829 version.merge_rev = pull_request.merge_rev
828 version.reviewer_data = pull_request.reviewer_data
830 version.reviewer_data = pull_request.reviewer_data
829
831
830 version.revisions = pull_request.revisions
832 version.revisions = pull_request.revisions
831 version.pull_request = pull_request
833 version.pull_request = pull_request
832 Session().add(version)
834 Session().add(version)
833 Session().flush()
835 Session().flush()
834
836
835 return version
837 return version
836
838
837 def _generate_update_diffs(self, pull_request, pull_request_version):
839 def _generate_update_diffs(self, pull_request, pull_request_version):
838
840
839 diff_context = (
841 diff_context = (
840 self.DIFF_CONTEXT +
842 self.DIFF_CONTEXT +
841 CommentsModel.needed_extra_diff_context())
843 CommentsModel.needed_extra_diff_context())
842 hide_whitespace_changes = False
844 hide_whitespace_changes = False
843 source_repo = pull_request_version.source_repo
845 source_repo = pull_request_version.source_repo
844 source_ref_id = pull_request_version.source_ref_parts.commit_id
846 source_ref_id = pull_request_version.source_ref_parts.commit_id
845 target_ref_id = pull_request_version.target_ref_parts.commit_id
847 target_ref_id = pull_request_version.target_ref_parts.commit_id
846 old_diff = self._get_diff_from_pr_or_version(
848 old_diff = self._get_diff_from_pr_or_version(
847 source_repo, source_ref_id, target_ref_id,
849 source_repo, source_ref_id, target_ref_id,
848 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
850 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
849
851
850 source_repo = pull_request.source_repo
852 source_repo = pull_request.source_repo
851 source_ref_id = pull_request.source_ref_parts.commit_id
853 source_ref_id = pull_request.source_ref_parts.commit_id
852 target_ref_id = pull_request.target_ref_parts.commit_id
854 target_ref_id = pull_request.target_ref_parts.commit_id
853
855
854 new_diff = self._get_diff_from_pr_or_version(
856 new_diff = self._get_diff_from_pr_or_version(
855 source_repo, source_ref_id, target_ref_id,
857 source_repo, source_ref_id, target_ref_id,
856 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
858 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
857
859
858 old_diff_data = diffs.DiffProcessor(old_diff)
860 old_diff_data = diffs.DiffProcessor(old_diff)
859 old_diff_data.prepare()
861 old_diff_data.prepare()
860 new_diff_data = diffs.DiffProcessor(new_diff)
862 new_diff_data = diffs.DiffProcessor(new_diff)
861 new_diff_data.prepare()
863 new_diff_data.prepare()
862
864
863 return old_diff_data, new_diff_data
865 return old_diff_data, new_diff_data
864
866
865 def _link_comments_to_version(self, pull_request_version):
867 def _link_comments_to_version(self, pull_request_version):
866 """
868 """
867 Link all unlinked comments of this pull request to the given version.
869 Link all unlinked comments of this pull request to the given version.
868
870
869 :param pull_request_version: The `PullRequestVersion` to which
871 :param pull_request_version: The `PullRequestVersion` to which
870 the comments shall be linked.
872 the comments shall be linked.
871
873
872 """
874 """
873 pull_request = pull_request_version.pull_request
875 pull_request = pull_request_version.pull_request
874 comments = ChangesetComment.query()\
876 comments = ChangesetComment.query()\
875 .filter(
877 .filter(
876 # TODO: johbo: Should we query for the repo at all here?
878 # TODO: johbo: Should we query for the repo at all here?
877 # Pending decision on how comments of PRs are to be related
879 # Pending decision on how comments of PRs are to be related
878 # to either the source repo, the target repo or no repo at all.
880 # to either the source repo, the target repo or no repo at all.
879 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
881 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
880 ChangesetComment.pull_request == pull_request,
882 ChangesetComment.pull_request == pull_request,
881 ChangesetComment.pull_request_version == None)\
883 ChangesetComment.pull_request_version == None)\
882 .order_by(ChangesetComment.comment_id.asc())
884 .order_by(ChangesetComment.comment_id.asc())
883
885
884 # TODO: johbo: Find out why this breaks if it is done in a bulk
886 # TODO: johbo: Find out why this breaks if it is done in a bulk
885 # operation.
887 # operation.
886 for comment in comments:
888 for comment in comments:
887 comment.pull_request_version_id = (
889 comment.pull_request_version_id = (
888 pull_request_version.pull_request_version_id)
890 pull_request_version.pull_request_version_id)
889 Session().add(comment)
891 Session().add(comment)
890
892
891 def _calculate_commit_id_changes(self, old_ids, new_ids):
893 def _calculate_commit_id_changes(self, old_ids, new_ids):
892 added = [x for x in new_ids if x not in old_ids]
894 added = [x for x in new_ids if x not in old_ids]
893 common = [x for x in new_ids if x in old_ids]
895 common = [x for x in new_ids if x in old_ids]
894 removed = [x for x in old_ids if x not in new_ids]
896 removed = [x for x in old_ids if x not in new_ids]
895 total = new_ids
897 total = new_ids
896 return ChangeTuple(added, common, removed, total)
898 return ChangeTuple(added, common, removed, total)
897
899
898 def _calculate_file_changes(self, old_diff_data, new_diff_data):
900 def _calculate_file_changes(self, old_diff_data, new_diff_data):
899
901
900 old_files = OrderedDict()
902 old_files = OrderedDict()
901 for diff_data in old_diff_data.parsed_diff:
903 for diff_data in old_diff_data.parsed_diff:
902 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
904 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
903
905
904 added_files = []
906 added_files = []
905 modified_files = []
907 modified_files = []
906 removed_files = []
908 removed_files = []
907 for diff_data in new_diff_data.parsed_diff:
909 for diff_data in new_diff_data.parsed_diff:
908 new_filename = diff_data['filename']
910 new_filename = diff_data['filename']
909 new_hash = md5_safe(diff_data['raw_diff'])
911 new_hash = md5_safe(diff_data['raw_diff'])
910
912
911 old_hash = old_files.get(new_filename)
913 old_hash = old_files.get(new_filename)
912 if not old_hash:
914 if not old_hash:
913 # file is not present in old diff, means it's added
915 # file is not present in old diff, means it's added
914 added_files.append(new_filename)
916 added_files.append(new_filename)
915 else:
917 else:
916 if new_hash != old_hash:
918 if new_hash != old_hash:
917 modified_files.append(new_filename)
919 modified_files.append(new_filename)
918 # now remove a file from old, since we have seen it already
920 # now remove a file from old, since we have seen it already
919 del old_files[new_filename]
921 del old_files[new_filename]
920
922
921 # removed files is when there are present in old, but not in NEW,
923 # removed files is when there are present in old, but not in NEW,
922 # since we remove old files that are present in new diff, left-overs
924 # since we remove old files that are present in new diff, left-overs
923 # if any should be the removed files
925 # if any should be the removed files
924 removed_files.extend(old_files.keys())
926 removed_files.extend(old_files.keys())
925
927
926 return FileChangeTuple(added_files, modified_files, removed_files)
928 return FileChangeTuple(added_files, modified_files, removed_files)
927
929
928 def _render_update_message(self, changes, file_changes):
930 def _render_update_message(self, changes, file_changes):
929 """
931 """
930 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
932 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
931 so it's always looking the same disregarding on which default
933 so it's always looking the same disregarding on which default
932 renderer system is using.
934 renderer system is using.
933
935
934 :param changes: changes named tuple
936 :param changes: changes named tuple
935 :param file_changes: file changes named tuple
937 :param file_changes: file changes named tuple
936
938
937 """
939 """
938 new_status = ChangesetStatus.get_status_lbl(
940 new_status = ChangesetStatus.get_status_lbl(
939 ChangesetStatus.STATUS_UNDER_REVIEW)
941 ChangesetStatus.STATUS_UNDER_REVIEW)
940
942
941 changed_files = (
943 changed_files = (
942 file_changes.added + file_changes.modified + file_changes.removed)
944 file_changes.added + file_changes.modified + file_changes.removed)
943
945
944 params = {
946 params = {
945 'under_review_label': new_status,
947 'under_review_label': new_status,
946 'added_commits': changes.added,
948 'added_commits': changes.added,
947 'removed_commits': changes.removed,
949 'removed_commits': changes.removed,
948 'changed_files': changed_files,
950 'changed_files': changed_files,
949 'added_files': file_changes.added,
951 'added_files': file_changes.added,
950 'modified_files': file_changes.modified,
952 'modified_files': file_changes.modified,
951 'removed_files': file_changes.removed,
953 'removed_files': file_changes.removed,
952 }
954 }
953 renderer = RstTemplateRenderer()
955 renderer = RstTemplateRenderer()
954 return renderer.render('pull_request_update.mako', **params)
956 return renderer.render('pull_request_update.mako', **params)
955
957
956 def edit(self, pull_request, title, description, description_renderer, user):
958 def edit(self, pull_request, title, description, description_renderer, user):
957 pull_request = self.__get_pull_request(pull_request)
959 pull_request = self.__get_pull_request(pull_request)
958 old_data = pull_request.get_api_data(with_merge_state=False)
960 old_data = pull_request.get_api_data(with_merge_state=False)
959 if pull_request.is_closed():
961 if pull_request.is_closed():
960 raise ValueError('This pull request is closed')
962 raise ValueError('This pull request is closed')
961 if title:
963 if title:
962 pull_request.title = title
964 pull_request.title = title
963 pull_request.description = description
965 pull_request.description = description
964 pull_request.updated_on = datetime.datetime.now()
966 pull_request.updated_on = datetime.datetime.now()
965 pull_request.description_renderer = description_renderer
967 pull_request.description_renderer = description_renderer
966 Session().add(pull_request)
968 Session().add(pull_request)
967 self._log_audit_action(
969 self._log_audit_action(
968 'repo.pull_request.edit', {'old_data': old_data},
970 'repo.pull_request.edit', {'old_data': old_data},
969 user, pull_request)
971 user, pull_request)
970
972
971 def update_reviewers(self, pull_request, reviewer_data, user):
973 def update_reviewers(self, pull_request, reviewer_data, user):
972 """
974 """
973 Update the reviewers in the pull request
975 Update the reviewers in the pull request
974
976
975 :param pull_request: the pr to update
977 :param pull_request: the pr to update
976 :param reviewer_data: list of tuples
978 :param reviewer_data: list of tuples
977 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
979 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
978 """
980 """
979 pull_request = self.__get_pull_request(pull_request)
981 pull_request = self.__get_pull_request(pull_request)
980 if pull_request.is_closed():
982 if pull_request.is_closed():
981 raise ValueError('This pull request is closed')
983 raise ValueError('This pull request is closed')
982
984
983 reviewers = {}
985 reviewers = {}
984 for user_id, reasons, mandatory, rules in reviewer_data:
986 for user_id, reasons, mandatory, rules in reviewer_data:
985 if isinstance(user_id, (int, basestring)):
987 if isinstance(user_id, (int, basestring)):
986 user_id = self._get_user(user_id).user_id
988 user_id = self._get_user(user_id).user_id
987 reviewers[user_id] = {
989 reviewers[user_id] = {
988 'reasons': reasons, 'mandatory': mandatory}
990 'reasons': reasons, 'mandatory': mandatory}
989
991
990 reviewers_ids = set(reviewers.keys())
992 reviewers_ids = set(reviewers.keys())
991 current_reviewers = PullRequestReviewers.query()\
993 current_reviewers = PullRequestReviewers.query()\
992 .filter(PullRequestReviewers.pull_request ==
994 .filter(PullRequestReviewers.pull_request ==
993 pull_request).all()
995 pull_request).all()
994 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
996 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
995
997
996 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
998 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
997 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
999 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
998
1000
999 log.debug("Adding %s reviewers", ids_to_add)
1001 log.debug("Adding %s reviewers", ids_to_add)
1000 log.debug("Removing %s reviewers", ids_to_remove)
1002 log.debug("Removing %s reviewers", ids_to_remove)
1001 changed = False
1003 changed = False
1002 for uid in ids_to_add:
1004 for uid in ids_to_add:
1003 changed = True
1005 changed = True
1004 _usr = self._get_user(uid)
1006 _usr = self._get_user(uid)
1005 reviewer = PullRequestReviewers()
1007 reviewer = PullRequestReviewers()
1006 reviewer.user = _usr
1008 reviewer.user = _usr
1007 reviewer.pull_request = pull_request
1009 reviewer.pull_request = pull_request
1008 reviewer.reasons = reviewers[uid]['reasons']
1010 reviewer.reasons = reviewers[uid]['reasons']
1009 # NOTE(marcink): mandatory shouldn't be changed now
1011 # NOTE(marcink): mandatory shouldn't be changed now
1010 # reviewer.mandatory = reviewers[uid]['reasons']
1012 # reviewer.mandatory = reviewers[uid]['reasons']
1011 Session().add(reviewer)
1013 Session().add(reviewer)
1012 self._log_audit_action(
1014 self._log_audit_action(
1013 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1015 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
1014 user, pull_request)
1016 user, pull_request)
1015
1017
1016 for uid in ids_to_remove:
1018 for uid in ids_to_remove:
1017 changed = True
1019 changed = True
1018 reviewers = PullRequestReviewers.query()\
1020 reviewers = PullRequestReviewers.query()\
1019 .filter(PullRequestReviewers.user_id == uid,
1021 .filter(PullRequestReviewers.user_id == uid,
1020 PullRequestReviewers.pull_request == pull_request)\
1022 PullRequestReviewers.pull_request == pull_request)\
1021 .all()
1023 .all()
1022 # use .all() in case we accidentally added the same person twice
1024 # use .all() in case we accidentally added the same person twice
1023 # this CAN happen due to the lack of DB checks
1025 # this CAN happen due to the lack of DB checks
1024 for obj in reviewers:
1026 for obj in reviewers:
1025 old_data = obj.get_dict()
1027 old_data = obj.get_dict()
1026 Session().delete(obj)
1028 Session().delete(obj)
1027 self._log_audit_action(
1029 self._log_audit_action(
1028 'repo.pull_request.reviewer.delete',
1030 'repo.pull_request.reviewer.delete',
1029 {'old_data': old_data}, user, pull_request)
1031 {'old_data': old_data}, user, pull_request)
1030
1032
1031 if changed:
1033 if changed:
1032 pull_request.updated_on = datetime.datetime.now()
1034 pull_request.updated_on = datetime.datetime.now()
1033 Session().add(pull_request)
1035 Session().add(pull_request)
1034
1036
1035 self.notify_reviewers(pull_request, ids_to_add)
1037 self.notify_reviewers(pull_request, ids_to_add)
1036 return ids_to_add, ids_to_remove
1038 return ids_to_add, ids_to_remove
1037
1039
1038 def get_url(self, pull_request, request=None, permalink=False):
1040 def get_url(self, pull_request, request=None, permalink=False):
1039 if not request:
1041 if not request:
1040 request = get_current_request()
1042 request = get_current_request()
1041
1043
1042 if permalink:
1044 if permalink:
1043 return request.route_url(
1045 return request.route_url(
1044 'pull_requests_global',
1046 'pull_requests_global',
1045 pull_request_id=pull_request.pull_request_id,)
1047 pull_request_id=pull_request.pull_request_id,)
1046 else:
1048 else:
1047 return request.route_url('pullrequest_show',
1049 return request.route_url('pullrequest_show',
1048 repo_name=safe_str(pull_request.target_repo.repo_name),
1050 repo_name=safe_str(pull_request.target_repo.repo_name),
1049 pull_request_id=pull_request.pull_request_id,)
1051 pull_request_id=pull_request.pull_request_id,)
1050
1052
1051 def get_shadow_clone_url(self, pull_request, request=None):
1053 def get_shadow_clone_url(self, pull_request, request=None):
1052 """
1054 """
1053 Returns qualified url pointing to the shadow repository. If this pull
1055 Returns qualified url pointing to the shadow repository. If this pull
1054 request is closed there is no shadow repository and ``None`` will be
1056 request is closed there is no shadow repository and ``None`` will be
1055 returned.
1057 returned.
1056 """
1058 """
1057 if pull_request.is_closed():
1059 if pull_request.is_closed():
1058 return None
1060 return None
1059 else:
1061 else:
1060 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1062 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1061 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1063 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1062
1064
1063 def notify_reviewers(self, pull_request, reviewers_ids):
1065 def notify_reviewers(self, pull_request, reviewers_ids):
1064 # notification to reviewers
1066 # notification to reviewers
1065 if not reviewers_ids:
1067 if not reviewers_ids:
1066 return
1068 return
1067
1069
1068 pull_request_obj = pull_request
1070 pull_request_obj = pull_request
1069 # get the current participants of this pull request
1071 # get the current participants of this pull request
1070 recipients = reviewers_ids
1072 recipients = reviewers_ids
1071 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1073 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1072
1074
1073 pr_source_repo = pull_request_obj.source_repo
1075 pr_source_repo = pull_request_obj.source_repo
1074 pr_target_repo = pull_request_obj.target_repo
1076 pr_target_repo = pull_request_obj.target_repo
1075
1077
1076 pr_url = h.route_url('pullrequest_show',
1078 pr_url = h.route_url('pullrequest_show',
1077 repo_name=pr_target_repo.repo_name,
1079 repo_name=pr_target_repo.repo_name,
1078 pull_request_id=pull_request_obj.pull_request_id,)
1080 pull_request_id=pull_request_obj.pull_request_id,)
1079
1081
1080 # set some variables for email notification
1082 # set some variables for email notification
1081 pr_target_repo_url = h.route_url(
1083 pr_target_repo_url = h.route_url(
1082 'repo_summary', repo_name=pr_target_repo.repo_name)
1084 'repo_summary', repo_name=pr_target_repo.repo_name)
1083
1085
1084 pr_source_repo_url = h.route_url(
1086 pr_source_repo_url = h.route_url(
1085 'repo_summary', repo_name=pr_source_repo.repo_name)
1087 'repo_summary', repo_name=pr_source_repo.repo_name)
1086
1088
1087 # pull request specifics
1089 # pull request specifics
1088 pull_request_commits = [
1090 pull_request_commits = [
1089 (x.raw_id, x.message)
1091 (x.raw_id, x.message)
1090 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1092 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1091
1093
1092 kwargs = {
1094 kwargs = {
1093 'user': pull_request.author,
1095 'user': pull_request.author,
1094 'pull_request': pull_request_obj,
1096 'pull_request': pull_request_obj,
1095 'pull_request_commits': pull_request_commits,
1097 'pull_request_commits': pull_request_commits,
1096
1098
1097 'pull_request_target_repo': pr_target_repo,
1099 'pull_request_target_repo': pr_target_repo,
1098 'pull_request_target_repo_url': pr_target_repo_url,
1100 'pull_request_target_repo_url': pr_target_repo_url,
1099
1101
1100 'pull_request_source_repo': pr_source_repo,
1102 'pull_request_source_repo': pr_source_repo,
1101 'pull_request_source_repo_url': pr_source_repo_url,
1103 'pull_request_source_repo_url': pr_source_repo_url,
1102
1104
1103 'pull_request_url': pr_url,
1105 'pull_request_url': pr_url,
1104 }
1106 }
1105
1107
1106 # pre-generate the subject for notification itself
1108 # pre-generate the subject for notification itself
1107 (subject,
1109 (subject,
1108 _h, _e, # we don't care about those
1110 _h, _e, # we don't care about those
1109 body_plaintext) = EmailNotificationModel().render_email(
1111 body_plaintext) = EmailNotificationModel().render_email(
1110 notification_type, **kwargs)
1112 notification_type, **kwargs)
1111
1113
1112 # create notification objects, and emails
1114 # create notification objects, and emails
1113 NotificationModel().create(
1115 NotificationModel().create(
1114 created_by=pull_request.author,
1116 created_by=pull_request.author,
1115 notification_subject=subject,
1117 notification_subject=subject,
1116 notification_body=body_plaintext,
1118 notification_body=body_plaintext,
1117 notification_type=notification_type,
1119 notification_type=notification_type,
1118 recipients=recipients,
1120 recipients=recipients,
1119 email_kwargs=kwargs,
1121 email_kwargs=kwargs,
1120 )
1122 )
1121
1123
1122 def delete(self, pull_request, user):
1124 def delete(self, pull_request, user):
1123 pull_request = self.__get_pull_request(pull_request)
1125 pull_request = self.__get_pull_request(pull_request)
1124 old_data = pull_request.get_api_data(with_merge_state=False)
1126 old_data = pull_request.get_api_data(with_merge_state=False)
1125 self._cleanup_merge_workspace(pull_request)
1127 self._cleanup_merge_workspace(pull_request)
1126 self._log_audit_action(
1128 self._log_audit_action(
1127 'repo.pull_request.delete', {'old_data': old_data},
1129 'repo.pull_request.delete', {'old_data': old_data},
1128 user, pull_request)
1130 user, pull_request)
1129 Session().delete(pull_request)
1131 Session().delete(pull_request)
1130
1132
1131 def close_pull_request(self, pull_request, user):
1133 def close_pull_request(self, pull_request, user):
1132 pull_request = self.__get_pull_request(pull_request)
1134 pull_request = self.__get_pull_request(pull_request)
1133 self._cleanup_merge_workspace(pull_request)
1135 self._cleanup_merge_workspace(pull_request)
1134 pull_request.status = PullRequest.STATUS_CLOSED
1136 pull_request.status = PullRequest.STATUS_CLOSED
1135 pull_request.updated_on = datetime.datetime.now()
1137 pull_request.updated_on = datetime.datetime.now()
1136 Session().add(pull_request)
1138 Session().add(pull_request)
1137 self._trigger_pull_request_hook(
1139 self._trigger_pull_request_hook(
1138 pull_request, pull_request.author, 'close')
1140 pull_request, pull_request.author, 'close')
1139
1141
1140 pr_data = pull_request.get_api_data(with_merge_state=False)
1142 pr_data = pull_request.get_api_data(with_merge_state=False)
1141 self._log_audit_action(
1143 self._log_audit_action(
1142 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1144 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1143
1145
1144 def close_pull_request_with_comment(
1146 def close_pull_request_with_comment(
1145 self, pull_request, user, repo, message=None, auth_user=None):
1147 self, pull_request, user, repo, message=None, auth_user=None):
1146
1148
1147 pull_request_review_status = pull_request.calculated_review_status()
1149 pull_request_review_status = pull_request.calculated_review_status()
1148
1150
1149 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1151 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1150 # approved only if we have voting consent
1152 # approved only if we have voting consent
1151 status = ChangesetStatus.STATUS_APPROVED
1153 status = ChangesetStatus.STATUS_APPROVED
1152 else:
1154 else:
1153 status = ChangesetStatus.STATUS_REJECTED
1155 status = ChangesetStatus.STATUS_REJECTED
1154 status_lbl = ChangesetStatus.get_status_lbl(status)
1156 status_lbl = ChangesetStatus.get_status_lbl(status)
1155
1157
1156 default_message = (
1158 default_message = (
1157 'Closing with status change {transition_icon} {status}.'
1159 'Closing with status change {transition_icon} {status}.'
1158 ).format(transition_icon='>', status=status_lbl)
1160 ).format(transition_icon='>', status=status_lbl)
1159 text = message or default_message
1161 text = message or default_message
1160
1162
1161 # create a comment, and link it to new status
1163 # create a comment, and link it to new status
1162 comment = CommentsModel().create(
1164 comment = CommentsModel().create(
1163 text=text,
1165 text=text,
1164 repo=repo.repo_id,
1166 repo=repo.repo_id,
1165 user=user.user_id,
1167 user=user.user_id,
1166 pull_request=pull_request.pull_request_id,
1168 pull_request=pull_request.pull_request_id,
1167 status_change=status_lbl,
1169 status_change=status_lbl,
1168 status_change_type=status,
1170 status_change_type=status,
1169 closing_pr=True,
1171 closing_pr=True,
1170 auth_user=auth_user,
1172 auth_user=auth_user,
1171 )
1173 )
1172
1174
1173 # calculate old status before we change it
1175 # calculate old status before we change it
1174 old_calculated_status = pull_request.calculated_review_status()
1176 old_calculated_status = pull_request.calculated_review_status()
1175 ChangesetStatusModel().set_status(
1177 ChangesetStatusModel().set_status(
1176 repo.repo_id,
1178 repo.repo_id,
1177 status,
1179 status,
1178 user.user_id,
1180 user.user_id,
1179 comment=comment,
1181 comment=comment,
1180 pull_request=pull_request.pull_request_id
1182 pull_request=pull_request.pull_request_id
1181 )
1183 )
1182
1184
1183 Session().flush()
1185 Session().flush()
1184 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1186 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1185 # we now calculate the status of pull request again, and based on that
1187 # we now calculate the status of pull request again, and based on that
1186 # calculation trigger status change. This might happen in cases
1188 # calculation trigger status change. This might happen in cases
1187 # that non-reviewer admin closes a pr, which means his vote doesn't
1189 # that non-reviewer admin closes a pr, which means his vote doesn't
1188 # change the status, while if he's a reviewer this might change it.
1190 # change the status, while if he's a reviewer this might change it.
1189 calculated_status = pull_request.calculated_review_status()
1191 calculated_status = pull_request.calculated_review_status()
1190 if old_calculated_status != calculated_status:
1192 if old_calculated_status != calculated_status:
1191 self._trigger_pull_request_hook(
1193 self._trigger_pull_request_hook(
1192 pull_request, user, 'review_status_change')
1194 pull_request, user, 'review_status_change')
1193
1195
1194 # finally close the PR
1196 # finally close the PR
1195 PullRequestModel().close_pull_request(
1197 PullRequestModel().close_pull_request(
1196 pull_request.pull_request_id, user)
1198 pull_request.pull_request_id, user)
1197
1199
1198 return comment, status
1200 return comment, status
1199
1201
1200 def merge_status(self, pull_request, translator=None,
1202 def merge_status(self, pull_request, translator=None,
1201 force_shadow_repo_refresh=False):
1203 force_shadow_repo_refresh=False):
1202 _ = translator or get_current_request().translate
1204 _ = translator or get_current_request().translate
1203
1205
1204 if not self._is_merge_enabled(pull_request):
1206 if not self._is_merge_enabled(pull_request):
1205 return False, _('Server-side pull request merging is disabled.')
1207 return False, _('Server-side pull request merging is disabled.')
1206 if pull_request.is_closed():
1208 if pull_request.is_closed():
1207 return False, _('This pull request is closed.')
1209 return False, _('This pull request is closed.')
1208 merge_possible, msg = self._check_repo_requirements(
1210 merge_possible, msg = self._check_repo_requirements(
1209 target=pull_request.target_repo, source=pull_request.source_repo,
1211 target=pull_request.target_repo, source=pull_request.source_repo,
1210 translator=_)
1212 translator=_)
1211 if not merge_possible:
1213 if not merge_possible:
1212 return merge_possible, msg
1214 return merge_possible, msg
1213
1215
1214 try:
1216 try:
1215 resp = self._try_merge(
1217 resp = self._try_merge(
1216 pull_request,
1218 pull_request,
1217 force_shadow_repo_refresh=force_shadow_repo_refresh)
1219 force_shadow_repo_refresh=force_shadow_repo_refresh)
1218 log.debug("Merge response: %s", resp)
1220 log.debug("Merge response: %s", resp)
1219 status = resp.possible, resp.merge_status_message
1221 status = resp.possible, resp.merge_status_message
1220 except NotImplementedError:
1222 except NotImplementedError:
1221 status = False, _('Pull request merging is not supported.')
1223 status = False, _('Pull request merging is not supported.')
1222
1224
1223 return status
1225 return status
1224
1226
1225 def _check_repo_requirements(self, target, source, translator):
1227 def _check_repo_requirements(self, target, source, translator):
1226 """
1228 """
1227 Check if `target` and `source` have compatible requirements.
1229 Check if `target` and `source` have compatible requirements.
1228
1230
1229 Currently this is just checking for largefiles.
1231 Currently this is just checking for largefiles.
1230 """
1232 """
1231 _ = translator
1233 _ = translator
1232 target_has_largefiles = self._has_largefiles(target)
1234 target_has_largefiles = self._has_largefiles(target)
1233 source_has_largefiles = self._has_largefiles(source)
1235 source_has_largefiles = self._has_largefiles(source)
1234 merge_possible = True
1236 merge_possible = True
1235 message = u''
1237 message = u''
1236
1238
1237 if target_has_largefiles != source_has_largefiles:
1239 if target_has_largefiles != source_has_largefiles:
1238 merge_possible = False
1240 merge_possible = False
1239 if source_has_largefiles:
1241 if source_has_largefiles:
1240 message = _(
1242 message = _(
1241 'Target repository large files support is disabled.')
1243 'Target repository large files support is disabled.')
1242 else:
1244 else:
1243 message = _(
1245 message = _(
1244 'Source repository large files support is disabled.')
1246 'Source repository large files support is disabled.')
1245
1247
1246 return merge_possible, message
1248 return merge_possible, message
1247
1249
1248 def _has_largefiles(self, repo):
1250 def _has_largefiles(self, repo):
1249 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1251 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1250 'extensions', 'largefiles')
1252 'extensions', 'largefiles')
1251 return largefiles_ui and largefiles_ui[0].active
1253 return largefiles_ui and largefiles_ui[0].active
1252
1254
1253 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1255 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1254 """
1256 """
1255 Try to merge the pull request and return the merge status.
1257 Try to merge the pull request and return the merge status.
1256 """
1258 """
1257 log.debug(
1259 log.debug(
1258 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1260 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1259 pull_request.pull_request_id, force_shadow_repo_refresh)
1261 pull_request.pull_request_id, force_shadow_repo_refresh)
1260 target_vcs = pull_request.target_repo.scm_instance()
1262 target_vcs = pull_request.target_repo.scm_instance()
1261 # Refresh the target reference.
1263 # Refresh the target reference.
1262 try:
1264 try:
1263 target_ref = self._refresh_reference(
1265 target_ref = self._refresh_reference(
1264 pull_request.target_ref_parts, target_vcs)
1266 pull_request.target_ref_parts, target_vcs)
1265 except CommitDoesNotExistError:
1267 except CommitDoesNotExistError:
1266 merge_state = MergeResponse(
1268 merge_state = MergeResponse(
1267 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1269 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1268 metadata={'target_ref': pull_request.target_ref_parts})
1270 metadata={'target_ref': pull_request.target_ref_parts})
1269 return merge_state
1271 return merge_state
1270
1272
1271 target_locked = pull_request.target_repo.locked
1273 target_locked = pull_request.target_repo.locked
1272 if target_locked and target_locked[0]:
1274 if target_locked and target_locked[0]:
1273 locked_by = 'user:{}'.format(target_locked[0])
1275 locked_by = 'user:{}'.format(target_locked[0])
1274 log.debug("The target repository is locked by %s.", locked_by)
1276 log.debug("The target repository is locked by %s.", locked_by)
1275 merge_state = MergeResponse(
1277 merge_state = MergeResponse(
1276 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1278 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1277 metadata={'locked_by': locked_by})
1279 metadata={'locked_by': locked_by})
1278 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1280 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1279 pull_request, target_ref):
1281 pull_request, target_ref):
1280 log.debug("Refreshing the merge status of the repository.")
1282 log.debug("Refreshing the merge status of the repository.")
1281 merge_state = self._refresh_merge_state(
1283 merge_state = self._refresh_merge_state(
1282 pull_request, target_vcs, target_ref)
1284 pull_request, target_vcs, target_ref)
1283 else:
1285 else:
1284 possible = pull_request.\
1286 possible = pull_request.\
1285 last_merge_status == MergeFailureReason.NONE
1287 last_merge_status == MergeFailureReason.NONE
1286 merge_state = MergeResponse(
1288 merge_state = MergeResponse(
1287 possible, False, None, pull_request.last_merge_status)
1289 possible, False, None, pull_request.last_merge_status)
1288
1290
1289 return merge_state
1291 return merge_state
1290
1292
1291 def _refresh_reference(self, reference, vcs_repository):
1293 def _refresh_reference(self, reference, vcs_repository):
1292 if reference.type in ('branch', 'book'):
1294 if reference.type in self.UPDATABLE_REF_TYPES:
1293 name_or_id = reference.name
1295 name_or_id = reference.name
1294 else:
1296 else:
1295 name_or_id = reference.commit_id
1297 name_or_id = reference.commit_id
1296 refreshed_commit = vcs_repository.get_commit(name_or_id)
1298 refreshed_commit = vcs_repository.get_commit(name_or_id)
1297 refreshed_reference = Reference(
1299 refreshed_reference = Reference(
1298 reference.type, reference.name, refreshed_commit.raw_id)
1300 reference.type, reference.name, refreshed_commit.raw_id)
1299 return refreshed_reference
1301 return refreshed_reference
1300
1302
1301 def _needs_merge_state_refresh(self, pull_request, target_reference):
1303 def _needs_merge_state_refresh(self, pull_request, target_reference):
1302 return not(
1304 return not(
1303 pull_request.revisions and
1305 pull_request.revisions and
1304 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1306 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1305 target_reference.commit_id == pull_request._last_merge_target_rev)
1307 target_reference.commit_id == pull_request._last_merge_target_rev)
1306
1308
1307 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1309 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1308 workspace_id = self._workspace_id(pull_request)
1310 workspace_id = self._workspace_id(pull_request)
1309 source_vcs = pull_request.source_repo.scm_instance()
1311 source_vcs = pull_request.source_repo.scm_instance()
1310 repo_id = pull_request.target_repo.repo_id
1312 repo_id = pull_request.target_repo.repo_id
1311 use_rebase = self._use_rebase_for_merging(pull_request)
1313 use_rebase = self._use_rebase_for_merging(pull_request)
1312 close_branch = self._close_branch_before_merging(pull_request)
1314 close_branch = self._close_branch_before_merging(pull_request)
1313 merge_state = target_vcs.merge(
1315 merge_state = target_vcs.merge(
1314 repo_id, workspace_id,
1316 repo_id, workspace_id,
1315 target_reference, source_vcs, pull_request.source_ref_parts,
1317 target_reference, source_vcs, pull_request.source_ref_parts,
1316 dry_run=True, use_rebase=use_rebase,
1318 dry_run=True, use_rebase=use_rebase,
1317 close_branch=close_branch)
1319 close_branch=close_branch)
1318
1320
1319 # Do not store the response if there was an unknown error.
1321 # Do not store the response if there was an unknown error.
1320 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1322 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1321 pull_request._last_merge_source_rev = \
1323 pull_request._last_merge_source_rev = \
1322 pull_request.source_ref_parts.commit_id
1324 pull_request.source_ref_parts.commit_id
1323 pull_request._last_merge_target_rev = target_reference.commit_id
1325 pull_request._last_merge_target_rev = target_reference.commit_id
1324 pull_request.last_merge_status = merge_state.failure_reason
1326 pull_request.last_merge_status = merge_state.failure_reason
1325 pull_request.shadow_merge_ref = merge_state.merge_ref
1327 pull_request.shadow_merge_ref = merge_state.merge_ref
1326 Session().add(pull_request)
1328 Session().add(pull_request)
1327 Session().commit()
1329 Session().commit()
1328
1330
1329 return merge_state
1331 return merge_state
1330
1332
1331 def _workspace_id(self, pull_request):
1333 def _workspace_id(self, pull_request):
1332 workspace_id = 'pr-%s' % pull_request.pull_request_id
1334 workspace_id = 'pr-%s' % pull_request.pull_request_id
1333 return workspace_id
1335 return workspace_id
1334
1336
1335 def generate_repo_data(self, repo, commit_id=None, branch=None,
1337 def generate_repo_data(self, repo, commit_id=None, branch=None,
1336 bookmark=None, translator=None):
1338 bookmark=None, translator=None):
1337 from rhodecode.model.repo import RepoModel
1339 from rhodecode.model.repo import RepoModel
1338
1340
1339 all_refs, selected_ref = \
1341 all_refs, selected_ref = \
1340 self._get_repo_pullrequest_sources(
1342 self._get_repo_pullrequest_sources(
1341 repo.scm_instance(), commit_id=commit_id,
1343 repo.scm_instance(), commit_id=commit_id,
1342 branch=branch, bookmark=bookmark, translator=translator)
1344 branch=branch, bookmark=bookmark, translator=translator)
1343
1345
1344 refs_select2 = []
1346 refs_select2 = []
1345 for element in all_refs:
1347 for element in all_refs:
1346 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1348 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1347 refs_select2.append({'text': element[1], 'children': children})
1349 refs_select2.append({'text': element[1], 'children': children})
1348
1350
1349 return {
1351 return {
1350 'user': {
1352 'user': {
1351 'user_id': repo.user.user_id,
1353 'user_id': repo.user.user_id,
1352 'username': repo.user.username,
1354 'username': repo.user.username,
1353 'firstname': repo.user.first_name,
1355 'firstname': repo.user.first_name,
1354 'lastname': repo.user.last_name,
1356 'lastname': repo.user.last_name,
1355 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1357 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1356 },
1358 },
1357 'name': repo.repo_name,
1359 'name': repo.repo_name,
1358 'link': RepoModel().get_url(repo),
1360 'link': RepoModel().get_url(repo),
1359 'description': h.chop_at_smart(repo.description_safe, '\n'),
1361 'description': h.chop_at_smart(repo.description_safe, '\n'),
1360 'refs': {
1362 'refs': {
1361 'all_refs': all_refs,
1363 'all_refs': all_refs,
1362 'selected_ref': selected_ref,
1364 'selected_ref': selected_ref,
1363 'select2_refs': refs_select2
1365 'select2_refs': refs_select2
1364 }
1366 }
1365 }
1367 }
1366
1368
1367 def generate_pullrequest_title(self, source, source_ref, target):
1369 def generate_pullrequest_title(self, source, source_ref, target):
1368 return u'{source}#{at_ref} to {target}'.format(
1370 return u'{source}#{at_ref} to {target}'.format(
1369 source=source,
1371 source=source,
1370 at_ref=source_ref,
1372 at_ref=source_ref,
1371 target=target,
1373 target=target,
1372 )
1374 )
1373
1375
1374 def _cleanup_merge_workspace(self, pull_request):
1376 def _cleanup_merge_workspace(self, pull_request):
1375 # Merging related cleanup
1377 # Merging related cleanup
1376 repo_id = pull_request.target_repo.repo_id
1378 repo_id = pull_request.target_repo.repo_id
1377 target_scm = pull_request.target_repo.scm_instance()
1379 target_scm = pull_request.target_repo.scm_instance()
1378 workspace_id = self._workspace_id(pull_request)
1380 workspace_id = self._workspace_id(pull_request)
1379
1381
1380 try:
1382 try:
1381 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1383 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1382 except NotImplementedError:
1384 except NotImplementedError:
1383 pass
1385 pass
1384
1386
1385 def _get_repo_pullrequest_sources(
1387 def _get_repo_pullrequest_sources(
1386 self, repo, commit_id=None, branch=None, bookmark=None,
1388 self, repo, commit_id=None, branch=None, bookmark=None,
1387 translator=None):
1389 translator=None):
1388 """
1390 """
1389 Return a structure with repo's interesting commits, suitable for
1391 Return a structure with repo's interesting commits, suitable for
1390 the selectors in pullrequest controller
1392 the selectors in pullrequest controller
1391
1393
1392 :param commit_id: a commit that must be in the list somehow
1394 :param commit_id: a commit that must be in the list somehow
1393 and selected by default
1395 and selected by default
1394 :param branch: a branch that must be in the list and selected
1396 :param branch: a branch that must be in the list and selected
1395 by default - even if closed
1397 by default - even if closed
1396 :param bookmark: a bookmark that must be in the list and selected
1398 :param bookmark: a bookmark that must be in the list and selected
1397 """
1399 """
1398 _ = translator or get_current_request().translate
1400 _ = translator or get_current_request().translate
1399
1401
1400 commit_id = safe_str(commit_id) if commit_id else None
1402 commit_id = safe_str(commit_id) if commit_id else None
1401 branch = safe_str(branch) if branch else None
1403 branch = safe_str(branch) if branch else None
1402 bookmark = safe_str(bookmark) if bookmark else None
1404 bookmark = safe_str(bookmark) if bookmark else None
1403
1405
1404 selected = None
1406 selected = None
1405
1407
1406 # order matters: first source that has commit_id in it will be selected
1408 # order matters: first source that has commit_id in it will be selected
1407 sources = []
1409 sources = []
1408 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1410 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1409 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1411 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1410
1412
1411 if commit_id:
1413 if commit_id:
1412 ref_commit = (h.short_id(commit_id), commit_id)
1414 ref_commit = (h.short_id(commit_id), commit_id)
1413 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1415 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1414
1416
1415 sources.append(
1417 sources.append(
1416 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1418 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1417 )
1419 )
1418
1420
1419 groups = []
1421 groups = []
1420 for group_key, ref_list, group_name, match in sources:
1422 for group_key, ref_list, group_name, match in sources:
1421 group_refs = []
1423 group_refs = []
1422 for ref_name, ref_id in ref_list:
1424 for ref_name, ref_id in ref_list:
1423 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1425 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1424 group_refs.append((ref_key, ref_name))
1426 group_refs.append((ref_key, ref_name))
1425
1427
1426 if not selected:
1428 if not selected:
1427 if set([commit_id, match]) & set([ref_id, ref_name]):
1429 if set([commit_id, match]) & set([ref_id, ref_name]):
1428 selected = ref_key
1430 selected = ref_key
1429
1431
1430 if group_refs:
1432 if group_refs:
1431 groups.append((group_refs, group_name))
1433 groups.append((group_refs, group_name))
1432
1434
1433 if not selected:
1435 if not selected:
1434 ref = commit_id or branch or bookmark
1436 ref = commit_id or branch or bookmark
1435 if ref:
1437 if ref:
1436 raise CommitDoesNotExistError(
1438 raise CommitDoesNotExistError(
1437 'No commit refs could be found matching: %s' % ref)
1439 'No commit refs could be found matching: %s' % ref)
1438 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1440 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1439 selected = 'branch:%s:%s' % (
1441 selected = 'branch:%s:%s' % (
1440 repo.DEFAULT_BRANCH_NAME,
1442 repo.DEFAULT_BRANCH_NAME,
1441 repo.branches[repo.DEFAULT_BRANCH_NAME]
1443 repo.branches[repo.DEFAULT_BRANCH_NAME]
1442 )
1444 )
1443 elif repo.commit_ids:
1445 elif repo.commit_ids:
1444 # make the user select in this case
1446 # make the user select in this case
1445 selected = None
1447 selected = None
1446 else:
1448 else:
1447 raise EmptyRepositoryError()
1449 raise EmptyRepositoryError()
1448 return groups, selected
1450 return groups, selected
1449
1451
1450 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1452 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1451 hide_whitespace_changes, diff_context):
1453 hide_whitespace_changes, diff_context):
1452
1454
1453 return self._get_diff_from_pr_or_version(
1455 return self._get_diff_from_pr_or_version(
1454 source_repo, source_ref_id, target_ref_id,
1456 source_repo, source_ref_id, target_ref_id,
1455 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1457 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1456
1458
1457 def _get_diff_from_pr_or_version(
1459 def _get_diff_from_pr_or_version(
1458 self, source_repo, source_ref_id, target_ref_id,
1460 self, source_repo, source_ref_id, target_ref_id,
1459 hide_whitespace_changes, diff_context):
1461 hide_whitespace_changes, diff_context):
1460
1462
1461 target_commit = source_repo.get_commit(
1463 target_commit = source_repo.get_commit(
1462 commit_id=safe_str(target_ref_id))
1464 commit_id=safe_str(target_ref_id))
1463 source_commit = source_repo.get_commit(
1465 source_commit = source_repo.get_commit(
1464 commit_id=safe_str(source_ref_id))
1466 commit_id=safe_str(source_ref_id))
1465 if isinstance(source_repo, Repository):
1467 if isinstance(source_repo, Repository):
1466 vcs_repo = source_repo.scm_instance()
1468 vcs_repo = source_repo.scm_instance()
1467 else:
1469 else:
1468 vcs_repo = source_repo
1470 vcs_repo = source_repo
1469
1471
1470 # TODO: johbo: In the context of an update, we cannot reach
1472 # TODO: johbo: In the context of an update, we cannot reach
1471 # the old commit anymore with our normal mechanisms. It needs
1473 # the old commit anymore with our normal mechanisms. It needs
1472 # some sort of special support in the vcs layer to avoid this
1474 # some sort of special support in the vcs layer to avoid this
1473 # workaround.
1475 # workaround.
1474 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1476 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1475 vcs_repo.alias == 'git'):
1477 vcs_repo.alias == 'git'):
1476 source_commit.raw_id = safe_str(source_ref_id)
1478 source_commit.raw_id = safe_str(source_ref_id)
1477
1479
1478 log.debug('calculating diff between '
1480 log.debug('calculating diff between '
1479 'source_ref:%s and target_ref:%s for repo `%s`',
1481 'source_ref:%s and target_ref:%s for repo `%s`',
1480 target_ref_id, source_ref_id,
1482 target_ref_id, source_ref_id,
1481 safe_unicode(vcs_repo.path))
1483 safe_unicode(vcs_repo.path))
1482
1484
1483 vcs_diff = vcs_repo.get_diff(
1485 vcs_diff = vcs_repo.get_diff(
1484 commit1=target_commit, commit2=source_commit,
1486 commit1=target_commit, commit2=source_commit,
1485 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1487 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1486 return vcs_diff
1488 return vcs_diff
1487
1489
1488 def _is_merge_enabled(self, pull_request):
1490 def _is_merge_enabled(self, pull_request):
1489 return self._get_general_setting(
1491 return self._get_general_setting(
1490 pull_request, 'rhodecode_pr_merge_enabled')
1492 pull_request, 'rhodecode_pr_merge_enabled')
1491
1493
1492 def _use_rebase_for_merging(self, pull_request):
1494 def _use_rebase_for_merging(self, pull_request):
1493 repo_type = pull_request.target_repo.repo_type
1495 repo_type = pull_request.target_repo.repo_type
1494 if repo_type == 'hg':
1496 if repo_type == 'hg':
1495 return self._get_general_setting(
1497 return self._get_general_setting(
1496 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1498 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1497 elif repo_type == 'git':
1499 elif repo_type == 'git':
1498 return self._get_general_setting(
1500 return self._get_general_setting(
1499 pull_request, 'rhodecode_git_use_rebase_for_merging')
1501 pull_request, 'rhodecode_git_use_rebase_for_merging')
1500
1502
1501 return False
1503 return False
1502
1504
1503 def _close_branch_before_merging(self, pull_request):
1505 def _close_branch_before_merging(self, pull_request):
1504 repo_type = pull_request.target_repo.repo_type
1506 repo_type = pull_request.target_repo.repo_type
1505 if repo_type == 'hg':
1507 if repo_type == 'hg':
1506 return self._get_general_setting(
1508 return self._get_general_setting(
1507 pull_request, 'rhodecode_hg_close_branch_before_merging')
1509 pull_request, 'rhodecode_hg_close_branch_before_merging')
1508 elif repo_type == 'git':
1510 elif repo_type == 'git':
1509 return self._get_general_setting(
1511 return self._get_general_setting(
1510 pull_request, 'rhodecode_git_close_branch_before_merging')
1512 pull_request, 'rhodecode_git_close_branch_before_merging')
1511
1513
1512 return False
1514 return False
1513
1515
1514 def _get_general_setting(self, pull_request, settings_key, default=False):
1516 def _get_general_setting(self, pull_request, settings_key, default=False):
1515 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1517 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1516 settings = settings_model.get_general_settings()
1518 settings = settings_model.get_general_settings()
1517 return settings.get(settings_key, default)
1519 return settings.get(settings_key, default)
1518
1520
1519 def _log_audit_action(self, action, action_data, user, pull_request):
1521 def _log_audit_action(self, action, action_data, user, pull_request):
1520 audit_logger.store(
1522 audit_logger.store(
1521 action=action,
1523 action=action,
1522 action_data=action_data,
1524 action_data=action_data,
1523 user=user,
1525 user=user,
1524 repo=pull_request.target_repo)
1526 repo=pull_request.target_repo)
1525
1527
1526 def get_reviewer_functions(self):
1528 def get_reviewer_functions(self):
1527 """
1529 """
1528 Fetches functions for validation and fetching default reviewers.
1530 Fetches functions for validation and fetching default reviewers.
1529 If available we use the EE package, else we fallback to CE
1531 If available we use the EE package, else we fallback to CE
1530 package functions
1532 package functions
1531 """
1533 """
1532 try:
1534 try:
1533 from rc_reviewers.utils import get_default_reviewers_data
1535 from rc_reviewers.utils import get_default_reviewers_data
1534 from rc_reviewers.utils import validate_default_reviewers
1536 from rc_reviewers.utils import validate_default_reviewers
1535 except ImportError:
1537 except ImportError:
1536 from rhodecode.apps.repository.utils import get_default_reviewers_data
1538 from rhodecode.apps.repository.utils import get_default_reviewers_data
1537 from rhodecode.apps.repository.utils import validate_default_reviewers
1539 from rhodecode.apps.repository.utils import validate_default_reviewers
1538
1540
1539 return get_default_reviewers_data, validate_default_reviewers
1541 return get_default_reviewers_data, validate_default_reviewers
1540
1542
1541
1543
1542 class MergeCheck(object):
1544 class MergeCheck(object):
1543 """
1545 """
1544 Perform Merge Checks and returns a check object which stores information
1546 Perform Merge Checks and returns a check object which stores information
1545 about merge errors, and merge conditions
1547 about merge errors, and merge conditions
1546 """
1548 """
1547 TODO_CHECK = 'todo'
1549 TODO_CHECK = 'todo'
1548 PERM_CHECK = 'perm'
1550 PERM_CHECK = 'perm'
1549 REVIEW_CHECK = 'review'
1551 REVIEW_CHECK = 'review'
1550 MERGE_CHECK = 'merge'
1552 MERGE_CHECK = 'merge'
1551
1553
1552 def __init__(self):
1554 def __init__(self):
1553 self.review_status = None
1555 self.review_status = None
1554 self.merge_possible = None
1556 self.merge_possible = None
1555 self.merge_msg = ''
1557 self.merge_msg = ''
1556 self.failed = None
1558 self.failed = None
1557 self.errors = []
1559 self.errors = []
1558 self.error_details = OrderedDict()
1560 self.error_details = OrderedDict()
1559
1561
1560 def push_error(self, error_type, message, error_key, details):
1562 def push_error(self, error_type, message, error_key, details):
1561 self.failed = True
1563 self.failed = True
1562 self.errors.append([error_type, message])
1564 self.errors.append([error_type, message])
1563 self.error_details[error_key] = dict(
1565 self.error_details[error_key] = dict(
1564 details=details,
1566 details=details,
1565 error_type=error_type,
1567 error_type=error_type,
1566 message=message
1568 message=message
1567 )
1569 )
1568
1570
1569 @classmethod
1571 @classmethod
1570 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1572 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1571 force_shadow_repo_refresh=False):
1573 force_shadow_repo_refresh=False):
1572 _ = translator
1574 _ = translator
1573 merge_check = cls()
1575 merge_check = cls()
1574
1576
1575 # permissions to merge
1577 # permissions to merge
1576 user_allowed_to_merge = PullRequestModel().check_user_merge(
1578 user_allowed_to_merge = PullRequestModel().check_user_merge(
1577 pull_request, auth_user)
1579 pull_request, auth_user)
1578 if not user_allowed_to_merge:
1580 if not user_allowed_to_merge:
1579 log.debug("MergeCheck: cannot merge, approval is pending.")
1581 log.debug("MergeCheck: cannot merge, approval is pending.")
1580
1582
1581 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1583 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1582 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1584 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1583 if fail_early:
1585 if fail_early:
1584 return merge_check
1586 return merge_check
1585
1587
1586 # permission to merge into the target branch
1588 # permission to merge into the target branch
1587 target_commit_id = pull_request.target_ref_parts.commit_id
1589 target_commit_id = pull_request.target_ref_parts.commit_id
1588 if pull_request.target_ref_parts.type == 'branch':
1590 if pull_request.target_ref_parts.type == 'branch':
1589 branch_name = pull_request.target_ref_parts.name
1591 branch_name = pull_request.target_ref_parts.name
1590 else:
1592 else:
1591 # for mercurial we can always figure out the branch from the commit
1593 # for mercurial we can always figure out the branch from the commit
1592 # in case of bookmark
1594 # in case of bookmark
1593 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1595 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1594 branch_name = target_commit.branch
1596 branch_name = target_commit.branch
1595
1597
1596 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1598 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1597 pull_request.target_repo.repo_name, branch_name)
1599 pull_request.target_repo.repo_name, branch_name)
1598 if branch_perm and branch_perm == 'branch.none':
1600 if branch_perm and branch_perm == 'branch.none':
1599 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1601 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1600 branch_name, rule)
1602 branch_name, rule)
1601 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1603 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1602 if fail_early:
1604 if fail_early:
1603 return merge_check
1605 return merge_check
1604
1606
1605 # review status, must be always present
1607 # review status, must be always present
1606 review_status = pull_request.calculated_review_status()
1608 review_status = pull_request.calculated_review_status()
1607 merge_check.review_status = review_status
1609 merge_check.review_status = review_status
1608
1610
1609 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1611 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1610 if not status_approved:
1612 if not status_approved:
1611 log.debug("MergeCheck: cannot merge, approval is pending.")
1613 log.debug("MergeCheck: cannot merge, approval is pending.")
1612
1614
1613 msg = _('Pull request reviewer approval is pending.')
1615 msg = _('Pull request reviewer approval is pending.')
1614
1616
1615 merge_check.push_error(
1617 merge_check.push_error(
1616 'warning', msg, cls.REVIEW_CHECK, review_status)
1618 'warning', msg, cls.REVIEW_CHECK, review_status)
1617
1619
1618 if fail_early:
1620 if fail_early:
1619 return merge_check
1621 return merge_check
1620
1622
1621 # left over TODOs
1623 # left over TODOs
1622 todos = CommentsModel().get_unresolved_todos(pull_request)
1624 todos = CommentsModel().get_unresolved_todos(pull_request)
1623 if todos:
1625 if todos:
1624 log.debug("MergeCheck: cannot merge, {} "
1626 log.debug("MergeCheck: cannot merge, {} "
1625 "unresolved todos left.".format(len(todos)))
1627 "unresolved todos left.".format(len(todos)))
1626
1628
1627 if len(todos) == 1:
1629 if len(todos) == 1:
1628 msg = _('Cannot merge, {} TODO still not resolved.').format(
1630 msg = _('Cannot merge, {} TODO still not resolved.').format(
1629 len(todos))
1631 len(todos))
1630 else:
1632 else:
1631 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1633 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1632 len(todos))
1634 len(todos))
1633
1635
1634 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1636 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1635
1637
1636 if fail_early:
1638 if fail_early:
1637 return merge_check
1639 return merge_check
1638
1640
1639 # merge possible, here is the filesystem simulation + shadow repo
1641 # merge possible, here is the filesystem simulation + shadow repo
1640 merge_status, msg = PullRequestModel().merge_status(
1642 merge_status, msg = PullRequestModel().merge_status(
1641 pull_request, translator=translator,
1643 pull_request, translator=translator,
1642 force_shadow_repo_refresh=force_shadow_repo_refresh)
1644 force_shadow_repo_refresh=force_shadow_repo_refresh)
1643 merge_check.merge_possible = merge_status
1645 merge_check.merge_possible = merge_status
1644 merge_check.merge_msg = msg
1646 merge_check.merge_msg = msg
1645 if not merge_status:
1647 if not merge_status:
1646 log.debug(
1648 log.debug(
1647 "MergeCheck: cannot merge, pull request merge not possible.")
1649 "MergeCheck: cannot merge, pull request merge not possible.")
1648 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1650 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1649
1651
1650 if fail_early:
1652 if fail_early:
1651 return merge_check
1653 return merge_check
1652
1654
1653 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1655 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1654 return merge_check
1656 return merge_check
1655
1657
1656 @classmethod
1658 @classmethod
1657 def get_merge_conditions(cls, pull_request, translator):
1659 def get_merge_conditions(cls, pull_request, translator):
1658 _ = translator
1660 _ = translator
1659 merge_details = {}
1661 merge_details = {}
1660
1662
1661 model = PullRequestModel()
1663 model = PullRequestModel()
1662 use_rebase = model._use_rebase_for_merging(pull_request)
1664 use_rebase = model._use_rebase_for_merging(pull_request)
1663
1665
1664 if use_rebase:
1666 if use_rebase:
1665 merge_details['merge_strategy'] = dict(
1667 merge_details['merge_strategy'] = dict(
1666 details={},
1668 details={},
1667 message=_('Merge strategy: rebase')
1669 message=_('Merge strategy: rebase')
1668 )
1670 )
1669 else:
1671 else:
1670 merge_details['merge_strategy'] = dict(
1672 merge_details['merge_strategy'] = dict(
1671 details={},
1673 details={},
1672 message=_('Merge strategy: explicit merge commit')
1674 message=_('Merge strategy: explicit merge commit')
1673 )
1675 )
1674
1676
1675 close_branch = model._close_branch_before_merging(pull_request)
1677 close_branch = model._close_branch_before_merging(pull_request)
1676 if close_branch:
1678 if close_branch:
1677 repo_type = pull_request.target_repo.repo_type
1679 repo_type = pull_request.target_repo.repo_type
1678 if repo_type == 'hg':
1680 if repo_type == 'hg':
1679 close_msg = _('Source branch will be closed after merge.')
1681 close_msg = _('Source branch will be closed after merge.')
1680 elif repo_type == 'git':
1682 elif repo_type == 'git':
1681 close_msg = _('Source branch will be deleted after merge.')
1683 close_msg = _('Source branch will be deleted after merge.')
1682
1684
1683 merge_details['close_branch'] = dict(
1685 merge_details['close_branch'] = dict(
1684 details={},
1686 details={},
1685 message=close_msg
1687 message=close_msg
1686 )
1688 )
1687
1689
1688 return merge_details
1690 return merge_details
1689
1691
1690 ChangeTuple = collections.namedtuple(
1692 ChangeTuple = collections.namedtuple(
1691 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1693 'ChangeTuple', ['added', 'common', 'removed', 'total'])
1692
1694
1693 FileChangeTuple = collections.namedtuple(
1695 FileChangeTuple = collections.namedtuple(
1694 'FileChangeTuple', ['added', 'modified', 'removed'])
1696 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,1023 +1,1025 b''
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
1 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2
2
3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
3 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
4 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 %></%def>
5 %></%def>
6
6
7 <%def name="action_class(action)">
7 <%def name="action_class(action)">
8 <%
8 <%
9 return {
9 return {
10 '-': 'cb-deletion',
10 '-': 'cb-deletion',
11 '+': 'cb-addition',
11 '+': 'cb-addition',
12 ' ': 'cb-context',
12 ' ': 'cb-context',
13 }.get(action, 'cb-empty')
13 }.get(action, 'cb-empty')
14 %>
14 %>
15 </%def>
15 </%def>
16
16
17 <%def name="op_class(op_id)">
17 <%def name="op_class(op_id)">
18 <%
18 <%
19 return {
19 return {
20 DEL_FILENODE: 'deletion', # file deleted
20 DEL_FILENODE: 'deletion', # file deleted
21 BIN_FILENODE: 'warning' # binary diff hidden
21 BIN_FILENODE: 'warning' # binary diff hidden
22 }.get(op_id, 'addition')
22 }.get(op_id, 'addition')
23 %>
23 %>
24 </%def>
24 </%def>
25
25
26
26
27
27
28 <%def name="render_diffset(diffset, commit=None,
28 <%def name="render_diffset(diffset, commit=None,
29
29
30 # collapse all file diff entries when there are more than this amount of files in the diff
30 # collapse all file diff entries when there are more than this amount of files in the diff
31 collapse_when_files_over=20,
31 collapse_when_files_over=20,
32
32
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
33 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 lines_changed_limit=500,
34 lines_changed_limit=500,
35
35
36 # add a ruler at to the output
36 # add a ruler at to the output
37 ruler_at_chars=0,
37 ruler_at_chars=0,
38
38
39 # show inline comments
39 # show inline comments
40 use_comments=False,
40 use_comments=False,
41
41
42 # disable new comments
42 # disable new comments
43 disable_new_comments=False,
43 disable_new_comments=False,
44
44
45 # special file-comments that were deleted in previous versions
45 # special file-comments that were deleted in previous versions
46 # it's used for showing outdated comments for deleted files in a PR
46 # it's used for showing outdated comments for deleted files in a PR
47 deleted_files_comments=None,
47 deleted_files_comments=None,
48
48
49 # for cache purpose
49 # for cache purpose
50 inline_comments=None,
50 inline_comments=None,
51
51
52 )">
52 )">
53 %if use_comments:
53 %if use_comments:
54 <div id="cb-comments-inline-container-template" class="js-template">
54 <div id="cb-comments-inline-container-template" class="js-template">
55 ${inline_comments_container([], inline_comments)}
55 ${inline_comments_container([], inline_comments)}
56 </div>
56 </div>
57 <div class="js-template" id="cb-comment-inline-form-template">
57 <div class="js-template" id="cb-comment-inline-form-template">
58 <div class="comment-inline-form ac">
58 <div class="comment-inline-form ac">
59
59
60 %if c.rhodecode_user.username != h.DEFAULT_USER:
60 %if c.rhodecode_user.username != h.DEFAULT_USER:
61 ## render template for inline comments
61 ## render template for inline comments
62 ${commentblock.comment_form(form_type='inline')}
62 ${commentblock.comment_form(form_type='inline')}
63 %else:
63 %else:
64 ${h.form('', class_='inline-form comment-form-login', method='get')}
64 ${h.form('', class_='inline-form comment-form-login', method='get')}
65 <div class="pull-left">
65 <div class="pull-left">
66 <div class="comment-help pull-right">
66 <div class="comment-help pull-right">
67 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
67 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
68 </div>
68 </div>
69 </div>
69 </div>
70 <div class="comment-button pull-right">
70 <div class="comment-button pull-right">
71 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
71 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
72 ${_('Cancel')}
72 ${_('Cancel')}
73 </button>
73 </button>
74 </div>
74 </div>
75 <div class="clearfix"></div>
75 <div class="clearfix"></div>
76 ${h.end_form()}
76 ${h.end_form()}
77 %endif
77 %endif
78 </div>
78 </div>
79 </div>
79 </div>
80
80
81 %endif
81 %endif
82 <%
82 <%
83 collapse_all = len(diffset.files) > collapse_when_files_over
83 collapse_all = len(diffset.files) > collapse_when_files_over
84 %>
84 %>
85
85
86 %if c.user_session_attrs["diffmode"] == 'sideside':
86 %if c.user_session_attrs["diffmode"] == 'sideside':
87 <style>
87 <style>
88 .wrapper {
88 .wrapper {
89 max-width: 1600px !important;
89 max-width: 1600px !important;
90 }
90 }
91 </style>
91 </style>
92 %endif
92 %endif
93
93
94 %if ruler_at_chars:
94 %if ruler_at_chars:
95 <style>
95 <style>
96 .diff table.cb .cb-content:after {
96 .diff table.cb .cb-content:after {
97 content: "";
97 content: "";
98 border-left: 1px solid blue;
98 border-left: 1px solid blue;
99 position: absolute;
99 position: absolute;
100 top: 0;
100 top: 0;
101 height: 18px;
101 height: 18px;
102 opacity: .2;
102 opacity: .2;
103 z-index: 10;
103 z-index: 10;
104 //## +5 to account for diff action (+/-)
104 //## +5 to account for diff action (+/-)
105 left: ${ruler_at_chars + 5}ch;
105 left: ${ruler_at_chars + 5}ch;
106 </style>
106 </style>
107 %endif
107 %endif
108
108
109 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
109 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
110 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
110 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
111 %if commit:
111 %if commit:
112 <div class="pull-right">
112 <div class="pull-right">
113 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
113 <a class="btn tooltip" title="${h.tooltip(_('Browse Files at revision {}').format(commit.raw_id))}" href="${h.route_path('repo_files',repo_name=diffset.repo_name, commit_id=commit.raw_id, f_path='')}">
114 ${_('Browse Files')}
114 ${_('Browse Files')}
115 </a>
115 </a>
116 </div>
116 </div>
117 %endif
117 %endif
118 <h2 class="clearinner">
118 <h2 class="clearinner">
119 ## invidual commit
119 ## invidual commit
120 % if commit:
120 % if commit:
121 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a> -
121 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.route_path('repo_commit',repo_name=diffset.repo_name,commit_id=commit.raw_id)}">${('r%s:%s' % (commit.idx,h.short_id(commit.raw_id)))}</a> -
122 ${h.age_component(commit.date)}
122 ${h.age_component(commit.date)}
123 % if diffset.limited_diff:
123 % if diffset.limited_diff:
124 - ${_('The requested commit is too big and content was truncated.')}
124 - ${_('The requested commit is too big and content was truncated.')}
125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
125 ${_ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
126 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
126 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
127 % elif hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
127 % elif hasattr(c, 'commit_ranges') and len(c.commit_ranges) > 1:
128 ## compare diff, has no file-selector and we want to show stats anyway
128 ## compare diff, has no file-selector and we want to show stats anyway
129 ${_ungettext('{num} file changed: {linesadd} inserted, ''{linesdel} deleted',
129 ${_ungettext('{num} file changed: {linesadd} inserted, ''{linesdel} deleted',
130 '{num} files changed: {linesadd} inserted, {linesdel} deleted', diffset.changed_files) \
130 '{num} files changed: {linesadd} inserted, {linesdel} deleted', diffset.changed_files) \
131 .format(num=diffset.changed_files, linesadd=diffset.lines_added, linesdel=diffset.lines_deleted)}
131 .format(num=diffset.changed_files, linesadd=diffset.lines_added, linesdel=diffset.lines_deleted)}
132 % endif
132 % endif
133 % else:
133 % else:
134 ## pull requests/compare
134 ## pull requests/compare
135 ${_('File Changes')}
135 ${_('File Changes')}
136 % endif
136 % endif
137
137
138 </h2>
138 </h2>
139 </div>
139 </div>
140
140
141 %if diffset.has_hidden_changes:
141 %if diffset.has_hidden_changes:
142 <p class="empty_data">${_('Some changes may be hidden')}</p>
142 <p class="empty_data">${_('Some changes may be hidden')}</p>
143 %elif not diffset.files:
143 %elif not diffset.files:
144 <p class="empty_data">${_('No files')}</p>
144 <p class="empty_data">${_('No files')}</p>
145 %endif
145 %endif
146
146
147 <div class="filediffs">
147 <div class="filediffs">
148
148
149 ## initial value could be marked as False later on
149 ## initial value could be marked as False later on
150 <% over_lines_changed_limit = False %>
150 <% over_lines_changed_limit = False %>
151 %for i, filediff in enumerate(diffset.files):
151 %for i, filediff in enumerate(diffset.files):
152
152
153 <%
153 <%
154 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
154 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
155 over_lines_changed_limit = lines_changed > lines_changed_limit
155 over_lines_changed_limit = lines_changed > lines_changed_limit
156 %>
156 %>
157 ## anchor with support of sticky header
157 ## anchor with support of sticky header
158 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
158 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
159
159
160 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
160 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
161 <div
161 <div
162 class="filediff"
162 class="filediff"
163 data-f-path="${filediff.patch['filename']}"
163 data-f-path="${filediff.patch['filename']}"
164 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
164 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
165 >
165 >
166 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
166 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
167 <div class="filediff-collapse-indicator"></div>
167 <div class="filediff-collapse-indicator"></div>
168 ${diff_ops(filediff)}
168 ${diff_ops(filediff)}
169 </label>
169 </label>
170
170
171 ${diff_menu(filediff, use_comments=use_comments)}
171 ${diff_menu(filediff, use_comments=use_comments)}
172 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
172 <table data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
173
173
174 ## new/deleted/empty content case
174 ## new/deleted/empty content case
175 % if not filediff.hunks:
175 % if not filediff.hunks:
176 ## Comment container, on "fakes" hunk that contains all data to render comments
176 ## Comment container, on "fakes" hunk that contains all data to render comments
177 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
177 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments)}
178 % endif
178 % endif
179
179
180 %if filediff.limited_diff:
180 %if filediff.limited_diff:
181 <tr class="cb-warning cb-collapser">
181 <tr class="cb-warning cb-collapser">
182 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
182 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
183 ${_('The requested commit is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
183 ${_('The requested commit is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
184 </td>
184 </td>
185 </tr>
185 </tr>
186 %else:
186 %else:
187 %if over_lines_changed_limit:
187 %if over_lines_changed_limit:
188 <tr class="cb-warning cb-collapser">
188 <tr class="cb-warning cb-collapser">
189 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
189 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
190 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
190 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
191 <a href="#" class="cb-expand"
191 <a href="#" class="cb-expand"
192 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
192 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
193 </a>
193 </a>
194 <a href="#" class="cb-collapse"
194 <a href="#" class="cb-collapse"
195 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
195 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
196 </a>
196 </a>
197 </td>
197 </td>
198 </tr>
198 </tr>
199 %endif
199 %endif
200 %endif
200 %endif
201
201
202 % for hunk in filediff.hunks:
202 % for hunk in filediff.hunks:
203 <tr class="cb-hunk">
203 <tr class="cb-hunk">
204 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
204 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
205 ## TODO: dan: add ajax loading of more context here
205 ## TODO: dan: add ajax loading of more context here
206 ## <a href="#">
206 ## <a href="#">
207 <i class="icon-more"></i>
207 <i class="icon-more"></i>
208 ## </a>
208 ## </a>
209 </td>
209 </td>
210 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
210 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
211 @@
211 @@
212 -${hunk.source_start},${hunk.source_length}
212 -${hunk.source_start},${hunk.source_length}
213 +${hunk.target_start},${hunk.target_length}
213 +${hunk.target_start},${hunk.target_length}
214 ${hunk.section_header}
214 ${hunk.section_header}
215 </td>
215 </td>
216 </tr>
216 </tr>
217 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
217 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments)}
218 % endfor
218 % endfor
219
219
220 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
220 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
221
221
222 ## outdated comments that do not fit into currently displayed lines
222 ## outdated comments that do not fit into currently displayed lines
223 % for lineno, comments in unmatched_comments.items():
223 % for lineno, comments in unmatched_comments.items():
224
224
225 %if c.user_session_attrs["diffmode"] == 'unified':
225 %if c.user_session_attrs["diffmode"] == 'unified':
226 % if loop.index == 0:
226 % if loop.index == 0:
227 <tr class="cb-hunk">
227 <tr class="cb-hunk">
228 <td colspan="3"></td>
228 <td colspan="3"></td>
229 <td>
229 <td>
230 <div>
230 <div>
231 ${_('Unmatched inline comments below')}
231 ${_('Unmatched inline comments below')}
232 </div>
232 </div>
233 </td>
233 </td>
234 </tr>
234 </tr>
235 % endif
235 % endif
236 <tr class="cb-line">
236 <tr class="cb-line">
237 <td class="cb-data cb-context"></td>
237 <td class="cb-data cb-context"></td>
238 <td class="cb-lineno cb-context"></td>
238 <td class="cb-lineno cb-context"></td>
239 <td class="cb-lineno cb-context"></td>
239 <td class="cb-lineno cb-context"></td>
240 <td class="cb-content cb-context">
240 <td class="cb-content cb-context">
241 ${inline_comments_container(comments, inline_comments)}
241 ${inline_comments_container(comments, inline_comments)}
242 </td>
242 </td>
243 </tr>
243 </tr>
244 %elif c.user_session_attrs["diffmode"] == 'sideside':
244 %elif c.user_session_attrs["diffmode"] == 'sideside':
245 % if loop.index == 0:
245 % if loop.index == 0:
246 <tr class="cb-comment-info">
246 <tr class="cb-comment-info">
247 <td colspan="2"></td>
247 <td colspan="2"></td>
248 <td class="cb-line">
248 <td class="cb-line">
249 <div>
249 <div>
250 ${_('Unmatched inline comments below')}
250 ${_('Unmatched inline comments below')}
251 </div>
251 </div>
252 </td>
252 </td>
253 <td colspan="2"></td>
253 <td colspan="2"></td>
254 <td class="cb-line">
254 <td class="cb-line">
255 <div>
255 <div>
256 ${_('Unmatched comments below')}
256 ${_('Unmatched comments below')}
257 </div>
257 </div>
258 </td>
258 </td>
259 </tr>
259 </tr>
260 % endif
260 % endif
261 <tr class="cb-line">
261 <tr class="cb-line">
262 <td class="cb-data cb-context"></td>
262 <td class="cb-data cb-context"></td>
263 <td class="cb-lineno cb-context"></td>
263 <td class="cb-lineno cb-context"></td>
264 <td class="cb-content cb-context">
264 <td class="cb-content cb-context">
265 % if lineno.startswith('o'):
265 % if lineno.startswith('o'):
266 ${inline_comments_container(comments, inline_comments)}
266 ${inline_comments_container(comments, inline_comments)}
267 % endif
267 % endif
268 </td>
268 </td>
269
269
270 <td class="cb-data cb-context"></td>
270 <td class="cb-data cb-context"></td>
271 <td class="cb-lineno cb-context"></td>
271 <td class="cb-lineno cb-context"></td>
272 <td class="cb-content cb-context">
272 <td class="cb-content cb-context">
273 % if lineno.startswith('n'):
273 % if lineno.startswith('n'):
274 ${inline_comments_container(comments, inline_comments)}
274 ${inline_comments_container(comments, inline_comments)}
275 % endif
275 % endif
276 </td>
276 </td>
277 </tr>
277 </tr>
278 %endif
278 %endif
279
279
280 % endfor
280 % endfor
281
281
282 </table>
282 </table>
283 </div>
283 </div>
284 %endfor
284 %endfor
285
285
286 ## outdated comments that are made for a file that has been deleted
286 ## outdated comments that are made for a file that has been deleted
287 % for filename, comments_dict in (deleted_files_comments or {}).items():
287 % for filename, comments_dict in (deleted_files_comments or {}).items():
288 <%
288 <%
289 display_state = 'display: none'
289 display_state = 'display: none'
290 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
290 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
291 if open_comments_in_file:
291 if open_comments_in_file:
292 display_state = ''
292 display_state = ''
293 %>
293 %>
294 <div class="filediffs filediff-outdated" style="${display_state}">
294 <div class="filediffs filediff-outdated" style="${display_state}">
295 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
295 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
296 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(filediff.raw_id, filename)}">
296 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(filediff.raw_id, filename)}">
297 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
297 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
298 <div class="filediff-collapse-indicator"></div>
298 <div class="filediff-collapse-indicator"></div>
299 <span class="pill">
299 <span class="pill">
300 ## file was deleted
300 ## file was deleted
301 <strong>${filename}</strong>
301 <strong>${filename}</strong>
302 </span>
302 </span>
303 <span class="pill-group" style="float: left">
303 <span class="pill-group" style="float: left">
304 ## file op, doesn't need translation
304 ## file op, doesn't need translation
305 <span class="pill" op="removed">removed in this version</span>
305 <span class="pill" op="removed">removed in this version</span>
306 </span>
306 </span>
307 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filename)}">ΒΆ</a>
307 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filename)}">ΒΆ</a>
308 <span class="pill-group" style="float: right">
308 <span class="pill-group" style="float: right">
309 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
309 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
310 </span>
310 </span>
311 </label>
311 </label>
312
312
313 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
313 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
314 <tr>
314 <tr>
315 % if c.user_session_attrs["diffmode"] == 'unified':
315 % if c.user_session_attrs["diffmode"] == 'unified':
316 <td></td>
316 <td></td>
317 %endif
317 %endif
318
318
319 <td></td>
319 <td></td>
320 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
320 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
321 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
321 ${_('File was deleted in this version. There are still outdated/unresolved comments attached to it.')}
322 </td>
322 </td>
323 </tr>
323 </tr>
324 %if c.user_session_attrs["diffmode"] == 'unified':
324 %if c.user_session_attrs["diffmode"] == 'unified':
325 <tr class="cb-line">
325 <tr class="cb-line">
326 <td class="cb-data cb-context"></td>
326 <td class="cb-data cb-context"></td>
327 <td class="cb-lineno cb-context"></td>
327 <td class="cb-lineno cb-context"></td>
328 <td class="cb-lineno cb-context"></td>
328 <td class="cb-lineno cb-context"></td>
329 <td class="cb-content cb-context">
329 <td class="cb-content cb-context">
330 ${inline_comments_container(comments_dict['comments'], inline_comments)}
330 ${inline_comments_container(comments_dict['comments'], inline_comments)}
331 </td>
331 </td>
332 </tr>
332 </tr>
333 %elif c.user_session_attrs["diffmode"] == 'sideside':
333 %elif c.user_session_attrs["diffmode"] == 'sideside':
334 <tr class="cb-line">
334 <tr class="cb-line">
335 <td class="cb-data cb-context"></td>
335 <td class="cb-data cb-context"></td>
336 <td class="cb-lineno cb-context"></td>
336 <td class="cb-lineno cb-context"></td>
337 <td class="cb-content cb-context"></td>
337 <td class="cb-content cb-context"></td>
338
338
339 <td class="cb-data cb-context"></td>
339 <td class="cb-data cb-context"></td>
340 <td class="cb-lineno cb-context"></td>
340 <td class="cb-lineno cb-context"></td>
341 <td class="cb-content cb-context">
341 <td class="cb-content cb-context">
342 ${inline_comments_container(comments_dict['comments'], inline_comments)}
342 ${inline_comments_container(comments_dict['comments'], inline_comments)}
343 </td>
343 </td>
344 </tr>
344 </tr>
345 %endif
345 %endif
346 </table>
346 </table>
347 </div>
347 </div>
348 </div>
348 </div>
349 % endfor
349 % endfor
350
350
351 </div>
351 </div>
352 </div>
352 </div>
353 </%def>
353 </%def>
354
354
355 <%def name="diff_ops(filediff)">
355 <%def name="diff_ops(filediff)">
356 <%
356 <%
357 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
357 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
358 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
358 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
359 %>
359 %>
360 <span class="pill">
360 <span class="pill">
361 %if filediff.source_file_path and filediff.target_file_path:
361 %if filediff.source_file_path and filediff.target_file_path:
362 %if filediff.source_file_path != filediff.target_file_path:
362 %if filediff.source_file_path != filediff.target_file_path:
363 ## file was renamed, or copied
363 ## file was renamed, or copied
364 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
364 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
365 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
365 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
366 <% final_path = filediff.target_file_path %>
366 <% final_path = filediff.target_file_path %>
367 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
367 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
368 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
368 <strong>${filediff.target_file_path}</strong> β¬… ${filediff.source_file_path}
369 <% final_path = filediff.target_file_path %>
369 <% final_path = filediff.target_file_path %>
370 %endif
370 %endif
371 %else:
371 %else:
372 ## file was modified
372 ## file was modified
373 <strong>${filediff.source_file_path}</strong>
373 <strong>${filediff.source_file_path}</strong>
374 <% final_path = filediff.source_file_path %>
374 <% final_path = filediff.source_file_path %>
375 %endif
375 %endif
376 %else:
376 %else:
377 %if filediff.source_file_path:
377 %if filediff.source_file_path:
378 ## file was deleted
378 ## file was deleted
379 <strong>${filediff.source_file_path}</strong>
379 <strong>${filediff.source_file_path}</strong>
380 <% final_path = filediff.source_file_path %>
380 <% final_path = filediff.source_file_path %>
381 %else:
381 %else:
382 ## file was added
382 ## file was added
383 <strong>${filediff.target_file_path}</strong>
383 <strong>${filediff.target_file_path}</strong>
384 <% final_path = filediff.target_file_path %>
384 <% final_path = filediff.target_file_path %>
385 %endif
385 %endif
386 %endif
386 %endif
387 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
387 <i style="color: #aaa" class="tooltip icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="${_('Copy the full path')}" onclick="return false;"></i>
388 </span>
388 </span>
389 ## anchor link
389 ## anchor link
390 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
390 <a class="pill filediff-anchor" href="#a_${h.FID(filediff.raw_id, filediff.patch['filename'])}">ΒΆ</a>
391
391
392 <span class="pill-group" style="float: right">
392 <span class="pill-group" style="float: right">
393
393
394 ## ops pills
394 ## ops pills
395 %if filediff.limited_diff:
395 %if filediff.limited_diff:
396 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
396 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
397 %endif
397 %endif
398
398
399 %if NEW_FILENODE in filediff.patch['stats']['ops']:
399 %if NEW_FILENODE in filediff.patch['stats']['ops']:
400 <span class="pill" op="created">created</span>
400 <span class="pill" op="created">created</span>
401 %if filediff['target_mode'].startswith('120'):
401 %if filediff['target_mode'].startswith('120'):
402 <span class="pill" op="symlink">symlink</span>
402 <span class="pill" op="symlink">symlink</span>
403 %else:
403 %else:
404 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
404 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
405 %endif
405 %endif
406 %endif
406 %endif
407
407
408 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
408 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
409 <span class="pill" op="renamed">renamed</span>
409 <span class="pill" op="renamed">renamed</span>
410 %endif
410 %endif
411
411
412 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
412 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
413 <span class="pill" op="copied">copied</span>
413 <span class="pill" op="copied">copied</span>
414 %endif
414 %endif
415
415
416 %if DEL_FILENODE in filediff.patch['stats']['ops']:
416 %if DEL_FILENODE in filediff.patch['stats']['ops']:
417 <span class="pill" op="removed">removed</span>
417 <span class="pill" op="removed">removed</span>
418 %endif
418 %endif
419
419
420 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
420 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
421 <span class="pill" op="mode">
421 <span class="pill" op="mode">
422 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
422 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
423 </span>
423 </span>
424 %endif
424 %endif
425
425
426 %if BIN_FILENODE in filediff.patch['stats']['ops']:
426 %if BIN_FILENODE in filediff.patch['stats']['ops']:
427 <span class="pill" op="binary">binary</span>
427 <span class="pill" op="binary">binary</span>
428 %if MOD_FILENODE in filediff.patch['stats']['ops']:
428 %if MOD_FILENODE in filediff.patch['stats']['ops']:
429 <span class="pill" op="modified">modified</span>
429 <span class="pill" op="modified">modified</span>
430 %endif
430 %endif
431 %endif
431 %endif
432
432
433 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
433 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
434 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
434 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
435
435
436 </span>
436 </span>
437
437
438 </%def>
438 </%def>
439
439
440 <%def name="nice_mode(filemode)">
440 <%def name="nice_mode(filemode)">
441 ${(filemode.startswith('100') and filemode[3:] or filemode)}
441 ${(filemode.startswith('100') and filemode[3:] or filemode)}
442 </%def>
442 </%def>
443
443
444 <%def name="diff_menu(filediff, use_comments=False)">
444 <%def name="diff_menu(filediff, use_comments=False)">
445 <div class="filediff-menu">
445 <div class="filediff-menu">
446
446
447 %if filediff.diffset.source_ref:
447 %if filediff.diffset.source_ref:
448
448
449 ## FILE BEFORE CHANGES
449 ## FILE BEFORE CHANGES
450 %if filediff.operation in ['D', 'M']:
450 %if filediff.operation in ['D', 'M']:
451 <a
451 <a
452 class="tooltip"
452 class="tooltip"
453 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
453 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
454 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
454 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
455 >
455 >
456 ${_('Show file before')}
456 ${_('Show file before')}
457 </a> |
457 </a> |
458 %else:
458 %else:
459 <span
459 <span
460 class="tooltip"
460 class="tooltip"
461 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
461 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
462 >
462 >
463 ${_('Show file before')}
463 ${_('Show file before')}
464 </span> |
464 </span> |
465 %endif
465 %endif
466
466
467 ## FILE AFTER CHANGES
467 ## FILE AFTER CHANGES
468 %if filediff.operation in ['A', 'M']:
468 %if filediff.operation in ['A', 'M']:
469 <a
469 <a
470 class="tooltip"
470 class="tooltip"
471 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
471 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
472 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
472 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
473 >
473 >
474 ${_('Show file after')}
474 ${_('Show file after')}
475 </a>
475 </a>
476 %else:
476 %else:
477 <span
477 <span
478 class="tooltip"
478 class="tooltip"
479 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
479 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
480 >
480 >
481 ${_('Show file after')}
481 ${_('Show file after')}
482 </span>
482 </span>
483 %endif
483 %endif
484
484
485 % if use_comments:
485 % if use_comments:
486 |
486 |
487 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
487 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
488 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
488 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
489 </a>
489 </a>
490 % endif
490 % endif
491
491
492 %endif
492 %endif
493
493
494 </div>
494 </div>
495 </%def>
495 </%def>
496
496
497
497
498 <%def name="inline_comments_container(comments, inline_comments)">
498 <%def name="inline_comments_container(comments, inline_comments)">
499 <div class="inline-comments">
499 <div class="inline-comments">
500 %for comment in comments:
500 %for comment in comments:
501 ${commentblock.comment_block(comment, inline=True)}
501 ${commentblock.comment_block(comment, inline=True)}
502 %endfor
502 %endfor
503 % if comments and comments[-1].outdated:
503 % if comments and comments[-1].outdated:
504 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
504 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
505 style="display: none;}">
505 style="display: none;}">
506 ${_('Add another comment')}
506 ${_('Add another comment')}
507 </span>
507 </span>
508 % else:
508 % else:
509 <span onclick="return Rhodecode.comments.createComment(this)"
509 <span onclick="return Rhodecode.comments.createComment(this)"
510 class="btn btn-secondary cb-comment-add-button">
510 class="btn btn-secondary cb-comment-add-button">
511 ${_('Add another comment')}
511 ${_('Add another comment')}
512 </span>
512 </span>
513 % endif
513 % endif
514
514
515 </div>
515 </div>
516 </%def>
516 </%def>
517
517
518 <%!
518 <%!
519 def get_comments_for(diff_type, comments, filename, line_version, line_number):
519 def get_comments_for(diff_type, comments, filename, line_version, line_number):
520 if hasattr(filename, 'unicode_path'):
520 if hasattr(filename, 'unicode_path'):
521 filename = filename.unicode_path
521 filename = filename.unicode_path
522
522
523 if not isinstance(filename, basestring):
523 if not isinstance(filename, basestring):
524 return None
524 return None
525
525
526 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
526 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
527
527
528 if comments and filename in comments:
528 if comments and filename in comments:
529 file_comments = comments[filename]
529 file_comments = comments[filename]
530 if line_key in file_comments:
530 if line_key in file_comments:
531 data = file_comments.pop(line_key)
531 data = file_comments.pop(line_key)
532 return data
532 return data
533 %>
533 %>
534
534
535 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
535 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None)">
536 %for i, line in enumerate(hunk.sideside):
536 %for i, line in enumerate(hunk.sideside):
537 <%
537 <%
538 old_line_anchor, new_line_anchor = None, None
538 old_line_anchor, new_line_anchor = None, None
539
539
540 if line.original.lineno:
540 if line.original.lineno:
541 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
541 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
542 if line.modified.lineno:
542 if line.modified.lineno:
543 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
543 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
544 %>
544 %>
545
545
546 <tr class="cb-line">
546 <tr class="cb-line">
547 <td class="cb-data ${action_class(line.original.action)}"
547 <td class="cb-data ${action_class(line.original.action)}"
548 data-line-no="${line.original.lineno}"
548 data-line-no="${line.original.lineno}"
549 >
549 >
550 <div>
550 <div>
551
551
552 <% line_old_comments = None %>
552 <% line_old_comments = None %>
553 %if line.original.get_comment_args:
553 %if line.original.get_comment_args:
554 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
554 <% line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args) %>
555 %endif
555 %endif
556 %if line_old_comments:
556 %if line_old_comments:
557 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
557 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
558 % if has_outdated:
558 % if has_outdated:
559 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
559 <i title="${_('comments including outdated')}:${len(line_old_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
560 % else:
560 % else:
561 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
561 <i title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
562 % endif
562 % endif
563 %endif
563 %endif
564 </div>
564 </div>
565 </td>
565 </td>
566 <td class="cb-lineno ${action_class(line.original.action)}"
566 <td class="cb-lineno ${action_class(line.original.action)}"
567 data-line-no="${line.original.lineno}"
567 data-line-no="${line.original.lineno}"
568 %if old_line_anchor:
568 %if old_line_anchor:
569 id="${old_line_anchor}"
569 id="${old_line_anchor}"
570 %endif
570 %endif
571 >
571 >
572 %if line.original.lineno:
572 %if line.original.lineno:
573 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
573 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
574 %endif
574 %endif
575 </td>
575 </td>
576 <td class="cb-content ${action_class(line.original.action)}"
576 <td class="cb-content ${action_class(line.original.action)}"
577 data-line-no="o${line.original.lineno}"
577 data-line-no="o${line.original.lineno}"
578 >
578 >
579 %if use_comments and line.original.lineno:
579 %if use_comments and line.original.lineno:
580 ${render_add_comment_button()}
580 ${render_add_comment_button()}
581 %endif
581 %endif
582 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
582 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
583
583
584 %if use_comments and line.original.lineno and line_old_comments:
584 %if use_comments and line.original.lineno and line_old_comments:
585 ${inline_comments_container(line_old_comments, inline_comments)}
585 ${inline_comments_container(line_old_comments, inline_comments)}
586 %endif
586 %endif
587
587
588 </td>
588 </td>
589 <td class="cb-data ${action_class(line.modified.action)}"
589 <td class="cb-data ${action_class(line.modified.action)}"
590 data-line-no="${line.modified.lineno}"
590 data-line-no="${line.modified.lineno}"
591 >
591 >
592 <div>
592 <div>
593
593
594 %if line.modified.get_comment_args:
594 %if line.modified.get_comment_args:
595 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
595 <% line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args) %>
596 %else:
596 %else:
597 <% line_new_comments = None%>
597 <% line_new_comments = None%>
598 %endif
598 %endif
599 %if line_new_comments:
599 %if line_new_comments:
600 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
600 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
601 % if has_outdated:
601 % if has_outdated:
602 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
602 <i title="${_('comments including outdated')}:${len(line_new_comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
603 % else:
603 % else:
604 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
604 <i title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
605 % endif
605 % endif
606 %endif
606 %endif
607 </div>
607 </div>
608 </td>
608 </td>
609 <td class="cb-lineno ${action_class(line.modified.action)}"
609 <td class="cb-lineno ${action_class(line.modified.action)}"
610 data-line-no="${line.modified.lineno}"
610 data-line-no="${line.modified.lineno}"
611 %if new_line_anchor:
611 %if new_line_anchor:
612 id="${new_line_anchor}"
612 id="${new_line_anchor}"
613 %endif
613 %endif
614 >
614 >
615 %if line.modified.lineno:
615 %if line.modified.lineno:
616 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
616 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
617 %endif
617 %endif
618 </td>
618 </td>
619 <td class="cb-content ${action_class(line.modified.action)}"
619 <td class="cb-content ${action_class(line.modified.action)}"
620 data-line-no="n${line.modified.lineno}"
620 data-line-no="n${line.modified.lineno}"
621 >
621 >
622 %if use_comments and line.modified.lineno:
622 %if use_comments and line.modified.lineno:
623 ${render_add_comment_button()}
623 ${render_add_comment_button()}
624 %endif
624 %endif
625 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
625 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
626 %if use_comments and line.modified.lineno and line_new_comments:
626 %if use_comments and line.modified.lineno and line_new_comments:
627 ${inline_comments_container(line_new_comments, inline_comments)}
627 ${inline_comments_container(line_new_comments, inline_comments)}
628 %endif
628 %endif
629 </td>
629 </td>
630 </tr>
630 </tr>
631 %endfor
631 %endfor
632 </%def>
632 </%def>
633
633
634
634
635 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
635 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None)">
636 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
636 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
637
637
638 <%
638 <%
639 old_line_anchor, new_line_anchor = None, None
639 old_line_anchor, new_line_anchor = None, None
640 if old_line_no:
640 if old_line_no:
641 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
641 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
642 if new_line_no:
642 if new_line_no:
643 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
643 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
644 %>
644 %>
645 <tr class="cb-line">
645 <tr class="cb-line">
646 <td class="cb-data ${action_class(action)}">
646 <td class="cb-data ${action_class(action)}">
647 <div>
647 <div>
648
648
649 %if comments_args:
649 %if comments_args:
650 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
650 <% comments = get_comments_for('unified', inline_comments, *comments_args) %>
651 %else:
651 %else:
652 <% comments = None %>
652 <% comments = None %>
653 %endif
653 %endif
654
654
655 % if comments:
655 % if comments:
656 <% has_outdated = any([x.outdated for x in comments]) %>
656 <% has_outdated = any([x.outdated for x in comments]) %>
657 % if has_outdated:
657 % if has_outdated:
658 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
658 <i title="${_('comments including outdated')}:${len(comments)}" class="icon-comment_toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
659 % else:
659 % else:
660 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
660 <i title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
661 % endif
661 % endif
662 % endif
662 % endif
663 </div>
663 </div>
664 </td>
664 </td>
665 <td class="cb-lineno ${action_class(action)}"
665 <td class="cb-lineno ${action_class(action)}"
666 data-line-no="${old_line_no}"
666 data-line-no="${old_line_no}"
667 %if old_line_anchor:
667 %if old_line_anchor:
668 id="${old_line_anchor}"
668 id="${old_line_anchor}"
669 %endif
669 %endif
670 >
670 >
671 %if old_line_anchor:
671 %if old_line_anchor:
672 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
672 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
673 %endif
673 %endif
674 </td>
674 </td>
675 <td class="cb-lineno ${action_class(action)}"
675 <td class="cb-lineno ${action_class(action)}"
676 data-line-no="${new_line_no}"
676 data-line-no="${new_line_no}"
677 %if new_line_anchor:
677 %if new_line_anchor:
678 id="${new_line_anchor}"
678 id="${new_line_anchor}"
679 %endif
679 %endif
680 >
680 >
681 %if new_line_anchor:
681 %if new_line_anchor:
682 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
682 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
683 %endif
683 %endif
684 </td>
684 </td>
685 <td class="cb-content ${action_class(action)}"
685 <td class="cb-content ${action_class(action)}"
686 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
686 data-line-no="${(new_line_no and 'n' or 'o')}${(new_line_no or old_line_no)}"
687 >
687 >
688 %if use_comments:
688 %if use_comments:
689 ${render_add_comment_button()}
689 ${render_add_comment_button()}
690 %endif
690 %endif
691 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
691 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
692 %if use_comments and comments:
692 %if use_comments and comments:
693 ${inline_comments_container(comments, inline_comments)}
693 ${inline_comments_container(comments, inline_comments)}
694 %endif
694 %endif
695 </td>
695 </td>
696 </tr>
696 </tr>
697 %endfor
697 %endfor
698 </%def>
698 </%def>
699
699
700
700
701 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
701 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments)">
702 % if diff_mode == 'unified':
702 % if diff_mode == 'unified':
703 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
703 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
704 % elif diff_mode == 'sideside':
704 % elif diff_mode == 'sideside':
705 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
705 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments)}
706 % else:
706 % else:
707 <tr class="cb-line">
707 <tr class="cb-line">
708 <td>unknown diff mode</td>
708 <td>unknown diff mode</td>
709 </tr>
709 </tr>
710 % endif
710 % endif
711 </%def>file changes
711 </%def>file changes
712
712
713
713
714 <%def name="render_add_comment_button()">
714 <%def name="render_add_comment_button()">
715 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
715 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
716 <span><i class="icon-comment"></i></span>
716 <span><i class="icon-comment"></i></span>
717 </button>
717 </button>
718 </%def>
718 </%def>
719
719
720 <%def name="render_diffset_menu(diffset=None, range_diff_on=None)">
720 <%def name="render_diffset_menu(diffset=None, range_diff_on=None)">
721
721
722 <div id="diff-file-sticky" class="diffset-menu clearinner">
722 <div id="diff-file-sticky" class="diffset-menu clearinner">
723 ## auto adjustable
723 ## auto adjustable
724 <div class="sidebar__inner">
724 <div class="sidebar__inner">
725 <div class="sidebar__bar">
725 <div class="sidebar__bar">
726 <div class="pull-right">
726 <div class="pull-right">
727 <div class="btn-group">
727 <div class="btn-group">
728
728
729 ## DIFF OPTIONS via Select2
729 ## DIFF OPTIONS via Select2
730 <div class="pull-left">
730 <div class="pull-left">
731 ${h.hidden('diff_menu')}
731 ${h.hidden('diff_menu')}
732 </div>
732 </div>
733
733
734 <a
734 <a
735 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-primary')} tooltip"
735 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-primary')} tooltip"
736 title="${h.tooltip(_('View side by side'))}"
736 title="${h.tooltip(_('View side by side'))}"
737 href="${h.current_route_path(request, diffmode='sideside')}">
737 href="${h.current_route_path(request, diffmode='sideside')}">
738 <span>${_('Side by Side')}</span>
738 <span>${_('Side by Side')}</span>
739 </a>
739 </a>
740
740
741 <a
741 <a
742 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-primary')} tooltip"
742 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-primary')} tooltip"
743 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
743 title="${h.tooltip(_('View unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
744 <span>${_('Unified')}</span>
744 <span>${_('Unified')}</span>
745 </a>
745 </a>
746
746
747 % if range_diff_on is True:
747 % if range_diff_on is True:
748 <a
748 <a
749 title="${_('Turn off: Show the diff as commit range')}"
749 title="${_('Turn off: Show the diff as commit range')}"
750 class="btn btn-primary"
750 class="btn btn-primary"
751 href="${h.current_route_path(request, **{"range-diff":"0"})}">
751 href="${h.current_route_path(request, **{"range-diff":"0"})}">
752 <span>${_('Range Diff')}</span>
752 <span>${_('Range Diff')}</span>
753 </a>
753 </a>
754 % elif range_diff_on is False:
754 % elif range_diff_on is False:
755 <a
755 <a
756 title="${_('Show the diff as commit range')}"
756 title="${_('Show the diff as commit range')}"
757 class="btn"
757 class="btn"
758 href="${h.current_route_path(request, **{"range-diff":"1"})}">
758 href="${h.current_route_path(request, **{"range-diff":"1"})}">
759 <span>${_('Range Diff')}</span>
759 <span>${_('Range Diff')}</span>
760 </a>
760 </a>
761 % endif
761 % endif
762 </div>
762 </div>
763 </div>
763 </div>
764 <div class="pull-left">
764 <div class="pull-left">
765 <div class="btn-group">
765 <div class="btn-group">
766 <div class="pull-left">
766 <div class="pull-left">
767 ${h.hidden('file_filter')}
767 ${h.hidden('file_filter')}
768 </div>
768 </div>
769 <a
769 <a
770 class="btn"
770 class="btn"
771 href="#"
771 href="#"
772 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); updateSticky(); return false">${_('Expand All Files')}</a>
772 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); updateSticky(); return false">${_('Expand All Files')}</a>
773 <a
773 <a
774 class="btn"
774 class="btn"
775 href="#"
775 href="#"
776 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); updateSticky(); return false">${_('Collapse All Files')}</a>
776 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); updateSticky(); return false">${_('Collapse All Files')}</a>
777 </div>
777 </div>
778 </div>
778 </div>
779 </div>
779 </div>
780 <div class="fpath-placeholder">
780 <div class="fpath-placeholder">
781 <i class="icon-file-text"></i>
781 <i class="icon-file-text"></i>
782 <strong class="fpath-placeholder-text">
782 <strong class="fpath-placeholder-text">
783 Context file:
783 Context file:
784 </strong>
784 </strong>
785 </div>
785 </div>
786 <div class="sidebar_inner_shadow"></div>
786 <div class="sidebar_inner_shadow"></div>
787 </div>
787 </div>
788 </div>
788 </div>
789
789
790 % if diffset:
790 % if diffset:
791
791
792 %if diffset.limited_diff:
792 %if diffset.limited_diff:
793 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
793 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
794 %else:
794 %else:
795 <% file_placeholder = _ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted', '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}%>
795 <% file_placeholder = _ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted', '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}%>
796 %endif
796 %endif
797 ## case on range-diff placeholder needs to be updated
797 ## case on range-diff placeholder needs to be updated
798 % if range_diff_on is True:
798 % if range_diff_on is True:
799 <% file_placeholder = _('Disabled on range diff') %>
799 <% file_placeholder = _('Disabled on range diff') %>
800 % endif
800 % endif
801
801
802 <script>
802 <script>
803
803
804 var feedFilesOptions = function (query, initialData) {
804 var feedFilesOptions = function (query, initialData) {
805 var data = {results: []};
805 var data = {results: []};
806 var isQuery = typeof query.term !== 'undefined';
806 var isQuery = typeof query.term !== 'undefined';
807
807
808 var section = _gettext('Changed files');
808 var section = _gettext('Changed files');
809 var filteredData = [];
809 var filteredData = [];
810
810
811 //filter results
811 //filter results
812 $.each(initialData.results, function (idx, value) {
812 $.each(initialData.results, function (idx, value) {
813
813
814 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
814 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
815 filteredData.push({
815 filteredData.push({
816 'id': this.id,
816 'id': this.id,
817 'text': this.text,
817 'text': this.text,
818 "ops": this.ops,
818 "ops": this.ops,
819 })
819 })
820 }
820 }
821
821
822 });
822 });
823
823
824 data.results = filteredData;
824 data.results = filteredData;
825
825
826 query.callback(data);
826 query.callback(data);
827 };
827 };
828
828
829 var formatFileResult = function(result, container, query, escapeMarkup) {
829 var formatFileResult = function(result, container, query, escapeMarkup) {
830 return function(data, escapeMarkup) {
830 return function(data, escapeMarkup) {
831 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
831 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
832 var tmpl = '<span style="margin-right:-50px"><strong>{0}</strong></span>'.format(escapeMarkup(data['text']));
832 var tmpl = '<span style="margin-right:-50px"><strong>{0}</strong></span>'.format(escapeMarkup(data['text']));
833 var pill = '<span class="pill-group" style="float: right;margin-right: -100px">' +
833 var pill = '<span class="pill-group" style="float: right;margin-right: -100px">' +
834 '<span class="pill" op="added">{0}</span>' +
834 '<span class="pill" op="added">{0}</span>' +
835 '<span class="pill" op="deleted">{1}</span>' +
835 '<span class="pill" op="deleted">{1}</span>' +
836 '</span>'
836 '</span>'
837 ;
837 ;
838 var added = data['ops']['added'];
838 var added = data['ops']['added'];
839 if (added === 0) {
839 if (added === 0) {
840 // don't show +0
840 // don't show +0
841 added = 0;
841 added = 0;
842 } else {
842 } else {
843 added = '+' + added;
843 added = '+' + added;
844 }
844 }
845
845
846 var deleted = -1*data['ops']['deleted'];
846 var deleted = -1*data['ops']['deleted'];
847
847
848 tmpl += pill.format(added, deleted);
848 tmpl += pill.format(added, deleted);
849 return container.format(tmpl);
849 return container.format(tmpl);
850
850
851 }(result, escapeMarkup);
851 }(result, escapeMarkup);
852 };
852 };
853
853
854 var preloadFileFilterData = {
854 var preloadFileFilterData = {
855 results: [
855 results: [
856 % for filediff in diffset.files:
856 % for filediff in diffset.files:
857 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
857 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
858 text:"${filediff.patch['filename']}",
858 text:"${filediff.patch['filename']}",
859 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
859 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
860 % endfor
860 % endfor
861 ]
861 ]
862 };
862 };
863
863
864 $(document).ready(function () {
864 $(document).ready(function () {
865
865
866 var fileFilter = $("#file_filter").select2({
866 var fileFilter = $("#file_filter").select2({
867 'dropdownAutoWidth': true,
867 'dropdownAutoWidth': true,
868 'width': 'auto',
868 'width': 'auto',
869 'placeholder': "${file_placeholder}",
869 'placeholder': "${file_placeholder}",
870 containerCssClass: "drop-menu",
870 containerCssClass: "drop-menu",
871 dropdownCssClass: "drop-menu-dropdown",
871 dropdownCssClass: "drop-menu-dropdown",
872 data: preloadFileFilterData,
872 data: preloadFileFilterData,
873 query: function(query) {
873 query: function(query) {
874 feedFilesOptions(query, preloadFileFilterData);
874 feedFilesOptions(query, preloadFileFilterData);
875 },
875 },
876 formatResult: formatFileResult
876 formatResult: formatFileResult
877 });
877 });
878
878
879 % if range_diff_on is True:
879 % if range_diff_on is True:
880 fileFilter.select2("enable", false);
880 fileFilter.select2("enable", false);
881 % endif
881 % endif
882
882
883 $("#file_filter").on('click', function (e) {
883 $("#file_filter").on('click', function (e) {
884 e.preventDefault();
884 e.preventDefault();
885 var selected = $('#file_filter').select2('data');
885 var selected = $('#file_filter').select2('data');
886 var idSelector = "#"+selected.id;
886 var idSelector = "#"+selected.id;
887 window.location.hash = idSelector;
887 window.location.hash = idSelector;
888 // expand the container if we quick-select the field
888 // expand the container if we quick-select the field
889 $(idSelector).next().prop('checked', false);
889 $(idSelector).next().prop('checked', false);
890 updateSticky()
890 updateSticky()
891 });
891 });
892
892
893 var contextPrefix = _gettext('Context file: ');
893 var contextPrefix = _gettext('Context file: ');
894 ## sticky sidebar
894 ## sticky sidebar
895 var sidebarElement = document.getElementById('diff-file-sticky');
895 var sidebarElement = document.getElementById('diff-file-sticky');
896 sidebar = new StickySidebar(sidebarElement, {
896 sidebar = new StickySidebar(sidebarElement, {
897 topSpacing: 0,
897 topSpacing: 0,
898 bottomSpacing: 0,
898 bottomSpacing: 0,
899 innerWrapperSelector: '.sidebar__inner'
899 innerWrapperSelector: '.sidebar__inner'
900 });
900 });
901 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
901 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
902 // reset our file so it's not holding new value
902 // reset our file so it's not holding new value
903 $('.fpath-placeholder-text').html(contextPrefix)
903 $('.fpath-placeholder-text').html(contextPrefix)
904 });
904 });
905
905
906 updateSticky = function () {
906 updateSticky = function () {
907 sidebar.updateSticky();
907 sidebar.updateSticky();
908 Waypoint.refreshAll();
908 Waypoint.refreshAll();
909 };
909 };
910
910
911 var animateText = $.debounce(100, function(fPath, anchorId) {
911 var animateText = $.debounce(100, function(fPath, anchorId) {
912 fPath = Select2.util.escapeMarkup(fPath);
913
912 // animate setting the text
914 // animate setting the text
913 var callback = function () {
915 var callback = function () {
914 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
916 $('.fpath-placeholder-text').animate({'opacity': 1.00}, 200)
915 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
917 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
916 };
918 };
917 $('.fpath-placeholder-text').animate({'opacity': 0.15}, 200, callback);
919 $('.fpath-placeholder-text').animate({'opacity': 0.15}, 200, callback);
918 });
920 });
919
921
920 ## dynamic file waypoints
922 ## dynamic file waypoints
921 var setFPathInfo = function(fPath, anchorId){
923 var setFPathInfo = function(fPath, anchorId){
922 animateText(fPath, anchorId)
924 animateText(fPath, anchorId)
923 };
925 };
924
926
925 var codeBlock = $('.filediff');
927 var codeBlock = $('.filediff');
926 // forward waypoint
928 // forward waypoint
927 codeBlock.waypoint(
929 codeBlock.waypoint(
928 function(direction) {
930 function(direction) {
929 if (direction === "down"){
931 if (direction === "down"){
930 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
932 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
931 }
933 }
932 }, {
934 }, {
933 offset: 70,
935 offset: 70,
934 context: '.fpath-placeholder'
936 context: '.fpath-placeholder'
935 }
937 }
936 );
938 );
937
939
938 // backward waypoint
940 // backward waypoint
939 codeBlock.waypoint(
941 codeBlock.waypoint(
940 function(direction) {
942 function(direction) {
941 if (direction === "up"){
943 if (direction === "up"){
942 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
944 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
943 }
945 }
944 }, {
946 }, {
945 offset: function () {
947 offset: function () {
946 return -this.element.clientHeight + 90
948 return -this.element.clientHeight + 90
947 },
949 },
948 context: '.fpath-placeholder'
950 context: '.fpath-placeholder'
949 }
951 }
950 );
952 );
951
953
952 var preloadDiffMenuData = {
954 var preloadDiffMenuData = {
953 results: [
955 results: [
954 ## Wide diff mode
956 ## Wide diff mode
955 {
957 {
956 id: 1,
958 id: 1,
957 text: _gettext('Toggle Wide Mode diff'),
959 text: _gettext('Toggle Wide Mode diff'),
958 action: function () {
960 action: function () {
959 updateSticky();
961 updateSticky();
960 Rhodecode.comments.toggleWideMode(this);
962 Rhodecode.comments.toggleWideMode(this);
961 return null;
963 return null;
962 },
964 },
963 url: null,
965 url: null,
964 },
966 },
965
967
966 ## Whitespace change
968 ## Whitespace change
967 % if request.GET.get('ignorews', '') == '1':
969 % if request.GET.get('ignorews', '') == '1':
968 {
970 {
969 id: 2,
971 id: 2,
970 text: _gettext('Show whitespace changes'),
972 text: _gettext('Show whitespace changes'),
971 action: function () {},
973 action: function () {},
972 url: "${h.current_route_path(request, ignorews=0)|n}"
974 url: "${h.current_route_path(request, ignorews=0)|n}"
973 },
975 },
974 % else:
976 % else:
975 {
977 {
976 id: 2,
978 id: 2,
977 text: _gettext('Hide whitespace changes'),
979 text: _gettext('Hide whitespace changes'),
978 action: function () {},
980 action: function () {},
979 url: "${h.current_route_path(request, ignorews=1)|n}"
981 url: "${h.current_route_path(request, ignorews=1)|n}"
980 },
982 },
981 % endif
983 % endif
982
984
983 ## FULL CONTEXT
985 ## FULL CONTEXT
984 % if request.GET.get('fullcontext', '') == '1':
986 % if request.GET.get('fullcontext', '') == '1':
985 {
987 {
986 id: 3,
988 id: 3,
987 text: _gettext('Hide full context diff'),
989 text: _gettext('Hide full context diff'),
988 action: function () {},
990 action: function () {},
989 url: "${h.current_route_path(request, fullcontext=0)|n}"
991 url: "${h.current_route_path(request, fullcontext=0)|n}"
990 },
992 },
991 % else:
993 % else:
992 {
994 {
993 id: 3,
995 id: 3,
994 text: _gettext('Show full context diff'),
996 text: _gettext('Show full context diff'),
995 action: function () {},
997 action: function () {},
996 url: "${h.current_route_path(request, fullcontext=1)|n}"
998 url: "${h.current_route_path(request, fullcontext=1)|n}"
997 },
999 },
998 % endif
1000 % endif
999
1001
1000 ]
1002 ]
1001 };
1003 };
1002
1004
1003 $("#diff_menu").select2({
1005 $("#diff_menu").select2({
1004 minimumResultsForSearch: -1,
1006 minimumResultsForSearch: -1,
1005 containerCssClass: "drop-menu",
1007 containerCssClass: "drop-menu",
1006 dropdownCssClass: "drop-menu-dropdown",
1008 dropdownCssClass: "drop-menu-dropdown",
1007 dropdownAutoWidth: true,
1009 dropdownAutoWidth: true,
1008 data: preloadDiffMenuData,
1010 data: preloadDiffMenuData,
1009 placeholder: "${_('Diff Options')}",
1011 placeholder: "${_('Diff Options')}",
1010 });
1012 });
1011 $("#diff_menu").on('select2-selecting', function (e) {
1013 $("#diff_menu").on('select2-selecting', function (e) {
1012 e.choice.action();
1014 e.choice.action();
1013 if (e.choice.url !== null) {
1015 if (e.choice.url !== null) {
1014 window.location = e.choice.url
1016 window.location = e.choice.url
1015 }
1017 }
1016 });
1018 });
1017
1019
1018 });
1020 });
1019
1021
1020 </script>
1022 </script>
1021 % endif
1023 % endif
1022
1024
1023 </%def>
1025 </%def>
@@ -1,201 +1,209 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 from StringIO import StringIO
21 from StringIO import StringIO
22
22
23 import pytest
23 import pytest
24 from mock import patch, Mock
24 from mock import patch, Mock
25
25
26 from rhodecode.lib.middleware.simplesvn import SimpleSvn, SimpleSvnApp
26 from rhodecode.lib.middleware.simplesvn import SimpleSvn, SimpleSvnApp
27 from rhodecode.lib.utils import get_rhodecode_base_path
27 from rhodecode.lib.utils import get_rhodecode_base_path
28
28
29
29
30 class TestSimpleSvn(object):
30 class TestSimpleSvn(object):
31 @pytest.fixture(autouse=True)
31 @pytest.fixture(autouse=True)
32 def simple_svn(self, baseapp, request_stub):
32 def simple_svn(self, baseapp, request_stub):
33 base_path = get_rhodecode_base_path()
33 base_path = get_rhodecode_base_path()
34 self.app = SimpleSvn(
34 self.app = SimpleSvn(
35 config={'auth_ret_code': '', 'base_path': base_path},
35 config={'auth_ret_code': '', 'base_path': base_path},
36 registry=request_stub.registry)
36 registry=request_stub.registry)
37
37
38 def test_get_config(self):
38 def test_get_config(self):
39 extras = {'foo': 'FOO', 'bar': 'BAR'}
39 extras = {'foo': 'FOO', 'bar': 'BAR'}
40 config = self.app._create_config(extras, repo_name='test-repo')
40 config = self.app._create_config(extras, repo_name='test-repo')
41 assert config == extras
41 assert config == extras
42
42
43 @pytest.mark.parametrize(
43 @pytest.mark.parametrize(
44 'method', ['OPTIONS', 'PROPFIND', 'GET', 'REPORT'])
44 'method', ['OPTIONS', 'PROPFIND', 'GET', 'REPORT'])
45 def test_get_action_returns_pull(self, method):
45 def test_get_action_returns_pull(self, method):
46 environment = {'REQUEST_METHOD': method}
46 environment = {'REQUEST_METHOD': method}
47 action = self.app._get_action(environment)
47 action = self.app._get_action(environment)
48 assert action == 'pull'
48 assert action == 'pull'
49
49
50 @pytest.mark.parametrize(
50 @pytest.mark.parametrize(
51 'method', [
51 'method', [
52 'MKACTIVITY', 'PROPPATCH', 'PUT', 'CHECKOUT', 'MKCOL', 'MOVE',
52 'MKACTIVITY', 'PROPPATCH', 'PUT', 'CHECKOUT', 'MKCOL', 'MOVE',
53 'COPY', 'DELETE', 'LOCK', 'UNLOCK', 'MERGE'
53 'COPY', 'DELETE', 'LOCK', 'UNLOCK', 'MERGE'
54 ])
54 ])
55 def test_get_action_returns_push(self, method):
55 def test_get_action_returns_push(self, method):
56 environment = {'REQUEST_METHOD': method}
56 environment = {'REQUEST_METHOD': method}
57 action = self.app._get_action(environment)
57 action = self.app._get_action(environment)
58 assert action == 'push'
58 assert action == 'push'
59
59
60 @pytest.mark.parametrize(
60 @pytest.mark.parametrize(
61 'path, expected_name', [
61 'path, expected_name', [
62 ('/hello-svn', 'hello-svn'),
62 ('/hello-svn', 'hello-svn'),
63 ('/hello-svn/', 'hello-svn'),
63 ('/hello-svn/', 'hello-svn'),
64 ('/group/hello-svn/', 'group/hello-svn'),
64 ('/group/hello-svn/', 'group/hello-svn'),
65 ('/group/hello-svn/!svn/vcc/default', 'group/hello-svn'),
65 ('/group/hello-svn/!svn/vcc/default', 'group/hello-svn'),
66 ])
66 ])
67 def test_get_repository_name(self, path, expected_name):
67 def test_get_repository_name(self, path, expected_name):
68 environment = {'PATH_INFO': path}
68 environment = {'PATH_INFO': path}
69 name = self.app._get_repository_name(environment)
69 name = self.app._get_repository_name(environment)
70 assert name == expected_name
70 assert name == expected_name
71
71
72 def test_get_repository_name_subfolder(self, backend_svn):
72 def test_get_repository_name_subfolder(self, backend_svn):
73 repo = backend_svn.repo
73 repo = backend_svn.repo
74 environment = {
74 environment = {
75 'PATH_INFO': '/{}/path/with/subfolders'.format(repo.repo_name)}
75 'PATH_INFO': '/{}/path/with/subfolders'.format(repo.repo_name)}
76 name = self.app._get_repository_name(environment)
76 name = self.app._get_repository_name(environment)
77 assert name == repo.repo_name
77 assert name == repo.repo_name
78
78
79 def test_create_wsgi_app(self):
79 def test_create_wsgi_app(self):
80 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
80 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
81 mock_method.return_value = False
81 mock_method.return_value = False
82 with patch('rhodecode.lib.middleware.simplesvn.DisabledSimpleSvnApp') as (
82 with patch('rhodecode.lib.middleware.simplesvn.DisabledSimpleSvnApp') as (
83 wsgi_app_mock):
83 wsgi_app_mock):
84 config = Mock()
84 config = Mock()
85 wsgi_app = self.app._create_wsgi_app(
85 wsgi_app = self.app._create_wsgi_app(
86 repo_path='', repo_name='', config=config)
86 repo_path='', repo_name='', config=config)
87
87
88 wsgi_app_mock.assert_called_once_with(config)
88 wsgi_app_mock.assert_called_once_with(config)
89 assert wsgi_app == wsgi_app_mock()
89 assert wsgi_app == wsgi_app_mock()
90
90
91 def test_create_wsgi_app_when_enabled(self):
91 def test_create_wsgi_app_when_enabled(self):
92 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
92 with patch.object(SimpleSvn, '_is_svn_enabled') as mock_method:
93 mock_method.return_value = True
93 mock_method.return_value = True
94 with patch('rhodecode.lib.middleware.simplesvn.SimpleSvnApp') as (
94 with patch('rhodecode.lib.middleware.simplesvn.SimpleSvnApp') as (
95 wsgi_app_mock):
95 wsgi_app_mock):
96 config = Mock()
96 config = Mock()
97 wsgi_app = self.app._create_wsgi_app(
97 wsgi_app = self.app._create_wsgi_app(
98 repo_path='', repo_name='', config=config)
98 repo_path='', repo_name='', config=config)
99
99
100 wsgi_app_mock.assert_called_once_with(config)
100 wsgi_app_mock.assert_called_once_with(config)
101 assert wsgi_app == wsgi_app_mock()
101 assert wsgi_app == wsgi_app_mock()
102
102
103
103
104 class TestSimpleSvnApp(object):
104 class TestSimpleSvnApp(object):
105 data = '<xml></xml>'
105 data = '<xml></xml>'
106 path = '/group/my-repo'
106 path = '/group/my-repo'
107 wsgi_input = StringIO(data)
107 wsgi_input = StringIO(data)
108 environment = {
108 environment = {
109 'HTTP_DAV': (
109 'HTTP_DAV': (
110 'http://subversion.tigris.org/xmlns/dav/svn/depth,'
110 'http://subversion.tigris.org/xmlns/dav/svn/depth,'
111 ' http://subversion.tigris.org/xmlns/dav/svn/mergeinfo'),
111 ' http://subversion.tigris.org/xmlns/dav/svn/mergeinfo'),
112 'HTTP_USER_AGENT': 'SVN/1.8.11 (x86_64-linux) serf/1.3.8',
112 'HTTP_USER_AGENT': 'SVN/1.8.11 (x86_64-linux) serf/1.3.8',
113 'REQUEST_METHOD': 'OPTIONS',
113 'REQUEST_METHOD': 'OPTIONS',
114 'PATH_INFO': path,
114 'PATH_INFO': path,
115 'wsgi.input': wsgi_input,
115 'wsgi.input': wsgi_input,
116 'CONTENT_TYPE': 'text/xml',
116 'CONTENT_TYPE': 'text/xml',
117 'CONTENT_LENGTH': '130'
117 'CONTENT_LENGTH': '130'
118 }
118 }
119
119
120 def setup_method(self, method):
120 def setup_method(self, method):
121 self.host = 'http://localhost/'
121 self.host = 'http://localhost/'
122 base_path = get_rhodecode_base_path()
122 base_path = get_rhodecode_base_path()
123 self.app = SimpleSvnApp(
123 self.app = SimpleSvnApp(
124 config={'subversion_http_server_url': self.host,
124 config={'subversion_http_server_url': self.host,
125 'base_path': base_path})
125 'base_path': base_path})
126
126
127 def test_get_request_headers_with_content_type(self):
127 def test_get_request_headers_with_content_type(self):
128 expected_headers = {
128 expected_headers = {
129 'Dav': self.environment['HTTP_DAV'],
129 'Dav': self.environment['HTTP_DAV'],
130 'User-Agent': self.environment['HTTP_USER_AGENT'],
130 'User-Agent': self.environment['HTTP_USER_AGENT'],
131 'Content-Type': self.environment['CONTENT_TYPE'],
131 'Content-Type': self.environment['CONTENT_TYPE'],
132 'Content-Length': self.environment['CONTENT_LENGTH']
132 'Content-Length': self.environment['CONTENT_LENGTH']
133 }
133 }
134 headers = self.app._get_request_headers(self.environment)
134 headers = self.app._get_request_headers(self.environment)
135 assert headers == expected_headers
135 assert headers == expected_headers
136
136
137 def test_get_request_headers_without_content_type(self):
137 def test_get_request_headers_without_content_type(self):
138 environment = self.environment.copy()
138 environment = self.environment.copy()
139 environment.pop('CONTENT_TYPE')
139 environment.pop('CONTENT_TYPE')
140 expected_headers = {
140 expected_headers = {
141 'Dav': environment['HTTP_DAV'],
141 'Dav': environment['HTTP_DAV'],
142 'Content-Length': self.environment['CONTENT_LENGTH'],
142 'Content-Length': self.environment['CONTENT_LENGTH'],
143 'User-Agent': environment['HTTP_USER_AGENT'],
143 'User-Agent': environment['HTTP_USER_AGENT'],
144 }
144 }
145 request_headers = self.app._get_request_headers(environment)
145 request_headers = self.app._get_request_headers(environment)
146 assert request_headers == expected_headers
146 assert request_headers == expected_headers
147
147
148 def test_get_response_headers(self):
148 def test_get_response_headers(self):
149 headers = {
149 headers = {
150 'Connection': 'keep-alive',
150 'Connection': 'keep-alive',
151 'Keep-Alive': 'timeout=5, max=100',
151 'Keep-Alive': 'timeout=5, max=100',
152 'Transfer-Encoding': 'chunked',
152 'Transfer-Encoding': 'chunked',
153 'Content-Encoding': 'gzip',
153 'Content-Encoding': 'gzip',
154 'MS-Author-Via': 'DAV',
154 'MS-Author-Via': 'DAV',
155 'SVN-Supported-Posts': 'create-txn-with-props'
155 'SVN-Supported-Posts': 'create-txn-with-props'
156 }
156 }
157 expected_headers = [
157 expected_headers = [
158 ('MS-Author-Via', 'DAV'),
158 ('MS-Author-Via', 'DAV'),
159 ('SVN-Supported-Posts', 'create-txn-with-props'),
159 ('SVN-Supported-Posts', 'create-txn-with-props'),
160 ]
160 ]
161 response_headers = self.app._get_response_headers(headers)
161 response_headers = self.app._get_response_headers(headers)
162 assert sorted(response_headers) == sorted(expected_headers)
162 assert sorted(response_headers) == sorted(expected_headers)
163
163
164 def test_get_url(self):
164 @pytest.mark.parametrize('svn_http_url, path_info, expected_url', [
165 url = self.app._get_url(self.path)
165 ('http://localhost:8200', '/repo_name', 'http://localhost:8200/repo_name'),
166 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
166 ('http://localhost:8200///', '/repo_name', 'http://localhost:8200/repo_name'),
167 ('http://localhost:8200', '/group/repo_name', 'http://localhost:8200/group/repo_name'),
168 ('http://localhost:8200/', '/group/repo_name', 'http://localhost:8200/group/repo_name'),
169 ('http://localhost:8200/prefix', '/repo_name', 'http://localhost:8200/prefix/repo_name'),
170 ('http://localhost:8200/prefix', 'repo_name', 'http://localhost:8200/prefix/repo_name'),
171 ('http://localhost:8200/prefix', '/group/repo_name', 'http://localhost:8200/prefix/group/repo_name')
172 ])
173 def test_get_url(self, svn_http_url, path_info, expected_url):
174 url = self.app._get_url(svn_http_url, path_info)
167 assert url == expected_url
175 assert url == expected_url
168
176
169 def test_call(self):
177 def test_call(self):
170 start_response = Mock()
178 start_response = Mock()
171 response_mock = Mock()
179 response_mock = Mock()
172 response_mock.headers = {
180 response_mock.headers = {
173 'Content-Encoding': 'gzip',
181 'Content-Encoding': 'gzip',
174 'MS-Author-Via': 'DAV',
182 'MS-Author-Via': 'DAV',
175 'SVN-Supported-Posts': 'create-txn-with-props'
183 'SVN-Supported-Posts': 'create-txn-with-props'
176 }
184 }
177 response_mock.status_code = 200
185 response_mock.status_code = 200
178 response_mock.reason = 'OK'
186 response_mock.reason = 'OK'
179 with patch('rhodecode.lib.middleware.simplesvn.requests.request') as (
187 with patch('rhodecode.lib.middleware.simplesvn.requests.request') as (
180 request_mock):
188 request_mock):
181 request_mock.return_value = response_mock
189 request_mock.return_value = response_mock
182 self.app(self.environment, start_response)
190 self.app(self.environment, start_response)
183
191
184 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
192 expected_url = '{}{}'.format(self.host.strip('/'), self.path)
185 expected_request_headers = {
193 expected_request_headers = {
186 'Dav': self.environment['HTTP_DAV'],
194 'Dav': self.environment['HTTP_DAV'],
187 'User-Agent': self.environment['HTTP_USER_AGENT'],
195 'User-Agent': self.environment['HTTP_USER_AGENT'],
188 'Content-Type': self.environment['CONTENT_TYPE'],
196 'Content-Type': self.environment['CONTENT_TYPE'],
189 'Content-Length': self.environment['CONTENT_LENGTH']
197 'Content-Length': self.environment['CONTENT_LENGTH']
190 }
198 }
191 expected_response_headers = [
199 expected_response_headers = [
192 ('SVN-Supported-Posts', 'create-txn-with-props'),
200 ('SVN-Supported-Posts', 'create-txn-with-props'),
193 ('MS-Author-Via', 'DAV'),
201 ('MS-Author-Via', 'DAV'),
194 ]
202 ]
195 request_mock.assert_called_once_with(
203 request_mock.assert_called_once_with(
196 self.environment['REQUEST_METHOD'], expected_url,
204 self.environment['REQUEST_METHOD'], expected_url,
197 data=self.data, headers=expected_request_headers, stream=False)
205 data=self.data, headers=expected_request_headers, stream=False)
198 response_mock.iter_content.assert_called_once_with(chunk_size=1024)
206 response_mock.iter_content.assert_called_once_with(chunk_size=1024)
199 args, _ = start_response.call_args
207 args, _ = start_response.call_args
200 assert args[0] == '200 OK'
208 assert args[0] == '200 OK'
201 assert sorted(args[1]) == sorted(expected_response_headers)
209 assert sorted(args[1]) == sorted(expected_response_headers)
General Comments 0
You need to be logged in to leave comments. Login now