##// END OF EJS Templates
release: merge back stable branch into default
milka -
r4642:cced0269 merge default
parent child Browse files
Show More

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

@@ -0,0 +1,46 b''
1 |RCE| 4.24.1 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2021-02-04
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18 - Core: added statsd client for statistics usage.
19 - Clone urls: allow custom clone by id template so users can set clone-by-id as default.
20 - Automation: enable check for new version for EE edition as automation task that will send notifications when new RhodeCode version is available
21
22 Security
23 ^^^^^^^^
24
25
26
27 Performance
28 ^^^^^^^^^^^
29
30 - Core: bumped git to 2.30.0
31
32
33 Fixes
34 ^^^^^
35
36 - Comments: add ability to resolve todos from the side-bar. This should prevent situations
37 when a TODO was left over in outdated/removed code pieces, and users needs to search to resolve them.
38 - Pull requests: fixed a case when template marker was used in description field causing 500 errors on commenting.
39 - Merges: fixed excessive data saved in merge metadata that could not fit inside the DB table.
40 - Exceptions: fixed problem with exceptions formatting resulting in limited exception data reporting.
41
42
43 Upgrade notes
44 ^^^^^^^^^^^^^
45
46 - Un-scheduled release addressing problems in 4.24.X releases.
@@ -0,0 +1,12 b''
1 diff -rup pytest-4.6.5-orig/setup.py pytest-4.6.5/setup.py
2 --- pytest-4.6.5-orig/setup.py 2018-04-10 10:23:04.000000000 +0200
3 +++ pytest-4.6.5/setup.py 2018-04-10 10:23:34.000000000 +0200
4 @@ -24,7 +24,7 @@ INSTALL_REQUIRES = [
5 def main():
6 setup(
7 use_scm_version={"write_to": "src/_pytest/_version.py"},
8 - setup_requires=["setuptools-scm", "setuptools>=40.0"],
9 + setup_requires=["setuptools-scm", "setuptools<=42.0"],
10 package_dir={"": "src"},
11 # fmt: off
12 extras_require={ No newline at end of file
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,75 +1,77 b''
1 1bd3e92b7e2e2d2024152b34bb88dff1db544a71 v4.0.0
1 1bd3e92b7e2e2d2024152b34bb88dff1db544a71 v4.0.0
2 170c5398320ea6cddd50955e88d408794c21d43a v4.0.1
2 170c5398320ea6cddd50955e88d408794c21d43a v4.0.1
3 c3fe200198f5aa34cf2e4066df2881a9cefe3704 v4.1.0
3 c3fe200198f5aa34cf2e4066df2881a9cefe3704 v4.1.0
4 7fd5c850745e2ea821fb4406af5f4bff9b0a7526 v4.1.1
4 7fd5c850745e2ea821fb4406af5f4bff9b0a7526 v4.1.1
5 41c87da28a179953df86061d817bc35533c66dd2 v4.1.2
5 41c87da28a179953df86061d817bc35533c66dd2 v4.1.2
6 baaf9f5bcea3bae0ef12ae20c8b270482e62abb6 v4.2.0
6 baaf9f5bcea3bae0ef12ae20c8b270482e62abb6 v4.2.0
7 32a70c7e56844a825f61df496ee5eaf8c3c4e189 v4.2.1
7 32a70c7e56844a825f61df496ee5eaf8c3c4e189 v4.2.1
8 fa695cdb411d294679ac081d595ac654e5613b03 v4.3.0
8 fa695cdb411d294679ac081d595ac654e5613b03 v4.3.0
9 0e4dc11b58cad833c513fe17bac39e6850edf959 v4.3.1
9 0e4dc11b58cad833c513fe17bac39e6850edf959 v4.3.1
10 8a876f48f5cb1d018b837db28ff928500cb32cfb v4.4.0
10 8a876f48f5cb1d018b837db28ff928500cb32cfb v4.4.0
11 8dd86b410b1aac086ffdfc524ef300f896af5047 v4.4.1
11 8dd86b410b1aac086ffdfc524ef300f896af5047 v4.4.1
12 d2514226abc8d3b4f6fb57765f47d1b6fb360a05 v4.4.2
12 d2514226abc8d3b4f6fb57765f47d1b6fb360a05 v4.4.2
13 27d783325930af6dad2741476c0d0b1b7c8415c2 v4.5.0
13 27d783325930af6dad2741476c0d0b1b7c8415c2 v4.5.0
14 7f2016f352abcbdba4a19d4039c386e9629449da v4.5.1
14 7f2016f352abcbdba4a19d4039c386e9629449da v4.5.1
15 416fec799314c70a5c780fb28b3357b08869333a v4.5.2
15 416fec799314c70a5c780fb28b3357b08869333a v4.5.2
16 27c3b85fafc83143e6678fbc3da69e1615bcac55 v4.6.0
16 27c3b85fafc83143e6678fbc3da69e1615bcac55 v4.6.0
17 5ad13deb9118c2a5243d4032d4d9cc174e5872db v4.6.1
17 5ad13deb9118c2a5243d4032d4d9cc174e5872db v4.6.1
18 2be921e01fa24bb102696ada596f87464c3666f6 v4.7.0
18 2be921e01fa24bb102696ada596f87464c3666f6 v4.7.0
19 7198bdec29c2872c974431d55200d0398354cdb1 v4.7.1
19 7198bdec29c2872c974431d55200d0398354cdb1 v4.7.1
20 bd1c8d230fe741c2dfd7100a0ef39fd0774fd581 v4.7.2
20 bd1c8d230fe741c2dfd7100a0ef39fd0774fd581 v4.7.2
21 9731914f89765d9628dc4dddc84bc9402aa124c8 v4.8.0
21 9731914f89765d9628dc4dddc84bc9402aa124c8 v4.8.0
22 c5a2b7d0e4bbdebc4a62d7b624befe375207b659 v4.9.0
22 c5a2b7d0e4bbdebc4a62d7b624befe375207b659 v4.9.0
23 d9aa3b27ac9f7e78359775c75fedf7bfece232f1 v4.9.1
23 d9aa3b27ac9f7e78359775c75fedf7bfece232f1 v4.9.1
24 4ba4d74981cec5d6b28b158f875a2540952c2f74 v4.10.0
24 4ba4d74981cec5d6b28b158f875a2540952c2f74 v4.10.0
25 0a6821cbd6b0b3c21503002f88800679fa35ab63 v4.10.1
25 0a6821cbd6b0b3c21503002f88800679fa35ab63 v4.10.1
26 434ad90ec8d621f4416074b84f6e9ce03964defb v4.10.2
26 434ad90ec8d621f4416074b84f6e9ce03964defb v4.10.2
27 68baee10e698da2724c6e0f698c03a6abb993bf2 v4.10.3
27 68baee10e698da2724c6e0f698c03a6abb993bf2 v4.10.3
28 00821d3afd1dce3f4767cc353f84a17f7d5218a1 v4.10.4
28 00821d3afd1dce3f4767cc353f84a17f7d5218a1 v4.10.4
29 22f6744ad8cc274311825f63f953e4dee2ea5cb9 v4.10.5
29 22f6744ad8cc274311825f63f953e4dee2ea5cb9 v4.10.5
30 96eb24bea2f5f9258775245e3f09f6fa0a4dda01 v4.10.6
30 96eb24bea2f5f9258775245e3f09f6fa0a4dda01 v4.10.6
31 3121217a812c956d7dd5a5875821bd73e8002a32 v4.11.0
31 3121217a812c956d7dd5a5875821bd73e8002a32 v4.11.0
32 fa98b454715ac5b912f39e84af54345909a2a805 v4.11.1
32 fa98b454715ac5b912f39e84af54345909a2a805 v4.11.1
33 3982abcfdcc229a723cebe52d3a9bcff10bba08e v4.11.2
33 3982abcfdcc229a723cebe52d3a9bcff10bba08e v4.11.2
34 33195f145db9172f0a8f1487e09207178a6ab065 v4.11.3
34 33195f145db9172f0a8f1487e09207178a6ab065 v4.11.3
35 194c74f33e32bbae6fc4d71ec5a999cff3c13605 v4.11.4
35 194c74f33e32bbae6fc4d71ec5a999cff3c13605 v4.11.4
36 8fbd8b0c3ddc2fa4ac9e4ca16942a03eb593df2d v4.11.5
36 8fbd8b0c3ddc2fa4ac9e4ca16942a03eb593df2d v4.11.5
37 f0609aa5d5d05a1ca2f97c3995542236131c9d8a v4.11.6
37 f0609aa5d5d05a1ca2f97c3995542236131c9d8a v4.11.6
38 b5b30547d90d2e088472a70c84878f429ffbf40d v4.12.0
38 b5b30547d90d2e088472a70c84878f429ffbf40d v4.12.0
39 9072253aa8894d20c00b4a43dc61c2168c1eff94 v4.12.1
39 9072253aa8894d20c00b4a43dc61c2168c1eff94 v4.12.1
40 6a517543ea9ef9987d74371bd2a315eb0b232dc9 v4.12.2
40 6a517543ea9ef9987d74371bd2a315eb0b232dc9 v4.12.2
41 7fc0731b024c3114be87865eda7ab621cc957e32 v4.12.3
41 7fc0731b024c3114be87865eda7ab621cc957e32 v4.12.3
42 6d531c0b068c6eda62dddceedc9f845ecb6feb6f v4.12.4
42 6d531c0b068c6eda62dddceedc9f845ecb6feb6f v4.12.4
43 3d6bf2d81b1564830eb5e83396110d2a9a93eb1e v4.13.0
43 3d6bf2d81b1564830eb5e83396110d2a9a93eb1e v4.13.0
44 5468fc89e708bd90e413cd0d54350017abbdbc0e v4.13.1
44 5468fc89e708bd90e413cd0d54350017abbdbc0e v4.13.1
45 610d621550521c314ee97b3d43473ac0bcf06fb8 v4.13.2
45 610d621550521c314ee97b3d43473ac0bcf06fb8 v4.13.2
46 7dc62c090881fb5d03268141e71e0940d7c3295d v4.13.3
46 7dc62c090881fb5d03268141e71e0940d7c3295d v4.13.3
47 9151328c1c46b72ba6f00d7640d9141e75aa1ca2 v4.14.0
47 9151328c1c46b72ba6f00d7640d9141e75aa1ca2 v4.14.0
48 a47eeac5dfa41fa6779d90452affba4091c3ade8 v4.14.1
48 a47eeac5dfa41fa6779d90452affba4091c3ade8 v4.14.1
49 4b34ce0d2c3c10510626b3b65044939bb7a2cddf v4.15.0
49 4b34ce0d2c3c10510626b3b65044939bb7a2cddf v4.15.0
50 14502561d22e6b70613674cd675ae9a604b7989f v4.15.1
50 14502561d22e6b70613674cd675ae9a604b7989f v4.15.1
51 4aaa40b605b01af78a9f6882eca561c54b525ef0 v4.15.2
51 4aaa40b605b01af78a9f6882eca561c54b525ef0 v4.15.2
52 797744642eca86640ed20bef2cd77445780abaec v4.16.0
52 797744642eca86640ed20bef2cd77445780abaec v4.16.0
53 6c3452c7c25ed35ff269690929e11960ed6ad7d3 v4.16.1
53 6c3452c7c25ed35ff269690929e11960ed6ad7d3 v4.16.1
54 5d8057df561c4b6b81b6401aed7d2f911e6e77f7 v4.16.2
54 5d8057df561c4b6b81b6401aed7d2f911e6e77f7 v4.16.2
55 13acfc008896ef4c62546bab5074e8f6f89b4fa7 v4.17.0
55 13acfc008896ef4c62546bab5074e8f6f89b4fa7 v4.17.0
56 45b9b610976f483877142fe75321808ce9ebac59 v4.17.1
56 45b9b610976f483877142fe75321808ce9ebac59 v4.17.1
57 ad5bd0c4bd322fdbd04bb825a3d027e08f7a3901 v4.17.2
57 ad5bd0c4bd322fdbd04bb825a3d027e08f7a3901 v4.17.2
58 037f5794b55a6236d68f6485a485372dde6566e0 v4.17.3
58 037f5794b55a6236d68f6485a485372dde6566e0 v4.17.3
59 83bc3100cfd6094c1d04f475ddb299b7dc3d0b33 v4.17.4
59 83bc3100cfd6094c1d04f475ddb299b7dc3d0b33 v4.17.4
60 e3de8c95baf8cc9109ca56aee8193a2cb6a54c8a v4.17.4
60 e3de8c95baf8cc9109ca56aee8193a2cb6a54c8a v4.17.4
61 f37a3126570477543507f0bc9d245ce75546181a v4.18.0
61 f37a3126570477543507f0bc9d245ce75546181a v4.18.0
62 71d8791463e87b64c1a18475de330ee600d37561 v4.18.1
62 71d8791463e87b64c1a18475de330ee600d37561 v4.18.1
63 4bd6b75dac1d25c64885d4d49385e5533f21c525 v4.18.2
63 4bd6b75dac1d25c64885d4d49385e5533f21c525 v4.18.2
64 12ed92fe57f2e9fc7b71dc0b65e26c2da5c7085f v4.18.3
64 12ed92fe57f2e9fc7b71dc0b65e26c2da5c7085f v4.18.3
65 ddef396a6567117de531d67d44c739cbbfc3eebb v4.19.0
65 ddef396a6567117de531d67d44c739cbbfc3eebb v4.19.0
66 c0c65acd73914bf4368222d510afe1161ab8c07c v4.19.1
66 c0c65acd73914bf4368222d510afe1161ab8c07c v4.19.1
67 7ac623a4a2405917e2af660d645ded662011e40d v4.19.2
67 7ac623a4a2405917e2af660d645ded662011e40d v4.19.2
68 ef7ffda65eeb90c3ba88590a6cb816ef9b0bc232 v4.19.3
68 ef7ffda65eeb90c3ba88590a6cb816ef9b0bc232 v4.19.3
69 3e635489bb7961df93b01e42454ad1a8730ae968 v4.20.0
69 3e635489bb7961df93b01e42454ad1a8730ae968 v4.20.0
70 7e2eb896a02ca7cd2cd9f0f853ef3dac3f0039e3 v4.20.1
70 7e2eb896a02ca7cd2cd9f0f853ef3dac3f0039e3 v4.20.1
71 8bb5fece08ab65986225b184e46f53d2a71729cb v4.21.0
71 8bb5fece08ab65986225b184e46f53d2a71729cb v4.21.0
72 90734aac31ee4563bbe665a43ff73190cc762275 v4.22.0
72 90734aac31ee4563bbe665a43ff73190cc762275 v4.22.0
73 a9655707f7cf4146affc51c12fe5ed8e02898a57 v4.23.0
73 a9655707f7cf4146affc51c12fe5ed8e02898a57 v4.23.0
74 56310d93b33b97535908ef9c7b0985b89bb7fad2 v4.23.1
74 56310d93b33b97535908ef9c7b0985b89bb7fad2 v4.23.1
75 7637c38528fa38c1eabc1fde6a869c20995a0da7 v4.23.2
75 7637c38528fa38c1eabc1fde6a869c20995a0da7 v4.23.2
76 6aeb4ac3ef7f0ac699c914740dad3688c9495e83 v4.24.0
77 6eaf953da06e468a4c4e5239d3d0e700bda6b163 v4.24.1
@@ -1,54 +1,56 b''
1 |RCE| 4.23.0 |RNS|
1 |RCE| 4.24.0 |RNS|
2 ------------------
2 ------------------
3
3
4 Release Date
4 Release Date
5 ^^^^^^^^^^^^
5 ^^^^^^^^^^^^
6
6
7 - 2021-01-10
7 - 2021-01-10
8
8
9
9
10 New Features
10 New Features
11 ^^^^^^^^^^^^
11 ^^^^^^^^^^^^
12
12
13 - Artifacts: expose additional headers, and content-disposition for downloads from artifacts exposing the real name of the file.
13 - Artifacts: expose additional headers, and content-disposition for downloads from artifacts exposing the real name of the file.
14 - Token access: allow token in headers not only in GET/URL.
14 - Token access: allow token in headers not only in GET/URL.
15 - File-store: added a stream upload endpoint, it allows to upload GBs of data into artifact store efficiently.
15 - File-store: added a stream upload endpoint, it allows to upload GBs of data into artifact store efficiently.
16 Can be used for backups etc.
16 Can be used for backups etc.
17 - Pull requests: expose commit versions in the pull-request commit list.
17 - Pull requests: expose commit versions in the pull-request commit list.
18
18
19
19 General
20 General
20 ^^^^^^^
21 ^^^^^^^
21
22
22 - Deps: bumped redis to 3.5.3
23 - Deps: bumped redis to 3.5.3
23 - rcextensions: improve examples
24 - Rcextensions: improve examples for some usage.
24 - Setup: added optional parameters to apply a default license, or skip re-creation of database at install.
25 - Setup: added optional parameters to apply a default license, or skip re-creation of database at install.
25 - Docs: update headers for NGINX
26 - Docs: update headers for NGINX
26 - Beaker cache: remove no longer used beaker cache init
27 - Beaker cache: remove no longer used beaker cache init
28 - Installation: the installer no longer requires gzip and bzip packages, and works on python 2 and 3
27
29
28
30
29 Security
31 Security
30 ^^^^^^^^
32 ^^^^^^^^
31
33
32
34
33
35
34 Performance
36 Performance
35 ^^^^^^^^^^^
37 ^^^^^^^^^^^
36
38
37 - Core: speed up cache loading on application startup.
39 - Core: speed up cache loading on application startup.
38 - Core: allow loading all auth plugins in once place for CE/EE code.
40 - Core: allow loading all auth plugins in once place for CE/EE code.
39 - Application: not use config.scan(), and replace all @add_view decorator into a explicit add_view call for faster app start.
41 - Application: not use config.scan(), and replace all @add_view decorator into a explicit add_view call for faster app start.
40
42
41
43
42 Fixes
44 Fixes
43 ^^^^^
45 ^^^^^
44
46
45 - Svn: don't print exceptions in case of safe calls
47 - Svn: don't print exceptions in case of safe calls
46 - Vcsserver: use safer maxfd reporting, some linux systems get a problem with this
48 - Vcsserver: use safer maxfd reporting, some linux systems get a problem with this
47 - Hooks-daemon: fixed problem with lost hooks value from .ini file.
49 - Hooks-daemon: fixed problem with lost hooks value from .ini file.
48 - Exceptions: fixed truncated exception text
50 - Exceptions: fixed truncated exception text
49
51
50
52
51 Upgrade notes
53 Upgrade notes
52 ^^^^^^^^^^^^^
54 ^^^^^^^^^^^^^
53
55
54 - Scheduled release 4.24.0
56 - Scheduled release 4.24.0
@@ -1,152 +1,153 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.24.1.rst
12 release-notes-4.24.0.rst
13 release-notes-4.24.0.rst
13 release-notes-4.23.2.rst
14 release-notes-4.23.2.rst
14 release-notes-4.23.1.rst
15 release-notes-4.23.1.rst
15 release-notes-4.23.0.rst
16 release-notes-4.23.0.rst
16 release-notes-4.22.0.rst
17 release-notes-4.22.0.rst
17 release-notes-4.21.0.rst
18 release-notes-4.21.0.rst
18 release-notes-4.20.1.rst
19 release-notes-4.20.1.rst
19 release-notes-4.20.0.rst
20 release-notes-4.20.0.rst
20 release-notes-4.19.3.rst
21 release-notes-4.19.3.rst
21 release-notes-4.19.2.rst
22 release-notes-4.19.2.rst
22 release-notes-4.19.1.rst
23 release-notes-4.19.1.rst
23 release-notes-4.19.0.rst
24 release-notes-4.19.0.rst
24 release-notes-4.18.3.rst
25 release-notes-4.18.3.rst
25 release-notes-4.18.2.rst
26 release-notes-4.18.2.rst
26 release-notes-4.18.1.rst
27 release-notes-4.18.1.rst
27 release-notes-4.18.0.rst
28 release-notes-4.18.0.rst
28 release-notes-4.17.4.rst
29 release-notes-4.17.4.rst
29 release-notes-4.17.3.rst
30 release-notes-4.17.3.rst
30 release-notes-4.17.2.rst
31 release-notes-4.17.2.rst
31 release-notes-4.17.1.rst
32 release-notes-4.17.1.rst
32 release-notes-4.17.0.rst
33 release-notes-4.17.0.rst
33 release-notes-4.16.2.rst
34 release-notes-4.16.2.rst
34 release-notes-4.16.1.rst
35 release-notes-4.16.1.rst
35 release-notes-4.16.0.rst
36 release-notes-4.16.0.rst
36 release-notes-4.15.2.rst
37 release-notes-4.15.2.rst
37 release-notes-4.15.1.rst
38 release-notes-4.15.1.rst
38 release-notes-4.15.0.rst
39 release-notes-4.15.0.rst
39 release-notes-4.14.1.rst
40 release-notes-4.14.1.rst
40 release-notes-4.14.0.rst
41 release-notes-4.14.0.rst
41 release-notes-4.13.3.rst
42 release-notes-4.13.3.rst
42 release-notes-4.13.2.rst
43 release-notes-4.13.2.rst
43 release-notes-4.13.1.rst
44 release-notes-4.13.1.rst
44 release-notes-4.13.0.rst
45 release-notes-4.13.0.rst
45 release-notes-4.12.4.rst
46 release-notes-4.12.4.rst
46 release-notes-4.12.3.rst
47 release-notes-4.12.3.rst
47 release-notes-4.12.2.rst
48 release-notes-4.12.2.rst
48 release-notes-4.12.1.rst
49 release-notes-4.12.1.rst
49 release-notes-4.12.0.rst
50 release-notes-4.12.0.rst
50 release-notes-4.11.6.rst
51 release-notes-4.11.6.rst
51 release-notes-4.11.5.rst
52 release-notes-4.11.5.rst
52 release-notes-4.11.4.rst
53 release-notes-4.11.4.rst
53 release-notes-4.11.3.rst
54 release-notes-4.11.3.rst
54 release-notes-4.11.2.rst
55 release-notes-4.11.2.rst
55 release-notes-4.11.1.rst
56 release-notes-4.11.1.rst
56 release-notes-4.11.0.rst
57 release-notes-4.11.0.rst
57 release-notes-4.10.6.rst
58 release-notes-4.10.6.rst
58 release-notes-4.10.5.rst
59 release-notes-4.10.5.rst
59 release-notes-4.10.4.rst
60 release-notes-4.10.4.rst
60 release-notes-4.10.3.rst
61 release-notes-4.10.3.rst
61 release-notes-4.10.2.rst
62 release-notes-4.10.2.rst
62 release-notes-4.10.1.rst
63 release-notes-4.10.1.rst
63 release-notes-4.10.0.rst
64 release-notes-4.10.0.rst
64 release-notes-4.9.1.rst
65 release-notes-4.9.1.rst
65 release-notes-4.9.0.rst
66 release-notes-4.9.0.rst
66 release-notes-4.8.0.rst
67 release-notes-4.8.0.rst
67 release-notes-4.7.2.rst
68 release-notes-4.7.2.rst
68 release-notes-4.7.1.rst
69 release-notes-4.7.1.rst
69 release-notes-4.7.0.rst
70 release-notes-4.7.0.rst
70 release-notes-4.6.1.rst
71 release-notes-4.6.1.rst
71 release-notes-4.6.0.rst
72 release-notes-4.6.0.rst
72 release-notes-4.5.2.rst
73 release-notes-4.5.2.rst
73 release-notes-4.5.1.rst
74 release-notes-4.5.1.rst
74 release-notes-4.5.0.rst
75 release-notes-4.5.0.rst
75 release-notes-4.4.2.rst
76 release-notes-4.4.2.rst
76 release-notes-4.4.1.rst
77 release-notes-4.4.1.rst
77 release-notes-4.4.0.rst
78 release-notes-4.4.0.rst
78 release-notes-4.3.1.rst
79 release-notes-4.3.1.rst
79 release-notes-4.3.0.rst
80 release-notes-4.3.0.rst
80 release-notes-4.2.1.rst
81 release-notes-4.2.1.rst
81 release-notes-4.2.0.rst
82 release-notes-4.2.0.rst
82 release-notes-4.1.2.rst
83 release-notes-4.1.2.rst
83 release-notes-4.1.1.rst
84 release-notes-4.1.1.rst
84 release-notes-4.1.0.rst
85 release-notes-4.1.0.rst
85 release-notes-4.0.1.rst
86 release-notes-4.0.1.rst
86 release-notes-4.0.0.rst
87 release-notes-4.0.0.rst
87
88
88 |RCE| 3.x Versions
89 |RCE| 3.x Versions
89 ------------------
90 ------------------
90
91
91 .. toctree::
92 .. toctree::
92 :maxdepth: 1
93 :maxdepth: 1
93
94
94 release-notes-3.8.4.rst
95 release-notes-3.8.4.rst
95 release-notes-3.8.3.rst
96 release-notes-3.8.3.rst
96 release-notes-3.8.2.rst
97 release-notes-3.8.2.rst
97 release-notes-3.8.1.rst
98 release-notes-3.8.1.rst
98 release-notes-3.8.0.rst
99 release-notes-3.8.0.rst
99 release-notes-3.7.1.rst
100 release-notes-3.7.1.rst
100 release-notes-3.7.0.rst
101 release-notes-3.7.0.rst
101 release-notes-3.6.1.rst
102 release-notes-3.6.1.rst
102 release-notes-3.6.0.rst
103 release-notes-3.6.0.rst
103 release-notes-3.5.2.rst
104 release-notes-3.5.2.rst
104 release-notes-3.5.1.rst
105 release-notes-3.5.1.rst
105 release-notes-3.5.0.rst
106 release-notes-3.5.0.rst
106 release-notes-3.4.1.rst
107 release-notes-3.4.1.rst
107 release-notes-3.4.0.rst
108 release-notes-3.4.0.rst
108 release-notes-3.3.4.rst
109 release-notes-3.3.4.rst
109 release-notes-3.3.3.rst
110 release-notes-3.3.3.rst
110 release-notes-3.3.2.rst
111 release-notes-3.3.2.rst
111 release-notes-3.3.1.rst
112 release-notes-3.3.1.rst
112 release-notes-3.3.0.rst
113 release-notes-3.3.0.rst
113 release-notes-3.2.3.rst
114 release-notes-3.2.3.rst
114 release-notes-3.2.2.rst
115 release-notes-3.2.2.rst
115 release-notes-3.2.1.rst
116 release-notes-3.2.1.rst
116 release-notes-3.2.0.rst
117 release-notes-3.2.0.rst
117 release-notes-3.1.1.rst
118 release-notes-3.1.1.rst
118 release-notes-3.1.0.rst
119 release-notes-3.1.0.rst
119 release-notes-3.0.2.rst
120 release-notes-3.0.2.rst
120 release-notes-3.0.1.rst
121 release-notes-3.0.1.rst
121 release-notes-3.0.0.rst
122 release-notes-3.0.0.rst
122
123
123 |RCE| 2.x Versions
124 |RCE| 2.x Versions
124 ------------------
125 ------------------
125
126
126 .. toctree::
127 .. toctree::
127 :maxdepth: 1
128 :maxdepth: 1
128
129
129 release-notes-2.2.8.rst
130 release-notes-2.2.8.rst
130 release-notes-2.2.7.rst
131 release-notes-2.2.7.rst
131 release-notes-2.2.6.rst
132 release-notes-2.2.6.rst
132 release-notes-2.2.5.rst
133 release-notes-2.2.5.rst
133 release-notes-2.2.4.rst
134 release-notes-2.2.4.rst
134 release-notes-2.2.3.rst
135 release-notes-2.2.3.rst
135 release-notes-2.2.2.rst
136 release-notes-2.2.2.rst
136 release-notes-2.2.1.rst
137 release-notes-2.2.1.rst
137 release-notes-2.2.0.rst
138 release-notes-2.2.0.rst
138 release-notes-2.1.0.rst
139 release-notes-2.1.0.rst
139 release-notes-2.0.2.rst
140 release-notes-2.0.2.rst
140 release-notes-2.0.1.rst
141 release-notes-2.0.1.rst
141 release-notes-2.0.0.rst
142 release-notes-2.0.0.rst
142
143
143 |RCE| 1.x Versions
144 |RCE| 1.x Versions
144 ------------------
145 ------------------
145
146
146 .. toctree::
147 .. toctree::
147 :maxdepth: 1
148 :maxdepth: 1
148
149
149 release-notes-1.7.2.rst
150 release-notes-1.7.2.rst
150 release-notes-1.7.1.rst
151 release-notes-1.7.1.rst
151 release-notes-1.7.0.rst
152 release-notes-1.7.0.rst
152 release-notes-1.6.0.rst
153 release-notes-1.6.0.rst
@@ -1,281 +1,287 b''
1 # Overrides for the generated python-packages.nix
1 # Overrides for the generated python-packages.nix
2 #
2 #
3 # This function is intended to be used as an extension to the generated file
3 # This function is intended to be used as an extension to the generated file
4 # python-packages.nix. The main objective is to add needed dependencies of C
4 # python-packages.nix. The main objective is to add needed dependencies of C
5 # libraries and tweak the build instructions where needed.
5 # libraries and tweak the build instructions where needed.
6
6
7 { pkgs
7 { pkgs
8 , basePythonPackages
8 , basePythonPackages
9 }:
9 }:
10
10
11 let
11 let
12 sed = "sed -i";
12 sed = "sed -i";
13
13
14 localLicenses = {
14 localLicenses = {
15 repoze = {
15 repoze = {
16 fullName = "Repoze License";
16 fullName = "Repoze License";
17 url = http://www.repoze.org/LICENSE.txt;
17 url = http://www.repoze.org/LICENSE.txt;
18 };
18 };
19 };
19 };
20
20
21 in
21 in
22
22
23 self: super: {
23 self: super: {
24
24
25 "appenlight-client" = super."appenlight-client".override (attrs: {
25 "appenlight-client" = super."appenlight-client".override (attrs: {
26 meta = {
26 meta = {
27 license = [ pkgs.lib.licenses.bsdOriginal ];
27 license = [ pkgs.lib.licenses.bsdOriginal ];
28 };
28 };
29 });
29 });
30
30
31 "beaker" = super."beaker".override (attrs: {
31 "beaker" = super."beaker".override (attrs: {
32 patches = [
32 patches = [
33 ./patches/beaker/patch-beaker-lock-func-debug.diff
33 ./patches/beaker/patch-beaker-lock-func-debug.diff
34 ./patches/beaker/patch-beaker-metadata-reuse.diff
34 ./patches/beaker/patch-beaker-metadata-reuse.diff
35 ./patches/beaker/patch-beaker-improved-redis.diff
35 ./patches/beaker/patch-beaker-improved-redis.diff
36 ./patches/beaker/patch-beaker-improved-redis-2.diff
36 ./patches/beaker/patch-beaker-improved-redis-2.diff
37 ];
37 ];
38 });
38 });
39
39
40 "cffi" = super."cffi".override (attrs: {
40 "cffi" = super."cffi".override (attrs: {
41 buildInputs = [
41 buildInputs = [
42 pkgs.libffi
42 pkgs.libffi
43 ];
43 ];
44 });
44 });
45
45
46 "cryptography" = super."cryptography".override (attrs: {
46 "cryptography" = super."cryptography".override (attrs: {
47 buildInputs = [
47 buildInputs = [
48 pkgs.openssl
48 pkgs.openssl
49 ];
49 ];
50 });
50 });
51
51
52 "gevent" = super."gevent".override (attrs: {
52 "gevent" = super."gevent".override (attrs: {
53 propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
53 propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
54 # NOTE: (marcink) odd requirements from gevent aren't set properly,
54 # NOTE: (marcink) odd requirements from gevent aren't set properly,
55 # thus we need to inject psutil manually
55 # thus we need to inject psutil manually
56 self."psutil"
56 self."psutil"
57 ];
57 ];
58 });
58 });
59
59
60 "future" = super."future".override (attrs: {
60 "future" = super."future".override (attrs: {
61 meta = {
61 meta = {
62 license = [ pkgs.lib.licenses.mit ];
62 license = [ pkgs.lib.licenses.mit ];
63 };
63 };
64 });
64 });
65
65
66 "testpath" = super."testpath".override (attrs: {
66 "testpath" = super."testpath".override (attrs: {
67 meta = {
67 meta = {
68 license = [ pkgs.lib.licenses.mit ];
68 license = [ pkgs.lib.licenses.mit ];
69 };
69 };
70 });
70 });
71
71
72 "gnureadline" = super."gnureadline".override (attrs: {
72 "gnureadline" = super."gnureadline".override (attrs: {
73 buildInputs = [
73 buildInputs = [
74 pkgs.ncurses
74 pkgs.ncurses
75 ];
75 ];
76 patchPhase = ''
76 patchPhase = ''
77 substituteInPlace setup.py --replace "/bin/bash" "${pkgs.bash}/bin/bash"
77 substituteInPlace setup.py --replace "/bin/bash" "${pkgs.bash}/bin/bash"
78 '';
78 '';
79 });
79 });
80
80
81 "gunicorn" = super."gunicorn".override (attrs: {
81 "gunicorn" = super."gunicorn".override (attrs: {
82 propagatedBuildInputs = [
82 propagatedBuildInputs = [
83 # johbo: futures is needed as long as we are on Python 2, otherwise
83 # johbo: futures is needed as long as we are on Python 2, otherwise
84 # gunicorn explodes if used with multiple threads per worker.
84 # gunicorn explodes if used with multiple threads per worker.
85 self."futures"
85 self."futures"
86 ];
86 ];
87 });
87 });
88
88
89 "nbconvert" = super."nbconvert".override (attrs: {
89 "nbconvert" = super."nbconvert".override (attrs: {
90 propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
90 propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
91 # marcink: plug in jupyter-client for notebook rendering
91 # marcink: plug in jupyter-client for notebook rendering
92 self."jupyter-client"
92 self."jupyter-client"
93 ];
93 ];
94 });
94 });
95
95
96 "ipython" = super."ipython".override (attrs: {
96 "ipython" = super."ipython".override (attrs: {
97 propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
97 propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
98 self."gnureadline"
98 self."gnureadline"
99 ];
99 ];
100 });
100 });
101
101
102 "lxml" = super."lxml".override (attrs: {
102 "lxml" = super."lxml".override (attrs: {
103 buildInputs = [
103 buildInputs = [
104 pkgs.libxml2
104 pkgs.libxml2
105 pkgs.libxslt
105 pkgs.libxslt
106 ];
106 ];
107 propagatedBuildInputs = [
107 propagatedBuildInputs = [
108 # Needed, so that "setup.py bdist_wheel" does work
108 # Needed, so that "setup.py bdist_wheel" does work
109 self."wheel"
109 self."wheel"
110 ];
110 ];
111 });
111 });
112
112
113 "mysql-python" = super."mysql-python".override (attrs: {
113 "mysql-python" = super."mysql-python".override (attrs: {
114 buildInputs = [
114 buildInputs = [
115 pkgs.openssl
115 pkgs.openssl
116 ];
116 ];
117 propagatedBuildInputs = [
117 propagatedBuildInputs = [
118 pkgs.libmysql
118 pkgs.libmysql
119 pkgs.zlib
119 pkgs.zlib
120 ];
120 ];
121 });
121 });
122
122
123 "psycopg2" = super."psycopg2".override (attrs: {
123 "psycopg2" = super."psycopg2".override (attrs: {
124 propagatedBuildInputs = [
124 propagatedBuildInputs = [
125 pkgs.postgresql
125 pkgs.postgresql
126 ];
126 ];
127 meta = {
127 meta = {
128 license = pkgs.lib.licenses.lgpl3Plus;
128 license = pkgs.lib.licenses.lgpl3Plus;
129 };
129 };
130 });
130 });
131
131
132 "pycurl" = super."pycurl".override (attrs: {
132 "pycurl" = super."pycurl".override (attrs: {
133 propagatedBuildInputs = [
133 propagatedBuildInputs = [
134 pkgs.curl
134 pkgs.curl
135 pkgs.openssl
135 pkgs.openssl
136 ];
136 ];
137
137
138 preConfigure = ''
138 preConfigure = ''
139 substituteInPlace setup.py --replace '--static-libs' '--libs'
139 substituteInPlace setup.py --replace '--static-libs' '--libs'
140 export PYCURL_SSL_LIBRARY=openssl
140 export PYCURL_SSL_LIBRARY=openssl
141 '';
141 '';
142
142
143 meta = {
143 meta = {
144 license = pkgs.lib.licenses.mit;
144 license = pkgs.lib.licenses.mit;
145 };
145 };
146 });
146 });
147
147
148 "pyramid" = super."pyramid".override (attrs: {
148 "pyramid" = super."pyramid".override (attrs: {
149 meta = {
149 meta = {
150 license = localLicenses.repoze;
150 license = localLicenses.repoze;
151 };
151 };
152 });
152 });
153
153
154 "pyramid-debugtoolbar" = super."pyramid-debugtoolbar".override (attrs: {
154 "pyramid-debugtoolbar" = super."pyramid-debugtoolbar".override (attrs: {
155 meta = {
155 meta = {
156 license = [ pkgs.lib.licenses.bsdOriginal localLicenses.repoze ];
156 license = [ pkgs.lib.licenses.bsdOriginal localLicenses.repoze ];
157 };
157 };
158 });
158 });
159
159
160 "pysqlite" = super."pysqlite".override (attrs: {
160 "pysqlite" = super."pysqlite".override (attrs: {
161 propagatedBuildInputs = [
161 propagatedBuildInputs = [
162 pkgs.sqlite
162 pkgs.sqlite
163 ];
163 ];
164 meta = {
164 meta = {
165 license = [ pkgs.lib.licenses.zlib pkgs.lib.licenses.libpng ];
165 license = [ pkgs.lib.licenses.zlib pkgs.lib.licenses.libpng ];
166 };
166 };
167 });
167 });
168
168
169 "python-ldap" = super."python-ldap".override (attrs: {
169 "python-ldap" = super."python-ldap".override (attrs: {
170 propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
170 propagatedBuildInputs = attrs.propagatedBuildInputs ++ [
171 pkgs.openldap
171 pkgs.openldap
172 pkgs.cyrus_sasl
172 pkgs.cyrus_sasl
173 pkgs.openssl
173 pkgs.openssl
174 ];
174 ];
175 });
175 });
176
176
177 "python-pam" = super."python-pam".override (attrs: {
177 "python-pam" = super."python-pam".override (attrs: {
178 propagatedBuildInputs = [
178 propagatedBuildInputs = [
179 pkgs.pam
179 pkgs.pam
180 ];
180 ];
181
181
182 # TODO: johbo: Check if this can be avoided, or transform into
182 # TODO: johbo: Check if this can be avoided, or transform into
183 # a real patch
183 # a real patch
184 patchPhase = ''
184 patchPhase = ''
185 substituteInPlace pam.py \
185 substituteInPlace pam.py \
186 --replace 'find_library("pam")' '"${pkgs.pam}/lib/libpam.so.0"'
186 --replace 'find_library("pam")' '"${pkgs.pam}/lib/libpam.so.0"'
187 '';
187 '';
188
188
189 });
189 });
190
190
191 "python-saml" = super."python-saml".override (attrs: {
191 "python-saml" = super."python-saml".override (attrs: {
192 buildInputs = [
192 buildInputs = [
193 pkgs.libxml2
193 pkgs.libxml2
194 pkgs.libxslt
194 pkgs.libxslt
195 ];
195 ];
196 });
196 });
197
197
198 "dm.xmlsec.binding" = super."dm.xmlsec.binding".override (attrs: {
198 "dm.xmlsec.binding" = super."dm.xmlsec.binding".override (attrs: {
199 buildInputs = [
199 buildInputs = [
200 pkgs.libxml2
200 pkgs.libxml2
201 pkgs.libxslt
201 pkgs.libxslt
202 pkgs.xmlsec
202 pkgs.xmlsec
203 pkgs.libtool
203 pkgs.libtool
204 ];
204 ];
205 });
205 });
206
206
207 "pyzmq" = super."pyzmq".override (attrs: {
207 "pyzmq" = super."pyzmq".override (attrs: {
208 buildInputs = [
208 buildInputs = [
209 pkgs.czmq
209 pkgs.czmq
210 ];
210 ];
211 });
211 });
212
212
213 "urlobject" = super."urlobject".override (attrs: {
213 "urlobject" = super."urlobject".override (attrs: {
214 meta = {
214 meta = {
215 license = {
215 license = {
216 spdxId = "Unlicense";
216 spdxId = "Unlicense";
217 fullName = "The Unlicense";
217 fullName = "The Unlicense";
218 url = http://unlicense.org/;
218 url = http://unlicense.org/;
219 };
219 };
220 };
220 };
221 });
221 });
222
222
223 "docutils" = super."docutils".override (attrs: {
223 "docutils" = super."docutils".override (attrs: {
224 meta = {
224 meta = {
225 license = pkgs.lib.licenses.bsd2;
225 license = pkgs.lib.licenses.bsd2;
226 };
226 };
227 });
227 });
228
228
229 "colander" = super."colander".override (attrs: {
229 "colander" = super."colander".override (attrs: {
230 meta = {
230 meta = {
231 license = localLicenses.repoze;
231 license = localLicenses.repoze;
232 };
232 };
233 });
233 });
234
234
235 "pyramid-beaker" = super."pyramid-beaker".override (attrs: {
235 "pyramid-beaker" = super."pyramid-beaker".override (attrs: {
236 meta = {
236 meta = {
237 license = localLicenses.repoze;
237 license = localLicenses.repoze;
238 };
238 };
239 });
239 });
240
240
241 "pyramid-mako" = super."pyramid-mako".override (attrs: {
241 "pyramid-mako" = super."pyramid-mako".override (attrs: {
242 meta = {
242 meta = {
243 license = localLicenses.repoze;
243 license = localLicenses.repoze;
244 };
244 };
245 });
245 });
246
246
247 "repoze.lru" = super."repoze.lru".override (attrs: {
247 "repoze.lru" = super."repoze.lru".override (attrs: {
248 meta = {
248 meta = {
249 license = localLicenses.repoze;
249 license = localLicenses.repoze;
250 };
250 };
251 });
251 });
252
252
253 "python-editor" = super."python-editor".override (attrs: {
253 "python-editor" = super."python-editor".override (attrs: {
254 meta = {
254 meta = {
255 license = pkgs.lib.licenses.asl20;
255 license = pkgs.lib.licenses.asl20;
256 };
256 };
257 });
257 });
258
258
259 "translationstring" = super."translationstring".override (attrs: {
259 "translationstring" = super."translationstring".override (attrs: {
260 meta = {
260 meta = {
261 license = localLicenses.repoze;
261 license = localLicenses.repoze;
262 };
262 };
263 });
263 });
264
264
265 "venusian" = super."venusian".override (attrs: {
265 "venusian" = super."venusian".override (attrs: {
266 meta = {
266 meta = {
267 license = localLicenses.repoze;
267 license = localLicenses.repoze;
268 };
268 };
269 });
269 });
270
270
271 "supervisor" = super."supervisor".override (attrs: {
271 "supervisor" = super."supervisor".override (attrs: {
272 patches = [
272 patches = [
273 ./patches/supervisor/patch-rlimits-old-kernel.diff
273 ./patches/supervisor/patch-rlimits-old-kernel.diff
274 ];
274 ];
275 });
275 });
276
276
277 "pytest" = super."pytest".override (attrs: {
278 patches = [
279 ./patches/pytest/setuptools.patch
280 ];
281 });
282
277 # Avoid that base packages screw up the build process
283 # Avoid that base packages screw up the build process
278 inherit (basePythonPackages)
284 inherit (basePythonPackages)
279 setuptools;
285 setuptools;
280
286
281 }
287 }
@@ -1,60 +1,60 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 from collections import OrderedDict
22 from collections import OrderedDict
23
23
24 import sys
24 import sys
25 import platform
25 import platform
26
26
27 VERSION = tuple(open(os.path.join(
27 VERSION = tuple(open(os.path.join(
28 os.path.dirname(__file__), 'VERSION')).read().split('.'))
28 os.path.dirname(__file__), 'VERSION')).read().split('.'))
29
29
30 BACKENDS = OrderedDict()
30 BACKENDS = OrderedDict()
31
31
32 BACKENDS['hg'] = 'Mercurial repository'
32 BACKENDS['hg'] = 'Mercurial repository'
33 BACKENDS['git'] = 'Git repository'
33 BACKENDS['git'] = 'Git repository'
34 BACKENDS['svn'] = 'Subversion repository'
34 BACKENDS['svn'] = 'Subversion repository'
35
35
36
36
37 CELERY_ENABLED = False
37 CELERY_ENABLED = False
38 CELERY_EAGER = False
38 CELERY_EAGER = False
39
39
40 # link to config for pyramid
40 # link to config for pyramid
41 CONFIG = {}
41 CONFIG = {}
42
42
43 # Populated with the settings dictionary from application init in
43 # Populated with the settings dictionary from application init in
44 # rhodecode.conf.environment.load_pyramid_environment
44 # rhodecode.conf.environment.load_pyramid_environment
45 PYRAMID_SETTINGS = {}
45 PYRAMID_SETTINGS = {}
46
46
47 # Linked module for extensions
47 # Linked module for extensions
48 EXTENSIONS = {}
48 EXTENSIONS = {}
49
49
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
51 __dbversion__ = 112 # defines current db version for migrations
51 __dbversion__ = 113 # defines current db version for migrations
52 __platform__ = platform.system()
52 __platform__ = platform.system()
53 __license__ = 'AGPLv3, and Commercial License'
53 __license__ = 'AGPLv3, and Commercial License'
54 __author__ = 'RhodeCode GmbH'
54 __author__ = 'RhodeCode GmbH'
55 __url__ = 'https://code.rhodecode.com'
55 __url__ = 'https://code.rhodecode.com'
56
56
57 is_windows = __platform__ in ['Windows']
57 is_windows = __platform__ in ['Windows']
58 is_unix = not is_windows
58 is_unix = not is_windows
59 is_test = False
59 is_test = False
60 disable_error_handler = False
60 disable_error_handler = False
@@ -1,719 +1,720 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 logging
22 import logging
23 import collections
23 import collections
24
24
25 import datetime
25 import datetime
26 import formencode
26 import formencode
27 import formencode.htmlfill
27 import formencode.htmlfill
28
28
29 import rhodecode
29 import rhodecode
30
30
31 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
31 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
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 from rhodecode.apps._base import BaseAppView
35 from rhodecode.apps._base import BaseAppView
36 from rhodecode.apps._base.navigation import navigation_list
36 from rhodecode.apps._base.navigation import navigation_list
37 from rhodecode.apps.svn_support.config_keys import generate_config
37 from rhodecode.apps.svn_support.config_keys import generate_config
38 from rhodecode.lib import helpers as h
38 from rhodecode.lib import helpers as h
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
40 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
41 from rhodecode.lib.celerylib import tasks, run_task
41 from rhodecode.lib.celerylib import tasks, run_task
42 from rhodecode.lib.utils import repo2db_mapper
42 from rhodecode.lib.utils import repo2db_mapper
43 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
43 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
44 from rhodecode.lib.index import searcher_from_config
44 from rhodecode.lib.index import searcher_from_config
45
45
46 from rhodecode.model.db import RhodeCodeUi, Repository
46 from rhodecode.model.db import RhodeCodeUi, Repository
47 from rhodecode.model.forms import (ApplicationSettingsForm,
47 from rhodecode.model.forms import (ApplicationSettingsForm,
48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 LabsSettingsForm, IssueTrackerPatternsForm)
49 LabsSettingsForm, IssueTrackerPatternsForm)
50 from rhodecode.model.permission import PermissionModel
50 from rhodecode.model.permission import PermissionModel
51 from rhodecode.model.repo_group import RepoGroupModel
51 from rhodecode.model.repo_group import RepoGroupModel
52
52
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54 from rhodecode.model.notification import EmailNotificationModel
54 from rhodecode.model.notification import EmailNotificationModel
55 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
56 from rhodecode.model.settings import (
56 from rhodecode.model.settings import (
57 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
57 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
58 SettingsModel)
58 SettingsModel)
59
59
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63
63
64 class AdminSettingsView(BaseAppView):
64 class AdminSettingsView(BaseAppView):
65
65
66 def load_default_context(self):
66 def load_default_context(self):
67 c = self._get_local_tmpl_context()
67 c = self._get_local_tmpl_context()
68 c.labs_active = str2bool(
68 c.labs_active = str2bool(
69 rhodecode.CONFIG.get('labs_settings_active', 'true'))
69 rhodecode.CONFIG.get('labs_settings_active', 'true'))
70 c.navlist = navigation_list(self.request)
70 c.navlist = navigation_list(self.request)
71 return c
71 return c
72
72
73 @classmethod
73 @classmethod
74 def _get_ui_settings(cls):
74 def _get_ui_settings(cls):
75 ret = RhodeCodeUi.query().all()
75 ret = RhodeCodeUi.query().all()
76
76
77 if not ret:
77 if not ret:
78 raise Exception('Could not get application ui settings !')
78 raise Exception('Could not get application ui settings !')
79 settings = {}
79 settings = {}
80 for each in ret:
80 for each in ret:
81 k = each.ui_key
81 k = each.ui_key
82 v = each.ui_value
82 v = each.ui_value
83 if k == '/':
83 if k == '/':
84 k = 'root_path'
84 k = 'root_path'
85
85
86 if k in ['push_ssl', 'publish', 'enabled']:
86 if k in ['push_ssl', 'publish', 'enabled']:
87 v = str2bool(v)
87 v = str2bool(v)
88
88
89 if k.find('.') != -1:
89 if k.find('.') != -1:
90 k = k.replace('.', '_')
90 k = k.replace('.', '_')
91
91
92 if each.ui_section in ['hooks', 'extensions']:
92 if each.ui_section in ['hooks', 'extensions']:
93 v = each.ui_active
93 v = each.ui_active
94
94
95 settings[each.ui_section + '_' + k] = v
95 settings[each.ui_section + '_' + k] = v
96 return settings
96 return settings
97
97
98 @classmethod
98 @classmethod
99 def _form_defaults(cls):
99 def _form_defaults(cls):
100 defaults = SettingsModel().get_all_settings()
100 defaults = SettingsModel().get_all_settings()
101 defaults.update(cls._get_ui_settings())
101 defaults.update(cls._get_ui_settings())
102
102
103 defaults.update({
103 defaults.update({
104 'new_svn_branch': '',
104 'new_svn_branch': '',
105 'new_svn_tag': '',
105 'new_svn_tag': '',
106 })
106 })
107 return defaults
107 return defaults
108
108
109 @LoginRequired()
109 @LoginRequired()
110 @HasPermissionAllDecorator('hg.admin')
110 @HasPermissionAllDecorator('hg.admin')
111 def settings_vcs(self):
111 def settings_vcs(self):
112 c = self.load_default_context()
112 c = self.load_default_context()
113 c.active = 'vcs'
113 c.active = 'vcs'
114 model = VcsSettingsModel()
114 model = VcsSettingsModel()
115 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
115 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
116 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
116 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
117
117
118 settings = self.request.registry.settings
118 settings = self.request.registry.settings
119 c.svn_proxy_generate_config = settings[generate_config]
119 c.svn_proxy_generate_config = settings[generate_config]
120
120
121 defaults = self._form_defaults()
121 defaults = self._form_defaults()
122
122
123 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
123 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
124
124
125 data = render('rhodecode:templates/admin/settings/settings.mako',
125 data = render('rhodecode:templates/admin/settings/settings.mako',
126 self._get_template_context(c), self.request)
126 self._get_template_context(c), self.request)
127 html = formencode.htmlfill.render(
127 html = formencode.htmlfill.render(
128 data,
128 data,
129 defaults=defaults,
129 defaults=defaults,
130 encoding="UTF-8",
130 encoding="UTF-8",
131 force_defaults=False
131 force_defaults=False
132 )
132 )
133 return Response(html)
133 return Response(html)
134
134
135 @LoginRequired()
135 @LoginRequired()
136 @HasPermissionAllDecorator('hg.admin')
136 @HasPermissionAllDecorator('hg.admin')
137 @CSRFRequired()
137 @CSRFRequired()
138 def settings_vcs_update(self):
138 def settings_vcs_update(self):
139 _ = self.request.translate
139 _ = self.request.translate
140 c = self.load_default_context()
140 c = self.load_default_context()
141 c.active = 'vcs'
141 c.active = 'vcs'
142
142
143 model = VcsSettingsModel()
143 model = VcsSettingsModel()
144 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
144 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
145 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
145 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
146
146
147 settings = self.request.registry.settings
147 settings = self.request.registry.settings
148 c.svn_proxy_generate_config = settings[generate_config]
148 c.svn_proxy_generate_config = settings[generate_config]
149
149
150 application_form = ApplicationUiSettingsForm(self.request.translate)()
150 application_form = ApplicationUiSettingsForm(self.request.translate)()
151
151
152 try:
152 try:
153 form_result = application_form.to_python(dict(self.request.POST))
153 form_result = application_form.to_python(dict(self.request.POST))
154 except formencode.Invalid as errors:
154 except formencode.Invalid as errors:
155 h.flash(
155 h.flash(
156 _("Some form inputs contain invalid data."),
156 _("Some form inputs contain invalid data."),
157 category='error')
157 category='error')
158 data = render('rhodecode:templates/admin/settings/settings.mako',
158 data = render('rhodecode:templates/admin/settings/settings.mako',
159 self._get_template_context(c), self.request)
159 self._get_template_context(c), self.request)
160 html = formencode.htmlfill.render(
160 html = formencode.htmlfill.render(
161 data,
161 data,
162 defaults=errors.value,
162 defaults=errors.value,
163 errors=errors.error_dict or {},
163 errors=errors.error_dict or {},
164 prefix_error=False,
164 prefix_error=False,
165 encoding="UTF-8",
165 encoding="UTF-8",
166 force_defaults=False
166 force_defaults=False
167 )
167 )
168 return Response(html)
168 return Response(html)
169
169
170 try:
170 try:
171 if c.visual.allow_repo_location_change:
171 if c.visual.allow_repo_location_change:
172 model.update_global_path_setting(form_result['paths_root_path'])
172 model.update_global_path_setting(form_result['paths_root_path'])
173
173
174 model.update_global_ssl_setting(form_result['web_push_ssl'])
174 model.update_global_ssl_setting(form_result['web_push_ssl'])
175 model.update_global_hook_settings(form_result)
175 model.update_global_hook_settings(form_result)
176
176
177 model.create_or_update_global_svn_settings(form_result)
177 model.create_or_update_global_svn_settings(form_result)
178 model.create_or_update_global_hg_settings(form_result)
178 model.create_or_update_global_hg_settings(form_result)
179 model.create_or_update_global_git_settings(form_result)
179 model.create_or_update_global_git_settings(form_result)
180 model.create_or_update_global_pr_settings(form_result)
180 model.create_or_update_global_pr_settings(form_result)
181 except Exception:
181 except Exception:
182 log.exception("Exception while updating settings")
182 log.exception("Exception while updating settings")
183 h.flash(_('Error occurred during updating '
183 h.flash(_('Error occurred during updating '
184 'application settings'), category='error')
184 'application settings'), category='error')
185 else:
185 else:
186 Session().commit()
186 Session().commit()
187 h.flash(_('Updated VCS settings'), category='success')
187 h.flash(_('Updated VCS settings'), category='success')
188 raise HTTPFound(h.route_path('admin_settings_vcs'))
188 raise HTTPFound(h.route_path('admin_settings_vcs'))
189
189
190 data = render('rhodecode:templates/admin/settings/settings.mako',
190 data = render('rhodecode:templates/admin/settings/settings.mako',
191 self._get_template_context(c), self.request)
191 self._get_template_context(c), self.request)
192 html = formencode.htmlfill.render(
192 html = formencode.htmlfill.render(
193 data,
193 data,
194 defaults=self._form_defaults(),
194 defaults=self._form_defaults(),
195 encoding="UTF-8",
195 encoding="UTF-8",
196 force_defaults=False
196 force_defaults=False
197 )
197 )
198 return Response(html)
198 return Response(html)
199
199
200 @LoginRequired()
200 @LoginRequired()
201 @HasPermissionAllDecorator('hg.admin')
201 @HasPermissionAllDecorator('hg.admin')
202 @CSRFRequired()
202 @CSRFRequired()
203 def settings_vcs_delete_svn_pattern(self):
203 def settings_vcs_delete_svn_pattern(self):
204 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
204 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
205 model = VcsSettingsModel()
205 model = VcsSettingsModel()
206 try:
206 try:
207 model.delete_global_svn_pattern(delete_pattern_id)
207 model.delete_global_svn_pattern(delete_pattern_id)
208 except SettingNotFound:
208 except SettingNotFound:
209 log.exception(
209 log.exception(
210 'Failed to delete svn_pattern with id %s', delete_pattern_id)
210 'Failed to delete svn_pattern with id %s', delete_pattern_id)
211 raise HTTPNotFound()
211 raise HTTPNotFound()
212
212
213 Session().commit()
213 Session().commit()
214 return True
214 return True
215
215
216 @LoginRequired()
216 @LoginRequired()
217 @HasPermissionAllDecorator('hg.admin')
217 @HasPermissionAllDecorator('hg.admin')
218 def settings_mapping(self):
218 def settings_mapping(self):
219 c = self.load_default_context()
219 c = self.load_default_context()
220 c.active = 'mapping'
220 c.active = 'mapping'
221
221
222 data = render('rhodecode:templates/admin/settings/settings.mako',
222 data = render('rhodecode:templates/admin/settings/settings.mako',
223 self._get_template_context(c), self.request)
223 self._get_template_context(c), self.request)
224 html = formencode.htmlfill.render(
224 html = formencode.htmlfill.render(
225 data,
225 data,
226 defaults=self._form_defaults(),
226 defaults=self._form_defaults(),
227 encoding="UTF-8",
227 encoding="UTF-8",
228 force_defaults=False
228 force_defaults=False
229 )
229 )
230 return Response(html)
230 return Response(html)
231
231
232 @LoginRequired()
232 @LoginRequired()
233 @HasPermissionAllDecorator('hg.admin')
233 @HasPermissionAllDecorator('hg.admin')
234 @CSRFRequired()
234 @CSRFRequired()
235 def settings_mapping_update(self):
235 def settings_mapping_update(self):
236 _ = self.request.translate
236 _ = self.request.translate
237 c = self.load_default_context()
237 c = self.load_default_context()
238 c.active = 'mapping'
238 c.active = 'mapping'
239 rm_obsolete = self.request.POST.get('destroy', False)
239 rm_obsolete = self.request.POST.get('destroy', False)
240 invalidate_cache = self.request.POST.get('invalidate', False)
240 invalidate_cache = self.request.POST.get('invalidate', False)
241 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
241 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
242
242
243 if invalidate_cache:
243 if invalidate_cache:
244 log.debug('invalidating all repositories cache')
244 log.debug('invalidating all repositories cache')
245 for repo in Repository.get_all():
245 for repo in Repository.get_all():
246 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
246 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
247
247
248 filesystem_repos = ScmModel().repo_scan()
248 filesystem_repos = ScmModel().repo_scan()
249 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
249 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
250 PermissionModel().trigger_permission_flush()
250 PermissionModel().trigger_permission_flush()
251
251
252 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
252 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
253 h.flash(_('Repositories successfully '
253 h.flash(_('Repositories successfully '
254 'rescanned added: %s ; removed: %s') %
254 'rescanned added: %s ; removed: %s') %
255 (_repr(added), _repr(removed)),
255 (_repr(added), _repr(removed)),
256 category='success')
256 category='success')
257 raise HTTPFound(h.route_path('admin_settings_mapping'))
257 raise HTTPFound(h.route_path('admin_settings_mapping'))
258
258
259 @LoginRequired()
259 @LoginRequired()
260 @HasPermissionAllDecorator('hg.admin')
260 @HasPermissionAllDecorator('hg.admin')
261 def settings_global(self):
261 def settings_global(self):
262 c = self.load_default_context()
262 c = self.load_default_context()
263 c.active = 'global'
263 c.active = 'global'
264 c.personal_repo_group_default_pattern = RepoGroupModel()\
264 c.personal_repo_group_default_pattern = RepoGroupModel()\
265 .get_personal_group_name_pattern()
265 .get_personal_group_name_pattern()
266
266
267 data = render('rhodecode:templates/admin/settings/settings.mako',
267 data = render('rhodecode:templates/admin/settings/settings.mako',
268 self._get_template_context(c), self.request)
268 self._get_template_context(c), self.request)
269 html = formencode.htmlfill.render(
269 html = formencode.htmlfill.render(
270 data,
270 data,
271 defaults=self._form_defaults(),
271 defaults=self._form_defaults(),
272 encoding="UTF-8",
272 encoding="UTF-8",
273 force_defaults=False
273 force_defaults=False
274 )
274 )
275 return Response(html)
275 return Response(html)
276
276
277 @LoginRequired()
277 @LoginRequired()
278 @HasPermissionAllDecorator('hg.admin')
278 @HasPermissionAllDecorator('hg.admin')
279 @CSRFRequired()
279 @CSRFRequired()
280 def settings_global_update(self):
280 def settings_global_update(self):
281 _ = self.request.translate
281 _ = self.request.translate
282 c = self.load_default_context()
282 c = self.load_default_context()
283 c.active = 'global'
283 c.active = 'global'
284 c.personal_repo_group_default_pattern = RepoGroupModel()\
284 c.personal_repo_group_default_pattern = RepoGroupModel()\
285 .get_personal_group_name_pattern()
285 .get_personal_group_name_pattern()
286 application_form = ApplicationSettingsForm(self.request.translate)()
286 application_form = ApplicationSettingsForm(self.request.translate)()
287 try:
287 try:
288 form_result = application_form.to_python(dict(self.request.POST))
288 form_result = application_form.to_python(dict(self.request.POST))
289 except formencode.Invalid as errors:
289 except formencode.Invalid as errors:
290 h.flash(
290 h.flash(
291 _("Some form inputs contain invalid data."),
291 _("Some form inputs contain invalid data."),
292 category='error')
292 category='error')
293 data = render('rhodecode:templates/admin/settings/settings.mako',
293 data = render('rhodecode:templates/admin/settings/settings.mako',
294 self._get_template_context(c), self.request)
294 self._get_template_context(c), self.request)
295 html = formencode.htmlfill.render(
295 html = formencode.htmlfill.render(
296 data,
296 data,
297 defaults=errors.value,
297 defaults=errors.value,
298 errors=errors.error_dict or {},
298 errors=errors.error_dict or {},
299 prefix_error=False,
299 prefix_error=False,
300 encoding="UTF-8",
300 encoding="UTF-8",
301 force_defaults=False
301 force_defaults=False
302 )
302 )
303 return Response(html)
303 return Response(html)
304
304
305 settings = [
305 settings = [
306 ('title', 'rhodecode_title', 'unicode'),
306 ('title', 'rhodecode_title', 'unicode'),
307 ('realm', 'rhodecode_realm', 'unicode'),
307 ('realm', 'rhodecode_realm', 'unicode'),
308 ('pre_code', 'rhodecode_pre_code', 'unicode'),
308 ('pre_code', 'rhodecode_pre_code', 'unicode'),
309 ('post_code', 'rhodecode_post_code', 'unicode'),
309 ('post_code', 'rhodecode_post_code', 'unicode'),
310 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
310 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
311 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
311 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
312 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
312 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
313 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
313 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
314 ]
314 ]
315 try:
315 try:
316 for setting, form_key, type_ in settings:
316 for setting, form_key, type_ in settings:
317 sett = SettingsModel().create_or_update_setting(
317 sett = SettingsModel().create_or_update_setting(
318 setting, form_result[form_key], type_)
318 setting, form_result[form_key], type_)
319 Session().add(sett)
319 Session().add(sett)
320
320
321 Session().commit()
321 Session().commit()
322 SettingsModel().invalidate_settings_cache()
322 SettingsModel().invalidate_settings_cache()
323 h.flash(_('Updated application settings'), category='success')
323 h.flash(_('Updated application settings'), category='success')
324 except Exception:
324 except Exception:
325 log.exception("Exception while updating application settings")
325 log.exception("Exception while updating application settings")
326 h.flash(
326 h.flash(
327 _('Error occurred during updating application settings'),
327 _('Error occurred during updating application settings'),
328 category='error')
328 category='error')
329
329
330 raise HTTPFound(h.route_path('admin_settings_global'))
330 raise HTTPFound(h.route_path('admin_settings_global'))
331
331
332 @LoginRequired()
332 @LoginRequired()
333 @HasPermissionAllDecorator('hg.admin')
333 @HasPermissionAllDecorator('hg.admin')
334 def settings_visual(self):
334 def settings_visual(self):
335 c = self.load_default_context()
335 c = self.load_default_context()
336 c.active = 'visual'
336 c.active = 'visual'
337
337
338 data = render('rhodecode:templates/admin/settings/settings.mako',
338 data = render('rhodecode:templates/admin/settings/settings.mako',
339 self._get_template_context(c), self.request)
339 self._get_template_context(c), self.request)
340 html = formencode.htmlfill.render(
340 html = formencode.htmlfill.render(
341 data,
341 data,
342 defaults=self._form_defaults(),
342 defaults=self._form_defaults(),
343 encoding="UTF-8",
343 encoding="UTF-8",
344 force_defaults=False
344 force_defaults=False
345 )
345 )
346 return Response(html)
346 return Response(html)
347
347
348 @LoginRequired()
348 @LoginRequired()
349 @HasPermissionAllDecorator('hg.admin')
349 @HasPermissionAllDecorator('hg.admin')
350 @CSRFRequired()
350 @CSRFRequired()
351 def settings_visual_update(self):
351 def settings_visual_update(self):
352 _ = self.request.translate
352 _ = self.request.translate
353 c = self.load_default_context()
353 c = self.load_default_context()
354 c.active = 'visual'
354 c.active = 'visual'
355 application_form = ApplicationVisualisationForm(self.request.translate)()
355 application_form = ApplicationVisualisationForm(self.request.translate)()
356 try:
356 try:
357 form_result = application_form.to_python(dict(self.request.POST))
357 form_result = application_form.to_python(dict(self.request.POST))
358 except formencode.Invalid as errors:
358 except formencode.Invalid as errors:
359 h.flash(
359 h.flash(
360 _("Some form inputs contain invalid data."),
360 _("Some form inputs contain invalid data."),
361 category='error')
361 category='error')
362 data = render('rhodecode:templates/admin/settings/settings.mako',
362 data = render('rhodecode:templates/admin/settings/settings.mako',
363 self._get_template_context(c), self.request)
363 self._get_template_context(c), self.request)
364 html = formencode.htmlfill.render(
364 html = formencode.htmlfill.render(
365 data,
365 data,
366 defaults=errors.value,
366 defaults=errors.value,
367 errors=errors.error_dict or {},
367 errors=errors.error_dict or {},
368 prefix_error=False,
368 prefix_error=False,
369 encoding="UTF-8",
369 encoding="UTF-8",
370 force_defaults=False
370 force_defaults=False
371 )
371 )
372 return Response(html)
372 return Response(html)
373
373
374 try:
374 try:
375 settings = [
375 settings = [
376 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
376 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
377 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
377 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
378 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
378 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
379 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
379 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
380 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
380 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
381 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
381 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
382 ('show_version', 'rhodecode_show_version', 'bool'),
382 ('show_version', 'rhodecode_show_version', 'bool'),
383 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
383 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
384 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
384 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
385 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
385 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
386 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
386 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
387 ('clone_uri_id_tmpl', 'rhodecode_clone_uri_id_tmpl', 'unicode'),
387 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
388 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
388 ('support_url', 'rhodecode_support_url', 'unicode'),
389 ('support_url', 'rhodecode_support_url', 'unicode'),
389 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
390 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
390 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
391 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
391 ]
392 ]
392 for setting, form_key, type_ in settings:
393 for setting, form_key, type_ in settings:
393 sett = SettingsModel().create_or_update_setting(
394 sett = SettingsModel().create_or_update_setting(
394 setting, form_result[form_key], type_)
395 setting, form_result[form_key], type_)
395 Session().add(sett)
396 Session().add(sett)
396
397
397 Session().commit()
398 Session().commit()
398 SettingsModel().invalidate_settings_cache()
399 SettingsModel().invalidate_settings_cache()
399 h.flash(_('Updated visualisation settings'), category='success')
400 h.flash(_('Updated visualisation settings'), category='success')
400 except Exception:
401 except Exception:
401 log.exception("Exception updating visualization settings")
402 log.exception("Exception updating visualization settings")
402 h.flash(_('Error occurred during updating '
403 h.flash(_('Error occurred during updating '
403 'visualisation settings'),
404 'visualisation settings'),
404 category='error')
405 category='error')
405
406
406 raise HTTPFound(h.route_path('admin_settings_visual'))
407 raise HTTPFound(h.route_path('admin_settings_visual'))
407
408
408 @LoginRequired()
409 @LoginRequired()
409 @HasPermissionAllDecorator('hg.admin')
410 @HasPermissionAllDecorator('hg.admin')
410 def settings_issuetracker(self):
411 def settings_issuetracker(self):
411 c = self.load_default_context()
412 c = self.load_default_context()
412 c.active = 'issuetracker'
413 c.active = 'issuetracker'
413 defaults = c.rc_config
414 defaults = c.rc_config
414
415
415 entry_key = 'rhodecode_issuetracker_pat_'
416 entry_key = 'rhodecode_issuetracker_pat_'
416
417
417 c.issuetracker_entries = {}
418 c.issuetracker_entries = {}
418 for k, v in defaults.items():
419 for k, v in defaults.items():
419 if k.startswith(entry_key):
420 if k.startswith(entry_key):
420 uid = k[len(entry_key):]
421 uid = k[len(entry_key):]
421 c.issuetracker_entries[uid] = None
422 c.issuetracker_entries[uid] = None
422
423
423 for uid in c.issuetracker_entries:
424 for uid in c.issuetracker_entries:
424 c.issuetracker_entries[uid] = AttributeDict({
425 c.issuetracker_entries[uid] = AttributeDict({
425 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
426 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
426 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
427 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
427 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
428 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
428 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
429 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
429 })
430 })
430
431
431 return self._get_template_context(c)
432 return self._get_template_context(c)
432
433
433 @LoginRequired()
434 @LoginRequired()
434 @HasPermissionAllDecorator('hg.admin')
435 @HasPermissionAllDecorator('hg.admin')
435 @CSRFRequired()
436 @CSRFRequired()
436 def settings_issuetracker_test(self):
437 def settings_issuetracker_test(self):
437 error_container = []
438 error_container = []
438
439
439 urlified_commit = h.urlify_commit_message(
440 urlified_commit = h.urlify_commit_message(
440 self.request.POST.get('test_text', ''),
441 self.request.POST.get('test_text', ''),
441 'repo_group/test_repo1', error_container=error_container)
442 'repo_group/test_repo1', error_container=error_container)
442 if error_container:
443 if error_container:
443 def converter(inp):
444 def converter(inp):
444 return h.html_escape(unicode(inp))
445 return h.html_escape(unicode(inp))
445
446
446 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
447 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
447
448
448 return urlified_commit
449 return urlified_commit
449
450
450 @LoginRequired()
451 @LoginRequired()
451 @HasPermissionAllDecorator('hg.admin')
452 @HasPermissionAllDecorator('hg.admin')
452 @CSRFRequired()
453 @CSRFRequired()
453 def settings_issuetracker_update(self):
454 def settings_issuetracker_update(self):
454 _ = self.request.translate
455 _ = self.request.translate
455 self.load_default_context()
456 self.load_default_context()
456 settings_model = IssueTrackerSettingsModel()
457 settings_model = IssueTrackerSettingsModel()
457
458
458 try:
459 try:
459 form = IssueTrackerPatternsForm(self.request.translate)()
460 form = IssueTrackerPatternsForm(self.request.translate)()
460 data = form.to_python(self.request.POST)
461 data = form.to_python(self.request.POST)
461 except formencode.Invalid as errors:
462 except formencode.Invalid as errors:
462 log.exception('Failed to add new pattern')
463 log.exception('Failed to add new pattern')
463 error = errors
464 error = errors
464 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
465 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
465 category='error')
466 category='error')
466 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
467 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
467
468
468 if data:
469 if data:
469 for uid in data.get('delete_patterns', []):
470 for uid in data.get('delete_patterns', []):
470 settings_model.delete_entries(uid)
471 settings_model.delete_entries(uid)
471
472
472 for pattern in data.get('patterns', []):
473 for pattern in data.get('patterns', []):
473 for setting, value, type_ in pattern:
474 for setting, value, type_ in pattern:
474 sett = settings_model.create_or_update_setting(
475 sett = settings_model.create_or_update_setting(
475 setting, value, type_)
476 setting, value, type_)
476 Session().add(sett)
477 Session().add(sett)
477
478
478 Session().commit()
479 Session().commit()
479
480
480 SettingsModel().invalidate_settings_cache()
481 SettingsModel().invalidate_settings_cache()
481 h.flash(_('Updated issue tracker entries'), category='success')
482 h.flash(_('Updated issue tracker entries'), category='success')
482 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
483 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
483
484
484 @LoginRequired()
485 @LoginRequired()
485 @HasPermissionAllDecorator('hg.admin')
486 @HasPermissionAllDecorator('hg.admin')
486 @CSRFRequired()
487 @CSRFRequired()
487 def settings_issuetracker_delete(self):
488 def settings_issuetracker_delete(self):
488 _ = self.request.translate
489 _ = self.request.translate
489 self.load_default_context()
490 self.load_default_context()
490 uid = self.request.POST.get('uid')
491 uid = self.request.POST.get('uid')
491 try:
492 try:
492 IssueTrackerSettingsModel().delete_entries(uid)
493 IssueTrackerSettingsModel().delete_entries(uid)
493 except Exception:
494 except Exception:
494 log.exception('Failed to delete issue tracker setting %s', uid)
495 log.exception('Failed to delete issue tracker setting %s', uid)
495 raise HTTPNotFound()
496 raise HTTPNotFound()
496
497
497 SettingsModel().invalidate_settings_cache()
498 SettingsModel().invalidate_settings_cache()
498 h.flash(_('Removed issue tracker entry.'), category='success')
499 h.flash(_('Removed issue tracker entry.'), category='success')
499
500
500 return {'deleted': uid}
501 return {'deleted': uid}
501
502
502 @LoginRequired()
503 @LoginRequired()
503 @HasPermissionAllDecorator('hg.admin')
504 @HasPermissionAllDecorator('hg.admin')
504 def settings_email(self):
505 def settings_email(self):
505 c = self.load_default_context()
506 c = self.load_default_context()
506 c.active = 'email'
507 c.active = 'email'
507 c.rhodecode_ini = rhodecode.CONFIG
508 c.rhodecode_ini = rhodecode.CONFIG
508
509
509 data = render('rhodecode:templates/admin/settings/settings.mako',
510 data = render('rhodecode:templates/admin/settings/settings.mako',
510 self._get_template_context(c), self.request)
511 self._get_template_context(c), self.request)
511 html = formencode.htmlfill.render(
512 html = formencode.htmlfill.render(
512 data,
513 data,
513 defaults=self._form_defaults(),
514 defaults=self._form_defaults(),
514 encoding="UTF-8",
515 encoding="UTF-8",
515 force_defaults=False
516 force_defaults=False
516 )
517 )
517 return Response(html)
518 return Response(html)
518
519
519 @LoginRequired()
520 @LoginRequired()
520 @HasPermissionAllDecorator('hg.admin')
521 @HasPermissionAllDecorator('hg.admin')
521 @CSRFRequired()
522 @CSRFRequired()
522 def settings_email_update(self):
523 def settings_email_update(self):
523 _ = self.request.translate
524 _ = self.request.translate
524 c = self.load_default_context()
525 c = self.load_default_context()
525 c.active = 'email'
526 c.active = 'email'
526
527
527 test_email = self.request.POST.get('test_email')
528 test_email = self.request.POST.get('test_email')
528
529
529 if not test_email:
530 if not test_email:
530 h.flash(_('Please enter email address'), category='error')
531 h.flash(_('Please enter email address'), category='error')
531 raise HTTPFound(h.route_path('admin_settings_email'))
532 raise HTTPFound(h.route_path('admin_settings_email'))
532
533
533 email_kwargs = {
534 email_kwargs = {
534 'date': datetime.datetime.now(),
535 'date': datetime.datetime.now(),
535 'user': self._rhodecode_db_user
536 'user': self._rhodecode_db_user
536 }
537 }
537
538
538 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
539 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
539 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
540 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
540
541
541 recipients = [test_email] if test_email else None
542 recipients = [test_email] if test_email else None
542
543
543 run_task(tasks.send_email, recipients, subject,
544 run_task(tasks.send_email, recipients, subject,
544 email_body_plaintext, email_body)
545 email_body_plaintext, email_body)
545
546
546 h.flash(_('Send email task created'), category='success')
547 h.flash(_('Send email task created'), category='success')
547 raise HTTPFound(h.route_path('admin_settings_email'))
548 raise HTTPFound(h.route_path('admin_settings_email'))
548
549
549 @LoginRequired()
550 @LoginRequired()
550 @HasPermissionAllDecorator('hg.admin')
551 @HasPermissionAllDecorator('hg.admin')
551 def settings_hooks(self):
552 def settings_hooks(self):
552 c = self.load_default_context()
553 c = self.load_default_context()
553 c.active = 'hooks'
554 c.active = 'hooks'
554
555
555 model = SettingsModel()
556 model = SettingsModel()
556 c.hooks = model.get_builtin_hooks()
557 c.hooks = model.get_builtin_hooks()
557 c.custom_hooks = model.get_custom_hooks()
558 c.custom_hooks = model.get_custom_hooks()
558
559
559 data = render('rhodecode:templates/admin/settings/settings.mako',
560 data = render('rhodecode:templates/admin/settings/settings.mako',
560 self._get_template_context(c), self.request)
561 self._get_template_context(c), self.request)
561 html = formencode.htmlfill.render(
562 html = formencode.htmlfill.render(
562 data,
563 data,
563 defaults=self._form_defaults(),
564 defaults=self._form_defaults(),
564 encoding="UTF-8",
565 encoding="UTF-8",
565 force_defaults=False
566 force_defaults=False
566 )
567 )
567 return Response(html)
568 return Response(html)
568
569
569 @LoginRequired()
570 @LoginRequired()
570 @HasPermissionAllDecorator('hg.admin')
571 @HasPermissionAllDecorator('hg.admin')
571 @CSRFRequired()
572 @CSRFRequired()
572 def settings_hooks_update(self):
573 def settings_hooks_update(self):
573 _ = self.request.translate
574 _ = self.request.translate
574 c = self.load_default_context()
575 c = self.load_default_context()
575 c.active = 'hooks'
576 c.active = 'hooks'
576 if c.visual.allow_custom_hooks_settings:
577 if c.visual.allow_custom_hooks_settings:
577 ui_key = self.request.POST.get('new_hook_ui_key')
578 ui_key = self.request.POST.get('new_hook_ui_key')
578 ui_value = self.request.POST.get('new_hook_ui_value')
579 ui_value = self.request.POST.get('new_hook_ui_value')
579
580
580 hook_id = self.request.POST.get('hook_id')
581 hook_id = self.request.POST.get('hook_id')
581 new_hook = False
582 new_hook = False
582
583
583 model = SettingsModel()
584 model = SettingsModel()
584 try:
585 try:
585 if ui_value and ui_key:
586 if ui_value and ui_key:
586 model.create_or_update_hook(ui_key, ui_value)
587 model.create_or_update_hook(ui_key, ui_value)
587 h.flash(_('Added new hook'), category='success')
588 h.flash(_('Added new hook'), category='success')
588 new_hook = True
589 new_hook = True
589 elif hook_id:
590 elif hook_id:
590 RhodeCodeUi.delete(hook_id)
591 RhodeCodeUi.delete(hook_id)
591 Session().commit()
592 Session().commit()
592
593
593 # check for edits
594 # check for edits
594 update = False
595 update = False
595 _d = self.request.POST.dict_of_lists()
596 _d = self.request.POST.dict_of_lists()
596 for k, v in zip(_d.get('hook_ui_key', []),
597 for k, v in zip(_d.get('hook_ui_key', []),
597 _d.get('hook_ui_value_new', [])):
598 _d.get('hook_ui_value_new', [])):
598 model.create_or_update_hook(k, v)
599 model.create_or_update_hook(k, v)
599 update = True
600 update = True
600
601
601 if update and not new_hook:
602 if update and not new_hook:
602 h.flash(_('Updated hooks'), category='success')
603 h.flash(_('Updated hooks'), category='success')
603 Session().commit()
604 Session().commit()
604 except Exception:
605 except Exception:
605 log.exception("Exception during hook creation")
606 log.exception("Exception during hook creation")
606 h.flash(_('Error occurred during hook creation'),
607 h.flash(_('Error occurred during hook creation'),
607 category='error')
608 category='error')
608
609
609 raise HTTPFound(h.route_path('admin_settings_hooks'))
610 raise HTTPFound(h.route_path('admin_settings_hooks'))
610
611
611 @LoginRequired()
612 @LoginRequired()
612 @HasPermissionAllDecorator('hg.admin')
613 @HasPermissionAllDecorator('hg.admin')
613 def settings_search(self):
614 def settings_search(self):
614 c = self.load_default_context()
615 c = self.load_default_context()
615 c.active = 'search'
616 c.active = 'search'
616
617
617 c.searcher = searcher_from_config(self.request.registry.settings)
618 c.searcher = searcher_from_config(self.request.registry.settings)
618 c.statistics = c.searcher.statistics(self.request.translate)
619 c.statistics = c.searcher.statistics(self.request.translate)
619
620
620 return self._get_template_context(c)
621 return self._get_template_context(c)
621
622
622 @LoginRequired()
623 @LoginRequired()
623 @HasPermissionAllDecorator('hg.admin')
624 @HasPermissionAllDecorator('hg.admin')
624 def settings_automation(self):
625 def settings_automation(self):
625 c = self.load_default_context()
626 c = self.load_default_context()
626 c.active = 'automation'
627 c.active = 'automation'
627
628
628 return self._get_template_context(c)
629 return self._get_template_context(c)
629
630
630 @LoginRequired()
631 @LoginRequired()
631 @HasPermissionAllDecorator('hg.admin')
632 @HasPermissionAllDecorator('hg.admin')
632 def settings_labs(self):
633 def settings_labs(self):
633 c = self.load_default_context()
634 c = self.load_default_context()
634 if not c.labs_active:
635 if not c.labs_active:
635 raise HTTPFound(h.route_path('admin_settings'))
636 raise HTTPFound(h.route_path('admin_settings'))
636
637
637 c.active = 'labs'
638 c.active = 'labs'
638 c.lab_settings = _LAB_SETTINGS
639 c.lab_settings = _LAB_SETTINGS
639
640
640 data = render('rhodecode:templates/admin/settings/settings.mako',
641 data = render('rhodecode:templates/admin/settings/settings.mako',
641 self._get_template_context(c), self.request)
642 self._get_template_context(c), self.request)
642 html = formencode.htmlfill.render(
643 html = formencode.htmlfill.render(
643 data,
644 data,
644 defaults=self._form_defaults(),
645 defaults=self._form_defaults(),
645 encoding="UTF-8",
646 encoding="UTF-8",
646 force_defaults=False
647 force_defaults=False
647 )
648 )
648 return Response(html)
649 return Response(html)
649
650
650 @LoginRequired()
651 @LoginRequired()
651 @HasPermissionAllDecorator('hg.admin')
652 @HasPermissionAllDecorator('hg.admin')
652 @CSRFRequired()
653 @CSRFRequired()
653 def settings_labs_update(self):
654 def settings_labs_update(self):
654 _ = self.request.translate
655 _ = self.request.translate
655 c = self.load_default_context()
656 c = self.load_default_context()
656 c.active = 'labs'
657 c.active = 'labs'
657
658
658 application_form = LabsSettingsForm(self.request.translate)()
659 application_form = LabsSettingsForm(self.request.translate)()
659 try:
660 try:
660 form_result = application_form.to_python(dict(self.request.POST))
661 form_result = application_form.to_python(dict(self.request.POST))
661 except formencode.Invalid as errors:
662 except formencode.Invalid as errors:
662 h.flash(
663 h.flash(
663 _("Some form inputs contain invalid data."),
664 _("Some form inputs contain invalid data."),
664 category='error')
665 category='error')
665 data = render('rhodecode:templates/admin/settings/settings.mako',
666 data = render('rhodecode:templates/admin/settings/settings.mako',
666 self._get_template_context(c), self.request)
667 self._get_template_context(c), self.request)
667 html = formencode.htmlfill.render(
668 html = formencode.htmlfill.render(
668 data,
669 data,
669 defaults=errors.value,
670 defaults=errors.value,
670 errors=errors.error_dict or {},
671 errors=errors.error_dict or {},
671 prefix_error=False,
672 prefix_error=False,
672 encoding="UTF-8",
673 encoding="UTF-8",
673 force_defaults=False
674 force_defaults=False
674 )
675 )
675 return Response(html)
676 return Response(html)
676
677
677 try:
678 try:
678 session = Session()
679 session = Session()
679 for setting in _LAB_SETTINGS:
680 for setting in _LAB_SETTINGS:
680 setting_name = setting.key[len('rhodecode_'):]
681 setting_name = setting.key[len('rhodecode_'):]
681 sett = SettingsModel().create_or_update_setting(
682 sett = SettingsModel().create_or_update_setting(
682 setting_name, form_result[setting.key], setting.type)
683 setting_name, form_result[setting.key], setting.type)
683 session.add(sett)
684 session.add(sett)
684
685
685 except Exception:
686 except Exception:
686 log.exception('Exception while updating lab settings')
687 log.exception('Exception while updating lab settings')
687 h.flash(_('Error occurred during updating labs settings'),
688 h.flash(_('Error occurred during updating labs settings'),
688 category='error')
689 category='error')
689 else:
690 else:
690 Session().commit()
691 Session().commit()
691 SettingsModel().invalidate_settings_cache()
692 SettingsModel().invalidate_settings_cache()
692 h.flash(_('Updated Labs settings'), category='success')
693 h.flash(_('Updated Labs settings'), category='success')
693 raise HTTPFound(h.route_path('admin_settings_labs'))
694 raise HTTPFound(h.route_path('admin_settings_labs'))
694
695
695 data = render('rhodecode:templates/admin/settings/settings.mako',
696 data = render('rhodecode:templates/admin/settings/settings.mako',
696 self._get_template_context(c), self.request)
697 self._get_template_context(c), self.request)
697 html = formencode.htmlfill.render(
698 html = formencode.htmlfill.render(
698 data,
699 data,
699 defaults=self._form_defaults(),
700 defaults=self._form_defaults(),
700 encoding="UTF-8",
701 encoding="UTF-8",
701 force_defaults=False
702 force_defaults=False
702 )
703 )
703 return Response(html)
704 return Response(html)
704
705
705
706
706 # :param key: name of the setting including the 'rhodecode_' prefix
707 # :param key: name of the setting including the 'rhodecode_' prefix
707 # :param type: the RhodeCodeSetting type to use.
708 # :param type: the RhodeCodeSetting type to use.
708 # :param group: the i18ned group in which we should dispaly this setting
709 # :param group: the i18ned group in which we should dispaly this setting
709 # :param label: the i18ned label we should display for this setting
710 # :param label: the i18ned label we should display for this setting
710 # :param help: the i18ned help we should dispaly for this setting
711 # :param help: the i18ned help we should dispaly for this setting
711 LabSetting = collections.namedtuple(
712 LabSetting = collections.namedtuple(
712 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
713 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
713
714
714
715
715 # This list has to be kept in sync with the form
716 # This list has to be kept in sync with the form
716 # rhodecode.model.forms.LabsSettingsForm.
717 # rhodecode.model.forms.LabsSettingsForm.
717 _LAB_SETTINGS = [
718 _LAB_SETTINGS = [
718
719
719 ]
720 ]
@@ -1,471 +1,476 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.renderers import render_to_response
25 from pyramid.renderers import render_to_response
26 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.lib.celerylib import run_task, tasks
27 from rhodecode.lib.celerylib import run_task, tasks
28 from rhodecode.lib.utils2 import AttributeDict
28 from rhodecode.lib.utils2 import AttributeDict
29 from rhodecode.model.db import User
29 from rhodecode.model.db import User
30 from rhodecode.model.notification import EmailNotificationModel
30 from rhodecode.model.notification import EmailNotificationModel
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34
34
35 class DebugStyleView(BaseAppView):
35 class DebugStyleView(BaseAppView):
36
36
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 return c
39 return c
40
40
41 def index(self):
41 def index(self):
42 c = self.load_default_context()
42 c = self.load_default_context()
43 c.active = 'index'
43 c.active = 'index'
44
44
45 return render_to_response(
45 return render_to_response(
46 'debug_style/index.html', self._get_template_context(c),
46 'debug_style/index.html', self._get_template_context(c),
47 request=self.request)
47 request=self.request)
48
48
49 def render_email(self):
49 def render_email(self):
50 c = self.load_default_context()
50 c = self.load_default_context()
51 email_id = self.request.matchdict['email_id']
51 email_id = self.request.matchdict['email_id']
52 c.active = 'emails'
52 c.active = 'emails'
53
53
54 pr = AttributeDict(
54 pr = AttributeDict(
55 pull_request_id=123,
55 pull_request_id=123,
56 title='digital_ocean: fix redis, elastic search start on boot, '
56 title='digital_ocean: fix redis, elastic search start on boot, '
57 'fix fd limits on supervisor, set postgres 11 version',
57 'fix fd limits on supervisor, set postgres 11 version',
58 description='''
58 description='''
59 Check if we should use full-topic or mini-topic.
59 Check if we should use full-topic or mini-topic.
60
60
61 - full topic produces some problems with merge states etc
61 - full topic produces some problems with merge states etc
62 - server-mini-topic needs probably tweeks.
62 - server-mini-topic needs probably tweeks.
63 ''',
63 ''',
64 repo_name='foobar',
64 repo_name='foobar',
65 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
65 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
66 target_ref_parts=AttributeDict(type='branch', name='master'),
66 target_ref_parts=AttributeDict(type='branch', name='master'),
67 )
67 )
68
68
69 target_repo = AttributeDict(repo_name='repo_group/target_repo')
69 target_repo = AttributeDict(repo_name='repo_group/target_repo')
70 source_repo = AttributeDict(repo_name='repo_group/source_repo')
70 source_repo = AttributeDict(repo_name='repo_group/source_repo')
71 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
71 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
72 # file/commit changes for PR update
72 # file/commit changes for PR update
73 commit_changes = AttributeDict({
73 commit_changes = AttributeDict({
74 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
74 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
75 'removed': ['eeeeeeeeeee'],
75 'removed': ['eeeeeeeeeee'],
76 })
76 })
77
77
78 file_changes = AttributeDict({
78 file_changes = AttributeDict({
79 'added': ['a/file1.md', 'file2.py'],
79 'added': ['a/file1.md', 'file2.py'],
80 'modified': ['b/modified_file.rst'],
80 'modified': ['b/modified_file.rst'],
81 'removed': ['.idea'],
81 'removed': ['.idea'],
82 })
82 })
83
83
84 exc_traceback = {
84 exc_traceback = {
85 'exc_utc_date': '2020-03-26T12:54:50.683281',
85 'exc_utc_date': '2020-03-26T12:54:50.683281',
86 'exc_id': 139638856342656,
86 'exc_id': 139638856342656,
87 'exc_timestamp': '1585227290.683288',
87 'exc_timestamp': '1585227290.683288',
88 'version': 'v1',
88 'version': 'v1',
89 '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',
89 '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',
90 'exc_type': 'AttributeError'
90 'exc_type': 'AttributeError'
91 }
91 }
92
92
93 email_kwargs = {
93 email_kwargs = {
94 'test': {},
94 'test': {},
95
95
96 'message': {
96 'message': {
97 'body': 'message body !'
97 'body': 'message body !'
98 },
98 },
99
99
100 'email_test': {
100 'email_test': {
101 'user': user,
101 'user': user,
102 'date': datetime.datetime.now(),
102 'date': datetime.datetime.now(),
103 },
103 },
104
104
105 'update_available': {
106 'current_ver': '4.23.0',
107 'latest_ver': '4.24.0',
108 },
109
105 'exception': {
110 'exception': {
106 'email_prefix': '[RHODECODE ERROR]',
111 'email_prefix': '[RHODECODE ERROR]',
107 'exc_id': exc_traceback['exc_id'],
112 'exc_id': exc_traceback['exc_id'],
108 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
113 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
109 'exc_type_name': 'NameError',
114 'exc_type_name': 'NameError',
110 'exc_traceback': exc_traceback,
115 'exc_traceback': exc_traceback,
111 },
116 },
112
117
113 'password_reset': {
118 'password_reset': {
114 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
119 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
115
120
116 'user': user,
121 'user': user,
117 'date': datetime.datetime.now(),
122 'date': datetime.datetime.now(),
118 'email': 'test@rhodecode.com',
123 'email': 'test@rhodecode.com',
119 'first_admin_email': User.get_first_super_admin().email
124 'first_admin_email': User.get_first_super_admin().email
120 },
125 },
121
126
122 'password_reset_confirmation': {
127 'password_reset_confirmation': {
123 'new_password': 'new-password-example',
128 'new_password': 'new-password-example',
124 'user': user,
129 'user': user,
125 'date': datetime.datetime.now(),
130 'date': datetime.datetime.now(),
126 'email': 'test@rhodecode.com',
131 'email': 'test@rhodecode.com',
127 'first_admin_email': User.get_first_super_admin().email
132 'first_admin_email': User.get_first_super_admin().email
128 },
133 },
129
134
130 'registration': {
135 'registration': {
131 'user': user,
136 'user': user,
132 'date': datetime.datetime.now(),
137 'date': datetime.datetime.now(),
133 },
138 },
134
139
135 'pull_request_comment': {
140 'pull_request_comment': {
136 'user': user,
141 'user': user,
137
142
138 'status_change': None,
143 'status_change': None,
139 'status_change_type': None,
144 'status_change_type': None,
140
145
141 'pull_request': pr,
146 'pull_request': pr,
142 'pull_request_commits': [],
147 'pull_request_commits': [],
143
148
144 'pull_request_target_repo': target_repo,
149 'pull_request_target_repo': target_repo,
145 'pull_request_target_repo_url': 'http://target-repo/url',
150 'pull_request_target_repo_url': 'http://target-repo/url',
146
151
147 'pull_request_source_repo': source_repo,
152 'pull_request_source_repo': source_repo,
148 'pull_request_source_repo_url': 'http://source-repo/url',
153 'pull_request_source_repo_url': 'http://source-repo/url',
149
154
150 'pull_request_url': 'http://localhost/pr1',
155 'pull_request_url': 'http://localhost/pr1',
151 'pr_comment_url': 'http://comment-url',
156 'pr_comment_url': 'http://comment-url',
152 'pr_comment_reply_url': 'http://comment-url#reply',
157 'pr_comment_reply_url': 'http://comment-url#reply',
153
158
154 'comment_file': None,
159 'comment_file': None,
155 'comment_line': None,
160 'comment_line': None,
156 'comment_type': 'note',
161 'comment_type': 'note',
157 'comment_body': 'This is my comment body. *I like !*',
162 'comment_body': 'This is my comment body. *I like !*',
158 'comment_id': 2048,
163 'comment_id': 2048,
159 'renderer_type': 'markdown',
164 'renderer_type': 'markdown',
160 'mention': True,
165 'mention': True,
161
166
162 },
167 },
163
168
164 'pull_request_comment+status': {
169 'pull_request_comment+status': {
165 'user': user,
170 'user': user,
166
171
167 'status_change': 'approved',
172 'status_change': 'approved',
168 'status_change_type': 'approved',
173 'status_change_type': 'approved',
169
174
170 'pull_request': pr,
175 'pull_request': pr,
171 'pull_request_commits': [],
176 'pull_request_commits': [],
172
177
173 'pull_request_target_repo': target_repo,
178 'pull_request_target_repo': target_repo,
174 'pull_request_target_repo_url': 'http://target-repo/url',
179 'pull_request_target_repo_url': 'http://target-repo/url',
175
180
176 'pull_request_source_repo': source_repo,
181 'pull_request_source_repo': source_repo,
177 'pull_request_source_repo_url': 'http://source-repo/url',
182 'pull_request_source_repo_url': 'http://source-repo/url',
178
183
179 'pull_request_url': 'http://localhost/pr1',
184 'pull_request_url': 'http://localhost/pr1',
180 'pr_comment_url': 'http://comment-url',
185 'pr_comment_url': 'http://comment-url',
181 'pr_comment_reply_url': 'http://comment-url#reply',
186 'pr_comment_reply_url': 'http://comment-url#reply',
182
187
183 'comment_type': 'todo',
188 'comment_type': 'todo',
184 'comment_file': None,
189 'comment_file': None,
185 'comment_line': None,
190 'comment_line': None,
186 'comment_body': '''
191 'comment_body': '''
187 I think something like this would be better
192 I think something like this would be better
188
193
189 ```py
194 ```py
190 // markdown renderer
195 // markdown renderer
191
196
192 def db():
197 def db():
193 global connection
198 global connection
194 return connection
199 return connection
195
200
196 ```
201 ```
197
202
198 ''',
203 ''',
199 'comment_id': 2048,
204 'comment_id': 2048,
200 'renderer_type': 'markdown',
205 'renderer_type': 'markdown',
201 'mention': True,
206 'mention': True,
202
207
203 },
208 },
204
209
205 'pull_request_comment+file': {
210 'pull_request_comment+file': {
206 'user': user,
211 'user': user,
207
212
208 'status_change': None,
213 'status_change': None,
209 'status_change_type': None,
214 'status_change_type': None,
210
215
211 'pull_request': pr,
216 'pull_request': pr,
212 'pull_request_commits': [],
217 'pull_request_commits': [],
213
218
214 'pull_request_target_repo': target_repo,
219 'pull_request_target_repo': target_repo,
215 'pull_request_target_repo_url': 'http://target-repo/url',
220 'pull_request_target_repo_url': 'http://target-repo/url',
216
221
217 'pull_request_source_repo': source_repo,
222 'pull_request_source_repo': source_repo,
218 'pull_request_source_repo_url': 'http://source-repo/url',
223 'pull_request_source_repo_url': 'http://source-repo/url',
219
224
220 'pull_request_url': 'http://localhost/pr1',
225 'pull_request_url': 'http://localhost/pr1',
221
226
222 'pr_comment_url': 'http://comment-url',
227 'pr_comment_url': 'http://comment-url',
223 'pr_comment_reply_url': 'http://comment-url#reply',
228 'pr_comment_reply_url': 'http://comment-url#reply',
224
229
225 'comment_file': 'rhodecode/model/get_flow_commits',
230 'comment_file': 'rhodecode/model/get_flow_commits',
226 'comment_line': 'o1210',
231 'comment_line': 'o1210',
227 'comment_type': 'todo',
232 'comment_type': 'todo',
228 'comment_body': '''
233 'comment_body': '''
229 I like this !
234 I like this !
230
235
231 But please check this code
236 But please check this code
232
237
233 .. code-block:: javascript
238 .. code-block:: javascript
234
239
235 // THIS IS RST CODE
240 // THIS IS RST CODE
236
241
237 this.createResolutionComment = function(commentId) {
242 this.createResolutionComment = function(commentId) {
238 // hide the trigger text
243 // hide the trigger text
239 $('#resolve-comment-{0}'.format(commentId)).hide();
244 $('#resolve-comment-{0}'.format(commentId)).hide();
240
245
241 var comment = $('#comment-'+commentId);
246 var comment = $('#comment-'+commentId);
242 var commentData = comment.data();
247 var commentData = comment.data();
243 if (commentData.commentInline) {
248 if (commentData.commentInline) {
244 this.createComment(comment, f_path, line_no, commentId)
249 this.createComment(comment, f_path, line_no, commentId)
245 } else {
250 } else {
246 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
251 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
247 }
252 }
248
253
249 return false;
254 return false;
250 };
255 };
251
256
252 This should work better !
257 This should work better !
253 ''',
258 ''',
254 'comment_id': 2048,
259 'comment_id': 2048,
255 'renderer_type': 'rst',
260 'renderer_type': 'rst',
256 'mention': True,
261 'mention': True,
257
262
258 },
263 },
259
264
260 'pull_request_update': {
265 'pull_request_update': {
261 'updating_user': user,
266 'updating_user': user,
262
267
263 'status_change': None,
268 'status_change': None,
264 'status_change_type': None,
269 'status_change_type': None,
265
270
266 'pull_request': pr,
271 'pull_request': pr,
267 'pull_request_commits': [],
272 'pull_request_commits': [],
268
273
269 'pull_request_target_repo': target_repo,
274 'pull_request_target_repo': target_repo,
270 'pull_request_target_repo_url': 'http://target-repo/url',
275 'pull_request_target_repo_url': 'http://target-repo/url',
271
276
272 'pull_request_source_repo': source_repo,
277 'pull_request_source_repo': source_repo,
273 'pull_request_source_repo_url': 'http://source-repo/url',
278 'pull_request_source_repo_url': 'http://source-repo/url',
274
279
275 'pull_request_url': 'http://localhost/pr1',
280 'pull_request_url': 'http://localhost/pr1',
276
281
277 # update comment links
282 # update comment links
278 'pr_comment_url': 'http://comment-url',
283 'pr_comment_url': 'http://comment-url',
279 'pr_comment_reply_url': 'http://comment-url#reply',
284 'pr_comment_reply_url': 'http://comment-url#reply',
280 'ancestor_commit_id': 'f39bd443',
285 'ancestor_commit_id': 'f39bd443',
281 'added_commits': commit_changes.added,
286 'added_commits': commit_changes.added,
282 'removed_commits': commit_changes.removed,
287 'removed_commits': commit_changes.removed,
283 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
288 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
284 'added_files': file_changes.added,
289 'added_files': file_changes.added,
285 'modified_files': file_changes.modified,
290 'modified_files': file_changes.modified,
286 'removed_files': file_changes.removed,
291 'removed_files': file_changes.removed,
287 },
292 },
288
293
289 'cs_comment': {
294 'cs_comment': {
290 'user': user,
295 'user': user,
291 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
296 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
292 'status_change': None,
297 'status_change': None,
293 'status_change_type': None,
298 'status_change_type': None,
294
299
295 'commit_target_repo_url': 'http://foo.example.com/#comment1',
300 'commit_target_repo_url': 'http://foo.example.com/#comment1',
296 'repo_name': 'test-repo',
301 'repo_name': 'test-repo',
297 'comment_type': 'note',
302 'comment_type': 'note',
298 'comment_file': None,
303 'comment_file': None,
299 'comment_line': None,
304 'comment_line': None,
300 'commit_comment_url': 'http://comment-url',
305 'commit_comment_url': 'http://comment-url',
301 'commit_comment_reply_url': 'http://comment-url#reply',
306 'commit_comment_reply_url': 'http://comment-url#reply',
302 'comment_body': 'This is my comment body. *I like !*',
307 'comment_body': 'This is my comment body. *I like !*',
303 'comment_id': 2048,
308 'comment_id': 2048,
304 'renderer_type': 'markdown',
309 'renderer_type': 'markdown',
305 'mention': True,
310 'mention': True,
306 },
311 },
307
312
308 'cs_comment+status': {
313 'cs_comment+status': {
309 'user': user,
314 'user': user,
310 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
315 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
311 'status_change': 'approved',
316 'status_change': 'approved',
312 'status_change_type': 'approved',
317 'status_change_type': 'approved',
313
318
314 'commit_target_repo_url': 'http://foo.example.com/#comment1',
319 'commit_target_repo_url': 'http://foo.example.com/#comment1',
315 'repo_name': 'test-repo',
320 'repo_name': 'test-repo',
316 'comment_type': 'note',
321 'comment_type': 'note',
317 'comment_file': None,
322 'comment_file': None,
318 'comment_line': None,
323 'comment_line': None,
319 'commit_comment_url': 'http://comment-url',
324 'commit_comment_url': 'http://comment-url',
320 'commit_comment_reply_url': 'http://comment-url#reply',
325 'commit_comment_reply_url': 'http://comment-url#reply',
321 'comment_body': '''
326 'comment_body': '''
322 Hello **world**
327 Hello **world**
323
328
324 This is a multiline comment :)
329 This is a multiline comment :)
325
330
326 - list
331 - list
327 - list2
332 - list2
328 ''',
333 ''',
329 'comment_id': 2048,
334 'comment_id': 2048,
330 'renderer_type': 'markdown',
335 'renderer_type': 'markdown',
331 'mention': True,
336 'mention': True,
332 },
337 },
333
338
334 'cs_comment+file': {
339 'cs_comment+file': {
335 'user': user,
340 'user': user,
336 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
341 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
337 'status_change': None,
342 'status_change': None,
338 'status_change_type': None,
343 'status_change_type': None,
339
344
340 'commit_target_repo_url': 'http://foo.example.com/#comment1',
345 'commit_target_repo_url': 'http://foo.example.com/#comment1',
341 'repo_name': 'test-repo',
346 'repo_name': 'test-repo',
342
347
343 'comment_type': 'note',
348 'comment_type': 'note',
344 'comment_file': 'test-file.py',
349 'comment_file': 'test-file.py',
345 'comment_line': 'n100',
350 'comment_line': 'n100',
346
351
347 'commit_comment_url': 'http://comment-url',
352 'commit_comment_url': 'http://comment-url',
348 'commit_comment_reply_url': 'http://comment-url#reply',
353 'commit_comment_reply_url': 'http://comment-url#reply',
349 'comment_body': 'This is my comment body. *I like !*',
354 'comment_body': 'This is my comment body. *I like !*',
350 'comment_id': 2048,
355 'comment_id': 2048,
351 'renderer_type': 'markdown',
356 'renderer_type': 'markdown',
352 'mention': True,
357 'mention': True,
353 },
358 },
354
359
355 'pull_request': {
360 'pull_request': {
356 'user': user,
361 'user': user,
357 'pull_request': pr,
362 'pull_request': pr,
358 'pull_request_commits': [
363 'pull_request_commits': [
359 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
364 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
360 my-account: moved email closer to profile as it's similar data just moved outside.
365 my-account: moved email closer to profile as it's similar data just moved outside.
361 '''),
366 '''),
362 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
367 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
363 users: description edit fixes
368 users: description edit fixes
364
369
365 - tests
370 - tests
366 - added metatags info
371 - added metatags info
367 '''),
372 '''),
368 ],
373 ],
369
374
370 'pull_request_target_repo': target_repo,
375 'pull_request_target_repo': target_repo,
371 'pull_request_target_repo_url': 'http://target-repo/url',
376 'pull_request_target_repo_url': 'http://target-repo/url',
372
377
373 'pull_request_source_repo': source_repo,
378 'pull_request_source_repo': source_repo,
374 'pull_request_source_repo_url': 'http://source-repo/url',
379 'pull_request_source_repo_url': 'http://source-repo/url',
375
380
376 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
381 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
377 'user_role': 'reviewer',
382 'user_role': 'reviewer',
378 },
383 },
379
384
380 'pull_request+reviewer_role': {
385 'pull_request+reviewer_role': {
381 'user': user,
386 'user': user,
382 'pull_request': pr,
387 'pull_request': pr,
383 'pull_request_commits': [
388 'pull_request_commits': [
384 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
389 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
385 my-account: moved email closer to profile as it's similar data just moved outside.
390 my-account: moved email closer to profile as it's similar data just moved outside.
386 '''),
391 '''),
387 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
392 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
388 users: description edit fixes
393 users: description edit fixes
389
394
390 - tests
395 - tests
391 - added metatags info
396 - added metatags info
392 '''),
397 '''),
393 ],
398 ],
394
399
395 'pull_request_target_repo': target_repo,
400 'pull_request_target_repo': target_repo,
396 'pull_request_target_repo_url': 'http://target-repo/url',
401 'pull_request_target_repo_url': 'http://target-repo/url',
397
402
398 'pull_request_source_repo': source_repo,
403 'pull_request_source_repo': source_repo,
399 'pull_request_source_repo_url': 'http://source-repo/url',
404 'pull_request_source_repo_url': 'http://source-repo/url',
400
405
401 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
406 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
402 'user_role': 'reviewer',
407 'user_role': 'reviewer',
403 },
408 },
404
409
405 'pull_request+observer_role': {
410 'pull_request+observer_role': {
406 'user': user,
411 'user': user,
407 'pull_request': pr,
412 'pull_request': pr,
408 'pull_request_commits': [
413 'pull_request_commits': [
409 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
414 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
410 my-account: moved email closer to profile as it's similar data just moved outside.
415 my-account: moved email closer to profile as it's similar data just moved outside.
411 '''),
416 '''),
412 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
417 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
413 users: description edit fixes
418 users: description edit fixes
414
419
415 - tests
420 - tests
416 - added metatags info
421 - added metatags info
417 '''),
422 '''),
418 ],
423 ],
419
424
420 'pull_request_target_repo': target_repo,
425 'pull_request_target_repo': target_repo,
421 'pull_request_target_repo_url': 'http://target-repo/url',
426 'pull_request_target_repo_url': 'http://target-repo/url',
422
427
423 'pull_request_source_repo': source_repo,
428 'pull_request_source_repo': source_repo,
424 'pull_request_source_repo_url': 'http://source-repo/url',
429 'pull_request_source_repo_url': 'http://source-repo/url',
425
430
426 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
431 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
427 'user_role': 'observer'
432 'user_role': 'observer'
428 }
433 }
429 }
434 }
430
435
431 template_type = email_id.split('+')[0]
436 template_type = email_id.split('+')[0]
432 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
437 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
433 template_type, **email_kwargs.get(email_id, {}))
438 template_type, **email_kwargs.get(email_id, {}))
434
439
435 test_email = self.request.GET.get('email')
440 test_email = self.request.GET.get('email')
436 if test_email:
441 if test_email:
437 recipients = [test_email]
442 recipients = [test_email]
438 run_task(tasks.send_email, recipients, c.subject,
443 run_task(tasks.send_email, recipients, c.subject,
439 c.email_body_plaintext, c.email_body)
444 c.email_body_plaintext, c.email_body)
440
445
441 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
446 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
442 template = 'debug_style/email_plain_rendered.mako'
447 template = 'debug_style/email_plain_rendered.mako'
443 else:
448 else:
444 template = 'debug_style/email.mako'
449 template = 'debug_style/email.mako'
445 return render_to_response(
450 return render_to_response(
446 template, self._get_template_context(c),
451 template, self._get_template_context(c),
447 request=self.request)
452 request=self.request)
448
453
449 def template(self):
454 def template(self):
450 t_path = self.request.matchdict['t_path']
455 t_path = self.request.matchdict['t_path']
451 c = self.load_default_context()
456 c = self.load_default_context()
452 c.active = os.path.splitext(t_path)[0]
457 c.active = os.path.splitext(t_path)[0]
453 c.came_from = ''
458 c.came_from = ''
454 # NOTE(marcink): extend the email types with variations based on data sets
459 # NOTE(marcink): extend the email types with variations based on data sets
455 c.email_types = {
460 c.email_types = {
456 'cs_comment+file': {},
461 'cs_comment+file': {},
457 'cs_comment+status': {},
462 'cs_comment+status': {},
458
463
459 'pull_request_comment+file': {},
464 'pull_request_comment+file': {},
460 'pull_request_comment+status': {},
465 'pull_request_comment+status': {},
461
466
462 'pull_request_update': {},
467 'pull_request_update': {},
463
468
464 'pull_request+reviewer_role': {},
469 'pull_request+reviewer_role': {},
465 'pull_request+observer_role': {},
470 'pull_request+observer_role': {},
466 }
471 }
467 c.email_types.update(EmailNotificationModel.email_types)
472 c.email_types.update(EmailNotificationModel.email_types)
468
473
469 return render_to_response(
474 return render_to_response(
470 'debug_style/' + t_path, self._get_template_context(c),
475 'debug_style/' + t_path, self._get_template_context(c),
471 request=self.request)
476 request=self.request)
@@ -1,1658 +1,1679 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 import mock
20 import mock
21 import pytest
21 import pytest
22
22
23 import rhodecode
23 import rhodecode
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
24 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
25 from rhodecode.lib.vcs.nodes import FileNode
25 from rhodecode.lib.vcs.nodes import FileNode
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.model.changeset_status import ChangesetStatusModel
27 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.db import (
28 from rhodecode.model.db import (
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
29 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment, Repository)
30 from rhodecode.model.meta import Session
30 from rhodecode.model.meta import Session
31 from rhodecode.model.pull_request import PullRequestModel
31 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.model.comment import CommentsModel
33 from rhodecode.model.comment import CommentsModel
34 from rhodecode.tests import (
34 from rhodecode.tests import (
35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
35 assert_session_flash, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN)
36
36
37
37
38 def route_path(name, params=None, **kwargs):
38 def route_path(name, params=None, **kwargs):
39 import urllib
39 import urllib
40
40
41 base_url = {
41 base_url = {
42 'repo_changelog': '/{repo_name}/changelog',
42 'repo_changelog': '/{repo_name}/changelog',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
43 'repo_changelog_file': '/{repo_name}/changelog/{commit_id}/{f_path}',
44 'repo_commits': '/{repo_name}/commits',
44 'repo_commits': '/{repo_name}/commits',
45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
45 'repo_commits_file': '/{repo_name}/commits/{commit_id}/{f_path}',
46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
46 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
47 'pullrequest_show_all': '/{repo_name}/pull-request',
47 'pullrequest_show_all': '/{repo_name}/pull-request',
48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
48 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
49 'pullrequest_repo_refs': '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
50 'pullrequest_repo_targets': '/{repo_name}/pull-request/repo-destinations',
51 'pullrequest_new': '/{repo_name}/pull-request/new',
51 'pullrequest_new': '/{repo_name}/pull-request/new',
52 'pullrequest_create': '/{repo_name}/pull-request/create',
52 'pullrequest_create': '/{repo_name}/pull-request/create',
53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
53 'pullrequest_update': '/{repo_name}/pull-request/{pull_request_id}/update',
54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
54 'pullrequest_merge': '/{repo_name}/pull-request/{pull_request_id}/merge',
55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
57 'pullrequest_comment_delete': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/delete',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
58 'pullrequest_comment_edit': '/{repo_name}/pull-request/{pull_request_id}/comment/{comment_id}/edit',
59 }[name].format(**kwargs)
59 }[name].format(**kwargs)
60
60
61 if params:
61 if params:
62 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
62 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
63 return base_url
63 return base_url
64
64
65
65
66 @pytest.mark.usefixtures('app', 'autologin_user')
66 @pytest.mark.usefixtures('app', 'autologin_user')
67 @pytest.mark.backends("git", "hg")
67 @pytest.mark.backends("git", "hg")
68 class TestPullrequestsView(object):
68 class TestPullrequestsView(object):
69
69
70 def test_index(self, backend):
70 def test_index(self, backend):
71 self.app.get(route_path(
71 self.app.get(route_path(
72 'pullrequest_new',
72 'pullrequest_new',
73 repo_name=backend.repo_name))
73 repo_name=backend.repo_name))
74
74
75 def test_option_menu_create_pull_request_exists(self, backend):
75 def test_option_menu_create_pull_request_exists(self, backend):
76 repo_name = backend.repo_name
76 repo_name = backend.repo_name
77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
77 response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
78
78
79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
79 create_pr_link = '<a href="%s">Create Pull Request</a>' % route_path(
80 'pullrequest_new', repo_name=repo_name)
80 'pullrequest_new', repo_name=repo_name)
81 response.mustcontain(create_pr_link)
81 response.mustcontain(create_pr_link)
82
82
83 def test_create_pr_form_with_raw_commit_id(self, backend):
83 def test_create_pr_form_with_raw_commit_id(self, backend):
84 repo = backend.repo
84 repo = backend.repo
85
85
86 self.app.get(
86 self.app.get(
87 route_path('pullrequest_new', repo_name=repo.repo_name,
87 route_path('pullrequest_new', repo_name=repo.repo_name,
88 commit=repo.get_commit().raw_id),
88 commit=repo.get_commit().raw_id),
89 status=200)
89 status=200)
90
90
91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
91 @pytest.mark.parametrize('pr_merge_enabled', [True, False])
92 @pytest.mark.parametrize('range_diff', ["0", "1"])
92 @pytest.mark.parametrize('range_diff', ["0", "1"])
93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
93 def test_show(self, pr_util, pr_merge_enabled, range_diff):
94 pull_request = pr_util.create_pull_request(
94 pull_request = pr_util.create_pull_request(
95 mergeable=pr_merge_enabled, enable_notifications=False)
95 mergeable=pr_merge_enabled, enable_notifications=False)
96
96
97 response = self.app.get(route_path(
97 response = self.app.get(route_path(
98 'pullrequest_show',
98 'pullrequest_show',
99 repo_name=pull_request.target_repo.scm_instance().name,
99 repo_name=pull_request.target_repo.scm_instance().name,
100 pull_request_id=pull_request.pull_request_id,
100 pull_request_id=pull_request.pull_request_id,
101 params={'range-diff': range_diff}))
101 params={'range-diff': range_diff}))
102
102
103 for commit_id in pull_request.revisions:
103 for commit_id in pull_request.revisions:
104 response.mustcontain(commit_id)
104 response.mustcontain(commit_id)
105
105
106 response.mustcontain(pull_request.target_ref_parts.type)
106 response.mustcontain(pull_request.target_ref_parts.type)
107 response.mustcontain(pull_request.target_ref_parts.name)
107 response.mustcontain(pull_request.target_ref_parts.name)
108
108
109 response.mustcontain('class="pull-request-merge"')
109 response.mustcontain('class="pull-request-merge"')
110
110
111 if pr_merge_enabled:
111 if pr_merge_enabled:
112 response.mustcontain('Pull request reviewer approval is pending')
112 response.mustcontain('Pull request reviewer approval is pending')
113 else:
113 else:
114 response.mustcontain('Server-side pull request merging is disabled.')
114 response.mustcontain('Server-side pull request merging is disabled.')
115
115
116 if range_diff == "1":
116 if range_diff == "1":
117 response.mustcontain('Turn off: Show the diff as commit range')
117 response.mustcontain('Turn off: Show the diff as commit range')
118
118
119 def test_show_versions_of_pr(self, backend, csrf_token):
119 def test_show_versions_of_pr(self, backend, csrf_token):
120 commits = [
120 commits = [
121 {'message': 'initial-commit',
121 {'message': 'initial-commit',
122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
123
123
124 {'message': 'commit-1',
124 {'message': 'commit-1',
125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
126 # Above is the initial version of PR that changes a single line
126 # Above is the initial version of PR that changes a single line
127
127
128 # from now on we'll add 3x commit adding a nother line on each step
128 # from now on we'll add 3x commit adding a nother line on each step
129 {'message': 'commit-2',
129 {'message': 'commit-2',
130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
131
131
132 {'message': 'commit-3',
132 {'message': 'commit-3',
133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
134
134
135 {'message': 'commit-4',
135 {'message': 'commit-4',
136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
137 ]
137 ]
138
138
139 commit_ids = backend.create_master_repo(commits)
139 commit_ids = backend.create_master_repo(commits)
140 target = backend.create_repo(heads=['initial-commit'])
140 target = backend.create_repo(heads=['initial-commit'])
141 source = backend.create_repo(heads=['commit-1'])
141 source = backend.create_repo(heads=['commit-1'])
142 source_repo_name = source.repo_name
142 source_repo_name = source.repo_name
143 target_repo_name = target.repo_name
143 target_repo_name = target.repo_name
144
144
145 target_ref = 'branch:{branch}:{commit_id}'.format(
145 target_ref = 'branch:{branch}:{commit_id}'.format(
146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
147 source_ref = 'branch:{branch}:{commit_id}'.format(
147 source_ref = 'branch:{branch}:{commit_id}'.format(
148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
149
149
150 response = self.app.post(
150 response = self.app.post(
151 route_path('pullrequest_create', repo_name=source.repo_name),
151 route_path('pullrequest_create', repo_name=source.repo_name),
152 [
152 [
153 ('source_repo', source_repo_name),
153 ('source_repo', source_repo_name),
154 ('source_ref', source_ref),
154 ('source_ref', source_ref),
155 ('target_repo', target_repo_name),
155 ('target_repo', target_repo_name),
156 ('target_ref', target_ref),
156 ('target_ref', target_ref),
157 ('common_ancestor', commit_ids['initial-commit']),
157 ('common_ancestor', commit_ids['initial-commit']),
158 ('pullrequest_title', 'Title'),
158 ('pullrequest_title', 'Title'),
159 ('pullrequest_desc', 'Description'),
159 ('pullrequest_desc', 'Description'),
160 ('description_renderer', 'markdown'),
160 ('description_renderer', 'markdown'),
161 ('__start__', 'review_members:sequence'),
161 ('__start__', 'review_members:sequence'),
162 ('__start__', 'reviewer:mapping'),
162 ('__start__', 'reviewer:mapping'),
163 ('user_id', '1'),
163 ('user_id', '1'),
164 ('__start__', 'reasons:sequence'),
164 ('__start__', 'reasons:sequence'),
165 ('reason', 'Some reason'),
165 ('reason', 'Some reason'),
166 ('__end__', 'reasons:sequence'),
166 ('__end__', 'reasons:sequence'),
167 ('__start__', 'rules:sequence'),
167 ('__start__', 'rules:sequence'),
168 ('__end__', 'rules:sequence'),
168 ('__end__', 'rules:sequence'),
169 ('mandatory', 'False'),
169 ('mandatory', 'False'),
170 ('__end__', 'reviewer:mapping'),
170 ('__end__', 'reviewer:mapping'),
171 ('__end__', 'review_members:sequence'),
171 ('__end__', 'review_members:sequence'),
172 ('__start__', 'revisions:sequence'),
172 ('__start__', 'revisions:sequence'),
173 ('revisions', commit_ids['commit-1']),
173 ('revisions', commit_ids['commit-1']),
174 ('__end__', 'revisions:sequence'),
174 ('__end__', 'revisions:sequence'),
175 ('user', ''),
175 ('user', ''),
176 ('csrf_token', csrf_token),
176 ('csrf_token', csrf_token),
177 ],
177 ],
178 status=302)
178 status=302)
179
179
180 location = response.headers['Location']
180 location = response.headers['Location']
181
181
182 pull_request_id = location.rsplit('/', 1)[1]
182 pull_request_id = location.rsplit('/', 1)[1]
183 assert pull_request_id != 'new'
183 assert pull_request_id != 'new'
184 pull_request = PullRequest.get(int(pull_request_id))
184 pull_request = PullRequest.get(int(pull_request_id))
185
185
186 pull_request_id = pull_request.pull_request_id
186 pull_request_id = pull_request.pull_request_id
187
187
188 # Show initial version of PR
188 # Show initial version of PR
189 response = self.app.get(
189 response = self.app.get(
190 route_path('pullrequest_show',
190 route_path('pullrequest_show',
191 repo_name=target_repo_name,
191 repo_name=target_repo_name,
192 pull_request_id=pull_request_id))
192 pull_request_id=pull_request_id))
193
193
194 response.mustcontain('commit-1')
194 response.mustcontain('commit-1')
195 response.mustcontain(no=['commit-2'])
195 response.mustcontain(no=['commit-2'])
196 response.mustcontain(no=['commit-3'])
196 response.mustcontain(no=['commit-3'])
197 response.mustcontain(no=['commit-4'])
197 response.mustcontain(no=['commit-4'])
198
198
199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
200 response.mustcontain(no=['LINE3'])
200 response.mustcontain(no=['LINE3'])
201 response.mustcontain(no=['LINE4'])
201 response.mustcontain(no=['LINE4'])
202 response.mustcontain(no=['LINE5'])
202 response.mustcontain(no=['LINE5'])
203
203
204 # update PR #1
204 # update PR #1
205 source_repo = Repository.get_by_repo_name(source_repo_name)
205 source_repo = Repository.get_by_repo_name(source_repo_name)
206 backend.pull_heads(source_repo, heads=['commit-2'])
206 backend.pull_heads(source_repo, heads=['commit-2'])
207 response = self.app.post(
207 response = self.app.post(
208 route_path('pullrequest_update',
208 route_path('pullrequest_update',
209 repo_name=target_repo_name, pull_request_id=pull_request_id),
209 repo_name=target_repo_name, pull_request_id=pull_request_id),
210 params={'update_commits': 'true', 'csrf_token': csrf_token})
210 params={'update_commits': 'true', 'csrf_token': csrf_token})
211
211
212 # update PR #2
212 # update PR #2
213 source_repo = Repository.get_by_repo_name(source_repo_name)
213 source_repo = Repository.get_by_repo_name(source_repo_name)
214 backend.pull_heads(source_repo, heads=['commit-3'])
214 backend.pull_heads(source_repo, heads=['commit-3'])
215 response = self.app.post(
215 response = self.app.post(
216 route_path('pullrequest_update',
216 route_path('pullrequest_update',
217 repo_name=target_repo_name, pull_request_id=pull_request_id),
217 repo_name=target_repo_name, pull_request_id=pull_request_id),
218 params={'update_commits': 'true', 'csrf_token': csrf_token})
218 params={'update_commits': 'true', 'csrf_token': csrf_token})
219
219
220 # update PR #3
220 # update PR #3
221 source_repo = Repository.get_by_repo_name(source_repo_name)
221 source_repo = Repository.get_by_repo_name(source_repo_name)
222 backend.pull_heads(source_repo, heads=['commit-4'])
222 backend.pull_heads(source_repo, heads=['commit-4'])
223 response = self.app.post(
223 response = self.app.post(
224 route_path('pullrequest_update',
224 route_path('pullrequest_update',
225 repo_name=target_repo_name, pull_request_id=pull_request_id),
225 repo_name=target_repo_name, pull_request_id=pull_request_id),
226 params={'update_commits': 'true', 'csrf_token': csrf_token})
226 params={'update_commits': 'true', 'csrf_token': csrf_token})
227
227
228 # Show final version !
228 # Show final version !
229 response = self.app.get(
229 response = self.app.get(
230 route_path('pullrequest_show',
230 route_path('pullrequest_show',
231 repo_name=target_repo_name,
231 repo_name=target_repo_name,
232 pull_request_id=pull_request_id))
232 pull_request_id=pull_request_id))
233
233
234 # 3 updates, and the latest == 4
234 # 3 updates, and the latest == 4
235 response.mustcontain('4 versions available for this pull request')
235 response.mustcontain('4 versions available for this pull request')
236 response.mustcontain(no=['rhodecode diff rendering error'])
236 response.mustcontain(no=['rhodecode diff rendering error'])
237
237
238 # initial show must have 3 commits, and 3 adds
238 # initial show must have 3 commits, and 3 adds
239 response.mustcontain('commit-1')
239 response.mustcontain('commit-1')
240 response.mustcontain('commit-2')
240 response.mustcontain('commit-2')
241 response.mustcontain('commit-3')
241 response.mustcontain('commit-3')
242 response.mustcontain('commit-4')
242 response.mustcontain('commit-4')
243
243
244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
248
248
249 # fetch versions
249 # fetch versions
250 pr = PullRequest.get(pull_request_id)
250 pr = PullRequest.get(pull_request_id)
251 versions = [x.pull_request_version_id for x in pr.versions.all()]
251 versions = [x.pull_request_version_id for x in pr.versions.all()]
252 assert len(versions) == 3
252 assert len(versions) == 3
253
253
254 # show v1,v2,v3,v4
254 # show v1,v2,v3,v4
255 def cb_line(text):
255 def cb_line(text):
256 return 'cb-addition"></span><span>{}</span>'.format(text)
256 return 'cb-addition"></span><span>{}</span>'.format(text)
257
257
258 def cb_context(text):
258 def cb_context(text):
259 return '<span class="cb-code"><span class="cb-action cb-context">' \
259 return '<span class="cb-code"><span class="cb-action cb-context">' \
260 '</span><span>{}</span></span>'.format(text)
260 '</span><span>{}</span></span>'.format(text)
261
261
262 commit_tests = {
262 commit_tests = {
263 # in response, not in response
263 # in response, not in response
264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
268 }
268 }
269 diff_tests = {
269 diff_tests = {
270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
274 }
274 }
275 for idx, ver in enumerate(versions, 1):
275 for idx, ver in enumerate(versions, 1):
276
276
277 response = self.app.get(
277 response = self.app.get(
278 route_path('pullrequest_show',
278 route_path('pullrequest_show',
279 repo_name=target_repo_name,
279 repo_name=target_repo_name,
280 pull_request_id=pull_request_id,
280 pull_request_id=pull_request_id,
281 params={'version': ver}))
281 params={'version': ver}))
282
282
283 response.mustcontain(no=['rhodecode diff rendering error'])
283 response.mustcontain(no=['rhodecode diff rendering error'])
284 response.mustcontain('Showing changes at v{}'.format(idx))
284 response.mustcontain('Showing changes at v{}'.format(idx))
285
285
286 yes, no = commit_tests[idx]
286 yes, no = commit_tests[idx]
287 for y in yes:
287 for y in yes:
288 response.mustcontain(y)
288 response.mustcontain(y)
289 for n in no:
289 for n in no:
290 response.mustcontain(no=n)
290 response.mustcontain(no=n)
291
291
292 yes, no = diff_tests[idx]
292 yes, no = diff_tests[idx]
293 for y in yes:
293 for y in yes:
294 response.mustcontain(cb_line(y))
294 response.mustcontain(cb_line(y))
295 for n in no:
295 for n in no:
296 response.mustcontain(no=n)
296 response.mustcontain(no=n)
297
297
298 # show diff between versions
298 # show diff between versions
299 diff_compare_tests = {
299 diff_compare_tests = {
300 1: (['LINE3'], ['LINE1', 'LINE2']),
300 1: (['LINE3'], ['LINE1', 'LINE2']),
301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
303 }
303 }
304 for idx, ver in enumerate(versions, 1):
304 for idx, ver in enumerate(versions, 1):
305 adds, context = diff_compare_tests[idx]
305 adds, context = diff_compare_tests[idx]
306
306
307 to_ver = ver+1
307 to_ver = ver+1
308 if idx == 3:
308 if idx == 3:
309 to_ver = 'latest'
309 to_ver = 'latest'
310
310
311 response = self.app.get(
311 response = self.app.get(
312 route_path('pullrequest_show',
312 route_path('pullrequest_show',
313 repo_name=target_repo_name,
313 repo_name=target_repo_name,
314 pull_request_id=pull_request_id,
314 pull_request_id=pull_request_id,
315 params={'from_version': versions[0], 'version': to_ver}))
315 params={'from_version': versions[0], 'version': to_ver}))
316
316
317 response.mustcontain(no=['rhodecode diff rendering error'])
317 response.mustcontain(no=['rhodecode diff rendering error'])
318
318
319 for a in adds:
319 for a in adds:
320 response.mustcontain(cb_line(a))
320 response.mustcontain(cb_line(a))
321 for c in context:
321 for c in context:
322 response.mustcontain(cb_context(c))
322 response.mustcontain(cb_context(c))
323
323
324 # test version v2 -> v3
324 # test version v2 -> v3
325 response = self.app.get(
325 response = self.app.get(
326 route_path('pullrequest_show',
326 route_path('pullrequest_show',
327 repo_name=target_repo_name,
327 repo_name=target_repo_name,
328 pull_request_id=pull_request_id,
328 pull_request_id=pull_request_id,
329 params={'from_version': versions[1], 'version': versions[2]}))
329 params={'from_version': versions[1], 'version': versions[2]}))
330
330
331 response.mustcontain(cb_context('LINE1'))
331 response.mustcontain(cb_context('LINE1'))
332 response.mustcontain(cb_context('LINE2'))
332 response.mustcontain(cb_context('LINE2'))
333 response.mustcontain(cb_context('LINE3'))
333 response.mustcontain(cb_context('LINE3'))
334 response.mustcontain(cb_line('LINE4'))
334 response.mustcontain(cb_line('LINE4'))
335
335
336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
337 # Logout
337 # Logout
338 response = self.app.post(
338 response = self.app.post(
339 h.route_path('logout'),
339 h.route_path('logout'),
340 params={'csrf_token': csrf_token})
340 params={'csrf_token': csrf_token})
341 # Login as regular user
341 # Login as regular user
342 response = self.app.post(h.route_path('login'),
342 response = self.app.post(h.route_path('login'),
343 {'username': TEST_USER_REGULAR_LOGIN,
343 {'username': TEST_USER_REGULAR_LOGIN,
344 'password': 'test12'})
344 'password': 'test12'})
345
345
346 pull_request = pr_util.create_pull_request(
346 pull_request = pr_util.create_pull_request(
347 author=TEST_USER_REGULAR_LOGIN)
347 author=TEST_USER_REGULAR_LOGIN)
348
348
349 response = self.app.get(route_path(
349 response = self.app.get(route_path(
350 'pullrequest_show',
350 'pullrequest_show',
351 repo_name=pull_request.target_repo.scm_instance().name,
351 repo_name=pull_request.target_repo.scm_instance().name,
352 pull_request_id=pull_request.pull_request_id))
352 pull_request_id=pull_request.pull_request_id))
353
353
354 response.mustcontain('Server-side pull request merging is disabled.')
354 response.mustcontain('Server-side pull request merging is disabled.')
355
355
356 assert_response = response.assert_response()
356 assert_response = response.assert_response()
357 # for regular user without a merge permissions, we don't see it
357 # for regular user without a merge permissions, we don't see it
358 assert_response.no_element_exists('#close-pull-request-action')
358 assert_response.no_element_exists('#close-pull-request-action')
359
359
360 user_util.grant_user_permission_to_repo(
360 user_util.grant_user_permission_to_repo(
361 pull_request.target_repo,
361 pull_request.target_repo,
362 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
362 UserModel().get_by_username(TEST_USER_REGULAR_LOGIN),
363 'repository.write')
363 'repository.write')
364 response = self.app.get(route_path(
364 response = self.app.get(route_path(
365 'pullrequest_show',
365 'pullrequest_show',
366 repo_name=pull_request.target_repo.scm_instance().name,
366 repo_name=pull_request.target_repo.scm_instance().name,
367 pull_request_id=pull_request.pull_request_id))
367 pull_request_id=pull_request.pull_request_id))
368
368
369 response.mustcontain('Server-side pull request merging is disabled.')
369 response.mustcontain('Server-side pull request merging is disabled.')
370
370
371 assert_response = response.assert_response()
371 assert_response = response.assert_response()
372 # now regular user has a merge permissions, we have CLOSE button
372 # now regular user has a merge permissions, we have CLOSE button
373 assert_response.one_element_exists('#close-pull-request-action')
373 assert_response.one_element_exists('#close-pull-request-action')
374
374
375 def test_show_invalid_commit_id(self, pr_util):
375 def test_show_invalid_commit_id(self, pr_util):
376 # Simulating invalid revisions which will cause a lookup error
376 # Simulating invalid revisions which will cause a lookup error
377 pull_request = pr_util.create_pull_request()
377 pull_request = pr_util.create_pull_request()
378 pull_request.revisions = ['invalid']
378 pull_request.revisions = ['invalid']
379 Session().add(pull_request)
379 Session().add(pull_request)
380 Session().commit()
380 Session().commit()
381
381
382 response = self.app.get(route_path(
382 response = self.app.get(route_path(
383 'pullrequest_show',
383 'pullrequest_show',
384 repo_name=pull_request.target_repo.scm_instance().name,
384 repo_name=pull_request.target_repo.scm_instance().name,
385 pull_request_id=pull_request.pull_request_id))
385 pull_request_id=pull_request.pull_request_id))
386
386
387 for commit_id in pull_request.revisions:
387 for commit_id in pull_request.revisions:
388 response.mustcontain(commit_id)
388 response.mustcontain(commit_id)
389
389
390 def test_show_invalid_source_reference(self, pr_util):
390 def test_show_invalid_source_reference(self, pr_util):
391 pull_request = pr_util.create_pull_request()
391 pull_request = pr_util.create_pull_request()
392 pull_request.source_ref = 'branch:b:invalid'
392 pull_request.source_ref = 'branch:b:invalid'
393 Session().add(pull_request)
393 Session().add(pull_request)
394 Session().commit()
394 Session().commit()
395
395
396 self.app.get(route_path(
396 self.app.get(route_path(
397 'pullrequest_show',
397 'pullrequest_show',
398 repo_name=pull_request.target_repo.scm_instance().name,
398 repo_name=pull_request.target_repo.scm_instance().name,
399 pull_request_id=pull_request.pull_request_id))
399 pull_request_id=pull_request.pull_request_id))
400
400
401 def test_edit_title_description(self, pr_util, csrf_token):
401 def test_edit_title_description(self, pr_util, csrf_token):
402 pull_request = pr_util.create_pull_request()
402 pull_request = pr_util.create_pull_request()
403 pull_request_id = pull_request.pull_request_id
403 pull_request_id = pull_request.pull_request_id
404
404
405 response = self.app.post(
405 response = self.app.post(
406 route_path('pullrequest_update',
406 route_path('pullrequest_update',
407 repo_name=pull_request.target_repo.repo_name,
407 repo_name=pull_request.target_repo.repo_name,
408 pull_request_id=pull_request_id),
408 pull_request_id=pull_request_id),
409 params={
409 params={
410 'edit_pull_request': 'true',
410 'edit_pull_request': 'true',
411 'title': 'New title',
411 'title': 'New title',
412 'description': 'New description',
412 'description': 'New description',
413 'csrf_token': csrf_token})
413 'csrf_token': csrf_token})
414
414
415 assert_session_flash(
415 assert_session_flash(
416 response, u'Pull request title & description updated.',
416 response, u'Pull request title & description updated.',
417 category='success')
417 category='success')
418
418
419 pull_request = PullRequest.get(pull_request_id)
419 pull_request = PullRequest.get(pull_request_id)
420 assert pull_request.title == 'New title'
420 assert pull_request.title == 'New title'
421 assert pull_request.description == 'New description'
421 assert pull_request.description == 'New description'
422
422
423 def test_edit_title_description(self, pr_util, csrf_token):
424 pull_request = pr_util.create_pull_request()
425 pull_request_id = pull_request.pull_request_id
426
427 response = self.app.post(
428 route_path('pullrequest_update',
429 repo_name=pull_request.target_repo.repo_name,
430 pull_request_id=pull_request_id),
431 params={
432 'edit_pull_request': 'true',
433 'title': 'New title {} {2} {foo}',
434 'description': 'New description',
435 'csrf_token': csrf_token})
436
437 assert_session_flash(
438 response, u'Pull request title & description updated.',
439 category='success')
440
441 pull_request = PullRequest.get(pull_request_id)
442 assert pull_request.title_safe == 'New title {{}} {{2}} {{foo}}'
443
423 def test_edit_title_description_closed(self, pr_util, csrf_token):
444 def test_edit_title_description_closed(self, pr_util, csrf_token):
424 pull_request = pr_util.create_pull_request()
445 pull_request = pr_util.create_pull_request()
425 pull_request_id = pull_request.pull_request_id
446 pull_request_id = pull_request.pull_request_id
426 repo_name = pull_request.target_repo.repo_name
447 repo_name = pull_request.target_repo.repo_name
427 pr_util.close()
448 pr_util.close()
428
449
429 response = self.app.post(
450 response = self.app.post(
430 route_path('pullrequest_update',
451 route_path('pullrequest_update',
431 repo_name=repo_name, pull_request_id=pull_request_id),
452 repo_name=repo_name, pull_request_id=pull_request_id),
432 params={
453 params={
433 'edit_pull_request': 'true',
454 'edit_pull_request': 'true',
434 'title': 'New title',
455 'title': 'New title',
435 'description': 'New description',
456 'description': 'New description',
436 'csrf_token': csrf_token}, status=200)
457 'csrf_token': csrf_token}, status=200)
437 assert_session_flash(
458 assert_session_flash(
438 response, u'Cannot update closed pull requests.',
459 response, u'Cannot update closed pull requests.',
439 category='error')
460 category='error')
440
461
441 def test_update_invalid_source_reference(self, pr_util, csrf_token):
462 def test_update_invalid_source_reference(self, pr_util, csrf_token):
442 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
463 from rhodecode.lib.vcs.backends.base import UpdateFailureReason
443
464
444 pull_request = pr_util.create_pull_request()
465 pull_request = pr_util.create_pull_request()
445 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
466 pull_request.source_ref = 'branch:invalid-branch:invalid-commit-id'
446 Session().add(pull_request)
467 Session().add(pull_request)
447 Session().commit()
468 Session().commit()
448
469
449 pull_request_id = pull_request.pull_request_id
470 pull_request_id = pull_request.pull_request_id
450
471
451 response = self.app.post(
472 response = self.app.post(
452 route_path('pullrequest_update',
473 route_path('pullrequest_update',
453 repo_name=pull_request.target_repo.repo_name,
474 repo_name=pull_request.target_repo.repo_name,
454 pull_request_id=pull_request_id),
475 pull_request_id=pull_request_id),
455 params={'update_commits': 'true', 'csrf_token': csrf_token})
476 params={'update_commits': 'true', 'csrf_token': csrf_token})
456
477
457 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
478 expected_msg = str(PullRequestModel.UPDATE_STATUS_MESSAGES[
458 UpdateFailureReason.MISSING_SOURCE_REF])
479 UpdateFailureReason.MISSING_SOURCE_REF])
459 assert_session_flash(response, expected_msg, category='error')
480 assert_session_flash(response, expected_msg, category='error')
460
481
461 def test_missing_target_reference(self, pr_util, csrf_token):
482 def test_missing_target_reference(self, pr_util, csrf_token):
462 from rhodecode.lib.vcs.backends.base import MergeFailureReason
483 from rhodecode.lib.vcs.backends.base import MergeFailureReason
463 pull_request = pr_util.create_pull_request(
484 pull_request = pr_util.create_pull_request(
464 approved=True, mergeable=True)
485 approved=True, mergeable=True)
465 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
486 unicode_reference = u'branch:invalid-branch:invalid-commit-id'
466 pull_request.target_ref = unicode_reference
487 pull_request.target_ref = unicode_reference
467 Session().add(pull_request)
488 Session().add(pull_request)
468 Session().commit()
489 Session().commit()
469
490
470 pull_request_id = pull_request.pull_request_id
491 pull_request_id = pull_request.pull_request_id
471 pull_request_url = route_path(
492 pull_request_url = route_path(
472 'pullrequest_show',
493 'pullrequest_show',
473 repo_name=pull_request.target_repo.repo_name,
494 repo_name=pull_request.target_repo.repo_name,
474 pull_request_id=pull_request_id)
495 pull_request_id=pull_request_id)
475
496
476 response = self.app.get(pull_request_url)
497 response = self.app.get(pull_request_url)
477 target_ref_id = 'invalid-branch'
498 target_ref_id = 'invalid-branch'
478 merge_resp = MergeResponse(
499 merge_resp = MergeResponse(
479 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
500 True, True, '', MergeFailureReason.MISSING_TARGET_REF,
480 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
501 metadata={'target_ref': PullRequest.unicode_to_reference(unicode_reference)})
481 response.assert_response().element_contains(
502 response.assert_response().element_contains(
482 'div[data-role="merge-message"]', merge_resp.merge_status_message)
503 'div[data-role="merge-message"]', merge_resp.merge_status_message)
483
504
484 def test_comment_and_close_pull_request_custom_message_approved(
505 def test_comment_and_close_pull_request_custom_message_approved(
485 self, pr_util, csrf_token, xhr_header):
506 self, pr_util, csrf_token, xhr_header):
486
507
487 pull_request = pr_util.create_pull_request(approved=True)
508 pull_request = pr_util.create_pull_request(approved=True)
488 pull_request_id = pull_request.pull_request_id
509 pull_request_id = pull_request.pull_request_id
489 author = pull_request.user_id
510 author = pull_request.user_id
490 repo = pull_request.target_repo.repo_id
511 repo = pull_request.target_repo.repo_id
491
512
492 self.app.post(
513 self.app.post(
493 route_path('pullrequest_comment_create',
514 route_path('pullrequest_comment_create',
494 repo_name=pull_request.target_repo.scm_instance().name,
515 repo_name=pull_request.target_repo.scm_instance().name,
495 pull_request_id=pull_request_id),
516 pull_request_id=pull_request_id),
496 params={
517 params={
497 'close_pull_request': '1',
518 'close_pull_request': '1',
498 'text': 'Closing a PR',
519 'text': 'Closing a PR',
499 'csrf_token': csrf_token},
520 'csrf_token': csrf_token},
500 extra_environ=xhr_header,)
521 extra_environ=xhr_header,)
501
522
502 journal = UserLog.query()\
523 journal = UserLog.query()\
503 .filter(UserLog.user_id == author)\
524 .filter(UserLog.user_id == author)\
504 .filter(UserLog.repository_id == repo) \
525 .filter(UserLog.repository_id == repo) \
505 .order_by(UserLog.user_log_id.asc()) \
526 .order_by(UserLog.user_log_id.asc()) \
506 .all()
527 .all()
507 assert journal[-1].action == 'repo.pull_request.close'
528 assert journal[-1].action == 'repo.pull_request.close'
508
529
509 pull_request = PullRequest.get(pull_request_id)
530 pull_request = PullRequest.get(pull_request_id)
510 assert pull_request.is_closed()
531 assert pull_request.is_closed()
511
532
512 status = ChangesetStatusModel().get_status(
533 status = ChangesetStatusModel().get_status(
513 pull_request.source_repo, pull_request=pull_request)
534 pull_request.source_repo, pull_request=pull_request)
514 assert status == ChangesetStatus.STATUS_APPROVED
535 assert status == ChangesetStatus.STATUS_APPROVED
515 comments = ChangesetComment().query() \
536 comments = ChangesetComment().query() \
516 .filter(ChangesetComment.pull_request == pull_request) \
537 .filter(ChangesetComment.pull_request == pull_request) \
517 .order_by(ChangesetComment.comment_id.asc())\
538 .order_by(ChangesetComment.comment_id.asc())\
518 .all()
539 .all()
519 assert comments[-1].text == 'Closing a PR'
540 assert comments[-1].text == 'Closing a PR'
520
541
521 def test_comment_force_close_pull_request_rejected(
542 def test_comment_force_close_pull_request_rejected(
522 self, pr_util, csrf_token, xhr_header):
543 self, pr_util, csrf_token, xhr_header):
523 pull_request = pr_util.create_pull_request()
544 pull_request = pr_util.create_pull_request()
524 pull_request_id = pull_request.pull_request_id
545 pull_request_id = pull_request.pull_request_id
525 PullRequestModel().update_reviewers(
546 PullRequestModel().update_reviewers(
526 pull_request_id, [
547 pull_request_id, [
527 (1, ['reason'], False, 'reviewer', []),
548 (1, ['reason'], False, 'reviewer', []),
528 (2, ['reason2'], False, 'reviewer', [])],
549 (2, ['reason2'], False, 'reviewer', [])],
529 pull_request.author)
550 pull_request.author)
530 author = pull_request.user_id
551 author = pull_request.user_id
531 repo = pull_request.target_repo.repo_id
552 repo = pull_request.target_repo.repo_id
532
553
533 self.app.post(
554 self.app.post(
534 route_path('pullrequest_comment_create',
555 route_path('pullrequest_comment_create',
535 repo_name=pull_request.target_repo.scm_instance().name,
556 repo_name=pull_request.target_repo.scm_instance().name,
536 pull_request_id=pull_request_id),
557 pull_request_id=pull_request_id),
537 params={
558 params={
538 'close_pull_request': '1',
559 'close_pull_request': '1',
539 'csrf_token': csrf_token},
560 'csrf_token': csrf_token},
540 extra_environ=xhr_header)
561 extra_environ=xhr_header)
541
562
542 pull_request = PullRequest.get(pull_request_id)
563 pull_request = PullRequest.get(pull_request_id)
543
564
544 journal = UserLog.query()\
565 journal = UserLog.query()\
545 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
566 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
546 .order_by(UserLog.user_log_id.asc()) \
567 .order_by(UserLog.user_log_id.asc()) \
547 .all()
568 .all()
548 assert journal[-1].action == 'repo.pull_request.close'
569 assert journal[-1].action == 'repo.pull_request.close'
549
570
550 # check only the latest status, not the review status
571 # check only the latest status, not the review status
551 status = ChangesetStatusModel().get_status(
572 status = ChangesetStatusModel().get_status(
552 pull_request.source_repo, pull_request=pull_request)
573 pull_request.source_repo, pull_request=pull_request)
553 assert status == ChangesetStatus.STATUS_REJECTED
574 assert status == ChangesetStatus.STATUS_REJECTED
554
575
555 def test_comment_and_close_pull_request(
576 def test_comment_and_close_pull_request(
556 self, pr_util, csrf_token, xhr_header):
577 self, pr_util, csrf_token, xhr_header):
557 pull_request = pr_util.create_pull_request()
578 pull_request = pr_util.create_pull_request()
558 pull_request_id = pull_request.pull_request_id
579 pull_request_id = pull_request.pull_request_id
559
580
560 response = self.app.post(
581 response = self.app.post(
561 route_path('pullrequest_comment_create',
582 route_path('pullrequest_comment_create',
562 repo_name=pull_request.target_repo.scm_instance().name,
583 repo_name=pull_request.target_repo.scm_instance().name,
563 pull_request_id=pull_request.pull_request_id),
584 pull_request_id=pull_request.pull_request_id),
564 params={
585 params={
565 'close_pull_request': 'true',
586 'close_pull_request': 'true',
566 'csrf_token': csrf_token},
587 'csrf_token': csrf_token},
567 extra_environ=xhr_header)
588 extra_environ=xhr_header)
568
589
569 assert response.json
590 assert response.json
570
591
571 pull_request = PullRequest.get(pull_request_id)
592 pull_request = PullRequest.get(pull_request_id)
572 assert pull_request.is_closed()
593 assert pull_request.is_closed()
573
594
574 # check only the latest status, not the review status
595 # check only the latest status, not the review status
575 status = ChangesetStatusModel().get_status(
596 status = ChangesetStatusModel().get_status(
576 pull_request.source_repo, pull_request=pull_request)
597 pull_request.source_repo, pull_request=pull_request)
577 assert status == ChangesetStatus.STATUS_REJECTED
598 assert status == ChangesetStatus.STATUS_REJECTED
578
599
579 def test_comment_and_close_pull_request_try_edit_comment(
600 def test_comment_and_close_pull_request_try_edit_comment(
580 self, pr_util, csrf_token, xhr_header
601 self, pr_util, csrf_token, xhr_header
581 ):
602 ):
582 pull_request = pr_util.create_pull_request()
603 pull_request = pr_util.create_pull_request()
583 pull_request_id = pull_request.pull_request_id
604 pull_request_id = pull_request.pull_request_id
584 target_scm = pull_request.target_repo.scm_instance()
605 target_scm = pull_request.target_repo.scm_instance()
585 target_scm_name = target_scm.name
606 target_scm_name = target_scm.name
586
607
587 response = self.app.post(
608 response = self.app.post(
588 route_path(
609 route_path(
589 'pullrequest_comment_create',
610 'pullrequest_comment_create',
590 repo_name=target_scm_name,
611 repo_name=target_scm_name,
591 pull_request_id=pull_request_id,
612 pull_request_id=pull_request_id,
592 ),
613 ),
593 params={
614 params={
594 'close_pull_request': 'true',
615 'close_pull_request': 'true',
595 'csrf_token': csrf_token,
616 'csrf_token': csrf_token,
596 },
617 },
597 extra_environ=xhr_header)
618 extra_environ=xhr_header)
598
619
599 assert response.json
620 assert response.json
600
621
601 pull_request = PullRequest.get(pull_request_id)
622 pull_request = PullRequest.get(pull_request_id)
602 target_scm = pull_request.target_repo.scm_instance()
623 target_scm = pull_request.target_repo.scm_instance()
603 target_scm_name = target_scm.name
624 target_scm_name = target_scm.name
604 assert pull_request.is_closed()
625 assert pull_request.is_closed()
605
626
606 # check only the latest status, not the review status
627 # check only the latest status, not the review status
607 status = ChangesetStatusModel().get_status(
628 status = ChangesetStatusModel().get_status(
608 pull_request.source_repo, pull_request=pull_request)
629 pull_request.source_repo, pull_request=pull_request)
609 assert status == ChangesetStatus.STATUS_REJECTED
630 assert status == ChangesetStatus.STATUS_REJECTED
610
631
611 for comment_id in response.json.keys():
632 for comment_id in response.json.keys():
612 test_text = 'test'
633 test_text = 'test'
613 response = self.app.post(
634 response = self.app.post(
614 route_path(
635 route_path(
615 'pullrequest_comment_edit',
636 'pullrequest_comment_edit',
616 repo_name=target_scm_name,
637 repo_name=target_scm_name,
617 pull_request_id=pull_request_id,
638 pull_request_id=pull_request_id,
618 comment_id=comment_id,
639 comment_id=comment_id,
619 ),
640 ),
620 extra_environ=xhr_header,
641 extra_environ=xhr_header,
621 params={
642 params={
622 'csrf_token': csrf_token,
643 'csrf_token': csrf_token,
623 'text': test_text,
644 'text': test_text,
624 },
645 },
625 status=403,
646 status=403,
626 )
647 )
627 assert response.status_int == 403
648 assert response.status_int == 403
628
649
629 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
650 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
630 pull_request = pr_util.create_pull_request()
651 pull_request = pr_util.create_pull_request()
631 target_scm = pull_request.target_repo.scm_instance()
652 target_scm = pull_request.target_repo.scm_instance()
632 target_scm_name = target_scm.name
653 target_scm_name = target_scm.name
633
654
634 response = self.app.post(
655 response = self.app.post(
635 route_path(
656 route_path(
636 'pullrequest_comment_create',
657 'pullrequest_comment_create',
637 repo_name=target_scm_name,
658 repo_name=target_scm_name,
638 pull_request_id=pull_request.pull_request_id),
659 pull_request_id=pull_request.pull_request_id),
639 params={
660 params={
640 'csrf_token': csrf_token,
661 'csrf_token': csrf_token,
641 'text': 'init',
662 'text': 'init',
642 },
663 },
643 extra_environ=xhr_header,
664 extra_environ=xhr_header,
644 )
665 )
645 assert response.json
666 assert response.json
646
667
647 for comment_id in response.json.keys():
668 for comment_id in response.json.keys():
648 assert comment_id
669 assert comment_id
649 test_text = 'test'
670 test_text = 'test'
650 self.app.post(
671 self.app.post(
651 route_path(
672 route_path(
652 'pullrequest_comment_edit',
673 'pullrequest_comment_edit',
653 repo_name=target_scm_name,
674 repo_name=target_scm_name,
654 pull_request_id=pull_request.pull_request_id,
675 pull_request_id=pull_request.pull_request_id,
655 comment_id=comment_id,
676 comment_id=comment_id,
656 ),
677 ),
657 extra_environ=xhr_header,
678 extra_environ=xhr_header,
658 params={
679 params={
659 'csrf_token': csrf_token,
680 'csrf_token': csrf_token,
660 'text': test_text,
681 'text': test_text,
661 'version': '0',
682 'version': '0',
662 },
683 },
663
684
664 )
685 )
665 text_form_db = ChangesetComment.query().filter(
686 text_form_db = ChangesetComment.query().filter(
666 ChangesetComment.comment_id == comment_id).first().text
687 ChangesetComment.comment_id == comment_id).first().text
667 assert test_text == text_form_db
688 assert test_text == text_form_db
668
689
669 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
690 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
670 pull_request = pr_util.create_pull_request()
691 pull_request = pr_util.create_pull_request()
671 target_scm = pull_request.target_repo.scm_instance()
692 target_scm = pull_request.target_repo.scm_instance()
672 target_scm_name = target_scm.name
693 target_scm_name = target_scm.name
673
694
674 response = self.app.post(
695 response = self.app.post(
675 route_path(
696 route_path(
676 'pullrequest_comment_create',
697 'pullrequest_comment_create',
677 repo_name=target_scm_name,
698 repo_name=target_scm_name,
678 pull_request_id=pull_request.pull_request_id),
699 pull_request_id=pull_request.pull_request_id),
679 params={
700 params={
680 'csrf_token': csrf_token,
701 'csrf_token': csrf_token,
681 'text': 'init',
702 'text': 'init',
682 },
703 },
683 extra_environ=xhr_header,
704 extra_environ=xhr_header,
684 )
705 )
685 assert response.json
706 assert response.json
686
707
687 for comment_id in response.json.keys():
708 for comment_id in response.json.keys():
688 test_text = 'init'
709 test_text = 'init'
689 response = self.app.post(
710 response = self.app.post(
690 route_path(
711 route_path(
691 'pullrequest_comment_edit',
712 'pullrequest_comment_edit',
692 repo_name=target_scm_name,
713 repo_name=target_scm_name,
693 pull_request_id=pull_request.pull_request_id,
714 pull_request_id=pull_request.pull_request_id,
694 comment_id=comment_id,
715 comment_id=comment_id,
695 ),
716 ),
696 extra_environ=xhr_header,
717 extra_environ=xhr_header,
697 params={
718 params={
698 'csrf_token': csrf_token,
719 'csrf_token': csrf_token,
699 'text': test_text,
720 'text': test_text,
700 'version': '0',
721 'version': '0',
701 },
722 },
702 status=404,
723 status=404,
703
724
704 )
725 )
705 assert response.status_int == 404
726 assert response.status_int == 404
706
727
707 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
728 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
708 pull_request = pr_util.create_pull_request()
729 pull_request = pr_util.create_pull_request()
709 target_scm = pull_request.target_repo.scm_instance()
730 target_scm = pull_request.target_repo.scm_instance()
710 target_scm_name = target_scm.name
731 target_scm_name = target_scm.name
711
732
712 response = self.app.post(
733 response = self.app.post(
713 route_path(
734 route_path(
714 'pullrequest_comment_create',
735 'pullrequest_comment_create',
715 repo_name=target_scm_name,
736 repo_name=target_scm_name,
716 pull_request_id=pull_request.pull_request_id),
737 pull_request_id=pull_request.pull_request_id),
717 params={
738 params={
718 'csrf_token': csrf_token,
739 'csrf_token': csrf_token,
719 'text': 'init',
740 'text': 'init',
720 },
741 },
721 extra_environ=xhr_header,
742 extra_environ=xhr_header,
722 )
743 )
723 assert response.json
744 assert response.json
724 for comment_id in response.json.keys():
745 for comment_id in response.json.keys():
725 test_text = 'test'
746 test_text = 'test'
726 self.app.post(
747 self.app.post(
727 route_path(
748 route_path(
728 'pullrequest_comment_edit',
749 'pullrequest_comment_edit',
729 repo_name=target_scm_name,
750 repo_name=target_scm_name,
730 pull_request_id=pull_request.pull_request_id,
751 pull_request_id=pull_request.pull_request_id,
731 comment_id=comment_id,
752 comment_id=comment_id,
732 ),
753 ),
733 extra_environ=xhr_header,
754 extra_environ=xhr_header,
734 params={
755 params={
735 'csrf_token': csrf_token,
756 'csrf_token': csrf_token,
736 'text': test_text,
757 'text': test_text,
737 'version': '0',
758 'version': '0',
738 },
759 },
739
760
740 )
761 )
741 test_text_v2 = 'test_v2'
762 test_text_v2 = 'test_v2'
742 response = self.app.post(
763 response = self.app.post(
743 route_path(
764 route_path(
744 'pullrequest_comment_edit',
765 'pullrequest_comment_edit',
745 repo_name=target_scm_name,
766 repo_name=target_scm_name,
746 pull_request_id=pull_request.pull_request_id,
767 pull_request_id=pull_request.pull_request_id,
747 comment_id=comment_id,
768 comment_id=comment_id,
748 ),
769 ),
749 extra_environ=xhr_header,
770 extra_environ=xhr_header,
750 params={
771 params={
751 'csrf_token': csrf_token,
772 'csrf_token': csrf_token,
752 'text': test_text_v2,
773 'text': test_text_v2,
753 'version': '0',
774 'version': '0',
754 },
775 },
755 status=409,
776 status=409,
756 )
777 )
757 assert response.status_int == 409
778 assert response.status_int == 409
758
779
759 text_form_db = ChangesetComment.query().filter(
780 text_form_db = ChangesetComment.query().filter(
760 ChangesetComment.comment_id == comment_id).first().text
781 ChangesetComment.comment_id == comment_id).first().text
761
782
762 assert test_text == text_form_db
783 assert test_text == text_form_db
763 assert test_text_v2 != text_form_db
784 assert test_text_v2 != text_form_db
764
785
765 def test_comment_and_comment_edit_permissions_forbidden(
786 def test_comment_and_comment_edit_permissions_forbidden(
766 self, autologin_regular_user, user_regular, user_admin, pr_util,
787 self, autologin_regular_user, user_regular, user_admin, pr_util,
767 csrf_token, xhr_header):
788 csrf_token, xhr_header):
768 pull_request = pr_util.create_pull_request(
789 pull_request = pr_util.create_pull_request(
769 author=user_admin.username, enable_notifications=False)
790 author=user_admin.username, enable_notifications=False)
770 comment = CommentsModel().create(
791 comment = CommentsModel().create(
771 text='test',
792 text='test',
772 repo=pull_request.target_repo.scm_instance().name,
793 repo=pull_request.target_repo.scm_instance().name,
773 user=user_admin,
794 user=user_admin,
774 pull_request=pull_request,
795 pull_request=pull_request,
775 )
796 )
776 response = self.app.post(
797 response = self.app.post(
777 route_path(
798 route_path(
778 'pullrequest_comment_edit',
799 'pullrequest_comment_edit',
779 repo_name=pull_request.target_repo.scm_instance().name,
800 repo_name=pull_request.target_repo.scm_instance().name,
780 pull_request_id=pull_request.pull_request_id,
801 pull_request_id=pull_request.pull_request_id,
781 comment_id=comment.comment_id,
802 comment_id=comment.comment_id,
782 ),
803 ),
783 extra_environ=xhr_header,
804 extra_environ=xhr_header,
784 params={
805 params={
785 'csrf_token': csrf_token,
806 'csrf_token': csrf_token,
786 'text': 'test_text',
807 'text': 'test_text',
787 },
808 },
788 status=403,
809 status=403,
789 )
810 )
790 assert response.status_int == 403
811 assert response.status_int == 403
791
812
792 def test_create_pull_request(self, backend, csrf_token):
813 def test_create_pull_request(self, backend, csrf_token):
793 commits = [
814 commits = [
794 {'message': 'ancestor'},
815 {'message': 'ancestor'},
795 {'message': 'change'},
816 {'message': 'change'},
796 {'message': 'change2'},
817 {'message': 'change2'},
797 ]
818 ]
798 commit_ids = backend.create_master_repo(commits)
819 commit_ids = backend.create_master_repo(commits)
799 target = backend.create_repo(heads=['ancestor'])
820 target = backend.create_repo(heads=['ancestor'])
800 source = backend.create_repo(heads=['change2'])
821 source = backend.create_repo(heads=['change2'])
801
822
802 response = self.app.post(
823 response = self.app.post(
803 route_path('pullrequest_create', repo_name=source.repo_name),
824 route_path('pullrequest_create', repo_name=source.repo_name),
804 [
825 [
805 ('source_repo', source.repo_name),
826 ('source_repo', source.repo_name),
806 ('source_ref', 'branch:default:' + commit_ids['change2']),
827 ('source_ref', 'branch:default:' + commit_ids['change2']),
807 ('target_repo', target.repo_name),
828 ('target_repo', target.repo_name),
808 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
829 ('target_ref', 'branch:default:' + commit_ids['ancestor']),
809 ('common_ancestor', commit_ids['ancestor']),
830 ('common_ancestor', commit_ids['ancestor']),
810 ('pullrequest_title', 'Title'),
831 ('pullrequest_title', 'Title'),
811 ('pullrequest_desc', 'Description'),
832 ('pullrequest_desc', 'Description'),
812 ('description_renderer', 'markdown'),
833 ('description_renderer', 'markdown'),
813 ('__start__', 'review_members:sequence'),
834 ('__start__', 'review_members:sequence'),
814 ('__start__', 'reviewer:mapping'),
835 ('__start__', 'reviewer:mapping'),
815 ('user_id', '1'),
836 ('user_id', '1'),
816 ('__start__', 'reasons:sequence'),
837 ('__start__', 'reasons:sequence'),
817 ('reason', 'Some reason'),
838 ('reason', 'Some reason'),
818 ('__end__', 'reasons:sequence'),
839 ('__end__', 'reasons:sequence'),
819 ('__start__', 'rules:sequence'),
840 ('__start__', 'rules:sequence'),
820 ('__end__', 'rules:sequence'),
841 ('__end__', 'rules:sequence'),
821 ('mandatory', 'False'),
842 ('mandatory', 'False'),
822 ('__end__', 'reviewer:mapping'),
843 ('__end__', 'reviewer:mapping'),
823 ('__end__', 'review_members:sequence'),
844 ('__end__', 'review_members:sequence'),
824 ('__start__', 'revisions:sequence'),
845 ('__start__', 'revisions:sequence'),
825 ('revisions', commit_ids['change']),
846 ('revisions', commit_ids['change']),
826 ('revisions', commit_ids['change2']),
847 ('revisions', commit_ids['change2']),
827 ('__end__', 'revisions:sequence'),
848 ('__end__', 'revisions:sequence'),
828 ('user', ''),
849 ('user', ''),
829 ('csrf_token', csrf_token),
850 ('csrf_token', csrf_token),
830 ],
851 ],
831 status=302)
852 status=302)
832
853
833 location = response.headers['Location']
854 location = response.headers['Location']
834 pull_request_id = location.rsplit('/', 1)[1]
855 pull_request_id = location.rsplit('/', 1)[1]
835 assert pull_request_id != 'new'
856 assert pull_request_id != 'new'
836 pull_request = PullRequest.get(int(pull_request_id))
857 pull_request = PullRequest.get(int(pull_request_id))
837
858
838 # check that we have now both revisions
859 # check that we have now both revisions
839 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
860 assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']]
840 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
861 assert pull_request.source_ref == 'branch:default:' + commit_ids['change2']
841 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
862 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
842 assert pull_request.target_ref == expected_target_ref
863 assert pull_request.target_ref == expected_target_ref
843
864
844 def test_reviewer_notifications(self, backend, csrf_token):
865 def test_reviewer_notifications(self, backend, csrf_token):
845 # We have to use the app.post for this test so it will create the
866 # We have to use the app.post for this test so it will create the
846 # notifications properly with the new PR
867 # notifications properly with the new PR
847 commits = [
868 commits = [
848 {'message': 'ancestor',
869 {'message': 'ancestor',
849 'added': [FileNode('file_A', content='content_of_ancestor')]},
870 'added': [FileNode('file_A', content='content_of_ancestor')]},
850 {'message': 'change',
871 {'message': 'change',
851 'added': [FileNode('file_a', content='content_of_change')]},
872 'added': [FileNode('file_a', content='content_of_change')]},
852 {'message': 'change-child'},
873 {'message': 'change-child'},
853 {'message': 'ancestor-child', 'parents': ['ancestor'],
874 {'message': 'ancestor-child', 'parents': ['ancestor'],
854 'added': [
875 'added': [
855 FileNode('file_B', content='content_of_ancestor_child')]},
876 FileNode('file_B', content='content_of_ancestor_child')]},
856 {'message': 'ancestor-child-2'},
877 {'message': 'ancestor-child-2'},
857 ]
878 ]
858 commit_ids = backend.create_master_repo(commits)
879 commit_ids = backend.create_master_repo(commits)
859 target = backend.create_repo(heads=['ancestor-child'])
880 target = backend.create_repo(heads=['ancestor-child'])
860 source = backend.create_repo(heads=['change'])
881 source = backend.create_repo(heads=['change'])
861
882
862 response = self.app.post(
883 response = self.app.post(
863 route_path('pullrequest_create', repo_name=source.repo_name),
884 route_path('pullrequest_create', repo_name=source.repo_name),
864 [
885 [
865 ('source_repo', source.repo_name),
886 ('source_repo', source.repo_name),
866 ('source_ref', 'branch:default:' + commit_ids['change']),
887 ('source_ref', 'branch:default:' + commit_ids['change']),
867 ('target_repo', target.repo_name),
888 ('target_repo', target.repo_name),
868 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
889 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
869 ('common_ancestor', commit_ids['ancestor']),
890 ('common_ancestor', commit_ids['ancestor']),
870 ('pullrequest_title', 'Title'),
891 ('pullrequest_title', 'Title'),
871 ('pullrequest_desc', 'Description'),
892 ('pullrequest_desc', 'Description'),
872 ('description_renderer', 'markdown'),
893 ('description_renderer', 'markdown'),
873 ('__start__', 'review_members:sequence'),
894 ('__start__', 'review_members:sequence'),
874 ('__start__', 'reviewer:mapping'),
895 ('__start__', 'reviewer:mapping'),
875 ('user_id', '2'),
896 ('user_id', '2'),
876 ('__start__', 'reasons:sequence'),
897 ('__start__', 'reasons:sequence'),
877 ('reason', 'Some reason'),
898 ('reason', 'Some reason'),
878 ('__end__', 'reasons:sequence'),
899 ('__end__', 'reasons:sequence'),
879 ('__start__', 'rules:sequence'),
900 ('__start__', 'rules:sequence'),
880 ('__end__', 'rules:sequence'),
901 ('__end__', 'rules:sequence'),
881 ('mandatory', 'False'),
902 ('mandatory', 'False'),
882 ('__end__', 'reviewer:mapping'),
903 ('__end__', 'reviewer:mapping'),
883 ('__end__', 'review_members:sequence'),
904 ('__end__', 'review_members:sequence'),
884 ('__start__', 'revisions:sequence'),
905 ('__start__', 'revisions:sequence'),
885 ('revisions', commit_ids['change']),
906 ('revisions', commit_ids['change']),
886 ('__end__', 'revisions:sequence'),
907 ('__end__', 'revisions:sequence'),
887 ('user', ''),
908 ('user', ''),
888 ('csrf_token', csrf_token),
909 ('csrf_token', csrf_token),
889 ],
910 ],
890 status=302)
911 status=302)
891
912
892 location = response.headers['Location']
913 location = response.headers['Location']
893
914
894 pull_request_id = location.rsplit('/', 1)[1]
915 pull_request_id = location.rsplit('/', 1)[1]
895 assert pull_request_id != 'new'
916 assert pull_request_id != 'new'
896 pull_request = PullRequest.get(int(pull_request_id))
917 pull_request = PullRequest.get(int(pull_request_id))
897
918
898 # Check that a notification was made
919 # Check that a notification was made
899 notifications = Notification.query()\
920 notifications = Notification.query()\
900 .filter(Notification.created_by == pull_request.author.user_id,
921 .filter(Notification.created_by == pull_request.author.user_id,
901 Notification.type_ == Notification.TYPE_PULL_REQUEST,
922 Notification.type_ == Notification.TYPE_PULL_REQUEST,
902 Notification.subject.contains(
923 Notification.subject.contains(
903 "requested a pull request review. !%s" % pull_request_id))
924 "requested a pull request review. !%s" % pull_request_id))
904 assert len(notifications.all()) == 1
925 assert len(notifications.all()) == 1
905
926
906 # Change reviewers and check that a notification was made
927 # Change reviewers and check that a notification was made
907 PullRequestModel().update_reviewers(
928 PullRequestModel().update_reviewers(
908 pull_request.pull_request_id, [
929 pull_request.pull_request_id, [
909 (1, [], False, 'reviewer', [])
930 (1, [], False, 'reviewer', [])
910 ],
931 ],
911 pull_request.author)
932 pull_request.author)
912 assert len(notifications.all()) == 2
933 assert len(notifications.all()) == 2
913
934
914 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
935 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
915 commits = [
936 commits = [
916 {'message': 'ancestor',
937 {'message': 'ancestor',
917 'added': [FileNode('file_A', content='content_of_ancestor')]},
938 'added': [FileNode('file_A', content='content_of_ancestor')]},
918 {'message': 'change',
939 {'message': 'change',
919 'added': [FileNode('file_a', content='content_of_change')]},
940 'added': [FileNode('file_a', content='content_of_change')]},
920 {'message': 'change-child'},
941 {'message': 'change-child'},
921 {'message': 'ancestor-child', 'parents': ['ancestor'],
942 {'message': 'ancestor-child', 'parents': ['ancestor'],
922 'added': [
943 'added': [
923 FileNode('file_B', content='content_of_ancestor_child')]},
944 FileNode('file_B', content='content_of_ancestor_child')]},
924 {'message': 'ancestor-child-2'},
945 {'message': 'ancestor-child-2'},
925 ]
946 ]
926 commit_ids = backend.create_master_repo(commits)
947 commit_ids = backend.create_master_repo(commits)
927 target = backend.create_repo(heads=['ancestor-child'])
948 target = backend.create_repo(heads=['ancestor-child'])
928 source = backend.create_repo(heads=['change'])
949 source = backend.create_repo(heads=['change'])
929
950
930 response = self.app.post(
951 response = self.app.post(
931 route_path('pullrequest_create', repo_name=source.repo_name),
952 route_path('pullrequest_create', repo_name=source.repo_name),
932 [
953 [
933 ('source_repo', source.repo_name),
954 ('source_repo', source.repo_name),
934 ('source_ref', 'branch:default:' + commit_ids['change']),
955 ('source_ref', 'branch:default:' + commit_ids['change']),
935 ('target_repo', target.repo_name),
956 ('target_repo', target.repo_name),
936 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
957 ('target_ref', 'branch:default:' + commit_ids['ancestor-child']),
937 ('common_ancestor', commit_ids['ancestor']),
958 ('common_ancestor', commit_ids['ancestor']),
938 ('pullrequest_title', 'Title'),
959 ('pullrequest_title', 'Title'),
939 ('pullrequest_desc', 'Description'),
960 ('pullrequest_desc', 'Description'),
940 ('description_renderer', 'markdown'),
961 ('description_renderer', 'markdown'),
941 ('__start__', 'review_members:sequence'),
962 ('__start__', 'review_members:sequence'),
942 ('__start__', 'reviewer:mapping'),
963 ('__start__', 'reviewer:mapping'),
943 ('user_id', '1'),
964 ('user_id', '1'),
944 ('__start__', 'reasons:sequence'),
965 ('__start__', 'reasons:sequence'),
945 ('reason', 'Some reason'),
966 ('reason', 'Some reason'),
946 ('__end__', 'reasons:sequence'),
967 ('__end__', 'reasons:sequence'),
947 ('__start__', 'rules:sequence'),
968 ('__start__', 'rules:sequence'),
948 ('__end__', 'rules:sequence'),
969 ('__end__', 'rules:sequence'),
949 ('mandatory', 'False'),
970 ('mandatory', 'False'),
950 ('__end__', 'reviewer:mapping'),
971 ('__end__', 'reviewer:mapping'),
951 ('__end__', 'review_members:sequence'),
972 ('__end__', 'review_members:sequence'),
952 ('__start__', 'revisions:sequence'),
973 ('__start__', 'revisions:sequence'),
953 ('revisions', commit_ids['change']),
974 ('revisions', commit_ids['change']),
954 ('__end__', 'revisions:sequence'),
975 ('__end__', 'revisions:sequence'),
955 ('user', ''),
976 ('user', ''),
956 ('csrf_token', csrf_token),
977 ('csrf_token', csrf_token),
957 ],
978 ],
958 status=302)
979 status=302)
959
980
960 location = response.headers['Location']
981 location = response.headers['Location']
961
982
962 pull_request_id = location.rsplit('/', 1)[1]
983 pull_request_id = location.rsplit('/', 1)[1]
963 assert pull_request_id != 'new'
984 assert pull_request_id != 'new'
964 pull_request = PullRequest.get(int(pull_request_id))
985 pull_request = PullRequest.get(int(pull_request_id))
965
986
966 # target_ref has to point to the ancestor's commit_id in order to
987 # target_ref has to point to the ancestor's commit_id in order to
967 # show the correct diff
988 # show the correct diff
968 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
989 expected_target_ref = 'branch:default:' + commit_ids['ancestor']
969 assert pull_request.target_ref == expected_target_ref
990 assert pull_request.target_ref == expected_target_ref
970
991
971 # Check generated diff contents
992 # Check generated diff contents
972 response = response.follow()
993 response = response.follow()
973 response.mustcontain(no=['content_of_ancestor'])
994 response.mustcontain(no=['content_of_ancestor'])
974 response.mustcontain(no=['content_of_ancestor-child'])
995 response.mustcontain(no=['content_of_ancestor-child'])
975 response.mustcontain('content_of_change')
996 response.mustcontain('content_of_change')
976
997
977 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
998 def test_merge_pull_request_enabled(self, pr_util, csrf_token):
978 # Clear any previous calls to rcextensions
999 # Clear any previous calls to rcextensions
979 rhodecode.EXTENSIONS.calls.clear()
1000 rhodecode.EXTENSIONS.calls.clear()
980
1001
981 pull_request = pr_util.create_pull_request(
1002 pull_request = pr_util.create_pull_request(
982 approved=True, mergeable=True)
1003 approved=True, mergeable=True)
983 pull_request_id = pull_request.pull_request_id
1004 pull_request_id = pull_request.pull_request_id
984 repo_name = pull_request.target_repo.scm_instance().name,
1005 repo_name = pull_request.target_repo.scm_instance().name,
985
1006
986 url = route_path('pullrequest_merge',
1007 url = route_path('pullrequest_merge',
987 repo_name=str(repo_name[0]),
1008 repo_name=str(repo_name[0]),
988 pull_request_id=pull_request_id)
1009 pull_request_id=pull_request_id)
989 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
1010 response = self.app.post(url, params={'csrf_token': csrf_token}).follow()
990
1011
991 pull_request = PullRequest.get(pull_request_id)
1012 pull_request = PullRequest.get(pull_request_id)
992
1013
993 assert response.status_int == 200
1014 assert response.status_int == 200
994 assert pull_request.is_closed()
1015 assert pull_request.is_closed()
995 assert_pull_request_status(
1016 assert_pull_request_status(
996 pull_request, ChangesetStatus.STATUS_APPROVED)
1017 pull_request, ChangesetStatus.STATUS_APPROVED)
997
1018
998 # Check the relevant log entries were added
1019 # Check the relevant log entries were added
999 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1020 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(3)
1000 actions = [log.action for log in user_logs]
1021 actions = [log.action for log in user_logs]
1001 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1022 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
1002 expected_actions = [
1023 expected_actions = [
1003 u'repo.pull_request.close',
1024 u'repo.pull_request.close',
1004 u'repo.pull_request.merge',
1025 u'repo.pull_request.merge',
1005 u'repo.pull_request.comment.create'
1026 u'repo.pull_request.comment.create'
1006 ]
1027 ]
1007 assert actions == expected_actions
1028 assert actions == expected_actions
1008
1029
1009 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1030 user_logs = UserLog.query().order_by(UserLog.user_log_id.desc()).limit(4)
1010 actions = [log for log in user_logs]
1031 actions = [log for log in user_logs]
1011 assert actions[-1].action == 'user.push'
1032 assert actions[-1].action == 'user.push'
1012 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1033 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
1013
1034
1014 # Check post_push rcextension was really executed
1035 # Check post_push rcextension was really executed
1015 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1036 push_calls = rhodecode.EXTENSIONS.calls['_push_hook']
1016 assert len(push_calls) == 1
1037 assert len(push_calls) == 1
1017 unused_last_call_args, last_call_kwargs = push_calls[0]
1038 unused_last_call_args, last_call_kwargs = push_calls[0]
1018 assert last_call_kwargs['action'] == 'push'
1039 assert last_call_kwargs['action'] == 'push'
1019 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1040 assert last_call_kwargs['commit_ids'] == pr_commit_ids
1020
1041
1021 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1042 def test_merge_pull_request_disabled(self, pr_util, csrf_token):
1022 pull_request = pr_util.create_pull_request(mergeable=False)
1043 pull_request = pr_util.create_pull_request(mergeable=False)
1023 pull_request_id = pull_request.pull_request_id
1044 pull_request_id = pull_request.pull_request_id
1024 pull_request = PullRequest.get(pull_request_id)
1045 pull_request = PullRequest.get(pull_request_id)
1025
1046
1026 response = self.app.post(
1047 response = self.app.post(
1027 route_path('pullrequest_merge',
1048 route_path('pullrequest_merge',
1028 repo_name=pull_request.target_repo.scm_instance().name,
1049 repo_name=pull_request.target_repo.scm_instance().name,
1029 pull_request_id=pull_request.pull_request_id),
1050 pull_request_id=pull_request.pull_request_id),
1030 params={'csrf_token': csrf_token}).follow()
1051 params={'csrf_token': csrf_token}).follow()
1031
1052
1032 assert response.status_int == 200
1053 assert response.status_int == 200
1033 response.mustcontain(
1054 response.mustcontain(
1034 'Merge is not currently possible because of below failed checks.')
1055 'Merge is not currently possible because of below failed checks.')
1035 response.mustcontain('Server-side pull request merging is disabled.')
1056 response.mustcontain('Server-side pull request merging is disabled.')
1036
1057
1037 @pytest.mark.skip_backends('svn')
1058 @pytest.mark.skip_backends('svn')
1038 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1059 def test_merge_pull_request_not_approved(self, pr_util, csrf_token):
1039 pull_request = pr_util.create_pull_request(mergeable=True)
1060 pull_request = pr_util.create_pull_request(mergeable=True)
1040 pull_request_id = pull_request.pull_request_id
1061 pull_request_id = pull_request.pull_request_id
1041 repo_name = pull_request.target_repo.scm_instance().name
1062 repo_name = pull_request.target_repo.scm_instance().name
1042
1063
1043 response = self.app.post(
1064 response = self.app.post(
1044 route_path('pullrequest_merge',
1065 route_path('pullrequest_merge',
1045 repo_name=repo_name, pull_request_id=pull_request_id),
1066 repo_name=repo_name, pull_request_id=pull_request_id),
1046 params={'csrf_token': csrf_token}).follow()
1067 params={'csrf_token': csrf_token}).follow()
1047
1068
1048 assert response.status_int == 200
1069 assert response.status_int == 200
1049
1070
1050 response.mustcontain(
1071 response.mustcontain(
1051 'Merge is not currently possible because of below failed checks.')
1072 'Merge is not currently possible because of below failed checks.')
1052 response.mustcontain('Pull request reviewer approval is pending.')
1073 response.mustcontain('Pull request reviewer approval is pending.')
1053
1074
1054 def test_merge_pull_request_renders_failure_reason(
1075 def test_merge_pull_request_renders_failure_reason(
1055 self, user_regular, csrf_token, pr_util):
1076 self, user_regular, csrf_token, pr_util):
1056 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1077 pull_request = pr_util.create_pull_request(mergeable=True, approved=True)
1057 pull_request_id = pull_request.pull_request_id
1078 pull_request_id = pull_request.pull_request_id
1058 repo_name = pull_request.target_repo.scm_instance().name
1079 repo_name = pull_request.target_repo.scm_instance().name
1059
1080
1060 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1081 merge_resp = MergeResponse(True, False, 'STUB_COMMIT_ID',
1061 MergeFailureReason.PUSH_FAILED,
1082 MergeFailureReason.PUSH_FAILED,
1062 metadata={'target': 'shadow repo',
1083 metadata={'target': 'shadow repo',
1063 'merge_commit': 'xxx'})
1084 'merge_commit': 'xxx'})
1064 model_patcher = mock.patch.multiple(
1085 model_patcher = mock.patch.multiple(
1065 PullRequestModel,
1086 PullRequestModel,
1066 merge_repo=mock.Mock(return_value=merge_resp),
1087 merge_repo=mock.Mock(return_value=merge_resp),
1067 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1088 merge_status=mock.Mock(return_value=(None, True, 'WRONG_MESSAGE')))
1068
1089
1069 with model_patcher:
1090 with model_patcher:
1070 response = self.app.post(
1091 response = self.app.post(
1071 route_path('pullrequest_merge',
1092 route_path('pullrequest_merge',
1072 repo_name=repo_name,
1093 repo_name=repo_name,
1073 pull_request_id=pull_request_id),
1094 pull_request_id=pull_request_id),
1074 params={'csrf_token': csrf_token}, status=302)
1095 params={'csrf_token': csrf_token}, status=302)
1075
1096
1076 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1097 merge_resp = MergeResponse(True, True, '', MergeFailureReason.PUSH_FAILED,
1077 metadata={'target': 'shadow repo',
1098 metadata={'target': 'shadow repo',
1078 'merge_commit': 'xxx'})
1099 'merge_commit': 'xxx'})
1079 assert_session_flash(response, merge_resp.merge_status_message)
1100 assert_session_flash(response, merge_resp.merge_status_message)
1080
1101
1081 def test_update_source_revision(self, backend, csrf_token):
1102 def test_update_source_revision(self, backend, csrf_token):
1082 commits = [
1103 commits = [
1083 {'message': 'ancestor'},
1104 {'message': 'ancestor'},
1084 {'message': 'change'},
1105 {'message': 'change'},
1085 {'message': 'change-2'},
1106 {'message': 'change-2'},
1086 ]
1107 ]
1087 commit_ids = backend.create_master_repo(commits)
1108 commit_ids = backend.create_master_repo(commits)
1088 target = backend.create_repo(heads=['ancestor'])
1109 target = backend.create_repo(heads=['ancestor'])
1089 source = backend.create_repo(heads=['change'])
1110 source = backend.create_repo(heads=['change'])
1090
1111
1091 # create pr from a in source to A in target
1112 # create pr from a in source to A in target
1092 pull_request = PullRequest()
1113 pull_request = PullRequest()
1093
1114
1094 pull_request.source_repo = source
1115 pull_request.source_repo = source
1095 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1116 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1096 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1117 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1097
1118
1098 pull_request.target_repo = target
1119 pull_request.target_repo = target
1099 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1120 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1100 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1121 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1101
1122
1102 pull_request.revisions = [commit_ids['change']]
1123 pull_request.revisions = [commit_ids['change']]
1103 pull_request.title = u"Test"
1124 pull_request.title = u"Test"
1104 pull_request.description = u"Description"
1125 pull_request.description = u"Description"
1105 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1126 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1106 pull_request.pull_request_state = PullRequest.STATE_CREATED
1127 pull_request.pull_request_state = PullRequest.STATE_CREATED
1107 Session().add(pull_request)
1128 Session().add(pull_request)
1108 Session().commit()
1129 Session().commit()
1109 pull_request_id = pull_request.pull_request_id
1130 pull_request_id = pull_request.pull_request_id
1110
1131
1111 # source has ancestor - change - change-2
1132 # source has ancestor - change - change-2
1112 backend.pull_heads(source, heads=['change-2'])
1133 backend.pull_heads(source, heads=['change-2'])
1113 target_repo_name = target.repo_name
1134 target_repo_name = target.repo_name
1114
1135
1115 # update PR
1136 # update PR
1116 self.app.post(
1137 self.app.post(
1117 route_path('pullrequest_update',
1138 route_path('pullrequest_update',
1118 repo_name=target_repo_name, pull_request_id=pull_request_id),
1139 repo_name=target_repo_name, pull_request_id=pull_request_id),
1119 params={'update_commits': 'true', 'csrf_token': csrf_token})
1140 params={'update_commits': 'true', 'csrf_token': csrf_token})
1120
1141
1121 response = self.app.get(
1142 response = self.app.get(
1122 route_path('pullrequest_show',
1143 route_path('pullrequest_show',
1123 repo_name=target_repo_name,
1144 repo_name=target_repo_name,
1124 pull_request_id=pull_request.pull_request_id))
1145 pull_request_id=pull_request.pull_request_id))
1125
1146
1126 assert response.status_int == 200
1147 assert response.status_int == 200
1127 response.mustcontain('Pull request updated to')
1148 response.mustcontain('Pull request updated to')
1128 response.mustcontain('with 1 added, 0 removed commits.')
1149 response.mustcontain('with 1 added, 0 removed commits.')
1129
1150
1130 # check that we have now both revisions
1151 # check that we have now both revisions
1131 pull_request = PullRequest.get(pull_request_id)
1152 pull_request = PullRequest.get(pull_request_id)
1132 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1153 assert pull_request.revisions == [commit_ids['change-2'], commit_ids['change']]
1133
1154
1134 def test_update_target_revision(self, backend, csrf_token):
1155 def test_update_target_revision(self, backend, csrf_token):
1135 commits = [
1156 commits = [
1136 {'message': 'ancestor'},
1157 {'message': 'ancestor'},
1137 {'message': 'change'},
1158 {'message': 'change'},
1138 {'message': 'ancestor-new', 'parents': ['ancestor']},
1159 {'message': 'ancestor-new', 'parents': ['ancestor']},
1139 {'message': 'change-rebased'},
1160 {'message': 'change-rebased'},
1140 ]
1161 ]
1141 commit_ids = backend.create_master_repo(commits)
1162 commit_ids = backend.create_master_repo(commits)
1142 target = backend.create_repo(heads=['ancestor'])
1163 target = backend.create_repo(heads=['ancestor'])
1143 source = backend.create_repo(heads=['change'])
1164 source = backend.create_repo(heads=['change'])
1144
1165
1145 # create pr from a in source to A in target
1166 # create pr from a in source to A in target
1146 pull_request = PullRequest()
1167 pull_request = PullRequest()
1147
1168
1148 pull_request.source_repo = source
1169 pull_request.source_repo = source
1149 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1170 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1150 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1171 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1151
1172
1152 pull_request.target_repo = target
1173 pull_request.target_repo = target
1153 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1174 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1154 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1175 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1155
1176
1156 pull_request.revisions = [commit_ids['change']]
1177 pull_request.revisions = [commit_ids['change']]
1157 pull_request.title = u"Test"
1178 pull_request.title = u"Test"
1158 pull_request.description = u"Description"
1179 pull_request.description = u"Description"
1159 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1180 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1160 pull_request.pull_request_state = PullRequest.STATE_CREATED
1181 pull_request.pull_request_state = PullRequest.STATE_CREATED
1161
1182
1162 Session().add(pull_request)
1183 Session().add(pull_request)
1163 Session().commit()
1184 Session().commit()
1164 pull_request_id = pull_request.pull_request_id
1185 pull_request_id = pull_request.pull_request_id
1165
1186
1166 # target has ancestor - ancestor-new
1187 # target has ancestor - ancestor-new
1167 # source has ancestor - ancestor-new - change-rebased
1188 # source has ancestor - ancestor-new - change-rebased
1168 backend.pull_heads(target, heads=['ancestor-new'])
1189 backend.pull_heads(target, heads=['ancestor-new'])
1169 backend.pull_heads(source, heads=['change-rebased'])
1190 backend.pull_heads(source, heads=['change-rebased'])
1170 target_repo_name = target.repo_name
1191 target_repo_name = target.repo_name
1171
1192
1172 # update PR
1193 # update PR
1173 url = route_path('pullrequest_update',
1194 url = route_path('pullrequest_update',
1174 repo_name=target_repo_name,
1195 repo_name=target_repo_name,
1175 pull_request_id=pull_request_id)
1196 pull_request_id=pull_request_id)
1176 self.app.post(url,
1197 self.app.post(url,
1177 params={'update_commits': 'true', 'csrf_token': csrf_token},
1198 params={'update_commits': 'true', 'csrf_token': csrf_token},
1178 status=200)
1199 status=200)
1179
1200
1180 # check that we have now both revisions
1201 # check that we have now both revisions
1181 pull_request = PullRequest.get(pull_request_id)
1202 pull_request = PullRequest.get(pull_request_id)
1182 assert pull_request.revisions == [commit_ids['change-rebased']]
1203 assert pull_request.revisions == [commit_ids['change-rebased']]
1183 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1204 assert pull_request.target_ref == 'branch:{branch}:{commit_id}'.format(
1184 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1205 branch=backend.default_branch_name, commit_id=commit_ids['ancestor-new'])
1185
1206
1186 response = self.app.get(
1207 response = self.app.get(
1187 route_path('pullrequest_show',
1208 route_path('pullrequest_show',
1188 repo_name=target_repo_name,
1209 repo_name=target_repo_name,
1189 pull_request_id=pull_request.pull_request_id))
1210 pull_request_id=pull_request.pull_request_id))
1190 assert response.status_int == 200
1211 assert response.status_int == 200
1191 response.mustcontain('Pull request updated to')
1212 response.mustcontain('Pull request updated to')
1192 response.mustcontain('with 1 added, 1 removed commits.')
1213 response.mustcontain('with 1 added, 1 removed commits.')
1193
1214
1194 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1215 def test_update_target_revision_with_removal_of_1_commit_git(self, backend_git, csrf_token):
1195 backend = backend_git
1216 backend = backend_git
1196 commits = [
1217 commits = [
1197 {'message': 'master-commit-1'},
1218 {'message': 'master-commit-1'},
1198 {'message': 'master-commit-2-change-1'},
1219 {'message': 'master-commit-2-change-1'},
1199 {'message': 'master-commit-3-change-2'},
1220 {'message': 'master-commit-3-change-2'},
1200
1221
1201 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1222 {'message': 'feat-commit-1', 'parents': ['master-commit-1']},
1202 {'message': 'feat-commit-2'},
1223 {'message': 'feat-commit-2'},
1203 ]
1224 ]
1204 commit_ids = backend.create_master_repo(commits)
1225 commit_ids = backend.create_master_repo(commits)
1205 target = backend.create_repo(heads=['master-commit-3-change-2'])
1226 target = backend.create_repo(heads=['master-commit-3-change-2'])
1206 source = backend.create_repo(heads=['feat-commit-2'])
1227 source = backend.create_repo(heads=['feat-commit-2'])
1207
1228
1208 # create pr from a in source to A in target
1229 # create pr from a in source to A in target
1209 pull_request = PullRequest()
1230 pull_request = PullRequest()
1210 pull_request.source_repo = source
1231 pull_request.source_repo = source
1211
1232
1212 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1233 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1213 branch=backend.default_branch_name,
1234 branch=backend.default_branch_name,
1214 commit_id=commit_ids['master-commit-3-change-2'])
1235 commit_id=commit_ids['master-commit-3-change-2'])
1215
1236
1216 pull_request.target_repo = target
1237 pull_request.target_repo = target
1217 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1238 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1218 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1239 branch=backend.default_branch_name, commit_id=commit_ids['feat-commit-2'])
1219
1240
1220 pull_request.revisions = [
1241 pull_request.revisions = [
1221 commit_ids['feat-commit-1'],
1242 commit_ids['feat-commit-1'],
1222 commit_ids['feat-commit-2']
1243 commit_ids['feat-commit-2']
1223 ]
1244 ]
1224 pull_request.title = u"Test"
1245 pull_request.title = u"Test"
1225 pull_request.description = u"Description"
1246 pull_request.description = u"Description"
1226 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1247 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1227 pull_request.pull_request_state = PullRequest.STATE_CREATED
1248 pull_request.pull_request_state = PullRequest.STATE_CREATED
1228 Session().add(pull_request)
1249 Session().add(pull_request)
1229 Session().commit()
1250 Session().commit()
1230 pull_request_id = pull_request.pull_request_id
1251 pull_request_id = pull_request.pull_request_id
1231
1252
1232 # PR is created, now we simulate a force-push into target,
1253 # PR is created, now we simulate a force-push into target,
1233 # that drops a 2 last commits
1254 # that drops a 2 last commits
1234 vcsrepo = target.scm_instance()
1255 vcsrepo = target.scm_instance()
1235 vcsrepo.config.clear_section('hooks')
1256 vcsrepo.config.clear_section('hooks')
1236 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1257 vcsrepo.run_git_command(['reset', '--soft', 'HEAD~2'])
1237 target_repo_name = target.repo_name
1258 target_repo_name = target.repo_name
1238
1259
1239 # update PR
1260 # update PR
1240 url = route_path('pullrequest_update',
1261 url = route_path('pullrequest_update',
1241 repo_name=target_repo_name,
1262 repo_name=target_repo_name,
1242 pull_request_id=pull_request_id)
1263 pull_request_id=pull_request_id)
1243 self.app.post(url,
1264 self.app.post(url,
1244 params={'update_commits': 'true', 'csrf_token': csrf_token},
1265 params={'update_commits': 'true', 'csrf_token': csrf_token},
1245 status=200)
1266 status=200)
1246
1267
1247 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1268 response = self.app.get(route_path('pullrequest_new', repo_name=target_repo_name))
1248 assert response.status_int == 200
1269 assert response.status_int == 200
1249 response.mustcontain('Pull request updated to')
1270 response.mustcontain('Pull request updated to')
1250 response.mustcontain('with 0 added, 0 removed commits.')
1271 response.mustcontain('with 0 added, 0 removed commits.')
1251
1272
1252 def test_update_of_ancestor_reference(self, backend, csrf_token):
1273 def test_update_of_ancestor_reference(self, backend, csrf_token):
1253 commits = [
1274 commits = [
1254 {'message': 'ancestor'},
1275 {'message': 'ancestor'},
1255 {'message': 'change'},
1276 {'message': 'change'},
1256 {'message': 'change-2'},
1277 {'message': 'change-2'},
1257 {'message': 'ancestor-new', 'parents': ['ancestor']},
1278 {'message': 'ancestor-new', 'parents': ['ancestor']},
1258 {'message': 'change-rebased'},
1279 {'message': 'change-rebased'},
1259 ]
1280 ]
1260 commit_ids = backend.create_master_repo(commits)
1281 commit_ids = backend.create_master_repo(commits)
1261 target = backend.create_repo(heads=['ancestor'])
1282 target = backend.create_repo(heads=['ancestor'])
1262 source = backend.create_repo(heads=['change'])
1283 source = backend.create_repo(heads=['change'])
1263
1284
1264 # create pr from a in source to A in target
1285 # create pr from a in source to A in target
1265 pull_request = PullRequest()
1286 pull_request = PullRequest()
1266 pull_request.source_repo = source
1287 pull_request.source_repo = source
1267
1288
1268 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1289 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1269 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1290 branch=backend.default_branch_name, commit_id=commit_ids['change'])
1270 pull_request.target_repo = target
1291 pull_request.target_repo = target
1271 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1292 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1272 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1293 branch=backend.default_branch_name, commit_id=commit_ids['ancestor'])
1273 pull_request.revisions = [commit_ids['change']]
1294 pull_request.revisions = [commit_ids['change']]
1274 pull_request.title = u"Test"
1295 pull_request.title = u"Test"
1275 pull_request.description = u"Description"
1296 pull_request.description = u"Description"
1276 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1297 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1277 pull_request.pull_request_state = PullRequest.STATE_CREATED
1298 pull_request.pull_request_state = PullRequest.STATE_CREATED
1278 Session().add(pull_request)
1299 Session().add(pull_request)
1279 Session().commit()
1300 Session().commit()
1280 pull_request_id = pull_request.pull_request_id
1301 pull_request_id = pull_request.pull_request_id
1281
1302
1282 # target has ancestor - ancestor-new
1303 # target has ancestor - ancestor-new
1283 # source has ancestor - ancestor-new - change-rebased
1304 # source has ancestor - ancestor-new - change-rebased
1284 backend.pull_heads(target, heads=['ancestor-new'])
1305 backend.pull_heads(target, heads=['ancestor-new'])
1285 backend.pull_heads(source, heads=['change-rebased'])
1306 backend.pull_heads(source, heads=['change-rebased'])
1286 target_repo_name = target.repo_name
1307 target_repo_name = target.repo_name
1287
1308
1288 # update PR
1309 # update PR
1289 self.app.post(
1310 self.app.post(
1290 route_path('pullrequest_update',
1311 route_path('pullrequest_update',
1291 repo_name=target_repo_name, pull_request_id=pull_request_id),
1312 repo_name=target_repo_name, pull_request_id=pull_request_id),
1292 params={'update_commits': 'true', 'csrf_token': csrf_token},
1313 params={'update_commits': 'true', 'csrf_token': csrf_token},
1293 status=200)
1314 status=200)
1294
1315
1295 # Expect the target reference to be updated correctly
1316 # Expect the target reference to be updated correctly
1296 pull_request = PullRequest.get(pull_request_id)
1317 pull_request = PullRequest.get(pull_request_id)
1297 assert pull_request.revisions == [commit_ids['change-rebased']]
1318 assert pull_request.revisions == [commit_ids['change-rebased']]
1298 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1319 expected_target_ref = 'branch:{branch}:{commit_id}'.format(
1299 branch=backend.default_branch_name,
1320 branch=backend.default_branch_name,
1300 commit_id=commit_ids['ancestor-new'])
1321 commit_id=commit_ids['ancestor-new'])
1301 assert pull_request.target_ref == expected_target_ref
1322 assert pull_request.target_ref == expected_target_ref
1302
1323
1303 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1324 def test_remove_pull_request_branch(self, backend_git, csrf_token):
1304 branch_name = 'development'
1325 branch_name = 'development'
1305 commits = [
1326 commits = [
1306 {'message': 'initial-commit'},
1327 {'message': 'initial-commit'},
1307 {'message': 'old-feature'},
1328 {'message': 'old-feature'},
1308 {'message': 'new-feature', 'branch': branch_name},
1329 {'message': 'new-feature', 'branch': branch_name},
1309 ]
1330 ]
1310 repo = backend_git.create_repo(commits)
1331 repo = backend_git.create_repo(commits)
1311 repo_name = repo.repo_name
1332 repo_name = repo.repo_name
1312 commit_ids = backend_git.commit_ids
1333 commit_ids = backend_git.commit_ids
1313
1334
1314 pull_request = PullRequest()
1335 pull_request = PullRequest()
1315 pull_request.source_repo = repo
1336 pull_request.source_repo = repo
1316 pull_request.target_repo = repo
1337 pull_request.target_repo = repo
1317 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1338 pull_request.source_ref = 'branch:{branch}:{commit_id}'.format(
1318 branch=branch_name, commit_id=commit_ids['new-feature'])
1339 branch=branch_name, commit_id=commit_ids['new-feature'])
1319 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1340 pull_request.target_ref = 'branch:{branch}:{commit_id}'.format(
1320 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1341 branch=backend_git.default_branch_name, commit_id=commit_ids['old-feature'])
1321 pull_request.revisions = [commit_ids['new-feature']]
1342 pull_request.revisions = [commit_ids['new-feature']]
1322 pull_request.title = u"Test"
1343 pull_request.title = u"Test"
1323 pull_request.description = u"Description"
1344 pull_request.description = u"Description"
1324 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1345 pull_request.author = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1325 pull_request.pull_request_state = PullRequest.STATE_CREATED
1346 pull_request.pull_request_state = PullRequest.STATE_CREATED
1326 Session().add(pull_request)
1347 Session().add(pull_request)
1327 Session().commit()
1348 Session().commit()
1328
1349
1329 pull_request_id = pull_request.pull_request_id
1350 pull_request_id = pull_request.pull_request_id
1330
1351
1331 vcs = repo.scm_instance()
1352 vcs = repo.scm_instance()
1332 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1353 vcs.remove_ref('refs/heads/{}'.format(branch_name))
1333 # NOTE(marcink): run GC to ensure the commits are gone
1354 # NOTE(marcink): run GC to ensure the commits are gone
1334 vcs.run_gc()
1355 vcs.run_gc()
1335
1356
1336 response = self.app.get(route_path(
1357 response = self.app.get(route_path(
1337 'pullrequest_show',
1358 'pullrequest_show',
1338 repo_name=repo_name,
1359 repo_name=repo_name,
1339 pull_request_id=pull_request_id))
1360 pull_request_id=pull_request_id))
1340
1361
1341 assert response.status_int == 200
1362 assert response.status_int == 200
1342
1363
1343 response.assert_response().element_contains(
1364 response.assert_response().element_contains(
1344 '#changeset_compare_view_content .alert strong',
1365 '#changeset_compare_view_content .alert strong',
1345 'Missing commits')
1366 'Missing commits')
1346 response.assert_response().element_contains(
1367 response.assert_response().element_contains(
1347 '#changeset_compare_view_content .alert',
1368 '#changeset_compare_view_content .alert',
1348 'This pull request cannot be displayed, because one or more'
1369 'This pull request cannot be displayed, because one or more'
1349 ' commits no longer exist in the source repository.')
1370 ' commits no longer exist in the source repository.')
1350
1371
1351 def test_strip_commits_from_pull_request(
1372 def test_strip_commits_from_pull_request(
1352 self, backend, pr_util, csrf_token):
1373 self, backend, pr_util, csrf_token):
1353 commits = [
1374 commits = [
1354 {'message': 'initial-commit'},
1375 {'message': 'initial-commit'},
1355 {'message': 'old-feature'},
1376 {'message': 'old-feature'},
1356 {'message': 'new-feature', 'parents': ['initial-commit']},
1377 {'message': 'new-feature', 'parents': ['initial-commit']},
1357 ]
1378 ]
1358 pull_request = pr_util.create_pull_request(
1379 pull_request = pr_util.create_pull_request(
1359 commits, target_head='initial-commit', source_head='new-feature',
1380 commits, target_head='initial-commit', source_head='new-feature',
1360 revisions=['new-feature'])
1381 revisions=['new-feature'])
1361
1382
1362 vcs = pr_util.source_repository.scm_instance()
1383 vcs = pr_util.source_repository.scm_instance()
1363 if backend.alias == 'git':
1384 if backend.alias == 'git':
1364 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1385 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1365 else:
1386 else:
1366 vcs.strip(pr_util.commit_ids['new-feature'])
1387 vcs.strip(pr_util.commit_ids['new-feature'])
1367
1388
1368 response = self.app.get(route_path(
1389 response = self.app.get(route_path(
1369 'pullrequest_show',
1390 'pullrequest_show',
1370 repo_name=pr_util.target_repository.repo_name,
1391 repo_name=pr_util.target_repository.repo_name,
1371 pull_request_id=pull_request.pull_request_id))
1392 pull_request_id=pull_request.pull_request_id))
1372
1393
1373 assert response.status_int == 200
1394 assert response.status_int == 200
1374
1395
1375 response.assert_response().element_contains(
1396 response.assert_response().element_contains(
1376 '#changeset_compare_view_content .alert strong',
1397 '#changeset_compare_view_content .alert strong',
1377 'Missing commits')
1398 'Missing commits')
1378 response.assert_response().element_contains(
1399 response.assert_response().element_contains(
1379 '#changeset_compare_view_content .alert',
1400 '#changeset_compare_view_content .alert',
1380 'This pull request cannot be displayed, because one or more'
1401 'This pull request cannot be displayed, because one or more'
1381 ' commits no longer exist in the source repository.')
1402 ' commits no longer exist in the source repository.')
1382 response.assert_response().element_contains(
1403 response.assert_response().element_contains(
1383 '#update_commits',
1404 '#update_commits',
1384 'Update commits')
1405 'Update commits')
1385
1406
1386 def test_strip_commits_and_update(
1407 def test_strip_commits_and_update(
1387 self, backend, pr_util, csrf_token):
1408 self, backend, pr_util, csrf_token):
1388 commits = [
1409 commits = [
1389 {'message': 'initial-commit'},
1410 {'message': 'initial-commit'},
1390 {'message': 'old-feature'},
1411 {'message': 'old-feature'},
1391 {'message': 'new-feature', 'parents': ['old-feature']},
1412 {'message': 'new-feature', 'parents': ['old-feature']},
1392 ]
1413 ]
1393 pull_request = pr_util.create_pull_request(
1414 pull_request = pr_util.create_pull_request(
1394 commits, target_head='old-feature', source_head='new-feature',
1415 commits, target_head='old-feature', source_head='new-feature',
1395 revisions=['new-feature'], mergeable=True)
1416 revisions=['new-feature'], mergeable=True)
1396 pr_id = pull_request.pull_request_id
1417 pr_id = pull_request.pull_request_id
1397 target_repo_name = pull_request.target_repo.repo_name
1418 target_repo_name = pull_request.target_repo.repo_name
1398
1419
1399 vcs = pr_util.source_repository.scm_instance()
1420 vcs = pr_util.source_repository.scm_instance()
1400 if backend.alias == 'git':
1421 if backend.alias == 'git':
1401 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1422 vcs.strip(pr_util.commit_ids['new-feature'], branch_name='master')
1402 else:
1423 else:
1403 vcs.strip(pr_util.commit_ids['new-feature'])
1424 vcs.strip(pr_util.commit_ids['new-feature'])
1404
1425
1405 url = route_path('pullrequest_update',
1426 url = route_path('pullrequest_update',
1406 repo_name=target_repo_name,
1427 repo_name=target_repo_name,
1407 pull_request_id=pr_id)
1428 pull_request_id=pr_id)
1408 response = self.app.post(url,
1429 response = self.app.post(url,
1409 params={'update_commits': 'true',
1430 params={'update_commits': 'true',
1410 'csrf_token': csrf_token})
1431 'csrf_token': csrf_token})
1411
1432
1412 assert response.status_int == 200
1433 assert response.status_int == 200
1413 assert response.body == '{"response": true, "redirect_url": null}'
1434 assert response.body == '{"response": true, "redirect_url": null}'
1414
1435
1415 # Make sure that after update, it won't raise 500 errors
1436 # Make sure that after update, it won't raise 500 errors
1416 response = self.app.get(route_path(
1437 response = self.app.get(route_path(
1417 'pullrequest_show',
1438 'pullrequest_show',
1418 repo_name=target_repo_name,
1439 repo_name=target_repo_name,
1419 pull_request_id=pr_id))
1440 pull_request_id=pr_id))
1420
1441
1421 assert response.status_int == 200
1442 assert response.status_int == 200
1422 response.assert_response().element_contains(
1443 response.assert_response().element_contains(
1423 '#changeset_compare_view_content .alert strong',
1444 '#changeset_compare_view_content .alert strong',
1424 'Missing commits')
1445 'Missing commits')
1425
1446
1426 def test_branch_is_a_link(self, pr_util):
1447 def test_branch_is_a_link(self, pr_util):
1427 pull_request = pr_util.create_pull_request()
1448 pull_request = pr_util.create_pull_request()
1428 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1449 pull_request.source_ref = 'branch:origin:1234567890abcdef'
1429 pull_request.target_ref = 'branch:target:abcdef1234567890'
1450 pull_request.target_ref = 'branch:target:abcdef1234567890'
1430 Session().add(pull_request)
1451 Session().add(pull_request)
1431 Session().commit()
1452 Session().commit()
1432
1453
1433 response = self.app.get(route_path(
1454 response = self.app.get(route_path(
1434 'pullrequest_show',
1455 'pullrequest_show',
1435 repo_name=pull_request.target_repo.scm_instance().name,
1456 repo_name=pull_request.target_repo.scm_instance().name,
1436 pull_request_id=pull_request.pull_request_id))
1457 pull_request_id=pull_request.pull_request_id))
1437 assert response.status_int == 200
1458 assert response.status_int == 200
1438
1459
1439 source = response.assert_response().get_element('.pr-source-info')
1460 source = response.assert_response().get_element('.pr-source-info')
1440 source_parent = source.getparent()
1461 source_parent = source.getparent()
1441 assert len(source_parent) == 1
1462 assert len(source_parent) == 1
1442
1463
1443 target = response.assert_response().get_element('.pr-target-info')
1464 target = response.assert_response().get_element('.pr-target-info')
1444 target_parent = target.getparent()
1465 target_parent = target.getparent()
1445 assert len(target_parent) == 1
1466 assert len(target_parent) == 1
1446
1467
1447 expected_origin_link = route_path(
1468 expected_origin_link = route_path(
1448 'repo_commits',
1469 'repo_commits',
1449 repo_name=pull_request.source_repo.scm_instance().name,
1470 repo_name=pull_request.source_repo.scm_instance().name,
1450 params=dict(branch='origin'))
1471 params=dict(branch='origin'))
1451 expected_target_link = route_path(
1472 expected_target_link = route_path(
1452 'repo_commits',
1473 'repo_commits',
1453 repo_name=pull_request.target_repo.scm_instance().name,
1474 repo_name=pull_request.target_repo.scm_instance().name,
1454 params=dict(branch='target'))
1475 params=dict(branch='target'))
1455 assert source_parent.attrib['href'] == expected_origin_link
1476 assert source_parent.attrib['href'] == expected_origin_link
1456 assert target_parent.attrib['href'] == expected_target_link
1477 assert target_parent.attrib['href'] == expected_target_link
1457
1478
1458 def test_bookmark_is_not_a_link(self, pr_util):
1479 def test_bookmark_is_not_a_link(self, pr_util):
1459 pull_request = pr_util.create_pull_request()
1480 pull_request = pr_util.create_pull_request()
1460 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1481 pull_request.source_ref = 'bookmark:origin:1234567890abcdef'
1461 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1482 pull_request.target_ref = 'bookmark:target:abcdef1234567890'
1462 Session().add(pull_request)
1483 Session().add(pull_request)
1463 Session().commit()
1484 Session().commit()
1464
1485
1465 response = self.app.get(route_path(
1486 response = self.app.get(route_path(
1466 'pullrequest_show',
1487 'pullrequest_show',
1467 repo_name=pull_request.target_repo.scm_instance().name,
1488 repo_name=pull_request.target_repo.scm_instance().name,
1468 pull_request_id=pull_request.pull_request_id))
1489 pull_request_id=pull_request.pull_request_id))
1469 assert response.status_int == 200
1490 assert response.status_int == 200
1470
1491
1471 source = response.assert_response().get_element('.pr-source-info')
1492 source = response.assert_response().get_element('.pr-source-info')
1472 assert source.text.strip() == 'bookmark:origin'
1493 assert source.text.strip() == 'bookmark:origin'
1473 assert source.getparent().attrib.get('href') is None
1494 assert source.getparent().attrib.get('href') is None
1474
1495
1475 target = response.assert_response().get_element('.pr-target-info')
1496 target = response.assert_response().get_element('.pr-target-info')
1476 assert target.text.strip() == 'bookmark:target'
1497 assert target.text.strip() == 'bookmark:target'
1477 assert target.getparent().attrib.get('href') is None
1498 assert target.getparent().attrib.get('href') is None
1478
1499
1479 def test_tag_is_not_a_link(self, pr_util):
1500 def test_tag_is_not_a_link(self, pr_util):
1480 pull_request = pr_util.create_pull_request()
1501 pull_request = pr_util.create_pull_request()
1481 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1502 pull_request.source_ref = 'tag:origin:1234567890abcdef'
1482 pull_request.target_ref = 'tag:target:abcdef1234567890'
1503 pull_request.target_ref = 'tag:target:abcdef1234567890'
1483 Session().add(pull_request)
1504 Session().add(pull_request)
1484 Session().commit()
1505 Session().commit()
1485
1506
1486 response = self.app.get(route_path(
1507 response = self.app.get(route_path(
1487 'pullrequest_show',
1508 'pullrequest_show',
1488 repo_name=pull_request.target_repo.scm_instance().name,
1509 repo_name=pull_request.target_repo.scm_instance().name,
1489 pull_request_id=pull_request.pull_request_id))
1510 pull_request_id=pull_request.pull_request_id))
1490 assert response.status_int == 200
1511 assert response.status_int == 200
1491
1512
1492 source = response.assert_response().get_element('.pr-source-info')
1513 source = response.assert_response().get_element('.pr-source-info')
1493 assert source.text.strip() == 'tag:origin'
1514 assert source.text.strip() == 'tag:origin'
1494 assert source.getparent().attrib.get('href') is None
1515 assert source.getparent().attrib.get('href') is None
1495
1516
1496 target = response.assert_response().get_element('.pr-target-info')
1517 target = response.assert_response().get_element('.pr-target-info')
1497 assert target.text.strip() == 'tag:target'
1518 assert target.text.strip() == 'tag:target'
1498 assert target.getparent().attrib.get('href') is None
1519 assert target.getparent().attrib.get('href') is None
1499
1520
1500 @pytest.mark.parametrize('mergeable', [True, False])
1521 @pytest.mark.parametrize('mergeable', [True, False])
1501 def test_shadow_repository_link(
1522 def test_shadow_repository_link(
1502 self, mergeable, pr_util, http_host_only_stub):
1523 self, mergeable, pr_util, http_host_only_stub):
1503 """
1524 """
1504 Check that the pull request summary page displays a link to the shadow
1525 Check that the pull request summary page displays a link to the shadow
1505 repository if the pull request is mergeable. If it is not mergeable
1526 repository if the pull request is mergeable. If it is not mergeable
1506 the link should not be displayed.
1527 the link should not be displayed.
1507 """
1528 """
1508 pull_request = pr_util.create_pull_request(
1529 pull_request = pr_util.create_pull_request(
1509 mergeable=mergeable, enable_notifications=False)
1530 mergeable=mergeable, enable_notifications=False)
1510 target_repo = pull_request.target_repo.scm_instance()
1531 target_repo = pull_request.target_repo.scm_instance()
1511 pr_id = pull_request.pull_request_id
1532 pr_id = pull_request.pull_request_id
1512 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1533 shadow_url = '{host}/{repo}/pull-request/{pr_id}/repository'.format(
1513 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1534 host=http_host_only_stub, repo=target_repo.name, pr_id=pr_id)
1514
1535
1515 response = self.app.get(route_path(
1536 response = self.app.get(route_path(
1516 'pullrequest_show',
1537 'pullrequest_show',
1517 repo_name=target_repo.name,
1538 repo_name=target_repo.name,
1518 pull_request_id=pr_id))
1539 pull_request_id=pr_id))
1519
1540
1520 if mergeable:
1541 if mergeable:
1521 response.assert_response().element_value_contains(
1542 response.assert_response().element_value_contains(
1522 'input.pr-mergeinfo', shadow_url)
1543 'input.pr-mergeinfo', shadow_url)
1523 response.assert_response().element_value_contains(
1544 response.assert_response().element_value_contains(
1524 'input.pr-mergeinfo ', 'pr-merge')
1545 'input.pr-mergeinfo ', 'pr-merge')
1525 else:
1546 else:
1526 response.assert_response().no_element_exists('.pr-mergeinfo')
1547 response.assert_response().no_element_exists('.pr-mergeinfo')
1527
1548
1528
1549
1529 @pytest.mark.usefixtures('app')
1550 @pytest.mark.usefixtures('app')
1530 @pytest.mark.backends("git", "hg")
1551 @pytest.mark.backends("git", "hg")
1531 class TestPullrequestsControllerDelete(object):
1552 class TestPullrequestsControllerDelete(object):
1532 def test_pull_request_delete_button_permissions_admin(
1553 def test_pull_request_delete_button_permissions_admin(
1533 self, autologin_user, user_admin, pr_util):
1554 self, autologin_user, user_admin, pr_util):
1534 pull_request = pr_util.create_pull_request(
1555 pull_request = pr_util.create_pull_request(
1535 author=user_admin.username, enable_notifications=False)
1556 author=user_admin.username, enable_notifications=False)
1536
1557
1537 response = self.app.get(route_path(
1558 response = self.app.get(route_path(
1538 'pullrequest_show',
1559 'pullrequest_show',
1539 repo_name=pull_request.target_repo.scm_instance().name,
1560 repo_name=pull_request.target_repo.scm_instance().name,
1540 pull_request_id=pull_request.pull_request_id))
1561 pull_request_id=pull_request.pull_request_id))
1541
1562
1542 response.mustcontain('id="delete_pullrequest"')
1563 response.mustcontain('id="delete_pullrequest"')
1543 response.mustcontain('Confirm to delete this pull request')
1564 response.mustcontain('Confirm to delete this pull request')
1544
1565
1545 def test_pull_request_delete_button_permissions_owner(
1566 def test_pull_request_delete_button_permissions_owner(
1546 self, autologin_regular_user, user_regular, pr_util):
1567 self, autologin_regular_user, user_regular, pr_util):
1547 pull_request = pr_util.create_pull_request(
1568 pull_request = pr_util.create_pull_request(
1548 author=user_regular.username, enable_notifications=False)
1569 author=user_regular.username, enable_notifications=False)
1549
1570
1550 response = self.app.get(route_path(
1571 response = self.app.get(route_path(
1551 'pullrequest_show',
1572 'pullrequest_show',
1552 repo_name=pull_request.target_repo.scm_instance().name,
1573 repo_name=pull_request.target_repo.scm_instance().name,
1553 pull_request_id=pull_request.pull_request_id))
1574 pull_request_id=pull_request.pull_request_id))
1554
1575
1555 response.mustcontain('id="delete_pullrequest"')
1576 response.mustcontain('id="delete_pullrequest"')
1556 response.mustcontain('Confirm to delete this pull request')
1577 response.mustcontain('Confirm to delete this pull request')
1557
1578
1558 def test_pull_request_delete_button_permissions_forbidden(
1579 def test_pull_request_delete_button_permissions_forbidden(
1559 self, autologin_regular_user, user_regular, user_admin, pr_util):
1580 self, autologin_regular_user, user_regular, user_admin, pr_util):
1560 pull_request = pr_util.create_pull_request(
1581 pull_request = pr_util.create_pull_request(
1561 author=user_admin.username, enable_notifications=False)
1582 author=user_admin.username, enable_notifications=False)
1562
1583
1563 response = self.app.get(route_path(
1584 response = self.app.get(route_path(
1564 'pullrequest_show',
1585 'pullrequest_show',
1565 repo_name=pull_request.target_repo.scm_instance().name,
1586 repo_name=pull_request.target_repo.scm_instance().name,
1566 pull_request_id=pull_request.pull_request_id))
1587 pull_request_id=pull_request.pull_request_id))
1567 response.mustcontain(no=['id="delete_pullrequest"'])
1588 response.mustcontain(no=['id="delete_pullrequest"'])
1568 response.mustcontain(no=['Confirm to delete this pull request'])
1589 response.mustcontain(no=['Confirm to delete this pull request'])
1569
1590
1570 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1591 def test_pull_request_delete_button_permissions_can_update_cannot_delete(
1571 self, autologin_regular_user, user_regular, user_admin, pr_util,
1592 self, autologin_regular_user, user_regular, user_admin, pr_util,
1572 user_util):
1593 user_util):
1573
1594
1574 pull_request = pr_util.create_pull_request(
1595 pull_request = pr_util.create_pull_request(
1575 author=user_admin.username, enable_notifications=False)
1596 author=user_admin.username, enable_notifications=False)
1576
1597
1577 user_util.grant_user_permission_to_repo(
1598 user_util.grant_user_permission_to_repo(
1578 pull_request.target_repo, user_regular,
1599 pull_request.target_repo, user_regular,
1579 'repository.write')
1600 'repository.write')
1580
1601
1581 response = self.app.get(route_path(
1602 response = self.app.get(route_path(
1582 'pullrequest_show',
1603 'pullrequest_show',
1583 repo_name=pull_request.target_repo.scm_instance().name,
1604 repo_name=pull_request.target_repo.scm_instance().name,
1584 pull_request_id=pull_request.pull_request_id))
1605 pull_request_id=pull_request.pull_request_id))
1585
1606
1586 response.mustcontain('id="open_edit_pullrequest"')
1607 response.mustcontain('id="open_edit_pullrequest"')
1587 response.mustcontain('id="delete_pullrequest"')
1608 response.mustcontain('id="delete_pullrequest"')
1588 response.mustcontain(no=['Confirm to delete this pull request'])
1609 response.mustcontain(no=['Confirm to delete this pull request'])
1589
1610
1590 def test_delete_comment_returns_404_if_comment_does_not_exist(
1611 def test_delete_comment_returns_404_if_comment_does_not_exist(
1591 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1612 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1592
1613
1593 pull_request = pr_util.create_pull_request(
1614 pull_request = pr_util.create_pull_request(
1594 author=user_admin.username, enable_notifications=False)
1615 author=user_admin.username, enable_notifications=False)
1595
1616
1596 self.app.post(
1617 self.app.post(
1597 route_path(
1618 route_path(
1598 'pullrequest_comment_delete',
1619 'pullrequest_comment_delete',
1599 repo_name=pull_request.target_repo.scm_instance().name,
1620 repo_name=pull_request.target_repo.scm_instance().name,
1600 pull_request_id=pull_request.pull_request_id,
1621 pull_request_id=pull_request.pull_request_id,
1601 comment_id=1024404),
1622 comment_id=1024404),
1602 extra_environ=xhr_header,
1623 extra_environ=xhr_header,
1603 params={'csrf_token': csrf_token},
1624 params={'csrf_token': csrf_token},
1604 status=404
1625 status=404
1605 )
1626 )
1606
1627
1607 def test_delete_comment(
1628 def test_delete_comment(
1608 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1629 self, autologin_user, pr_util, user_admin, csrf_token, xhr_header):
1609
1630
1610 pull_request = pr_util.create_pull_request(
1631 pull_request = pr_util.create_pull_request(
1611 author=user_admin.username, enable_notifications=False)
1632 author=user_admin.username, enable_notifications=False)
1612 comment = pr_util.create_comment()
1633 comment = pr_util.create_comment()
1613 comment_id = comment.comment_id
1634 comment_id = comment.comment_id
1614
1635
1615 response = self.app.post(
1636 response = self.app.post(
1616 route_path(
1637 route_path(
1617 'pullrequest_comment_delete',
1638 'pullrequest_comment_delete',
1618 repo_name=pull_request.target_repo.scm_instance().name,
1639 repo_name=pull_request.target_repo.scm_instance().name,
1619 pull_request_id=pull_request.pull_request_id,
1640 pull_request_id=pull_request.pull_request_id,
1620 comment_id=comment_id),
1641 comment_id=comment_id),
1621 extra_environ=xhr_header,
1642 extra_environ=xhr_header,
1622 params={'csrf_token': csrf_token},
1643 params={'csrf_token': csrf_token},
1623 status=200
1644 status=200
1624 )
1645 )
1625 assert response.body == 'true'
1646 assert response.body == 'true'
1626
1647
1627 @pytest.mark.parametrize('url_type', [
1648 @pytest.mark.parametrize('url_type', [
1628 'pullrequest_new',
1649 'pullrequest_new',
1629 'pullrequest_create',
1650 'pullrequest_create',
1630 'pullrequest_update',
1651 'pullrequest_update',
1631 'pullrequest_merge',
1652 'pullrequest_merge',
1632 ])
1653 ])
1633 def test_pull_request_is_forbidden_on_archived_repo(
1654 def test_pull_request_is_forbidden_on_archived_repo(
1634 self, autologin_user, backend, xhr_header, user_util, url_type):
1655 self, autologin_user, backend, xhr_header, user_util, url_type):
1635
1656
1636 # create a temporary repo
1657 # create a temporary repo
1637 source = user_util.create_repo(repo_type=backend.alias)
1658 source = user_util.create_repo(repo_type=backend.alias)
1638 repo_name = source.repo_name
1659 repo_name = source.repo_name
1639 repo = Repository.get_by_repo_name(repo_name)
1660 repo = Repository.get_by_repo_name(repo_name)
1640 repo.archived = True
1661 repo.archived = True
1641 Session().commit()
1662 Session().commit()
1642
1663
1643 response = self.app.get(
1664 response = self.app.get(
1644 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1665 route_path(url_type, repo_name=repo_name, pull_request_id=1), status=302)
1645
1666
1646 msg = 'Action not supported for archived repository.'
1667 msg = 'Action not supported for archived repository.'
1647 assert_session_flash(response, msg)
1668 assert_session_flash(response, msg)
1648
1669
1649
1670
1650 def assert_pull_request_status(pull_request, expected_status):
1671 def assert_pull_request_status(pull_request, expected_status):
1651 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1672 status = ChangesetStatusModel().calculated_review_status(pull_request=pull_request)
1652 assert status == expected_status
1673 assert status == expected_status
1653
1674
1654
1675
1655 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1676 @pytest.mark.parametrize('route', ['pullrequest_new', 'pullrequest_create'])
1656 @pytest.mark.usefixtures("autologin_user")
1677 @pytest.mark.usefixtures("autologin_user")
1657 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1678 def test_forbidde_to_repo_summary_for_svn_repositories(backend_svn, app, route):
1658 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
1679 app.get(route_path(route, repo_name=backend_svn.repo_name), status=404)
@@ -1,293 +1,289 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 string
22 import string
23 import time
23 import time
24
24
25 import rhodecode
25 import rhodecode
26
26
27
27
28
28
29 from rhodecode.lib.view_utils import get_format_ref_id
29 from rhodecode.lib.view_utils import get_format_ref_id
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
31 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
32 from rhodecode.lib import helpers as h, rc_cache
32 from rhodecode.lib import helpers as h, rc_cache
33 from rhodecode.lib.utils2 import safe_str, safe_int
33 from rhodecode.lib.utils2 import safe_str, safe_int
34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.vcs.backends.base import EmptyCommit
36 from rhodecode.lib.vcs.backends.base import EmptyCommit
37 from rhodecode.lib.vcs.exceptions import (
37 from rhodecode.lib.vcs.exceptions import (
38 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
38 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
39 from rhodecode.model.db import Statistics, CacheKey, User
39 from rhodecode.model.db import Statistics, CacheKey, User
40 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
41 from rhodecode.model.scm import ScmModel
41 from rhodecode.model.scm import ScmModel
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 class RepoSummaryView(RepoAppView):
46 class RepoSummaryView(RepoAppView):
47
47
48 def load_default_context(self):
48 def load_default_context(self):
49 c = self._get_local_tmpl_context(include_app_defaults=True)
49 c = self._get_local_tmpl_context(include_app_defaults=True)
50 c.rhodecode_repo = None
50 c.rhodecode_repo = None
51 if not c.repository_requirements_missing:
51 if not c.repository_requirements_missing:
52 c.rhodecode_repo = self.rhodecode_vcs_repo
52 c.rhodecode_repo = self.rhodecode_vcs_repo
53 return c
53 return c
54
54
55 def _load_commits_context(self, c):
55 def _load_commits_context(self, c):
56 p = safe_int(self.request.GET.get('page'), 1)
56 p = safe_int(self.request.GET.get('page'), 1)
57 size = safe_int(self.request.GET.get('size'), 10)
57 size = safe_int(self.request.GET.get('size'), 10)
58
58
59 def url_generator(page_num):
59 def url_generator(page_num):
60 query_params = {
60 query_params = {
61 'page': page_num,
61 'page': page_num,
62 'size': size
62 'size': size
63 }
63 }
64 return h.route_path(
64 return h.route_path(
65 'repo_summary_commits',
65 'repo_summary_commits',
66 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
66 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
67
67
68 pre_load = ['author', 'branch', 'date', 'message']
68 pre_load = ['author', 'branch', 'date', 'message']
69 try:
69 try:
70 collection = self.rhodecode_vcs_repo.get_commits(
70 collection = self.rhodecode_vcs_repo.get_commits(
71 pre_load=pre_load, translate_tags=False)
71 pre_load=pre_load, translate_tags=False)
72 except EmptyRepositoryError:
72 except EmptyRepositoryError:
73 collection = self.rhodecode_vcs_repo
73 collection = self.rhodecode_vcs_repo
74
74
75 c.repo_commits = h.RepoPage(
75 c.repo_commits = h.RepoPage(
76 collection, page=p, items_per_page=size, url_maker=url_generator)
76 collection, page=p, items_per_page=size, url_maker=url_generator)
77 page_ids = [x.raw_id for x in c.repo_commits]
77 page_ids = [x.raw_id for x in c.repo_commits]
78 c.comments = self.db_repo.get_comments(page_ids)
78 c.comments = self.db_repo.get_comments(page_ids)
79 c.statuses = self.db_repo.statuses(page_ids)
79 c.statuses = self.db_repo.statuses(page_ids)
80
80
81 def _prepare_and_set_clone_url(self, c):
81 def _prepare_and_set_clone_url(self, c):
82 username = ''
82 username = ''
83 if self._rhodecode_user.username != User.DEFAULT_USER:
83 if self._rhodecode_user.username != User.DEFAULT_USER:
84 username = safe_str(self._rhodecode_user.username)
84 username = safe_str(self._rhodecode_user.username)
85
85
86 _def_clone_uri = _def_clone_uri_id = c.clone_uri_tmpl
86 _def_clone_uri = c.clone_uri_tmpl
87 _def_clone_uri_id = c.clone_uri_id_tmpl
87 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
88 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
88
89
89 if '{repo}' in _def_clone_uri:
90 _def_clone_uri_id = _def_clone_uri.replace('{repo}', '_{repoid}')
91 elif '{repoid}' in _def_clone_uri:
92 _def_clone_uri_id = _def_clone_uri.replace('_{repoid}', '{repo}')
93
94 c.clone_repo_url = self.db_repo.clone_url(
90 c.clone_repo_url = self.db_repo.clone_url(
95 user=username, uri_tmpl=_def_clone_uri)
91 user=username, uri_tmpl=_def_clone_uri)
96 c.clone_repo_url_id = self.db_repo.clone_url(
92 c.clone_repo_url_id = self.db_repo.clone_url(
97 user=username, uri_tmpl=_def_clone_uri_id)
93 user=username, uri_tmpl=_def_clone_uri_id)
98 c.clone_repo_url_ssh = self.db_repo.clone_url(
94 c.clone_repo_url_ssh = self.db_repo.clone_url(
99 uri_tmpl=_def_clone_uri_ssh, ssh=True)
95 uri_tmpl=_def_clone_uri_ssh, ssh=True)
100
96
101 @LoginRequired()
97 @LoginRequired()
102 @HasRepoPermissionAnyDecorator(
98 @HasRepoPermissionAnyDecorator(
103 'repository.read', 'repository.write', 'repository.admin')
99 'repository.read', 'repository.write', 'repository.admin')
104 def summary_commits(self):
100 def summary_commits(self):
105 c = self.load_default_context()
101 c = self.load_default_context()
106 self._prepare_and_set_clone_url(c)
102 self._prepare_and_set_clone_url(c)
107 self._load_commits_context(c)
103 self._load_commits_context(c)
108 return self._get_template_context(c)
104 return self._get_template_context(c)
109
105
110 @LoginRequired()
106 @LoginRequired()
111 @HasRepoPermissionAnyDecorator(
107 @HasRepoPermissionAnyDecorator(
112 'repository.read', 'repository.write', 'repository.admin')
108 'repository.read', 'repository.write', 'repository.admin')
113 def summary(self):
109 def summary(self):
114 c = self.load_default_context()
110 c = self.load_default_context()
115
111
116 # Prepare the clone URL
112 # Prepare the clone URL
117 self._prepare_and_set_clone_url(c)
113 self._prepare_and_set_clone_url(c)
118
114
119 # If enabled, get statistics data
115 # If enabled, get statistics data
120 c.show_stats = bool(self.db_repo.enable_statistics)
116 c.show_stats = bool(self.db_repo.enable_statistics)
121
117
122 stats = Session().query(Statistics) \
118 stats = Session().query(Statistics) \
123 .filter(Statistics.repository == self.db_repo) \
119 .filter(Statistics.repository == self.db_repo) \
124 .scalar()
120 .scalar()
125
121
126 c.stats_percentage = 0
122 c.stats_percentage = 0
127
123
128 if stats and stats.languages:
124 if stats and stats.languages:
129 c.no_data = False is self.db_repo.enable_statistics
125 c.no_data = False is self.db_repo.enable_statistics
130 lang_stats_d = json.loads(stats.languages)
126 lang_stats_d = json.loads(stats.languages)
131
127
132 # Sort first by decreasing count and second by the file extension,
128 # Sort first by decreasing count and second by the file extension,
133 # so we have a consistent output.
129 # so we have a consistent output.
134 lang_stats_items = sorted(lang_stats_d.iteritems(),
130 lang_stats_items = sorted(lang_stats_d.iteritems(),
135 key=lambda k: (-k[1], k[0]))[:10]
131 key=lambda k: (-k[1], k[0]))[:10]
136 lang_stats = [(x, {"count": y,
132 lang_stats = [(x, {"count": y,
137 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
133 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
138 for x, y in lang_stats_items]
134 for x, y in lang_stats_items]
139
135
140 c.trending_languages = json.dumps(lang_stats)
136 c.trending_languages = json.dumps(lang_stats)
141 else:
137 else:
142 c.no_data = True
138 c.no_data = True
143 c.trending_languages = json.dumps({})
139 c.trending_languages = json.dumps({})
144
140
145 scm_model = ScmModel()
141 scm_model = ScmModel()
146 c.enable_downloads = self.db_repo.enable_downloads
142 c.enable_downloads = self.db_repo.enable_downloads
147 c.repository_followers = scm_model.get_followers(self.db_repo)
143 c.repository_followers = scm_model.get_followers(self.db_repo)
148 c.repository_forks = scm_model.get_forks(self.db_repo)
144 c.repository_forks = scm_model.get_forks(self.db_repo)
149
145
150 # first interaction with the VCS instance after here...
146 # first interaction with the VCS instance after here...
151 if c.repository_requirements_missing:
147 if c.repository_requirements_missing:
152 self.request.override_renderer = \
148 self.request.override_renderer = \
153 'rhodecode:templates/summary/missing_requirements.mako'
149 'rhodecode:templates/summary/missing_requirements.mako'
154 return self._get_template_context(c)
150 return self._get_template_context(c)
155
151
156 c.readme_data, c.readme_file = \
152 c.readme_data, c.readme_file = \
157 self._get_readme_data(self.db_repo, c.visual.default_renderer)
153 self._get_readme_data(self.db_repo, c.visual.default_renderer)
158
154
159 # loads the summary commits template context
155 # loads the summary commits template context
160 self._load_commits_context(c)
156 self._load_commits_context(c)
161
157
162 return self._get_template_context(c)
158 return self._get_template_context(c)
163
159
164 @LoginRequired()
160 @LoginRequired()
165 @HasRepoPermissionAnyDecorator(
161 @HasRepoPermissionAnyDecorator(
166 'repository.read', 'repository.write', 'repository.admin')
162 'repository.read', 'repository.write', 'repository.admin')
167 def repo_stats(self):
163 def repo_stats(self):
168 show_stats = bool(self.db_repo.enable_statistics)
164 show_stats = bool(self.db_repo.enable_statistics)
169 repo_id = self.db_repo.repo_id
165 repo_id = self.db_repo.repo_id
170
166
171 landing_commit = self.db_repo.get_landing_commit()
167 landing_commit = self.db_repo.get_landing_commit()
172 if isinstance(landing_commit, EmptyCommit):
168 if isinstance(landing_commit, EmptyCommit):
173 return {'size': 0, 'code_stats': {}}
169 return {'size': 0, 'code_stats': {}}
174
170
175 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
171 cache_seconds = safe_int(rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
176 cache_on = cache_seconds > 0
172 cache_on = cache_seconds > 0
177
173
178 log.debug(
174 log.debug(
179 'Computing REPO STATS for repo_id %s commit_id `%s` '
175 'Computing REPO STATS for repo_id %s commit_id `%s` '
180 'with caching: %s[TTL: %ss]' % (
176 'with caching: %s[TTL: %ss]' % (
181 repo_id, landing_commit, cache_on, cache_seconds or 0))
177 repo_id, landing_commit, cache_on, cache_seconds or 0))
182
178
183 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
179 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
184 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
180 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
185
181
186 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
182 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
187 condition=cache_on)
183 condition=cache_on)
188 def compute_stats(repo_id, commit_id, _show_stats):
184 def compute_stats(repo_id, commit_id, _show_stats):
189 code_stats = {}
185 code_stats = {}
190 size = 0
186 size = 0
191 try:
187 try:
192 commit = self.db_repo.get_commit(commit_id)
188 commit = self.db_repo.get_commit(commit_id)
193
189
194 for node in commit.get_filenodes_generator():
190 for node in commit.get_filenodes_generator():
195 size += node.size
191 size += node.size
196 if not _show_stats:
192 if not _show_stats:
197 continue
193 continue
198 ext = string.lower(node.extension)
194 ext = string.lower(node.extension)
199 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
195 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
200 if ext_info:
196 if ext_info:
201 if ext in code_stats:
197 if ext in code_stats:
202 code_stats[ext]['count'] += 1
198 code_stats[ext]['count'] += 1
203 else:
199 else:
204 code_stats[ext] = {"count": 1, "desc": ext_info}
200 code_stats[ext] = {"count": 1, "desc": ext_info}
205 except (EmptyRepositoryError, CommitDoesNotExistError):
201 except (EmptyRepositoryError, CommitDoesNotExistError):
206 pass
202 pass
207 return {'size': h.format_byte_size_binary(size),
203 return {'size': h.format_byte_size_binary(size),
208 'code_stats': code_stats}
204 'code_stats': code_stats}
209
205
210 stats = compute_stats(self.db_repo.repo_id, landing_commit.raw_id, show_stats)
206 stats = compute_stats(self.db_repo.repo_id, landing_commit.raw_id, show_stats)
211 return stats
207 return stats
212
208
213 @LoginRequired()
209 @LoginRequired()
214 @HasRepoPermissionAnyDecorator(
210 @HasRepoPermissionAnyDecorator(
215 'repository.read', 'repository.write', 'repository.admin')
211 'repository.read', 'repository.write', 'repository.admin')
216 def repo_refs_data(self):
212 def repo_refs_data(self):
217 _ = self.request.translate
213 _ = self.request.translate
218 self.load_default_context()
214 self.load_default_context()
219
215
220 repo = self.rhodecode_vcs_repo
216 repo = self.rhodecode_vcs_repo
221 refs_to_create = [
217 refs_to_create = [
222 (_("Branch"), repo.branches, 'branch'),
218 (_("Branch"), repo.branches, 'branch'),
223 (_("Tag"), repo.tags, 'tag'),
219 (_("Tag"), repo.tags, 'tag'),
224 (_("Bookmark"), repo.bookmarks, 'book'),
220 (_("Bookmark"), repo.bookmarks, 'book'),
225 ]
221 ]
226 res = self._create_reference_data(repo, self.db_repo_name, refs_to_create)
222 res = self._create_reference_data(repo, self.db_repo_name, refs_to_create)
227 data = {
223 data = {
228 'more': False,
224 'more': False,
229 'results': res
225 'results': res
230 }
226 }
231 return data
227 return data
232
228
233 @LoginRequired()
229 @LoginRequired()
234 @HasRepoPermissionAnyDecorator(
230 @HasRepoPermissionAnyDecorator(
235 'repository.read', 'repository.write', 'repository.admin')
231 'repository.read', 'repository.write', 'repository.admin')
236 def repo_refs_changelog_data(self):
232 def repo_refs_changelog_data(self):
237 _ = self.request.translate
233 _ = self.request.translate
238 self.load_default_context()
234 self.load_default_context()
239
235
240 repo = self.rhodecode_vcs_repo
236 repo = self.rhodecode_vcs_repo
241
237
242 refs_to_create = [
238 refs_to_create = [
243 (_("Branches"), repo.branches, 'branch'),
239 (_("Branches"), repo.branches, 'branch'),
244 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
240 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
245 # TODO: enable when vcs can handle bookmarks filters
241 # TODO: enable when vcs can handle bookmarks filters
246 # (_("Bookmarks"), repo.bookmarks, "book"),
242 # (_("Bookmarks"), repo.bookmarks, "book"),
247 ]
243 ]
248 res = self._create_reference_data(
244 res = self._create_reference_data(
249 repo, self.db_repo_name, refs_to_create)
245 repo, self.db_repo_name, refs_to_create)
250 data = {
246 data = {
251 'more': False,
247 'more': False,
252 'results': res
248 'results': res
253 }
249 }
254 return data
250 return data
255
251
256 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
252 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
257 format_ref_id = get_format_ref_id(repo)
253 format_ref_id = get_format_ref_id(repo)
258
254
259 result = []
255 result = []
260 for title, refs, ref_type in refs_to_create:
256 for title, refs, ref_type in refs_to_create:
261 if refs:
257 if refs:
262 result.append({
258 result.append({
263 'text': title,
259 'text': title,
264 'children': self._create_reference_items(
260 'children': self._create_reference_items(
265 repo, full_repo_name, refs, ref_type,
261 repo, full_repo_name, refs, ref_type,
266 format_ref_id),
262 format_ref_id),
267 })
263 })
268 return result
264 return result
269
265
270 def _create_reference_items(self, repo, full_repo_name, refs, ref_type, format_ref_id):
266 def _create_reference_items(self, repo, full_repo_name, refs, ref_type, format_ref_id):
271 result = []
267 result = []
272 is_svn = h.is_svn(repo)
268 is_svn = h.is_svn(repo)
273 for ref_name, raw_id in refs.iteritems():
269 for ref_name, raw_id in refs.iteritems():
274 files_url = self._create_files_url(
270 files_url = self._create_files_url(
275 repo, full_repo_name, ref_name, raw_id, is_svn)
271 repo, full_repo_name, ref_name, raw_id, is_svn)
276 result.append({
272 result.append({
277 'text': ref_name,
273 'text': ref_name,
278 'id': format_ref_id(ref_name, raw_id),
274 'id': format_ref_id(ref_name, raw_id),
279 'raw_id': raw_id,
275 'raw_id': raw_id,
280 'type': ref_type,
276 'type': ref_type,
281 'files_url': files_url,
277 'files_url': files_url,
282 'idx': 0,
278 'idx': 0,
283 })
279 })
284 return result
280 return result
285
281
286 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
282 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
287 use_commit_id = '/' in ref_name or is_svn
283 use_commit_id = '/' in ref_name or is_svn
288 return h.route_path(
284 return h.route_path(
289 'repo_files',
285 'repo_files',
290 repo_name=full_repo_name,
286 repo_name=full_repo_name,
291 f_path=ref_name if is_svn else '',
287 f_path=ref_name if is_svn else '',
292 commit_id=raw_id if use_commit_id else ref_name,
288 commit_id=raw_id if use_commit_id else ref_name,
293 _query=dict(at=ref_name))
289 _query=dict(at=ref_name))
@@ -1,778 +1,782 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 import sys
22 import sys
23 import logging
23 import logging
24 import collections
24 import collections
25 import tempfile
25 import tempfile
26 import time
26 import time
27
27
28 from paste.gzipper import make_gzip_middleware
28 from paste.gzipper import make_gzip_middleware
29 import pyramid.events
29 import pyramid.events
30 from pyramid.wsgi import wsgiapp
30 from pyramid.wsgi import wsgiapp
31 from pyramid.authorization import ACLAuthorizationPolicy
31 from pyramid.authorization import ACLAuthorizationPolicy
32 from pyramid.config import Configurator
32 from pyramid.config import Configurator
33 from pyramid.settings import asbool, aslist
33 from pyramid.settings import asbool, aslist
34 from pyramid.httpexceptions import (
34 from pyramid.httpexceptions import (
35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
35 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
36 from pyramid.renderers import render_to_response
36 from pyramid.renderers import render_to_response
37
37
38 from rhodecode.model import meta
38 from rhodecode.model import meta
39 from rhodecode.config import patches
39 from rhodecode.config import patches
40 from rhodecode.config import utils as config_utils
40 from rhodecode.config import utils as config_utils
41 from rhodecode.config.environment import load_pyramid_environment
41 from rhodecode.config.environment import load_pyramid_environment
42
42
43 import rhodecode.events
43 import rhodecode.events
44 from rhodecode.lib.middleware.vcs import VCSMiddleware
44 from rhodecode.lib.middleware.vcs import VCSMiddleware
45 from rhodecode.lib.request import Request
45 from rhodecode.lib.request import Request
46 from rhodecode.lib.vcs import VCSCommunicationError
46 from rhodecode.lib.vcs import VCSCommunicationError
47 from rhodecode.lib.exceptions import VCSServerUnavailable
47 from rhodecode.lib.exceptions import VCSServerUnavailable
48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 from rhodecode.lib.middleware.https_fixup import HttpsFixup
49 from rhodecode.lib.middleware.https_fixup import HttpsFixup
50 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
51 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
51 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
52 from rhodecode.lib.exc_tracking import store_exception
52 from rhodecode.lib.exc_tracking import store_exception
53 from rhodecode.subscribers import (
53 from rhodecode.subscribers import (
54 scan_repositories_if_enabled, write_js_routes_if_enabled,
54 scan_repositories_if_enabled, write_js_routes_if_enabled,
55 write_metadata_if_needed, write_usage_data)
55 write_metadata_if_needed, write_usage_data)
56
56
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60
60
61 def is_http_error(response):
61 def is_http_error(response):
62 # error which should have traceback
62 # error which should have traceback
63 return response.status_code > 499
63 return response.status_code > 499
64
64
65
65
66 def should_load_all():
66 def should_load_all():
67 """
67 """
68 Returns if all application components should be loaded. In some cases it's
68 Returns if all application components should be loaded. In some cases it's
69 desired to skip apps loading for faster shell script execution
69 desired to skip apps loading for faster shell script execution
70 """
70 """
71 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
71 ssh_cmd = os.environ.get('RC_CMD_SSH_WRAPPER')
72 if ssh_cmd:
72 if ssh_cmd:
73 return False
73 return False
74
74
75 return True
75 return True
76
76
77
77
78 def make_pyramid_app(global_config, **settings):
78 def make_pyramid_app(global_config, **settings):
79 """
79 """
80 Constructs the WSGI application based on Pyramid.
80 Constructs the WSGI application based on Pyramid.
81
81
82 Specials:
82 Specials:
83
83
84 * The application can also be integrated like a plugin via the call to
84 * The application can also be integrated like a plugin via the call to
85 `includeme`. This is accompanied with the other utility functions which
85 `includeme`. This is accompanied with the other utility functions which
86 are called. Changing this should be done with great care to not break
86 are called. Changing this should be done with great care to not break
87 cases when these fragments are assembled from another place.
87 cases when these fragments are assembled from another place.
88
88
89 """
89 """
90
90
91 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
91 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
92 # will be replaced by the value of the environment variable "NAME" in this case.
92 # will be replaced by the value of the environment variable "NAME" in this case.
93 start_time = time.time()
93 start_time = time.time()
94 log.info('Pyramid app config starting')
94 log.info('Pyramid app config starting')
95
95
96 debug = asbool(global_config.get('debug'))
96 debug = asbool(global_config.get('debug'))
97 if debug:
97 if debug:
98 enable_debug()
98 enable_debug()
99
99
100 environ = {'ENV_{}'.format(key): value for key, value in os.environ.items()}
100 environ = {'ENV_{}'.format(key): value for key, value in os.environ.items()}
101
101
102 global_config = _substitute_values(global_config, environ)
102 global_config = _substitute_values(global_config, environ)
103 settings = _substitute_values(settings, environ)
103 settings = _substitute_values(settings, environ)
104
104
105 sanitize_settings_and_apply_defaults(global_config, settings)
105 sanitize_settings_and_apply_defaults(global_config, settings)
106
106
107 config = Configurator(settings=settings)
107 config = Configurator(settings=settings)
108
108
109 # Apply compatibility patches
109 # Apply compatibility patches
110 patches.inspect_getargspec()
110 patches.inspect_getargspec()
111
111
112 load_pyramid_environment(global_config, settings)
112 load_pyramid_environment(global_config, settings)
113
113
114 # Static file view comes first
114 # Static file view comes first
115 includeme_first(config)
115 includeme_first(config)
116
116
117 includeme(config)
117 includeme(config)
118
118
119 pyramid_app = config.make_wsgi_app()
119 pyramid_app = config.make_wsgi_app()
120 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
120 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
121 pyramid_app.config = config
121 pyramid_app.config = config
122
122
123 config.configure_celery(global_config['__file__'])
123 config.configure_celery(global_config['__file__'])
124
124
125 # creating the app uses a connection - return it after we are done
125 # creating the app uses a connection - return it after we are done
126 meta.Session.remove()
126 meta.Session.remove()
127 total_time = time.time() - start_time
127 total_time = time.time() - start_time
128 log.info('Pyramid app `%s` created and configured in %.2fs',
128 log.info('Pyramid app `%s` created and configured in %.2fs',
129 pyramid_app.func_name, total_time)
129 pyramid_app.func_name, total_time)
130
130
131 return pyramid_app
131 return pyramid_app
132
132
133
133
134 def not_found_view(request):
134 def not_found_view(request):
135 """
135 """
136 This creates the view which should be registered as not-found-view to
136 This creates the view which should be registered as not-found-view to
137 pyramid.
137 pyramid.
138 """
138 """
139
139
140 if not getattr(request, 'vcs_call', None):
140 if not getattr(request, 'vcs_call', None):
141 # handle like regular case with our error_handler
141 # handle like regular case with our error_handler
142 return error_handler(HTTPNotFound(), request)
142 return error_handler(HTTPNotFound(), request)
143
143
144 # handle not found view as a vcs call
144 # handle not found view as a vcs call
145 settings = request.registry.settings
145 settings = request.registry.settings
146 ae_client = getattr(request, 'ae_client', None)
146 ae_client = getattr(request, 'ae_client', None)
147 vcs_app = VCSMiddleware(
147 vcs_app = VCSMiddleware(
148 HTTPNotFound(), request.registry, settings,
148 HTTPNotFound(), request.registry, settings,
149 appenlight_client=ae_client)
149 appenlight_client=ae_client)
150
150
151 return wsgiapp(vcs_app)(None, request)
151 return wsgiapp(vcs_app)(None, request)
152
152
153
153
154 def error_handler(exception, request):
154 def error_handler(exception, request):
155 import rhodecode
155 import rhodecode
156 from rhodecode.lib import helpers
156 from rhodecode.lib import helpers
157
157
158 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
158 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
159
159
160 base_response = HTTPInternalServerError()
160 base_response = HTTPInternalServerError()
161 # prefer original exception for the response since it may have headers set
161 # prefer original exception for the response since it may have headers set
162 if isinstance(exception, HTTPException):
162 if isinstance(exception, HTTPException):
163 base_response = exception
163 base_response = exception
164 elif isinstance(exception, VCSCommunicationError):
164 elif isinstance(exception, VCSCommunicationError):
165 base_response = VCSServerUnavailable()
165 base_response = VCSServerUnavailable()
166
166
167 if is_http_error(base_response):
167 if is_http_error(base_response):
168 log.exception(
168 log.exception(
169 'error occurred handling this request for path: %s', request.path)
169 'error occurred handling this request for path: %s', request.path)
170
170
171 error_explanation = base_response.explanation or str(base_response)
171 error_explanation = base_response.explanation or str(base_response)
172 if base_response.status_code == 404:
172 if base_response.status_code == 404:
173 error_explanation += " Optionally you don't have permission to access this page."
173 error_explanation += " Optionally you don't have permission to access this page."
174 c = AttributeDict()
174 c = AttributeDict()
175 c.error_message = base_response.status
175 c.error_message = base_response.status
176 c.error_explanation = error_explanation
176 c.error_explanation = error_explanation
177 c.visual = AttributeDict()
177 c.visual = AttributeDict()
178
178
179 c.visual.rhodecode_support_url = (
179 c.visual.rhodecode_support_url = (
180 request.registry.settings.get('rhodecode_support_url') or
180 request.registry.settings.get('rhodecode_support_url') or
181 request.route_url('rhodecode_support')
181 request.route_url('rhodecode_support')
182 )
182 )
183 c.redirect_time = 0
183 c.redirect_time = 0
184 c.rhodecode_name = rhodecode_title
184 c.rhodecode_name = rhodecode_title
185 if not c.rhodecode_name:
185 if not c.rhodecode_name:
186 c.rhodecode_name = 'Rhodecode'
186 c.rhodecode_name = 'Rhodecode'
187
187
188 c.causes = []
188 c.causes = []
189 if is_http_error(base_response):
189 if is_http_error(base_response):
190 c.causes.append('Server is overloaded.')
190 c.causes.append('Server is overloaded.')
191 c.causes.append('Server database connection is lost.')
191 c.causes.append('Server database connection is lost.')
192 c.causes.append('Server expected unhandled error.')
192 c.causes.append('Server expected unhandled error.')
193
193
194 if hasattr(base_response, 'causes'):
194 if hasattr(base_response, 'causes'):
195 c.causes = base_response.causes
195 c.causes = base_response.causes
196
196
197 c.messages = helpers.flash.pop_messages(request=request)
197 c.messages = helpers.flash.pop_messages(request=request)
198
198
199 exc_info = sys.exc_info()
199 exc_info = sys.exc_info()
200 c.exception_id = id(exc_info)
200 c.exception_id = id(exc_info)
201 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
201 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
202 or base_response.status_code > 499
202 or base_response.status_code > 499
203 c.exception_id_url = request.route_url(
203 c.exception_id_url = request.route_url(
204 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
204 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
205
205
206 if c.show_exception_id:
206 if c.show_exception_id:
207 store_exception(c.exception_id, exc_info)
207 store_exception(c.exception_id, exc_info)
208
208
209 response = render_to_response(
209 response = render_to_response(
210 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
210 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
211 response=base_response)
211 response=base_response)
212
212
213 return response
213 return response
214
214
215
215
216 def includeme_first(config):
216 def includeme_first(config):
217 # redirect automatic browser favicon.ico requests to correct place
217 # redirect automatic browser favicon.ico requests to correct place
218 def favicon_redirect(context, request):
218 def favicon_redirect(context, request):
219 return HTTPFound(
219 return HTTPFound(
220 request.static_path('rhodecode:public/images/favicon.ico'))
220 request.static_path('rhodecode:public/images/favicon.ico'))
221
221
222 config.add_view(favicon_redirect, route_name='favicon')
222 config.add_view(favicon_redirect, route_name='favicon')
223 config.add_route('favicon', '/favicon.ico')
223 config.add_route('favicon', '/favicon.ico')
224
224
225 def robots_redirect(context, request):
225 def robots_redirect(context, request):
226 return HTTPFound(
226 return HTTPFound(
227 request.static_path('rhodecode:public/robots.txt'))
227 request.static_path('rhodecode:public/robots.txt'))
228
228
229 config.add_view(robots_redirect, route_name='robots')
229 config.add_view(robots_redirect, route_name='robots')
230 config.add_route('robots', '/robots.txt')
230 config.add_route('robots', '/robots.txt')
231
231
232 config.add_static_view(
232 config.add_static_view(
233 '_static/deform', 'deform:static')
233 '_static/deform', 'deform:static')
234 config.add_static_view(
234 config.add_static_view(
235 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
235 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
236
236
237
237
238 def includeme(config, auth_resources=None):
238 def includeme(config, auth_resources=None):
239 from rhodecode.lib.celerylib.loader import configure_celery
239 from rhodecode.lib.celerylib.loader import configure_celery
240 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
240 log.debug('Initializing main includeme from %s', os.path.basename(__file__))
241 settings = config.registry.settings
241 settings = config.registry.settings
242 config.set_request_factory(Request)
242 config.set_request_factory(Request)
243
243
244 # plugin information
244 # plugin information
245 config.registry.rhodecode_plugins = collections.OrderedDict()
245 config.registry.rhodecode_plugins = collections.OrderedDict()
246
246
247 config.add_directive(
247 config.add_directive(
248 'register_rhodecode_plugin', register_rhodecode_plugin)
248 'register_rhodecode_plugin', register_rhodecode_plugin)
249
249
250 config.add_directive('configure_celery', configure_celery)
250 config.add_directive('configure_celery', configure_celery)
251
251
252 if asbool(settings.get('appenlight', 'false')):
252 if asbool(settings.get('appenlight', 'false')):
253 config.include('appenlight_client.ext.pyramid_tween')
253 config.include('appenlight_client.ext.pyramid_tween')
254
254
255 load_all = should_load_all()
255 load_all = should_load_all()
256
256
257 # Includes which are required. The application would fail without them.
257 # Includes which are required. The application would fail without them.
258 config.include('pyramid_mako')
258 config.include('pyramid_mako')
259 config.include('rhodecode.lib.rc_beaker')
259 config.include('rhodecode.lib.rc_beaker')
260 config.include('rhodecode.lib.rc_cache')
260 config.include('rhodecode.lib.rc_cache')
261 config.include('rhodecode.apps._base.navigation')
261 config.include('rhodecode.apps._base.navigation')
262 config.include('rhodecode.apps._base.subscribers')
262 config.include('rhodecode.apps._base.subscribers')
263 config.include('rhodecode.tweens')
263 config.include('rhodecode.tweens')
264 config.include('rhodecode.authentication')
264 config.include('rhodecode.authentication')
265
265
266 if load_all:
266 if load_all:
267 ce_auth_resources = [
267 ce_auth_resources = [
268 'rhodecode.authentication.plugins.auth_crowd',
268 'rhodecode.authentication.plugins.auth_crowd',
269 'rhodecode.authentication.plugins.auth_headers',
269 'rhodecode.authentication.plugins.auth_headers',
270 'rhodecode.authentication.plugins.auth_jasig_cas',
270 'rhodecode.authentication.plugins.auth_jasig_cas',
271 'rhodecode.authentication.plugins.auth_ldap',
271 'rhodecode.authentication.plugins.auth_ldap',
272 'rhodecode.authentication.plugins.auth_pam',
272 'rhodecode.authentication.plugins.auth_pam',
273 'rhodecode.authentication.plugins.auth_rhodecode',
273 'rhodecode.authentication.plugins.auth_rhodecode',
274 'rhodecode.authentication.plugins.auth_token',
274 'rhodecode.authentication.plugins.auth_token',
275 ]
275 ]
276
276
277 # load CE authentication plugins
277 # load CE authentication plugins
278
278
279 if auth_resources:
279 if auth_resources:
280 ce_auth_resources.extend(auth_resources)
280 ce_auth_resources.extend(auth_resources)
281
281
282 for resource in ce_auth_resources:
282 for resource in ce_auth_resources:
283 config.include(resource)
283 config.include(resource)
284
284
285 # Auto discover authentication plugins and include their configuration.
285 # Auto discover authentication plugins and include their configuration.
286 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
286 if asbool(settings.get('auth_plugin.import_legacy_plugins', 'true')):
287 from rhodecode.authentication import discover_legacy_plugins
287 from rhodecode.authentication import discover_legacy_plugins
288 discover_legacy_plugins(config)
288 discover_legacy_plugins(config)
289
289
290 # apps
290 # apps
291 if load_all:
291 if load_all:
292 config.include('rhodecode.api')
292 config.include('rhodecode.api')
293 config.include('rhodecode.apps._base')
293 config.include('rhodecode.apps._base')
294 config.include('rhodecode.apps.hovercards')
294 config.include('rhodecode.apps.hovercards')
295 config.include('rhodecode.apps.ops')
295 config.include('rhodecode.apps.ops')
296 config.include('rhodecode.apps.channelstream')
296 config.include('rhodecode.apps.channelstream')
297 config.include('rhodecode.apps.file_store')
297 config.include('rhodecode.apps.file_store')
298 config.include('rhodecode.apps.admin')
298 config.include('rhodecode.apps.admin')
299 config.include('rhodecode.apps.login')
299 config.include('rhodecode.apps.login')
300 config.include('rhodecode.apps.home')
300 config.include('rhodecode.apps.home')
301 config.include('rhodecode.apps.journal')
301 config.include('rhodecode.apps.journal')
302
302
303 config.include('rhodecode.apps.repository')
303 config.include('rhodecode.apps.repository')
304 config.include('rhodecode.apps.repo_group')
304 config.include('rhodecode.apps.repo_group')
305 config.include('rhodecode.apps.user_group')
305 config.include('rhodecode.apps.user_group')
306 config.include('rhodecode.apps.search')
306 config.include('rhodecode.apps.search')
307 config.include('rhodecode.apps.user_profile')
307 config.include('rhodecode.apps.user_profile')
308 config.include('rhodecode.apps.user_group_profile')
308 config.include('rhodecode.apps.user_group_profile')
309 config.include('rhodecode.apps.my_account')
309 config.include('rhodecode.apps.my_account')
310 config.include('rhodecode.apps.gist')
310 config.include('rhodecode.apps.gist')
311
311
312 config.include('rhodecode.apps.svn_support')
312 config.include('rhodecode.apps.svn_support')
313 config.include('rhodecode.apps.ssh_support')
313 config.include('rhodecode.apps.ssh_support')
314 config.include('rhodecode.apps.debug_style')
314 config.include('rhodecode.apps.debug_style')
315
315
316 if load_all:
316 if load_all:
317 config.include('rhodecode.integrations')
317 config.include('rhodecode.integrations')
318
318
319 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
319 config.add_route('rhodecode_support', 'https://rhodecode.com/help/', static=True)
320 config.add_translation_dirs('rhodecode:i18n/')
320 config.add_translation_dirs('rhodecode:i18n/')
321 settings['default_locale_name'] = settings.get('lang', 'en')
321 settings['default_locale_name'] = settings.get('lang', 'en')
322
322
323 # Add subscribers.
323 # Add subscribers.
324 if load_all:
324 if load_all:
325 config.add_subscriber(scan_repositories_if_enabled,
325 config.add_subscriber(scan_repositories_if_enabled,
326 pyramid.events.ApplicationCreated)
326 pyramid.events.ApplicationCreated)
327 config.add_subscriber(write_metadata_if_needed,
327 config.add_subscriber(write_metadata_if_needed,
328 pyramid.events.ApplicationCreated)
328 pyramid.events.ApplicationCreated)
329 config.add_subscriber(write_usage_data,
329 config.add_subscriber(write_usage_data,
330 pyramid.events.ApplicationCreated)
330 pyramid.events.ApplicationCreated)
331 config.add_subscriber(write_js_routes_if_enabled,
331 config.add_subscriber(write_js_routes_if_enabled,
332 pyramid.events.ApplicationCreated)
332 pyramid.events.ApplicationCreated)
333
333
334 # request custom methods
334 # request custom methods
335 config.add_request_method(
335 config.add_request_method(
336 'rhodecode.lib.partial_renderer.get_partial_renderer',
336 'rhodecode.lib.partial_renderer.get_partial_renderer',
337 'get_partial_renderer')
337 'get_partial_renderer')
338
338
339 config.add_request_method(
339 config.add_request_method(
340 'rhodecode.lib.request_counter.get_request_counter',
340 'rhodecode.lib.request_counter.get_request_counter',
341 'request_count')
341 'request_count')
342
342
343 config.add_request_method(
344 'rhodecode.lib._vendor.statsd.get_statsd_client',
345 'statsd', reify=True)
346
343 # Set the authorization policy.
347 # Set the authorization policy.
344 authz_policy = ACLAuthorizationPolicy()
348 authz_policy = ACLAuthorizationPolicy()
345 config.set_authorization_policy(authz_policy)
349 config.set_authorization_policy(authz_policy)
346
350
347 # Set the default renderer for HTML templates to mako.
351 # Set the default renderer for HTML templates to mako.
348 config.add_mako_renderer('.html')
352 config.add_mako_renderer('.html')
349
353
350 config.add_renderer(
354 config.add_renderer(
351 name='json_ext',
355 name='json_ext',
352 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
356 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
353
357
354 config.add_renderer(
358 config.add_renderer(
355 name='string_html',
359 name='string_html',
356 factory='rhodecode.lib.string_renderer.html')
360 factory='rhodecode.lib.string_renderer.html')
357
361
358 # include RhodeCode plugins
362 # include RhodeCode plugins
359 includes = aslist(settings.get('rhodecode.includes', []))
363 includes = aslist(settings.get('rhodecode.includes', []))
360 for inc in includes:
364 for inc in includes:
361 config.include(inc)
365 config.include(inc)
362
366
363 # custom not found view, if our pyramid app doesn't know how to handle
367 # custom not found view, if our pyramid app doesn't know how to handle
364 # the request pass it to potential VCS handling ap
368 # the request pass it to potential VCS handling ap
365 config.add_notfound_view(not_found_view)
369 config.add_notfound_view(not_found_view)
366 if not settings.get('debugtoolbar.enabled', False):
370 if not settings.get('debugtoolbar.enabled', False):
367 # disabled debugtoolbar handle all exceptions via the error_handlers
371 # disabled debugtoolbar handle all exceptions via the error_handlers
368 config.add_view(error_handler, context=Exception)
372 config.add_view(error_handler, context=Exception)
369
373
370 # all errors including 403/404/50X
374 # all errors including 403/404/50X
371 config.add_view(error_handler, context=HTTPError)
375 config.add_view(error_handler, context=HTTPError)
372
376
373
377
374 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
378 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
375 """
379 """
376 Apply outer WSGI middlewares around the application.
380 Apply outer WSGI middlewares around the application.
377 """
381 """
378 registry = config.registry
382 registry = config.registry
379 settings = registry.settings
383 settings = registry.settings
380
384
381 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
385 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
382 pyramid_app = HttpsFixup(pyramid_app, settings)
386 pyramid_app = HttpsFixup(pyramid_app, settings)
383
387
384 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
388 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
385 pyramid_app, settings)
389 pyramid_app, settings)
386 registry.ae_client = _ae_client
390 registry.ae_client = _ae_client
387
391
388 if settings['gzip_responses']:
392 if settings['gzip_responses']:
389 pyramid_app = make_gzip_middleware(
393 pyramid_app = make_gzip_middleware(
390 pyramid_app, settings, compress_level=1)
394 pyramid_app, settings, compress_level=1)
391
395
392 # this should be the outer most middleware in the wsgi stack since
396 # this should be the outer most middleware in the wsgi stack since
393 # middleware like Routes make database calls
397 # middleware like Routes make database calls
394 def pyramid_app_with_cleanup(environ, start_response):
398 def pyramid_app_with_cleanup(environ, start_response):
395 try:
399 try:
396 return pyramid_app(environ, start_response)
400 return pyramid_app(environ, start_response)
397 finally:
401 finally:
398 # Dispose current database session and rollback uncommitted
402 # Dispose current database session and rollback uncommitted
399 # transactions.
403 # transactions.
400 meta.Session.remove()
404 meta.Session.remove()
401
405
402 # In a single threaded mode server, on non sqlite db we should have
406 # In a single threaded mode server, on non sqlite db we should have
403 # '0 Current Checked out connections' at the end of a request,
407 # '0 Current Checked out connections' at the end of a request,
404 # if not, then something, somewhere is leaving a connection open
408 # if not, then something, somewhere is leaving a connection open
405 pool = meta.Base.metadata.bind.engine.pool
409 pool = meta.Base.metadata.bind.engine.pool
406 log.debug('sa pool status: %s', pool.status())
410 log.debug('sa pool status: %s', pool.status())
407 log.debug('Request processing finalized')
411 log.debug('Request processing finalized')
408
412
409 return pyramid_app_with_cleanup
413 return pyramid_app_with_cleanup
410
414
411
415
412 def sanitize_settings_and_apply_defaults(global_config, settings):
416 def sanitize_settings_and_apply_defaults(global_config, settings):
413 """
417 """
414 Applies settings defaults and does all type conversion.
418 Applies settings defaults and does all type conversion.
415
419
416 We would move all settings parsing and preparation into this place, so that
420 We would move all settings parsing and preparation into this place, so that
417 we have only one place left which deals with this part. The remaining parts
421 we have only one place left which deals with this part. The remaining parts
418 of the application would start to rely fully on well prepared settings.
422 of the application would start to rely fully on well prepared settings.
419
423
420 This piece would later be split up per topic to avoid a big fat monster
424 This piece would later be split up per topic to avoid a big fat monster
421 function.
425 function.
422 """
426 """
423
427
424 settings.setdefault('rhodecode.edition', 'Community Edition')
428 settings.setdefault('rhodecode.edition', 'Community Edition')
425 settings.setdefault('rhodecode.edition_id', 'CE')
429 settings.setdefault('rhodecode.edition_id', 'CE')
426
430
427 if 'mako.default_filters' not in settings:
431 if 'mako.default_filters' not in settings:
428 # set custom default filters if we don't have it defined
432 # set custom default filters if we don't have it defined
429 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
433 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
430 settings['mako.default_filters'] = 'h_filter'
434 settings['mako.default_filters'] = 'h_filter'
431
435
432 if 'mako.directories' not in settings:
436 if 'mako.directories' not in settings:
433 mako_directories = settings.setdefault('mako.directories', [
437 mako_directories = settings.setdefault('mako.directories', [
434 # Base templates of the original application
438 # Base templates of the original application
435 'rhodecode:templates',
439 'rhodecode:templates',
436 ])
440 ])
437 log.debug(
441 log.debug(
438 "Using the following Mako template directories: %s",
442 "Using the following Mako template directories: %s",
439 mako_directories)
443 mako_directories)
440
444
441 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
445 # NOTE(marcink): fix redis requirement for schema of connection since 3.X
442 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
446 if 'beaker.session.type' in settings and settings['beaker.session.type'] == 'ext:redis':
443 raw_url = settings['beaker.session.url']
447 raw_url = settings['beaker.session.url']
444 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
448 if not raw_url.startswith(('redis://', 'rediss://', 'unix://')):
445 settings['beaker.session.url'] = 'redis://' + raw_url
449 settings['beaker.session.url'] = 'redis://' + raw_url
446
450
447 # Default includes, possible to change as a user
451 # Default includes, possible to change as a user
448 pyramid_includes = settings.setdefault('pyramid.includes', [])
452 pyramid_includes = settings.setdefault('pyramid.includes', [])
449 log.debug(
453 log.debug(
450 "Using the following pyramid.includes: %s",
454 "Using the following pyramid.includes: %s",
451 pyramid_includes)
455 pyramid_includes)
452
456
453 # TODO: johbo: Re-think this, usually the call to config.include
457 # TODO: johbo: Re-think this, usually the call to config.include
454 # should allow to pass in a prefix.
458 # should allow to pass in a prefix.
455 settings.setdefault('rhodecode.api.url', '/_admin/api')
459 settings.setdefault('rhodecode.api.url', '/_admin/api')
456 settings.setdefault('__file__', global_config.get('__file__'))
460 settings.setdefault('__file__', global_config.get('__file__'))
457
461
458 # Sanitize generic settings.
462 # Sanitize generic settings.
459 _list_setting(settings, 'default_encoding', 'UTF-8')
463 _list_setting(settings, 'default_encoding', 'UTF-8')
460 _bool_setting(settings, 'is_test', 'false')
464 _bool_setting(settings, 'is_test', 'false')
461 _bool_setting(settings, 'gzip_responses', 'false')
465 _bool_setting(settings, 'gzip_responses', 'false')
462
466
463 # Call split out functions that sanitize settings for each topic.
467 # Call split out functions that sanitize settings for each topic.
464 _sanitize_appenlight_settings(settings)
468 _sanitize_appenlight_settings(settings)
465 _sanitize_vcs_settings(settings)
469 _sanitize_vcs_settings(settings)
466 _sanitize_cache_settings(settings)
470 _sanitize_cache_settings(settings)
467
471
468 # configure instance id
472 # configure instance id
469 config_utils.set_instance_id(settings)
473 config_utils.set_instance_id(settings)
470
474
471 return settings
475 return settings
472
476
473
477
474 def enable_debug():
478 def enable_debug():
475 """
479 """
476 Helper to enable debug on running instance
480 Helper to enable debug on running instance
477 :return:
481 :return:
478 """
482 """
479 import tempfile
483 import tempfile
480 import textwrap
484 import textwrap
481 import logging.config
485 import logging.config
482
486
483 ini_template = textwrap.dedent("""
487 ini_template = textwrap.dedent("""
484 #####################################
488 #####################################
485 ### DEBUG LOGGING CONFIGURATION ####
489 ### DEBUG LOGGING CONFIGURATION ####
486 #####################################
490 #####################################
487 [loggers]
491 [loggers]
488 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
492 keys = root, sqlalchemy, beaker, celery, rhodecode, ssh_wrapper
489
493
490 [handlers]
494 [handlers]
491 keys = console, console_sql
495 keys = console, console_sql
492
496
493 [formatters]
497 [formatters]
494 keys = generic, color_formatter, color_formatter_sql
498 keys = generic, color_formatter, color_formatter_sql
495
499
496 #############
500 #############
497 ## LOGGERS ##
501 ## LOGGERS ##
498 #############
502 #############
499 [logger_root]
503 [logger_root]
500 level = NOTSET
504 level = NOTSET
501 handlers = console
505 handlers = console
502
506
503 [logger_sqlalchemy]
507 [logger_sqlalchemy]
504 level = INFO
508 level = INFO
505 handlers = console_sql
509 handlers = console_sql
506 qualname = sqlalchemy.engine
510 qualname = sqlalchemy.engine
507 propagate = 0
511 propagate = 0
508
512
509 [logger_beaker]
513 [logger_beaker]
510 level = DEBUG
514 level = DEBUG
511 handlers =
515 handlers =
512 qualname = beaker.container
516 qualname = beaker.container
513 propagate = 1
517 propagate = 1
514
518
515 [logger_rhodecode]
519 [logger_rhodecode]
516 level = DEBUG
520 level = DEBUG
517 handlers =
521 handlers =
518 qualname = rhodecode
522 qualname = rhodecode
519 propagate = 1
523 propagate = 1
520
524
521 [logger_ssh_wrapper]
525 [logger_ssh_wrapper]
522 level = DEBUG
526 level = DEBUG
523 handlers =
527 handlers =
524 qualname = ssh_wrapper
528 qualname = ssh_wrapper
525 propagate = 1
529 propagate = 1
526
530
527 [logger_celery]
531 [logger_celery]
528 level = DEBUG
532 level = DEBUG
529 handlers =
533 handlers =
530 qualname = celery
534 qualname = celery
531
535
532
536
533 ##############
537 ##############
534 ## HANDLERS ##
538 ## HANDLERS ##
535 ##############
539 ##############
536
540
537 [handler_console]
541 [handler_console]
538 class = StreamHandler
542 class = StreamHandler
539 args = (sys.stderr, )
543 args = (sys.stderr, )
540 level = DEBUG
544 level = DEBUG
541 formatter = color_formatter
545 formatter = color_formatter
542
546
543 [handler_console_sql]
547 [handler_console_sql]
544 # "level = DEBUG" logs SQL queries and results.
548 # "level = DEBUG" logs SQL queries and results.
545 # "level = INFO" logs SQL queries.
549 # "level = INFO" logs SQL queries.
546 # "level = WARN" logs neither. (Recommended for production systems.)
550 # "level = WARN" logs neither. (Recommended for production systems.)
547 class = StreamHandler
551 class = StreamHandler
548 args = (sys.stderr, )
552 args = (sys.stderr, )
549 level = WARN
553 level = WARN
550 formatter = color_formatter_sql
554 formatter = color_formatter_sql
551
555
552 ################
556 ################
553 ## FORMATTERS ##
557 ## FORMATTERS ##
554 ################
558 ################
555
559
556 [formatter_generic]
560 [formatter_generic]
557 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
561 class = rhodecode.lib.logging_formatter.ExceptionAwareFormatter
558 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
562 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
559 datefmt = %Y-%m-%d %H:%M:%S
563 datefmt = %Y-%m-%d %H:%M:%S
560
564
561 [formatter_color_formatter]
565 [formatter_color_formatter]
562 class = rhodecode.lib.logging_formatter.ColorRequestTrackingFormatter
566 class = rhodecode.lib.logging_formatter.ColorRequestTrackingFormatter
563 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
567 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s | %(req_id)s
564 datefmt = %Y-%m-%d %H:%M:%S
568 datefmt = %Y-%m-%d %H:%M:%S
565
569
566 [formatter_color_formatter_sql]
570 [formatter_color_formatter_sql]
567 class = rhodecode.lib.logging_formatter.ColorFormatterSql
571 class = rhodecode.lib.logging_formatter.ColorFormatterSql
568 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
572 format = %(asctime)s.%(msecs)03d [%(process)d] %(levelname)-5.5s [%(name)s] %(message)s
569 datefmt = %Y-%m-%d %H:%M:%S
573 datefmt = %Y-%m-%d %H:%M:%S
570 """)
574 """)
571
575
572 with tempfile.NamedTemporaryFile(prefix='rc_debug_logging_', suffix='.ini',
576 with tempfile.NamedTemporaryFile(prefix='rc_debug_logging_', suffix='.ini',
573 delete=False) as f:
577 delete=False) as f:
574 log.info('Saved Temporary DEBUG config at %s', f.name)
578 log.info('Saved Temporary DEBUG config at %s', f.name)
575 f.write(ini_template)
579 f.write(ini_template)
576
580
577 logging.config.fileConfig(f.name)
581 logging.config.fileConfig(f.name)
578 log.debug('DEBUG MODE ON')
582 log.debug('DEBUG MODE ON')
579 os.remove(f.name)
583 os.remove(f.name)
580
584
581
585
582 def _sanitize_appenlight_settings(settings):
586 def _sanitize_appenlight_settings(settings):
583 _bool_setting(settings, 'appenlight', 'false')
587 _bool_setting(settings, 'appenlight', 'false')
584
588
585
589
586 def _sanitize_vcs_settings(settings):
590 def _sanitize_vcs_settings(settings):
587 """
591 """
588 Applies settings defaults and does type conversion for all VCS related
592 Applies settings defaults and does type conversion for all VCS related
589 settings.
593 settings.
590 """
594 """
591 _string_setting(settings, 'vcs.svn.compatible_version', '')
595 _string_setting(settings, 'vcs.svn.compatible_version', '')
592 _string_setting(settings, 'vcs.hooks.protocol', 'http')
596 _string_setting(settings, 'vcs.hooks.protocol', 'http')
593 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
597 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
594 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
598 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
595 _string_setting(settings, 'vcs.server', '')
599 _string_setting(settings, 'vcs.server', '')
596 _string_setting(settings, 'vcs.server.protocol', 'http')
600 _string_setting(settings, 'vcs.server.protocol', 'http')
597 _bool_setting(settings, 'startup.import_repos', 'false')
601 _bool_setting(settings, 'startup.import_repos', 'false')
598 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
602 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
599 _bool_setting(settings, 'vcs.server.enable', 'true')
603 _bool_setting(settings, 'vcs.server.enable', 'true')
600 _bool_setting(settings, 'vcs.start_server', 'false')
604 _bool_setting(settings, 'vcs.start_server', 'false')
601 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
605 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
602 _int_setting(settings, 'vcs.connection_timeout', 3600)
606 _int_setting(settings, 'vcs.connection_timeout', 3600)
603
607
604 # Support legacy values of vcs.scm_app_implementation. Legacy
608 # Support legacy values of vcs.scm_app_implementation. Legacy
605 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
609 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http', or
606 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
610 # disabled since 4.13 'vcsserver.scm_app' which is now mapped to 'http'.
607 scm_app_impl = settings['vcs.scm_app_implementation']
611 scm_app_impl = settings['vcs.scm_app_implementation']
608 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
612 if scm_app_impl in ['rhodecode.lib.middleware.utils.scm_app_http', 'vcsserver.scm_app']:
609 settings['vcs.scm_app_implementation'] = 'http'
613 settings['vcs.scm_app_implementation'] = 'http'
610
614
611
615
612 def _sanitize_cache_settings(settings):
616 def _sanitize_cache_settings(settings):
613 temp_store = tempfile.gettempdir()
617 temp_store = tempfile.gettempdir()
614 default_cache_dir = os.path.join(temp_store, 'rc_cache')
618 default_cache_dir = os.path.join(temp_store, 'rc_cache')
615
619
616 # save default, cache dir, and use it for all backends later.
620 # save default, cache dir, and use it for all backends later.
617 default_cache_dir = _string_setting(
621 default_cache_dir = _string_setting(
618 settings,
622 settings,
619 'cache_dir',
623 'cache_dir',
620 default_cache_dir, lower=False, default_when_empty=True)
624 default_cache_dir, lower=False, default_when_empty=True)
621
625
622 # ensure we have our dir created
626 # ensure we have our dir created
623 if not os.path.isdir(default_cache_dir):
627 if not os.path.isdir(default_cache_dir):
624 os.makedirs(default_cache_dir, mode=0o755)
628 os.makedirs(default_cache_dir, mode=0o755)
625
629
626 # exception store cache
630 # exception store cache
627 _string_setting(
631 _string_setting(
628 settings,
632 settings,
629 'exception_tracker.store_path',
633 'exception_tracker.store_path',
630 temp_store, lower=False, default_when_empty=True)
634 temp_store, lower=False, default_when_empty=True)
631 _bool_setting(
635 _bool_setting(
632 settings,
636 settings,
633 'exception_tracker.send_email',
637 'exception_tracker.send_email',
634 'false')
638 'false')
635 _string_setting(
639 _string_setting(
636 settings,
640 settings,
637 'exception_tracker.email_prefix',
641 'exception_tracker.email_prefix',
638 '[RHODECODE ERROR]', lower=False, default_when_empty=True)
642 '[RHODECODE ERROR]', lower=False, default_when_empty=True)
639
643
640 # cache_perms
644 # cache_perms
641 _string_setting(
645 _string_setting(
642 settings,
646 settings,
643 'rc_cache.cache_perms.backend',
647 'rc_cache.cache_perms.backend',
644 'dogpile.cache.rc.file_namespace', lower=False)
648 'dogpile.cache.rc.file_namespace', lower=False)
645 _int_setting(
649 _int_setting(
646 settings,
650 settings,
647 'rc_cache.cache_perms.expiration_time',
651 'rc_cache.cache_perms.expiration_time',
648 60)
652 60)
649 _string_setting(
653 _string_setting(
650 settings,
654 settings,
651 'rc_cache.cache_perms.arguments.filename',
655 'rc_cache.cache_perms.arguments.filename',
652 os.path.join(default_cache_dir, 'rc_cache_1'), lower=False)
656 os.path.join(default_cache_dir, 'rc_cache_1'), lower=False)
653
657
654 # cache_repo
658 # cache_repo
655 _string_setting(
659 _string_setting(
656 settings,
660 settings,
657 'rc_cache.cache_repo.backend',
661 'rc_cache.cache_repo.backend',
658 'dogpile.cache.rc.file_namespace', lower=False)
662 'dogpile.cache.rc.file_namespace', lower=False)
659 _int_setting(
663 _int_setting(
660 settings,
664 settings,
661 'rc_cache.cache_repo.expiration_time',
665 'rc_cache.cache_repo.expiration_time',
662 60)
666 60)
663 _string_setting(
667 _string_setting(
664 settings,
668 settings,
665 'rc_cache.cache_repo.arguments.filename',
669 'rc_cache.cache_repo.arguments.filename',
666 os.path.join(default_cache_dir, 'rc_cache_2'), lower=False)
670 os.path.join(default_cache_dir, 'rc_cache_2'), lower=False)
667
671
668 # cache_license
672 # cache_license
669 _string_setting(
673 _string_setting(
670 settings,
674 settings,
671 'rc_cache.cache_license.backend',
675 'rc_cache.cache_license.backend',
672 'dogpile.cache.rc.file_namespace', lower=False)
676 'dogpile.cache.rc.file_namespace', lower=False)
673 _int_setting(
677 _int_setting(
674 settings,
678 settings,
675 'rc_cache.cache_license.expiration_time',
679 'rc_cache.cache_license.expiration_time',
676 5*60)
680 5*60)
677 _string_setting(
681 _string_setting(
678 settings,
682 settings,
679 'rc_cache.cache_license.arguments.filename',
683 'rc_cache.cache_license.arguments.filename',
680 os.path.join(default_cache_dir, 'rc_cache_3'), lower=False)
684 os.path.join(default_cache_dir, 'rc_cache_3'), lower=False)
681
685
682 # cache_repo_longterm memory, 96H
686 # cache_repo_longterm memory, 96H
683 _string_setting(
687 _string_setting(
684 settings,
688 settings,
685 'rc_cache.cache_repo_longterm.backend',
689 'rc_cache.cache_repo_longterm.backend',
686 'dogpile.cache.rc.memory_lru', lower=False)
690 'dogpile.cache.rc.memory_lru', lower=False)
687 _int_setting(
691 _int_setting(
688 settings,
692 settings,
689 'rc_cache.cache_repo_longterm.expiration_time',
693 'rc_cache.cache_repo_longterm.expiration_time',
690 345600)
694 345600)
691 _int_setting(
695 _int_setting(
692 settings,
696 settings,
693 'rc_cache.cache_repo_longterm.max_size',
697 'rc_cache.cache_repo_longterm.max_size',
694 10000)
698 10000)
695
699
696 # sql_cache_short
700 # sql_cache_short
697 _string_setting(
701 _string_setting(
698 settings,
702 settings,
699 'rc_cache.sql_cache_short.backend',
703 'rc_cache.sql_cache_short.backend',
700 'dogpile.cache.rc.memory_lru', lower=False)
704 'dogpile.cache.rc.memory_lru', lower=False)
701 _int_setting(
705 _int_setting(
702 settings,
706 settings,
703 'rc_cache.sql_cache_short.expiration_time',
707 'rc_cache.sql_cache_short.expiration_time',
704 30)
708 30)
705 _int_setting(
709 _int_setting(
706 settings,
710 settings,
707 'rc_cache.sql_cache_short.max_size',
711 'rc_cache.sql_cache_short.max_size',
708 10000)
712 10000)
709
713
710
714
711 def _int_setting(settings, name, default):
715 def _int_setting(settings, name, default):
712 settings[name] = int(settings.get(name, default))
716 settings[name] = int(settings.get(name, default))
713 return settings[name]
717 return settings[name]
714
718
715
719
716 def _bool_setting(settings, name, default):
720 def _bool_setting(settings, name, default):
717 input_val = settings.get(name, default)
721 input_val = settings.get(name, default)
718 if isinstance(input_val, unicode):
722 if isinstance(input_val, unicode):
719 input_val = input_val.encode('utf8')
723 input_val = input_val.encode('utf8')
720 settings[name] = asbool(input_val)
724 settings[name] = asbool(input_val)
721 return settings[name]
725 return settings[name]
722
726
723
727
724 def _list_setting(settings, name, default):
728 def _list_setting(settings, name, default):
725 raw_value = settings.get(name, default)
729 raw_value = settings.get(name, default)
726
730
727 old_separator = ','
731 old_separator = ','
728 if old_separator in raw_value:
732 if old_separator in raw_value:
729 # If we get a comma separated list, pass it to our own function.
733 # If we get a comma separated list, pass it to our own function.
730 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
734 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
731 else:
735 else:
732 # Otherwise we assume it uses pyramids space/newline separation.
736 # Otherwise we assume it uses pyramids space/newline separation.
733 settings[name] = aslist(raw_value)
737 settings[name] = aslist(raw_value)
734 return settings[name]
738 return settings[name]
735
739
736
740
737 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
741 def _string_setting(settings, name, default, lower=True, default_when_empty=False):
738 value = settings.get(name, default)
742 value = settings.get(name, default)
739
743
740 if default_when_empty and not value:
744 if default_when_empty and not value:
741 # use default value when value is empty
745 # use default value when value is empty
742 value = default
746 value = default
743
747
744 if lower:
748 if lower:
745 value = value.lower()
749 value = value.lower()
746 settings[name] = value
750 settings[name] = value
747 return settings[name]
751 return settings[name]
748
752
749
753
750 def _substitute_values(mapping, substitutions):
754 def _substitute_values(mapping, substitutions):
751 result = {}
755 result = {}
752
756
753 try:
757 try:
754 for key, value in mapping.items():
758 for key, value in mapping.items():
755 # initialize without substitution first
759 # initialize without substitution first
756 result[key] = value
760 result[key] = value
757
761
758 # Note: Cannot use regular replacements, since they would clash
762 # Note: Cannot use regular replacements, since they would clash
759 # with the implementation of ConfigParser. Using "format" instead.
763 # with the implementation of ConfigParser. Using "format" instead.
760 try:
764 try:
761 result[key] = value.format(**substitutions)
765 result[key] = value.format(**substitutions)
762 except KeyError as e:
766 except KeyError as e:
763 env_var = '{}'.format(e.args[0])
767 env_var = '{}'.format(e.args[0])
764
768
765 msg = 'Failed to substitute: `{key}={{{var}}}` with environment entry. ' \
769 msg = 'Failed to substitute: `{key}={{{var}}}` with environment entry. ' \
766 'Make sure your environment has {var} set, or remove this ' \
770 'Make sure your environment has {var} set, or remove this ' \
767 'variable from config file'.format(key=key, var=env_var)
771 'variable from config file'.format(key=key, var=env_var)
768
772
769 if env_var.startswith('ENV_'):
773 if env_var.startswith('ENV_'):
770 raise ValueError(msg)
774 raise ValueError(msg)
771 else:
775 else:
772 log.warning(msg)
776 log.warning(msg)
773
777
774 except ValueError as e:
778 except ValueError as e:
775 log.warning('Failed to substitute ENV variable: %s', e)
779 log.warning('Failed to substitute ENV variable: %s', e)
776 result = mapping
780 result = mapping
777
781
778 return result
782 return result
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