##// END OF EJS Templates
release: merge back stable branch into default
marcink -
r4390:4a659147 merge default
parent child Browse files
Show More

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

@@ -0,0 +1,41 b''
1 |RCE| 4.19.1 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-05-25
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18
19
20 Security
21 ^^^^^^^^
22
23 - issue-tracker: fixed XSS inside the newly generated description fields.
24
25
26 Performance
27 ^^^^^^^^^^^
28
29 -
30
31
32 Fixes
33 ^^^^^
34
35 - HTTP: fixed headers problems in the application.
36
37
38 Upgrade notes
39 ^^^^^^^^^^^^^
40
41 - Un-scheduled release addressing problems in 4.19.X releases.
@@ -0,0 +1,52 b''
1 |RCE| 4.19.2 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-06-10
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13 - Files: landing refs will be the default for files view, resulting in names of branches instead of hashes.
14 This fixes some problems reported with navigation, and also SVN.
15 - Diffs: expose per-file comment counts.
16
17
18 General
19 ^^^^^^^
20
21 - Navigation: explicitly link to summary page for summary link.
22 - Main Page: simplify footer, and expose docs link.
23 - Docs: added mention how to change default integration templates.
24 - Files: use ref names in the url, and make usage of default landing refs.
25 - Files: report the name of missing commit.
26 - Sweet alerts: reduced font size.
27
28
29 Security
30 ^^^^^^^^
31
32 - Branch permissions: fix XSS on branch permissions adding screen.
33
34
35 Performance
36 ^^^^^^^^^^^
37
38
39
40 Fixes
41 ^^^^^
42
43 - Emails: improved styling, and fixed problems with some email clients rendering.
44 - Files: fixed label for copy-path action.
45 - Files: use a common function to handle url-by-refs, and fix landing refs for SVN.
46
47
48 Upgrade notes
49 ^^^^^^^^^^^^^
50
51 - Un-scheduled release addressing problems in 4.19.X releases.
52 It brings some added features that weren't ready for 4.19.0.
@@ -1,64 +1,67 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
66 c0c65acd73914bf4368222d510afe1161ab8c07c v4.19.1
67 7ac623a4a2405917e2af660d645ded662011e40d v4.19.2
@@ -1,27 +1,34 b''
1 .. _integrations-jira:
1 .. _integrations-jira:
2
2
3 JIRA integration
3 JIRA integration
4 ================
4 ================
5
5
6 .. important::
6 .. important::
7
7
8 JIRA integration is only available in |RCEE|.
8 JIRA integration is only available in |RCEE|.
9
9
10
10
11 .. important::
11 .. important::
12
12
13 In order to make issue numbers clickable in commit messages, see the
13 In order to make issue numbers clickable in commit messages, see the
14 :ref:`rhodecode-issue-trackers-ref` section. The JIRA integration
14 :ref:`rhodecode-issue-trackers-ref` section. The JIRA integration
15 only deals with altering JIRA issues.
15 only deals with altering JIRA issues.
16
16
17
17
18 The JIRA integration allows you to reference and change issue statuses in
18 The JIRA integration allows you to reference and change issue statuses in
19 JIRA directly from commit messages using commit message patterns such as
19 JIRA directly from commit messages using commit message patterns such as
20 ``fixes #JIRA-235`` in order to change the status of issue JIRA-235 to
20 ``fixes #JIRA-235`` in order to change the status of issue JIRA-235 to
21 eg. "Resolved".
21 eg. "Resolved".
22
22
23 In order to apply a status to a JIRA issue, it is necessary to find the
23 In order to apply a status to a JIRA issue, it is necessary to find the
24 transition status id in the *Workflow* section of JIRA.
24 transition status id in the *Workflow* section of JIRA.
25
25
26 Once you have the transition status id, you can create a JIRA integration
26 Once you have the transition status id, you can create a JIRA integration
27 as outlined in :ref:`creating-integrations`.
27 as outlined in :ref:`creating-integrations`.
28
29
30 .. note::
31
32 There's an option to configure integration templates.
33 Please see :ref:`integrations-rcextensions` section.
34 rcextensions examples are here: https://code.rhodecode.com/rhodecode-enterprise-ce/files/default/rhodecode/config/rcextensions/examples/custom_integration_templates.py
@@ -1,28 +1,34 b''
1 .. _integrations-redmine:
1 .. _integrations-redmine:
2
2
3 Redmine integration
3 Redmine integration
4 ===================
4 ===================
5
5
6 .. important::
6 .. important::
7
7
8 Redmine integration is only available in |RCEE|.
8 Redmine integration is only available in |RCEE|.
9
9
10
10
11 .. important::
11 .. important::
12
12
13 In order to make issue numbers clickable in commit messages, see the section
13 In order to make issue numbers clickable in commit messages, see the section
14 :ref:`rhodecode-issue-trackers-ref`. Redmine integration is specifically for
14 :ref:`rhodecode-issue-trackers-ref`. Redmine integration is specifically for
15 altering Redmine issues.
15 altering Redmine issues.
16
16
17
17
18 Redmine integration allows you to reference and change issue statuses in
18 Redmine integration allows you to reference and change issue statuses in
19 Redmine directly from commit messages, using commit message patterns such as
19 Redmine directly from commit messages, using commit message patterns such as
20 ``fixes #235`` in order to change the status of issue 235 to eg. "Resolved".
20 ``fixes #235`` in order to change the status of issue 235 to eg. "Resolved".
21
21
22 To set a Redmine integration up, it is first necessary to obtain a Redmine API
22 To set a Redmine integration up, it is first necessary to obtain a Redmine API
23 key. This can be found under *My Account* in the Redmine application.
23 key. This can be found under *My Account* in the Redmine application.
24 You may have to enable API Access in Redmine settings if it is not already
24 You may have to enable API Access in Redmine settings if it is not already
25 available.
25 available.
26
26
27 Once you have the API key, create a Redmine integration as outlined in
27 Once you have the API key, create a Redmine integration as outlined in
28 :ref:`creating-integrations`.
28 :ref:`creating-integrations`.
29
30
31 .. note::
32
33 There's an option to configure integration templates. Please see :ref:`integrations-rcextensions` section.
34 rcextensions examples are here: https://code.rhodecode.com/rhodecode-enterprise-ce/files/default/rhodecode/config/rcextensions/examples/custom_integration_templates.py No newline at end of file
@@ -1,141 +1,143 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.19.2.rst
13 release-notes-4.19.1.rst
12 release-notes-4.19.0.rst
14 release-notes-4.19.0.rst
13 release-notes-4.18.3.rst
15 release-notes-4.18.3.rst
14 release-notes-4.18.2.rst
16 release-notes-4.18.2.rst
15 release-notes-4.18.1.rst
17 release-notes-4.18.1.rst
16 release-notes-4.18.0.rst
18 release-notes-4.18.0.rst
17 release-notes-4.17.4.rst
19 release-notes-4.17.4.rst
18 release-notes-4.17.3.rst
20 release-notes-4.17.3.rst
19 release-notes-4.17.2.rst
21 release-notes-4.17.2.rst
20 release-notes-4.17.1.rst
22 release-notes-4.17.1.rst
21 release-notes-4.17.0.rst
23 release-notes-4.17.0.rst
22 release-notes-4.16.2.rst
24 release-notes-4.16.2.rst
23 release-notes-4.16.1.rst
25 release-notes-4.16.1.rst
24 release-notes-4.16.0.rst
26 release-notes-4.16.0.rst
25 release-notes-4.15.2.rst
27 release-notes-4.15.2.rst
26 release-notes-4.15.1.rst
28 release-notes-4.15.1.rst
27 release-notes-4.15.0.rst
29 release-notes-4.15.0.rst
28 release-notes-4.14.1.rst
30 release-notes-4.14.1.rst
29 release-notes-4.14.0.rst
31 release-notes-4.14.0.rst
30 release-notes-4.13.3.rst
32 release-notes-4.13.3.rst
31 release-notes-4.13.2.rst
33 release-notes-4.13.2.rst
32 release-notes-4.13.1.rst
34 release-notes-4.13.1.rst
33 release-notes-4.13.0.rst
35 release-notes-4.13.0.rst
34 release-notes-4.12.4.rst
36 release-notes-4.12.4.rst
35 release-notes-4.12.3.rst
37 release-notes-4.12.3.rst
36 release-notes-4.12.2.rst
38 release-notes-4.12.2.rst
37 release-notes-4.12.1.rst
39 release-notes-4.12.1.rst
38 release-notes-4.12.0.rst
40 release-notes-4.12.0.rst
39 release-notes-4.11.6.rst
41 release-notes-4.11.6.rst
40 release-notes-4.11.5.rst
42 release-notes-4.11.5.rst
41 release-notes-4.11.4.rst
43 release-notes-4.11.4.rst
42 release-notes-4.11.3.rst
44 release-notes-4.11.3.rst
43 release-notes-4.11.2.rst
45 release-notes-4.11.2.rst
44 release-notes-4.11.1.rst
46 release-notes-4.11.1.rst
45 release-notes-4.11.0.rst
47 release-notes-4.11.0.rst
46 release-notes-4.10.6.rst
48 release-notes-4.10.6.rst
47 release-notes-4.10.5.rst
49 release-notes-4.10.5.rst
48 release-notes-4.10.4.rst
50 release-notes-4.10.4.rst
49 release-notes-4.10.3.rst
51 release-notes-4.10.3.rst
50 release-notes-4.10.2.rst
52 release-notes-4.10.2.rst
51 release-notes-4.10.1.rst
53 release-notes-4.10.1.rst
52 release-notes-4.10.0.rst
54 release-notes-4.10.0.rst
53 release-notes-4.9.1.rst
55 release-notes-4.9.1.rst
54 release-notes-4.9.0.rst
56 release-notes-4.9.0.rst
55 release-notes-4.8.0.rst
57 release-notes-4.8.0.rst
56 release-notes-4.7.2.rst
58 release-notes-4.7.2.rst
57 release-notes-4.7.1.rst
59 release-notes-4.7.1.rst
58 release-notes-4.7.0.rst
60 release-notes-4.7.0.rst
59 release-notes-4.6.1.rst
61 release-notes-4.6.1.rst
60 release-notes-4.6.0.rst
62 release-notes-4.6.0.rst
61 release-notes-4.5.2.rst
63 release-notes-4.5.2.rst
62 release-notes-4.5.1.rst
64 release-notes-4.5.1.rst
63 release-notes-4.5.0.rst
65 release-notes-4.5.0.rst
64 release-notes-4.4.2.rst
66 release-notes-4.4.2.rst
65 release-notes-4.4.1.rst
67 release-notes-4.4.1.rst
66 release-notes-4.4.0.rst
68 release-notes-4.4.0.rst
67 release-notes-4.3.1.rst
69 release-notes-4.3.1.rst
68 release-notes-4.3.0.rst
70 release-notes-4.3.0.rst
69 release-notes-4.2.1.rst
71 release-notes-4.2.1.rst
70 release-notes-4.2.0.rst
72 release-notes-4.2.0.rst
71 release-notes-4.1.2.rst
73 release-notes-4.1.2.rst
72 release-notes-4.1.1.rst
74 release-notes-4.1.1.rst
73 release-notes-4.1.0.rst
75 release-notes-4.1.0.rst
74 release-notes-4.0.1.rst
76 release-notes-4.0.1.rst
75 release-notes-4.0.0.rst
77 release-notes-4.0.0.rst
76
78
77 |RCE| 3.x Versions
79 |RCE| 3.x Versions
78 ------------------
80 ------------------
79
81
80 .. toctree::
82 .. toctree::
81 :maxdepth: 1
83 :maxdepth: 1
82
84
83 release-notes-3.8.4.rst
85 release-notes-3.8.4.rst
84 release-notes-3.8.3.rst
86 release-notes-3.8.3.rst
85 release-notes-3.8.2.rst
87 release-notes-3.8.2.rst
86 release-notes-3.8.1.rst
88 release-notes-3.8.1.rst
87 release-notes-3.8.0.rst
89 release-notes-3.8.0.rst
88 release-notes-3.7.1.rst
90 release-notes-3.7.1.rst
89 release-notes-3.7.0.rst
91 release-notes-3.7.0.rst
90 release-notes-3.6.1.rst
92 release-notes-3.6.1.rst
91 release-notes-3.6.0.rst
93 release-notes-3.6.0.rst
92 release-notes-3.5.2.rst
94 release-notes-3.5.2.rst
93 release-notes-3.5.1.rst
95 release-notes-3.5.1.rst
94 release-notes-3.5.0.rst
96 release-notes-3.5.0.rst
95 release-notes-3.4.1.rst
97 release-notes-3.4.1.rst
96 release-notes-3.4.0.rst
98 release-notes-3.4.0.rst
97 release-notes-3.3.4.rst
99 release-notes-3.3.4.rst
98 release-notes-3.3.3.rst
100 release-notes-3.3.3.rst
99 release-notes-3.3.2.rst
101 release-notes-3.3.2.rst
100 release-notes-3.3.1.rst
102 release-notes-3.3.1.rst
101 release-notes-3.3.0.rst
103 release-notes-3.3.0.rst
102 release-notes-3.2.3.rst
104 release-notes-3.2.3.rst
103 release-notes-3.2.2.rst
105 release-notes-3.2.2.rst
104 release-notes-3.2.1.rst
106 release-notes-3.2.1.rst
105 release-notes-3.2.0.rst
107 release-notes-3.2.0.rst
106 release-notes-3.1.1.rst
108 release-notes-3.1.1.rst
107 release-notes-3.1.0.rst
109 release-notes-3.1.0.rst
108 release-notes-3.0.2.rst
110 release-notes-3.0.2.rst
109 release-notes-3.0.1.rst
111 release-notes-3.0.1.rst
110 release-notes-3.0.0.rst
112 release-notes-3.0.0.rst
111
113
112 |RCE| 2.x Versions
114 |RCE| 2.x Versions
113 ------------------
115 ------------------
114
116
115 .. toctree::
117 .. toctree::
116 :maxdepth: 1
118 :maxdepth: 1
117
119
118 release-notes-2.2.8.rst
120 release-notes-2.2.8.rst
119 release-notes-2.2.7.rst
121 release-notes-2.2.7.rst
120 release-notes-2.2.6.rst
122 release-notes-2.2.6.rst
121 release-notes-2.2.5.rst
123 release-notes-2.2.5.rst
122 release-notes-2.2.4.rst
124 release-notes-2.2.4.rst
123 release-notes-2.2.3.rst
125 release-notes-2.2.3.rst
124 release-notes-2.2.2.rst
126 release-notes-2.2.2.rst
125 release-notes-2.2.1.rst
127 release-notes-2.2.1.rst
126 release-notes-2.2.0.rst
128 release-notes-2.2.0.rst
127 release-notes-2.1.0.rst
129 release-notes-2.1.0.rst
128 release-notes-2.0.2.rst
130 release-notes-2.0.2.rst
129 release-notes-2.0.1.rst
131 release-notes-2.0.1.rst
130 release-notes-2.0.0.rst
132 release-notes-2.0.0.rst
131
133
132 |RCE| 1.x Versions
134 |RCE| 1.x Versions
133 ------------------
135 ------------------
134
136
135 .. toctree::
137 .. toctree::
136 :maxdepth: 1
138 :maxdepth: 1
137
139
138 release-notes-1.7.2.rst
140 release-notes-1.7.2.rst
139 release-notes-1.7.1.rst
141 release-notes-1.7.1.rst
140 release-notes-1.7.0.rst
142 release-notes-1.7.0.rst
141 release-notes-1.6.0.rst
143 release-notes-1.6.0.rst
@@ -1,807 +1,808 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 time
21 import time
22 import logging
22 import logging
23 import operator
23 import operator
24
24
25 from pyramid import compat
25 from pyramid import compat
26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
27
27
28 from rhodecode.lib import helpers as h, diffs, rc_cache
28 from rhodecode.lib import helpers as h, diffs, rc_cache
29 from rhodecode.lib.utils2 import (
29 from rhodecode.lib.utils2 import (
30 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
30 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
31 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
31 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
32 from rhodecode.lib.vcs.backends.base import EmptyCommit
32 from rhodecode.lib.vcs.backends.base import EmptyCommit
33 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
33 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
34 from rhodecode.model import repo
34 from rhodecode.model import repo
35 from rhodecode.model import repo_group
35 from rhodecode.model import repo_group
36 from rhodecode.model import user_group
36 from rhodecode.model import user_group
37 from rhodecode.model import user
37 from rhodecode.model import user
38 from rhodecode.model.db import User
38 from rhodecode.model.db import User
39 from rhodecode.model.scm import ScmModel
39 from rhodecode.model.scm import ScmModel
40 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
40 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
41 from rhodecode.model.repo import ReadmeFinder
41 from rhodecode.model.repo import ReadmeFinder
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 ADMIN_PREFIX = '/_admin'
46 ADMIN_PREFIX = '/_admin'
47 STATIC_FILE_PREFIX = '/_static'
47 STATIC_FILE_PREFIX = '/_static'
48
48
49 URL_NAME_REQUIREMENTS = {
49 URL_NAME_REQUIREMENTS = {
50 # group name can have a slash in them, but they must not end with a slash
50 # group name can have a slash in them, but they must not end with a slash
51 'group_name': r'.*?[^/]',
51 'group_name': r'.*?[^/]',
52 'repo_group_name': r'.*?[^/]',
52 'repo_group_name': r'.*?[^/]',
53 # repo names can have a slash in them, but they must not end with a slash
53 # repo names can have a slash in them, but they must not end with a slash
54 'repo_name': r'.*?[^/]',
54 'repo_name': r'.*?[^/]',
55 # file path eats up everything at the end
55 # file path eats up everything at the end
56 'f_path': r'.*',
56 'f_path': r'.*',
57 # reference types
57 # reference types
58 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
58 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
59 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
59 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
60 }
60 }
61
61
62
62
63 def add_route_with_slash(config,name, pattern, **kw):
63 def add_route_with_slash(config,name, pattern, **kw):
64 config.add_route(name, pattern, **kw)
64 config.add_route(name, pattern, **kw)
65 if not pattern.endswith('/'):
65 if not pattern.endswith('/'):
66 config.add_route(name + '_slash', pattern + '/', **kw)
66 config.add_route(name + '_slash', pattern + '/', **kw)
67
67
68
68
69 def add_route_requirements(route_path, requirements=None):
69 def add_route_requirements(route_path, requirements=None):
70 """
70 """
71 Adds regex requirements to pyramid routes using a mapping dict
71 Adds regex requirements to pyramid routes using a mapping dict
72 e.g::
72 e.g::
73 add_route_requirements('{repo_name}/settings')
73 add_route_requirements('{repo_name}/settings')
74 """
74 """
75 requirements = requirements or URL_NAME_REQUIREMENTS
75 requirements = requirements or URL_NAME_REQUIREMENTS
76 for key, regex in requirements.items():
76 for key, regex in requirements.items():
77 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
77 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
78 return route_path
78 return route_path
79
79
80
80
81 def get_format_ref_id(repo):
81 def get_format_ref_id(repo):
82 """Returns a `repo` specific reference formatter function"""
82 """Returns a `repo` specific reference formatter function"""
83 if h.is_svn(repo):
83 if h.is_svn(repo):
84 return _format_ref_id_svn
84 return _format_ref_id_svn
85 else:
85 else:
86 return _format_ref_id
86 return _format_ref_id
87
87
88
88
89 def _format_ref_id(name, raw_id):
89 def _format_ref_id(name, raw_id):
90 """Default formatting of a given reference `name`"""
90 """Default formatting of a given reference `name`"""
91 return name
91 return name
92
92
93
93
94 def _format_ref_id_svn(name, raw_id):
94 def _format_ref_id_svn(name, raw_id):
95 """Special way of formatting a reference for Subversion including path"""
95 """Special way of formatting a reference for Subversion including path"""
96 return '%s@%s' % (name, raw_id)
96 return '%s@%s' % (name, raw_id)
97
97
98
98
99 class TemplateArgs(StrictAttributeDict):
99 class TemplateArgs(StrictAttributeDict):
100 pass
100 pass
101
101
102
102
103 class BaseAppView(object):
103 class BaseAppView(object):
104
104
105 def __init__(self, context, request):
105 def __init__(self, context, request):
106 self.request = request
106 self.request = request
107 self.context = context
107 self.context = context
108 self.session = request.session
108 self.session = request.session
109 if not hasattr(request, 'user'):
109 if not hasattr(request, 'user'):
110 # NOTE(marcink): edge case, we ended up in matched route
110 # NOTE(marcink): edge case, we ended up in matched route
111 # but probably of web-app context, e.g API CALL/VCS CALL
111 # but probably of web-app context, e.g API CALL/VCS CALL
112 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
112 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
113 log.warning('Unable to process request `%s` in this scope', request)
113 log.warning('Unable to process request `%s` in this scope', request)
114 raise HTTPBadRequest()
114 raise HTTPBadRequest()
115
115
116 self._rhodecode_user = request.user # auth user
116 self._rhodecode_user = request.user # auth user
117 self._rhodecode_db_user = self._rhodecode_user.get_instance()
117 self._rhodecode_db_user = self._rhodecode_user.get_instance()
118 self._maybe_needs_password_change(
118 self._maybe_needs_password_change(
119 request.matched_route.name, self._rhodecode_db_user)
119 request.matched_route.name, self._rhodecode_db_user)
120
120
121 def _maybe_needs_password_change(self, view_name, user_obj):
121 def _maybe_needs_password_change(self, view_name, user_obj):
122 log.debug('Checking if user %s needs password change on view %s',
122 log.debug('Checking if user %s needs password change on view %s',
123 user_obj, view_name)
123 user_obj, view_name)
124 skip_user_views = [
124 skip_user_views = [
125 'logout', 'login',
125 'logout', 'login',
126 'my_account_password', 'my_account_password_update'
126 'my_account_password', 'my_account_password_update'
127 ]
127 ]
128
128
129 if not user_obj:
129 if not user_obj:
130 return
130 return
131
131
132 if user_obj.username == User.DEFAULT_USER:
132 if user_obj.username == User.DEFAULT_USER:
133 return
133 return
134
134
135 now = time.time()
135 now = time.time()
136 should_change = user_obj.user_data.get('force_password_change')
136 should_change = user_obj.user_data.get('force_password_change')
137 change_after = safe_int(should_change) or 0
137 change_after = safe_int(should_change) or 0
138 if should_change and now > change_after:
138 if should_change and now > change_after:
139 log.debug('User %s requires password change', user_obj)
139 log.debug('User %s requires password change', user_obj)
140 h.flash('You are required to change your password', 'warning',
140 h.flash('You are required to change your password', 'warning',
141 ignore_duplicate=True)
141 ignore_duplicate=True)
142
142
143 if view_name not in skip_user_views:
143 if view_name not in skip_user_views:
144 raise HTTPFound(
144 raise HTTPFound(
145 self.request.route_path('my_account_password'))
145 self.request.route_path('my_account_password'))
146
146
147 def _log_creation_exception(self, e, repo_name):
147 def _log_creation_exception(self, e, repo_name):
148 _ = self.request.translate
148 _ = self.request.translate
149 reason = None
149 reason = None
150 if len(e.args) == 2:
150 if len(e.args) == 2:
151 reason = e.args[1]
151 reason = e.args[1]
152
152
153 if reason == 'INVALID_CERTIFICATE':
153 if reason == 'INVALID_CERTIFICATE':
154 log.exception(
154 log.exception(
155 'Exception creating a repository: invalid certificate')
155 'Exception creating a repository: invalid certificate')
156 msg = (_('Error creating repository %s: invalid certificate')
156 msg = (_('Error creating repository %s: invalid certificate')
157 % repo_name)
157 % repo_name)
158 else:
158 else:
159 log.exception("Exception creating a repository")
159 log.exception("Exception creating a repository")
160 msg = (_('Error creating repository %s')
160 msg = (_('Error creating repository %s')
161 % repo_name)
161 % repo_name)
162 return msg
162 return msg
163
163
164 def _get_local_tmpl_context(self, include_app_defaults=True):
164 def _get_local_tmpl_context(self, include_app_defaults=True):
165 c = TemplateArgs()
165 c = TemplateArgs()
166 c.auth_user = self.request.user
166 c.auth_user = self.request.user
167 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
167 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
168 c.rhodecode_user = self.request.user
168 c.rhodecode_user = self.request.user
169
169
170 if include_app_defaults:
170 if include_app_defaults:
171 from rhodecode.lib.base import attach_context_attributes
171 from rhodecode.lib.base import attach_context_attributes
172 attach_context_attributes(c, self.request, self.request.user.user_id)
172 attach_context_attributes(c, self.request, self.request.user.user_id)
173
173
174 c.is_super_admin = c.auth_user.is_admin
174 c.is_super_admin = c.auth_user.is_admin
175
175
176 c.can_create_repo = c.is_super_admin
176 c.can_create_repo = c.is_super_admin
177 c.can_create_repo_group = c.is_super_admin
177 c.can_create_repo_group = c.is_super_admin
178 c.can_create_user_group = c.is_super_admin
178 c.can_create_user_group = c.is_super_admin
179
179
180 c.is_delegated_admin = False
180 c.is_delegated_admin = False
181
181
182 if not c.auth_user.is_default and not c.is_super_admin:
182 if not c.auth_user.is_default and not c.is_super_admin:
183 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
183 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
184 user=self.request.user)
184 user=self.request.user)
185 repositories = c.auth_user.repositories_admin or c.can_create_repo
185 repositories = c.auth_user.repositories_admin or c.can_create_repo
186
186
187 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
187 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
188 user=self.request.user)
188 user=self.request.user)
189 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
189 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
190
190
191 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
191 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
192 user=self.request.user)
192 user=self.request.user)
193 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
193 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
194 # delegated admin can create, or manage some objects
194 # delegated admin can create, or manage some objects
195 c.is_delegated_admin = repositories or repository_groups or user_groups
195 c.is_delegated_admin = repositories or repository_groups or user_groups
196 return c
196 return c
197
197
198 def _get_template_context(self, tmpl_args, **kwargs):
198 def _get_template_context(self, tmpl_args, **kwargs):
199
199
200 local_tmpl_args = {
200 local_tmpl_args = {
201 'defaults': {},
201 'defaults': {},
202 'errors': {},
202 'errors': {},
203 'c': tmpl_args
203 'c': tmpl_args
204 }
204 }
205 local_tmpl_args.update(kwargs)
205 local_tmpl_args.update(kwargs)
206 return local_tmpl_args
206 return local_tmpl_args
207
207
208 def load_default_context(self):
208 def load_default_context(self):
209 """
209 """
210 example:
210 example:
211
211
212 def load_default_context(self):
212 def load_default_context(self):
213 c = self._get_local_tmpl_context()
213 c = self._get_local_tmpl_context()
214 c.custom_var = 'foobar'
214 c.custom_var = 'foobar'
215
215
216 return c
216 return c
217 """
217 """
218 raise NotImplementedError('Needs implementation in view class')
218 raise NotImplementedError('Needs implementation in view class')
219
219
220
220
221 class RepoAppView(BaseAppView):
221 class RepoAppView(BaseAppView):
222
222
223 def __init__(self, context, request):
223 def __init__(self, context, request):
224 super(RepoAppView, self).__init__(context, request)
224 super(RepoAppView, self).__init__(context, request)
225 self.db_repo = request.db_repo
225 self.db_repo = request.db_repo
226 self.db_repo_name = self.db_repo.repo_name
226 self.db_repo_name = self.db_repo.repo_name
227 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
227 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
228 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
228 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
229 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
229 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
230
230
231 def _handle_missing_requirements(self, error):
231 def _handle_missing_requirements(self, error):
232 log.error(
232 log.error(
233 'Requirements are missing for repository %s: %s',
233 'Requirements are missing for repository %s: %s',
234 self.db_repo_name, safe_unicode(error))
234 self.db_repo_name, safe_unicode(error))
235
235
236 def _get_local_tmpl_context(self, include_app_defaults=True):
236 def _get_local_tmpl_context(self, include_app_defaults=True):
237 _ = self.request.translate
237 _ = self.request.translate
238 c = super(RepoAppView, self)._get_local_tmpl_context(
238 c = super(RepoAppView, self)._get_local_tmpl_context(
239 include_app_defaults=include_app_defaults)
239 include_app_defaults=include_app_defaults)
240
240
241 # register common vars for this type of view
241 # register common vars for this type of view
242 c.rhodecode_db_repo = self.db_repo
242 c.rhodecode_db_repo = self.db_repo
243 c.repo_name = self.db_repo_name
243 c.repo_name = self.db_repo_name
244 c.repository_pull_requests = self.db_repo_pull_requests
244 c.repository_pull_requests = self.db_repo_pull_requests
245 c.repository_artifacts = self.db_repo_artifacts
245 c.repository_artifacts = self.db_repo_artifacts
246 c.repository_is_user_following = ScmModel().is_following_repo(
246 c.repository_is_user_following = ScmModel().is_following_repo(
247 self.db_repo_name, self._rhodecode_user.user_id)
247 self.db_repo_name, self._rhodecode_user.user_id)
248 self.path_filter = PathFilter(None)
248 self.path_filter = PathFilter(None)
249
249
250 c.repository_requirements_missing = {}
250 c.repository_requirements_missing = {}
251 try:
251 try:
252 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
252 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
253 # NOTE(marcink):
253 # NOTE(marcink):
254 # comparison to None since if it's an object __bool__ is expensive to
254 # comparison to None since if it's an object __bool__ is expensive to
255 # calculate
255 # calculate
256 if self.rhodecode_vcs_repo is not None:
256 if self.rhodecode_vcs_repo is not None:
257 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
257 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
258 c.auth_user.username)
258 c.auth_user.username)
259 self.path_filter = PathFilter(path_perms)
259 self.path_filter = PathFilter(path_perms)
260 except RepositoryRequirementError as e:
260 except RepositoryRequirementError as e:
261 c.repository_requirements_missing = {'error': str(e)}
261 c.repository_requirements_missing = {'error': str(e)}
262 self._handle_missing_requirements(e)
262 self._handle_missing_requirements(e)
263 self.rhodecode_vcs_repo = None
263 self.rhodecode_vcs_repo = None
264
264
265 c.path_filter = self.path_filter # used by atom_feed_entry.mako
265 c.path_filter = self.path_filter # used by atom_feed_entry.mako
266
266
267 if self.rhodecode_vcs_repo is None:
267 if self.rhodecode_vcs_repo is None:
268 # unable to fetch this repo as vcs instance, report back to user
268 # unable to fetch this repo as vcs instance, report back to user
269 h.flash(_(
269 h.flash(_(
270 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
270 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
271 "Please check if it exist, or is not damaged.") %
271 "Please check if it exist, or is not damaged.") %
272 {'repo_name': c.repo_name},
272 {'repo_name': c.repo_name},
273 category='error', ignore_duplicate=True)
273 category='error', ignore_duplicate=True)
274 if c.repository_requirements_missing:
274 if c.repository_requirements_missing:
275 route = self.request.matched_route.name
275 route = self.request.matched_route.name
276 if route.startswith(('edit_repo', 'repo_summary')):
276 if route.startswith(('edit_repo', 'repo_summary')):
277 # allow summary and edit repo on missing requirements
277 # allow summary and edit repo on missing requirements
278 return c
278 return c
279
279
280 raise HTTPFound(
280 raise HTTPFound(
281 h.route_path('repo_summary', repo_name=self.db_repo_name))
281 h.route_path('repo_summary', repo_name=self.db_repo_name))
282
282
283 else: # redirect if we don't show missing requirements
283 else: # redirect if we don't show missing requirements
284 raise HTTPFound(h.route_path('home'))
284 raise HTTPFound(h.route_path('home'))
285
285
286 c.has_origin_repo_read_perm = False
286 c.has_origin_repo_read_perm = False
287 if self.db_repo.fork:
287 if self.db_repo.fork:
288 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
288 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
289 'repository.write', 'repository.read', 'repository.admin')(
289 'repository.write', 'repository.read', 'repository.admin')(
290 self.db_repo.fork.repo_name, 'summary fork link')
290 self.db_repo.fork.repo_name, 'summary fork link')
291
291
292 return c
292 return c
293
293
294 def _get_f_path_unchecked(self, matchdict, default=None):
294 def _get_f_path_unchecked(self, matchdict, default=None):
295 """
295 """
296 Should only be used by redirects, everything else should call _get_f_path
296 Should only be used by redirects, everything else should call _get_f_path
297 """
297 """
298 f_path = matchdict.get('f_path')
298 f_path = matchdict.get('f_path')
299 if f_path:
299 if f_path:
300 # fix for multiple initial slashes that causes errors for GIT
300 # fix for multiple initial slashes that causes errors for GIT
301 return f_path.lstrip('/')
301 return f_path.lstrip('/')
302
302
303 return default
303 return default
304
304
305 def _get_f_path(self, matchdict, default=None):
305 def _get_f_path(self, matchdict, default=None):
306 f_path_match = self._get_f_path_unchecked(matchdict, default)
306 f_path_match = self._get_f_path_unchecked(matchdict, default)
307 return self.path_filter.assert_path_permissions(f_path_match)
307 return self.path_filter.assert_path_permissions(f_path_match)
308
308
309 def _get_general_setting(self, target_repo, settings_key, default=False):
309 def _get_general_setting(self, target_repo, settings_key, default=False):
310 settings_model = VcsSettingsModel(repo=target_repo)
310 settings_model = VcsSettingsModel(repo=target_repo)
311 settings = settings_model.get_general_settings()
311 settings = settings_model.get_general_settings()
312 return settings.get(settings_key, default)
312 return settings.get(settings_key, default)
313
313
314 def _get_repo_setting(self, target_repo, settings_key, default=False):
314 def _get_repo_setting(self, target_repo, settings_key, default=False):
315 settings_model = VcsSettingsModel(repo=target_repo)
315 settings_model = VcsSettingsModel(repo=target_repo)
316 settings = settings_model.get_repo_settings_inherited()
316 settings = settings_model.get_repo_settings_inherited()
317 return settings.get(settings_key, default)
317 return settings.get(settings_key, default)
318
318
319 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path='/'):
319 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path='/'):
320 log.debug('Looking for README file at path %s', path)
320 log.debug('Looking for README file at path %s', path)
321 if commit_id:
321 if commit_id:
322 landing_commit_id = commit_id
322 landing_commit_id = commit_id
323 else:
323 else:
324 landing_commit = db_repo.get_landing_commit()
324 landing_commit = db_repo.get_landing_commit()
325 if isinstance(landing_commit, EmptyCommit):
325 if isinstance(landing_commit, EmptyCommit):
326 return None, None
326 return None, None
327 landing_commit_id = landing_commit.raw_id
327 landing_commit_id = landing_commit.raw_id
328
328
329 cache_namespace_uid = 'cache_repo.{}'.format(db_repo.repo_id)
329 cache_namespace_uid = 'cache_repo.{}'.format(db_repo.repo_id)
330 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
330 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
331 start = time.time()
331 start = time.time()
332
332
333 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
333 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
334 def generate_repo_readme(repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type):
334 def generate_repo_readme(repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type):
335 readme_data = None
335 readme_data = None
336 readme_filename = None
336 readme_filename = None
337
337
338 commit = db_repo.get_commit(_commit_id)
338 commit = db_repo.get_commit(_commit_id)
339 log.debug("Searching for a README file at commit %s.", _commit_id)
339 log.debug("Searching for a README file at commit %s.", _commit_id)
340 readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path)
340 readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path)
341
341
342 if readme_node:
342 if readme_node:
343 log.debug('Found README node: %s', readme_node)
343 log.debug('Found README node: %s', readme_node)
344 relative_urls = {
344 relative_urls = {
345 'raw': h.route_path(
345 'raw': h.route_path(
346 'repo_file_raw', repo_name=_repo_name,
346 'repo_file_raw', repo_name=_repo_name,
347 commit_id=commit.raw_id, f_path=readme_node.path),
347 commit_id=commit.raw_id, f_path=readme_node.path),
348 'standard': h.route_path(
348 'standard': h.route_path(
349 'repo_files', repo_name=_repo_name,
349 'repo_files', repo_name=_repo_name,
350 commit_id=commit.raw_id, f_path=readme_node.path),
350 commit_id=commit.raw_id, f_path=readme_node.path),
351 }
351 }
352 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
352 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
353 readme_filename = readme_node.unicode_path
353 readme_filename = readme_node.unicode_path
354
354
355 return readme_data, readme_filename
355 return readme_data, readme_filename
356
356
357 readme_data, readme_filename = generate_repo_readme(
357 readme_data, readme_filename = generate_repo_readme(
358 db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, renderer_type,)
358 db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, renderer_type,)
359 compute_time = time.time() - start
359 compute_time = time.time() - start
360 log.debug('Repo README for path %s generated and computed in %.4fs',
360 log.debug('Repo README for path %s generated and computed in %.4fs',
361 path, compute_time)
361 path, compute_time)
362 return readme_data, readme_filename
362 return readme_data, readme_filename
363
363
364 def _render_readme_or_none(self, commit, readme_node, relative_urls):
364 def _render_readme_or_none(self, commit, readme_node, relative_urls):
365 log.debug('Found README file `%s` rendering...', readme_node.path)
365 log.debug('Found README file `%s` rendering...', readme_node.path)
366 renderer = MarkupRenderer()
366 renderer = MarkupRenderer()
367 try:
367 try:
368 html_source = renderer.render(
368 html_source = renderer.render(
369 readme_node.content, filename=readme_node.path)
369 readme_node.content, filename=readme_node.path)
370 if relative_urls:
370 if relative_urls:
371 return relative_links(html_source, relative_urls)
371 return relative_links(html_source, relative_urls)
372 return html_source
372 return html_source
373 except Exception:
373 except Exception:
374 log.exception(
374 log.exception(
375 "Exception while trying to render the README")
375 "Exception while trying to render the README")
376
376
377 def get_recache_flag(self):
377 def get_recache_flag(self):
378 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
378 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
379 flag_val = self.request.GET.get(flag_name)
379 flag_val = self.request.GET.get(flag_name)
380 if str2bool(flag_val):
380 if str2bool(flag_val):
381 return True
381 return True
382 return False
382 return False
383
383
384
384
385 class PathFilter(object):
385 class PathFilter(object):
386
386
387 # Expects and instance of BasePathPermissionChecker or None
387 # Expects and instance of BasePathPermissionChecker or None
388 def __init__(self, permission_checker):
388 def __init__(self, permission_checker):
389 self.permission_checker = permission_checker
389 self.permission_checker = permission_checker
390
390
391 def assert_path_permissions(self, path):
391 def assert_path_permissions(self, path):
392 if self.path_access_allowed(path):
392 if self.path_access_allowed(path):
393 return path
393 return path
394 raise HTTPForbidden()
394 raise HTTPForbidden()
395
395
396 def path_access_allowed(self, path):
396 def path_access_allowed(self, path):
397 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
397 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
398 if self.permission_checker:
398 if self.permission_checker:
399 has_access = path and self.permission_checker.has_access(path)
399 has_access = path and self.permission_checker.has_access(path)
400 log.debug('ACL Permissions checker enabled, ACL Check has_access: %s', has_access)
400 log.debug('ACL Permissions checker enabled, ACL Check has_access: %s', has_access)
401 return has_access
401 return has_access
402
402
403 log.debug('ACL permissions checker not enabled, skipping...')
403 log.debug('ACL permissions checker not enabled, skipping...')
404 return True
404 return True
405
405
406 def filter_patchset(self, patchset):
406 def filter_patchset(self, patchset):
407 if not self.permission_checker or not patchset:
407 if not self.permission_checker or not patchset:
408 return patchset, False
408 return patchset, False
409 had_filtered = False
409 had_filtered = False
410 filtered_patchset = []
410 filtered_patchset = []
411 for patch in patchset:
411 for patch in patchset:
412 filename = patch.get('filename', None)
412 filename = patch.get('filename', None)
413 if not filename or self.permission_checker.has_access(filename):
413 if not filename or self.permission_checker.has_access(filename):
414 filtered_patchset.append(patch)
414 filtered_patchset.append(patch)
415 else:
415 else:
416 had_filtered = True
416 had_filtered = True
417 if had_filtered:
417 if had_filtered:
418 if isinstance(patchset, diffs.LimitedDiffContainer):
418 if isinstance(patchset, diffs.LimitedDiffContainer):
419 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
419 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
420 return filtered_patchset, True
420 return filtered_patchset, True
421 else:
421 else:
422 return patchset, False
422 return patchset, False
423
423
424 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
424 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
425 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
425 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
426 result = diffset.render_patchset(
426 result = diffset.render_patchset(
427 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
427 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
428 result.has_hidden_changes = has_hidden_changes
428 result.has_hidden_changes = has_hidden_changes
429 return result
429 return result
430
430
431 def get_raw_patch(self, diff_processor):
431 def get_raw_patch(self, diff_processor):
432 if self.permission_checker is None:
432 if self.permission_checker is None:
433 return diff_processor.as_raw()
433 return diff_processor.as_raw()
434 elif self.permission_checker.has_full_access:
434 elif self.permission_checker.has_full_access:
435 return diff_processor.as_raw()
435 return diff_processor.as_raw()
436 else:
436 else:
437 return '# Repository has user-specific filters, raw patch generation is disabled.'
437 return '# Repository has user-specific filters, raw patch generation is disabled.'
438
438
439 @property
439 @property
440 def is_enabled(self):
440 def is_enabled(self):
441 return self.permission_checker is not None
441 return self.permission_checker is not None
442
442
443
443
444 class RepoGroupAppView(BaseAppView):
444 class RepoGroupAppView(BaseAppView):
445 def __init__(self, context, request):
445 def __init__(self, context, request):
446 super(RepoGroupAppView, self).__init__(context, request)
446 super(RepoGroupAppView, self).__init__(context, request)
447 self.db_repo_group = request.db_repo_group
447 self.db_repo_group = request.db_repo_group
448 self.db_repo_group_name = self.db_repo_group.group_name
448 self.db_repo_group_name = self.db_repo_group.group_name
449
449
450 def _get_local_tmpl_context(self, include_app_defaults=True):
450 def _get_local_tmpl_context(self, include_app_defaults=True):
451 _ = self.request.translate
451 _ = self.request.translate
452 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
452 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
453 include_app_defaults=include_app_defaults)
453 include_app_defaults=include_app_defaults)
454 c.repo_group = self.db_repo_group
454 c.repo_group = self.db_repo_group
455 return c
455 return c
456
456
457 def _revoke_perms_on_yourself(self, form_result):
457 def _revoke_perms_on_yourself(self, form_result):
458 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
458 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
459 form_result['perm_updates'])
459 form_result['perm_updates'])
460 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
460 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
461 form_result['perm_additions'])
461 form_result['perm_additions'])
462 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
462 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
463 form_result['perm_deletions'])
463 form_result['perm_deletions'])
464 admin_perm = 'group.admin'
464 admin_perm = 'group.admin'
465 if _updates and _updates[0][1] != admin_perm or \
465 if _updates and _updates[0][1] != admin_perm or \
466 _additions and _additions[0][1] != admin_perm or \
466 _additions and _additions[0][1] != admin_perm or \
467 _deletions and _deletions[0][1] != admin_perm:
467 _deletions and _deletions[0][1] != admin_perm:
468 return True
468 return True
469 return False
469 return False
470
470
471
471
472 class UserGroupAppView(BaseAppView):
472 class UserGroupAppView(BaseAppView):
473 def __init__(self, context, request):
473 def __init__(self, context, request):
474 super(UserGroupAppView, self).__init__(context, request)
474 super(UserGroupAppView, self).__init__(context, request)
475 self.db_user_group = request.db_user_group
475 self.db_user_group = request.db_user_group
476 self.db_user_group_name = self.db_user_group.users_group_name
476 self.db_user_group_name = self.db_user_group.users_group_name
477
477
478
478
479 class UserAppView(BaseAppView):
479 class UserAppView(BaseAppView):
480 def __init__(self, context, request):
480 def __init__(self, context, request):
481 super(UserAppView, self).__init__(context, request)
481 super(UserAppView, self).__init__(context, request)
482 self.db_user = request.db_user
482 self.db_user = request.db_user
483 self.db_user_id = self.db_user.user_id
483 self.db_user_id = self.db_user.user_id
484
484
485 _ = self.request.translate
485 _ = self.request.translate
486 if not request.db_user_supports_default:
486 if not request.db_user_supports_default:
487 if self.db_user.username == User.DEFAULT_USER:
487 if self.db_user.username == User.DEFAULT_USER:
488 h.flash(_("Editing user `{}` is disabled.".format(
488 h.flash(_("Editing user `{}` is disabled.".format(
489 User.DEFAULT_USER)), category='warning')
489 User.DEFAULT_USER)), category='warning')
490 raise HTTPFound(h.route_path('users'))
490 raise HTTPFound(h.route_path('users'))
491
491
492
492
493 class DataGridAppView(object):
493 class DataGridAppView(object):
494 """
494 """
495 Common class to have re-usable grid rendering components
495 Common class to have re-usable grid rendering components
496 """
496 """
497
497
498 def _extract_ordering(self, request, column_map=None):
498 def _extract_ordering(self, request, column_map=None):
499 column_map = column_map or {}
499 column_map = column_map or {}
500 column_index = safe_int(request.GET.get('order[0][column]'))
500 column_index = safe_int(request.GET.get('order[0][column]'))
501 order_dir = request.GET.get(
501 order_dir = request.GET.get(
502 'order[0][dir]', 'desc')
502 'order[0][dir]', 'desc')
503 order_by = request.GET.get(
503 order_by = request.GET.get(
504 'columns[%s][data][sort]' % column_index, 'name_raw')
504 'columns[%s][data][sort]' % column_index, 'name_raw')
505
505
506 # translate datatable to DB columns
506 # translate datatable to DB columns
507 order_by = column_map.get(order_by) or order_by
507 order_by = column_map.get(order_by) or order_by
508
508
509 search_q = request.GET.get('search[value]')
509 search_q = request.GET.get('search[value]')
510 return search_q, order_by, order_dir
510 return search_q, order_by, order_dir
511
511
512 def _extract_chunk(self, request):
512 def _extract_chunk(self, request):
513 start = safe_int(request.GET.get('start'), 0)
513 start = safe_int(request.GET.get('start'), 0)
514 length = safe_int(request.GET.get('length'), 25)
514 length = safe_int(request.GET.get('length'), 25)
515 draw = safe_int(request.GET.get('draw'))
515 draw = safe_int(request.GET.get('draw'))
516 return draw, start, length
516 return draw, start, length
517
517
518 def _get_order_col(self, order_by, model):
518 def _get_order_col(self, order_by, model):
519 if isinstance(order_by, compat.string_types):
519 if isinstance(order_by, compat.string_types):
520 try:
520 try:
521 return operator.attrgetter(order_by)(model)
521 return operator.attrgetter(order_by)(model)
522 except AttributeError:
522 except AttributeError:
523 return None
523 return None
524 else:
524 else:
525 return order_by
525 return order_by
526
526
527
527
528 class BaseReferencesView(RepoAppView):
528 class BaseReferencesView(RepoAppView):
529 """
529 """
530 Base for reference view for branches, tags and bookmarks.
530 Base for reference view for branches, tags and bookmarks.
531 """
531 """
532 def load_default_context(self):
532 def load_default_context(self):
533 c = self._get_local_tmpl_context()
533 c = self._get_local_tmpl_context()
534
535
536 return c
534 return c
537
535
538 def load_refs_context(self, ref_items, partials_template):
536 def load_refs_context(self, ref_items, partials_template):
539 _render = self.request.get_partial_renderer(partials_template)
537 _render = self.request.get_partial_renderer(partials_template)
540 pre_load = ["author", "date", "message", "parents"]
538 pre_load = ["author", "date", "message", "parents"]
541
539
542 is_svn = h.is_svn(self.rhodecode_vcs_repo)
540 is_svn = h.is_svn(self.rhodecode_vcs_repo)
543 is_hg = h.is_hg(self.rhodecode_vcs_repo)
541 is_hg = h.is_hg(self.rhodecode_vcs_repo)
544
542
545 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
543 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
546
544
547 closed_refs = {}
545 closed_refs = {}
548 if is_hg:
546 if is_hg:
549 closed_refs = self.rhodecode_vcs_repo.branches_closed
547 closed_refs = self.rhodecode_vcs_repo.branches_closed
550
548
551 data = []
549 data = []
552 for ref_name, commit_id in ref_items:
550 for ref_name, commit_id in ref_items:
553 commit = self.rhodecode_vcs_repo.get_commit(
551 commit = self.rhodecode_vcs_repo.get_commit(
554 commit_id=commit_id, pre_load=pre_load)
552 commit_id=commit_id, pre_load=pre_load)
555 closed = ref_name in closed_refs
553 closed = ref_name in closed_refs
556
554
557 # TODO: johbo: Unify generation of reference links
555 # TODO: johbo: Unify generation of reference links
558 use_commit_id = '/' in ref_name or is_svn
556 use_commit_id = '/' in ref_name or is_svn
559
557
560 if use_commit_id:
558 if use_commit_id:
561 files_url = h.route_path(
559 files_url = h.route_path(
562 'repo_files',
560 'repo_files',
563 repo_name=self.db_repo_name,
561 repo_name=self.db_repo_name,
564 f_path=ref_name if is_svn else '',
562 f_path=ref_name if is_svn else '',
565 commit_id=commit_id)
563 commit_id=commit_id,
564 _query=dict(at=ref_name)
565 )
566
566
567 else:
567 else:
568 files_url = h.route_path(
568 files_url = h.route_path(
569 'repo_files',
569 'repo_files',
570 repo_name=self.db_repo_name,
570 repo_name=self.db_repo_name,
571 f_path=ref_name if is_svn else '',
571 f_path=ref_name if is_svn else '',
572 commit_id=ref_name,
572 commit_id=ref_name,
573 _query=dict(at=ref_name))
573 _query=dict(at=ref_name)
574 )
574
575
575 data.append({
576 data.append({
576 "name": _render('name', ref_name, files_url, closed),
577 "name": _render('name', ref_name, files_url, closed),
577 "name_raw": ref_name,
578 "name_raw": ref_name,
578 "date": _render('date', commit.date),
579 "date": _render('date', commit.date),
579 "date_raw": datetime_to_time(commit.date),
580 "date_raw": datetime_to_time(commit.date),
580 "author": _render('author', commit.author),
581 "author": _render('author', commit.author),
581 "commit": _render(
582 "commit": _render(
582 'commit', commit.message, commit.raw_id, commit.idx),
583 'commit', commit.message, commit.raw_id, commit.idx),
583 "commit_raw": commit.idx,
584 "commit_raw": commit.idx,
584 "compare": _render(
585 "compare": _render(
585 'compare', format_ref_id(ref_name, commit.raw_id)),
586 'compare', format_ref_id(ref_name, commit.raw_id)),
586 })
587 })
587
588
588 return data
589 return data
589
590
590
591
591 class RepoRoutePredicate(object):
592 class RepoRoutePredicate(object):
592 def __init__(self, val, config):
593 def __init__(self, val, config):
593 self.val = val
594 self.val = val
594
595
595 def text(self):
596 def text(self):
596 return 'repo_route = %s' % self.val
597 return 'repo_route = %s' % self.val
597
598
598 phash = text
599 phash = text
599
600
600 def __call__(self, info, request):
601 def __call__(self, info, request):
601 if hasattr(request, 'vcs_call'):
602 if hasattr(request, 'vcs_call'):
602 # skip vcs calls
603 # skip vcs calls
603 return
604 return
604
605
605 repo_name = info['match']['repo_name']
606 repo_name = info['match']['repo_name']
606 repo_model = repo.RepoModel()
607 repo_model = repo.RepoModel()
607
608
608 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
609 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
609
610
610 def redirect_if_creating(route_info, db_repo):
611 def redirect_if_creating(route_info, db_repo):
611 skip_views = ['edit_repo_advanced_delete']
612 skip_views = ['edit_repo_advanced_delete']
612 route = route_info['route']
613 route = route_info['route']
613 # we should skip delete view so we can actually "remove" repositories
614 # we should skip delete view so we can actually "remove" repositories
614 # if they get stuck in creating state.
615 # if they get stuck in creating state.
615 if route.name in skip_views:
616 if route.name in skip_views:
616 return
617 return
617
618
618 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
619 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
619 repo_creating_url = request.route_path(
620 repo_creating_url = request.route_path(
620 'repo_creating', repo_name=db_repo.repo_name)
621 'repo_creating', repo_name=db_repo.repo_name)
621 raise HTTPFound(repo_creating_url)
622 raise HTTPFound(repo_creating_url)
622
623
623 if by_name_match:
624 if by_name_match:
624 # register this as request object we can re-use later
625 # register this as request object we can re-use later
625 request.db_repo = by_name_match
626 request.db_repo = by_name_match
626 redirect_if_creating(info, by_name_match)
627 redirect_if_creating(info, by_name_match)
627 return True
628 return True
628
629
629 by_id_match = repo_model.get_repo_by_id(repo_name)
630 by_id_match = repo_model.get_repo_by_id(repo_name)
630 if by_id_match:
631 if by_id_match:
631 request.db_repo = by_id_match
632 request.db_repo = by_id_match
632 redirect_if_creating(info, by_id_match)
633 redirect_if_creating(info, by_id_match)
633 return True
634 return True
634
635
635 return False
636 return False
636
637
637
638
638 class RepoForbidArchivedRoutePredicate(object):
639 class RepoForbidArchivedRoutePredicate(object):
639 def __init__(self, val, config):
640 def __init__(self, val, config):
640 self.val = val
641 self.val = val
641
642
642 def text(self):
643 def text(self):
643 return 'repo_forbid_archived = %s' % self.val
644 return 'repo_forbid_archived = %s' % self.val
644
645
645 phash = text
646 phash = text
646
647
647 def __call__(self, info, request):
648 def __call__(self, info, request):
648 _ = request.translate
649 _ = request.translate
649 rhodecode_db_repo = request.db_repo
650 rhodecode_db_repo = request.db_repo
650
651
651 log.debug(
652 log.debug(
652 '%s checking if archived flag for repo for %s',
653 '%s checking if archived flag for repo for %s',
653 self.__class__.__name__, rhodecode_db_repo.repo_name)
654 self.__class__.__name__, rhodecode_db_repo.repo_name)
654
655
655 if rhodecode_db_repo.archived:
656 if rhodecode_db_repo.archived:
656 log.warning('Current view is not supported for archived repo:%s',
657 log.warning('Current view is not supported for archived repo:%s',
657 rhodecode_db_repo.repo_name)
658 rhodecode_db_repo.repo_name)
658
659
659 h.flash(
660 h.flash(
660 h.literal(_('Action not supported for archived repository.')),
661 h.literal(_('Action not supported for archived repository.')),
661 category='warning')
662 category='warning')
662 summary_url = request.route_path(
663 summary_url = request.route_path(
663 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
664 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
664 raise HTTPFound(summary_url)
665 raise HTTPFound(summary_url)
665 return True
666 return True
666
667
667
668
668 class RepoTypeRoutePredicate(object):
669 class RepoTypeRoutePredicate(object):
669 def __init__(self, val, config):
670 def __init__(self, val, config):
670 self.val = val or ['hg', 'git', 'svn']
671 self.val = val or ['hg', 'git', 'svn']
671
672
672 def text(self):
673 def text(self):
673 return 'repo_accepted_type = %s' % self.val
674 return 'repo_accepted_type = %s' % self.val
674
675
675 phash = text
676 phash = text
676
677
677 def __call__(self, info, request):
678 def __call__(self, info, request):
678 if hasattr(request, 'vcs_call'):
679 if hasattr(request, 'vcs_call'):
679 # skip vcs calls
680 # skip vcs calls
680 return
681 return
681
682
682 rhodecode_db_repo = request.db_repo
683 rhodecode_db_repo = request.db_repo
683
684
684 log.debug(
685 log.debug(
685 '%s checking repo type for %s in %s',
686 '%s checking repo type for %s in %s',
686 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
687 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
687
688
688 if rhodecode_db_repo.repo_type in self.val:
689 if rhodecode_db_repo.repo_type in self.val:
689 return True
690 return True
690 else:
691 else:
691 log.warning('Current view is not supported for repo type:%s',
692 log.warning('Current view is not supported for repo type:%s',
692 rhodecode_db_repo.repo_type)
693 rhodecode_db_repo.repo_type)
693 return False
694 return False
694
695
695
696
696 class RepoGroupRoutePredicate(object):
697 class RepoGroupRoutePredicate(object):
697 def __init__(self, val, config):
698 def __init__(self, val, config):
698 self.val = val
699 self.val = val
699
700
700 def text(self):
701 def text(self):
701 return 'repo_group_route = %s' % self.val
702 return 'repo_group_route = %s' % self.val
702
703
703 phash = text
704 phash = text
704
705
705 def __call__(self, info, request):
706 def __call__(self, info, request):
706 if hasattr(request, 'vcs_call'):
707 if hasattr(request, 'vcs_call'):
707 # skip vcs calls
708 # skip vcs calls
708 return
709 return
709
710
710 repo_group_name = info['match']['repo_group_name']
711 repo_group_name = info['match']['repo_group_name']
711 repo_group_model = repo_group.RepoGroupModel()
712 repo_group_model = repo_group.RepoGroupModel()
712 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
713 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
713
714
714 if by_name_match:
715 if by_name_match:
715 # register this as request object we can re-use later
716 # register this as request object we can re-use later
716 request.db_repo_group = by_name_match
717 request.db_repo_group = by_name_match
717 return True
718 return True
718
719
719 return False
720 return False
720
721
721
722
722 class UserGroupRoutePredicate(object):
723 class UserGroupRoutePredicate(object):
723 def __init__(self, val, config):
724 def __init__(self, val, config):
724 self.val = val
725 self.val = val
725
726
726 def text(self):
727 def text(self):
727 return 'user_group_route = %s' % self.val
728 return 'user_group_route = %s' % self.val
728
729
729 phash = text
730 phash = text
730
731
731 def __call__(self, info, request):
732 def __call__(self, info, request):
732 if hasattr(request, 'vcs_call'):
733 if hasattr(request, 'vcs_call'):
733 # skip vcs calls
734 # skip vcs calls
734 return
735 return
735
736
736 user_group_id = info['match']['user_group_id']
737 user_group_id = info['match']['user_group_id']
737 user_group_model = user_group.UserGroup()
738 user_group_model = user_group.UserGroup()
738 by_id_match = user_group_model.get(user_group_id, cache=False)
739 by_id_match = user_group_model.get(user_group_id, cache=False)
739
740
740 if by_id_match:
741 if by_id_match:
741 # register this as request object we can re-use later
742 # register this as request object we can re-use later
742 request.db_user_group = by_id_match
743 request.db_user_group = by_id_match
743 return True
744 return True
744
745
745 return False
746 return False
746
747
747
748
748 class UserRoutePredicateBase(object):
749 class UserRoutePredicateBase(object):
749 supports_default = None
750 supports_default = None
750
751
751 def __init__(self, val, config):
752 def __init__(self, val, config):
752 self.val = val
753 self.val = val
753
754
754 def text(self):
755 def text(self):
755 raise NotImplementedError()
756 raise NotImplementedError()
756
757
757 def __call__(self, info, request):
758 def __call__(self, info, request):
758 if hasattr(request, 'vcs_call'):
759 if hasattr(request, 'vcs_call'):
759 # skip vcs calls
760 # skip vcs calls
760 return
761 return
761
762
762 user_id = info['match']['user_id']
763 user_id = info['match']['user_id']
763 user_model = user.User()
764 user_model = user.User()
764 by_id_match = user_model.get(user_id, cache=False)
765 by_id_match = user_model.get(user_id, cache=False)
765
766
766 if by_id_match:
767 if by_id_match:
767 # register this as request object we can re-use later
768 # register this as request object we can re-use later
768 request.db_user = by_id_match
769 request.db_user = by_id_match
769 request.db_user_supports_default = self.supports_default
770 request.db_user_supports_default = self.supports_default
770 return True
771 return True
771
772
772 return False
773 return False
773
774
774
775
775 class UserRoutePredicate(UserRoutePredicateBase):
776 class UserRoutePredicate(UserRoutePredicateBase):
776 supports_default = False
777 supports_default = False
777
778
778 def text(self):
779 def text(self):
779 return 'user_route = %s' % self.val
780 return 'user_route = %s' % self.val
780
781
781 phash = text
782 phash = text
782
783
783
784
784 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
785 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
785 supports_default = True
786 supports_default = True
786
787
787 def text(self):
788 def text(self):
788 return 'user_with_default_route = %s' % self.val
789 return 'user_with_default_route = %s' % self.val
789
790
790 phash = text
791 phash = text
791
792
792
793
793 def includeme(config):
794 def includeme(config):
794 config.add_route_predicate(
795 config.add_route_predicate(
795 'repo_route', RepoRoutePredicate)
796 'repo_route', RepoRoutePredicate)
796 config.add_route_predicate(
797 config.add_route_predicate(
797 'repo_accepted_types', RepoTypeRoutePredicate)
798 'repo_accepted_types', RepoTypeRoutePredicate)
798 config.add_route_predicate(
799 config.add_route_predicate(
799 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
800 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
800 config.add_route_predicate(
801 config.add_route_predicate(
801 'repo_group_route', RepoGroupRoutePredicate)
802 'repo_group_route', RepoGroupRoutePredicate)
802 config.add_route_predicate(
803 config.add_route_predicate(
803 'user_group_route', UserGroupRoutePredicate)
804 'user_group_route', UserGroupRoutePredicate)
804 config.add_route_predicate(
805 config.add_route_predicate(
805 'user_route_with_default', UserRouteWithDefaultPredicate)
806 'user_route_with_default', UserRouteWithDefaultPredicate)
806 config.add_route_predicate(
807 config.add_route_predicate(
807 'user_route', UserRoutePredicate)
808 'user_route', UserRoutePredicate)
@@ -1,402 +1,419 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 logging
22 import logging
23 import datetime
23 import datetime
24
24
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.renderers import render_to_response
26 from pyramid.renderers import render_to_response
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib.celerylib import run_task, tasks
28 from rhodecode.lib.celerylib import run_task, tasks
29 from rhodecode.lib.utils2 import AttributeDict
29 from rhodecode.lib.utils2 import AttributeDict
30 from rhodecode.model.db import User
30 from rhodecode.model.db import User
31 from rhodecode.model.notification import EmailNotificationModel
31 from rhodecode.model.notification import EmailNotificationModel
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class DebugStyleView(BaseAppView):
36 class DebugStyleView(BaseAppView):
37 def load_default_context(self):
37 def load_default_context(self):
38 c = self._get_local_tmpl_context()
38 c = self._get_local_tmpl_context()
39
39
40 return c
40 return c
41
41
42 @view_config(
42 @view_config(
43 route_name='debug_style_home', request_method='GET',
43 route_name='debug_style_home', request_method='GET',
44 renderer=None)
44 renderer=None)
45 def index(self):
45 def index(self):
46 c = self.load_default_context()
46 c = self.load_default_context()
47 c.active = 'index'
47 c.active = 'index'
48
48
49 return render_to_response(
49 return render_to_response(
50 'debug_style/index.html', self._get_template_context(c),
50 'debug_style/index.html', self._get_template_context(c),
51 request=self.request)
51 request=self.request)
52
52
53 @view_config(
53 @view_config(
54 route_name='debug_style_email', request_method='GET',
54 route_name='debug_style_email', request_method='GET',
55 renderer=None)
55 renderer=None)
56 @view_config(
56 @view_config(
57 route_name='debug_style_email_plain_rendered', request_method='GET',
57 route_name='debug_style_email_plain_rendered', request_method='GET',
58 renderer=None)
58 renderer=None)
59 def render_email(self):
59 def render_email(self):
60 c = self.load_default_context()
60 c = self.load_default_context()
61 email_id = self.request.matchdict['email_id']
61 email_id = self.request.matchdict['email_id']
62 c.active = 'emails'
62 c.active = 'emails'
63
63
64 pr = AttributeDict(
64 pr = AttributeDict(
65 pull_request_id=123,
65 pull_request_id=123,
66 title='digital_ocean: fix redis, elastic search start on boot, '
66 title='digital_ocean: fix redis, elastic search start on boot, '
67 'fix fd limits on supervisor, set postgres 11 version',
67 'fix fd limits on supervisor, set postgres 11 version',
68 description='''
68 description='''
69 Check if we should use full-topic or mini-topic.
69 Check if we should use full-topic or mini-topic.
70
70
71 - full topic produces some problems with merge states etc
71 - full topic produces some problems with merge states etc
72 - server-mini-topic needs probably tweeks.
72 - server-mini-topic needs probably tweeks.
73 ''',
73 ''',
74 repo_name='foobar',
74 repo_name='foobar',
75 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
75 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 target_ref_parts=AttributeDict(type='branch', name='master'),
76 target_ref_parts=AttributeDict(type='branch', name='master'),
77 )
77 )
78 target_repo = AttributeDict(repo_name='repo_group/target_repo')
78 target_repo = AttributeDict(repo_name='repo_group/target_repo')
79 source_repo = AttributeDict(repo_name='repo_group/source_repo')
79 source_repo = AttributeDict(repo_name='repo_group/source_repo')
80 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
80 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
81 # file/commit changes for PR update
81 # file/commit changes for PR update
82 commit_changes = AttributeDict({
82 commit_changes = AttributeDict({
83 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
83 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
84 'removed': ['eeeeeeeeeee'],
84 'removed': ['eeeeeeeeeee'],
85 })
85 })
86 file_changes = AttributeDict({
86 file_changes = AttributeDict({
87 'added': ['a/file1.md', 'file2.py'],
87 'added': ['a/file1.md', 'file2.py'],
88 'modified': ['b/modified_file.rst'],
88 'modified': ['b/modified_file.rst'],
89 'removed': ['.idea'],
89 'removed': ['.idea'],
90 })
90 })
91
91
92 exc_traceback = {
92 exc_traceback = {
93 'exc_utc_date': '2020-03-26T12:54:50.683281',
93 'exc_utc_date': '2020-03-26T12:54:50.683281',
94 'exc_id': 139638856342656,
94 'exc_id': 139638856342656,
95 'exc_timestamp': '1585227290.683288',
95 'exc_timestamp': '1585227290.683288',
96 'version': 'v1',
96 'version': 'v1',
97 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
97 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
98 'exc_type': 'AttributeError'
98 'exc_type': 'AttributeError'
99 }
99 }
100 email_kwargs = {
100 email_kwargs = {
101 'test': {},
101 'test': {},
102 'message': {
102 'message': {
103 'body': 'message body !'
103 'body': 'message body !'
104 },
104 },
105 'email_test': {
105 'email_test': {
106 'user': user,
106 'user': user,
107 'date': datetime.datetime.now(),
107 'date': datetime.datetime.now(),
108 },
108 },
109 'exception': {
109 'exception': {
110 'email_prefix': '[RHODECODE ERROR]',
110 'email_prefix': '[RHODECODE ERROR]',
111 'exc_id': exc_traceback['exc_id'],
111 'exc_id': exc_traceback['exc_id'],
112 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
112 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
113 'exc_type_name': 'NameError',
113 'exc_type_name': 'NameError',
114 'exc_traceback': exc_traceback,
114 'exc_traceback': exc_traceback,
115 },
115 },
116 'password_reset': {
116 'password_reset': {
117 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
117 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
118
118
119 'user': user,
119 'user': user,
120 'date': datetime.datetime.now(),
120 'date': datetime.datetime.now(),
121 'email': 'test@rhodecode.com',
121 'email': 'test@rhodecode.com',
122 'first_admin_email': User.get_first_super_admin().email
122 'first_admin_email': User.get_first_super_admin().email
123 },
123 },
124 'password_reset_confirmation': {
124 'password_reset_confirmation': {
125 'new_password': 'new-password-example',
125 'new_password': 'new-password-example',
126 'user': user,
126 'user': user,
127 'date': datetime.datetime.now(),
127 'date': datetime.datetime.now(),
128 'email': 'test@rhodecode.com',
128 'email': 'test@rhodecode.com',
129 'first_admin_email': User.get_first_super_admin().email
129 'first_admin_email': User.get_first_super_admin().email
130 },
130 },
131 'registration': {
131 'registration': {
132 'user': user,
132 'user': user,
133 'date': datetime.datetime.now(),
133 'date': datetime.datetime.now(),
134 },
134 },
135
135
136 'pull_request_comment': {
136 'pull_request_comment': {
137 'user': user,
137 'user': user,
138
138
139 'status_change': None,
139 'status_change': None,
140 'status_change_type': None,
140 'status_change_type': None,
141
141
142 'pull_request': pr,
142 'pull_request': pr,
143 'pull_request_commits': [],
143 'pull_request_commits': [],
144
144
145 'pull_request_target_repo': target_repo,
145 'pull_request_target_repo': target_repo,
146 'pull_request_target_repo_url': 'http://target-repo/url',
146 'pull_request_target_repo_url': 'http://target-repo/url',
147
147
148 'pull_request_source_repo': source_repo,
148 'pull_request_source_repo': source_repo,
149 'pull_request_source_repo_url': 'http://source-repo/url',
149 'pull_request_source_repo_url': 'http://source-repo/url',
150
150
151 'pull_request_url': 'http://localhost/pr1',
151 'pull_request_url': 'http://localhost/pr1',
152 'pr_comment_url': 'http://comment-url',
152 'pr_comment_url': 'http://comment-url',
153 'pr_comment_reply_url': 'http://comment-url#reply',
153 'pr_comment_reply_url': 'http://comment-url#reply',
154
154
155 'comment_file': None,
155 'comment_file': None,
156 'comment_line': None,
156 'comment_line': None,
157 'comment_type': 'note',
157 'comment_type': 'note',
158 'comment_body': 'This is my comment body. *I like !*',
158 'comment_body': 'This is my comment body. *I like !*',
159 'comment_id': 2048,
159 'comment_id': 2048,
160 'renderer_type': 'markdown',
160 'renderer_type': 'markdown',
161 'mention': True,
161 'mention': True,
162
162
163 },
163 },
164 'pull_request_comment+status': {
164 'pull_request_comment+status': {
165 'user': user,
165 'user': user,
166
166
167 'status_change': 'approved',
167 'status_change': 'approved',
168 'status_change_type': 'approved',
168 'status_change_type': 'approved',
169
169
170 'pull_request': pr,
170 'pull_request': pr,
171 'pull_request_commits': [],
171 'pull_request_commits': [],
172
172
173 'pull_request_target_repo': target_repo,
173 'pull_request_target_repo': target_repo,
174 'pull_request_target_repo_url': 'http://target-repo/url',
174 'pull_request_target_repo_url': 'http://target-repo/url',
175
175
176 'pull_request_source_repo': source_repo,
176 'pull_request_source_repo': source_repo,
177 'pull_request_source_repo_url': 'http://source-repo/url',
177 'pull_request_source_repo_url': 'http://source-repo/url',
178
178
179 'pull_request_url': 'http://localhost/pr1',
179 'pull_request_url': 'http://localhost/pr1',
180 'pr_comment_url': 'http://comment-url',
180 'pr_comment_url': 'http://comment-url',
181 'pr_comment_reply_url': 'http://comment-url#reply',
181 'pr_comment_reply_url': 'http://comment-url#reply',
182
182
183 'comment_type': 'todo',
183 'comment_type': 'todo',
184 'comment_file': None,
184 'comment_file': None,
185 'comment_line': None,
185 'comment_line': None,
186 'comment_body': '''
186 'comment_body': '''
187 I think something like this would be better
187 I think something like this would be better
188
188
189 ```py
189 ```py
190 // markdown renderer
190
191
191 def db():
192 def db():
192 global connection
193 global connection
193 return connection
194 return connection
194
195
195 ```
196 ```
196
197
197 ''',
198 ''',
198 'comment_id': 2048,
199 'comment_id': 2048,
199 'renderer_type': 'markdown',
200 'renderer_type': 'markdown',
200 'mention': True,
201 'mention': True,
201
202
202 },
203 },
203 'pull_request_comment+file': {
204 'pull_request_comment+file': {
204 'user': user,
205 'user': user,
205
206
206 'status_change': None,
207 'status_change': None,
207 'status_change_type': None,
208 'status_change_type': None,
208
209
209 'pull_request': pr,
210 'pull_request': pr,
210 'pull_request_commits': [],
211 'pull_request_commits': [],
211
212
212 'pull_request_target_repo': target_repo,
213 'pull_request_target_repo': target_repo,
213 'pull_request_target_repo_url': 'http://target-repo/url',
214 'pull_request_target_repo_url': 'http://target-repo/url',
214
215
215 'pull_request_source_repo': source_repo,
216 'pull_request_source_repo': source_repo,
216 'pull_request_source_repo_url': 'http://source-repo/url',
217 'pull_request_source_repo_url': 'http://source-repo/url',
217
218
218 'pull_request_url': 'http://localhost/pr1',
219 'pull_request_url': 'http://localhost/pr1',
219
220
220 'pr_comment_url': 'http://comment-url',
221 'pr_comment_url': 'http://comment-url',
221 'pr_comment_reply_url': 'http://comment-url#reply',
222 'pr_comment_reply_url': 'http://comment-url#reply',
222
223
223 'comment_file': 'rhodecode/model/get_flow_commits',
224 'comment_file': 'rhodecode/model/get_flow_commits',
224 'comment_line': 'o1210',
225 'comment_line': 'o1210',
225 'comment_type': 'todo',
226 'comment_type': 'todo',
226 'comment_body': '''
227 'comment_body': '''
227 I like this !
228 I like this !
228
229
229 But please check this code::
230 But please check this code
230
231
231 def main():
232 .. code-block:: javascript
232 print 'ok'
233
234 // THIS IS RST CODE
235
236 this.createResolutionComment = function(commentId) {
237 // hide the trigger text
238 $('#resolve-comment-{0}'.format(commentId)).hide();
239
240 var comment = $('#comment-'+commentId);
241 var commentData = comment.data();
242 if (commentData.commentInline) {
243 this.createComment(comment, commentId)
244 } else {
245 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
246 }
247
248 return false;
249 };
233
250
234 This should work better !
251 This should work better !
235 ''',
252 ''',
236 'comment_id': 2048,
253 'comment_id': 2048,
237 'renderer_type': 'rst',
254 'renderer_type': 'rst',
238 'mention': True,
255 'mention': True,
239
256
240 },
257 },
241
258
242 'pull_request_update': {
259 'pull_request_update': {
243 'updating_user': user,
260 'updating_user': user,
244
261
245 'status_change': None,
262 'status_change': None,
246 'status_change_type': None,
263 'status_change_type': None,
247
264
248 'pull_request': pr,
265 'pull_request': pr,
249 'pull_request_commits': [],
266 'pull_request_commits': [],
250
267
251 'pull_request_target_repo': target_repo,
268 'pull_request_target_repo': target_repo,
252 'pull_request_target_repo_url': 'http://target-repo/url',
269 'pull_request_target_repo_url': 'http://target-repo/url',
253
270
254 'pull_request_source_repo': source_repo,
271 'pull_request_source_repo': source_repo,
255 'pull_request_source_repo_url': 'http://source-repo/url',
272 'pull_request_source_repo_url': 'http://source-repo/url',
256
273
257 'pull_request_url': 'http://localhost/pr1',
274 'pull_request_url': 'http://localhost/pr1',
258
275
259 # update comment links
276 # update comment links
260 'pr_comment_url': 'http://comment-url',
277 'pr_comment_url': 'http://comment-url',
261 'pr_comment_reply_url': 'http://comment-url#reply',
278 'pr_comment_reply_url': 'http://comment-url#reply',
262 'ancestor_commit_id': 'f39bd443',
279 'ancestor_commit_id': 'f39bd443',
263 'added_commits': commit_changes.added,
280 'added_commits': commit_changes.added,
264 'removed_commits': commit_changes.removed,
281 'removed_commits': commit_changes.removed,
265 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
282 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
266 'added_files': file_changes.added,
283 'added_files': file_changes.added,
267 'modified_files': file_changes.modified,
284 'modified_files': file_changes.modified,
268 'removed_files': file_changes.removed,
285 'removed_files': file_changes.removed,
269 },
286 },
270
287
271 'cs_comment': {
288 'cs_comment': {
272 'user': user,
289 'user': user,
273 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
290 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
274 'status_change': None,
291 'status_change': None,
275 'status_change_type': None,
292 'status_change_type': None,
276
293
277 'commit_target_repo_url': 'http://foo.example.com/#comment1',
294 'commit_target_repo_url': 'http://foo.example.com/#comment1',
278 'repo_name': 'test-repo',
295 'repo_name': 'test-repo',
279 'comment_type': 'note',
296 'comment_type': 'note',
280 'comment_file': None,
297 'comment_file': None,
281 'comment_line': None,
298 'comment_line': None,
282 'commit_comment_url': 'http://comment-url',
299 'commit_comment_url': 'http://comment-url',
283 'commit_comment_reply_url': 'http://comment-url#reply',
300 'commit_comment_reply_url': 'http://comment-url#reply',
284 'comment_body': 'This is my comment body. *I like !*',
301 'comment_body': 'This is my comment body. *I like !*',
285 'comment_id': 2048,
302 'comment_id': 2048,
286 'renderer_type': 'markdown',
303 'renderer_type': 'markdown',
287 'mention': True,
304 'mention': True,
288 },
305 },
289 'cs_comment+status': {
306 'cs_comment+status': {
290 'user': user,
307 'user': user,
291 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
308 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
292 'status_change': 'approved',
309 'status_change': 'approved',
293 'status_change_type': 'approved',
310 'status_change_type': 'approved',
294
311
295 'commit_target_repo_url': 'http://foo.example.com/#comment1',
312 'commit_target_repo_url': 'http://foo.example.com/#comment1',
296 'repo_name': 'test-repo',
313 'repo_name': 'test-repo',
297 'comment_type': 'note',
314 'comment_type': 'note',
298 'comment_file': None,
315 'comment_file': None,
299 'comment_line': None,
316 'comment_line': None,
300 'commit_comment_url': 'http://comment-url',
317 'commit_comment_url': 'http://comment-url',
301 'commit_comment_reply_url': 'http://comment-url#reply',
318 'commit_comment_reply_url': 'http://comment-url#reply',
302 'comment_body': '''
319 'comment_body': '''
303 Hello **world**
320 Hello **world**
304
321
305 This is a multiline comment :)
322 This is a multiline comment :)
306
323
307 - list
324 - list
308 - list2
325 - list2
309 ''',
326 ''',
310 'comment_id': 2048,
327 'comment_id': 2048,
311 'renderer_type': 'markdown',
328 'renderer_type': 'markdown',
312 'mention': True,
329 'mention': True,
313 },
330 },
314 'cs_comment+file': {
331 'cs_comment+file': {
315 'user': user,
332 'user': user,
316 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
333 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
317 'status_change': None,
334 'status_change': None,
318 'status_change_type': None,
335 'status_change_type': None,
319
336
320 'commit_target_repo_url': 'http://foo.example.com/#comment1',
337 'commit_target_repo_url': 'http://foo.example.com/#comment1',
321 'repo_name': 'test-repo',
338 'repo_name': 'test-repo',
322
339
323 'comment_type': 'note',
340 'comment_type': 'note',
324 'comment_file': 'test-file.py',
341 'comment_file': 'test-file.py',
325 'comment_line': 'n100',
342 'comment_line': 'n100',
326
343
327 'commit_comment_url': 'http://comment-url',
344 'commit_comment_url': 'http://comment-url',
328 'commit_comment_reply_url': 'http://comment-url#reply',
345 'commit_comment_reply_url': 'http://comment-url#reply',
329 'comment_body': 'This is my comment body. *I like !*',
346 'comment_body': 'This is my comment body. *I like !*',
330 'comment_id': 2048,
347 'comment_id': 2048,
331 'renderer_type': 'markdown',
348 'renderer_type': 'markdown',
332 'mention': True,
349 'mention': True,
333 },
350 },
334
351
335 'pull_request': {
352 'pull_request': {
336 'user': user,
353 'user': user,
337 'pull_request': pr,
354 'pull_request': pr,
338 'pull_request_commits': [
355 'pull_request_commits': [
339 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
356 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
340 my-account: moved email closer to profile as it's similar data just moved outside.
357 my-account: moved email closer to profile as it's similar data just moved outside.
341 '''),
358 '''),
342 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
359 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
343 users: description edit fixes
360 users: description edit fixes
344
361
345 - tests
362 - tests
346 - added metatags info
363 - added metatags info
347 '''),
364 '''),
348 ],
365 ],
349
366
350 'pull_request_target_repo': target_repo,
367 'pull_request_target_repo': target_repo,
351 'pull_request_target_repo_url': 'http://target-repo/url',
368 'pull_request_target_repo_url': 'http://target-repo/url',
352
369
353 'pull_request_source_repo': source_repo,
370 'pull_request_source_repo': source_repo,
354 'pull_request_source_repo_url': 'http://source-repo/url',
371 'pull_request_source_repo_url': 'http://source-repo/url',
355
372
356 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
373 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
357 }
374 }
358
375
359 }
376 }
360
377
361 template_type = email_id.split('+')[0]
378 template_type = email_id.split('+')[0]
362 (c.subject, c.headers, c.email_body,
379 (c.subject, c.headers, c.email_body,
363 c.email_body_plaintext) = EmailNotificationModel().render_email(
380 c.email_body_plaintext) = EmailNotificationModel().render_email(
364 template_type, **email_kwargs.get(email_id, {}))
381 template_type, **email_kwargs.get(email_id, {}))
365
382
366 test_email = self.request.GET.get('email')
383 test_email = self.request.GET.get('email')
367 if test_email:
384 if test_email:
368 recipients = [test_email]
385 recipients = [test_email]
369 run_task(tasks.send_email, recipients, c.subject,
386 run_task(tasks.send_email, recipients, c.subject,
370 c.email_body_plaintext, c.email_body)
387 c.email_body_plaintext, c.email_body)
371
388
372 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
389 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
373 template = 'debug_style/email_plain_rendered.mako'
390 template = 'debug_style/email_plain_rendered.mako'
374 else:
391 else:
375 template = 'debug_style/email.mako'
392 template = 'debug_style/email.mako'
376 return render_to_response(
393 return render_to_response(
377 template, self._get_template_context(c),
394 template, self._get_template_context(c),
378 request=self.request)
395 request=self.request)
379
396
380 @view_config(
397 @view_config(
381 route_name='debug_style_template', request_method='GET',
398 route_name='debug_style_template', request_method='GET',
382 renderer=None)
399 renderer=None)
383 def template(self):
400 def template(self):
384 t_path = self.request.matchdict['t_path']
401 t_path = self.request.matchdict['t_path']
385 c = self.load_default_context()
402 c = self.load_default_context()
386 c.active = os.path.splitext(t_path)[0]
403 c.active = os.path.splitext(t_path)[0]
387 c.came_from = ''
404 c.came_from = ''
388 c.email_types = {
405 c.email_types = {
389 'cs_comment+file': {},
406 'cs_comment+file': {},
390 'cs_comment+status': {},
407 'cs_comment+status': {},
391
408
392 'pull_request_comment+file': {},
409 'pull_request_comment+file': {},
393 'pull_request_comment+status': {},
410 'pull_request_comment+status': {},
394
411
395 'pull_request_update': {},
412 'pull_request_update': {},
396 }
413 }
397 c.email_types.update(EmailNotificationModel.email_types)
414 c.email_types.update(EmailNotificationModel.email_types)
398
415
399 return render_to_response(
416 return render_to_response(
400 'debug_style/' + t_path, self._get_template_context(c),
417 'debug_style/' + t_path, self._get_template_context(c),
401 request=self.request)
418 request=self.request)
402
419
@@ -1,179 +1,179 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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
21
22 import pytest
22 import pytest
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.model.db import Repository, RepoGroup, User
25 from rhodecode.model.db import Repository, RepoGroup, User
26 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
27 from rhodecode.model.repo import RepoModel
27 from rhodecode.model.repo import RepoModel
28 from rhodecode.model.repo_group import RepoGroupModel
28 from rhodecode.model.repo_group import RepoGroupModel
29 from rhodecode.model.settings import SettingsModel
29 from rhodecode.model.settings import SettingsModel
30 from rhodecode.tests import TestController
30 from rhodecode.tests import TestController
31 from rhodecode.tests.fixture import Fixture
31 from rhodecode.tests.fixture import Fixture
32 from rhodecode.lib import helpers as h
32 from rhodecode.lib import helpers as h
33
33
34 fixture = Fixture()
34 fixture = Fixture()
35
35
36
36
37 def route_path(name, **kwargs):
37 def route_path(name, **kwargs):
38 return {
38 return {
39 'home': '/',
39 'home': '/',
40 'main_page_repos_data': '/_home_repos',
40 'main_page_repos_data': '/_home_repos',
41 'main_page_repo_groups_data': '/_home_repo_groups',
41 'main_page_repo_groups_data': '/_home_repo_groups',
42 'repo_group_home': '/{repo_group_name}'
42 'repo_group_home': '/{repo_group_name}'
43 }[name].format(**kwargs)
43 }[name].format(**kwargs)
44
44
45
45
46 class TestHomeController(TestController):
46 class TestHomeController(TestController):
47
47
48 def test_index(self):
48 def test_index(self):
49 self.log_user()
49 self.log_user()
50 response = self.app.get(route_path('home'))
50 response = self.app.get(route_path('home'))
51 # if global permission is set
51 # if global permission is set
52 response.mustcontain('New Repository')
52 response.mustcontain('New Repository')
53
53
54 def test_index_grid_repos(self, xhr_header):
54 def test_index_grid_repos(self, xhr_header):
55 self.log_user()
55 self.log_user()
56 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
56 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
57 # search for objects inside the JavaScript JSON
57 # search for objects inside the JavaScript JSON
58 for obj in Repository.getAll():
58 for obj in Repository.getAll():
59 response.mustcontain('<a href=\\"/{}\\">'.format(obj.repo_name))
59 response.mustcontain('<a href=\\"/{}\\">'.format(obj.repo_name))
60
60
61 def test_index_grid_repo_groups(self, xhr_header):
61 def test_index_grid_repo_groups(self, xhr_header):
62 self.log_user()
62 self.log_user()
63 response = self.app.get(route_path('main_page_repo_groups_data'),
63 response = self.app.get(route_path('main_page_repo_groups_data'),
64 extra_environ=xhr_header,)
64 extra_environ=xhr_header,)
65
65
66 # search for objects inside the JavaScript JSON
66 # search for objects inside the JavaScript JSON
67 for obj in RepoGroup.getAll():
67 for obj in RepoGroup.getAll():
68 response.mustcontain('<a href=\\"/{}\\">'.format(obj.group_name))
68 response.mustcontain('<a href=\\"/{}\\">'.format(obj.group_name))
69
69
70 def test_index_grid_repo_groups_without_access(self, xhr_header, user_util):
70 def test_index_grid_repo_groups_without_access(self, xhr_header, user_util):
71 user = user_util.create_user(password='qweqwe')
71 user = user_util.create_user(password='qweqwe')
72 group_ok = user_util.create_repo_group(owner=user)
72 group_ok = user_util.create_repo_group(owner=user)
73 group_id_ok = group_ok.group_id
73 group_id_ok = group_ok.group_id
74
74
75 group_forbidden = user_util.create_repo_group(owner=User.get_first_super_admin())
75 group_forbidden = user_util.create_repo_group(owner=User.get_first_super_admin())
76 group_id_forbidden = group_forbidden.group_id
76 group_id_forbidden = group_forbidden.group_id
77
77
78 user_util.grant_user_permission_to_repo_group(group_forbidden, user, 'group.none')
78 user_util.grant_user_permission_to_repo_group(group_forbidden, user, 'group.none')
79 self.log_user(user.username, 'qweqwe')
79 self.log_user(user.username, 'qweqwe')
80
80
81 self.app.get(route_path('main_page_repo_groups_data'),
81 self.app.get(route_path('main_page_repo_groups_data'),
82 extra_environ=xhr_header,
82 extra_environ=xhr_header,
83 params={'repo_group_id': group_id_ok}, status=200)
83 params={'repo_group_id': group_id_ok}, status=200)
84
84
85 self.app.get(route_path('main_page_repo_groups_data'),
85 self.app.get(route_path('main_page_repo_groups_data'),
86 extra_environ=xhr_header,
86 extra_environ=xhr_header,
87 params={'repo_group_id': group_id_forbidden}, status=404)
87 params={'repo_group_id': group_id_forbidden}, status=404)
88
88
89 def test_index_contains_statics_with_ver(self):
89 def test_index_contains_statics_with_ver(self):
90 from rhodecode.lib.base import calculate_version_hash
90 from rhodecode.lib.base import calculate_version_hash
91
91
92 self.log_user()
92 self.log_user()
93 response = self.app.get(route_path('home'))
93 response = self.app.get(route_path('home'))
94
94
95 rhodecode_version_hash = calculate_version_hash(
95 rhodecode_version_hash = calculate_version_hash(
96 {'beaker.session.secret': 'test-rc-uytcxaz'})
96 {'beaker.session.secret': 'test-rc-uytcxaz'})
97 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
97 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
98 response.mustcontain('scripts.min.js?ver={0}'.format(rhodecode_version_hash))
98 response.mustcontain('scripts.min.js?ver={0}'.format(rhodecode_version_hash))
99
99
100 def test_index_contains_backend_specific_details(self, backend, xhr_header):
100 def test_index_contains_backend_specific_details(self, backend, xhr_header):
101 self.log_user()
101 self.log_user()
102 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
102 response = self.app.get(route_path('main_page_repos_data'), extra_environ=xhr_header)
103 tip = backend.repo.get_commit().raw_id
103 tip = backend.repo.get_commit().raw_id
104
104
105 # html in javascript variable:
105 # html in javascript variable:
106 response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
106 response.mustcontain(r'<i class=\"icon-%s\"' % (backend.alias, ))
107 response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
107 response.mustcontain(r'href=\"/%s\"' % (backend.repo_name, ))
108
108
109 response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
109 response.mustcontain("""/%s/changeset/%s""" % (backend.repo_name, tip))
110 response.mustcontain("""Added a symlink""")
110 response.mustcontain("""Added a symlink""")
111
111
112 def test_index_with_anonymous_access_disabled(self):
112 def test_index_with_anonymous_access_disabled(self):
113 with fixture.anon_access(False):
113 with fixture.anon_access(False):
114 response = self.app.get(route_path('home'), status=302)
114 response = self.app.get(route_path('home'), status=302)
115 assert 'login' in response.location
115 assert 'login' in response.location
116
116
117 def test_index_page_on_groups_with_wrong_group_id(self, autologin_user, xhr_header):
117 def test_index_page_on_groups_with_wrong_group_id(self, autologin_user, xhr_header):
118 group_id = 918123
118 group_id = 918123
119 self.app.get(
119 self.app.get(
120 route_path('main_page_repo_groups_data'),
120 route_path('main_page_repo_groups_data'),
121 params={'repo_group_id': group_id},
121 params={'repo_group_id': group_id},
122 status=404, extra_environ=xhr_header)
122 status=404, extra_environ=xhr_header)
123
123
124 def test_index_page_on_groups(self, autologin_user, user_util, xhr_header):
124 def test_index_page_on_groups(self, autologin_user, user_util, xhr_header):
125 gr = user_util.create_repo_group()
125 gr = user_util.create_repo_group()
126 repo = user_util.create_repo(parent=gr)
126 repo = user_util.create_repo(parent=gr)
127 repo_name = repo.repo_name
127 repo_name = repo.repo_name
128 group_id = gr.group_id
128 group_id = gr.group_id
129
129
130 response = self.app.get(route_path(
130 response = self.app.get(route_path(
131 'repo_group_home', repo_group_name=gr.group_name))
131 'repo_group_home', repo_group_name=gr.group_name))
132 response.mustcontain('d.repo_group_id = {}'.format(group_id))
132 response.mustcontain('d.repo_group_id = {}'.format(group_id))
133
133
134 response = self.app.get(
134 response = self.app.get(
135 route_path('main_page_repos_data'),
135 route_path('main_page_repos_data'),
136 params={'repo_group_id': group_id},
136 params={'repo_group_id': group_id},
137 extra_environ=xhr_header,)
137 extra_environ=xhr_header,)
138 response.mustcontain(repo_name)
138 response.mustcontain(repo_name)
139
139
140 def test_index_page_on_group_with_trailing_slash(self, autologin_user, user_util, xhr_header):
140 def test_index_page_on_group_with_trailing_slash(self, autologin_user, user_util, xhr_header):
141 gr = user_util.create_repo_group()
141 gr = user_util.create_repo_group()
142 repo = user_util.create_repo(parent=gr)
142 repo = user_util.create_repo(parent=gr)
143 repo_name = repo.repo_name
143 repo_name = repo.repo_name
144 group_id = gr.group_id
144 group_id = gr.group_id
145
145
146 response = self.app.get(route_path(
146 response = self.app.get(route_path(
147 'repo_group_home', repo_group_name=gr.group_name+'/'))
147 'repo_group_home', repo_group_name=gr.group_name+'/'))
148 response.mustcontain('d.repo_group_id = {}'.format(group_id))
148 response.mustcontain('d.repo_group_id = {}'.format(group_id))
149
149
150 response = self.app.get(
150 response = self.app.get(
151 route_path('main_page_repos_data'),
151 route_path('main_page_repos_data'),
152 params={'repo_group_id': group_id},
152 params={'repo_group_id': group_id},
153 extra_environ=xhr_header, )
153 extra_environ=xhr_header, )
154 response.mustcontain(repo_name)
154 response.mustcontain(repo_name)
155
155
156 @pytest.mark.parametrize("name, state", [
156 @pytest.mark.parametrize("name, state", [
157 ('Disabled', False),
157 ('Disabled', False),
158 ('Enabled', True),
158 ('Enabled', True),
159 ])
159 ])
160 def test_index_show_version(self, autologin_user, name, state):
160 def test_index_show_version(self, autologin_user, name, state):
161 version_string = 'RhodeCode Enterprise %s' % rhodecode.__version__
161 version_string = 'RhodeCode %s' % rhodecode.__version__
162
162
163 sett = SettingsModel().create_or_update_setting(
163 sett = SettingsModel().create_or_update_setting(
164 'show_version', state, 'bool')
164 'show_version', state, 'bool')
165 Session().add(sett)
165 Session().add(sett)
166 Session().commit()
166 Session().commit()
167 SettingsModel().invalidate_settings_cache()
167 SettingsModel().invalidate_settings_cache()
168
168
169 response = self.app.get(route_path('home'))
169 response = self.app.get(route_path('home'))
170 if state is True:
170 if state is True:
171 response.mustcontain(version_string)
171 response.mustcontain(version_string)
172 if state is False:
172 if state is False:
173 response.mustcontain(no=[version_string])
173 response.mustcontain(no=[version_string])
174
174
175 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
175 def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
176 response = self.app.get(route_path('home'))
176 response = self.app.get(route_path('home'))
177 assert_response = response.assert_response()
177 assert_response = response.assert_response()
178 element = assert_response.get_element('.logout [name=csrf_token]')
178 element = assert_response.get_element('.logout [name=csrf_token]')
179 assert element.value == csrf_token
179 assert element.value == csrf_token
@@ -1,1070 +1,1070 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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
22
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
26 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
27 from rhodecode.apps.repository.views.repo_files import RepoFilesView
27 from rhodecode.apps.repository.views.repo_files import RepoFilesView
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.compat import OrderedDict
29 from rhodecode.lib.compat import OrderedDict
30 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.ext_json import json
31 from rhodecode.lib.vcs import nodes
31 from rhodecode.lib.vcs import nodes
32
32
33 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.lib.vcs.conf import settings
34 from rhodecode.tests import assert_session_flash
34 from rhodecode.tests import assert_session_flash
35 from rhodecode.tests.fixture import Fixture
35 from rhodecode.tests.fixture import Fixture
36 from rhodecode.model.db import Session
36 from rhodecode.model.db import Session
37
37
38 fixture = Fixture()
38 fixture = Fixture()
39
39
40
40
41 def get_node_history(backend_type):
41 def get_node_history(backend_type):
42 return {
42 return {
43 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
43 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
44 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
44 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
45 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
45 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
46 }[backend_type]
46 }[backend_type]
47
47
48
48
49 def route_path(name, params=None, **kwargs):
49 def route_path(name, params=None, **kwargs):
50 import urllib
50 import urllib
51
51
52 base_url = {
52 base_url = {
53 'repo_summary': '/{repo_name}',
53 'repo_summary': '/{repo_name}',
54 'repo_archivefile': '/{repo_name}/archive/{fname}',
54 'repo_archivefile': '/{repo_name}/archive/{fname}',
55 'repo_files_diff': '/{repo_name}/diff/{f_path}',
55 'repo_files_diff': '/{repo_name}/diff/{f_path}',
56 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
56 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
57 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
57 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
58 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
58 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
59 'repo_files:default_commit': '/{repo_name}/files',
59 'repo_files:default_commit': '/{repo_name}/files',
60 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
60 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
61 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
61 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
62 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
62 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
63 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
63 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
64 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
64 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
65 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
65 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
66 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
66 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
67 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
67 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
68 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
68 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
69 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
69 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
70 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
70 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
71 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
71 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
72 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
72 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
73 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
73 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
74 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
74 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
75 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
75 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
76 }[name].format(**kwargs)
76 }[name].format(**kwargs)
77
77
78 if params:
78 if params:
79 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
79 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
80 return base_url
80 return base_url
81
81
82
82
83 def assert_files_in_response(response, files, params):
83 def assert_files_in_response(response, files, params):
84 template = (
84 template = (
85 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
85 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
86 _assert_items_in_response(response, files, template, params)
86 _assert_items_in_response(response, files, template, params)
87
87
88
88
89 def assert_dirs_in_response(response, dirs, params):
89 def assert_dirs_in_response(response, dirs, params):
90 template = (
90 template = (
91 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
91 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
92 _assert_items_in_response(response, dirs, template, params)
92 _assert_items_in_response(response, dirs, template, params)
93
93
94
94
95 def _assert_items_in_response(response, items, template, params):
95 def _assert_items_in_response(response, items, template, params):
96 for item in items:
96 for item in items:
97 item_params = {'name': item}
97 item_params = {'name': item}
98 item_params.update(params)
98 item_params.update(params)
99 response.mustcontain(template % item_params)
99 response.mustcontain(template % item_params)
100
100
101
101
102 def assert_timeago_in_response(response, items, params):
102 def assert_timeago_in_response(response, items, params):
103 for item in items:
103 for item in items:
104 response.mustcontain(h.age_component(params['date']))
104 response.mustcontain(h.age_component(params['date']))
105
105
106
106
107 @pytest.mark.usefixtures("app")
107 @pytest.mark.usefixtures("app")
108 class TestFilesViews(object):
108 class TestFilesViews(object):
109
109
110 def test_show_files(self, backend):
110 def test_show_files(self, backend):
111 response = self.app.get(
111 response = self.app.get(
112 route_path('repo_files',
112 route_path('repo_files',
113 repo_name=backend.repo_name,
113 repo_name=backend.repo_name,
114 commit_id='tip', f_path='/'))
114 commit_id='tip', f_path='/'))
115 commit = backend.repo.get_commit()
115 commit = backend.repo.get_commit()
116
116
117 params = {
117 params = {
118 'repo_name': backend.repo_name,
118 'repo_name': backend.repo_name,
119 'commit_id': commit.raw_id,
119 'commit_id': commit.raw_id,
120 'date': commit.date
120 'date': commit.date
121 }
121 }
122 assert_dirs_in_response(response, ['docs', 'vcs'], params)
122 assert_dirs_in_response(response, ['docs', 'vcs'], params)
123 files = [
123 files = [
124 '.gitignore',
124 '.gitignore',
125 '.hgignore',
125 '.hgignore',
126 '.hgtags',
126 '.hgtags',
127 # TODO: missing in Git
127 # TODO: missing in Git
128 # '.travis.yml',
128 # '.travis.yml',
129 'MANIFEST.in',
129 'MANIFEST.in',
130 'README.rst',
130 'README.rst',
131 # TODO: File is missing in svn repository
131 # TODO: File is missing in svn repository
132 # 'run_test_and_report.sh',
132 # 'run_test_and_report.sh',
133 'setup.cfg',
133 'setup.cfg',
134 'setup.py',
134 'setup.py',
135 'test_and_report.sh',
135 'test_and_report.sh',
136 'tox.ini',
136 'tox.ini',
137 ]
137 ]
138 assert_files_in_response(response, files, params)
138 assert_files_in_response(response, files, params)
139 assert_timeago_in_response(response, files, params)
139 assert_timeago_in_response(response, files, params)
140
140
141 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
141 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
142 repo = backend_hg['subrepos']
142 repo = backend_hg['subrepos']
143 response = self.app.get(
143 response = self.app.get(
144 route_path('repo_files',
144 route_path('repo_files',
145 repo_name=repo.repo_name,
145 repo_name=repo.repo_name,
146 commit_id='tip', f_path='/'))
146 commit_id='tip', f_path='/'))
147 assert_response = response.assert_response()
147 assert_response = response.assert_response()
148 assert_response.contains_one_link(
148 assert_response.contains_one_link(
149 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
149 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
150
150
151 def test_show_files_links_submodules_with_absolute_url_subpaths(
151 def test_show_files_links_submodules_with_absolute_url_subpaths(
152 self, backend_hg):
152 self, backend_hg):
153 repo = backend_hg['subrepos']
153 repo = backend_hg['subrepos']
154 response = self.app.get(
154 response = self.app.get(
155 route_path('repo_files',
155 route_path('repo_files',
156 repo_name=repo.repo_name,
156 repo_name=repo.repo_name,
157 commit_id='tip', f_path='/'))
157 commit_id='tip', f_path='/'))
158 assert_response = response.assert_response()
158 assert_response = response.assert_response()
159 assert_response.contains_one_link(
159 assert_response.contains_one_link(
160 'subpaths-path @ 000000000000',
160 'subpaths-path @ 000000000000',
161 'http://sub-base.example.com/subpaths-path')
161 'http://sub-base.example.com/subpaths-path')
162
162
163 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
163 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
164 def test_files_menu(self, backend):
164 def test_files_menu(self, backend):
165 new_branch = "temp_branch_name"
165 new_branch = "temp_branch_name"
166 commits = [
166 commits = [
167 {'message': 'a'},
167 {'message': 'a'},
168 {'message': 'b', 'branch': new_branch}
168 {'message': 'b', 'branch': new_branch}
169 ]
169 ]
170 backend.create_repo(commits)
170 backend.create_repo(commits)
171 backend.repo.landing_rev = "branch:%s" % new_branch
171 backend.repo.landing_rev = "branch:%s" % new_branch
172 Session().commit()
172 Session().commit()
173
173
174 # get response based on tip and not new commit
174 # get response based on tip and not new commit
175 response = self.app.get(
175 response = self.app.get(
176 route_path('repo_files',
176 route_path('repo_files',
177 repo_name=backend.repo_name,
177 repo_name=backend.repo_name,
178 commit_id='tip', f_path='/'))
178 commit_id='tip', f_path='/'))
179
179
180 # make sure Files menu url is not tip but new commit
180 # make sure Files menu url is not tip but new commit
181 landing_rev = backend.repo.landing_rev[1]
181 landing_rev = backend.repo.landing_ref_name
182 files_url = route_path('repo_files:default_path',
182 files_url = route_path('repo_files:default_path',
183 repo_name=backend.repo_name,
183 repo_name=backend.repo_name,
184 commit_id=landing_rev)
184 commit_id=landing_rev, params={'at': landing_rev})
185
185
186 assert landing_rev != 'tip'
186 assert landing_rev != 'tip'
187 response.mustcontain(
187 response.mustcontain(
188 '<li class="active"><a class="menulink" href="%s">' % files_url)
188 '<li class="active"><a class="menulink" href="%s">' % files_url)
189
189
190 def test_show_files_commit(self, backend):
190 def test_show_files_commit(self, backend):
191 commit = backend.repo.get_commit(commit_idx=32)
191 commit = backend.repo.get_commit(commit_idx=32)
192
192
193 response = self.app.get(
193 response = self.app.get(
194 route_path('repo_files',
194 route_path('repo_files',
195 repo_name=backend.repo_name,
195 repo_name=backend.repo_name,
196 commit_id=commit.raw_id, f_path='/'))
196 commit_id=commit.raw_id, f_path='/'))
197
197
198 dirs = ['docs', 'tests']
198 dirs = ['docs', 'tests']
199 files = ['README.rst']
199 files = ['README.rst']
200 params = {
200 params = {
201 'repo_name': backend.repo_name,
201 'repo_name': backend.repo_name,
202 'commit_id': commit.raw_id,
202 'commit_id': commit.raw_id,
203 }
203 }
204 assert_dirs_in_response(response, dirs, params)
204 assert_dirs_in_response(response, dirs, params)
205 assert_files_in_response(response, files, params)
205 assert_files_in_response(response, files, params)
206
206
207 def test_show_files_different_branch(self, backend):
207 def test_show_files_different_branch(self, backend):
208 branches = dict(
208 branches = dict(
209 hg=(150, ['git']),
209 hg=(150, ['git']),
210 # TODO: Git test repository does not contain other branches
210 # TODO: Git test repository does not contain other branches
211 git=(633, ['master']),
211 git=(633, ['master']),
212 # TODO: Branch support in Subversion
212 # TODO: Branch support in Subversion
213 svn=(150, [])
213 svn=(150, [])
214 )
214 )
215 idx, branches = branches[backend.alias]
215 idx, branches = branches[backend.alias]
216 commit = backend.repo.get_commit(commit_idx=idx)
216 commit = backend.repo.get_commit(commit_idx=idx)
217 response = self.app.get(
217 response = self.app.get(
218 route_path('repo_files',
218 route_path('repo_files',
219 repo_name=backend.repo_name,
219 repo_name=backend.repo_name,
220 commit_id=commit.raw_id, f_path='/'))
220 commit_id=commit.raw_id, f_path='/'))
221
221
222 assert_response = response.assert_response()
222 assert_response = response.assert_response()
223 for branch in branches:
223 for branch in branches:
224 assert_response.element_contains('.tags .branchtag', branch)
224 assert_response.element_contains('.tags .branchtag', branch)
225
225
226 def test_show_files_paging(self, backend):
226 def test_show_files_paging(self, backend):
227 repo = backend.repo
227 repo = backend.repo
228 indexes = [73, 92, 109, 1, 0]
228 indexes = [73, 92, 109, 1, 0]
229 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
229 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
230 for rev in indexes]
230 for rev in indexes]
231
231
232 for idx in idx_map:
232 for idx in idx_map:
233 response = self.app.get(
233 response = self.app.get(
234 route_path('repo_files',
234 route_path('repo_files',
235 repo_name=backend.repo_name,
235 repo_name=backend.repo_name,
236 commit_id=idx[1], f_path='/'))
236 commit_id=idx[1], f_path='/'))
237
237
238 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
238 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
239
239
240 def test_file_source(self, backend):
240 def test_file_source(self, backend):
241 commit = backend.repo.get_commit(commit_idx=167)
241 commit = backend.repo.get_commit(commit_idx=167)
242 response = self.app.get(
242 response = self.app.get(
243 route_path('repo_files',
243 route_path('repo_files',
244 repo_name=backend.repo_name,
244 repo_name=backend.repo_name,
245 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
245 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
246
246
247 msgbox = """<div class="commit">%s</div>"""
247 msgbox = """<div class="commit">%s</div>"""
248 response.mustcontain(msgbox % (commit.message, ))
248 response.mustcontain(msgbox % (commit.message, ))
249
249
250 assert_response = response.assert_response()
250 assert_response = response.assert_response()
251 if commit.branch:
251 if commit.branch:
252 assert_response.element_contains(
252 assert_response.element_contains(
253 '.tags.tags-main .branchtag', commit.branch)
253 '.tags.tags-main .branchtag', commit.branch)
254 if commit.tags:
254 if commit.tags:
255 for tag in commit.tags:
255 for tag in commit.tags:
256 assert_response.element_contains('.tags.tags-main .tagtag', tag)
256 assert_response.element_contains('.tags.tags-main .tagtag', tag)
257
257
258 def test_file_source_annotated(self, backend):
258 def test_file_source_annotated(self, backend):
259 response = self.app.get(
259 response = self.app.get(
260 route_path('repo_files:annotated',
260 route_path('repo_files:annotated',
261 repo_name=backend.repo_name,
261 repo_name=backend.repo_name,
262 commit_id='tip', f_path='vcs/nodes.py'))
262 commit_id='tip', f_path='vcs/nodes.py'))
263 expected_commits = {
263 expected_commits = {
264 'hg': 'r356',
264 'hg': 'r356',
265 'git': 'r345',
265 'git': 'r345',
266 'svn': 'r208',
266 'svn': 'r208',
267 }
267 }
268 response.mustcontain(expected_commits[backend.alias])
268 response.mustcontain(expected_commits[backend.alias])
269
269
270 def test_file_source_authors(self, backend):
270 def test_file_source_authors(self, backend):
271 response = self.app.get(
271 response = self.app.get(
272 route_path('repo_file_authors',
272 route_path('repo_file_authors',
273 repo_name=backend.repo_name,
273 repo_name=backend.repo_name,
274 commit_id='tip', f_path='vcs/nodes.py'))
274 commit_id='tip', f_path='vcs/nodes.py'))
275 expected_authors = {
275 expected_authors = {
276 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
276 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
278 'svn': ('marcin', 'lukasz'),
278 'svn': ('marcin', 'lukasz'),
279 }
279 }
280
280
281 for author in expected_authors[backend.alias]:
281 for author in expected_authors[backend.alias]:
282 response.mustcontain(author)
282 response.mustcontain(author)
283
283
284 def test_file_source_authors_with_annotation(self, backend):
284 def test_file_source_authors_with_annotation(self, backend):
285 response = self.app.get(
285 response = self.app.get(
286 route_path('repo_file_authors',
286 route_path('repo_file_authors',
287 repo_name=backend.repo_name,
287 repo_name=backend.repo_name,
288 commit_id='tip', f_path='vcs/nodes.py',
288 commit_id='tip', f_path='vcs/nodes.py',
289 params=dict(annotate=1)))
289 params=dict(annotate=1)))
290 expected_authors = {
290 expected_authors = {
291 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
291 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
293 'svn': ('marcin', 'lukasz'),
293 'svn': ('marcin', 'lukasz'),
294 }
294 }
295
295
296 for author in expected_authors[backend.alias]:
296 for author in expected_authors[backend.alias]:
297 response.mustcontain(author)
297 response.mustcontain(author)
298
298
299 def test_file_source_history(self, backend, xhr_header):
299 def test_file_source_history(self, backend, xhr_header):
300 response = self.app.get(
300 response = self.app.get(
301 route_path('repo_file_history',
301 route_path('repo_file_history',
302 repo_name=backend.repo_name,
302 repo_name=backend.repo_name,
303 commit_id='tip', f_path='vcs/nodes.py'),
303 commit_id='tip', f_path='vcs/nodes.py'),
304 extra_environ=xhr_header)
304 extra_environ=xhr_header)
305 assert get_node_history(backend.alias) == json.loads(response.body)
305 assert get_node_history(backend.alias) == json.loads(response.body)
306
306
307 def test_file_source_history_svn(self, backend_svn, xhr_header):
307 def test_file_source_history_svn(self, backend_svn, xhr_header):
308 simple_repo = backend_svn['svn-simple-layout']
308 simple_repo = backend_svn['svn-simple-layout']
309 response = self.app.get(
309 response = self.app.get(
310 route_path('repo_file_history',
310 route_path('repo_file_history',
311 repo_name=simple_repo.repo_name,
311 repo_name=simple_repo.repo_name,
312 commit_id='tip', f_path='trunk/example.py'),
312 commit_id='tip', f_path='trunk/example.py'),
313 extra_environ=xhr_header)
313 extra_environ=xhr_header)
314
314
315 expected_data = json.loads(
315 expected_data = json.loads(
316 fixture.load_resource('svn_node_history_branches.json'))
316 fixture.load_resource('svn_node_history_branches.json'))
317
317
318 assert expected_data == response.json
318 assert expected_data == response.json
319
319
320 def test_file_source_history_with_annotation(self, backend, xhr_header):
320 def test_file_source_history_with_annotation(self, backend, xhr_header):
321 response = self.app.get(
321 response = self.app.get(
322 route_path('repo_file_history',
322 route_path('repo_file_history',
323 repo_name=backend.repo_name,
323 repo_name=backend.repo_name,
324 commit_id='tip', f_path='vcs/nodes.py',
324 commit_id='tip', f_path='vcs/nodes.py',
325 params=dict(annotate=1)),
325 params=dict(annotate=1)),
326
326
327 extra_environ=xhr_header)
327 extra_environ=xhr_header)
328 assert get_node_history(backend.alias) == json.loads(response.body)
328 assert get_node_history(backend.alias) == json.loads(response.body)
329
329
330 def test_tree_search_top_level(self, backend, xhr_header):
330 def test_tree_search_top_level(self, backend, xhr_header):
331 commit = backend.repo.get_commit(commit_idx=173)
331 commit = backend.repo.get_commit(commit_idx=173)
332 response = self.app.get(
332 response = self.app.get(
333 route_path('repo_files_nodelist',
333 route_path('repo_files_nodelist',
334 repo_name=backend.repo_name,
334 repo_name=backend.repo_name,
335 commit_id=commit.raw_id, f_path='/'),
335 commit_id=commit.raw_id, f_path='/'),
336 extra_environ=xhr_header)
336 extra_environ=xhr_header)
337 assert 'nodes' in response.json
337 assert 'nodes' in response.json
338 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
338 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
339
339
340 def test_tree_search_missing_xhr(self, backend):
340 def test_tree_search_missing_xhr(self, backend):
341 self.app.get(
341 self.app.get(
342 route_path('repo_files_nodelist',
342 route_path('repo_files_nodelist',
343 repo_name=backend.repo_name,
343 repo_name=backend.repo_name,
344 commit_id='tip', f_path='/'),
344 commit_id='tip', f_path='/'),
345 status=404)
345 status=404)
346
346
347 def test_tree_search_at_path(self, backend, xhr_header):
347 def test_tree_search_at_path(self, backend, xhr_header):
348 commit = backend.repo.get_commit(commit_idx=173)
348 commit = backend.repo.get_commit(commit_idx=173)
349 response = self.app.get(
349 response = self.app.get(
350 route_path('repo_files_nodelist',
350 route_path('repo_files_nodelist',
351 repo_name=backend.repo_name,
351 repo_name=backend.repo_name,
352 commit_id=commit.raw_id, f_path='/docs'),
352 commit_id=commit.raw_id, f_path='/docs'),
353 extra_environ=xhr_header)
353 extra_environ=xhr_header)
354 assert 'nodes' in response.json
354 assert 'nodes' in response.json
355 nodes = response.json['nodes']
355 nodes = response.json['nodes']
356 assert {'name': 'docs/api', 'type': 'dir'} in nodes
356 assert {'name': 'docs/api', 'type': 'dir'} in nodes
357 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
357 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
358
358
359 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
359 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
360 commit = backend.repo.get_commit(commit_idx=173)
360 commit = backend.repo.get_commit(commit_idx=173)
361 response = self.app.get(
361 response = self.app.get(
362 route_path('repo_files_nodelist',
362 route_path('repo_files_nodelist',
363 repo_name=backend.repo_name,
363 repo_name=backend.repo_name,
364 commit_id=commit.raw_id, f_path='/docs/api'),
364 commit_id=commit.raw_id, f_path='/docs/api'),
365 extra_environ=xhr_header)
365 extra_environ=xhr_header)
366 assert 'nodes' in response.json
366 assert 'nodes' in response.json
367 nodes = response.json['nodes']
367 nodes = response.json['nodes']
368 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
368 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
369
369
370 def test_tree_search_at_path_missing_xhr(self, backend):
370 def test_tree_search_at_path_missing_xhr(self, backend):
371 self.app.get(
371 self.app.get(
372 route_path('repo_files_nodelist',
372 route_path('repo_files_nodelist',
373 repo_name=backend.repo_name,
373 repo_name=backend.repo_name,
374 commit_id='tip', f_path='/docs'),
374 commit_id='tip', f_path='/docs'),
375 status=404)
375 status=404)
376
376
377 def test_nodetree(self, backend, xhr_header):
377 def test_nodetree(self, backend, xhr_header):
378 commit = backend.repo.get_commit(commit_idx=173)
378 commit = backend.repo.get_commit(commit_idx=173)
379 response = self.app.get(
379 response = self.app.get(
380 route_path('repo_nodetree_full',
380 route_path('repo_nodetree_full',
381 repo_name=backend.repo_name,
381 repo_name=backend.repo_name,
382 commit_id=commit.raw_id, f_path='/'),
382 commit_id=commit.raw_id, f_path='/'),
383 extra_environ=xhr_header)
383 extra_environ=xhr_header)
384
384
385 assert_response = response.assert_response()
385 assert_response = response.assert_response()
386
386
387 for attr in ['data-commit-id', 'data-date', 'data-author']:
387 for attr in ['data-commit-id', 'data-date', 'data-author']:
388 elements = assert_response.get_elements('[{}]'.format(attr))
388 elements = assert_response.get_elements('[{}]'.format(attr))
389 assert len(elements) > 1
389 assert len(elements) > 1
390
390
391 for element in elements:
391 for element in elements:
392 assert element.get(attr)
392 assert element.get(attr)
393
393
394 def test_nodetree_if_file(self, backend, xhr_header):
394 def test_nodetree_if_file(self, backend, xhr_header):
395 commit = backend.repo.get_commit(commit_idx=173)
395 commit = backend.repo.get_commit(commit_idx=173)
396 response = self.app.get(
396 response = self.app.get(
397 route_path('repo_nodetree_full',
397 route_path('repo_nodetree_full',
398 repo_name=backend.repo_name,
398 repo_name=backend.repo_name,
399 commit_id=commit.raw_id, f_path='README.rst'),
399 commit_id=commit.raw_id, f_path='README.rst'),
400 extra_environ=xhr_header)
400 extra_environ=xhr_header)
401 assert response.body == ''
401 assert response.body == ''
402
402
403 def test_nodetree_wrong_path(self, backend, xhr_header):
403 def test_nodetree_wrong_path(self, backend, xhr_header):
404 commit = backend.repo.get_commit(commit_idx=173)
404 commit = backend.repo.get_commit(commit_idx=173)
405 response = self.app.get(
405 response = self.app.get(
406 route_path('repo_nodetree_full',
406 route_path('repo_nodetree_full',
407 repo_name=backend.repo_name,
407 repo_name=backend.repo_name,
408 commit_id=commit.raw_id, f_path='/dont-exist'),
408 commit_id=commit.raw_id, f_path='/dont-exist'),
409 extra_environ=xhr_header)
409 extra_environ=xhr_header)
410
410
411 err = 'error: There is no file nor ' \
411 err = 'error: There is no file nor ' \
412 'directory at the given path'
412 'directory at the given path'
413 assert err in response.body
413 assert err in response.body
414
414
415 def test_nodetree_missing_xhr(self, backend):
415 def test_nodetree_missing_xhr(self, backend):
416 self.app.get(
416 self.app.get(
417 route_path('repo_nodetree_full',
417 route_path('repo_nodetree_full',
418 repo_name=backend.repo_name,
418 repo_name=backend.repo_name,
419 commit_id='tip', f_path='/'),
419 commit_id='tip', f_path='/'),
420 status=404)
420 status=404)
421
421
422
422
423 @pytest.mark.usefixtures("app", "autologin_user")
423 @pytest.mark.usefixtures("app", "autologin_user")
424 class TestRawFileHandling(object):
424 class TestRawFileHandling(object):
425
425
426 def test_download_file(self, backend):
426 def test_download_file(self, backend):
427 commit = backend.repo.get_commit(commit_idx=173)
427 commit = backend.repo.get_commit(commit_idx=173)
428 response = self.app.get(
428 response = self.app.get(
429 route_path('repo_file_download',
429 route_path('repo_file_download',
430 repo_name=backend.repo_name,
430 repo_name=backend.repo_name,
431 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
431 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
432
432
433 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
433 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
434 assert response.content_type == "text/x-python"
434 assert response.content_type == "text/x-python"
435
435
436 def test_download_file_wrong_cs(self, backend):
436 def test_download_file_wrong_cs(self, backend):
437 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
437 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
438
438
439 response = self.app.get(
439 response = self.app.get(
440 route_path('repo_file_download',
440 route_path('repo_file_download',
441 repo_name=backend.repo_name,
441 repo_name=backend.repo_name,
442 commit_id=raw_id, f_path='vcs/nodes.svg'),
442 commit_id=raw_id, f_path='vcs/nodes.svg'),
443 status=404)
443 status=404)
444
444
445 msg = """No such commit exists for this repository"""
445 msg = """No such commit exists for this repository"""
446 response.mustcontain(msg)
446 response.mustcontain(msg)
447
447
448 def test_download_file_wrong_f_path(self, backend):
448 def test_download_file_wrong_f_path(self, backend):
449 commit = backend.repo.get_commit(commit_idx=173)
449 commit = backend.repo.get_commit(commit_idx=173)
450 f_path = 'vcs/ERRORnodes.py'
450 f_path = 'vcs/ERRORnodes.py'
451
451
452 response = self.app.get(
452 response = self.app.get(
453 route_path('repo_file_download',
453 route_path('repo_file_download',
454 repo_name=backend.repo_name,
454 repo_name=backend.repo_name,
455 commit_id=commit.raw_id, f_path=f_path),
455 commit_id=commit.raw_id, f_path=f_path),
456 status=404)
456 status=404)
457
457
458 msg = (
458 msg = (
459 "There is no file nor directory at the given path: "
459 "There is no file nor directory at the given path: "
460 "`%s` at commit %s" % (f_path, commit.short_id))
460 "`%s` at commit %s" % (f_path, commit.short_id))
461 response.mustcontain(msg)
461 response.mustcontain(msg)
462
462
463 def test_file_raw(self, backend):
463 def test_file_raw(self, backend):
464 commit = backend.repo.get_commit(commit_idx=173)
464 commit = backend.repo.get_commit(commit_idx=173)
465 response = self.app.get(
465 response = self.app.get(
466 route_path('repo_file_raw',
466 route_path('repo_file_raw',
467 repo_name=backend.repo_name,
467 repo_name=backend.repo_name,
468 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
468 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
469
469
470 assert response.content_type == "text/plain"
470 assert response.content_type == "text/plain"
471
471
472 def test_file_raw_binary(self, backend):
472 def test_file_raw_binary(self, backend):
473 commit = backend.repo.get_commit()
473 commit = backend.repo.get_commit()
474 response = self.app.get(
474 response = self.app.get(
475 route_path('repo_file_raw',
475 route_path('repo_file_raw',
476 repo_name=backend.repo_name,
476 repo_name=backend.repo_name,
477 commit_id=commit.raw_id,
477 commit_id=commit.raw_id,
478 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
478 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
479
479
480 assert response.content_disposition == 'inline'
480 assert response.content_disposition == 'inline'
481
481
482 def test_raw_file_wrong_cs(self, backend):
482 def test_raw_file_wrong_cs(self, backend):
483 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
483 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
484
484
485 response = self.app.get(
485 response = self.app.get(
486 route_path('repo_file_raw',
486 route_path('repo_file_raw',
487 repo_name=backend.repo_name,
487 repo_name=backend.repo_name,
488 commit_id=raw_id, f_path='vcs/nodes.svg'),
488 commit_id=raw_id, f_path='vcs/nodes.svg'),
489 status=404)
489 status=404)
490
490
491 msg = """No such commit exists for this repository"""
491 msg = """No such commit exists for this repository"""
492 response.mustcontain(msg)
492 response.mustcontain(msg)
493
493
494 def test_raw_wrong_f_path(self, backend):
494 def test_raw_wrong_f_path(self, backend):
495 commit = backend.repo.get_commit(commit_idx=173)
495 commit = backend.repo.get_commit(commit_idx=173)
496 f_path = 'vcs/ERRORnodes.py'
496 f_path = 'vcs/ERRORnodes.py'
497 response = self.app.get(
497 response = self.app.get(
498 route_path('repo_file_raw',
498 route_path('repo_file_raw',
499 repo_name=backend.repo_name,
499 repo_name=backend.repo_name,
500 commit_id=commit.raw_id, f_path=f_path),
500 commit_id=commit.raw_id, f_path=f_path),
501 status=404)
501 status=404)
502
502
503 msg = (
503 msg = (
504 "There is no file nor directory at the given path: "
504 "There is no file nor directory at the given path: "
505 "`%s` at commit %s" % (f_path, commit.short_id))
505 "`%s` at commit %s" % (f_path, commit.short_id))
506 response.mustcontain(msg)
506 response.mustcontain(msg)
507
507
508 def test_raw_svg_should_not_be_rendered(self, backend):
508 def test_raw_svg_should_not_be_rendered(self, backend):
509 backend.create_repo()
509 backend.create_repo()
510 backend.ensure_file("xss.svg")
510 backend.ensure_file("xss.svg")
511 response = self.app.get(
511 response = self.app.get(
512 route_path('repo_file_raw',
512 route_path('repo_file_raw',
513 repo_name=backend.repo_name,
513 repo_name=backend.repo_name,
514 commit_id='tip', f_path='xss.svg'),)
514 commit_id='tip', f_path='xss.svg'),)
515 # If the content type is image/svg+xml then it allows to render HTML
515 # If the content type is image/svg+xml then it allows to render HTML
516 # and malicious SVG.
516 # and malicious SVG.
517 assert response.content_type == "text/plain"
517 assert response.content_type == "text/plain"
518
518
519
519
520 @pytest.mark.usefixtures("app")
520 @pytest.mark.usefixtures("app")
521 class TestRepositoryArchival(object):
521 class TestRepositoryArchival(object):
522
522
523 def test_archival(self, backend):
523 def test_archival(self, backend):
524 backend.enable_downloads()
524 backend.enable_downloads()
525 commit = backend.repo.get_commit(commit_idx=173)
525 commit = backend.repo.get_commit(commit_idx=173)
526 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
526 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
527
527
528 short = commit.short_id + extension
528 short = commit.short_id + extension
529 fname = commit.raw_id + extension
529 fname = commit.raw_id + extension
530 filename = '%s-%s' % (backend.repo_name, short)
530 filename = '%s-%s' % (backend.repo_name, short)
531 response = self.app.get(
531 response = self.app.get(
532 route_path('repo_archivefile',
532 route_path('repo_archivefile',
533 repo_name=backend.repo_name,
533 repo_name=backend.repo_name,
534 fname=fname))
534 fname=fname))
535
535
536 assert response.status == '200 OK'
536 assert response.status == '200 OK'
537 headers = [
537 headers = [
538 ('Content-Disposition', 'attachment; filename=%s' % filename),
538 ('Content-Disposition', 'attachment; filename=%s' % filename),
539 ('Content-Type', '%s' % content_type),
539 ('Content-Type', '%s' % content_type),
540 ]
540 ]
541
541
542 for header in headers:
542 for header in headers:
543 assert header in response.headers.items()
543 assert header in response.headers.items()
544
544
545 @pytest.mark.parametrize('arch_ext',[
545 @pytest.mark.parametrize('arch_ext',[
546 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
546 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
547 def test_archival_wrong_ext(self, backend, arch_ext):
547 def test_archival_wrong_ext(self, backend, arch_ext):
548 backend.enable_downloads()
548 backend.enable_downloads()
549 commit = backend.repo.get_commit(commit_idx=173)
549 commit = backend.repo.get_commit(commit_idx=173)
550
550
551 fname = commit.raw_id + '.' + arch_ext
551 fname = commit.raw_id + '.' + arch_ext
552
552
553 response = self.app.get(
553 response = self.app.get(
554 route_path('repo_archivefile',
554 route_path('repo_archivefile',
555 repo_name=backend.repo_name,
555 repo_name=backend.repo_name,
556 fname=fname))
556 fname=fname))
557 response.mustcontain(
557 response.mustcontain(
558 'Unknown archive type for: `{}`'.format(fname))
558 'Unknown archive type for: `{}`'.format(fname))
559
559
560 @pytest.mark.parametrize('commit_id', [
560 @pytest.mark.parametrize('commit_id', [
561 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
561 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
562 def test_archival_wrong_commit_id(self, backend, commit_id):
562 def test_archival_wrong_commit_id(self, backend, commit_id):
563 backend.enable_downloads()
563 backend.enable_downloads()
564 fname = '%s.zip' % commit_id
564 fname = '%s.zip' % commit_id
565
565
566 response = self.app.get(
566 response = self.app.get(
567 route_path('repo_archivefile',
567 route_path('repo_archivefile',
568 repo_name=backend.repo_name,
568 repo_name=backend.repo_name,
569 fname=fname))
569 fname=fname))
570 response.mustcontain('Unknown commit_id')
570 response.mustcontain('Unknown commit_id')
571
571
572
572
573 @pytest.mark.usefixtures("app")
573 @pytest.mark.usefixtures("app")
574 class TestFilesDiff(object):
574 class TestFilesDiff(object):
575
575
576 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
576 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
577 def test_file_full_diff(self, backend, diff):
577 def test_file_full_diff(self, backend, diff):
578 commit1 = backend.repo.get_commit(commit_idx=-1)
578 commit1 = backend.repo.get_commit(commit_idx=-1)
579 commit2 = backend.repo.get_commit(commit_idx=-2)
579 commit2 = backend.repo.get_commit(commit_idx=-2)
580
580
581 response = self.app.get(
581 response = self.app.get(
582 route_path('repo_files_diff',
582 route_path('repo_files_diff',
583 repo_name=backend.repo_name,
583 repo_name=backend.repo_name,
584 f_path='README'),
584 f_path='README'),
585 params={
585 params={
586 'diff1': commit2.raw_id,
586 'diff1': commit2.raw_id,
587 'diff2': commit1.raw_id,
587 'diff2': commit1.raw_id,
588 'fulldiff': '1',
588 'fulldiff': '1',
589 'diff': diff,
589 'diff': diff,
590 })
590 })
591
591
592 if diff == 'diff':
592 if diff == 'diff':
593 # use redirect since this is OLD view redirecting to compare page
593 # use redirect since this is OLD view redirecting to compare page
594 response = response.follow()
594 response = response.follow()
595
595
596 # It's a symlink to README.rst
596 # It's a symlink to README.rst
597 response.mustcontain('README.rst')
597 response.mustcontain('README.rst')
598 response.mustcontain('No newline at end of file')
598 response.mustcontain('No newline at end of file')
599
599
600 def test_file_binary_diff(self, backend):
600 def test_file_binary_diff(self, backend):
601 commits = [
601 commits = [
602 {'message': 'First commit'},
602 {'message': 'First commit'},
603 {'message': 'Commit with binary',
603 {'message': 'Commit with binary',
604 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
604 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
605 ]
605 ]
606 repo = backend.create_repo(commits=commits)
606 repo = backend.create_repo(commits=commits)
607
607
608 response = self.app.get(
608 response = self.app.get(
609 route_path('repo_files_diff',
609 route_path('repo_files_diff',
610 repo_name=backend.repo_name,
610 repo_name=backend.repo_name,
611 f_path='file.bin'),
611 f_path='file.bin'),
612 params={
612 params={
613 'diff1': repo.get_commit(commit_idx=0).raw_id,
613 'diff1': repo.get_commit(commit_idx=0).raw_id,
614 'diff2': repo.get_commit(commit_idx=1).raw_id,
614 'diff2': repo.get_commit(commit_idx=1).raw_id,
615 'fulldiff': '1',
615 'fulldiff': '1',
616 'diff': 'diff',
616 'diff': 'diff',
617 })
617 })
618 # use redirect since this is OLD view redirecting to compare page
618 # use redirect since this is OLD view redirecting to compare page
619 response = response.follow()
619 response = response.follow()
620 response.mustcontain('Collapse 1 commit')
620 response.mustcontain('Collapse 1 commit')
621 file_changes = (1, 0, 0)
621 file_changes = (1, 0, 0)
622
622
623 compare_page = ComparePage(response)
623 compare_page = ComparePage(response)
624 compare_page.contains_change_summary(*file_changes)
624 compare_page.contains_change_summary(*file_changes)
625
625
626 if backend.alias == 'svn':
626 if backend.alias == 'svn':
627 response.mustcontain('new file 10644')
627 response.mustcontain('new file 10644')
628 # TODO(marcink): SVN doesn't yet detect binary changes
628 # TODO(marcink): SVN doesn't yet detect binary changes
629 else:
629 else:
630 response.mustcontain('new file 100644')
630 response.mustcontain('new file 100644')
631 response.mustcontain('binary diff hidden')
631 response.mustcontain('binary diff hidden')
632
632
633 def test_diff_2way(self, backend):
633 def test_diff_2way(self, backend):
634 commit1 = backend.repo.get_commit(commit_idx=-1)
634 commit1 = backend.repo.get_commit(commit_idx=-1)
635 commit2 = backend.repo.get_commit(commit_idx=-2)
635 commit2 = backend.repo.get_commit(commit_idx=-2)
636 response = self.app.get(
636 response = self.app.get(
637 route_path('repo_files_diff_2way_redirect',
637 route_path('repo_files_diff_2way_redirect',
638 repo_name=backend.repo_name,
638 repo_name=backend.repo_name,
639 f_path='README'),
639 f_path='README'),
640 params={
640 params={
641 'diff1': commit2.raw_id,
641 'diff1': commit2.raw_id,
642 'diff2': commit1.raw_id,
642 'diff2': commit1.raw_id,
643 })
643 })
644 # use redirect since this is OLD view redirecting to compare page
644 # use redirect since this is OLD view redirecting to compare page
645 response = response.follow()
645 response = response.follow()
646
646
647 # It's a symlink to README.rst
647 # It's a symlink to README.rst
648 response.mustcontain('README.rst')
648 response.mustcontain('README.rst')
649 response.mustcontain('No newline at end of file')
649 response.mustcontain('No newline at end of file')
650
650
651 def test_requires_one_commit_id(self, backend, autologin_user):
651 def test_requires_one_commit_id(self, backend, autologin_user):
652 response = self.app.get(
652 response = self.app.get(
653 route_path('repo_files_diff',
653 route_path('repo_files_diff',
654 repo_name=backend.repo_name,
654 repo_name=backend.repo_name,
655 f_path='README.rst'),
655 f_path='README.rst'),
656 status=400)
656 status=400)
657 response.mustcontain(
657 response.mustcontain(
658 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
658 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
659
659
660 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
660 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
661 repo = vcsbackend.repo
661 repo = vcsbackend.repo
662 response = self.app.get(
662 response = self.app.get(
663 route_path('repo_files_diff',
663 route_path('repo_files_diff',
664 repo_name=repo.name,
664 repo_name=repo.name,
665 f_path='does-not-exist-in-any-commit'),
665 f_path='does-not-exist-in-any-commit'),
666 params={
666 params={
667 'diff1': repo[0].raw_id,
667 'diff1': repo[0].raw_id,
668 'diff2': repo[1].raw_id
668 'diff2': repo[1].raw_id
669 })
669 })
670
670
671 response = response.follow()
671 response = response.follow()
672 response.mustcontain('No files')
672 response.mustcontain('No files')
673
673
674 def test_returns_redirect_if_file_not_changed(self, backend):
674 def test_returns_redirect_if_file_not_changed(self, backend):
675 commit = backend.repo.get_commit(commit_idx=-1)
675 commit = backend.repo.get_commit(commit_idx=-1)
676 response = self.app.get(
676 response = self.app.get(
677 route_path('repo_files_diff_2way_redirect',
677 route_path('repo_files_diff_2way_redirect',
678 repo_name=backend.repo_name,
678 repo_name=backend.repo_name,
679 f_path='README'),
679 f_path='README'),
680 params={
680 params={
681 'diff1': commit.raw_id,
681 'diff1': commit.raw_id,
682 'diff2': commit.raw_id,
682 'diff2': commit.raw_id,
683 })
683 })
684
684
685 response = response.follow()
685 response = response.follow()
686 response.mustcontain('No files')
686 response.mustcontain('No files')
687 response.mustcontain('No commits in this compare')
687 response.mustcontain('No commits in this compare')
688
688
689 def test_supports_diff_to_different_path_svn(self, backend_svn):
689 def test_supports_diff_to_different_path_svn(self, backend_svn):
690 #TODO: check this case
690 #TODO: check this case
691 return
691 return
692
692
693 repo = backend_svn['svn-simple-layout'].scm_instance()
693 repo = backend_svn['svn-simple-layout'].scm_instance()
694 commit_id_1 = '24'
694 commit_id_1 = '24'
695 commit_id_2 = '26'
695 commit_id_2 = '26'
696
696
697 response = self.app.get(
697 response = self.app.get(
698 route_path('repo_files_diff',
698 route_path('repo_files_diff',
699 repo_name=backend_svn.repo_name,
699 repo_name=backend_svn.repo_name,
700 f_path='trunk/example.py'),
700 f_path='trunk/example.py'),
701 params={
701 params={
702 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
702 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
703 'diff2': commit_id_2,
703 'diff2': commit_id_2,
704 })
704 })
705
705
706 response = response.follow()
706 response = response.follow()
707 response.mustcontain(
707 response.mustcontain(
708 # diff contains this
708 # diff contains this
709 "Will print out a useful message on invocation.")
709 "Will print out a useful message on invocation.")
710
710
711 # Note: Expecting that we indicate the user what's being compared
711 # Note: Expecting that we indicate the user what's being compared
712 response.mustcontain("trunk/example.py")
712 response.mustcontain("trunk/example.py")
713 response.mustcontain("tags/v0.2/example.py")
713 response.mustcontain("tags/v0.2/example.py")
714
714
715 def test_show_rev_redirects_to_svn_path(self, backend_svn):
715 def test_show_rev_redirects_to_svn_path(self, backend_svn):
716 #TODO: check this case
716 #TODO: check this case
717 return
717 return
718
718
719 repo = backend_svn['svn-simple-layout'].scm_instance()
719 repo = backend_svn['svn-simple-layout'].scm_instance()
720 commit_id = repo[-1].raw_id
720 commit_id = repo[-1].raw_id
721
721
722 response = self.app.get(
722 response = self.app.get(
723 route_path('repo_files_diff',
723 route_path('repo_files_diff',
724 repo_name=backend_svn.repo_name,
724 repo_name=backend_svn.repo_name,
725 f_path='trunk/example.py'),
725 f_path='trunk/example.py'),
726 params={
726 params={
727 'diff1': 'branches/argparse/example.py@' + commit_id,
727 'diff1': 'branches/argparse/example.py@' + commit_id,
728 'diff2': commit_id,
728 'diff2': commit_id,
729 },
729 },
730 status=302)
730 status=302)
731 response = response.follow()
731 response = response.follow()
732 assert response.headers['Location'].endswith(
732 assert response.headers['Location'].endswith(
733 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
733 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
734
734
735 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
735 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
736 #TODO: check this case
736 #TODO: check this case
737 return
737 return
738
738
739 repo = backend_svn['svn-simple-layout'].scm_instance()
739 repo = backend_svn['svn-simple-layout'].scm_instance()
740 commit_id = repo[-1].raw_id
740 commit_id = repo[-1].raw_id
741 response = self.app.get(
741 response = self.app.get(
742 route_path('repo_files_diff',
742 route_path('repo_files_diff',
743 repo_name=backend_svn.repo_name,
743 repo_name=backend_svn.repo_name,
744 f_path='trunk/example.py'),
744 f_path='trunk/example.py'),
745 params={
745 params={
746 'diff1': 'branches/argparse/example.py@' + commit_id,
746 'diff1': 'branches/argparse/example.py@' + commit_id,
747 'diff2': commit_id,
747 'diff2': commit_id,
748 'show_rev': 'Show at Revision',
748 'show_rev': 'Show at Revision',
749 'annotate': 'true',
749 'annotate': 'true',
750 },
750 },
751 status=302)
751 status=302)
752 response = response.follow()
752 response = response.follow()
753 assert response.headers['Location'].endswith(
753 assert response.headers['Location'].endswith(
754 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
754 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
755
755
756
756
757 @pytest.mark.usefixtures("app", "autologin_user")
757 @pytest.mark.usefixtures("app", "autologin_user")
758 class TestModifyFilesWithWebInterface(object):
758 class TestModifyFilesWithWebInterface(object):
759
759
760 def test_add_file_view(self, backend):
760 def test_add_file_view(self, backend):
761 self.app.get(
761 self.app.get(
762 route_path('repo_files_add_file',
762 route_path('repo_files_add_file',
763 repo_name=backend.repo_name,
763 repo_name=backend.repo_name,
764 commit_id='tip', f_path='/')
764 commit_id='tip', f_path='/')
765 )
765 )
766
766
767 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
767 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
768 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
768 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
769 backend.create_repo()
769 backend.create_repo()
770 filename = 'init.py'
770 filename = 'init.py'
771 response = self.app.post(
771 response = self.app.post(
772 route_path('repo_files_create_file',
772 route_path('repo_files_create_file',
773 repo_name=backend.repo_name,
773 repo_name=backend.repo_name,
774 commit_id='tip', f_path='/'),
774 commit_id='tip', f_path='/'),
775 params={
775 params={
776 'content': "",
776 'content': "",
777 'filename': filename,
777 'filename': filename,
778 'csrf_token': csrf_token,
778 'csrf_token': csrf_token,
779 },
779 },
780 status=302)
780 status=302)
781 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
781 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
782 assert_session_flash(response, expected_msg)
782 assert_session_flash(response, expected_msg)
783
783
784 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
784 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
785 commit_id = backend.repo.get_commit().raw_id
785 commit_id = backend.repo.get_commit().raw_id
786 response = self.app.post(
786 response = self.app.post(
787 route_path('repo_files_create_file',
787 route_path('repo_files_create_file',
788 repo_name=backend.repo_name,
788 repo_name=backend.repo_name,
789 commit_id=commit_id, f_path='/'),
789 commit_id=commit_id, f_path='/'),
790 params={
790 params={
791 'content': "foo",
791 'content': "foo",
792 'csrf_token': csrf_token,
792 'csrf_token': csrf_token,
793 },
793 },
794 status=302)
794 status=302)
795
795
796 assert_session_flash(response, 'No filename specified')
796 assert_session_flash(response, 'No filename specified')
797
797
798 def test_add_file_into_repo_errors_and_no_commits(
798 def test_add_file_into_repo_errors_and_no_commits(
799 self, backend, csrf_token):
799 self, backend, csrf_token):
800 repo = backend.create_repo()
800 repo = backend.create_repo()
801 # Create a file with no filename, it will display an error but
801 # Create a file with no filename, it will display an error but
802 # the repo has no commits yet
802 # the repo has no commits yet
803 response = self.app.post(
803 response = self.app.post(
804 route_path('repo_files_create_file',
804 route_path('repo_files_create_file',
805 repo_name=repo.repo_name,
805 repo_name=repo.repo_name,
806 commit_id='tip', f_path='/'),
806 commit_id='tip', f_path='/'),
807 params={
807 params={
808 'content': "foo",
808 'content': "foo",
809 'csrf_token': csrf_token,
809 'csrf_token': csrf_token,
810 },
810 },
811 status=302)
811 status=302)
812
812
813 assert_session_flash(response, 'No filename specified')
813 assert_session_flash(response, 'No filename specified')
814
814
815 # Not allowed, redirect to the summary
815 # Not allowed, redirect to the summary
816 redirected = response.follow()
816 redirected = response.follow()
817 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
817 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
818
818
819 # As there are no commits, displays the summary page with the error of
819 # As there are no commits, displays the summary page with the error of
820 # creating a file with no filename
820 # creating a file with no filename
821
821
822 assert redirected.request.path == summary_url
822 assert redirected.request.path == summary_url
823
823
824 @pytest.mark.parametrize("filename, clean_filename", [
824 @pytest.mark.parametrize("filename, clean_filename", [
825 ('/abs/foo', 'abs/foo'),
825 ('/abs/foo', 'abs/foo'),
826 ('../rel/foo', 'rel/foo'),
826 ('../rel/foo', 'rel/foo'),
827 ('file/../foo/foo', 'file/foo/foo'),
827 ('file/../foo/foo', 'file/foo/foo'),
828 ])
828 ])
829 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
829 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
830 repo = backend.create_repo()
830 repo = backend.create_repo()
831 commit_id = repo.get_commit().raw_id
831 commit_id = repo.get_commit().raw_id
832
832
833 response = self.app.post(
833 response = self.app.post(
834 route_path('repo_files_create_file',
834 route_path('repo_files_create_file',
835 repo_name=repo.repo_name,
835 repo_name=repo.repo_name,
836 commit_id=commit_id, f_path='/'),
836 commit_id=commit_id, f_path='/'),
837 params={
837 params={
838 'content': "foo",
838 'content': "foo",
839 'filename': filename,
839 'filename': filename,
840 'csrf_token': csrf_token,
840 'csrf_token': csrf_token,
841 },
841 },
842 status=302)
842 status=302)
843
843
844 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
844 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
845 assert_session_flash(response, expected_msg)
845 assert_session_flash(response, expected_msg)
846
846
847 @pytest.mark.parametrize("cnt, filename, content", [
847 @pytest.mark.parametrize("cnt, filename, content", [
848 (1, 'foo.txt', "Content"),
848 (1, 'foo.txt', "Content"),
849 (2, 'dir/foo.rst', "Content"),
849 (2, 'dir/foo.rst', "Content"),
850 (3, 'dir/foo-second.rst', "Content"),
850 (3, 'dir/foo-second.rst', "Content"),
851 (4, 'rel/dir/foo.bar', "Content"),
851 (4, 'rel/dir/foo.bar', "Content"),
852 ])
852 ])
853 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
853 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
854 repo = backend.create_repo()
854 repo = backend.create_repo()
855 commit_id = repo.get_commit().raw_id
855 commit_id = repo.get_commit().raw_id
856 response = self.app.post(
856 response = self.app.post(
857 route_path('repo_files_create_file',
857 route_path('repo_files_create_file',
858 repo_name=repo.repo_name,
858 repo_name=repo.repo_name,
859 commit_id=commit_id, f_path='/'),
859 commit_id=commit_id, f_path='/'),
860 params={
860 params={
861 'content': content,
861 'content': content,
862 'filename': filename,
862 'filename': filename,
863 'csrf_token': csrf_token,
863 'csrf_token': csrf_token,
864 },
864 },
865 status=302)
865 status=302)
866
866
867 expected_msg = 'Successfully committed new file `{}`'.format(filename)
867 expected_msg = 'Successfully committed new file `{}`'.format(filename)
868 assert_session_flash(response, expected_msg)
868 assert_session_flash(response, expected_msg)
869
869
870 def test_edit_file_view(self, backend):
870 def test_edit_file_view(self, backend):
871 response = self.app.get(
871 response = self.app.get(
872 route_path('repo_files_edit_file',
872 route_path('repo_files_edit_file',
873 repo_name=backend.repo_name,
873 repo_name=backend.repo_name,
874 commit_id=backend.default_head_id,
874 commit_id=backend.default_head_id,
875 f_path='vcs/nodes.py'),
875 f_path='vcs/nodes.py'),
876 status=200)
876 status=200)
877 response.mustcontain("Module holding everything related to vcs nodes.")
877 response.mustcontain("Module holding everything related to vcs nodes.")
878
878
879 def test_edit_file_view_not_on_branch(self, backend):
879 def test_edit_file_view_not_on_branch(self, backend):
880 repo = backend.create_repo()
880 repo = backend.create_repo()
881 backend.ensure_file("vcs/nodes.py")
881 backend.ensure_file("vcs/nodes.py")
882
882
883 response = self.app.get(
883 response = self.app.get(
884 route_path('repo_files_edit_file',
884 route_path('repo_files_edit_file',
885 repo_name=repo.repo_name,
885 repo_name=repo.repo_name,
886 commit_id='tip',
886 commit_id='tip',
887 f_path='vcs/nodes.py'),
887 f_path='vcs/nodes.py'),
888 status=302)
888 status=302)
889 assert_session_flash(
889 assert_session_flash(
890 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
890 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
891
891
892 def test_edit_file_view_commit_changes(self, backend, csrf_token):
892 def test_edit_file_view_commit_changes(self, backend, csrf_token):
893 repo = backend.create_repo()
893 repo = backend.create_repo()
894 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
894 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
895
895
896 response = self.app.post(
896 response = self.app.post(
897 route_path('repo_files_update_file',
897 route_path('repo_files_update_file',
898 repo_name=repo.repo_name,
898 repo_name=repo.repo_name,
899 commit_id=backend.default_head_id,
899 commit_id=backend.default_head_id,
900 f_path='vcs/nodes.py'),
900 f_path='vcs/nodes.py'),
901 params={
901 params={
902 'content': "print 'hello world'",
902 'content': "print 'hello world'",
903 'message': 'I committed',
903 'message': 'I committed',
904 'filename': "vcs/nodes.py",
904 'filename': "vcs/nodes.py",
905 'csrf_token': csrf_token,
905 'csrf_token': csrf_token,
906 },
906 },
907 status=302)
907 status=302)
908 assert_session_flash(
908 assert_session_flash(
909 response, 'Successfully committed changes to file `vcs/nodes.py`')
909 response, 'Successfully committed changes to file `vcs/nodes.py`')
910 tip = repo.get_commit(commit_idx=-1)
910 tip = repo.get_commit(commit_idx=-1)
911 assert tip.message == 'I committed'
911 assert tip.message == 'I committed'
912
912
913 def test_edit_file_view_commit_changes_default_message(self, backend,
913 def test_edit_file_view_commit_changes_default_message(self, backend,
914 csrf_token):
914 csrf_token):
915 repo = backend.create_repo()
915 repo = backend.create_repo()
916 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
916 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
917
917
918 commit_id = (
918 commit_id = (
919 backend.default_branch_name or
919 backend.default_branch_name or
920 backend.repo.scm_instance().commit_ids[-1])
920 backend.repo.scm_instance().commit_ids[-1])
921
921
922 response = self.app.post(
922 response = self.app.post(
923 route_path('repo_files_update_file',
923 route_path('repo_files_update_file',
924 repo_name=repo.repo_name,
924 repo_name=repo.repo_name,
925 commit_id=commit_id,
925 commit_id=commit_id,
926 f_path='vcs/nodes.py'),
926 f_path='vcs/nodes.py'),
927 params={
927 params={
928 'content': "print 'hello world'",
928 'content': "print 'hello world'",
929 'message': '',
929 'message': '',
930 'filename': "vcs/nodes.py",
930 'filename': "vcs/nodes.py",
931 'csrf_token': csrf_token,
931 'csrf_token': csrf_token,
932 },
932 },
933 status=302)
933 status=302)
934 assert_session_flash(
934 assert_session_flash(
935 response, 'Successfully committed changes to file `vcs/nodes.py`')
935 response, 'Successfully committed changes to file `vcs/nodes.py`')
936 tip = repo.get_commit(commit_idx=-1)
936 tip = repo.get_commit(commit_idx=-1)
937 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
937 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
938
938
939 def test_delete_file_view(self, backend):
939 def test_delete_file_view(self, backend):
940 self.app.get(
940 self.app.get(
941 route_path('repo_files_remove_file',
941 route_path('repo_files_remove_file',
942 repo_name=backend.repo_name,
942 repo_name=backend.repo_name,
943 commit_id=backend.default_head_id,
943 commit_id=backend.default_head_id,
944 f_path='vcs/nodes.py'),
944 f_path='vcs/nodes.py'),
945 status=200)
945 status=200)
946
946
947 def test_delete_file_view_not_on_branch(self, backend):
947 def test_delete_file_view_not_on_branch(self, backend):
948 repo = backend.create_repo()
948 repo = backend.create_repo()
949 backend.ensure_file('vcs/nodes.py')
949 backend.ensure_file('vcs/nodes.py')
950
950
951 response = self.app.get(
951 response = self.app.get(
952 route_path('repo_files_remove_file',
952 route_path('repo_files_remove_file',
953 repo_name=repo.repo_name,
953 repo_name=repo.repo_name,
954 commit_id='tip',
954 commit_id='tip',
955 f_path='vcs/nodes.py'),
955 f_path='vcs/nodes.py'),
956 status=302)
956 status=302)
957 assert_session_flash(
957 assert_session_flash(
958 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
958 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
959
959
960 def test_delete_file_view_commit_changes(self, backend, csrf_token):
960 def test_delete_file_view_commit_changes(self, backend, csrf_token):
961 repo = backend.create_repo()
961 repo = backend.create_repo()
962 backend.ensure_file("vcs/nodes.py")
962 backend.ensure_file("vcs/nodes.py")
963
963
964 response = self.app.post(
964 response = self.app.post(
965 route_path('repo_files_delete_file',
965 route_path('repo_files_delete_file',
966 repo_name=repo.repo_name,
966 repo_name=repo.repo_name,
967 commit_id=backend.default_head_id,
967 commit_id=backend.default_head_id,
968 f_path='vcs/nodes.py'),
968 f_path='vcs/nodes.py'),
969 params={
969 params={
970 'message': 'i commited',
970 'message': 'i commited',
971 'csrf_token': csrf_token,
971 'csrf_token': csrf_token,
972 },
972 },
973 status=302)
973 status=302)
974 assert_session_flash(
974 assert_session_flash(
975 response, 'Successfully deleted file `vcs/nodes.py`')
975 response, 'Successfully deleted file `vcs/nodes.py`')
976
976
977
977
978 @pytest.mark.usefixtures("app")
978 @pytest.mark.usefixtures("app")
979 class TestFilesViewOtherCases(object):
979 class TestFilesViewOtherCases(object):
980
980
981 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
981 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
982 self, backend_stub, autologin_regular_user, user_regular,
982 self, backend_stub, autologin_regular_user, user_regular,
983 user_util):
983 user_util):
984
984
985 repo = backend_stub.create_repo()
985 repo = backend_stub.create_repo()
986 user_util.grant_user_permission_to_repo(
986 user_util.grant_user_permission_to_repo(
987 repo, user_regular, 'repository.write')
987 repo, user_regular, 'repository.write')
988 response = self.app.get(
988 response = self.app.get(
989 route_path('repo_files',
989 route_path('repo_files',
990 repo_name=repo.repo_name,
990 repo_name=repo.repo_name,
991 commit_id='tip', f_path='/'))
991 commit_id='tip', f_path='/'))
992
992
993 repo_file_add_url = route_path(
993 repo_file_add_url = route_path(
994 'repo_files_add_file',
994 'repo_files_add_file',
995 repo_name=repo.repo_name,
995 repo_name=repo.repo_name,
996 commit_id=0, f_path='')
996 commit_id=0, f_path='')
997
997
998 assert_session_flash(
998 assert_session_flash(
999 response,
999 response,
1000 'There are no files yet. <a class="alert-link" '
1000 'There are no files yet. <a class="alert-link" '
1001 'href="{}">Click here to add a new file.</a>'
1001 'href="{}">Click here to add a new file.</a>'
1002 .format(repo_file_add_url))
1002 .format(repo_file_add_url))
1003
1003
1004 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1004 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1005 self, backend_stub, autologin_regular_user):
1005 self, backend_stub, autologin_regular_user):
1006 repo = backend_stub.create_repo()
1006 repo = backend_stub.create_repo()
1007 # init session for anon user
1007 # init session for anon user
1008 route_path('repo_summary', repo_name=repo.repo_name)
1008 route_path('repo_summary', repo_name=repo.repo_name)
1009
1009
1010 repo_file_add_url = route_path(
1010 repo_file_add_url = route_path(
1011 'repo_files_add_file',
1011 'repo_files_add_file',
1012 repo_name=repo.repo_name,
1012 repo_name=repo.repo_name,
1013 commit_id=0, f_path='')
1013 commit_id=0, f_path='')
1014
1014
1015 response = self.app.get(
1015 response = self.app.get(
1016 route_path('repo_files',
1016 route_path('repo_files',
1017 repo_name=repo.repo_name,
1017 repo_name=repo.repo_name,
1018 commit_id='tip', f_path='/'))
1018 commit_id='tip', f_path='/'))
1019
1019
1020 assert_session_flash(response, no_=repo_file_add_url)
1020 assert_session_flash(response, no_=repo_file_add_url)
1021
1021
1022 @pytest.mark.parametrize('file_node', [
1022 @pytest.mark.parametrize('file_node', [
1023 'archive/file.zip',
1023 'archive/file.zip',
1024 'diff/my-file.txt',
1024 'diff/my-file.txt',
1025 'render.py',
1025 'render.py',
1026 'render',
1026 'render',
1027 'remove_file',
1027 'remove_file',
1028 'remove_file/to-delete.txt',
1028 'remove_file/to-delete.txt',
1029 ])
1029 ])
1030 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1030 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1031 backend.create_repo()
1031 backend.create_repo()
1032 backend.ensure_file(file_node)
1032 backend.ensure_file(file_node)
1033
1033
1034 self.app.get(
1034 self.app.get(
1035 route_path('repo_files',
1035 route_path('repo_files',
1036 repo_name=backend.repo_name,
1036 repo_name=backend.repo_name,
1037 commit_id='tip', f_path=file_node),
1037 commit_id='tip', f_path=file_node),
1038 status=200)
1038 status=200)
1039
1039
1040
1040
1041 class TestAdjustFilePathForSvn(object):
1041 class TestAdjustFilePathForSvn(object):
1042 """
1042 """
1043 SVN specific adjustments of node history in RepoFilesView.
1043 SVN specific adjustments of node history in RepoFilesView.
1044 """
1044 """
1045
1045
1046 def test_returns_path_relative_to_matched_reference(self):
1046 def test_returns_path_relative_to_matched_reference(self):
1047 repo = self._repo(branches=['trunk'])
1047 repo = self._repo(branches=['trunk'])
1048 self.assert_file_adjustment('trunk/file', 'file', repo)
1048 self.assert_file_adjustment('trunk/file', 'file', repo)
1049
1049
1050 def test_does_not_modify_file_if_no_reference_matches(self):
1050 def test_does_not_modify_file_if_no_reference_matches(self):
1051 repo = self._repo(branches=['trunk'])
1051 repo = self._repo(branches=['trunk'])
1052 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1052 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1053
1053
1054 def test_does_not_adjust_partial_directory_names(self):
1054 def test_does_not_adjust_partial_directory_names(self):
1055 repo = self._repo(branches=['trun'])
1055 repo = self._repo(branches=['trun'])
1056 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1056 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1057
1057
1058 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1058 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1059 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1059 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1060 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1060 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1061
1061
1062 def assert_file_adjustment(self, f_path, expected, repo):
1062 def assert_file_adjustment(self, f_path, expected, repo):
1063 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1063 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1064 assert result == expected
1064 assert result == expected
1065
1065
1066 def _repo(self, branches=None):
1066 def _repo(self, branches=None):
1067 repo = mock.Mock()
1067 repo = mock.Mock()
1068 repo.branches = OrderedDict((name, '0') for name in branches or [])
1068 repo.branches = OrderedDict((name, '0') for name in branches or [])
1069 repo.tags = {}
1069 repo.tags = {}
1070 return repo
1070 return repo
@@ -1,1603 +1,1618 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 branch_name, rule)
128 h.escape(branch_name), 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_rev[1]
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):
184 except (CommitDoesNotExistError, LookupError) as e:
185 msg = _('No such commit exists for this repository')
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):
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 not repo.is_empty():
241 commit = repo.get_commit(commit_id=commit_id)
241 commit = repo.get_commit(commit_id=commit_id)
242 if commit:
242 if commit:
243 branch_name = commit.branch
243 branch_name = commit.branch
244 sha_commit_id = commit.raw_id
244 sha_commit_id = commit.raw_id
245
245
246 return branch_name, sha_commit_id, is_head
246 return branch_name, sha_commit_id, is_head
247
247
248 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
248 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
249
249
250 repo_id = self.db_repo.repo_id
250 repo_id = self.db_repo.repo_id
251 force_recache = self.get_recache_flag()
251 force_recache = self.get_recache_flag()
252
252
253 cache_seconds = safe_int(
253 cache_seconds = safe_int(
254 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
254 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
255 cache_on = not force_recache and cache_seconds > 0
255 cache_on = not force_recache and cache_seconds > 0
256 log.debug(
256 log.debug(
257 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
257 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
258 'with caching: %s[TTL: %ss]' % (
258 'with caching: %s[TTL: %ss]' % (
259 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
259 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
260
260
261 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
261 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
262 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
262 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
263
263
264 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
264 @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):
265 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',
266 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
267 ver, _repo_id, _commit_id, _f_path)
267 ver, _repo_id, _commit_id, _f_path)
268
268
269 c.full_load = _full_load
269 c.full_load = _full_load
270 return render(
270 return render(
271 'rhodecode:templates/files/files_browser_tree.mako',
271 'rhodecode:templates/files/files_browser_tree.mako',
272 self._get_template_context(c), self.request, _at_rev)
272 self._get_template_context(c), self.request, _at_rev)
273
273
274 return compute_file_tree(
274 return compute_file_tree(
275 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
275 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)
276 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
277
277
278 def _get_archive_spec(self, fname):
278 def _get_archive_spec(self, fname):
279 log.debug('Detecting archive spec for: `%s`', fname)
279 log.debug('Detecting archive spec for: `%s`', fname)
280
280
281 fileformat = None
281 fileformat = None
282 ext = None
282 ext = None
283 content_type = None
283 content_type = None
284 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
284 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
285
285
286 if fname.endswith(extension):
286 if fname.endswith(extension):
287 fileformat = a_type
287 fileformat = a_type
288 log.debug('archive is of type: %s', fileformat)
288 log.debug('archive is of type: %s', fileformat)
289 ext = extension
289 ext = extension
290 break
290 break
291
291
292 if not fileformat:
292 if not fileformat:
293 raise ValueError()
293 raise ValueError()
294
294
295 # left over part of whole fname is the commit
295 # left over part of whole fname is the commit
296 commit_id = fname[:-len(ext)]
296 commit_id = fname[:-len(ext)]
297
297
298 return commit_id, ext, fileformat, content_type
298 return commit_id, ext, fileformat, content_type
299
299
300 def create_pure_path(self, *parts):
300 def create_pure_path(self, *parts):
301 # Split paths and sanitize them, removing any ../ etc
301 # Split paths and sanitize them, removing any ../ etc
302 sanitized_path = [
302 sanitized_path = [
303 x for x in pathlib2.PurePath(*parts).parts
303 x for x in pathlib2.PurePath(*parts).parts
304 if x not in ['.', '..']]
304 if x not in ['.', '..']]
305
305
306 pure_path = pathlib2.PurePath(*sanitized_path)
306 pure_path = pathlib2.PurePath(*sanitized_path)
307 return pure_path
307 return pure_path
308
308
309 def _is_lf_enabled(self, target_repo):
309 def _is_lf_enabled(self, target_repo):
310 lf_enabled = False
310 lf_enabled = False
311
311
312 lf_key_for_vcs_map = {
312 lf_key_for_vcs_map = {
313 'hg': 'extensions_largefiles',
313 'hg': 'extensions_largefiles',
314 'git': 'vcs_git_lfs_enabled'
314 'git': 'vcs_git_lfs_enabled'
315 }
315 }
316
316
317 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
317 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
318
318
319 if lf_key_for_vcs:
319 if lf_key_for_vcs:
320 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
320 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
321
321
322 return lf_enabled
322 return lf_enabled
323
323
324 @LoginRequired()
324 @LoginRequired()
325 @HasRepoPermissionAnyDecorator(
325 @HasRepoPermissionAnyDecorator(
326 'repository.read', 'repository.write', 'repository.admin')
326 'repository.read', 'repository.write', 'repository.admin')
327 @view_config(
327 @view_config(
328 route_name='repo_archivefile', request_method='GET',
328 route_name='repo_archivefile', request_method='GET',
329 renderer=None)
329 renderer=None)
330 def repo_archivefile(self):
330 def repo_archivefile(self):
331 # archive cache config
331 # archive cache config
332 from rhodecode import CONFIG
332 from rhodecode import CONFIG
333 _ = self.request.translate
333 _ = self.request.translate
334 self.load_default_context()
334 self.load_default_context()
335 default_at_path = '/'
335 default_at_path = '/'
336 fname = self.request.matchdict['fname']
336 fname = self.request.matchdict['fname']
337 subrepos = self.request.GET.get('subrepos') == 'true'
337 subrepos = self.request.GET.get('subrepos') == 'true'
338 at_path = self.request.GET.get('at_path') or default_at_path
338 at_path = self.request.GET.get('at_path') or default_at_path
339
339
340 if not self.db_repo.enable_downloads:
340 if not self.db_repo.enable_downloads:
341 return Response(_('Downloads disabled'))
341 return Response(_('Downloads disabled'))
342
342
343 try:
343 try:
344 commit_id, ext, fileformat, content_type = \
344 commit_id, ext, fileformat, content_type = \
345 self._get_archive_spec(fname)
345 self._get_archive_spec(fname)
346 except ValueError:
346 except ValueError:
347 return Response(_('Unknown archive type for: `{}`').format(
347 return Response(_('Unknown archive type for: `{}`').format(
348 h.escape(fname)))
348 h.escape(fname)))
349
349
350 try:
350 try:
351 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
351 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
352 except CommitDoesNotExistError:
352 except CommitDoesNotExistError:
353 return Response(_('Unknown commit_id {}').format(
353 return Response(_('Unknown commit_id {}').format(
354 h.escape(commit_id)))
354 h.escape(commit_id)))
355 except EmptyRepositoryError:
355 except EmptyRepositoryError:
356 return Response(_('Empty repository'))
356 return Response(_('Empty repository'))
357
357
358 try:
358 try:
359 at_path = commit.get_node(at_path).path or default_at_path
359 at_path = commit.get_node(at_path).path or default_at_path
360 except Exception:
360 except Exception:
361 return Response(_('No node at path {} for this repository').format(at_path))
361 return Response(_('No node at path {} for this repository').format(at_path))
362
362
363 path_sha = sha1(at_path)[:8]
363 path_sha = sha1(at_path)[:8]
364
364
365 # original backward compat name of archive
365 # original backward compat name of archive
366 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
366 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
367 short_sha = safe_str(commit.short_id)
367 short_sha = safe_str(commit.short_id)
368
368
369 if at_path == default_at_path:
369 if at_path == default_at_path:
370 archive_name = '{}-{}{}{}'.format(
370 archive_name = '{}-{}{}{}'.format(
371 clean_name,
371 clean_name,
372 '-sub' if subrepos else '',
372 '-sub' if subrepos else '',
373 short_sha,
373 short_sha,
374 ext)
374 ext)
375 # custom path and new name
375 # custom path and new name
376 else:
376 else:
377 archive_name = '{}-{}{}-{}{}'.format(
377 archive_name = '{}-{}{}-{}{}'.format(
378 clean_name,
378 clean_name,
379 '-sub' if subrepos else '',
379 '-sub' if subrepos else '',
380 short_sha,
380 short_sha,
381 path_sha,
381 path_sha,
382 ext)
382 ext)
383
383
384 use_cached_archive = False
384 use_cached_archive = False
385 archive_cache_enabled = CONFIG.get(
385 archive_cache_enabled = CONFIG.get(
386 'archive_cache_dir') and not self.request.GET.get('no_cache')
386 'archive_cache_dir') and not self.request.GET.get('no_cache')
387 cached_archive_path = None
387 cached_archive_path = None
388
388
389 if archive_cache_enabled:
389 if archive_cache_enabled:
390 # check if we it's ok to write
390 # check if we it's ok to write
391 if not os.path.isdir(CONFIG['archive_cache_dir']):
391 if not os.path.isdir(CONFIG['archive_cache_dir']):
392 os.makedirs(CONFIG['archive_cache_dir'])
392 os.makedirs(CONFIG['archive_cache_dir'])
393 cached_archive_path = os.path.join(
393 cached_archive_path = os.path.join(
394 CONFIG['archive_cache_dir'], archive_name)
394 CONFIG['archive_cache_dir'], archive_name)
395 if os.path.isfile(cached_archive_path):
395 if os.path.isfile(cached_archive_path):
396 log.debug('Found cached archive in %s', cached_archive_path)
396 log.debug('Found cached archive in %s', cached_archive_path)
397 fd, archive = None, cached_archive_path
397 fd, archive = None, cached_archive_path
398 use_cached_archive = True
398 use_cached_archive = True
399 else:
399 else:
400 log.debug('Archive %s is not yet cached', archive_name)
400 log.debug('Archive %s is not yet cached', archive_name)
401
401
402 if not use_cached_archive:
402 if not use_cached_archive:
403 # generate new archive
403 # generate new archive
404 fd, archive = tempfile.mkstemp()
404 fd, archive = tempfile.mkstemp()
405 log.debug('Creating new temp archive in %s', archive)
405 log.debug('Creating new temp archive in %s', archive)
406 try:
406 try:
407 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
407 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
408 archive_at_path=at_path)
408 archive_at_path=at_path)
409 except ImproperArchiveTypeError:
409 except ImproperArchiveTypeError:
410 return _('Unknown archive type')
410 return _('Unknown archive type')
411 if archive_cache_enabled:
411 if archive_cache_enabled:
412 # if we generated the archive and we have cache enabled
412 # if we generated the archive and we have cache enabled
413 # let's use this for future
413 # let's use this for future
414 log.debug('Storing new archive in %s', cached_archive_path)
414 log.debug('Storing new archive in %s', cached_archive_path)
415 shutil.move(archive, cached_archive_path)
415 shutil.move(archive, cached_archive_path)
416 archive = cached_archive_path
416 archive = cached_archive_path
417
417
418 # store download action
418 # store download action
419 audit_logger.store_web(
419 audit_logger.store_web(
420 'repo.archive.download', action_data={
420 'repo.archive.download', action_data={
421 'user_agent': self.request.user_agent,
421 'user_agent': self.request.user_agent,
422 'archive_name': archive_name,
422 'archive_name': archive_name,
423 'archive_spec': fname,
423 'archive_spec': fname,
424 'archive_cached': use_cached_archive},
424 'archive_cached': use_cached_archive},
425 user=self._rhodecode_user,
425 user=self._rhodecode_user,
426 repo=self.db_repo,
426 repo=self.db_repo,
427 commit=True
427 commit=True
428 )
428 )
429
429
430 def get_chunked_archive(archive_path):
430 def get_chunked_archive(archive_path):
431 with open(archive_path, 'rb') as stream:
431 with open(archive_path, 'rb') as stream:
432 while True:
432 while True:
433 data = stream.read(16 * 1024)
433 data = stream.read(16 * 1024)
434 if not data:
434 if not data:
435 if fd: # fd means we used temporary file
435 if fd: # fd means we used temporary file
436 os.close(fd)
436 os.close(fd)
437 if not archive_cache_enabled:
437 if not archive_cache_enabled:
438 log.debug('Destroying temp archive %s', archive_path)
438 log.debug('Destroying temp archive %s', archive_path)
439 os.remove(archive_path)
439 os.remove(archive_path)
440 break
440 break
441 yield data
441 yield data
442
442
443 response = Response(app_iter=get_chunked_archive(archive))
443 response = Response(app_iter=get_chunked_archive(archive))
444 response.content_disposition = str(
444 response.content_disposition = str(
445 'attachment; filename=%s' % archive_name)
445 'attachment; filename=%s' % archive_name)
446 response.content_type = str(content_type)
446 response.content_type = str(content_type)
447
447
448 return response
448 return response
449
449
450 def _get_file_node(self, commit_id, f_path):
450 def _get_file_node(self, commit_id, f_path):
451 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
451 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
452 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
452 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
453 try:
453 try:
454 node = commit.get_node(f_path)
454 node = commit.get_node(f_path)
455 if node.is_dir():
455 if node.is_dir():
456 raise NodeError('%s path is a %s not a file'
456 raise NodeError('%s path is a %s not a file'
457 % (node, type(node)))
457 % (node, type(node)))
458 except NodeDoesNotExistError:
458 except NodeDoesNotExistError:
459 commit = EmptyCommit(
459 commit = EmptyCommit(
460 commit_id=commit_id,
460 commit_id=commit_id,
461 idx=commit.idx,
461 idx=commit.idx,
462 repo=commit.repository,
462 repo=commit.repository,
463 alias=commit.repository.alias,
463 alias=commit.repository.alias,
464 message=commit.message,
464 message=commit.message,
465 author=commit.author,
465 author=commit.author,
466 date=commit.date)
466 date=commit.date)
467 node = FileNode(f_path, '', commit=commit)
467 node = FileNode(f_path, '', commit=commit)
468 else:
468 else:
469 commit = EmptyCommit(
469 commit = EmptyCommit(
470 repo=self.rhodecode_vcs_repo,
470 repo=self.rhodecode_vcs_repo,
471 alias=self.rhodecode_vcs_repo.alias)
471 alias=self.rhodecode_vcs_repo.alias)
472 node = FileNode(f_path, '', commit=commit)
472 node = FileNode(f_path, '', commit=commit)
473 return node
473 return node
474
474
475 @LoginRequired()
475 @LoginRequired()
476 @HasRepoPermissionAnyDecorator(
476 @HasRepoPermissionAnyDecorator(
477 'repository.read', 'repository.write', 'repository.admin')
477 'repository.read', 'repository.write', 'repository.admin')
478 @view_config(
478 @view_config(
479 route_name='repo_files_diff', request_method='GET',
479 route_name='repo_files_diff', request_method='GET',
480 renderer=None)
480 renderer=None)
481 def repo_files_diff(self):
481 def repo_files_diff(self):
482 c = self.load_default_context()
482 c = self.load_default_context()
483 f_path = self._get_f_path(self.request.matchdict)
483 f_path = self._get_f_path(self.request.matchdict)
484 diff1 = self.request.GET.get('diff1', '')
484 diff1 = self.request.GET.get('diff1', '')
485 diff2 = self.request.GET.get('diff2', '')
485 diff2 = self.request.GET.get('diff2', '')
486
486
487 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
487 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
488
488
489 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
489 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
490 line_context = self.request.GET.get('context', 3)
490 line_context = self.request.GET.get('context', 3)
491
491
492 if not any((diff1, diff2)):
492 if not any((diff1, diff2)):
493 h.flash(
493 h.flash(
494 'Need query parameter "diff1" or "diff2" to generate a diff.',
494 'Need query parameter "diff1" or "diff2" to generate a diff.',
495 category='error')
495 category='error')
496 raise HTTPBadRequest()
496 raise HTTPBadRequest()
497
497
498 c.action = self.request.GET.get('diff')
498 c.action = self.request.GET.get('diff')
499 if c.action not in ['download', 'raw']:
499 if c.action not in ['download', 'raw']:
500 compare_url = h.route_path(
500 compare_url = h.route_path(
501 'repo_compare',
501 'repo_compare',
502 repo_name=self.db_repo_name,
502 repo_name=self.db_repo_name,
503 source_ref_type='rev',
503 source_ref_type='rev',
504 source_ref=diff1,
504 source_ref=diff1,
505 target_repo=self.db_repo_name,
505 target_repo=self.db_repo_name,
506 target_ref_type='rev',
506 target_ref_type='rev',
507 target_ref=diff2,
507 target_ref=diff2,
508 _query=dict(f_path=f_path))
508 _query=dict(f_path=f_path))
509 # redirect to new view if we render diff
509 # redirect to new view if we render diff
510 raise HTTPFound(compare_url)
510 raise HTTPFound(compare_url)
511
511
512 try:
512 try:
513 node1 = self._get_file_node(diff1, path1)
513 node1 = self._get_file_node(diff1, path1)
514 node2 = self._get_file_node(diff2, f_path)
514 node2 = self._get_file_node(diff2, f_path)
515 except (RepositoryError, NodeError):
515 except (RepositoryError, NodeError):
516 log.exception("Exception while trying to get node from repository")
516 log.exception("Exception while trying to get node from repository")
517 raise HTTPFound(
517 raise HTTPFound(
518 h.route_path('repo_files', repo_name=self.db_repo_name,
518 h.route_path('repo_files', repo_name=self.db_repo_name,
519 commit_id='tip', f_path=f_path))
519 commit_id='tip', f_path=f_path))
520
520
521 if all(isinstance(node.commit, EmptyCommit)
521 if all(isinstance(node.commit, EmptyCommit)
522 for node in (node1, node2)):
522 for node in (node1, node2)):
523 raise HTTPNotFound()
523 raise HTTPNotFound()
524
524
525 c.commit_1 = node1.commit
525 c.commit_1 = node1.commit
526 c.commit_2 = node2.commit
526 c.commit_2 = node2.commit
527
527
528 if c.action == 'download':
528 if c.action == 'download':
529 _diff = diffs.get_gitdiff(node1, node2,
529 _diff = diffs.get_gitdiff(node1, node2,
530 ignore_whitespace=ignore_whitespace,
530 ignore_whitespace=ignore_whitespace,
531 context=line_context)
531 context=line_context)
532 diff = diffs.DiffProcessor(_diff, format='gitdiff')
532 diff = diffs.DiffProcessor(_diff, format='gitdiff')
533
533
534 response = Response(self.path_filter.get_raw_patch(diff))
534 response = Response(self.path_filter.get_raw_patch(diff))
535 response.content_type = 'text/plain'
535 response.content_type = 'text/plain'
536 response.content_disposition = (
536 response.content_disposition = (
537 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
537 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
538 )
538 )
539 charset = self._get_default_encoding(c)
539 charset = self._get_default_encoding(c)
540 if charset:
540 if charset:
541 response.charset = charset
541 response.charset = charset
542 return response
542 return response
543
543
544 elif c.action == 'raw':
544 elif c.action == 'raw':
545 _diff = diffs.get_gitdiff(node1, node2,
545 _diff = diffs.get_gitdiff(node1, node2,
546 ignore_whitespace=ignore_whitespace,
546 ignore_whitespace=ignore_whitespace,
547 context=line_context)
547 context=line_context)
548 diff = diffs.DiffProcessor(_diff, format='gitdiff')
548 diff = diffs.DiffProcessor(_diff, format='gitdiff')
549
549
550 response = Response(self.path_filter.get_raw_patch(diff))
550 response = Response(self.path_filter.get_raw_patch(diff))
551 response.content_type = 'text/plain'
551 response.content_type = 'text/plain'
552 charset = self._get_default_encoding(c)
552 charset = self._get_default_encoding(c)
553 if charset:
553 if charset:
554 response.charset = charset
554 response.charset = charset
555 return response
555 return response
556
556
557 # in case we ever end up here
557 # in case we ever end up here
558 raise HTTPNotFound()
558 raise HTTPNotFound()
559
559
560 @LoginRequired()
560 @LoginRequired()
561 @HasRepoPermissionAnyDecorator(
561 @HasRepoPermissionAnyDecorator(
562 'repository.read', 'repository.write', 'repository.admin')
562 'repository.read', 'repository.write', 'repository.admin')
563 @view_config(
563 @view_config(
564 route_name='repo_files_diff_2way_redirect', request_method='GET',
564 route_name='repo_files_diff_2way_redirect', request_method='GET',
565 renderer=None)
565 renderer=None)
566 def repo_files_diff_2way_redirect(self):
566 def repo_files_diff_2way_redirect(self):
567 """
567 """
568 Kept only to make OLD links work
568 Kept only to make OLD links work
569 """
569 """
570 f_path = self._get_f_path_unchecked(self.request.matchdict)
570 f_path = self._get_f_path_unchecked(self.request.matchdict)
571 diff1 = self.request.GET.get('diff1', '')
571 diff1 = self.request.GET.get('diff1', '')
572 diff2 = self.request.GET.get('diff2', '')
572 diff2 = self.request.GET.get('diff2', '')
573
573
574 if not any((diff1, diff2)):
574 if not any((diff1, diff2)):
575 h.flash(
575 h.flash(
576 'Need query parameter "diff1" or "diff2" to generate a diff.',
576 'Need query parameter "diff1" or "diff2" to generate a diff.',
577 category='error')
577 category='error')
578 raise HTTPBadRequest()
578 raise HTTPBadRequest()
579
579
580 compare_url = h.route_path(
580 compare_url = h.route_path(
581 'repo_compare',
581 'repo_compare',
582 repo_name=self.db_repo_name,
582 repo_name=self.db_repo_name,
583 source_ref_type='rev',
583 source_ref_type='rev',
584 source_ref=diff1,
584 source_ref=diff1,
585 target_ref_type='rev',
585 target_ref_type='rev',
586 target_ref=diff2,
586 target_ref=diff2,
587 _query=dict(f_path=f_path, diffmode='sideside',
587 _query=dict(f_path=f_path, diffmode='sideside',
588 target_repo=self.db_repo_name,))
588 target_repo=self.db_repo_name,))
589 raise HTTPFound(compare_url)
589 raise HTTPFound(compare_url)
590
590
591 @LoginRequired()
591 @LoginRequired()
592 @view_config(
593 route_name='repo_files:default_commit', request_method='GET',
594 renderer=None)
595 def repo_files_default(self):
596 c = self.load_default_context()
597 ref_name = c.rhodecode_db_repo.landing_ref_name
598 landing_url = h.repo_files_by_ref_url(
599 c.rhodecode_db_repo.repo_name,
600 c.rhodecode_db_repo.repo_type,
601 f_path='',
602 ref_name=ref_name,
603 commit_id='tip',
604 query=dict(at=ref_name)
605 )
606
607 raise HTTPFound(landing_url)
608
609 @LoginRequired()
592 @HasRepoPermissionAnyDecorator(
610 @HasRepoPermissionAnyDecorator(
593 'repository.read', 'repository.write', 'repository.admin')
611 'repository.read', 'repository.write', 'repository.admin')
594 @view_config(
612 @view_config(
595 route_name='repo_files', request_method='GET',
613 route_name='repo_files', request_method='GET',
596 renderer=None)
614 renderer=None)
597 @view_config(
615 @view_config(
598 route_name='repo_files:default_path', request_method='GET',
616 route_name='repo_files:default_path', request_method='GET',
599 renderer=None)
617 renderer=None)
600 @view_config(
618 @view_config(
601 route_name='repo_files:default_commit', request_method='GET',
602 renderer=None)
603 @view_config(
604 route_name='repo_files:rendered', request_method='GET',
619 route_name='repo_files:rendered', request_method='GET',
605 renderer=None)
620 renderer=None)
606 @view_config(
621 @view_config(
607 route_name='repo_files:annotated', request_method='GET',
622 route_name='repo_files:annotated', request_method='GET',
608 renderer=None)
623 renderer=None)
609 def repo_files(self):
624 def repo_files(self):
610 c = self.load_default_context()
625 c = self.load_default_context()
611
626
612 view_name = getattr(self.request.matched_route, 'name', None)
627 view_name = getattr(self.request.matched_route, 'name', None)
613
628
614 c.annotate = view_name == 'repo_files:annotated'
629 c.annotate = view_name == 'repo_files:annotated'
615 # default is false, but .rst/.md files later are auto rendered, we can
630 # default is false, but .rst/.md files later are auto rendered, we can
616 # overwrite auto rendering by setting this GET flag
631 # overwrite auto rendering by setting this GET flag
617 c.renderer = view_name == 'repo_files:rendered' or \
632 c.renderer = view_name == 'repo_files:rendered' or \
618 not self.request.GET.get('no-render', False)
633 not self.request.GET.get('no-render', False)
619
634
620 commit_id, f_path = self._get_commit_and_path()
635 commit_id, f_path = self._get_commit_and_path()
621
636
622 c.commit = self._get_commit_or_redirect(commit_id)
637 c.commit = self._get_commit_or_redirect(commit_id)
623 c.branch = self.request.GET.get('branch', None)
638 c.branch = self.request.GET.get('branch', None)
624 c.f_path = f_path
639 c.f_path = f_path
625 at_rev = self.request.GET.get('at')
640 at_rev = self.request.GET.get('at')
626
641
627 # prev link
642 # prev link
628 try:
643 try:
629 prev_commit = c.commit.prev(c.branch)
644 prev_commit = c.commit.prev(c.branch)
630 c.prev_commit = prev_commit
645 c.prev_commit = prev_commit
631 c.url_prev = h.route_path(
646 c.url_prev = h.route_path(
632 'repo_files', repo_name=self.db_repo_name,
647 'repo_files', repo_name=self.db_repo_name,
633 commit_id=prev_commit.raw_id, f_path=f_path)
648 commit_id=prev_commit.raw_id, f_path=f_path)
634 if c.branch:
649 if c.branch:
635 c.url_prev += '?branch=%s' % c.branch
650 c.url_prev += '?branch=%s' % c.branch
636 except (CommitDoesNotExistError, VCSError):
651 except (CommitDoesNotExistError, VCSError):
637 c.url_prev = '#'
652 c.url_prev = '#'
638 c.prev_commit = EmptyCommit()
653 c.prev_commit = EmptyCommit()
639
654
640 # next link
655 # next link
641 try:
656 try:
642 next_commit = c.commit.next(c.branch)
657 next_commit = c.commit.next(c.branch)
643 c.next_commit = next_commit
658 c.next_commit = next_commit
644 c.url_next = h.route_path(
659 c.url_next = h.route_path(
645 'repo_files', repo_name=self.db_repo_name,
660 'repo_files', repo_name=self.db_repo_name,
646 commit_id=next_commit.raw_id, f_path=f_path)
661 commit_id=next_commit.raw_id, f_path=f_path)
647 if c.branch:
662 if c.branch:
648 c.url_next += '?branch=%s' % c.branch
663 c.url_next += '?branch=%s' % c.branch
649 except (CommitDoesNotExistError, VCSError):
664 except (CommitDoesNotExistError, VCSError):
650 c.url_next = '#'
665 c.url_next = '#'
651 c.next_commit = EmptyCommit()
666 c.next_commit = EmptyCommit()
652
667
653 # files or dirs
668 # files or dirs
654 try:
669 try:
655 c.file = c.commit.get_node(f_path)
670 c.file = c.commit.get_node(f_path)
656 c.file_author = True
671 c.file_author = True
657 c.file_tree = ''
672 c.file_tree = ''
658
673
659 # load file content
674 # load file content
660 if c.file.is_file():
675 if c.file.is_file():
661 c.lf_node = {}
676 c.lf_node = {}
662
677
663 has_lf_enabled = self._is_lf_enabled(self.db_repo)
678 has_lf_enabled = self._is_lf_enabled(self.db_repo)
664 if has_lf_enabled:
679 if has_lf_enabled:
665 c.lf_node = c.file.get_largefile_node()
680 c.lf_node = c.file.get_largefile_node()
666
681
667 c.file_source_page = 'true'
682 c.file_source_page = 'true'
668 c.file_last_commit = c.file.last_commit
683 c.file_last_commit = c.file.last_commit
669
684
670 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
685 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
671
686
672 if not (c.file_size_too_big or c.file.is_binary):
687 if not (c.file_size_too_big or c.file.is_binary):
673 if c.annotate: # annotation has precedence over renderer
688 if c.annotate: # annotation has precedence over renderer
674 c.annotated_lines = filenode_as_annotated_lines_tokens(
689 c.annotated_lines = filenode_as_annotated_lines_tokens(
675 c.file
690 c.file
676 )
691 )
677 else:
692 else:
678 c.renderer = (
693 c.renderer = (
679 c.renderer and h.renderer_from_filename(c.file.path)
694 c.renderer and h.renderer_from_filename(c.file.path)
680 )
695 )
681 if not c.renderer:
696 if not c.renderer:
682 c.lines = filenode_as_lines_tokens(c.file)
697 c.lines = filenode_as_lines_tokens(c.file)
683
698
684 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
699 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
685 commit_id, self.rhodecode_vcs_repo)
700 commit_id, self.rhodecode_vcs_repo)
686 c.on_branch_head = is_head
701 c.on_branch_head = is_head
687
702
688 branch = c.commit.branch if (
703 branch = c.commit.branch if (
689 c.commit.branch and '/' not in c.commit.branch) else None
704 c.commit.branch and '/' not in c.commit.branch) else None
690 c.branch_or_raw_id = branch or c.commit.raw_id
705 c.branch_or_raw_id = branch or c.commit.raw_id
691 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
706 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
692
707
693 author = c.file_last_commit.author
708 author = c.file_last_commit.author
694 c.authors = [[
709 c.authors = [[
695 h.email(author),
710 h.email(author),
696 h.person(author, 'username_or_name_or_email'),
711 h.person(author, 'username_or_name_or_email'),
697 1
712 1
698 ]]
713 ]]
699
714
700 else: # load tree content at path
715 else: # load tree content at path
701 c.file_source_page = 'false'
716 c.file_source_page = 'false'
702 c.authors = []
717 c.authors = []
703 # this loads a simple tree without metadata to speed things up
718 # this loads a simple tree without metadata to speed things up
704 # later via ajax we call repo_nodetree_full and fetch whole
719 # later via ajax we call repo_nodetree_full and fetch whole
705 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
720 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
706
721
707 c.readme_data, c.readme_file = \
722 c.readme_data, c.readme_file = \
708 self._get_readme_data(self.db_repo, c.visual.default_renderer,
723 self._get_readme_data(self.db_repo, c.visual.default_renderer,
709 c.commit.raw_id, f_path)
724 c.commit.raw_id, f_path)
710
725
711 except RepositoryError as e:
726 except RepositoryError as e:
712 h.flash(safe_str(h.escape(e)), category='error')
727 h.flash(safe_str(h.escape(e)), category='error')
713 raise HTTPNotFound()
728 raise HTTPNotFound()
714
729
715 if self.request.environ.get('HTTP_X_PJAX'):
730 if self.request.environ.get('HTTP_X_PJAX'):
716 html = render('rhodecode:templates/files/files_pjax.mako',
731 html = render('rhodecode:templates/files/files_pjax.mako',
717 self._get_template_context(c), self.request)
732 self._get_template_context(c), self.request)
718 else:
733 else:
719 html = render('rhodecode:templates/files/files.mako',
734 html = render('rhodecode:templates/files/files.mako',
720 self._get_template_context(c), self.request)
735 self._get_template_context(c), self.request)
721 return Response(html)
736 return Response(html)
722
737
723 @HasRepoPermissionAnyDecorator(
738 @HasRepoPermissionAnyDecorator(
724 'repository.read', 'repository.write', 'repository.admin')
739 'repository.read', 'repository.write', 'repository.admin')
725 @view_config(
740 @view_config(
726 route_name='repo_files:annotated_previous', request_method='GET',
741 route_name='repo_files:annotated_previous', request_method='GET',
727 renderer=None)
742 renderer=None)
728 def repo_files_annotated_previous(self):
743 def repo_files_annotated_previous(self):
729 self.load_default_context()
744 self.load_default_context()
730
745
731 commit_id, f_path = self._get_commit_and_path()
746 commit_id, f_path = self._get_commit_and_path()
732 commit = self._get_commit_or_redirect(commit_id)
747 commit = self._get_commit_or_redirect(commit_id)
733 prev_commit_id = commit.raw_id
748 prev_commit_id = commit.raw_id
734 line_anchor = self.request.GET.get('line_anchor')
749 line_anchor = self.request.GET.get('line_anchor')
735 is_file = False
750 is_file = False
736 try:
751 try:
737 _file = commit.get_node(f_path)
752 _file = commit.get_node(f_path)
738 is_file = _file.is_file()
753 is_file = _file.is_file()
739 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
754 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
740 pass
755 pass
741
756
742 if is_file:
757 if is_file:
743 history = commit.get_path_history(f_path)
758 history = commit.get_path_history(f_path)
744 prev_commit_id = history[1].raw_id \
759 prev_commit_id = history[1].raw_id \
745 if len(history) > 1 else prev_commit_id
760 if len(history) > 1 else prev_commit_id
746 prev_url = h.route_path(
761 prev_url = h.route_path(
747 'repo_files:annotated', repo_name=self.db_repo_name,
762 'repo_files:annotated', repo_name=self.db_repo_name,
748 commit_id=prev_commit_id, f_path=f_path,
763 commit_id=prev_commit_id, f_path=f_path,
749 _anchor='L{}'.format(line_anchor))
764 _anchor='L{}'.format(line_anchor))
750
765
751 raise HTTPFound(prev_url)
766 raise HTTPFound(prev_url)
752
767
753 @LoginRequired()
768 @LoginRequired()
754 @HasRepoPermissionAnyDecorator(
769 @HasRepoPermissionAnyDecorator(
755 'repository.read', 'repository.write', 'repository.admin')
770 'repository.read', 'repository.write', 'repository.admin')
756 @view_config(
771 @view_config(
757 route_name='repo_nodetree_full', request_method='GET',
772 route_name='repo_nodetree_full', request_method='GET',
758 renderer=None, xhr=True)
773 renderer=None, xhr=True)
759 @view_config(
774 @view_config(
760 route_name='repo_nodetree_full:default_path', request_method='GET',
775 route_name='repo_nodetree_full:default_path', request_method='GET',
761 renderer=None, xhr=True)
776 renderer=None, xhr=True)
762 def repo_nodetree_full(self):
777 def repo_nodetree_full(self):
763 """
778 """
764 Returns rendered html of file tree that contains commit date,
779 Returns rendered html of file tree that contains commit date,
765 author, commit_id for the specified combination of
780 author, commit_id for the specified combination of
766 repo, commit_id and file path
781 repo, commit_id and file path
767 """
782 """
768 c = self.load_default_context()
783 c = self.load_default_context()
769
784
770 commit_id, f_path = self._get_commit_and_path()
785 commit_id, f_path = self._get_commit_and_path()
771 commit = self._get_commit_or_redirect(commit_id)
786 commit = self._get_commit_or_redirect(commit_id)
772 try:
787 try:
773 dir_node = commit.get_node(f_path)
788 dir_node = commit.get_node(f_path)
774 except RepositoryError as e:
789 except RepositoryError as e:
775 return Response('error: {}'.format(h.escape(safe_str(e))))
790 return Response('error: {}'.format(h.escape(safe_str(e))))
776
791
777 if dir_node.is_file():
792 if dir_node.is_file():
778 return Response('')
793 return Response('')
779
794
780 c.file = dir_node
795 c.file = dir_node
781 c.commit = commit
796 c.commit = commit
782 at_rev = self.request.GET.get('at')
797 at_rev = self.request.GET.get('at')
783
798
784 html = self._get_tree_at_commit(
799 html = self._get_tree_at_commit(
785 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
800 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
786
801
787 return Response(html)
802 return Response(html)
788
803
789 def _get_attachement_headers(self, f_path):
804 def _get_attachement_headers(self, f_path):
790 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
805 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
791 safe_path = f_name.replace('"', '\\"')
806 safe_path = f_name.replace('"', '\\"')
792 encoded_path = urllib.quote(f_name)
807 encoded_path = urllib.quote(f_name)
793
808
794 return "attachment; " \
809 return "attachment; " \
795 "filename=\"{}\"; " \
810 "filename=\"{}\"; " \
796 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
811 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
797
812
798 @LoginRequired()
813 @LoginRequired()
799 @HasRepoPermissionAnyDecorator(
814 @HasRepoPermissionAnyDecorator(
800 'repository.read', 'repository.write', 'repository.admin')
815 'repository.read', 'repository.write', 'repository.admin')
801 @view_config(
816 @view_config(
802 route_name='repo_file_raw', request_method='GET',
817 route_name='repo_file_raw', request_method='GET',
803 renderer=None)
818 renderer=None)
804 def repo_file_raw(self):
819 def repo_file_raw(self):
805 """
820 """
806 Action for show as raw, some mimetypes are "rendered",
821 Action for show as raw, some mimetypes are "rendered",
807 those include images, icons.
822 those include images, icons.
808 """
823 """
809 c = self.load_default_context()
824 c = self.load_default_context()
810
825
811 commit_id, f_path = self._get_commit_and_path()
826 commit_id, f_path = self._get_commit_and_path()
812 commit = self._get_commit_or_redirect(commit_id)
827 commit = self._get_commit_or_redirect(commit_id)
813 file_node = self._get_filenode_or_redirect(commit, f_path)
828 file_node = self._get_filenode_or_redirect(commit, f_path)
814
829
815 raw_mimetype_mapping = {
830 raw_mimetype_mapping = {
816 # map original mimetype to a mimetype used for "show as raw"
831 # map original mimetype to a mimetype used for "show as raw"
817 # you can also provide a content-disposition to override the
832 # you can also provide a content-disposition to override the
818 # default "attachment" disposition.
833 # default "attachment" disposition.
819 # orig_type: (new_type, new_dispo)
834 # orig_type: (new_type, new_dispo)
820
835
821 # show images inline:
836 # show images inline:
822 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
837 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
823 # for example render an SVG with javascript inside or even render
838 # for example render an SVG with javascript inside or even render
824 # HTML.
839 # HTML.
825 'image/x-icon': ('image/x-icon', 'inline'),
840 'image/x-icon': ('image/x-icon', 'inline'),
826 'image/png': ('image/png', 'inline'),
841 'image/png': ('image/png', 'inline'),
827 'image/gif': ('image/gif', 'inline'),
842 'image/gif': ('image/gif', 'inline'),
828 'image/jpeg': ('image/jpeg', 'inline'),
843 'image/jpeg': ('image/jpeg', 'inline'),
829 'application/pdf': ('application/pdf', 'inline'),
844 'application/pdf': ('application/pdf', 'inline'),
830 }
845 }
831
846
832 mimetype = file_node.mimetype
847 mimetype = file_node.mimetype
833 try:
848 try:
834 mimetype, disposition = raw_mimetype_mapping[mimetype]
849 mimetype, disposition = raw_mimetype_mapping[mimetype]
835 except KeyError:
850 except KeyError:
836 # we don't know anything special about this, handle it safely
851 # we don't know anything special about this, handle it safely
837 if file_node.is_binary:
852 if file_node.is_binary:
838 # do same as download raw for binary files
853 # do same as download raw for binary files
839 mimetype, disposition = 'application/octet-stream', 'attachment'
854 mimetype, disposition = 'application/octet-stream', 'attachment'
840 else:
855 else:
841 # do not just use the original mimetype, but force text/plain,
856 # do not just use the original mimetype, but force text/plain,
842 # otherwise it would serve text/html and that might be unsafe.
857 # otherwise it would serve text/html and that might be unsafe.
843 # Note: underlying vcs library fakes text/plain mimetype if the
858 # Note: underlying vcs library fakes text/plain mimetype if the
844 # mimetype can not be determined and it thinks it is not
859 # mimetype can not be determined and it thinks it is not
845 # binary.This might lead to erroneous text display in some
860 # binary.This might lead to erroneous text display in some
846 # cases, but helps in other cases, like with text files
861 # cases, but helps in other cases, like with text files
847 # without extension.
862 # without extension.
848 mimetype, disposition = 'text/plain', 'inline'
863 mimetype, disposition = 'text/plain', 'inline'
849
864
850 if disposition == 'attachment':
865 if disposition == 'attachment':
851 disposition = self._get_attachement_headers(f_path)
866 disposition = self._get_attachement_headers(f_path)
852
867
853 stream_content = file_node.stream_bytes()
868 stream_content = file_node.stream_bytes()
854
869
855 response = Response(app_iter=stream_content)
870 response = Response(app_iter=stream_content)
856 response.content_disposition = disposition
871 response.content_disposition = disposition
857 response.content_type = mimetype
872 response.content_type = mimetype
858
873
859 charset = self._get_default_encoding(c)
874 charset = self._get_default_encoding(c)
860 if charset:
875 if charset:
861 response.charset = charset
876 response.charset = charset
862
877
863 return response
878 return response
864
879
865 @LoginRequired()
880 @LoginRequired()
866 @HasRepoPermissionAnyDecorator(
881 @HasRepoPermissionAnyDecorator(
867 'repository.read', 'repository.write', 'repository.admin')
882 'repository.read', 'repository.write', 'repository.admin')
868 @view_config(
883 @view_config(
869 route_name='repo_file_download', request_method='GET',
884 route_name='repo_file_download', request_method='GET',
870 renderer=None)
885 renderer=None)
871 @view_config(
886 @view_config(
872 route_name='repo_file_download:legacy', request_method='GET',
887 route_name='repo_file_download:legacy', request_method='GET',
873 renderer=None)
888 renderer=None)
874 def repo_file_download(self):
889 def repo_file_download(self):
875 c = self.load_default_context()
890 c = self.load_default_context()
876
891
877 commit_id, f_path = self._get_commit_and_path()
892 commit_id, f_path = self._get_commit_and_path()
878 commit = self._get_commit_or_redirect(commit_id)
893 commit = self._get_commit_or_redirect(commit_id)
879 file_node = self._get_filenode_or_redirect(commit, f_path)
894 file_node = self._get_filenode_or_redirect(commit, f_path)
880
895
881 if self.request.GET.get('lf'):
896 if self.request.GET.get('lf'):
882 # only if lf get flag is passed, we download this file
897 # only if lf get flag is passed, we download this file
883 # as LFS/Largefile
898 # as LFS/Largefile
884 lf_node = file_node.get_largefile_node()
899 lf_node = file_node.get_largefile_node()
885 if lf_node:
900 if lf_node:
886 # overwrite our pointer with the REAL large-file
901 # overwrite our pointer with the REAL large-file
887 file_node = lf_node
902 file_node = lf_node
888
903
889 disposition = self._get_attachement_headers(f_path)
904 disposition = self._get_attachement_headers(f_path)
890
905
891 stream_content = file_node.stream_bytes()
906 stream_content = file_node.stream_bytes()
892
907
893 response = Response(app_iter=stream_content)
908 response = Response(app_iter=stream_content)
894 response.content_disposition = disposition
909 response.content_disposition = disposition
895 response.content_type = file_node.mimetype
910 response.content_type = file_node.mimetype
896
911
897 charset = self._get_default_encoding(c)
912 charset = self._get_default_encoding(c)
898 if charset:
913 if charset:
899 response.charset = charset
914 response.charset = charset
900
915
901 return response
916 return response
902
917
903 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
918 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
904
919
905 cache_seconds = safe_int(
920 cache_seconds = safe_int(
906 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
921 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
907 cache_on = cache_seconds > 0
922 cache_on = cache_seconds > 0
908 log.debug(
923 log.debug(
909 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
924 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
910 'with caching: %s[TTL: %ss]' % (
925 'with caching: %s[TTL: %ss]' % (
911 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
926 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
912
927
913 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
928 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
914 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
929 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
915
930
916 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
931 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
917 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
932 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
918 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
933 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
919 _repo_id, commit_id, f_path)
934 _repo_id, commit_id, f_path)
920 try:
935 try:
921 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
936 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
922 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
937 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
923 log.exception(safe_str(e))
938 log.exception(safe_str(e))
924 h.flash(safe_str(h.escape(e)), category='error')
939 h.flash(safe_str(h.escape(e)), category='error')
925 raise HTTPFound(h.route_path(
940 raise HTTPFound(h.route_path(
926 'repo_files', repo_name=self.db_repo_name,
941 'repo_files', repo_name=self.db_repo_name,
927 commit_id='tip', f_path='/'))
942 commit_id='tip', f_path='/'))
928
943
929 return _d + _f
944 return _d + _f
930
945
931 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
946 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
932 commit_id, f_path)
947 commit_id, f_path)
933 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
948 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
934
949
935 @LoginRequired()
950 @LoginRequired()
936 @HasRepoPermissionAnyDecorator(
951 @HasRepoPermissionAnyDecorator(
937 'repository.read', 'repository.write', 'repository.admin')
952 'repository.read', 'repository.write', 'repository.admin')
938 @view_config(
953 @view_config(
939 route_name='repo_files_nodelist', request_method='GET',
954 route_name='repo_files_nodelist', request_method='GET',
940 renderer='json_ext', xhr=True)
955 renderer='json_ext', xhr=True)
941 def repo_nodelist(self):
956 def repo_nodelist(self):
942 self.load_default_context()
957 self.load_default_context()
943
958
944 commit_id, f_path = self._get_commit_and_path()
959 commit_id, f_path = self._get_commit_and_path()
945 commit = self._get_commit_or_redirect(commit_id)
960 commit = self._get_commit_or_redirect(commit_id)
946
961
947 metadata = self._get_nodelist_at_commit(
962 metadata = self._get_nodelist_at_commit(
948 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
963 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
949 return {'nodes': metadata}
964 return {'nodes': metadata}
950
965
951 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
966 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
952 items = []
967 items = []
953 for name, commit_id in branches_or_tags.items():
968 for name, commit_id in branches_or_tags.items():
954 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
969 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
955 items.append((sym_ref, name, ref_type))
970 items.append((sym_ref, name, ref_type))
956 return items
971 return items
957
972
958 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
973 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
959 return commit_id
974 return commit_id
960
975
961 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
976 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
962 return commit_id
977 return commit_id
963
978
964 # NOTE(dan): old code we used in "diff" mode compare
979 # NOTE(dan): old code we used in "diff" mode compare
965 new_f_path = vcspath.join(name, f_path)
980 new_f_path = vcspath.join(name, f_path)
966 return u'%s@%s' % (new_f_path, commit_id)
981 return u'%s@%s' % (new_f_path, commit_id)
967
982
968 def _get_node_history(self, commit_obj, f_path, commits=None):
983 def _get_node_history(self, commit_obj, f_path, commits=None):
969 """
984 """
970 get commit history for given node
985 get commit history for given node
971
986
972 :param commit_obj: commit to calculate history
987 :param commit_obj: commit to calculate history
973 :param f_path: path for node to calculate history for
988 :param f_path: path for node to calculate history for
974 :param commits: if passed don't calculate history and take
989 :param commits: if passed don't calculate history and take
975 commits defined in this list
990 commits defined in this list
976 """
991 """
977 _ = self.request.translate
992 _ = self.request.translate
978
993
979 # calculate history based on tip
994 # calculate history based on tip
980 tip = self.rhodecode_vcs_repo.get_commit()
995 tip = self.rhodecode_vcs_repo.get_commit()
981 if commits is None:
996 if commits is None:
982 pre_load = ["author", "branch"]
997 pre_load = ["author", "branch"]
983 try:
998 try:
984 commits = tip.get_path_history(f_path, pre_load=pre_load)
999 commits = tip.get_path_history(f_path, pre_load=pre_load)
985 except (NodeDoesNotExistError, CommitError):
1000 except (NodeDoesNotExistError, CommitError):
986 # this node is not present at tip!
1001 # this node is not present at tip!
987 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
1002 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
988
1003
989 history = []
1004 history = []
990 commits_group = ([], _("Changesets"))
1005 commits_group = ([], _("Changesets"))
991 for commit in commits:
1006 for commit in commits:
992 branch = ' (%s)' % commit.branch if commit.branch else ''
1007 branch = ' (%s)' % commit.branch if commit.branch else ''
993 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1008 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
994 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
1009 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
995 history.append(commits_group)
1010 history.append(commits_group)
996
1011
997 symbolic_reference = self._symbolic_reference
1012 symbolic_reference = self._symbolic_reference
998
1013
999 if self.rhodecode_vcs_repo.alias == 'svn':
1014 if self.rhodecode_vcs_repo.alias == 'svn':
1000 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1015 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1001 f_path, self.rhodecode_vcs_repo)
1016 f_path, self.rhodecode_vcs_repo)
1002 if adjusted_f_path != f_path:
1017 if adjusted_f_path != f_path:
1003 log.debug(
1018 log.debug(
1004 'Recognized svn tag or branch in file "%s", using svn '
1019 'Recognized svn tag or branch in file "%s", using svn '
1005 'specific symbolic references', f_path)
1020 'specific symbolic references', f_path)
1006 f_path = adjusted_f_path
1021 f_path = adjusted_f_path
1007 symbolic_reference = self._symbolic_reference_svn
1022 symbolic_reference = self._symbolic_reference_svn
1008
1023
1009 branches = self._create_references(
1024 branches = self._create_references(
1010 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1025 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1011 branches_group = (branches, _("Branches"))
1026 branches_group = (branches, _("Branches"))
1012
1027
1013 tags = self._create_references(
1028 tags = self._create_references(
1014 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1029 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1015 tags_group = (tags, _("Tags"))
1030 tags_group = (tags, _("Tags"))
1016
1031
1017 history.append(branches_group)
1032 history.append(branches_group)
1018 history.append(tags_group)
1033 history.append(tags_group)
1019
1034
1020 return history, commits
1035 return history, commits
1021
1036
1022 @LoginRequired()
1037 @LoginRequired()
1023 @HasRepoPermissionAnyDecorator(
1038 @HasRepoPermissionAnyDecorator(
1024 'repository.read', 'repository.write', 'repository.admin')
1039 'repository.read', 'repository.write', 'repository.admin')
1025 @view_config(
1040 @view_config(
1026 route_name='repo_file_history', request_method='GET',
1041 route_name='repo_file_history', request_method='GET',
1027 renderer='json_ext')
1042 renderer='json_ext')
1028 def repo_file_history(self):
1043 def repo_file_history(self):
1029 self.load_default_context()
1044 self.load_default_context()
1030
1045
1031 commit_id, f_path = self._get_commit_and_path()
1046 commit_id, f_path = self._get_commit_and_path()
1032 commit = self._get_commit_or_redirect(commit_id)
1047 commit = self._get_commit_or_redirect(commit_id)
1033 file_node = self._get_filenode_or_redirect(commit, f_path)
1048 file_node = self._get_filenode_or_redirect(commit, f_path)
1034
1049
1035 if file_node.is_file():
1050 if file_node.is_file():
1036 file_history, _hist = self._get_node_history(commit, f_path)
1051 file_history, _hist = self._get_node_history(commit, f_path)
1037
1052
1038 res = []
1053 res = []
1039 for section_items, section in file_history:
1054 for section_items, section in file_history:
1040 items = []
1055 items = []
1041 for obj_id, obj_text, obj_type in section_items:
1056 for obj_id, obj_text, obj_type in section_items:
1042 at_rev = ''
1057 at_rev = ''
1043 if obj_type in ['branch', 'bookmark', 'tag']:
1058 if obj_type in ['branch', 'bookmark', 'tag']:
1044 at_rev = obj_text
1059 at_rev = obj_text
1045 entry = {
1060 entry = {
1046 'id': obj_id,
1061 'id': obj_id,
1047 'text': obj_text,
1062 'text': obj_text,
1048 'type': obj_type,
1063 'type': obj_type,
1049 'at_rev': at_rev
1064 'at_rev': at_rev
1050 }
1065 }
1051
1066
1052 items.append(entry)
1067 items.append(entry)
1053
1068
1054 res.append({
1069 res.append({
1055 'text': section,
1070 'text': section,
1056 'children': items
1071 'children': items
1057 })
1072 })
1058
1073
1059 data = {
1074 data = {
1060 'more': False,
1075 'more': False,
1061 'results': res
1076 'results': res
1062 }
1077 }
1063 return data
1078 return data
1064
1079
1065 log.warning('Cannot fetch history for directory')
1080 log.warning('Cannot fetch history for directory')
1066 raise HTTPBadRequest()
1081 raise HTTPBadRequest()
1067
1082
1068 @LoginRequired()
1083 @LoginRequired()
1069 @HasRepoPermissionAnyDecorator(
1084 @HasRepoPermissionAnyDecorator(
1070 'repository.read', 'repository.write', 'repository.admin')
1085 'repository.read', 'repository.write', 'repository.admin')
1071 @view_config(
1086 @view_config(
1072 route_name='repo_file_authors', request_method='GET',
1087 route_name='repo_file_authors', request_method='GET',
1073 renderer='rhodecode:templates/files/file_authors_box.mako')
1088 renderer='rhodecode:templates/files/file_authors_box.mako')
1074 def repo_file_authors(self):
1089 def repo_file_authors(self):
1075 c = self.load_default_context()
1090 c = self.load_default_context()
1076
1091
1077 commit_id, f_path = self._get_commit_and_path()
1092 commit_id, f_path = self._get_commit_and_path()
1078 commit = self._get_commit_or_redirect(commit_id)
1093 commit = self._get_commit_or_redirect(commit_id)
1079 file_node = self._get_filenode_or_redirect(commit, f_path)
1094 file_node = self._get_filenode_or_redirect(commit, f_path)
1080
1095
1081 if not file_node.is_file():
1096 if not file_node.is_file():
1082 raise HTTPBadRequest()
1097 raise HTTPBadRequest()
1083
1098
1084 c.file_last_commit = file_node.last_commit
1099 c.file_last_commit = file_node.last_commit
1085 if self.request.GET.get('annotate') == '1':
1100 if self.request.GET.get('annotate') == '1':
1086 # use _hist from annotation if annotation mode is on
1101 # use _hist from annotation if annotation mode is on
1087 commit_ids = set(x[1] for x in file_node.annotate)
1102 commit_ids = set(x[1] for x in file_node.annotate)
1088 _hist = (
1103 _hist = (
1089 self.rhodecode_vcs_repo.get_commit(commit_id)
1104 self.rhodecode_vcs_repo.get_commit(commit_id)
1090 for commit_id in commit_ids)
1105 for commit_id in commit_ids)
1091 else:
1106 else:
1092 _f_history, _hist = self._get_node_history(commit, f_path)
1107 _f_history, _hist = self._get_node_history(commit, f_path)
1093 c.file_author = False
1108 c.file_author = False
1094
1109
1095 unique = collections.OrderedDict()
1110 unique = collections.OrderedDict()
1096 for commit in _hist:
1111 for commit in _hist:
1097 author = commit.author
1112 author = commit.author
1098 if author not in unique:
1113 if author not in unique:
1099 unique[commit.author] = [
1114 unique[commit.author] = [
1100 h.email(author),
1115 h.email(author),
1101 h.person(author, 'username_or_name_or_email'),
1116 h.person(author, 'username_or_name_or_email'),
1102 1 # counter
1117 1 # counter
1103 ]
1118 ]
1104
1119
1105 else:
1120 else:
1106 # increase counter
1121 # increase counter
1107 unique[commit.author][2] += 1
1122 unique[commit.author][2] += 1
1108
1123
1109 c.authors = [val for val in unique.values()]
1124 c.authors = [val for val in unique.values()]
1110
1125
1111 return self._get_template_context(c)
1126 return self._get_template_context(c)
1112
1127
1113 @LoginRequired()
1128 @LoginRequired()
1114 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1129 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1115 @view_config(
1130 @view_config(
1116 route_name='repo_files_check_head', request_method='POST',
1131 route_name='repo_files_check_head', request_method='POST',
1117 renderer='json_ext', xhr=True)
1132 renderer='json_ext', xhr=True)
1118 def repo_files_check_head(self):
1133 def repo_files_check_head(self):
1119 self.load_default_context()
1134 self.load_default_context()
1120
1135
1121 commit_id, f_path = self._get_commit_and_path()
1136 commit_id, f_path = self._get_commit_and_path()
1122 _branch_name, _sha_commit_id, is_head = \
1137 _branch_name, _sha_commit_id, is_head = \
1123 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1138 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1124
1139
1125 new_path = self.request.POST.get('path')
1140 new_path = self.request.POST.get('path')
1126 operation = self.request.POST.get('operation')
1141 operation = self.request.POST.get('operation')
1127 path_exist = ''
1142 path_exist = ''
1128
1143
1129 if new_path and operation in ['create', 'upload']:
1144 if new_path and operation in ['create', 'upload']:
1130 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1145 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1131 try:
1146 try:
1132 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1147 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1133 # NOTE(dan): construct whole path without leading /
1148 # NOTE(dan): construct whole path without leading /
1134 file_node = commit_obj.get_node(new_f_path)
1149 file_node = commit_obj.get_node(new_f_path)
1135 if file_node is not None:
1150 if file_node is not None:
1136 path_exist = new_f_path
1151 path_exist = new_f_path
1137 except EmptyRepositoryError:
1152 except EmptyRepositoryError:
1138 pass
1153 pass
1139 except Exception:
1154 except Exception:
1140 pass
1155 pass
1141
1156
1142 return {
1157 return {
1143 'branch': _branch_name,
1158 'branch': _branch_name,
1144 'sha': _sha_commit_id,
1159 'sha': _sha_commit_id,
1145 'is_head': is_head,
1160 'is_head': is_head,
1146 'path_exists': path_exist
1161 'path_exists': path_exist
1147 }
1162 }
1148
1163
1149 @LoginRequired()
1164 @LoginRequired()
1150 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1165 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1151 @view_config(
1166 @view_config(
1152 route_name='repo_files_remove_file', request_method='GET',
1167 route_name='repo_files_remove_file', request_method='GET',
1153 renderer='rhodecode:templates/files/files_delete.mako')
1168 renderer='rhodecode:templates/files/files_delete.mako')
1154 def repo_files_remove_file(self):
1169 def repo_files_remove_file(self):
1155 _ = self.request.translate
1170 _ = self.request.translate
1156 c = self.load_default_context()
1171 c = self.load_default_context()
1157 commit_id, f_path = self._get_commit_and_path()
1172 commit_id, f_path = self._get_commit_and_path()
1158
1173
1159 self._ensure_not_locked()
1174 self._ensure_not_locked()
1160 _branch_name, _sha_commit_id, is_head = \
1175 _branch_name, _sha_commit_id, is_head = \
1161 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1176 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1162
1177
1163 self.forbid_non_head(is_head, f_path)
1178 self.forbid_non_head(is_head, f_path)
1164 self.check_branch_permission(_branch_name)
1179 self.check_branch_permission(_branch_name)
1165
1180
1166 c.commit = self._get_commit_or_redirect(commit_id)
1181 c.commit = self._get_commit_or_redirect(commit_id)
1167 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1182 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1168
1183
1169 c.default_message = _(
1184 c.default_message = _(
1170 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1185 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1171 c.f_path = f_path
1186 c.f_path = f_path
1172
1187
1173 return self._get_template_context(c)
1188 return self._get_template_context(c)
1174
1189
1175 @LoginRequired()
1190 @LoginRequired()
1176 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1191 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1177 @CSRFRequired()
1192 @CSRFRequired()
1178 @view_config(
1193 @view_config(
1179 route_name='repo_files_delete_file', request_method='POST',
1194 route_name='repo_files_delete_file', request_method='POST',
1180 renderer=None)
1195 renderer=None)
1181 def repo_files_delete_file(self):
1196 def repo_files_delete_file(self):
1182 _ = self.request.translate
1197 _ = self.request.translate
1183
1198
1184 c = self.load_default_context()
1199 c = self.load_default_context()
1185 commit_id, f_path = self._get_commit_and_path()
1200 commit_id, f_path = self._get_commit_and_path()
1186
1201
1187 self._ensure_not_locked()
1202 self._ensure_not_locked()
1188 _branch_name, _sha_commit_id, is_head = \
1203 _branch_name, _sha_commit_id, is_head = \
1189 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1204 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1190
1205
1191 self.forbid_non_head(is_head, f_path)
1206 self.forbid_non_head(is_head, f_path)
1192 self.check_branch_permission(_branch_name)
1207 self.check_branch_permission(_branch_name)
1193
1208
1194 c.commit = self._get_commit_or_redirect(commit_id)
1209 c.commit = self._get_commit_or_redirect(commit_id)
1195 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1210 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1196
1211
1197 c.default_message = _(
1212 c.default_message = _(
1198 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1213 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1199 c.f_path = f_path
1214 c.f_path = f_path
1200 node_path = f_path
1215 node_path = f_path
1201 author = self._rhodecode_db_user.full_contact
1216 author = self._rhodecode_db_user.full_contact
1202 message = self.request.POST.get('message') or c.default_message
1217 message = self.request.POST.get('message') or c.default_message
1203 try:
1218 try:
1204 nodes = {
1219 nodes = {
1205 node_path: {
1220 node_path: {
1206 'content': ''
1221 'content': ''
1207 }
1222 }
1208 }
1223 }
1209 ScmModel().delete_nodes(
1224 ScmModel().delete_nodes(
1210 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1225 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1211 message=message,
1226 message=message,
1212 nodes=nodes,
1227 nodes=nodes,
1213 parent_commit=c.commit,
1228 parent_commit=c.commit,
1214 author=author,
1229 author=author,
1215 )
1230 )
1216
1231
1217 h.flash(
1232 h.flash(
1218 _('Successfully deleted file `{}`').format(
1233 _('Successfully deleted file `{}`').format(
1219 h.escape(f_path)), category='success')
1234 h.escape(f_path)), category='success')
1220 except Exception:
1235 except Exception:
1221 log.exception('Error during commit operation')
1236 log.exception('Error during commit operation')
1222 h.flash(_('Error occurred during commit'), category='error')
1237 h.flash(_('Error occurred during commit'), category='error')
1223 raise HTTPFound(
1238 raise HTTPFound(
1224 h.route_path('repo_commit', repo_name=self.db_repo_name,
1239 h.route_path('repo_commit', repo_name=self.db_repo_name,
1225 commit_id='tip'))
1240 commit_id='tip'))
1226
1241
1227 @LoginRequired()
1242 @LoginRequired()
1228 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1243 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1229 @view_config(
1244 @view_config(
1230 route_name='repo_files_edit_file', request_method='GET',
1245 route_name='repo_files_edit_file', request_method='GET',
1231 renderer='rhodecode:templates/files/files_edit.mako')
1246 renderer='rhodecode:templates/files/files_edit.mako')
1232 def repo_files_edit_file(self):
1247 def repo_files_edit_file(self):
1233 _ = self.request.translate
1248 _ = self.request.translate
1234 c = self.load_default_context()
1249 c = self.load_default_context()
1235 commit_id, f_path = self._get_commit_and_path()
1250 commit_id, f_path = self._get_commit_and_path()
1236
1251
1237 self._ensure_not_locked()
1252 self._ensure_not_locked()
1238 _branch_name, _sha_commit_id, is_head = \
1253 _branch_name, _sha_commit_id, is_head = \
1239 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1254 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1240
1255
1241 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1256 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1242 self.check_branch_permission(_branch_name, commit_id=commit_id)
1257 self.check_branch_permission(_branch_name, commit_id=commit_id)
1243
1258
1244 c.commit = self._get_commit_or_redirect(commit_id)
1259 c.commit = self._get_commit_or_redirect(commit_id)
1245 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1260 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1246
1261
1247 if c.file.is_binary:
1262 if c.file.is_binary:
1248 files_url = h.route_path(
1263 files_url = h.route_path(
1249 'repo_files',
1264 'repo_files',
1250 repo_name=self.db_repo_name,
1265 repo_name=self.db_repo_name,
1251 commit_id=c.commit.raw_id, f_path=f_path)
1266 commit_id=c.commit.raw_id, f_path=f_path)
1252 raise HTTPFound(files_url)
1267 raise HTTPFound(files_url)
1253
1268
1254 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1269 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1255 c.f_path = f_path
1270 c.f_path = f_path
1256
1271
1257 return self._get_template_context(c)
1272 return self._get_template_context(c)
1258
1273
1259 @LoginRequired()
1274 @LoginRequired()
1260 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1275 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1261 @CSRFRequired()
1276 @CSRFRequired()
1262 @view_config(
1277 @view_config(
1263 route_name='repo_files_update_file', request_method='POST',
1278 route_name='repo_files_update_file', request_method='POST',
1264 renderer=None)
1279 renderer=None)
1265 def repo_files_update_file(self):
1280 def repo_files_update_file(self):
1266 _ = self.request.translate
1281 _ = self.request.translate
1267 c = self.load_default_context()
1282 c = self.load_default_context()
1268 commit_id, f_path = self._get_commit_and_path()
1283 commit_id, f_path = self._get_commit_and_path()
1269
1284
1270 self._ensure_not_locked()
1285 self._ensure_not_locked()
1271
1286
1272 c.commit = self._get_commit_or_redirect(commit_id)
1287 c.commit = self._get_commit_or_redirect(commit_id)
1273 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1288 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1274
1289
1275 if c.file.is_binary:
1290 if c.file.is_binary:
1276 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1291 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1277 commit_id=c.commit.raw_id, f_path=f_path))
1292 commit_id=c.commit.raw_id, f_path=f_path))
1278
1293
1279 _branch_name, _sha_commit_id, is_head = \
1294 _branch_name, _sha_commit_id, is_head = \
1280 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1295 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1281
1296
1282 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1297 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1283 self.check_branch_permission(_branch_name, commit_id=commit_id)
1298 self.check_branch_permission(_branch_name, commit_id=commit_id)
1284
1299
1285 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1300 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1286 c.f_path = f_path
1301 c.f_path = f_path
1287
1302
1288 old_content = c.file.content
1303 old_content = c.file.content
1289 sl = old_content.splitlines(1)
1304 sl = old_content.splitlines(1)
1290 first_line = sl[0] if sl else ''
1305 first_line = sl[0] if sl else ''
1291
1306
1292 r_post = self.request.POST
1307 r_post = self.request.POST
1293 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1308 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1294 line_ending_mode = detect_mode(first_line, 0)
1309 line_ending_mode = detect_mode(first_line, 0)
1295 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1310 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1296
1311
1297 message = r_post.get('message') or c.default_message
1312 message = r_post.get('message') or c.default_message
1298 org_node_path = c.file.unicode_path
1313 org_node_path = c.file.unicode_path
1299 filename = r_post['filename']
1314 filename = r_post['filename']
1300
1315
1301 root_path = c.file.dir_path
1316 root_path = c.file.dir_path
1302 pure_path = self.create_pure_path(root_path, filename)
1317 pure_path = self.create_pure_path(root_path, filename)
1303 node_path = safe_unicode(bytes(pure_path))
1318 node_path = safe_unicode(bytes(pure_path))
1304
1319
1305 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1320 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1306 commit_id=commit_id)
1321 commit_id=commit_id)
1307 if content == old_content and node_path == org_node_path:
1322 if content == old_content and node_path == org_node_path:
1308 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1323 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1309 category='warning')
1324 category='warning')
1310 raise HTTPFound(default_redirect_url)
1325 raise HTTPFound(default_redirect_url)
1311
1326
1312 try:
1327 try:
1313 mapping = {
1328 mapping = {
1314 org_node_path: {
1329 org_node_path: {
1315 'org_filename': org_node_path,
1330 'org_filename': org_node_path,
1316 'filename': node_path,
1331 'filename': node_path,
1317 'content': content,
1332 'content': content,
1318 'lexer': '',
1333 'lexer': '',
1319 'op': 'mod',
1334 'op': 'mod',
1320 'mode': c.file.mode
1335 'mode': c.file.mode
1321 }
1336 }
1322 }
1337 }
1323
1338
1324 commit = ScmModel().update_nodes(
1339 commit = ScmModel().update_nodes(
1325 user=self._rhodecode_db_user.user_id,
1340 user=self._rhodecode_db_user.user_id,
1326 repo=self.db_repo,
1341 repo=self.db_repo,
1327 message=message,
1342 message=message,
1328 nodes=mapping,
1343 nodes=mapping,
1329 parent_commit=c.commit,
1344 parent_commit=c.commit,
1330 )
1345 )
1331
1346
1332 h.flash(_('Successfully committed changes to file `{}`').format(
1347 h.flash(_('Successfully committed changes to file `{}`').format(
1333 h.escape(f_path)), category='success')
1348 h.escape(f_path)), category='success')
1334 default_redirect_url = h.route_path(
1349 default_redirect_url = h.route_path(
1335 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1350 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1336
1351
1337 except Exception:
1352 except Exception:
1338 log.exception('Error occurred during commit')
1353 log.exception('Error occurred during commit')
1339 h.flash(_('Error occurred during commit'), category='error')
1354 h.flash(_('Error occurred during commit'), category='error')
1340
1355
1341 raise HTTPFound(default_redirect_url)
1356 raise HTTPFound(default_redirect_url)
1342
1357
1343 @LoginRequired()
1358 @LoginRequired()
1344 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1359 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1345 @view_config(
1360 @view_config(
1346 route_name='repo_files_add_file', request_method='GET',
1361 route_name='repo_files_add_file', request_method='GET',
1347 renderer='rhodecode:templates/files/files_add.mako')
1362 renderer='rhodecode:templates/files/files_add.mako')
1348 @view_config(
1363 @view_config(
1349 route_name='repo_files_upload_file', request_method='GET',
1364 route_name='repo_files_upload_file', request_method='GET',
1350 renderer='rhodecode:templates/files/files_upload.mako')
1365 renderer='rhodecode:templates/files/files_upload.mako')
1351 def repo_files_add_file(self):
1366 def repo_files_add_file(self):
1352 _ = self.request.translate
1367 _ = self.request.translate
1353 c = self.load_default_context()
1368 c = self.load_default_context()
1354 commit_id, f_path = self._get_commit_and_path()
1369 commit_id, f_path = self._get_commit_and_path()
1355
1370
1356 self._ensure_not_locked()
1371 self._ensure_not_locked()
1357
1372
1358 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1373 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1359 if c.commit is None:
1374 if c.commit is None:
1360 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1375 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1361
1376
1362 if self.rhodecode_vcs_repo.is_empty():
1377 if self.rhodecode_vcs_repo.is_empty():
1363 # for empty repository we cannot check for current branch, we rely on
1378 # for empty repository we cannot check for current branch, we rely on
1364 # c.commit.branch instead
1379 # c.commit.branch instead
1365 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1380 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1366 else:
1381 else:
1367 _branch_name, _sha_commit_id, is_head = \
1382 _branch_name, _sha_commit_id, is_head = \
1368 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1383 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1369
1384
1370 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1385 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1371 self.check_branch_permission(_branch_name, commit_id=commit_id)
1386 self.check_branch_permission(_branch_name, commit_id=commit_id)
1372
1387
1373 c.default_message = (_('Added file via RhodeCode Enterprise'))
1388 c.default_message = (_('Added file via RhodeCode Enterprise'))
1374 c.f_path = f_path.lstrip('/') # ensure not relative path
1389 c.f_path = f_path.lstrip('/') # ensure not relative path
1375
1390
1376 return self._get_template_context(c)
1391 return self._get_template_context(c)
1377
1392
1378 @LoginRequired()
1393 @LoginRequired()
1379 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1394 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1380 @CSRFRequired()
1395 @CSRFRequired()
1381 @view_config(
1396 @view_config(
1382 route_name='repo_files_create_file', request_method='POST',
1397 route_name='repo_files_create_file', request_method='POST',
1383 renderer=None)
1398 renderer=None)
1384 def repo_files_create_file(self):
1399 def repo_files_create_file(self):
1385 _ = self.request.translate
1400 _ = self.request.translate
1386 c = self.load_default_context()
1401 c = self.load_default_context()
1387 commit_id, f_path = self._get_commit_and_path()
1402 commit_id, f_path = self._get_commit_and_path()
1388
1403
1389 self._ensure_not_locked()
1404 self._ensure_not_locked()
1390
1405
1391 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1406 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1392 if c.commit is None:
1407 if c.commit is None:
1393 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1408 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1394
1409
1395 # calculate redirect URL
1410 # calculate redirect URL
1396 if self.rhodecode_vcs_repo.is_empty():
1411 if self.rhodecode_vcs_repo.is_empty():
1397 default_redirect_url = h.route_path(
1412 default_redirect_url = h.route_path(
1398 'repo_summary', repo_name=self.db_repo_name)
1413 'repo_summary', repo_name=self.db_repo_name)
1399 else:
1414 else:
1400 default_redirect_url = h.route_path(
1415 default_redirect_url = h.route_path(
1401 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1416 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1402
1417
1403 if self.rhodecode_vcs_repo.is_empty():
1418 if self.rhodecode_vcs_repo.is_empty():
1404 # for empty repository we cannot check for current branch, we rely on
1419 # for empty repository we cannot check for current branch, we rely on
1405 # c.commit.branch instead
1420 # c.commit.branch instead
1406 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1421 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1407 else:
1422 else:
1408 _branch_name, _sha_commit_id, is_head = \
1423 _branch_name, _sha_commit_id, is_head = \
1409 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1424 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1410
1425
1411 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1426 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1412 self.check_branch_permission(_branch_name, commit_id=commit_id)
1427 self.check_branch_permission(_branch_name, commit_id=commit_id)
1413
1428
1414 c.default_message = (_('Added file via RhodeCode Enterprise'))
1429 c.default_message = (_('Added file via RhodeCode Enterprise'))
1415 c.f_path = f_path
1430 c.f_path = f_path
1416
1431
1417 r_post = self.request.POST
1432 r_post = self.request.POST
1418 message = r_post.get('message') or c.default_message
1433 message = r_post.get('message') or c.default_message
1419 filename = r_post.get('filename')
1434 filename = r_post.get('filename')
1420 unix_mode = 0
1435 unix_mode = 0
1421 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1436 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1422
1437
1423 if not filename:
1438 if not filename:
1424 # If there's no commit, redirect to repo summary
1439 # If there's no commit, redirect to repo summary
1425 if type(c.commit) is EmptyCommit:
1440 if type(c.commit) is EmptyCommit:
1426 redirect_url = h.route_path(
1441 redirect_url = h.route_path(
1427 'repo_summary', repo_name=self.db_repo_name)
1442 'repo_summary', repo_name=self.db_repo_name)
1428 else:
1443 else:
1429 redirect_url = default_redirect_url
1444 redirect_url = default_redirect_url
1430 h.flash(_('No filename specified'), category='warning')
1445 h.flash(_('No filename specified'), category='warning')
1431 raise HTTPFound(redirect_url)
1446 raise HTTPFound(redirect_url)
1432
1447
1433 root_path = f_path
1448 root_path = f_path
1434 pure_path = self.create_pure_path(root_path, filename)
1449 pure_path = self.create_pure_path(root_path, filename)
1435 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1450 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1436
1451
1437 author = self._rhodecode_db_user.full_contact
1452 author = self._rhodecode_db_user.full_contact
1438 nodes = {
1453 nodes = {
1439 node_path: {
1454 node_path: {
1440 'content': content
1455 'content': content
1441 }
1456 }
1442 }
1457 }
1443
1458
1444 try:
1459 try:
1445
1460
1446 commit = ScmModel().create_nodes(
1461 commit = ScmModel().create_nodes(
1447 user=self._rhodecode_db_user.user_id,
1462 user=self._rhodecode_db_user.user_id,
1448 repo=self.db_repo,
1463 repo=self.db_repo,
1449 message=message,
1464 message=message,
1450 nodes=nodes,
1465 nodes=nodes,
1451 parent_commit=c.commit,
1466 parent_commit=c.commit,
1452 author=author,
1467 author=author,
1453 )
1468 )
1454
1469
1455 h.flash(_('Successfully committed new file `{}`').format(
1470 h.flash(_('Successfully committed new file `{}`').format(
1456 h.escape(node_path)), category='success')
1471 h.escape(node_path)), category='success')
1457
1472
1458 default_redirect_url = h.route_path(
1473 default_redirect_url = h.route_path(
1459 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1474 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1460
1475
1461 except NonRelativePathError:
1476 except NonRelativePathError:
1462 log.exception('Non Relative path found')
1477 log.exception('Non Relative path found')
1463 h.flash(_('The location specified must be a relative path and must not '
1478 h.flash(_('The location specified must be a relative path and must not '
1464 'contain .. in the path'), category='warning')
1479 'contain .. in the path'), category='warning')
1465 raise HTTPFound(default_redirect_url)
1480 raise HTTPFound(default_redirect_url)
1466 except (NodeError, NodeAlreadyExistsError) as e:
1481 except (NodeError, NodeAlreadyExistsError) as e:
1467 h.flash(_(h.escape(e)), category='error')
1482 h.flash(_(h.escape(e)), category='error')
1468 except Exception:
1483 except Exception:
1469 log.exception('Error occurred during commit')
1484 log.exception('Error occurred during commit')
1470 h.flash(_('Error occurred during commit'), category='error')
1485 h.flash(_('Error occurred during commit'), category='error')
1471
1486
1472 raise HTTPFound(default_redirect_url)
1487 raise HTTPFound(default_redirect_url)
1473
1488
1474 @LoginRequired()
1489 @LoginRequired()
1475 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1490 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1476 @CSRFRequired()
1491 @CSRFRequired()
1477 @view_config(
1492 @view_config(
1478 route_name='repo_files_upload_file', request_method='POST',
1493 route_name='repo_files_upload_file', request_method='POST',
1479 renderer='json_ext')
1494 renderer='json_ext')
1480 def repo_files_upload_file(self):
1495 def repo_files_upload_file(self):
1481 _ = self.request.translate
1496 _ = self.request.translate
1482 c = self.load_default_context()
1497 c = self.load_default_context()
1483 commit_id, f_path = self._get_commit_and_path()
1498 commit_id, f_path = self._get_commit_and_path()
1484
1499
1485 self._ensure_not_locked()
1500 self._ensure_not_locked()
1486
1501
1487 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1502 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1488 if c.commit is None:
1503 if c.commit is None:
1489 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1504 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1490
1505
1491 # calculate redirect URL
1506 # calculate redirect URL
1492 if self.rhodecode_vcs_repo.is_empty():
1507 if self.rhodecode_vcs_repo.is_empty():
1493 default_redirect_url = h.route_path(
1508 default_redirect_url = h.route_path(
1494 'repo_summary', repo_name=self.db_repo_name)
1509 'repo_summary', repo_name=self.db_repo_name)
1495 else:
1510 else:
1496 default_redirect_url = h.route_path(
1511 default_redirect_url = h.route_path(
1497 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1512 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1498
1513
1499 if self.rhodecode_vcs_repo.is_empty():
1514 if self.rhodecode_vcs_repo.is_empty():
1500 # for empty repository we cannot check for current branch, we rely on
1515 # for empty repository we cannot check for current branch, we rely on
1501 # c.commit.branch instead
1516 # c.commit.branch instead
1502 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1517 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1503 else:
1518 else:
1504 _branch_name, _sha_commit_id, is_head = \
1519 _branch_name, _sha_commit_id, is_head = \
1505 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1520 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1506
1521
1507 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1522 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1508 if error:
1523 if error:
1509 return {
1524 return {
1510 'error': error,
1525 'error': error,
1511 'redirect_url': default_redirect_url
1526 'redirect_url': default_redirect_url
1512 }
1527 }
1513 error = self.check_branch_permission(_branch_name, json_mode=True)
1528 error = self.check_branch_permission(_branch_name, json_mode=True)
1514 if error:
1529 if error:
1515 return {
1530 return {
1516 'error': error,
1531 'error': error,
1517 'redirect_url': default_redirect_url
1532 'redirect_url': default_redirect_url
1518 }
1533 }
1519
1534
1520 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1535 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1521 c.f_path = f_path
1536 c.f_path = f_path
1522
1537
1523 r_post = self.request.POST
1538 r_post = self.request.POST
1524
1539
1525 message = c.default_message
1540 message = c.default_message
1526 user_message = r_post.getall('message')
1541 user_message = r_post.getall('message')
1527 if isinstance(user_message, list) and user_message:
1542 if isinstance(user_message, list) and user_message:
1528 # we take the first from duplicated results if it's not empty
1543 # we take the first from duplicated results if it's not empty
1529 message = user_message[0] if user_message[0] else message
1544 message = user_message[0] if user_message[0] else message
1530
1545
1531 nodes = {}
1546 nodes = {}
1532
1547
1533 for file_obj in r_post.getall('files_upload') or []:
1548 for file_obj in r_post.getall('files_upload') or []:
1534 content = file_obj.file
1549 content = file_obj.file
1535 filename = file_obj.filename
1550 filename = file_obj.filename
1536
1551
1537 root_path = f_path
1552 root_path = f_path
1538 pure_path = self.create_pure_path(root_path, filename)
1553 pure_path = self.create_pure_path(root_path, filename)
1539 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1554 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1540
1555
1541 nodes[node_path] = {
1556 nodes[node_path] = {
1542 'content': content
1557 'content': content
1543 }
1558 }
1544
1559
1545 if not nodes:
1560 if not nodes:
1546 error = 'missing files'
1561 error = 'missing files'
1547 return {
1562 return {
1548 'error': error,
1563 'error': error,
1549 'redirect_url': default_redirect_url
1564 'redirect_url': default_redirect_url
1550 }
1565 }
1551
1566
1552 author = self._rhodecode_db_user.full_contact
1567 author = self._rhodecode_db_user.full_contact
1553
1568
1554 try:
1569 try:
1555 commit = ScmModel().create_nodes(
1570 commit = ScmModel().create_nodes(
1556 user=self._rhodecode_db_user.user_id,
1571 user=self._rhodecode_db_user.user_id,
1557 repo=self.db_repo,
1572 repo=self.db_repo,
1558 message=message,
1573 message=message,
1559 nodes=nodes,
1574 nodes=nodes,
1560 parent_commit=c.commit,
1575 parent_commit=c.commit,
1561 author=author,
1576 author=author,
1562 )
1577 )
1563 if len(nodes) == 1:
1578 if len(nodes) == 1:
1564 flash_message = _('Successfully committed {} new files').format(len(nodes))
1579 flash_message = _('Successfully committed {} new files').format(len(nodes))
1565 else:
1580 else:
1566 flash_message = _('Successfully committed 1 new file')
1581 flash_message = _('Successfully committed 1 new file')
1567
1582
1568 h.flash(flash_message, category='success')
1583 h.flash(flash_message, category='success')
1569
1584
1570 default_redirect_url = h.route_path(
1585 default_redirect_url = h.route_path(
1571 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1586 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1572
1587
1573 except NonRelativePathError:
1588 except NonRelativePathError:
1574 log.exception('Non Relative path found')
1589 log.exception('Non Relative path found')
1575 error = _('The location specified must be a relative path and must not '
1590 error = _('The location specified must be a relative path and must not '
1576 'contain .. in the path')
1591 'contain .. in the path')
1577 h.flash(error, category='warning')
1592 h.flash(error, category='warning')
1578
1593
1579 return {
1594 return {
1580 'error': error,
1595 'error': error,
1581 'redirect_url': default_redirect_url
1596 'redirect_url': default_redirect_url
1582 }
1597 }
1583 except (NodeError, NodeAlreadyExistsError) as e:
1598 except (NodeError, NodeAlreadyExistsError) as e:
1584 error = h.escape(e)
1599 error = h.escape(e)
1585 h.flash(error, category='error')
1600 h.flash(error, category='error')
1586
1601
1587 return {
1602 return {
1588 'error': error,
1603 'error': error,
1589 'redirect_url': default_redirect_url
1604 'redirect_url': default_redirect_url
1590 }
1605 }
1591 except Exception:
1606 except Exception:
1592 log.exception('Error occurred during commit')
1607 log.exception('Error occurred during commit')
1593 error = _('Error occurred during commit')
1608 error = _('Error occurred during commit')
1594 h.flash(error, category='error')
1609 h.flash(error, category='error')
1595 return {
1610 return {
1596 'error': error,
1611 'error': error,
1597 'redirect_url': default_redirect_url
1612 'redirect_url': default_redirect_url
1598 }
1613 }
1599
1614
1600 return {
1615 return {
1601 'error': None,
1616 'error': None,
1602 'redirect_url': default_redirect_url
1617 'redirect_url': default_redirect_url
1603 }
1618 }
@@ -1,266 +1,266 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 datetime
22 import datetime
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
27 from pyramid.view import view_config
28 from pyramid.renderers import render
28 from pyramid.renderers import render
29 from pyramid.response import Response
29 from pyramid.response import Response
30
30
31 from rhodecode import events
31 from rhodecode import events
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
35 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
35 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
36 import rhodecode.lib.helpers as h
36 import rhodecode.lib.helpers as h
37 from rhodecode.lib.celerylib.utils import get_task_id
37 from rhodecode.lib.celerylib.utils import get_task_id
38 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
38 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
39 from rhodecode.model.permission import PermissionModel
39 from rhodecode.model.permission import PermissionModel
40 from rhodecode.model.repo import RepoModel
40 from rhodecode.model.repo import RepoModel
41 from rhodecode.model.forms import RepoForkForm
41 from rhodecode.model.forms import RepoForkForm
42 from rhodecode.model.scm import ScmModel, RepoGroupList
42 from rhodecode.model.scm import ScmModel, RepoGroupList
43 from rhodecode.lib.utils2 import safe_int, safe_unicode
43 from rhodecode.lib.utils2 import safe_int, safe_unicode
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class RepoForksView(RepoAppView, DataGridAppView):
48 class RepoForksView(RepoAppView, DataGridAppView):
49
49
50 def load_default_context(self):
50 def load_default_context(self):
51 c = self._get_local_tmpl_context(include_app_defaults=True)
51 c = self._get_local_tmpl_context(include_app_defaults=True)
52 c.rhodecode_repo = self.rhodecode_vcs_repo
52 c.rhodecode_repo = self.rhodecode_vcs_repo
53
53
54 acl_groups = RepoGroupList(
54 acl_groups = RepoGroupList(
55 RepoGroup.query().all(),
55 RepoGroup.query().all(),
56 perm_set=['group.write', 'group.admin'])
56 perm_set=['group.write', 'group.admin'])
57 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
57 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
58 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
58 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
59
59
60 c.personal_repo_group = c.rhodecode_user.personal_repo_group
60 c.personal_repo_group = c.rhodecode_user.personal_repo_group
61
61
62 return c
62 return c
63
63
64 @LoginRequired()
64 @LoginRequired()
65 @HasRepoPermissionAnyDecorator(
65 @HasRepoPermissionAnyDecorator(
66 'repository.read', 'repository.write', 'repository.admin')
66 'repository.read', 'repository.write', 'repository.admin')
67 @view_config(
67 @view_config(
68 route_name='repo_forks_show_all', request_method='GET',
68 route_name='repo_forks_show_all', request_method='GET',
69 renderer='rhodecode:templates/forks/forks.mako')
69 renderer='rhodecode:templates/forks/forks.mako')
70 def repo_forks_show_all(self):
70 def repo_forks_show_all(self):
71 c = self.load_default_context()
71 c = self.load_default_context()
72 return self._get_template_context(c)
72 return self._get_template_context(c)
73
73
74 @LoginRequired()
74 @LoginRequired()
75 @HasRepoPermissionAnyDecorator(
75 @HasRepoPermissionAnyDecorator(
76 'repository.read', 'repository.write', 'repository.admin')
76 'repository.read', 'repository.write', 'repository.admin')
77 @view_config(
77 @view_config(
78 route_name='repo_forks_data', request_method='GET',
78 route_name='repo_forks_data', request_method='GET',
79 renderer='json_ext', xhr=True)
79 renderer='json_ext', xhr=True)
80 def repo_forks_data(self):
80 def repo_forks_data(self):
81 _ = self.request.translate
81 _ = self.request.translate
82 self.load_default_context()
82 self.load_default_context()
83 column_map = {
83 column_map = {
84 'fork_name': 'repo_name',
84 'fork_name': 'repo_name',
85 'fork_date': 'created_on',
85 'fork_date': 'created_on',
86 'last_activity': 'updated_on'
86 'last_activity': 'updated_on'
87 }
87 }
88 draw, start, limit = self._extract_chunk(self.request)
88 draw, start, limit = self._extract_chunk(self.request)
89 search_q, order_by, order_dir = self._extract_ordering(
89 search_q, order_by, order_dir = self._extract_ordering(
90 self.request, column_map=column_map)
90 self.request, column_map=column_map)
91
91
92 acl_check = HasRepoPermissionAny(
92 acl_check = HasRepoPermissionAny(
93 'repository.read', 'repository.write', 'repository.admin')
93 'repository.read', 'repository.write', 'repository.admin')
94 repo_id = self.db_repo.repo_id
94 repo_id = self.db_repo.repo_id
95 allowed_ids = [-1]
95 allowed_ids = [-1]
96 for f in Repository.query().filter(Repository.fork_id == repo_id):
96 for f in Repository.query().filter(Repository.fork_id == repo_id):
97 if acl_check(f.repo_name, 'get forks check'):
97 if acl_check(f.repo_name, 'get forks check'):
98 allowed_ids.append(f.repo_id)
98 allowed_ids.append(f.repo_id)
99
99
100 forks_data_total_count = Repository.query()\
100 forks_data_total_count = Repository.query()\
101 .filter(Repository.fork_id == repo_id)\
101 .filter(Repository.fork_id == repo_id)\
102 .filter(Repository.repo_id.in_(allowed_ids))\
102 .filter(Repository.repo_id.in_(allowed_ids))\
103 .count()
103 .count()
104
104
105 # json generate
105 # json generate
106 base_q = Repository.query()\
106 base_q = Repository.query()\
107 .filter(Repository.fork_id == repo_id)\
107 .filter(Repository.fork_id == repo_id)\
108 .filter(Repository.repo_id.in_(allowed_ids))\
108 .filter(Repository.repo_id.in_(allowed_ids))\
109
109
110 if search_q:
110 if search_q:
111 like_expression = u'%{}%'.format(safe_unicode(search_q))
111 like_expression = u'%{}%'.format(safe_unicode(search_q))
112 base_q = base_q.filter(or_(
112 base_q = base_q.filter(or_(
113 Repository.repo_name.ilike(like_expression),
113 Repository.repo_name.ilike(like_expression),
114 Repository.description.ilike(like_expression),
114 Repository.description.ilike(like_expression),
115 ))
115 ))
116
116
117 forks_data_total_filtered_count = base_q.count()
117 forks_data_total_filtered_count = base_q.count()
118
118
119 sort_col = getattr(Repository, order_by, None)
119 sort_col = getattr(Repository, order_by, None)
120 if sort_col:
120 if sort_col:
121 if order_dir == 'asc':
121 if order_dir == 'asc':
122 # handle null values properly to order by NULL last
122 # handle null values properly to order by NULL last
123 if order_by in ['last_activity']:
123 if order_by in ['last_activity']:
124 sort_col = coalesce(sort_col, datetime.date.max)
124 sort_col = coalesce(sort_col, datetime.date.max)
125 sort_col = sort_col.asc()
125 sort_col = sort_col.asc()
126 else:
126 else:
127 # handle null values properly to order by NULL last
127 # handle null values properly to order by NULL last
128 if order_by in ['last_activity']:
128 if order_by in ['last_activity']:
129 sort_col = coalesce(sort_col, datetime.date.min)
129 sort_col = coalesce(sort_col, datetime.date.min)
130 sort_col = sort_col.desc()
130 sort_col = sort_col.desc()
131
131
132 base_q = base_q.order_by(sort_col)
132 base_q = base_q.order_by(sort_col)
133 base_q = base_q.offset(start).limit(limit)
133 base_q = base_q.offset(start).limit(limit)
134
134
135 fork_list = base_q.all()
135 fork_list = base_q.all()
136
136
137 def fork_actions(fork):
137 def fork_actions(fork):
138 url_link = h.route_path(
138 url_link = h.route_path(
139 'repo_compare',
139 'repo_compare',
140 repo_name=fork.repo_name,
140 repo_name=fork.repo_name,
141 source_ref_type=self.db_repo.landing_rev[0],
141 source_ref_type=self.db_repo.landing_ref_type,
142 source_ref=self.db_repo.landing_rev[1],
142 source_ref=self.db_repo.landing_ref_name,
143 target_ref_type=self.db_repo.landing_rev[0],
143 target_ref_type=self.db_repo.landing_ref_type,
144 target_ref=self.db_repo.landing_rev[1],
144 target_ref=self.db_repo.landing_ref_name,
145 _query=dict(merge=1, target_repo=f.repo_name))
145 _query=dict(merge=1, target_repo=f.repo_name))
146 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
146 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
147
147
148 def fork_name(fork):
148 def fork_name(fork):
149 return h.link_to(fork.repo_name,
149 return h.link_to(fork.repo_name,
150 h.route_path('repo_summary', repo_name=fork.repo_name))
150 h.route_path('repo_summary', repo_name=fork.repo_name))
151
151
152 forks_data = []
152 forks_data = []
153 for fork in fork_list:
153 for fork in fork_list:
154 forks_data.append({
154 forks_data.append({
155 "username": h.gravatar_with_user(self.request, fork.user.username),
155 "username": h.gravatar_with_user(self.request, fork.user.username),
156 "fork_name": fork_name(fork),
156 "fork_name": fork_name(fork),
157 "description": fork.description_safe,
157 "description": fork.description_safe,
158 "fork_date": h.age_component(fork.created_on, time_is_local=True),
158 "fork_date": h.age_component(fork.created_on, time_is_local=True),
159 "last_activity": h.format_date(fork.updated_on),
159 "last_activity": h.format_date(fork.updated_on),
160 "action": fork_actions(fork),
160 "action": fork_actions(fork),
161 })
161 })
162
162
163 data = ({
163 data = ({
164 'draw': draw,
164 'draw': draw,
165 'data': forks_data,
165 'data': forks_data,
166 'recordsTotal': forks_data_total_count,
166 'recordsTotal': forks_data_total_count,
167 'recordsFiltered': forks_data_total_filtered_count,
167 'recordsFiltered': forks_data_total_filtered_count,
168 })
168 })
169
169
170 return data
170 return data
171
171
172 @LoginRequired()
172 @LoginRequired()
173 @NotAnonymous()
173 @NotAnonymous()
174 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
174 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
175 @HasRepoPermissionAnyDecorator(
175 @HasRepoPermissionAnyDecorator(
176 'repository.read', 'repository.write', 'repository.admin')
176 'repository.read', 'repository.write', 'repository.admin')
177 @view_config(
177 @view_config(
178 route_name='repo_fork_new', request_method='GET',
178 route_name='repo_fork_new', request_method='GET',
179 renderer='rhodecode:templates/forks/forks.mako')
179 renderer='rhodecode:templates/forks/forks.mako')
180 def repo_fork_new(self):
180 def repo_fork_new(self):
181 c = self.load_default_context()
181 c = self.load_default_context()
182
182
183 defaults = RepoModel()._get_defaults(self.db_repo_name)
183 defaults = RepoModel()._get_defaults(self.db_repo_name)
184 # alter the description to indicate a fork
184 # alter the description to indicate a fork
185 defaults['description'] = (
185 defaults['description'] = (
186 'fork of repository: %s \n%s' % (
186 'fork of repository: %s \n%s' % (
187 defaults['repo_name'], defaults['description']))
187 defaults['repo_name'], defaults['description']))
188 # add suffix to fork
188 # add suffix to fork
189 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
189 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
190
190
191 data = render('rhodecode:templates/forks/fork.mako',
191 data = render('rhodecode:templates/forks/fork.mako',
192 self._get_template_context(c), self.request)
192 self._get_template_context(c), self.request)
193 html = formencode.htmlfill.render(
193 html = formencode.htmlfill.render(
194 data,
194 data,
195 defaults=defaults,
195 defaults=defaults,
196 encoding="UTF-8",
196 encoding="UTF-8",
197 force_defaults=False
197 force_defaults=False
198 )
198 )
199 return Response(html)
199 return Response(html)
200
200
201 @LoginRequired()
201 @LoginRequired()
202 @NotAnonymous()
202 @NotAnonymous()
203 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
203 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
204 @HasRepoPermissionAnyDecorator(
204 @HasRepoPermissionAnyDecorator(
205 'repository.read', 'repository.write', 'repository.admin')
205 'repository.read', 'repository.write', 'repository.admin')
206 @CSRFRequired()
206 @CSRFRequired()
207 @view_config(
207 @view_config(
208 route_name='repo_fork_create', request_method='POST',
208 route_name='repo_fork_create', request_method='POST',
209 renderer='rhodecode:templates/forks/fork.mako')
209 renderer='rhodecode:templates/forks/fork.mako')
210 def repo_fork_create(self):
210 def repo_fork_create(self):
211 _ = self.request.translate
211 _ = self.request.translate
212 c = self.load_default_context()
212 c = self.load_default_context()
213
213
214 _form = RepoForkForm(self.request.translate,
214 _form = RepoForkForm(self.request.translate,
215 old_data={'repo_type': self.db_repo.repo_type},
215 old_data={'repo_type': self.db_repo.repo_type},
216 repo_groups=c.repo_groups_choices)()
216 repo_groups=c.repo_groups_choices)()
217 post_data = dict(self.request.POST)
217 post_data = dict(self.request.POST)
218
218
219 # forbid injecting other repo by forging a request
219 # forbid injecting other repo by forging a request
220 post_data['fork_parent_id'] = self.db_repo.repo_id
220 post_data['fork_parent_id'] = self.db_repo.repo_id
221 post_data['landing_rev'] = self.db_repo._landing_revision
221 post_data['landing_rev'] = self.db_repo._landing_revision
222
222
223 form_result = {}
223 form_result = {}
224 task_id = None
224 task_id = None
225 try:
225 try:
226 form_result = _form.to_python(post_data)
226 form_result = _form.to_python(post_data)
227 copy_permissions = form_result.get('copy_permissions')
227 copy_permissions = form_result.get('copy_permissions')
228 # create fork is done sometimes async on celery, db transaction
228 # create fork is done sometimes async on celery, db transaction
229 # management is handled there.
229 # management is handled there.
230 task = RepoModel().create_fork(
230 task = RepoModel().create_fork(
231 form_result, c.rhodecode_user.user_id)
231 form_result, c.rhodecode_user.user_id)
232
232
233 task_id = get_task_id(task)
233 task_id = get_task_id(task)
234 except formencode.Invalid as errors:
234 except formencode.Invalid as errors:
235 c.rhodecode_db_repo = self.db_repo
235 c.rhodecode_db_repo = self.db_repo
236
236
237 data = render('rhodecode:templates/forks/fork.mako',
237 data = render('rhodecode:templates/forks/fork.mako',
238 self._get_template_context(c), self.request)
238 self._get_template_context(c), self.request)
239 html = formencode.htmlfill.render(
239 html = formencode.htmlfill.render(
240 data,
240 data,
241 defaults=errors.value,
241 defaults=errors.value,
242 errors=errors.error_dict or {},
242 errors=errors.error_dict or {},
243 prefix_error=False,
243 prefix_error=False,
244 encoding="UTF-8",
244 encoding="UTF-8",
245 force_defaults=False
245 force_defaults=False
246 )
246 )
247 return Response(html)
247 return Response(html)
248 except Exception:
248 except Exception:
249 log.exception(
249 log.exception(
250 u'Exception while trying to fork the repository %s', self.db_repo_name)
250 u'Exception while trying to fork the repository %s', self.db_repo_name)
251 msg = _('An error occurred during repository forking %s') % (self.db_repo_name, )
251 msg = _('An error occurred during repository forking %s') % (self.db_repo_name, )
252 h.flash(msg, category='error')
252 h.flash(msg, category='error')
253 raise HTTPFound(h.route_path('home'))
253 raise HTTPFound(h.route_path('home'))
254
254
255 repo_name = form_result.get('repo_name_full', self.db_repo_name)
255 repo_name = form_result.get('repo_name_full', self.db_repo_name)
256
256
257 affected_user_ids = [self._rhodecode_user.user_id]
257 affected_user_ids = [self._rhodecode_user.user_id]
258 if copy_permissions:
258 if copy_permissions:
259 # permission flush is done in repo creating
259 # permission flush is done in repo creating
260 pass
260 pass
261
261
262 PermissionModel().trigger_permission_flush(affected_user_ids)
262 PermissionModel().trigger_permission_flush(affected_user_ids)
263
263
264 raise HTTPFound(
264 raise HTTPFound(
265 h.route_path('repo_creating', repo_name=repo_name,
265 h.route_path('repo_creating', repo_name=repo_name,
266 _query=dict(task_id=task_id)))
266 _query=dict(task_id=task_id)))
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
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
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