##// END OF EJS Templates
release: merge back stable branch into default
marcink -
r4469:9d632087 merge default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,42 b''
1 |RCE| 4.20.1 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-07-27
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18 - Permissions: rename write+ to write or higher for more explicit meaning.
19
20
21 Security
22 ^^^^^^^^
23
24
25
26 Performance
27 ^^^^^^^^^^^
28
29
30
31 Fixes
32 ^^^^^
33
34 - Files: fixed creation of new files for empty repos.
35 - Notifications: properly inject the custom email headers into templates.
36 - Store file integration: fixed support for nested subdirs.
37
38
39 Upgrade notes
40 ^^^^^^^^^^^^^
41
42 - Un-scheduled release addressing problems in 4.20.X releases.
@@ -1,68 +1,70 b''
1 1bd3e92b7e2e2d2024152b34bb88dff1db544a71 v4.0.0
1 1bd3e92b7e2e2d2024152b34bb88dff1db544a71 v4.0.0
2 170c5398320ea6cddd50955e88d408794c21d43a v4.0.1
2 170c5398320ea6cddd50955e88d408794c21d43a v4.0.1
3 c3fe200198f5aa34cf2e4066df2881a9cefe3704 v4.1.0
3 c3fe200198f5aa34cf2e4066df2881a9cefe3704 v4.1.0
4 7fd5c850745e2ea821fb4406af5f4bff9b0a7526 v4.1.1
4 7fd5c850745e2ea821fb4406af5f4bff9b0a7526 v4.1.1
5 41c87da28a179953df86061d817bc35533c66dd2 v4.1.2
5 41c87da28a179953df86061d817bc35533c66dd2 v4.1.2
6 baaf9f5bcea3bae0ef12ae20c8b270482e62abb6 v4.2.0
6 baaf9f5bcea3bae0ef12ae20c8b270482e62abb6 v4.2.0
7 32a70c7e56844a825f61df496ee5eaf8c3c4e189 v4.2.1
7 32a70c7e56844a825f61df496ee5eaf8c3c4e189 v4.2.1
8 fa695cdb411d294679ac081d595ac654e5613b03 v4.3.0
8 fa695cdb411d294679ac081d595ac654e5613b03 v4.3.0
9 0e4dc11b58cad833c513fe17bac39e6850edf959 v4.3.1
9 0e4dc11b58cad833c513fe17bac39e6850edf959 v4.3.1
10 8a876f48f5cb1d018b837db28ff928500cb32cfb v4.4.0
10 8a876f48f5cb1d018b837db28ff928500cb32cfb v4.4.0
11 8dd86b410b1aac086ffdfc524ef300f896af5047 v4.4.1
11 8dd86b410b1aac086ffdfc524ef300f896af5047 v4.4.1
12 d2514226abc8d3b4f6fb57765f47d1b6fb360a05 v4.4.2
12 d2514226abc8d3b4f6fb57765f47d1b6fb360a05 v4.4.2
13 27d783325930af6dad2741476c0d0b1b7c8415c2 v4.5.0
13 27d783325930af6dad2741476c0d0b1b7c8415c2 v4.5.0
14 7f2016f352abcbdba4a19d4039c386e9629449da v4.5.1
14 7f2016f352abcbdba4a19d4039c386e9629449da v4.5.1
15 416fec799314c70a5c780fb28b3357b08869333a v4.5.2
15 416fec799314c70a5c780fb28b3357b08869333a v4.5.2
16 27c3b85fafc83143e6678fbc3da69e1615bcac55 v4.6.0
16 27c3b85fafc83143e6678fbc3da69e1615bcac55 v4.6.0
17 5ad13deb9118c2a5243d4032d4d9cc174e5872db v4.6.1
17 5ad13deb9118c2a5243d4032d4d9cc174e5872db v4.6.1
18 2be921e01fa24bb102696ada596f87464c3666f6 v4.7.0
18 2be921e01fa24bb102696ada596f87464c3666f6 v4.7.0
19 7198bdec29c2872c974431d55200d0398354cdb1 v4.7.1
19 7198bdec29c2872c974431d55200d0398354cdb1 v4.7.1
20 bd1c8d230fe741c2dfd7100a0ef39fd0774fd581 v4.7.2
20 bd1c8d230fe741c2dfd7100a0ef39fd0774fd581 v4.7.2
21 9731914f89765d9628dc4dddc84bc9402aa124c8 v4.8.0
21 9731914f89765d9628dc4dddc84bc9402aa124c8 v4.8.0
22 c5a2b7d0e4bbdebc4a62d7b624befe375207b659 v4.9.0
22 c5a2b7d0e4bbdebc4a62d7b624befe375207b659 v4.9.0
23 d9aa3b27ac9f7e78359775c75fedf7bfece232f1 v4.9.1
23 d9aa3b27ac9f7e78359775c75fedf7bfece232f1 v4.9.1
24 4ba4d74981cec5d6b28b158f875a2540952c2f74 v4.10.0
24 4ba4d74981cec5d6b28b158f875a2540952c2f74 v4.10.0
25 0a6821cbd6b0b3c21503002f88800679fa35ab63 v4.10.1
25 0a6821cbd6b0b3c21503002f88800679fa35ab63 v4.10.1
26 434ad90ec8d621f4416074b84f6e9ce03964defb v4.10.2
26 434ad90ec8d621f4416074b84f6e9ce03964defb v4.10.2
27 68baee10e698da2724c6e0f698c03a6abb993bf2 v4.10.3
27 68baee10e698da2724c6e0f698c03a6abb993bf2 v4.10.3
28 00821d3afd1dce3f4767cc353f84a17f7d5218a1 v4.10.4
28 00821d3afd1dce3f4767cc353f84a17f7d5218a1 v4.10.4
29 22f6744ad8cc274311825f63f953e4dee2ea5cb9 v4.10.5
29 22f6744ad8cc274311825f63f953e4dee2ea5cb9 v4.10.5
30 96eb24bea2f5f9258775245e3f09f6fa0a4dda01 v4.10.6
30 96eb24bea2f5f9258775245e3f09f6fa0a4dda01 v4.10.6
31 3121217a812c956d7dd5a5875821bd73e8002a32 v4.11.0
31 3121217a812c956d7dd5a5875821bd73e8002a32 v4.11.0
32 fa98b454715ac5b912f39e84af54345909a2a805 v4.11.1
32 fa98b454715ac5b912f39e84af54345909a2a805 v4.11.1
33 3982abcfdcc229a723cebe52d3a9bcff10bba08e v4.11.2
33 3982abcfdcc229a723cebe52d3a9bcff10bba08e v4.11.2
34 33195f145db9172f0a8f1487e09207178a6ab065 v4.11.3
34 33195f145db9172f0a8f1487e09207178a6ab065 v4.11.3
35 194c74f33e32bbae6fc4d71ec5a999cff3c13605 v4.11.4
35 194c74f33e32bbae6fc4d71ec5a999cff3c13605 v4.11.4
36 8fbd8b0c3ddc2fa4ac9e4ca16942a03eb593df2d v4.11.5
36 8fbd8b0c3ddc2fa4ac9e4ca16942a03eb593df2d v4.11.5
37 f0609aa5d5d05a1ca2f97c3995542236131c9d8a v4.11.6
37 f0609aa5d5d05a1ca2f97c3995542236131c9d8a v4.11.6
38 b5b30547d90d2e088472a70c84878f429ffbf40d v4.12.0
38 b5b30547d90d2e088472a70c84878f429ffbf40d v4.12.0
39 9072253aa8894d20c00b4a43dc61c2168c1eff94 v4.12.1
39 9072253aa8894d20c00b4a43dc61c2168c1eff94 v4.12.1
40 6a517543ea9ef9987d74371bd2a315eb0b232dc9 v4.12.2
40 6a517543ea9ef9987d74371bd2a315eb0b232dc9 v4.12.2
41 7fc0731b024c3114be87865eda7ab621cc957e32 v4.12.3
41 7fc0731b024c3114be87865eda7ab621cc957e32 v4.12.3
42 6d531c0b068c6eda62dddceedc9f845ecb6feb6f v4.12.4
42 6d531c0b068c6eda62dddceedc9f845ecb6feb6f v4.12.4
43 3d6bf2d81b1564830eb5e83396110d2a9a93eb1e v4.13.0
43 3d6bf2d81b1564830eb5e83396110d2a9a93eb1e v4.13.0
44 5468fc89e708bd90e413cd0d54350017abbdbc0e v4.13.1
44 5468fc89e708bd90e413cd0d54350017abbdbc0e v4.13.1
45 610d621550521c314ee97b3d43473ac0bcf06fb8 v4.13.2
45 610d621550521c314ee97b3d43473ac0bcf06fb8 v4.13.2
46 7dc62c090881fb5d03268141e71e0940d7c3295d v4.13.3
46 7dc62c090881fb5d03268141e71e0940d7c3295d v4.13.3
47 9151328c1c46b72ba6f00d7640d9141e75aa1ca2 v4.14.0
47 9151328c1c46b72ba6f00d7640d9141e75aa1ca2 v4.14.0
48 a47eeac5dfa41fa6779d90452affba4091c3ade8 v4.14.1
48 a47eeac5dfa41fa6779d90452affba4091c3ade8 v4.14.1
49 4b34ce0d2c3c10510626b3b65044939bb7a2cddf v4.15.0
49 4b34ce0d2c3c10510626b3b65044939bb7a2cddf v4.15.0
50 14502561d22e6b70613674cd675ae9a604b7989f v4.15.1
50 14502561d22e6b70613674cd675ae9a604b7989f v4.15.1
51 4aaa40b605b01af78a9f6882eca561c54b525ef0 v4.15.2
51 4aaa40b605b01af78a9f6882eca561c54b525ef0 v4.15.2
52 797744642eca86640ed20bef2cd77445780abaec v4.16.0
52 797744642eca86640ed20bef2cd77445780abaec v4.16.0
53 6c3452c7c25ed35ff269690929e11960ed6ad7d3 v4.16.1
53 6c3452c7c25ed35ff269690929e11960ed6ad7d3 v4.16.1
54 5d8057df561c4b6b81b6401aed7d2f911e6e77f7 v4.16.2
54 5d8057df561c4b6b81b6401aed7d2f911e6e77f7 v4.16.2
55 13acfc008896ef4c62546bab5074e8f6f89b4fa7 v4.17.0
55 13acfc008896ef4c62546bab5074e8f6f89b4fa7 v4.17.0
56 45b9b610976f483877142fe75321808ce9ebac59 v4.17.1
56 45b9b610976f483877142fe75321808ce9ebac59 v4.17.1
57 ad5bd0c4bd322fdbd04bb825a3d027e08f7a3901 v4.17.2
57 ad5bd0c4bd322fdbd04bb825a3d027e08f7a3901 v4.17.2
58 037f5794b55a6236d68f6485a485372dde6566e0 v4.17.3
58 037f5794b55a6236d68f6485a485372dde6566e0 v4.17.3
59 83bc3100cfd6094c1d04f475ddb299b7dc3d0b33 v4.17.4
59 83bc3100cfd6094c1d04f475ddb299b7dc3d0b33 v4.17.4
60 e3de8c95baf8cc9109ca56aee8193a2cb6a54c8a v4.17.4
60 e3de8c95baf8cc9109ca56aee8193a2cb6a54c8a v4.17.4
61 f37a3126570477543507f0bc9d245ce75546181a v4.18.0
61 f37a3126570477543507f0bc9d245ce75546181a v4.18.0
62 71d8791463e87b64c1a18475de330ee600d37561 v4.18.1
62 71d8791463e87b64c1a18475de330ee600d37561 v4.18.1
63 4bd6b75dac1d25c64885d4d49385e5533f21c525 v4.18.2
63 4bd6b75dac1d25c64885d4d49385e5533f21c525 v4.18.2
64 12ed92fe57f2e9fc7b71dc0b65e26c2da5c7085f v4.18.3
64 12ed92fe57f2e9fc7b71dc0b65e26c2da5c7085f v4.18.3
65 ddef396a6567117de531d67d44c739cbbfc3eebb v4.19.0
65 ddef396a6567117de531d67d44c739cbbfc3eebb v4.19.0
66 c0c65acd73914bf4368222d510afe1161ab8c07c v4.19.1
66 c0c65acd73914bf4368222d510afe1161ab8c07c v4.19.1
67 7ac623a4a2405917e2af660d645ded662011e40d v4.19.2
67 7ac623a4a2405917e2af660d645ded662011e40d v4.19.2
68 ef7ffda65eeb90c3ba88590a6cb816ef9b0bc232 v4.19.3
68 ef7ffda65eeb90c3ba88590a6cb816ef9b0bc232 v4.19.3
69 3e635489bb7961df93b01e42454ad1a8730ae968 v4.20.0
70 7e2eb896a02ca7cd2cd9f0f853ef3dac3f0039e3 v4.20.1
@@ -1,145 +1,146 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.20.1.rst
12 release-notes-4.20.0.rst
13 release-notes-4.20.0.rst
13 release-notes-4.19.3.rst
14 release-notes-4.19.3.rst
14 release-notes-4.19.2.rst
15 release-notes-4.19.2.rst
15 release-notes-4.19.1.rst
16 release-notes-4.19.1.rst
16 release-notes-4.19.0.rst
17 release-notes-4.19.0.rst
17 release-notes-4.18.3.rst
18 release-notes-4.18.3.rst
18 release-notes-4.18.2.rst
19 release-notes-4.18.2.rst
19 release-notes-4.18.1.rst
20 release-notes-4.18.1.rst
20 release-notes-4.18.0.rst
21 release-notes-4.18.0.rst
21 release-notes-4.17.4.rst
22 release-notes-4.17.4.rst
22 release-notes-4.17.3.rst
23 release-notes-4.17.3.rst
23 release-notes-4.17.2.rst
24 release-notes-4.17.2.rst
24 release-notes-4.17.1.rst
25 release-notes-4.17.1.rst
25 release-notes-4.17.0.rst
26 release-notes-4.17.0.rst
26 release-notes-4.16.2.rst
27 release-notes-4.16.2.rst
27 release-notes-4.16.1.rst
28 release-notes-4.16.1.rst
28 release-notes-4.16.0.rst
29 release-notes-4.16.0.rst
29 release-notes-4.15.2.rst
30 release-notes-4.15.2.rst
30 release-notes-4.15.1.rst
31 release-notes-4.15.1.rst
31 release-notes-4.15.0.rst
32 release-notes-4.15.0.rst
32 release-notes-4.14.1.rst
33 release-notes-4.14.1.rst
33 release-notes-4.14.0.rst
34 release-notes-4.14.0.rst
34 release-notes-4.13.3.rst
35 release-notes-4.13.3.rst
35 release-notes-4.13.2.rst
36 release-notes-4.13.2.rst
36 release-notes-4.13.1.rst
37 release-notes-4.13.1.rst
37 release-notes-4.13.0.rst
38 release-notes-4.13.0.rst
38 release-notes-4.12.4.rst
39 release-notes-4.12.4.rst
39 release-notes-4.12.3.rst
40 release-notes-4.12.3.rst
40 release-notes-4.12.2.rst
41 release-notes-4.12.2.rst
41 release-notes-4.12.1.rst
42 release-notes-4.12.1.rst
42 release-notes-4.12.0.rst
43 release-notes-4.12.0.rst
43 release-notes-4.11.6.rst
44 release-notes-4.11.6.rst
44 release-notes-4.11.5.rst
45 release-notes-4.11.5.rst
45 release-notes-4.11.4.rst
46 release-notes-4.11.4.rst
46 release-notes-4.11.3.rst
47 release-notes-4.11.3.rst
47 release-notes-4.11.2.rst
48 release-notes-4.11.2.rst
48 release-notes-4.11.1.rst
49 release-notes-4.11.1.rst
49 release-notes-4.11.0.rst
50 release-notes-4.11.0.rst
50 release-notes-4.10.6.rst
51 release-notes-4.10.6.rst
51 release-notes-4.10.5.rst
52 release-notes-4.10.5.rst
52 release-notes-4.10.4.rst
53 release-notes-4.10.4.rst
53 release-notes-4.10.3.rst
54 release-notes-4.10.3.rst
54 release-notes-4.10.2.rst
55 release-notes-4.10.2.rst
55 release-notes-4.10.1.rst
56 release-notes-4.10.1.rst
56 release-notes-4.10.0.rst
57 release-notes-4.10.0.rst
57 release-notes-4.9.1.rst
58 release-notes-4.9.1.rst
58 release-notes-4.9.0.rst
59 release-notes-4.9.0.rst
59 release-notes-4.8.0.rst
60 release-notes-4.8.0.rst
60 release-notes-4.7.2.rst
61 release-notes-4.7.2.rst
61 release-notes-4.7.1.rst
62 release-notes-4.7.1.rst
62 release-notes-4.7.0.rst
63 release-notes-4.7.0.rst
63 release-notes-4.6.1.rst
64 release-notes-4.6.1.rst
64 release-notes-4.6.0.rst
65 release-notes-4.6.0.rst
65 release-notes-4.5.2.rst
66 release-notes-4.5.2.rst
66 release-notes-4.5.1.rst
67 release-notes-4.5.1.rst
67 release-notes-4.5.0.rst
68 release-notes-4.5.0.rst
68 release-notes-4.4.2.rst
69 release-notes-4.4.2.rst
69 release-notes-4.4.1.rst
70 release-notes-4.4.1.rst
70 release-notes-4.4.0.rst
71 release-notes-4.4.0.rst
71 release-notes-4.3.1.rst
72 release-notes-4.3.1.rst
72 release-notes-4.3.0.rst
73 release-notes-4.3.0.rst
73 release-notes-4.2.1.rst
74 release-notes-4.2.1.rst
74 release-notes-4.2.0.rst
75 release-notes-4.2.0.rst
75 release-notes-4.1.2.rst
76 release-notes-4.1.2.rst
76 release-notes-4.1.1.rst
77 release-notes-4.1.1.rst
77 release-notes-4.1.0.rst
78 release-notes-4.1.0.rst
78 release-notes-4.0.1.rst
79 release-notes-4.0.1.rst
79 release-notes-4.0.0.rst
80 release-notes-4.0.0.rst
80
81
81 |RCE| 3.x Versions
82 |RCE| 3.x Versions
82 ------------------
83 ------------------
83
84
84 .. toctree::
85 .. toctree::
85 :maxdepth: 1
86 :maxdepth: 1
86
87
87 release-notes-3.8.4.rst
88 release-notes-3.8.4.rst
88 release-notes-3.8.3.rst
89 release-notes-3.8.3.rst
89 release-notes-3.8.2.rst
90 release-notes-3.8.2.rst
90 release-notes-3.8.1.rst
91 release-notes-3.8.1.rst
91 release-notes-3.8.0.rst
92 release-notes-3.8.0.rst
92 release-notes-3.7.1.rst
93 release-notes-3.7.1.rst
93 release-notes-3.7.0.rst
94 release-notes-3.7.0.rst
94 release-notes-3.6.1.rst
95 release-notes-3.6.1.rst
95 release-notes-3.6.0.rst
96 release-notes-3.6.0.rst
96 release-notes-3.5.2.rst
97 release-notes-3.5.2.rst
97 release-notes-3.5.1.rst
98 release-notes-3.5.1.rst
98 release-notes-3.5.0.rst
99 release-notes-3.5.0.rst
99 release-notes-3.4.1.rst
100 release-notes-3.4.1.rst
100 release-notes-3.4.0.rst
101 release-notes-3.4.0.rst
101 release-notes-3.3.4.rst
102 release-notes-3.3.4.rst
102 release-notes-3.3.3.rst
103 release-notes-3.3.3.rst
103 release-notes-3.3.2.rst
104 release-notes-3.3.2.rst
104 release-notes-3.3.1.rst
105 release-notes-3.3.1.rst
105 release-notes-3.3.0.rst
106 release-notes-3.3.0.rst
106 release-notes-3.2.3.rst
107 release-notes-3.2.3.rst
107 release-notes-3.2.2.rst
108 release-notes-3.2.2.rst
108 release-notes-3.2.1.rst
109 release-notes-3.2.1.rst
109 release-notes-3.2.0.rst
110 release-notes-3.2.0.rst
110 release-notes-3.1.1.rst
111 release-notes-3.1.1.rst
111 release-notes-3.1.0.rst
112 release-notes-3.1.0.rst
112 release-notes-3.0.2.rst
113 release-notes-3.0.2.rst
113 release-notes-3.0.1.rst
114 release-notes-3.0.1.rst
114 release-notes-3.0.0.rst
115 release-notes-3.0.0.rst
115
116
116 |RCE| 2.x Versions
117 |RCE| 2.x Versions
117 ------------------
118 ------------------
118
119
119 .. toctree::
120 .. toctree::
120 :maxdepth: 1
121 :maxdepth: 1
121
122
122 release-notes-2.2.8.rst
123 release-notes-2.2.8.rst
123 release-notes-2.2.7.rst
124 release-notes-2.2.7.rst
124 release-notes-2.2.6.rst
125 release-notes-2.2.6.rst
125 release-notes-2.2.5.rst
126 release-notes-2.2.5.rst
126 release-notes-2.2.4.rst
127 release-notes-2.2.4.rst
127 release-notes-2.2.3.rst
128 release-notes-2.2.3.rst
128 release-notes-2.2.2.rst
129 release-notes-2.2.2.rst
129 release-notes-2.2.1.rst
130 release-notes-2.2.1.rst
130 release-notes-2.2.0.rst
131 release-notes-2.2.0.rst
131 release-notes-2.1.0.rst
132 release-notes-2.1.0.rst
132 release-notes-2.0.2.rst
133 release-notes-2.0.2.rst
133 release-notes-2.0.1.rst
134 release-notes-2.0.1.rst
134 release-notes-2.0.0.rst
135 release-notes-2.0.0.rst
135
136
136 |RCE| 1.x Versions
137 |RCE| 1.x Versions
137 ------------------
138 ------------------
138
139
139 .. toctree::
140 .. toctree::
140 :maxdepth: 1
141 :maxdepth: 1
141
142
142 release-notes-1.7.2.rst
143 release-notes-1.7.2.rst
143 release-notes-1.7.1.rst
144 release-notes-1.7.1.rst
144 release-notes-1.7.0.rst
145 release-notes-1.7.0.rst
145 release-notes-1.6.0.rst
146 release-notes-1.6.0.rst
@@ -1,1618 +1,1631 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 import urllib
28 import pathlib2
28 import pathlib2
29
29
30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31 from pyramid.view import view_config
31 from pyramid.view import view_config
32 from pyramid.renderers import render
32 from pyramid.renderers import render
33 from pyramid.response import Response
33 from pyramid.response import Response
34
34
35 import rhodecode
35 import rhodecode
36 from rhodecode.apps._base import RepoAppView
36 from rhodecode.apps._base import RepoAppView
37
37
38
38
39 from rhodecode.lib import diffs, helpers as h, rc_cache
39 from rhodecode.lib import diffs, helpers as h, rc_cache
40 from rhodecode.lib import audit_logger
40 from rhodecode.lib import audit_logger
41 from rhodecode.lib.view_utils import parse_path_ref
41 from rhodecode.lib.view_utils import parse_path_ref
42 from rhodecode.lib.exceptions import NonRelativePathError
42 from rhodecode.lib.exceptions import NonRelativePathError
43 from rhodecode.lib.codeblocks import (
43 from rhodecode.lib.codeblocks import (
44 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
44 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
45 from rhodecode.lib.utils2 import (
45 from rhodecode.lib.utils2 import (
46 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1, safe_unicode)
46 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1, safe_unicode)
47 from rhodecode.lib.auth import (
47 from rhodecode.lib.auth import (
48 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
48 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
49 from rhodecode.lib.vcs import path as vcspath
49 from rhodecode.lib.vcs import path as vcspath
50 from rhodecode.lib.vcs.backends.base import EmptyCommit
50 from rhodecode.lib.vcs.backends.base import EmptyCommit
51 from rhodecode.lib.vcs.conf import settings
51 from rhodecode.lib.vcs.conf import settings
52 from rhodecode.lib.vcs.nodes import FileNode
52 from rhodecode.lib.vcs.nodes import FileNode
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
54 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
55 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
55 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
56 NodeDoesNotExistError, CommitError, NodeError)
56 NodeDoesNotExistError, CommitError, NodeError)
57
57
58 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.db import Repository
59 from rhodecode.model.db import Repository
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63
63
64 class RepoFilesView(RepoAppView):
64 class RepoFilesView(RepoAppView):
65
65
66 @staticmethod
66 @staticmethod
67 def adjust_file_path_for_svn(f_path, repo):
67 def adjust_file_path_for_svn(f_path, repo):
68 """
68 """
69 Computes the relative path of `f_path`.
69 Computes the relative path of `f_path`.
70
70
71 This is mainly based on prefix matching of the recognized tags and
71 This is mainly based on prefix matching of the recognized tags and
72 branches in the underlying repository.
72 branches in the underlying repository.
73 """
73 """
74 tags_and_branches = itertools.chain(
74 tags_and_branches = itertools.chain(
75 repo.branches.iterkeys(),
75 repo.branches.iterkeys(),
76 repo.tags.iterkeys())
76 repo.tags.iterkeys())
77 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
77 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
78
78
79 for name in tags_and_branches:
79 for name in tags_and_branches:
80 if f_path.startswith('{}/'.format(name)):
80 if f_path.startswith('{}/'.format(name)):
81 f_path = vcspath.relpath(f_path, name)
81 f_path = vcspath.relpath(f_path, name)
82 break
82 break
83 return f_path
83 return f_path
84
84
85 def load_default_context(self):
85 def load_default_context(self):
86 c = self._get_local_tmpl_context(include_app_defaults=True)
86 c = self._get_local_tmpl_context(include_app_defaults=True)
87 c.rhodecode_repo = self.rhodecode_vcs_repo
87 c.rhodecode_repo = self.rhodecode_vcs_repo
88 c.enable_downloads = self.db_repo.enable_downloads
88 c.enable_downloads = self.db_repo.enable_downloads
89 return c
89 return c
90
90
91 def _ensure_not_locked(self, commit_id='tip'):
91 def _ensure_not_locked(self, commit_id='tip'):
92 _ = self.request.translate
92 _ = self.request.translate
93
93
94 repo = self.db_repo
94 repo = self.db_repo
95 if repo.enable_locking and repo.locked[0]:
95 if repo.enable_locking and repo.locked[0]:
96 h.flash(_('This repository has been locked by %s on %s')
96 h.flash(_('This repository has been locked by %s on %s')
97 % (h.person_by_id(repo.locked[0]),
97 % (h.person_by_id(repo.locked[0]),
98 h.format_date(h.time_to_datetime(repo.locked[1]))),
98 h.format_date(h.time_to_datetime(repo.locked[1]))),
99 'warning')
99 'warning')
100 files_url = h.route_path(
100 files_url = h.route_path(
101 'repo_files:default_path',
101 'repo_files:default_path',
102 repo_name=self.db_repo_name, commit_id=commit_id)
102 repo_name=self.db_repo_name, commit_id=commit_id)
103 raise HTTPFound(files_url)
103 raise HTTPFound(files_url)
104
104
105 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
105 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
106 _ = self.request.translate
106 _ = self.request.translate
107
107
108 if not is_head:
108 if not is_head:
109 message = _('Cannot modify file. '
109 message = _('Cannot modify file. '
110 'Given commit `{}` is not head of a branch.').format(commit_id)
110 'Given commit `{}` is not head of a branch.').format(commit_id)
111 h.flash(message, category='warning')
111 h.flash(message, category='warning')
112
112
113 if json_mode:
113 if json_mode:
114 return message
114 return message
115
115
116 files_url = h.route_path(
116 files_url = h.route_path(
117 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
117 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
118 f_path=f_path)
118 f_path=f_path)
119 raise HTTPFound(files_url)
119 raise HTTPFound(files_url)
120
120
121 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
121 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
122 _ = self.request.translate
122 _ = self.request.translate
123
123
124 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
124 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
125 self.db_repo_name, branch_name)
125 self.db_repo_name, branch_name)
126 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
126 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
127 message = _('Branch `{}` changes forbidden by rule {}.').format(
127 message = _('Branch `{}` changes forbidden by rule {}.').format(
128 h.escape(branch_name), h.escape(rule))
128 h.escape(branch_name), h.escape(rule))
129 h.flash(message, 'warning')
129 h.flash(message, 'warning')
130
130
131 if json_mode:
131 if json_mode:
132 return message
132 return message
133
133
134 files_url = h.route_path(
134 files_url = h.route_path(
135 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
135 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
136
136
137 raise HTTPFound(files_url)
137 raise HTTPFound(files_url)
138
138
139 def _get_commit_and_path(self):
139 def _get_commit_and_path(self):
140 default_commit_id = self.db_repo.landing_ref_name
140 default_commit_id = self.db_repo.landing_ref_name
141 default_f_path = '/'
141 default_f_path = '/'
142
142
143 commit_id = self.request.matchdict.get(
143 commit_id = self.request.matchdict.get(
144 'commit_id', default_commit_id)
144 'commit_id', default_commit_id)
145 f_path = self._get_f_path(self.request.matchdict, default_f_path)
145 f_path = self._get_f_path(self.request.matchdict, default_f_path)
146 return commit_id, f_path
146 return commit_id, f_path
147
147
148 def _get_default_encoding(self, c):
148 def _get_default_encoding(self, c):
149 enc_list = getattr(c, 'default_encodings', [])
149 enc_list = getattr(c, 'default_encodings', [])
150 return enc_list[0] if enc_list else 'UTF-8'
150 return enc_list[0] if enc_list else 'UTF-8'
151
151
152 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
152 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
153 """
153 """
154 This is a safe way to get commit. If an error occurs it redirects to
154 This is a safe way to get commit. If an error occurs it redirects to
155 tip with proper message
155 tip with proper message
156
156
157 :param commit_id: id of commit to fetch
157 :param commit_id: id of commit to fetch
158 :param redirect_after: toggle redirection
158 :param redirect_after: toggle redirection
159 """
159 """
160 _ = self.request.translate
160 _ = self.request.translate
161
161
162 try:
162 try:
163 return self.rhodecode_vcs_repo.get_commit(commit_id)
163 return self.rhodecode_vcs_repo.get_commit(commit_id)
164 except EmptyRepositoryError:
164 except EmptyRepositoryError:
165 if not redirect_after:
165 if not redirect_after:
166 return None
166 return None
167
167
168 _url = h.route_path(
168 _url = h.route_path(
169 'repo_files_add_file',
169 'repo_files_add_file',
170 repo_name=self.db_repo_name, commit_id=0, f_path='')
170 repo_name=self.db_repo_name, commit_id=0, f_path='')
171
171
172 if h.HasRepoPermissionAny(
172 if h.HasRepoPermissionAny(
173 'repository.write', 'repository.admin')(self.db_repo_name):
173 'repository.write', 'repository.admin')(self.db_repo_name):
174 add_new = h.link_to(
174 add_new = h.link_to(
175 _('Click here to add a new file.'), _url, class_="alert-link")
175 _('Click here to add a new file.'), _url, class_="alert-link")
176 else:
176 else:
177 add_new = ""
177 add_new = ""
178
178
179 h.flash(h.literal(
179 h.flash(h.literal(
180 _('There are no files yet. %s') % add_new), category='warning')
180 _('There are no files yet. %s') % add_new), category='warning')
181 raise HTTPFound(
181 raise HTTPFound(
182 h.route_path('repo_summary', repo_name=self.db_repo_name))
182 h.route_path('repo_summary', repo_name=self.db_repo_name))
183
183
184 except (CommitDoesNotExistError, LookupError) as e:
184 except (CommitDoesNotExistError, LookupError) as e:
185 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
185 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
186 h.flash(msg, category='error')
186 h.flash(msg, category='error')
187 raise HTTPNotFound()
187 raise HTTPNotFound()
188 except RepositoryError as e:
188 except RepositoryError as e:
189 h.flash(safe_str(h.escape(e)), category='error')
189 h.flash(safe_str(h.escape(e)), category='error')
190 raise HTTPNotFound()
190 raise HTTPNotFound()
191
191
192 def _get_filenode_or_redirect(self, commit_obj, path):
192 def _get_filenode_or_redirect(self, commit_obj, path):
193 """
193 """
194 Returns file_node, if error occurs or given path is directory,
194 Returns file_node, if error occurs or given path is directory,
195 it'll redirect to top level path
195 it'll redirect to top level path
196 """
196 """
197 _ = self.request.translate
197 _ = self.request.translate
198
198
199 try:
199 try:
200 file_node = commit_obj.get_node(path)
200 file_node = commit_obj.get_node(path)
201 if file_node.is_dir():
201 if file_node.is_dir():
202 raise RepositoryError('The given path is a directory')
202 raise RepositoryError('The given path is a directory')
203 except CommitDoesNotExistError:
203 except CommitDoesNotExistError:
204 log.exception('No such commit exists for this repository')
204 log.exception('No such commit exists for this repository')
205 h.flash(_('No such commit exists for this repository'), category='error')
205 h.flash(_('No such commit exists for this repository'), category='error')
206 raise HTTPNotFound()
206 raise HTTPNotFound()
207 except RepositoryError as e:
207 except RepositoryError as e:
208 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
208 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
209 h.flash(safe_str(h.escape(e)), category='error')
209 h.flash(safe_str(h.escape(e)), category='error')
210 raise HTTPNotFound()
210 raise HTTPNotFound()
211
211
212 return file_node
212 return file_node
213
213
214 def _is_valid_head(self, commit_id, repo):
214 def _is_valid_head(self, commit_id, repo, landing_ref):
215 branch_name = sha_commit_id = ''
215 branch_name = sha_commit_id = ''
216 is_head = False
216 is_head = False
217 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
217 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
218
218
219 for _branch_name, branch_commit_id in repo.branches.items():
219 for _branch_name, branch_commit_id in repo.branches.items():
220 # simple case we pass in branch name, it's a HEAD
220 # simple case we pass in branch name, it's a HEAD
221 if commit_id == _branch_name:
221 if commit_id == _branch_name:
222 is_head = True
222 is_head = True
223 branch_name = _branch_name
223 branch_name = _branch_name
224 sha_commit_id = branch_commit_id
224 sha_commit_id = branch_commit_id
225 break
225 break
226 # case when we pass in full sha commit_id, which is a head
226 # case when we pass in full sha commit_id, which is a head
227 elif commit_id == branch_commit_id:
227 elif commit_id == branch_commit_id:
228 is_head = True
228 is_head = True
229 branch_name = _branch_name
229 branch_name = _branch_name
230 sha_commit_id = branch_commit_id
230 sha_commit_id = branch_commit_id
231 break
231 break
232
232
233 if h.is_svn(repo) and not repo.is_empty():
233 if h.is_svn(repo) and not repo.is_empty():
234 # Note: Subversion only has one head.
234 # Note: Subversion only has one head.
235 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
235 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
236 is_head = True
236 is_head = True
237 return branch_name, sha_commit_id, is_head
237 return branch_name, sha_commit_id, is_head
238
238
239 # checked branches, means we only need to try to get the branch/commit_sha
239 # checked branches, means we only need to try to get the branch/commit_sha
240 if not repo.is_empty():
240 if repo.is_empty():
241 is_head = True
242 branch_name = landing_ref
243 sha_commit_id = EmptyCommit().raw_id
244 else:
241 commit = repo.get_commit(commit_id=commit_id)
245 commit = repo.get_commit(commit_id=commit_id)
242 if commit:
246 if commit:
243 branch_name = commit.branch
247 branch_name = commit.branch
244 sha_commit_id = commit.raw_id
248 sha_commit_id = commit.raw_id
245
249
246 return branch_name, sha_commit_id, is_head
250 return branch_name, sha_commit_id, is_head
247
251
248 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
252 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
249
253
250 repo_id = self.db_repo.repo_id
254 repo_id = self.db_repo.repo_id
251 force_recache = self.get_recache_flag()
255 force_recache = self.get_recache_flag()
252
256
253 cache_seconds = safe_int(
257 cache_seconds = safe_int(
254 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
258 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
255 cache_on = not force_recache and cache_seconds > 0
259 cache_on = not force_recache and cache_seconds > 0
256 log.debug(
260 log.debug(
257 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
261 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
258 'with caching: %s[TTL: %ss]' % (
262 'with caching: %s[TTL: %ss]' % (
259 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
263 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
260
264
261 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
265 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
262 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
266 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
263
267
264 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
268 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
265 def compute_file_tree(ver, _name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
269 def compute_file_tree(ver, _name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
266 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
270 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
267 ver, _repo_id, _commit_id, _f_path)
271 ver, _repo_id, _commit_id, _f_path)
268
272
269 c.full_load = _full_load
273 c.full_load = _full_load
270 return render(
274 return render(
271 'rhodecode:templates/files/files_browser_tree.mako',
275 'rhodecode:templates/files/files_browser_tree.mako',
272 self._get_template_context(c), self.request, _at_rev)
276 self._get_template_context(c), self.request, _at_rev)
273
277
274 return compute_file_tree(
278 return compute_file_tree(
275 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
279 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
276 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
280 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
277
281
278 def _get_archive_spec(self, fname):
282 def _get_archive_spec(self, fname):
279 log.debug('Detecting archive spec for: `%s`', fname)
283 log.debug('Detecting archive spec for: `%s`', fname)
280
284
281 fileformat = None
285 fileformat = None
282 ext = None
286 ext = None
283 content_type = None
287 content_type = None
284 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
288 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
285
289
286 if fname.endswith(extension):
290 if fname.endswith(extension):
287 fileformat = a_type
291 fileformat = a_type
288 log.debug('archive is of type: %s', fileformat)
292 log.debug('archive is of type: %s', fileformat)
289 ext = extension
293 ext = extension
290 break
294 break
291
295
292 if not fileformat:
296 if not fileformat:
293 raise ValueError()
297 raise ValueError()
294
298
295 # left over part of whole fname is the commit
299 # left over part of whole fname is the commit
296 commit_id = fname[:-len(ext)]
300 commit_id = fname[:-len(ext)]
297
301
298 return commit_id, ext, fileformat, content_type
302 return commit_id, ext, fileformat, content_type
299
303
300 def create_pure_path(self, *parts):
304 def create_pure_path(self, *parts):
301 # Split paths and sanitize them, removing any ../ etc
305 # Split paths and sanitize them, removing any ../ etc
302 sanitized_path = [
306 sanitized_path = [
303 x for x in pathlib2.PurePath(*parts).parts
307 x for x in pathlib2.PurePath(*parts).parts
304 if x not in ['.', '..']]
308 if x not in ['.', '..']]
305
309
306 pure_path = pathlib2.PurePath(*sanitized_path)
310 pure_path = pathlib2.PurePath(*sanitized_path)
307 return pure_path
311 return pure_path
308
312
309 def _is_lf_enabled(self, target_repo):
313 def _is_lf_enabled(self, target_repo):
310 lf_enabled = False
314 lf_enabled = False
311
315
312 lf_key_for_vcs_map = {
316 lf_key_for_vcs_map = {
313 'hg': 'extensions_largefiles',
317 'hg': 'extensions_largefiles',
314 'git': 'vcs_git_lfs_enabled'
318 'git': 'vcs_git_lfs_enabled'
315 }
319 }
316
320
317 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
321 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
318
322
319 if lf_key_for_vcs:
323 if lf_key_for_vcs:
320 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
324 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
321
325
322 return lf_enabled
326 return lf_enabled
323
327
324 @LoginRequired()
328 @LoginRequired()
325 @HasRepoPermissionAnyDecorator(
329 @HasRepoPermissionAnyDecorator(
326 'repository.read', 'repository.write', 'repository.admin')
330 'repository.read', 'repository.write', 'repository.admin')
327 @view_config(
331 @view_config(
328 route_name='repo_archivefile', request_method='GET',
332 route_name='repo_archivefile', request_method='GET',
329 renderer=None)
333 renderer=None)
330 def repo_archivefile(self):
334 def repo_archivefile(self):
331 # archive cache config
335 # archive cache config
332 from rhodecode import CONFIG
336 from rhodecode import CONFIG
333 _ = self.request.translate
337 _ = self.request.translate
334 self.load_default_context()
338 self.load_default_context()
335 default_at_path = '/'
339 default_at_path = '/'
336 fname = self.request.matchdict['fname']
340 fname = self.request.matchdict['fname']
337 subrepos = self.request.GET.get('subrepos') == 'true'
341 subrepos = self.request.GET.get('subrepos') == 'true'
338 at_path = self.request.GET.get('at_path') or default_at_path
342 at_path = self.request.GET.get('at_path') or default_at_path
339
343
340 if not self.db_repo.enable_downloads:
344 if not self.db_repo.enable_downloads:
341 return Response(_('Downloads disabled'))
345 return Response(_('Downloads disabled'))
342
346
343 try:
347 try:
344 commit_id, ext, fileformat, content_type = \
348 commit_id, ext, fileformat, content_type = \
345 self._get_archive_spec(fname)
349 self._get_archive_spec(fname)
346 except ValueError:
350 except ValueError:
347 return Response(_('Unknown archive type for: `{}`').format(
351 return Response(_('Unknown archive type for: `{}`').format(
348 h.escape(fname)))
352 h.escape(fname)))
349
353
350 try:
354 try:
351 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
355 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
352 except CommitDoesNotExistError:
356 except CommitDoesNotExistError:
353 return Response(_('Unknown commit_id {}').format(
357 return Response(_('Unknown commit_id {}').format(
354 h.escape(commit_id)))
358 h.escape(commit_id)))
355 except EmptyRepositoryError:
359 except EmptyRepositoryError:
356 return Response(_('Empty repository'))
360 return Response(_('Empty repository'))
357
361
358 try:
362 try:
359 at_path = commit.get_node(at_path).path or default_at_path
363 at_path = commit.get_node(at_path).path or default_at_path
360 except Exception:
364 except Exception:
361 return Response(_('No node at path {} for this repository').format(at_path))
365 return Response(_('No node at path {} for this repository').format(at_path))
362
366
363 path_sha = sha1(at_path)[:8]
367 path_sha = sha1(at_path)[:8]
364
368
365 # original backward compat name of archive
369 # original backward compat name of archive
366 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
370 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
367 short_sha = safe_str(commit.short_id)
371 short_sha = safe_str(commit.short_id)
368
372
369 if at_path == default_at_path:
373 if at_path == default_at_path:
370 archive_name = '{}-{}{}{}'.format(
374 archive_name = '{}-{}{}{}'.format(
371 clean_name,
375 clean_name,
372 '-sub' if subrepos else '',
376 '-sub' if subrepos else '',
373 short_sha,
377 short_sha,
374 ext)
378 ext)
375 # custom path and new name
379 # custom path and new name
376 else:
380 else:
377 archive_name = '{}-{}{}-{}{}'.format(
381 archive_name = '{}-{}{}-{}{}'.format(
378 clean_name,
382 clean_name,
379 '-sub' if subrepos else '',
383 '-sub' if subrepos else '',
380 short_sha,
384 short_sha,
381 path_sha,
385 path_sha,
382 ext)
386 ext)
383
387
384 use_cached_archive = False
388 use_cached_archive = False
385 archive_cache_enabled = CONFIG.get(
389 archive_cache_enabled = CONFIG.get(
386 'archive_cache_dir') and not self.request.GET.get('no_cache')
390 'archive_cache_dir') and not self.request.GET.get('no_cache')
387 cached_archive_path = None
391 cached_archive_path = None
388
392
389 if archive_cache_enabled:
393 if archive_cache_enabled:
390 # check if we it's ok to write
394 # check if we it's ok to write
391 if not os.path.isdir(CONFIG['archive_cache_dir']):
395 if not os.path.isdir(CONFIG['archive_cache_dir']):
392 os.makedirs(CONFIG['archive_cache_dir'])
396 os.makedirs(CONFIG['archive_cache_dir'])
393 cached_archive_path = os.path.join(
397 cached_archive_path = os.path.join(
394 CONFIG['archive_cache_dir'], archive_name)
398 CONFIG['archive_cache_dir'], archive_name)
395 if os.path.isfile(cached_archive_path):
399 if os.path.isfile(cached_archive_path):
396 log.debug('Found cached archive in %s', cached_archive_path)
400 log.debug('Found cached archive in %s', cached_archive_path)
397 fd, archive = None, cached_archive_path
401 fd, archive = None, cached_archive_path
398 use_cached_archive = True
402 use_cached_archive = True
399 else:
403 else:
400 log.debug('Archive %s is not yet cached', archive_name)
404 log.debug('Archive %s is not yet cached', archive_name)
401
405
402 if not use_cached_archive:
406 if not use_cached_archive:
403 # generate new archive
407 # generate new archive
404 fd, archive = tempfile.mkstemp()
408 fd, archive = tempfile.mkstemp()
405 log.debug('Creating new temp archive in %s', archive)
409 log.debug('Creating new temp archive in %s', archive)
406 try:
410 try:
407 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
411 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
408 archive_at_path=at_path)
412 archive_at_path=at_path)
409 except ImproperArchiveTypeError:
413 except ImproperArchiveTypeError:
410 return _('Unknown archive type')
414 return _('Unknown archive type')
411 if archive_cache_enabled:
415 if archive_cache_enabled:
412 # if we generated the archive and we have cache enabled
416 # if we generated the archive and we have cache enabled
413 # let's use this for future
417 # let's use this for future
414 log.debug('Storing new archive in %s', cached_archive_path)
418 log.debug('Storing new archive in %s', cached_archive_path)
415 shutil.move(archive, cached_archive_path)
419 shutil.move(archive, cached_archive_path)
416 archive = cached_archive_path
420 archive = cached_archive_path
417
421
418 # store download action
422 # store download action
419 audit_logger.store_web(
423 audit_logger.store_web(
420 'repo.archive.download', action_data={
424 'repo.archive.download', action_data={
421 'user_agent': self.request.user_agent,
425 'user_agent': self.request.user_agent,
422 'archive_name': archive_name,
426 'archive_name': archive_name,
423 'archive_spec': fname,
427 'archive_spec': fname,
424 'archive_cached': use_cached_archive},
428 'archive_cached': use_cached_archive},
425 user=self._rhodecode_user,
429 user=self._rhodecode_user,
426 repo=self.db_repo,
430 repo=self.db_repo,
427 commit=True
431 commit=True
428 )
432 )
429
433
430 def get_chunked_archive(archive_path):
434 def get_chunked_archive(archive_path):
431 with open(archive_path, 'rb') as stream:
435 with open(archive_path, 'rb') as stream:
432 while True:
436 while True:
433 data = stream.read(16 * 1024)
437 data = stream.read(16 * 1024)
434 if not data:
438 if not data:
435 if fd: # fd means we used temporary file
439 if fd: # fd means we used temporary file
436 os.close(fd)
440 os.close(fd)
437 if not archive_cache_enabled:
441 if not archive_cache_enabled:
438 log.debug('Destroying temp archive %s', archive_path)
442 log.debug('Destroying temp archive %s', archive_path)
439 os.remove(archive_path)
443 os.remove(archive_path)
440 break
444 break
441 yield data
445 yield data
442
446
443 response = Response(app_iter=get_chunked_archive(archive))
447 response = Response(app_iter=get_chunked_archive(archive))
444 response.content_disposition = str(
448 response.content_disposition = str(
445 'attachment; filename=%s' % archive_name)
449 'attachment; filename=%s' % archive_name)
446 response.content_type = str(content_type)
450 response.content_type = str(content_type)
447
451
448 return response
452 return response
449
453
450 def _get_file_node(self, commit_id, f_path):
454 def _get_file_node(self, commit_id, f_path):
451 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
455 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
452 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
456 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
453 try:
457 try:
454 node = commit.get_node(f_path)
458 node = commit.get_node(f_path)
455 if node.is_dir():
459 if node.is_dir():
456 raise NodeError('%s path is a %s not a file'
460 raise NodeError('%s path is a %s not a file'
457 % (node, type(node)))
461 % (node, type(node)))
458 except NodeDoesNotExistError:
462 except NodeDoesNotExistError:
459 commit = EmptyCommit(
463 commit = EmptyCommit(
460 commit_id=commit_id,
464 commit_id=commit_id,
461 idx=commit.idx,
465 idx=commit.idx,
462 repo=commit.repository,
466 repo=commit.repository,
463 alias=commit.repository.alias,
467 alias=commit.repository.alias,
464 message=commit.message,
468 message=commit.message,
465 author=commit.author,
469 author=commit.author,
466 date=commit.date)
470 date=commit.date)
467 node = FileNode(f_path, '', commit=commit)
471 node = FileNode(f_path, '', commit=commit)
468 else:
472 else:
469 commit = EmptyCommit(
473 commit = EmptyCommit(
470 repo=self.rhodecode_vcs_repo,
474 repo=self.rhodecode_vcs_repo,
471 alias=self.rhodecode_vcs_repo.alias)
475 alias=self.rhodecode_vcs_repo.alias)
472 node = FileNode(f_path, '', commit=commit)
476 node = FileNode(f_path, '', commit=commit)
473 return node
477 return node
474
478
475 @LoginRequired()
479 @LoginRequired()
476 @HasRepoPermissionAnyDecorator(
480 @HasRepoPermissionAnyDecorator(
477 'repository.read', 'repository.write', 'repository.admin')
481 'repository.read', 'repository.write', 'repository.admin')
478 @view_config(
482 @view_config(
479 route_name='repo_files_diff', request_method='GET',
483 route_name='repo_files_diff', request_method='GET',
480 renderer=None)
484 renderer=None)
481 def repo_files_diff(self):
485 def repo_files_diff(self):
482 c = self.load_default_context()
486 c = self.load_default_context()
483 f_path = self._get_f_path(self.request.matchdict)
487 f_path = self._get_f_path(self.request.matchdict)
484 diff1 = self.request.GET.get('diff1', '')
488 diff1 = self.request.GET.get('diff1', '')
485 diff2 = self.request.GET.get('diff2', '')
489 diff2 = self.request.GET.get('diff2', '')
486
490
487 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
491 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
488
492
489 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
493 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
490 line_context = self.request.GET.get('context', 3)
494 line_context = self.request.GET.get('context', 3)
491
495
492 if not any((diff1, diff2)):
496 if not any((diff1, diff2)):
493 h.flash(
497 h.flash(
494 'Need query parameter "diff1" or "diff2" to generate a diff.',
498 'Need query parameter "diff1" or "diff2" to generate a diff.',
495 category='error')
499 category='error')
496 raise HTTPBadRequest()
500 raise HTTPBadRequest()
497
501
498 c.action = self.request.GET.get('diff')
502 c.action = self.request.GET.get('diff')
499 if c.action not in ['download', 'raw']:
503 if c.action not in ['download', 'raw']:
500 compare_url = h.route_path(
504 compare_url = h.route_path(
501 'repo_compare',
505 'repo_compare',
502 repo_name=self.db_repo_name,
506 repo_name=self.db_repo_name,
503 source_ref_type='rev',
507 source_ref_type='rev',
504 source_ref=diff1,
508 source_ref=diff1,
505 target_repo=self.db_repo_name,
509 target_repo=self.db_repo_name,
506 target_ref_type='rev',
510 target_ref_type='rev',
507 target_ref=diff2,
511 target_ref=diff2,
508 _query=dict(f_path=f_path))
512 _query=dict(f_path=f_path))
509 # redirect to new view if we render diff
513 # redirect to new view if we render diff
510 raise HTTPFound(compare_url)
514 raise HTTPFound(compare_url)
511
515
512 try:
516 try:
513 node1 = self._get_file_node(diff1, path1)
517 node1 = self._get_file_node(diff1, path1)
514 node2 = self._get_file_node(diff2, f_path)
518 node2 = self._get_file_node(diff2, f_path)
515 except (RepositoryError, NodeError):
519 except (RepositoryError, NodeError):
516 log.exception("Exception while trying to get node from repository")
520 log.exception("Exception while trying to get node from repository")
517 raise HTTPFound(
521 raise HTTPFound(
518 h.route_path('repo_files', repo_name=self.db_repo_name,
522 h.route_path('repo_files', repo_name=self.db_repo_name,
519 commit_id='tip', f_path=f_path))
523 commit_id='tip', f_path=f_path))
520
524
521 if all(isinstance(node.commit, EmptyCommit)
525 if all(isinstance(node.commit, EmptyCommit)
522 for node in (node1, node2)):
526 for node in (node1, node2)):
523 raise HTTPNotFound()
527 raise HTTPNotFound()
524
528
525 c.commit_1 = node1.commit
529 c.commit_1 = node1.commit
526 c.commit_2 = node2.commit
530 c.commit_2 = node2.commit
527
531
528 if c.action == 'download':
532 if c.action == 'download':
529 _diff = diffs.get_gitdiff(node1, node2,
533 _diff = diffs.get_gitdiff(node1, node2,
530 ignore_whitespace=ignore_whitespace,
534 ignore_whitespace=ignore_whitespace,
531 context=line_context)
535 context=line_context)
532 diff = diffs.DiffProcessor(_diff, format='gitdiff')
536 diff = diffs.DiffProcessor(_diff, format='gitdiff')
533
537
534 response = Response(self.path_filter.get_raw_patch(diff))
538 response = Response(self.path_filter.get_raw_patch(diff))
535 response.content_type = 'text/plain'
539 response.content_type = 'text/plain'
536 response.content_disposition = (
540 response.content_disposition = (
537 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
541 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
538 )
542 )
539 charset = self._get_default_encoding(c)
543 charset = self._get_default_encoding(c)
540 if charset:
544 if charset:
541 response.charset = charset
545 response.charset = charset
542 return response
546 return response
543
547
544 elif c.action == 'raw':
548 elif c.action == 'raw':
545 _diff = diffs.get_gitdiff(node1, node2,
549 _diff = diffs.get_gitdiff(node1, node2,
546 ignore_whitespace=ignore_whitespace,
550 ignore_whitespace=ignore_whitespace,
547 context=line_context)
551 context=line_context)
548 diff = diffs.DiffProcessor(_diff, format='gitdiff')
552 diff = diffs.DiffProcessor(_diff, format='gitdiff')
549
553
550 response = Response(self.path_filter.get_raw_patch(diff))
554 response = Response(self.path_filter.get_raw_patch(diff))
551 response.content_type = 'text/plain'
555 response.content_type = 'text/plain'
552 charset = self._get_default_encoding(c)
556 charset = self._get_default_encoding(c)
553 if charset:
557 if charset:
554 response.charset = charset
558 response.charset = charset
555 return response
559 return response
556
560
557 # in case we ever end up here
561 # in case we ever end up here
558 raise HTTPNotFound()
562 raise HTTPNotFound()
559
563
560 @LoginRequired()
564 @LoginRequired()
561 @HasRepoPermissionAnyDecorator(
565 @HasRepoPermissionAnyDecorator(
562 'repository.read', 'repository.write', 'repository.admin')
566 'repository.read', 'repository.write', 'repository.admin')
563 @view_config(
567 @view_config(
564 route_name='repo_files_diff_2way_redirect', request_method='GET',
568 route_name='repo_files_diff_2way_redirect', request_method='GET',
565 renderer=None)
569 renderer=None)
566 def repo_files_diff_2way_redirect(self):
570 def repo_files_diff_2way_redirect(self):
567 """
571 """
568 Kept only to make OLD links work
572 Kept only to make OLD links work
569 """
573 """
570 f_path = self._get_f_path_unchecked(self.request.matchdict)
574 f_path = self._get_f_path_unchecked(self.request.matchdict)
571 diff1 = self.request.GET.get('diff1', '')
575 diff1 = self.request.GET.get('diff1', '')
572 diff2 = self.request.GET.get('diff2', '')
576 diff2 = self.request.GET.get('diff2', '')
573
577
574 if not any((diff1, diff2)):
578 if not any((diff1, diff2)):
575 h.flash(
579 h.flash(
576 'Need query parameter "diff1" or "diff2" to generate a diff.',
580 'Need query parameter "diff1" or "diff2" to generate a diff.',
577 category='error')
581 category='error')
578 raise HTTPBadRequest()
582 raise HTTPBadRequest()
579
583
580 compare_url = h.route_path(
584 compare_url = h.route_path(
581 'repo_compare',
585 'repo_compare',
582 repo_name=self.db_repo_name,
586 repo_name=self.db_repo_name,
583 source_ref_type='rev',
587 source_ref_type='rev',
584 source_ref=diff1,
588 source_ref=diff1,
585 target_ref_type='rev',
589 target_ref_type='rev',
586 target_ref=diff2,
590 target_ref=diff2,
587 _query=dict(f_path=f_path, diffmode='sideside',
591 _query=dict(f_path=f_path, diffmode='sideside',
588 target_repo=self.db_repo_name,))
592 target_repo=self.db_repo_name,))
589 raise HTTPFound(compare_url)
593 raise HTTPFound(compare_url)
590
594
591 @LoginRequired()
595 @LoginRequired()
592 @view_config(
596 @view_config(
593 route_name='repo_files:default_commit', request_method='GET',
597 route_name='repo_files:default_commit', request_method='GET',
594 renderer=None)
598 renderer=None)
595 def repo_files_default(self):
599 def repo_files_default(self):
596 c = self.load_default_context()
600 c = self.load_default_context()
597 ref_name = c.rhodecode_db_repo.landing_ref_name
601 ref_name = c.rhodecode_db_repo.landing_ref_name
598 landing_url = h.repo_files_by_ref_url(
602 landing_url = h.repo_files_by_ref_url(
599 c.rhodecode_db_repo.repo_name,
603 c.rhodecode_db_repo.repo_name,
600 c.rhodecode_db_repo.repo_type,
604 c.rhodecode_db_repo.repo_type,
601 f_path='',
605 f_path='',
602 ref_name=ref_name,
606 ref_name=ref_name,
603 commit_id='tip',
607 commit_id='tip',
604 query=dict(at=ref_name)
608 query=dict(at=ref_name)
605 )
609 )
606
610
607 raise HTTPFound(landing_url)
611 raise HTTPFound(landing_url)
608
612
609 @LoginRequired()
613 @LoginRequired()
610 @HasRepoPermissionAnyDecorator(
614 @HasRepoPermissionAnyDecorator(
611 'repository.read', 'repository.write', 'repository.admin')
615 'repository.read', 'repository.write', 'repository.admin')
612 @view_config(
616 @view_config(
613 route_name='repo_files', request_method='GET',
617 route_name='repo_files', request_method='GET',
614 renderer=None)
618 renderer=None)
615 @view_config(
619 @view_config(
616 route_name='repo_files:default_path', request_method='GET',
620 route_name='repo_files:default_path', request_method='GET',
617 renderer=None)
621 renderer=None)
618 @view_config(
622 @view_config(
619 route_name='repo_files:rendered', request_method='GET',
623 route_name='repo_files:rendered', request_method='GET',
620 renderer=None)
624 renderer=None)
621 @view_config(
625 @view_config(
622 route_name='repo_files:annotated', request_method='GET',
626 route_name='repo_files:annotated', request_method='GET',
623 renderer=None)
627 renderer=None)
624 def repo_files(self):
628 def repo_files(self):
625 c = self.load_default_context()
629 c = self.load_default_context()
626
630
627 view_name = getattr(self.request.matched_route, 'name', None)
631 view_name = getattr(self.request.matched_route, 'name', None)
628
632
629 c.annotate = view_name == 'repo_files:annotated'
633 c.annotate = view_name == 'repo_files:annotated'
630 # default is false, but .rst/.md files later are auto rendered, we can
634 # default is false, but .rst/.md files later are auto rendered, we can
631 # overwrite auto rendering by setting this GET flag
635 # overwrite auto rendering by setting this GET flag
632 c.renderer = view_name == 'repo_files:rendered' or \
636 c.renderer = view_name == 'repo_files:rendered' or \
633 not self.request.GET.get('no-render', False)
637 not self.request.GET.get('no-render', False)
634
638
635 commit_id, f_path = self._get_commit_and_path()
639 commit_id, f_path = self._get_commit_and_path()
636
640
637 c.commit = self._get_commit_or_redirect(commit_id)
641 c.commit = self._get_commit_or_redirect(commit_id)
638 c.branch = self.request.GET.get('branch', None)
642 c.branch = self.request.GET.get('branch', None)
639 c.f_path = f_path
643 c.f_path = f_path
640 at_rev = self.request.GET.get('at')
644 at_rev = self.request.GET.get('at')
641
645
642 # prev link
646 # prev link
643 try:
647 try:
644 prev_commit = c.commit.prev(c.branch)
648 prev_commit = c.commit.prev(c.branch)
645 c.prev_commit = prev_commit
649 c.prev_commit = prev_commit
646 c.url_prev = h.route_path(
650 c.url_prev = h.route_path(
647 'repo_files', repo_name=self.db_repo_name,
651 'repo_files', repo_name=self.db_repo_name,
648 commit_id=prev_commit.raw_id, f_path=f_path)
652 commit_id=prev_commit.raw_id, f_path=f_path)
649 if c.branch:
653 if c.branch:
650 c.url_prev += '?branch=%s' % c.branch
654 c.url_prev += '?branch=%s' % c.branch
651 except (CommitDoesNotExistError, VCSError):
655 except (CommitDoesNotExistError, VCSError):
652 c.url_prev = '#'
656 c.url_prev = '#'
653 c.prev_commit = EmptyCommit()
657 c.prev_commit = EmptyCommit()
654
658
655 # next link
659 # next link
656 try:
660 try:
657 next_commit = c.commit.next(c.branch)
661 next_commit = c.commit.next(c.branch)
658 c.next_commit = next_commit
662 c.next_commit = next_commit
659 c.url_next = h.route_path(
663 c.url_next = h.route_path(
660 'repo_files', repo_name=self.db_repo_name,
664 'repo_files', repo_name=self.db_repo_name,
661 commit_id=next_commit.raw_id, f_path=f_path)
665 commit_id=next_commit.raw_id, f_path=f_path)
662 if c.branch:
666 if c.branch:
663 c.url_next += '?branch=%s' % c.branch
667 c.url_next += '?branch=%s' % c.branch
664 except (CommitDoesNotExistError, VCSError):
668 except (CommitDoesNotExistError, VCSError):
665 c.url_next = '#'
669 c.url_next = '#'
666 c.next_commit = EmptyCommit()
670 c.next_commit = EmptyCommit()
667
671
668 # files or dirs
672 # files or dirs
669 try:
673 try:
670 c.file = c.commit.get_node(f_path)
674 c.file = c.commit.get_node(f_path)
671 c.file_author = True
675 c.file_author = True
672 c.file_tree = ''
676 c.file_tree = ''
673
677
674 # load file content
678 # load file content
675 if c.file.is_file():
679 if c.file.is_file():
676 c.lf_node = {}
680 c.lf_node = {}
677
681
678 has_lf_enabled = self._is_lf_enabled(self.db_repo)
682 has_lf_enabled = self._is_lf_enabled(self.db_repo)
679 if has_lf_enabled:
683 if has_lf_enabled:
680 c.lf_node = c.file.get_largefile_node()
684 c.lf_node = c.file.get_largefile_node()
681
685
682 c.file_source_page = 'true'
686 c.file_source_page = 'true'
683 c.file_last_commit = c.file.last_commit
687 c.file_last_commit = c.file.last_commit
684
688
685 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
689 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
686
690
687 if not (c.file_size_too_big or c.file.is_binary):
691 if not (c.file_size_too_big or c.file.is_binary):
688 if c.annotate: # annotation has precedence over renderer
692 if c.annotate: # annotation has precedence over renderer
689 c.annotated_lines = filenode_as_annotated_lines_tokens(
693 c.annotated_lines = filenode_as_annotated_lines_tokens(
690 c.file
694 c.file
691 )
695 )
692 else:
696 else:
693 c.renderer = (
697 c.renderer = (
694 c.renderer and h.renderer_from_filename(c.file.path)
698 c.renderer and h.renderer_from_filename(c.file.path)
695 )
699 )
696 if not c.renderer:
700 if not c.renderer:
697 c.lines = filenode_as_lines_tokens(c.file)
701 c.lines = filenode_as_lines_tokens(c.file)
698
702
699 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
703 _branch_name, _sha_commit_id, is_head = \
700 commit_id, self.rhodecode_vcs_repo)
704 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
705 landing_ref=self.db_repo.landing_ref_name)
701 c.on_branch_head = is_head
706 c.on_branch_head = is_head
702
707
703 branch = c.commit.branch if (
708 branch = c.commit.branch if (
704 c.commit.branch and '/' not in c.commit.branch) else None
709 c.commit.branch and '/' not in c.commit.branch) else None
705 c.branch_or_raw_id = branch or c.commit.raw_id
710 c.branch_or_raw_id = branch or c.commit.raw_id
706 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
711 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
707
712
708 author = c.file_last_commit.author
713 author = c.file_last_commit.author
709 c.authors = [[
714 c.authors = [[
710 h.email(author),
715 h.email(author),
711 h.person(author, 'username_or_name_or_email'),
716 h.person(author, 'username_or_name_or_email'),
712 1
717 1
713 ]]
718 ]]
714
719
715 else: # load tree content at path
720 else: # load tree content at path
716 c.file_source_page = 'false'
721 c.file_source_page = 'false'
717 c.authors = []
722 c.authors = []
718 # this loads a simple tree without metadata to speed things up
723 # this loads a simple tree without metadata to speed things up
719 # later via ajax we call repo_nodetree_full and fetch whole
724 # later via ajax we call repo_nodetree_full and fetch whole
720 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
725 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
721
726
722 c.readme_data, c.readme_file = \
727 c.readme_data, c.readme_file = \
723 self._get_readme_data(self.db_repo, c.visual.default_renderer,
728 self._get_readme_data(self.db_repo, c.visual.default_renderer,
724 c.commit.raw_id, f_path)
729 c.commit.raw_id, f_path)
725
730
726 except RepositoryError as e:
731 except RepositoryError as e:
727 h.flash(safe_str(h.escape(e)), category='error')
732 h.flash(safe_str(h.escape(e)), category='error')
728 raise HTTPNotFound()
733 raise HTTPNotFound()
729
734
730 if self.request.environ.get('HTTP_X_PJAX'):
735 if self.request.environ.get('HTTP_X_PJAX'):
731 html = render('rhodecode:templates/files/files_pjax.mako',
736 html = render('rhodecode:templates/files/files_pjax.mako',
732 self._get_template_context(c), self.request)
737 self._get_template_context(c), self.request)
733 else:
738 else:
734 html = render('rhodecode:templates/files/files.mako',
739 html = render('rhodecode:templates/files/files.mako',
735 self._get_template_context(c), self.request)
740 self._get_template_context(c), self.request)
736 return Response(html)
741 return Response(html)
737
742
738 @HasRepoPermissionAnyDecorator(
743 @HasRepoPermissionAnyDecorator(
739 'repository.read', 'repository.write', 'repository.admin')
744 'repository.read', 'repository.write', 'repository.admin')
740 @view_config(
745 @view_config(
741 route_name='repo_files:annotated_previous', request_method='GET',
746 route_name='repo_files:annotated_previous', request_method='GET',
742 renderer=None)
747 renderer=None)
743 def repo_files_annotated_previous(self):
748 def repo_files_annotated_previous(self):
744 self.load_default_context()
749 self.load_default_context()
745
750
746 commit_id, f_path = self._get_commit_and_path()
751 commit_id, f_path = self._get_commit_and_path()
747 commit = self._get_commit_or_redirect(commit_id)
752 commit = self._get_commit_or_redirect(commit_id)
748 prev_commit_id = commit.raw_id
753 prev_commit_id = commit.raw_id
749 line_anchor = self.request.GET.get('line_anchor')
754 line_anchor = self.request.GET.get('line_anchor')
750 is_file = False
755 is_file = False
751 try:
756 try:
752 _file = commit.get_node(f_path)
757 _file = commit.get_node(f_path)
753 is_file = _file.is_file()
758 is_file = _file.is_file()
754 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
759 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
755 pass
760 pass
756
761
757 if is_file:
762 if is_file:
758 history = commit.get_path_history(f_path)
763 history = commit.get_path_history(f_path)
759 prev_commit_id = history[1].raw_id \
764 prev_commit_id = history[1].raw_id \
760 if len(history) > 1 else prev_commit_id
765 if len(history) > 1 else prev_commit_id
761 prev_url = h.route_path(
766 prev_url = h.route_path(
762 'repo_files:annotated', repo_name=self.db_repo_name,
767 'repo_files:annotated', repo_name=self.db_repo_name,
763 commit_id=prev_commit_id, f_path=f_path,
768 commit_id=prev_commit_id, f_path=f_path,
764 _anchor='L{}'.format(line_anchor))
769 _anchor='L{}'.format(line_anchor))
765
770
766 raise HTTPFound(prev_url)
771 raise HTTPFound(prev_url)
767
772
768 @LoginRequired()
773 @LoginRequired()
769 @HasRepoPermissionAnyDecorator(
774 @HasRepoPermissionAnyDecorator(
770 'repository.read', 'repository.write', 'repository.admin')
775 'repository.read', 'repository.write', 'repository.admin')
771 @view_config(
776 @view_config(
772 route_name='repo_nodetree_full', request_method='GET',
777 route_name='repo_nodetree_full', request_method='GET',
773 renderer=None, xhr=True)
778 renderer=None, xhr=True)
774 @view_config(
779 @view_config(
775 route_name='repo_nodetree_full:default_path', request_method='GET',
780 route_name='repo_nodetree_full:default_path', request_method='GET',
776 renderer=None, xhr=True)
781 renderer=None, xhr=True)
777 def repo_nodetree_full(self):
782 def repo_nodetree_full(self):
778 """
783 """
779 Returns rendered html of file tree that contains commit date,
784 Returns rendered html of file tree that contains commit date,
780 author, commit_id for the specified combination of
785 author, commit_id for the specified combination of
781 repo, commit_id and file path
786 repo, commit_id and file path
782 """
787 """
783 c = self.load_default_context()
788 c = self.load_default_context()
784
789
785 commit_id, f_path = self._get_commit_and_path()
790 commit_id, f_path = self._get_commit_and_path()
786 commit = self._get_commit_or_redirect(commit_id)
791 commit = self._get_commit_or_redirect(commit_id)
787 try:
792 try:
788 dir_node = commit.get_node(f_path)
793 dir_node = commit.get_node(f_path)
789 except RepositoryError as e:
794 except RepositoryError as e:
790 return Response('error: {}'.format(h.escape(safe_str(e))))
795 return Response('error: {}'.format(h.escape(safe_str(e))))
791
796
792 if dir_node.is_file():
797 if dir_node.is_file():
793 return Response('')
798 return Response('')
794
799
795 c.file = dir_node
800 c.file = dir_node
796 c.commit = commit
801 c.commit = commit
797 at_rev = self.request.GET.get('at')
802 at_rev = self.request.GET.get('at')
798
803
799 html = self._get_tree_at_commit(
804 html = self._get_tree_at_commit(
800 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
805 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
801
806
802 return Response(html)
807 return Response(html)
803
808
804 def _get_attachement_headers(self, f_path):
809 def _get_attachement_headers(self, f_path):
805 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
810 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
806 safe_path = f_name.replace('"', '\\"')
811 safe_path = f_name.replace('"', '\\"')
807 encoded_path = urllib.quote(f_name)
812 encoded_path = urllib.quote(f_name)
808
813
809 return "attachment; " \
814 return "attachment; " \
810 "filename=\"{}\"; " \
815 "filename=\"{}\"; " \
811 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
816 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
812
817
813 @LoginRequired()
818 @LoginRequired()
814 @HasRepoPermissionAnyDecorator(
819 @HasRepoPermissionAnyDecorator(
815 'repository.read', 'repository.write', 'repository.admin')
820 'repository.read', 'repository.write', 'repository.admin')
816 @view_config(
821 @view_config(
817 route_name='repo_file_raw', request_method='GET',
822 route_name='repo_file_raw', request_method='GET',
818 renderer=None)
823 renderer=None)
819 def repo_file_raw(self):
824 def repo_file_raw(self):
820 """
825 """
821 Action for show as raw, some mimetypes are "rendered",
826 Action for show as raw, some mimetypes are "rendered",
822 those include images, icons.
827 those include images, icons.
823 """
828 """
824 c = self.load_default_context()
829 c = self.load_default_context()
825
830
826 commit_id, f_path = self._get_commit_and_path()
831 commit_id, f_path = self._get_commit_and_path()
827 commit = self._get_commit_or_redirect(commit_id)
832 commit = self._get_commit_or_redirect(commit_id)
828 file_node = self._get_filenode_or_redirect(commit, f_path)
833 file_node = self._get_filenode_or_redirect(commit, f_path)
829
834
830 raw_mimetype_mapping = {
835 raw_mimetype_mapping = {
831 # map original mimetype to a mimetype used for "show as raw"
836 # map original mimetype to a mimetype used for "show as raw"
832 # you can also provide a content-disposition to override the
837 # you can also provide a content-disposition to override the
833 # default "attachment" disposition.
838 # default "attachment" disposition.
834 # orig_type: (new_type, new_dispo)
839 # orig_type: (new_type, new_dispo)
835
840
836 # show images inline:
841 # show images inline:
837 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
842 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
838 # for example render an SVG with javascript inside or even render
843 # for example render an SVG with javascript inside or even render
839 # HTML.
844 # HTML.
840 'image/x-icon': ('image/x-icon', 'inline'),
845 'image/x-icon': ('image/x-icon', 'inline'),
841 'image/png': ('image/png', 'inline'),
846 'image/png': ('image/png', 'inline'),
842 'image/gif': ('image/gif', 'inline'),
847 'image/gif': ('image/gif', 'inline'),
843 'image/jpeg': ('image/jpeg', 'inline'),
848 'image/jpeg': ('image/jpeg', 'inline'),
844 'application/pdf': ('application/pdf', 'inline'),
849 'application/pdf': ('application/pdf', 'inline'),
845 }
850 }
846
851
847 mimetype = file_node.mimetype
852 mimetype = file_node.mimetype
848 try:
853 try:
849 mimetype, disposition = raw_mimetype_mapping[mimetype]
854 mimetype, disposition = raw_mimetype_mapping[mimetype]
850 except KeyError:
855 except KeyError:
851 # we don't know anything special about this, handle it safely
856 # we don't know anything special about this, handle it safely
852 if file_node.is_binary:
857 if file_node.is_binary:
853 # do same as download raw for binary files
858 # do same as download raw for binary files
854 mimetype, disposition = 'application/octet-stream', 'attachment'
859 mimetype, disposition = 'application/octet-stream', 'attachment'
855 else:
860 else:
856 # do not just use the original mimetype, but force text/plain,
861 # do not just use the original mimetype, but force text/plain,
857 # otherwise it would serve text/html and that might be unsafe.
862 # otherwise it would serve text/html and that might be unsafe.
858 # Note: underlying vcs library fakes text/plain mimetype if the
863 # Note: underlying vcs library fakes text/plain mimetype if the
859 # mimetype can not be determined and it thinks it is not
864 # mimetype can not be determined and it thinks it is not
860 # binary.This might lead to erroneous text display in some
865 # binary.This might lead to erroneous text display in some
861 # cases, but helps in other cases, like with text files
866 # cases, but helps in other cases, like with text files
862 # without extension.
867 # without extension.
863 mimetype, disposition = 'text/plain', 'inline'
868 mimetype, disposition = 'text/plain', 'inline'
864
869
865 if disposition == 'attachment':
870 if disposition == 'attachment':
866 disposition = self._get_attachement_headers(f_path)
871 disposition = self._get_attachement_headers(f_path)
867
872
868 stream_content = file_node.stream_bytes()
873 stream_content = file_node.stream_bytes()
869
874
870 response = Response(app_iter=stream_content)
875 response = Response(app_iter=stream_content)
871 response.content_disposition = disposition
876 response.content_disposition = disposition
872 response.content_type = mimetype
877 response.content_type = mimetype
873
878
874 charset = self._get_default_encoding(c)
879 charset = self._get_default_encoding(c)
875 if charset:
880 if charset:
876 response.charset = charset
881 response.charset = charset
877
882
878 return response
883 return response
879
884
880 @LoginRequired()
885 @LoginRequired()
881 @HasRepoPermissionAnyDecorator(
886 @HasRepoPermissionAnyDecorator(
882 'repository.read', 'repository.write', 'repository.admin')
887 'repository.read', 'repository.write', 'repository.admin')
883 @view_config(
888 @view_config(
884 route_name='repo_file_download', request_method='GET',
889 route_name='repo_file_download', request_method='GET',
885 renderer=None)
890 renderer=None)
886 @view_config(
891 @view_config(
887 route_name='repo_file_download:legacy', request_method='GET',
892 route_name='repo_file_download:legacy', request_method='GET',
888 renderer=None)
893 renderer=None)
889 def repo_file_download(self):
894 def repo_file_download(self):
890 c = self.load_default_context()
895 c = self.load_default_context()
891
896
892 commit_id, f_path = self._get_commit_and_path()
897 commit_id, f_path = self._get_commit_and_path()
893 commit = self._get_commit_or_redirect(commit_id)
898 commit = self._get_commit_or_redirect(commit_id)
894 file_node = self._get_filenode_or_redirect(commit, f_path)
899 file_node = self._get_filenode_or_redirect(commit, f_path)
895
900
896 if self.request.GET.get('lf'):
901 if self.request.GET.get('lf'):
897 # only if lf get flag is passed, we download this file
902 # only if lf get flag is passed, we download this file
898 # as LFS/Largefile
903 # as LFS/Largefile
899 lf_node = file_node.get_largefile_node()
904 lf_node = file_node.get_largefile_node()
900 if lf_node:
905 if lf_node:
901 # overwrite our pointer with the REAL large-file
906 # overwrite our pointer with the REAL large-file
902 file_node = lf_node
907 file_node = lf_node
903
908
904 disposition = self._get_attachement_headers(f_path)
909 disposition = self._get_attachement_headers(f_path)
905
910
906 stream_content = file_node.stream_bytes()
911 stream_content = file_node.stream_bytes()
907
912
908 response = Response(app_iter=stream_content)
913 response = Response(app_iter=stream_content)
909 response.content_disposition = disposition
914 response.content_disposition = disposition
910 response.content_type = file_node.mimetype
915 response.content_type = file_node.mimetype
911
916
912 charset = self._get_default_encoding(c)
917 charset = self._get_default_encoding(c)
913 if charset:
918 if charset:
914 response.charset = charset
919 response.charset = charset
915
920
916 return response
921 return response
917
922
918 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
923 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
919
924
920 cache_seconds = safe_int(
925 cache_seconds = safe_int(
921 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
926 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
922 cache_on = cache_seconds > 0
927 cache_on = cache_seconds > 0
923 log.debug(
928 log.debug(
924 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
929 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
925 'with caching: %s[TTL: %ss]' % (
930 'with caching: %s[TTL: %ss]' % (
926 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
931 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
927
932
928 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
933 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
929 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
934 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
930
935
931 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
936 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
932 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
937 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
933 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
938 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
934 _repo_id, commit_id, f_path)
939 _repo_id, commit_id, f_path)
935 try:
940 try:
936 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
941 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
937 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
942 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
938 log.exception(safe_str(e))
943 log.exception(safe_str(e))
939 h.flash(safe_str(h.escape(e)), category='error')
944 h.flash(safe_str(h.escape(e)), category='error')
940 raise HTTPFound(h.route_path(
945 raise HTTPFound(h.route_path(
941 'repo_files', repo_name=self.db_repo_name,
946 'repo_files', repo_name=self.db_repo_name,
942 commit_id='tip', f_path='/'))
947 commit_id='tip', f_path='/'))
943
948
944 return _d + _f
949 return _d + _f
945
950
946 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
951 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
947 commit_id, f_path)
952 commit_id, f_path)
948 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
953 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
949
954
950 @LoginRequired()
955 @LoginRequired()
951 @HasRepoPermissionAnyDecorator(
956 @HasRepoPermissionAnyDecorator(
952 'repository.read', 'repository.write', 'repository.admin')
957 'repository.read', 'repository.write', 'repository.admin')
953 @view_config(
958 @view_config(
954 route_name='repo_files_nodelist', request_method='GET',
959 route_name='repo_files_nodelist', request_method='GET',
955 renderer='json_ext', xhr=True)
960 renderer='json_ext', xhr=True)
956 def repo_nodelist(self):
961 def repo_nodelist(self):
957 self.load_default_context()
962 self.load_default_context()
958
963
959 commit_id, f_path = self._get_commit_and_path()
964 commit_id, f_path = self._get_commit_and_path()
960 commit = self._get_commit_or_redirect(commit_id)
965 commit = self._get_commit_or_redirect(commit_id)
961
966
962 metadata = self._get_nodelist_at_commit(
967 metadata = self._get_nodelist_at_commit(
963 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
968 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
964 return {'nodes': metadata}
969 return {'nodes': metadata}
965
970
966 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
971 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
967 items = []
972 items = []
968 for name, commit_id in branches_or_tags.items():
973 for name, commit_id in branches_or_tags.items():
969 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
974 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
970 items.append((sym_ref, name, ref_type))
975 items.append((sym_ref, name, ref_type))
971 return items
976 return items
972
977
973 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
978 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
974 return commit_id
979 return commit_id
975
980
976 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
981 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
977 return commit_id
982 return commit_id
978
983
979 # NOTE(dan): old code we used in "diff" mode compare
984 # NOTE(dan): old code we used in "diff" mode compare
980 new_f_path = vcspath.join(name, f_path)
985 new_f_path = vcspath.join(name, f_path)
981 return u'%s@%s' % (new_f_path, commit_id)
986 return u'%s@%s' % (new_f_path, commit_id)
982
987
983 def _get_node_history(self, commit_obj, f_path, commits=None):
988 def _get_node_history(self, commit_obj, f_path, commits=None):
984 """
989 """
985 get commit history for given node
990 get commit history for given node
986
991
987 :param commit_obj: commit to calculate history
992 :param commit_obj: commit to calculate history
988 :param f_path: path for node to calculate history for
993 :param f_path: path for node to calculate history for
989 :param commits: if passed don't calculate history and take
994 :param commits: if passed don't calculate history and take
990 commits defined in this list
995 commits defined in this list
991 """
996 """
992 _ = self.request.translate
997 _ = self.request.translate
993
998
994 # calculate history based on tip
999 # calculate history based on tip
995 tip = self.rhodecode_vcs_repo.get_commit()
1000 tip = self.rhodecode_vcs_repo.get_commit()
996 if commits is None:
1001 if commits is None:
997 pre_load = ["author", "branch"]
1002 pre_load = ["author", "branch"]
998 try:
1003 try:
999 commits = tip.get_path_history(f_path, pre_load=pre_load)
1004 commits = tip.get_path_history(f_path, pre_load=pre_load)
1000 except (NodeDoesNotExistError, CommitError):
1005 except (NodeDoesNotExistError, CommitError):
1001 # this node is not present at tip!
1006 # this node is not present at tip!
1002 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
1007 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
1003
1008
1004 history = []
1009 history = []
1005 commits_group = ([], _("Changesets"))
1010 commits_group = ([], _("Changesets"))
1006 for commit in commits:
1011 for commit in commits:
1007 branch = ' (%s)' % commit.branch if commit.branch else ''
1012 branch = ' (%s)' % commit.branch if commit.branch else ''
1008 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1013 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1009 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
1014 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
1010 history.append(commits_group)
1015 history.append(commits_group)
1011
1016
1012 symbolic_reference = self._symbolic_reference
1017 symbolic_reference = self._symbolic_reference
1013
1018
1014 if self.rhodecode_vcs_repo.alias == 'svn':
1019 if self.rhodecode_vcs_repo.alias == 'svn':
1015 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1020 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1016 f_path, self.rhodecode_vcs_repo)
1021 f_path, self.rhodecode_vcs_repo)
1017 if adjusted_f_path != f_path:
1022 if adjusted_f_path != f_path:
1018 log.debug(
1023 log.debug(
1019 'Recognized svn tag or branch in file "%s", using svn '
1024 'Recognized svn tag or branch in file "%s", using svn '
1020 'specific symbolic references', f_path)
1025 'specific symbolic references', f_path)
1021 f_path = adjusted_f_path
1026 f_path = adjusted_f_path
1022 symbolic_reference = self._symbolic_reference_svn
1027 symbolic_reference = self._symbolic_reference_svn
1023
1028
1024 branches = self._create_references(
1029 branches = self._create_references(
1025 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1030 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1026 branches_group = (branches, _("Branches"))
1031 branches_group = (branches, _("Branches"))
1027
1032
1028 tags = self._create_references(
1033 tags = self._create_references(
1029 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1034 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1030 tags_group = (tags, _("Tags"))
1035 tags_group = (tags, _("Tags"))
1031
1036
1032 history.append(branches_group)
1037 history.append(branches_group)
1033 history.append(tags_group)
1038 history.append(tags_group)
1034
1039
1035 return history, commits
1040 return history, commits
1036
1041
1037 @LoginRequired()
1042 @LoginRequired()
1038 @HasRepoPermissionAnyDecorator(
1043 @HasRepoPermissionAnyDecorator(
1039 'repository.read', 'repository.write', 'repository.admin')
1044 'repository.read', 'repository.write', 'repository.admin')
1040 @view_config(
1045 @view_config(
1041 route_name='repo_file_history', request_method='GET',
1046 route_name='repo_file_history', request_method='GET',
1042 renderer='json_ext')
1047 renderer='json_ext')
1043 def repo_file_history(self):
1048 def repo_file_history(self):
1044 self.load_default_context()
1049 self.load_default_context()
1045
1050
1046 commit_id, f_path = self._get_commit_and_path()
1051 commit_id, f_path = self._get_commit_and_path()
1047 commit = self._get_commit_or_redirect(commit_id)
1052 commit = self._get_commit_or_redirect(commit_id)
1048 file_node = self._get_filenode_or_redirect(commit, f_path)
1053 file_node = self._get_filenode_or_redirect(commit, f_path)
1049
1054
1050 if file_node.is_file():
1055 if file_node.is_file():
1051 file_history, _hist = self._get_node_history(commit, f_path)
1056 file_history, _hist = self._get_node_history(commit, f_path)
1052
1057
1053 res = []
1058 res = []
1054 for section_items, section in file_history:
1059 for section_items, section in file_history:
1055 items = []
1060 items = []
1056 for obj_id, obj_text, obj_type in section_items:
1061 for obj_id, obj_text, obj_type in section_items:
1057 at_rev = ''
1062 at_rev = ''
1058 if obj_type in ['branch', 'bookmark', 'tag']:
1063 if obj_type in ['branch', 'bookmark', 'tag']:
1059 at_rev = obj_text
1064 at_rev = obj_text
1060 entry = {
1065 entry = {
1061 'id': obj_id,
1066 'id': obj_id,
1062 'text': obj_text,
1067 'text': obj_text,
1063 'type': obj_type,
1068 'type': obj_type,
1064 'at_rev': at_rev
1069 'at_rev': at_rev
1065 }
1070 }
1066
1071
1067 items.append(entry)
1072 items.append(entry)
1068
1073
1069 res.append({
1074 res.append({
1070 'text': section,
1075 'text': section,
1071 'children': items
1076 'children': items
1072 })
1077 })
1073
1078
1074 data = {
1079 data = {
1075 'more': False,
1080 'more': False,
1076 'results': res
1081 'results': res
1077 }
1082 }
1078 return data
1083 return data
1079
1084
1080 log.warning('Cannot fetch history for directory')
1085 log.warning('Cannot fetch history for directory')
1081 raise HTTPBadRequest()
1086 raise HTTPBadRequest()
1082
1087
1083 @LoginRequired()
1088 @LoginRequired()
1084 @HasRepoPermissionAnyDecorator(
1089 @HasRepoPermissionAnyDecorator(
1085 'repository.read', 'repository.write', 'repository.admin')
1090 'repository.read', 'repository.write', 'repository.admin')
1086 @view_config(
1091 @view_config(
1087 route_name='repo_file_authors', request_method='GET',
1092 route_name='repo_file_authors', request_method='GET',
1088 renderer='rhodecode:templates/files/file_authors_box.mako')
1093 renderer='rhodecode:templates/files/file_authors_box.mako')
1089 def repo_file_authors(self):
1094 def repo_file_authors(self):
1090 c = self.load_default_context()
1095 c = self.load_default_context()
1091
1096
1092 commit_id, f_path = self._get_commit_and_path()
1097 commit_id, f_path = self._get_commit_and_path()
1093 commit = self._get_commit_or_redirect(commit_id)
1098 commit = self._get_commit_or_redirect(commit_id)
1094 file_node = self._get_filenode_or_redirect(commit, f_path)
1099 file_node = self._get_filenode_or_redirect(commit, f_path)
1095
1100
1096 if not file_node.is_file():
1101 if not file_node.is_file():
1097 raise HTTPBadRequest()
1102 raise HTTPBadRequest()
1098
1103
1099 c.file_last_commit = file_node.last_commit
1104 c.file_last_commit = file_node.last_commit
1100 if self.request.GET.get('annotate') == '1':
1105 if self.request.GET.get('annotate') == '1':
1101 # use _hist from annotation if annotation mode is on
1106 # use _hist from annotation if annotation mode is on
1102 commit_ids = set(x[1] for x in file_node.annotate)
1107 commit_ids = set(x[1] for x in file_node.annotate)
1103 _hist = (
1108 _hist = (
1104 self.rhodecode_vcs_repo.get_commit(commit_id)
1109 self.rhodecode_vcs_repo.get_commit(commit_id)
1105 for commit_id in commit_ids)
1110 for commit_id in commit_ids)
1106 else:
1111 else:
1107 _f_history, _hist = self._get_node_history(commit, f_path)
1112 _f_history, _hist = self._get_node_history(commit, f_path)
1108 c.file_author = False
1113 c.file_author = False
1109
1114
1110 unique = collections.OrderedDict()
1115 unique = collections.OrderedDict()
1111 for commit in _hist:
1116 for commit in _hist:
1112 author = commit.author
1117 author = commit.author
1113 if author not in unique:
1118 if author not in unique:
1114 unique[commit.author] = [
1119 unique[commit.author] = [
1115 h.email(author),
1120 h.email(author),
1116 h.person(author, 'username_or_name_or_email'),
1121 h.person(author, 'username_or_name_or_email'),
1117 1 # counter
1122 1 # counter
1118 ]
1123 ]
1119
1124
1120 else:
1125 else:
1121 # increase counter
1126 # increase counter
1122 unique[commit.author][2] += 1
1127 unique[commit.author][2] += 1
1123
1128
1124 c.authors = [val for val in unique.values()]
1129 c.authors = [val for val in unique.values()]
1125
1130
1126 return self._get_template_context(c)
1131 return self._get_template_context(c)
1127
1132
1128 @LoginRequired()
1133 @LoginRequired()
1129 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1134 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1130 @view_config(
1135 @view_config(
1131 route_name='repo_files_check_head', request_method='POST',
1136 route_name='repo_files_check_head', request_method='POST',
1132 renderer='json_ext', xhr=True)
1137 renderer='json_ext', xhr=True)
1133 def repo_files_check_head(self):
1138 def repo_files_check_head(self):
1134 self.load_default_context()
1139 self.load_default_context()
1135
1140
1136 commit_id, f_path = self._get_commit_and_path()
1141 commit_id, f_path = self._get_commit_and_path()
1137 _branch_name, _sha_commit_id, is_head = \
1142 _branch_name, _sha_commit_id, is_head = \
1138 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1143 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1144 landing_ref=self.db_repo.landing_ref_name)
1139
1145
1140 new_path = self.request.POST.get('path')
1146 new_path = self.request.POST.get('path')
1141 operation = self.request.POST.get('operation')
1147 operation = self.request.POST.get('operation')
1142 path_exist = ''
1148 path_exist = ''
1143
1149
1144 if new_path and operation in ['create', 'upload']:
1150 if new_path and operation in ['create', 'upload']:
1145 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1151 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1146 try:
1152 try:
1147 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1153 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1148 # NOTE(dan): construct whole path without leading /
1154 # NOTE(dan): construct whole path without leading /
1149 file_node = commit_obj.get_node(new_f_path)
1155 file_node = commit_obj.get_node(new_f_path)
1150 if file_node is not None:
1156 if file_node is not None:
1151 path_exist = new_f_path
1157 path_exist = new_f_path
1152 except EmptyRepositoryError:
1158 except EmptyRepositoryError:
1153 pass
1159 pass
1154 except Exception:
1160 except Exception:
1155 pass
1161 pass
1156
1162
1157 return {
1163 return {
1158 'branch': _branch_name,
1164 'branch': _branch_name,
1159 'sha': _sha_commit_id,
1165 'sha': _sha_commit_id,
1160 'is_head': is_head,
1166 'is_head': is_head,
1161 'path_exists': path_exist
1167 'path_exists': path_exist
1162 }
1168 }
1163
1169
1164 @LoginRequired()
1170 @LoginRequired()
1165 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1171 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1166 @view_config(
1172 @view_config(
1167 route_name='repo_files_remove_file', request_method='GET',
1173 route_name='repo_files_remove_file', request_method='GET',
1168 renderer='rhodecode:templates/files/files_delete.mako')
1174 renderer='rhodecode:templates/files/files_delete.mako')
1169 def repo_files_remove_file(self):
1175 def repo_files_remove_file(self):
1170 _ = self.request.translate
1176 _ = self.request.translate
1171 c = self.load_default_context()
1177 c = self.load_default_context()
1172 commit_id, f_path = self._get_commit_and_path()
1178 commit_id, f_path = self._get_commit_and_path()
1173
1179
1174 self._ensure_not_locked()
1180 self._ensure_not_locked()
1175 _branch_name, _sha_commit_id, is_head = \
1181 _branch_name, _sha_commit_id, is_head = \
1176 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1182 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1183 landing_ref=self.db_repo.landing_ref_name)
1177
1184
1178 self.forbid_non_head(is_head, f_path)
1185 self.forbid_non_head(is_head, f_path)
1179 self.check_branch_permission(_branch_name)
1186 self.check_branch_permission(_branch_name)
1180
1187
1181 c.commit = self._get_commit_or_redirect(commit_id)
1188 c.commit = self._get_commit_or_redirect(commit_id)
1182 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1189 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1183
1190
1184 c.default_message = _(
1191 c.default_message = _(
1185 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1192 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1186 c.f_path = f_path
1193 c.f_path = f_path
1187
1194
1188 return self._get_template_context(c)
1195 return self._get_template_context(c)
1189
1196
1190 @LoginRequired()
1197 @LoginRequired()
1191 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1198 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1192 @CSRFRequired()
1199 @CSRFRequired()
1193 @view_config(
1200 @view_config(
1194 route_name='repo_files_delete_file', request_method='POST',
1201 route_name='repo_files_delete_file', request_method='POST',
1195 renderer=None)
1202 renderer=None)
1196 def repo_files_delete_file(self):
1203 def repo_files_delete_file(self):
1197 _ = self.request.translate
1204 _ = self.request.translate
1198
1205
1199 c = self.load_default_context()
1206 c = self.load_default_context()
1200 commit_id, f_path = self._get_commit_and_path()
1207 commit_id, f_path = self._get_commit_and_path()
1201
1208
1202 self._ensure_not_locked()
1209 self._ensure_not_locked()
1203 _branch_name, _sha_commit_id, is_head = \
1210 _branch_name, _sha_commit_id, is_head = \
1204 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1211 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1212 landing_ref=self.db_repo.landing_ref_name)
1205
1213
1206 self.forbid_non_head(is_head, f_path)
1214 self.forbid_non_head(is_head, f_path)
1207 self.check_branch_permission(_branch_name)
1215 self.check_branch_permission(_branch_name)
1208
1216
1209 c.commit = self._get_commit_or_redirect(commit_id)
1217 c.commit = self._get_commit_or_redirect(commit_id)
1210 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1218 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1211
1219
1212 c.default_message = _(
1220 c.default_message = _(
1213 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1221 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1214 c.f_path = f_path
1222 c.f_path = f_path
1215 node_path = f_path
1223 node_path = f_path
1216 author = self._rhodecode_db_user.full_contact
1224 author = self._rhodecode_db_user.full_contact
1217 message = self.request.POST.get('message') or c.default_message
1225 message = self.request.POST.get('message') or c.default_message
1218 try:
1226 try:
1219 nodes = {
1227 nodes = {
1220 node_path: {
1228 node_path: {
1221 'content': ''
1229 'content': ''
1222 }
1230 }
1223 }
1231 }
1224 ScmModel().delete_nodes(
1232 ScmModel().delete_nodes(
1225 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1233 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1226 message=message,
1234 message=message,
1227 nodes=nodes,
1235 nodes=nodes,
1228 parent_commit=c.commit,
1236 parent_commit=c.commit,
1229 author=author,
1237 author=author,
1230 )
1238 )
1231
1239
1232 h.flash(
1240 h.flash(
1233 _('Successfully deleted file `{}`').format(
1241 _('Successfully deleted file `{}`').format(
1234 h.escape(f_path)), category='success')
1242 h.escape(f_path)), category='success')
1235 except Exception:
1243 except Exception:
1236 log.exception('Error during commit operation')
1244 log.exception('Error during commit operation')
1237 h.flash(_('Error occurred during commit'), category='error')
1245 h.flash(_('Error occurred during commit'), category='error')
1238 raise HTTPFound(
1246 raise HTTPFound(
1239 h.route_path('repo_commit', repo_name=self.db_repo_name,
1247 h.route_path('repo_commit', repo_name=self.db_repo_name,
1240 commit_id='tip'))
1248 commit_id='tip'))
1241
1249
1242 @LoginRequired()
1250 @LoginRequired()
1243 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1251 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1244 @view_config(
1252 @view_config(
1245 route_name='repo_files_edit_file', request_method='GET',
1253 route_name='repo_files_edit_file', request_method='GET',
1246 renderer='rhodecode:templates/files/files_edit.mako')
1254 renderer='rhodecode:templates/files/files_edit.mako')
1247 def repo_files_edit_file(self):
1255 def repo_files_edit_file(self):
1248 _ = self.request.translate
1256 _ = self.request.translate
1249 c = self.load_default_context()
1257 c = self.load_default_context()
1250 commit_id, f_path = self._get_commit_and_path()
1258 commit_id, f_path = self._get_commit_and_path()
1251
1259
1252 self._ensure_not_locked()
1260 self._ensure_not_locked()
1253 _branch_name, _sha_commit_id, is_head = \
1261 _branch_name, _sha_commit_id, is_head = \
1254 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1262 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1263 landing_ref=self.db_repo.landing_ref_name)
1255
1264
1256 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1265 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1257 self.check_branch_permission(_branch_name, commit_id=commit_id)
1266 self.check_branch_permission(_branch_name, commit_id=commit_id)
1258
1267
1259 c.commit = self._get_commit_or_redirect(commit_id)
1268 c.commit = self._get_commit_or_redirect(commit_id)
1260 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1269 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1261
1270
1262 if c.file.is_binary:
1271 if c.file.is_binary:
1263 files_url = h.route_path(
1272 files_url = h.route_path(
1264 'repo_files',
1273 'repo_files',
1265 repo_name=self.db_repo_name,
1274 repo_name=self.db_repo_name,
1266 commit_id=c.commit.raw_id, f_path=f_path)
1275 commit_id=c.commit.raw_id, f_path=f_path)
1267 raise HTTPFound(files_url)
1276 raise HTTPFound(files_url)
1268
1277
1269 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1278 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1270 c.f_path = f_path
1279 c.f_path = f_path
1271
1280
1272 return self._get_template_context(c)
1281 return self._get_template_context(c)
1273
1282
1274 @LoginRequired()
1283 @LoginRequired()
1275 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1284 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1276 @CSRFRequired()
1285 @CSRFRequired()
1277 @view_config(
1286 @view_config(
1278 route_name='repo_files_update_file', request_method='POST',
1287 route_name='repo_files_update_file', request_method='POST',
1279 renderer=None)
1288 renderer=None)
1280 def repo_files_update_file(self):
1289 def repo_files_update_file(self):
1281 _ = self.request.translate
1290 _ = self.request.translate
1282 c = self.load_default_context()
1291 c = self.load_default_context()
1283 commit_id, f_path = self._get_commit_and_path()
1292 commit_id, f_path = self._get_commit_and_path()
1284
1293
1285 self._ensure_not_locked()
1294 self._ensure_not_locked()
1286
1295
1287 c.commit = self._get_commit_or_redirect(commit_id)
1296 c.commit = self._get_commit_or_redirect(commit_id)
1288 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1297 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1289
1298
1290 if c.file.is_binary:
1299 if c.file.is_binary:
1291 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1300 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1292 commit_id=c.commit.raw_id, f_path=f_path))
1301 commit_id=c.commit.raw_id, f_path=f_path))
1293
1302
1294 _branch_name, _sha_commit_id, is_head = \
1303 _branch_name, _sha_commit_id, is_head = \
1295 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1304 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1305 landing_ref=self.db_repo.landing_ref_name)
1296
1306
1297 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1307 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1298 self.check_branch_permission(_branch_name, commit_id=commit_id)
1308 self.check_branch_permission(_branch_name, commit_id=commit_id)
1299
1309
1300 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1310 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1301 c.f_path = f_path
1311 c.f_path = f_path
1302
1312
1303 old_content = c.file.content
1313 old_content = c.file.content
1304 sl = old_content.splitlines(1)
1314 sl = old_content.splitlines(1)
1305 first_line = sl[0] if sl else ''
1315 first_line = sl[0] if sl else ''
1306
1316
1307 r_post = self.request.POST
1317 r_post = self.request.POST
1308 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1318 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1309 line_ending_mode = detect_mode(first_line, 0)
1319 line_ending_mode = detect_mode(first_line, 0)
1310 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1320 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1311
1321
1312 message = r_post.get('message') or c.default_message
1322 message = r_post.get('message') or c.default_message
1313 org_node_path = c.file.unicode_path
1323 org_node_path = c.file.unicode_path
1314 filename = r_post['filename']
1324 filename = r_post['filename']
1315
1325
1316 root_path = c.file.dir_path
1326 root_path = c.file.dir_path
1317 pure_path = self.create_pure_path(root_path, filename)
1327 pure_path = self.create_pure_path(root_path, filename)
1318 node_path = safe_unicode(bytes(pure_path))
1328 node_path = safe_unicode(bytes(pure_path))
1319
1329
1320 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1330 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1321 commit_id=commit_id)
1331 commit_id=commit_id)
1322 if content == old_content and node_path == org_node_path:
1332 if content == old_content and node_path == org_node_path:
1323 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1333 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1324 category='warning')
1334 category='warning')
1325 raise HTTPFound(default_redirect_url)
1335 raise HTTPFound(default_redirect_url)
1326
1336
1327 try:
1337 try:
1328 mapping = {
1338 mapping = {
1329 org_node_path: {
1339 org_node_path: {
1330 'org_filename': org_node_path,
1340 'org_filename': org_node_path,
1331 'filename': node_path,
1341 'filename': node_path,
1332 'content': content,
1342 'content': content,
1333 'lexer': '',
1343 'lexer': '',
1334 'op': 'mod',
1344 'op': 'mod',
1335 'mode': c.file.mode
1345 'mode': c.file.mode
1336 }
1346 }
1337 }
1347 }
1338
1348
1339 commit = ScmModel().update_nodes(
1349 commit = ScmModel().update_nodes(
1340 user=self._rhodecode_db_user.user_id,
1350 user=self._rhodecode_db_user.user_id,
1341 repo=self.db_repo,
1351 repo=self.db_repo,
1342 message=message,
1352 message=message,
1343 nodes=mapping,
1353 nodes=mapping,
1344 parent_commit=c.commit,
1354 parent_commit=c.commit,
1345 )
1355 )
1346
1356
1347 h.flash(_('Successfully committed changes to file `{}`').format(
1357 h.flash(_('Successfully committed changes to file `{}`').format(
1348 h.escape(f_path)), category='success')
1358 h.escape(f_path)), category='success')
1349 default_redirect_url = h.route_path(
1359 default_redirect_url = h.route_path(
1350 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1360 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1351
1361
1352 except Exception:
1362 except Exception:
1353 log.exception('Error occurred during commit')
1363 log.exception('Error occurred during commit')
1354 h.flash(_('Error occurred during commit'), category='error')
1364 h.flash(_('Error occurred during commit'), category='error')
1355
1365
1356 raise HTTPFound(default_redirect_url)
1366 raise HTTPFound(default_redirect_url)
1357
1367
1358 @LoginRequired()
1368 @LoginRequired()
1359 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1369 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1360 @view_config(
1370 @view_config(
1361 route_name='repo_files_add_file', request_method='GET',
1371 route_name='repo_files_add_file', request_method='GET',
1362 renderer='rhodecode:templates/files/files_add.mako')
1372 renderer='rhodecode:templates/files/files_add.mako')
1363 @view_config(
1373 @view_config(
1364 route_name='repo_files_upload_file', request_method='GET',
1374 route_name='repo_files_upload_file', request_method='GET',
1365 renderer='rhodecode:templates/files/files_upload.mako')
1375 renderer='rhodecode:templates/files/files_upload.mako')
1366 def repo_files_add_file(self):
1376 def repo_files_add_file(self):
1367 _ = self.request.translate
1377 _ = self.request.translate
1368 c = self.load_default_context()
1378 c = self.load_default_context()
1369 commit_id, f_path = self._get_commit_and_path()
1379 commit_id, f_path = self._get_commit_and_path()
1370
1380
1371 self._ensure_not_locked()
1381 self._ensure_not_locked()
1372
1382
1373 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1383 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1374 if c.commit is None:
1384 if c.commit is None:
1375 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1385 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1376
1386
1377 if self.rhodecode_vcs_repo.is_empty():
1387 if self.rhodecode_vcs_repo.is_empty():
1378 # for empty repository we cannot check for current branch, we rely on
1388 # for empty repository we cannot check for current branch, we rely on
1379 # c.commit.branch instead
1389 # c.commit.branch instead
1380 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1390 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1381 else:
1391 else:
1382 _branch_name, _sha_commit_id, is_head = \
1392 _branch_name, _sha_commit_id, is_head = \
1383 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1393 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1394 landing_ref=self.db_repo.landing_ref_name)
1384
1395
1385 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1396 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1386 self.check_branch_permission(_branch_name, commit_id=commit_id)
1397 self.check_branch_permission(_branch_name, commit_id=commit_id)
1387
1398
1388 c.default_message = (_('Added file via RhodeCode Enterprise'))
1399 c.default_message = (_('Added file via RhodeCode Enterprise'))
1389 c.f_path = f_path.lstrip('/') # ensure not relative path
1400 c.f_path = f_path.lstrip('/') # ensure not relative path
1390
1401
1391 return self._get_template_context(c)
1402 return self._get_template_context(c)
1392
1403
1393 @LoginRequired()
1404 @LoginRequired()
1394 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1405 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1395 @CSRFRequired()
1406 @CSRFRequired()
1396 @view_config(
1407 @view_config(
1397 route_name='repo_files_create_file', request_method='POST',
1408 route_name='repo_files_create_file', request_method='POST',
1398 renderer=None)
1409 renderer=None)
1399 def repo_files_create_file(self):
1410 def repo_files_create_file(self):
1400 _ = self.request.translate
1411 _ = self.request.translate
1401 c = self.load_default_context()
1412 c = self.load_default_context()
1402 commit_id, f_path = self._get_commit_and_path()
1413 commit_id, f_path = self._get_commit_and_path()
1403
1414
1404 self._ensure_not_locked()
1415 self._ensure_not_locked()
1405
1416
1406 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1417 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1407 if c.commit is None:
1418 if c.commit is None:
1408 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1419 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1409
1420
1410 # calculate redirect URL
1421 # calculate redirect URL
1411 if self.rhodecode_vcs_repo.is_empty():
1422 if self.rhodecode_vcs_repo.is_empty():
1412 default_redirect_url = h.route_path(
1423 default_redirect_url = h.route_path(
1413 'repo_summary', repo_name=self.db_repo_name)
1424 'repo_summary', repo_name=self.db_repo_name)
1414 else:
1425 else:
1415 default_redirect_url = h.route_path(
1426 default_redirect_url = h.route_path(
1416 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1427 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1417
1428
1418 if self.rhodecode_vcs_repo.is_empty():
1429 if self.rhodecode_vcs_repo.is_empty():
1419 # for empty repository we cannot check for current branch, we rely on
1430 # for empty repository we cannot check for current branch, we rely on
1420 # c.commit.branch instead
1431 # c.commit.branch instead
1421 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1432 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1422 else:
1433 else:
1423 _branch_name, _sha_commit_id, is_head = \
1434 _branch_name, _sha_commit_id, is_head = \
1424 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1435 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1436 landing_ref=self.db_repo.landing_ref_name)
1425
1437
1426 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1438 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1427 self.check_branch_permission(_branch_name, commit_id=commit_id)
1439 self.check_branch_permission(_branch_name, commit_id=commit_id)
1428
1440
1429 c.default_message = (_('Added file via RhodeCode Enterprise'))
1441 c.default_message = (_('Added file via RhodeCode Enterprise'))
1430 c.f_path = f_path
1442 c.f_path = f_path
1431
1443
1432 r_post = self.request.POST
1444 r_post = self.request.POST
1433 message = r_post.get('message') or c.default_message
1445 message = r_post.get('message') or c.default_message
1434 filename = r_post.get('filename')
1446 filename = r_post.get('filename')
1435 unix_mode = 0
1447 unix_mode = 0
1436 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1448 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1437
1449
1438 if not filename:
1450 if not filename:
1439 # If there's no commit, redirect to repo summary
1451 # If there's no commit, redirect to repo summary
1440 if type(c.commit) is EmptyCommit:
1452 if type(c.commit) is EmptyCommit:
1441 redirect_url = h.route_path(
1453 redirect_url = h.route_path(
1442 'repo_summary', repo_name=self.db_repo_name)
1454 'repo_summary', repo_name=self.db_repo_name)
1443 else:
1455 else:
1444 redirect_url = default_redirect_url
1456 redirect_url = default_redirect_url
1445 h.flash(_('No filename specified'), category='warning')
1457 h.flash(_('No filename specified'), category='warning')
1446 raise HTTPFound(redirect_url)
1458 raise HTTPFound(redirect_url)
1447
1459
1448 root_path = f_path
1460 root_path = f_path
1449 pure_path = self.create_pure_path(root_path, filename)
1461 pure_path = self.create_pure_path(root_path, filename)
1450 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1462 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1451
1463
1452 author = self._rhodecode_db_user.full_contact
1464 author = self._rhodecode_db_user.full_contact
1453 nodes = {
1465 nodes = {
1454 node_path: {
1466 node_path: {
1455 'content': content
1467 'content': content
1456 }
1468 }
1457 }
1469 }
1458
1470
1459 try:
1471 try:
1460
1472
1461 commit = ScmModel().create_nodes(
1473 commit = ScmModel().create_nodes(
1462 user=self._rhodecode_db_user.user_id,
1474 user=self._rhodecode_db_user.user_id,
1463 repo=self.db_repo,
1475 repo=self.db_repo,
1464 message=message,
1476 message=message,
1465 nodes=nodes,
1477 nodes=nodes,
1466 parent_commit=c.commit,
1478 parent_commit=c.commit,
1467 author=author,
1479 author=author,
1468 )
1480 )
1469
1481
1470 h.flash(_('Successfully committed new file `{}`').format(
1482 h.flash(_('Successfully committed new file `{}`').format(
1471 h.escape(node_path)), category='success')
1483 h.escape(node_path)), category='success')
1472
1484
1473 default_redirect_url = h.route_path(
1485 default_redirect_url = h.route_path(
1474 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1486 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1475
1487
1476 except NonRelativePathError:
1488 except NonRelativePathError:
1477 log.exception('Non Relative path found')
1489 log.exception('Non Relative path found')
1478 h.flash(_('The location specified must be a relative path and must not '
1490 h.flash(_('The location specified must be a relative path and must not '
1479 'contain .. in the path'), category='warning')
1491 'contain .. in the path'), category='warning')
1480 raise HTTPFound(default_redirect_url)
1492 raise HTTPFound(default_redirect_url)
1481 except (NodeError, NodeAlreadyExistsError) as e:
1493 except (NodeError, NodeAlreadyExistsError) as e:
1482 h.flash(_(h.escape(e)), category='error')
1494 h.flash(_(h.escape(e)), category='error')
1483 except Exception:
1495 except Exception:
1484 log.exception('Error occurred during commit')
1496 log.exception('Error occurred during commit')
1485 h.flash(_('Error occurred during commit'), category='error')
1497 h.flash(_('Error occurred during commit'), category='error')
1486
1498
1487 raise HTTPFound(default_redirect_url)
1499 raise HTTPFound(default_redirect_url)
1488
1500
1489 @LoginRequired()
1501 @LoginRequired()
1490 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1502 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1491 @CSRFRequired()
1503 @CSRFRequired()
1492 @view_config(
1504 @view_config(
1493 route_name='repo_files_upload_file', request_method='POST',
1505 route_name='repo_files_upload_file', request_method='POST',
1494 renderer='json_ext')
1506 renderer='json_ext')
1495 def repo_files_upload_file(self):
1507 def repo_files_upload_file(self):
1496 _ = self.request.translate
1508 _ = self.request.translate
1497 c = self.load_default_context()
1509 c = self.load_default_context()
1498 commit_id, f_path = self._get_commit_and_path()
1510 commit_id, f_path = self._get_commit_and_path()
1499
1511
1500 self._ensure_not_locked()
1512 self._ensure_not_locked()
1501
1513
1502 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1514 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1503 if c.commit is None:
1515 if c.commit is None:
1504 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1516 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1505
1517
1506 # calculate redirect URL
1518 # calculate redirect URL
1507 if self.rhodecode_vcs_repo.is_empty():
1519 if self.rhodecode_vcs_repo.is_empty():
1508 default_redirect_url = h.route_path(
1520 default_redirect_url = h.route_path(
1509 'repo_summary', repo_name=self.db_repo_name)
1521 'repo_summary', repo_name=self.db_repo_name)
1510 else:
1522 else:
1511 default_redirect_url = h.route_path(
1523 default_redirect_url = h.route_path(
1512 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1524 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1513
1525
1514 if self.rhodecode_vcs_repo.is_empty():
1526 if self.rhodecode_vcs_repo.is_empty():
1515 # for empty repository we cannot check for current branch, we rely on
1527 # for empty repository we cannot check for current branch, we rely on
1516 # c.commit.branch instead
1528 # c.commit.branch instead
1517 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1529 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1518 else:
1530 else:
1519 _branch_name, _sha_commit_id, is_head = \
1531 _branch_name, _sha_commit_id, is_head = \
1520 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1532 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1533 landing_ref=self.db_repo.landing_ref_name)
1521
1534
1522 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1535 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1523 if error:
1536 if error:
1524 return {
1537 return {
1525 'error': error,
1538 'error': error,
1526 'redirect_url': default_redirect_url
1539 'redirect_url': default_redirect_url
1527 }
1540 }
1528 error = self.check_branch_permission(_branch_name, json_mode=True)
1541 error = self.check_branch_permission(_branch_name, json_mode=True)
1529 if error:
1542 if error:
1530 return {
1543 return {
1531 'error': error,
1544 'error': error,
1532 'redirect_url': default_redirect_url
1545 'redirect_url': default_redirect_url
1533 }
1546 }
1534
1547
1535 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1548 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1536 c.f_path = f_path
1549 c.f_path = f_path
1537
1550
1538 r_post = self.request.POST
1551 r_post = self.request.POST
1539
1552
1540 message = c.default_message
1553 message = c.default_message
1541 user_message = r_post.getall('message')
1554 user_message = r_post.getall('message')
1542 if isinstance(user_message, list) and user_message:
1555 if isinstance(user_message, list) and user_message:
1543 # we take the first from duplicated results if it's not empty
1556 # we take the first from duplicated results if it's not empty
1544 message = user_message[0] if user_message[0] else message
1557 message = user_message[0] if user_message[0] else message
1545
1558
1546 nodes = {}
1559 nodes = {}
1547
1560
1548 for file_obj in r_post.getall('files_upload') or []:
1561 for file_obj in r_post.getall('files_upload') or []:
1549 content = file_obj.file
1562 content = file_obj.file
1550 filename = file_obj.filename
1563 filename = file_obj.filename
1551
1564
1552 root_path = f_path
1565 root_path = f_path
1553 pure_path = self.create_pure_path(root_path, filename)
1566 pure_path = self.create_pure_path(root_path, filename)
1554 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1567 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1555
1568
1556 nodes[node_path] = {
1569 nodes[node_path] = {
1557 'content': content
1570 'content': content
1558 }
1571 }
1559
1572
1560 if not nodes:
1573 if not nodes:
1561 error = 'missing files'
1574 error = 'missing files'
1562 return {
1575 return {
1563 'error': error,
1576 'error': error,
1564 'redirect_url': default_redirect_url
1577 'redirect_url': default_redirect_url
1565 }
1578 }
1566
1579
1567 author = self._rhodecode_db_user.full_contact
1580 author = self._rhodecode_db_user.full_contact
1568
1581
1569 try:
1582 try:
1570 commit = ScmModel().create_nodes(
1583 commit = ScmModel().create_nodes(
1571 user=self._rhodecode_db_user.user_id,
1584 user=self._rhodecode_db_user.user_id,
1572 repo=self.db_repo,
1585 repo=self.db_repo,
1573 message=message,
1586 message=message,
1574 nodes=nodes,
1587 nodes=nodes,
1575 parent_commit=c.commit,
1588 parent_commit=c.commit,
1576 author=author,
1589 author=author,
1577 )
1590 )
1578 if len(nodes) == 1:
1591 if len(nodes) == 1:
1579 flash_message = _('Successfully committed {} new files').format(len(nodes))
1592 flash_message = _('Successfully committed {} new files').format(len(nodes))
1580 else:
1593 else:
1581 flash_message = _('Successfully committed 1 new file')
1594 flash_message = _('Successfully committed 1 new file')
1582
1595
1583 h.flash(flash_message, category='success')
1596 h.flash(flash_message, category='success')
1584
1597
1585 default_redirect_url = h.route_path(
1598 default_redirect_url = h.route_path(
1586 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1599 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1587
1600
1588 except NonRelativePathError:
1601 except NonRelativePathError:
1589 log.exception('Non Relative path found')
1602 log.exception('Non Relative path found')
1590 error = _('The location specified must be a relative path and must not '
1603 error = _('The location specified must be a relative path and must not '
1591 'contain .. in the path')
1604 'contain .. in the path')
1592 h.flash(error, category='warning')
1605 h.flash(error, category='warning')
1593
1606
1594 return {
1607 return {
1595 'error': error,
1608 'error': error,
1596 'redirect_url': default_redirect_url
1609 'redirect_url': default_redirect_url
1597 }
1610 }
1598 except (NodeError, NodeAlreadyExistsError) as e:
1611 except (NodeError, NodeAlreadyExistsError) as e:
1599 error = h.escape(e)
1612 error = h.escape(e)
1600 h.flash(error, category='error')
1613 h.flash(error, category='error')
1601
1614
1602 return {
1615 return {
1603 'error': error,
1616 'error': error,
1604 'redirect_url': default_redirect_url
1617 'redirect_url': default_redirect_url
1605 }
1618 }
1606 except Exception:
1619 except Exception:
1607 log.exception('Error occurred during commit')
1620 log.exception('Error occurred during commit')
1608 error = _('Error occurred during commit')
1621 error = _('Error occurred during commit')
1609 h.flash(error, category='error')
1622 h.flash(error, category='error')
1610 return {
1623 return {
1611 'error': error,
1624 'error': error,
1612 'redirect_url': default_redirect_url
1625 'redirect_url': default_redirect_url
1613 }
1626 }
1614
1627
1615 return {
1628 return {
1616 'error': None,
1629 'error': None,
1617 'redirect_url': default_redirect_url
1630 'redirect_url': default_redirect_url
1618 }
1631 }
@@ -1,140 +1,140 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 from pyramid.httpexceptions import HTTPFound
23 from pyramid.httpexceptions import HTTPFound
24 from pyramid.view import view_config
24 from pyramid.view import view_config
25
25
26 from rhodecode.apps._base import RepoAppView
26 from rhodecode.apps._base import RepoAppView
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib import audit_logger
28 from rhodecode.lib import audit_logger
29 from rhodecode.lib.auth import (
29 from rhodecode.lib.auth import (
30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
31 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.lib.utils2 import str2bool
32 from rhodecode.model.db import User
32 from rhodecode.model.db import User
33 from rhodecode.model.forms import RepoPermsForm
33 from rhodecode.model.forms import RepoPermsForm
34 from rhodecode.model.meta import Session
34 from rhodecode.model.meta import Session
35 from rhodecode.model.permission import PermissionModel
35 from rhodecode.model.permission import PermissionModel
36 from rhodecode.model.repo import RepoModel
36 from rhodecode.model.repo import RepoModel
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class RepoSettingsPermissionsView(RepoAppView):
41 class RepoSettingsPermissionsView(RepoAppView):
42
42
43 def load_default_context(self):
43 def load_default_context(self):
44 c = self._get_local_tmpl_context()
44 c = self._get_local_tmpl_context()
45 return c
45 return c
46
46
47 @LoginRequired()
47 @LoginRequired()
48 @HasRepoPermissionAnyDecorator('repository.admin')
48 @HasRepoPermissionAnyDecorator('repository.admin')
49 @view_config(
49 @view_config(
50 route_name='edit_repo_perms', request_method='GET',
50 route_name='edit_repo_perms', request_method='GET',
51 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
51 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
52 def edit_permissions(self):
52 def edit_permissions(self):
53 _ = self.request.translate
53 _ = self.request.translate
54 c = self.load_default_context()
54 c = self.load_default_context()
55 c.active = 'permissions'
55 c.active = 'permissions'
56 if self.request.GET.get('branch_permissions'):
56 if self.request.GET.get('branch_permissions'):
57 h.flash(_('Explicitly add user or user group with write+ '
57 h.flash(_('Explicitly add user or user group with write or higher '
58 'permission to modify their branch permissions.'),
58 'permission to modify their branch permissions.'),
59 category='notice')
59 category='notice')
60 return self._get_template_context(c)
60 return self._get_template_context(c)
61
61
62 @LoginRequired()
62 @LoginRequired()
63 @HasRepoPermissionAnyDecorator('repository.admin')
63 @HasRepoPermissionAnyDecorator('repository.admin')
64 @CSRFRequired()
64 @CSRFRequired()
65 @view_config(
65 @view_config(
66 route_name='edit_repo_perms', request_method='POST',
66 route_name='edit_repo_perms', request_method='POST',
67 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
67 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
68 def edit_permissions_update(self):
68 def edit_permissions_update(self):
69 _ = self.request.translate
69 _ = self.request.translate
70 c = self.load_default_context()
70 c = self.load_default_context()
71 c.active = 'permissions'
71 c.active = 'permissions'
72 data = self.request.POST
72 data = self.request.POST
73 # store private flag outside of HTML to verify if we can modify
73 # store private flag outside of HTML to verify if we can modify
74 # default user permissions, prevents submission of FAKE post data
74 # default user permissions, prevents submission of FAKE post data
75 # into the form for private repos
75 # into the form for private repos
76 data['repo_private'] = self.db_repo.private
76 data['repo_private'] = self.db_repo.private
77 form = RepoPermsForm(self.request.translate)().to_python(data)
77 form = RepoPermsForm(self.request.translate)().to_python(data)
78 changes = RepoModel().update_permissions(
78 changes = RepoModel().update_permissions(
79 self.db_repo_name, form['perm_additions'], form['perm_updates'],
79 self.db_repo_name, form['perm_additions'], form['perm_updates'],
80 form['perm_deletions'])
80 form['perm_deletions'])
81
81
82 action_data = {
82 action_data = {
83 'added': changes['added'],
83 'added': changes['added'],
84 'updated': changes['updated'],
84 'updated': changes['updated'],
85 'deleted': changes['deleted'],
85 'deleted': changes['deleted'],
86 }
86 }
87 audit_logger.store_web(
87 audit_logger.store_web(
88 'repo.edit.permissions', action_data=action_data,
88 'repo.edit.permissions', action_data=action_data,
89 user=self._rhodecode_user, repo=self.db_repo)
89 user=self._rhodecode_user, repo=self.db_repo)
90
90
91 Session().commit()
91 Session().commit()
92 h.flash(_('Repository access permissions updated'), category='success')
92 h.flash(_('Repository access permissions updated'), category='success')
93
93
94 affected_user_ids = None
94 affected_user_ids = None
95 if changes.get('default_user_changed', False):
95 if changes.get('default_user_changed', False):
96 # if we change the default user, we need to flush everyone permissions
96 # if we change the default user, we need to flush everyone permissions
97 affected_user_ids = User.get_all_user_ids()
97 affected_user_ids = User.get_all_user_ids()
98 PermissionModel().flush_user_permission_caches(
98 PermissionModel().flush_user_permission_caches(
99 changes, affected_user_ids=affected_user_ids)
99 changes, affected_user_ids=affected_user_ids)
100
100
101 raise HTTPFound(
101 raise HTTPFound(
102 h.route_path('edit_repo_perms', repo_name=self.db_repo_name))
102 h.route_path('edit_repo_perms', repo_name=self.db_repo_name))
103
103
104 @LoginRequired()
104 @LoginRequired()
105 @HasRepoPermissionAnyDecorator('repository.admin')
105 @HasRepoPermissionAnyDecorator('repository.admin')
106 @CSRFRequired()
106 @CSRFRequired()
107 @view_config(
107 @view_config(
108 route_name='edit_repo_perms_set_private', request_method='POST',
108 route_name='edit_repo_perms_set_private', request_method='POST',
109 renderer='json_ext')
109 renderer='json_ext')
110 def edit_permissions_set_private_repo(self):
110 def edit_permissions_set_private_repo(self):
111 _ = self.request.translate
111 _ = self.request.translate
112 self.load_default_context()
112 self.load_default_context()
113
113
114 private_flag = str2bool(self.request.POST.get('private'))
114 private_flag = str2bool(self.request.POST.get('private'))
115
115
116 try:
116 try:
117 repo = RepoModel().get(self.db_repo.repo_id)
117 repo = RepoModel().get(self.db_repo.repo_id)
118 repo.private = private_flag
118 repo.private = private_flag
119 Session().add(repo)
119 Session().add(repo)
120 RepoModel().grant_user_permission(
120 RepoModel().grant_user_permission(
121 repo=self.db_repo, user=User.DEFAULT_USER, perm='repository.none'
121 repo=self.db_repo, user=User.DEFAULT_USER, perm='repository.none'
122 )
122 )
123
123
124 Session().commit()
124 Session().commit()
125
125
126 h.flash(_('Repository `{}` private mode set successfully').format(self.db_repo_name),
126 h.flash(_('Repository `{}` private mode set successfully').format(self.db_repo_name),
127 category='success')
127 category='success')
128 # NOTE(dan): we change repo private mode we need to notify all USERS
128 # NOTE(dan): we change repo private mode we need to notify all USERS
129 affected_user_ids = User.get_all_user_ids()
129 affected_user_ids = User.get_all_user_ids()
130 PermissionModel().trigger_permission_flush(affected_user_ids)
130 PermissionModel().trigger_permission_flush(affected_user_ids)
131
131
132 except Exception:
132 except Exception:
133 log.exception("Exception during update of repository")
133 log.exception("Exception during update of repository")
134 h.flash(_('Error occurred during update of repository {}').format(
134 h.flash(_('Error occurred during update of repository {}').format(
135 self.db_repo_name), category='error')
135 self.db_repo_name), category='error')
136
136
137 return {
137 return {
138 'redirect_url': h.route_path('edit_repo_perms', repo_name=self.db_repo_name),
138 'redirect_url': h.route_path('edit_repo_perms', repo_name=self.db_repo_name),
139 'private': private_flag
139 'private': private_flag
140 }
140 }
@@ -1,1631 +1,1637 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.db import (
48 from rhodecode.model.db import (
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
50 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59
59
60 def load_default_context(self):
60 def load_default_context(self):
61 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 # backward compat., we use for OLD PRs a plain renderer
64 # backward compat., we use for OLD PRs a plain renderer
65 c.renderer = 'plain'
65 c.renderer = 'plain'
66 return c
66 return c
67
67
68 def _get_pull_requests_list(
68 def _get_pull_requests_list(
69 self, repo_name, source, filter_type, opened_by, statuses):
69 self, repo_name, source, filter_type, opened_by, statuses):
70
70
71 draw, start, limit = self._extract_chunk(self.request)
71 draw, start, limit = self._extract_chunk(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 _render = self.request.get_partial_renderer(
73 _render = self.request.get_partial_renderer(
74 'rhodecode:templates/data_table/_dt_elements.mako')
74 'rhodecode:templates/data_table/_dt_elements.mako')
75
75
76 # pagination
76 # pagination
77
77
78 if filter_type == 'awaiting_review':
78 if filter_type == 'awaiting_review':
79 pull_requests = PullRequestModel().get_awaiting_review(
79 pull_requests = PullRequestModel().get_awaiting_review(
80 repo_name, search_q=search_q, source=source, opened_by=opened_by,
80 repo_name, search_q=search_q, source=source, opened_by=opened_by,
81 statuses=statuses, offset=start, length=limit,
81 statuses=statuses, offset=start, length=limit,
82 order_by=order_by, order_dir=order_dir)
82 order_by=order_by, order_dir=order_dir)
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 repo_name, search_q=search_q, source=source, statuses=statuses,
84 repo_name, search_q=search_q, source=source, statuses=statuses,
85 opened_by=opened_by)
85 opened_by=opened_by)
86 elif filter_type == 'awaiting_my_review':
86 elif filter_type == 'awaiting_my_review':
87 pull_requests = PullRequestModel().get_awaiting_my_review(
87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 repo_name, search_q=search_q, source=source, opened_by=opened_by,
88 repo_name, search_q=search_q, source=source, opened_by=opened_by,
89 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 offset=start, length=limit, order_by=order_by,
90 offset=start, length=limit, order_by=order_by,
91 order_dir=order_dir)
91 order_dir=order_dir)
92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
93 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
94 statuses=statuses, opened_by=opened_by)
94 statuses=statuses, opened_by=opened_by)
95 else:
95 else:
96 pull_requests = PullRequestModel().get_all(
96 pull_requests = PullRequestModel().get_all(
97 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 statuses=statuses, offset=start, length=limit,
98 statuses=statuses, offset=start, length=limit,
99 order_by=order_by, order_dir=order_dir)
99 order_by=order_by, order_dir=order_dir)
100 pull_requests_total_count = PullRequestModel().count_all(
100 pull_requests_total_count = PullRequestModel().count_all(
101 repo_name, search_q=search_q, source=source, statuses=statuses,
101 repo_name, search_q=search_q, source=source, statuses=statuses,
102 opened_by=opened_by)
102 opened_by=opened_by)
103
103
104 data = []
104 data = []
105 comments_model = CommentsModel()
105 comments_model = CommentsModel()
106 for pr in pull_requests:
106 for pr in pull_requests:
107 comments = comments_model.get_all_comments(
107 comments = comments_model.get_all_comments(
108 self.db_repo.repo_id, pull_request=pr)
108 self.db_repo.repo_id, pull_request=pr)
109
109
110 data.append({
110 data.append({
111 'name': _render('pullrequest_name',
111 'name': _render('pullrequest_name',
112 pr.pull_request_id, pr.pull_request_state,
112 pr.pull_request_id, pr.pull_request_state,
113 pr.work_in_progress, pr.target_repo.repo_name),
113 pr.work_in_progress, pr.target_repo.repo_name),
114 'name_raw': pr.pull_request_id,
114 'name_raw': pr.pull_request_id,
115 'status': _render('pullrequest_status',
115 'status': _render('pullrequest_status',
116 pr.calculated_review_status()),
116 pr.calculated_review_status()),
117 'title': _render('pullrequest_title', pr.title, pr.description),
117 'title': _render('pullrequest_title', pr.title, pr.description),
118 'description': h.escape(pr.description),
118 'description': h.escape(pr.description),
119 'updated_on': _render('pullrequest_updated_on',
119 'updated_on': _render('pullrequest_updated_on',
120 h.datetime_to_time(pr.updated_on)),
120 h.datetime_to_time(pr.updated_on)),
121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 'created_on': _render('pullrequest_updated_on',
122 'created_on': _render('pullrequest_updated_on',
123 h.datetime_to_time(pr.created_on)),
123 h.datetime_to_time(pr.created_on)),
124 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'created_on_raw': h.datetime_to_time(pr.created_on),
125 'state': pr.pull_request_state,
125 'state': pr.pull_request_state,
126 'author': _render('pullrequest_author',
126 'author': _render('pullrequest_author',
127 pr.author.full_contact, ),
127 pr.author.full_contact, ),
128 'author_raw': pr.author.full_name,
128 'author_raw': pr.author.full_name,
129 'comments': _render('pullrequest_comments', len(comments)),
129 'comments': _render('pullrequest_comments', len(comments)),
130 'comments_raw': len(comments),
130 'comments_raw': len(comments),
131 'closed': pr.is_closed(),
131 'closed': pr.is_closed(),
132 })
132 })
133
133
134 data = ({
134 data = ({
135 'draw': draw,
135 'draw': draw,
136 'data': data,
136 'data': data,
137 'recordsTotal': pull_requests_total_count,
137 'recordsTotal': pull_requests_total_count,
138 'recordsFiltered': pull_requests_total_count,
138 'recordsFiltered': pull_requests_total_count,
139 })
139 })
140 return data
140 return data
141
141
142 @LoginRequired()
142 @LoginRequired()
143 @HasRepoPermissionAnyDecorator(
143 @HasRepoPermissionAnyDecorator(
144 'repository.read', 'repository.write', 'repository.admin')
144 'repository.read', 'repository.write', 'repository.admin')
145 @view_config(
145 @view_config(
146 route_name='pullrequest_show_all', request_method='GET',
146 route_name='pullrequest_show_all', request_method='GET',
147 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
147 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
148 def pull_request_list(self):
148 def pull_request_list(self):
149 c = self.load_default_context()
149 c = self.load_default_context()
150
150
151 req_get = self.request.GET
151 req_get = self.request.GET
152 c.source = str2bool(req_get.get('source'))
152 c.source = str2bool(req_get.get('source'))
153 c.closed = str2bool(req_get.get('closed'))
153 c.closed = str2bool(req_get.get('closed'))
154 c.my = str2bool(req_get.get('my'))
154 c.my = str2bool(req_get.get('my'))
155 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
155 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
156 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
156 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
157
157
158 c.active = 'open'
158 c.active = 'open'
159 if c.my:
159 if c.my:
160 c.active = 'my'
160 c.active = 'my'
161 if c.closed:
161 if c.closed:
162 c.active = 'closed'
162 c.active = 'closed'
163 if c.awaiting_review and not c.source:
163 if c.awaiting_review and not c.source:
164 c.active = 'awaiting'
164 c.active = 'awaiting'
165 if c.source and not c.awaiting_review:
165 if c.source and not c.awaiting_review:
166 c.active = 'source'
166 c.active = 'source'
167 if c.awaiting_my_review:
167 if c.awaiting_my_review:
168 c.active = 'awaiting_my'
168 c.active = 'awaiting_my'
169
169
170 return self._get_template_context(c)
170 return self._get_template_context(c)
171
171
172 @LoginRequired()
172 @LoginRequired()
173 @HasRepoPermissionAnyDecorator(
173 @HasRepoPermissionAnyDecorator(
174 'repository.read', 'repository.write', 'repository.admin')
174 'repository.read', 'repository.write', 'repository.admin')
175 @view_config(
175 @view_config(
176 route_name='pullrequest_show_all_data', request_method='GET',
176 route_name='pullrequest_show_all_data', request_method='GET',
177 renderer='json_ext', xhr=True)
177 renderer='json_ext', xhr=True)
178 def pull_request_list_data(self):
178 def pull_request_list_data(self):
179 self.load_default_context()
179 self.load_default_context()
180
180
181 # additional filters
181 # additional filters
182 req_get = self.request.GET
182 req_get = self.request.GET
183 source = str2bool(req_get.get('source'))
183 source = str2bool(req_get.get('source'))
184 closed = str2bool(req_get.get('closed'))
184 closed = str2bool(req_get.get('closed'))
185 my = str2bool(req_get.get('my'))
185 my = str2bool(req_get.get('my'))
186 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_review = str2bool(req_get.get('awaiting_review'))
187 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
188
188
189 filter_type = 'awaiting_review' if awaiting_review \
189 filter_type = 'awaiting_review' if awaiting_review \
190 else 'awaiting_my_review' if awaiting_my_review \
190 else 'awaiting_my_review' if awaiting_my_review \
191 else None
191 else None
192
192
193 opened_by = None
193 opened_by = None
194 if my:
194 if my:
195 opened_by = [self._rhodecode_user.user_id]
195 opened_by = [self._rhodecode_user.user_id]
196
196
197 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
198 if closed:
198 if closed:
199 statuses = [PullRequest.STATUS_CLOSED]
199 statuses = [PullRequest.STATUS_CLOSED]
200
200
201 data = self._get_pull_requests_list(
201 data = self._get_pull_requests_list(
202 repo_name=self.db_repo_name, source=source,
202 repo_name=self.db_repo_name, source=source,
203 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
204
204
205 return data
205 return data
206
206
207 def _is_diff_cache_enabled(self, target_repo):
207 def _is_diff_cache_enabled(self, target_repo):
208 caching_enabled = self._get_general_setting(
208 caching_enabled = self._get_general_setting(
209 target_repo, 'rhodecode_diff_cache')
209 target_repo, 'rhodecode_diff_cache')
210 log.debug('Diff caching enabled: %s', caching_enabled)
210 log.debug('Diff caching enabled: %s', caching_enabled)
211 return caching_enabled
211 return caching_enabled
212
212
213 def _get_diffset(self, source_repo_name, source_repo,
213 def _get_diffset(self, source_repo_name, source_repo,
214 ancestor_commit,
214 ancestor_commit,
215 source_ref_id, target_ref_id,
215 source_ref_id, target_ref_id,
216 target_commit, source_commit, diff_limit, file_limit,
216 target_commit, source_commit, diff_limit, file_limit,
217 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
217 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
218
218
219 if use_ancestor:
219 if use_ancestor:
220 # we might want to not use it for versions
220 # we might want to not use it for versions
221 target_ref_id = ancestor_commit.raw_id
221 target_ref_id = ancestor_commit.raw_id
222
222
223 vcs_diff = PullRequestModel().get_diff(
223 vcs_diff = PullRequestModel().get_diff(
224 source_repo, source_ref_id, target_ref_id,
224 source_repo, source_ref_id, target_ref_id,
225 hide_whitespace_changes, diff_context)
225 hide_whitespace_changes, diff_context)
226
226
227 diff_processor = diffs.DiffProcessor(
227 diff_processor = diffs.DiffProcessor(
228 vcs_diff, format='newdiff', diff_limit=diff_limit,
228 vcs_diff, format='newdiff', diff_limit=diff_limit,
229 file_limit=file_limit, show_full_diff=fulldiff)
229 file_limit=file_limit, show_full_diff=fulldiff)
230
230
231 _parsed = diff_processor.prepare()
231 _parsed = diff_processor.prepare()
232
232
233 diffset = codeblocks.DiffSet(
233 diffset = codeblocks.DiffSet(
234 repo_name=self.db_repo_name,
234 repo_name=self.db_repo_name,
235 source_repo_name=source_repo_name,
235 source_repo_name=source_repo_name,
236 source_node_getter=codeblocks.diffset_node_getter(target_commit),
236 source_node_getter=codeblocks.diffset_node_getter(target_commit),
237 target_node_getter=codeblocks.diffset_node_getter(source_commit),
237 target_node_getter=codeblocks.diffset_node_getter(source_commit),
238 )
238 )
239 diffset = self.path_filter.render_patchset_filtered(
239 diffset = self.path_filter.render_patchset_filtered(
240 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
240 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
241
241
242 return diffset
242 return diffset
243
243
244 def _get_range_diffset(self, source_scm, source_repo,
244 def _get_range_diffset(self, source_scm, source_repo,
245 commit1, commit2, diff_limit, file_limit,
245 commit1, commit2, diff_limit, file_limit,
246 fulldiff, hide_whitespace_changes, diff_context):
246 fulldiff, hide_whitespace_changes, diff_context):
247 vcs_diff = source_scm.get_diff(
247 vcs_diff = source_scm.get_diff(
248 commit1, commit2,
248 commit1, commit2,
249 ignore_whitespace=hide_whitespace_changes,
249 ignore_whitespace=hide_whitespace_changes,
250 context=diff_context)
250 context=diff_context)
251
251
252 diff_processor = diffs.DiffProcessor(
252 diff_processor = diffs.DiffProcessor(
253 vcs_diff, format='newdiff', diff_limit=diff_limit,
253 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 file_limit=file_limit, show_full_diff=fulldiff)
254 file_limit=file_limit, show_full_diff=fulldiff)
255
255
256 _parsed = diff_processor.prepare()
256 _parsed = diff_processor.prepare()
257
257
258 diffset = codeblocks.DiffSet(
258 diffset = codeblocks.DiffSet(
259 repo_name=source_repo.repo_name,
259 repo_name=source_repo.repo_name,
260 source_node_getter=codeblocks.diffset_node_getter(commit1),
260 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 target_node_getter=codeblocks.diffset_node_getter(commit2))
261 target_node_getter=codeblocks.diffset_node_getter(commit2))
262
262
263 diffset = self.path_filter.render_patchset_filtered(
263 diffset = self.path_filter.render_patchset_filtered(
264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265
265
266 return diffset
266 return diffset
267
267
268 @LoginRequired()
268 @LoginRequired()
269 @HasRepoPermissionAnyDecorator(
269 @HasRepoPermissionAnyDecorator(
270 'repository.read', 'repository.write', 'repository.admin')
270 'repository.read', 'repository.write', 'repository.admin')
271 @view_config(
271 @view_config(
272 route_name='pullrequest_show', request_method='GET',
272 route_name='pullrequest_show', request_method='GET',
273 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
273 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
274 def pull_request_show(self):
274 def pull_request_show(self):
275 _ = self.request.translate
275 _ = self.request.translate
276 c = self.load_default_context()
276 c = self.load_default_context()
277
277
278 pull_request = PullRequest.get_or_404(
278 pull_request = PullRequest.get_or_404(
279 self.request.matchdict['pull_request_id'])
279 self.request.matchdict['pull_request_id'])
280 pull_request_id = pull_request.pull_request_id
280 pull_request_id = pull_request.pull_request_id
281
281
282 c.state_progressing = pull_request.is_state_changing()
282 c.state_progressing = pull_request.is_state_changing()
283
283
284 _new_state = {
284 _new_state = {
285 'created': PullRequest.STATE_CREATED,
285 'created': PullRequest.STATE_CREATED,
286 }.get(self.request.GET.get('force_state'))
286 }.get(self.request.GET.get('force_state'))
287
287
288 if c.is_super_admin and _new_state:
288 if c.is_super_admin and _new_state:
289 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
289 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
290 h.flash(
290 h.flash(
291 _('Pull Request state was force changed to `{}`').format(_new_state),
291 _('Pull Request state was force changed to `{}`').format(_new_state),
292 category='success')
292 category='success')
293 Session().commit()
293 Session().commit()
294
294
295 raise HTTPFound(h.route_path(
295 raise HTTPFound(h.route_path(
296 'pullrequest_show', repo_name=self.db_repo_name,
296 'pullrequest_show', repo_name=self.db_repo_name,
297 pull_request_id=pull_request_id))
297 pull_request_id=pull_request_id))
298
298
299 version = self.request.GET.get('version')
299 version = self.request.GET.get('version')
300 from_version = self.request.GET.get('from_version') or version
300 from_version = self.request.GET.get('from_version') or version
301 merge_checks = self.request.GET.get('merge_checks')
301 merge_checks = self.request.GET.get('merge_checks')
302 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
302 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
303
303
304 # fetch global flags of ignore ws or context lines
304 # fetch global flags of ignore ws or context lines
305 diff_context = diffs.get_diff_context(self.request)
305 diff_context = diffs.get_diff_context(self.request)
306 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
306 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
307
307
308 force_refresh = str2bool(self.request.GET.get('force_refresh'))
308 force_refresh = str2bool(self.request.GET.get('force_refresh'))
309
309
310 (pull_request_latest,
310 (pull_request_latest,
311 pull_request_at_ver,
311 pull_request_at_ver,
312 pull_request_display_obj,
312 pull_request_display_obj,
313 at_version) = PullRequestModel().get_pr_version(
313 at_version) = PullRequestModel().get_pr_version(
314 pull_request_id, version=version)
314 pull_request_id, version=version)
315 pr_closed = pull_request_latest.is_closed()
315 pr_closed = pull_request_latest.is_closed()
316
316
317 if pr_closed and (version or from_version):
317 if pr_closed and (version or from_version):
318 # not allow to browse versions
318 # not allow to browse versions
319 raise HTTPFound(h.route_path(
319 raise HTTPFound(h.route_path(
320 'pullrequest_show', repo_name=self.db_repo_name,
320 'pullrequest_show', repo_name=self.db_repo_name,
321 pull_request_id=pull_request_id))
321 pull_request_id=pull_request_id))
322
322
323 versions = pull_request_display_obj.versions()
323 versions = pull_request_display_obj.versions()
324 # used to store per-commit range diffs
324 # used to store per-commit range diffs
325 c.changes = collections.OrderedDict()
325 c.changes = collections.OrderedDict()
326 c.range_diff_on = self.request.GET.get('range-diff') == "1"
326 c.range_diff_on = self.request.GET.get('range-diff') == "1"
327
327
328 c.at_version = at_version
328 c.at_version = at_version
329 c.at_version_num = (at_version
329 c.at_version_num = (at_version
330 if at_version and at_version != 'latest'
330 if at_version and at_version != 'latest'
331 else None)
331 else None)
332 c.at_version_pos = ChangesetComment.get_index_from_version(
332 c.at_version_pos = ChangesetComment.get_index_from_version(
333 c.at_version_num, versions)
333 c.at_version_num, versions)
334
334
335 (prev_pull_request_latest,
335 (prev_pull_request_latest,
336 prev_pull_request_at_ver,
336 prev_pull_request_at_ver,
337 prev_pull_request_display_obj,
337 prev_pull_request_display_obj,
338 prev_at_version) = PullRequestModel().get_pr_version(
338 prev_at_version) = PullRequestModel().get_pr_version(
339 pull_request_id, version=from_version)
339 pull_request_id, version=from_version)
340
340
341 c.from_version = prev_at_version
341 c.from_version = prev_at_version
342 c.from_version_num = (prev_at_version
342 c.from_version_num = (prev_at_version
343 if prev_at_version and prev_at_version != 'latest'
343 if prev_at_version and prev_at_version != 'latest'
344 else None)
344 else None)
345 c.from_version_pos = ChangesetComment.get_index_from_version(
345 c.from_version_pos = ChangesetComment.get_index_from_version(
346 c.from_version_num, versions)
346 c.from_version_num, versions)
347
347
348 # define if we're in COMPARE mode or VIEW at version mode
348 # define if we're in COMPARE mode or VIEW at version mode
349 compare = at_version != prev_at_version
349 compare = at_version != prev_at_version
350
350
351 # pull_requests repo_name we opened it against
351 # pull_requests repo_name we opened it against
352 # ie. target_repo must match
352 # ie. target_repo must match
353 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
353 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
354 raise HTTPNotFound()
354 raise HTTPNotFound()
355
355
356 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
356 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
357 pull_request_at_ver)
357 pull_request_at_ver)
358
358
359 c.pull_request = pull_request_display_obj
359 c.pull_request = pull_request_display_obj
360 c.renderer = pull_request_at_ver.description_renderer or c.renderer
360 c.renderer = pull_request_at_ver.description_renderer or c.renderer
361 c.pull_request_latest = pull_request_latest
361 c.pull_request_latest = pull_request_latest
362
362
363 if compare or (at_version and not at_version == 'latest'):
363 if compare or (at_version and not at_version == 'latest'):
364 c.allowed_to_change_status = False
364 c.allowed_to_change_status = False
365 c.allowed_to_update = False
365 c.allowed_to_update = False
366 c.allowed_to_merge = False
366 c.allowed_to_merge = False
367 c.allowed_to_delete = False
367 c.allowed_to_delete = False
368 c.allowed_to_comment = False
368 c.allowed_to_comment = False
369 c.allowed_to_close = False
369 c.allowed_to_close = False
370 else:
370 else:
371 can_change_status = PullRequestModel().check_user_change_status(
371 can_change_status = PullRequestModel().check_user_change_status(
372 pull_request_at_ver, self._rhodecode_user)
372 pull_request_at_ver, self._rhodecode_user)
373 c.allowed_to_change_status = can_change_status and not pr_closed
373 c.allowed_to_change_status = can_change_status and not pr_closed
374
374
375 c.allowed_to_update = PullRequestModel().check_user_update(
375 c.allowed_to_update = PullRequestModel().check_user_update(
376 pull_request_latest, self._rhodecode_user) and not pr_closed
376 pull_request_latest, self._rhodecode_user) and not pr_closed
377 c.allowed_to_merge = PullRequestModel().check_user_merge(
377 c.allowed_to_merge = PullRequestModel().check_user_merge(
378 pull_request_latest, self._rhodecode_user) and not pr_closed
378 pull_request_latest, self._rhodecode_user) and not pr_closed
379 c.allowed_to_delete = PullRequestModel().check_user_delete(
379 c.allowed_to_delete = PullRequestModel().check_user_delete(
380 pull_request_latest, self._rhodecode_user) and not pr_closed
380 pull_request_latest, self._rhodecode_user) and not pr_closed
381 c.allowed_to_comment = not pr_closed
381 c.allowed_to_comment = not pr_closed
382 c.allowed_to_close = c.allowed_to_merge and not pr_closed
382 c.allowed_to_close = c.allowed_to_merge and not pr_closed
383
383
384 c.forbid_adding_reviewers = False
384 c.forbid_adding_reviewers = False
385 c.forbid_author_to_review = False
385 c.forbid_author_to_review = False
386 c.forbid_commit_author_to_review = False
386 c.forbid_commit_author_to_review = False
387
387
388 if pull_request_latest.reviewer_data and \
388 if pull_request_latest.reviewer_data and \
389 'rules' in pull_request_latest.reviewer_data:
389 'rules' in pull_request_latest.reviewer_data:
390 rules = pull_request_latest.reviewer_data['rules'] or {}
390 rules = pull_request_latest.reviewer_data['rules'] or {}
391 try:
391 try:
392 c.forbid_adding_reviewers = rules.get(
392 c.forbid_adding_reviewers = rules.get(
393 'forbid_adding_reviewers')
393 'forbid_adding_reviewers')
394 c.forbid_author_to_review = rules.get(
394 c.forbid_author_to_review = rules.get(
395 'forbid_author_to_review')
395 'forbid_author_to_review')
396 c.forbid_commit_author_to_review = rules.get(
396 c.forbid_commit_author_to_review = rules.get(
397 'forbid_commit_author_to_review')
397 'forbid_commit_author_to_review')
398 except Exception:
398 except Exception:
399 pass
399 pass
400
400
401 # check merge capabilities
401 # check merge capabilities
402 _merge_check = MergeCheck.validate(
402 _merge_check = MergeCheck.validate(
403 pull_request_latest, auth_user=self._rhodecode_user,
403 pull_request_latest, auth_user=self._rhodecode_user,
404 translator=self.request.translate,
404 translator=self.request.translate,
405 force_shadow_repo_refresh=force_refresh)
405 force_shadow_repo_refresh=force_refresh)
406
406
407 c.pr_merge_errors = _merge_check.error_details
407 c.pr_merge_errors = _merge_check.error_details
408 c.pr_merge_possible = not _merge_check.failed
408 c.pr_merge_possible = not _merge_check.failed
409 c.pr_merge_message = _merge_check.merge_msg
409 c.pr_merge_message = _merge_check.merge_msg
410 c.pr_merge_source_commit = _merge_check.source_commit
410 c.pr_merge_source_commit = _merge_check.source_commit
411 c.pr_merge_target_commit = _merge_check.target_commit
411 c.pr_merge_target_commit = _merge_check.target_commit
412
412
413 c.pr_merge_info = MergeCheck.get_merge_conditions(
413 c.pr_merge_info = MergeCheck.get_merge_conditions(
414 pull_request_latest, translator=self.request.translate)
414 pull_request_latest, translator=self.request.translate)
415
415
416 c.pull_request_review_status = _merge_check.review_status
416 c.pull_request_review_status = _merge_check.review_status
417 if merge_checks:
417 if merge_checks:
418 self.request.override_renderer = \
418 self.request.override_renderer = \
419 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
419 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
420 return self._get_template_context(c)
420 return self._get_template_context(c)
421
421
422 comments_model = CommentsModel()
422 comments_model = CommentsModel()
423
423
424 # reviewers and statuses
424 # reviewers and statuses
425 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
425 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
426 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
426 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
427
427
428 # GENERAL COMMENTS with versions #
428 # GENERAL COMMENTS with versions #
429 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
429 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
430 q = q.order_by(ChangesetComment.comment_id.asc())
430 q = q.order_by(ChangesetComment.comment_id.asc())
431 general_comments = q
431 general_comments = q
432
432
433 # pick comments we want to render at current version
433 # pick comments we want to render at current version
434 c.comment_versions = comments_model.aggregate_comments(
434 c.comment_versions = comments_model.aggregate_comments(
435 general_comments, versions, c.at_version_num)
435 general_comments, versions, c.at_version_num)
436 c.comments = c.comment_versions[c.at_version_num]['until']
436 c.comments = c.comment_versions[c.at_version_num]['until']
437
437
438 # INLINE COMMENTS with versions #
438 # INLINE COMMENTS with versions #
439 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
439 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
440 q = q.order_by(ChangesetComment.comment_id.asc())
440 q = q.order_by(ChangesetComment.comment_id.asc())
441 inline_comments = q
441 inline_comments = q
442
442
443 c.inline_versions = comments_model.aggregate_comments(
443 c.inline_versions = comments_model.aggregate_comments(
444 inline_comments, versions, c.at_version_num, inline=True)
444 inline_comments, versions, c.at_version_num, inline=True)
445
445
446 # TODOs
446 # TODOs
447 c.unresolved_comments = CommentsModel() \
447 c.unresolved_comments = CommentsModel() \
448 .get_pull_request_unresolved_todos(pull_request)
448 .get_pull_request_unresolved_todos(pull_request)
449 c.resolved_comments = CommentsModel() \
449 c.resolved_comments = CommentsModel() \
450 .get_pull_request_resolved_todos(pull_request)
450 .get_pull_request_resolved_todos(pull_request)
451
451
452 # inject latest version
452 # inject latest version
453 latest_ver = PullRequest.get_pr_display_object(
453 latest_ver = PullRequest.get_pr_display_object(
454 pull_request_latest, pull_request_latest)
454 pull_request_latest, pull_request_latest)
455
455
456 c.versions = versions + [latest_ver]
456 c.versions = versions + [latest_ver]
457
457
458 # if we use version, then do not show later comments
458 # if we use version, then do not show later comments
459 # than current version
459 # than current version
460 display_inline_comments = collections.defaultdict(
460 display_inline_comments = collections.defaultdict(
461 lambda: collections.defaultdict(list))
461 lambda: collections.defaultdict(list))
462 for co in inline_comments:
462 for co in inline_comments:
463 if c.at_version_num:
463 if c.at_version_num:
464 # pick comments that are at least UPTO given version, so we
464 # pick comments that are at least UPTO given version, so we
465 # don't render comments for higher version
465 # don't render comments for higher version
466 should_render = co.pull_request_version_id and \
466 should_render = co.pull_request_version_id and \
467 co.pull_request_version_id <= c.at_version_num
467 co.pull_request_version_id <= c.at_version_num
468 else:
468 else:
469 # showing all, for 'latest'
469 # showing all, for 'latest'
470 should_render = True
470 should_render = True
471
471
472 if should_render:
472 if should_render:
473 display_inline_comments[co.f_path][co.line_no].append(co)
473 display_inline_comments[co.f_path][co.line_no].append(co)
474
474
475 # load diff data into template context, if we use compare mode then
475 # load diff data into template context, if we use compare mode then
476 # diff is calculated based on changes between versions of PR
476 # diff is calculated based on changes between versions of PR
477
477
478 source_repo = pull_request_at_ver.source_repo
478 source_repo = pull_request_at_ver.source_repo
479 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
479 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
480
480
481 target_repo = pull_request_at_ver.target_repo
481 target_repo = pull_request_at_ver.target_repo
482 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
482 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
483
483
484 if compare:
484 if compare:
485 # in compare switch the diff base to latest commit from prev version
485 # in compare switch the diff base to latest commit from prev version
486 target_ref_id = prev_pull_request_display_obj.revisions[0]
486 target_ref_id = prev_pull_request_display_obj.revisions[0]
487
487
488 # despite opening commits for bookmarks/branches/tags, we always
488 # despite opening commits for bookmarks/branches/tags, we always
489 # convert this to rev to prevent changes after bookmark or branch change
489 # convert this to rev to prevent changes after bookmark or branch change
490 c.source_ref_type = 'rev'
490 c.source_ref_type = 'rev'
491 c.source_ref = source_ref_id
491 c.source_ref = source_ref_id
492
492
493 c.target_ref_type = 'rev'
493 c.target_ref_type = 'rev'
494 c.target_ref = target_ref_id
494 c.target_ref = target_ref_id
495
495
496 c.source_repo = source_repo
496 c.source_repo = source_repo
497 c.target_repo = target_repo
497 c.target_repo = target_repo
498
498
499 c.commit_ranges = []
499 c.commit_ranges = []
500 source_commit = EmptyCommit()
500 source_commit = EmptyCommit()
501 target_commit = EmptyCommit()
501 target_commit = EmptyCommit()
502 c.missing_requirements = False
502 c.missing_requirements = False
503
503
504 source_scm = source_repo.scm_instance()
504 source_scm = source_repo.scm_instance()
505 target_scm = target_repo.scm_instance()
505 target_scm = target_repo.scm_instance()
506
506
507 shadow_scm = None
507 shadow_scm = None
508 try:
508 try:
509 shadow_scm = pull_request_latest.get_shadow_repo()
509 shadow_scm = pull_request_latest.get_shadow_repo()
510 except Exception:
510 except Exception:
511 log.debug('Failed to get shadow repo', exc_info=True)
511 log.debug('Failed to get shadow repo', exc_info=True)
512 # try first the existing source_repo, and then shadow
512 # try first the existing source_repo, and then shadow
513 # repo if we can obtain one
513 # repo if we can obtain one
514 commits_source_repo = source_scm
514 commits_source_repo = source_scm
515 if shadow_scm:
515 if shadow_scm:
516 commits_source_repo = shadow_scm
516 commits_source_repo = shadow_scm
517
517
518 c.commits_source_repo = commits_source_repo
518 c.commits_source_repo = commits_source_repo
519 c.ancestor = None # set it to None, to hide it from PR view
519 c.ancestor = None # set it to None, to hide it from PR view
520
520
521 # empty version means latest, so we keep this to prevent
521 # empty version means latest, so we keep this to prevent
522 # double caching
522 # double caching
523 version_normalized = version or 'latest'
523 version_normalized = version or 'latest'
524 from_version_normalized = from_version or 'latest'
524 from_version_normalized = from_version or 'latest'
525
525
526 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
526 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
527 cache_file_path = diff_cache_exist(
527 cache_file_path = diff_cache_exist(
528 cache_path, 'pull_request', pull_request_id, version_normalized,
528 cache_path, 'pull_request', pull_request_id, version_normalized,
529 from_version_normalized, source_ref_id, target_ref_id,
529 from_version_normalized, source_ref_id, target_ref_id,
530 hide_whitespace_changes, diff_context, c.fulldiff)
530 hide_whitespace_changes, diff_context, c.fulldiff)
531
531
532 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
532 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
533 force_recache = self.get_recache_flag()
533 force_recache = self.get_recache_flag()
534
534
535 cached_diff = None
535 cached_diff = None
536 if caching_enabled:
536 if caching_enabled:
537 cached_diff = load_cached_diff(cache_file_path)
537 cached_diff = load_cached_diff(cache_file_path)
538
538
539 has_proper_commit_cache = (
539 has_proper_commit_cache = (
540 cached_diff and cached_diff.get('commits')
540 cached_diff and cached_diff.get('commits')
541 and len(cached_diff.get('commits', [])) == 5
541 and len(cached_diff.get('commits', [])) == 5
542 and cached_diff.get('commits')[0]
542 and cached_diff.get('commits')[0]
543 and cached_diff.get('commits')[3])
543 and cached_diff.get('commits')[3])
544
544
545 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
545 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
546 diff_commit_cache = \
546 diff_commit_cache = \
547 (ancestor_commit, commit_cache, missing_requirements,
547 (ancestor_commit, commit_cache, missing_requirements,
548 source_commit, target_commit) = cached_diff['commits']
548 source_commit, target_commit) = cached_diff['commits']
549 else:
549 else:
550 # NOTE(marcink): we reach potentially unreachable errors when a PR has
550 # NOTE(marcink): we reach potentially unreachable errors when a PR has
551 # merge errors resulting in potentially hidden commits in the shadow repo.
551 # merge errors resulting in potentially hidden commits in the shadow repo.
552 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
552 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
553 and _merge_check.merge_response
553 and _merge_check.merge_response
554 maybe_unreachable = maybe_unreachable \
554 maybe_unreachable = maybe_unreachable \
555 and _merge_check.merge_response.metadata.get('unresolved_files')
555 and _merge_check.merge_response.metadata.get('unresolved_files')
556 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
556 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
557 diff_commit_cache = \
557 diff_commit_cache = \
558 (ancestor_commit, commit_cache, missing_requirements,
558 (ancestor_commit, commit_cache, missing_requirements,
559 source_commit, target_commit) = self.get_commits(
559 source_commit, target_commit) = self.get_commits(
560 commits_source_repo,
560 commits_source_repo,
561 pull_request_at_ver,
561 pull_request_at_ver,
562 source_commit,
562 source_commit,
563 source_ref_id,
563 source_ref_id,
564 source_scm,
564 source_scm,
565 target_commit,
565 target_commit,
566 target_ref_id,
566 target_ref_id,
567 target_scm,
567 target_scm,
568 maybe_unreachable=maybe_unreachable)
568 maybe_unreachable=maybe_unreachable)
569
569
570 # register our commit range
570 # register our commit range
571 for comm in commit_cache.values():
571 for comm in commit_cache.values():
572 c.commit_ranges.append(comm)
572 c.commit_ranges.append(comm)
573
573
574 c.missing_requirements = missing_requirements
574 c.missing_requirements = missing_requirements
575 c.ancestor_commit = ancestor_commit
575 c.ancestor_commit = ancestor_commit
576 c.statuses = source_repo.statuses(
576 c.statuses = source_repo.statuses(
577 [x.raw_id for x in c.commit_ranges])
577 [x.raw_id for x in c.commit_ranges])
578
578
579 # auto collapse if we have more than limit
579 # auto collapse if we have more than limit
580 collapse_limit = diffs.DiffProcessor._collapse_commits_over
580 collapse_limit = diffs.DiffProcessor._collapse_commits_over
581 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
581 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
582 c.compare_mode = compare
582 c.compare_mode = compare
583
583
584 # diff_limit is the old behavior, will cut off the whole diff
584 # diff_limit is the old behavior, will cut off the whole diff
585 # if the limit is applied otherwise will just hide the
585 # if the limit is applied otherwise will just hide the
586 # big files from the front-end
586 # big files from the front-end
587 diff_limit = c.visual.cut_off_limit_diff
587 diff_limit = c.visual.cut_off_limit_diff
588 file_limit = c.visual.cut_off_limit_file
588 file_limit = c.visual.cut_off_limit_file
589
589
590 c.missing_commits = False
590 c.missing_commits = False
591 if (c.missing_requirements
591 if (c.missing_requirements
592 or isinstance(source_commit, EmptyCommit)
592 or isinstance(source_commit, EmptyCommit)
593 or source_commit == target_commit):
593 or source_commit == target_commit):
594
594
595 c.missing_commits = True
595 c.missing_commits = True
596 else:
596 else:
597 c.inline_comments = display_inline_comments
597 c.inline_comments = display_inline_comments
598
598
599 use_ancestor = True
599 use_ancestor = True
600 if from_version_normalized != version_normalized:
600 if from_version_normalized != version_normalized:
601 use_ancestor = False
601 use_ancestor = False
602
602
603 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
603 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
604 if not force_recache and has_proper_diff_cache:
604 if not force_recache and has_proper_diff_cache:
605 c.diffset = cached_diff['diff']
605 c.diffset = cached_diff['diff']
606 else:
606 else:
607 c.diffset = self._get_diffset(
607 try:
608 c.source_repo.repo_name, commits_source_repo,
608 c.diffset = self._get_diffset(
609 c.ancestor_commit,
609 c.source_repo.repo_name, commits_source_repo,
610 source_ref_id, target_ref_id,
610 c.ancestor_commit,
611 target_commit, source_commit,
611 source_ref_id, target_ref_id,
612 diff_limit, file_limit, c.fulldiff,
612 target_commit, source_commit,
613 hide_whitespace_changes, diff_context,
613 diff_limit, file_limit, c.fulldiff,
614 use_ancestor=use_ancestor
614 hide_whitespace_changes, diff_context,
615 )
615 use_ancestor=use_ancestor
616
616 )
617 # save cached diff
618 if caching_enabled:
619 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
620
621 c.limited_diff = c.diffset.limited_diff
622
623 # calculate removed files that are bound to comments
624 comment_deleted_files = [
625 fname for fname in display_inline_comments
626 if fname not in c.diffset.file_stats]
627
628 c.deleted_files_comments = collections.defaultdict(dict)
629 for fname, per_line_comments in display_inline_comments.items():
630 if fname in comment_deleted_files:
631 c.deleted_files_comments[fname]['stats'] = 0
632 c.deleted_files_comments[fname]['comments'] = list()
633 for lno, comments in per_line_comments.items():
634 c.deleted_files_comments[fname]['comments'].extend(comments)
635
636 # maybe calculate the range diff
637 if c.range_diff_on:
638 # TODO(marcink): set whitespace/context
639 context_lcl = 3
640 ign_whitespace_lcl = False
641
642 for commit in c.commit_ranges:
643 commit2 = commit
644 commit1 = commit.first_parent
645
646 range_diff_cache_file_path = diff_cache_exist(
647 cache_path, 'diff', commit.raw_id,
648 ign_whitespace_lcl, context_lcl, c.fulldiff)
649
650 cached_diff = None
651 if caching_enabled:
652 cached_diff = load_cached_diff(range_diff_cache_file_path)
653
654 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
655 if not force_recache and has_proper_diff_cache:
656 diffset = cached_diff['diff']
657 else:
658 diffset = self._get_range_diffset(
659 commits_source_repo, source_repo,
660 commit1, commit2, diff_limit, file_limit,
661 c.fulldiff, ign_whitespace_lcl, context_lcl
662 )
663
617
664 # save cached diff
618 # save cached diff
665 if caching_enabled:
619 if caching_enabled:
666 cache_diff(range_diff_cache_file_path, diffset, None)
620 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
621 except CommitDoesNotExistError:
622 log.exception('Failed to generate diffset')
623 c.missing_commits = True
624
625 if not c.missing_commits:
626
627 c.limited_diff = c.diffset.limited_diff
628
629 # calculate removed files that are bound to comments
630 comment_deleted_files = [
631 fname for fname in display_inline_comments
632 if fname not in c.diffset.file_stats]
633
634 c.deleted_files_comments = collections.defaultdict(dict)
635 for fname, per_line_comments in display_inline_comments.items():
636 if fname in comment_deleted_files:
637 c.deleted_files_comments[fname]['stats'] = 0
638 c.deleted_files_comments[fname]['comments'] = list()
639 for lno, comments in per_line_comments.items():
640 c.deleted_files_comments[fname]['comments'].extend(comments)
641
642 # maybe calculate the range diff
643 if c.range_diff_on:
644 # TODO(marcink): set whitespace/context
645 context_lcl = 3
646 ign_whitespace_lcl = False
667
647
668 c.changes[commit.raw_id] = diffset
648 for commit in c.commit_ranges:
649 commit2 = commit
650 commit1 = commit.first_parent
651
652 range_diff_cache_file_path = diff_cache_exist(
653 cache_path, 'diff', commit.raw_id,
654 ign_whitespace_lcl, context_lcl, c.fulldiff)
655
656 cached_diff = None
657 if caching_enabled:
658 cached_diff = load_cached_diff(range_diff_cache_file_path)
659
660 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
661 if not force_recache and has_proper_diff_cache:
662 diffset = cached_diff['diff']
663 else:
664 diffset = self._get_range_diffset(
665 commits_source_repo, source_repo,
666 commit1, commit2, diff_limit, file_limit,
667 c.fulldiff, ign_whitespace_lcl, context_lcl
668 )
669
670 # save cached diff
671 if caching_enabled:
672 cache_diff(range_diff_cache_file_path, diffset, None)
673
674 c.changes[commit.raw_id] = diffset
669
675
670 # this is a hack to properly display links, when creating PR, the
676 # this is a hack to properly display links, when creating PR, the
671 # compare view and others uses different notation, and
677 # compare view and others uses different notation, and
672 # compare_commits.mako renders links based on the target_repo.
678 # compare_commits.mako renders links based on the target_repo.
673 # We need to swap that here to generate it properly on the html side
679 # We need to swap that here to generate it properly on the html side
674 c.target_repo = c.source_repo
680 c.target_repo = c.source_repo
675
681
676 c.commit_statuses = ChangesetStatus.STATUSES
682 c.commit_statuses = ChangesetStatus.STATUSES
677
683
678 c.show_version_changes = not pr_closed
684 c.show_version_changes = not pr_closed
679 if c.show_version_changes:
685 if c.show_version_changes:
680 cur_obj = pull_request_at_ver
686 cur_obj = pull_request_at_ver
681 prev_obj = prev_pull_request_at_ver
687 prev_obj = prev_pull_request_at_ver
682
688
683 old_commit_ids = prev_obj.revisions
689 old_commit_ids = prev_obj.revisions
684 new_commit_ids = cur_obj.revisions
690 new_commit_ids = cur_obj.revisions
685 commit_changes = PullRequestModel()._calculate_commit_id_changes(
691 commit_changes = PullRequestModel()._calculate_commit_id_changes(
686 old_commit_ids, new_commit_ids)
692 old_commit_ids, new_commit_ids)
687 c.commit_changes_summary = commit_changes
693 c.commit_changes_summary = commit_changes
688
694
689 # calculate the diff for commits between versions
695 # calculate the diff for commits between versions
690 c.commit_changes = []
696 c.commit_changes = []
691
697
692 def mark(cs, fw):
698 def mark(cs, fw):
693 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
699 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
694
700
695 for c_type, raw_id in mark(commit_changes.added, 'a') \
701 for c_type, raw_id in mark(commit_changes.added, 'a') \
696 + mark(commit_changes.removed, 'r') \
702 + mark(commit_changes.removed, 'r') \
697 + mark(commit_changes.common, 'c'):
703 + mark(commit_changes.common, 'c'):
698
704
699 if raw_id in commit_cache:
705 if raw_id in commit_cache:
700 commit = commit_cache[raw_id]
706 commit = commit_cache[raw_id]
701 else:
707 else:
702 try:
708 try:
703 commit = commits_source_repo.get_commit(raw_id)
709 commit = commits_source_repo.get_commit(raw_id)
704 except CommitDoesNotExistError:
710 except CommitDoesNotExistError:
705 # in case we fail extracting still use "dummy" commit
711 # in case we fail extracting still use "dummy" commit
706 # for display in commit diff
712 # for display in commit diff
707 commit = h.AttributeDict(
713 commit = h.AttributeDict(
708 {'raw_id': raw_id,
714 {'raw_id': raw_id,
709 'message': 'EMPTY or MISSING COMMIT'})
715 'message': 'EMPTY or MISSING COMMIT'})
710 c.commit_changes.append([c_type, commit])
716 c.commit_changes.append([c_type, commit])
711
717
712 # current user review statuses for each version
718 # current user review statuses for each version
713 c.review_versions = {}
719 c.review_versions = {}
714 if self._rhodecode_user.user_id in allowed_reviewers:
720 if self._rhodecode_user.user_id in allowed_reviewers:
715 for co in general_comments:
721 for co in general_comments:
716 if co.author.user_id == self._rhodecode_user.user_id:
722 if co.author.user_id == self._rhodecode_user.user_id:
717 status = co.status_change
723 status = co.status_change
718 if status:
724 if status:
719 _ver_pr = status[0].comment.pull_request_version_id
725 _ver_pr = status[0].comment.pull_request_version_id
720 c.review_versions[_ver_pr] = status[0]
726 c.review_versions[_ver_pr] = status[0]
721
727
722 return self._get_template_context(c)
728 return self._get_template_context(c)
723
729
724 def get_commits(
730 def get_commits(
725 self, commits_source_repo, pull_request_at_ver, source_commit,
731 self, commits_source_repo, pull_request_at_ver, source_commit,
726 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
732 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
727 maybe_unreachable=False):
733 maybe_unreachable=False):
728
734
729 commit_cache = collections.OrderedDict()
735 commit_cache = collections.OrderedDict()
730 missing_requirements = False
736 missing_requirements = False
731
737
732 try:
738 try:
733 pre_load = ["author", "date", "message", "branch", "parents"]
739 pre_load = ["author", "date", "message", "branch", "parents"]
734
740
735 pull_request_commits = pull_request_at_ver.revisions
741 pull_request_commits = pull_request_at_ver.revisions
736 log.debug('Loading %s commits from %s',
742 log.debug('Loading %s commits from %s',
737 len(pull_request_commits), commits_source_repo)
743 len(pull_request_commits), commits_source_repo)
738
744
739 for rev in pull_request_commits:
745 for rev in pull_request_commits:
740 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
746 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
741 maybe_unreachable=maybe_unreachable)
747 maybe_unreachable=maybe_unreachable)
742 commit_cache[comm.raw_id] = comm
748 commit_cache[comm.raw_id] = comm
743
749
744 # Order here matters, we first need to get target, and then
750 # Order here matters, we first need to get target, and then
745 # the source
751 # the source
746 target_commit = commits_source_repo.get_commit(
752 target_commit = commits_source_repo.get_commit(
747 commit_id=safe_str(target_ref_id))
753 commit_id=safe_str(target_ref_id))
748
754
749 source_commit = commits_source_repo.get_commit(
755 source_commit = commits_source_repo.get_commit(
750 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
756 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
751 except CommitDoesNotExistError:
757 except CommitDoesNotExistError:
752 log.warning('Failed to get commit from `{}` repo'.format(
758 log.warning('Failed to get commit from `{}` repo'.format(
753 commits_source_repo), exc_info=True)
759 commits_source_repo), exc_info=True)
754 except RepositoryRequirementError:
760 except RepositoryRequirementError:
755 log.warning('Failed to get all required data from repo', exc_info=True)
761 log.warning('Failed to get all required data from repo', exc_info=True)
756 missing_requirements = True
762 missing_requirements = True
757
763
758 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
764 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
759
765
760 try:
766 try:
761 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
767 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
762 except Exception:
768 except Exception:
763 ancestor_commit = None
769 ancestor_commit = None
764
770
765 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
771 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
766
772
767 def assure_not_empty_repo(self):
773 def assure_not_empty_repo(self):
768 _ = self.request.translate
774 _ = self.request.translate
769
775
770 try:
776 try:
771 self.db_repo.scm_instance().get_commit()
777 self.db_repo.scm_instance().get_commit()
772 except EmptyRepositoryError:
778 except EmptyRepositoryError:
773 h.flash(h.literal(_('There are no commits yet')),
779 h.flash(h.literal(_('There are no commits yet')),
774 category='warning')
780 category='warning')
775 raise HTTPFound(
781 raise HTTPFound(
776 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
782 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
777
783
778 @LoginRequired()
784 @LoginRequired()
779 @NotAnonymous()
785 @NotAnonymous()
780 @HasRepoPermissionAnyDecorator(
786 @HasRepoPermissionAnyDecorator(
781 'repository.read', 'repository.write', 'repository.admin')
787 'repository.read', 'repository.write', 'repository.admin')
782 @view_config(
788 @view_config(
783 route_name='pullrequest_new', request_method='GET',
789 route_name='pullrequest_new', request_method='GET',
784 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
790 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
785 def pull_request_new(self):
791 def pull_request_new(self):
786 _ = self.request.translate
792 _ = self.request.translate
787 c = self.load_default_context()
793 c = self.load_default_context()
788
794
789 self.assure_not_empty_repo()
795 self.assure_not_empty_repo()
790 source_repo = self.db_repo
796 source_repo = self.db_repo
791
797
792 commit_id = self.request.GET.get('commit')
798 commit_id = self.request.GET.get('commit')
793 branch_ref = self.request.GET.get('branch')
799 branch_ref = self.request.GET.get('branch')
794 bookmark_ref = self.request.GET.get('bookmark')
800 bookmark_ref = self.request.GET.get('bookmark')
795
801
796 try:
802 try:
797 source_repo_data = PullRequestModel().generate_repo_data(
803 source_repo_data = PullRequestModel().generate_repo_data(
798 source_repo, commit_id=commit_id,
804 source_repo, commit_id=commit_id,
799 branch=branch_ref, bookmark=bookmark_ref,
805 branch=branch_ref, bookmark=bookmark_ref,
800 translator=self.request.translate)
806 translator=self.request.translate)
801 except CommitDoesNotExistError as e:
807 except CommitDoesNotExistError as e:
802 log.exception(e)
808 log.exception(e)
803 h.flash(_('Commit does not exist'), 'error')
809 h.flash(_('Commit does not exist'), 'error')
804 raise HTTPFound(
810 raise HTTPFound(
805 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
811 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
806
812
807 default_target_repo = source_repo
813 default_target_repo = source_repo
808
814
809 if source_repo.parent and c.has_origin_repo_read_perm:
815 if source_repo.parent and c.has_origin_repo_read_perm:
810 parent_vcs_obj = source_repo.parent.scm_instance()
816 parent_vcs_obj = source_repo.parent.scm_instance()
811 if parent_vcs_obj and not parent_vcs_obj.is_empty():
817 if parent_vcs_obj and not parent_vcs_obj.is_empty():
812 # change default if we have a parent repo
818 # change default if we have a parent repo
813 default_target_repo = source_repo.parent
819 default_target_repo = source_repo.parent
814
820
815 target_repo_data = PullRequestModel().generate_repo_data(
821 target_repo_data = PullRequestModel().generate_repo_data(
816 default_target_repo, translator=self.request.translate)
822 default_target_repo, translator=self.request.translate)
817
823
818 selected_source_ref = source_repo_data['refs']['selected_ref']
824 selected_source_ref = source_repo_data['refs']['selected_ref']
819 title_source_ref = ''
825 title_source_ref = ''
820 if selected_source_ref:
826 if selected_source_ref:
821 title_source_ref = selected_source_ref.split(':', 2)[1]
827 title_source_ref = selected_source_ref.split(':', 2)[1]
822 c.default_title = PullRequestModel().generate_pullrequest_title(
828 c.default_title = PullRequestModel().generate_pullrequest_title(
823 source=source_repo.repo_name,
829 source=source_repo.repo_name,
824 source_ref=title_source_ref,
830 source_ref=title_source_ref,
825 target=default_target_repo.repo_name
831 target=default_target_repo.repo_name
826 )
832 )
827
833
828 c.default_repo_data = {
834 c.default_repo_data = {
829 'source_repo_name': source_repo.repo_name,
835 'source_repo_name': source_repo.repo_name,
830 'source_refs_json': json.dumps(source_repo_data),
836 'source_refs_json': json.dumps(source_repo_data),
831 'target_repo_name': default_target_repo.repo_name,
837 'target_repo_name': default_target_repo.repo_name,
832 'target_refs_json': json.dumps(target_repo_data),
838 'target_refs_json': json.dumps(target_repo_data),
833 }
839 }
834 c.default_source_ref = selected_source_ref
840 c.default_source_ref = selected_source_ref
835
841
836 return self._get_template_context(c)
842 return self._get_template_context(c)
837
843
838 @LoginRequired()
844 @LoginRequired()
839 @NotAnonymous()
845 @NotAnonymous()
840 @HasRepoPermissionAnyDecorator(
846 @HasRepoPermissionAnyDecorator(
841 'repository.read', 'repository.write', 'repository.admin')
847 'repository.read', 'repository.write', 'repository.admin')
842 @view_config(
848 @view_config(
843 route_name='pullrequest_repo_refs', request_method='GET',
849 route_name='pullrequest_repo_refs', request_method='GET',
844 renderer='json_ext', xhr=True)
850 renderer='json_ext', xhr=True)
845 def pull_request_repo_refs(self):
851 def pull_request_repo_refs(self):
846 self.load_default_context()
852 self.load_default_context()
847 target_repo_name = self.request.matchdict['target_repo_name']
853 target_repo_name = self.request.matchdict['target_repo_name']
848 repo = Repository.get_by_repo_name(target_repo_name)
854 repo = Repository.get_by_repo_name(target_repo_name)
849 if not repo:
855 if not repo:
850 raise HTTPNotFound()
856 raise HTTPNotFound()
851
857
852 target_perm = HasRepoPermissionAny(
858 target_perm = HasRepoPermissionAny(
853 'repository.read', 'repository.write', 'repository.admin')(
859 'repository.read', 'repository.write', 'repository.admin')(
854 target_repo_name)
860 target_repo_name)
855 if not target_perm:
861 if not target_perm:
856 raise HTTPNotFound()
862 raise HTTPNotFound()
857
863
858 return PullRequestModel().generate_repo_data(
864 return PullRequestModel().generate_repo_data(
859 repo, translator=self.request.translate)
865 repo, translator=self.request.translate)
860
866
861 @LoginRequired()
867 @LoginRequired()
862 @NotAnonymous()
868 @NotAnonymous()
863 @HasRepoPermissionAnyDecorator(
869 @HasRepoPermissionAnyDecorator(
864 'repository.read', 'repository.write', 'repository.admin')
870 'repository.read', 'repository.write', 'repository.admin')
865 @view_config(
871 @view_config(
866 route_name='pullrequest_repo_targets', request_method='GET',
872 route_name='pullrequest_repo_targets', request_method='GET',
867 renderer='json_ext', xhr=True)
873 renderer='json_ext', xhr=True)
868 def pullrequest_repo_targets(self):
874 def pullrequest_repo_targets(self):
869 _ = self.request.translate
875 _ = self.request.translate
870 filter_query = self.request.GET.get('query')
876 filter_query = self.request.GET.get('query')
871
877
872 # get the parents
878 # get the parents
873 parent_target_repos = []
879 parent_target_repos = []
874 if self.db_repo.parent:
880 if self.db_repo.parent:
875 parents_query = Repository.query() \
881 parents_query = Repository.query() \
876 .order_by(func.length(Repository.repo_name)) \
882 .order_by(func.length(Repository.repo_name)) \
877 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
883 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
878
884
879 if filter_query:
885 if filter_query:
880 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
886 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
881 parents_query = parents_query.filter(
887 parents_query = parents_query.filter(
882 Repository.repo_name.ilike(ilike_expression))
888 Repository.repo_name.ilike(ilike_expression))
883 parents = parents_query.limit(20).all()
889 parents = parents_query.limit(20).all()
884
890
885 for parent in parents:
891 for parent in parents:
886 parent_vcs_obj = parent.scm_instance()
892 parent_vcs_obj = parent.scm_instance()
887 if parent_vcs_obj and not parent_vcs_obj.is_empty():
893 if parent_vcs_obj and not parent_vcs_obj.is_empty():
888 parent_target_repos.append(parent)
894 parent_target_repos.append(parent)
889
895
890 # get other forks, and repo itself
896 # get other forks, and repo itself
891 query = Repository.query() \
897 query = Repository.query() \
892 .order_by(func.length(Repository.repo_name)) \
898 .order_by(func.length(Repository.repo_name)) \
893 .filter(
899 .filter(
894 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
900 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
895 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
901 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
896 ) \
902 ) \
897 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
903 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
898
904
899 if filter_query:
905 if filter_query:
900 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
906 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
901 query = query.filter(Repository.repo_name.ilike(ilike_expression))
907 query = query.filter(Repository.repo_name.ilike(ilike_expression))
902
908
903 limit = max(20 - len(parent_target_repos), 5) # not less then 5
909 limit = max(20 - len(parent_target_repos), 5) # not less then 5
904 target_repos = query.limit(limit).all()
910 target_repos = query.limit(limit).all()
905
911
906 all_target_repos = target_repos + parent_target_repos
912 all_target_repos = target_repos + parent_target_repos
907
913
908 repos = []
914 repos = []
909 # This checks permissions to the repositories
915 # This checks permissions to the repositories
910 for obj in ScmModel().get_repos(all_target_repos):
916 for obj in ScmModel().get_repos(all_target_repos):
911 repos.append({
917 repos.append({
912 'id': obj['name'],
918 'id': obj['name'],
913 'text': obj['name'],
919 'text': obj['name'],
914 'type': 'repo',
920 'type': 'repo',
915 'repo_id': obj['dbrepo']['repo_id'],
921 'repo_id': obj['dbrepo']['repo_id'],
916 'repo_type': obj['dbrepo']['repo_type'],
922 'repo_type': obj['dbrepo']['repo_type'],
917 'private': obj['dbrepo']['private'],
923 'private': obj['dbrepo']['private'],
918
924
919 })
925 })
920
926
921 data = {
927 data = {
922 'more': False,
928 'more': False,
923 'results': [{
929 'results': [{
924 'text': _('Repositories'),
930 'text': _('Repositories'),
925 'children': repos
931 'children': repos
926 }] if repos else []
932 }] if repos else []
927 }
933 }
928 return data
934 return data
929
935
930 @LoginRequired()
936 @LoginRequired()
931 @NotAnonymous()
937 @NotAnonymous()
932 @HasRepoPermissionAnyDecorator(
938 @HasRepoPermissionAnyDecorator(
933 'repository.read', 'repository.write', 'repository.admin')
939 'repository.read', 'repository.write', 'repository.admin')
934 @CSRFRequired()
940 @CSRFRequired()
935 @view_config(
941 @view_config(
936 route_name='pullrequest_create', request_method='POST',
942 route_name='pullrequest_create', request_method='POST',
937 renderer=None)
943 renderer=None)
938 def pull_request_create(self):
944 def pull_request_create(self):
939 _ = self.request.translate
945 _ = self.request.translate
940 self.assure_not_empty_repo()
946 self.assure_not_empty_repo()
941 self.load_default_context()
947 self.load_default_context()
942
948
943 controls = peppercorn.parse(self.request.POST.items())
949 controls = peppercorn.parse(self.request.POST.items())
944
950
945 try:
951 try:
946 form = PullRequestForm(
952 form = PullRequestForm(
947 self.request.translate, self.db_repo.repo_id)()
953 self.request.translate, self.db_repo.repo_id)()
948 _form = form.to_python(controls)
954 _form = form.to_python(controls)
949 except formencode.Invalid as errors:
955 except formencode.Invalid as errors:
950 if errors.error_dict.get('revisions'):
956 if errors.error_dict.get('revisions'):
951 msg = 'Revisions: %s' % errors.error_dict['revisions']
957 msg = 'Revisions: %s' % errors.error_dict['revisions']
952 elif errors.error_dict.get('pullrequest_title'):
958 elif errors.error_dict.get('pullrequest_title'):
953 msg = errors.error_dict.get('pullrequest_title')
959 msg = errors.error_dict.get('pullrequest_title')
954 else:
960 else:
955 msg = _('Error creating pull request: {}').format(errors)
961 msg = _('Error creating pull request: {}').format(errors)
956 log.exception(msg)
962 log.exception(msg)
957 h.flash(msg, 'error')
963 h.flash(msg, 'error')
958
964
959 # would rather just go back to form ...
965 # would rather just go back to form ...
960 raise HTTPFound(
966 raise HTTPFound(
961 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
967 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
962
968
963 source_repo = _form['source_repo']
969 source_repo = _form['source_repo']
964 source_ref = _form['source_ref']
970 source_ref = _form['source_ref']
965 target_repo = _form['target_repo']
971 target_repo = _form['target_repo']
966 target_ref = _form['target_ref']
972 target_ref = _form['target_ref']
967 commit_ids = _form['revisions'][::-1]
973 commit_ids = _form['revisions'][::-1]
968 common_ancestor_id = _form['common_ancestor']
974 common_ancestor_id = _form['common_ancestor']
969
975
970 # find the ancestor for this pr
976 # find the ancestor for this pr
971 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
977 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
972 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
978 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
973
979
974 if not (source_db_repo or target_db_repo):
980 if not (source_db_repo or target_db_repo):
975 h.flash(_('source_repo or target repo not found'), category='error')
981 h.flash(_('source_repo or target repo not found'), category='error')
976 raise HTTPFound(
982 raise HTTPFound(
977 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
983 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
978
984
979 # re-check permissions again here
985 # re-check permissions again here
980 # source_repo we must have read permissions
986 # source_repo we must have read permissions
981
987
982 source_perm = HasRepoPermissionAny(
988 source_perm = HasRepoPermissionAny(
983 'repository.read', 'repository.write', 'repository.admin')(
989 'repository.read', 'repository.write', 'repository.admin')(
984 source_db_repo.repo_name)
990 source_db_repo.repo_name)
985 if not source_perm:
991 if not source_perm:
986 msg = _('Not Enough permissions to source repo `{}`.'.format(
992 msg = _('Not Enough permissions to source repo `{}`.'.format(
987 source_db_repo.repo_name))
993 source_db_repo.repo_name))
988 h.flash(msg, category='error')
994 h.flash(msg, category='error')
989 # copy the args back to redirect
995 # copy the args back to redirect
990 org_query = self.request.GET.mixed()
996 org_query = self.request.GET.mixed()
991 raise HTTPFound(
997 raise HTTPFound(
992 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
998 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
993 _query=org_query))
999 _query=org_query))
994
1000
995 # target repo we must have read permissions, and also later on
1001 # target repo we must have read permissions, and also later on
996 # we want to check branch permissions here
1002 # we want to check branch permissions here
997 target_perm = HasRepoPermissionAny(
1003 target_perm = HasRepoPermissionAny(
998 'repository.read', 'repository.write', 'repository.admin')(
1004 'repository.read', 'repository.write', 'repository.admin')(
999 target_db_repo.repo_name)
1005 target_db_repo.repo_name)
1000 if not target_perm:
1006 if not target_perm:
1001 msg = _('Not Enough permissions to target repo `{}`.'.format(
1007 msg = _('Not Enough permissions to target repo `{}`.'.format(
1002 target_db_repo.repo_name))
1008 target_db_repo.repo_name))
1003 h.flash(msg, category='error')
1009 h.flash(msg, category='error')
1004 # copy the args back to redirect
1010 # copy the args back to redirect
1005 org_query = self.request.GET.mixed()
1011 org_query = self.request.GET.mixed()
1006 raise HTTPFound(
1012 raise HTTPFound(
1007 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1013 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1008 _query=org_query))
1014 _query=org_query))
1009
1015
1010 source_scm = source_db_repo.scm_instance()
1016 source_scm = source_db_repo.scm_instance()
1011 target_scm = target_db_repo.scm_instance()
1017 target_scm = target_db_repo.scm_instance()
1012
1018
1013 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1019 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1014 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1020 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1015
1021
1016 ancestor = source_scm.get_common_ancestor(
1022 ancestor = source_scm.get_common_ancestor(
1017 source_commit.raw_id, target_commit.raw_id, target_scm)
1023 source_commit.raw_id, target_commit.raw_id, target_scm)
1018
1024
1019 # recalculate target ref based on ancestor
1025 # recalculate target ref based on ancestor
1020 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1026 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1021 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1027 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1022
1028
1023 get_default_reviewers_data, validate_default_reviewers = \
1029 get_default_reviewers_data, validate_default_reviewers = \
1024 PullRequestModel().get_reviewer_functions()
1030 PullRequestModel().get_reviewer_functions()
1025
1031
1026 # recalculate reviewers logic, to make sure we can validate this
1032 # recalculate reviewers logic, to make sure we can validate this
1027 reviewer_rules = get_default_reviewers_data(
1033 reviewer_rules = get_default_reviewers_data(
1028 self._rhodecode_db_user, source_db_repo,
1034 self._rhodecode_db_user, source_db_repo,
1029 source_commit, target_db_repo, target_commit)
1035 source_commit, target_db_repo, target_commit)
1030
1036
1031 given_reviewers = _form['review_members']
1037 given_reviewers = _form['review_members']
1032 reviewers = validate_default_reviewers(
1038 reviewers = validate_default_reviewers(
1033 given_reviewers, reviewer_rules)
1039 given_reviewers, reviewer_rules)
1034
1040
1035 pullrequest_title = _form['pullrequest_title']
1041 pullrequest_title = _form['pullrequest_title']
1036 title_source_ref = source_ref.split(':', 2)[1]
1042 title_source_ref = source_ref.split(':', 2)[1]
1037 if not pullrequest_title:
1043 if not pullrequest_title:
1038 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1044 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1039 source=source_repo,
1045 source=source_repo,
1040 source_ref=title_source_ref,
1046 source_ref=title_source_ref,
1041 target=target_repo
1047 target=target_repo
1042 )
1048 )
1043
1049
1044 description = _form['pullrequest_desc']
1050 description = _form['pullrequest_desc']
1045 description_renderer = _form['description_renderer']
1051 description_renderer = _form['description_renderer']
1046
1052
1047 try:
1053 try:
1048 pull_request = PullRequestModel().create(
1054 pull_request = PullRequestModel().create(
1049 created_by=self._rhodecode_user.user_id,
1055 created_by=self._rhodecode_user.user_id,
1050 source_repo=source_repo,
1056 source_repo=source_repo,
1051 source_ref=source_ref,
1057 source_ref=source_ref,
1052 target_repo=target_repo,
1058 target_repo=target_repo,
1053 target_ref=target_ref,
1059 target_ref=target_ref,
1054 revisions=commit_ids,
1060 revisions=commit_ids,
1055 common_ancestor_id=common_ancestor_id,
1061 common_ancestor_id=common_ancestor_id,
1056 reviewers=reviewers,
1062 reviewers=reviewers,
1057 title=pullrequest_title,
1063 title=pullrequest_title,
1058 description=description,
1064 description=description,
1059 description_renderer=description_renderer,
1065 description_renderer=description_renderer,
1060 reviewer_data=reviewer_rules,
1066 reviewer_data=reviewer_rules,
1061 auth_user=self._rhodecode_user
1067 auth_user=self._rhodecode_user
1062 )
1068 )
1063 Session().commit()
1069 Session().commit()
1064
1070
1065 h.flash(_('Successfully opened new pull request'),
1071 h.flash(_('Successfully opened new pull request'),
1066 category='success')
1072 category='success')
1067 except Exception:
1073 except Exception:
1068 msg = _('Error occurred during creation of this pull request.')
1074 msg = _('Error occurred during creation of this pull request.')
1069 log.exception(msg)
1075 log.exception(msg)
1070 h.flash(msg, category='error')
1076 h.flash(msg, category='error')
1071
1077
1072 # copy the args back to redirect
1078 # copy the args back to redirect
1073 org_query = self.request.GET.mixed()
1079 org_query = self.request.GET.mixed()
1074 raise HTTPFound(
1080 raise HTTPFound(
1075 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1081 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1076 _query=org_query))
1082 _query=org_query))
1077
1083
1078 raise HTTPFound(
1084 raise HTTPFound(
1079 h.route_path('pullrequest_show', repo_name=target_repo,
1085 h.route_path('pullrequest_show', repo_name=target_repo,
1080 pull_request_id=pull_request.pull_request_id))
1086 pull_request_id=pull_request.pull_request_id))
1081
1087
1082 @LoginRequired()
1088 @LoginRequired()
1083 @NotAnonymous()
1089 @NotAnonymous()
1084 @HasRepoPermissionAnyDecorator(
1090 @HasRepoPermissionAnyDecorator(
1085 'repository.read', 'repository.write', 'repository.admin')
1091 'repository.read', 'repository.write', 'repository.admin')
1086 @CSRFRequired()
1092 @CSRFRequired()
1087 @view_config(
1093 @view_config(
1088 route_name='pullrequest_update', request_method='POST',
1094 route_name='pullrequest_update', request_method='POST',
1089 renderer='json_ext')
1095 renderer='json_ext')
1090 def pull_request_update(self):
1096 def pull_request_update(self):
1091 pull_request = PullRequest.get_or_404(
1097 pull_request = PullRequest.get_or_404(
1092 self.request.matchdict['pull_request_id'])
1098 self.request.matchdict['pull_request_id'])
1093 _ = self.request.translate
1099 _ = self.request.translate
1094
1100
1095 self.load_default_context()
1101 self.load_default_context()
1096 redirect_url = None
1102 redirect_url = None
1097
1103
1098 if pull_request.is_closed():
1104 if pull_request.is_closed():
1099 log.debug('update: forbidden because pull request is closed')
1105 log.debug('update: forbidden because pull request is closed')
1100 msg = _(u'Cannot update closed pull requests.')
1106 msg = _(u'Cannot update closed pull requests.')
1101 h.flash(msg, category='error')
1107 h.flash(msg, category='error')
1102 return {'response': True,
1108 return {'response': True,
1103 'redirect_url': redirect_url}
1109 'redirect_url': redirect_url}
1104
1110
1105 is_state_changing = pull_request.is_state_changing()
1111 is_state_changing = pull_request.is_state_changing()
1106
1112
1107 # only owner or admin can update it
1113 # only owner or admin can update it
1108 allowed_to_update = PullRequestModel().check_user_update(
1114 allowed_to_update = PullRequestModel().check_user_update(
1109 pull_request, self._rhodecode_user)
1115 pull_request, self._rhodecode_user)
1110 if allowed_to_update:
1116 if allowed_to_update:
1111 controls = peppercorn.parse(self.request.POST.items())
1117 controls = peppercorn.parse(self.request.POST.items())
1112 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1118 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1113
1119
1114 if 'review_members' in controls:
1120 if 'review_members' in controls:
1115 self._update_reviewers(
1121 self._update_reviewers(
1116 pull_request, controls['review_members'],
1122 pull_request, controls['review_members'],
1117 pull_request.reviewer_data)
1123 pull_request.reviewer_data)
1118 elif str2bool(self.request.POST.get('update_commits', 'false')):
1124 elif str2bool(self.request.POST.get('update_commits', 'false')):
1119 if is_state_changing:
1125 if is_state_changing:
1120 log.debug('commits update: forbidden because pull request is in state %s',
1126 log.debug('commits update: forbidden because pull request is in state %s',
1121 pull_request.pull_request_state)
1127 pull_request.pull_request_state)
1122 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1128 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1123 u'Current state is: `{}`').format(
1129 u'Current state is: `{}`').format(
1124 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1130 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1125 h.flash(msg, category='error')
1131 h.flash(msg, category='error')
1126 return {'response': True,
1132 return {'response': True,
1127 'redirect_url': redirect_url}
1133 'redirect_url': redirect_url}
1128
1134
1129 self._update_commits(pull_request)
1135 self._update_commits(pull_request)
1130 if force_refresh:
1136 if force_refresh:
1131 redirect_url = h.route_path(
1137 redirect_url = h.route_path(
1132 'pullrequest_show', repo_name=self.db_repo_name,
1138 'pullrequest_show', repo_name=self.db_repo_name,
1133 pull_request_id=pull_request.pull_request_id,
1139 pull_request_id=pull_request.pull_request_id,
1134 _query={"force_refresh": 1})
1140 _query={"force_refresh": 1})
1135 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1141 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1136 self._edit_pull_request(pull_request)
1142 self._edit_pull_request(pull_request)
1137 else:
1143 else:
1138 raise HTTPBadRequest()
1144 raise HTTPBadRequest()
1139
1145
1140 return {'response': True,
1146 return {'response': True,
1141 'redirect_url': redirect_url}
1147 'redirect_url': redirect_url}
1142 raise HTTPForbidden()
1148 raise HTTPForbidden()
1143
1149
1144 def _edit_pull_request(self, pull_request):
1150 def _edit_pull_request(self, pull_request):
1145 _ = self.request.translate
1151 _ = self.request.translate
1146
1152
1147 try:
1153 try:
1148 PullRequestModel().edit(
1154 PullRequestModel().edit(
1149 pull_request,
1155 pull_request,
1150 self.request.POST.get('title'),
1156 self.request.POST.get('title'),
1151 self.request.POST.get('description'),
1157 self.request.POST.get('description'),
1152 self.request.POST.get('description_renderer'),
1158 self.request.POST.get('description_renderer'),
1153 self._rhodecode_user)
1159 self._rhodecode_user)
1154 except ValueError:
1160 except ValueError:
1155 msg = _(u'Cannot update closed pull requests.')
1161 msg = _(u'Cannot update closed pull requests.')
1156 h.flash(msg, category='error')
1162 h.flash(msg, category='error')
1157 return
1163 return
1158 else:
1164 else:
1159 Session().commit()
1165 Session().commit()
1160
1166
1161 msg = _(u'Pull request title & description updated.')
1167 msg = _(u'Pull request title & description updated.')
1162 h.flash(msg, category='success')
1168 h.flash(msg, category='success')
1163 return
1169 return
1164
1170
1165 def _update_commits(self, pull_request):
1171 def _update_commits(self, pull_request):
1166 _ = self.request.translate
1172 _ = self.request.translate
1167
1173
1168 with pull_request.set_state(PullRequest.STATE_UPDATING):
1174 with pull_request.set_state(PullRequest.STATE_UPDATING):
1169 resp = PullRequestModel().update_commits(
1175 resp = PullRequestModel().update_commits(
1170 pull_request, self._rhodecode_db_user)
1176 pull_request, self._rhodecode_db_user)
1171
1177
1172 if resp.executed:
1178 if resp.executed:
1173
1179
1174 if resp.target_changed and resp.source_changed:
1180 if resp.target_changed and resp.source_changed:
1175 changed = 'target and source repositories'
1181 changed = 'target and source repositories'
1176 elif resp.target_changed and not resp.source_changed:
1182 elif resp.target_changed and not resp.source_changed:
1177 changed = 'target repository'
1183 changed = 'target repository'
1178 elif not resp.target_changed and resp.source_changed:
1184 elif not resp.target_changed and resp.source_changed:
1179 changed = 'source repository'
1185 changed = 'source repository'
1180 else:
1186 else:
1181 changed = 'nothing'
1187 changed = 'nothing'
1182
1188
1183 msg = _(u'Pull request updated to "{source_commit_id}" with '
1189 msg = _(u'Pull request updated to "{source_commit_id}" with '
1184 u'{count_added} added, {count_removed} removed commits. '
1190 u'{count_added} added, {count_removed} removed commits. '
1185 u'Source of changes: {change_source}')
1191 u'Source of changes: {change_source}')
1186 msg = msg.format(
1192 msg = msg.format(
1187 source_commit_id=pull_request.source_ref_parts.commit_id,
1193 source_commit_id=pull_request.source_ref_parts.commit_id,
1188 count_added=len(resp.changes.added),
1194 count_added=len(resp.changes.added),
1189 count_removed=len(resp.changes.removed),
1195 count_removed=len(resp.changes.removed),
1190 change_source=changed)
1196 change_source=changed)
1191 h.flash(msg, category='success')
1197 h.flash(msg, category='success')
1192
1198
1193 channel = '/repo${}$/pr/{}'.format(
1199 channel = '/repo${}$/pr/{}'.format(
1194 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1200 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1195 message = msg + (
1201 message = msg + (
1196 ' - <a onclick="window.location.reload()">'
1202 ' - <a onclick="window.location.reload()">'
1197 '<strong>{}</strong></a>'.format(_('Reload page')))
1203 '<strong>{}</strong></a>'.format(_('Reload page')))
1198 channelstream.post_message(
1204 channelstream.post_message(
1199 channel, message, self._rhodecode_user.username,
1205 channel, message, self._rhodecode_user.username,
1200 registry=self.request.registry)
1206 registry=self.request.registry)
1201 else:
1207 else:
1202 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1208 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1203 warning_reasons = [
1209 warning_reasons = [
1204 UpdateFailureReason.NO_CHANGE,
1210 UpdateFailureReason.NO_CHANGE,
1205 UpdateFailureReason.WRONG_REF_TYPE,
1211 UpdateFailureReason.WRONG_REF_TYPE,
1206 ]
1212 ]
1207 category = 'warning' if resp.reason in warning_reasons else 'error'
1213 category = 'warning' if resp.reason in warning_reasons else 'error'
1208 h.flash(msg, category=category)
1214 h.flash(msg, category=category)
1209
1215
1210 @LoginRequired()
1216 @LoginRequired()
1211 @NotAnonymous()
1217 @NotAnonymous()
1212 @HasRepoPermissionAnyDecorator(
1218 @HasRepoPermissionAnyDecorator(
1213 'repository.read', 'repository.write', 'repository.admin')
1219 'repository.read', 'repository.write', 'repository.admin')
1214 @CSRFRequired()
1220 @CSRFRequired()
1215 @view_config(
1221 @view_config(
1216 route_name='pullrequest_merge', request_method='POST',
1222 route_name='pullrequest_merge', request_method='POST',
1217 renderer='json_ext')
1223 renderer='json_ext')
1218 def pull_request_merge(self):
1224 def pull_request_merge(self):
1219 """
1225 """
1220 Merge will perform a server-side merge of the specified
1226 Merge will perform a server-side merge of the specified
1221 pull request, if the pull request is approved and mergeable.
1227 pull request, if the pull request is approved and mergeable.
1222 After successful merging, the pull request is automatically
1228 After successful merging, the pull request is automatically
1223 closed, with a relevant comment.
1229 closed, with a relevant comment.
1224 """
1230 """
1225 pull_request = PullRequest.get_or_404(
1231 pull_request = PullRequest.get_or_404(
1226 self.request.matchdict['pull_request_id'])
1232 self.request.matchdict['pull_request_id'])
1227 _ = self.request.translate
1233 _ = self.request.translate
1228
1234
1229 if pull_request.is_state_changing():
1235 if pull_request.is_state_changing():
1230 log.debug('show: forbidden because pull request is in state %s',
1236 log.debug('show: forbidden because pull request is in state %s',
1231 pull_request.pull_request_state)
1237 pull_request.pull_request_state)
1232 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1238 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1233 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1239 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1234 pull_request.pull_request_state)
1240 pull_request.pull_request_state)
1235 h.flash(msg, category='error')
1241 h.flash(msg, category='error')
1236 raise HTTPFound(
1242 raise HTTPFound(
1237 h.route_path('pullrequest_show',
1243 h.route_path('pullrequest_show',
1238 repo_name=pull_request.target_repo.repo_name,
1244 repo_name=pull_request.target_repo.repo_name,
1239 pull_request_id=pull_request.pull_request_id))
1245 pull_request_id=pull_request.pull_request_id))
1240
1246
1241 self.load_default_context()
1247 self.load_default_context()
1242
1248
1243 with pull_request.set_state(PullRequest.STATE_UPDATING):
1249 with pull_request.set_state(PullRequest.STATE_UPDATING):
1244 check = MergeCheck.validate(
1250 check = MergeCheck.validate(
1245 pull_request, auth_user=self._rhodecode_user,
1251 pull_request, auth_user=self._rhodecode_user,
1246 translator=self.request.translate)
1252 translator=self.request.translate)
1247 merge_possible = not check.failed
1253 merge_possible = not check.failed
1248
1254
1249 for err_type, error_msg in check.errors:
1255 for err_type, error_msg in check.errors:
1250 h.flash(error_msg, category=err_type)
1256 h.flash(error_msg, category=err_type)
1251
1257
1252 if merge_possible:
1258 if merge_possible:
1253 log.debug("Pre-conditions checked, trying to merge.")
1259 log.debug("Pre-conditions checked, trying to merge.")
1254 extras = vcs_operation_context(
1260 extras = vcs_operation_context(
1255 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1261 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1256 username=self._rhodecode_db_user.username, action='push',
1262 username=self._rhodecode_db_user.username, action='push',
1257 scm=pull_request.target_repo.repo_type)
1263 scm=pull_request.target_repo.repo_type)
1258 with pull_request.set_state(PullRequest.STATE_UPDATING):
1264 with pull_request.set_state(PullRequest.STATE_UPDATING):
1259 self._merge_pull_request(
1265 self._merge_pull_request(
1260 pull_request, self._rhodecode_db_user, extras)
1266 pull_request, self._rhodecode_db_user, extras)
1261 else:
1267 else:
1262 log.debug("Pre-conditions failed, NOT merging.")
1268 log.debug("Pre-conditions failed, NOT merging.")
1263
1269
1264 raise HTTPFound(
1270 raise HTTPFound(
1265 h.route_path('pullrequest_show',
1271 h.route_path('pullrequest_show',
1266 repo_name=pull_request.target_repo.repo_name,
1272 repo_name=pull_request.target_repo.repo_name,
1267 pull_request_id=pull_request.pull_request_id))
1273 pull_request_id=pull_request.pull_request_id))
1268
1274
1269 def _merge_pull_request(self, pull_request, user, extras):
1275 def _merge_pull_request(self, pull_request, user, extras):
1270 _ = self.request.translate
1276 _ = self.request.translate
1271 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1277 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1272
1278
1273 if merge_resp.executed:
1279 if merge_resp.executed:
1274 log.debug("The merge was successful, closing the pull request.")
1280 log.debug("The merge was successful, closing the pull request.")
1275 PullRequestModel().close_pull_request(
1281 PullRequestModel().close_pull_request(
1276 pull_request.pull_request_id, user)
1282 pull_request.pull_request_id, user)
1277 Session().commit()
1283 Session().commit()
1278 msg = _('Pull request was successfully merged and closed.')
1284 msg = _('Pull request was successfully merged and closed.')
1279 h.flash(msg, category='success')
1285 h.flash(msg, category='success')
1280 else:
1286 else:
1281 log.debug(
1287 log.debug(
1282 "The merge was not successful. Merge response: %s", merge_resp)
1288 "The merge was not successful. Merge response: %s", merge_resp)
1283 msg = merge_resp.merge_status_message
1289 msg = merge_resp.merge_status_message
1284 h.flash(msg, category='error')
1290 h.flash(msg, category='error')
1285
1291
1286 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1292 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1287 _ = self.request.translate
1293 _ = self.request.translate
1288
1294
1289 get_default_reviewers_data, validate_default_reviewers = \
1295 get_default_reviewers_data, validate_default_reviewers = \
1290 PullRequestModel().get_reviewer_functions()
1296 PullRequestModel().get_reviewer_functions()
1291
1297
1292 try:
1298 try:
1293 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1299 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1294 except ValueError as e:
1300 except ValueError as e:
1295 log.error('Reviewers Validation: {}'.format(e))
1301 log.error('Reviewers Validation: {}'.format(e))
1296 h.flash(e, category='error')
1302 h.flash(e, category='error')
1297 return
1303 return
1298
1304
1299 old_calculated_status = pull_request.calculated_review_status()
1305 old_calculated_status = pull_request.calculated_review_status()
1300 PullRequestModel().update_reviewers(
1306 PullRequestModel().update_reviewers(
1301 pull_request, reviewers, self._rhodecode_user)
1307 pull_request, reviewers, self._rhodecode_user)
1302 h.flash(_('Pull request reviewers updated.'), category='success')
1308 h.flash(_('Pull request reviewers updated.'), category='success')
1303 Session().commit()
1309 Session().commit()
1304
1310
1305 # trigger status changed if change in reviewers changes the status
1311 # trigger status changed if change in reviewers changes the status
1306 calculated_status = pull_request.calculated_review_status()
1312 calculated_status = pull_request.calculated_review_status()
1307 if old_calculated_status != calculated_status:
1313 if old_calculated_status != calculated_status:
1308 PullRequestModel().trigger_pull_request_hook(
1314 PullRequestModel().trigger_pull_request_hook(
1309 pull_request, self._rhodecode_user, 'review_status_change',
1315 pull_request, self._rhodecode_user, 'review_status_change',
1310 data={'status': calculated_status})
1316 data={'status': calculated_status})
1311
1317
1312 @LoginRequired()
1318 @LoginRequired()
1313 @NotAnonymous()
1319 @NotAnonymous()
1314 @HasRepoPermissionAnyDecorator(
1320 @HasRepoPermissionAnyDecorator(
1315 'repository.read', 'repository.write', 'repository.admin')
1321 'repository.read', 'repository.write', 'repository.admin')
1316 @CSRFRequired()
1322 @CSRFRequired()
1317 @view_config(
1323 @view_config(
1318 route_name='pullrequest_delete', request_method='POST',
1324 route_name='pullrequest_delete', request_method='POST',
1319 renderer='json_ext')
1325 renderer='json_ext')
1320 def pull_request_delete(self):
1326 def pull_request_delete(self):
1321 _ = self.request.translate
1327 _ = self.request.translate
1322
1328
1323 pull_request = PullRequest.get_or_404(
1329 pull_request = PullRequest.get_or_404(
1324 self.request.matchdict['pull_request_id'])
1330 self.request.matchdict['pull_request_id'])
1325 self.load_default_context()
1331 self.load_default_context()
1326
1332
1327 pr_closed = pull_request.is_closed()
1333 pr_closed = pull_request.is_closed()
1328 allowed_to_delete = PullRequestModel().check_user_delete(
1334 allowed_to_delete = PullRequestModel().check_user_delete(
1329 pull_request, self._rhodecode_user) and not pr_closed
1335 pull_request, self._rhodecode_user) and not pr_closed
1330
1336
1331 # only owner can delete it !
1337 # only owner can delete it !
1332 if allowed_to_delete:
1338 if allowed_to_delete:
1333 PullRequestModel().delete(pull_request, self._rhodecode_user)
1339 PullRequestModel().delete(pull_request, self._rhodecode_user)
1334 Session().commit()
1340 Session().commit()
1335 h.flash(_('Successfully deleted pull request'),
1341 h.flash(_('Successfully deleted pull request'),
1336 category='success')
1342 category='success')
1337 raise HTTPFound(h.route_path('pullrequest_show_all',
1343 raise HTTPFound(h.route_path('pullrequest_show_all',
1338 repo_name=self.db_repo_name))
1344 repo_name=self.db_repo_name))
1339
1345
1340 log.warning('user %s tried to delete pull request without access',
1346 log.warning('user %s tried to delete pull request without access',
1341 self._rhodecode_user)
1347 self._rhodecode_user)
1342 raise HTTPNotFound()
1348 raise HTTPNotFound()
1343
1349
1344 @LoginRequired()
1350 @LoginRequired()
1345 @NotAnonymous()
1351 @NotAnonymous()
1346 @HasRepoPermissionAnyDecorator(
1352 @HasRepoPermissionAnyDecorator(
1347 'repository.read', 'repository.write', 'repository.admin')
1353 'repository.read', 'repository.write', 'repository.admin')
1348 @CSRFRequired()
1354 @CSRFRequired()
1349 @view_config(
1355 @view_config(
1350 route_name='pullrequest_comment_create', request_method='POST',
1356 route_name='pullrequest_comment_create', request_method='POST',
1351 renderer='json_ext')
1357 renderer='json_ext')
1352 def pull_request_comment_create(self):
1358 def pull_request_comment_create(self):
1353 _ = self.request.translate
1359 _ = self.request.translate
1354
1360
1355 pull_request = PullRequest.get_or_404(
1361 pull_request = PullRequest.get_or_404(
1356 self.request.matchdict['pull_request_id'])
1362 self.request.matchdict['pull_request_id'])
1357 pull_request_id = pull_request.pull_request_id
1363 pull_request_id = pull_request.pull_request_id
1358
1364
1359 if pull_request.is_closed():
1365 if pull_request.is_closed():
1360 log.debug('comment: forbidden because pull request is closed')
1366 log.debug('comment: forbidden because pull request is closed')
1361 raise HTTPForbidden()
1367 raise HTTPForbidden()
1362
1368
1363 allowed_to_comment = PullRequestModel().check_user_comment(
1369 allowed_to_comment = PullRequestModel().check_user_comment(
1364 pull_request, self._rhodecode_user)
1370 pull_request, self._rhodecode_user)
1365 if not allowed_to_comment:
1371 if not allowed_to_comment:
1366 log.debug(
1372 log.debug(
1367 'comment: forbidden because pull request is from forbidden repo')
1373 'comment: forbidden because pull request is from forbidden repo')
1368 raise HTTPForbidden()
1374 raise HTTPForbidden()
1369
1375
1370 c = self.load_default_context()
1376 c = self.load_default_context()
1371
1377
1372 status = self.request.POST.get('changeset_status', None)
1378 status = self.request.POST.get('changeset_status', None)
1373 text = self.request.POST.get('text')
1379 text = self.request.POST.get('text')
1374 comment_type = self.request.POST.get('comment_type')
1380 comment_type = self.request.POST.get('comment_type')
1375 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1381 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1376 close_pull_request = self.request.POST.get('close_pull_request')
1382 close_pull_request = self.request.POST.get('close_pull_request')
1377
1383
1378 # the logic here should work like following, if we submit close
1384 # the logic here should work like following, if we submit close
1379 # pr comment, use `close_pull_request_with_comment` function
1385 # pr comment, use `close_pull_request_with_comment` function
1380 # else handle regular comment logic
1386 # else handle regular comment logic
1381
1387
1382 if close_pull_request:
1388 if close_pull_request:
1383 # only owner or admin or person with write permissions
1389 # only owner or admin or person with write permissions
1384 allowed_to_close = PullRequestModel().check_user_update(
1390 allowed_to_close = PullRequestModel().check_user_update(
1385 pull_request, self._rhodecode_user)
1391 pull_request, self._rhodecode_user)
1386 if not allowed_to_close:
1392 if not allowed_to_close:
1387 log.debug('comment: forbidden because not allowed to close '
1393 log.debug('comment: forbidden because not allowed to close '
1388 'pull request %s', pull_request_id)
1394 'pull request %s', pull_request_id)
1389 raise HTTPForbidden()
1395 raise HTTPForbidden()
1390
1396
1391 # This also triggers `review_status_change`
1397 # This also triggers `review_status_change`
1392 comment, status = PullRequestModel().close_pull_request_with_comment(
1398 comment, status = PullRequestModel().close_pull_request_with_comment(
1393 pull_request, self._rhodecode_user, self.db_repo, message=text,
1399 pull_request, self._rhodecode_user, self.db_repo, message=text,
1394 auth_user=self._rhodecode_user)
1400 auth_user=self._rhodecode_user)
1395 Session().flush()
1401 Session().flush()
1396
1402
1397 PullRequestModel().trigger_pull_request_hook(
1403 PullRequestModel().trigger_pull_request_hook(
1398 pull_request, self._rhodecode_user, 'comment',
1404 pull_request, self._rhodecode_user, 'comment',
1399 data={'comment': comment})
1405 data={'comment': comment})
1400
1406
1401 else:
1407 else:
1402 # regular comment case, could be inline, or one with status.
1408 # regular comment case, could be inline, or one with status.
1403 # for that one we check also permissions
1409 # for that one we check also permissions
1404
1410
1405 allowed_to_change_status = PullRequestModel().check_user_change_status(
1411 allowed_to_change_status = PullRequestModel().check_user_change_status(
1406 pull_request, self._rhodecode_user)
1412 pull_request, self._rhodecode_user)
1407
1413
1408 if status and allowed_to_change_status:
1414 if status and allowed_to_change_status:
1409 message = (_('Status change %(transition_icon)s %(status)s')
1415 message = (_('Status change %(transition_icon)s %(status)s')
1410 % {'transition_icon': '>',
1416 % {'transition_icon': '>',
1411 'status': ChangesetStatus.get_status_lbl(status)})
1417 'status': ChangesetStatus.get_status_lbl(status)})
1412 text = text or message
1418 text = text or message
1413
1419
1414 comment = CommentsModel().create(
1420 comment = CommentsModel().create(
1415 text=text,
1421 text=text,
1416 repo=self.db_repo.repo_id,
1422 repo=self.db_repo.repo_id,
1417 user=self._rhodecode_user.user_id,
1423 user=self._rhodecode_user.user_id,
1418 pull_request=pull_request,
1424 pull_request=pull_request,
1419 f_path=self.request.POST.get('f_path'),
1425 f_path=self.request.POST.get('f_path'),
1420 line_no=self.request.POST.get('line'),
1426 line_no=self.request.POST.get('line'),
1421 status_change=(ChangesetStatus.get_status_lbl(status)
1427 status_change=(ChangesetStatus.get_status_lbl(status)
1422 if status and allowed_to_change_status else None),
1428 if status and allowed_to_change_status else None),
1423 status_change_type=(status
1429 status_change_type=(status
1424 if status and allowed_to_change_status else None),
1430 if status and allowed_to_change_status else None),
1425 comment_type=comment_type,
1431 comment_type=comment_type,
1426 resolves_comment_id=resolves_comment_id,
1432 resolves_comment_id=resolves_comment_id,
1427 auth_user=self._rhodecode_user
1433 auth_user=self._rhodecode_user
1428 )
1434 )
1429
1435
1430 if allowed_to_change_status:
1436 if allowed_to_change_status:
1431 # calculate old status before we change it
1437 # calculate old status before we change it
1432 old_calculated_status = pull_request.calculated_review_status()
1438 old_calculated_status = pull_request.calculated_review_status()
1433
1439
1434 # get status if set !
1440 # get status if set !
1435 if status:
1441 if status:
1436 ChangesetStatusModel().set_status(
1442 ChangesetStatusModel().set_status(
1437 self.db_repo.repo_id,
1443 self.db_repo.repo_id,
1438 status,
1444 status,
1439 self._rhodecode_user.user_id,
1445 self._rhodecode_user.user_id,
1440 comment,
1446 comment,
1441 pull_request=pull_request
1447 pull_request=pull_request
1442 )
1448 )
1443
1449
1444 Session().flush()
1450 Session().flush()
1445 # this is somehow required to get access to some relationship
1451 # this is somehow required to get access to some relationship
1446 # loaded on comment
1452 # loaded on comment
1447 Session().refresh(comment)
1453 Session().refresh(comment)
1448
1454
1449 PullRequestModel().trigger_pull_request_hook(
1455 PullRequestModel().trigger_pull_request_hook(
1450 pull_request, self._rhodecode_user, 'comment',
1456 pull_request, self._rhodecode_user, 'comment',
1451 data={'comment': comment})
1457 data={'comment': comment})
1452
1458
1453 # we now calculate the status of pull request, and based on that
1459 # we now calculate the status of pull request, and based on that
1454 # calculation we set the commits status
1460 # calculation we set the commits status
1455 calculated_status = pull_request.calculated_review_status()
1461 calculated_status = pull_request.calculated_review_status()
1456 if old_calculated_status != calculated_status:
1462 if old_calculated_status != calculated_status:
1457 PullRequestModel().trigger_pull_request_hook(
1463 PullRequestModel().trigger_pull_request_hook(
1458 pull_request, self._rhodecode_user, 'review_status_change',
1464 pull_request, self._rhodecode_user, 'review_status_change',
1459 data={'status': calculated_status})
1465 data={'status': calculated_status})
1460
1466
1461 Session().commit()
1467 Session().commit()
1462
1468
1463 data = {
1469 data = {
1464 'target_id': h.safeid(h.safe_unicode(
1470 'target_id': h.safeid(h.safe_unicode(
1465 self.request.POST.get('f_path'))),
1471 self.request.POST.get('f_path'))),
1466 }
1472 }
1467 if comment:
1473 if comment:
1468 c.co = comment
1474 c.co = comment
1469 rendered_comment = render(
1475 rendered_comment = render(
1470 'rhodecode:templates/changeset/changeset_comment_block.mako',
1476 'rhodecode:templates/changeset/changeset_comment_block.mako',
1471 self._get_template_context(c), self.request)
1477 self._get_template_context(c), self.request)
1472
1478
1473 data.update(comment.get_dict())
1479 data.update(comment.get_dict())
1474 data.update({'rendered_text': rendered_comment})
1480 data.update({'rendered_text': rendered_comment})
1475
1481
1476 return data
1482 return data
1477
1483
1478 @LoginRequired()
1484 @LoginRequired()
1479 @NotAnonymous()
1485 @NotAnonymous()
1480 @HasRepoPermissionAnyDecorator(
1486 @HasRepoPermissionAnyDecorator(
1481 'repository.read', 'repository.write', 'repository.admin')
1487 'repository.read', 'repository.write', 'repository.admin')
1482 @CSRFRequired()
1488 @CSRFRequired()
1483 @view_config(
1489 @view_config(
1484 route_name='pullrequest_comment_delete', request_method='POST',
1490 route_name='pullrequest_comment_delete', request_method='POST',
1485 renderer='json_ext')
1491 renderer='json_ext')
1486 def pull_request_comment_delete(self):
1492 def pull_request_comment_delete(self):
1487 pull_request = PullRequest.get_or_404(
1493 pull_request = PullRequest.get_or_404(
1488 self.request.matchdict['pull_request_id'])
1494 self.request.matchdict['pull_request_id'])
1489
1495
1490 comment = ChangesetComment.get_or_404(
1496 comment = ChangesetComment.get_or_404(
1491 self.request.matchdict['comment_id'])
1497 self.request.matchdict['comment_id'])
1492 comment_id = comment.comment_id
1498 comment_id = comment.comment_id
1493
1499
1494 if comment.immutable:
1500 if comment.immutable:
1495 # don't allow deleting comments that are immutable
1501 # don't allow deleting comments that are immutable
1496 raise HTTPForbidden()
1502 raise HTTPForbidden()
1497
1503
1498 if pull_request.is_closed():
1504 if pull_request.is_closed():
1499 log.debug('comment: forbidden because pull request is closed')
1505 log.debug('comment: forbidden because pull request is closed')
1500 raise HTTPForbidden()
1506 raise HTTPForbidden()
1501
1507
1502 if not comment:
1508 if not comment:
1503 log.debug('Comment with id:%s not found, skipping', comment_id)
1509 log.debug('Comment with id:%s not found, skipping', comment_id)
1504 # comment already deleted in another call probably
1510 # comment already deleted in another call probably
1505 return True
1511 return True
1506
1512
1507 if comment.pull_request.is_closed():
1513 if comment.pull_request.is_closed():
1508 # don't allow deleting comments on closed pull request
1514 # don't allow deleting comments on closed pull request
1509 raise HTTPForbidden()
1515 raise HTTPForbidden()
1510
1516
1511 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1517 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1512 super_admin = h.HasPermissionAny('hg.admin')()
1518 super_admin = h.HasPermissionAny('hg.admin')()
1513 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1519 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1514 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1520 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1515 comment_repo_admin = is_repo_admin and is_repo_comment
1521 comment_repo_admin = is_repo_admin and is_repo_comment
1516
1522
1517 if super_admin or comment_owner or comment_repo_admin:
1523 if super_admin or comment_owner or comment_repo_admin:
1518 old_calculated_status = comment.pull_request.calculated_review_status()
1524 old_calculated_status = comment.pull_request.calculated_review_status()
1519 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1525 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1520 Session().commit()
1526 Session().commit()
1521 calculated_status = comment.pull_request.calculated_review_status()
1527 calculated_status = comment.pull_request.calculated_review_status()
1522 if old_calculated_status != calculated_status:
1528 if old_calculated_status != calculated_status:
1523 PullRequestModel().trigger_pull_request_hook(
1529 PullRequestModel().trigger_pull_request_hook(
1524 comment.pull_request, self._rhodecode_user, 'review_status_change',
1530 comment.pull_request, self._rhodecode_user, 'review_status_change',
1525 data={'status': calculated_status})
1531 data={'status': calculated_status})
1526 return True
1532 return True
1527 else:
1533 else:
1528 log.warning('No permissions for user %s to delete comment_id: %s',
1534 log.warning('No permissions for user %s to delete comment_id: %s',
1529 self._rhodecode_db_user, comment_id)
1535 self._rhodecode_db_user, comment_id)
1530 raise HTTPNotFound()
1536 raise HTTPNotFound()
1531
1537
1532 @LoginRequired()
1538 @LoginRequired()
1533 @NotAnonymous()
1539 @NotAnonymous()
1534 @HasRepoPermissionAnyDecorator(
1540 @HasRepoPermissionAnyDecorator(
1535 'repository.read', 'repository.write', 'repository.admin')
1541 'repository.read', 'repository.write', 'repository.admin')
1536 @CSRFRequired()
1542 @CSRFRequired()
1537 @view_config(
1543 @view_config(
1538 route_name='pullrequest_comment_edit', request_method='POST',
1544 route_name='pullrequest_comment_edit', request_method='POST',
1539 renderer='json_ext')
1545 renderer='json_ext')
1540 def pull_request_comment_edit(self):
1546 def pull_request_comment_edit(self):
1541 self.load_default_context()
1547 self.load_default_context()
1542
1548
1543 pull_request = PullRequest.get_or_404(
1549 pull_request = PullRequest.get_or_404(
1544 self.request.matchdict['pull_request_id']
1550 self.request.matchdict['pull_request_id']
1545 )
1551 )
1546 comment = ChangesetComment.get_or_404(
1552 comment = ChangesetComment.get_or_404(
1547 self.request.matchdict['comment_id']
1553 self.request.matchdict['comment_id']
1548 )
1554 )
1549 comment_id = comment.comment_id
1555 comment_id = comment.comment_id
1550
1556
1551 if comment.immutable:
1557 if comment.immutable:
1552 # don't allow deleting comments that are immutable
1558 # don't allow deleting comments that are immutable
1553 raise HTTPForbidden()
1559 raise HTTPForbidden()
1554
1560
1555 if pull_request.is_closed():
1561 if pull_request.is_closed():
1556 log.debug('comment: forbidden because pull request is closed')
1562 log.debug('comment: forbidden because pull request is closed')
1557 raise HTTPForbidden()
1563 raise HTTPForbidden()
1558
1564
1559 if not comment:
1565 if not comment:
1560 log.debug('Comment with id:%s not found, skipping', comment_id)
1566 log.debug('Comment with id:%s not found, skipping', comment_id)
1561 # comment already deleted in another call probably
1567 # comment already deleted in another call probably
1562 return True
1568 return True
1563
1569
1564 if comment.pull_request.is_closed():
1570 if comment.pull_request.is_closed():
1565 # don't allow deleting comments on closed pull request
1571 # don't allow deleting comments on closed pull request
1566 raise HTTPForbidden()
1572 raise HTTPForbidden()
1567
1573
1568 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1574 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1569 super_admin = h.HasPermissionAny('hg.admin')()
1575 super_admin = h.HasPermissionAny('hg.admin')()
1570 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1576 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1571 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1577 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1572 comment_repo_admin = is_repo_admin and is_repo_comment
1578 comment_repo_admin = is_repo_admin and is_repo_comment
1573
1579
1574 if super_admin or comment_owner or comment_repo_admin:
1580 if super_admin or comment_owner or comment_repo_admin:
1575 text = self.request.POST.get('text')
1581 text = self.request.POST.get('text')
1576 version = self.request.POST.get('version')
1582 version = self.request.POST.get('version')
1577 if text == comment.text:
1583 if text == comment.text:
1578 log.warning(
1584 log.warning(
1579 'Comment(PR): '
1585 'Comment(PR): '
1580 'Trying to create new version '
1586 'Trying to create new version '
1581 'with the same comment body {}'.format(
1587 'with the same comment body {}'.format(
1582 comment_id,
1588 comment_id,
1583 )
1589 )
1584 )
1590 )
1585 raise HTTPNotFound()
1591 raise HTTPNotFound()
1586
1592
1587 if version.isdigit():
1593 if version.isdigit():
1588 version = int(version)
1594 version = int(version)
1589 else:
1595 else:
1590 log.warning(
1596 log.warning(
1591 'Comment(PR): Wrong version type {} {} '
1597 'Comment(PR): Wrong version type {} {} '
1592 'for comment {}'.format(
1598 'for comment {}'.format(
1593 version,
1599 version,
1594 type(version),
1600 type(version),
1595 comment_id,
1601 comment_id,
1596 )
1602 )
1597 )
1603 )
1598 raise HTTPNotFound()
1604 raise HTTPNotFound()
1599
1605
1600 try:
1606 try:
1601 comment_history = CommentsModel().edit(
1607 comment_history = CommentsModel().edit(
1602 comment_id=comment_id,
1608 comment_id=comment_id,
1603 text=text,
1609 text=text,
1604 auth_user=self._rhodecode_user,
1610 auth_user=self._rhodecode_user,
1605 version=version,
1611 version=version,
1606 )
1612 )
1607 except CommentVersionMismatch:
1613 except CommentVersionMismatch:
1608 raise HTTPConflict()
1614 raise HTTPConflict()
1609
1615
1610 if not comment_history:
1616 if not comment_history:
1611 raise HTTPNotFound()
1617 raise HTTPNotFound()
1612
1618
1613 Session().commit()
1619 Session().commit()
1614
1620
1615 PullRequestModel().trigger_pull_request_hook(
1621 PullRequestModel().trigger_pull_request_hook(
1616 pull_request, self._rhodecode_user, 'comment_edit',
1622 pull_request, self._rhodecode_user, 'comment_edit',
1617 data={'comment': comment})
1623 data={'comment': comment})
1618
1624
1619 return {
1625 return {
1620 'comment_history_id': comment_history.comment_history_id,
1626 'comment_history_id': comment_history.comment_history_id,
1621 'comment_id': comment.comment_id,
1627 'comment_id': comment.comment_id,
1622 'comment_version': comment_history.version,
1628 'comment_version': comment_history.version,
1623 'comment_author_username': comment_history.author.username,
1629 'comment_author_username': comment_history.author.username,
1624 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1630 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1625 'comment_created_on': h.age_component(comment_history.created_on,
1631 'comment_created_on': h.age_component(comment_history.created_on,
1626 time_is_local=True),
1632 time_is_local=True),
1627 }
1633 }
1628 else:
1634 else:
1629 log.warning('No permissions for user %s to edit comment_id: %s',
1635 log.warning('No permissions for user %s to edit comment_id: %s',
1630 self._rhodecode_db_user, comment_id)
1636 self._rhodecode_db_user, comment_id)
1631 raise HTTPNotFound()
1637 raise HTTPNotFound()
@@ -1,162 +1,162 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 import sys
22 import sys
23 import json
23 import json
24 import logging
24 import logging
25
25
26 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
26 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
27 from rhodecode.lib.vcs.conf import settings as vcs_settings
27 from rhodecode.lib.vcs.conf import settings as vcs_settings
28 from rhodecode.model.scm import ScmModel
28 from rhodecode.model.scm import ScmModel
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 class VcsServer(object):
33 class VcsServer(object):
34 _path = None # set executable path for hg/git/svn binary
34 _path = None # set executable path for hg/git/svn binary
35 backend = None # set in child classes
35 backend = None # set in child classes
36 tunnel = None # subprocess handling tunnel
36 tunnel = None # subprocess handling tunnel
37 write_perms = ['repository.admin', 'repository.write']
37 write_perms = ['repository.admin', 'repository.write']
38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
39
39
40 def __init__(self, user, user_permissions, config, env):
40 def __init__(self, user, user_permissions, config, env):
41 self.user = user
41 self.user = user
42 self.user_permissions = user_permissions
42 self.user_permissions = user_permissions
43 self.config = config
43 self.config = config
44 self.env = env
44 self.env = env
45 self.stdin = sys.stdin
45 self.stdin = sys.stdin
46
46
47 self.repo_name = None
47 self.repo_name = None
48 self.repo_mode = None
48 self.repo_mode = None
49 self.store = ''
49 self.store = ''
50 self.ini_path = ''
50 self.ini_path = ''
51
51
52 def _invalidate_cache(self, repo_name):
52 def _invalidate_cache(self, repo_name):
53 """
53 """
54 Set's cache for this repository for invalidation on next access
54 Set's cache for this repository for invalidation on next access
55
55
56 :param repo_name: full repo name, also a cache key
56 :param repo_name: full repo name, also a cache key
57 """
57 """
58 ScmModel().mark_for_invalidation(repo_name)
58 ScmModel().mark_for_invalidation(repo_name)
59
59
60 def has_write_perm(self):
60 def has_write_perm(self):
61 permission = self.user_permissions.get(self.repo_name)
61 permission = self.user_permissions.get(self.repo_name)
62 if permission in ['repository.write', 'repository.admin']:
62 if permission in ['repository.write', 'repository.admin']:
63 return True
63 return True
64
64
65 return False
65 return False
66
66
67 def _check_permissions(self, action):
67 def _check_permissions(self, action):
68 permission = self.user_permissions.get(self.repo_name)
68 permission = self.user_permissions.get(self.repo_name)
69 log.debug('permission for %s on %s are: %s',
69 log.debug('permission for %s on %s are: %s',
70 self.user, self.repo_name, permission)
70 self.user, self.repo_name, permission)
71
71
72 if not permission:
72 if not permission:
73 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
73 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
74 self.user, self.repo_name)
74 self.user, self.repo_name)
75 return -2
75 return -2
76
76
77 if action == 'pull':
77 if action == 'pull':
78 if permission in self.read_perms:
78 if permission in self.read_perms:
79 log.info(
79 log.info(
80 'READ Permissions for User "%s" detected to repo "%s"!',
80 'READ Permissions for User "%s" detected to repo "%s"!',
81 self.user, self.repo_name)
81 self.user, self.repo_name)
82 return 0
82 return 0
83 else:
83 else:
84 if permission in self.write_perms:
84 if permission in self.write_perms:
85 log.info(
85 log.info(
86 'WRITE+ Permissions for User "%s" detected to repo "%s"!',
86 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
87 self.user, self.repo_name)
87 self.user, self.repo_name)
88 return 0
88 return 0
89
89
90 log.error('Cannot properly fetch or verify user `%s` permissions. '
90 log.error('Cannot properly fetch or verify user `%s` permissions. '
91 'Permissions: %s, vcs action: %s',
91 'Permissions: %s, vcs action: %s',
92 self.user, permission, action)
92 self.user, permission, action)
93 return -2
93 return -2
94
94
95 def update_environment(self, action, extras=None):
95 def update_environment(self, action, extras=None):
96
96
97 scm_data = {
97 scm_data = {
98 'ip': os.environ['SSH_CLIENT'].split()[0],
98 'ip': os.environ['SSH_CLIENT'].split()[0],
99 'username': self.user.username,
99 'username': self.user.username,
100 'user_id': self.user.user_id,
100 'user_id': self.user.user_id,
101 'action': action,
101 'action': action,
102 'repository': self.repo_name,
102 'repository': self.repo_name,
103 'scm': self.backend,
103 'scm': self.backend,
104 'config': self.ini_path,
104 'config': self.ini_path,
105 'repo_store': self.store,
105 'repo_store': self.store,
106 'make_lock': None,
106 'make_lock': None,
107 'locked_by': [None, None],
107 'locked_by': [None, None],
108 'server_url': None,
108 'server_url': None,
109 'user_agent': 'ssh-user-agent',
109 'user_agent': 'ssh-user-agent',
110 'hooks': ['push', 'pull'],
110 'hooks': ['push', 'pull'],
111 'hooks_module': 'rhodecode.lib.hooks_daemon',
111 'hooks_module': 'rhodecode.lib.hooks_daemon',
112 'is_shadow_repo': False,
112 'is_shadow_repo': False,
113 'detect_force_push': False,
113 'detect_force_push': False,
114 'check_branch_perms': False,
114 'check_branch_perms': False,
115
115
116 'SSH': True,
116 'SSH': True,
117 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
117 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
118 }
118 }
119 if extras:
119 if extras:
120 scm_data.update(extras)
120 scm_data.update(extras)
121 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
121 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
122
122
123 def get_root_store(self):
123 def get_root_store(self):
124 root_store = self.store
124 root_store = self.store
125 if not root_store.endswith('/'):
125 if not root_store.endswith('/'):
126 # always append trailing slash
126 # always append trailing slash
127 root_store = root_store + '/'
127 root_store = root_store + '/'
128 return root_store
128 return root_store
129
129
130 def _handle_tunnel(self, extras):
130 def _handle_tunnel(self, extras):
131 # pre-auth
131 # pre-auth
132 action = 'pull'
132 action = 'pull'
133 exit_code = self._check_permissions(action)
133 exit_code = self._check_permissions(action)
134 if exit_code:
134 if exit_code:
135 return exit_code, False
135 return exit_code, False
136
136
137 req = self.env['request']
137 req = self.env['request']
138 server_url = req.host_url + req.script_name
138 server_url = req.host_url + req.script_name
139 extras['server_url'] = server_url
139 extras['server_url'] = server_url
140
140
141 log.debug('Using %s binaries from path %s', self.backend, self._path)
141 log.debug('Using %s binaries from path %s', self.backend, self._path)
142 exit_code = self.tunnel.run(extras)
142 exit_code = self.tunnel.run(extras)
143
143
144 return exit_code, action == "push"
144 return exit_code, action == "push"
145
145
146 def run(self, tunnel_extras=None):
146 def run(self, tunnel_extras=None):
147 tunnel_extras = tunnel_extras or {}
147 tunnel_extras = tunnel_extras or {}
148 extras = {}
148 extras = {}
149 extras.update(tunnel_extras)
149 extras.update(tunnel_extras)
150
150
151 callback_daemon, extras = prepare_callback_daemon(
151 callback_daemon, extras = prepare_callback_daemon(
152 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
152 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
153 host=vcs_settings.HOOKS_HOST,
153 host=vcs_settings.HOOKS_HOST,
154 use_direct_calls=False)
154 use_direct_calls=False)
155
155
156 with callback_daemon:
156 with callback_daemon:
157 try:
157 try:
158 return self._handle_tunnel(extras)
158 return self._handle_tunnel(extras)
159 finally:
159 finally:
160 log.debug('Running cleanup with cache invalidation')
160 log.debug('Running cleanup with cache invalidation')
161 if self.repo_name:
161 if self.repo_name:
162 self._invalidate_cache(self.repo_name)
162 self._invalidate_cache(self.repo_name)
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now