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