##// END OF EJS Templates
merge with rc1
marcink -
r3700:3563bb7b merge default
parent child Browse files
Show More

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

@@ -0,0 +1,11 b''
1 [default]
2 api_url = http://your.rhodecode.server:5000/_admin/api
3 api_user = admin
4 api_key = XXXXXXXXXXXX
5
6 ldap_uri = ldap://your.ldap.server:389
7 ldap_user = cn=rhodecode,ou=binders,dc=linaro,dc=org
8 ldap_key = XXXXXXXXX
9 base_dn = dc=linaro,dc=org
10
11 sync_users = True No newline at end of file
@@ -0,0 +1,237 b''
1 # This program is free software: you can redistribute it and/or modify
2 # it under the terms of the GNU General Public License as published by
3 # the Free Software Foundation, either version 3 of the License, or
4 # (at your option) any later version.
5 #
6 # This program is distributed in the hope that it will be useful,
7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # GNU General Public License for more details.
10 #
11 # You should have received a copy of the GNU General Public License
12 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13
14 import ldap
15 import urllib2
16 import uuid
17 import json
18
19 from ConfigParser import ConfigParser
20
21 config = ConfigParser()
22 config.read('ldap_sync.conf')
23
24
25 class InvalidResponseIDError(Exception):
26 """ Request and response don't have the same UUID. """
27
28
29 class RhodecodeResponseError(Exception):
30 """ Response has an error, something went wrong with request execution. """
31
32
33 class UserAlreadyInGroupError(Exception):
34 """ User is already a member of the target group. """
35
36
37 class UserNotInGroupError(Exception):
38 """ User is not a member of the target group. """
39
40
41 class RhodecodeAPI():
42
43 def __init__(self, url, key):
44 self.url = url
45 self.key = key
46
47 def get_api_data(self, uid, method, args):
48 """Prepare dict for API post."""
49 return {
50 "id": uid,
51 "api_key": self.key,
52 "method": method,
53 "args": args
54 }
55
56 def rhodecode_api_post(self, method, args):
57 """Send a generic API post to Rhodecode.
58
59 This will generate the UUID for validation check after the
60 response is returned. Handle errors and get the result back.
61 """
62 uid = str(uuid.uuid1())
63 data = self.get_api_data(uid, method, args)
64
65 data = json.dumps(data)
66 headers = {'content-type': 'text/plain'}
67 req = urllib2.Request(self.url, data, headers)
68
69 response = urllib2.urlopen(req)
70 response = json.load(response)
71
72 if uid != response["id"]:
73 raise InvalidResponseIDError("UUID does not match.")
74
75 if response["error"] != None:
76 raise RhodecodeResponseError(response["error"])
77
78 return response["result"]
79
80 def create_group(self, name, active=True):
81 """Create the Rhodecode user group."""
82 args = {
83 "group_name": name,
84 "active": str(active)
85 }
86 self.rhodecode_api_post("create_users_group", args)
87
88 def add_membership(self, group, username):
89 """Add specific user to a group."""
90 args = {
91 "usersgroupid": group,
92 "userid": username
93 }
94 result = self.rhodecode_api_post("add_user_to_users_group", args)
95 if not result["success"]:
96 raise UserAlreadyInGroupError("User %s already in group %s." %
97 (username, group))
98
99 def remove_membership(self, group, username):
100 """Remove specific user from a group."""
101 args = {
102 "usersgroupid": group,
103 "userid": username
104 }
105 result = self.rhodecode_api_post("remove_user_from_users_group", args)
106 if not result["success"]:
107 raise UserNotInGroupError("User %s not in group %s." %
108 (username, group))
109
110 def get_group_members(self, name):
111 """Get the list of member usernames from a user group."""
112 args = {"usersgroupid": name}
113 members = self.rhodecode_api_post("get_users_group", args)['members']
114 member_list = []
115 for member in members:
116 member_list.append(member["username"])
117 return member_list
118
119 def get_group(self, name):
120 """Return group info."""
121 args = {"usersgroupid": name}
122 return self.rhodecode_api_post("get_users_group", args)
123
124 def get_user(self, username):
125 """Return user info."""
126 args = {"userid": username}
127 return self.rhodecode_api_post("get_user", args)
128
129
130 class LdapClient():
131
132 def __init__(self, uri, user, key, base_dn):
133 self.client = ldap.initialize(uri, trace_level=0)
134 self.client.set_option(ldap.OPT_REFERRALS, 0)
135 self.client.simple_bind(user, key)
136 self.base_dn = base_dn
137
138 def __del__(self):
139 self.client.unbind()
140
141 def get_groups(self):
142 """Get all the groups in form of dict {group_name: group_info,...}."""
143 searchFilter = "objectClass=groupOfUniqueNames"
144 result = self.client.search_s(self.base_dn, ldap.SCOPE_SUBTREE,
145 searchFilter)
146
147 groups = {}
148 for group in result:
149 groups[group[1]['cn'][0]] = group[1]
150
151 return groups
152
153 def get_group_users(self, groups, group):
154 """Returns all the users belonging to a single group.
155
156 Based on the list of groups and memberships, returns all the
157 users belonging to a single group, searching recursively.
158 """
159 users = []
160 for member in groups[group]["uniqueMember"]:
161 member = self.parse_member_string(member)
162 if member[0] == "uid":
163 users.append(member[1])
164 elif member[0] == "cn":
165 users += self.get_group_users(groups, member[1])
166
167 return users
168
169 def parse_member_string(self, member):
170 """Parses the member string and returns a touple of type and name.
171
172 Unique member can be either user or group. Users will have 'uid' as
173 prefix while groups will have 'cn'.
174 """
175 member = member.split(",")[0]
176 return member.split('=')
177
178
179 class LdapSync(object):
180
181 def __init__(self):
182 self.ldap_client = LdapClient(config.get("default", "ldap_uri"),
183 config.get("default", "ldap_user"),
184 config.get("default", "ldap_key"),
185 config.get("default", "base_dn"))
186 self.rhodocode_api = RhodecodeAPI(config.get("default", "api_url"),
187 config.get("default", "api_key"))
188
189 def update_groups_from_ldap(self):
190 """Add all the groups from LDAP to Rhodecode."""
191 added = existing = 0
192 groups = self.ldap_client.get_groups()
193 for group in groups:
194 try:
195 self.rhodecode_api.create_group(group)
196 added += 1
197 except Exception:
198 existing += 1
199
200 return added, existing
201
202 def update_memberships_from_ldap(self, group):
203 """Update memberships in rhodecode based on the LDAP groups."""
204 groups = self.ldap_client.get_groups()
205 group_users = self.ldap_client.get_group_users(groups, group)
206
207 # Delete memberships first from each group which are not part
208 # of the group any more.
209 rhodecode_members = self.rhodecode_api.get_group_members(group)
210 for rhodecode_member in rhodecode_members:
211 if rhodecode_member not in group_users:
212 try:
213 self.rhodocode_api.remove_membership(group,
214 rhodecode_member)
215 except UserNotInGroupError:
216 pass
217
218 # Add memberships.
219 for member in group_users:
220 try:
221 self.rhodecode_api.add_membership(group, member)
222 except UserAlreadyInGroupError:
223 # TODO: handle somehow maybe..
224 pass
225
226
227 if __name__ == '__main__':
228 sync = LdapSync()
229 print sync.update_groups_from_ldap()
230
231 for gr in sync.ldap_client.get_groups():
232 # TODO: exception when user does not exist during add membership...
233 # How should we handle this.. Either sync users as well at this step,
234 # or just ignore those who don't exist. If we want the second case,
235 # we need to find a way to recognize the right exception (we always get
236 # RhodecodeResponseError with no error code so maybe by return msg (?)
237 sync.update_memberships_from_ldap(gr)
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,35 +1,36 b''
1 1 List of contributors to RhodeCode project:
2 2 Marcin Kuźmiński <marcin@python-works.com>
3 3 Lukasz Balcerzak <lukaszbalcerzak@gmail.com>
4 4 Jason Harris <jason@jasonfharris.com>
5 5 Thayne Harbaugh <thayne@fusionio.com>
6 6 cejones <>
7 7 Thomas Waldmann <tw-public@gmx.de>
8 8 Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>
9 9 Dmitri Kuznetsov <>
10 10 Jared Bunting <jared.bunting@peachjean.com>
11 11 Steve Romanow <slestak989@gmail.com>
12 12 Augosto Hermann <augusto.herrmann@planejamento.gov.br>
13 13 Ankit Solanki <ankit.solanki@gmail.com>
14 14 Liad Shani <liadff@gmail.com>
15 15 Les Peabody <lpeabody@gmail.com>
16 16 Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>
17 17 Matt Zuba <matt.zuba@goodwillaz.org>
18 18 Aras Pranckevicius <aras@unity3d.com>
19 19 Tony Bussieres <t.bussieres@gmail.com>
20 20 Erwin Kroon <e.kroon@smartmetersolutions.nl>
21 21 nansenat16 <nansenat16@null.tw>
22 22 Vincent Duvert <vincent@duvert.net>
23 23 Takumi IINO <trot.thunder@gmail.com>
24 24 Indra Talip <indra.talip@gmail.com>
25 25 James Rhodes <jrhodes@redpointsoftware.com.au>
26 26 Dominik Ruf <dominikruf@gmail.com>
27 27 xpol <xpolife@gmail.com>
28 28 Vincent Caron <vcaron@bearstech.com>
29 29 Zachary Auclair <zach101@gmail.com>
30 30 Stefan Engel <mail@engel-stefan.de>
31 31 Andrew Shadura <bugzilla@tut.by>
32 32 Raoul Thill <raoul.thill@gmail.com>
33 33 Philip Jameson <philip.j@hostdime.com>
34 34 Mads Kiilerich <madski@unity3d.com>
35 35 Dan Sheridan <djs@adelard.com>
36 Dennis Brakhane <brakhane@googlemail.com>
@@ -1,179 +1,179 b''
1 1 =========
2 2 RhodeCode
3 3 =========
4 4
5 5 About
6 6 -----
7 7
8 8 ``RhodeCode`` is a fast and powerful management tool for Mercurial_ and GIT_
9 9 with a built in push/pull server, full text search and code-review.
10 10 It works on http/https and has a built in permission/authentication system with
11 11 the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also provides
12 12 simple API so it's easy integrable with existing external systems.
13 13
14 14 RhodeCode is similar in some respects to github_ or bitbucket_,
15 15 however RhodeCode can be run as standalone hosted application on your own server.
16 16 It is open source and donation ware and focuses more on providing a customized,
17 17 self administered interface for Mercurial_ and GIT_ repositories.
18 18 RhodeCode works on \*nix systems and Windows it is powered by a vcs_ library
19 19 that Lukasz Balcerzak and Marcin Kuzminski created to handle multiple
20 20 different version control systems.
21 21
22 22 RhodeCode uses `PEP386 versioning <http://www.python.org/dev/peps/pep-0386/>`_
23 23
24 24 Installation
25 25 ------------
26 26 Stable releases of RhodeCode are best installed via::
27 27
28 28 easy_install rhodecode
29 29
30 30 Or::
31 31
32 32 pip install rhodecode
33 33
34 34 Detailed instructions and links may be found on the Installation page.
35 35
36 36 Please visit http://packages.python.org/RhodeCode/installation.html for
37 37 more details
38 38
39 39 RhodeCode demo
40 40 --------------
41 41
42 42 http://demo.rhodecode.org
43 43
44 44 The default access is anonymous but you can login to an administrative account
45 45 using the following credentials:
46 46
47 47 - username: demo
48 48 - password: demo12
49 49
50 50 Source code
51 51 -----------
52 52
53 53 The latest sources can be obtained from official RhodeCode instance
54 54 https://secure.rhodecode.org
55 55
56 56
57 57 MIRRORS:
58 58
59 59 Issue tracker and sources at bitbucket_
60 60
61 61 http://bitbucket.org/marcinkuzminski/rhodecode
62 62
63 63 Sources at github_
64 64
65 65 https://github.com/marcinkuzminski/rhodecode
66 66
67 67
68 68 RhodeCode Features
69 69 ------------------
70 70
71 71 - Has its own middleware to handle mercurial_ and git_ protocol requests.
72 72 Each request is authenticated and logged together with IP address.
73 73 - Build for speed and performance. You can make multiple pulls/pushes simultaneous.
74 74 Proven to work with 1000s of repositories and users
75 75 - Supports http/https, LDAP, AD, proxy-pass authentication.
76 76 - Full permissions (private/read/write/admin) together with IP restrictions for each repository,
77 77 additional explicit forking and repository creation permissions.
78 - Users groups for easier permission management
78 - User groups for easier permission management
79 79 - Repository groups let you group repos and manage them easier.
80 80 - Users can fork other users repos, and compare them at any time.
81 81 - Integrates easily with other systems, with custom created mappers you can connect it to almost
82 82 any issue tracker, and with an JSON-RPC API you can make much more
83 83 - Build in commit-api let's you add, edit and commit files right from RhodeCode
84 84 web interface using simple editor or upload binary files using simple form.
85 85 - Powerfull pull-request driven review system with inline commenting,
86 86 changeset statuses, and notification system.
87 87 - Importing and syncing repositories from remote locations for GIT_, Mercurial_ and SVN.
88 88 - Mako templates let's you customize the look and feel of the application.
89 89 - Beautiful diffs, annotations and source code browsing all colored by pygments.
90 90 Raw diffs are made in git-diff format for both VCS systems, including GIT_ binary-patches
91 91 - Mercurial_ and Git_ DAG graphs and yui-flot powered graphs with zooming and statistics
92 92 to track activity for repositories
93 93 - Admin interface with user/permission management. Admin activity journal, logs
94 94 pulls, pushes, forks, registrations and other actions made by all users.
95 95 - Server side forks. It is possible to fork a project and modify it freely
96 96 without breaking the main repository.
97 97 - rst and markdown README support for repositories.
98 98 - Full text search powered by Whoosh on the source files, commit messages, and file names.
99 99 Build in indexing daemons, with optional incremental index build
100 100 (no external search servers required all in one application)
101 101 - Setup project descriptions/tags and info inside built in db for easy, non
102 102 file-system operations.
103 103 - Intelligent cache with invalidation after push or project change, provides
104 104 high performance and always up to date data.
105 105 - RSS / Atom feeds, gravatar support, downloadable sources as zip/tar/gz
106 106 - Optional async tasks for speed and performance using celery_
107 107 - Backup scripts can do backup of whole app and send it over scp to desired
108 108 location
109 109 - Based on pylons / sqlalchemy / sqlite / whoosh / vcs
110 110
111 111
112 112 Incoming / Plans
113 113 ----------------
114 114
115 115 - Finer granular permissions per branch, or subrepo
116 116 - Web based merges for pull requests
117 117 - Tracking history for each lines in files
118 118 - Simple issue tracker
119 119 - SSH based authentication with server side key management
120 120 - Commit based built in wiki system
121 121 - Gist server
122 122 - More statistics and graph (global annotation + some more statistics)
123 123 - Other advancements as development continues (or you can of course make
124 124 additions and or requests)
125 125
126 126 License
127 127 -------
128 128
129 129 ``RhodeCode`` is released under the GPLv3 license.
130 130
131 131
132 132 Getting help
133 133 ------------
134 134
135 135 Listed bellow are various support resources that should help.
136 136
137 137 .. note::
138 138
139 139 Please try to read the documentation before posting any issues, especially
140 140 the **troubleshooting section**
141 141
142 142 - Join the `Google group <http://groups.google.com/group/rhodecode>`_ and ask
143 143 any questions.
144 144
145 145 - Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
146 146
147 147 - Join #rhodecode on FreeNode (irc.freenode.net)
148 148 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
149 149
150 150 - You can also follow me on twitter **@marcinkuzminski** where i often post some
151 151 news about RhodeCode
152 152
153 153
154 154 Online documentation
155 155 --------------------
156 156
157 157 Online documentation for the current version of RhodeCode is available at
158 158 - http://packages.python.org/RhodeCode/
159 159 - http://rhodecode.readthedocs.org/en/latest/index.html
160 160
161 161 You may also build the documentation for yourself - go into ``docs/`` and run::
162 162
163 163 make html
164 164
165 165 (You need to have sphinx_ installed to build the documentation. If you don't
166 166 have sphinx_ installed you can install it via the command:
167 167 ``easy_install sphinx``)
168 168
169 169 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
170 170 .. _python: http://www.python.org/
171 171 .. _sphinx: http://sphinx.pocoo.org/
172 172 .. _mercurial: http://mercurial.selenic.com/
173 173 .. _bitbucket: http://bitbucket.org/
174 174 .. _github: http://github.com/
175 175 .. _subversion: http://subversion.tigris.org/
176 176 .. _git: http://git-scm.com/
177 177 .. _celery: http://celeryproject.org/
178 178 .. _Sphinx: http://sphinx.pocoo.org/
179 179 .. _vcs: http://pypi.python.org/pypi/vcs
@@ -1,439 +1,481 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # RhodeCode - Pylons environment configuration #
4 4 # #
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 ################################################################################
7 7
8 8 [DEFAULT]
9 9 debug = true
10 10 pdebug = false
11 11 ################################################################################
12 12 ## Uncomment and replace with the address which should receive ##
13 13 ## any error reports after application crash ##
14 14 ## Additionally those settings will be used by RhodeCode mailing system ##
15 15 ################################################################################
16 16 #email_to = admin@localhost
17 17 #error_email_from = paste_error@localhost
18 18 #app_email_from = rhodecode-noreply@localhost
19 19 #error_message =
20 20 #email_prefix = [RhodeCode]
21 21
22 22 #smtp_server = mail.server.com
23 23 #smtp_username =
24 24 #smtp_password =
25 25 #smtp_port =
26 26 #smtp_use_tls = false
27 27 #smtp_use_ssl = true
28 # Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
28 ## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
29 29 #smtp_auth =
30 30
31 31 [server:main]
32 32 ## PASTE
33 33 ##nr of threads to spawn
34 34 #threadpool_workers = 5
35 35
36 36 ##max request before thread respawn
37 37 #threadpool_max_requests = 10
38 38
39 39 ##option to use threads of process
40 40 #use_threadpool = true
41 41
42 42 #use = egg:Paste#http
43 43
44 #WAITRESS
44 ## WAITRESS
45 45 threads = 5
46 ## 100GB
47 max_request_body_size = 107374182400
46 48 use = egg:waitress#main
47 49
48 50 host = 0.0.0.0
49 51 port = 5000
50 52
51 [filter:proxy-prefix]
52 # prefix middleware for rc
53 use = egg:PasteDeploy#prefix
54 prefix = /<your-prefix>
53 ## prefix middleware for rc
54 #[filter:proxy-prefix]
55 #use = egg:PasteDeploy#prefix
56 #prefix = /<your-prefix>
55 57
56 58 [app:main]
57 59 use = egg:rhodecode
60 ## enable proxy prefix middleware
58 61 #filter-with = proxy-prefix
62
59 63 full_stack = true
60 64 static_files = true
61 # Optional Languages
62 # en, fr, ja, pt_BR, zh_CN, zh_TW, pl
65 ## Optional Languages
66 ## en, fr, ja, pt_BR, zh_CN, zh_TW, pl
63 67 lang = en
64 68 cache_dir = %(here)s/data
65 69 index_dir = %(here)s/data/index
66 app_instance_uuid = rc-develop
70
71 ## uncomment and set this path to use archive download cache
72 #archive_cache_dir = /tmp/tarballcache
73
74 ## change this to unique ID for security
75 app_instance_uuid = rc-production
76
77 ## cut off limit for large diffs (size in bytes)
67 78 cut_off_limit = 256000
68 vcs_full_cache = True
79
80 ## use cache version of scm repo everywhere
81 vcs_full_cache = true
82
83 ## force https in RhodeCode, fixes https redirects, assumes it's always https
69 84 force_https = false
85
86 ## use Strict-Transport-Security headers
87 use_htsts = false
88
89 ## number of commits stats will parse on each iteration
70 90 commit_parse_limit = 25
71 # number of items displayed in lightweight dashboard before paginating
91
92 ## number of items displayed in lightweight dashboard before paginating is shown
72 93 dashboard_items = 100
94
95 ## use gravatar service to display avatars
73 96 use_gravatar = true
74 97
98 ## path to git executable
99 git_path = git
100
101 ## git rev filter option, --all is the default filter, if you need to
102 ## hide all refs in changelog switch this to --branches --tags
103 git_rev_filter=--all
104
75 105 ## RSS feed options
76
77 106 rss_cut_off_limit = 256000
78 107 rss_items_per_page = 10
79 108 rss_include_diff = false
80 109
110 ## options for showing and identifying changesets
111 show_sha_length = 12
112 show_revision_number = true
113
81 114
82 115 ## alternative_gravatar_url allows you to use your own avatar server application
83 116 ## the following parts of the URL will be replaced
84 117 ## {email} user email
85 118 ## {md5email} md5 hash of the user email (like at gravatar.com)
86 119 ## {size} size of the image that is expected from the server application
87 120 ## {scheme} http/https from RhodeCode server
88 121 ## {netloc} network location from RhodeCode server
89 122 #alternative_gravatar_url = http://myavatarserver.com/getbyemail/{email}/{size}
90 123 #alternative_gravatar_url = http://myavatarserver.com/getbymd5/{md5email}?s={size}
91 124
125
126 ## container auth options
92 127 container_auth_enabled = false
93 128 proxypass_auth_enabled = false
129
94 130 ## default encoding used to convert from and to unicode
95 131 ## can be also a comma seperated list of encoding in case of mixed encodings
96 132 default_encoding = utf8
97 133
98 134 ## overwrite schema of clone url
99 135 ## available vars:
100 136 ## scheme - http/https
101 137 ## user - current user
102 138 ## pass - password
103 139 ## netloc - network location
104 140 ## path - usually repo_name
105 141
106 142 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
107 143
108 144 ## issue tracking mapping for commits messages
109 145 ## comment out issue_pat, issue_server, issue_prefix to enable
110 146
111 147 ## pattern to get the issues from commit messages
112 148 ## default one used here is #<numbers> with a regex passive group for `#`
113 149 ## {id} will be all groups matched from this pattern
114 150
115 151 issue_pat = (?:\s*#)(\d+)
116 152
117 153 ## server url to the issue, each {id} will be replaced with match
118 154 ## fetched from the regex and {repo} is replaced with full repository name
119 155 ## including groups {repo_name} is replaced with just name of repo
120 156
121 157 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
122 158
123 159 ## prefix to add to link to indicate it's an url
124 160 ## #314 will be replaced by <issue_prefix><id>
125 161
126 162 issue_prefix = #
127 163
128 164 ## issue_pat, issue_server_link, issue_prefix can have suffixes to specify
129 165 ## multiple patterns, to other issues server, wiki or others
130 166 ## below an example how to create a wiki pattern
131 167 # #wiki-some-id -> https://mywiki.com/some-id
132 168
133 169 #issue_pat_wiki = (?:wiki-)(.+)
134 170 #issue_server_link_wiki = https://mywiki.com/{id}
135 171 #issue_prefix_wiki = WIKI-
136 172
137 173
138 174 ## instance-id prefix
139 175 ## a prefix key for this instance used for cache invalidation when running
140 176 ## multiple instances of rhodecode, make sure it's globally unique for
141 177 ## all running rhodecode instances. Leave empty if you don't use it
142 178 instance_id =
143 179
144 180 ## alternative return HTTP header for failed authentication. Default HTTP
145 181 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
146 182 ## handling that. Set this variable to 403 to return HTTPForbidden
147 183 auth_ret_code =
148 184
185 ## locking return code. When repository is locked return this HTTP code. 2XX
186 ## codes don't break the transactions while 4XX codes do
187 lock_ret_code = 423
188
189
149 190 ####################################
150 191 ### CELERY CONFIG ####
151 192 ####################################
152 193 use_celery = false
153 194 broker.host = localhost
154 195 broker.vhost = rabbitmqhost
155 196 broker.port = 5672
156 197 broker.user = rabbitmq
157 198 broker.password = qweqwe
158 199
159 200 celery.imports = rhodecode.lib.celerylib.tasks
160 201
161 202 celery.result.backend = amqp
162 203 celery.result.dburi = amqp://
163 204 celery.result.serialier = json
164 205
165 206 #celery.send.task.error.emails = true
166 207 #celery.amqp.task.result.expires = 18000
167 208
168 209 celeryd.concurrency = 2
169 210 #celeryd.log.file = celeryd.log
170 211 celeryd.log.level = debug
171 212 celeryd.max.tasks.per.child = 1
172 213
173 #tasks will never be sent to the queue, but executed locally instead.
214 ## tasks will never be sent to the queue, but executed locally instead.
174 215 celery.always.eager = false
175 216
176 217 ####################################
177 218 ### BEAKER CACHE ####
178 219 ####################################
179 220 beaker.cache.data_dir=%(here)s/data/cache/data
180 221 beaker.cache.lock_dir=%(here)s/data/cache/lock
181 222
182 223 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
183 224
184 225 beaker.cache.super_short_term.type=memory
185 226 beaker.cache.super_short_term.expire=10
186 227 beaker.cache.super_short_term.key_length = 256
187 228
188 229 beaker.cache.short_term.type=memory
189 230 beaker.cache.short_term.expire=60
190 231 beaker.cache.short_term.key_length = 256
191 232
192 233 beaker.cache.long_term.type=memory
193 234 beaker.cache.long_term.expire=36000
194 235 beaker.cache.long_term.key_length = 256
195 236
196 237 beaker.cache.sql_cache_short.type=memory
197 238 beaker.cache.sql_cache_short.expire=10
198 239 beaker.cache.sql_cache_short.key_length = 256
199 240
200 241 beaker.cache.sql_cache_med.type=memory
201 242 beaker.cache.sql_cache_med.expire=360
202 243 beaker.cache.sql_cache_med.key_length = 256
203 244
204 245 beaker.cache.sql_cache_long.type=file
205 246 beaker.cache.sql_cache_long.expire=3600
206 247 beaker.cache.sql_cache_long.key_length = 256
207 248
208 249 ####################################
209 250 ### BEAKER SESSION ####
210 251 ####################################
211 252 ## Type of storage used for the session, current types are
212 253 ## dbm, file, memcached, database, and memory.
213 254 ## The storage uses the Container API
214 255 ## that is also used by the cache system.
215 256
216 257 ## db session ##
217 258 #beaker.session.type = ext:database
218 259 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
219 260 #beaker.session.table_name = db_session
220 261
221 262 ## encrypted cookie client side session, good for many instances ##
222 263 #beaker.session.type = cookie
223 264
224 265 ## file based cookies (default) ##
225 266 #beaker.session.type = file
226 267
227 268
228 269 beaker.session.key = rhodecode
229 ## secure cookie requires AES python libraries ##
230 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
231 #beaker.session.validate_key = 9712sds2212c--zxc123
270 ## secure cookie requires AES python libraries
271 #beaker.session.encrypt_key = <key_for_encryption>
272 #beaker.session.validate_key = <validation_key>
273
232 274 ## sets session as invalid if it haven't been accessed for given amount of time
233 275 beaker.session.timeout = 2592000
234 276 beaker.session.httponly = true
235 277 #beaker.session.cookie_path = /<your-prefix>
236 278
237 ## uncomment for https secure cookie ##
279 ## uncomment for https secure cookie
238 280 beaker.session.secure = false
239 281
240 ## auto save the session to not to use .save() ##
282 ## auto save the session to not to use .save()
241 283 beaker.session.auto = False
242 284
243 285 ## default cookie expiration time in seconds `true` expire at browser close ##
244 286 #beaker.session.cookie_expires = 3600
245 287
246 288
247 289 ############################
248 290 ## ERROR HANDLING SYSTEMS ##
249 291 ############################
250 292
251 293 ####################
252 294 ### [errormator] ###
253 295 ####################
254 296
255 # Errormator is tailored to work with RhodeCode, see
256 # http://errormator.com for details how to obtain an account
257 # you must install python package `errormator_client` to make it work
297 ## Errormator is tailored to work with RhodeCode, see
298 ## http://errormator.com for details how to obtain an account
299 ## you must install python package `errormator_client` to make it work
258 300
259 # errormator enabled
260 errormator = true
301 ## errormator enabled
302 errormator = false
261 303
262 304 errormator.server_url = https://api.errormator.com
263 305 errormator.api_key = YOUR_API_KEY
264 306
265 # TWEAK AMOUNT OF INFO SENT HERE
307 ## TWEAK AMOUNT OF INFO SENT HERE
266 308
267 # enables 404 error logging (default False)
309 ## enables 404 error logging (default False)
268 310 errormator.report_404 = false
269 311
270 # time in seconds after request is considered being slow (default 1)
312 ## time in seconds after request is considered being slow (default 1)
271 313 errormator.slow_request_time = 1
272 314
273 # record slow requests in application
274 # (needs to be enabled for slow datastore recording and time tracking)
315 ## record slow requests in application
316 ## (needs to be enabled for slow datastore recording and time tracking)
275 317 errormator.slow_requests = true
276 318
277 # enable hooking to application loggers
319 ## enable hooking to application loggers
278 320 # errormator.logging = true
279 321
280 # minimum log level for log capture
322 ## minimum log level for log capture
281 323 # errormator.logging.level = WARNING
282 324
283 # send logs only from erroneous/slow requests
284 # (saves API quota for intensive logging)
325 ## send logs only from erroneous/slow requests
326 ## (saves API quota for intensive logging)
285 327 errormator.logging_on_error = false
286 328
287 # list of additonal keywords that should be grabbed from environ object
288 # can be string with comma separated list of words in lowercase
289 # (by default client will always send following info:
290 # 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
291 # start with HTTP* this list be extended with additional keywords here
329 ## list of additonal keywords that should be grabbed from environ object
330 ## can be string with comma separated list of words in lowercase
331 ## (by default client will always send following info:
332 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
333 ## start with HTTP* this list be extended with additional keywords here
292 334 errormator.environ_keys_whitelist =
293 335
294 336
295 # list of keywords that should be blanked from request object
296 # can be string with comma separated list of words in lowercase
297 # (by default client will always blank keys that contain following words
298 # 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
299 # this list be extended with additional keywords set here
337 ## list of keywords that should be blanked from request object
338 ## can be string with comma separated list of words in lowercase
339 ## (by default client will always blank keys that contain following words
340 ## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
341 ## this list be extended with additional keywords set here
300 342 errormator.request_keys_blacklist =
301 343
302 344
303 # list of namespaces that should be ignores when gathering log entries
304 # can be string with comma separated list of namespaces
305 # (by default the client ignores own entries: errormator_client.client)
345 ## list of namespaces that should be ignores when gathering log entries
346 ## can be string with comma separated list of namespaces
347 ## (by default the client ignores own entries: errormator_client.client)
306 348 errormator.log_namespace_blacklist =
307 349
308 350
309 351 ################
310 352 ### [sentry] ###
311 353 ################
312 354
313 # sentry is a alternative open source error aggregator
314 # you must install python packages `sentry` and `raven` to enable
355 ## sentry is a alternative open source error aggregator
356 ## you must install python packages `sentry` and `raven` to enable
315 357
316 358 sentry.dsn = YOUR_DNS
317 359 sentry.servers =
318 360 sentry.name =
319 361 sentry.key =
320 362 sentry.public_key =
321 363 sentry.secret_key =
322 364 sentry.project =
323 365 sentry.site =
324 366 sentry.include_paths =
325 367 sentry.exclude_paths =
326 368
327 369
328 370 ################################################################################
329 371 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
330 372 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
331 373 ## execute malicious code after an exception is raised. ##
332 374 ################################################################################
333 375 #set debug = false
334 376
335 377 ##################################
336 378 ### LOGVIEW CONFIG ###
337 379 ##################################
338 380 logview.sqlalchemy = #faa
339 381 logview.pylons.templating = #bfb
340 382 logview.pylons.util = #eee
341 383
342 384 #########################################################
343 385 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
344 386 #########################################################
345 387 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
346 388 sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode
347 389 sqlalchemy.db1.echo = false
348 390 sqlalchemy.db1.pool_recycle = 3600
349 391 sqlalchemy.db1.convert_unicode = true
350 392
351 393 ################################
352 394 ### LOGGING CONFIGURATION ####
353 395 ################################
354 396 [loggers]
355 397 keys = root, routes, rhodecode, sqlalchemy, beaker, templates, whoosh_indexer
356 398
357 399 [handlers]
358 400 keys = console, console_sql
359 401
360 402 [formatters]
361 403 keys = generic, color_formatter, color_formatter_sql
362 404
363 405 #############
364 406 ## LOGGERS ##
365 407 #############
366 408 [logger_root]
367 409 level = NOTSET
368 410 handlers = console
369 411
370 412 [logger_routes]
371 413 level = DEBUG
372 414 handlers =
373 415 qualname = routes.middleware
374 # "level = DEBUG" logs the route matched and routing variables.
416 ## "level = DEBUG" logs the route matched and routing variables.
375 417 propagate = 1
376 418
377 419 [logger_beaker]
378 420 level = DEBUG
379 421 handlers =
380 422 qualname = beaker.container
381 423 propagate = 1
382 424
383 425 [logger_templates]
384 426 level = INFO
385 427 handlers =
386 428 qualname = pylons.templating
387 429 propagate = 1
388 430
389 431 [logger_rhodecode]
390 432 level = DEBUG
391 433 handlers =
392 434 qualname = rhodecode
393 435 propagate = 1
394 436
395 437 [logger_sqlalchemy]
396 438 level = INFO
397 439 handlers = console_sql
398 440 qualname = sqlalchemy.engine
399 441 propagate = 0
400 442
401 443 [logger_whoosh_indexer]
402 444 level = DEBUG
403 445 handlers =
404 446 qualname = whoosh_indexer
405 447 propagate = 1
406 448
407 449 ##############
408 450 ## HANDLERS ##
409 451 ##############
410 452
411 453 [handler_console]
412 454 class = StreamHandler
413 455 args = (sys.stderr,)
414 456 level = DEBUG
415 457 formatter = color_formatter
416 458
417 459 [handler_console_sql]
418 460 class = StreamHandler
419 461 args = (sys.stderr,)
420 462 level = DEBUG
421 463 formatter = color_formatter_sql
422 464
423 465 ################
424 466 ## FORMATTERS ##
425 467 ################
426 468
427 469 [formatter_generic]
428 470 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
429 471 datefmt = %Y-%m-%d %H:%M:%S
430 472
431 473 [formatter_color_formatter]
432 474 class=rhodecode.lib.colored_formatter.ColorFormatter
433 475 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
434 476 datefmt = %Y-%m-%d %H:%M:%S
435 477
436 478 [formatter_color_formatter_sql]
437 479 class=rhodecode.lib.colored_formatter.ColorFormatterSql
438 480 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
439 481 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,942 +1,985 b''
1 1 .. _api:
2 2
3 3 ===
4 4 API
5 5 ===
6 6
7 7
8 8 Starting from RhodeCode version 1.2 a simple API was implemented.
9 9 There's a single schema for calling all api methods. API is implemented
10 10 with JSON protocol both ways. An url to send API request to RhodeCode is
11 11 <your_server>/_admin/api
12 12
13 13 API ACCESS FOR WEB VIEWS
14 14 ++++++++++++++++++++++++
15 15
16 16 API access can also be turned on for each web view in RhodeCode that is
17 17 decorated with `@LoginRequired` decorator. To enable API access simple change
18 18 the standard login decorator to `@LoginRequired(api_access=True)`.
19 19 After this change, a rhodecode view can be accessed without login by adding a
20 20 GET parameter `?api_key=<api_key>` to url. By default this is only
21 21 enabled on RSS/ATOM feed views.
22 22
23 23
24 24 API ACCESS
25 25 ++++++++++
26 26
27 27 All clients are required to send JSON-RPC spec JSON data::
28 28
29 29 {
30 30 "id:"<id>",
31 31 "api_key":"<api_key>",
32 32 "method":"<method_name>",
33 33 "args":{"<arg_key>":"<arg_val>"}
34 34 }
35 35
36 36 Example call for autopulling remotes repos using curl::
37 37 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"id":1,"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}'
38 38
39 39 Simply provide
40 40 - *id* A value of any type, which is used to match the response with the request that it is replying to.
41 41 - *api_key* for access and permission validation.
42 42 - *method* is name of method to call
43 43 - *args* is an key:value list of arguments to pass to method
44 44
45 45 .. note::
46 46
47 47 api_key can be found in your user account page
48 48
49 49
50 50 RhodeCode API will return always a JSON-RPC response::
51 51
52 52 {
53 53 "id":<id>, # matching id sent by request
54 54 "result": "<result>"|null, # JSON formatted result, null if any errors
55 55 "error": "null"|<error_message> # JSON formatted error (if any)
56 56 }
57 57
58 58 All responses from API will be `HTTP/1.0 200 OK`, if there's an error while
59 59 calling api *error* key from response will contain failure description
60 60 and result will be null.
61 61
62 62
63 63 API CLIENT
64 64 ++++++++++
65 65
66 66 From version 1.4 RhodeCode adds a script that allows to easily
67 67 communicate with API. After installing RhodeCode a `rhodecode-api` script
68 68 will be available.
69 69
70 70 To get started quickly simply run::
71 71
72 72 rhodecode-api _create_config --apikey=<youapikey> --apihost=<rhodecode host>
73 73
74 74 This will create a file named .config in the directory you executed it storing
75 75 json config file with credentials. You can skip this step and always provide
76 76 both of the arguments to be able to communicate with server
77 77
78 78
79 79 after that simply run any api command for example get_repo::
80 80
81 81 rhodecode-api get_repo
82 82
83 83 calling {"api_key": "<apikey>", "id": 75, "args": {}, "method": "get_repo"} to http://127.0.0.1:5000
84 84 rhodecode said:
85 85 {'error': 'Missing non optional `repoid` arg in JSON DATA',
86 86 'id': 75,
87 87 'result': None}
88 88
89 89 Ups looks like we forgot to add an argument
90 90
91 91 Let's try again now giving the repoid as parameters::
92 92
93 93 rhodecode-api get_repo repoid:rhodecode
94 94
95 95 calling {"api_key": "<apikey>", "id": 39, "args": {"repoid": "rhodecode"}, "method": "get_repo"} to http://127.0.0.1:5000
96 96 rhodecode said:
97 97 {'error': None,
98 98 'id': 39,
99 99 'result': <json data...>}
100 100
101 101
102 102
103 103 API METHODS
104 104 +++++++++++
105 105
106 106
107 107 pull
108 108 ----
109 109
110 110 Pulls given repo from remote location. Can be used to automatically keep
111 111 remote repos up to date. This command can be executed only using api_key
112 112 belonging to user with admin rights
113 113
114 114 INPUT::
115 115
116 116 id : <id_for_response>
117 117 api_key : "<api_key>"
118 118 method : "pull"
119 119 args : {
120 120 "repoid" : "<reponame or repo_id>"
121 121 }
122 122
123 123 OUTPUT::
124 124
125 125 id : <id_given_in_input>
126 126 result : "Pulled from `<reponame>`"
127 127 error : null
128 128
129 129
130 130 rescan_repos
131 131 ------------
132 132
133 133 Dispatch rescan repositories action. If remove_obsolete is set
134 134 RhodeCode will delete repos that are in database but not in the filesystem.
135 135 This command can be executed only using api_key belonging to user with admin
136 136 rights.
137 137
138 138 INPUT::
139 139
140 140 id : <id_for_response>
141 141 api_key : "<api_key>"
142 142 method : "rescan_repos"
143 143 args : {
144 144 "remove_obsolete" : "<boolean = Optional(False)>"
145 145 }
146 146
147 147 OUTPUT::
148 148
149 149 id : <id_given_in_input>
150 150 result : "{'added': [<list of names of added repos>],
151 151 'removed': [<list of names of removed repos>]}"
152 152 error : null
153 153
154 154
155 invalidate_cache
156 ----------------
157
158 Invalidate cache for repository.
159 This command can be executed only using api_key belonging to user with admin
160 rights or regular user that have write or admin or write access to repository.
161
162 INPUT::
163
164 id : <id_for_response>
165 api_key : "<api_key>"
166 method : "invalidate_cache"
167 args : {
168 "repoid" : "<reponame or repo_id>"
169 }
170
171 OUTPUT::
172
173 id : <id_given_in_input>
174 result : "Cache for repository `<reponame>` was invalidated: invalidated cache keys: <list_of_cache_keys>"
175 error : null
176
155 177 lock
156 178 ----
157 179
158 180 Set locking state on given repository by given user. If userid param is skipped
159 , then it is set to id of user whos calling this method.
181 , then it is set to id of user whos calling this method. If locked param is skipped
182 then function shows current lock state of given repo.
160 183 This command can be executed only using api_key belonging to user with admin
161 184 rights or regular user that have admin or write access to repository.
162 185
163 186 INPUT::
164 187
165 188 id : <id_for_response>
166 189 api_key : "<api_key>"
167 190 method : "lock"
168 191 args : {
169 192 "repoid" : "<reponame or repo_id>"
170 193 "userid" : "<user_id or username = Optional(=apiuser)>",
171 "locked" : "<bool true|false>"
194 "locked" : "<bool true|false = Optional(=None)>"
172 195 }
173 196
174 197 OUTPUT::
175 198
176 199 id : <id_given_in_input>
177 200 result : "User `<username>` set lock state for repo `<reponame>` to `true|false`"
178 201 error : null
179 202
180 203
181 204 show_ip
182 205 -------
183 206
184 207 Shows IP address as seen from RhodeCode server, together with all
185 208 defined IP addresses for given user.
186 209 This command can be executed only using api_key belonging to user with admin
187 210 rights.
188 211
189 212 INPUT::
190 213
191 214 id : <id_for_response>
192 215 api_key : "<api_key>"
193 216 method : "show_ip"
194 217 args : {
195 218 "userid" : "<user_id or username>",
196 219 }
197 220
198 221 OUTPUT::
199 222
200 223 id : <id_given_in_input>
201 224 result : {
202 225 "ip_addr_server": <ip_from_clien>",
203 226 "user_ips": [
204 227 {
205 228 "ip_addr": "<ip_with_mask>",
206 229 "ip_range": ["<start_ip>", "<end_ip>"],
207 230 },
208 231 ...
209 232 ]
210 233 }
211 234
212 235 error : null
213 236
214 237
215 238 get_user
216 239 --------
217 240
218 241 Get's an user by username or user_id, Returns empty result if user is not found.
219 242 If userid param is skipped it is set to id of user who is calling this method.
220 243 This command can be executed only using api_key belonging to user with admin
221 244 rights, or regular users that cannot specify different userid than theirs
222 245
223 246
224 247 INPUT::
225 248
226 249 id : <id_for_response>
227 250 api_key : "<api_key>"
228 251 method : "get_user"
229 252 args : {
230 253 "userid" : "<username or user_id Optional(=apiuser)>"
231 254 }
232 255
233 256 OUTPUT::
234 257
235 258 id : <id_given_in_input>
236 259 result: None if user does not exist or
237 260 {
238 261 "user_id" : "<user_id>",
262 "api_key" : "<api_key>",
239 263 "username" : "<username>",
240 264 "firstname": "<firstname>",
241 265 "lastname" : "<lastname>",
242 266 "email" : "<email>",
243 267 "emails": "<list_of_all_additional_emails>",
244 268 "ip_addresses": "<list_of_ip_addresses_for_user>",
245 269 "active" : "<bool>",
246 270 "admin" :  "<bool>",
247 271 "ldap_dn" : "<ldap_dn>",
248 272 "last_login": "<last_login>",
249 273 "permissions": {
250 274 "global": ["hg.create.repository",
251 275 "repository.read",
252 276 "hg.register.manual_activate"],
253 277 "repositories": {"repo1": "repository.none"},
254 278 "repositories_groups": {"Group1": "group.read"}
255 279 },
256 280 }
257 281
258 282 error: null
259 283
260 284
261 285 get_users
262 286 ---------
263 287
264 288 Lists all existing users. This command can be executed only using api_key
265 289 belonging to user with admin rights.
266 290
267 291
268 292 INPUT::
269 293
270 294 id : <id_for_response>
271 295 api_key : "<api_key>"
272 296 method : "get_users"
273 297 args : { }
274 298
275 299 OUTPUT::
276 300
277 301 id : <id_given_in_input>
278 302 result: [
279 303 {
280 304 "user_id" : "<user_id>",
281 305 "username" : "<username>",
282 306 "firstname": "<firstname>",
283 307 "lastname" : "<lastname>",
284 308 "email" : "<email>",
285 309 "emails": "<list_of_all_additional_emails>",
286 310 "ip_addresses": "<list_of_ip_addresses_for_user>",
287 311 "active" : "<bool>",
288 312 "admin" :  "<bool>",
289 313 "ldap_dn" : "<ldap_dn>",
290 314 "last_login": "<last_login>",
291 315 },
292 316
293 317 ]
294 318 error: null
295 319
296 320
297 321 create_user
298 322 -----------
299 323
300 324 Creates new user. This command can
301 325 be executed only using api_key belonging to user with admin rights.
302 326
303 327
304 328 INPUT::
305 329
306 330 id : <id_for_response>
307 331 api_key : "<api_key>"
308 332 method : "create_user"
309 333 args : {
310 334 "username" : "<username>",
311 335 "email" : "<useremail>",
312 336 "password" : "<password>",
313 337 "firstname" : "<firstname> = Optional(None)",
314 338 "lastname" : "<lastname> = Optional(None)",
315 339 "active" : "<bool> = Optional(True)",
316 340 "admin" : "<bool> = Optional(False)",
317 341 "ldap_dn" : "<ldap_dn> = Optional(None)"
318 342 }
319 343
320 344 OUTPUT::
321 345
322 346 id : <id_given_in_input>
323 347 result: {
324 348 "msg" : "created new user `<username>`",
325 349 "user": {
326 350 "user_id" : "<user_id>",
327 351 "username" : "<username>",
328 352 "firstname": "<firstname>",
329 353 "lastname" : "<lastname>",
330 354 "email" : "<email>",
331 355 "emails": "<list_of_all_additional_emails>",
332 356 "active" : "<bool>",
333 357 "admin" :  "<bool>",
334 358 "ldap_dn" : "<ldap_dn>",
335 359 "last_login": "<last_login>",
336 360 },
337 361 }
338 362 error: null
339 363
340 364
341 365 update_user
342 366 -----------
343 367
344 368 updates given user if such user exists. This command can
345 369 be executed only using api_key belonging to user with admin rights.
346 370
347 371
348 372 INPUT::
349 373
350 374 id : <id_for_response>
351 375 api_key : "<api_key>"
352 376 method : "update_user"
353 377 args : {
354 378 "userid" : "<user_id or username>",
355 379 "username" : "<username> = Optional(None)",
356 380 "email" : "<useremail> = Optional(None)",
357 381 "password" : "<password> = Optional(None)",
358 382 "firstname" : "<firstname> = Optional(None)",
359 383 "lastname" : "<lastname> = Optional(None)",
360 384 "active" : "<bool> = Optional(None)",
361 385 "admin" : "<bool> = Optional(None)",
362 386 "ldap_dn" : "<ldap_dn> = Optional(None)"
363 387 }
364 388
365 389 OUTPUT::
366 390
367 391 id : <id_given_in_input>
368 392 result: {
369 393 "msg" : "updated user ID:<userid> <username>",
370 394 "user": {
371 395 "user_id" : "<user_id>",
372 396 "username" : "<username>",
373 397 "firstname": "<firstname>",
374 398 "lastname" : "<lastname>",
375 399 "email" : "<email>",
376 400 "emails": "<list_of_all_additional_emails>",
377 401 "active" : "<bool>",
378 402 "admin" :  "<bool>",
379 403 "ldap_dn" : "<ldap_dn>",
380 404 "last_login": "<last_login>",
381 405 },
382 406 }
383 407 error: null
384 408
385 409
386 410 delete_user
387 411 -----------
388 412
389 413
390 414 deletes givenuser if such user exists. This command can
391 415 be executed only using api_key belonging to user with admin rights.
392 416
393 417
394 418 INPUT::
395 419
396 420 id : <id_for_response>
397 421 api_key : "<api_key>"
398 422 method : "delete_user"
399 423 args : {
400 424 "userid" : "<user_id or username>",
401 425 }
402 426
403 427 OUTPUT::
404 428
405 429 id : <id_given_in_input>
406 430 result: {
407 431 "msg" : "deleted user ID:<userid> <username>",
408 432 "user": null
409 433 }
410 434 error: null
411 435
412 436
413 437 get_users_group
414 438 ---------------
415 439
416 Gets an existing users group. This command can be executed only using api_key
440 Gets an existing user group. This command can be executed only using api_key
417 441 belonging to user with admin rights.
418 442
419 443
420 444 INPUT::
421 445
422 446 id : <id_for_response>
423 447 api_key : "<api_key>"
424 448 method : "get_users_group"
425 449 args : {
426 "usersgroupid" : "<users group id or name>"
450 "usersgroupid" : "<user group id or name>"
427 451 }
428 452
429 453 OUTPUT::
430 454
431 455 id : <id_given_in_input>
432 456 result : None if group not exist
433 457 {
434 458 "users_group_id" : "<id>",
435 459 "group_name" : "<groupname>",
436 460 "active": "<bool>",
437 461 "members" : [
438 462 {
439 463 "user_id" : "<user_id>",
440 464 "username" : "<username>",
441 465 "firstname": "<firstname>",
442 466 "lastname" : "<lastname>",
443 467 "email" : "<email>",
444 468 "emails": "<list_of_all_additional_emails>",
445 469 "active" : "<bool>",
446 470 "admin" :  "<bool>",
447 471 "ldap_dn" : "<ldap_dn>",
448 472 "last_login": "<last_login>",
449 473 },
450 474
451 475 ]
452 476 }
453 477 error : null
454 478
455 479
456 480 get_users_groups
457 481 ----------------
458 482
459 Lists all existing users groups. This command can be executed only using
483 Lists all existing user groups. This command can be executed only using
460 484 api_key belonging to user with admin rights.
461 485
462 486
463 487 INPUT::
464 488
465 489 id : <id_for_response>
466 490 api_key : "<api_key>"
467 491 method : "get_users_groups"
468 492 args : { }
469 493
470 494 OUTPUT::
471 495
472 496 id : <id_given_in_input>
473 497 result : [
474 498 {
475 499 "users_group_id" : "<id>",
476 500 "group_name" : "<groupname>",
477 501 "active": "<bool>",
478 502 },
479 503
480 504 ]
481 505 error : null
482 506
483 507
484 508 create_users_group
485 509 ------------------
486 510
487 Creates new users group. This command can be executed only using api_key
511 Creates new user group. This command can be executed only using api_key
488 512 belonging to user with admin rights
489 513
490 514
491 515 INPUT::
492 516
493 517 id : <id_for_response>
494 518 api_key : "<api_key>"
495 519 method : "create_users_group"
496 520 args: {
497 521 "group_name": "<groupname>",
498 522 "active":"<bool> = Optional(True)"
499 523 }
500 524
501 525 OUTPUT::
502 526
503 527 id : <id_given_in_input>
504 528 result: {
505 "msg": "created new users group `<groupname>`",
529 "msg": "created new user group `<groupname>`",
506 530 "users_group": {
507 531 "users_group_id" : "<id>",
508 532 "group_name" : "<groupname>",
509 533 "active": "<bool>",
510 534 },
511 535 }
512 536 error: null
513 537
514 538
515 539 add_user_to_users_group
516 540 -----------------------
517 541
518 Adds a user to a users group. If user exists in that group success will be
542 Adds a user to a user group. If user exists in that group success will be
519 543 `false`. This command can be executed only using api_key
520 544 belonging to user with admin rights
521 545
522 546
523 547 INPUT::
524 548
525 549 id : <id_for_response>
526 550 api_key : "<api_key>"
527 551 method : "add_user_users_group"
528 552 args: {
529 "usersgroupid" : "<users group id or name>",
553 "usersgroupid" : "<user group id or name>",
530 554 "userid" : "<user_id or username>",
531 555 }
532 556
533 557 OUTPUT::
534 558
535 559 id : <id_given_in_input>
536 560 result: {
537 561 "success": True|False # depends on if member is in group
538 "msg": "added member `<username>` to users group `<groupname>` |
562 "msg": "added member `<username>` to user group `<groupname>` |
539 563 User is already in that group"
540 564 }
541 565 error: null
542 566
543 567
544 568 remove_user_from_users_group
545 569 ----------------------------
546 570
547 Removes a user from a users group. If user is not in given group success will
571 Removes a user from a user group. If user is not in given group success will
548 572 be `false`. This command can be executed only
549 573 using api_key belonging to user with admin rights
550 574
551 575
552 576 INPUT::
553 577
554 578 id : <id_for_response>
555 579 api_key : "<api_key>"
556 580 method : "remove_user_from_users_group"
557 581 args: {
558 "usersgroupid" : "<users group id or name>",
582 "usersgroupid" : "<user group id or name>",
559 583 "userid" : "<user_id or username>",
560 584 }
561 585
562 586 OUTPUT::
563 587
564 588 id : <id_given_in_input>
565 589 result: {
566 590 "success": True|False, # depends on if member is in group
567 "msg": "removed member <username> from users group <groupname> |
591 "msg": "removed member <username> from user group <groupname> |
568 592 User wasn't in group"
569 593 }
570 594 error: null
571 595
572 596
573 597 get_repo
574 598 --------
575 599
576 600 Gets an existing repository by it's name or repository_id. Members will return
577 601 either users_group or user associated to that repository. This command can be
578 602 executed only using api_key belonging to user with admin
579 603 rights or regular user that have at least read access to repository.
580 604
581 605
582 606 INPUT::
583 607
584 608 id : <id_for_response>
585 609 api_key : "<api_key>"
586 610 method : "get_repo"
587 611 args: {
588 612 "repoid" : "<reponame or repo_id>"
589 613 }
590 614
591 615 OUTPUT::
592 616
593 617 id : <id_given_in_input>
594 618 result: None if repository does not exist or
595 619 {
596 620 "repo_id" : "<repo_id>",
597 621 "repo_name" : "<reponame>"
598 622 "repo_type" : "<repo_type>",
599 623 "clone_uri" : "<clone_uri>",
600 624 "enable_downloads": "<bool>",
601 625 "enable_locking": "<bool>",
602 626 "enable_statistics": "<bool>",
603 627 "private": "<bool>",
604 628 "created_on" : "<date_time_created>",
605 629 "description" : "<description>",
606 630 "landing_rev": "<landing_rev>",
607 631 "last_changeset": {
608 632 "author": "<full_author>",
609 633 "date": "<date_time_of_commit>",
610 634 "message": "<commit_message>",
611 635 "raw_id": "<raw_id>",
612 636 "revision": "<numeric_revision>",
613 637 "short_id": "<short_id>"
614 638 }
615 639 "owner": "<repo_owner>",
616 640 "fork_of": "<name_of_fork_parent>",
617 641 "members" : [
618 642 {
619 643 "type": "user",
620 644 "user_id" : "<user_id>",
621 645 "username" : "<username>",
622 646 "firstname": "<firstname>",
623 647 "lastname" : "<lastname>",
624 648 "email" : "<email>",
625 649 "emails": "<list_of_all_additional_emails>",
626 650 "active" : "<bool>",
627 651 "admin" :  "<bool>",
628 652 "ldap_dn" : "<ldap_dn>",
629 653 "last_login": "<last_login>",
630 654 "permission" : "repository.(read|write|admin)"
631 655 },
632 656
633 657 {
634 658 "type": "users_group",
635 659 "id" : "<usersgroupid>",
636 660 "name" : "<usersgroupname>",
637 661 "active": "<bool>",
638 662 "permission" : "repository.(read|write|admin)"
639 663 },
640 664
641 665 ]
666 "followers": [
667 {
668 "user_id" : "<user_id>",
669 "username" : "<username>",
670 "firstname": "<firstname>",
671 "lastname" : "<lastname>",
672 "email" : "<email>",
673 "emails": "<list_of_all_additional_emails>",
674 "ip_addresses": "<list_of_ip_addresses_for_user>",
675 "active" : "<bool>",
676 "admin" :  "<bool>",
677 "ldap_dn" : "<ldap_dn>",
678 "last_login": "<last_login>",
679 },
680
681 ]
642 682 }
643 683 error: null
644 684
645 685
646 686 get_repos
647 687 ---------
648 688
649 689 Lists all existing repositories. This command can be executed only using
650 690 api_key belonging to user with admin rights or regular user that have
651 691 admin, write or read access to repository.
652 692
653 693
654 694 INPUT::
655 695
656 696 id : <id_for_response>
657 697 api_key : "<api_key>"
658 698 method : "get_repos"
659 699 args: { }
660 700
661 701 OUTPUT::
662 702
663 703 id : <id_given_in_input>
664 704 result: [
665 705 {
666 706 "repo_id" : "<repo_id>",
667 707 "repo_name" : "<reponame>"
668 708 "repo_type" : "<repo_type>",
669 709 "clone_uri" : "<clone_uri>",
670 710 "private": : "<bool>",
671 711 "created_on" : "<datetimecreated>",
672 712 "description" : "<description>",
673 713 "landing_rev": "<landing_rev>",
674 714 "owner": "<repo_owner>",
675 715 "fork_of": "<name_of_fork_parent>",
676 716 "enable_downloads": "<bool>",
677 717 "enable_locking": "<bool>",
678 718 "enable_statistics": "<bool>",
679 719 },
680 720
681 721 ]
682 722 error: null
683 723
684 724
685 725 get_repo_nodes
686 726 --------------
687 727
688 728 returns a list of nodes and it's children in a flat list for a given path
689 729 at given revision. It's possible to specify ret_type to show only `files` or
690 730 `dirs`. This command can be executed only using api_key belonging to user
691 731 with admin rights
692 732
693 733
694 734 INPUT::
695 735
696 736 id : <id_for_response>
697 737 api_key : "<api_key>"
698 738 method : "get_repo_nodes"
699 739 args: {
700 740 "repoid" : "<reponame or repo_id>"
701 741 "revision" : "<revision>",
702 742 "root_path" : "<root_path>",
703 743 "ret_type" : "<ret_type> = Optional('all')"
704 744 }
705 745
706 746 OUTPUT::
707 747
708 748 id : <id_given_in_input>
709 749 result: [
710 750 {
711 751 "name" : "<name>"
712 752 "type" : "<type>",
713 753 },
714 754
715 755 ]
716 756 error: null
717 757
718 758
719 759 create_repo
720 760 -----------
721 761
722 762 Creates a repository. If repository name contains "/", all needed repository
723 763 groups will be created. For example "foo/bar/baz" will create groups
724 764 "foo", "bar" (with "foo" as parent), and create "baz" repository with
725 765 "bar" as group. This command can be executed only using api_key belonging to user with admin
726 766 rights or regular user that have create repository permission. Regular users
727 767 cannot specify owner parameter
728 768
729 769
730 770 INPUT::
731 771
732 772 id : <id_for_response>
733 773 api_key : "<api_key>"
734 774 method : "create_repo"
735 775 args: {
736 776 "repo_name" : "<reponame>",
737 777 "owner" : "<onwer_name_or_id = Optional(=apiuser)>",
738 778 "repo_type" : "<repo_type> = Optional('hg')",
739 779 "description" : "<description> = Optional('')",
740 780 "private" : "<bool> = Optional(False)",
741 781 "clone_uri" : "<clone_uri> = Optional(None)",
742 782 "landing_rev" : "<landing_rev> = Optional('tip')",
743 783 "enable_downloads": "<bool> = Optional(False)",
744 784 "enable_locking": "<bool> = Optional(False)",
745 785 "enable_statistics": "<bool> = Optional(False)",
746 786 }
747 787
748 788 OUTPUT::
749 789
750 790 id : <id_given_in_input>
751 791 result: {
752 792 "msg": "Created new repository `<reponame>`",
753 793 "repo": {
754 794 "repo_id" : "<repo_id>",
755 795 "repo_name" : "<reponame>"
756 796 "repo_type" : "<repo_type>",
757 797 "clone_uri" : "<clone_uri>",
758 798 "private": : "<bool>",
759 799 "created_on" : "<datetimecreated>",
760 800 "description" : "<description>",
761 801 "landing_rev": "<landing_rev>",
762 802 "owner": "<username or user_id>",
763 803 "fork_of": "<name_of_fork_parent>",
764 804 "enable_downloads": "<bool>",
765 805 "enable_locking": "<bool>",
766 806 "enable_statistics": "<bool>",
767 807 },
768 808 }
769 809 error: null
770 810
771 811
772 812 fork_repo
773 813 ---------
774 814
775 815 Creates a fork of given repo. In case of using celery this will
776 816 immidiatelly return success message, while fork is going to be created
777 817 asynchronous. This command can be executed only using api_key belonging to
778 818 user with admin rights or regular user that have fork permission, and at least
779 819 read access to forking repository. Regular users cannot specify owner parameter.
780 820
781 821
782 822 INPUT::
783 823
784 824 id : <id_for_response>
785 825 api_key : "<api_key>"
786 826 method : "fork_repo"
787 827 args: {
788 828 "repoid" : "<reponame or repo_id>",
789 829 "fork_name": "<forkname>",
790 830 "owner": "<username or user_id = Optional(=apiuser)>",
791 831 "description": "<description>",
792 832 "copy_permissions": "<bool>",
793 833 "private": "<bool>",
794 834 "landing_rev": "<landing_rev>"
795 835
796 836 }
797 837
798 838 OUTPUT::
799 839
800 840 id : <id_given_in_input>
801 841 result: {
802 842 "msg": "Created fork of `<reponame>` as `<forkname>`",
803 843 "success": true
804 844 }
805 845 error: null
806 846
807 847
808 848 delete_repo
809 849 -----------
810 850
811 Deletes a repository. This command can be executed only using api_key belonging to user with admin
812 rights or regular user that have admin access to repository.
851 Deletes a repository. This command can be executed only using api_key belonging
852 to user with admin rights or regular user that have admin access to repository.
853 When `forks` param is set it's possible to detach or delete forks of deleting
854 repository
813 855
814 856
815 857 INPUT::
816 858
817 859 id : <id_for_response>
818 860 api_key : "<api_key>"
819 861 method : "delete_repo"
820 862 args: {
821 "repoid" : "<reponame or repo_id>"
863 "repoid" : "<reponame or repo_id>",
864 "forks" : "`delete` or `detach` = Optional(None)"
822 865 }
823 866
824 867 OUTPUT::
825 868
826 869 id : <id_given_in_input>
827 870 result: {
828 871 "msg": "Deleted repository `<reponame>`",
829 872 "success": true
830 873 }
831 874 error: null
832 875
833 876
834 877 grant_user_permission
835 878 ---------------------
836 879
837 880 Grant permission for user on given repository, or update existing one
838 881 if found. This command can be executed only using api_key belonging to user
839 882 with admin rights.
840 883
841 884
842 885 INPUT::
843 886
844 887 id : <id_for_response>
845 888 api_key : "<api_key>"
846 889 method : "grant_user_permission"
847 890 args: {
848 891 "repoid" : "<reponame or repo_id>"
849 892 "userid" : "<username or user_id>"
850 893 "perm" : "(repository.(none|read|write|admin))",
851 894 }
852 895
853 896 OUTPUT::
854 897
855 898 id : <id_given_in_input>
856 899 result: {
857 900 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
858 901 "success": true
859 902 }
860 903 error: null
861 904
862 905
863 906 revoke_user_permission
864 907 ----------------------
865 908
866 909 Revoke permission for user on given repository. This command can be executed
867 910 only using api_key belonging to user with admin rights.
868 911
869 912
870 913 INPUT::
871 914
872 915 id : <id_for_response>
873 916 api_key : "<api_key>"
874 917 method : "revoke_user_permission"
875 918 args: {
876 919 "repoid" : "<reponame or repo_id>"
877 920 "userid" : "<username or user_id>"
878 921 }
879 922
880 923 OUTPUT::
881 924
882 925 id : <id_given_in_input>
883 926 result: {
884 927 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
885 928 "success": true
886 929 }
887 930 error: null
888 931
889 932
890 933 grant_users_group_permission
891 934 ----------------------------
892 935
893 Grant permission for users group on given repository, or update
936 Grant permission for user group on given repository, or update
894 937 existing one if found. This command can be executed only using
895 938 api_key belonging to user with admin rights.
896 939
897 940
898 941 INPUT::
899 942
900 943 id : <id_for_response>
901 944 api_key : "<api_key>"
902 945 method : "grant_users_group_permission"
903 946 args: {
904 947 "repoid" : "<reponame or repo_id>"
905 "usersgroupid" : "<users group id or name>"
948 "usersgroupid" : "<user group id or name>"
906 949 "perm" : "(repository.(none|read|write|admin))",
907 950 }
908 951
909 952 OUTPUT::
910 953
911 954 id : <id_given_in_input>
912 955 result: {
913 956 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
914 957 "success": true
915 958 }
916 959 error: null
917 960
918 961
919 962 revoke_users_group_permission
920 963 -----------------------------
921 964
922 Revoke permission for users group on given repository.This command can be
965 Revoke permission for user group on given repository.This command can be
923 966 executed only using api_key belonging to user with admin rights.
924 967
925 968 INPUT::
926 969
927 970 id : <id_for_response>
928 971 api_key : "<api_key>"
929 972 method : "revoke_users_group_permission"
930 973 args: {
931 974 "repoid" : "<reponame or repo_id>"
932 "usersgroupid" : "<users group id or name>"
975 "usersgroupid" : "<user group id or name>"
933 976 }
934 977
935 978 OUTPUT::
936 979
937 980 id : <id_given_in_input>
938 981 result: {
939 982 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
940 983 "success": true
941 984 }
942 error: null No newline at end of file
985 error: null
@@ -1,35 +1,35 b''
1 1 .. _models:
2 2
3 3 ========================
4 4 The :mod:`models` Module
5 5 ========================
6 6
7 7 .. automodule:: rhodecode.model
8 8 :members:
9 9
10 10 .. automodule:: rhodecode.model.comment
11 11 :members:
12 12
13 13 .. automodule:: rhodecode.model.notification
14 14 :members:
15 15
16 16 .. automodule:: rhodecode.model.permission
17 17 :members:
18 18
19 19 .. automodule:: rhodecode.model.repo_permission
20 20 :members:
21 21
22 22 .. automodule:: rhodecode.model.repo
23 23 :members:
24 24
25 25 .. automodule:: rhodecode.model.repos_group
26 26 :members:
27 27
28 28 .. automodule:: rhodecode.model.scm
29 29 :members:
30 30
31 31 .. automodule:: rhodecode.model.user
32 32 :members:
33 33
34 34 .. automodule:: rhodecode.model.users_group
35 :members: No newline at end of file
35 :members:
@@ -1,976 +1,1053 b''
1 1 .. _changelog:
2 2
3 3 =========
4 4 Changelog
5 5 =========
6 6
7 1.6.0rc1 (**2013-04-07**)
8 -------------------------
9
10 news
11 ++++
12
13 - Redesign UI, with lots of small improvements.
14 - Group management delegation. Group admin can manage a group, and repos
15 under it, admin can create child groups inside group he manages.
16 - Repository extra fields. Optional unlimited extra fields can be defined for
17 each repository to store custom data.
18 - API get_repo call includes repo followers now.
19 - Large amounts of improvements in pull requests.
20 - #734 repo switcher is available in all pages.
21 - #733 API invalidate_cache function.
22 - Added option to turn on HSTS headers when using SSL.
23 - #83 show repo size on summary page.
24 - #745 added show full diff link into to big diff message.
25 - Deprecated RSS links - ATOM is the present and the future.
26 - Add option to define custom lexers for custom extensions for code highlight
27 in rcextension module.
28 - Git executable is now configurable via .ini files.
29 - #689 repositories now has optional detach/delete option for connected forks.
30 - Obfuscate password when cloning a remote repo with credentials.
31 - #788 tarball cache. zip or compressed tarballs can be optionally cached for
32 faster serving.
33 - Speed up of last_changeset extraction in VCS.
34 - API get_locks function.
35 - Configurable HTTP codes for repository locking.
36 - Possible to use closed branches in ?branch= in changelog.
37 - Linaro's ldap sync scripts.
38 - #797 git refs filter is now configurable via .ini file.
39 - New ishell paster command for easier administrative tasks.
40
41 fixes
42 +++++
43
44 - #654 switch to handles `/` in branch/tag/bookmark names.
45 - #572 moved out password reset tasks from celery.
46 - #730 filter out repo groups choices to only ones that you have write+ access.
47 - #462 disable file editing when not on branch head.
48 - #731 update-repoinfo sometimes failed to update data when changesets were
49 initial commits.
50 - #749,#805 and #516 Removed duplication of repo settings for rhodecode admins
51 and repo admins.
52 - Global permission update with "overwrite existing settings" shouldn't
53 override private repositories.
54 - #642 added recursion limit for stats gathering.
55 - #739 Delete/Edit repositories should only point to admin links if the user
56 is an super admin.
57 - Fixed escaping of html in "patch" view for GIT repos.
58 - #747 load changeset cache after forking to refresh lightweight dashboard caches.
59 - Quick repo list: public/private icon control should only control icons,
60 not repo visibility.
61 - #746 UnicodeDedode errors on feed controllers.
62 - #756 cleanup repos didn't properly compose paths of repos to be cleaned up.
63 - #763 gravatar helper function should fallback into default image if somehow
64 email provided is empty.
65 - Fixes #762, LDAP and container created users are now activated based on
66 the registration settings in permissions.
67 - Cleanup would recurse into every leaf and could thus not be used on lots of
68 large repositories.
69 - Better detection of deleting groups with subgroups inside.
70 - Fixed issue with renaming repos group together with changing parents with
71 multiple nested trees.
72 - #594 web interface file committing executes push hooks.
73 - Disallow cloning from different URI's that http[s]/svn/git/hg.
74 - Handling of RhodeCode extra params in consistent way.
75 - Don't normalize path if it's empty on adding a file through web interface.
76 - #808 missing changesets and files should return 404 not redirect
77 - #809 added url quote in clone url.
78 - Fixed issues with importing non-ascii repo names.
79 - Automatically assign instance_id for host and process if it has been set to *
80 - Fixed multiple IP addresses in each of extracted IP.
81 - Lot of other small bug fixes and improvements.
82
7 83 1.5.4 (**2013-03-13**)
8 84 ----------------------
9 85
10 86 news
11 87 ++++
12 88
13 89
14 90 fixes
15 91 +++++
16 92
17 93 - fixed webtest dependency issues
18 94 - fixed issues with celery tasks for password reset
19 95 - fixed #763 gravatar helper function should fallback into default image
20 96 if email is empty
21 97 - fixes #762 user global activation flag is also respected for LDAP created
22 98 accounts
23 99 - use password obfuscate when clonning a remote repo with credentials inside
24 - fixed issue with renaming repos group together with changing parents
100 - fixed issue with renaming repository group together with changing parents
25 101 - disallow cloning from file:/// URIs
26 102 - handle all cases with multiple IP addresses in proxy headers
27 103
28 104 1.5.3 (**2013-02-12**)
29 105 ----------------------
30 106
31 107 news
32 108 ++++
33 109
34 110 - IP restrictions now also enabled for IPv6
35 111
36 112 fixes
37 113 +++++
38 114
39 115 - fixed issues with private checkbox not always working
40 116 - fixed #746 unicodeDedode errors on feed controllers
41 117 - fixes issue #756 cleanup repos didn't properly compose paths of repos to be cleaned up.
42 118 - fixed cache invalidation issues together with vcs_full_cache option
43 119 - repo scan should skip directories with starting with '.'
44 120 - fixes for issue #731, update-repoinfo sometimes failed to update data when changesets
45 121 were initial commits
46 122 - recursive mode of setting permission skips private repositories
47 123
48 124 1.5.2 (**2013-01-14**)
49 125 ----------------------
50 126
51 127 news
52 128 ++++
53 129
54 130 - IP restrictions for users. Each user can get a set of whitelist IP+mask for
55 131 extra protection. Useful for buildbots etc.
56 132 - added full last changeset info to lightweight dashboard. lightweight dashboard
57 133 is now fully functional replacement of original dashboard.
58 134 - implemented certain API calls for non-admin users.
59 135 - enabled all Markdown Extra plugins
60 136 - implemented #725 Pull Request View - Show origin repo URL
61 137 - show comments from pull requests into associated changesets
62 138
63 139 fixes
64 140 +++++
65 141
66 142 - update repoinfo script is more failsafe
67 143 - fixed #687 Lazy loaded tooltip bug with simultaneous ajax requests
68 144 - fixed #691: Notifications for pull requests: move link to top for better
69 145 readability
70 146 - fixed #699: fix missing fork docs for API
71 147 - fixed #693 Opening changeset from pull request fails
72 148 - fixed #710 File view stripping empty lines from beginning and end of file
73 149 - fixed issues with getting repos by path on windows, caused GIT hooks to fail
74 150 - fixed issues with groups paginator on main dashboard
75 151 - improved fetch/pull command for git repos, now pulling all refs
76 152 - fixed issue #719 Journal revision ID tooltip AJAX query path is incorrect
77 153 when running in a subdir
78 154 - fixed issue #702 API methods without arguments fail when "args":null
79 155 - set the status of changesets initially on pull request. Fixes issues #690 and #587
80 156
81 157 1.5.1 (**2012-12-13**)
82 158 ----------------------
83 159
84 160 news
85 161 ++++
86 162
87 163 - implements #677: Don't allow to close pull requests when they are
88 164 under-review status
89 165 - implemented #670 Implementation of Roles in Pull Request
90 166
91 167 fixes
92 168 +++++
93 169
94 170 - default permissions can get duplicated after migration
95 171 - fixed changeset status labels, they now select radio buttons
96 172 - #682 translation difficult for multi-line text
97 173 - #683 fixed difference between messages about not mapped repositories
174 - email: fail nicely when no SMTP server has been configured
98 175
99 176 1.5.0 (**2012-12-12**)
100 177 ----------------------
101 178
102 179 news
103 180 ++++
104 181
105 182 - new rewritten from scratch diff engine. 10x faster in edge cases. Handling
106 183 of file renames, copies, change flags and binary files
107 184 - added lightweight dashboard option. ref #500. New version of dashboard
108 185 page that doesn't use any VCS data and is super fast to render. Recommended
109 186 for large amount of repositories.
110 187 - implements #648 write Script for updating last modification time for
111 188 lightweight dashboard
112 189 - implemented compare engine for git repositories.
113 190 - LDAP failover, option to specify multiple servers
114 191 - added Errormator and Sentry support for monitoring RhodeCode
115 192 - implemented #628: Pass server URL to rc-extensions hooks
116 193 - new tooltip implementation - added lazy loading of changesets from journal
117 194 pages. This can significantly improve speed of rendering the page
118 195 - implements #632,added branch/tag/bookmarks info into feeds
119 196 added changeset link to body of message
120 197 - implemented #638 permissions overview to groups
121 198 - implements #636, lazy loading of history and authors to speed up source
122 199 pages rendering
123 200 - implemented #647, option to pass list of default encoding used to
124 201 encode to/decode from unicode
125 202 - added caching layer into RSS/ATOM feeds.
126 203 - basic implementation of cherry picking changesets for pull request, ref #575
127 204 - implemented #661 Add option to include diff in RSS feed
128 205 - implemented file history page for showing detailed changelog for a given file
129 206 - implemented #663 Admin/permission: specify default repogroup perms
130 207 - implemented #379 defaults settings page for creation of repositories, locking
131 208 statistics, downloads, repository type
132 209 - implemented #210 filtering of admin journal based on Whoosh Query language
133 210 - added parents/children links in changeset viewref #650
134 211
135 212 fixes
136 213 +++++
137 214
138 215 - fixed git version checker
139 216 - #586 patched basic auth handler to fix issues with git behind proxy
140 217 - #589 search urlgenerator didn't properly escape special characters
141 218 - fixed issue #614 Include repo name in delete confirmation dialog
142 219 - fixed #623: Lang meta-tag doesn't work with C#/C++
143 220 - fixes #612 Double quotes to Single quotes result in bad html in diff
144 221 - fixes #630 git statistics do too much work making them slow.
145 222 - fixes #625 Git-Tags are not displayed in Shortlog
146 223 - fix for issue #602, enforce str when setting mercurial UI object.
147 224 When this is used together with mercurial internal translation system
148 225 it can lead to UnicodeDecodeErrors
149 226 - fixes #645 Fix git handler when doing delete remote branch
150 - implements #649 added two seperate method for author and commiter to VCS
151 changeset class switch author for git backed to be the real author not commiter
227 - implements #649 added two seperate method for author and committer to VCS
228 changeset class switch author for git backed to be the real author not committer
152 229 - fix issue #504 RhodeCode is showing different versions of README on
153 230 different summary page loads
154 231 - implemented #658 Changing username in LDAP-Mode should not be allowed.
155 232 - fixes #652 switch to generator approach when doing file annotation to prevent
156 233 huge memory consumption
157 234 - fixes #666 move lockkey path location to cache_dir to ensure this path is
158 235 always writable for rhodecode server
159 236 - many more small fixes and improvements
160 237 - fixed issues with recursive scans on removed repositories that could take
161 238 long time on instance start
162 239
163 240 1.4.4 (**2012-10-08**)
164 241 ----------------------
165 242
166 243 news
167 244 ++++
168 245
169 246 - obfuscate db password in logs for engine connection string
170 247 - #574 Show pull request status also in shortlog (if any)
171 248 - remember selected tab in my account page
172 249 - Bumped mercurial version to 2.3.2
173 250 - #595 rcextension hook for repository delete
174 251
175 252 fixes
176 253 +++++
177 254
178 255 - Add git version detection to warn users that Git used in system is to
179 256 old. Ref #588 - also show git version in system details in settings page
180 257 - fixed files quick filter links
181 258 - #590 Add GET flag that controls the way the diff are generated, for pull
182 259 requests we want to use non-bundle based diffs, That are far better for
183 260 doing code reviews. The /compare url still uses bundle compare for full
184 261 comparison including the incoming changesets
185 262 - Fixed #585, checks for status of revision where to strict, and made
186 263 opening pull request with those revision impossible due to previously set
187 264 status. Checks now are made also for the repository.
188 265 - fixes #591 git backend was causing encoding errors when handling binary
189 266 files - added a test case for VCS lib tests
190 267 - fixed #597 commits in future get negative age.
191 268 - fixed #598 API docs methods had wrong members parameter as returned data
192 269
193 270 1.4.3 (**2012-09-28**)
194 271 ----------------------
195 272
196 273 news
197 274 ++++
198 275
199 276 - #558 Added config file to hooks extra data
200 277 - bumped mercurial version to 2.3.1
201 278 - #518 added possibility of specifying multiple patterns for issues
202 279 - update codemirror to latest version
203 280
204 281 fixes
205 282 +++++
206 283
207 - fixed #570 explicit users group permissions can overwrite owner permissions
284 - fixed #570 explicit user group permissions can overwrite owner permissions
208 285 - fixed #578 set proper PATH with current Python for Git
209 286 hooks to execute within same Python as RhodeCode
210 287 - fixed issue with Git bare repos that ends with .git in name
211 288
212 289 1.4.2 (**2012-09-12**)
213 290 ----------------------
214 291
215 292 news
216 293 ++++
217 294
218 295 - added option to menu to quick lock/unlock repository for users that have
219 296 write access to
220 297 - Implemented permissions for writing to repo
221 298 groups. Now only write access to group allows to create a repostiory
222 299 within that group
223 300 - #565 Add support for {netloc} and {scheme} to alternative_gravatar_url
224 301 - updated translation for zh_CN
225 302
226 303 fixes
227 304 +++++
228 305
229 - fixed visual permissions check on repos groups inside groups
306 - fixed visual permissions check on repository groups inside groups
230 307 - fixed issues with non-ascii search terms in search, and indexers
231 308 - fixed parsing of page number in GET parameters
232 309 - fixed issues with generating pull-request overview for repos with
233 310 bookmarks and tags, also preview doesn't loose chosen revision from
234 311 select dropdown
235 312
236 313 1.4.1 (**2012-09-07**)
237 314 ----------------------
238 315
239 316 news
240 317 ++++
241 318
242 319 - always put a comment about code-review status change even if user send
243 320 empty data
244 321 - modified_on column saves repository update and it's going to be used
245 322 later for light version of main page ref #500
246 323 - pull request notifications send much nicer emails with details about pull
247 324 request
248 325 - #551 show breadcrumbs in summary view for repositories inside a group
249 326
250 327 fixes
251 328 +++++
252 329
253 330 - fixed migrations of permissions that can lead to inconsistency.
254 331 Some users sent feedback that after upgrading from older versions issues
255 332 with updating default permissions occurred. RhodeCode detects that now and
256 333 resets default user permission to initial state if there is a need for that.
257 334 Also forces users to set the default value for new forking permission.
258 335 - #535 improved apache wsgi example configuration in docs
259 336 - fixes #550 mercurial repositories comparision failed when origin repo had
260 337 additional not-common changesets
261 338 - fixed status of code-review in preview windows of pull request
262 339 - git forks were not initialized at bare repos
263 340 - fixes #555 fixes issues with comparing non-related repositories
264 341 - fixes #557 follower counter always counts up
265 342 - fixed issue #560 require push ssl checkbox wasn't shown when option was
266 343 enabled
267 344 - fixed #559
268 345 - fixed issue #559 fixed bug in routing that mapped repo names with <name>_<num> in name as
269 346 if it was a request to url by repository ID
270 347
271 348 1.4.0 (**2012-09-03**)
272 349 ----------------------
273 350
274 351 news
275 352 ++++
276 353
277 354 - new codereview system
278 355 - email map, allowing users to have multiple email addresses mapped into
279 356 their accounts
280 357 - improved git-hook system. Now all actions for git are logged into journal
281 358 including pushed revisions, user and IP address
282 359 - changed setup-app into setup-rhodecode and added default options to it.
283 360 - new git repos are created as bare now by default
284 361 - #464 added links to groups in permission box
285 362 - #465 mentions autocomplete inside comments boxes
286 363 - #469 added --update-only option to whoosh to re-index only given list
287 364 of repos in index
288 365 - rhodecode-api CLI client
289 366 - new git http protocol replaced buggy dulwich implementation.
290 367 Now based on pygrack & gitweb
291 368 - Improved RSS/ATOM feeds. Discoverable by browsers using proper headers, and
292 369 reformated based on user suggestions. Additional rss/atom feeds for user
293 370 journal
294 371 - various i18n improvements
295 372 - #478 permissions overview for admin in user edit view
296 373 - File view now displays small gravatars off all authors of given file
297 374 - Implemented landing revisions. Each repository will get landing_rev attribute
298 375 that defines 'default' revision/branch for generating readme files
299 376 - Implemented #509, RhodeCode enforces SSL for push/pulling if requested at
300 377 earliest possible call.
301 378 - Import remote svn repositories to mercurial using hgsubversion.
302 379 - Fixed #508 RhodeCode now has a option to explicitly set forking permissions
303 380 - RhodeCode can use alternative server for generating avatar icons
304 381 - implemented repositories locking. Pull locks, push unlocks. Also can be done
305 382 via API calls
306 383 - #538 form for permissions can handle multiple users at once
307 384
308 385 fixes
309 386 +++++
310 387
311 388 - improved translations
312 389 - fixes issue #455 Creating an archive generates an exception on Windows
313 390 - fixes #448 Download ZIP archive keeps file in /tmp open and results
314 391 in out of disk space
315 392 - fixes issue #454 Search results under Windows include proceeding
316 393 backslash
317 394 - fixed issue #450. Rhodecode no longer will crash when bad revision is
318 395 present in journal data.
319 396 - fix for issue #417, git execution was broken on windows for certain
320 397 commands.
321 398 - fixed #413. Don't disable .git directory for bare repos on deleting
322 399 - fixed issue #459. Changed the way of obtaining logger in reindex task.
323 400 - fixed #453 added ID field in whoosh SCHEMA that solves the issue of
324 401 reindexing modified files
325 402 - fixed #481 rhodecode emails are sent without Date header
326 403 - fixed #458 wrong count when no repos are present
327 404 - fixed issue #492 missing `\ No newline at end of file` test at the end of
328 405 new chunk in html diff
329 406 - full text search now works also for commit messages
330 407
331 408 1.3.6 (**2012-05-17**)
332 409 ----------------------
333 410
334 411 news
335 412 ++++
336 413
337 414 - chinese traditional translation
338 415 - changed setup-app into setup-rhodecode and added arguments for auto-setup
339 416 mode that doesn't need user interaction
340 417
341 418 fixes
342 419 +++++
343 420
344 421 - fixed no scm found warning
345 422 - fixed __future__ import error on rcextensions
346 423 - made simplejson required lib for speedup on JSON encoding
347 424 - fixes #449 bad regex could get more than revisions from parsing history
348 425 - don't clear DB session when CELERY_EAGER is turned ON
349 426
350 427 1.3.5 (**2012-05-10**)
351 428 ----------------------
352 429
353 430 news
354 431 ++++
355 432
356 433 - use ext_json for json module
357 434 - unified annotation view with file source view
358 435 - notification improvements, better inbox + css
359 436 - #419 don't strip passwords for login forms, make rhodecode
360 437 more compatible with LDAP servers
361 438 - Added HTTP_X_FORWARDED_FOR as another method of extracting
362 439 IP for pull/push logs. - moved all to base controller
363 440 - #415: Adding comment to changeset causes reload.
364 441 Comments are now added via ajax and doesn't reload the page
365 442 - #374 LDAP config is discarded when LDAP can't be activated
366 443 - limited push/pull operations are now logged for git in the journal
367 444 - bumped mercurial to 2.2.X series
368 445 - added support for displaying submodules in file-browser
369 446 - #421 added bookmarks in changelog view
370 447
371 448 fixes
372 449 +++++
373 450
374 451 - fixed dev-version marker for stable when served from source codes
375 452 - fixed missing permission checks on show forks page
376 453 - #418 cast to unicode fixes in notification objects
377 454 - #426 fixed mention extracting regex
378 455 - fixed remote-pulling for git remotes remopositories
379 456 - fixed #434: Error when accessing files or changesets of a git repository
380 457 with submodules
381 458 - fixed issue with empty APIKEYS for users after registration ref. #438
382 459 - fixed issue with getting README files from git repositories
383 460
384 461 1.3.4 (**2012-03-28**)
385 462 ----------------------
386 463
387 464 news
388 465 ++++
389 466
390 467 - Whoosh logging is now controlled by the .ini files logging setup
391 468 - added clone-url into edit form on /settings page
392 469 - added help text into repo add/edit forms
393 470 - created rcextensions module with additional mappings (ref #322) and
394 471 post push/pull/create repo hooks callbacks
395 472 - implemented #377 Users view for his own permissions on account page
396 - #399 added inheritance of permissions for users group on repos groups
473 - #399 added inheritance of permissions for user group on repository groups
397 474 - #401 repository group is automatically pre-selected when adding repos
398 475 inside a repository group
399 476 - added alternative HTTP 403 response when client failed to authenticate. Helps
400 477 solving issues with Mercurial and LDAP
401 478 - #402 removed group prefix from repository name when listing repositories
402 479 inside a group
403 480 - added gravatars into permission view and permissions autocomplete
404 481 - #347 when running multiple RhodeCode instances, properly invalidates cache
405 482 for all registered servers
406 483
407 484 fixes
408 485 +++++
409 486
410 487 - fixed #390 cache invalidation problems on repos inside group
411 488 - fixed #385 clone by ID url was loosing proxy prefix in URL
412 489 - fixed some unicode problems with waitress
413 490 - fixed issue with escaping < and > in changeset commits
414 491 - fixed error occurring during recursive group creation in API
415 492 create_repo function
416 493 - fixed #393 py2.5 fixes for routes url generator
417 494 - fixed #397 Private repository groups shows up before login
418 495 - fixed #396 fixed problems with revoking users in nested groups
419 496 - fixed mysql unicode issues + specified InnoDB as default engine with
420 497 utf8 charset
421 498 - #406 trim long branch/tag names in changelog to not break UI
422 499
423 500 1.3.3 (**2012-03-02**)
424 501 ----------------------
425 502
426 503 news
427 504 ++++
428 505
429 506
430 507 fixes
431 508 +++++
432 509
433 510 - fixed some python2.5 compatibility issues
434 511 - fixed issues with removed repos was accidentally added as groups, after
435 512 full rescan of paths
436 513 - fixes #376 Cannot edit user (using container auth)
437 514 - fixes #378 Invalid image urls on changeset screen with proxy-prefix
438 515 configuration
439 516 - fixed initial sorting of repos inside repo group
440 517 - fixes issue when user tried to resubmit same permission into user/user_groups
441 518 - bumped beaker version that fixes #375 leap error bug
442 519 - fixed raw_changeset for git. It was generated with hg patch headers
443 520 - fixed vcs issue with last_changeset for filenodes
444 521 - fixed missing commit after hook delete
445 522 - fixed #372 issues with git operation detection that caused a security issue
446 523 for git repos
447 524
448 525 1.3.2 (**2012-02-28**)
449 526 ----------------------
450 527
451 528 news
452 529 ++++
453 530
454 531
455 532 fixes
456 533 +++++
457 534
458 535 - fixed git protocol issues with repos-groups
459 536 - fixed git remote repos validator that prevented from cloning remote git repos
460 537 - fixes #370 ending slashes fixes for repo and groups
461 538 - fixes #368 improved git-protocol detection to handle other clients
462 539 - fixes #366 When Setting Repository Group To Blank Repo Group Wont Be
463 540 Moved To Root
464 541 - fixes #371 fixed issues with beaker/sqlalchemy and non-ascii cache keys
465 542 - fixed #373 missing cascade drop on user_group_to_perm table
466 543
467 544 1.3.1 (**2012-02-27**)
468 545 ----------------------
469 546
470 547 news
471 548 ++++
472 549
473 550
474 551 fixes
475 552 +++++
476 553
477 554 - redirection loop occurs when remember-me wasn't checked during login
478 555 - fixes issues with git blob history generation
479 556 - don't fetch branch for git in file history dropdown. Causes unneeded slowness
480 557
481 558 1.3.0 (**2012-02-26**)
482 559 ----------------------
483 560
484 561 news
485 562 ++++
486 563
487 564 - code review, inspired by github code-comments
488 565 - #215 rst and markdown README files support
489 566 - #252 Container-based and proxy pass-through authentication support
490 567 - #44 branch browser. Filtering of changelog by branches
491 568 - mercurial bookmarks support
492 569 - new hover top menu, optimized to add maximum size for important views
493 570 - configurable clone url template with possibility to specify protocol like
494 571 ssh:// or http:// and also manually alter other parts of clone_url.
495 572 - enabled largefiles extension by default
496 573 - optimized summary file pages and saved a lot of unused space in them
497 574 - #239 option to manually mark repository as fork
498 575 - #320 mapping of commit authors to RhodeCode users
499 576 - #304 hashes are displayed using monospace font
500 577 - diff configuration, toggle white lines and context lines
501 578 - #307 configurable diffs, whitespace toggle, increasing context lines
502 579 - sorting on branches, tags and bookmarks using YUI datatable
503 580 - improved file filter on files page
504 581 - implements #330 api method for listing nodes ar particular revision
505 582 - #73 added linking issues in commit messages to chosen issue tracker url
506 583 based on user defined regular expression
507 584 - added linking of changesets in commit messages
508 585 - new compact changelog with expandable commit messages
509 586 - firstname and lastname are optional in user creation
510 587 - #348 added post-create repository hook
511 588 - #212 global encoding settings is now configurable from .ini files
512 589 - #227 added repository groups permissions
513 590 - markdown gets codehilite extensions
514 591 - new API methods, delete_repositories, grante/revoke permissions for groups
515 592 and repos
516 593
517 594
518 595 fixes
519 596 +++++
520 597
521 598 - rewrote dbsession management for atomic operations, and better error handling
522 599 - fixed sorting of repo tables
523 600 - #326 escape of special html entities in diffs
524 601 - normalized user_name => username in api attributes
525 602 - fixes #298 ldap created users with mixed case emails created conflicts
526 603 on saving a form
527 604 - fixes issue when owner of a repo couldn't revoke permissions for users
528 605 and groups
529 606 - fixes #271 rare JSON serialization problem with statistics
530 607 - fixes #337 missing validation check for conflicting names of a group with a
531 repositories group
608 repository group
532 609 - #340 fixed session problem for mysql and celery tasks
533 610 - fixed #331 RhodeCode mangles repository names if the a repository group
534 611 contains the "full path" to the repositories
535 612 - #355 RhodeCode doesn't store encrypted LDAP passwords
536 613
537 614 1.2.5 (**2012-01-28**)
538 615 ----------------------
539 616
540 617 news
541 618 ++++
542 619
543 620 fixes
544 621 +++++
545 622
546 623 - #340 Celery complains about MySQL server gone away, added session cleanup
547 624 for celery tasks
548 625 - #341 "scanning for repositories in None" log message during Rescan was missing
549 626 a parameter
550 627 - fixed creating archives with subrepos. Some hooks were triggered during that
551 628 operation leading to crash.
552 629 - fixed missing email in account page.
553 630 - Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
554 631 forking on windows impossible
555 632
556 633 1.2.4 (**2012-01-19**)
557 634 ----------------------
558 635
559 636 news
560 637 ++++
561 638
562 639 - RhodeCode is bundled with mercurial series 2.0.X by default, with
563 640 full support to largefiles extension. Enabled by default in new installations
564 641 - #329 Ability to Add/Remove Groups to/from a Repository via AP
565 642 - added requires.txt file with requirements
566 643
567 644 fixes
568 645 +++++
569 646
570 647 - fixes db session issues with celery when emailing admins
571 648 - #331 RhodeCode mangles repository names if the a repository group
572 649 contains the "full path" to the repositories
573 650 - #298 Conflicting e-mail addresses for LDAP and RhodeCode users
574 651 - DB session cleanup after hg protocol operations, fixes issues with
575 652 `mysql has gone away` errors
576 653 - #333 doc fixes for get_repo api function
577 654 - #271 rare JSON serialization problem with statistics enabled
578 655 - #337 Fixes issues with validation of repository name conflicting with
579 656 a group name. A proper message is now displayed.
580 657 - #292 made ldap_dn in user edit readonly, to get rid of confusion that field
581 658 doesn't work
582 659 - #316 fixes issues with web description in hgrc files
583 660
584 661 1.2.3 (**2011-11-02**)
585 662 ----------------------
586 663
587 664 news
588 665 ++++
589 666
590 - added option to manage repos group for non admin users
667 - added option to manage repository group for non admin users
591 668 - added following API methods for get_users, create_user, get_users_groups,
592 669 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
593 670 get_repo, create_repo, add_user_to_repo
594 671 - implements #237 added password confirmation for my account
595 672 and admin edit user.
596 673 - implements #291 email notification for global events are now sent to all
597 674 administrator users, and global config email.
598 675
599 676 fixes
600 677 +++++
601 678
602 679 - added option for passing auth method for smtp mailer
603 680 - #276 issue with adding a single user with id>10 to usergroups
604 681 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
605 682 - #288 fixes managing of repos in a group for non admin user
606 683
607 684 1.2.2 (**2011-10-17**)
608 685 ----------------------
609 686
610 687 news
611 688 ++++
612 689
613 690 - #226 repo groups are available by path instead of numerical id
614 691
615 692 fixes
616 693 +++++
617 694
618 695 - #259 Groups with the same name but with different parent group
619 696 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
620 697 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
621 698 - #265 ldap save fails sometimes on converting attributes to booleans,
622 699 added getter and setter into model that will prevent from this on db model level
623 700 - fixed problems with timestamps issues #251 and #213
624 701 - fixes #266 RhodeCode allows to create repo with the same name and in
625 702 the same parent as group
626 703 - fixes #245 Rescan of the repositories on Windows
627 704 - fixes #248 cannot edit repos inside a group on windows
628 705 - fixes #219 forking problems on windows
629 706
630 707 1.2.1 (**2011-10-08**)
631 708 ----------------------
632 709
633 710 news
634 711 ++++
635 712
636 713
637 714 fixes
638 715 +++++
639 716
640 717 - fixed problems with basic auth and push problems
641 718 - gui fixes
642 719 - fixed logger
643 720
644 721 1.2.0 (**2011-10-07**)
645 722 ----------------------
646 723
647 724 news
648 725 ++++
649 726
650 727 - implemented #47 repository groups
651 728 - implemented #89 Can setup google analytics code from settings menu
652 729 - implemented #91 added nicer looking archive urls with more download options
653 730 like tags, branches
654 731 - implemented #44 into file browsing, and added follow branch option
655 732 - implemented #84 downloads can be enabled/disabled for each repository
656 733 - anonymous repository can be cloned without having to pass default:default
657 734 into clone url
658 735 - fixed #90 whoosh indexer can index chooses repositories passed in command
659 736 line
660 737 - extended journal with day aggregates and paging
661 738 - implemented #107 source code lines highlight ranges
662 739 - implemented #93 customizable changelog on combined revision ranges -
663 740 equivalent of githubs compare view
664 741 - implemented #108 extended and more powerful LDAP configuration
665 - implemented #56 users groups
742 - implemented #56 user groups
666 743 - major code rewrites optimized codes for speed and memory usage
667 744 - raw and diff downloads are now in git format
668 745 - setup command checks for write access to given path
669 746 - fixed many issues with international characters and unicode. It uses utf8
670 747 decode with replace to provide less errors even with non utf8 encoded strings
671 748 - #125 added API KEY access to feeds
672 749 - #109 Repository can be created from external Mercurial link (aka. remote
673 750 repository, and manually updated (via pull) from admin panel
674 751 - beta git support - push/pull server + basic view for git repos
675 752 - added followers page and forks page
676 753 - server side file creation (with binary file upload interface)
677 754 and edition with commits powered by codemirror
678 755 - #111 file browser file finder, quick lookup files on whole file tree
679 756 - added quick login sliding menu into main page
680 757 - changelog uses lazy loading of affected files details, in some scenarios
681 758 this can improve speed of changelog page dramatically especially for
682 759 larger repositories.
683 760 - implements #214 added support for downloading subrepos in download menu.
684 761 - Added basic API for direct operations on rhodecode via JSON
685 762 - Implemented advanced hook management
686 763
687 764 fixes
688 765 +++++
689 766
690 767 - fixed file browser bug, when switching into given form revision the url was
691 768 not changing
692 769 - fixed propagation to error controller on simplehg and simplegit middlewares
693 770 - fixed error when trying to make a download on empty repository
694 771 - fixed problem with '[' chars in commit messages in journal
695 772 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
696 773 - journal fork fixes
697 774 - removed issue with space inside renamed repository after deletion
698 775 - fixed strange issue on formencode imports
699 776 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
700 777 - #150 fixes for errors on repositories mapped in db but corrupted in
701 778 filesystem
702 779 - fixed problem with ascendant characters in realm #181
703 780 - fixed problem with sqlite file based database connection pool
704 781 - whoosh indexer and code stats share the same dynamic extensions map
705 782 - fixes #188 - relationship delete of repo_to_perm entry on user removal
706 783 - fixes issue #189 Trending source files shows "show more" when no more exist
707 784 - fixes issue #197 Relative paths for pidlocks
708 785 - fixes issue #198 password will require only 3 chars now for login form
709 786 - fixes issue #199 wrong redirection for non admin users after creating a repository
710 787 - fixes issues #202, bad db constraint made impossible to attach same group
711 788 more than one time. Affects only mysql/postgres
712 789 - fixes #218 os.kill patch for windows was missing sig param
713 790 - improved rendering of dag (they are not trimmed anymore when number of
714 791 heads exceeds 5)
715 792
716 793 1.1.8 (**2011-04-12**)
717 794 ----------------------
718 795
719 796 news
720 797 ++++
721 798
722 799 - improved windows support
723 800
724 801 fixes
725 802 +++++
726 803
727 804 - fixed #140 freeze of python dateutil library, since new version is python2.x
728 805 incompatible
729 806 - setup-app will check for write permission in given path
730 807 - cleaned up license info issue #149
731 808 - fixes for issues #137,#116 and problems with unicode and accented characters.
732 809 - fixes crashes on gravatar, when passed in email as unicode
733 810 - fixed tooltip flickering problems
734 811 - fixed came_from redirection on windows
735 812 - fixed logging modules, and sql formatters
736 813 - windows fixes for os.kill issue #133
737 814 - fixes path splitting for windows issues #148
738 815 - fixed issue #143 wrong import on migration to 1.1.X
739 816 - fixed problems with displaying binary files, thanks to Thomas Waldmann
740 817 - removed name from archive files since it's breaking ui for long repo names
741 818 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
742 819 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
743 820 Thomas Waldmann
744 821 - fixed issue #166 summary pager was skipping 10 revisions on second page
745 822
746 823
747 824 1.1.7 (**2011-03-23**)
748 825 ----------------------
749 826
750 827 news
751 828 ++++
752 829
753 830 fixes
754 831 +++++
755 832
756 833 - fixed (again) #136 installation support for FreeBSD
757 834
758 835
759 836 1.1.6 (**2011-03-21**)
760 837 ----------------------
761 838
762 839 news
763 840 ++++
764 841
765 842 fixes
766 843 +++++
767 844
768 845 - fixed #136 installation support for FreeBSD
769 846 - RhodeCode will check for python version during installation
770 847
771 848 1.1.5 (**2011-03-17**)
772 849 ----------------------
773 850
774 851 news
775 852 ++++
776 853
777 854 - basic windows support, by exchanging pybcrypt into sha256 for windows only
778 855 highly inspired by idea of mantis406
779 856
780 857 fixes
781 858 +++++
782 859
783 860 - fixed sorting by author in main page
784 861 - fixed crashes with diffs on binary files
785 862 - fixed #131 problem with boolean values for LDAP
786 863 - fixed #122 mysql problems thanks to striker69
787 864 - fixed problem with errors on calling raw/raw_files/annotate functions
788 865 with unknown revisions
789 866 - fixed returned rawfiles attachment names with international character
790 867 - cleaned out docs, big thanks to Jason Harris
791 868
792 869 1.1.4 (**2011-02-19**)
793 870 ----------------------
794 871
795 872 news
796 873 ++++
797 874
798 875 fixes
799 876 +++++
800 877
801 878 - fixed formencode import problem on settings page, that caused server crash
802 879 when that page was accessed as first after server start
803 880 - journal fixes
804 881 - fixed option to access repository just by entering http://server/<repo_name>
805 882
806 883 1.1.3 (**2011-02-16**)
807 884 ----------------------
808 885
809 886 news
810 887 ++++
811 888
812 889 - implemented #102 allowing the '.' character in username
813 890 - added option to access repository just by entering http://server/<repo_name>
814 891 - celery task ignores result for better performance
815 892
816 893 fixes
817 894 +++++
818 895
819 896 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
820 897 apollo13 and Johan Walles
821 898 - small fixes in journal
822 899 - fixed problems with getting setting for celery from .ini files
823 900 - registration, password reset and login boxes share the same title as main
824 901 application now
825 902 - fixed #113: to high permissions to fork repository
826 903 - fixed problem with '[' chars in commit messages in journal
827 904 - removed issue with space inside renamed repository after deletion
828 905 - db transaction fixes when filesystem repository creation failed
829 906 - fixed #106 relation issues on databases different than sqlite
830 907 - fixed static files paths links to use of url() method
831 908
832 909 1.1.2 (**2011-01-12**)
833 910 ----------------------
834 911
835 912 news
836 913 ++++
837 914
838 915
839 916 fixes
840 917 +++++
841 918
842 919 - fixes #98 protection against float division of percentage stats
843 920 - fixed graph bug
844 921 - forced webhelpers version since it was making troubles during installation
845 922
846 923 1.1.1 (**2011-01-06**)
847 924 ----------------------
848 925
849 926 news
850 927 ++++
851 928
852 929 - added force https option into ini files for easier https usage (no need to
853 930 set server headers with this options)
854 931 - small css updates
855 932
856 933 fixes
857 934 +++++
858 935
859 936 - fixed #96 redirect loop on files view on repositories without changesets
860 937 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
861 938 and server crashed with errors
862 939 - fixed large tooltips problems on main page
863 940 - fixed #92 whoosh indexer is more error proof
864 941
865 942 1.1.0 (**2010-12-18**)
866 943 ----------------------
867 944
868 945 news
869 946 ++++
870 947
871 948 - rewrite of internals for vcs >=0.1.10
872 949 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
873 950 with older clients
874 951 - anonymous access, authentication via ldap
875 952 - performance upgrade for cached repos list - each repository has its own
876 953 cache that's invalidated when needed.
877 954 - performance upgrades on repositories with large amount of commits (20K+)
878 955 - main page quick filter for filtering repositories
879 956 - user dashboards with ability to follow chosen repositories actions
880 957 - sends email to admin on new user registration
881 958 - added cache/statistics reset options into repository settings
882 959 - more detailed action logger (based on hooks) with pushed changesets lists
883 960 and options to disable those hooks from admin panel
884 961 - introduced new enhanced changelog for merges that shows more accurate results
885 962 - new improved and faster code stats (based on pygments lexers mapping tables,
886 963 showing up to 10 trending sources for each repository. Additionally stats
887 964 can be disabled in repository settings.
888 965 - gui optimizations, fixed application width to 1024px
889 966 - added cut off (for large files/changesets) limit into config files
890 967 - whoosh, celeryd, upgrade moved to paster command
891 968 - other than sqlite database backends can be used
892 969
893 970 fixes
894 971 +++++
895 972
896 973 - fixes #61 forked repo was showing only after cache expired
897 974 - fixes #76 no confirmation on user deletes
898 975 - fixes #66 Name field misspelled
899 976 - fixes #72 block user removal when he owns repositories
900 977 - fixes #69 added password confirmation fields
901 978 - fixes #87 RhodeCode crashes occasionally on updating repository owner
902 979 - fixes #82 broken annotations on files with more than 1 blank line at the end
903 980 - a lot of fixes and tweaks for file browser
904 981 - fixed detached session issues
905 982 - fixed when user had no repos he would see all repos listed in my account
906 983 - fixed ui() instance bug when global hgrc settings was loaded for server
907 984 instance and all hgrc options were merged with our db ui() object
908 985 - numerous small bugfixes
909 986
910 987 (special thanks for TkSoh for detailed feedback)
911 988
912 989
913 990 1.0.2 (**2010-11-12**)
914 991 ----------------------
915 992
916 993 news
917 994 ++++
918 995
919 996 - tested under python2.7
920 997 - bumped sqlalchemy and celery versions
921 998
922 999 fixes
923 1000 +++++
924 1001
925 1002 - fixed #59 missing graph.js
926 1003 - fixed repo_size crash when repository had broken symlinks
927 1004 - fixed python2.5 crashes.
928 1005
929 1006
930 1007 1.0.1 (**2010-11-10**)
931 1008 ----------------------
932 1009
933 1010 news
934 1011 ++++
935 1012
936 1013 - small css updated
937 1014
938 1015 fixes
939 1016 +++++
940 1017
941 1018 - fixed #53 python2.5 incompatible enumerate calls
942 1019 - fixed #52 disable mercurial extension for web
943 1020 - fixed #51 deleting repositories don't delete it's dependent objects
944 1021
945 1022
946 1023 1.0.0 (**2010-11-02**)
947 1024 ----------------------
948 1025
949 1026 - security bugfix simplehg wasn't checking for permissions on commands
950 1027 other than pull or push.
951 1028 - fixed doubled messages after push or pull in admin journal
952 1029 - templating and css corrections, fixed repo switcher on chrome, updated titles
953 1030 - admin menu accessible from options menu on repository view
954 1031 - permissions cached queries
955 1032
956 1033 1.0.0rc4 (**2010-10-12**)
957 1034 --------------------------
958 1035
959 1036 - fixed python2.5 missing simplejson imports (thanks to Jens Bäckman)
960 1037 - removed cache_manager settings from sqlalchemy meta
961 1038 - added sqlalchemy cache settings to ini files
962 1039 - validated password length and added second try of failure on paster setup-app
963 1040 - fixed setup database destroy prompt even when there was no db
964 1041
965 1042
966 1043 1.0.0rc3 (**2010-10-11**)
967 1044 -------------------------
968 1045
969 1046 - fixed i18n during installation.
970 1047
971 1048 1.0.0rc2 (**2010-10-11**)
972 1049 -------------------------
973 1050
974 1051 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
975 1052 occure. After vcs is fixed it'll be put back again.
976 1053 - templating/css rewrites, optimized css.
@@ -1,64 +1,64 b''
1 1 .. _index:
2 2
3 3 .. include:: ./../README.rst
4 4
5 5 Users Guide
6 6 -----------
7 7
8 8 **Installation:**
9 9
10 10 .. toctree::
11 11 :maxdepth: 1
12 12
13 13 installation
14 14 setup
15 15 upgrade
16 16
17 17 **Usage**
18 18
19 19 .. toctree::
20 20 :maxdepth: 1
21 21
22 22 usage/general
23 23 usage/git_support
24 24 usage/performance
25 25 usage/locking
26 26 usage/statistics
27 27 usage/backup
28 28 usage/subrepos
29 29 usage/debugging
30 30 usage/troubleshooting
31 31
32 32 **Develop**
33 33
34 34 .. toctree::
35 35 :maxdepth: 1
36 36
37 37 contributing
38 38 changelog
39 39
40 40 **API**
41 41
42 42 .. toctree::
43 43 :maxdepth: 1
44 44
45 45 api/api
46 46 api/models
47 47
48 48
49 49 Other topics
50 50 ------------
51 51
52 52 * :ref:`genindex`
53 53 * :ref:`search`
54 54
55 55 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
56 56 .. _python: http://www.python.org/
57 57 .. _django: http://www.djangoproject.com/
58 58 .. _mercurial: http://mercurial.selenic.com/
59 59 .. _bitbucket: http://bitbucket.org/
60 60 .. _subversion: http://subversion.tigris.org/
61 61 .. _git: http://git-scm.com/
62 62 .. _celery: http://celeryproject.org/
63 63 .. _Sphinx: http://sphinx.pocoo.org/
64 .. _vcs: http://pypi.python.org/pypi/vcs No newline at end of file
64 .. _vcs: http://pypi.python.org/pypi/vcs
@@ -1,735 +1,735 b''
1 1 .. _setup:
2 2
3 3 =====
4 4 Setup
5 5 =====
6 6
7 7
8 8 Setting up RhodeCode
9 9 --------------------
10 10
11 11 First, you will need to create a RhodeCode configuration file. Run the
12 12 following command to do this::
13 13
14 14 paster make-config RhodeCode production.ini
15 15
16 16 - This will create the file `production.ini` in the current directory. This
17 17 configuration file contains the various settings for RhodeCode, e.g proxy
18 18 port, email settings, usage of static files, cache, celery settings and
19 19 logging.
20 20
21 21
22 22 Next, you need to create the databases used by RhodeCode. I recommend that you
23 23 use postgresql or sqlite (default). If you choose a database other than the
24 24 default ensure you properly adjust the db url in your production.ini
25 25 configuration file to use this other database. RhodeCode currently supports
26 26 postgresql, sqlite and mysql databases. Create the database by running
27 27 the following command::
28 28
29 29 paster setup-rhodecode production.ini
30 30
31 31 This will prompt you for a "root" path. This "root" path is the location where
32 32 RhodeCode will store all of its repositories on the current machine. After
33 33 entering this "root" path ``setup-rhodecode`` will also prompt you for a username
34 34 and password for the initial admin account which ``setup-rhodecode`` sets
35 35 up for you.
36 36
37 37 setup process can be fully automated, example for lazy::
38 38
39 39 paster setup-rhodecode production.ini --user=marcink --password=secret --email=marcin@rhodecode.org --repos=/home/marcink/my_repos
40 40
41 41
42 42 - The ``setup-rhodecode`` command will create all of the needed tables and an
43 43 admin account. When choosing a root path you can either use a new empty
44 44 location, or a location which already contains existing repositories. If you
45 45 choose a location which contains existing repositories RhodeCode will simply
46 46 add all of the repositories at the chosen location to it's database.
47 47 (Note: make sure you specify the correct path to the root).
48 48 - Note: the given path for mercurial_ repositories **must** be write accessible
49 49 for the application. It's very important since the RhodeCode web interface
50 50 will work without write access, but when trying to do a push it will
51 51 eventually fail with permission denied errors unless it has write access.
52 52
53 53 You are now ready to use RhodeCode, to run it simply execute::
54 54
55 55 paster serve production.ini
56 56
57 57 - This command runs the RhodeCode server. The web app should be available at the
58 58 127.0.0.1:5000. This ip and port is configurable via the production.ini
59 59 file created in previous step
60 60 - Use the admin account you created above when running ``setup-rhodecode``
61 61 to login to the web app.
62 62 - The default permissions on each repository is read, and the owner is admin.
63 63 Remember to update these if needed.
64 64 - In the admin panel you can toggle ldap, anonymous, permissions settings. As
65 65 well as edit more advanced options on users and repositories
66 66
67 67 Optionally users can create `rcextensions` package that extends RhodeCode
68 68 functionality. To do this simply execute::
69 69
70 70 paster make-rcext production.ini
71 71
72 72 This will create `rcextensions` package in the same place that your `ini` file
73 73 lives. With `rcextensions` it's possible to add additional mapping for whoosh,
74 74 stats and add additional code into the push/pull/create/delete repo hooks.
75 75 For example for sending signals to build-bots such as jenkins.
76 76 Please see the `__init__.py` file inside `rcextensions` package
77 77 for more details.
78 78
79 79
80 80 Using RhodeCode with SSH
81 81 ------------------------
82 82
83 83 RhodeCode currently only hosts repositories using http and https. (The addition
84 84 of ssh hosting is a planned future feature.) However you can easily use ssh in
85 85 parallel with RhodeCode. (Repository access via ssh is a standard "out of
86 86 the box" feature of mercurial_ and you can use this to access any of the
87 87 repositories that RhodeCode is hosting. See PublishingRepositories_)
88 88
89 89 RhodeCode repository structures are kept in directories with the same name
90 90 as the project. When using repository groups, each group is a subdirectory.
91 91 This allows you to easily use ssh for accessing repositories.
92 92
93 93 In order to use ssh you need to make sure that your web-server and the users
94 94 login accounts have the correct permissions set on the appropriate directories.
95 95 (Note that these permissions are independent of any permissions you have set up
96 96 using the RhodeCode web interface.)
97 97
98 98 If your main directory (the same as set in RhodeCode settings) is for example
99 99 set to **/home/hg** and the repository you are using is named `rhodecode`, then
100 100 to clone via ssh you should run::
101 101
102 102 hg clone ssh://user@server.com/home/hg/rhodecode
103 103
104 104 Using other external tools such as mercurial-server_ or using ssh key based
105 105 authentication is fully supported.
106 106
107 107 Note: In an advanced setup, in order for your ssh access to use the same
108 108 permissions as set up via the RhodeCode web interface, you can create an
109 109 authentication hook to connect to the rhodecode db and runs check functions for
110 110 permissions against that.
111 111
112 112 Setting up Whoosh full text search
113 113 ----------------------------------
114 114
115 115 Starting from version 1.1 the whoosh index can be build by using the paster
116 116 command ``make-index``. To use ``make-index`` you must specify the configuration
117 117 file that stores the location of the index. You may specify the location of the
118 118 repositories (`--repo-location`). If not specified, this value is retrieved
119 119 from the RhodeCode database. This was required prior to 1.2. Starting from
120 120 version 1.2 it is also possible to specify a comma separated list of
121 121 repositories (`--index-only`) to build index only on chooses repositories
122 122 skipping any other found in repos location
123 123
124 124 You may optionally pass the option `-f` to enable a full index rebuild. Without
125 125 the `-f` option, indexing will run always in "incremental" mode.
126 126
127 127 For an incremental index build use::
128 128
129 129 paster make-index production.ini
130 130
131 131 For a full index rebuild use::
132 132
133 133 paster make-index production.ini -f
134 134
135 135
136 136 building index just for chosen repositories is possible with such command::
137 137
138 138 paster make-index production.ini --index-only=vcs,rhodecode
139 139
140 140
141 141 In order to do periodical index builds and keep your index always up to date.
142 142 It's recommended to do a crontab entry for incremental indexing.
143 143 An example entry might look like this::
144 144
145 145 /path/to/python/bin/paster make-index /path/to/rhodecode/production.ini
146 146
147 147 When using incremental mode (the default) whoosh will check the last
148 148 modification date of each file and add it to be reindexed if a newer file is
149 149 available. The indexing daemon checks for any removed files and removes them
150 150 from index.
151 151
152 152 If you want to rebuild index from scratch, you can use the `-f` flag as above,
153 153 or in the admin panel you can check `build from scratch` flag.
154 154
155 155
156 156 Setting up LDAP support
157 157 -----------------------
158 158
159 159 RhodeCode starting from version 1.1 supports ldap authentication. In order
160 160 to use LDAP, you have to install the python-ldap_ package. This package is
161 161 available via pypi, so you can install it by running
162 162
163 163 using easy_install::
164 164
165 165 easy_install python-ldap
166 166
167 167 using pip::
168 168
169 169 pip install python-ldap
170 170
171 171 .. note::
172 172 python-ldap requires some certain libs on your system, so before installing
173 173 it check that you have at least `openldap`, and `sasl` libraries.
174 174
175 175 LDAP settings are located in admin->ldap section,
176 176
177 177 Here's a typical ldap setup::
178 178
179 179 Connection settings
180 180 Enable LDAP = checked
181 181 Host = host.example.org
182 182 Port = 389
183 183 Account = <account>
184 184 Password = <password>
185 185 Connection Security = LDAPS connection
186 186 Certificate Checks = DEMAND
187 187
188 188 Search settings
189 189 Base DN = CN=users,DC=host,DC=example,DC=org
190 190 LDAP Filter = (&(objectClass=user)(!(objectClass=computer)))
191 191 LDAP Search Scope = SUBTREE
192 192
193 193 Attribute mappings
194 194 Login Attribute = uid
195 195 First Name Attribute = firstName
196 196 Last Name Attribute = lastName
197 197 E-mail Attribute = mail
198 198
199 199 .. _enable_ldap:
200 200
201 201 Enable LDAP : required
202 202 Whether to use LDAP for authenticating users.
203 203
204 204 .. _ldap_host:
205 205
206 206 Host : required
207 207 LDAP server hostname or IP address. Can be also a comma separated
208 208 list of servers to support LDAP fail-over.
209 209
210 210 .. _Port:
211 211
212 212 Port : required
213 213 389 for un-encrypted LDAP, 636 for SSL-encrypted LDAP.
214 214
215 215 .. _ldap_account:
216 216
217 217 Account : optional
218 218 Only required if the LDAP server does not allow anonymous browsing of
219 219 records. This should be a special account for record browsing. This
220 220 will require `LDAP Password`_ below.
221 221
222 222 .. _LDAP Password:
223 223
224 224 Password : optional
225 225 Only required if the LDAP server does not allow anonymous browsing of
226 226 records.
227 227
228 228 .. _Enable LDAPS:
229 229
230 230 Connection Security : required
231 231 Defines the connection to LDAP server
232 232
233 233 No encryption
234 234 Plain non encrypted connection
235 235
236 236 LDAPS connection
237 237 Enable ldaps connection. It will likely require `Port`_ to be set to
238 238 a different value (standard LDAPS port is 636). When LDAPS is enabled
239 239 then `Certificate Checks`_ is required.
240 240
241 241 START_TLS on LDAP connection
242 242 START TLS connection
243 243
244 244 .. _Certificate Checks:
245 245
246 246 Certificate Checks : optional
247 247 How SSL certificates verification is handled - this is only useful when
248 248 `Enable LDAPS`_ is enabled. Only DEMAND or HARD offer full SSL security
249 249 while the other options are susceptible to man-in-the-middle attacks. SSL
250 250 certificates can be installed to /etc/openldap/cacerts so that the
251 251 DEMAND or HARD options can be used with self-signed certificates or
252 252 certificates that do not have traceable certificates of authority.
253 253
254 254 NEVER
255 255 A serve certificate will never be requested or checked.
256 256
257 257 ALLOW
258 258 A server certificate is requested. Failure to provide a
259 259 certificate or providing a bad certificate will not terminate the
260 260 session.
261 261
262 262 TRY
263 263 A server certificate is requested. Failure to provide a
264 264 certificate does not halt the session; providing a bad certificate
265 265 halts the session.
266 266
267 267 DEMAND
268 268 A server certificate is requested and must be provided and
269 269 authenticated for the session to proceed.
270 270
271 271 HARD
272 272 The same as DEMAND.
273 273
274 274 .. _Base DN:
275 275
276 276 Base DN : required
277 277 The Distinguished Name (DN) where searches for users will be performed.
278 278 Searches can be controlled by `LDAP Filter`_ and `LDAP Search Scope`_.
279 279
280 280 .. _LDAP Filter:
281 281
282 282 LDAP Filter : optional
283 283 A LDAP filter defined by RFC 2254. This is more useful when `LDAP
284 284 Search Scope`_ is set to SUBTREE. The filter is useful for limiting
285 285 which LDAP objects are identified as representing Users for
286 286 authentication. The filter is augmented by `Login Attribute`_ below.
287 287 This can commonly be left blank.
288 288
289 289 .. _LDAP Search Scope:
290 290
291 291 LDAP Search Scope : required
292 292 This limits how far LDAP will search for a matching object.
293 293
294 294 BASE
295 295 Only allows searching of `Base DN`_ and is usually not what you
296 296 want.
297 297
298 298 ONELEVEL
299 299 Searches all entries under `Base DN`_, but not Base DN itself.
300 300
301 301 SUBTREE
302 302 Searches all entries below `Base DN`_, but not Base DN itself.
303 303 When using SUBTREE `LDAP Filter`_ is useful to limit object
304 304 location.
305 305
306 306 .. _Login Attribute:
307 307
308 308 Login Attribute : required
309 309 The LDAP record attribute that will be matched as the USERNAME or
310 310 ACCOUNT used to connect to RhodeCode. This will be added to `LDAP
311 311 Filter`_ for locating the User object. If `LDAP Filter`_ is specified as
312 312 "LDAPFILTER", `Login Attribute`_ is specified as "uid" and the user has
313 313 connected as "jsmith" then the `LDAP Filter`_ will be augmented as below
314 314 ::
315 315
316 316 (&(LDAPFILTER)(uid=jsmith))
317 317
318 318 .. _ldap_attr_firstname:
319 319
320 320 First Name Attribute : required
321 321 The LDAP record attribute which represents the user's first name.
322 322
323 323 .. _ldap_attr_lastname:
324 324
325 325 Last Name Attribute : required
326 326 The LDAP record attribute which represents the user's last name.
327 327
328 328 .. _ldap_attr_email:
329 329
330 330 Email Attribute : required
331 331 The LDAP record attribute which represents the user's email address.
332 332
333 333 If all data are entered correctly, and python-ldap_ is properly installed
334 334 users should be granted access to RhodeCode with ldap accounts. At this
335 335 time user information is copied from LDAP into the RhodeCode user database.
336 336 This means that updates of an LDAP user object may not be reflected as a
337 337 user update in RhodeCode.
338 338
339 339 If You have problems with LDAP access and believe You entered correct
340 340 information check out the RhodeCode logs, any error messages sent from LDAP
341 341 will be saved there.
342 342
343 343 Active Directory
344 344 ''''''''''''''''
345 345
346 346 RhodeCode can use Microsoft Active Directory for user authentication. This
347 347 is done through an LDAP or LDAPS connection to Active Directory. The
348 348 following LDAP configuration settings are typical for using Active
349 349 Directory ::
350 350
351 351 Base DN = OU=SBSUsers,OU=Users,OU=MyBusiness,DC=v3sys,DC=local
352 352 Login Attribute = sAMAccountName
353 353 First Name Attribute = givenName
354 354 Last Name Attribute = sn
355 355 E-mail Attribute = mail
356 356
357 357 All other LDAP settings will likely be site-specific and should be
358 358 appropriately configured.
359 359
360 360
361 361 Authentication by container or reverse-proxy
362 362 --------------------------------------------
363 363
364 364 Starting with version 1.3, RhodeCode supports delegating the authentication
365 365 of users to its WSGI container, or to a reverse-proxy server through which all
366 366 clients access the application.
367 367
368 368 When these authentication methods are enabled in RhodeCode, it uses the
369 369 username that the container/proxy (Apache/Nginx/etc) authenticated and doesn't
370 370 perform the authentication itself. The authorization, however, is still done by
371 371 RhodeCode according to its settings.
372 372
373 373 When a user logs in for the first time using these authentication methods,
374 374 a matching user account is created in RhodeCode with default permissions. An
375 375 administrator can then modify it using RhodeCode's admin interface.
376 376 It's also possible for an administrator to create accounts and configure their
377 377 permissions before the user logs in for the first time.
378 378
379 379 Container-based authentication
380 380 ''''''''''''''''''''''''''''''
381 381
382 382 In a container-based authentication setup, RhodeCode reads the user name from
383 383 the ``REMOTE_USER`` server variable provided by the WSGI container.
384 384
385 385 After setting up your container (see `Apache's WSGI config`_), you'd need
386 386 to configure it to require authentication on the location configured for
387 387 RhodeCode.
388 388
389 389 In order for RhodeCode to start using the provided username, you should set the
390 390 following in the [app:main] section of your .ini file::
391 391
392 392 container_auth_enabled = true
393 393
394 394
395 395 Proxy pass-through authentication
396 396 '''''''''''''''''''''''''''''''''
397 397
398 398 In a proxy pass-through authentication setup, RhodeCode reads the user name
399 399 from the ``X-Forwarded-User`` request header, which should be configured to be
400 400 sent by the reverse-proxy server.
401 401
402 402 After setting up your proxy solution (see `Apache virtual host reverse proxy example`_,
403 403 `Apache as subdirectory`_ or `Nginx virtual host example`_), you'd need to
404 404 configure the authentication and add the username in a request header named
405 405 ``X-Forwarded-User``.
406 406
407 407 For example, the following config section for Apache sets a subdirectory in a
408 408 reverse-proxy setup with basic auth::
409 409
410 410 <Location /<someprefix> >
411 411 ProxyPass http://127.0.0.1:5000/<someprefix>
412 412 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
413 413 SetEnvIf X-Url-Scheme https HTTPS=1
414 414
415 415 AuthType Basic
416 416 AuthName "RhodeCode authentication"
417 417 AuthUserFile /home/web/rhodecode/.htpasswd
418 418 require valid-user
419 419
420 420 RequestHeader unset X-Forwarded-User
421 421
422 422 RewriteEngine On
423 423 RewriteCond %{LA-U:REMOTE_USER} (.+)
424 424 RewriteRule .* - [E=RU:%1]
425 425 RequestHeader set X-Forwarded-User %{RU}e
426 426 </Location>
427 427
428 428 In order for RhodeCode to start using the forwarded username, you should set
429 429 the following in the [app:main] section of your .ini file::
430 430
431 431 proxypass_auth_enabled = true
432 432
433 433 .. note::
434 434 If you enable proxy pass-through authentication, make sure your server is
435 435 only accessible through the proxy. Otherwise, any client would be able to
436 436 forge the authentication header and could effectively become authenticated
437 437 using any account of their liking.
438 438
439 439 Integration with Issue trackers
440 440 -------------------------------
441 441
442 442 RhodeCode provides a simple integration with issue trackers. It's possible
443 443 to define a regular expression that will fetch issue id stored in commit
444 444 messages and replace that with an url to this issue. To enable this simply
445 445 uncomment following variables in the ini file::
446 446
447 447 url_pat = (?:^#|\s#)(\w+)
448 448 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
449 449 issue_prefix = #
450 450
451 451 `url_pat` is the regular expression that will fetch issues from commit messages.
452 452 Default regex will match issues in format of #<number> eg. #300.
453 453
454 454 Matched issues will be replace with the link specified as `issue_server_link`
455 455 {id} will be replaced with issue id, and {repo} with repository name.
456 456 Since the # is striped `issue_prefix` is added as a prefix to url.
457 457 `issue_prefix` can be something different than # if you pass
458 458 ISSUE- as issue prefix this will generate an url in format::
459 459
460 460 <a href="https://myissueserver.com/example_repo/issue/300">ISSUE-300</a>
461 461
462 462 Hook management
463 463 ---------------
464 464
465 465 Hooks can be managed in similar way to this used in .hgrc files.
466 466 To access hooks setting click `advanced setup` on Hooks section of Mercurial
467 467 Settings in Admin.
468 468
469 469 There are 4 built in hooks that cannot be changed (only enable/disable by
470 470 checkboxes on previos section).
471 471 To add another custom hook simply fill in first section with
472 472 <name>.<hook_type> and the second one with hook path. Example hooks
473 473 can be found at *rhodecode.lib.hooks*.
474 474
475 475
476 476 Changing default encoding
477 477 -------------------------
478 478
479 479 By default RhodeCode uses utf8 encoding, starting from 1.3 series this
480 480 can be changed, simply edit default_encoding in .ini file to desired one.
481 This affects many parts in rhodecode including commiters names, filenames,
481 This affects many parts in rhodecode including committers names, filenames,
482 482 encoding of commit messages. In addition RhodeCode can detect if `chardet`
483 483 library is installed. If `chardet` is detected RhodeCode will fallback to it
484 484 when there are encode/decode errors.
485 485
486 486
487 487 Setting Up Celery
488 488 -----------------
489 489
490 490 Since version 1.1 celery is configured by the rhodecode ini configuration files.
491 491 Simply set use_celery=true in the ini file then add / change the configuration
492 492 variables inside the ini file.
493 493
494 494 Remember that the ini files use the format with '.' not with '_' like celery.
495 495 So for example setting `BROKER_HOST` in celery means setting `broker.host` in
496 496 the config file.
497 497
498 498 In order to start using celery run::
499 499
500 500 paster celeryd <configfile.ini>
501 501
502 502
503 503 .. note::
504 504 Make sure you run this command from the same virtualenv, and with the same
505 505 user that rhodecode runs.
506 506
507 507 HTTPS support
508 508 -------------
509 509
510 510 There are two ways to enable https:
511 511
512 512 - Set HTTP_X_URL_SCHEME in your http server headers, than rhodecode will
513 513 recognize this headers and make proper https redirections
514 514 - Alternatively, change the `force_https = true` flag in the ini configuration
515 515 to force using https, no headers are needed than to enable https
516 516
517 517
518 518 Nginx virtual host example
519 519 --------------------------
520 520
521 521 Sample config for nginx using proxy::
522 522
523 523 upstream rc {
524 524 server 127.0.0.1:5000;
525 525 # add more instances for load balancing
526 526 #server 127.0.0.1:5001;
527 527 #server 127.0.0.1:5002;
528 528 }
529 529
530 530 server {
531 531 listen 443;
532 532 server_name rhodecode.myserver.com;
533 533 access_log /var/log/nginx/rhodecode.access.log;
534 534 error_log /var/log/nginx/rhodecode.error.log;
535 535
536 536 ssl on;
537 537 ssl_certificate rhodecode.myserver.com.crt;
538 538 ssl_certificate_key rhodecode.myserver.com.key;
539 539
540 540 ssl_session_timeout 5m;
541 541
542 542 ssl_protocols SSLv3 TLSv1;
543 543 ssl_ciphers DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:EDH-RSA-DES-CBC3-SHA:AES256-SHA:DES-CBC3-SHA:AES128-SHA:RC4-SHA:RC4-MD5;
544 544 ssl_prefer_server_ciphers on;
545 545
546 546 # uncomment if you have nginx with chunking module compiled
547 547 # fixes the issues of having to put postBuffer data for large git
548 548 # pushes
549 549 #chunkin on;
550 550 #error_page 411 = @my_411_error;
551 551 #location @my_411_error {
552 552 # chunkin_resume;
553 553 #}
554 554
555 555 # uncomment if you want to serve static files by nginx
556 556 #root /path/to/installation/rhodecode/public;
557 557
558 558 location / {
559 559 try_files $uri @rhode;
560 560 }
561 561
562 562 location @rhode {
563 563 proxy_pass http://rc;
564 564 include /etc/nginx/proxy.conf;
565 565 }
566 566
567 567 }
568 568
569 569 Here's the proxy.conf. It's tuned so it will not timeout on long
570 570 pushes or large pushes::
571 571
572 572 proxy_redirect off;
573 573 proxy_set_header Host $host;
574 574 proxy_set_header X-Url-Scheme $scheme;
575 575 proxy_set_header X-Host $http_host;
576 576 proxy_set_header X-Real-IP $remote_addr;
577 577 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
578 578 proxy_set_header Proxy-host $proxy_host;
579 579 client_max_body_size 400m;
580 580 client_body_buffer_size 128k;
581 581 proxy_buffering off;
582 582 proxy_connect_timeout 7200;
583 583 proxy_send_timeout 7200;
584 584 proxy_read_timeout 7200;
585 585 proxy_buffers 8 32k;
586 586
587 587 Also, when using root path with nginx you might set the static files to false
588 588 in the production.ini file::
589 589
590 590 [app:main]
591 591 use = egg:rhodecode
592 592 full_stack = true
593 593 static_files = false
594 594 lang=en
595 595 cache_dir = %(here)s/data
596 596
597 597 In order to not have the statics served by the application. This improves speed.
598 598
599 599
600 600 Apache virtual host reverse proxy example
601 601 -----------------------------------------
602 602
603 603 Here is a sample configuration file for apache using proxy::
604 604
605 605 <VirtualHost *:80>
606 606 ServerName hg.myserver.com
607 607 ServerAlias hg.myserver.com
608 608
609 609 <Proxy *>
610 610 Order allow,deny
611 611 Allow from all
612 612 </Proxy>
613 613
614 614 #important !
615 615 #Directive to properly generate url (clone url) for pylons
616 616 ProxyPreserveHost On
617 617
618 618 #rhodecode instance
619 619 ProxyPass / http://127.0.0.1:5000/
620 620 ProxyPassReverse / http://127.0.0.1:5000/
621 621
622 622 #to enable https use line below
623 623 #SetEnvIf X-Url-Scheme https HTTPS=1
624 624
625 625 </VirtualHost>
626 626
627 627
628 628 Additional tutorial
629 629 http://wiki.pylonshq.com/display/pylonscookbook/Apache+as+a+reverse+proxy+for+Pylons
630 630
631 631
632 632 Apache as subdirectory
633 633 ----------------------
634 634
635 635 Apache subdirectory part::
636 636
637 637 <Location /<someprefix> >
638 638 ProxyPass http://127.0.0.1:5000/<someprefix>
639 639 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
640 640 SetEnvIf X-Url-Scheme https HTTPS=1
641 641 </Location>
642 642
643 643 Besides the regular apache setup you will need to add the following line
644 644 into [app:main] section of your .ini file::
645 645
646 646 filter-with = proxy-prefix
647 647
648 648 Add the following at the end of the .ini file::
649 649
650 650 [filter:proxy-prefix]
651 651 use = egg:PasteDeploy#prefix
652 652 prefix = /<someprefix>
653 653
654 654
655 then change <someprefix> into your choosen prefix
655 then change <someprefix> into your chosen prefix
656 656
657 657 Apache's WSGI config
658 658 --------------------
659 659
660 660 Alternatively, RhodeCode can be set up with Apache under mod_wsgi. For
661 661 that, you'll need to:
662 662
663 663 - Install mod_wsgi. If using a Debian-based distro, you can install
664 664 the package libapache2-mod-wsgi::
665 665
666 666 aptitude install libapache2-mod-wsgi
667 667
668 668 - Enable mod_wsgi::
669 669
670 670 a2enmod wsgi
671 671
672 672 - Create a wsgi dispatch script, like the one below. Make sure you
673 673 check the paths correctly point to where you installed RhodeCode
674 674 and its Python Virtual Environment.
675 675 - Enable the WSGIScriptAlias directive for the wsgi dispatch script,
676 676 as in the following example. Once again, check the paths are
677 677 correctly specified.
678 678
679 679 Here is a sample excerpt from an Apache Virtual Host configuration file::
680 680
681 681 WSGIDaemonProcess pylons \
682 682 threads=4 \
683 683 python-path=/home/web/rhodecode/pyenv/lib/python2.6/site-packages
684 684 WSGIScriptAlias / /home/web/rhodecode/dispatch.wsgi
685 685 WSGIPassAuthorization On
686 686
687 687 .. note::
688 688 when running apache as root please add: `user=www-data group=www-data`
689 689 into above configuration
690 690
691 691 .. note::
692 692 Running RhodeCode in multiprocess mode in apache is not supported,
693 693 make sure you don't specify `processes=num` directive in the config
694 694
695 695
696 696 Example wsgi dispatch script::
697 697
698 698 import os
699 699 os.environ["HGENCODING"] = "UTF-8"
700 700 os.environ['PYTHON_EGG_CACHE'] = '/home/web/rhodecode/.egg-cache'
701 701
702 702 # sometimes it's needed to set the curent dir
703 703 os.chdir('/home/web/rhodecode/')
704 704
705 705 import site
706 706 site.addsitedir("/home/web/rhodecode/pyenv/lib/python2.6/site-packages")
707 707
708 708 from paste.deploy import loadapp
709 709 from paste.script.util.logging_config import fileConfig
710 710
711 711 fileConfig('/home/web/rhodecode/production.ini')
712 712 application = loadapp('config:/home/web/rhodecode/production.ini')
713 713
714 714 Note: when using mod_wsgi you'll need to install the same version of
715 715 Mercurial that's inside RhodeCode's virtualenv also on the system's Python
716 716 environment.
717 717
718 718
719 719 Other configuration files
720 720 -------------------------
721 721
722 722 Some example init.d scripts can be found in init.d directory::
723 723
724 724 https://secure.rhodecode.org/rhodecode/files/beta/init.d
725 725
726 726 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
727 727 .. _python: http://www.python.org/
728 728 .. _mercurial: http://mercurial.selenic.com/
729 729 .. _celery: http://celeryproject.org/
730 730 .. _rabbitmq: http://www.rabbitmq.com/
731 731 .. _python-ldap: http://www.python-ldap.org/
732 732 .. _mercurial-server: http://www.lshift.net/mercurial-server.html
733 733 .. _PublishingRepositories: http://mercurial.selenic.com/wiki/PublishingRepositories
734 734 .. _Issues tracker: https://bitbucket.org/marcinkuzminski/rhodecode/issues
735 735 .. _google group rhodecode: http://groups.google.com/group/rhodecode
@@ -1,54 +1,54 b''
1 1 .c { color: #999988; font-style: italic } /* Comment */
2 2 .k { font-weight: bold } /* Keyword */
3 3 .o { font-weight: bold } /* Operator */
4 4 .cm { color: #999988; font-style: italic } /* Comment.Multiline */
5 5 .cp { color: #999999; font-weight: bold } /* Comment.preproc */
6 6 .c1 { color: #999988; font-style: italic } /* Comment.Single */
7 7 .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
8 8 .ge { font-style: italic } /* Generic.Emph */
9 9 .gr { color: #aa0000 } /* Generic.Error */
10 10 .gh { color: #999999 } /* Generic.Heading */
11 11 .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
12 12 .go { color: #111 } /* Generic.Output */
13 13 .gp { color: #555555 } /* Generic.Prompt */
14 14 .gs { font-weight: bold } /* Generic.Strong */
15 15 .gu { color: #aaaaaa } /* Generic.Subheading */
16 16 .gt { color: #aa0000 } /* Generic.Traceback */
17 17 .kc { font-weight: bold } /* Keyword.Constant */
18 18 .kd { font-weight: bold } /* Keyword.Declaration */
19 19 .kp { font-weight: bold } /* Keyword.Pseudo */
20 20 .kr { font-weight: bold } /* Keyword.Reserved */
21 21 .kt { color: #445588; font-weight: bold } /* Keyword.Type */
22 22 .m { color: #009999 } /* Literal.Number */
23 23 .s { color: #bb8844 } /* Literal.String */
24 24 .na { color: #008080 } /* Name.Attribute */
25 25 .nb { color: #999999 } /* Name.Builtin */
26 26 .nc { color: #445588; font-weight: bold } /* Name.Class */
27 27 .no { color: #ff99ff } /* Name.Constant */
28 28 .ni { color: #800080 } /* Name.Entity */
29 29 .ne { color: #990000; font-weight: bold } /* Name.Exception */
30 30 .nf { color: #990000; font-weight: bold } /* Name.Function */
31 31 .nn { color: #555555 } /* Name.Namespace */
32 32 .nt { color: #000080 } /* Name.Tag */
33 33 .nv { color: purple } /* Name.Variable */
34 34 .ow { font-weight: bold } /* Operator.Word */
35 35 .mf { color: #009999 } /* Literal.Number.Float */
36 36 .mh { color: #009999 } /* Literal.Number.Hex */
37 37 .mi { color: #009999 } /* Literal.Number.Integer */
38 38 .mo { color: #009999 } /* Literal.Number.Oct */
39 39 .sb { color: #bb8844 } /* Literal.String.Backtick */
40 40 .sc { color: #bb8844 } /* Literal.String.Char */
41 41 .sd { color: #bb8844 } /* Literal.String.Doc */
42 42 .s2 { color: #bb8844 } /* Literal.String.Double */
43 43 .se { color: #bb8844 } /* Literal.String.Escape */
44 44 .sh { color: #bb8844 } /* Literal.String.Heredoc */
45 45 .si { color: #bb8844 } /* Literal.String.Interpol */
46 46 .sx { color: #bb8844 } /* Literal.String.Other */
47 47 .sr { color: #808000 } /* Literal.String.Regex */
48 48 .s1 { color: #bb8844 } /* Literal.String.Single */
49 49 .ss { color: #bb8844 } /* Literal.String.Symbol */
50 50 .bp { color: #999999 } /* Name.Builtin.Pseudo */
51 51 .vc { color: #ff99ff } /* Name.Variable.Class */
52 52 .vg { color: #ff99ff } /* Name.Variable.Global */
53 53 .vi { color: #ff99ff } /* Name.Variable.Instance */
54 .il { color: #009999 } /* Literal.Number.Integer.Long */ No newline at end of file
54 .il { color: #009999 } /* Literal.Number.Integer.Long */
@@ -1,26 +1,26 b''
1 1 .. _backup:
2 2
3 3 ====================
4 4 Backing up RhodeCode
5 5 ====================
6 6
7 7
8 8 Settings
9 9 --------
10 10
11 11 Just copy your .ini file, it contains all RhodeCode settings.
12 12
13 13 Whoosh index
14 14 ------------
15 15
16 16 Whoosh index is located in **/data/index** directory where you installed
17 17 RhodeCode ie. the same place where the ini file is located
18 18
19 19
20 20 Database
21 21 --------
22 22
23 23 When using sqlite just copy rhodecode.db.
24 24 Any other database engine requires a manual backup operation.
25 25
26 Database backup will contain all gathered statistics No newline at end of file
26 Database backup will contain all gathered statistics
@@ -1,115 +1,115 b''
1 1 .. _general:
2 2
3 3 =======================
4 4 General RhodeCode usage
5 5 =======================
6 6
7 7
8 8 Repository deleting
9 9 -------------------
10 10
11 11 Currently when admin/owner deletes a repository, RhodeCode does not physically
12 12 delete a repository from filesystem, it renames it in a special way so it's
13 13 not possible to push,clone or access repository. It's worth a notice that,
14 14 even if someone will be given administrative access to RhodeCode and will
15 15 delete a repository You can easy restore such action by restoring `rm__<date>`
16 16 from the repository name, and internal repository storage (.hg/.git). There
17 17 is also a special command for cleaning such archived repos::
18 18
19 19 paster cleanup-repos --older-than=30d production.ini
20 20
21 21 This command will scan for archived repositories that are older than 30d,
22 22 display them and ask if you want to delete them (there's a --dont-ask flag also)
23 23 If you host big amount of repositories with forks that are constantly deleted
24 24 it's recommended that you run such command via crontab.
25 25
26 26 Follow current branch in file view
27 27 ----------------------------------
28 28
29 29 In file view when this checkbox is checked the << and >> arrows will jump
30 30 to changesets within the same branch currently viewing. So for example
31 31 if someone is viewing files at 'beta' branch and marks `follow current branch`
32 32 checkbox the << and >> buttons will only show him revisions for 'beta' branch
33 33
34 34
35 35 Compare view from changelog
36 36 ---------------------------
37 37
38 38 Checkboxes in compare view allow users to view combined compare view. You can
39 39 only show the range between the first and last checkbox (no cherry pick).
40 40 Clicking more than one checkbox will activate a link in top saying
41 `Show selected changes <from-rev> -> <to-rev>` clicking this will bring
41 `Show selected changesets <from-rev> -> <to-rev>` clicking this will bring
42 42 compare view. In this view also it's possible to switch to combined compare.
43 43
44 44 Compare view is also available from the journal on pushes having more than
45 45 one changeset
46 46
47 47
48 48 Non changeable repository urls
49 49 ------------------------------
50 50
51 51 Due to complicated nature of repository grouping, often urls of repositories
52 52 can change.
53 53
54 54 example::
55 55
56 56 #before
57 57 http://server.com/repo_name
58 58 # after insertion to test_group group the url will be
59 59 http://server.com/test_group/repo_name
60 60
61 61 This can be an issue for build systems and any other hardcoded scripts, moving
62 62 repository to a group leads to a need for changing external systems. To
63 63 overcome this RhodeCode introduces a non changable replacement url. It's
64 64 simply an repository ID prefixed with `_` above urls are also accessible as::
65 65
66 66 http://server.com/_<ID>
67 67
68 68 Since ID are always the same moving the repository will not affect such url.
69 69 the _<ID> syntax can be used anywhere in the system so urls with repo_name
70 70 for changelogs, files and other can be exchanged with _<ID> syntax.
71 71
72 72
73 73 Mailing
74 74 -------
75 75
76 76 When administrator will fill up the mailing settings in .ini files
77 77 RhodeCode will send mails on user registration, or when RhodeCode errors occur
78 78 on errors the mails will have a detailed traceback of error.
79 79
80 80
81 81 Mails are also sent for code comments. If someone comments on a changeset
82 82 mail is sent to all participants, the person who commited the changeset
83 83 (if present in RhodeCode), and to all people mentioned with @mention system.
84 84
85 85
86 86 Trending source files
87 87 ---------------------
88 88
89 89 Trending source files are calculated based on pre defined dict of known
90 90 types and extensions. If You miss some extension or Would like to scan some
91 91 custom files it's possible to add new types in `LANGUAGES_EXTENSIONS_MAP` dict
92 92 located in `/rhodecode/lib/celerylib/tasks.py`
93 93
94 94
95 95 Cloning remote repositories
96 96 ---------------------------
97 97
98 98 RhodeCode has an ability to clone remote repos from given remote locations.
99 99 Currently it support following options:
100 100
101 101 - hg -> hg clone
102 102 - svn -> hg clone
103 103 - git -> git clone
104 104
105 105
106 106 .. note::
107 107
108 * `svn -> hg` cloning requires `hgsubversion` library to be installed.*
108 - *`svn -> hg` cloning requires `hgsubversion` library to be installed.*
109 109
110 110 If you need to clone repositories that are protected via basic auth, you
111 111 might pass the url with stored credentials inside eg.
112 112 `http://user:passw@remote.server/repo`, RhodeCode will try to login and clone
113 113 using given credentials. Please take a note that they will be stored as
114 114 plaintext inside the database. RhodeCode will remove auth info when showing the
115 115 clone url in summary page.
@@ -1,55 +1,55 b''
1 1 .. _git_support:
2 2
3 3 ===========
4 4 GIT support
5 5 ===========
6 6
7 7
8 8 Git support in RhodeCode 1.3 was enabled by default. You need to have a git
9 9 client installed on the machine to make git fully work.
10 10
11 11 Although There is one limitation on git usage.
12 12
13 13 - large pushes requires a http server with chunked encoding support.
14 14
15 15 if you plan to use git you need to run RhodeCode with some
16 16 http server that supports chunked encoding which git http protocol uses,
17 17 i recommend using waitress_ or gunicorn_ (linux only) for `paste` wsgi app
18 18 replacement. Starting from version 1.4 waitress_ is the default wsgi server
19 19 used in RhodeCode.
20 20
21 21 To use, simply change change the following in the .ini file::
22 22
23 23 use = egg:Paste#http
24 24
25 25 to::
26 26
27 27 use = egg:waitress#main
28 28
29 29 or::
30 30
31 31 use = egg:gunicorn#main
32 32
33 33
34 34 And comment out bellow options::
35 35
36 36 threadpool_workers =
37 37 threadpool_max_requests =
38 38 use_threadpool =
39 39
40 40
41 41 You can simply run `paster serve` as usual.
42 42
43 43
44 44 You can always disable git/hg support by editing a
45 45 file **rhodecode/__init__.py** and commenting out backends
46 46
47 47 .. code-block:: python
48 48
49 49 BACKENDS = {
50 50 'hg': 'Mercurial repository',
51 51 #'git': 'Git repository',
52 52 }
53 53
54 54 .. _waitress: http://pypi.python.org/pypi/waitress
55 .. _gunicorn: http://pypi.python.org/pypi/gunicorn No newline at end of file
55 .. _gunicorn: http://pypi.python.org/pypi/gunicorn
@@ -1,41 +1,41 b''
1 1 .. _locking:
2 2
3 3 ===================================
4 4 RhodeCode repository locking system
5 5 ===================================
6 6
7 7
8 8 | Repos with **locking function=disabled** is the default, that's how repos work
9 9 today.
10 10 | Repos with **locking function=enabled** behaves like follows:
11 11
12 12 Repos have a state called `locked` that can be true or false.
13 13 The hg/git commands `hg/git clone`, `hg/git pull`, and `hg/git push`
14 14 influence this state:
15 15
16 16 - The command `hg/git pull <repo>` will lock that repo (locked=true)
17 17 if the user has write/admin permissions on this repo
18 18
19 19 - The command `hg/git clone <repo>` will lock that repo (locked=true) if the
20 20 user has write/admin permissions on this repo
21 21
22 22
23 23 RhodeCode will remember the user id who locked the repo
24 24 only this specific user can unlock the repo (locked=false) by calling
25 25
26 26 - `hg/git push <repo>`
27 27
28 28 every other command on that repo from this user and
29 29 every command from any other user will result in http return code 423 (locked)
30 30
31 31
32 32 additionally the http error includes the <user> that locked the repo
33 33 (e.g. “repository <repo> locked by user <user>”)
34 34
35 35
36 36 So the scenario of use for repos with `locking function` enabled is that
37 37 every initial clone and every pull gives users (with write permission)
38 38 the exclusive right to do a push.
39 39
40 40
41 Each repo can be manually unlocked by admin from the repo settings menu. No newline at end of file
41 Each repo can be manually unlocked by admin from the repo settings menu.
@@ -1,50 +1,62 b''
1 1 .. _performance:
2 2
3 3 ================================
4 4 Optimizing RhodeCode Performance
5 5 ================================
6 6
7 7 When serving large amount of big repositories RhodeCode can start
8 8 performing slower than expected. Because of demanding nature of handling large
9 9 amount of data from version control systems here are some tips how to get
10 10 the best performance.
11 11
12 12 * RhodeCode will perform better on machines with faster disks (SSD/SAN). It's
13 13 more important to have faster disk than faster CPU.
14 14
15 15 * Slowness on initial page can be easily fixed by grouping repositories, and/or
16 increasing cache size (see below)
16 increasing cache size (see below), that includes using lightweight dashboard
17 option and vcs_full_cache setting in .ini file
17 18
18 19
19 20 Follow these few steps to improve performance of RhodeCode system.
20 21
21 22
22 23 1. Increase cache
23 24
24 25 in the .ini file::
25 26
26 27 beaker.cache.sql_cache_long.expire=3600 <-- set this to higher number
27 28
28 29 This option affects the cache expiration time for main page. Having
29 30 few hundreds of repositories on main page can sometimes make the system
30 31 to behave slow when cache expires for all of them. Increasing `expire`
31 32 option to day (86400) or a week (604800) will improve general response
32 33 times for the main page. RhodeCode has an intelligent cache expiration
33 34 system and it will expire cache for repositories that had been changed.
34 35
35 36 2. Switch from sqlite to postgres or mysql
36 37
37 38 sqlite is a good option when having small load on the system. But due to
38 39 locking issues with sqlite, it's not recommended to use it for larger
39 40 setup. Switching to mysql or postgres will result in a immediate
40 41 performance increase.
41 42
42 43 3. Scale RhodeCode horizontally
43 44
44 - running two or more instances on the same server can speed up things a lot
45 - load balance using round robin or ip hash
46 - you need to handle consistent user session storage by switching to
47 db sessions, client side sessions or sharing session data folder across
48 instances. See http://beaker.readthedocs.org/ docs for details.
49 - remember that each instance needs it's own .ini file and unique
50 `instance_id` set in them No newline at end of file
45 Scaling horizontally can give huge performance increase when dealing with
46 large traffic (large amount of users, CI servers etc). RhodeCode can be
47 scaled horizontally on one (recommended) or multiple machines. In order
48 to scale horizontally you need to do the following:
49
50 - each instance needs it's own .ini file and unique `instance_id` set in them
51 - each instance `data` storage needs to be configured to be stored on a
52 shared disk storage, preferably together with repositories. This `data`
53 dir contains template caches, sessions, whoosh index and it's used for
54 tasks locking (so it's safe across multiple instances). Set the
55 `cache_dir`, `index_dir`, `beaker.cache.data_dir`, `beaker.cache.lock_dir`
56 variables in each .ini file to shared location across RhodeCode instances
57 - if celery is used each instance should run separate celery instance, but
58 the message broken should be common to all of them (ex one rabbitmq
59 shared server)
60 - load balance using round robin or ip hash, recommended is writing LB rules
61 that will separate regular user traffic from automated processes like CI
62 servers or build bots.
@@ -1,37 +1,37 b''
1 1 .. _subrepos:
2 2
3 3 =============================================
4 4 working with RhodeCode and mercurial subrepos
5 5 =============================================
6 6
7 7 example usage of Subrepos with RhodeCode::
8 8
9 9 ## init a simple repo
10 10 hg init repo1
11 11 cd repo1
12 12 echo "file1" > file1
13 13 hg add file1
14 14 hg ci --message "initial file 1"
15 15
16 16 #clone subrepo we want to add
17 17 hg clone http://rc.local/subrepo
18 18
19 19 ## use path like url to existing repo in RhodeCode
20 20 echo "subrepo = http://rc.local/subrepo" > .hgsub
21 21
22 22 hg add .hgsub
23 23 hg ci --message "added remote subrepo"
24 24
25 25
26 26
27 27 In file list of repo1 you will see a connected subrepo at revision it was
28 28 during cloning.
29 29 Clicking in subrepos link should send you to proper repository in RhodeCode
30 30
31 31 cloning repo1 will also clone attached subrepository.
32 32
33 33 Next we can edit the subrepo data, and push back to RhodeCode. This will update
34 34 both of repositories.
35 35
36 36 see http://mercurial.aragost.com/kick-start/en/subrepositories/ for more
37 information about subrepositories No newline at end of file
37 information about subrepositories
@@ -1,76 +1,90 b''
1 1 #!/bin/sh -e
2 2 ########################################
3 3 #### THIS IS A DEBIAN INIT.D SCRIPT ####
4 4 ########################################
5 5
6 6 ### BEGIN INIT INFO
7 7 # Provides: rhodecode
8 8 # Required-Start: $all
9 9 # Required-Stop: $all
10 10 # Default-Start: 2 3 4 5
11 11 # Default-Stop: 0 1 6
12 12 # Short-Description: starts instance of rhodecode
13 13 # Description: starts instance of rhodecode using start-stop-daemon
14 14 ### END INIT INFO
15 15
16 16 APP_NAME="rhodecode"
17 APP_HOMEDIR="marcink/python_workspace"
18 APP_PATH="/home/$APP_HOMEDIR/$APP_NAME"
17 APP_HOMEDIR="opt"
18 APP_PATH="/$APP_HOMEDIR/$APP_NAME"
19 19
20 20 CONF_NAME="production.ini"
21 21
22 22 PID_PATH="$APP_PATH/$APP_NAME.pid"
23 23 LOG_PATH="$APP_PATH/$APP_NAME.log"
24 24
25 PYTHON_PATH="/home/$APP_HOMEDIR/v-env"
25 PYTHON_PATH="/$APP_HOMEDIR/$APP_NAME-venv"
26 26
27 RUN_AS="marcink"
27 RUN_AS="root"
28 28
29 29 DAEMON="$PYTHON_PATH/bin/paster"
30 30
31 31 DAEMON_OPTS="serve --daemon \
32 32 --user=$RUN_AS \
33 33 --group=$RUN_AS \
34 34 --pid-file=$PID_PATH \
35 35 --log-file=$LOG_PATH $APP_PATH/$CONF_NAME"
36 36
37 37
38 38 start() {
39 39 echo "Starting $APP_NAME"
40 40 PYTHON_EGG_CACHE="/tmp" start-stop-daemon -d $APP_PATH \
41 41 --start --quiet \
42 42 --pidfile $PID_PATH \
43 43 --user $RUN_AS \
44 44 --exec $DAEMON -- $DAEMON_OPTS
45 45 }
46 46
47 47 stop() {
48 48 echo "Stopping $APP_NAME"
49 49 start-stop-daemon -d $APP_PATH \
50 50 --stop --quiet \
51 51 --pidfile $PID_PATH || echo "$APP_NAME - Not running!"
52 52
53 53 if [ -f $PID_PATH ]; then
54 54 rm $PID_PATH
55 55 fi
56 56 }
57 57
58 status() {
59 echo -n "Checking status of $APP_NAME ... "
60 pid=`cat $PID_PATH`
61 status=`ps ax | grep $pid | grep -ve grep`
62 if [ "$?" -eq 0 ]; then
63 echo "running"
64 else
65 echo "NOT running"
66 fi
67 }
68
58 69 case "$1" in
70 status)
71 status
72 ;;
59 73 start)
60 74 start
61 75 ;;
62 76 stop)
63 77 stop
64 78 ;;
65 79 restart)
66 80 echo "Restarting $APP_NAME"
67 81 ### stop ###
68 82 stop
69 83 wait
70 84 ### start ###
71 85 start
72 86 ;;
73 87 *)
74 88 echo "Usage: $0 {start|stop|restart}"
75 89 exit 1
76 esac
90 esac No newline at end of file
@@ -1,439 +1,481 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # RhodeCode - Pylons environment configuration #
4 4 # #
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 ################################################################################
7 7
8 8 [DEFAULT]
9 9 debug = true
10 10 pdebug = false
11 11 ################################################################################
12 12 ## Uncomment and replace with the address which should receive ##
13 13 ## any error reports after application crash ##
14 14 ## Additionally those settings will be used by RhodeCode mailing system ##
15 15 ################################################################################
16 16 #email_to = admin@localhost
17 17 #error_email_from = paste_error@localhost
18 18 #app_email_from = rhodecode-noreply@localhost
19 19 #error_message =
20 20 #email_prefix = [RhodeCode]
21 21
22 22 #smtp_server = mail.server.com
23 23 #smtp_username =
24 24 #smtp_password =
25 25 #smtp_port =
26 26 #smtp_use_tls = false
27 27 #smtp_use_ssl = true
28 # Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
28 ## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
29 29 #smtp_auth =
30 30
31 31 [server:main]
32 32 ## PASTE
33 33 ##nr of threads to spawn
34 34 #threadpool_workers = 5
35 35
36 36 ##max request before thread respawn
37 37 #threadpool_max_requests = 10
38 38
39 39 ##option to use threads of process
40 40 #use_threadpool = true
41 41
42 42 #use = egg:Paste#http
43 43
44 #WAITRESS
44 ## WAITRESS
45 45 threads = 5
46 ## 100GB
47 max_request_body_size = 107374182400
46 48 use = egg:waitress#main
47 49
48 50 host = 127.0.0.1
49 51 port = 8001
50 52
51 [filter:proxy-prefix]
52 # prefix middleware for rc
53 use = egg:PasteDeploy#prefix
54 prefix = /<your-prefix>
53 ## prefix middleware for rc
54 #[filter:proxy-prefix]
55 #use = egg:PasteDeploy#prefix
56 #prefix = /<your-prefix>
55 57
56 58 [app:main]
57 59 use = egg:rhodecode
60 ## enable proxy prefix middleware
58 61 #filter-with = proxy-prefix
62
59 63 full_stack = true
60 64 static_files = true
61 # Optional Languages
62 # en, fr, ja, pt_BR, zh_CN, zh_TW, pl
65 ## Optional Languages
66 ## en, fr, ja, pt_BR, zh_CN, zh_TW, pl
63 67 lang = en
64 68 cache_dir = %(here)s/data
65 69 index_dir = %(here)s/data/index
70
71 ## uncomment and set this path to use archive download cache
72 #archive_cache_dir = /tmp/tarballcache
73
74 ## change this to unique ID for security
66 75 app_instance_uuid = rc-production
76
77 ## cut off limit for large diffs (size in bytes)
67 78 cut_off_limit = 256000
68 vcs_full_cache = True
79
80 ## use cache version of scm repo everywhere
81 vcs_full_cache = true
82
83 ## force https in RhodeCode, fixes https redirects, assumes it's always https
69 84 force_https = false
70 commit_parse_limit = 50
71 # number of items displayed in lightweight dashboard before paginating
85
86 ## use Strict-Transport-Security headers
87 use_htsts = false
88
89 ## number of commits stats will parse on each iteration
90 commit_parse_limit = 25
91
92 ## number of items displayed in lightweight dashboard before paginating is shown
72 93 dashboard_items = 100
94
95 ## use gravatar service to display avatars
73 96 use_gravatar = true
74 97
98 ## path to git executable
99 git_path = git
100
101 ## git rev filter option, --all is the default filter, if you need to
102 ## hide all refs in changelog switch this to --branches --tags
103 git_rev_filter=--all
104
75 105 ## RSS feed options
76
77 106 rss_cut_off_limit = 256000
78 107 rss_items_per_page = 10
79 108 rss_include_diff = false
80 109
110 ## options for showing and identifying changesets
111 show_sha_length = 12
112 show_revision_number = true
113
81 114
82 115 ## alternative_gravatar_url allows you to use your own avatar server application
83 116 ## the following parts of the URL will be replaced
84 117 ## {email} user email
85 118 ## {md5email} md5 hash of the user email (like at gravatar.com)
86 119 ## {size} size of the image that is expected from the server application
87 120 ## {scheme} http/https from RhodeCode server
88 121 ## {netloc} network location from RhodeCode server
89 122 #alternative_gravatar_url = http://myavatarserver.com/getbyemail/{email}/{size}
90 123 #alternative_gravatar_url = http://myavatarserver.com/getbymd5/{md5email}?s={size}
91 124
125
126 ## container auth options
92 127 container_auth_enabled = false
93 128 proxypass_auth_enabled = false
129
94 130 ## default encoding used to convert from and to unicode
95 131 ## can be also a comma seperated list of encoding in case of mixed encodings
96 132 default_encoding = utf8
97 133
98 134 ## overwrite schema of clone url
99 135 ## available vars:
100 136 ## scheme - http/https
101 137 ## user - current user
102 138 ## pass - password
103 139 ## netloc - network location
104 140 ## path - usually repo_name
105 141
106 142 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
107 143
108 144 ## issue tracking mapping for commits messages
109 145 ## comment out issue_pat, issue_server, issue_prefix to enable
110 146
111 147 ## pattern to get the issues from commit messages
112 148 ## default one used here is #<numbers> with a regex passive group for `#`
113 149 ## {id} will be all groups matched from this pattern
114 150
115 151 issue_pat = (?:\s*#)(\d+)
116 152
117 153 ## server url to the issue, each {id} will be replaced with match
118 154 ## fetched from the regex and {repo} is replaced with full repository name
119 155 ## including groups {repo_name} is replaced with just name of repo
120 156
121 157 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
122 158
123 159 ## prefix to add to link to indicate it's an url
124 160 ## #314 will be replaced by <issue_prefix><id>
125 161
126 162 issue_prefix = #
127 163
128 164 ## issue_pat, issue_server_link, issue_prefix can have suffixes to specify
129 165 ## multiple patterns, to other issues server, wiki or others
130 166 ## below an example how to create a wiki pattern
131 167 # #wiki-some-id -> https://mywiki.com/some-id
132 168
133 169 #issue_pat_wiki = (?:wiki-)(.+)
134 170 #issue_server_link_wiki = https://mywiki.com/{id}
135 171 #issue_prefix_wiki = WIKI-
136 172
137 173
138 174 ## instance-id prefix
139 175 ## a prefix key for this instance used for cache invalidation when running
140 176 ## multiple instances of rhodecode, make sure it's globally unique for
141 177 ## all running rhodecode instances. Leave empty if you don't use it
142 178 instance_id =
143 179
144 180 ## alternative return HTTP header for failed authentication. Default HTTP
145 181 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
146 182 ## handling that. Set this variable to 403 to return HTTPForbidden
147 183 auth_ret_code =
148 184
185 ## locking return code. When repository is locked return this HTTP code. 2XX
186 ## codes don't break the transactions while 4XX codes do
187 lock_ret_code = 423
188
189
149 190 ####################################
150 191 ### CELERY CONFIG ####
151 192 ####################################
152 193 use_celery = false
153 194 broker.host = localhost
154 195 broker.vhost = rabbitmqhost
155 196 broker.port = 5672
156 197 broker.user = rabbitmq
157 198 broker.password = qweqwe
158 199
159 200 celery.imports = rhodecode.lib.celerylib.tasks
160 201
161 202 celery.result.backend = amqp
162 203 celery.result.dburi = amqp://
163 204 celery.result.serialier = json
164 205
165 206 #celery.send.task.error.emails = true
166 207 #celery.amqp.task.result.expires = 18000
167 208
168 209 celeryd.concurrency = 2
169 210 #celeryd.log.file = celeryd.log
170 211 celeryd.log.level = debug
171 212 celeryd.max.tasks.per.child = 1
172 213
173 #tasks will never be sent to the queue, but executed locally instead.
214 ## tasks will never be sent to the queue, but executed locally instead.
174 215 celery.always.eager = false
175 216
176 217 ####################################
177 218 ### BEAKER CACHE ####
178 219 ####################################
179 220 beaker.cache.data_dir=%(here)s/data/cache/data
180 221 beaker.cache.lock_dir=%(here)s/data/cache/lock
181 222
182 223 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
183 224
184 225 beaker.cache.super_short_term.type=memory
185 226 beaker.cache.super_short_term.expire=10
186 227 beaker.cache.super_short_term.key_length = 256
187 228
188 229 beaker.cache.short_term.type=memory
189 230 beaker.cache.short_term.expire=60
190 231 beaker.cache.short_term.key_length = 256
191 232
192 233 beaker.cache.long_term.type=memory
193 234 beaker.cache.long_term.expire=36000
194 235 beaker.cache.long_term.key_length = 256
195 236
196 237 beaker.cache.sql_cache_short.type=memory
197 238 beaker.cache.sql_cache_short.expire=10
198 239 beaker.cache.sql_cache_short.key_length = 256
199 240
200 241 beaker.cache.sql_cache_med.type=memory
201 242 beaker.cache.sql_cache_med.expire=360
202 243 beaker.cache.sql_cache_med.key_length = 256
203 244
204 245 beaker.cache.sql_cache_long.type=file
205 246 beaker.cache.sql_cache_long.expire=3600
206 247 beaker.cache.sql_cache_long.key_length = 256
207 248
208 249 ####################################
209 250 ### BEAKER SESSION ####
210 251 ####################################
211 252 ## Type of storage used for the session, current types are
212 253 ## dbm, file, memcached, database, and memory.
213 254 ## The storage uses the Container API
214 255 ## that is also used by the cache system.
215 256
216 257 ## db session ##
217 258 #beaker.session.type = ext:database
218 259 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
219 260 #beaker.session.table_name = db_session
220 261
221 262 ## encrypted cookie client side session, good for many instances ##
222 263 #beaker.session.type = cookie
223 264
224 265 ## file based cookies (default) ##
225 266 #beaker.session.type = file
226 267
227 268
228 269 beaker.session.key = rhodecode
229 ## secure cookie requires AES python libraries ##
230 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
231 #beaker.session.validate_key = 9712sds2212c--zxc123
270 ## secure cookie requires AES python libraries
271 #beaker.session.encrypt_key = <key_for_encryption>
272 #beaker.session.validate_key = <validation_key>
273
232 274 ## sets session as invalid if it haven't been accessed for given amount of time
233 275 beaker.session.timeout = 2592000
234 276 beaker.session.httponly = true
235 277 #beaker.session.cookie_path = /<your-prefix>
236 278
237 ## uncomment for https secure cookie ##
279 ## uncomment for https secure cookie
238 280 beaker.session.secure = false
239 281
240 ## auto save the session to not to use .save() ##
282 ## auto save the session to not to use .save()
241 283 beaker.session.auto = False
242 284
243 285 ## default cookie expiration time in seconds `true` expire at browser close ##
244 286 #beaker.session.cookie_expires = 3600
245 287
246 288
247 289 ############################
248 290 ## ERROR HANDLING SYSTEMS ##
249 291 ############################
250 292
251 293 ####################
252 294 ### [errormator] ###
253 295 ####################
254 296
255 # Errormator is tailored to work with RhodeCode, see
256 # http://errormator.com for details how to obtain an account
257 # you must install python package `errormator_client` to make it work
297 ## Errormator is tailored to work with RhodeCode, see
298 ## http://errormator.com for details how to obtain an account
299 ## you must install python package `errormator_client` to make it work
258 300
259 # errormator enabled
260 errormator = true
301 ## errormator enabled
302 errormator = false
261 303
262 304 errormator.server_url = https://api.errormator.com
263 305 errormator.api_key = YOUR_API_KEY
264 306
265 # TWEAK AMOUNT OF INFO SENT HERE
307 ## TWEAK AMOUNT OF INFO SENT HERE
266 308
267 # enables 404 error logging (default False)
309 ## enables 404 error logging (default False)
268 310 errormator.report_404 = false
269 311
270 # time in seconds after request is considered being slow (default 1)
312 ## time in seconds after request is considered being slow (default 1)
271 313 errormator.slow_request_time = 1
272 314
273 # record slow requests in application
274 # (needs to be enabled for slow datastore recording and time tracking)
315 ## record slow requests in application
316 ## (needs to be enabled for slow datastore recording and time tracking)
275 317 errormator.slow_requests = true
276 318
277 # enable hooking to application loggers
319 ## enable hooking to application loggers
278 320 # errormator.logging = true
279 321
280 # minimum log level for log capture
322 ## minimum log level for log capture
281 323 # errormator.logging.level = WARNING
282 324
283 # send logs only from erroneous/slow requests
284 # (saves API quota for intensive logging)
325 ## send logs only from erroneous/slow requests
326 ## (saves API quota for intensive logging)
285 327 errormator.logging_on_error = false
286 328
287 # list of additonal keywords that should be grabbed from environ object
288 # can be string with comma separated list of words in lowercase
289 # (by default client will always send following info:
290 # 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
291 # start with HTTP* this list be extended with additional keywords here
329 ## list of additonal keywords that should be grabbed from environ object
330 ## can be string with comma separated list of words in lowercase
331 ## (by default client will always send following info:
332 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
333 ## start with HTTP* this list be extended with additional keywords here
292 334 errormator.environ_keys_whitelist =
293 335
294 336
295 # list of keywords that should be blanked from request object
296 # can be string with comma separated list of words in lowercase
297 # (by default client will always blank keys that contain following words
298 # 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
299 # this list be extended with additional keywords set here
337 ## list of keywords that should be blanked from request object
338 ## can be string with comma separated list of words in lowercase
339 ## (by default client will always blank keys that contain following words
340 ## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
341 ## this list be extended with additional keywords set here
300 342 errormator.request_keys_blacklist =
301 343
302 344
303 # list of namespaces that should be ignores when gathering log entries
304 # can be string with comma separated list of namespaces
305 # (by default the client ignores own entries: errormator_client.client)
345 ## list of namespaces that should be ignores when gathering log entries
346 ## can be string with comma separated list of namespaces
347 ## (by default the client ignores own entries: errormator_client.client)
306 348 errormator.log_namespace_blacklist =
307 349
308 350
309 351 ################
310 352 ### [sentry] ###
311 353 ################
312 354
313 # sentry is a alternative open source error aggregator
314 # you must install python packages `sentry` and `raven` to enable
355 ## sentry is a alternative open source error aggregator
356 ## you must install python packages `sentry` and `raven` to enable
315 357
316 358 sentry.dsn = YOUR_DNS
317 359 sentry.servers =
318 360 sentry.name =
319 361 sentry.key =
320 362 sentry.public_key =
321 363 sentry.secret_key =
322 364 sentry.project =
323 365 sentry.site =
324 366 sentry.include_paths =
325 367 sentry.exclude_paths =
326 368
327 369
328 370 ################################################################################
329 371 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
330 372 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
331 373 ## execute malicious code after an exception is raised. ##
332 374 ################################################################################
333 375 set debug = false
334 376
335 377 ##################################
336 378 ### LOGVIEW CONFIG ###
337 379 ##################################
338 380 logview.sqlalchemy = #faa
339 381 logview.pylons.templating = #bfb
340 382 logview.pylons.util = #eee
341 383
342 384 #########################################################
343 385 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
344 386 #########################################################
345 387 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
346 388 sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode
347 389 sqlalchemy.db1.echo = false
348 390 sqlalchemy.db1.pool_recycle = 3600
349 391 sqlalchemy.db1.convert_unicode = true
350 392
351 393 ################################
352 394 ### LOGGING CONFIGURATION ####
353 395 ################################
354 396 [loggers]
355 397 keys = root, routes, rhodecode, sqlalchemy, beaker, templates, whoosh_indexer
356 398
357 399 [handlers]
358 400 keys = console, console_sql
359 401
360 402 [formatters]
361 403 keys = generic, color_formatter, color_formatter_sql
362 404
363 405 #############
364 406 ## LOGGERS ##
365 407 #############
366 408 [logger_root]
367 409 level = NOTSET
368 410 handlers = console
369 411
370 412 [logger_routes]
371 413 level = DEBUG
372 414 handlers =
373 415 qualname = routes.middleware
374 # "level = DEBUG" logs the route matched and routing variables.
416 ## "level = DEBUG" logs the route matched and routing variables.
375 417 propagate = 1
376 418
377 419 [logger_beaker]
378 420 level = DEBUG
379 421 handlers =
380 422 qualname = beaker.container
381 423 propagate = 1
382 424
383 425 [logger_templates]
384 426 level = INFO
385 427 handlers =
386 428 qualname = pylons.templating
387 429 propagate = 1
388 430
389 431 [logger_rhodecode]
390 432 level = DEBUG
391 433 handlers =
392 434 qualname = rhodecode
393 435 propagate = 1
394 436
395 437 [logger_sqlalchemy]
396 438 level = INFO
397 439 handlers = console_sql
398 440 qualname = sqlalchemy.engine
399 441 propagate = 0
400 442
401 443 [logger_whoosh_indexer]
402 444 level = DEBUG
403 445 handlers =
404 446 qualname = whoosh_indexer
405 447 propagate = 1
406 448
407 449 ##############
408 450 ## HANDLERS ##
409 451 ##############
410 452
411 453 [handler_console]
412 454 class = StreamHandler
413 455 args = (sys.stderr,)
414 456 level = INFO
415 457 formatter = generic
416 458
417 459 [handler_console_sql]
418 460 class = StreamHandler
419 461 args = (sys.stderr,)
420 462 level = WARN
421 463 formatter = generic
422 464
423 465 ################
424 466 ## FORMATTERS ##
425 467 ################
426 468
427 469 [formatter_generic]
428 470 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
429 471 datefmt = %Y-%m-%d %H:%M:%S
430 472
431 473 [formatter_color_formatter]
432 474 class=rhodecode.lib.colored_formatter.ColorFormatter
433 475 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
434 476 datefmt = %Y-%m-%d %H:%M:%S
435 477
436 478 [formatter_color_formatter_sql]
437 479 class=rhodecode.lib.colored_formatter.ColorFormatterSql
438 480 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
439 481 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,67 +1,67 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.__init__
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 RhodeCode, a web based repository management based on pylons
7 7 versioning implementation: http://www.python.org/dev/peps/pep-0386/
8 8
9 9 :created_on: Apr 9, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import sys
27 27 import platform
28 28
29 VERSION = (1, 5, 4)
29 VERSION = (1, 6, 0, 'rc1')
30 30
31 31 try:
32 32 from rhodecode.lib import get_current_revision
33 33 _rev = get_current_revision(quiet=True)
34 34 if _rev and len(VERSION) > 3:
35 35 VERSION += ('dev%s' % _rev[0],)
36 36 except ImportError:
37 37 pass
38 38
39 39 __version__ = ('.'.join((str(each) for each in VERSION[:3])) +
40 40 '.'.join(VERSION[3:]))
41 __dbversion__ = 10 # defines current db version for migrations
41 __dbversion__ = 11 # defines current db version for migrations
42 42 __platform__ = platform.system()
43 43 __license__ = 'GPLv3'
44 44 __py_version__ = sys.version_info
45 45 __author__ = 'Marcin Kuzminski'
46 46 __url__ = 'http://rhodecode.org'
47 47
48 48 PLATFORM_WIN = ('Windows')
49 49 PLATFORM_OTHERS = ('Linux', 'Darwin', 'FreeBSD', 'OpenBSD', 'SunOS') #depracated
50 50
51 51 is_windows = __platform__ in PLATFORM_WIN
52 52 is_unix = not is_windows
53 53
54 54
55 55 BACKENDS = {
56 56 'hg': 'Mercurial repository',
57 57 'git': 'Git repository',
58 58 }
59 59
60 60 CELERY_ON = False
61 61 CELERY_EAGER = False
62 62
63 63 # link to config for pylons
64 64 CONFIG = {}
65 65
66 66 # Linked module for extensions
67 67 EXTENSIONS = {}
@@ -1,249 +1,249 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.bin.backup_manager
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Api CLI client for RhodeCode
7 7
8 8 :created_on: Jun 3, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 from __future__ import with_statement
27 27 import os
28 28 import sys
29 29 import random
30 30 import urllib2
31 31 import pprint
32 32 import argparse
33 33
34 34 try:
35 35 from rhodecode.lib.ext_json import json
36 36 except ImportError:
37 37 try:
38 38 import simplejson as json
39 39 except ImportError:
40 40 import json
41 41
42 42
43 43 CONFIG_NAME = '.rhodecode'
44 44 FORMAT_PRETTY = 'pretty'
45 45 FORMAT_JSON = 'json'
46 46
47 47
48 48 class RcConf(object):
49 49 """
50 50 RhodeCode config for API
51 51
52 52 conf = RcConf()
53 53 conf['key']
54 54
55 55 """
56 56
57 57 def __init__(self, config_location=None, autoload=True, autocreate=False,
58 58 config=None):
59 59 self._conf_name = CONFIG_NAME if not config_location else config_location
60 60 self._conf = {}
61 61 if autocreate:
62 62 self.make_config(config)
63 63 if autoload:
64 64 self._conf = self.load_config()
65 65
66 66 def __getitem__(self, key):
67 67 return self._conf[key]
68 68
69 69 def __nonzero__(self):
70 70 if self._conf:
71 71 return True
72 72 return False
73 73
74 74 def __eq__(self):
75 75 return self._conf.__eq__()
76 76
77 77 def __repr__(self):
78 78 return 'RcConf<%s>' % self._conf.__repr__()
79 79
80 80 def make_config(self, config):
81 81 """
82 82 Saves given config as a JSON dump in the _conf_name location
83 83
84 84 :param config:
85 85 :type config:
86 86 """
87 87 update = False
88 88 if os.path.exists(self._conf_name):
89 89 update = True
90 90 with open(self._conf_name, 'wb') as f:
91 91 json.dump(config, f, indent=4)
92 92
93 93 if update:
94 94 sys.stdout.write('Updated config in %s\n' % self._conf_name)
95 95 else:
96 96 sys.stdout.write('Created new config in %s\n' % self._conf_name)
97 97
98 98 def update_config(self, new_config):
99 99 """
100 100 Reads the JSON config updates it's values with new_config and
101 101 saves it back as JSON dump
102 102
103 103 :param new_config:
104 104 """
105 105 config = {}
106 106 try:
107 107 with open(self._conf_name, 'rb') as conf:
108 108 config = json.load(conf)
109 109 except IOError, e:
110 110 sys.stderr.write(str(e) + '\n')
111 111
112 112 config.update(new_config)
113 113 self.make_config(config)
114 114
115 115 def load_config(self):
116 116 """
117 117 Loads config from file and returns loaded JSON object
118 118 """
119 119 try:
120 120 with open(self._conf_name, 'rb') as conf:
121 121 return json.load(conf)
122 122 except IOError, e:
123 123 #sys.stderr.write(str(e) + '\n')
124 124 pass
125 125
126 126
127 127 def api_call(apikey, apihost, format, method=None, **kw):
128 128 """
129 129 Api_call wrapper for RhodeCode
130 130
131 131 :param apikey:
132 132 :param apihost:
133 133 :param format: formatting, pretty means prints and pprint of json
134 134 json returns unparsed json
135 135 :param method:
136 136 """
137 137 def _build_data(random_id):
138 138 """
139 139 Builds API data with given random ID
140 140
141 141 :param random_id:
142 142 :type random_id:
143 143 """
144 144 return {
145 145 "id": random_id,
146 146 "api_key": apikey,
147 147 "method": method,
148 148 "args": kw
149 149 }
150 150
151 151 if not method:
152 152 raise Exception('please specify method name !')
153 153 id_ = random.randrange(1, 9999)
154 154 req = urllib2.Request('%s/_admin/api' % apihost,
155 155 data=json.dumps(_build_data(id_)),
156 156 headers={'content-type': 'text/plain'})
157 157 if format == FORMAT_PRETTY:
158 158 sys.stdout.write('calling %s to %s \n' % (req.get_data(), apihost))
159 159 ret = urllib2.urlopen(req)
160 160 raw_json = ret.read()
161 161 json_data = json.loads(raw_json)
162 162 id_ret = json_data['id']
163 163 _formatted_json = pprint.pformat(json_data)
164 164 if id_ret == id_:
165 165 if format == FORMAT_JSON:
166 166 sys.stdout.write(str(raw_json))
167 167 else:
168 168 sys.stdout.write('rhodecode returned:\n%s\n' % (_formatted_json))
169 169
170 170 else:
171 171 raise Exception('something went wrong. '
172 172 'ID mismatch got %s, expected %s | %s' % (
173 173 id_ret, id_, _formatted_json))
174 174
175 175
176 176 def argparser(argv):
177 177 usage = (
178 178 "rhodecode_api [-h] [--format=FORMAT] [--apikey=APIKEY] [--apihost=APIHOST] "
179 179 " [--config=CONFIG] "
180 180 "_create_config or METHOD <key:val> <key2:val> ..."
181 181 )
182 182
183 183 parser = argparse.ArgumentParser(description='RhodeCode API cli',
184 184 usage=usage)
185 185
186 186 ## config
187 187 group = parser.add_argument_group('config')
188 188 group.add_argument('--apikey', help='api access key')
189 189 group.add_argument('--apihost', help='api host')
190 190 group.add_argument('--config', help='config file')
191 191
192 192 group = parser.add_argument_group('API')
193 193 group.add_argument('method', metavar='METHOD', type=str,
194 194 help='API method name to call followed by key:value attributes',
195 195 )
196 196 group.add_argument('--format', dest='format', type=str,
197 197 help='output format default: `pretty` can '
198 198 'be also `%s`' % FORMAT_JSON,
199 199 default=FORMAT_PRETTY
200 200 )
201 201 args, other = parser.parse_known_args()
202 202 return parser, args, other
203 203
204 204
205 205 def main(argv=None):
206 206 """
207 207 Main execution function for cli
208 208
209 209 :param argv:
210 210 :type argv:
211 211 """
212 212 if argv is None:
213 213 argv = sys.argv
214 214
215 215 conf = None
216 216 parser, args, other = argparser(argv)
217 217
218 218 api_credentials_given = (args.apikey and args.apihost)
219 219 if args.method == '_create_config':
220 220 if not api_credentials_given:
221 221 raise parser.error('_create_config requires --apikey and --apihost')
222 222 conf = RcConf(config_location=args.config,
223 223 autocreate=True, config={'apikey': args.apikey,
224 224 'apihost': args.apihost})
225 225
226 226 if not conf:
227 227 conf = RcConf(config_location=args.config, autoload=True)
228 228 if not conf:
229 229 if not api_credentials_given:
230 230 parser.error('Could not find config file and missing '
231 231 '--apikey or --apihost in params')
232 232
233 233 apikey = args.apikey or conf['apikey']
234 234 host = args.apihost or conf['apihost']
235 235 method = args.method
236 236 if method == '_create_config':
237 237 sys.exit()
238 238
239 239 try:
240 240 margs = dict(map(lambda s: s.split(':', 1), other))
241 except:
241 except Exception:
242 242 sys.stderr.write('Error parsing arguments \n')
243 243 sys.exit()
244 244
245 245 api_call(apikey, host, args.format, method, **margs)
246 246 return 0
247 247
248 248 if __name__ == '__main__':
249 249 sys.exit(main(sys.argv))
@@ -1,449 +1,491 b''
1 1 ################################################################################
2 2 ################################################################################
3 3 # RhodeCode - Pylons environment configuration #
4 4 # #
5 5 # The %(here)s variable will be replaced with the parent directory of this file#
6 6 ################################################################################
7 7
8 8 [DEFAULT]
9 9 debug = true
10 10 pdebug = false
11 11 ################################################################################
12 12 ## Uncomment and replace with the address which should receive ##
13 13 ## any error reports after application crash ##
14 14 ## Additionally those settings will be used by RhodeCode mailing system ##
15 15 ################################################################################
16 16 #email_to = admin@localhost
17 17 #error_email_from = paste_error@localhost
18 18 #app_email_from = rhodecode-noreply@localhost
19 19 #error_message =
20 20 #email_prefix = [RhodeCode]
21 21
22 22 #smtp_server = mail.server.com
23 23 #smtp_username =
24 24 #smtp_password =
25 25 #smtp_port =
26 26 #smtp_use_tls = false
27 27 #smtp_use_ssl = true
28 # Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
28 ## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
29 29 #smtp_auth =
30 30
31 31 [server:main]
32 32 ## PASTE
33 33 ##nr of threads to spawn
34 34 #threadpool_workers = 5
35 35
36 36 ##max request before thread respawn
37 37 #threadpool_max_requests = 10
38 38
39 39 ##option to use threads of process
40 40 #use_threadpool = true
41 41
42 42 #use = egg:Paste#http
43 43
44 #WAITRESS
44 ## WAITRESS
45 45 threads = 5
46 ## 100GB
47 max_request_body_size = 107374182400
46 48 use = egg:waitress#main
47 49
48 50 host = 127.0.0.1
49 51 port = 5000
50 52
51 [filter:proxy-prefix]
52 # prefix middleware for rc
53 use = egg:PasteDeploy#prefix
54 prefix = /<your-prefix>
53 ## prefix middleware for rc
54 #[filter:proxy-prefix]
55 #use = egg:PasteDeploy#prefix
56 #prefix = /<your-prefix>
55 57
56 58 [app:main]
57 59 use = egg:rhodecode
60 ## enable proxy prefix middleware
58 61 #filter-with = proxy-prefix
62
59 63 full_stack = true
60 64 static_files = true
61 # Optional Languages
62 # en, fr, ja, pt_BR, zh_CN, zh_TW, pl
65 ## Optional Languages
66 ## en, fr, ja, pt_BR, zh_CN, zh_TW, pl
63 67 lang = en
64 68 cache_dir = %(here)s/data
65 69 index_dir = %(here)s/data/index
70
71 ## uncomment and set this path to use archive download cache
72 #archive_cache_dir = /tmp/tarballcache
73
74 ## change this to unique ID for security
66 75 app_instance_uuid = ${app_instance_uuid}
76
77 ## cut off limit for large diffs (size in bytes)
67 78 cut_off_limit = 256000
68 vcs_full_cache = True
79
80 ## use cache version of scm repo everywhere
81 vcs_full_cache = true
82
83 ## force https in RhodeCode, fixes https redirects, assumes it's always https
69 84 force_https = false
70 commit_parse_limit = 50
71 # number of items displayed in lightweight dashboard before paginating
85
86 ## use Strict-Transport-Security headers
87 use_htsts = false
88
89 ## number of commits stats will parse on each iteration
90 commit_parse_limit = 25
91
92 ## number of items displayed in lightweight dashboard before paginating is shown
72 93 dashboard_items = 100
94
95 ## use gravatar service to display avatars
73 96 use_gravatar = true
74 97
98 ## path to git executable
99 git_path = git
100
101 ## git rev filter option, --all is the default filter, if you need to
102 ## hide all refs in changelog switch this to --branches --tags
103 git_rev_filter=--all
104
75 105 ## RSS feed options
76
77 106 rss_cut_off_limit = 256000
78 107 rss_items_per_page = 10
79 108 rss_include_diff = false
80 109
110 ## options for showing and identifying changesets
111 show_sha_length = 12
112 show_revision_number = true
113
81 114
82 115 ## alternative_gravatar_url allows you to use your own avatar server application
83 116 ## the following parts of the URL will be replaced
84 117 ## {email} user email
85 118 ## {md5email} md5 hash of the user email (like at gravatar.com)
86 119 ## {size} size of the image that is expected from the server application
87 120 ## {scheme} http/https from RhodeCode server
88 121 ## {netloc} network location from RhodeCode server
89 122 #alternative_gravatar_url = http://myavatarserver.com/getbyemail/{email}/{size}
90 123 #alternative_gravatar_url = http://myavatarserver.com/getbymd5/{md5email}?s={size}
91 124
125
126 ## container auth options
92 127 container_auth_enabled = false
93 128 proxypass_auth_enabled = false
129
94 130 ## default encoding used to convert from and to unicode
95 131 ## can be also a comma seperated list of encoding in case of mixed encodings
96 132 default_encoding = utf8
97 133
98 134 ## overwrite schema of clone url
99 135 ## available vars:
100 136 ## scheme - http/https
101 137 ## user - current user
102 138 ## pass - password
103 139 ## netloc - network location
104 140 ## path - usually repo_name
105 141
106 142 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
107 143
108 144 ## issue tracking mapping for commits messages
109 145 ## comment out issue_pat, issue_server, issue_prefix to enable
110 146
111 147 ## pattern to get the issues from commit messages
112 148 ## default one used here is #<numbers> with a regex passive group for `#`
113 149 ## {id} will be all groups matched from this pattern
114 150
115 151 issue_pat = (?:\s*#)(\d+)
116 152
117 153 ## server url to the issue, each {id} will be replaced with match
118 154 ## fetched from the regex and {repo} is replaced with full repository name
119 155 ## including groups {repo_name} is replaced with just name of repo
120 156
121 157 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
122 158
123 159 ## prefix to add to link to indicate it's an url
124 160 ## #314 will be replaced by <issue_prefix><id>
125 161
126 162 issue_prefix = #
127 163
128 164 ## issue_pat, issue_server_link, issue_prefix can have suffixes to specify
129 165 ## multiple patterns, to other issues server, wiki or others
130 166 ## below an example how to create a wiki pattern
131 167 # #wiki-some-id -> https://mywiki.com/some-id
132 168
133 169 #issue_pat_wiki = (?:wiki-)(.+)
134 170 #issue_server_link_wiki = https://mywiki.com/{id}
135 171 #issue_prefix_wiki = WIKI-
136 172
137 173
138 174 ## instance-id prefix
139 175 ## a prefix key for this instance used for cache invalidation when running
140 176 ## multiple instances of rhodecode, make sure it's globally unique for
141 177 ## all running rhodecode instances. Leave empty if you don't use it
142 178 instance_id =
143 179
144 180 ## alternative return HTTP header for failed authentication. Default HTTP
145 181 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
146 182 ## handling that. Set this variable to 403 to return HTTPForbidden
147 183 auth_ret_code =
148 184
185 ## locking return code. When repository is locked return this HTTP code. 2XX
186 ## codes don't break the transactions while 4XX codes do
187 lock_ret_code = 423
188
189
149 190 ####################################
150 191 ### CELERY CONFIG ####
151 192 ####################################
152 193 use_celery = false
153 194 broker.host = localhost
154 195 broker.vhost = rabbitmqhost
155 196 broker.port = 5672
156 197 broker.user = rabbitmq
157 198 broker.password = qweqwe
158 199
159 200 celery.imports = rhodecode.lib.celerylib.tasks
160 201
161 202 celery.result.backend = amqp
162 203 celery.result.dburi = amqp://
163 204 celery.result.serialier = json
164 205
165 206 #celery.send.task.error.emails = true
166 207 #celery.amqp.task.result.expires = 18000
167 208
168 209 celeryd.concurrency = 2
169 210 #celeryd.log.file = celeryd.log
170 211 celeryd.log.level = debug
171 212 celeryd.max.tasks.per.child = 1
172 213
173 #tasks will never be sent to the queue, but executed locally instead.
214 ## tasks will never be sent to the queue, but executed locally instead.
174 215 celery.always.eager = false
175 216
176 217 ####################################
177 218 ### BEAKER CACHE ####
178 219 ####################################
179 220 beaker.cache.data_dir=%(here)s/data/cache/data
180 221 beaker.cache.lock_dir=%(here)s/data/cache/lock
181 222
182 223 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
183 224
184 225 beaker.cache.super_short_term.type=memory
185 226 beaker.cache.super_short_term.expire=10
186 227 beaker.cache.super_short_term.key_length = 256
187 228
188 229 beaker.cache.short_term.type=memory
189 230 beaker.cache.short_term.expire=60
190 231 beaker.cache.short_term.key_length = 256
191 232
192 233 beaker.cache.long_term.type=memory
193 234 beaker.cache.long_term.expire=36000
194 235 beaker.cache.long_term.key_length = 256
195 236
196 237 beaker.cache.sql_cache_short.type=memory
197 238 beaker.cache.sql_cache_short.expire=10
198 239 beaker.cache.sql_cache_short.key_length = 256
199 240
200 241 beaker.cache.sql_cache_med.type=memory
201 242 beaker.cache.sql_cache_med.expire=360
202 243 beaker.cache.sql_cache_med.key_length = 256
203 244
204 245 beaker.cache.sql_cache_long.type=file
205 246 beaker.cache.sql_cache_long.expire=3600
206 247 beaker.cache.sql_cache_long.key_length = 256
207 248
208 249 ####################################
209 250 ### BEAKER SESSION ####
210 251 ####################################
211 252 ## Type of storage used for the session, current types are
212 253 ## dbm, file, memcached, database, and memory.
213 254 ## The storage uses the Container API
214 255 ## that is also used by the cache system.
215 256
216 257 ## db session ##
217 258 #beaker.session.type = ext:database
218 259 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
219 260 #beaker.session.table_name = db_session
220 261
221 262 ## encrypted cookie client side session, good for many instances ##
222 263 #beaker.session.type = cookie
223 264
224 265 ## file based cookies (default) ##
225 266 #beaker.session.type = file
226 267
227 268
228 269 beaker.session.key = rhodecode
229 ## secure cookie requires AES python libraries ##
230 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
231 #beaker.session.validate_key = 9712sds2212c--zxc123
270 ## secure cookie requires AES python libraries
271 #beaker.session.encrypt_key = <key_for_encryption>
272 #beaker.session.validate_key = <validation_key>
273
232 274 ## sets session as invalid if it haven't been accessed for given amount of time
233 275 beaker.session.timeout = 2592000
234 276 beaker.session.httponly = true
235 277 #beaker.session.cookie_path = /<your-prefix>
236 278
237 ## uncomment for https secure cookie ##
279 ## uncomment for https secure cookie
238 280 beaker.session.secure = false
239 281
240 ## auto save the session to not to use .save() ##
282 ## auto save the session to not to use .save()
241 283 beaker.session.auto = False
242 284
243 285 ## default cookie expiration time in seconds `true` expire at browser close ##
244 286 #beaker.session.cookie_expires = 3600
245 287
246 288
247 289 ############################
248 290 ## ERROR HANDLING SYSTEMS ##
249 291 ############################
250 292
251 293 ####################
252 294 ### [errormator] ###
253 295 ####################
254 296
255 # Errormator is tailored to work with RhodeCode, see
256 # http://errormator.com for details how to obtain an account
257 # you must install python package `errormator_client` to make it work
297 ## Errormator is tailored to work with RhodeCode, see
298 ## http://errormator.com for details how to obtain an account
299 ## you must install python package `errormator_client` to make it work
258 300
259 # errormator enabled
260 errormator = true
301 ## errormator enabled
302 errormator = false
261 303
262 304 errormator.server_url = https://api.errormator.com
263 305 errormator.api_key = YOUR_API_KEY
264 306
265 # TWEAK AMOUNT OF INFO SENT HERE
307 ## TWEAK AMOUNT OF INFO SENT HERE
266 308
267 # enables 404 error logging (default False)
309 ## enables 404 error logging (default False)
268 310 errormator.report_404 = false
269 311
270 # time in seconds after request is considered being slow (default 1)
312 ## time in seconds after request is considered being slow (default 1)
271 313 errormator.slow_request_time = 1
272 314
273 # record slow requests in application
274 # (needs to be enabled for slow datastore recording and time tracking)
315 ## record slow requests in application
316 ## (needs to be enabled for slow datastore recording and time tracking)
275 317 errormator.slow_requests = true
276 318
277 # enable hooking to application loggers
319 ## enable hooking to application loggers
278 320 # errormator.logging = true
279 321
280 # minimum log level for log capture
322 ## minimum log level for log capture
281 323 # errormator.logging.level = WARNING
282 324
283 # send logs only from erroneous/slow requests
284 # (saves API quota for intensive logging)
325 ## send logs only from erroneous/slow requests
326 ## (saves API quota for intensive logging)
285 327 errormator.logging_on_error = false
286 328
287 # list of additonal keywords that should be grabbed from environ object
288 # can be string with comma separated list of words in lowercase
289 # (by default client will always send following info:
290 # 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
291 # start with HTTP* this list be extended with additional keywords here
329 ## list of additonal keywords that should be grabbed from environ object
330 ## can be string with comma separated list of words in lowercase
331 ## (by default client will always send following info:
332 ## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
333 ## start with HTTP* this list be extended with additional keywords here
292 334 errormator.environ_keys_whitelist =
293 335
294 336
295 # list of keywords that should be blanked from request object
296 # can be string with comma separated list of words in lowercase
297 # (by default client will always blank keys that contain following words
298 # 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
299 # this list be extended with additional keywords set here
337 ## list of keywords that should be blanked from request object
338 ## can be string with comma separated list of words in lowercase
339 ## (by default client will always blank keys that contain following words
340 ## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
341 ## this list be extended with additional keywords set here
300 342 errormator.request_keys_blacklist =
301 343
302 344
303 # list of namespaces that should be ignores when gathering log entries
304 # can be string with comma separated list of namespaces
305 # (by default the client ignores own entries: errormator_client.client)
345 ## list of namespaces that should be ignores when gathering log entries
346 ## can be string with comma separated list of namespaces
347 ## (by default the client ignores own entries: errormator_client.client)
306 348 errormator.log_namespace_blacklist =
307 349
308 350
309 351 ################
310 352 ### [sentry] ###
311 353 ################
312 354
313 # sentry is a alternative open source error aggregator
314 # you must install python packages `sentry` and `raven` to enable
355 ## sentry is a alternative open source error aggregator
356 ## you must install python packages `sentry` and `raven` to enable
315 357
316 358 sentry.dsn = YOUR_DNS
317 359 sentry.servers =
318 360 sentry.name =
319 361 sentry.key =
320 362 sentry.public_key =
321 363 sentry.secret_key =
322 364 sentry.project =
323 365 sentry.site =
324 366 sentry.include_paths =
325 367 sentry.exclude_paths =
326 368
327 369
328 370 ################################################################################
329 371 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
330 372 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
331 373 ## execute malicious code after an exception is raised. ##
332 374 ################################################################################
333 375 set debug = false
334 376
335 377 ##################################
336 378 ### LOGVIEW CONFIG ###
337 379 ##################################
338 380 logview.sqlalchemy = #faa
339 381 logview.pylons.templating = #bfb
340 382 logview.pylons.util = #eee
341 383
342 384 #########################################################
343 385 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
344 386 #########################################################
345 387
346 388 # SQLITE [default]
347 389 sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
348 390
349 391 # POSTGRESQL
350 392 # sqlalchemy.db1.url = postgresql://user:pass@localhost/rhodecode
351 393
352 394 # MySQL
353 395 # sqlalchemy.db1.url = mysql://user:pass@localhost/rhodecode
354 396
355 397 # see sqlalchemy docs for others
356 398
357 399 sqlalchemy.db1.echo = false
358 400 sqlalchemy.db1.pool_recycle = 3600
359 401 sqlalchemy.db1.convert_unicode = true
360 402
361 403 ################################
362 404 ### LOGGING CONFIGURATION ####
363 405 ################################
364 406 [loggers]
365 407 keys = root, routes, rhodecode, sqlalchemy, beaker, templates, whoosh_indexer
366 408
367 409 [handlers]
368 410 keys = console, console_sql
369 411
370 412 [formatters]
371 413 keys = generic, color_formatter, color_formatter_sql
372 414
373 415 #############
374 416 ## LOGGERS ##
375 417 #############
376 418 [logger_root]
377 419 level = NOTSET
378 420 handlers = console
379 421
380 422 [logger_routes]
381 423 level = DEBUG
382 424 handlers =
383 425 qualname = routes.middleware
384 # "level = DEBUG" logs the route matched and routing variables.
426 ## "level = DEBUG" logs the route matched and routing variables.
385 427 propagate = 1
386 428
387 429 [logger_beaker]
388 430 level = DEBUG
389 431 handlers =
390 432 qualname = beaker.container
391 433 propagate = 1
392 434
393 435 [logger_templates]
394 436 level = INFO
395 437 handlers =
396 438 qualname = pylons.templating
397 439 propagate = 1
398 440
399 441 [logger_rhodecode]
400 442 level = DEBUG
401 443 handlers =
402 444 qualname = rhodecode
403 445 propagate = 1
404 446
405 447 [logger_sqlalchemy]
406 448 level = INFO
407 449 handlers = console_sql
408 450 qualname = sqlalchemy.engine
409 451 propagate = 0
410 452
411 453 [logger_whoosh_indexer]
412 454 level = DEBUG
413 455 handlers =
414 456 qualname = whoosh_indexer
415 457 propagate = 1
416 458
417 459 ##############
418 460 ## HANDLERS ##
419 461 ##############
420 462
421 463 [handler_console]
422 464 class = StreamHandler
423 465 args = (sys.stderr,)
424 466 level = INFO
425 467 formatter = generic
426 468
427 469 [handler_console_sql]
428 470 class = StreamHandler
429 471 args = (sys.stderr,)
430 472 level = WARN
431 473 formatter = generic
432 474
433 475 ################
434 476 ## FORMATTERS ##
435 477 ################
436 478
437 479 [formatter_generic]
438 480 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
439 481 datefmt = %Y-%m-%d %H:%M:%S
440 482
441 483 [formatter_color_formatter]
442 484 class=rhodecode.lib.colored_formatter.ColorFormatter
443 485 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
444 486 datefmt = %Y-%m-%d %H:%M:%S
445 487
446 488 [formatter_color_formatter_sql]
447 489 class=rhodecode.lib.colored_formatter.ColorFormatterSql
448 490 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
449 491 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,109 +1,116 b''
1 1 """Pylons environment configuration"""
2 2
3 3 import os
4 4 import logging
5 5 import rhodecode
6 6
7 7 from mako.lookup import TemplateLookup
8 8 from pylons.configuration import PylonsConfig
9 9 from pylons.error import handle_mako_error
10 10
11 11 # don't remove this import it does magic for celery
12 12 from rhodecode.lib import celerypylons
13 13
14 14 import rhodecode.lib.app_globals as app_globals
15 15
16 16 from rhodecode.config.routing import make_map
17 17
18 18 from rhodecode.lib import helpers
19 19 from rhodecode.lib.auth import set_available_permissions
20 20 from rhodecode.lib.utils import repo2db_mapper, make_ui, set_rhodecode_config,\
21 21 load_rcextensions, check_git_version
22 22 from rhodecode.lib.utils2 import engine_from_config, str2bool
23 from rhodecode.lib.db_manage import DbManage
23 24 from rhodecode.model import init_model
24 25 from rhodecode.model.scm import ScmModel
25 26
26 27 log = logging.getLogger(__name__)
27 28
28 29
29 30 def load_environment(global_conf, app_conf, initial=False):
30 31 """
31 32 Configure the Pylons environment via the ``pylons.config``
32 33 object
33 34 """
34 35 config = PylonsConfig()
35 36
36 37 # Pylons paths
37 38 root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
38 39 paths = dict(
39 40 root=root,
40 41 controllers=os.path.join(root, 'controllers'),
41 42 static_files=os.path.join(root, 'public'),
42 43 templates=[os.path.join(root, 'templates')]
43 44 )
44 45
45 46 # Initialize config with the basic options
46 47 config.init_app(global_conf, app_conf, package='rhodecode', paths=paths)
47 48
48 49 # store some globals into rhodecode
49 50 rhodecode.CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
50 51 rhodecode.CELERY_EAGER = str2bool(config['app_conf'].get('celery.always.eager'))
51 52
52 53 config['routes.map'] = make_map(config)
53 54 config['pylons.app_globals'] = app_globals.Globals(config)
54 55 config['pylons.h'] = helpers
55 56 rhodecode.CONFIG = config
56 57
57 58 load_rcextensions(root_path=config['here'])
58 59
59 60 # Setup cache object as early as possible
60 61 import pylons
61 62 pylons.cache._push_object(config['pylons.app_globals'].cache)
62 63
63 64 # Create the Mako TemplateLookup, with the default auto-escaping
64 65 config['pylons.app_globals'].mako_lookup = TemplateLookup(
65 66 directories=paths['templates'],
66 67 error_handler=handle_mako_error,
67 68 module_directory=os.path.join(app_conf['cache_dir'], 'templates'),
68 69 input_encoding='utf-8', default_filters=['escape'],
69 70 imports=['from webhelpers.html import escape'])
70 71
71 72 # sets the c attribute access when don't existing attribute are accessed
72 73 config['pylons.strict_tmpl_context'] = True
73 74 test = os.path.split(config['__file__'])[-1] == 'test.ini'
74 75 if test:
75 76 if os.environ.get('TEST_DB'):
76 77 # swap config if we pass enviroment variable
77 78 config['sqlalchemy.db1.url'] = os.environ.get('TEST_DB')
78 79
79 80 from rhodecode.lib.utils import create_test_env, create_test_index
80 81 from rhodecode.tests import TESTS_TMP_PATH
81 82 # set RC_NO_TMP_PATH=1 to disable re-creating the database and
82 83 # test repos
83 84 if not int(os.environ.get('RC_NO_TMP_PATH', 0)):
84 85 create_test_env(TESTS_TMP_PATH, config)
85 86 # set RC_WHOOSH_TEST_DISABLE=1 to disable whoosh index during tests
86 87 if not int(os.environ.get('RC_WHOOSH_TEST_DISABLE', 0)):
87 88 create_test_index(TESTS_TMP_PATH, config, True)
88 89
89 90 #check git version
90 91 check_git_version()
91
92 DbManage.check_waitress()
92 93 # MULTIPLE DB configs
93 94 # Setup the SQLAlchemy database engine
94 95 sa_engine_db1 = engine_from_config(config, 'sqlalchemy.db1.')
95 96 init_model(sa_engine_db1)
96 97
97 98 repos_path = make_ui('db').configitems('paths')[0][1]
98 99 repo2db_mapper(ScmModel().repo_scan(repos_path),
99 100 remove_obsolete=False, install_git_hook=False)
100 101 set_available_permissions(config)
101 102 config['base_path'] = repos_path
102 103 set_rhodecode_config(config)
104
105 instance_id = rhodecode.CONFIG.get('instance_id')
106 if instance_id == '*':
107 instance_id = '%s-%s' % (os.uname()[1], os.getpid())
108 rhodecode.CONFIG['instance_id'] = instance_id
109
103 110 # CONFIGURATION OPTIONS HERE (note: all config options will override
104 111 # any Pylons config options)
105 112
106 113 # store config reference into our module to skip import magic of
107 114 # pylons
108 115 rhodecode.CONFIG.update(config)
109 116 return config
@@ -1,92 +1,93 b''
1 1 """Pylons middleware initialization"""
2 2
3 3 from beaker.middleware import SessionMiddleware
4 4 from routes.middleware import RoutesMiddleware
5 5 from paste.cascade import Cascade
6 6 from paste.registry import RegistryManager
7 7 from paste.urlparser import StaticURLParser
8 8 from paste.deploy.converters import asbool
9 9 from paste.gzipper import make_gzip_middleware
10 10
11 11 from pylons.middleware import ErrorHandler, StatusCodeRedirect
12 12 from pylons.wsgiapp import PylonsApp
13 13
14 14 from rhodecode.lib.middleware.simplehg import SimpleHg
15 15 from rhodecode.lib.middleware.simplegit import SimpleGit
16 16 from rhodecode.lib.middleware.https_fixup import HttpsFixup
17 17 from rhodecode.config.environment import load_environment
18 from rhodecode.lib.middleware.wrapper import RequestWrapper
18 19
19 20
20 21 def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
21 22 """Create a Pylons WSGI application and return it
22 23
23 24 ``global_conf``
24 25 The inherited configuration for this application. Normally from
25 26 the [DEFAULT] section of the Paste ini file.
26 27
27 28 ``full_stack``
28 29 Whether or not this application provides a full WSGI stack (by
29 30 default, meaning it handles its own exceptions and errors).
30 31 Disable full_stack when this application is "managed" by
31 32 another WSGI middleware.
32 33
33 34 ``app_conf``
34 35 The application's local configuration. Normally specified in
35 36 the [app:<name>] section of the Paste ini file (where <name>
36 37 defaults to main).
37 38
38 39 """
39 40 # Configure the Pylons environment
40 41 config = load_environment(global_conf, app_conf)
41 42
42 43 # The Pylons WSGI app
43 44 app = PylonsApp(config=config)
44 45
45 46 # Routing/Session/Cache Middleware
46 47 app = RoutesMiddleware(app, config['routes.map'])
47 48 app = SessionMiddleware(app, config)
48 49
49 50 # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
50 51 if asbool(config['pdebug']):
51 52 from rhodecode.lib.profiler import ProfilingMiddleware
52 53 app = ProfilingMiddleware(app)
53 54
54 55 if asbool(full_stack):
55 56
56 57 from rhodecode.lib.middleware.sentry import Sentry
57 58 from rhodecode.lib.middleware.errormator import Errormator
58 if Errormator:
59 if Errormator and asbool(config['app_conf'].get('errormator')):
59 60 app = Errormator(app, config)
60 61 elif Sentry:
61 62 app = Sentry(app, config)
62 63
63 64 # Handle Python exceptions
64 65 app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
65 66
66 67 # we want our low level middleware to get to the request ASAP. We don't
67 68 # need any pylons stack middleware in them
68 69 app = SimpleHg(app, config)
69 70 app = SimpleGit(app, config)
70
71 app = RequestWrapper(app, config)
71 72 # Display error documents for 401, 403, 404 status codes (and
72 73 # 500 when debug is disabled)
73 74 if asbool(config['debug']):
74 75 app = StatusCodeRedirect(app)
75 76 else:
76 77 app = StatusCodeRedirect(app, [400, 401, 403, 404, 500])
77 78
78 79 #enable https redirets based on HTTP_X_URL_SCHEME set by proxy
79 80 app = HttpsFixup(app, config)
80 81
81 82 # Establish the Registry for this application
82 83 app = RegistryManager(app)
83 84
84 85 if asbool(static_files):
85 86 # Serve static files
86 87 static_app = StaticURLParser(config['pylons.paths']['static_files'])
87 88 app = Cascade([static_app, app])
88 89 app = make_gzip_middleware(app, global_conf, compress_level=1)
89 90
90 91 app.config = config
91 92
92 93 return app
@@ -1,118 +1,126 b''
1 1 # Additional mappings that are not present in the pygments lexers
2 2 # used for building stats
3 3 # format is {'ext':['Names']} eg. {'py':['Python']} note: there can be
4 4 # more than one name for extension
5 5 # NOTE: that this will overide any mappings in LANGUAGES_EXTENSIONS_MAP
6 6 # build by pygments
7 7 EXTRA_MAPPINGS = {}
8 8
9 # additional lexer definitions for custom files
10 # it's overrides pygments lexers, and uses defined name of lexer to colorize the
11 # files. Format is {'ext': 'lexer_name'}
12 # List of lexers can be printed running:
13 # python -c "import pprint;from pygments import lexers;pprint.pprint([(x[0], x[1]) for x in lexers.get_all_lexers()]);"
14
15 EXTRA_LEXERS = {}
16
9 17 #==============================================================================
10 18 # WHOOSH INDEX EXTENSIONS
11 19 #==============================================================================
12 20 # if INDEX_EXTENSIONS is [] it'll use pygments lexers extensions by default.
13 21 # To set your own just add to this list extensions to index with content
14 22 INDEX_EXTENSIONS = []
15 23
16 24 # additional extensions for indexing besides the default from pygments
17 25 # those get's added to INDEX_EXTENSIONS
18 26 EXTRA_INDEX_EXTENSIONS = []
19 27
20 28
21 29 #==============================================================================
22 30 # POST CREATE REPOSITORY HOOK
23 31 #==============================================================================
24 32 # this function will be executed after each repository is created
25 33 def _crhook(*args, **kwargs):
26 34 """
27 35 Post create repository HOOK
28 36 kwargs available:
29 37 :param repo_name:
30 38 :param repo_type:
31 39 :param description:
32 40 :param private:
33 41 :param created_on:
34 42 :param enable_downloads:
35 43 :param repo_id:
36 44 :param user_id:
37 45 :param enable_statistics:
38 46 :param clone_uri:
39 47 :param fork_id:
40 48 :param group_id:
41 49 :param created_by:
42 50 """
43 51 return 0
44 52 CREATE_REPO_HOOK = _crhook
45 53
46 54
47 55 #==============================================================================
48 56 # POST DELETE REPOSITORY HOOK
49 57 #==============================================================================
50 58 # this function will be executed after each repository deletion
51 59 def _dlhook(*args, **kwargs):
52 60 """
53 61 Post create repository HOOK
54 62 kwargs available:
55 63 :param repo_name:
56 64 :param repo_type:
57 65 :param description:
58 66 :param private:
59 67 :param created_on:
60 68 :param enable_downloads:
61 69 :param repo_id:
62 70 :param user_id:
63 71 :param enable_statistics:
64 72 :param clone_uri:
65 73 :param fork_id:
66 74 :param group_id:
67 75 :param deleted_by:
68 76 :param deleted_on:
69 77 """
70 78 return 0
71 79 DELETE_REPO_HOOK = _dlhook
72 80
73 81
74 82 #==============================================================================
75 83 # POST PUSH HOOK
76 84 #==============================================================================
77 85
78 86 # this function will be executed after each push it's executed after the
79 87 # build-in hook that RhodeCode uses for logging pushes
80 88 def _pushhook(*args, **kwargs):
81 89 """
82 90 Post push hook
83 91 kwargs available:
84 92
85 93 :param server_url: url of instance that triggered this hook
86 94 :param config: path to .ini config used
87 95 :param scm: type of VS 'git' or 'hg'
88 96 :param username: name of user who pushed
89 97 :param ip: ip of who pushed
90 98 :param action: push
91 99 :param repository: repository name
92 100 :param pushed_revs: list of pushed revisions
93 101 """
94 102 return 0
95 103 PUSH_HOOK = _pushhook
96 104
97 105
98 106 #==============================================================================
99 107 # POST PULL HOOK
100 108 #==============================================================================
101 109
102 110 # this function will be executed after each push it's executed after the
103 111 # build-in hook that RhodeCode uses for logging pulls
104 112 def _pullhook(*args, **kwargs):
105 113 """
106 114 Post pull hook
107 115 kwargs available::
108 116
109 117 :param server_url: url of instance that triggered this hook
110 118 :param config: path to .ini config used
111 119 :param scm: type of VS 'git' or 'hg'
112 120 :param username: name of user who pulled
113 121 :param ip: ip of who pulled
114 122 :param action: pull
115 123 :param repository: repository name
116 124 """
117 125 return 0
118 126 PULL_HOOK = _pullhook
@@ -1,626 +1,656 b''
1 1 """
2 2 Routes configuration
3 3
4 4 The more specific and detailed routes should be defined first so they
5 5 may take precedent over the more generic routes. For more information
6 6 refer to the routes manual at http://routes.groovie.org/docs/
7 7 """
8 8 from __future__ import with_statement
9 9 from routes import Mapper
10 10
11 11 # prefix for non repository related links needs to be prefixed with `/`
12 12 ADMIN_PREFIX = '/_admin'
13 13
14 14
15 15 def make_map(config):
16 16 """Create, configure and return the routes Mapper"""
17 17 rmap = Mapper(directory=config['pylons.paths']['controllers'],
18 18 always_scan=config['debug'])
19 19 rmap.minimization = False
20 20 rmap.explicit = False
21 21
22 22 from rhodecode.lib.utils import is_valid_repo
23 23 from rhodecode.lib.utils import is_valid_repos_group
24 24
25 25 def check_repo(environ, match_dict):
26 26 """
27 27 check for valid repository for proper 404 handling
28 28
29 29 :param environ:
30 30 :param match_dict:
31 31 """
32 32 from rhodecode.model.db import Repository
33 33 repo_name = match_dict.get('repo_name')
34 34
35 35 if match_dict.get('f_path'):
36 36 #fix for multiple initial slashes that causes errors
37 37 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
38 38
39 39 try:
40 40 by_id = repo_name.split('_')
41 41 if len(by_id) == 2 and by_id[1].isdigit() and by_id[0] == '':
42 42 repo_name = Repository.get(by_id[1]).repo_name
43 43 match_dict['repo_name'] = repo_name
44 except:
44 except Exception:
45 45 pass
46 46
47 47 return is_valid_repo(repo_name, config['base_path'])
48 48
49 49 def check_group(environ, match_dict):
50 50 """
51 check for valid repositories group for proper 404 handling
51 check for valid repository group for proper 404 handling
52 52
53 53 :param environ:
54 54 :param match_dict:
55 55 """
56 56 repos_group_name = match_dict.get('group_name')
57 return is_valid_repos_group(repos_group_name, config['base_path'])
57 58
58 return is_valid_repos_group(repos_group_name, config['base_path'])
59 def check_group_skip_path(environ, match_dict):
60 """
61 check for valid repository group for proper 404 handling, but skips
62 verification of existing path
63
64 :param environ:
65 :param match_dict:
66 """
67 repos_group_name = match_dict.get('group_name')
68 return is_valid_repos_group(repos_group_name, config['base_path'],
69 skip_path_check=True)
59 70
60 71 def check_int(environ, match_dict):
61 72 return match_dict.get('id').isdigit()
62 73
63 74 # The ErrorController route (handles 404/500 error pages); it should
64 75 # likely stay at the top, ensuring it can always be resolved
65 76 rmap.connect('/error/{action}', controller='error')
66 77 rmap.connect('/error/{action}/{id}', controller='error')
67 78
68 79 #==========================================================================
69 80 # CUSTOM ROUTES HERE
70 81 #==========================================================================
71 82
72 83 #MAIN PAGE
73 84 rmap.connect('home', '/', controller='home', action='index')
74 85 rmap.connect('repo_switcher', '/repos', controller='home',
75 86 action='repo_switcher')
76 87 rmap.connect('branch_tag_switcher', '/branches-tags/{repo_name:.*?}',
77 88 controller='home', action='branch_tag_switcher')
78 89 rmap.connect('bugtracker',
79 90 "http://bitbucket.org/marcinkuzminski/rhodecode/issues",
80 91 _static=True)
81 92 rmap.connect('rst_help',
82 93 "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
83 94 _static=True)
84 95 rmap.connect('rhodecode_official', "http://rhodecode.org", _static=True)
85 96
86 97 #ADMIN REPOSITORY REST ROUTES
87 98 with rmap.submapper(path_prefix=ADMIN_PREFIX,
88 99 controller='admin/repos') as m:
89 100 m.connect("repos", "/repos",
90 101 action="create", conditions=dict(method=["POST"]))
91 102 m.connect("repos", "/repos",
92 103 action="index", conditions=dict(method=["GET"]))
93 104 m.connect("formatted_repos", "/repos.{format}",
94 105 action="index",
95 106 conditions=dict(method=["GET"]))
96 m.connect("new_repo", "/repos/new",
97 action="new", conditions=dict(method=["GET"]))
98 m.connect("formatted_new_repo", "/repos/new.{format}",
99 action="new", conditions=dict(method=["GET"]))
107 m.connect("new_repo", "/create_repository",
108 action="create_repository", conditions=dict(method=["GET"]))
100 109 m.connect("/repos/{repo_name:.*?}",
101 110 action="update", conditions=dict(method=["PUT"],
102 111 function=check_repo))
103 112 m.connect("/repos/{repo_name:.*?}",
104 113 action="delete", conditions=dict(method=["DELETE"],
105 114 function=check_repo))
106 m.connect("edit_repo", "/repos/{repo_name:.*?}/edit",
107 action="edit", conditions=dict(method=["GET"],
108 function=check_repo))
109 115 m.connect("formatted_edit_repo", "/repos/{repo_name:.*?}.{format}/edit",
110 116 action="edit", conditions=dict(method=["GET"],
111 117 function=check_repo))
112 118 m.connect("repo", "/repos/{repo_name:.*?}",
113 119 action="show", conditions=dict(method=["GET"],
114 120 function=check_repo))
115 121 m.connect("formatted_repo", "/repos/{repo_name:.*?}.{format}",
116 122 action="show", conditions=dict(method=["GET"],
117 123 function=check_repo))
124 #add repo perm member
125 m.connect('set_repo_perm_member', "/set_repo_perm_member/{repo_name:.*?}",
126 action="set_repo_perm_member",
127 conditions=dict(method=["POST"], function=check_repo))
128
118 129 #ajax delete repo perm user
119 130 m.connect('delete_repo_user', "/repos_delete_user/{repo_name:.*?}",
120 131 action="delete_perm_user",
121 132 conditions=dict(method=["DELETE"], function=check_repo))
122 133
123 134 #ajax delete repo perm users_group
124 135 m.connect('delete_repo_users_group',
125 136 "/repos_delete_users_group/{repo_name:.*?}",
126 137 action="delete_perm_users_group",
127 138 conditions=dict(method=["DELETE"], function=check_repo))
128 139
129 140 #settings actions
130 141 m.connect('repo_stats', "/repos_stats/{repo_name:.*?}",
131 142 action="repo_stats", conditions=dict(method=["DELETE"],
132 143 function=check_repo))
133 144 m.connect('repo_cache', "/repos_cache/{repo_name:.*?}",
134 145 action="repo_cache", conditions=dict(method=["DELETE"],
135 146 function=check_repo))
136 147 m.connect('repo_public_journal', "/repos_public_journal/{repo_name:.*?}",
137 148 action="repo_public_journal", conditions=dict(method=["PUT"],
138 149 function=check_repo))
139 150 m.connect('repo_pull', "/repo_pull/{repo_name:.*?}",
140 151 action="repo_pull", conditions=dict(method=["PUT"],
141 152 function=check_repo))
142 153 m.connect('repo_as_fork', "/repo_as_fork/{repo_name:.*?}",
143 154 action="repo_as_fork", conditions=dict(method=["PUT"],
144 155 function=check_repo))
145 156 m.connect('repo_locking', "/repo_locking/{repo_name:.*?}",
146 157 action="repo_locking", conditions=dict(method=["PUT"],
147 158 function=check_repo))
159 m.connect('toggle_locking', "/locking_toggle/{repo_name:.*?}",
160 action="toggle_locking", conditions=dict(method=["GET"],
161 function=check_repo))
162
163 #repo fields
164 m.connect('create_repo_fields', "/repo_fields/{repo_name:.*?}/new",
165 action="create_repo_field", conditions=dict(method=["PUT"],
166 function=check_repo))
167
168 m.connect('delete_repo_fields', "/repo_fields/{repo_name:.*?}/{field_id}",
169 action="delete_repo_field", conditions=dict(method=["DELETE"],
170 function=check_repo))
148 171
149 172 with rmap.submapper(path_prefix=ADMIN_PREFIX,
150 173 controller='admin/repos_groups') as m:
151 174 m.connect("repos_groups", "/repos_groups",
152 175 action="create", conditions=dict(method=["POST"]))
153 176 m.connect("repos_groups", "/repos_groups",
154 177 action="index", conditions=dict(method=["GET"]))
155 178 m.connect("formatted_repos_groups", "/repos_groups.{format}",
156 179 action="index", conditions=dict(method=["GET"]))
157 180 m.connect("new_repos_group", "/repos_groups/new",
158 181 action="new", conditions=dict(method=["GET"]))
159 182 m.connect("formatted_new_repos_group", "/repos_groups/new.{format}",
160 183 action="new", conditions=dict(method=["GET"]))
161 m.connect("update_repos_group", "/repos_groups/{id}",
184 m.connect("update_repos_group", "/repos_groups/{group_name:.*?}",
162 185 action="update", conditions=dict(method=["PUT"],
163 function=check_int))
164 m.connect("delete_repos_group", "/repos_groups/{id}",
186 function=check_group))
187 m.connect("delete_repos_group", "/repos_groups/{group_name:.*?}",
165 188 action="delete", conditions=dict(method=["DELETE"],
166 function=check_int))
167 m.connect("edit_repos_group", "/repos_groups/{id:.*?}/edit",
168 action="edit", conditions=dict(method=["GET"],))
169 m.connect("formatted_edit_repos_group",
170 "/repos_groups/{id}.{format}/edit",
189 function=check_group_skip_path))
190 m.connect("edit_repos_group", "/repos_groups/{group_name:.*?}/edit",
171 191 action="edit", conditions=dict(method=["GET"],
172 function=check_int))
173 m.connect("repos_group", "/repos_groups/{id}",
192 function=check_group))
193 m.connect("formatted_edit_repos_group",
194 "/repos_groups/{group_name:.*?}.{format}/edit",
195 action="edit", conditions=dict(method=["GET"],
196 function=check_group))
197 m.connect("repos_group", "/repos_groups/{group_name:.*?}",
174 198 action="show", conditions=dict(method=["GET"],
175 function=check_int))
176 m.connect("formatted_repos_group", "/repos_groups/{id}.{format}",
199 function=check_group))
200 m.connect("formatted_repos_group", "/repos_groups/{group_name:.*?}.{format}",
177 201 action="show", conditions=dict(method=["GET"],
178 function=check_int))
179 # ajax delete repos group perm user
202 function=check_group))
203 # ajax delete repository group perm user
180 204 m.connect('delete_repos_group_user_perm',
181 "/delete_repos_group_user_perm/{group_name:.*}",
205 "/delete_repos_group_user_perm/{group_name:.*?}",
182 206 action="delete_repos_group_user_perm",
183 207 conditions=dict(method=["DELETE"], function=check_group))
184 208
185 # ajax delete repos group perm users_group
209 # ajax delete repository group perm users_group
186 210 m.connect('delete_repos_group_users_group_perm',
187 "/delete_repos_group_users_group_perm/{group_name:.*}",
211 "/delete_repos_group_users_group_perm/{group_name:.*?}",
188 212 action="delete_repos_group_users_group_perm",
189 213 conditions=dict(method=["DELETE"], function=check_group))
190 214
191 215 #ADMIN USER REST ROUTES
192 216 with rmap.submapper(path_prefix=ADMIN_PREFIX,
193 217 controller='admin/users') as m:
194 218 m.connect("users", "/users",
195 219 action="create", conditions=dict(method=["POST"]))
196 220 m.connect("users", "/users",
197 221 action="index", conditions=dict(method=["GET"]))
198 222 m.connect("formatted_users", "/users.{format}",
199 223 action="index", conditions=dict(method=["GET"]))
200 224 m.connect("new_user", "/users/new",
201 225 action="new", conditions=dict(method=["GET"]))
202 226 m.connect("formatted_new_user", "/users/new.{format}",
203 227 action="new", conditions=dict(method=["GET"]))
204 228 m.connect("update_user", "/users/{id}",
205 229 action="update", conditions=dict(method=["PUT"]))
206 230 m.connect("delete_user", "/users/{id}",
207 231 action="delete", conditions=dict(method=["DELETE"]))
208 232 m.connect("edit_user", "/users/{id}/edit",
209 233 action="edit", conditions=dict(method=["GET"]))
210 234 m.connect("formatted_edit_user",
211 235 "/users/{id}.{format}/edit",
212 236 action="edit", conditions=dict(method=["GET"]))
213 237 m.connect("user", "/users/{id}",
214 238 action="show", conditions=dict(method=["GET"]))
215 239 m.connect("formatted_user", "/users/{id}.{format}",
216 240 action="show", conditions=dict(method=["GET"]))
217 241
218 242 #EXTRAS USER ROUTES
219 243 m.connect("user_perm", "/users_perm/{id}",
220 244 action="update_perm", conditions=dict(method=["PUT"]))
221 245 m.connect("user_emails", "/users_emails/{id}",
222 246 action="add_email", conditions=dict(method=["PUT"]))
223 247 m.connect("user_emails_delete", "/users_emails/{id}",
224 248 action="delete_email", conditions=dict(method=["DELETE"]))
225 249 m.connect("user_ips", "/users_ips/{id}",
226 250 action="add_ip", conditions=dict(method=["PUT"]))
227 251 m.connect("user_ips_delete", "/users_ips/{id}",
228 252 action="delete_ip", conditions=dict(method=["DELETE"]))
229 253
230 #ADMIN USERS GROUPS REST ROUTES
254 #ADMIN USER GROUPS REST ROUTES
231 255 with rmap.submapper(path_prefix=ADMIN_PREFIX,
232 256 controller='admin/users_groups') as m:
233 257 m.connect("users_groups", "/users_groups",
234 258 action="create", conditions=dict(method=["POST"]))
235 259 m.connect("users_groups", "/users_groups",
236 260 action="index", conditions=dict(method=["GET"]))
237 261 m.connect("formatted_users_groups", "/users_groups.{format}",
238 262 action="index", conditions=dict(method=["GET"]))
239 263 m.connect("new_users_group", "/users_groups/new",
240 264 action="new", conditions=dict(method=["GET"]))
241 265 m.connect("formatted_new_users_group", "/users_groups/new.{format}",
242 266 action="new", conditions=dict(method=["GET"]))
243 267 m.connect("update_users_group", "/users_groups/{id}",
244 268 action="update", conditions=dict(method=["PUT"]))
245 269 m.connect("delete_users_group", "/users_groups/{id}",
246 270 action="delete", conditions=dict(method=["DELETE"]))
247 271 m.connect("edit_users_group", "/users_groups/{id}/edit",
248 272 action="edit", conditions=dict(method=["GET"]))
249 273 m.connect("formatted_edit_users_group",
250 274 "/users_groups/{id}.{format}/edit",
251 275 action="edit", conditions=dict(method=["GET"]))
252 276 m.connect("users_group", "/users_groups/{id}",
253 277 action="show", conditions=dict(method=["GET"]))
254 278 m.connect("formatted_users_group", "/users_groups/{id}.{format}",
255 279 action="show", conditions=dict(method=["GET"]))
256 280
257 281 #EXTRAS USER ROUTES
258 282 m.connect("users_group_perm", "/users_groups_perm/{id}",
259 283 action="update_perm", conditions=dict(method=["PUT"]))
260 284
261 285 #ADMIN GROUP REST ROUTES
262 286 rmap.resource('group', 'groups',
263 287 controller='admin/groups', path_prefix=ADMIN_PREFIX)
264 288
265 289 #ADMIN PERMISSIONS REST ROUTES
266 290 rmap.resource('permission', 'permissions',
267 291 controller='admin/permissions', path_prefix=ADMIN_PREFIX)
268 292
269 293 #ADMIN DEFAULTS REST ROUTES
270 294 rmap.resource('default', 'defaults',
271 295 controller='admin/defaults', path_prefix=ADMIN_PREFIX)
272 296
273 297 ##ADMIN LDAP SETTINGS
274 298 rmap.connect('ldap_settings', '%s/ldap' % ADMIN_PREFIX,
275 299 controller='admin/ldap_settings', action='ldap_settings',
276 300 conditions=dict(method=["POST"]))
277 301
278 302 rmap.connect('ldap_home', '%s/ldap' % ADMIN_PREFIX,
279 303 controller='admin/ldap_settings')
280 304
281 305 #ADMIN SETTINGS REST ROUTES
282 306 with rmap.submapper(path_prefix=ADMIN_PREFIX,
283 307 controller='admin/settings') as m:
284 308 m.connect("admin_settings", "/settings",
285 309 action="create", conditions=dict(method=["POST"]))
286 310 m.connect("admin_settings", "/settings",
287 311 action="index", conditions=dict(method=["GET"]))
288 312 m.connect("formatted_admin_settings", "/settings.{format}",
289 313 action="index", conditions=dict(method=["GET"]))
290 314 m.connect("admin_new_setting", "/settings/new",
291 315 action="new", conditions=dict(method=["GET"]))
292 316 m.connect("formatted_admin_new_setting", "/settings/new.{format}",
293 317 action="new", conditions=dict(method=["GET"]))
294 318 m.connect("/settings/{setting_id}",
295 319 action="update", conditions=dict(method=["PUT"]))
296 320 m.connect("/settings/{setting_id}",
297 321 action="delete", conditions=dict(method=["DELETE"]))
298 322 m.connect("admin_edit_setting", "/settings/{setting_id}/edit",
299 323 action="edit", conditions=dict(method=["GET"]))
300 324 m.connect("formatted_admin_edit_setting",
301 325 "/settings/{setting_id}.{format}/edit",
302 326 action="edit", conditions=dict(method=["GET"]))
303 327 m.connect("admin_setting", "/settings/{setting_id}",
304 328 action="show", conditions=dict(method=["GET"]))
305 329 m.connect("formatted_admin_setting", "/settings/{setting_id}.{format}",
306 330 action="show", conditions=dict(method=["GET"]))
307 331 m.connect("admin_settings_my_account", "/my_account",
308 332 action="my_account", conditions=dict(method=["GET"]))
309 333 m.connect("admin_settings_my_account_update", "/my_account_update",
310 334 action="my_account_update", conditions=dict(method=["PUT"]))
311 m.connect("admin_settings_create_repository", "/create_repository",
312 action="create_repository", conditions=dict(method=["GET"]))
313 335 m.connect("admin_settings_my_repos", "/my_account/repos",
314 336 action="my_account_my_repos", conditions=dict(method=["GET"]))
315 337 m.connect("admin_settings_my_pullrequests", "/my_account/pull_requests",
316 338 action="my_account_my_pullrequests", conditions=dict(method=["GET"]))
317 339
318 340 #NOTIFICATION REST ROUTES
319 341 with rmap.submapper(path_prefix=ADMIN_PREFIX,
320 342 controller='admin/notifications') as m:
321 343 m.connect("notifications", "/notifications",
322 344 action="create", conditions=dict(method=["POST"]))
323 345 m.connect("notifications", "/notifications",
324 346 action="index", conditions=dict(method=["GET"]))
325 347 m.connect("notifications_mark_all_read", "/notifications/mark_all_read",
326 348 action="mark_all_read", conditions=dict(method=["GET"]))
327 349 m.connect("formatted_notifications", "/notifications.{format}",
328 350 action="index", conditions=dict(method=["GET"]))
329 351 m.connect("new_notification", "/notifications/new",
330 352 action="new", conditions=dict(method=["GET"]))
331 353 m.connect("formatted_new_notification", "/notifications/new.{format}",
332 354 action="new", conditions=dict(method=["GET"]))
333 355 m.connect("/notification/{notification_id}",
334 356 action="update", conditions=dict(method=["PUT"]))
335 357 m.connect("/notification/{notification_id}",
336 358 action="delete", conditions=dict(method=["DELETE"]))
337 359 m.connect("edit_notification", "/notification/{notification_id}/edit",
338 360 action="edit", conditions=dict(method=["GET"]))
339 361 m.connect("formatted_edit_notification",
340 362 "/notification/{notification_id}.{format}/edit",
341 363 action="edit", conditions=dict(method=["GET"]))
342 364 m.connect("notification", "/notification/{notification_id}",
343 365 action="show", conditions=dict(method=["GET"]))
344 366 m.connect("formatted_notification", "/notifications/{notification_id}.{format}",
345 367 action="show", conditions=dict(method=["GET"]))
346 368
347 369 #ADMIN MAIN PAGES
348 370 with rmap.submapper(path_prefix=ADMIN_PREFIX,
349 371 controller='admin/admin') as m:
350 372 m.connect('admin_home', '', action='index')
351 373 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
352 374 action='add_repo')
353 375
354 376 #==========================================================================
355 377 # API V2
356 378 #==========================================================================
357 379 with rmap.submapper(path_prefix=ADMIN_PREFIX,
358 380 controller='api/api') as m:
359 381 m.connect('api', '/api')
360 382
361 383 #USER JOURNAL
362 384 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX,
363 385 controller='journal', action='index')
364 386 rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX,
365 387 controller='journal', action='journal_rss')
366 388 rmap.connect('journal_atom', '%s/journal/atom' % ADMIN_PREFIX,
367 389 controller='journal', action='journal_atom')
368 390
369 391 rmap.connect('public_journal', '%s/public_journal' % ADMIN_PREFIX,
370 392 controller='journal', action="public_journal")
371 393
372 394 rmap.connect('public_journal_rss', '%s/public_journal/rss' % ADMIN_PREFIX,
373 395 controller='journal', action="public_journal_rss")
374 396
375 397 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % ADMIN_PREFIX,
376 398 controller='journal', action="public_journal_rss")
377 399
378 400 rmap.connect('public_journal_atom',
379 401 '%s/public_journal/atom' % ADMIN_PREFIX, controller='journal',
380 402 action="public_journal_atom")
381 403
382 404 rmap.connect('public_journal_atom_old',
383 405 '%s/public_journal_atom' % ADMIN_PREFIX, controller='journal',
384 406 action="public_journal_atom")
385 407
386 408 rmap.connect('toggle_following', '%s/toggle_following' % ADMIN_PREFIX,
387 409 controller='journal', action='toggle_following',
388 410 conditions=dict(method=["POST"]))
389 411
390 412 #SEARCH
391 413 rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
392 rmap.connect('search_repo', '%s/search/{search_repo:.*}' % ADMIN_PREFIX,
393 controller='search')
414 rmap.connect('search_repo_admin', '%s/search/{repo_name:.*}' % ADMIN_PREFIX,
415 controller='search',
416 conditions=dict(function=check_repo))
417 rmap.connect('search_repo', '/{repo_name:.*?}/search',
418 controller='search',
419 conditions=dict(function=check_repo),
420 )
394 421
395 422 #LOGIN/LOGOUT/REGISTER/SIGN IN
396 423 rmap.connect('login_home', '%s/login' % ADMIN_PREFIX, controller='login')
397 424 rmap.connect('logout_home', '%s/logout' % ADMIN_PREFIX, controller='login',
398 425 action='logout')
399 426
400 427 rmap.connect('register', '%s/register' % ADMIN_PREFIX, controller='login',
401 428 action='register')
402 429
403 430 rmap.connect('reset_password', '%s/password_reset' % ADMIN_PREFIX,
404 431 controller='login', action='password_reset')
405 432
406 433 rmap.connect('reset_password_confirmation',
407 434 '%s/password_reset_confirmation' % ADMIN_PREFIX,
408 435 controller='login', action='password_reset_confirmation')
409 436
410 437 #FEEDS
411 438 rmap.connect('rss_feed_home', '/{repo_name:.*?}/feed/rss',
412 439 controller='feed', action='rss',
413 440 conditions=dict(function=check_repo))
414 441
415 442 rmap.connect('atom_feed_home', '/{repo_name:.*?}/feed/atom',
416 443 controller='feed', action='atom',
417 444 conditions=dict(function=check_repo))
418 445
419 446 #==========================================================================
420 447 # REPOSITORY ROUTES
421 448 #==========================================================================
422 449 rmap.connect('summary_home', '/{repo_name:.*?}',
423 450 controller='summary',
424 451 conditions=dict(function=check_repo))
425 452
453 rmap.connect('repo_size', '/{repo_name:.*?}/repo_size',
454 controller='summary', action='repo_size',
455 conditions=dict(function=check_repo))
456
426 457 rmap.connect('repos_group_home', '/{group_name:.*}',
427 458 controller='admin/repos_groups', action="show_by_name",
428 459 conditions=dict(function=check_group))
429 460
430 461 rmap.connect('changeset_home', '/{repo_name:.*?}/changeset/{revision}',
431 462 controller='changeset', revision='tip',
432 463 conditions=dict(function=check_repo))
433 464
465 # no longer user, but kept for routes to work
466 rmap.connect("_edit_repo", "/{repo_name:.*?}/edit",
467 controller='admin/repos', action="edit",
468 conditions=dict(method=["GET"], function=check_repo)
469 )
470
471 rmap.connect("edit_repo", "/{repo_name:.*?}/settings",
472 controller='admin/repos', action="edit",
473 conditions=dict(method=["GET"], function=check_repo)
474 )
475
434 476 #still working url for backward compat.
435 477 rmap.connect('raw_changeset_home_depraced',
436 478 '/{repo_name:.*?}/raw-changeset/{revision}',
437 479 controller='changeset', action='changeset_raw',
438 480 revision='tip', conditions=dict(function=check_repo))
439 481
440 482 ## new URLs
441 483 rmap.connect('changeset_raw_home',
442 484 '/{repo_name:.*?}/changeset-diff/{revision}',
443 485 controller='changeset', action='changeset_raw',
444 486 revision='tip', conditions=dict(function=check_repo))
445 487
446 488 rmap.connect('changeset_patch_home',
447 489 '/{repo_name:.*?}/changeset-patch/{revision}',
448 490 controller='changeset', action='changeset_patch',
449 491 revision='tip', conditions=dict(function=check_repo))
450 492
451 493 rmap.connect('changeset_download_home',
452 494 '/{repo_name:.*?}/changeset-download/{revision}',
453 495 controller='changeset', action='changeset_download',
454 496 revision='tip', conditions=dict(function=check_repo))
455 497
456 498 rmap.connect('changeset_comment',
457 499 '/{repo_name:.*?}/changeset/{revision}/comment',
458 500 controller='changeset', revision='tip', action='comment',
459 501 conditions=dict(function=check_repo))
460 502
461 503 rmap.connect('changeset_comment_delete',
462 504 '/{repo_name:.*?}/changeset/comment/{comment_id}/delete',
463 505 controller='changeset', action='delete_comment',
464 506 conditions=dict(function=check_repo, method=["DELETE"]))
465 507
466 508 rmap.connect('changeset_info', '/changeset_info/{repo_name:.*?}/{revision}',
467 509 controller='changeset', action='changeset_info')
468 510
469 511 rmap.connect('compare_url',
470 512 '/{repo_name:.*?}/compare/{org_ref_type}@{org_ref:.*?}...{other_ref_type}@{other_ref:.*?}',
471 513 controller='compare', action='index',
472 514 conditions=dict(function=check_repo),
473 515 requirements=dict(
474 org_ref_type='(branch|book|tag|rev|org_ref_type)',
475 other_ref_type='(branch|book|tag|rev|other_ref_type)')
516 org_ref_type='(branch|book|tag|rev|__other_ref_type__)',
517 other_ref_type='(branch|book|tag|rev|__org_ref_type__)')
476 518 )
477 519
478 520 rmap.connect('pullrequest_home',
479 521 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
480 522 action='index', conditions=dict(function=check_repo,
481 523 method=["GET"]))
482 524
483 525 rmap.connect('pullrequest',
484 526 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
485 527 action='create', conditions=dict(function=check_repo,
486 528 method=["POST"]))
487 529
488 530 rmap.connect('pullrequest_show',
489 531 '/{repo_name:.*?}/pull-request/{pull_request_id}',
490 532 controller='pullrequests',
491 533 action='show', conditions=dict(function=check_repo,
492 534 method=["GET"]))
493 535 rmap.connect('pullrequest_update',
494 536 '/{repo_name:.*?}/pull-request/{pull_request_id}',
495 537 controller='pullrequests',
496 538 action='update', conditions=dict(function=check_repo,
497 539 method=["PUT"]))
498 540 rmap.connect('pullrequest_delete',
499 541 '/{repo_name:.*?}/pull-request/{pull_request_id}',
500 542 controller='pullrequests',
501 543 action='delete', conditions=dict(function=check_repo,
502 544 method=["DELETE"]))
503 545
504 546 rmap.connect('pullrequest_show_all',
505 547 '/{repo_name:.*?}/pull-request',
506 548 controller='pullrequests',
507 549 action='show_all', conditions=dict(function=check_repo,
508 550 method=["GET"]))
509 551
510 552 rmap.connect('pullrequest_comment',
511 553 '/{repo_name:.*?}/pull-request-comment/{pull_request_id}',
512 554 controller='pullrequests',
513 555 action='comment', conditions=dict(function=check_repo,
514 556 method=["POST"]))
515 557
516 558 rmap.connect('pullrequest_comment_delete',
517 559 '/{repo_name:.*?}/pull-request-comment/{comment_id}/delete',
518 560 controller='pullrequests', action='delete_comment',
519 561 conditions=dict(function=check_repo, method=["DELETE"]))
520 562
521 rmap.connect('summary_home', '/{repo_name:.*?}/summary',
563 rmap.connect('summary_home_summary', '/{repo_name:.*?}/summary',
522 564 controller='summary', conditions=dict(function=check_repo))
523 565
524 566 rmap.connect('shortlog_home', '/{repo_name:.*?}/shortlog',
525 567 controller='shortlog', conditions=dict(function=check_repo))
526 568
527 569 rmap.connect('shortlog_file_home', '/{repo_name:.*?}/shortlog/{revision}/{f_path:.*}',
528 570 controller='shortlog', f_path=None,
529 571 conditions=dict(function=check_repo))
530 572
531 573 rmap.connect('branches_home', '/{repo_name:.*?}/branches',
532 574 controller='branches', conditions=dict(function=check_repo))
533 575
534 576 rmap.connect('tags_home', '/{repo_name:.*?}/tags',
535 577 controller='tags', conditions=dict(function=check_repo))
536 578
537 579 rmap.connect('bookmarks_home', '/{repo_name:.*?}/bookmarks',
538 580 controller='bookmarks', conditions=dict(function=check_repo))
539 581
540 582 rmap.connect('changelog_home', '/{repo_name:.*?}/changelog',
541 583 controller='changelog', conditions=dict(function=check_repo))
542 584
543 585 rmap.connect('changelog_details', '/{repo_name:.*?}/changelog_details/{cs}',
544 586 controller='changelog', action='changelog_details',
545 587 conditions=dict(function=check_repo))
546 588
547 589 rmap.connect('files_home', '/{repo_name:.*?}/files/{revision}/{f_path:.*}',
548 590 controller='files', revision='tip', f_path='',
549 591 conditions=dict(function=check_repo))
550 592
593 rmap.connect('files_home_nopath', '/{repo_name:.*?}/files/{revision}',
594 controller='files', revision='tip', f_path='',
595 conditions=dict(function=check_repo))
596
551 597 rmap.connect('files_history_home',
552 598 '/{repo_name:.*?}/history/{revision}/{f_path:.*}',
553 599 controller='files', action='history', revision='tip', f_path='',
554 600 conditions=dict(function=check_repo))
555 601
556 602 rmap.connect('files_diff_home', '/{repo_name:.*?}/diff/{f_path:.*}',
557 603 controller='files', action='diff', revision='tip', f_path='',
558 604 conditions=dict(function=check_repo))
559 605
560 606 rmap.connect('files_rawfile_home',
561 607 '/{repo_name:.*?}/rawfile/{revision}/{f_path:.*}',
562 608 controller='files', action='rawfile', revision='tip',
563 609 f_path='', conditions=dict(function=check_repo))
564 610
565 611 rmap.connect('files_raw_home',
566 612 '/{repo_name:.*?}/raw/{revision}/{f_path:.*}',
567 613 controller='files', action='raw', revision='tip', f_path='',
568 614 conditions=dict(function=check_repo))
569 615
570 616 rmap.connect('files_annotate_home',
571 617 '/{repo_name:.*?}/annotate/{revision}/{f_path:.*}',
572 618 controller='files', action='index', revision='tip',
573 619 f_path='', annotate=True, conditions=dict(function=check_repo))
574 620
575 621 rmap.connect('files_edit_home',
576 622 '/{repo_name:.*?}/edit/{revision}/{f_path:.*}',
577 623 controller='files', action='edit', revision='tip',
578 624 f_path='', conditions=dict(function=check_repo))
579 625
580 626 rmap.connect('files_add_home',
581 627 '/{repo_name:.*?}/add/{revision}/{f_path:.*}',
582 628 controller='files', action='add', revision='tip',
583 629 f_path='', conditions=dict(function=check_repo))
584 630
585 631 rmap.connect('files_archive_home', '/{repo_name:.*?}/archive/{fname}',
586 632 controller='files', action='archivefile',
587 633 conditions=dict(function=check_repo))
588 634
589 635 rmap.connect('files_nodelist_home',
590 636 '/{repo_name:.*?}/nodelist/{revision}/{f_path:.*}',
591 637 controller='files', action='nodelist',
592 638 conditions=dict(function=check_repo))
593 639
594 rmap.connect('repo_settings_delete', '/{repo_name:.*?}/settings',
595 controller='settings', action="delete",
596 conditions=dict(method=["DELETE"], function=check_repo))
597
598 rmap.connect('repo_settings_update', '/{repo_name:.*?}/settings',
599 controller='settings', action="update",
600 conditions=dict(method=["PUT"], function=check_repo))
601
602 rmap.connect('repo_settings_home', '/{repo_name:.*?}/settings',
603 controller='settings', action='index',
604 conditions=dict(function=check_repo))
605
606 rmap.connect('toggle_locking', "/{repo_name:.*?}/locking_toggle",
607 controller='settings', action="toggle_locking",
608 conditions=dict(method=["GET"], function=check_repo))
609
610 640 rmap.connect('repo_fork_create_home', '/{repo_name:.*?}/fork',
611 641 controller='forks', action='fork_create',
612 642 conditions=dict(function=check_repo, method=["POST"]))
613 643
614 644 rmap.connect('repo_fork_home', '/{repo_name:.*?}/fork',
615 645 controller='forks', action='fork',
616 646 conditions=dict(function=check_repo))
617 647
618 648 rmap.connect('repo_forks_home', '/{repo_name:.*?}/forks',
619 649 controller='forks', action='forks',
620 650 conditions=dict(function=check_repo))
621 651
622 652 rmap.connect('repo_followers_home', '/{repo_name:.*?}/followers',
623 653 controller='followers', action='followers',
624 654 conditions=dict(function=check_repo))
625 655
626 656 return rmap
@@ -1,149 +1,149 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.admin
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Controller for Admin panel of Rhodecode
7 7
8 8 :created_on: Apr 7, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27
28 28 from pylons import request, tmpl_context as c, url
29 29 from sqlalchemy.orm import joinedload
30 30 from webhelpers.paginate import Page
31 31 from whoosh.qparser.default import QueryParser
32 32 from whoosh import query
33 33 from sqlalchemy.sql.expression import or_, and_, func
34 34
35 35 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
36 36 from rhodecode.lib.base import BaseController, render
37 37 from rhodecode.model.db import UserLog, User
38 38 from rhodecode.lib.utils2 import safe_int, remove_prefix, remove_suffix
39 39 from rhodecode.lib.indexers import JOURNAL_SCHEMA
40 40 from whoosh.qparser.dateparse import DateParserPlugin
41 41
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 def _journal_filter(user_log, search_term):
47 47 """
48 48 Filters sqlalchemy user_log based on search_term with whoosh Query language
49 49 http://packages.python.org/Whoosh/querylang.html
50 50
51 51 :param user_log:
52 52 :param search_term:
53 53 """
54 54 log.debug('Initial search term: %r' % search_term)
55 55 qry = None
56 56 if search_term:
57 57 qp = QueryParser('repository', schema=JOURNAL_SCHEMA)
58 58 qp.add_plugin(DateParserPlugin())
59 59 qry = qp.parse(unicode(search_term))
60 60 log.debug('Filtering using parsed query %r' % qry)
61 61
62 62 def wildcard_handler(col, wc_term):
63 63 if wc_term.startswith('*') and not wc_term.endswith('*'):
64 64 #postfix == endswith
65 65 wc_term = remove_prefix(wc_term, prefix='*')
66 66 return func.lower(col).endswith(wc_term)
67 67 elif wc_term.startswith('*') and wc_term.endswith('*'):
68 68 #wildcard == ilike
69 69 wc_term = remove_prefix(wc_term, prefix='*')
70 70 wc_term = remove_suffix(wc_term, suffix='*')
71 71 return func.lower(col).contains(wc_term)
72 72
73 73 def get_filterion(field, val, term):
74 74
75 75 if field == 'repository':
76 76 field = getattr(UserLog, 'repository_name')
77 77 elif field == 'ip':
78 78 field = getattr(UserLog, 'user_ip')
79 79 elif field == 'date':
80 80 field = getattr(UserLog, 'action_date')
81 81 elif field == 'username':
82 82 field = getattr(UserLog, 'username')
83 83 else:
84 84 field = getattr(UserLog, field)
85 85 log.debug('filter field: %s val=>%s' % (field, val))
86 86
87 87 #sql filtering
88 88 if isinstance(term, query.Wildcard):
89 89 return wildcard_handler(field, val)
90 90 elif isinstance(term, query.Prefix):
91 91 return func.lower(field).startswith(func.lower(val))
92 92 elif isinstance(term, query.DateRange):
93 93 return and_(field >= val[0], field <= val[1])
94 94 return func.lower(field) == func.lower(val)
95 95
96 96 if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard,
97 97 query.DateRange)):
98 98 if not isinstance(qry, query.And):
99 99 qry = [qry]
100 100 for term in qry:
101 101 field = term.fieldname
102 102 val = (term.text if not isinstance(term, query.DateRange)
103 103 else [term.startdate, term.enddate])
104 104 user_log = user_log.filter(get_filterion(field, val, term))
105 105 elif isinstance(qry, query.Or):
106 106 filters = []
107 107 for term in qry:
108 108 field = term.fieldname
109 109 val = (term.text if not isinstance(term, query.DateRange)
110 110 else [term.startdate, term.enddate])
111 111 filters.append(get_filterion(field, val, term))
112 112 user_log = user_log.filter(or_(*filters))
113 113
114 114 return user_log
115 115
116 116
117 117 class AdminController(BaseController):
118 118
119 119 @LoginRequired()
120 120 def __before__(self):
121 121 super(AdminController, self).__before__()
122 122
123 123 @HasPermissionAllDecorator('hg.admin')
124 124 def index(self):
125 125 users_log = UserLog.query()\
126 126 .options(joinedload(UserLog.user))\
127 127 .options(joinedload(UserLog.repository))
128 128
129 129 #FILTERING
130 130 c.search_term = request.GET.get('filter')
131 131 try:
132 132 users_log = _journal_filter(users_log, c.search_term)
133 except:
133 except Exception:
134 134 # we want this to crash for now
135 135 raise
136 136
137 137 users_log = users_log.order_by(UserLog.action_date.desc())
138 138
139 139 p = safe_int(request.params.get('page', 1), 1)
140 140
141 141 def url_generator(**kw):
142 142 return url.current(filter=c.search_term, **kw)
143 143
144 144 c.users_log = Page(users_log, page=p, items_per_page=10, url=url_generator)
145 145 c.log_data = render('admin/admin_log.html')
146 146
147 147 if request.environ.get('HTTP_X_PARTIAL_XHR'):
148 148 return c.log_data
149 149 return render('admin/admin.html')
@@ -1,130 +1,130 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.defaults
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 default settings controller for Rhodecode
7 7
8 8 :created_on: Apr 27, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29 from formencode import htmlfill
30 30
31 31 from pylons import request, session, tmpl_context as c, url
32 32 from pylons.controllers.util import abort, redirect
33 33 from pylons.i18n.translation import _
34 34
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
37 37 from rhodecode.lib.base import BaseController, render
38 38 from rhodecode.model.forms import DefaultsForm
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode import BACKENDS
41 41 from rhodecode.model.db import RhodeCodeSetting
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class DefaultsController(BaseController):
47 47 """REST Controller styled on the Atom Publishing Protocol"""
48 48 # To properly map this controller, ensure your config/routing.py
49 49 # file has a resource setup:
50 50 # map.resource('default', 'defaults')
51 51
52 52 @LoginRequired()
53 53 @HasPermissionAllDecorator('hg.admin')
54 54 def __before__(self):
55 55 super(DefaultsController, self).__before__()
56 56
57 57 def index(self, format='html'):
58 58 """GET /defaults: All items in the collection"""
59 59 # url('defaults')
60 60 c.backends = BACKENDS.keys()
61 61 defaults = RhodeCodeSetting.get_default_repo_settings()
62 62
63 63 return htmlfill.render(
64 64 render('admin/defaults/defaults.html'),
65 65 defaults=defaults,
66 66 encoding="UTF-8",
67 67 force_defaults=False
68 68 )
69 69
70 70 def create(self):
71 71 """POST /defaults: Create a new item"""
72 72 # url('defaults')
73 73
74 74 def new(self, format='html'):
75 75 """GET /defaults/new: Form to create a new item"""
76 76 # url('new_default')
77 77
78 78 def update(self, id):
79 79 """PUT /defaults/id: Update an existing item"""
80 80 # Forms posted to this method should contain a hidden field:
81 81 # <input type="hidden" name="_method" value="PUT" />
82 82 # Or using helpers:
83 83 # h.form(url('default', id=ID),
84 84 # method='put')
85 85 # url('default', id=ID)
86 86
87 87 _form = DefaultsForm()()
88 88
89 89 try:
90 90 form_result = _form.to_python(dict(request.POST))
91 91 for k, v in form_result.iteritems():
92 92 setting = RhodeCodeSetting.get_by_name_or_create(k)
93 93 setting.app_settings_value = v
94 94 Session().add(setting)
95 95 Session().commit()
96 96 h.flash(_('Default settings updated successfully'),
97 97 category='success')
98 98
99 99 except formencode.Invalid, errors:
100 100 defaults = errors.value
101 101
102 102 return htmlfill.render(
103 103 render('admin/defaults/defaults.html'),
104 104 defaults=defaults,
105 105 errors=errors.error_dict or {},
106 106 prefix_error=False,
107 107 encoding="UTF-8")
108 108 except Exception:
109 109 log.error(traceback.format_exc())
110 h.flash(_('error occurred during update of defaults'),
110 h.flash(_('Error occurred during update of defaults'),
111 111 category='error')
112 112
113 113 return redirect(url('defaults'))
114 114
115 115 def delete(self, id):
116 116 """DELETE /defaults/id: Delete an existing item"""
117 117 # Forms posted to this method should contain a hidden field:
118 118 # <input type="hidden" name="_method" value="DELETE" />
119 119 # Or using helpers:
120 120 # h.form(url('default', id=ID),
121 121 # method='delete')
122 122 # url('default', id=ID)
123 123
124 124 def show(self, id, format='html'):
125 125 """GET /defaults/id: Show a specific item"""
126 126 # url('default', id=ID)
127 127
128 128 def edit(self, id, format='html'):
129 129 """GET /defaults/id/edit: Form to edit an existing item"""
130 130 # url('edit_default', id=ID)
@@ -1,150 +1,150 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.ldap_settings
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 ldap controller for RhodeCode
7 7
8 8 :created_on: Nov 26, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26 import formencode
27 27 import traceback
28 28
29 29 from formencode import htmlfill
30 30
31 31 from pylons import request, response, session, tmpl_context as c, url
32 32 from pylons.controllers.util import abort, redirect
33 33 from pylons.i18n.translation import _
34 34
35 35 from sqlalchemy.exc import DatabaseError
36 36
37 37 from rhodecode.lib.base import BaseController, render
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
40 40 from rhodecode.lib.exceptions import LdapImportError
41 41 from rhodecode.model.forms import LdapSettingsForm
42 42 from rhodecode.model.db import RhodeCodeSetting
43 43 from rhodecode.model.meta import Session
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class LdapSettingsController(BaseController):
49 49
50 50 search_scope_choices = [('BASE', _('BASE'),),
51 51 ('ONELEVEL', _('ONELEVEL'),),
52 52 ('SUBTREE', _('SUBTREE'),),
53 53 ]
54 54 search_scope_default = 'SUBTREE'
55 55
56 56 tls_reqcert_choices = [('NEVER', _('NEVER'),),
57 57 ('ALLOW', _('ALLOW'),),
58 58 ('TRY', _('TRY'),),
59 59 ('DEMAND', _('DEMAND'),),
60 60 ('HARD', _('HARD'),),
61 61 ]
62 62 tls_reqcert_default = 'DEMAND'
63 63
64 64 tls_kind_choices = [('PLAIN', _('No encryption'),),
65 65 ('LDAPS', _('LDAPS connection'),),
66 66 ('START_TLS', _('START_TLS on LDAP connection'),)
67 67 ]
68 68
69 69 tls_kind_default = 'PLAIN'
70 70
71 71 @LoginRequired()
72 72 @HasPermissionAllDecorator('hg.admin')
73 73 def __before__(self):
74 74 c.admin_user = session.get('admin_user')
75 75 c.admin_username = session.get('admin_username')
76 76 c.search_scope_choices = self.search_scope_choices
77 77 c.tls_reqcert_choices = self.tls_reqcert_choices
78 78 c.tls_kind_choices = self.tls_kind_choices
79 79
80 80 c.search_scope_cur = self.search_scope_default
81 81 c.tls_reqcert_cur = self.tls_reqcert_default
82 82 c.tls_kind_cur = self.tls_kind_default
83 83
84 84 super(LdapSettingsController, self).__before__()
85 85
86 86 def index(self):
87 87 defaults = RhodeCodeSetting.get_ldap_settings()
88 88 c.search_scope_cur = defaults.get('ldap_search_scope')
89 89 c.tls_reqcert_cur = defaults.get('ldap_tls_reqcert')
90 90 c.tls_kind_cur = defaults.get('ldap_tls_kind')
91 91
92 92 return htmlfill.render(
93 93 render('admin/ldap/ldap.html'),
94 94 defaults=defaults,
95 95 encoding="UTF-8",
96 96 force_defaults=True,)
97 97
98 98 def ldap_settings(self):
99 99 """POST ldap create and store ldap settings"""
100 100
101 101 _form = LdapSettingsForm([x[0] for x in self.tls_reqcert_choices],
102 102 [x[0] for x in self.search_scope_choices],
103 103 [x[0] for x in self.tls_kind_choices])()
104 104 # check the ldap lib
105 105 ldap_active = False
106 106 try:
107 107 import ldap
108 108 ldap_active = True
109 109 except ImportError:
110 110 pass
111 111
112 112 try:
113 113 form_result = _form.to_python(dict(request.POST))
114 114
115 115 try:
116 116
117 117 for k, v in form_result.items():
118 118 if k.startswith('ldap_'):
119 119 if k == 'ldap_active':
120 120 v = ldap_active
121 121 setting = RhodeCodeSetting.get_by_name(k)
122 122 setting.app_settings_value = v
123 123 Session().add(setting)
124 124
125 125 Session().commit()
126 h.flash(_('Ldap settings updated successfully'),
126 h.flash(_('LDAP settings updated successfully'),
127 127 category='success')
128 128 if not ldap_active:
129 129 #if ldap is missing send an info to user
130 130 h.flash(_('Unable to activate ldap. The "python-ldap" library '
131 131 'is missing.'), category='warning')
132 132
133 133 except (DatabaseError,):
134 134 raise
135 135
136 136 except formencode.Invalid, errors:
137 137 e = errors.error_dict or {}
138 138
139 139 return htmlfill.render(
140 140 render('admin/ldap/ldap.html'),
141 141 defaults=errors.value,
142 142 errors=e,
143 143 prefix_error=False,
144 144 encoding="UTF-8")
145 145 except Exception:
146 146 log.error(traceback.format_exc())
147 h.flash(_('error occurred during update of ldap settings'),
147 h.flash(_('Error occurred during update of ldap settings'),
148 148 category='error')
149 149
150 150 return redirect(url('ldap_home'))
@@ -1,172 +1,173 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.notifications
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 notifications controller for RhodeCode
7 7
8 8 :created_on: Nov 23, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons import request
30 30 from pylons import tmpl_context as c, url
31 from pylons.controllers.util import redirect
31 from pylons.controllers.util import redirect, abort
32 32
33 33 from webhelpers.paginate import Page
34 34
35 35 from rhodecode.lib.base import BaseController, render
36 36 from rhodecode.model.db import Notification
37 37
38 38 from rhodecode.model.notification import NotificationModel
39 39 from rhodecode.lib.auth import LoginRequired, NotAnonymous
40 40 from rhodecode.lib import helpers as h
41 41 from rhodecode.model.meta import Session
42 42 from rhodecode.lib.utils2 import safe_int
43 43
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class NotificationsController(BaseController):
49 49 """REST Controller styled on the Atom Publishing Protocol"""
50 50 # To properly map this controller, ensure your config/routing.py
51 51 # file has a resource setup:
52 52 # map.resource('notification', 'notifications', controller='_admin/notifications',
53 53 # path_prefix='/_admin', name_prefix='_admin_')
54 54
55 55 @LoginRequired()
56 56 @NotAnonymous()
57 57 def __before__(self):
58 58 super(NotificationsController, self).__before__()
59 59
60 60 def index(self, format='html'):
61 61 """GET /_admin/notifications: All items in the collection"""
62 62 # url('notifications')
63 63 c.user = self.rhodecode_user
64 64 notif = NotificationModel().get_for_user(self.rhodecode_user.user_id,
65 65 filter_=request.GET.getall('type'))
66 66
67 67 p = safe_int(request.params.get('page', 1), 1)
68 68 c.notifications = Page(notif, page=p, items_per_page=10)
69 69 c.pull_request_type = Notification.TYPE_PULL_REQUEST
70 70 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
71 71 Notification.TYPE_PULL_REQUEST_COMMENT]
72 72
73 73 _current_filter = request.GET.getall('type')
74 74 c.current_filter = 'all'
75 75 if _current_filter == [c.pull_request_type]:
76 76 c.current_filter = 'pull_request'
77 77 elif _current_filter == c.comment_type:
78 78 c.current_filter = 'comment'
79 79
80 80 return render('admin/notifications/notifications.html')
81 81
82 82 def mark_all_read(self):
83 83 if request.environ.get('HTTP_X_PARTIAL_XHR'):
84 84 nm = NotificationModel()
85 85 # mark all read
86 86 nm.mark_all_read_for_user(self.rhodecode_user.user_id,
87 87 filter_=request.GET.getall('type'))
88 88 Session().commit()
89 89 c.user = self.rhodecode_user
90 90 notif = nm.get_for_user(self.rhodecode_user.user_id,
91 91 filter_=request.GET.getall('type'))
92 92 c.notifications = Page(notif, page=1, items_per_page=10)
93 93 return render('admin/notifications/notifications_data.html')
94 94
95 95 def create(self):
96 96 """POST /_admin/notifications: Create a new item"""
97 97 # url('notifications')
98 98
99 99 def new(self, format='html'):
100 100 """GET /_admin/notifications/new: Form to create a new item"""
101 101 # url('new_notification')
102 102
103 103 def update(self, notification_id):
104 104 """PUT /_admin/notifications/id: Update an existing item"""
105 105 # Forms posted to this method should contain a hidden field:
106 106 # <input type="hidden" name="_method" value="PUT" />
107 107 # Or using helpers:
108 108 # h.form(url('notification', notification_id=ID),
109 109 # method='put')
110 110 # url('notification', notification_id=ID)
111 111 try:
112 112 no = Notification.get(notification_id)
113 113 owner = all(un.user.user_id == c.rhodecode_user.user_id
114 114 for un in no.notifications_to_users)
115 115 if h.HasPermissionAny('hg.admin')() or owner:
116 116 NotificationModel().mark_read(c.rhodecode_user.user_id, no)
117 117 Session().commit()
118 118 return 'ok'
119 119 except Exception:
120 Session.rollback()
120 Session().rollback()
121 121 log.error(traceback.format_exc())
122 122 return 'fail'
123 123
124 124 def delete(self, notification_id):
125 125 """DELETE /_admin/notifications/id: Delete an existing item"""
126 126 # Forms posted to this method should contain a hidden field:
127 127 # <input type="hidden" name="_method" value="DELETE" />
128 128 # Or using helpers:
129 129 # h.form(url('notification', notification_id=ID),
130 130 # method='delete')
131 131 # url('notification', notification_id=ID)
132 132
133 133 try:
134 134 no = Notification.get(notification_id)
135 135 owner = all(un.user.user_id == c.rhodecode_user.user_id
136 136 for un in no.notifications_to_users)
137 137 if h.HasPermissionAny('hg.admin')() or owner:
138 138 NotificationModel().delete(c.rhodecode_user.user_id, no)
139 139 Session().commit()
140 140 return 'ok'
141 141 except Exception:
142 Session.rollback()
142 Session().rollback()
143 143 log.error(traceback.format_exc())
144 144 return 'fail'
145 145
146 146 def show(self, notification_id, format='html'):
147 147 """GET /_admin/notifications/id: Show a specific item"""
148 148 # url('notification', notification_id=ID)
149 149 c.user = self.rhodecode_user
150 150 no = Notification.get(notification_id)
151 151
152 owner = all(un.user.user_id == c.rhodecode_user.user_id
152 owner = any(un.user.user_id == c.rhodecode_user.user_id
153 153 for un in no.notifications_to_users)
154
154 155 if no and (h.HasPermissionAny('hg.admin', 'repository.admin')() or owner):
155 156 unotification = NotificationModel()\
156 157 .get_user_notification(c.user.user_id, no)
157 158
158 159 # if this association to user is not valid, we don't want to show
159 160 # this message
160 161 if unotification:
161 if unotification.read is False:
162 if not unotification.read:
162 163 unotification.mark_as_read()
163 164 Session().commit()
164 165 c.notification = no
165 166
166 167 return render('admin/notifications/show_notification.html')
167 168
168 return redirect(url('notifications'))
169 return abort(403)
169 170
170 171 def edit(self, notification_id, format='html'):
171 172 """GET /_admin/notifications/id/edit: Form to edit an existing item"""
172 173 # url('edit_notification', notification_id=ID)
@@ -1,194 +1,194 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.permissions
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 permissions controller for Rhodecode
7 7
8 8 :created_on: Apr 27, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29 from formencode import htmlfill
30 30
31 31 from pylons import request, session, tmpl_context as c, url
32 32 from pylons.controllers.util import abort, redirect
33 33 from pylons.i18n.translation import _
34 34
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator,\
37 37 AuthUser
38 38 from rhodecode.lib.base import BaseController, render
39 39 from rhodecode.model.forms import DefaultPermissionsForm
40 40 from rhodecode.model.permission import PermissionModel
41 41 from rhodecode.model.db import User, UserIpMap
42 42 from rhodecode.model.meta import Session
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class PermissionsController(BaseController):
48 48 """REST Controller styled on the Atom Publishing Protocol"""
49 49 # To properly map this controller, ensure your config/routing.py
50 50 # file has a resource setup:
51 51 # map.resource('permission', 'permissions')
52 52
53 53 @LoginRequired()
54 54 @HasPermissionAllDecorator('hg.admin')
55 55 def __before__(self):
56 56 c.admin_user = session.get('admin_user')
57 57 c.admin_username = session.get('admin_username')
58 58 super(PermissionsController, self).__before__()
59 59
60 60 self.repo_perms_choices = [('repository.none', _('None'),),
61 61 ('repository.read', _('Read'),),
62 62 ('repository.write', _('Write'),),
63 63 ('repository.admin', _('Admin'),)]
64 64 self.group_perms_choices = [('group.none', _('None'),),
65 65 ('group.read', _('Read'),),
66 66 ('group.write', _('Write'),),
67 67 ('group.admin', _('Admin'),)]
68 68 self.register_choices = [
69 69 ('hg.register.none',
70 _('disabled')),
70 _('Disabled')),
71 71 ('hg.register.manual_activate',
72 _('allowed with manual account activation')),
72 _('Allowed with manual account activation')),
73 73 ('hg.register.auto_activate',
74 _('allowed with automatic account activation')), ]
74 _('Allowed with automatic account activation')), ]
75 75
76 76 self.create_choices = [('hg.create.none', _('Disabled')),
77 77 ('hg.create.repository', _('Enabled'))]
78 78
79 79 self.fork_choices = [('hg.fork.none', _('Disabled')),
80 80 ('hg.fork.repository', _('Enabled'))]
81 81
82 82 # set the global template variables
83 83 c.repo_perms_choices = self.repo_perms_choices
84 84 c.group_perms_choices = self.group_perms_choices
85 85 c.register_choices = self.register_choices
86 86 c.create_choices = self.create_choices
87 87 c.fork_choices = self.fork_choices
88 88
89 89 def index(self, format='html'):
90 90 """GET /permissions: All items in the collection"""
91 91 # url('permissions')
92 92
93 93 def create(self):
94 94 """POST /permissions: Create a new item"""
95 95 # url('permissions')
96 96
97 97 def new(self, format='html'):
98 98 """GET /permissions/new: Form to create a new item"""
99 99 # url('new_permission')
100 100
101 101 def update(self, id):
102 102 """PUT /permissions/id: Update an existing item"""
103 103 # Forms posted to this method should contain a hidden field:
104 104 # <input type="hidden" name="_method" value="PUT" />
105 105 # Or using helpers:
106 106 # h.form(url('permission', id=ID),
107 107 # method='put')
108 108 # url('permission', id=ID)
109 109 if id == 'default':
110 110 c.user = default_user = User.get_by_username('default')
111 111 c.perm_user = AuthUser(user_id=default_user.user_id)
112 112 c.user_ip_map = UserIpMap.query()\
113 113 .filter(UserIpMap.user == default_user).all()
114 114 permission_model = PermissionModel()
115 115
116 116 _form = DefaultPermissionsForm(
117 117 [x[0] for x in self.repo_perms_choices],
118 118 [x[0] for x in self.group_perms_choices],
119 119 [x[0] for x in self.register_choices],
120 120 [x[0] for x in self.create_choices],
121 121 [x[0] for x in self.fork_choices])()
122 122
123 123 try:
124 124 form_result = _form.to_python(dict(request.POST))
125 125 form_result.update({'perm_user_name': id})
126 126 permission_model.update(form_result)
127 127 Session().commit()
128 128 h.flash(_('Default permissions updated successfully'),
129 129 category='success')
130 130
131 131 except formencode.Invalid, errors:
132 132 defaults = errors.value
133 133
134 134 return htmlfill.render(
135 135 render('admin/permissions/permissions.html'),
136 136 defaults=defaults,
137 137 errors=errors.error_dict or {},
138 138 prefix_error=False,
139 139 encoding="UTF-8")
140 140 except Exception:
141 141 log.error(traceback.format_exc())
142 h.flash(_('error occurred during update of permissions'),
142 h.flash(_('Error occurred during update of permissions'),
143 143 category='error')
144 144
145 145 return redirect(url('edit_permission', id=id))
146 146
147 147 def delete(self, id):
148 148 """DELETE /permissions/id: Delete an existing item"""
149 149 # Forms posted to this method should contain a hidden field:
150 150 # <input type="hidden" name="_method" value="DELETE" />
151 151 # Or using helpers:
152 152 # h.form(url('permission', id=ID),
153 153 # method='delete')
154 154 # url('permission', id=ID)
155 155
156 156 def show(self, id, format='html'):
157 157 """GET /permissions/id: Show a specific item"""
158 158 # url('permission', id=ID)
159 159
160 160 def edit(self, id, format='html'):
161 161 """GET /permissions/id/edit: Form to edit an existing item"""
162 162 #url('edit_permission', id=ID)
163 163
164 164 #this form can only edit default user permissions
165 165 if id == 'default':
166 166 c.user = default_user = User.get_by_username('default')
167 167 defaults = {'anonymous': default_user.active}
168 168 c.perm_user = AuthUser(user_id=default_user.user_id)
169 169 c.user_ip_map = UserIpMap.query()\
170 170 .filter(UserIpMap.user == default_user).all()
171 171 for p in default_user.user_perms:
172 172 if p.permission.permission_name.startswith('repository.'):
173 173 defaults['default_repo_perm'] = p.permission.permission_name
174 174
175 175 if p.permission.permission_name.startswith('group.'):
176 176 defaults['default_group_perm'] = p.permission.permission_name
177 177
178 178 if p.permission.permission_name.startswith('hg.register.'):
179 179 defaults['default_register'] = p.permission.permission_name
180 180
181 181 if p.permission.permission_name.startswith('hg.create.'):
182 182 defaults['default_create'] = p.permission.permission_name
183 183
184 184 if p.permission.permission_name.startswith('hg.fork.'):
185 185 defaults['default_fork'] = p.permission.permission_name
186 186
187 187 return htmlfill.render(
188 188 render('admin/permissions/permissions.html'),
189 189 defaults=defaults,
190 190 encoding="UTF-8",
191 191 force_defaults=False
192 192 )
193 193 else:
194 194 return redirect(url('admin_home'))
@@ -1,482 +1,606 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.repos
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Repositories controller for RhodeCode
7 7
8 8 :created_on: Apr 7, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29 from formencode import htmlfill
30 30
31 from webob.exc import HTTPInternalServerError
31 from webob.exc import HTTPInternalServerError, HTTPForbidden
32 32 from pylons import request, session, tmpl_context as c, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from sqlalchemy.exc import IntegrityError
36 36
37 37 import rhodecode
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
40 HasPermissionAnyDecorator, HasRepoPermissionAllDecorator
41 from rhodecode.lib.base import BaseController, render
40 HasPermissionAnyDecorator, HasRepoPermissionAllDecorator, NotAnonymous,\
41 HasPermissionAny, HasReposGroupPermissionAny, HasRepoPermissionAnyDecorator
42 from rhodecode.lib.base import BaseRepoController, render
42 43 from rhodecode.lib.utils import invalidate_cache, action_logger, repo_name_slug
43 44 from rhodecode.lib.helpers import get_token
44 45 from rhodecode.model.meta import Session
45 46 from rhodecode.model.db import User, Repository, UserFollowing, RepoGroup,\
46 RhodeCodeSetting
47 from rhodecode.model.forms import RepoForm
48 from rhodecode.model.scm import ScmModel
47 RhodeCodeSetting, RepositoryField
48 from rhodecode.model.forms import RepoForm, RepoFieldForm, RepoPermsForm
49 from rhodecode.model.scm import ScmModel, GroupList
49 50 from rhodecode.model.repo import RepoModel
50 51 from rhodecode.lib.compat import json
51 52 from sqlalchemy.sql.expression import func
53 from rhodecode.lib.exceptions import AttachedForksError
52 54
53 55 log = logging.getLogger(__name__)
54 56
55 57
56 class ReposController(BaseController):
58 class ReposController(BaseRepoController):
57 59 """
58 60 REST Controller styled on the Atom Publishing Protocol"""
59 61 # To properly map this controller, ensure your config/routing.py
60 62 # file has a resource setup:
61 63 # map.resource('repo', 'repos')
62 64
63 65 @LoginRequired()
64 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
65 66 def __before__(self):
66 67 c.admin_user = session.get('admin_user')
67 68 c.admin_username = session.get('admin_username')
68 69 super(ReposController, self).__before__()
69 70
70 71 def __load_defaults(self):
71 c.repo_groups = RepoGroup.groups_choices(check_perms=True)
72 acl_groups = GroupList(RepoGroup.query().all(),
73 perm_set=['group.write', 'group.admin'])
74 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
72 75 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
73 76
74 77 repo_model = RepoModel()
75 78 c.users_array = repo_model.get_users_js()
76 79 c.users_groups_array = repo_model.get_users_groups_js()
77 80 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
78 81 c.landing_revs_choices = choices
79 82
80 83 def __load_data(self, repo_name=None):
81 84 """
82 85 Load defaults settings for edit, and update
83 86
84 87 :param repo_name:
85 88 """
86 89 self.__load_defaults()
87 90
88 91 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
89 92 repo = db_repo.scm_instance
90 93
91 94 if c.repo_info is None:
92 95 h.not_mapped_error(repo_name)
93 96 return redirect(url('repos'))
94 97
95 98 ##override defaults for exact repo info here git/hg etc
96 99 choices, c.landing_revs = ScmModel().get_repo_landing_revs(c.repo_info)
97 100 c.landing_revs_choices = choices
98 101
99 102 c.default_user_id = User.get_by_username('default').user_id
100 103 c.in_public_journal = UserFollowing.query()\
101 104 .filter(UserFollowing.user_id == c.default_user_id)\
102 105 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
103 106
104 107 if c.repo_info.stats:
105 108 # this is on what revision we ended up so we add +1 for count
106 109 last_rev = c.repo_info.stats.stat_on_revision + 1
107 110 else:
108 111 last_rev = 0
109 112 c.stats_revision = last_rev
110 113
111 114 c.repo_last_rev = repo.count() if repo.revisions else 0
112 115
113 116 if last_rev == 0 or c.repo_last_rev == 0:
114 117 c.stats_percentage = 0
115 118 else:
116 119 c.stats_percentage = '%.2f' % ((float((last_rev)) /
117 120 c.repo_last_rev) * 100)
118 121
122 c.repo_fields = RepositoryField.query()\
123 .filter(RepositoryField.repository == db_repo).all()
124
119 125 defaults = RepoModel()._get_defaults(repo_name)
120 126
121 127 c.repos_list = [('', _('--REMOVE FORK--'))]
122 128 c.repos_list += [(x.repo_id, x.repo_name) for x in
123 129 Repository.query().order_by(Repository.repo_name).all()
124 130 if x.repo_id != c.repo_info.repo_id]
125 131
126 132 defaults['id_fork_of'] = db_repo.fork.repo_id if db_repo.fork else ''
127 133 return defaults
128 134
129 135 @HasPermissionAllDecorator('hg.admin')
130 136 def index(self, format='html'):
131 137 """GET /repos: All items in the collection"""
132 138 # url('repos')
133 139
134 140 c.repos_list = Repository.query()\
135 141 .order_by(func.lower(Repository.repo_name))\
136 142 .all()
137 143
138 144 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
139 admin=True)
145 admin=True,
146 super_user_actions=True)
140 147 #json used to render the grid
141 148 c.data = json.dumps(repos_data)
142 149
143 150 return render('admin/repos/repos.html')
144 151
145 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
152 @NotAnonymous()
146 153 def create(self):
147 154 """
148 155 POST /repos: Create a new item"""
149 156 # url('repos')
150 157
151 158 self.__load_defaults()
152 159 form_result = {}
153 160 try:
154 161 form_result = RepoForm(repo_groups=c.repo_groups_choices,
155 162 landing_revs=c.landing_revs_choices)()\
156 163 .to_python(dict(request.POST))
164
157 165 new_repo = RepoModel().create(form_result,
158 166 self.rhodecode_user.user_id)
159 167 if form_result['clone_uri']:
160 h.flash(_('created repository %s from %s') \
168 h.flash(_('Created repository %s from %s') \
161 169 % (form_result['repo_name'], form_result['clone_uri']),
162 170 category='success')
163 171 else:
164 h.flash(_('created repository %s') % form_result['repo_name'],
172 repo_url = h.link_to(form_result['repo_name'],
173 h.url('summary_home', repo_name=form_result['repo_name_full']))
174 h.flash(h.literal(_('Created repository %s') % repo_url),
165 175 category='success')
166 176
167 177 if request.POST.get('user_created'):
168 178 # created by regular non admin user
169 179 action_logger(self.rhodecode_user, 'user_created_repo',
170 180 form_result['repo_name_full'], self.ip_addr,
171 181 self.sa)
172 182 else:
173 183 action_logger(self.rhodecode_user, 'admin_created_repo',
174 184 form_result['repo_name_full'], self.ip_addr,
175 185 self.sa)
176 186 Session().commit()
177 187 except formencode.Invalid, errors:
178
179 c.new_repo = errors.value['repo_name']
180
181 if request.POST.get('user_created'):
182 r = render('admin/repos/repo_add_create_repository.html')
183 else:
184 r = render('admin/repos/repo_add.html')
185
186 188 return htmlfill.render(
187 r,
189 render('admin/repos/repo_add.html'),
188 190 defaults=errors.value,
189 191 errors=errors.error_dict or {},
190 192 prefix_error=False,
191 193 encoding="UTF-8")
192 194
193 195 except Exception:
194 196 log.error(traceback.format_exc())
195 msg = _('error occurred during creation of repository %s') \
197 msg = _('Error creating repository %s') \
196 198 % form_result.get('repo_name')
197 199 h.flash(msg, category='error')
200 if c.rhodecode_user.is_admin:
198 201 return redirect(url('repos'))
202 return redirect(url('home'))
199 203 #redirect to our new repo !
200 204 return redirect(url('summary_home', repo_name=new_repo.repo_name))
201 205
202 @HasPermissionAllDecorator('hg.admin')
203 def new(self, format='html'):
204 """GET /repos/new: Form to create a new item"""
206 @NotAnonymous()
207 def create_repository(self):
208 """GET /_admin/create_repository: Form to create a new item"""
205 209 new_repo = request.GET.get('repo', '')
210 parent_group = request.GET.get('parent_group')
211 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
212 #you're not super admin nor have global create permissions,
213 #but maybe you have at least write permission to a parent group ?
214 _gr = RepoGroup.get(parent_group)
215 gr_name = _gr.group_name if _gr else None
216 if not HasReposGroupPermissionAny('group.admin', 'group.write')(group_name=gr_name):
217 raise HTTPForbidden
218
219 acl_groups = GroupList(RepoGroup.query().all(),
220 perm_set=['group.write', 'group.admin'])
221 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
222 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
223 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
224
206 225 c.new_repo = repo_name_slug(new_repo)
207 self.__load_defaults()
226
208 227 ## apply the defaults from defaults page
209 228 defaults = RhodeCodeSetting.get_default_repo_settings(strip_prefix=True)
229 if parent_group:
230 defaults.update({'repo_group': parent_group})
231
210 232 return htmlfill.render(
211 233 render('admin/repos/repo_add.html'),
212 234 defaults=defaults,
213 235 errors={},
214 236 prefix_error=False,
215 237 encoding="UTF-8"
216 238 )
217 239
218 @HasPermissionAllDecorator('hg.admin')
240 @HasRepoPermissionAllDecorator('repository.admin')
219 241 def update(self, repo_name):
220 242 """
221 243 PUT /repos/repo_name: Update an existing item"""
222 244 # Forms posted to this method should contain a hidden field:
223 245 # <input type="hidden" name="_method" value="PUT" />
224 246 # Or using helpers:
225 247 # h.form(url('repo', repo_name=ID),
226 248 # method='put')
227 249 # url('repo', repo_name=ID)
228 250 self.__load_defaults()
229 251 repo_model = RepoModel()
230 252 changed_name = repo_name
231 253 #override the choices with extracted revisions !
232 254 choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo_name)
233 255 c.landing_revs_choices = choices
234 256 repo = Repository.get_by_repo_name(repo_name)
235 257 _form = RepoForm(edit=True, old_data={'repo_name': repo_name,
236 258 'repo_group': repo.group.get_dict() \
237 259 if repo.group else {}},
238 260 repo_groups=c.repo_groups_choices,
239 261 landing_revs=c.landing_revs_choices)()
240 262 try:
241 263 form_result = _form.to_python(dict(request.POST))
242 264 repo = repo_model.update(repo_name, **form_result)
243 265 invalidate_cache('get_repo_cached_%s' % repo_name)
244 266 h.flash(_('Repository %s updated successfully') % repo_name,
245 267 category='success')
246 268 changed_name = repo.repo_name
247 269 action_logger(self.rhodecode_user, 'admin_updated_repo',
248 270 changed_name, self.ip_addr, self.sa)
249 271 Session().commit()
250 272 except formencode.Invalid, errors:
251 273 defaults = self.__load_data(repo_name)
252 274 defaults.update(errors.value)
253 275 return htmlfill.render(
254 276 render('admin/repos/repo_edit.html'),
255 277 defaults=defaults,
256 278 errors=errors.error_dict or {},
257 279 prefix_error=False,
258 280 encoding="UTF-8")
259 281
260 282 except Exception:
261 283 log.error(traceback.format_exc())
262 h.flash(_('error occurred during update of repository %s') \
284 h.flash(_('Error occurred during update of repository %s') \
263 285 % repo_name, category='error')
264 286 return redirect(url('edit_repo', repo_name=changed_name))
265 287
266 @HasPermissionAllDecorator('hg.admin')
288 @HasRepoPermissionAllDecorator('repository.admin')
267 289 def delete(self, repo_name):
268 290 """
269 291 DELETE /repos/repo_name: Delete an existing item"""
270 292 # Forms posted to this method should contain a hidden field:
271 293 # <input type="hidden" name="_method" value="DELETE" />
272 294 # Or using helpers:
273 295 # h.form(url('repo', repo_name=ID),
274 296 # method='delete')
275 297 # url('repo', repo_name=ID)
276 298
277 299 repo_model = RepoModel()
278 300 repo = repo_model.get_by_repo_name(repo_name)
279 301 if not repo:
280 302 h.not_mapped_error(repo_name)
281 303 return redirect(url('repos'))
282 304 try:
305 _forks = repo.forks.count()
306 handle_forks = None
307 if _forks and request.POST.get('forks'):
308 do = request.POST['forks']
309 if do == 'detach_forks':
310 handle_forks = 'detach'
311 h.flash(_('Detached %s forks') % _forks, category='success')
312 elif do == 'delete_forks':
313 handle_forks = 'delete'
314 h.flash(_('Deleted %s forks') % _forks, category='success')
315 repo_model.delete(repo, forks=handle_forks)
283 316 action_logger(self.rhodecode_user, 'admin_deleted_repo',
284 317 repo_name, self.ip_addr, self.sa)
285 repo_model.delete(repo)
286 318 invalidate_cache('get_repo_cached_%s' % repo_name)
287 h.flash(_('deleted repository %s') % repo_name, category='success')
319 h.flash(_('Deleted repository %s') % repo_name, category='success')
288 320 Session().commit()
289 except IntegrityError, e:
290 if e.message.find('repositories_fork_id_fkey') != -1:
291 log.error(traceback.format_exc())
292 h.flash(_('Cannot delete %s it still contains attached '
293 'forks') % repo_name,
294 category='warning')
295 else:
296 log.error(traceback.format_exc())
297 h.flash(_('An error occurred during '
298 'deletion of %s') % repo_name,
299 category='error')
321 except AttachedForksError:
322 h.flash(_('Cannot delete %s it still contains attached forks')
323 % repo_name, category='warning')
300 324
301 except Exception, e:
325 except Exception:
302 326 log.error(traceback.format_exc())
303 327 h.flash(_('An error occurred during deletion of %s') % repo_name,
304 328 category='error')
305 329
306 330 return redirect(url('repos'))
307 331
308 332 @HasRepoPermissionAllDecorator('repository.admin')
333 def set_repo_perm_member(self, repo_name):
334 form = RepoPermsForm()().to_python(request.POST)
335
336 perms_new = form['perms_new']
337 perms_updates = form['perms_updates']
338 cur_repo = repo_name
339
340 # update permissions
341 for member, perm, member_type in perms_updates:
342 if member_type == 'user':
343 # this updates existing one
344 RepoModel().grant_user_permission(
345 repo=cur_repo, user=member, perm=perm
346 )
347 else:
348 RepoModel().grant_users_group_permission(
349 repo=cur_repo, group_name=member, perm=perm
350 )
351 # set new permissions
352 for member, perm, member_type in perms_new:
353 if member_type == 'user':
354 RepoModel().grant_user_permission(
355 repo=cur_repo, user=member, perm=perm
356 )
357 else:
358 RepoModel().grant_users_group_permission(
359 repo=cur_repo, group_name=member, perm=perm
360 )
361 #TODO: implement this
362 #action_logger(self.rhodecode_user, 'admin_changed_repo_permissions',
363 # repo_name, self.ip_addr, self.sa)
364 Session().commit()
365 h.flash(_('Repository permissions updated'), category='success')
366 return redirect(url('edit_repo', repo_name=repo_name))
367
368 @HasRepoPermissionAllDecorator('repository.admin')
309 369 def delete_perm_user(self, repo_name):
310 370 """
311 371 DELETE an existing repository permission user
312 372
313 373 :param repo_name:
314 374 """
315 375 try:
316 376 RepoModel().revoke_user_permission(repo=repo_name,
317 377 user=request.POST['user_id'])
378 #TODO: implement this
379 #action_logger(self.rhodecode_user, 'admin_revoked_repo_permissions',
380 # repo_name, self.ip_addr, self.sa)
318 381 Session().commit()
319 382 except Exception:
320 383 log.error(traceback.format_exc())
321 384 h.flash(_('An error occurred during deletion of repository user'),
322 385 category='error')
323 386 raise HTTPInternalServerError()
324 387
325 388 @HasRepoPermissionAllDecorator('repository.admin')
326 389 def delete_perm_users_group(self, repo_name):
327 390 """
328 DELETE an existing repository permission users group
391 DELETE an existing repository permission user group
329 392
330 393 :param repo_name:
331 394 """
332 395
333 396 try:
334 397 RepoModel().revoke_users_group_permission(
335 398 repo=repo_name, group_name=request.POST['users_group_id']
336 399 )
337 400 Session().commit()
338 401 except Exception:
339 402 log.error(traceback.format_exc())
340 403 h.flash(_('An error occurred during deletion of repository'
341 ' users groups'),
404 ' user groups'),
342 405 category='error')
343 406 raise HTTPInternalServerError()
344 407
345 @HasPermissionAllDecorator('hg.admin')
408 @HasRepoPermissionAllDecorator('repository.admin')
346 409 def repo_stats(self, repo_name):
347 410 """
348 411 DELETE an existing repository statistics
349 412
350 413 :param repo_name:
351 414 """
352 415
353 416 try:
354 417 RepoModel().delete_stats(repo_name)
355 418 Session().commit()
356 419 except Exception, e:
357 420 log.error(traceback.format_exc())
358 421 h.flash(_('An error occurred during deletion of repository stats'),
359 422 category='error')
360 423 return redirect(url('edit_repo', repo_name=repo_name))
361 424
362 @HasPermissionAllDecorator('hg.admin')
425 @HasRepoPermissionAllDecorator('repository.admin')
363 426 def repo_cache(self, repo_name):
364 427 """
365 428 INVALIDATE existing repository cache
366 429
367 430 :param repo_name:
368 431 """
369 432
370 433 try:
371 434 ScmModel().mark_for_invalidation(repo_name)
372 435 Session().commit()
373 436 except Exception, e:
374 437 log.error(traceback.format_exc())
375 438 h.flash(_('An error occurred during cache invalidation'),
376 439 category='error')
377 440 return redirect(url('edit_repo', repo_name=repo_name))
378 441
379 @HasPermissionAllDecorator('hg.admin')
442 @HasRepoPermissionAllDecorator('repository.admin')
380 443 def repo_locking(self, repo_name):
381 444 """
382 445 Unlock repository when it is locked !
383 446
384 447 :param repo_name:
385 448 """
386 449
387 450 try:
388 451 repo = Repository.get_by_repo_name(repo_name)
389 452 if request.POST.get('set_lock'):
390 453 Repository.lock(repo, c.rhodecode_user.user_id)
391 454 elif request.POST.get('set_unlock'):
392 455 Repository.unlock(repo)
393 456 except Exception, e:
394 457 log.error(traceback.format_exc())
395 458 h.flash(_('An error occurred during unlocking'),
396 459 category='error')
397 460 return redirect(url('edit_repo', repo_name=repo_name))
398 461
399 @HasPermissionAllDecorator('hg.admin')
462 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
463 def toggle_locking(self, repo_name):
464 """
465 Toggle locking of repository by simple GET call to url
466
467 :param repo_name:
468 """
469
470 try:
471 repo = Repository.get_by_repo_name(repo_name)
472
473 if repo.enable_locking:
474 if repo.locked[0]:
475 Repository.unlock(repo)
476 action = _('Unlocked')
477 else:
478 Repository.lock(repo, c.rhodecode_user.user_id)
479 action = _('Locked')
480
481 h.flash(_('Repository has been %s') % action,
482 category='success')
483 except Exception, e:
484 log.error(traceback.format_exc())
485 h.flash(_('An error occurred during unlocking'),
486 category='error')
487 return redirect(url('summary_home', repo_name=repo_name))
488
489 @HasRepoPermissionAllDecorator('repository.admin')
400 490 def repo_public_journal(self, repo_name):
401 491 """
402 492 Set's this repository to be visible in public journal,
403 493 in other words assing default user to follow this repo
404 494
405 495 :param repo_name:
406 496 """
407 497
408 498 cur_token = request.POST.get('auth_token')
409 499 token = get_token()
410 500 if cur_token == token:
411 501 try:
412 502 repo_id = Repository.get_by_repo_name(repo_name).repo_id
413 503 user_id = User.get_by_username('default').user_id
414 504 self.scm_model.toggle_following_repo(repo_id, user_id)
415 505 h.flash(_('Updated repository visibility in public journal'),
416 506 category='success')
417 507 Session().commit()
418 except:
508 except Exception:
419 509 h.flash(_('An error occurred during setting this'
420 510 ' repository in public journal'),
421 511 category='error')
422 512
423 513 else:
424 514 h.flash(_('Token mismatch'), category='error')
425 515 return redirect(url('edit_repo', repo_name=repo_name))
426 516
427 @HasPermissionAllDecorator('hg.admin')
517 @HasRepoPermissionAllDecorator('repository.admin')
428 518 def repo_pull(self, repo_name):
429 519 """
430 520 Runs task to update given repository with remote changes,
431 521 ie. make pull on remote location
432 522
433 523 :param repo_name:
434 524 """
435 525 try:
436 526 ScmModel().pull_changes(repo_name, self.rhodecode_user.username)
437 527 h.flash(_('Pulled from remote location'), category='success')
438 528 except Exception, e:
439 529 h.flash(_('An error occurred during pull from remote location'),
440 530 category='error')
441 531
442 532 return redirect(url('edit_repo', repo_name=repo_name))
443 533
444 @HasPermissionAllDecorator('hg.admin')
534 @HasRepoPermissionAllDecorator('repository.admin')
445 535 def repo_as_fork(self, repo_name):
446 536 """
447 537 Mark given repository as a fork of another
448 538
449 539 :param repo_name:
450 540 """
451 541 try:
452 542 fork_id = request.POST.get('id_fork_of')
453 543 repo = ScmModel().mark_as_fork(repo_name, fork_id,
454 544 self.rhodecode_user.username)
455 545 fork = repo.fork.repo_name if repo.fork else _('Nothing')
456 546 Session().commit()
457 547 h.flash(_('Marked repo %s as fork of %s') % (repo_name, fork),
458 548 category='success')
459 549 except Exception, e:
460 550 log.error(traceback.format_exc())
461 551 h.flash(_('An error occurred during this operation'),
462 552 category='error')
463 553
464 554 return redirect(url('edit_repo', repo_name=repo_name))
465 555
466 556 @HasPermissionAllDecorator('hg.admin')
467 557 def show(self, repo_name, format='html'):
468 558 """GET /repos/repo_name: Show a specific item"""
469 559 # url('repo', repo_name=ID)
470 560
471 @HasPermissionAllDecorator('hg.admin')
561 @HasRepoPermissionAllDecorator('repository.admin')
472 562 def edit(self, repo_name, format='html'):
473 563 """GET /repos/repo_name/edit: Form to edit an existing item"""
474 564 # url('edit_repo', repo_name=ID)
475 565 defaults = self.__load_data(repo_name)
476 566
477 567 return htmlfill.render(
478 568 render('admin/repos/repo_edit.html'),
479 569 defaults=defaults,
480 570 encoding="UTF-8",
481 571 force_defaults=False
482 572 )
573
574 @HasPermissionAllDecorator('hg.admin')
575 def create_repo_field(self, repo_name):
576 try:
577 form_result = RepoFieldForm()().to_python(dict(request.POST))
578 new_field = RepositoryField()
579 new_field.repository = Repository.get_by_repo_name(repo_name)
580 new_field.field_key = form_result['new_field_key']
581 new_field.field_type = form_result['new_field_type'] # python type
582 new_field.field_value = form_result['new_field_value'] # set initial blank value
583 new_field.field_desc = form_result['new_field_desc']
584 new_field.field_label = form_result['new_field_label']
585 Session().add(new_field)
586 Session().commit()
587
588 except Exception, e:
589 log.error(traceback.format_exc())
590 msg = _('An error occurred during creation of field')
591 if isinstance(e, formencode.Invalid):
592 msg += ". " + e.msg
593 h.flash(msg, category='error')
594 return redirect(url('edit_repo', repo_name=repo_name))
595
596 @HasPermissionAllDecorator('hg.admin')
597 def delete_repo_field(self, repo_name, field_id):
598 field = RepositoryField.get_or_404(field_id)
599 try:
600 Session().delete(field)
601 Session().commit()
602 except Exception, e:
603 log.error(traceback.format_exc())
604 msg = _('An error occurred during removal of field')
605 h.flash(msg, category='error')
606 return redirect(url('edit_repo', repo_name=repo_name))
@@ -1,330 +1,392 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.repos_groups
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 Repositories groups controller for RhodeCode
6 Repository groups controller for RhodeCode
7 7
8 8 :created_on: Mar 23, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29
30 30 from formencode import htmlfill
31 31
32 32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import abort, redirect
34 34 from pylons.i18n.translation import _
35 35
36 36 from sqlalchemy.exc import IntegrityError
37 37
38 38 import rhodecode
39 39 from rhodecode.lib import helpers as h
40 40 from rhodecode.lib.ext_json import json
41 41 from rhodecode.lib.auth import LoginRequired, HasPermissionAnyDecorator,\
42 HasReposGroupPermissionAnyDecorator
42 HasReposGroupPermissionAnyDecorator, HasReposGroupPermissionAll,\
43 HasPermissionAll
43 44 from rhodecode.lib.base import BaseController, render
44 45 from rhodecode.model.db import RepoGroup, Repository
45 46 from rhodecode.model.repos_group import ReposGroupModel
46 47 from rhodecode.model.forms import ReposGroupForm
47 48 from rhodecode.model.meta import Session
48 49 from rhodecode.model.repo import RepoModel
49 50 from webob.exc import HTTPInternalServerError, HTTPNotFound
50 from rhodecode.lib.utils2 import str2bool
51 from rhodecode.lib.utils2 import str2bool, safe_int
51 52 from sqlalchemy.sql.expression import func
53 from rhodecode.model.scm import GroupList
52 54
53 55 log = logging.getLogger(__name__)
54 56
55 57
56 58 class ReposGroupsController(BaseController):
57 59 """REST Controller styled on the Atom Publishing Protocol"""
58 60 # To properly map this controller, ensure your config/routing.py
59 61 # file has a resource setup:
60 62 # map.resource('repos_group', 'repos_groups')
61 63
62 64 @LoginRequired()
63 65 def __before__(self):
64 66 super(ReposGroupsController, self).__before__()
65 67
66 def __load_defaults(self):
67 c.repo_groups = RepoGroup.groups_choices()
68 def __load_defaults(self, allow_empty_group=False, exclude_group_ids=[]):
69 if HasPermissionAll('hg.admin')('group edit'):
70 #we're global admin, we're ok and we can create TOP level groups
71 allow_empty_group = True
72
73 #override the choices for this form, we need to filter choices
74 #and display only those we have ADMIN right
75 groups_with_admin_rights = GroupList(RepoGroup.query().all(),
76 perm_set=['group.admin'])
77 c.repo_groups = RepoGroup.groups_choices(groups=groups_with_admin_rights,
78 show_empty_group=allow_empty_group)
79 # exclude filtered ids
80 c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids,
81 c.repo_groups)
68 82 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
69
70 83 repo_model = RepoModel()
71 84 c.users_array = repo_model.get_users_js()
72 85 c.users_groups_array = repo_model.get_users_groups_js()
73 86
74 87 def __load_data(self, group_id):
75 88 """
76 89 Load defaults settings for edit, and update
77 90
78 91 :param group_id:
79 92 """
80 self.__load_defaults()
81 93 repo_group = RepoGroup.get_or_404(group_id)
82 94 data = repo_group.get_dict()
83 95 data['group_name'] = repo_group.name
84 96
85 97 # fill repository users
86 98 for p in repo_group.repo_group_to_perm:
87 99 data.update({'u_perm_%s' % p.user.username:
88 100 p.permission.permission_name})
89 101
90 102 # fill repository groups
91 103 for p in repo_group.users_group_to_perm:
92 104 data.update({'g_perm_%s' % p.users_group.users_group_name:
93 105 p.permission.permission_name})
94 106
95 107 return data
96 108
97 @HasPermissionAnyDecorator('hg.admin')
109 def _revoke_perms_on_yourself(self, form_result):
110 _up = filter(lambda u: c.rhodecode_user.username == u[0],
111 form_result['perms_updates'])
112 _new = filter(lambda u: c.rhodecode_user.username == u[0],
113 form_result['perms_new'])
114 if _new and _new[0][1] != 'group.admin' or _up and _up[0][1] != 'group.admin':
115 return True
116 return False
117
98 118 def index(self, format='html'):
99 119 """GET /repos_groups: All items in the collection"""
100 120 # url('repos_groups')
121 group_iter = GroupList(RepoGroup.query().all(), perm_set=['group.admin'])
101 122 sk = lambda g: g.parents[0].group_name if g.parents else g.group_name
102 c.groups = sorted(RepoGroup.query().all(), key=sk)
123 c.groups = sorted(group_iter, key=sk)
103 124 return render('admin/repos_groups/repos_groups_show.html')
104 125
105 @HasPermissionAnyDecorator('hg.admin')
106 126 def create(self):
107 127 """POST /repos_groups: Create a new item"""
108 128 # url('repos_groups')
129
109 130 self.__load_defaults()
131
132 # permissions for can create group based on parent_id are checked
133 # here in the Form
110 134 repos_group_form = ReposGroupForm(available_groups =
111 c.repo_groups_choices)()
135 map(lambda k: unicode(k[0]), c.repo_groups))()
112 136 try:
113 137 form_result = repos_group_form.to_python(dict(request.POST))
114 138 ReposGroupModel().create(
115 139 group_name=form_result['group_name'],
116 140 group_description=form_result['group_description'],
117 parent=form_result['group_parent_id']
141 parent=form_result['group_parent_id'],
142 owner=self.rhodecode_user.user_id
118 143 )
119 144 Session().commit()
120 h.flash(_('created repos group %s') \
145 h.flash(_('Created repository group %s') \
121 146 % form_result['group_name'], category='success')
122 147 #TODO: in futureaction_logger(, '', '', '', self.sa)
123 148 except formencode.Invalid, errors:
124
125 149 return htmlfill.render(
126 150 render('admin/repos_groups/repos_groups_add.html'),
127 151 defaults=errors.value,
128 152 errors=errors.error_dict or {},
129 153 prefix_error=False,
130 154 encoding="UTF-8")
131 155 except Exception:
132 156 log.error(traceback.format_exc())
133 h.flash(_('error occurred during creation of repos group %s') \
157 h.flash(_('Error occurred during creation of repository group %s') \
134 158 % request.POST.get('group_name'), category='error')
159 parent_group_id = form_result['group_parent_id']
160 #TODO: maybe we should get back to the main view, not the admin one
161 return redirect(url('repos_groups', parent_group=parent_group_id))
135 162
136 return redirect(url('repos_groups'))
137
138 @HasPermissionAnyDecorator('hg.admin')
139 163 def new(self, format='html'):
140 164 """GET /repos_groups/new: Form to create a new item"""
141 165 # url('new_repos_group')
166 if HasPermissionAll('hg.admin')('group create'):
167 #we're global admin, we're ok and we can create TOP level groups
168 pass
169 else:
170 # we pass in parent group into creation form, thus we know
171 # what would be the group, we can check perms here !
172 group_id = safe_int(request.GET.get('parent_group'))
173 group = RepoGroup.get(group_id) if group_id else None
174 group_name = group.group_name if group else None
175 if HasReposGroupPermissionAll('group.admin')(group_name, 'group create'):
176 pass
177 else:
178 return abort(403)
179
142 180 self.__load_defaults()
143 181 return render('admin/repos_groups/repos_groups_add.html')
144 182
145 @HasPermissionAnyDecorator('hg.admin')
146 def update(self, id):
147 """PUT /repos_groups/id: Update an existing item"""
183 @HasReposGroupPermissionAnyDecorator('group.admin')
184 def update(self, group_name):
185 """PUT /repos_groups/group_name: Update an existing item"""
148 186 # Forms posted to this method should contain a hidden field:
149 187 # <input type="hidden" name="_method" value="PUT" />
150 188 # Or using helpers:
151 # h.form(url('repos_group', id=ID),
189 # h.form(url('repos_group', group_name=GROUP_NAME),
152 190 # method='put')
153 # url('repos_group', id=ID)
191 # url('repos_group', group_name=GROUP_NAME)
154 192
155 self.__load_defaults()
156 c.repos_group = RepoGroup.get(id)
193 c.repos_group = ReposGroupModel()._get_repos_group(group_name)
194 if HasPermissionAll('hg.admin')('group edit'):
195 #we're global admin, we're ok and we can create TOP level groups
196 allow_empty_group = True
197 elif not c.repos_group.parent_group:
198 allow_empty_group = True
199 else:
200 allow_empty_group = False
201 self.__load_defaults(allow_empty_group=allow_empty_group,
202 exclude_group_ids=[c.repos_group.group_id])
157 203
158 204 repos_group_form = ReposGroupForm(
159 205 edit=True,
160 206 old_data=c.repos_group.get_dict(),
161 available_groups=c.repo_groups_choices
207 available_groups=c.repo_groups_choices,
208 can_create_in_root=allow_empty_group,
162 209 )()
163 210 try:
164 211 form_result = repos_group_form.to_python(dict(request.POST))
165 ReposGroupModel().update(id, form_result)
212 if not c.rhodecode_user.is_admin:
213 if self._revoke_perms_on_yourself(form_result):
214 msg = _('Cannot revoke permission for yourself as admin')
215 h.flash(msg, category='warning')
216 raise Exception('revoke admin permission on self')
217
218 new_gr = ReposGroupModel().update(group_name, form_result)
166 219 Session().commit()
167 h.flash(_('updated repos group %s') \
220 h.flash(_('Updated repository group %s') \
168 221 % form_result['group_name'], category='success')
222 # we now have new name !
223 group_name = new_gr.group_name
169 224 #TODO: in future action_logger(, '', '', '', self.sa)
170 225 except formencode.Invalid, errors:
171 226
172 227 return htmlfill.render(
173 228 render('admin/repos_groups/repos_groups_edit.html'),
174 229 defaults=errors.value,
175 230 errors=errors.error_dict or {},
176 231 prefix_error=False,
177 232 encoding="UTF-8")
178 233 except Exception:
179 234 log.error(traceback.format_exc())
180 h.flash(_('error occurred during update of repos group %s') \
235 h.flash(_('Error occurred during update of repository group %s') \
181 236 % request.POST.get('group_name'), category='error')
182 237
183 return redirect(url('edit_repos_group', id=id))
238 return redirect(url('edit_repos_group', group_name=group_name))
184 239
185 @HasPermissionAnyDecorator('hg.admin')
186 def delete(self, id):
187 """DELETE /repos_groups/id: Delete an existing item"""
240 @HasReposGroupPermissionAnyDecorator('group.admin')
241 def delete(self, group_name):
242 """DELETE /repos_groups/group_name: Delete an existing item"""
188 243 # Forms posted to this method should contain a hidden field:
189 244 # <input type="hidden" name="_method" value="DELETE" />
190 245 # Or using helpers:
191 # h.form(url('repos_group', id=ID),
246 # h.form(url('repos_group', group_name=GROUP_NAME),
192 247 # method='delete')
193 # url('repos_group', id=ID)
248 # url('repos_group', group_name=GROUP_NAME)
194 249
195 gr = RepoGroup.get(id)
250 gr = c.repos_group = ReposGroupModel()._get_repos_group(group_name)
196 251 repos = gr.repositories.all()
197 252 if repos:
198 253 h.flash(_('This group contains %s repositores and cannot be '
199 'deleted') % len(repos),
200 category='error')
254 'deleted') % len(repos), category='warning')
255 return redirect(url('repos_groups'))
256
257 children = gr.children.all()
258 if children:
259 h.flash(_('This group contains %s subgroups and cannot be deleted'
260 % (len(children))), category='warning')
201 261 return redirect(url('repos_groups'))
202 262
203 263 try:
204 ReposGroupModel().delete(id)
264 ReposGroupModel().delete(group_name)
205 265 Session().commit()
206 h.flash(_('removed repos group %s') % gr.group_name,
266 h.flash(_('Removed repository group %s') % group_name,
207 267 category='success')
208 268 #TODO: in future action_logger(, '', '', '', self.sa)
209 except IntegrityError, e:
210 if str(e.message).find('groups_group_parent_id_fkey') != -1:
211 log.error(traceback.format_exc())
212 h.flash(_('Cannot delete this group it still contains '
213 'subgroups'),
214 category='warning')
215 else:
216 log.error(traceback.format_exc())
217 h.flash(_('error occurred during deletion of repos '
218 'group %s') % gr.group_name, category='error')
219
220 269 except Exception:
221 270 log.error(traceback.format_exc())
222 h.flash(_('error occurred during deletion of repos '
223 'group %s') % gr.group_name, category='error')
271 h.flash(_('Error occurred during deletion of repos '
272 'group %s') % group_name, category='error')
224 273
225 274 return redirect(url('repos_groups'))
226 275
227 276 @HasReposGroupPermissionAnyDecorator('group.admin')
228 277 def delete_repos_group_user_perm(self, group_name):
229 278 """
230 DELETE an existing repositories group permission user
279 DELETE an existing repository group permission user
231 280
232 281 :param group_name:
233 282 """
234 283 try:
284 if not c.rhodecode_user.is_admin:
285 if c.rhodecode_user.user_id == safe_int(request.POST['user_id']):
286 msg = _('Cannot revoke permission for yourself as admin')
287 h.flash(msg, category='warning')
288 raise Exception('revoke admin permission on self')
235 289 recursive = str2bool(request.POST.get('recursive', False))
236 290 ReposGroupModel().delete_permission(
237 291 repos_group=group_name, obj=request.POST['user_id'],
238 292 obj_type='user', recursive=recursive
239 293 )
240 294 Session().commit()
241 295 except Exception:
242 296 log.error(traceback.format_exc())
243 297 h.flash(_('An error occurred during deletion of group user'),
244 298 category='error')
245 299 raise HTTPInternalServerError()
246 300
247 301 @HasReposGroupPermissionAnyDecorator('group.admin')
248 302 def delete_repos_group_users_group_perm(self, group_name):
249 303 """
250 DELETE an existing repositories group permission users group
304 DELETE an existing repository group permission user group
251 305
252 306 :param group_name:
253 307 """
254 308
255 309 try:
256 310 recursive = str2bool(request.POST.get('recursive', False))
257 311 ReposGroupModel().delete_permission(
258 312 repos_group=group_name, obj=request.POST['users_group_id'],
259 313 obj_type='users_group', recursive=recursive
260 314 )
261 315 Session().commit()
262 316 except Exception:
263 317 log.error(traceback.format_exc())
264 318 h.flash(_('An error occurred during deletion of group'
265 ' users groups'),
319 ' user groups'),
266 320 category='error')
267 321 raise HTTPInternalServerError()
268 322
269 323 def show_by_name(self, group_name):
270 324 """
271 325 This is a proxy that does a lookup group_name -> id, and shows
272 326 the group by id view instead
273 327 """
274 328 group_name = group_name.rstrip('/')
275 329 id_ = RepoGroup.get_by_group_name(group_name)
276 330 if id_:
277 331 return self.show(id_.group_id)
278 332 raise HTTPNotFound
279 333
280 334 @HasReposGroupPermissionAnyDecorator('group.read', 'group.write',
281 335 'group.admin')
282 def show(self, id, format='html'):
283 """GET /repos_groups/id: Show a specific item"""
284 # url('repos_group', id=ID)
336 def show(self, group_name, format='html'):
337 """GET /repos_groups/group_name: Show a specific item"""
338 # url('repos_group', group_name=GROUP_NAME)
285 339
286 c.group = RepoGroup.get_or_404(id)
340 c.group = c.repos_group = ReposGroupModel()._get_repos_group(group_name)
287 341 c.group_repos = c.group.repositories.all()
288 342
289 343 #overwrite our cached list with current filter
290 344 gr_filter = c.group_repos
291 345 c.repo_cnt = 0
292 346
293 347 groups = RepoGroup.query().order_by(RepoGroup.group_name)\
294 .filter(RepoGroup.group_parent_id == id).all()
348 .filter(RepoGroup.group_parent_id == c.group.group_id).all()
295 349 c.groups = self.scm_model.get_repos_groups(groups)
296 350
297 if c.visual.lightweight_dashboard is False:
351 if not c.visual.lightweight_dashboard:
298 352 c.repos_list = self.scm_model.get_repos(all_repos=gr_filter)
299 353 ## lightweight version of dashboard
300 354 else:
301 355 c.repos_list = Repository.query()\
302 .filter(Repository.group_id == id)\
356 .filter(Repository.group_id == c.group.group_id)\
303 357 .order_by(func.lower(Repository.repo_name))\
304 358 .all()
305 359
306 360 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
307 361 admin=False)
308 362 #json used to render the grid
309 363 c.data = json.dumps(repos_data)
310 364
311 365 return render('admin/repos_groups/repos_groups.html')
312 366
313 @HasPermissionAnyDecorator('hg.admin')
314 def edit(self, id, format='html'):
315 """GET /repos_groups/id/edit: Form to edit an existing item"""
316 # url('edit_repos_group', id=ID)
367 @HasReposGroupPermissionAnyDecorator('group.admin')
368 def edit(self, group_name, format='html'):
369 """GET /repos_groups/group_name/edit: Form to edit an existing item"""
370 # url('edit_repos_group', group_name=GROUP_NAME)
317 371
318 c.repos_group = ReposGroupModel()._get_repos_group(id)
319 defaults = self.__load_data(c.repos_group.group_id)
372 c.repos_group = ReposGroupModel()._get_repos_group(group_name)
373 #we can only allow moving empty group if it's already a top-level
374 #group, ie has no parents, or we're admin
375 if HasPermissionAll('hg.admin')('group edit'):
376 #we're global admin, we're ok and we can create TOP level groups
377 allow_empty_group = True
378 elif not c.repos_group.parent_group:
379 allow_empty_group = True
380 else:
381 allow_empty_group = False
320 382
321 # we need to exclude this group from the group list for editing
322 c.repo_groups = filter(lambda x: x[0] != c.repos_group.group_id,
323 c.repo_groups)
383 self.__load_defaults(allow_empty_group=allow_empty_group,
384 exclude_group_ids=[c.repos_group.group_id])
385 defaults = self.__load_data(c.repos_group.group_id)
324 386
325 387 return htmlfill.render(
326 388 render('admin/repos_groups/repos_groups_edit.html'),
327 389 defaults=defaults,
328 390 encoding="UTF-8",
329 391 force_defaults=False
330 392 )
@@ -1,525 +1,516 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.settings
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 settings controller for rhodecode admin
7 7
8 8 :created_on: Jul 14, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29 import pkg_resources
30 30 import platform
31 31
32 32 from sqlalchemy import func
33 33 from formencode import htmlfill
34 34 from pylons import request, session, tmpl_context as c, url, config
35 35 from pylons.controllers.util import abort, redirect
36 36 from pylons.i18n.translation import _
37 37
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
40 HasPermissionAnyDecorator, NotAnonymous
40 HasPermissionAnyDecorator, NotAnonymous, HasPermissionAny,\
41 HasReposGroupPermissionAll, HasReposGroupPermissionAny, AuthUser
41 42 from rhodecode.lib.base import BaseController, render
42 43 from rhodecode.lib.celerylib import tasks, run_task
43 44 from rhodecode.lib.utils import repo2db_mapper, invalidate_cache, \
44 45 set_rhodecode_config, repo_name_slug, check_git_version
45 46 from rhodecode.model.db import RhodeCodeUi, Repository, RepoGroup, \
46 47 RhodeCodeSetting, PullRequest, PullRequestReviewers
47 48 from rhodecode.model.forms import UserForm, ApplicationSettingsForm, \
48 49 ApplicationUiSettingsForm, ApplicationVisualisationForm
49 from rhodecode.model.scm import ScmModel
50 from rhodecode.model.scm import ScmModel, GroupList
50 51 from rhodecode.model.user import UserModel
51 52 from rhodecode.model.repo import RepoModel
52 53 from rhodecode.model.db import User
53 54 from rhodecode.model.notification import EmailNotificationModel
54 55 from rhodecode.model.meta import Session
55 56 from rhodecode.lib.utils2 import str2bool, safe_unicode
56 57 from rhodecode.lib.compat import json
58 from webob.exc import HTTPForbidden
57 59 log = logging.getLogger(__name__)
58 60
59 61
60 62 class SettingsController(BaseController):
61 63 """REST Controller styled on the Atom Publishing Protocol"""
62 64 # To properly map this controller, ensure your config/routing.py
63 65 # file has a resource setup:
64 66 # map.resource('setting', 'settings', controller='admin/settings',
65 67 # path_prefix='/admin', name_prefix='admin_')
66 68
67 69 @LoginRequired()
68 70 def __before__(self):
69 71 c.admin_user = session.get('admin_user')
70 72 c.admin_username = session.get('admin_username')
71 73 c.modules = sorted([(p.project_name, p.version)
72 74 for p in pkg_resources.working_set]
73 75 + [('git', check_git_version())],
74 76 key=lambda k: k[0].lower())
75 77 c.py_version = platform.python_version()
76 78 c.platform = platform.platform()
77 79 super(SettingsController, self).__before__()
78 80
79 81 @HasPermissionAllDecorator('hg.admin')
80 82 def index(self, format='html'):
81 83 """GET /admin/settings: All items in the collection"""
82 84 # url('admin_settings')
83 85
84 86 defaults = RhodeCodeSetting.get_app_settings()
85 87 defaults.update(self._get_hg_ui_settings())
86 88
87 89 return htmlfill.render(
88 90 render('admin/settings/settings.html'),
89 91 defaults=defaults,
90 92 encoding="UTF-8",
91 93 force_defaults=False
92 94 )
93 95
94 96 @HasPermissionAllDecorator('hg.admin')
95 97 def create(self):
96 98 """POST /admin/settings: Create a new item"""
97 99 # url('admin_settings')
98 100
99 101 @HasPermissionAllDecorator('hg.admin')
100 102 def new(self, format='html'):
101 103 """GET /admin/settings/new: Form to create a new item"""
102 104 # url('admin_new_setting')
103 105
104 106 @HasPermissionAllDecorator('hg.admin')
105 107 def update(self, setting_id):
106 108 """PUT /admin/settings/setting_id: Update an existing item"""
107 109 # Forms posted to this method should contain a hidden field:
108 110 # <input type="hidden" name="_method" value="PUT" />
109 111 # Or using helpers:
110 112 # h.form(url('admin_setting', setting_id=ID),
111 113 # method='put')
112 114 # url('admin_setting', setting_id=ID)
113 115
114 116 if setting_id == 'mapping':
115 117 rm_obsolete = request.POST.get('destroy', False)
116 118 log.debug('Rescanning directories with destroy=%s' % rm_obsolete)
117 119 initial = ScmModel().repo_scan()
118 120 log.debug('invalidating all repositories')
119 121 for repo_name in initial.keys():
120 122 invalidate_cache('get_repo_cached_%s' % repo_name)
121 123
122 124 added, removed = repo2db_mapper(initial, rm_obsolete)
123 125 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
124 126 h.flash(_('Repositories successfully '
125 127 'rescanned added: %s ; removed: %s') %
126 128 (_repr(added), _repr(removed)),
127 129 category='success')
128 130
129 131 if setting_id == 'whoosh':
130 132 repo_location = self._get_hg_ui_settings()['paths_root_path']
131 133 full_index = request.POST.get('full_index', False)
132 134 run_task(tasks.whoosh_index, repo_location, full_index)
133 135 h.flash(_('Whoosh reindex task scheduled'), category='success')
134 136
135 137 if setting_id == 'global':
136 138
137 139 application_form = ApplicationSettingsForm()()
138 140 try:
139 141 form_result = application_form.to_python(dict(request.POST))
140 142 except formencode.Invalid, errors:
141 143 return htmlfill.render(
142 144 render('admin/settings/settings.html'),
143 145 defaults=errors.value,
144 146 errors=errors.error_dict or {},
145 147 prefix_error=False,
146 148 encoding="UTF-8"
147 149 )
148 150
149 151 try:
150 152 sett1 = RhodeCodeSetting.get_by_name_or_create('title')
151 153 sett1.app_settings_value = form_result['rhodecode_title']
152 154 Session().add(sett1)
153 155
154 156 sett2 = RhodeCodeSetting.get_by_name_or_create('realm')
155 157 sett2.app_settings_value = form_result['rhodecode_realm']
156 158 Session().add(sett2)
157 159
158 160 sett3 = RhodeCodeSetting.get_by_name_or_create('ga_code')
159 161 sett3.app_settings_value = form_result['rhodecode_ga_code']
160 162 Session().add(sett3)
161 163
162 164 Session().commit()
163 165 set_rhodecode_config(config)
164 166 h.flash(_('Updated application settings'), category='success')
165 167
166 168 except Exception:
167 169 log.error(traceback.format_exc())
168 h.flash(_('error occurred during updating '
170 h.flash(_('Error occurred during updating '
169 171 'application settings'),
170 172 category='error')
171 173
172 174 if setting_id == 'visual':
173 175
174 176 application_form = ApplicationVisualisationForm()()
175 177 try:
176 178 form_result = application_form.to_python(dict(request.POST))
177 179 except formencode.Invalid, errors:
178 180 return htmlfill.render(
179 181 render('admin/settings/settings.html'),
180 182 defaults=errors.value,
181 183 errors=errors.error_dict or {},
182 184 prefix_error=False,
183 185 encoding="UTF-8"
184 186 )
185 187
186 188 try:
187 189 sett1 = RhodeCodeSetting.get_by_name_or_create('show_public_icon')
188 190 sett1.app_settings_value = \
189 191 form_result['rhodecode_show_public_icon']
190 192 Session().add(sett1)
191 193
192 194 sett2 = RhodeCodeSetting.get_by_name_or_create('show_private_icon')
193 195 sett2.app_settings_value = \
194 196 form_result['rhodecode_show_private_icon']
195 197 Session().add(sett2)
196 198
197 199 sett3 = RhodeCodeSetting.get_by_name_or_create('stylify_metatags')
198 200 sett3.app_settings_value = \
199 201 form_result['rhodecode_stylify_metatags']
200 202 Session().add(sett3)
201 203
202 204 sett4 = RhodeCodeSetting.get_by_name_or_create('lightweight_dashboard')
203 205 sett4.app_settings_value = \
204 206 form_result['rhodecode_lightweight_dashboard']
205 207 Session().add(sett4)
206 208
209 sett4 = RhodeCodeSetting.get_by_name_or_create('repository_fields')
210 sett4.app_settings_value = \
211 form_result['rhodecode_repository_fields']
212 Session().add(sett4)
213
207 214 Session().commit()
208 215 set_rhodecode_config(config)
209 216 h.flash(_('Updated visualisation settings'),
210 217 category='success')
211 218
212 219 except Exception:
213 220 log.error(traceback.format_exc())
214 h.flash(_('error occurred during updating '
221 h.flash(_('Error occurred during updating '
215 222 'visualisation settings'),
216 223 category='error')
217 224
218 225 if setting_id == 'vcs':
219 226 application_form = ApplicationUiSettingsForm()()
220 227 try:
221 228 form_result = application_form.to_python(dict(request.POST))
222 229 except formencode.Invalid, errors:
223 230 return htmlfill.render(
224 231 render('admin/settings/settings.html'),
225 232 defaults=errors.value,
226 233 errors=errors.error_dict or {},
227 234 prefix_error=False,
228 235 encoding="UTF-8"
229 236 )
230 237
231 238 try:
232 # fix namespaces for hooks and extensions
233 _f = lambda s: s.replace('.', '_')
234
235 239 sett = RhodeCodeUi.get_by_key('push_ssl')
236 240 sett.ui_value = form_result['web_push_ssl']
237 241 Session().add(sett)
238 242
239 243 sett = RhodeCodeUi.get_by_key('/')
240 244 sett.ui_value = form_result['paths_root_path']
241 245 Session().add(sett)
242 246
243 247 #HOOKS
244 248 sett = RhodeCodeUi.get_by_key(RhodeCodeUi.HOOK_UPDATE)
245 sett.ui_active = form_result[_f('hooks_%s' %
246 RhodeCodeUi.HOOK_UPDATE)]
249 sett.ui_active = form_result['hooks_changegroup_update']
247 250 Session().add(sett)
248 251
249 252 sett = RhodeCodeUi.get_by_key(RhodeCodeUi.HOOK_REPO_SIZE)
250 sett.ui_active = form_result[_f('hooks_%s' %
251 RhodeCodeUi.HOOK_REPO_SIZE)]
253 sett.ui_active = form_result['hooks_changegroup_repo_size']
252 254 Session().add(sett)
253 255
254 256 sett = RhodeCodeUi.get_by_key(RhodeCodeUi.HOOK_PUSH)
255 sett.ui_active = form_result[_f('hooks_%s' %
256 RhodeCodeUi.HOOK_PUSH)]
257 sett.ui_active = form_result['hooks_changegroup_push_logger']
257 258 Session().add(sett)
258 259
259 260 sett = RhodeCodeUi.get_by_key(RhodeCodeUi.HOOK_PULL)
260 sett.ui_active = form_result[_f('hooks_%s' %
261 RhodeCodeUi.HOOK_PULL)]
261 sett.ui_active = form_result['hooks_outgoing_pull_logger']
262 262
263 263 Session().add(sett)
264 264
265 265 ## EXTENSIONS
266 266 sett = RhodeCodeUi.get_by_key('largefiles')
267 267 if not sett:
268 268 #make one if it's not there !
269 269 sett = RhodeCodeUi()
270 270 sett.ui_key = 'largefiles'
271 271 sett.ui_section = 'extensions'
272 sett.ui_active = form_result[_f('extensions_largefiles')]
272 sett.ui_active = form_result['extensions_largefiles']
273 273 Session().add(sett)
274 274
275 275 sett = RhodeCodeUi.get_by_key('hgsubversion')
276 276 if not sett:
277 277 #make one if it's not there !
278 278 sett = RhodeCodeUi()
279 279 sett.ui_key = 'hgsubversion'
280 280 sett.ui_section = 'extensions'
281 281
282 sett.ui_active = form_result[_f('extensions_hgsubversion')]
282 sett.ui_active = form_result['extensions_hgsubversion']
283 283 Session().add(sett)
284 284
285 285 # sett = RhodeCodeUi.get_by_key('hggit')
286 286 # if not sett:
287 287 # #make one if it's not there !
288 288 # sett = RhodeCodeUi()
289 289 # sett.ui_key = 'hggit'
290 290 # sett.ui_section = 'extensions'
291 291 #
292 # sett.ui_active = form_result[_f('extensions_hggit')]
292 # sett.ui_active = form_result['extensions_hggit']
293 293 # Session().add(sett)
294 294
295 295 Session().commit()
296 296
297 297 h.flash(_('Updated VCS settings'), category='success')
298 298
299 299 except Exception:
300 300 log.error(traceback.format_exc())
301 h.flash(_('error occurred during updating '
301 h.flash(_('Error occurred during updating '
302 302 'application settings'), category='error')
303 303
304 304 if setting_id == 'hooks':
305 305 ui_key = request.POST.get('new_hook_ui_key')
306 306 ui_value = request.POST.get('new_hook_ui_value')
307 307 try:
308 308
309 309 if ui_value and ui_key:
310 310 RhodeCodeUi.create_or_update_hook(ui_key, ui_value)
311 311 h.flash(_('Added new hook'),
312 312 category='success')
313 313
314 314 # check for edits
315 315 update = False
316 316 _d = request.POST.dict_of_lists()
317 317 for k, v in zip(_d.get('hook_ui_key', []),
318 318 _d.get('hook_ui_value_new', [])):
319 319 RhodeCodeUi.create_or_update_hook(k, v)
320 320 update = True
321 321
322 322 if update:
323 323 h.flash(_('Updated hooks'), category='success')
324 324 Session().commit()
325 325 except Exception:
326 326 log.error(traceback.format_exc())
327 h.flash(_('error occurred during hook creation'),
327 h.flash(_('Error occurred during hook creation'),
328 328 category='error')
329 329
330 330 return redirect(url('admin_edit_setting', setting_id='hooks'))
331 331
332 332 if setting_id == 'email':
333 333 test_email = request.POST.get('test_email')
334 334 test_email_subj = 'RhodeCode TestEmail'
335 335 test_email_body = 'RhodeCode Email test'
336 336
337 337 test_email_html_body = EmailNotificationModel()\
338 338 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
339 339 body=test_email_body)
340 340
341 341 recipients = [test_email] if test_email else None
342 342
343 343 run_task(tasks.send_email, recipients, test_email_subj,
344 344 test_email_body, test_email_html_body)
345 345
346 346 h.flash(_('Email task created'), category='success')
347 347 return redirect(url('admin_settings'))
348 348
349 349 @HasPermissionAllDecorator('hg.admin')
350 350 def delete(self, setting_id):
351 351 """DELETE /admin/settings/setting_id: Delete an existing item"""
352 352 # Forms posted to this method should contain a hidden field:
353 353 # <input type="hidden" name="_method" value="DELETE" />
354 354 # Or using helpers:
355 355 # h.form(url('admin_setting', setting_id=ID),
356 356 # method='delete')
357 357 # url('admin_setting', setting_id=ID)
358 358 if setting_id == 'hooks':
359 359 hook_id = request.POST.get('hook_id')
360 360 RhodeCodeUi.delete(hook_id)
361 361 Session().commit()
362 362
363 363 @HasPermissionAllDecorator('hg.admin')
364 364 def show(self, setting_id, format='html'):
365 365 """
366 366 GET /admin/settings/setting_id: Show a specific item"""
367 367 # url('admin_setting', setting_id=ID)
368 368
369 369 @HasPermissionAllDecorator('hg.admin')
370 370 def edit(self, setting_id, format='html'):
371 371 """
372 372 GET /admin/settings/setting_id/edit: Form to
373 373 edit an existing item"""
374 374 # url('admin_edit_setting', setting_id=ID)
375 375
376 376 c.hooks = RhodeCodeUi.get_builtin_hooks()
377 377 c.custom_hooks = RhodeCodeUi.get_custom_hooks()
378 378
379 379 return htmlfill.render(
380 380 render('admin/settings/hooks.html'),
381 381 defaults={},
382 382 encoding="UTF-8",
383 383 force_defaults=False
384 384 )
385 385
386 386 def _load_my_repos_data(self):
387 387 repos_list = Session().query(Repository)\
388 388 .filter(Repository.user_id ==
389 389 self.rhodecode_user.user_id)\
390 390 .order_by(func.lower(Repository.repo_name)).all()
391 391
392 392 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
393 393 admin=True)
394 394 #json used to render the grid
395 395 return json.dumps(repos_data)
396 396
397 397 @NotAnonymous()
398 398 def my_account(self):
399 399 """
400 400 GET /_admin/my_account Displays info about my account
401 401 """
402 402 # url('admin_settings_my_account')
403 403
404 404 c.user = User.get(self.rhodecode_user.user_id)
405 c.perm_user = AuthUser(user_id=self.rhodecode_user.user_id,
406 ip_addr=self.ip_addr)
405 407 c.ldap_dn = c.user.ldap_dn
406 408
407 409 if c.user.username == 'default':
408 410 h.flash(_("You can't edit this user since it's"
409 411 " crucial for entire application"), category='warning')
410 412 return redirect(url('users'))
411 413
412 414 #json used to render the grid
413 415 c.data = self._load_my_repos_data()
414 416
415 417 defaults = c.user.get_dict()
416 418
417 419 c.form = htmlfill.render(
418 420 render('admin/users/user_edit_my_account_form.html'),
419 421 defaults=defaults,
420 422 encoding="UTF-8",
421 423 force_defaults=False
422 424 )
423 425 return render('admin/users/user_edit_my_account.html')
424 426
425 427 @NotAnonymous()
426 428 def my_account_update(self):
427 429 """PUT /_admin/my_account_update: Update an existing item"""
428 430 # Forms posted to this method should contain a hidden field:
429 431 # <input type="hidden" name="_method" value="PUT" />
430 432 # Or using helpers:
431 433 # h.form(url('admin_settings_my_account_update'),
432 434 # method='put')
433 435 # url('admin_settings_my_account_update', id=ID)
434 436 uid = self.rhodecode_user.user_id
435 437 c.user = User.get(self.rhodecode_user.user_id)
438 c.perm_user = AuthUser(user_id=self.rhodecode_user.user_id,
439 ip_addr=self.ip_addr)
436 440 c.ldap_dn = c.user.ldap_dn
437 441 email = self.rhodecode_user.email
438 442 _form = UserForm(edit=True,
439 443 old_data={'user_id': uid, 'email': email})()
440 444 form_result = {}
441 445 try:
442 446 form_result = _form.to_python(dict(request.POST))
443 447 skip_attrs = ['admin', 'active'] # skip attr for my account
444 448 if c.ldap_dn:
445 449 #forbid updating username for ldap accounts
446 450 skip_attrs.append('username')
447 451 UserModel().update(uid, form_result, skip_attrs=skip_attrs)
448 452 h.flash(_('Your account was updated successfully'),
449 453 category='success')
450 454 Session().commit()
451 455 except formencode.Invalid, errors:
452 456 #json used to render the grid
453 457 c.data = self._load_my_repos_data()
454 458 c.form = htmlfill.render(
455 459 render('admin/users/user_edit_my_account_form.html'),
456 460 defaults=errors.value,
457 461 errors=errors.error_dict or {},
458 462 prefix_error=False,
459 463 encoding="UTF-8")
460 464 return render('admin/users/user_edit_my_account.html')
461 465 except Exception:
462 466 log.error(traceback.format_exc())
463 h.flash(_('error occurred during update of user %s') \
467 h.flash(_('Error occurred during update of user %s') \
464 468 % form_result.get('username'), category='error')
465 469
466 470 return redirect(url('my_account'))
467 471
468 472 @NotAnonymous()
469 473 def my_account_my_pullrequests(self):
470 c.my_pull_requests = PullRequest.query()\
474 c.show_closed = request.GET.get('pr_show_closed')
475
476 def _filter(pr):
477 s = sorted(pr, key=lambda o: o.created_on, reverse=True)
478 if not c.show_closed:
479 s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
480 return s
481
482 c.my_pull_requests = _filter(PullRequest.query()\
471 483 .filter(PullRequest.user_id ==
472 484 self.rhodecode_user.user_id)\
473 .all()
474 c.participate_in_pull_requests = \
475 [x.pull_request for x in PullRequestReviewers.query()\
476 .filter(PullRequestReviewers.user_id ==
477 self.rhodecode_user.user_id)\
478 .all()]
479 return render('admin/users/user_edit_my_account_pullrequests.html')
480
481 @NotAnonymous()
482 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
483 def create_repository(self):
484 """GET /_admin/create_repository: Form to create a new item"""
485 .all())
485 486
486 c.repo_groups = RepoGroup.groups_choices(check_perms=True)
487 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
488 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
489
490 new_repo = request.GET.get('repo', '')
491 c.new_repo = repo_name_slug(new_repo)
487 c.participate_in_pull_requests = _filter([
488 x.pull_request for x in PullRequestReviewers.query()\
489 .filter(PullRequestReviewers.user_id ==
490 self.rhodecode_user.user_id).all()])
492 491
493 ## apply the defaults from defaults page
494 defaults = RhodeCodeSetting.get_default_repo_settings(strip_prefix=True)
495 return htmlfill.render(
496 render('admin/repos/repo_add_create_repository.html'),
497 defaults=defaults,
498 errors={},
499 prefix_error=False,
500 encoding="UTF-8"
501 )
492 return render('admin/users/user_edit_my_account_pullrequests.html')
502 493
503 494 def _get_hg_ui_settings(self):
504 495 ret = RhodeCodeUi.query().all()
505 496
506 497 if not ret:
507 498 raise Exception('Could not get application ui settings !')
508 499 settings = {}
509 500 for each in ret:
510 501 k = each.ui_key
511 502 v = each.ui_value
512 503 if k == '/':
513 504 k = 'root_path'
514 505
515 506 if k == 'push_ssl':
516 507 v = str2bool(v)
517 508
518 509 if k.find('.') != -1:
519 510 k = k.replace('.', '_')
520 511
521 512 if each.ui_section in ['hooks', 'extensions']:
522 513 v = each.ui_active
523 514
524 515 settings[each.ui_section + '_' + k] = v
525 516 return settings
@@ -1,362 +1,362 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.users
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Users crud controller for pylons
7 7
8 8 :created_on: Apr 4, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29 from pylons import response
30 30
31 31 from formencode import htmlfill
32 32 from pylons import request, session, tmpl_context as c, url, config
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35
36 36 import rhodecode
37 37 from rhodecode.lib.exceptions import DefaultUserException, \
38 38 UserOwnsReposException
39 39 from rhodecode.lib import helpers as h
40 40 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
41 41 AuthUser
42 42 from rhodecode.lib.base import BaseController, render
43 43
44 44 from rhodecode.model.db import User, UserEmailMap, UserIpMap
45 45 from rhodecode.model.forms import UserForm
46 46 from rhodecode.model.user import UserModel
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.lib.utils import action_logger
49 49 from rhodecode.lib.compat import json
50 50 from rhodecode.lib.utils2 import datetime_to_time, str2bool
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class UsersController(BaseController):
56 56 """REST Controller styled on the Atom Publishing Protocol"""
57 57 # To properly map this controller, ensure your config/routing.py
58 58 # file has a resource setup:
59 59 # map.resource('user', 'users')
60 60
61 61 @LoginRequired()
62 62 @HasPermissionAllDecorator('hg.admin')
63 63 def __before__(self):
64 64 c.admin_user = session.get('admin_user')
65 65 c.admin_username = session.get('admin_username')
66 66 super(UsersController, self).__before__()
67 67 c.available_permissions = config['available_permissions']
68 68
69 69 def index(self, format='html'):
70 70 """GET /users: All items in the collection"""
71 71 # url('users')
72 72
73 73 c.users_list = User.query().order_by(User.username).all()
74 74
75 75 users_data = []
76 76 total_records = len(c.users_list)
77 77 _tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
78 78 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
79 79
80 80 grav_tmpl = lambda user_email, size: (
81 81 template.get_def("user_gravatar")
82 82 .render(user_email, size, _=_, h=h, c=c))
83 83
84 84 user_lnk = lambda user_id, username: (
85 85 template.get_def("user_name")
86 86 .render(user_id, username, _=_, h=h, c=c))
87 87
88 88 user_actions = lambda user_id, username: (
89 89 template.get_def("user_actions")
90 90 .render(user_id, username, _=_, h=h, c=c))
91 91
92 92 for user in c.users_list:
93 93
94 94 users_data.append({
95 95 "gravatar": grav_tmpl(user. email, 24),
96 96 "raw_username": user.username,
97 97 "username": user_lnk(user.user_id, user.username),
98 98 "firstname": user.name,
99 99 "lastname": user.lastname,
100 100 "last_login": h.fmt_date(user.last_login),
101 101 "last_login_raw": datetime_to_time(user.last_login),
102 "active": h.bool2icon(user.active),
103 "admin": h.bool2icon(user.admin),
104 "ldap": h.bool2icon(bool(user.ldap_dn)),
102 "active": h.boolicon(user.active),
103 "admin": h.boolicon(user.admin),
104 "ldap": h.boolicon(bool(user.ldap_dn)),
105 105 "action": user_actions(user.user_id, user.username),
106 106 })
107 107
108 108 c.data = json.dumps({
109 109 "totalRecords": total_records,
110 110 "startIndex": 0,
111 111 "sort": None,
112 112 "dir": "asc",
113 113 "records": users_data
114 114 })
115 115
116 116 return render('admin/users/users.html')
117 117
118 118 def create(self):
119 119 """POST /users: Create a new item"""
120 120 # url('users')
121 121
122 122 user_model = UserModel()
123 123 user_form = UserForm()()
124 124 try:
125 125 form_result = user_form.to_python(dict(request.POST))
126 126 user_model.create(form_result)
127 127 usr = form_result['username']
128 128 action_logger(self.rhodecode_user, 'admin_created_user:%s' % usr,
129 129 None, self.ip_addr, self.sa)
130 h.flash(_('created user %s') % usr,
130 h.flash(_('Created user %s') % usr,
131 131 category='success')
132 132 Session().commit()
133 133 except formencode.Invalid, errors:
134 134 return htmlfill.render(
135 135 render('admin/users/user_add.html'),
136 136 defaults=errors.value,
137 137 errors=errors.error_dict or {},
138 138 prefix_error=False,
139 139 encoding="UTF-8")
140 140 except Exception:
141 141 log.error(traceback.format_exc())
142 h.flash(_('error occurred during creation of user %s') \
142 h.flash(_('Error occurred during creation of user %s') \
143 143 % request.POST.get('username'), category='error')
144 144 return redirect(url('users'))
145 145
146 146 def new(self, format='html'):
147 147 """GET /users/new: Form to create a new item"""
148 148 # url('new_user')
149 149 return render('admin/users/user_add.html')
150 150
151 151 def update(self, id):
152 152 """PUT /users/id: Update an existing item"""
153 153 # Forms posted to this method should contain a hidden field:
154 154 # <input type="hidden" name="_method" value="PUT" />
155 155 # Or using helpers:
156 156 # h.form(url('update_user', id=ID),
157 157 # method='put')
158 158 # url('user', id=ID)
159 159 user_model = UserModel()
160 160 c.user = user_model.get(id)
161 161 c.ldap_dn = c.user.ldap_dn
162 162 c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr)
163 163 _form = UserForm(edit=True, old_data={'user_id': id,
164 164 'email': c.user.email})()
165 165 form_result = {}
166 166 try:
167 167 form_result = _form.to_python(dict(request.POST))
168 168 skip_attrs = []
169 169 if c.ldap_dn:
170 170 #forbid updating username for ldap accounts
171 171 skip_attrs = ['username']
172 172 user_model.update(id, form_result, skip_attrs=skip_attrs)
173 173 usr = form_result['username']
174 174 action_logger(self.rhodecode_user, 'admin_updated_user:%s' % usr,
175 175 None, self.ip_addr, self.sa)
176 176 h.flash(_('User updated successfully'), category='success')
177 177 Session().commit()
178 178 except formencode.Invalid, errors:
179 179 c.user_email_map = UserEmailMap.query()\
180 180 .filter(UserEmailMap.user == c.user).all()
181 181 c.user_ip_map = UserIpMap.query()\
182 182 .filter(UserIpMap.user == c.user).all()
183 183 defaults = errors.value
184 184 e = errors.error_dict or {}
185 185 defaults.update({
186 186 'create_repo_perm': user_model.has_perm(id, 'hg.create.repository'),
187 187 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
188 188 '_method': 'put'
189 189 })
190 190 return htmlfill.render(
191 191 render('admin/users/user_edit.html'),
192 192 defaults=defaults,
193 193 errors=e,
194 194 prefix_error=False,
195 195 encoding="UTF-8")
196 196 except Exception:
197 197 log.error(traceback.format_exc())
198 h.flash(_('error occurred during update of user %s') \
198 h.flash(_('Error occurred during update of user %s') \
199 199 % form_result.get('username'), category='error')
200 200 return redirect(url('edit_user', id=id))
201 201
202 202 def delete(self, id):
203 203 """DELETE /users/id: Delete an existing item"""
204 204 # Forms posted to this method should contain a hidden field:
205 205 # <input type="hidden" name="_method" value="DELETE" />
206 206 # Or using helpers:
207 207 # h.form(url('delete_user', id=ID),
208 208 # method='delete')
209 209 # url('user', id=ID)
210 210 usr = User.get_or_404(id)
211 211 try:
212 212 UserModel().delete(usr)
213 213 Session().commit()
214 h.flash(_('successfully deleted user'), category='success')
214 h.flash(_('Successfully deleted user'), category='success')
215 215 except (UserOwnsReposException, DefaultUserException), e:
216 216 h.flash(e, category='warning')
217 217 except Exception:
218 218 log.error(traceback.format_exc())
219 219 h.flash(_('An error occurred during deletion of user'),
220 220 category='error')
221 221 return redirect(url('users'))
222 222
223 223 def show(self, id, format='html'):
224 224 """GET /users/id: Show a specific item"""
225 225 # url('user', id=ID)
226 226
227 227 def edit(self, id, format='html'):
228 228 """GET /users/id/edit: Form to edit an existing item"""
229 229 # url('edit_user', id=ID)
230 230 c.user = User.get_or_404(id)
231 231
232 232 if c.user.username == 'default':
233 233 h.flash(_("You can't edit this user"), category='warning')
234 234 return redirect(url('users'))
235 235
236 236 c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr)
237 237 c.user.permissions = {}
238 238 c.granted_permissions = UserModel().fill_perms(c.user)\
239 239 .permissions['global']
240 240 c.user_email_map = UserEmailMap.query()\
241 241 .filter(UserEmailMap.user == c.user).all()
242 242 c.user_ip_map = UserIpMap.query()\
243 243 .filter(UserIpMap.user == c.user).all()
244 244 user_model = UserModel()
245 245 c.ldap_dn = c.user.ldap_dn
246 246 defaults = c.user.get_dict()
247 247 defaults.update({
248 248 'create_repo_perm': user_model.has_perm(id, 'hg.create.repository'),
249 249 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
250 250 })
251 251
252 252 return htmlfill.render(
253 253 render('admin/users/user_edit.html'),
254 254 defaults=defaults,
255 255 encoding="UTF-8",
256 256 force_defaults=False
257 257 )
258 258
259 259 def update_perm(self, id):
260 260 """PUT /users_perm/id: Update an existing item"""
261 261 # url('user_perm', id=ID, method='put')
262 262 usr = User.get_or_404(id)
263 263 grant_create_perm = str2bool(request.POST.get('create_repo_perm'))
264 264 grant_fork_perm = str2bool(request.POST.get('fork_repo_perm'))
265 265 inherit_perms = str2bool(request.POST.get('inherit_default_permissions'))
266 266
267 267 user_model = UserModel()
268 268
269 269 try:
270 270 usr.inherit_default_permissions = inherit_perms
271 271 Session().add(usr)
272 272
273 273 if grant_create_perm:
274 274 user_model.revoke_perm(usr, 'hg.create.none')
275 275 user_model.grant_perm(usr, 'hg.create.repository')
276 276 h.flash(_("Granted 'repository create' permission to user"),
277 277 category='success')
278 278 else:
279 279 user_model.revoke_perm(usr, 'hg.create.repository')
280 280 user_model.grant_perm(usr, 'hg.create.none')
281 281 h.flash(_("Revoked 'repository create' permission to user"),
282 282 category='success')
283 283
284 284 if grant_fork_perm:
285 285 user_model.revoke_perm(usr, 'hg.fork.none')
286 286 user_model.grant_perm(usr, 'hg.fork.repository')
287 287 h.flash(_("Granted 'repository fork' permission to user"),
288 288 category='success')
289 289 else:
290 290 user_model.revoke_perm(usr, 'hg.fork.repository')
291 291 user_model.grant_perm(usr, 'hg.fork.none')
292 292 h.flash(_("Revoked 'repository fork' permission to user"),
293 293 category='success')
294 294
295 295 Session().commit()
296 296 except Exception:
297 297 log.error(traceback.format_exc())
298 298 h.flash(_('An error occurred during permissions saving'),
299 299 category='error')
300 300 return redirect(url('edit_user', id=id))
301 301
302 302 def add_email(self, id):
303 303 """POST /user_emails:Add an existing item"""
304 304 # url('user_emails', id=ID, method='put')
305 305
306 306 email = request.POST.get('new_email')
307 307 user_model = UserModel()
308 308
309 309 try:
310 310 user_model.add_extra_email(id, email)
311 311 Session().commit()
312 312 h.flash(_("Added email %s to user") % email, category='success')
313 313 except formencode.Invalid, error:
314 314 msg = error.error_dict['email']
315 315 h.flash(msg, category='error')
316 316 except Exception:
317 317 log.error(traceback.format_exc())
318 318 h.flash(_('An error occurred during email saving'),
319 319 category='error')
320 320 return redirect(url('edit_user', id=id))
321 321
322 322 def delete_email(self, id):
323 323 """DELETE /user_emails_delete/id: Delete an existing item"""
324 324 # url('user_emails_delete', id=ID, method='delete')
325 325 user_model = UserModel()
326 326 user_model.delete_extra_email(id, request.POST.get('del_email'))
327 327 Session().commit()
328 328 h.flash(_("Removed email from user"), category='success')
329 329 return redirect(url('edit_user', id=id))
330 330
331 331 def add_ip(self, id):
332 332 """POST /user_ips:Add an existing item"""
333 333 # url('user_ips', id=ID, method='put')
334 334
335 335 ip = request.POST.get('new_ip')
336 336 user_model = UserModel()
337 337
338 338 try:
339 339 user_model.add_extra_ip(id, ip)
340 340 Session().commit()
341 341 h.flash(_("Added ip %s to user") % ip, category='success')
342 342 except formencode.Invalid, error:
343 343 msg = error.error_dict['ip']
344 344 h.flash(msg, category='error')
345 345 except Exception:
346 346 log.error(traceback.format_exc())
347 347 h.flash(_('An error occurred during ip saving'),
348 348 category='error')
349 349 if 'default_user' in request.POST:
350 350 return redirect(url('edit_permission', id='default'))
351 351 return redirect(url('edit_user', id=id))
352 352
353 353 def delete_ip(self, id):
354 354 """DELETE /user_ips_delete/id: Delete an existing item"""
355 355 # url('user_ips_delete', id=ID, method='delete')
356 356 user_model = UserModel()
357 357 user_model.delete_extra_ip(id, request.POST.get('del_ip'))
358 358 Session().commit()
359 359 h.flash(_("Removed ip from user"), category='success')
360 360 if 'default_user' in request.POST:
361 361 return redirect(url('edit_permission', id='default'))
362 362 return redirect(url('edit_user', id=id))
@@ -1,282 +1,284 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.users_groups
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 Users Groups crud controller for pylons
6 User Groups crud controller for pylons
7 7
8 8 :created_on: Jan 25, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28 import formencode
29 29
30 30 from formencode import htmlfill
31 31 from pylons import request, session, tmpl_context as c, url, config
32 32 from pylons.controllers.util import abort, redirect
33 33 from pylons.i18n.translation import _
34 34
35 35 from rhodecode.lib import helpers as h
36 from rhodecode.lib.exceptions import UsersGroupsAssignedException
36 from rhodecode.lib.exceptions import UserGroupsAssignedException
37 37 from rhodecode.lib.utils2 import safe_unicode, str2bool
38 38 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
39 39 from rhodecode.lib.base import BaseController, render
40 40
41 from rhodecode.model.users_group import UsersGroupModel
41 from rhodecode.model.users_group import UserGroupModel
42 42
43 from rhodecode.model.db import User, UsersGroup, UsersGroupToPerm,\
44 UsersGroupRepoToPerm, UsersGroupRepoGroupToPerm
45 from rhodecode.model.forms import UsersGroupForm
43 from rhodecode.model.db import User, UserGroup, UserGroupToPerm,\
44 UserGroupRepoToPerm, UserGroupRepoGroupToPerm
45 from rhodecode.model.forms import UserGroupForm
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.lib.utils import action_logger
48 48 from sqlalchemy.orm import joinedload
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class UsersGroupsController(BaseController):
54 54 """REST Controller styled on the Atom Publishing Protocol"""
55 55 # To properly map this controller, ensure your config/routing.py
56 56 # file has a resource setup:
57 57 # map.resource('users_group', 'users_groups')
58 58
59 59 @LoginRequired()
60 60 @HasPermissionAllDecorator('hg.admin')
61 61 def __before__(self):
62 62 c.admin_user = session.get('admin_user')
63 63 c.admin_username = session.get('admin_username')
64 64 super(UsersGroupsController, self).__before__()
65 65 c.available_permissions = config['available_permissions']
66 66
67 67 def index(self, format='html'):
68 68 """GET /users_groups: All items in the collection"""
69 69 # url('users_groups')
70 c.users_groups_list = UsersGroup().query().all()
70 c.users_groups_list = UserGroup().query().all()
71 71 return render('admin/users_groups/users_groups.html')
72 72
73 73 def create(self):
74 74 """POST /users_groups: Create a new item"""
75 75 # url('users_groups')
76 76
77 users_group_form = UsersGroupForm()()
77 users_group_form = UserGroupForm()()
78 78 try:
79 79 form_result = users_group_form.to_python(dict(request.POST))
80 UsersGroupModel().create(name=form_result['users_group_name'],
80 UserGroupModel().create(name=form_result['users_group_name'],
81 81 active=form_result['users_group_active'])
82 82 gr = form_result['users_group_name']
83 83 action_logger(self.rhodecode_user,
84 84 'admin_created_users_group:%s' % gr,
85 85 None, self.ip_addr, self.sa)
86 h.flash(_('created users group %s') % gr, category='success')
86 h.flash(_('Created user group %s') % gr, category='success')
87 87 Session().commit()
88 88 except formencode.Invalid, errors:
89 89 return htmlfill.render(
90 90 render('admin/users_groups/users_group_add.html'),
91 91 defaults=errors.value,
92 92 errors=errors.error_dict or {},
93 93 prefix_error=False,
94 94 encoding="UTF-8")
95 95 except Exception:
96 96 log.error(traceback.format_exc())
97 h.flash(_('error occurred during creation of users group %s') \
97 h.flash(_('Error occurred during creation of user group %s') \
98 98 % request.POST.get('users_group_name'), category='error')
99 99
100 100 return redirect(url('users_groups'))
101 101
102 102 def new(self, format='html'):
103 103 """GET /users_groups/new: Form to create a new item"""
104 104 # url('new_users_group')
105 105 return render('admin/users_groups/users_group_add.html')
106 106
107 107 def _load_data(self, id):
108 108 c.users_group.permissions = {
109 109 'repositories': {},
110 110 'repositories_groups': {}
111 111 }
112 112
113 ugroup_repo_perms = UsersGroupRepoToPerm.query()\
114 .options(joinedload(UsersGroupRepoToPerm.permission))\
115 .options(joinedload(UsersGroupRepoToPerm.repository))\
116 .filter(UsersGroupRepoToPerm.users_group_id == id)\
113 ugroup_repo_perms = UserGroupRepoToPerm.query()\
114 .options(joinedload(UserGroupRepoToPerm.permission))\
115 .options(joinedload(UserGroupRepoToPerm.repository))\
116 .filter(UserGroupRepoToPerm.users_group_id == id)\
117 117 .all()
118 118
119 119 for gr in ugroup_repo_perms:
120 120 c.users_group.permissions['repositories'][gr.repository.repo_name] \
121 121 = gr.permission.permission_name
122 122
123 ugroup_group_perms = UsersGroupRepoGroupToPerm.query()\
124 .options(joinedload(UsersGroupRepoGroupToPerm.permission))\
125 .options(joinedload(UsersGroupRepoGroupToPerm.group))\
126 .filter(UsersGroupRepoGroupToPerm.users_group_id == id)\
123 ugroup_group_perms = UserGroupRepoGroupToPerm.query()\
124 .options(joinedload(UserGroupRepoGroupToPerm.permission))\
125 .options(joinedload(UserGroupRepoGroupToPerm.group))\
126 .filter(UserGroupRepoGroupToPerm.users_group_id == id)\
127 127 .all()
128 128
129 129 for gr in ugroup_group_perms:
130 130 c.users_group.permissions['repositories_groups'][gr.group.group_name] \
131 131 = gr.permission.permission_name
132 132
133 c.group_members_obj = [x.user for x in c.users_group.members]
133 c.group_members_obj = sorted((x.user for x in c.users_group.members),
134 key=lambda u: u.username.lower())
134 135 c.group_members = [(x.user_id, x.username) for x in
135 136 c.group_members_obj]
136 c.available_members = [(x.user_id, x.username) for x in
137 User.query().all()]
137 c.available_members = sorted(((x.user_id, x.username) for x in
138 User.query().all()),
139 key=lambda u: u[1].lower())
138 140
139 141 def update(self, id):
140 142 """PUT /users_groups/id: Update an existing item"""
141 143 # Forms posted to this method should contain a hidden field:
142 144 # <input type="hidden" name="_method" value="PUT" />
143 145 # Or using helpers:
144 146 # h.form(url('users_group', id=ID),
145 147 # method='put')
146 148 # url('users_group', id=ID)
147 149
148 c.users_group = UsersGroup.get_or_404(id)
150 c.users_group = UserGroup.get_or_404(id)
149 151 self._load_data(id)
150 152
151 153 available_members = [safe_unicode(x[0]) for x in c.available_members]
152 154
153 users_group_form = UsersGroupForm(edit=True,
155 users_group_form = UserGroupForm(edit=True,
154 156 old_data=c.users_group.get_dict(),
155 157 available_members=available_members)()
156 158
157 159 try:
158 160 form_result = users_group_form.to_python(request.POST)
159 UsersGroupModel().update(c.users_group, form_result)
161 UserGroupModel().update(c.users_group, form_result)
160 162 gr = form_result['users_group_name']
161 163 action_logger(self.rhodecode_user,
162 164 'admin_updated_users_group:%s' % gr,
163 165 None, self.ip_addr, self.sa)
164 h.flash(_('updated users group %s') % gr, category='success')
166 h.flash(_('Updated user group %s') % gr, category='success')
165 167 Session().commit()
166 168 except formencode.Invalid, errors:
167 ug_model = UsersGroupModel()
169 ug_model = UserGroupModel()
168 170 defaults = errors.value
169 171 e = errors.error_dict or {}
170 172 defaults.update({
171 173 'create_repo_perm': ug_model.has_perm(id,
172 174 'hg.create.repository'),
173 175 'fork_repo_perm': ug_model.has_perm(id,
174 176 'hg.fork.repository'),
175 177 '_method': 'put'
176 178 })
177 179
178 180 return htmlfill.render(
179 181 render('admin/users_groups/users_group_edit.html'),
180 182 defaults=defaults,
181 183 errors=e,
182 184 prefix_error=False,
183 185 encoding="UTF-8")
184 186 except Exception:
185 187 log.error(traceback.format_exc())
186 h.flash(_('error occurred during update of users group %s') \
188 h.flash(_('Error occurred during update of user group %s') \
187 189 % request.POST.get('users_group_name'), category='error')
188 190
189 191 return redirect(url('edit_users_group', id=id))
190 192
191 193 def delete(self, id):
192 194 """DELETE /users_groups/id: Delete an existing item"""
193 195 # Forms posted to this method should contain a hidden field:
194 196 # <input type="hidden" name="_method" value="DELETE" />
195 197 # Or using helpers:
196 198 # h.form(url('users_group', id=ID),
197 199 # method='delete')
198 200 # url('users_group', id=ID)
199 usr_gr = UsersGroup.get_or_404(id)
201 usr_gr = UserGroup.get_or_404(id)
200 202 try:
201 UsersGroupModel().delete(usr_gr)
203 UserGroupModel().delete(usr_gr)
202 204 Session().commit()
203 h.flash(_('successfully deleted users group'), category='success')
204 except UsersGroupsAssignedException, e:
205 h.flash(_('Successfully deleted user group'), category='success')
206 except UserGroupsAssignedException, e:
205 207 h.flash(e, category='error')
206 208 except Exception:
207 209 log.error(traceback.format_exc())
208 h.flash(_('An error occurred during deletion of users group'),
210 h.flash(_('An error occurred during deletion of user group'),
209 211 category='error')
210 212 return redirect(url('users_groups'))
211 213
212 214 def show(self, id, format='html'):
213 215 """GET /users_groups/id: Show a specific item"""
214 216 # url('users_group', id=ID)
215 217
216 218 def edit(self, id, format='html'):
217 219 """GET /users_groups/id/edit: Form to edit an existing item"""
218 220 # url('edit_users_group', id=ID)
219 221
220 c.users_group = UsersGroup.get_or_404(id)
222 c.users_group = UserGroup.get_or_404(id)
221 223 self._load_data(id)
222 224
223 ug_model = UsersGroupModel()
225 ug_model = UserGroupModel()
224 226 defaults = c.users_group.get_dict()
225 227 defaults.update({
226 228 'create_repo_perm': ug_model.has_perm(c.users_group,
227 229 'hg.create.repository'),
228 230 'fork_repo_perm': ug_model.has_perm(c.users_group,
229 231 'hg.fork.repository'),
230 232 })
231 233
232 234 return htmlfill.render(
233 235 render('admin/users_groups/users_group_edit.html'),
234 236 defaults=defaults,
235 237 encoding="UTF-8",
236 238 force_defaults=False
237 239 )
238 240
239 241 def update_perm(self, id):
240 242 """PUT /users_perm/id: Update an existing item"""
241 243 # url('users_group_perm', id=ID, method='put')
242 244
243 users_group = UsersGroup.get_or_404(id)
245 users_group = UserGroup.get_or_404(id)
244 246 grant_create_perm = str2bool(request.POST.get('create_repo_perm'))
245 247 grant_fork_perm = str2bool(request.POST.get('fork_repo_perm'))
246 248 inherit_perms = str2bool(request.POST.get('inherit_default_permissions'))
247 249
248 usersgroup_model = UsersGroupModel()
250 usergroup_model = UserGroupModel()
249 251
250 252 try:
251 253 users_group.inherit_default_permissions = inherit_perms
252 254 Session().add(users_group)
253 255
254 256 if grant_create_perm:
255 usersgroup_model.revoke_perm(id, 'hg.create.none')
256 usersgroup_model.grant_perm(id, 'hg.create.repository')
257 h.flash(_("Granted 'repository create' permission to users group"),
257 usergroup_model.revoke_perm(id, 'hg.create.none')
258 usergroup_model.grant_perm(id, 'hg.create.repository')
259 h.flash(_("Granted 'repository create' permission to user group"),
258 260 category='success')
259 261 else:
260 usersgroup_model.revoke_perm(id, 'hg.create.repository')
261 usersgroup_model.grant_perm(id, 'hg.create.none')
262 h.flash(_("Revoked 'repository create' permission to users group"),
262 usergroup_model.revoke_perm(id, 'hg.create.repository')
263 usergroup_model.grant_perm(id, 'hg.create.none')
264 h.flash(_("Revoked 'repository create' permission to user group"),
263 265 category='success')
264 266
265 267 if grant_fork_perm:
266 usersgroup_model.revoke_perm(id, 'hg.fork.none')
267 usersgroup_model.grant_perm(id, 'hg.fork.repository')
268 h.flash(_("Granted 'repository fork' permission to users group"),
268 usergroup_model.revoke_perm(id, 'hg.fork.none')
269 usergroup_model.grant_perm(id, 'hg.fork.repository')
270 h.flash(_("Granted 'repository fork' permission to user group"),
269 271 category='success')
270 272 else:
271 usersgroup_model.revoke_perm(id, 'hg.fork.repository')
272 usersgroup_model.grant_perm(id, 'hg.fork.none')
273 h.flash(_("Revoked 'repository fork' permission to users group"),
273 usergroup_model.revoke_perm(id, 'hg.fork.repository')
274 usergroup_model.grant_perm(id, 'hg.fork.none')
275 h.flash(_("Revoked 'repository fork' permission to user group"),
274 276 category='success')
275 277
276 278 Session().commit()
277 279 except Exception:
278 280 log.error(traceback.format_exc())
279 281 h.flash(_('An error occurred during permissions saving'),
280 282 category='error')
281 283
282 284 return redirect(url('edit_users_group', id=id))
@@ -1,927 +1,1031 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.api
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 API controller for RhodeCode
7 7
8 8 :created_on: Aug 20, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software; you can redistribute it and/or
14 14 # modify it under the terms of the GNU General Public License
15 15 # as published by the Free Software Foundation; version 2
16 16 # of the License or (at your opinion) any later version of the license.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program; if not, write to the Free Software
25 25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 26 # MA 02110-1301, USA.
27 27
28 28 import traceback
29 29 import logging
30 from pylons.controllers.util import abort
31 30
32 31 from rhodecode.controllers.api import JSONRPCController, JSONRPCError
33 32 from rhodecode.lib.auth import PasswordGenerator, AuthUser, \
34 33 HasPermissionAllDecorator, HasPermissionAnyDecorator, \
35 34 HasPermissionAnyApi, HasRepoPermissionAnyApi
36 35 from rhodecode.lib.utils import map_groups, repo2db_mapper
36 from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_int
37 from rhodecode.lib import helpers as h
37 38 from rhodecode.model.meta import Session
38 39 from rhodecode.model.scm import ScmModel
39 40 from rhodecode.model.repo import RepoModel
40 41 from rhodecode.model.user import UserModel
41 from rhodecode.model.users_group import UsersGroupModel
42 from rhodecode.model.users_group import UserGroupModel
42 43 from rhodecode.model.permission import PermissionModel
43 44 from rhodecode.model.db import Repository, RhodeCodeSetting, UserIpMap
45 from rhodecode.lib.compat import json
44 46
45 47 log = logging.getLogger(__name__)
46 48
47 49
48 50 class OptionalAttr(object):
49 51 """
50 52 Special Optional Option that defines other attribute
51 53 """
52 54 def __init__(self, attr_name):
53 55 self.attr_name = attr_name
54 56
55 57 def __repr__(self):
56 58 return '<OptionalAttr:%s>' % self.attr_name
57 59
58 60 def __call__(self):
59 61 return self
60 62 #alias
61 63 OAttr = OptionalAttr
62 64
63 65
64 66 class Optional(object):
65 67 """
66 68 Defines an optional parameter::
67 69
68 70 param = param.getval() if isinstance(param, Optional) else param
69 71 param = param() if isinstance(param, Optional) else param
70 72
71 73 is equivalent of::
72 74
73 75 param = Optional.extract(param)
74 76
75 77 """
76 78 def __init__(self, type_):
77 79 self.type_ = type_
78 80
79 81 def __repr__(self):
80 82 return '<Optional:%s>' % self.type_.__repr__()
81 83
82 84 def __call__(self):
83 85 return self.getval()
84 86
85 87 def getval(self):
86 88 """
87 89 returns value from this Optional instance
88 90 """
89 91 return self.type_
90 92
91 93 @classmethod
92 94 def extract(cls, val):
93 95 if isinstance(val, cls):
94 96 return val.getval()
95 97 return val
96 98
97 99
98 100 def get_user_or_error(userid):
99 101 """
100 102 Get user by id or name or return JsonRPCError if not found
101 103
102 104 :param userid:
103 105 """
104 106 user = UserModel().get_user(userid)
105 107 if user is None:
106 108 raise JSONRPCError("user `%s` does not exist" % userid)
107 109 return user
108 110
109 111
110 112 def get_repo_or_error(repoid):
111 113 """
112 114 Get repo by id or name or return JsonRPCError if not found
113 115
114 116 :param userid:
115 117 """
116 118 repo = RepoModel().get_repo(repoid)
117 119 if repo is None:
118 120 raise JSONRPCError('repository `%s` does not exist' % (repoid))
119 121 return repo
120 122
121 123
122 124 def get_users_group_or_error(usersgroupid):
123 125 """
124 Get users group by id or name or return JsonRPCError if not found
126 Get user group by id or name or return JsonRPCError if not found
125 127
126 128 :param userid:
127 129 """
128 users_group = UsersGroupModel().get_group(usersgroupid)
130 users_group = UserGroupModel().get_group(usersgroupid)
129 131 if users_group is None:
130 raise JSONRPCError('users group `%s` does not exist' % usersgroupid)
132 raise JSONRPCError('user group `%s` does not exist' % usersgroupid)
131 133 return users_group
132 134
133 135
134 136 def get_perm_or_error(permid):
135 137 """
136 138 Get permission by id or name or return JsonRPCError if not found
137 139
138 140 :param userid:
139 141 """
140 142 perm = PermissionModel().get_permission_by_name(permid)
141 143 if perm is None:
142 144 raise JSONRPCError('permission `%s` does not exist' % (permid))
143 145 return perm
144 146
145 147
146 148 class ApiController(JSONRPCController):
147 149 """
148 150 API Controller
149 151
150 152
151 153 Each method needs to have USER as argument this is then based on given
152 154 API_KEY propagated as instance of user object
153 155
154 156 Preferably this should be first argument also
155 157
156 158
157 159 Each function should also **raise** JSONRPCError for any
158 160 errors that happens
159 161
160 162 """
161 163
162 164 @HasPermissionAllDecorator('hg.admin')
163 165 def pull(self, apiuser, repoid):
164 166 """
165 167 Dispatch pull action on given repo
166 168
167 169 :param apiuser:
168 170 :param repoid:
169 171 """
170 172
171 173 repo = get_repo_or_error(repoid)
172 174
173 175 try:
174 176 ScmModel().pull_changes(repo.repo_name,
175 177 self.rhodecode_user.username)
176 178 return 'Pulled from `%s`' % repo.repo_name
177 179 except Exception:
178 180 log.error(traceback.format_exc())
179 181 raise JSONRPCError(
180 182 'Unable to pull changes from `%s`' % repo.repo_name
181 183 )
182 184
183 185 @HasPermissionAllDecorator('hg.admin')
184 186 def rescan_repos(self, apiuser, remove_obsolete=Optional(False)):
185 187 """
186 188 Dispatch rescan repositories action. If remove_obsolete is set
187 189 than also delete repos that are in database but not in the filesystem.
188 190 aka "clean zombies"
189 191
190 192 :param apiuser:
191 193 :param remove_obsolete:
192 194 """
193 195
194 196 try:
195 197 rm_obsolete = Optional.extract(remove_obsolete)
196 198 added, removed = repo2db_mapper(ScmModel().repo_scan(),
197 199 remove_obsolete=rm_obsolete)
198 200 return {'added': added, 'removed': removed}
199 201 except Exception:
200 202 log.error(traceback.format_exc())
201 203 raise JSONRPCError(
202 204 'Error occurred during rescan repositories action'
203 205 )
204 206
205 def lock(self, apiuser, repoid, locked, userid=Optional(OAttr('apiuser'))):
207 def invalidate_cache(self, apiuser, repoid):
208 """
209 Dispatch cache invalidation action on given repo
210
211 :param apiuser:
212 :param repoid:
213 """
214 repo = get_repo_or_error(repoid)
215 if HasPermissionAnyApi('hg.admin')(user=apiuser) is False:
216 # check if we have admin permission for this repo !
217 if HasRepoPermissionAnyApi('repository.admin',
218 'repository.write')(user=apiuser,
219 repo_name=repo.repo_name) is False:
220 raise JSONRPCError('repository `%s` does not exist' % (repoid))
221
222 try:
223 invalidated_keys = ScmModel().mark_for_invalidation(repo.repo_name)
224 Session().commit()
225 return ('Cache for repository `%s` was invalidated: '
226 'invalidated cache keys: %s' % (repoid, invalidated_keys))
227 except Exception:
228 log.error(traceback.format_exc())
229 raise JSONRPCError(
230 'Error occurred during cache invalidation action'
231 )
232
233 def lock(self, apiuser, repoid, locked=Optional(None),
234 userid=Optional(OAttr('apiuser'))):
206 235 """
207 236 Set locking state on particular repository by given user, if
208 237 this command is runned by non-admin account userid is set to user
209 238 who is calling this method
210 239
211 240 :param apiuser:
212 241 :param repoid:
213 242 :param userid:
214 243 :param locked:
215 244 """
216 245 repo = get_repo_or_error(repoid)
217 246 if HasPermissionAnyApi('hg.admin')(user=apiuser):
218 247 pass
219 248 elif HasRepoPermissionAnyApi('repository.admin',
220 249 'repository.write')(user=apiuser,
221 250 repo_name=repo.repo_name):
222 251 #make sure normal user does not pass someone else userid,
223 252 #he is not allowed to do that
224 253 if not isinstance(userid, Optional) and userid != apiuser.user_id:
225 254 raise JSONRPCError(
226 255 'userid is not the same as your user'
227 256 )
228 257 else:
229 258 raise JSONRPCError('repository `%s` does not exist' % (repoid))
230 259
231 260 if isinstance(userid, Optional):
232 261 userid = apiuser.user_id
262
233 263 user = get_user_or_error(userid)
234 locked = bool(locked)
264
265 if isinstance(locked, Optional):
266 lockobj = Repository.getlock(repo)
267
268 if lockobj[0] is None:
269 return ('Repo `%s` not locked. Locked=`False`.'
270 % (repo.repo_name))
271 else:
272 userid, time_ = lockobj
273 user = get_user_or_error(userid)
274
275 return ('Repo `%s` locked by `%s`. Locked=`True`. '
276 'Locked since: `%s`'
277 % (repo.repo_name, user.username,
278 json.dumps(time_to_datetime(time_))))
279
280 else:
281 locked = str2bool(locked)
235 282 try:
236 283 if locked:
237 284 Repository.lock(repo, user.user_id)
238 285 else:
239 286 Repository.unlock(repo)
240 287
241 288 return ('User `%s` set lock state for repo `%s` to `%s`'
242 289 % (user.username, repo.repo_name, locked))
243 290 except Exception:
244 291 log.error(traceback.format_exc())
245 292 raise JSONRPCError(
246 293 'Error occurred locking repository `%s`' % repo.repo_name
247 294 )
248 295
296 def get_locks(self, apiuser, userid=Optional(OAttr('apiuser'))):
297 """
298 Get all locks for given userid, if
299 this command is runned by non-admin account userid is set to user
300 who is calling this method, thus returning locks for himself
301
302 :param apiuser:
303 :param userid:
304 """
305 if HasPermissionAnyApi('hg.admin')(user=apiuser):
306 pass
307 else:
308 #make sure normal user does not pass someone else userid,
309 #he is not allowed to do that
310 if not isinstance(userid, Optional) and userid != apiuser.user_id:
311 raise JSONRPCError(
312 'userid is not the same as your user'
313 )
314 ret = []
315 if isinstance(userid, Optional):
316 user = None
317 else:
318 user = get_user_or_error(userid)
319
320 #show all locks
321 for r in Repository.getAll():
322 userid, time_ = r.locked
323 if time_:
324 _api_data = r.get_api_data()
325 # if we use userfilter just show the locks for this user
326 if user:
327 if safe_int(userid) == user.user_id:
328 ret.append(_api_data)
329 else:
330 ret.append(_api_data)
331
332 return ret
333
249 334 @HasPermissionAllDecorator('hg.admin')
250 335 def show_ip(self, apiuser, userid):
251 336 """
252 337 Shows IP address as seen from RhodeCode server, together with all
253 338 defined IP addresses for given user
254 339
255 340 :param apiuser:
256 341 :param userid:
257 342 """
258 343 user = get_user_or_error(userid)
259 344 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
260 345 return dict(
261 346 ip_addr_server=self.ip_addr,
262 347 user_ips=ips
263 348 )
264 349
265 350 def get_user(self, apiuser, userid=Optional(OAttr('apiuser'))):
266 351 """"
267 352 Get a user by username, or userid, if userid is given
268 353
269 354 :param apiuser:
270 355 :param userid:
271 356 """
272 357 if HasPermissionAnyApi('hg.admin')(user=apiuser) is False:
273 358 #make sure normal user does not pass someone else userid,
274 359 #he is not allowed to do that
275 360 if not isinstance(userid, Optional) and userid != apiuser.user_id:
276 361 raise JSONRPCError(
277 362 'userid is not the same as your user'
278 363 )
279 364
280 365 if isinstance(userid, Optional):
281 366 userid = apiuser.user_id
282 367
283 368 user = get_user_or_error(userid)
284 369 data = user.get_api_data()
285 370 data['permissions'] = AuthUser(user_id=user.user_id).permissions
286 371 return data
287 372
288 373 @HasPermissionAllDecorator('hg.admin')
289 374 def get_users(self, apiuser):
290 375 """"
291 376 Get all users
292 377
293 378 :param apiuser:
294 379 """
295 380
296 381 result = []
297 382 for user in UserModel().get_all():
298 383 result.append(user.get_api_data())
299 384 return result
300 385
301 386 @HasPermissionAllDecorator('hg.admin')
302 387 def create_user(self, apiuser, username, email, password,
303 388 firstname=Optional(None), lastname=Optional(None),
304 389 active=Optional(True), admin=Optional(False),
305 390 ldap_dn=Optional(None)):
306 391 """
307 392 Create new user
308 393
309 394 :param apiuser:
310 395 :param username:
311 396 :param email:
312 397 :param password:
313 398 :param firstname:
314 399 :param lastname:
315 400 :param active:
316 401 :param admin:
317 402 :param ldap_dn:
318 403 """
319 404
320 405 if UserModel().get_by_username(username):
321 406 raise JSONRPCError("user `%s` already exist" % username)
322 407
323 408 if UserModel().get_by_email(email, case_insensitive=True):
324 409 raise JSONRPCError("email `%s` already exist" % email)
325 410
326 411 if Optional.extract(ldap_dn):
327 412 # generate temporary password if ldap_dn
328 413 password = PasswordGenerator().gen_password(length=8)
329 414
330 415 try:
331 416 user = UserModel().create_or_update(
332 417 username=Optional.extract(username),
333 418 password=Optional.extract(password),
334 419 email=Optional.extract(email),
335 420 firstname=Optional.extract(firstname),
336 421 lastname=Optional.extract(lastname),
337 422 active=Optional.extract(active),
338 423 admin=Optional.extract(admin),
339 424 ldap_dn=Optional.extract(ldap_dn)
340 425 )
341 426 Session().commit()
342 427 return dict(
343 428 msg='created new user `%s`' % username,
344 429 user=user.get_api_data()
345 430 )
346 431 except Exception:
347 432 log.error(traceback.format_exc())
348 433 raise JSONRPCError('failed to create user `%s`' % username)
349 434
350 435 @HasPermissionAllDecorator('hg.admin')
351 436 def update_user(self, apiuser, userid, username=Optional(None),
352 437 email=Optional(None), firstname=Optional(None),
353 438 lastname=Optional(None), active=Optional(None),
354 439 admin=Optional(None), ldap_dn=Optional(None),
355 440 password=Optional(None)):
356 441 """
357 442 Updates given user
358 443
359 444 :param apiuser:
360 445 :param userid:
361 446 :param username:
362 447 :param email:
363 448 :param firstname:
364 449 :param lastname:
365 450 :param active:
366 451 :param admin:
367 452 :param ldap_dn:
368 453 :param password:
369 454 """
370 455
371 456 user = get_user_or_error(userid)
372 457
373 458 # call function and store only updated arguments
374 459 updates = {}
375 460
376 461 def store_update(attr, name):
377 462 if not isinstance(attr, Optional):
378 463 updates[name] = attr
379 464
380 465 try:
381 466
382 467 store_update(username, 'username')
383 468 store_update(password, 'password')
384 469 store_update(email, 'email')
385 470 store_update(firstname, 'name')
386 471 store_update(lastname, 'lastname')
387 472 store_update(active, 'active')
388 473 store_update(admin, 'admin')
389 474 store_update(ldap_dn, 'ldap_dn')
390 475
391 476 user = UserModel().update_user(user, **updates)
392 477 Session().commit()
393 478 return dict(
394 479 msg='updated user ID:%s %s' % (user.user_id, user.username),
395 480 user=user.get_api_data()
396 481 )
397 482 except Exception:
398 483 log.error(traceback.format_exc())
399 484 raise JSONRPCError('failed to update user `%s`' % userid)
400 485
401 486 @HasPermissionAllDecorator('hg.admin')
402 487 def delete_user(self, apiuser, userid):
403 488 """"
404 489 Deletes an user
405 490
406 491 :param apiuser:
407 492 :param userid:
408 493 """
409 494 user = get_user_or_error(userid)
410 495
411 496 try:
412 497 UserModel().delete(userid)
413 498 Session().commit()
414 499 return dict(
415 500 msg='deleted user ID:%s %s' % (user.user_id, user.username),
416 501 user=None
417 502 )
418 503 except Exception:
419 504 log.error(traceback.format_exc())
420 505 raise JSONRPCError('failed to delete ID:%s %s' % (user.user_id,
421 506 user.username))
422 507
423 508 @HasPermissionAllDecorator('hg.admin')
424 509 def get_users_group(self, apiuser, usersgroupid):
425 510 """"
426 Get users group by name or id
511 Get user group by name or id
427 512
428 513 :param apiuser:
429 514 :param usersgroupid:
430 515 """
431 516 users_group = get_users_group_or_error(usersgroupid)
432 517
433 518 data = users_group.get_api_data()
434 519
435 520 members = []
436 521 for user in users_group.members:
437 522 user = user.user
438 523 members.append(user.get_api_data())
439 524 data['members'] = members
440 525 return data
441 526
442 527 @HasPermissionAllDecorator('hg.admin')
443 528 def get_users_groups(self, apiuser):
444 529 """"
445 Get all users groups
530 Get all user groups
446 531
447 532 :param apiuser:
448 533 """
449 534
450 535 result = []
451 for users_group in UsersGroupModel().get_all():
536 for users_group in UserGroupModel().get_all():
452 537 result.append(users_group.get_api_data())
453 538 return result
454 539
455 540 @HasPermissionAllDecorator('hg.admin')
456 541 def create_users_group(self, apiuser, group_name, active=Optional(True)):
457 542 """
458 543 Creates an new usergroup
459 544
460 545 :param apiuser:
461 546 :param group_name:
462 547 :param active:
463 548 """
464 549
465 if UsersGroupModel().get_by_name(group_name):
466 raise JSONRPCError("users group `%s` already exist" % group_name)
550 if UserGroupModel().get_by_name(group_name):
551 raise JSONRPCError("user group `%s` already exist" % group_name)
467 552
468 553 try:
469 554 active = Optional.extract(active)
470 ug = UsersGroupModel().create(name=group_name, active=active)
555 ug = UserGroupModel().create(name=group_name, active=active)
471 556 Session().commit()
472 557 return dict(
473 msg='created new users group `%s`' % group_name,
558 msg='created new user group `%s`' % group_name,
474 559 users_group=ug.get_api_data()
475 560 )
476 561 except Exception:
477 562 log.error(traceback.format_exc())
478 563 raise JSONRPCError('failed to create group `%s`' % group_name)
479 564
480 565 @HasPermissionAllDecorator('hg.admin')
481 566 def add_user_to_users_group(self, apiuser, usersgroupid, userid):
482 567 """"
483 Add a user to a users group
568 Add a user to a user group
484 569
485 570 :param apiuser:
486 571 :param usersgroupid:
487 572 :param userid:
488 573 """
489 574 user = get_user_or_error(userid)
490 575 users_group = get_users_group_or_error(usersgroupid)
491 576
492 577 try:
493 ugm = UsersGroupModel().add_user_to_group(users_group, user)
578 ugm = UserGroupModel().add_user_to_group(users_group, user)
494 579 success = True if ugm != True else False
495 msg = 'added member `%s` to users group `%s`' % (
580 msg = 'added member `%s` to user group `%s`' % (
496 581 user.username, users_group.users_group_name
497 582 )
498 583 msg = msg if success else 'User is already in that group'
499 584 Session().commit()
500 585
501 586 return dict(
502 587 success=success,
503 588 msg=msg
504 589 )
505 590 except Exception:
506 591 log.error(traceback.format_exc())
507 592 raise JSONRPCError(
508 'failed to add member to users group `%s`' % (
593 'failed to add member to user group `%s`' % (
509 594 users_group.users_group_name
510 595 )
511 596 )
512 597
513 598 @HasPermissionAllDecorator('hg.admin')
514 599 def remove_user_from_users_group(self, apiuser, usersgroupid, userid):
515 600 """
516 601 Remove user from a group
517 602
518 603 :param apiuser:
519 604 :param usersgroupid:
520 605 :param userid:
521 606 """
522 607 user = get_user_or_error(userid)
523 608 users_group = get_users_group_or_error(usersgroupid)
524 609
525 610 try:
526 success = UsersGroupModel().remove_user_from_group(users_group,
611 success = UserGroupModel().remove_user_from_group(users_group,
527 612 user)
528 msg = 'removed member `%s` from users group `%s`' % (
613 msg = 'removed member `%s` from user group `%s`' % (
529 614 user.username, users_group.users_group_name
530 615 )
531 616 msg = msg if success else "User wasn't in group"
532 617 Session().commit()
533 618 return dict(success=success, msg=msg)
534 619 except Exception:
535 620 log.error(traceback.format_exc())
536 621 raise JSONRPCError(
537 'failed to remove member from users group `%s`' % (
622 'failed to remove member from user group `%s`' % (
538 623 users_group.users_group_name
539 624 )
540 625 )
541 626
542 627 def get_repo(self, apiuser, repoid):
543 628 """"
544 629 Get repository by name
545 630
546 631 :param apiuser:
547 632 :param repoid:
548 633 """
549 634 repo = get_repo_or_error(repoid)
550 635
551 636 if HasPermissionAnyApi('hg.admin')(user=apiuser) is False:
552 637 # check if we have admin permission for this repo !
553 638 if HasRepoPermissionAnyApi('repository.admin')(user=apiuser,
554 639 repo_name=repo.repo_name) is False:
555 640 raise JSONRPCError('repository `%s` does not exist' % (repoid))
556 641
557 642 members = []
643 followers = []
558 644 for user in repo.repo_to_perm:
559 645 perm = user.permission.permission_name
560 646 user = user.user
561 647 user_data = user.get_api_data()
562 648 user_data['type'] = "user"
563 649 user_data['permission'] = perm
564 650 members.append(user_data)
565 651
566 652 for users_group in repo.users_group_to_perm:
567 653 perm = users_group.permission.permission_name
568 654 users_group = users_group.users_group
569 655 users_group_data = users_group.get_api_data()
570 656 users_group_data['type'] = "users_group"
571 657 users_group_data['permission'] = perm
572 658 members.append(users_group_data)
573 659
660 for user in repo.followers:
661 followers.append(user.user.get_api_data())
662
574 663 data = repo.get_api_data()
575 664 data['members'] = members
665 data['followers'] = followers
576 666 return data
577 667
578 668 def get_repos(self, apiuser):
579 669 """"
580 670 Get all repositories
581 671
582 672 :param apiuser:
583 673 """
584 674 result = []
585 675 if HasPermissionAnyApi('hg.admin')(user=apiuser) is False:
586 676 repos = RepoModel().get_all_user_repos(user=apiuser)
587 677 else:
588 678 repos = RepoModel().get_all()
589 679
590 680 for repo in repos:
591 681 result.append(repo.get_api_data())
592 682 return result
593 683
594 684 @HasPermissionAllDecorator('hg.admin')
595 685 def get_repo_nodes(self, apiuser, repoid, revision, root_path,
596 686 ret_type='all'):
597 687 """
598 688 returns a list of nodes and it's children
599 689 for a given path at given revision. It's possible to specify ret_type
600 690 to show only files or dirs
601 691
602 692 :param apiuser:
603 693 :param repoid: name or id of repository
604 694 :param revision: revision for which listing should be done
605 695 :param root_path: path from which start displaying
606 696 :param ret_type: return type 'all|files|dirs' nodes
607 697 """
608 698 repo = get_repo_or_error(repoid)
609 699 try:
610 700 _d, _f = ScmModel().get_nodes(repo, revision, root_path,
611 701 flat=False)
612 702 _map = {
613 703 'all': _d + _f,
614 704 'files': _f,
615 705 'dirs': _d,
616 706 }
617 707 return _map[ret_type]
618 708 except KeyError:
619 709 raise JSONRPCError('ret_type must be one of %s' % _map.keys())
620 710 except Exception:
621 711 log.error(traceback.format_exc())
622 712 raise JSONRPCError(
623 713 'failed to get repo: `%s` nodes' % repo.repo_name
624 714 )
625 715
626 716 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
627 717 def create_repo(self, apiuser, repo_name, owner=Optional(OAttr('apiuser')),
628 718 repo_type=Optional('hg'),
629 719 description=Optional(''), private=Optional(False),
630 720 clone_uri=Optional(None), landing_rev=Optional('tip'),
631 721 enable_statistics=Optional(False),
632 722 enable_locking=Optional(False),
633 723 enable_downloads=Optional(False)):
634 724 """
635 725 Create repository, if clone_url is given it makes a remote clone
636 726 if repo_name is within a group name the groups will be created
637 727 automatically if they aren't present
638 728
639 729 :param apiuser:
640 730 :param repo_name:
641 731 :param onwer:
642 732 :param repo_type:
643 733 :param description:
644 734 :param private:
645 735 :param clone_uri:
646 736 :param landing_rev:
647 737 """
648 738 if HasPermissionAnyApi('hg.admin')(user=apiuser) is False:
649 739 if not isinstance(owner, Optional):
650 740 #forbid setting owner for non-admins
651 741 raise JSONRPCError(
652 742 'Only RhodeCode admin can specify `owner` param'
653 743 )
654 744 if isinstance(owner, Optional):
655 745 owner = apiuser.user_id
656 746
657 747 owner = get_user_or_error(owner)
658 748
659 749 if RepoModel().get_by_repo_name(repo_name):
660 750 raise JSONRPCError("repo `%s` already exist" % repo_name)
661 751
662 752 defs = RhodeCodeSetting.get_default_repo_settings(strip_prefix=True)
663 753 if isinstance(private, Optional):
664 754 private = defs.get('repo_private') or Optional.extract(private)
665 755 if isinstance(repo_type, Optional):
666 756 repo_type = defs.get('repo_type')
667 757 if isinstance(enable_statistics, Optional):
668 758 enable_statistics = defs.get('repo_enable_statistics')
669 759 if isinstance(enable_locking, Optional):
670 760 enable_locking = defs.get('repo_enable_locking')
671 761 if isinstance(enable_downloads, Optional):
672 762 enable_downloads = defs.get('repo_enable_downloads')
673 763
674 764 clone_uri = Optional.extract(clone_uri)
675 765 description = Optional.extract(description)
676 766 landing_rev = Optional.extract(landing_rev)
677 767
678 768 try:
679 769 # create structure of groups and return the last group
680 770 group = map_groups(repo_name)
681 771
682 772 repo = RepoModel().create_repo(
683 773 repo_name=repo_name,
684 774 repo_type=repo_type,
685 775 description=description,
686 776 owner=owner,
687 777 private=private,
688 778 clone_uri=clone_uri,
689 779 repos_group=group,
690 780 landing_rev=landing_rev,
691 781 enable_statistics=enable_statistics,
692 782 enable_downloads=enable_downloads,
693 783 enable_locking=enable_locking
694 784 )
695 785
696 786 Session().commit()
697 787 return dict(
698 788 msg="Created new repository `%s`" % (repo.repo_name),
699 789 repo=repo.get_api_data()
700 790 )
701 791 except Exception:
702 792 log.error(traceback.format_exc())
703 793 raise JSONRPCError('failed to create repository `%s`' % repo_name)
704 794
705 795 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
706 796 def fork_repo(self, apiuser, repoid, fork_name, owner=Optional(OAttr('apiuser')),
707 797 description=Optional(''), copy_permissions=Optional(False),
708 798 private=Optional(False), landing_rev=Optional('tip')):
709 799 repo = get_repo_or_error(repoid)
710 800 repo_name = repo.repo_name
711 801
712 802 _repo = RepoModel().get_by_repo_name(fork_name)
713 803 if _repo:
714 804 type_ = 'fork' if _repo.fork else 'repo'
715 805 raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
716 806
717 807 if HasPermissionAnyApi('hg.admin')(user=apiuser):
718 808 pass
719 809 elif HasRepoPermissionAnyApi('repository.admin',
720 810 'repository.write',
721 811 'repository.read')(user=apiuser,
722 812 repo_name=repo.repo_name):
723 813 if not isinstance(owner, Optional):
724 814 #forbid setting owner for non-admins
725 815 raise JSONRPCError(
726 816 'Only RhodeCode admin can specify `owner` param'
727 817 )
728 818 else:
729 819 raise JSONRPCError('repository `%s` does not exist' % (repoid))
730 820
731 821 if isinstance(owner, Optional):
732 822 owner = apiuser.user_id
733 823
734 824 owner = get_user_or_error(owner)
735 825
736 826 try:
737 827 # create structure of groups and return the last group
738 828 group = map_groups(fork_name)
739 829
740 830 form_data = dict(
741 831 repo_name=fork_name,
742 832 repo_name_full=fork_name,
743 833 repo_group=group,
744 834 repo_type=repo.repo_type,
745 835 description=Optional.extract(description),
746 836 private=Optional.extract(private),
747 837 copy_permissions=Optional.extract(copy_permissions),
748 838 landing_rev=Optional.extract(landing_rev),
749 839 update_after_clone=False,
750 840 fork_parent_id=repo.repo_id,
751 841 )
752 842 RepoModel().create_fork(form_data, cur_user=owner)
753 843 return dict(
754 844 msg='Created fork of `%s` as `%s`' % (repo.repo_name,
755 845 fork_name),
756 846 success=True # cannot return the repo data here since fork
757 847 # cann be done async
758 848 )
759 849 except Exception:
760 850 log.error(traceback.format_exc())
761 851 raise JSONRPCError(
762 852 'failed to fork repository `%s` as `%s`' % (repo_name,
763 853 fork_name)
764 854 )
765 855
766 def delete_repo(self, apiuser, repoid):
856 def delete_repo(self, apiuser, repoid, forks=Optional(None)):
767 857 """
768 858 Deletes a given repository
769 859
770 860 :param apiuser:
771 861 :param repoid:
862 :param forks: detach or delete, what do do with attached forks for repo
772 863 """
773 864 repo = get_repo_or_error(repoid)
774 865
775 866 if HasPermissionAnyApi('hg.admin')(user=apiuser) is False:
776 867 # check if we have admin permission for this repo !
777 868 if HasRepoPermissionAnyApi('repository.admin')(user=apiuser,
778 869 repo_name=repo.repo_name) is False:
779 870 raise JSONRPCError('repository `%s` does not exist' % (repoid))
780 871
781 872 try:
782 RepoModel().delete(repo)
873 handle_forks = Optional.extract(forks)
874 _forks_msg = ''
875 _forks = [f for f in repo.forks]
876 if handle_forks == 'detach':
877 _forks_msg = ' ' + _('Detached %s forks') % len(_forks)
878 elif handle_forks == 'delete':
879 _forks_msg = ' ' + _('Deleted %s forks') % len(_forks)
880 elif _forks:
881 raise JSONRPCError(
882 'Cannot delete `%s` it still contains attached forks'
883 % repo.repo_name
884 )
885
886 RepoModel().delete(repo, forks=forks)
783 887 Session().commit()
784 888 return dict(
785 msg='Deleted repository `%s`' % repo.repo_name,
889 msg='Deleted repository `%s`%s' % (repo.repo_name, _forks_msg),
786 890 success=True
787 891 )
788 892 except Exception:
789 893 log.error(traceback.format_exc())
790 894 raise JSONRPCError(
791 895 'failed to delete repository `%s`' % repo.repo_name
792 896 )
793 897
794 898 @HasPermissionAllDecorator('hg.admin')
795 899 def grant_user_permission(self, apiuser, repoid, userid, perm):
796 900 """
797 901 Grant permission for user on given repository, or update existing one
798 902 if found
799 903
800 904 :param repoid:
801 905 :param userid:
802 906 :param perm:
803 907 """
804 908 repo = get_repo_or_error(repoid)
805 909 user = get_user_or_error(userid)
806 910 perm = get_perm_or_error(perm)
807 911
808 912 try:
809 913
810 914 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
811 915
812 916 Session().commit()
813 917 return dict(
814 918 msg='Granted perm: `%s` for user: `%s` in repo: `%s`' % (
815 919 perm.permission_name, user.username, repo.repo_name
816 920 ),
817 921 success=True
818 922 )
819 923 except Exception:
820 924 log.error(traceback.format_exc())
821 925 raise JSONRPCError(
822 926 'failed to edit permission for user: `%s` in repo: `%s`' % (
823 927 userid, repoid
824 928 )
825 929 )
826 930
827 931 @HasPermissionAllDecorator('hg.admin')
828 932 def revoke_user_permission(self, apiuser, repoid, userid):
829 933 """
830 934 Revoke permission for user on given repository
831 935
832 936 :param apiuser:
833 937 :param repoid:
834 938 :param userid:
835 939 """
836 940
837 941 repo = get_repo_or_error(repoid)
838 942 user = get_user_or_error(userid)
839 943 try:
840 944
841 945 RepoModel().revoke_user_permission(repo=repo, user=user)
842 946
843 947 Session().commit()
844 948 return dict(
845 949 msg='Revoked perm for user: `%s` in repo: `%s`' % (
846 950 user.username, repo.repo_name
847 951 ),
848 952 success=True
849 953 )
850 954 except Exception:
851 955 log.error(traceback.format_exc())
852 956 raise JSONRPCError(
853 957 'failed to edit permission for user: `%s` in repo: `%s`' % (
854 958 userid, repoid
855 959 )
856 960 )
857 961
858 962 @HasPermissionAllDecorator('hg.admin')
859 963 def grant_users_group_permission(self, apiuser, repoid, usersgroupid,
860 964 perm):
861 965 """
862 Grant permission for users group on given repository, or update
966 Grant permission for user group on given repository, or update
863 967 existing one if found
864 968
865 969 :param apiuser:
866 970 :param repoid:
867 971 :param usersgroupid:
868 972 :param perm:
869 973 """
870 974 repo = get_repo_or_error(repoid)
871 975 perm = get_perm_or_error(perm)
872 976 users_group = get_users_group_or_error(usersgroupid)
873 977
874 978 try:
875 979 RepoModel().grant_users_group_permission(repo=repo,
876 980 group_name=users_group,
877 981 perm=perm)
878 982
879 983 Session().commit()
880 984 return dict(
881 msg='Granted perm: `%s` for users group: `%s` in '
985 msg='Granted perm: `%s` for user group: `%s` in '
882 986 'repo: `%s`' % (
883 987 perm.permission_name, users_group.users_group_name,
884 988 repo.repo_name
885 989 ),
886 990 success=True
887 991 )
888 992 except Exception:
889 993 log.error(traceback.format_exc())
890 994 raise JSONRPCError(
891 'failed to edit permission for users group: `%s` in '
995 'failed to edit permission for user group: `%s` in '
892 996 'repo: `%s`' % (
893 997 usersgroupid, repo.repo_name
894 998 )
895 999 )
896 1000
897 1001 @HasPermissionAllDecorator('hg.admin')
898 1002 def revoke_users_group_permission(self, apiuser, repoid, usersgroupid):
899 1003 """
900 Revoke permission for users group on given repository
1004 Revoke permission for user group on given repository
901 1005
902 1006 :param apiuser:
903 1007 :param repoid:
904 1008 :param usersgroupid:
905 1009 """
906 1010 repo = get_repo_or_error(repoid)
907 1011 users_group = get_users_group_or_error(usersgroupid)
908 1012
909 1013 try:
910 1014 RepoModel().revoke_users_group_permission(repo=repo,
911 1015 group_name=users_group)
912 1016
913 1017 Session().commit()
914 1018 return dict(
915 msg='Revoked perm for users group: `%s` in repo: `%s`' % (
1019 msg='Revoked perm for user group: `%s` in repo: `%s`' % (
916 1020 users_group.users_group_name, repo.repo_name
917 1021 ),
918 1022 success=True
919 1023 )
920 1024 except Exception:
921 1025 log.error(traceback.format_exc())
922 1026 raise JSONRPCError(
923 'failed to edit permission for users group: `%s` in '
1027 'failed to edit permission for user group: `%s` in '
924 1028 'repo: `%s`' % (
925 1029 users_group.users_group_name, repo.repo_name
926 1030 )
927 1031 )
@@ -1,125 +1,125 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changelog
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 changelog controller for rhodecode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pylons import request, url, session, tmpl_context as c
30 30 from pylons.controllers.util import redirect
31 31 from pylons.i18n.translation import _
32 32
33 33 import rhodecode.lib.helpers as h
34 34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 35 from rhodecode.lib.base import BaseRepoController, render
36 36 from rhodecode.lib.helpers import RepoPage
37 37 from rhodecode.lib.compat import json
38 38 from rhodecode.lib.graphmod import _colored, _dagwalker
39 39 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError
40 40 from rhodecode.lib.utils2 import safe_int
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class ChangelogController(BaseRepoController):
46 46
47 47 @LoginRequired()
48 48 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
49 49 'repository.admin')
50 50 def __before__(self):
51 51 super(ChangelogController, self).__before__()
52 52 c.affected_files_cut_off = 60
53 53
54 54 def index(self):
55 55 limit = 100
56 56 default = 20
57 57 if request.params.get('size'):
58 58 try:
59 59 int_size = int(request.params.get('size'))
60 60 except ValueError:
61 61 int_size = default
62 62 c.size = max(min(int_size, limit), 1)
63 63 session['changelog_size'] = c.size
64 64 session.save()
65 65 else:
66 66 c.size = int(session.get('changelog_size', default))
67 67 # min size must be 1
68 68 c.size = max(c.size, 1)
69 69 p = safe_int(request.params.get('page', 1), 1)
70 70 branch_name = request.params.get('branch', None)
71 71 try:
72 72 if branch_name:
73 73 collection = [z for z in
74 74 c.rhodecode_repo.get_changesets(start=0,
75 75 branch_name=branch_name)]
76 76 c.total_cs = len(collection)
77 77 else:
78 78 collection = c.rhodecode_repo
79 79 c.total_cs = len(c.rhodecode_repo)
80 80
81 81 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
82 82 items_per_page=c.size, branch=branch_name)
83 83 collection = list(c.pagination)
84 84 page_revisions = [x.raw_id for x in collection]
85 85 c.comments = c.rhodecode_db_repo.get_comments(page_revisions)
86 86 c.statuses = c.rhodecode_db_repo.statuses(page_revisions)
87 87 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
88 88 log.error(traceback.format_exc())
89 h.flash(str(e), category='warning')
90 return redirect(url('home'))
89 h.flash(str(e), category='error')
90 return redirect(url('changelog_home', repo_name=c.repo_name))
91 91
92 92 self._graph(c.rhodecode_repo, collection, c.total_cs, c.size, p)
93 93
94 94 c.branch_name = branch_name
95 95 c.branch_filters = [('', _('All Branches'))] + \
96 96 [(k, k) for k in c.rhodecode_repo.branches.keys()]
97 97
98 98 return render('changelog/changelog.html')
99 99
100 100 def changelog_details(self, cs):
101 101 if request.environ.get('HTTP_X_PARTIAL_XHR'):
102 102 c.cs = c.rhodecode_repo.get_changeset(cs)
103 103 return render('changelog/changelog_details.html')
104 104
105 105 def _graph(self, repo, collection, repo_size, size, p):
106 106 """
107 107 Generates a DAG graph for mercurial
108 108
109 109 :param repo: repo instance
110 110 :param size: number of commits to show
111 111 :param p: page number
112 112 """
113 113 if not collection:
114 114 c.jsdata = json.dumps([])
115 115 return
116 116
117 117 data = []
118 118 revs = [x.revision for x in collection]
119 119
120 120 dag = _dagwalker(repo, revs, repo.alias)
121 121 dag = _colored(dag)
122 122 for (id, type, ctx, vtx, edges) in dag:
123 123 data.append(['', vtx, edges])
124 124
125 125 c.jsdata = json.dumps(data)
@@ -1,405 +1,404 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changeset
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 changeset controller for pylons showoing changes beetween
7 7 revisions
8 8
9 9 :created_on: Apr 25, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import logging
27 27 import traceback
28 28 from collections import defaultdict
29 from webob.exc import HTTPForbidden, HTTPBadRequest
29 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
30 30
31 31 from pylons import tmpl_context as c, url, request, response
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from rhodecode.lib.utils import jsonify
35 35
36 36 from rhodecode.lib.vcs.exceptions import RepositoryError, \
37 37 ChangesetDoesNotExistError
38 38
39 39 import rhodecode.lib.helpers as h
40 40 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
41 41 from rhodecode.lib.base import BaseRepoController, render
42 42 from rhodecode.lib.utils import action_logger
43 43 from rhodecode.lib.compat import OrderedDict
44 44 from rhodecode.lib import diffs
45 45 from rhodecode.model.db import ChangesetComment, ChangesetStatus
46 46 from rhodecode.model.comment import ChangesetCommentsModel
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.repo import RepoModel
50 50 from rhodecode.lib.diffs import LimitedDiffContainer
51 51 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
52 52 from rhodecode.lib.vcs.backends.base import EmptyChangeset
53 53 from rhodecode.lib.utils2 import safe_unicode
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 def _update_with_GET(params, GET):
59 59 for k in ['diff1', 'diff2', 'diff']:
60 60 params[k] += GET.getall(k)
61 61
62 62
63 63 def anchor_url(revision, path, GET):
64 64 fid = h.FID(revision, path)
65 65 return h.url.current(anchor=fid, **dict(GET))
66 66
67 67
68 68 def get_ignore_ws(fid, GET):
69 69 ig_ws_global = GET.get('ignorews')
70 70 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
71 71 if ig_ws:
72 72 try:
73 73 return int(ig_ws[0].split(':')[-1])
74 except:
74 except Exception:
75 75 pass
76 76 return ig_ws_global
77 77
78 78
79 79 def _ignorews_url(GET, fileid=None):
80 80 fileid = str(fileid) if fileid else None
81 81 params = defaultdict(list)
82 82 _update_with_GET(params, GET)
83 lbl = _('show white space')
83 lbl = _('Show white space')
84 84 ig_ws = get_ignore_ws(fileid, GET)
85 85 ln_ctx = get_line_ctx(fileid, GET)
86 86 # global option
87 87 if fileid is None:
88 88 if ig_ws is None:
89 89 params['ignorews'] += [1]
90 lbl = _('ignore white space')
90 lbl = _('Ignore white space')
91 91 ctx_key = 'context'
92 92 ctx_val = ln_ctx
93 93 # per file options
94 94 else:
95 95 if ig_ws is None:
96 96 params[fileid] += ['WS:1']
97 lbl = _('ignore white space')
97 lbl = _('Ignore white space')
98 98
99 99 ctx_key = fileid
100 100 ctx_val = 'C:%s' % ln_ctx
101 101 # if we have passed in ln_ctx pass it along to our params
102 102 if ln_ctx:
103 103 params[ctx_key] += [ctx_val]
104 104
105 105 params['anchor'] = fileid
106 106 img = h.image(h.url('/images/icons/text_strikethrough.png'), lbl, class_='icon')
107 107 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
108 108
109 109
110 110 def get_line_ctx(fid, GET):
111 111 ln_ctx_global = GET.get('context')
112 112 if fid:
113 113 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
114 114 else:
115 115 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
116 116 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
117 117 if ln_ctx:
118 118 ln_ctx = [ln_ctx]
119 119
120 120 if ln_ctx:
121 121 retval = ln_ctx[0].split(':')[-1]
122 122 else:
123 123 retval = ln_ctx_global
124 124
125 125 try:
126 126 return int(retval)
127 except:
127 except Exception:
128 128 return 3
129 129
130 130
131 131 def _context_url(GET, fileid=None):
132 132 """
133 133 Generates url for context lines
134 134
135 135 :param fileid:
136 136 """
137 137
138 138 fileid = str(fileid) if fileid else None
139 139 ig_ws = get_ignore_ws(fileid, GET)
140 140 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
141 141
142 142 params = defaultdict(list)
143 143 _update_with_GET(params, GET)
144 144
145 145 # global option
146 146 if fileid is None:
147 147 if ln_ctx > 0:
148 148 params['context'] += [ln_ctx]
149 149
150 150 if ig_ws:
151 151 ig_ws_key = 'ignorews'
152 152 ig_ws_val = 1
153 153
154 154 # per file option
155 155 else:
156 156 params[fileid] += ['C:%s' % ln_ctx]
157 157 ig_ws_key = fileid
158 158 ig_ws_val = 'WS:%s' % 1
159 159
160 160 if ig_ws:
161 161 params[ig_ws_key] += [ig_ws_val]
162 162
163 163 lbl = _('%s line context') % ln_ctx
164 164
165 165 params['anchor'] = fileid
166 166 img = h.image(h.url('/images/icons/table_add.png'), lbl, class_='icon')
167 167 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
168 168
169 169
170 170 class ChangesetController(BaseRepoController):
171 171
172 172 @LoginRequired()
173 173 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
174 174 'repository.admin')
175 175 def __before__(self):
176 176 super(ChangesetController, self).__before__()
177 177 c.affected_files_cut_off = 60
178 178 repo_model = RepoModel()
179 179 c.users_array = repo_model.get_users_js()
180 180 c.users_groups_array = repo_model.get_users_groups_js()
181 181
182 182 def index(self, revision, method='show'):
183 183 c.anchor_url = anchor_url
184 184 c.ignorews_url = _ignorews_url
185 185 c.context_url = _context_url
186 186 c.fulldiff = fulldiff = request.GET.get('fulldiff')
187 187 #get ranges of revisions if preset
188 188 rev_range = revision.split('...')[:2]
189 189 enable_comments = True
190 190 try:
191 191 if len(rev_range) == 2:
192 192 enable_comments = False
193 193 rev_start = rev_range[0]
194 194 rev_end = rev_range[1]
195 195 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
196 196 end=rev_end)
197 197 else:
198 198 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
199 199
200 200 c.cs_ranges = list(rev_ranges)
201 201 if not c.cs_ranges:
202 202 raise RepositoryError('Changeset range returned empty result')
203 203
204 204 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
205 205 log.error(traceback.format_exc())
206 h.flash(str(e), category='warning')
207 return redirect(url('home'))
206 h.flash(str(e), category='error')
207 raise HTTPNotFound()
208 208
209 209 c.changes = OrderedDict()
210 210
211 211 c.lines_added = 0 # count of lines added
212 212 c.lines_deleted = 0 # count of lines removes
213 213
214 214 c.changeset_statuses = ChangesetStatus.STATUSES
215 215 c.comments = []
216 216 c.statuses = []
217 217 c.inline_comments = []
218 218 c.inline_cnt = 0
219 219
220 220 # Iterate over ranges (default changeset view is always one changeset)
221 221 for changeset in c.cs_ranges:
222 222 inlines = []
223 223 if method == 'show':
224 224 c.statuses.extend([ChangesetStatusModel().get_status(
225 225 c.rhodecode_db_repo.repo_id, changeset.raw_id)])
226 226
227 227 c.comments.extend(ChangesetCommentsModel()\
228 228 .get_comments(c.rhodecode_db_repo.repo_id,
229 229 revision=changeset.raw_id))
230 230
231 231 #comments from PR
232 232 st = ChangesetStatusModel().get_statuses(
233 233 c.rhodecode_db_repo.repo_id, changeset.raw_id,
234 234 with_revisions=True)
235 235 # from associated statuses, check the pull requests, and
236 236 # show comments from them
237 237
238 238 prs = set([x.pull_request for x in
239 239 filter(lambda x: x.pull_request != None, st)])
240 240
241 241 for pr in prs:
242 242 c.comments.extend(pr.comments)
243 243 inlines = ChangesetCommentsModel()\
244 244 .get_inline_comments(c.rhodecode_db_repo.repo_id,
245 245 revision=changeset.raw_id)
246 246 c.inline_comments.extend(inlines)
247 247
248 248 c.changes[changeset.raw_id] = []
249 249
250 250 cs2 = changeset.raw_id
251 251 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset()
252 252 context_lcl = get_line_ctx('', request.GET)
253 253 ign_whitespace_lcl = ign_whitespace_lcl = get_ignore_ws('', request.GET)
254 254
255 255 _diff = c.rhodecode_repo.get_diff(cs1, cs2,
256 256 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
257 257 diff_limit = self.cut_off_limit if not fulldiff else None
258 258 diff_processor = diffs.DiffProcessor(_diff,
259 259 vcs=c.rhodecode_repo.alias,
260 260 format='gitdiff',
261 261 diff_limit=diff_limit)
262 262 cs_changes = OrderedDict()
263 263 if method == 'show':
264 264 _parsed = diff_processor.prepare()
265 265 c.limited_diff = False
266 266 if isinstance(_parsed, LimitedDiffContainer):
267 267 c.limited_diff = True
268 268 for f in _parsed:
269 269 st = f['stats']
270 270 if st[0] != 'b':
271 271 c.lines_added += st[0]
272 272 c.lines_deleted += st[1]
273 273 fid = h.FID(changeset.raw_id, f['filename'])
274 274 diff = diff_processor.as_html(enable_comments=enable_comments,
275 275 parsed_lines=[f])
276 276 cs_changes[fid] = [cs1, cs2, f['operation'], f['filename'],
277 277 diff, st]
278 278 else:
279 279 # downloads/raw we only need RAW diff nothing else
280 280 diff = diff_processor.as_raw()
281 281 cs_changes[''] = [None, None, None, None, diff, None]
282 282 c.changes[changeset.raw_id] = cs_changes
283 283
284 284 #sort comments by how they were generated
285 285 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
286 286
287 287 # count inline comments
288 288 for __, lines in c.inline_comments:
289 289 for comments in lines.values():
290 290 c.inline_cnt += len(comments)
291 291
292 292 if len(c.cs_ranges) == 1:
293 293 c.changeset = c.cs_ranges[0]
294 294 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
295 295 for x in c.changeset.parents])
296 296 if method == 'download':
297 297 response.content_type = 'text/plain'
298 298 response.content_disposition = 'attachment; filename=%s.diff' \
299 299 % revision[:12]
300 300 return diff
301 301 elif method == 'patch':
302 302 response.content_type = 'text/plain'
303 303 c.diff = safe_unicode(diff)
304 304 return render('changeset/patch_changeset.html')
305 305 elif method == 'raw':
306 306 response.content_type = 'text/plain'
307 307 return diff
308 308 elif method == 'show':
309 309 if len(c.cs_ranges) == 1:
310 310 return render('changeset/changeset.html')
311 311 else:
312 312 return render('changeset/changeset_range.html')
313 313
314 314 def changeset_raw(self, revision):
315 315 return self.index(revision, method='raw')
316 316
317 317 def changeset_patch(self, revision):
318 318 return self.index(revision, method='patch')
319 319
320 320 def changeset_download(self, revision):
321 321 return self.index(revision, method='download')
322 322
323 323 @jsonify
324 324 def comment(self, repo_name, revision):
325 325 status = request.POST.get('changeset_status')
326 326 change_status = request.POST.get('change_changeset_status')
327 327 text = request.POST.get('text')
328 328 if status and change_status:
329 329 text = text or (_('Status change -> %s')
330 330 % ChangesetStatus.get_status_lbl(status))
331 331
332 comm = ChangesetCommentsModel().create(
332 c.co = comm = ChangesetCommentsModel().create(
333 333 text=text,
334 334 repo=c.rhodecode_db_repo.repo_id,
335 335 user=c.rhodecode_user.user_id,
336 336 revision=revision,
337 337 f_path=request.POST.get('f_path'),
338 338 line_no=request.POST.get('line'),
339 339 status_change=(ChangesetStatus.get_status_lbl(status)
340 340 if status and change_status else None)
341 341 )
342 342
343 343 # get status if set !
344 344 if status and change_status:
345 345 # if latest status was from pull request and it's closed
346 346 # disallow changing status !
347 347 # dont_allow_on_closed_pull_request = True !
348 348
349 349 try:
350 350 ChangesetStatusModel().set_status(
351 351 c.rhodecode_db_repo.repo_id,
352 352 status,
353 353 c.rhodecode_user.user_id,
354 354 comm,
355 355 revision=revision,
356 356 dont_allow_on_closed_pull_request=True
357 357 )
358 358 except StatusChangeOnClosedPullRequestError:
359 359 log.error(traceback.format_exc())
360 360 msg = _('Changing status on a changeset associated with '
361 361 'a closed pull request is not allowed')
362 362 h.flash(msg, category='warning')
363 363 return redirect(h.url('changeset_home', repo_name=repo_name,
364 364 revision=revision))
365 365 action_logger(self.rhodecode_user,
366 366 'user_commented_revision:%s' % revision,
367 367 c.rhodecode_db_repo, self.ip_addr, self.sa)
368 368
369 369 Session().commit()
370 370
371 371 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
372 372 return redirect(h.url('changeset_home', repo_name=repo_name,
373 373 revision=revision))
374
374 #only ajax below
375 375 data = {
376 376 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
377 377 }
378 378 if comm:
379 c.co = comm
380 379 data.update(comm.get_dict())
381 380 data.update({'rendered_text':
382 381 render('changeset/changeset_comment_block.html')})
383 382
384 383 return data
385 384
386 385 @jsonify
387 386 def delete_comment(self, repo_name, comment_id):
388 387 co = ChangesetComment.get(comment_id)
389 388 owner = co.author.user_id == c.rhodecode_user.user_id
390 389 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
391 390 ChangesetCommentsModel().delete(comment=co)
392 391 Session().commit()
393 392 return True
394 393 else:
395 394 raise HTTPForbidden()
396 395
397 396 @jsonify
398 397 def changeset_info(self, repo_name, revision):
399 398 if request.is_xhr:
400 399 try:
401 400 return c.rhodecode_repo.get_changeset(revision)
402 401 except ChangesetDoesNotExistError, e:
403 402 return EmptyChangeset(message=str(e))
404 403 else:
405 404 raise HTTPBadRequest()
@@ -1,184 +1,188 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.compare
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 compare controller for pylons showoing differences between two
6 compare controller for pylons showing differences between two
7 7 repos, branches, bookmarks or tips
8 8
9 9 :created_on: May 6, 2012
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import logging
27 27 import traceback
28 28
29 29 from webob.exc import HTTPNotFound
30 30 from pylons import request, response, session, tmpl_context as c, url
31 31 from pylons.controllers.util import abort, redirect
32 32 from pylons.i18n.translation import _
33 33
34 34 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError, RepositoryError
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.base import BaseRepoController, render
37 37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 38 from rhodecode.lib import diffs
39 39
40 40 from rhodecode.model.db import Repository
41 41 from rhodecode.model.pull_request import PullRequestModel
42 42 from webob.exc import HTTPBadRequest
43 from rhodecode.lib.utils2 import str2bool
44 43 from rhodecode.lib.diffs import LimitedDiffContainer
45 44 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 45
47 46 log = logging.getLogger(__name__)
48 47
49 48
50 49 class CompareController(BaseRepoController):
51 50
52 51 @LoginRequired()
53 52 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
54 53 'repository.admin')
55 54 def __before__(self):
56 55 super(CompareController, self).__before__()
57 56
58 57 def __get_cs_or_redirect(self, rev, repo, redirect_after=True,
59 58 partial=False):
60 59 """
61 60 Safe way to get changeset if error occur it redirects to changeset with
62 61 proper message. If partial is set then don't do redirect raise Exception
63 62 instead
64 63
65 64 :param rev: revision to fetch
66 65 :param repo: repo instance
67 66 """
68 67
69 68 try:
70 69 type_, rev = rev
71 70 return repo.scm_instance.get_changeset(rev)
72 71 except EmptyRepositoryError, e:
73 72 if not redirect_after:
74 73 return None
75 74 h.flash(h.literal(_('There are no changesets yet')),
76 75 category='warning')
77 76 redirect(url('summary_home', repo_name=repo.repo_name))
78 77
79 78 except RepositoryError, e:
80 79 log.error(traceback.format_exc())
81 80 h.flash(str(e), category='warning')
82 81 if not partial:
83 82 redirect(h.url('summary_home', repo_name=repo.repo_name))
84 83 raise HTTPBadRequest()
85 84
86 85 def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
87
86 # org_ref will be evaluated in org_repo
88 87 org_repo = c.rhodecode_db_repo.repo_name
89 88 org_ref = (org_ref_type, org_ref)
89 # other_ref will be evaluated in other_repo
90 90 other_ref = (other_ref_type, other_ref)
91 other_repo = request.GET.get('repo', org_repo)
92 incoming_changesets = str2bool(request.GET.get('bundle', False))
93 c.fulldiff = fulldiff = request.GET.get('fulldiff')
94 rev_start = request.GET.get('rev_start')
95 rev_end = request.GET.get('rev_end')
96
97 c.swap_url = h.url('compare_url', repo_name=other_repo,
91 other_repo = request.GET.get('other_repo', org_repo)
92 # If merge is True:
93 # Show what org would get if merged with other:
94 # List changesets that are ancestors of other but not of org.
95 # New changesets in org is thus ignored.
96 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
97 # If merge is False:
98 # Make a raw diff from org to other, no matter if related or not.
99 # Changesets in one and not in the other will be ignored
100 merge = bool(request.GET.get('merge'))
101 # fulldiff disables cut_off_limit
102 c.fulldiff = request.GET.get('fulldiff')
103 # partial uses compare_cs.html template directly
104 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
105 # as_form puts hidden input field with changeset revisions
106 c.as_form = partial and request.GET.get('as_form')
107 # swap url for compare_diff page - never partial and never as_form
108 c.swap_url = h.url('compare_url',
109 repo_name=other_repo,
98 110 org_ref_type=other_ref[0], org_ref=other_ref[1],
111 other_repo=org_repo,
99 112 other_ref_type=org_ref[0], other_ref=org_ref[1],
100 repo=org_repo, as_form=request.GET.get('as_form'),
101 bundle=incoming_changesets)
113 merge=merge or '')
102 114
103 c.org_repo = org_repo = Repository.get_by_repo_name(org_repo)
104 c.other_repo = other_repo = Repository.get_by_repo_name(other_repo)
115 org_repo = Repository.get_by_repo_name(org_repo)
116 other_repo = Repository.get_by_repo_name(other_repo)
105 117
106 if c.org_repo is None:
118 if org_repo is None:
107 119 log.error('Could not find org repo %s' % org_repo)
108 120 raise HTTPNotFound
109 if c.other_repo is None:
121 if other_repo is None:
110 122 log.error('Could not find other repo %s' % other_repo)
111 123 raise HTTPNotFound
112 124
113 if c.org_repo != c.other_repo and h.is_git(c.rhodecode_repo):
125 if org_repo != other_repo and h.is_git(org_repo):
114 126 log.error('compare of two remote repos not available for GIT REPOS')
115 127 raise HTTPNotFound
116 128
117 if c.org_repo.scm_instance.alias != c.other_repo.scm_instance.alias:
129 if org_repo.scm_instance.alias != other_repo.scm_instance.alias:
118 130 log.error('compare of two different kind of remote repos not available')
119 131 raise HTTPNotFound
120 132
121 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
122 133 self.__get_cs_or_redirect(rev=org_ref, repo=org_repo, partial=partial)
123 134 self.__get_cs_or_redirect(rev=other_ref, repo=other_repo, partial=partial)
124 135
125 if rev_start and rev_end:
126 #replace our org_ref with given CS
127 org_ref = ('rev', rev_start)
128 other_ref = ('rev', rev_end)
136 c.org_repo = org_repo
137 c.other_repo = other_repo
138 c.org_ref = org_ref[1]
139 c.other_ref = other_ref[1]
140 c.org_ref_type = org_ref[0]
141 c.other_ref_type = other_ref[0]
129 142
130 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
131 org_repo, org_ref, other_repo, other_ref,
132 )
143 c.cs_ranges, c.ancestor = PullRequestModel().get_compare_data(
144 org_repo, org_ref, other_repo, other_ref, merge)
133 145
134 146 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
135 147 c.cs_ranges])
136 c.target_repo = c.repo_name
137 # defines that we need hidden inputs with changesets
138 c.as_form = request.GET.get('as_form', False)
139 148 if partial:
149 assert c.ancestor
140 150 return render('compare/compare_cs.html')
141 151
142 c.org_ref = org_ref[1]
143 c.other_ref = other_ref[1]
152 if c.ancestor:
153 assert merge
154 # case we want a simple diff without incoming changesets,
155 # previewing what will be merged.
156 # Make the diff on the other repo (which is known to have other_ref)
157 log.debug('Using ancestor %s as org_ref instead of %s'
158 % (c.ancestor, org_ref))
159 org_ref = ('rev', c.ancestor)
160 org_repo = other_repo
144 161
145 if not incoming_changesets and c.cs_ranges and c.org_repo != c.other_repo:
146 # case we want a simple diff without incoming changesets, just
147 # for review purposes. Make the diff on the forked repo, with
148 # revision that is common ancestor
149 _org_ref = org_ref
150 org_ref = ('rev', getattr(c.cs_ranges[0].parents[0]
151 if c.cs_ranges[0].parents
152 else EmptyChangeset(), 'raw_id'))
153 log.debug('Changed org_ref from %s to %s' % (_org_ref, org_ref))
154 other_repo = org_repo
162 diff_limit = self.cut_off_limit if not c.fulldiff else None
155 163
156 diff_limit = self.cut_off_limit if not fulldiff else None
157
158 _diff = diffs.differ(org_repo, org_ref, other_repo, other_ref,
159 discovery_data,
160 remote_compare=incoming_changesets)
164 _diff = diffs.differ(org_repo, org_ref, other_repo, other_ref)
161 165
162 166 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
163 167 diff_limit=diff_limit)
164 168 _parsed = diff_processor.prepare()
165 169
166 170 c.limited_diff = False
167 171 if isinstance(_parsed, LimitedDiffContainer):
168 172 c.limited_diff = True
169 173
170 174 c.files = []
171 175 c.changes = {}
172 176 c.lines_added = 0
173 177 c.lines_deleted = 0
174 178 for f in _parsed:
175 179 st = f['stats']
176 180 if st[0] != 'b':
177 181 c.lines_added += st[0]
178 182 c.lines_deleted += st[1]
179 183 fid = h.FID('', f['filename'])
180 184 c.files.append([fid, f['operation'], f['filename'], f['stats']])
181 185 diff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
182 186 c.changes[fid] = [f['operation'], f['filename'], diff]
183 187
184 188 return render('compare/compare_diff.html')
@@ -1,108 +1,108 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.error
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 RhodeCode error controller
7 7
8 8 :created_on: Dec 8, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import os
26 26 import cgi
27 27 import logging
28 28 import paste.fileapp
29 29
30 30 from pylons import tmpl_context as c, request, config, url
31 31 from pylons.i18n.translation import _
32 32 from pylons.middleware import media_path
33 33
34 34 from rhodecode.lib.base import BaseController, render
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class ErrorController(BaseController):
40 40 """Generates error documents as and when they are required.
41 41
42 42 The ErrorDocuments middleware forwards to ErrorController when error
43 43 related status codes are returned from the application.
44 44
45 45 This behavior can be altered by changing the parameters to the
46 46 ErrorDocuments middleware in your config/middleware.py file.
47 47 """
48 48
49 49 def __before__(self):
50 50 #disable all base actions since we don't need them here
51 51 pass
52 52
53 53 def document(self):
54 54 resp = request.environ.get('pylons.original_response')
55 55 c.rhodecode_name = config.get('rhodecode_title')
56 56
57 57 log.debug('### %s ###' % resp.status)
58 58
59 59 e = request.environ
60 60 c.serv_p = r'%(protocol)s://%(host)s/' \
61 61 % {'protocol': e.get('wsgi.url_scheme'),
62 62 'host': e.get('HTTP_HOST'), }
63 63
64 64 c.error_message = cgi.escape(request.GET.get('code', str(resp.status)))
65 65 c.error_explanation = self.get_error_explanation(resp.status_int)
66 66
67 67 # redirect to when error with given seconds
68 68 c.redirect_time = 0
69 69 c.redirect_module = _('Home page')
70 70 c.url_redirect = "/"
71 71
72 72 return render('/errors/error_document.html')
73 73
74 74 def img(self, id):
75 75 """Serve Pylons' stock images"""
76 76 return self._serve_file(os.path.join(media_path, 'img', id))
77 77
78 78 def style(self, id):
79 79 """Serve Pylons' stock stylesheets"""
80 80 return self._serve_file(os.path.join(media_path, 'style', id))
81 81
82 82 def _serve_file(self, path):
83 83 """Call Paste's FileApp (a WSGI application) to serve the file
84 84 at the specified path
85 85 """
86 86 fapp = paste.fileapp.FileApp(path)
87 87 return fapp(request.environ, self.start_response)
88 88
89 89 def get_error_explanation(self, code):
90 90 ''' get the error explanations of int codes
91 91 [400, 401, 403, 404, 500]'''
92 92 try:
93 93 code = int(code)
94 except:
94 except Exception:
95 95 code = 500
96 96
97 97 if code == 400:
98 98 return _('The request could not be understood by the server'
99 99 ' due to malformed syntax.')
100 100 if code == 401:
101 101 return _('Unauthorized access to resource')
102 102 if code == 403:
103 103 return _("You don't have permission to view this page")
104 104 if code == 404:
105 105 return _('The resource could not be found')
106 106 if code == 500:
107 107 return _('The server encountered an unexpected condition'
108 108 ' which prevented it from fulfilling the request.')
@@ -1,182 +1,181 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.feed
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Feed controller for rhodecode
7 7
8 8 :created_on: Apr 23, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27
28 28 from pylons import url, response, tmpl_context as c
29 29 from pylons.i18n.translation import _
30 30
31 31 from beaker.cache import cache_region, region_invalidate
32 32 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
33 33
34 34 from rhodecode.lib import helpers as h
35 35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 36 from rhodecode.lib.base import BaseRepoController
37 37 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
38 38 from rhodecode.model.db import CacheInvalidation
39 39 from rhodecode.lib.utils2 import safe_int, str2bool, safe_unicode
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class FeedController(BaseRepoController):
45 45
46 46 @LoginRequired(api_access=True)
47 47 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
48 48 'repository.admin')
49 49 def __before__(self):
50 50 super(FeedController, self).__before__()
51 51 #common values for feeds
52 52 self.description = _('Changes on %s repository')
53 53 self.title = self.title = _('%s %s feed') % (c.rhodecode_name, '%s')
54 54 self.language = 'en-us'
55 55 self.ttl = "5"
56 56 import rhodecode
57 57 CONF = rhodecode.CONFIG
58 58 self.include_diff = str2bool(CONF.get('rss_include_diff', False))
59 59 self.feed_nr = safe_int(CONF.get('rss_items_per_page', 20))
60 60 # we need to protect from parsing huge diffs here other way
61 61 # we can kill the server
62 62 self.feed_diff_limit = safe_int(CONF.get('rss_cut_off_limit', 32 * 1024))
63 63
64 64 def _get_title(self, cs):
65 65 return "%s" % (
66 66 h.shorter(cs.message, 160)
67 67 )
68 68
69 69 def __changes(self, cs):
70 70 changes = []
71 71 diff_processor = DiffProcessor(cs.diff(),
72 72 diff_limit=self.feed_diff_limit)
73 73 _parsed = diff_processor.prepare(inline_diff=False)
74 74 limited_diff = False
75 75 if isinstance(_parsed, LimitedDiffContainer):
76 76 limited_diff = True
77 77
78 78 for st in _parsed:
79 79 st.update({'added': st['stats'][0],
80 80 'removed': st['stats'][1]})
81 81 changes.append('\n %(operation)s %(filename)s '
82 82 '(%(added)s lines added, %(removed)s lines removed)'
83 83 % st)
84 84 if limited_diff:
85 85 changes = changes + ['\n ' +
86 86 _('Changeset was too big and was cut off...')]
87 87 return diff_processor, changes
88 88
89 89 def __get_desc(self, cs):
90 90 desc_msg = []
91 desc_msg.append('%s %s %s<br/>' % (h.person(cs.author),
92 _('commited on'),
93 h.fmt_date(cs.date)))
91 desc_msg.append((_('%s committed on %s')
92 % (h.person(cs.author), h.fmt_date(cs.date))) + '<br/>')
94 93 #branches, tags, bookmarks
95 94 if cs.branch:
96 95 desc_msg.append('branch: %s<br/>' % cs.branch)
97 96 if h.is_hg(c.rhodecode_repo):
98 97 for book in cs.bookmarks:
99 98 desc_msg.append('bookmark: %s<br/>' % book)
100 99 for tag in cs.tags:
101 100 desc_msg.append('tag: %s<br/>' % tag)
102 101 diff_processor, changes = self.__changes(cs)
103 102 # rev link
104 103 _url = url('changeset_home', repo_name=cs.repository.name,
105 104 revision=cs.raw_id, qualified=True)
106 desc_msg.append('changesest: <a href="%s">%s</a>' % (_url, cs.raw_id[:8]))
105 desc_msg.append('changeset: <a href="%s">%s</a>' % (_url, cs.raw_id[:8]))
107 106
108 107 desc_msg.append('<pre>')
109 108 desc_msg.append(cs.message)
110 109 desc_msg.append('\n')
111 110 desc_msg.extend(changes)
112 111 if self.include_diff:
113 112 desc_msg.append('\n\n')
114 113 desc_msg.append(diff_processor.as_raw())
115 114 desc_msg.append('</pre>')
116 115 return map(safe_unicode, desc_msg)
117 116
118 117 def atom(self, repo_name):
119 118 """Produce an atom-1.0 feed via feedgenerator module"""
120 119
121 120 @cache_region('long_term')
122 121 def _get_feed_from_cache(key):
123 122 feed = Atom1Feed(
124 123 title=self.title % repo_name,
125 124 link=url('summary_home', repo_name=repo_name,
126 125 qualified=True),
127 126 description=self.description % repo_name,
128 127 language=self.language,
129 128 ttl=self.ttl
130 129 )
131 130
132 131 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
133 132 feed.add_item(title=self._get_title(cs),
134 133 link=url('changeset_home', repo_name=repo_name,
135 134 revision=cs.raw_id, qualified=True),
136 135 author_name=cs.author,
137 136 description=''.join(self.__get_desc(cs)),
138 137 pubdate=cs.date,
139 138 )
140 139
141 140 response.content_type = feed.mime_type
142 141 return feed.writeString('utf-8')
143 142
144 143 key = repo_name + '_ATOM'
145 144 inv = CacheInvalidation.invalidate(key)
146 145 if inv is not None:
147 146 region_invalidate(_get_feed_from_cache, None, key)
148 147 CacheInvalidation.set_valid(inv.cache_key)
149 148 return _get_feed_from_cache(key)
150 149
151 150 def rss(self, repo_name):
152 151 """Produce an rss2 feed via feedgenerator module"""
153 152
154 153 @cache_region('long_term')
155 154 def _get_feed_from_cache(key):
156 155 feed = Rss201rev2Feed(
157 156 title=self.title % repo_name,
158 157 link=url('summary_home', repo_name=repo_name,
159 158 qualified=True),
160 159 description=self.description % repo_name,
161 160 language=self.language,
162 161 ttl=self.ttl
163 162 )
164 163
165 164 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
166 165 feed.add_item(title=self._get_title(cs),
167 166 link=url('changeset_home', repo_name=repo_name,
168 167 revision=cs.raw_id, qualified=True),
169 168 author_name=cs.author,
170 169 description=''.join(self.__get_desc(cs)),
171 170 pubdate=cs.date,
172 171 )
173 172
174 173 response.content_type = feed.mime_type
175 174 return feed.writeString('utf-8')
176 175
177 176 key = repo_name + '_RSS'
178 177 inv = CacheInvalidation.invalidate(key)
179 178 if inv is not None:
180 179 region_invalidate(_get_feed_from_cache, None, key)
181 180 CacheInvalidation.set_valid(inv.cache_key)
182 181 return _get_feed_from_cache(key)
@@ -1,594 +1,647 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.files
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Files controller for RhodeCode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 from __future__ import with_statement
26 26 import os
27 27 import logging
28 28 import traceback
29 29 import tempfile
30 import shutil
30 31
31 32 from pylons import request, response, tmpl_context as c, url
32 33 from pylons.i18n.translation import _
33 34 from pylons.controllers.util import redirect
34 35 from rhodecode.lib.utils import jsonify
35 36
36 37 from rhodecode.lib import diffs
37 38 from rhodecode.lib import helpers as h
38 39
39 40 from rhodecode.lib.compat import OrderedDict
40 41 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str,\
41 42 str2bool
42 43 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
43 44 from rhodecode.lib.base import BaseRepoController, render
44 45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45 46 from rhodecode.lib.vcs.conf import settings
46 47 from rhodecode.lib.vcs.exceptions import RepositoryError, \
47 48 ChangesetDoesNotExistError, EmptyRepositoryError, \
48 49 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,\
49 50 NodeDoesNotExistError, ChangesetError, NodeError
50 51 from rhodecode.lib.vcs.nodes import FileNode
51 52
52 53 from rhodecode.model.repo import RepoModel
53 54 from rhodecode.model.scm import ScmModel
54 55 from rhodecode.model.db import Repository
55 56
56 57 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
57 58 _context_url, get_line_ctx, get_ignore_ws
59 from webob.exc import HTTPNotFound
58 60
59 61
60 62 log = logging.getLogger(__name__)
61 63
62 64
63 65 class FilesController(BaseRepoController):
64 66
65 67 def __before__(self):
66 68 super(FilesController, self).__before__()
67 69 c.cut_off_limit = self.cut_off_limit
68 70
69 71 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
70 72 """
71 73 Safe way to get changeset if error occur it redirects to tip with
72 74 proper message
73 75
74 76 :param rev: revision to fetch
75 77 :param repo_name: repo name to redirect after
76 78 """
77 79
78 80 try:
79 81 return c.rhodecode_repo.get_changeset(rev)
80 82 except EmptyRepositoryError, e:
81 83 if not redirect_after:
82 84 return None
83 85 url_ = url('files_add_home',
84 86 repo_name=c.repo_name,
85 87 revision=0, f_path='')
86 add_new = '<a href="%s">[%s]</a>' % (url_, _('click here to add new file'))
88 add_new = h.link_to(_('Click here to add new file'), url_)
87 89 h.flash(h.literal(_('There are no files yet %s') % add_new),
88 90 category='warning')
89 91 redirect(h.url('summary_home', repo_name=repo_name))
90 92
91 except RepositoryError, e:
92 h.flash(str(e), category='warning')
93 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
93 except RepositoryError, e: # including ChangesetDoesNotExistError
94 h.flash(str(e), category='error')
95 raise HTTPNotFound()
94 96
95 97 def __get_filenode_or_redirect(self, repo_name, cs, path):
96 98 """
97 99 Returns file_node, if error occurs or given path is directory,
98 100 it'll redirect to top level path
99 101
100 102 :param repo_name: repo_name
101 103 :param cs: given changeset
102 104 :param path: path to lookup
103 105 """
104 106
105 107 try:
106 108 file_node = cs.get_node(path)
107 109 if file_node.is_dir():
108 110 raise RepositoryError('given path is a directory')
109 111 except RepositoryError, e:
110 h.flash(str(e), category='warning')
111 redirect(h.url('files_home', repo_name=repo_name,
112 revision=cs.raw_id))
112 h.flash(str(e), category='error')
113 raise HTTPNotFound()
113 114
114 115 return file_node
115 116
116 117 @LoginRequired()
117 118 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
118 119 'repository.admin')
119 120 def index(self, repo_name, revision, f_path, annotate=False):
120 121 # redirect to given revision from form if given
121 122 post_revision = request.POST.get('at_rev', None)
122 123 if post_revision:
123 124 cs = self.__get_cs_or_redirect(post_revision, repo_name)
124 redirect(url('files_home', repo_name=c.repo_name,
125 revision=cs.raw_id, f_path=f_path))
126 125
127 126 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
128 127 c.branch = request.GET.get('branch', None)
129 128 c.f_path = f_path
130 129 c.annotate = annotate
130 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
131 131 cur_rev = c.changeset.revision
132 132
133 133 # prev link
134 134 try:
135 135 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
136 136 c.url_prev = url('files_home', repo_name=c.repo_name,
137 137 revision=prev_rev.raw_id, f_path=f_path)
138 138 if c.branch:
139 139 c.url_prev += '?branch=%s' % c.branch
140 140 except (ChangesetDoesNotExistError, VCSError):
141 141 c.url_prev = '#'
142 142
143 143 # next link
144 144 try:
145 145 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
146 146 c.url_next = url('files_home', repo_name=c.repo_name,
147 147 revision=next_rev.raw_id, f_path=f_path)
148 148 if c.branch:
149 149 c.url_next += '?branch=%s' % c.branch
150 150 except (ChangesetDoesNotExistError, VCSError):
151 151 c.url_next = '#'
152 152
153 153 # files or dirs
154 154 try:
155 155 c.file = c.changeset.get_node(f_path)
156 156
157 157 if c.file.is_file():
158 158 c.load_full_history = False
159 159 file_last_cs = c.file.last_changeset
160 160 c.file_changeset = (c.changeset
161 161 if c.changeset.revision < file_last_cs.revision
162 162 else file_last_cs)
163 #determine if we're on branch head
164 _branches = c.rhodecode_repo.branches
165 c.on_branch_head = revision in _branches.keys() + _branches.values()
163 166 _hist = []
164 167 c.file_history = []
165 168 if c.load_full_history:
166 169 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
167 170
168 171 c.authors = []
169 172 for a in set([x.author for x in _hist]):
170 173 c.authors.append((h.email(a), h.person(a)))
171 174 else:
172 175 c.authors = c.file_history = []
173 176 except RepositoryError, e:
174 h.flash(str(e), category='warning')
175 redirect(h.url('files_home', repo_name=repo_name,
176 revision='tip'))
177 h.flash(str(e), category='error')
178 raise HTTPNotFound()
177 179
178 180 if request.environ.get('HTTP_X_PARTIAL_XHR'):
179 181 return render('files/files_ypjax.html')
180 182
181 183 return render('files/files.html')
182 184
183 185 def history(self, repo_name, revision, f_path, annotate=False):
184 186 if request.environ.get('HTTP_X_PARTIAL_XHR'):
185 187 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
186 188 c.f_path = f_path
187 189 c.annotate = annotate
188 190 c.file = c.changeset.get_node(f_path)
189 191 if c.file.is_file():
190 192 file_last_cs = c.file.last_changeset
191 193 c.file_changeset = (c.changeset
192 194 if c.changeset.revision < file_last_cs.revision
193 195 else file_last_cs)
194 196 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
195 197 c.authors = []
196 198 for a in set([x.author for x in _hist]):
197 199 c.authors.append((h.email(a), h.person(a)))
198 200 return render('files/files_history_box.html')
199 201
200 202 @LoginRequired()
201 203 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
202 204 'repository.admin')
203 205 def rawfile(self, repo_name, revision, f_path):
204 206 cs = self.__get_cs_or_redirect(revision, repo_name)
205 207 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
206 208
207 209 response.content_disposition = 'attachment; filename=%s' % \
208 210 safe_str(f_path.split(Repository.url_sep())[-1])
209 211
210 212 response.content_type = file_node.mimetype
211 213 return file_node.content
212 214
213 215 @LoginRequired()
214 216 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
215 217 'repository.admin')
216 218 def raw(self, repo_name, revision, f_path):
217 219 cs = self.__get_cs_or_redirect(revision, repo_name)
218 220 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
219 221
220 222 raw_mimetype_mapping = {
221 223 # map original mimetype to a mimetype used for "show as raw"
222 224 # you can also provide a content-disposition to override the
223 225 # default "attachment" disposition.
224 226 # orig_type: (new_type, new_dispo)
225 227
226 228 # show images inline:
227 229 'image/x-icon': ('image/x-icon', 'inline'),
228 230 'image/png': ('image/png', 'inline'),
229 231 'image/gif': ('image/gif', 'inline'),
230 232 'image/jpeg': ('image/jpeg', 'inline'),
231 233 'image/svg+xml': ('image/svg+xml', 'inline'),
232 234 }
233 235
234 236 mimetype = file_node.mimetype
235 237 try:
236 238 mimetype, dispo = raw_mimetype_mapping[mimetype]
237 239 except KeyError:
238 240 # we don't know anything special about this, handle it safely
239 241 if file_node.is_binary:
240 242 # do same as download raw for binary files
241 243 mimetype, dispo = 'application/octet-stream', 'attachment'
242 244 else:
243 245 # do not just use the original mimetype, but force text/plain,
244 246 # otherwise it would serve text/html and that might be unsafe.
245 247 # Note: underlying vcs library fakes text/plain mimetype if the
246 248 # mimetype can not be determined and it thinks it is not
247 249 # binary.This might lead to erroneous text display in some
248 250 # cases, but helps in other cases, like with text files
249 251 # without extension.
250 252 mimetype, dispo = 'text/plain', 'inline'
251 253
252 254 if dispo == 'attachment':
253 255 dispo = 'attachment; filename=%s' % \
254 256 safe_str(f_path.split(os.sep)[-1])
255 257
256 258 response.content_disposition = dispo
257 259 response.content_type = mimetype
258 260 return file_node.content
259 261
260 262 @LoginRequired()
261 263 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
262 264 def edit(self, repo_name, revision, f_path):
263 repo = Repository.get_by_repo_name(repo_name)
265 repo = c.rhodecode_db_repo
264 266 if repo.enable_locking and repo.locked[0]:
265 267 h.flash(_('This repository is has been locked by %s on %s')
266 268 % (h.person_by_id(repo.locked[0]),
267 269 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
268 270 'warning')
269 271 return redirect(h.url('files_home',
270 272 repo_name=repo_name, revision='tip'))
271 273
274 # check if revision is a branch identifier- basically we cannot
275 # create multiple heads via file editing
276 _branches = repo.scm_instance.branches
277 # check if revision is a branch name or branch hash
278 if revision not in _branches.keys() + _branches.values():
279 h.flash(_('You can only edit files with revision '
280 'being a valid branch '), category='warning')
281 return redirect(h.url('files_home',
282 repo_name=repo_name, revision='tip',
283 f_path=f_path))
284
272 285 r_post = request.POST
273 286
274 287 c.cs = self.__get_cs_or_redirect(revision, repo_name)
275 288 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
276 289
277 290 if c.file.is_binary:
278 291 return redirect(url('files_home', repo_name=c.repo_name,
279 292 revision=c.cs.raw_id, f_path=f_path))
280
293 c.default_message = _('Edited file %s via RhodeCode') % (f_path)
281 294 c.f_path = f_path
282 295
283 296 if r_post:
284 297
285 298 old_content = c.file.content
286 299 sl = old_content.splitlines(1)
287 300 first_line = sl[0] if sl else ''
288 301 # modes: 0 - Unix, 1 - Mac, 2 - DOS
289 302 mode = detect_mode(first_line, 0)
290 303 content = convert_line_endings(r_post.get('content'), mode)
291 304
292 message = r_post.get('message') or (_('Edited %s via RhodeCode')
293 % (f_path))
305 message = r_post.get('message') or c.default_message
294 306 author = self.rhodecode_user.full_contact
295 307
296 308 if content == old_content:
297 h.flash(_('No changes'),
298 category='warning')
309 h.flash(_('No changes'), category='warning')
299 310 return redirect(url('changeset_home', repo_name=c.repo_name,
300 311 revision='tip'))
301
302 312 try:
303 313 self.scm_model.commit_change(repo=c.rhodecode_repo,
304 314 repo_name=repo_name, cs=c.cs,
305 user=self.rhodecode_user,
315 user=self.rhodecode_user.user_id,
306 316 author=author, message=message,
307 317 content=content, f_path=f_path)
308 318 h.flash(_('Successfully committed to %s') % f_path,
309 319 category='success')
310 320
311 321 except Exception:
312 322 log.error(traceback.format_exc())
313 323 h.flash(_('Error occurred during commit'), category='error')
314 324 return redirect(url('changeset_home',
315 325 repo_name=c.repo_name, revision='tip'))
316 326
317 327 return render('files/files_edit.html')
318 328
319 329 @LoginRequired()
320 330 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
321 331 def add(self, repo_name, revision, f_path):
322 332
323 333 repo = Repository.get_by_repo_name(repo_name)
324 334 if repo.enable_locking and repo.locked[0]:
325 335 h.flash(_('This repository is has been locked by %s on %s')
326 336 % (h.person_by_id(repo.locked[0]),
327 337 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
328 338 'warning')
329 339 return redirect(h.url('files_home',
330 340 repo_name=repo_name, revision='tip'))
331 341
332 342 r_post = request.POST
333 343 c.cs = self.__get_cs_or_redirect(revision, repo_name,
334 344 redirect_after=False)
335 345 if c.cs is None:
336 346 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
337
347 c.default_message = (_('Added file via RhodeCode'))
338 348 c.f_path = f_path
339 349
340 350 if r_post:
341 351 unix_mode = 0
342 352 content = convert_line_endings(r_post.get('content'), unix_mode)
343 353
344 message = r_post.get('message') or (_('Added %s via RhodeCode')
345 % (f_path))
354 message = r_post.get('message') or c.default_message
355 filename = r_post.get('filename')
346 356 location = r_post.get('location')
347 filename = r_post.get('filename')
348 357 file_obj = r_post.get('upload_file', None)
349 358
350 359 if file_obj is not None and hasattr(file_obj, 'filename'):
351 360 filename = file_obj.filename
352 361 content = file_obj.file
353 362
354 node_path = os.path.join(location, filename)
355 author = self.rhodecode_user.full_contact
356
357 363 if not content:
358 364 h.flash(_('No content'), category='warning')
359 365 return redirect(url('changeset_home', repo_name=c.repo_name,
360 366 revision='tip'))
361 367 if not filename:
362 368 h.flash(_('No filename'), category='warning')
363 369 return redirect(url('changeset_home', repo_name=c.repo_name,
364 370 revision='tip'))
371 if location.startswith('/') or location.startswith('.') or '../' in location:
372 h.flash(_('Location must be relative path and must not '
373 'contain .. in path'), category='warning')
374 return redirect(url('changeset_home', repo_name=c.repo_name,
375 revision='tip'))
376 if location:
377 location = os.path.normpath(location)
378 filename = os.path.basename(filename)
379 node_path = os.path.join(location, filename)
380 author = self.rhodecode_user.full_contact
365 381
366 382 try:
367 383 self.scm_model.create_node(repo=c.rhodecode_repo,
368 384 repo_name=repo_name, cs=c.cs,
369 user=self.rhodecode_user,
385 user=self.rhodecode_user.user_id,
370 386 author=author, message=message,
371 387 content=content, f_path=node_path)
372 388 h.flash(_('Successfully committed to %s') % node_path,
373 389 category='success')
374 except NodeAlreadyExistsError, e:
390 except (NodeError, NodeAlreadyExistsError), e:
375 391 h.flash(_(e), category='error')
376 392 except Exception:
377 393 log.error(traceback.format_exc())
378 394 h.flash(_('Error occurred during commit'), category='error')
379 395 return redirect(url('changeset_home',
380 396 repo_name=c.repo_name, revision='tip'))
381 397
382 398 return render('files/files_add.html')
383 399
384 400 @LoginRequired()
385 401 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
386 402 'repository.admin')
387 403 def archivefile(self, repo_name, fname):
388 404
389 405 fileformat = None
390 406 revision = None
391 407 ext = None
392 408 subrepos = request.GET.get('subrepos') == 'true'
393 409
394 410 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
395 411 archive_spec = fname.split(ext_data[1])
396 412 if len(archive_spec) == 2 and archive_spec[1] == '':
397 413 fileformat = a_type or ext_data[1]
398 414 revision = archive_spec[0]
399 415 ext = ext_data[1]
400 416
401 417 try:
402 418 dbrepo = RepoModel().get_by_repo_name(repo_name)
403 if dbrepo.enable_downloads is False:
404 return _('downloads disabled')
419 if not dbrepo.enable_downloads:
420 return _('Downloads disabled')
405 421
406 422 if c.rhodecode_repo.alias == 'hg':
407 423 # patch and reset hooks section of UI config to not run any
408 424 # hooks on fetching archives with subrepos
409 425 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
410 426 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
411 427
412 428 cs = c.rhodecode_repo.get_changeset(revision)
413 429 content_type = settings.ARCHIVE_SPECS[fileformat][0]
414 430 except ChangesetDoesNotExistError:
415 431 return _('Unknown revision %s') % revision
416 432 except EmptyRepositoryError:
417 433 return _('Empty repository')
418 434 except (ImproperArchiveTypeError, KeyError):
419 435 return _('Unknown archive type')
436 # archive cache
437 from rhodecode import CONFIG
438 rev_name = cs.raw_id[:12]
439 archive_name = '%s-%s%s' % (safe_str(repo_name.replace('/', '_')),
440 safe_str(rev_name), ext)
420 441
442 use_cached_archive = False # defines if we use cached version of archive
443 archive_cache_enabled = CONFIG.get('archive_cache_dir')
444 if not subrepos and archive_cache_enabled:
445 #check if we it's ok to write
446 if not os.path.isdir(CONFIG['archive_cache_dir']):
447 os.makedirs(CONFIG['archive_cache_dir'])
448 cached_archive_path = os.path.join(CONFIG['archive_cache_dir'], archive_name)
449 if os.path.isfile(cached_archive_path):
450 log.debug('Found cached archive in %s' % cached_archive_path)
451 fd, archive = None, cached_archive_path
452 use_cached_archive = True
453 else:
454 log.debug('Archive %s is not yet cached' % (archive_name))
455
456 if not use_cached_archive:
457 #generate new archive
458 try:
421 459 fd, archive = tempfile.mkstemp()
422 460 t = open(archive, 'wb')
461 log.debug('Creating new temp archive in %s' % archive)
423 462 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
463 if archive_cache_enabled:
464 #if we generated the archive and use cache rename that
465 log.debug('Storing new archive in %s' % cached_archive_path)
466 shutil.move(archive, cached_archive_path)
467 archive = cached_archive_path
468 finally:
424 469 t.close()
425 470
426 471 def get_chunked_archive(archive):
427 472 stream = open(archive, 'rb')
428 473 while True:
429 474 data = stream.read(16 * 1024)
430 475 if not data:
431 476 stream.close()
477 if fd: # fd means we used temporary file
432 478 os.close(fd)
479 if not archive_cache_enabled:
480 log.debug('Destroing temp archive %s' % archive)
433 481 os.remove(archive)
434 482 break
435 483 yield data
436 484
437 response.content_disposition = str('attachment; filename=%s-%s%s' \
438 % (repo_name, revision[:12], ext))
485 response.content_disposition = str('attachment; filename=%s' % (archive_name))
439 486 response.content_type = str(content_type)
440 487 return get_chunked_archive(archive)
441 488
442 489 @LoginRequired()
443 490 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
444 491 'repository.admin')
445 492 def diff(self, repo_name, f_path):
446 493 ignore_whitespace = request.GET.get('ignorews') == '1'
447 494 line_context = request.GET.get('context', 3)
448 495 diff1 = request.GET.get('diff1', '')
449 496 diff2 = request.GET.get('diff2', '')
450 497 c.action = request.GET.get('diff')
451 498 c.no_changes = diff1 == diff2
452 499 c.f_path = f_path
453 500 c.big_diff = False
454 501 c.anchor_url = anchor_url
455 502 c.ignorews_url = _ignorews_url
456 503 c.context_url = _context_url
457 504 c.changes = OrderedDict()
458 505 c.changes[diff2] = []
459 506
460 507 #special case if we want a show rev only, it's impl here
461 508 #to reduce JS and callbacks
462 509
463 510 if request.GET.get('show_rev'):
464 511 if str2bool(request.GET.get('annotate', 'False')):
465 512 _url = url('files_annotate_home', repo_name=c.repo_name,
466 513 revision=diff1, f_path=c.f_path)
467 514 else:
468 515 _url = url('files_home', repo_name=c.repo_name,
469 516 revision=diff1, f_path=c.f_path)
470 517
471 518 return redirect(_url)
472 519 try:
473 520 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
474 521 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
475 522 try:
476 523 node1 = c.changeset_1.get_node(f_path)
524 if node1.is_dir():
525 raise NodeError('%s path is a %s not a file'
526 % (node1, type(node1)))
477 527 except NodeDoesNotExistError:
478 528 c.changeset_1 = EmptyChangeset(cs=diff1,
479 529 revision=c.changeset_1.revision,
480 530 repo=c.rhodecode_repo)
481 531 node1 = FileNode(f_path, '', changeset=c.changeset_1)
482 532 else:
483 533 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
484 534 node1 = FileNode(f_path, '', changeset=c.changeset_1)
485 535
486 536 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
487 537 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
488 538 try:
489 539 node2 = c.changeset_2.get_node(f_path)
540 if node2.is_dir():
541 raise NodeError('%s path is a %s not a file'
542 % (node2, type(node2)))
490 543 except NodeDoesNotExistError:
491 544 c.changeset_2 = EmptyChangeset(cs=diff2,
492 545 revision=c.changeset_2.revision,
493 546 repo=c.rhodecode_repo)
494 547 node2 = FileNode(f_path, '', changeset=c.changeset_2)
495 548 else:
496 549 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
497 550 node2 = FileNode(f_path, '', changeset=c.changeset_2)
498 551 except (RepositoryError, NodeError):
499 552 log.error(traceback.format_exc())
500 553 return redirect(url('files_home', repo_name=c.repo_name,
501 554 f_path=f_path))
502 555
503 556 if c.action == 'download':
504 557 _diff = diffs.get_gitdiff(node1, node2,
505 558 ignore_whitespace=ignore_whitespace,
506 559 context=line_context)
507 560 diff = diffs.DiffProcessor(_diff, format='gitdiff')
508 561
509 562 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
510 563 response.content_type = 'text/plain'
511 564 response.content_disposition = (
512 565 'attachment; filename=%s' % diff_name
513 566 )
514 567 return diff.as_raw()
515 568
516 569 elif c.action == 'raw':
517 570 _diff = diffs.get_gitdiff(node1, node2,
518 571 ignore_whitespace=ignore_whitespace,
519 572 context=line_context)
520 573 diff = diffs.DiffProcessor(_diff, format='gitdiff')
521 574 response.content_type = 'text/plain'
522 575 return diff.as_raw()
523 576
524 577 else:
525 578 fid = h.FID(diff2, node2.path)
526 579 line_context_lcl = get_line_ctx(fid, request.GET)
527 580 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
528 581
529 582 lim = request.GET.get('fulldiff') or self.cut_off_limit
530 583 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
531 584 filenode_new=node2,
532 585 cut_off_limit=lim,
533 586 ignore_whitespace=ign_whitespace_lcl,
534 587 line_context=line_context_lcl,
535 588 enable_comments=False)
536 589 op = ''
537 590 filename = node1.path
538 591 cs_changes = {
539 592 'fid': [cs1, cs2, op, filename, diff, st]
540 593 }
541 594 c.changes = cs_changes
542 595
543 596 return render('files/file_diff.html')
544 597
545 598 def _get_node_history(self, cs, f_path, changesets=None):
546 599 """
547 600 get changesets history for given node
548 601
549 602 :param cs: changeset to calculate history
550 603 :param f_path: path for node to calculate history for
551 604 :param changesets: if passed don't calculate history and take
552 605 changesets defined in this list
553 606 """
554 607 # calculate history based on tip
555 608 tip_cs = c.rhodecode_repo.get_changeset()
556 609 if changesets is None:
557 610 try:
558 611 changesets = tip_cs.get_file_history(f_path)
559 612 except (NodeDoesNotExistError, ChangesetError):
560 613 #this node is not present at tip !
561 614 changesets = cs.get_file_history(f_path)
562 615 hist_l = []
563 616
564 617 changesets_group = ([], _("Changesets"))
565 618 branches_group = ([], _("Branches"))
566 619 tags_group = ([], _("Tags"))
567 620 _hg = cs.repository.alias == 'hg'
568 621 for chs in changesets:
569 622 #_branch = '(%s)' % chs.branch if _hg else ''
570 623 _branch = chs.branch
571 624 n_desc = 'r%s:%s (%s)' % (chs.revision, chs.short_id, _branch)
572 625 changesets_group[0].append((chs.raw_id, n_desc,))
573 626 hist_l.append(changesets_group)
574 627
575 628 for name, chs in c.rhodecode_repo.branches.items():
576 629 branches_group[0].append((chs, name),)
577 630 hist_l.append(branches_group)
578 631
579 632 for name, chs in c.rhodecode_repo.tags.items():
580 633 tags_group[0].append((chs, name),)
581 634 hist_l.append(tags_group)
582 635
583 636 return hist_l, changesets
584 637
585 638 @LoginRequired()
586 639 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
587 640 'repository.admin')
588 641 @jsonify
589 642 def nodelist(self, repo_name, revision, f_path):
590 643 if request.environ.get('HTTP_X_PARTIAL_XHR'):
591 644 cs = self.__get_cs_or_redirect(revision, repo_name)
592 645 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
593 646 flat=False)
594 647 return {'nodes': _d + _f}
@@ -1,175 +1,192 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.forks
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 forks controller for rhodecode
7 7
8 8 :created_on: Apr 23, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26 import formencode
27 27 import traceback
28 28 from formencode import htmlfill
29 29
30 30 from pylons import tmpl_context as c, request, url
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33
34 34 import rhodecode.lib.helpers as h
35 35
36 36 from rhodecode.lib.helpers import Page
37 37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
38 38 NotAnonymous, HasRepoPermissionAny, HasPermissionAllDecorator,\
39 39 HasPermissionAnyDecorator
40 40 from rhodecode.lib.base import BaseRepoController, render
41 from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User
41 from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User,\
42 RhodeCodeUi
42 43 from rhodecode.model.repo import RepoModel
43 44 from rhodecode.model.forms import RepoForkForm
44 from rhodecode.model.scm import ScmModel
45 from rhodecode.model.scm import ScmModel, GroupList
45 46 from rhodecode.lib.utils2 import safe_int
46 47
47 48 log = logging.getLogger(__name__)
48 49
49 50
50 51 class ForksController(BaseRepoController):
51 52
52 53 @LoginRequired()
53 54 def __before__(self):
54 55 super(ForksController, self).__before__()
55 56
56 57 def __load_defaults(self):
57 c.repo_groups = RepoGroup.groups_choices(check_perms=True)
58 acl_groups = GroupList(RepoGroup.query().all(),
59 perm_set=['group.write', 'group.admin'])
60 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
58 61 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
59 62 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
60 63 c.landing_revs_choices = choices
61 64
62 65 def __load_data(self, repo_name=None):
63 66 """
64 67 Load defaults settings for edit, and update
65 68
66 69 :param repo_name:
67 70 """
68 71 self.__load_defaults()
69 72
70 73 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
71 74 repo = db_repo.scm_instance
72 75
73 76 if c.repo_info is None:
74 77 h.not_mapped_error(repo_name)
75 78 return redirect(url('repos'))
76 79
77 80 c.default_user_id = User.get_by_username('default').user_id
78 81 c.in_public_journal = UserFollowing.query()\
79 82 .filter(UserFollowing.user_id == c.default_user_id)\
80 83 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
81 84
82 85 if c.repo_info.stats:
83 86 last_rev = c.repo_info.stats.stat_on_revision+1
84 87 else:
85 88 last_rev = 0
86 89 c.stats_revision = last_rev
87 90
88 91 c.repo_last_rev = repo.count() if repo.revisions else 0
89 92
90 93 if last_rev == 0 or c.repo_last_rev == 0:
91 94 c.stats_percentage = 0
92 95 else:
93 96 c.stats_percentage = '%.2f' % ((float((last_rev)) /
94 97 c.repo_last_rev) * 100)
95 98
99 c.can_update = RhodeCodeUi.get_by_key(RhodeCodeUi.HOOK_UPDATE).ui_active
100
96 101 defaults = RepoModel()._get_defaults(repo_name)
102 # alter the description to indicate a fork
103 defaults['description'] = ('fork of repository: %s \n%s'
104 % (defaults['repo_name'],
105 defaults['description']))
97 106 # add suffix to fork
98 107 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
108
99 109 return defaults
100 110
101 111 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
102 112 'repository.admin')
103 113 def forks(self, repo_name):
104 114 p = safe_int(request.params.get('page', 1), 1)
105 115 repo_id = c.rhodecode_db_repo.repo_id
106 116 d = []
107 117 for r in Repository.get_repo_forks(repo_id):
108 118 if not HasRepoPermissionAny(
109 119 'repository.read', 'repository.write', 'repository.admin'
110 120 )(r.repo_name, 'get forks check'):
111 121 continue
112 122 d.append(r)
113 123 c.forks_pager = Page(d, page=p, items_per_page=20)
114 124
115 125 c.forks_data = render('/forks/forks_data.html')
116 126
117 127 if request.environ.get('HTTP_X_PARTIAL_XHR'):
118 128 return c.forks_data
119 129
120 130 return render('/forks/forks.html')
121 131
122 132 @NotAnonymous()
123 133 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
124 134 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
125 135 'repository.admin')
126 136 def fork(self, repo_name):
127 137 c.repo_info = Repository.get_by_repo_name(repo_name)
128 138 if not c.repo_info:
129 139 h.not_mapped_error(repo_name)
130 140 return redirect(url('home'))
131 141
132 142 defaults = self.__load_data(repo_name)
133 143
134 144 return htmlfill.render(
135 145 render('forks/fork.html'),
136 146 defaults=defaults,
137 147 encoding="UTF-8",
138 148 force_defaults=False
139 149 )
140 150
141 151 @NotAnonymous()
142 152 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
143 153 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
144 154 'repository.admin')
145 155 def fork_create(self, repo_name):
146 156 self.__load_defaults()
147 157 c.repo_info = Repository.get_by_repo_name(repo_name)
148 158 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
149 159 repo_groups=c.repo_groups_choices,
150 160 landing_revs=c.landing_revs_choices)()
151 161 form_result = {}
152 162 try:
153 163 form_result = _form.to_python(dict(request.POST))
154 164
165 # an approximation that is better than nothing
166 if not RhodeCodeUi.get_by_key(RhodeCodeUi.HOOK_UPDATE).ui_active:
167 form_result['update_after_clone'] = False
168
155 169 # create fork is done sometimes async on celery, db transaction
156 170 # management is handled there.
157 171 RepoModel().create_fork(form_result, self.rhodecode_user.user_id)
158 h.flash(_('forked %s repository as %s') \
159 % (repo_name, form_result['repo_name']),
172 fork_url = h.link_to(form_result['repo_name_full'],
173 h.url('summary_home', repo_name=form_result['repo_name_full']))
174
175 h.flash(h.literal(_('Forked repository %s as %s') \
176 % (repo_name, fork_url)),
160 177 category='success')
161 178 except formencode.Invalid, errors:
162 179 c.new_repo = errors.value['repo_name']
163 180
164 181 return htmlfill.render(
165 182 render('forks/fork.html'),
166 183 defaults=errors.value,
167 184 errors=errors.error_dict or {},
168 185 prefix_error=False,
169 186 encoding="UTF-8")
170 187 except Exception:
171 188 log.error(traceback.format_exc())
172 189 h.flash(_('An error occurred during repository forking %s') %
173 190 repo_name, category='error')
174 191
175 return redirect(url('home'))
192 return redirect(h.url('summary_home', repo_name=repo_name))
@@ -1,87 +1,87 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.home
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Home controller for Rhodecode
7 7
8 8 :created_on: Feb 18, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27
28 28 from pylons import tmpl_context as c, request
29 29 from pylons.i18n.translation import _
30 30 from webob.exc import HTTPBadRequest
31 31 from sqlalchemy.sql.expression import func
32 32
33 33 import rhodecode
34 34 from rhodecode.lib import helpers as h
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.auth import LoginRequired
37 37 from rhodecode.lib.base import BaseController, render
38 38 from rhodecode.model.db import Repository
39 39 from rhodecode.model.repo import RepoModel
40 40
41 41
42 42 log = logging.getLogger(__name__)
43 43
44 44
45 45 class HomeController(BaseController):
46 46
47 47 @LoginRequired()
48 48 def __before__(self):
49 49 super(HomeController, self).__before__()
50 50
51 51 def index(self):
52 52 c.groups = self.scm_model.get_repos_groups()
53 53 c.group = None
54 54
55 if c.visual.lightweight_dashboard is False:
55 if not c.visual.lightweight_dashboard:
56 56 c.repos_list = self.scm_model.get_repos()
57 57 ## lightweight version of dashboard
58 58 else:
59 59 c.repos_list = Repository.query()\
60 60 .filter(Repository.group_id == None)\
61 61 .order_by(func.lower(Repository.repo_name))\
62 62 .all()
63 63
64 64 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
65 65 admin=False)
66 66 #json used to render the grid
67 67 c.data = json.dumps(repos_data)
68 68
69 69 return render('/index.html')
70 70
71 71 def repo_switcher(self):
72 72 if request.is_xhr:
73 73 all_repos = Repository.query().order_by(Repository.repo_name).all()
74 74 c.repos_list = self.scm_model.get_repos(all_repos,
75 75 sort_key='name_sort',
76 76 simple=True)
77 77 return render('/repo_switcher_list.html')
78 78 else:
79 79 raise HTTPBadRequest()
80 80
81 81 def branch_tag_switcher(self, repo_name):
82 82 if request.is_xhr:
83 83 c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
84 if c.rhodecode_db_repo:
84 85 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
85 86 return render('/switch_to_list.html')
86 else:
87 87 raise HTTPBadRequest()
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, binary diff hidden
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, binary diff hidden
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, binary diff hidden
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, binary diff hidden
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, binary diff hidden
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, binary diff hidden
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, binary diff hidden
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: file renamed from rhodecode/lib/cleanup.py to rhodecode/lib/paster_commands/cleanup.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rhodecode/config/rcextensions/make_rcextensions.py to rhodecode/lib/paster_commands/make_rcextensions.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rhodecode/config/setup_rhodecode.py to rhodecode/lib/paster_commands/setup_rhodecode.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rhodecode/lib/update_repoinfo.py to rhodecode/lib/paster_commands/update_repoinfo.py
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 chmod 100644 => 100755
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
1 NO CONTENT: modified file, binary diff hidden
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 chmod 100644 => 100755
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
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 chmod 100644 => 100755
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: file was removed
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
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