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

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

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