##// END OF EJS Templates
Merge beta branch into stable
marcink -
r2776:63e58ef8 merge default
parent child Browse files
Show More

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

@@ -0,0 +1,38 b''
1 language: python
2 python:
3 - "2.5"
4 - "2.6"
5 - "2.7"
6
7 env:
8 - TEST_DB=sqlite:////tmp/rhodecode_test.sqlite
9 - TEST_DB=mysql://root@127.0.0.1/rhodecode_test
10 - TEST_DB=postgresql://postgres@127.0.0.1/rhodecode_test
11
12 # command to install dependencies
13 before_script:
14 - mysql -e 'create database rhodecode_test;'
15 - psql -c 'create database rhodecode_test;' -U postgres
16 - git --version
17
18 before_install:
19 - sudo apt-get remove git
20 - sudo add-apt-repository ppa:pdoes/ppa -y
21 - sudo apt-get update -y
22 - sudo apt-get install git -y
23
24 install:
25 - pip install mysql-python psycopg2 mock unittest2
26 - pip install . --use-mirrors
27
28 # command to run tests
29 script: nosetests
30
31 notifications:
32 email:
33 - marcinkuz@gmail.com
34 irc: "irc.freenode.org#rhodecode"
35
36 branches:
37 only:
38 - dev
@@ -0,0 +1,244 b''
1 .. _installation_win:
2
3
4 Step by step Installation for Windows
5 =====================================
6
7
8 RhodeCode step-by-step install Guide for Windows
9
10 Target OS: Windows XP SP3 English (Clean installation)
11 + All Windows Updates until 24-may-2012
12
13 Step1 - Install Visual Studio 2008 Express
14 ------------------------------------------
15
16
17 Optional: You can also install MingW, but VS2008 installation is easier
18
19 Download "Visual C++ 2008 Express Edition with SP1" from:
20 http://www.microsoft.com/visualstudio/en-us/products/2008-editions/express
21 (if not found or relocated, google for "visual studio 2008 express" for
22 updated link)
23
24 You can also download full ISO file for offline installation, just
25 choose "All - Offline Install ISO image file" in the previous page and
26 choose "Visual C++ 2008 Express" when installing.
27
28
29 .. note::
30
31 Silverlight Runtime and SQL Server 2008 Express Edition are not
32 required, you can uncheck them
33
34
35 Step2 - Install Python
36 ----------------------
37
38 Install Python 2.x.y (x >= 5) x86 version (32bit). DO NOT USE A 3.x version.
39 Download Python 2.x.y from:
40 http://www.python.org/download/
41
42 Choose "Windows Installer" (32bit version) not "Windows X86-64
43 Installer". While writing this guide, the latest version was v2.7.3.
44 Remember the specific major and minor version installed, because it will
45 be needed in the next step. In this case, it is "2.7".
46
47
48 Step3 - Install Win32py extensions
49 ----------------------------------
50
51 Download pywin32 from:
52 http://sourceforge.net/projects/pywin32/files/
53
54 - Click on "pywin32" folder
55 - Click on the first folder (in this case, Build 217, maybe newer when you try)
56 - Choose the file ending with ".win32-py2.x.exe" -> x being the minor
57 version of Python you installed (in this case, 7)
58 When writing this guide, the file was:
59 http://sourceforge.net/projects/pywin32/files/pywin32/Build%20217/pywin32-217.win32-py2.7.exe/download
60
61
62 Step4 - Python BIN
63 ------------------
64
65 Add Python BIN folder to the path
66
67 You have to add the Python folder to the path, you can do it manually
68 (editing "PATH" environment variable) or using Windows Support Tools
69 that came preinstalled in Vista/7 and can be installed in Windows XP.
70
71 - Using support tools on WINDOWS XP:
72 If you use Windows XP you can install them using Windows XP CD and
73 navigating to \SUPPORT\TOOLS. There, execute Setup.EXE (not MSI).
74 Afterwards, open a CMD and type::
75
76 SETX PATH "%PATH%;[your-python-path]" -M
77
78 Close CMD (the path variable will be updated then)
79
80 - Using support tools on WINDOWS Vista/7:
81
82 Open a CMD and type::
83
84 SETX PATH "%PATH%;[your-python-path]" /M
85
86 Please substitute [your-python-path] with your Python installation path.
87 Typically: C:\\Python27
88
89
90 Step5 - RhodeCode folder structure
91 ----------------------------------
92
93 Create a RhodeCode folder structure
94
95 This is only a example to install RhodeCode, you can of course change
96 it. However, this guide will follow the proposed structure, so please
97 later adapt the paths if you change them. My recommendation is to use
98 folders with NO SPACES. But you can try if you are brave...
99
100 Create the following folder structure::
101
102 C:\RhodeCode
103 C:\RhodeCode\Bin
104 C:\RhodeCode\Env
105 C:\RhodeCode\Repos
106
107
108 Step6 - Install virtualenv
109 ---------------------------
110
111 Install Virtual Env for Python
112
113 Navigate to: http://www.virtualenv.org/en/latest/index.html#installation
114 Right click on "virtualenv.py" file and choose "Save link as...".
115 Download to C:\\RhodeCode (or whatever you want)
116 (the file is located at
117 https://raw.github.com/pypa/virtualenv/master/virtualenv.py)
118
119 Create a virtual Python environment in C:\\RhodeCode\\Env (or similar). To
120 do so, open a CMD (Python Path should be included in Step3), navigate
121 where you downloaded "virtualenv.py", and write::
122
123 python virtualenv.py C:\RhodeCode\Env
124
125 (--no-site-packages is now the default behaviour of virtualenv, no need
126 to include it)
127
128
129 Step7 - Install RhodeCode
130 -------------------------
131
132 Finally, install RhodeCode
133
134 Close previously opened command prompt/s, and open a Visual Studio 2008
135 Command Prompt (**IMPORTANT!!**). To do so, go to Start Menu, and then open
136 "Microsoft Visual C++ 2008 Express Edition" -> "Visual Studio Tools" ->
137 "Visual Studio 2008 Command Prompt"
138
139 In that CMD (loaded with VS2008 PATHs) type::
140
141 cd C:\RhodeCode\Env\Scripts (or similar)
142 activate
143
144 The prompt will change into "(Env) C:\\RhodeCode\\Env\\Scripts" or similar
145 (depending of your folder structure). Then type::
146
147 pip install rhodecode
148
149 (long step, please wait until fully complete)
150
151 Some warnings will appear, don't worry as they are normal.
152
153
154 Step8 - Configuring RhodeCode
155 -----------------------------
156
157
158 steps taken from http://packages.python.org/RhodeCode/setup.html
159
160 You have to use the same Visual Studio 2008 command prompt as Step7, so
161 if you closed it reopen it following the same commands (including the
162 "activate" one). When ready, just type::
163
164 cd C:\RhodeCode\Bin
165 paster make-config RhodeCode production.ini
166
167 Then, you must edit production.ini to fit your needs (ip address, ip
168 port, mail settings, database, whatever). I recommend using NotePad++
169 (free) or similar text editor, as it handles well the EndOfLine
170 character differences between Unix and Windows
171 (http://notepad-plus-plus.org/)
172
173 For the sake of simplicity lets run it with the default settings. After
174 your edits (if any), in the previous Command Prompt, type::
175
176 paster setup-rhodecode production.ini
177
178 (this time a NEW database will be installed, you must follow a different
179 step to later UPGRADE to a newer RhodeCode version)
180
181 The script will ask you for confirmation about creating a NEW database,
182 answer yes (y)
183 The script will ask you for repository path, answer C:\\RhodeCode\\Repos
184 (or similar)
185 The script will ask you for admin username and password, answer "admin"
186 + "123456" (or whatever you want)
187 The script will ask you for admin mail, answer "admin@xxxx.com" (or
188 whatever you want)
189
190 If you make some mistake and the script does not end, don't worry, start
191 it again.
192
193
194 Step9 - Running RhodeCode
195 -------------------------
196
197
198 In the previous command prompt, being in the C:\\RhodeCode\\Bin folder,
199 just type::
200
201 paster serve production.ini
202
203 Open yout web server, and go to http://127.0.0.1:5000
204
205 It works!! :-)
206
207 Remark:
208 If it does not work first time, just Ctrl-C the CMD process and start it
209 again. Don't forget the "http://" in Internet Explorer
210
211
212
213 What this Guide does not cover:
214
215 - Installing Celery
216 - Running RhodeCode as Windows Service. You can investigate here:
217
218 - http://pypi.python.org/pypi/wsgisvc
219 - http://ryrobes.com/python/running-python-scripts-as-a-windows-service/
220 - http://wiki.pylonshq.com/display/pylonscookbook/How+to+run+Pylons+as+a+Windows+service
221
222 - Using Apache. You can investigate here:
223
224 - https://groups.google.com/group/rhodecode/msg/c433074e813ffdc4
225
226
227 Upgrading
228 =========
229
230 Stop running RhodeCode
231 Open a CommandPrompt like in Step7 (VS2008 path + activate) and type::
232
233 easy_install -U rhodecode
234 cd \RhodeCode\Bin
235
236 { backup your production.ini file now} ::
237
238 paster make-config RhodeCode production.ini
239
240 (check changes and update your production.ini accordingly) ::
241
242 paster upgrade-db production.ini (update database)
243
244 Full steps in http://packages.python.org/RhodeCode/upgrade.html No newline at end of file
@@ -0,0 +1,41 b''
1 .. _locking:
2
3 ===================================
4 RhodeCode repository locking system
5 ===================================
6
7
8 | Repos with **locking function=disabled** is the default, that's how repos work
9 today.
10 | Repos with **locking function=enabled** behaves like follows:
11
12 Repos have a state called `locked` that can be true or false.
13 The hg/git commands `hg/git clone`, `hg/git pull`, and `hg/git push`
14 influence this state:
15
16 - The command `hg/git pull <repo>` will lock that repo (locked=true)
17 if the user has write/admin permissions on this repo
18
19 - The command `hg/git clone <repo>` will lock that repo (locked=true) if the
20 user has write/admin permissions on this repo
21
22
23 RhodeCode will remember the user id who locked the repo
24 only this specific user can unlock the repo (locked=false) by calling
25
26 - `hg/git push <repo>`
27
28 every other command on that repo from this user and
29 every command from any other user will result in http return code 423 (locked)
30
31
32 additionally the http error includes the <user> that locked the repo
33 (e.g. “repository <repo> locked by user <user>”)
34
35
36 So the scenario of use for repos with `locking function` enabled is that
37 every initial clone and every pull gives users (with write permission)
38 the exclusive right to do a push.
39
40
41 Each repo can be manually unlocked by admin from the repo settings menu. No newline at end of file
@@ -0,0 +1,50 b''
1 .. _performance:
2
3 ================================
4 Optimizing RhodeCode Performance
5 ================================
6
7 When serving large amount of big repositories RhodeCode can start
8 performing slower than expected. Because of demanding nature of handling large
9 amount of data from version control systems here are some tips how to get
10 the best performance.
11
12 * RhodeCode will perform better on machines with faster disks (SSD/SAN). It's
13 more important to have faster disk than faster CPU.
14
15 * Slowness on initial page can be easily fixed by grouping repositories, and/or
16 increasing cache size (see below)
17
18
19 Follow these few steps to improve performance of RhodeCode system.
20
21
22 1. Increase cache
23
24 in the .ini file::
25
26 beaker.cache.sql_cache_long.expire=3600 <-- set this to higher number
27
28 This option affects the cache expiration time for main page. Having
29 few hundreds of repositories on main page can sometimes make the system
30 to behave slow when cache expires for all of them. Increasing `expire`
31 option to day (86400) or a week (604800) will improve general response
32 times for the main page. RhodeCode has an intelligent cache expiration
33 system and it will expire cache for repositories that had been changed.
34
35 2. Switch from sqlite to postgres or mysql
36
37 sqlite is a good option when having small load on the system. But due to
38 locking issues with sqlite, it's not recommended to use it for larger
39 setup. Switching to mysql or postgres will result in a immediate
40 performance increase.
41
42 3. Scale RhodeCode horizontally
43
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
@@ -0,0 +1,70 b''
1 .. _troubleshooting:
2
3
4 ===============
5 Troubleshooting
6 ===============
7
8 :Q: **Missing static files?**
9 :A: Make sure either to set the `static_files = true` in the .ini file or
10 double check the root path for your http setup. It should point to
11 for example:
12 /home/my-virtual-python/lib/python2.6/site-packages/rhodecode/public
13
14 |
15
16 :Q: **Can't install celery/rabbitmq?**
17 :A: Don't worry RhodeCode works without them too. No extra setup is required.
18 Try out great celery docs for further help.
19
20 |
21
22 :Q: **Long lasting push timeouts?**
23 :A: Make sure you set a longer timeouts in your proxy/fcgi settings, timeouts
24 are caused by https server and not RhodeCode.
25
26 |
27
28 :Q: **Large pushes timeouts?**
29 :A: Make sure you set a proper max_body_size for the http server. Very often
30 Apache, Nginx or other http servers kill the connection due to to large
31 body.
32
33 |
34
35 :Q: **Apache doesn't pass basicAuth on pull/push?**
36 :A: Make sure you added `WSGIPassAuthorization true`.
37
38 |
39
40 :Q: **Git fails on push/pull?**
41 :A: Make sure you're using an wsgi http server that can handle chunked encoding
42 such as `waitress` or `gunicorn`
43
44 |
45
46 :Q: **How i use hooks in RhodeCode?**
47 :A: It's easy if they are python hooks just use advanced link in hooks section
48 in Admin panel, that works only for Mercurial. If you want to use githooks,
49 just install proper one in repository eg. create file in
50 `/gitrepo/hooks/pre-receive`. You can also use RhodeCode-extensions to
51 connect to callback hooks, for both Git and Mercurial.
52
53 |
54
55 :Q: **RhodeCode is slow for me, how can i make it faster?**
56 :A: See the :ref:`performance` section
57
58 For further questions search the `Issues tracker`_, or post a message in the
59 `google group rhodecode`_
60
61 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
62 .. _python: http://www.python.org/
63 .. _mercurial: http://mercurial.selenic.com/
64 .. _celery: http://celeryproject.org/
65 .. _rabbitmq: http://www.rabbitmq.com/
66 .. _python-ldap: http://www.python-ldap.org/
67 .. _mercurial-server: http://www.lshift.net/mercurial-server.html
68 .. _PublishingRepositories: http://mercurial.selenic.com/wiki/PublishingRepositories
69 .. _Issues tracker: https://bitbucket.org/marcinkuzminski/rhodecode/issues
70 .. _google group rhodecode: http://groups.google.com/group/rhodecode No newline at end of file
@@ -0,0 +1,51 b''
1 ; RhodeCode Supervisord
2 ; ##########################
3 ; for help see http://supervisord.org/configuration.html
4 ; ##########################
5
6 [inet_http_server] ; inet (TCP) server disabled by default
7 port=127.0.0.1:9001 ; (ip_address:port specifier, *:port for all iface)
8 ;username=user ; (default is no username (open server))
9 ;password=123 ; (default is no password (open server))
10
11 [supervisord]
12 logfile=/%(here)s/supervisord_rhodecode.log ; (main log file;default $CWD/supervisord.log)
13 logfile_maxbytes=50MB ; (max main logfile bytes b4 rotation;default 50MB)
14 logfile_backups=10 ; (num of main logfile rotation backups;default 10)
15 loglevel=info ; (log level;default info; others: debug,warn,trace)
16 pidfile=/%(here)s/supervisord_rhodecode.pid ; (supervisord pidfile;default supervisord.pid)
17 nodaemon=true ; (start in foreground if true;default false)
18 minfds=1024 ; (min. avail startup file descriptors;default 1024)
19 minprocs=200 ; (min. avail process descriptors;default 200)
20 umask=022 ; (process file creation umask;default 022)
21 user=marcink ; (default is current user, required if root)
22 ;identifier=supervisor ; (supervisord identifier, default is 'supervisor')
23 ;directory=/tmp ; (default is not to cd during start)
24 ;nocleanup=true ; (don't clean up tempfiles at start;default false)
25 ;childlogdir=/tmp ; ('AUTO' child log dir, default $TEMP)
26 environment=HOME=/home/marcink ; (key value pairs to add to environment)
27 ;strip_ansi=false ; (strip ansi escape codes in logs; def. false)
28
29 ; the below section must remain in the config file for RPC
30 ; (supervisorctl/web interface) to work, additional interfaces may be
31 ; added by defining them in separate rpcinterface: sections
32 [rpcinterface:supervisor]
33 supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
34
35 [supervisorctl]
36 serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket
37 ;username=user ; should be same as http_username if set
38 ;password=123 ; should be same as http_password if set
39 ;prompt=mysupervisor ; cmd line prompt (default "supervisor")
40 ;history_file=~/.sc_history ; use readline history if available
41
42
43 ; restart with supervisorctl restart rhodecode:*
44 [program:rhodecode]
45 numprocs = 1
46 numprocs_start = 5000 # possible should match ports
47 directory=/home/marcink/rhodecode-dir
48 command = /home/marcink/v-env/bin/paster serve rc.ini
49 process_name = %(program_name)s_%(process_num)04d
50 redirect_stderr=true
51 stdout_logfile=/%(here)s/rhodecode.log No newline at end of file
1 NO CONTENT: new file 100644
@@ -0,0 +1,249 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.bin.backup_manager
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 Api CLI client for RhodeCode
7
8 :created_on: Jun 3, 2012
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
26 from __future__ import with_statement
27 import os
28 import sys
29 import random
30 import urllib2
31 import pprint
32 import argparse
33
34 try:
35 from rhodecode.lib.ext_json import json
36 except ImportError:
37 try:
38 import simplejson as json
39 except ImportError:
40 import json
41
42
43 CONFIG_NAME = '.rhodecode'
44 FORMAT_PRETTY = 'pretty'
45 FORMAT_JSON = 'json'
46
47
48 class RcConf(object):
49 """
50 RhodeCode config for API
51
52 conf = RcConf()
53 conf['key']
54
55 """
56
57 def __init__(self, config_location=None, autoload=True, autocreate=False,
58 config=None):
59 self._conf_name = CONFIG_NAME if not config_location else config_location
60 self._conf = {}
61 if autocreate:
62 self.make_config(config)
63 if autoload:
64 self._conf = self.load_config()
65
66 def __getitem__(self, key):
67 return self._conf[key]
68
69 def __nonzero__(self):
70 if self._conf:
71 return True
72 return False
73
74 def __eq__(self):
75 return self._conf.__eq__()
76
77 def __repr__(self):
78 return 'RcConf<%s>' % self._conf.__repr__()
79
80 def make_config(self, config):
81 """
82 Saves given config as a JSON dump in the _conf_name location
83
84 :param config:
85 :type config:
86 """
87 update = False
88 if os.path.exists(self._conf_name):
89 update = True
90 with open(self._conf_name, 'wb') as f:
91 json.dump(config, f, indent=4)
92
93 if update:
94 sys.stdout.write('Updated config in %s\n' % self._conf_name)
95 else:
96 sys.stdout.write('Created new config in %s\n' % self._conf_name)
97
98 def update_config(self, new_config):
99 """
100 Reads the JSON config updates it's values with new_config and
101 saves it back as JSON dump
102
103 :param new_config:
104 """
105 config = {}
106 try:
107 with open(self._conf_name, 'rb') as conf:
108 config = json.load(conf)
109 except IOError, e:
110 sys.stderr.write(str(e) + '\n')
111
112 config.update(new_config)
113 self.make_config(config)
114
115 def load_config(self):
116 """
117 Loads config from file and returns loaded JSON object
118 """
119 try:
120 with open(self._conf_name, 'rb') as conf:
121 return json.load(conf)
122 except IOError, e:
123 #sys.stderr.write(str(e) + '\n')
124 pass
125
126
127 def api_call(apikey, apihost, format, method=None, **kw):
128 """
129 Api_call wrapper for RhodeCode
130
131 :param apikey:
132 :param apihost:
133 :param format: formatting, pretty means prints and pprint of json
134 json returns unparsed json
135 :param method:
136 """
137 def _build_data(random_id):
138 """
139 Builds API data with given random ID
140
141 :param random_id:
142 :type random_id:
143 """
144 return {
145 "id": random_id,
146 "api_key": apikey,
147 "method": method,
148 "args": kw
149 }
150
151 if not method:
152 raise Exception('please specify method name !')
153 id_ = random.randrange(1, 9999)
154 req = urllib2.Request('%s/_admin/api' % apihost,
155 data=json.dumps(_build_data(id_)),
156 headers={'content-type': 'text/plain'})
157 if format == FORMAT_PRETTY:
158 sys.stdout.write('calling %s to %s \n' % (req.get_data(), apihost))
159 ret = urllib2.urlopen(req)
160 raw_json = ret.read()
161 json_data = json.loads(raw_json)
162 id_ret = json_data['id']
163 _formatted_json = pprint.pformat(json_data)
164 if id_ret == id_:
165 if format == FORMAT_JSON:
166 sys.stdout.write(str(raw_json))
167 else:
168 sys.stdout.write('rhodecode returned:\n%s\n' % (_formatted_json))
169
170 else:
171 raise Exception('something went wrong. '
172 'ID mismatch got %s, expected %s | %s' % (
173 id_ret, id_, _formatted_json))
174
175
176 def argparser(argv):
177 usage = (
178 "rhodecode_api [-h] [--format=FORMAT] [--apikey=APIKEY] [--apihost=APIHOST] "
179 " [--config=CONFIG] "
180 "_create_config or METHOD <key:val> <key2:val> ..."
181 )
182
183 parser = argparse.ArgumentParser(description='RhodeCode API cli',
184 usage=usage)
185
186 ## config
187 group = parser.add_argument_group('config')
188 group.add_argument('--apikey', help='api access key')
189 group.add_argument('--apihost', help='api host')
190 group.add_argument('--config', help='config file')
191
192 group = parser.add_argument_group('API')
193 group.add_argument('method', metavar='METHOD', type=str,
194 help='API method name to call followed by key:value attributes',
195 )
196 group.add_argument('--format', dest='format', type=str,
197 help='output format default: `pretty` can '
198 'be also `%s`' % FORMAT_JSON,
199 default=FORMAT_PRETTY
200 )
201 args, other = parser.parse_known_args()
202 return parser, args, other
203
204
205 def main(argv=None):
206 """
207 Main execution function for cli
208
209 :param argv:
210 :type argv:
211 """
212 if argv is None:
213 argv = sys.argv
214
215 conf = None
216 parser, args, other = argparser(argv)
217
218 api_credentials_given = (args.apikey and args.apihost)
219 if args.method == '_create_config':
220 if not api_credentials_given:
221 raise parser.error('_create_config requires --apikey and --apihost')
222 conf = RcConf(config_location=args.config,
223 autocreate=True, config={'apikey': args.apikey,
224 'apihost': args.apihost})
225
226 if not conf:
227 conf = RcConf(config_location=args.config, autoload=True)
228 if not conf:
229 if not api_credentials_given:
230 parser.error('Could not find config file and missing '
231 '--apikey or --apihost in params')
232
233 apikey = args.apikey or conf['apikey']
234 host = args.apihost or conf['apihost']
235 method = args.method
236 if method == '_create_config':
237 sys.exit()
238
239 try:
240 margs = dict(map(lambda s: s.split(':', 1), other))
241 except:
242 sys.stderr.write('Error parsing arguments \n')
243 sys.exit()
244
245 api_call(apikey, host, args.format, method, **margs)
246 return 0
247
248 if __name__ == '__main__':
249 sys.exit(main(sys.argv))
@@ -0,0 +1,31 b''
1 #!/usr/bin/env python
2 import os
3 import sys
4
5 try:
6 import rhodecode
7 RC_HOOK_VER = '_TMPL_'
8 os.environ['RC_HOOK_VER'] = RC_HOOK_VER
9 from rhodecode.lib.hooks import handle_git_post_receive
10 except ImportError:
11 rhodecode = None
12
13
14 def main():
15 if rhodecode is None:
16 # exit with success if we cannot import rhodecode !!
17 # this allows simply push to this repo even without
18 # rhodecode
19 sys.exit(0)
20
21 repo_path = os.path.abspath('.')
22 push_data = sys.stdin.readlines()
23 # os.environ is modified here by a subprocess call that
24 # runs git and later git executes this hook.
25 # Environ get's some additional info from rhodecode system
26 # like IP or username from basic-auth
27 handle_git_post_receive(repo_path, push_data, os.environ)
28 sys.exit(0)
29
30 if __name__ == '__main__':
31 main()
@@ -0,0 +1,31 b''
1 #!/usr/bin/env python
2 import os
3 import sys
4
5 try:
6 import rhodecode
7 RC_HOOK_VER = '_TMPL_'
8 os.environ['RC_HOOK_VER'] = RC_HOOK_VER
9 from rhodecode.lib.hooks import handle_git_pre_receive
10 except ImportError:
11 rhodecode = None
12
13
14 def main():
15 if rhodecode is None:
16 # exit with success if we cannot import rhodecode !!
17 # this allows simply push to this repo even without
18 # rhodecode
19 sys.exit(0)
20
21 repo_path = os.path.abspath('.')
22 push_data = sys.stdin.readlines()
23 # os.environ is modified here by a subprocess call that
24 # runs git and later git executes this hook.
25 # Environ get's some additional info from rhodecode system
26 # like IP or username from basic-auth
27 handle_git_pre_receive(repo_path, push_data, os.environ)
28 sys.exit(0)
29
30 if __name__ == '__main__':
31 main()
@@ -0,0 +1,133 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.controllers.compare
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 compare controller for pylons showoing differences between two
7 repos, branches, bookmarks or tips
8
9 :created_on: May 6, 2012
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
13 """
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
18 #
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
23 #
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 import logging
27 import traceback
28
29 from webob.exc import HTTPNotFound
30 from pylons import request, response, session, tmpl_context as c, url
31 from pylons.controllers.util import abort, redirect
32 from pylons.i18n.translation import _
33
34 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError, RepositoryError
35 from rhodecode.lib import helpers as h
36 from rhodecode.lib.base import BaseRepoController, render
37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 from rhodecode.lib import diffs
39
40 from rhodecode.model.db import Repository
41 from rhodecode.model.pull_request import PullRequestModel
42
43 log = logging.getLogger(__name__)
44
45
46 class CompareController(BaseRepoController):
47
48 @LoginRequired()
49 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
50 'repository.admin')
51 def __before__(self):
52 super(CompareController, self).__before__()
53
54 def __get_cs_or_redirect(self, rev, repo, redirect_after=True):
55 """
56 Safe way to get changeset if error occur it redirects to changeset with
57 proper message
58
59 :param rev: revision to fetch
60 :param repo: repo instance
61 """
62
63 try:
64 type_, rev = rev
65 return repo.scm_instance.get_changeset(rev)
66 except EmptyRepositoryError, e:
67 if not redirect_after:
68 return None
69 h.flash(h.literal(_('There are no changesets yet')),
70 category='warning')
71 redirect(url('summary_home', repo_name=repo.repo_name))
72
73 except RepositoryError, e:
74 log.error(traceback.format_exc())
75 h.flash(str(e), category='warning')
76 redirect(h.url('summary_home', repo_name=repo.repo_name))
77
78 def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
79
80 org_repo = c.rhodecode_db_repo.repo_name
81 org_ref = (org_ref_type, org_ref)
82 other_ref = (other_ref_type, other_ref)
83 other_repo = request.GET.get('repo', org_repo)
84
85 c.swap_url = h.url('compare_url', repo_name=other_repo,
86 org_ref_type=other_ref[0], org_ref=other_ref[1],
87 other_ref_type=org_ref[0], other_ref=org_ref[1],
88 repo=org_repo)
89
90 c.org_repo = org_repo = Repository.get_by_repo_name(org_repo)
91 c.other_repo = other_repo = Repository.get_by_repo_name(other_repo)
92
93 if c.org_repo is None or c.other_repo is None:
94 log.error('Could not found repo %s or %s' % (org_repo, other_repo))
95 raise HTTPNotFound
96
97 if c.org_repo.scm_instance.alias != 'hg':
98 log.error('Review not available for GIT REPOS')
99 raise HTTPNotFound
100
101 self.__get_cs_or_redirect(rev=org_ref, repo=org_repo)
102 self.__get_cs_or_redirect(rev=other_ref, repo=other_repo)
103
104 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
105 org_repo, org_ref, other_repo, other_ref
106 )
107
108 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
109 c.cs_ranges])
110 c.target_repo = c.repo_name
111 # defines that we need hidden inputs with changesets
112 c.as_form = request.GET.get('as_form', False)
113 if request.environ.get('HTTP_X_PARTIAL_XHR'):
114 return render('compare/compare_cs.html')
115
116 c.org_ref = org_ref[1]
117 c.other_ref = other_ref[1]
118 # diff needs to have swapped org with other to generate proper diff
119 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
120 discovery_data)
121 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
122 _parsed = diff_processor.prepare()
123
124 c.files = []
125 c.changes = {}
126
127 for f in _parsed:
128 fid = h.FID('', f['filename'])
129 c.files.append([fid, f['operation'], f['filename'], f['stats']])
130 diff = diff_processor.as_html(enable_comments=False, diff_lines=[f])
131 c.changes[fid] = [f['operation'], f['filename'], diff]
132
133 return render('compare/compare_diff.html')
@@ -0,0 +1,404 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.controllers.pullrequests
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 pull requests controller for rhodecode for initializing pull requests
7
8 :created_on: May 7, 2012
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 import logging
26 import traceback
27 import formencode
28
29 from webob.exc import HTTPNotFound, HTTPForbidden
30 from collections import defaultdict
31 from itertools import groupby
32
33 from pylons import request, response, session, tmpl_context as c, url
34 from pylons.controllers.util import abort, redirect
35 from pylons.i18n.translation import _
36 from pylons.decorators import jsonify
37
38 from rhodecode.lib.compat import json
39 from rhodecode.lib.base import BaseRepoController, render
40 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
41 NotAnonymous
42 from rhodecode.lib import helpers as h
43 from rhodecode.lib import diffs
44 from rhodecode.lib.utils import action_logger
45 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
46 ChangesetComment
47 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.meta import Session
49 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.comment import ChangesetCommentsModel
51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 from rhodecode.model.forms import PullRequestForm
53
54 log = logging.getLogger(__name__)
55
56
57 class PullrequestsController(BaseRepoController):
58
59 @LoginRequired()
60 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
61 'repository.admin')
62 def __before__(self):
63 super(PullrequestsController, self).__before__()
64 repo_model = RepoModel()
65 c.users_array = repo_model.get_users_js()
66 c.users_groups_array = repo_model.get_users_groups_js()
67
68 def _get_repo_refs(self, repo):
69 hist_l = []
70
71 branches_group = ([('branch:%s:%s' % (k, v), k) for
72 k, v in repo.branches.iteritems()], _("Branches"))
73 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
74 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
75 tags_group = ([('tag:%s:%s' % (k, v), k) for
76 k, v in repo.tags.iteritems()], _("Tags"))
77
78 hist_l.append(bookmarks_group)
79 hist_l.append(branches_group)
80 hist_l.append(tags_group)
81
82 return hist_l
83
84 def show_all(self, repo_name):
85 c.pull_requests = PullRequestModel().get_all(repo_name)
86 c.repo_name = repo_name
87 return render('/pullrequests/pullrequest_show_all.html')
88
89 @NotAnonymous()
90 def index(self):
91 org_repo = c.rhodecode_db_repo
92
93 if org_repo.scm_instance.alias != 'hg':
94 log.error('Review not available for GIT REPOS')
95 raise HTTPNotFound
96
97 other_repos_info = {}
98
99 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
100 c.org_repos = []
101 c.other_repos = []
102 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
103 org_repo.user.username, c.repo_name))
104 )
105
106 # add org repo to other so we can open pull request agains itself
107 c.other_repos.extend(c.org_repos)
108
109 c.default_pull_request = org_repo.repo_name
110 c.default_revs = self._get_repo_refs(org_repo.scm_instance)
111 #add orginal repo
112 other_repos_info[org_repo.repo_name] = {
113 'gravatar': h.gravatar_url(org_repo.user.email, 24),
114 'description': org_repo.description,
115 'revs': h.select('other_ref', '', c.default_revs, class_='refs')
116 }
117
118 #gather forks and add to this list
119 for fork in org_repo.forks:
120 c.other_repos.append((fork.repo_name, '%s/%s' % (
121 fork.user.username, fork.repo_name))
122 )
123 other_repos_info[fork.repo_name] = {
124 'gravatar': h.gravatar_url(fork.user.email, 24),
125 'description': fork.description,
126 'revs': h.select('other_ref', '',
127 self._get_repo_refs(fork.scm_instance),
128 class_='refs')
129 }
130 #add parents of this fork also
131 if org_repo.parent:
132 c.default_pull_request = org_repo.parent.repo_name
133 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
134 org_repo.parent.user.username,
135 org_repo.parent.repo_name))
136 )
137 other_repos_info[org_repo.parent.repo_name] = {
138 'gravatar': h.gravatar_url(org_repo.parent.user.email, 24),
139 'description': org_repo.parent.description,
140 'revs': h.select('other_ref', '',
141 self._get_repo_refs(org_repo.parent.scm_instance),
142 class_='refs')
143 }
144
145 c.other_repos_info = json.dumps(other_repos_info)
146 c.review_members = [org_repo.user]
147 return render('/pullrequests/pullrequest.html')
148
149 @NotAnonymous()
150 def create(self, repo_name):
151 try:
152 _form = PullRequestForm()().to_python(request.POST)
153 except formencode.Invalid, errors:
154 log.error(traceback.format_exc())
155 if errors.error_dict.get('revisions'):
156 msg = 'Revisions: %s' % errors.error_dict['revisions']
157 elif errors.error_dict.get('pullrequest_title'):
158 msg = _('Pull request requires a title with min. 3 chars')
159 else:
160 msg = _('error during creation of pull request')
161
162 h.flash(msg, 'error')
163 return redirect(url('pullrequest_home', repo_name=repo_name))
164
165 org_repo = _form['org_repo']
166 org_ref = _form['org_ref']
167 other_repo = _form['other_repo']
168 other_ref = _form['other_ref']
169 revisions = _form['revisions']
170 reviewers = _form['review_members']
171
172 title = _form['pullrequest_title']
173 description = _form['pullrequest_desc']
174
175 try:
176 pull_request = PullRequestModel().create(
177 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
178 other_ref, revisions, reviewers, title, description
179 )
180 Session().commit()
181 h.flash(_('Successfully opened new pull request'),
182 category='success')
183 except Exception:
184 h.flash(_('Error occurred during sending pull request'),
185 category='error')
186 log.error(traceback.format_exc())
187 return redirect(url('pullrequest_home', repo_name=repo_name))
188
189 return redirect(url('pullrequest_show', repo_name=other_repo,
190 pull_request_id=pull_request.pull_request_id))
191
192 @NotAnonymous()
193 @jsonify
194 def update(self, repo_name, pull_request_id):
195 pull_request = PullRequest.get_or_404(pull_request_id)
196 if pull_request.is_closed():
197 raise HTTPForbidden()
198 #only owner or admin can update it
199 owner = pull_request.author.user_id == c.rhodecode_user.user_id
200 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
201 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
202 request.POST.get('reviewers_ids', '').split(',')))
203
204 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
205 Session.commit()
206 return True
207 raise HTTPForbidden()
208
209 @NotAnonymous()
210 @jsonify
211 def delete(self, repo_name, pull_request_id):
212 pull_request = PullRequest.get_or_404(pull_request_id)
213 #only owner can delete it !
214 if pull_request.author.user_id == c.rhodecode_user.user_id:
215 PullRequestModel().delete(pull_request)
216 Session().commit()
217 h.flash(_('Successfully deleted pull request'),
218 category='success')
219 return redirect(url('admin_settings_my_account'))
220 raise HTTPForbidden()
221
222 def _load_compare_data(self, pull_request, enable_comments=True):
223 """
224 Load context data needed for generating compare diff
225
226 :param pull_request:
227 :type pull_request:
228 """
229
230 org_repo = pull_request.org_repo
231 (org_ref_type,
232 org_ref_name,
233 org_ref_rev) = pull_request.org_ref.split(':')
234
235 other_repo = pull_request.other_repo
236 (other_ref_type,
237 other_ref_name,
238 other_ref_rev) = pull_request.other_ref.split(':')
239
240 # despite opening revisions for bookmarks/branches/tags, we always
241 # convert this to rev to prevent changes after book or branch change
242 org_ref = ('rev', org_ref_rev)
243 other_ref = ('rev', other_ref_rev)
244
245 c.org_repo = org_repo
246 c.other_repo = other_repo
247
248 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
249 org_repo, org_ref, other_repo, other_ref
250 )
251
252 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
253 c.cs_ranges])
254 # defines that we need hidden inputs with changesets
255 c.as_form = request.GET.get('as_form', False)
256
257 c.org_ref = org_ref[1]
258 c.other_ref = other_ref[1]
259 # diff needs to have swapped org with other to generate proper diff
260 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
261 discovery_data)
262 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
263 _parsed = diff_processor.prepare()
264
265 c.files = []
266 c.changes = {}
267
268 for f in _parsed:
269 fid = h.FID('', f['filename'])
270 c.files.append([fid, f['operation'], f['filename'], f['stats']])
271 diff = diff_processor.as_html(enable_comments=enable_comments,
272 diff_lines=[f])
273 c.changes[fid] = [f['operation'], f['filename'], diff]
274
275 def show(self, repo_name, pull_request_id):
276 repo_model = RepoModel()
277 c.users_array = repo_model.get_users_js()
278 c.users_groups_array = repo_model.get_users_groups_js()
279 c.pull_request = PullRequest.get_or_404(pull_request_id)
280
281 cc_model = ChangesetCommentsModel()
282 cs_model = ChangesetStatusModel()
283 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
284 pull_request=c.pull_request,
285 with_revisions=True)
286
287 cs_statuses = defaultdict(list)
288 for st in _cs_statuses:
289 cs_statuses[st.author.username] += [st]
290
291 c.pull_request_reviewers = []
292 c.pull_request_pending_reviewers = []
293 for o in c.pull_request.reviewers:
294 st = cs_statuses.get(o.user.username, None)
295 if st:
296 sorter = lambda k: k.version
297 st = [(x, list(y)[0])
298 for x, y in (groupby(sorted(st, key=sorter), sorter))]
299 else:
300 c.pull_request_pending_reviewers.append(o.user)
301 c.pull_request_reviewers.append([o.user, st])
302
303 # pull_requests repo_name we opened it against
304 # ie. other_repo must match
305 if repo_name != c.pull_request.other_repo.repo_name:
306 raise HTTPNotFound
307
308 # load compare data into template context
309 enable_comments = not c.pull_request.is_closed()
310 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
311
312 # inline comments
313 c.inline_cnt = 0
314 c.inline_comments = cc_model.get_inline_comments(
315 c.rhodecode_db_repo.repo_id,
316 pull_request=pull_request_id)
317 # count inline comments
318 for __, lines in c.inline_comments:
319 for comments in lines.values():
320 c.inline_cnt += len(comments)
321 # comments
322 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
323 pull_request=pull_request_id)
324
325 # changeset(pull-request) status
326 c.current_changeset_status = cs_model.calculate_status(
327 c.pull_request_reviewers
328 )
329 c.changeset_statuses = ChangesetStatus.STATUSES
330 c.target_repo = c.pull_request.org_repo.repo_name
331 return render('/pullrequests/pullrequest_show.html')
332
333 @NotAnonymous()
334 @jsonify
335 def comment(self, repo_name, pull_request_id):
336 pull_request = PullRequest.get_or_404(pull_request_id)
337 if pull_request.is_closed():
338 raise HTTPForbidden()
339
340 status = request.POST.get('changeset_status')
341 change_status = request.POST.get('change_changeset_status')
342
343 comm = ChangesetCommentsModel().create(
344 text=request.POST.get('text'),
345 repo=c.rhodecode_db_repo.repo_id,
346 user=c.rhodecode_user.user_id,
347 pull_request=pull_request_id,
348 f_path=request.POST.get('f_path'),
349 line_no=request.POST.get('line'),
350 status_change=(ChangesetStatus.get_status_lbl(status)
351 if status and change_status else None)
352 )
353
354 # get status if set !
355 if status and change_status:
356 ChangesetStatusModel().set_status(
357 c.rhodecode_db_repo.repo_id,
358 status,
359 c.rhodecode_user.user_id,
360 comm,
361 pull_request=pull_request_id
362 )
363 action_logger(self.rhodecode_user,
364 'user_commented_pull_request:%s' % pull_request_id,
365 c.rhodecode_db_repo, self.ip_addr, self.sa)
366
367 if request.POST.get('save_close'):
368 PullRequestModel().close_pull_request(pull_request_id)
369 action_logger(self.rhodecode_user,
370 'user_closed_pull_request:%s' % pull_request_id,
371 c.rhodecode_db_repo, self.ip_addr, self.sa)
372
373 Session().commit()
374
375 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
376 return redirect(h.url('pullrequest_show', repo_name=repo_name,
377 pull_request_id=pull_request_id))
378
379 data = {
380 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
381 }
382 if comm:
383 c.co = comm
384 data.update(comm.get_dict())
385 data.update({'rendered_text':
386 render('changeset/changeset_comment_block.html')})
387
388 return data
389
390 @NotAnonymous()
391 @jsonify
392 def delete_comment(self, repo_name, comment_id):
393 co = ChangesetComment.get(comment_id)
394 if co.pull_request.is_closed():
395 #don't allow deleting comments on closed pull request
396 raise HTTPForbidden()
397
398 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
399 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
400 ChangesetCommentsModel().delete(comment=co)
401 Session().commit()
402 return True
403 else:
404 raise HTTPForbidden()
1 NO CONTENT: new file 100644, binary diff hidden
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, binary diff hidden
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, binary diff hidden
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 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, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
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 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
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 100755
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 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 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, binary diff hidden
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,22 +1,24 b''
1 1 syntax: glob
2 2 *.pyc
3 3 *.swp
4 4 *.sqlite
5 *.tox
5 6 *.egg-info
6 7 *.egg
7 8
8 9 syntax: regexp
9 10 ^rcextensions
10 11 ^build
11 12 ^docs/build/
12 13 ^docs/_build/
13 14 ^data$
14 15 ^\.settings$
15 16 ^\.project$
16 17 ^\.pydevproject$
17 18 ^\.coverage$
18 19 ^rhodecode\.db$
19 20 ^test\.db$
20 21 ^RhodeCode\.egg-info$
21 22 ^rc\.ini$
22 23 ^fabfile.py
24 ^\.rhodecode$
@@ -1,21 +1,26 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 nansenat16 <nansenat16@null.tw> No newline at end of file
21 nansenat16 <nansenat16@null.tw>
22 Vincent Duvert <vincent@duvert.net>
23 Takumi IINO <trot.thunder@gmail.com>
24 Indra Talip <indra.talip@gmail.com>
25 James Rhodes <jrhodes@redpointsoftware.com.au>
26 Dominik Ruf <dominikruf@gmail.com> No newline at end of file
@@ -1,174 +1,177 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 and 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_ protocol requests.
72 72 Each request can be logged and authenticated.
73 73 - Runs on threads unlike hgweb. You can make multiple pulls/pushes simultaneous.
74 74 Supports http/https and LDAP
75 - Full permissions (private/read/write/admin) and authentication per project.
76 One account for web interface and mercurial_ push/pull/clone operations.
75 - Full permissions (private/read/write/admin) for each repository, additional
76 explicit forking and repository permissions.
77 77 - Have built in users groups for easier permission management
78 78 - Repository groups let you group repos and manage them easier.
79 79 - Users can fork other users repo. RhodeCode have also compare view to see
80 80 combined changeset for all changeset made within single push.
81 81 - Build in commit-api let's you add, edit and commit files right from RhodeCode
82 82 interface using simple editor or upload form for binaries.
83 - Powerfull pull-request driven review system with inline commenting, and
84 changeset statuses, notification system.
85 - Importing SVN repositories from remote locations into RhodeCode.
83 86 - Mako templates let's you customize the look and feel of the application.
84 87 - Beautiful diffs, annotations and source code browsing all colored by pygments.
85 Raw diffs are made in git-diff format, including git_ binary-patches
88 Raw diffs are made in git-diff format, including GIT_ binary-patches
86 89 - Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics
87 90 - Admin interface with user/permission management. Admin activity journal, logs
88 91 pulls, pushes, forks, registrations and other actions made by all users.
89 92 - Server side forks. It is possible to fork a project and modify it freely
90 93 without breaking the main repository. You can even write Your own hooks
91 94 and install them
92 - code review with notification system, inline commenting, all parsed using
93 rst syntax
94 95 - rst and markdown README support for repositories
95 96 - Full text search powered by Whoosh on the source files, and file names.
96 97 Build in indexing daemons, with optional incremental index build
97 98 (no external search servers required all in one application)
98 99 - Setup project descriptions and info inside built in db for easy, non
99 100 file-system operations
100 101 - Intelligent cache with invalidation after push or project change, provides
101 102 high performance and always up to date data.
102 103 - Rss / atom feeds, gravatar support, download sources as zip/tar/gz
103 104 - Optional async tasks for speed and performance using celery_
104 105 - Backup scripts can do backup of whole app and send it over scp to desired
105 106 location
106 107 - Based on pylons / sqlalchemy / sqlite / whoosh / vcs
107 108
108 109
109 110 Incoming / Plans
110 111 ----------------
111 112
112 113 - Finer granular permissions per branch, repo group or subrepo
113 - pull requests and web based merges
114 - per line file history
114 - Pull requests with web based merges
115 - Per line file history
116 - Simple issue tracker
115 117 - SSH based authentication with server side key management
116 118 - Commit based built in wiki system
117 119 - More statistics and graph (global annotation + some more statistics)
118 120 - Other advancements as development continues (or you can of course make
119 121 additions and or requests)
120 122
121 123 License
122 124 -------
123 125
124 126 ``RhodeCode`` is released under the GPLv3 license.
125 127
126 128
127 129 Getting help
128 130 ------------
129 131
130 132 Listed bellow are various support resources that should help.
131 133
132 134 .. note::
133 135
134 Please try to read the documentation before posting any issues
136 Please try to read the documentation before posting any issues, especially
137 the **troubleshooting section**
135 138
136 139 - Join the `Google group <http://groups.google.com/group/rhodecode>`_ and ask
137 140 any questions.
138 141
139 142 - Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
140 143
141 144
142 145 - Join #rhodecode on FreeNode (irc.freenode.net)
143 146 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
144 147
145 148 - You can also follow me on twitter **@marcinkuzminski** where i often post some
146 149 news about RhodeCode
147 150
148 151
149 152 Online documentation
150 153 --------------------
151 154
152 155 Online documentation for the current version of RhodeCode is available at
153 156 - http://packages.python.org/RhodeCode/
154 157 - http://rhodecode.readthedocs.org/en/latest/index.html
155 158
156 159 You may also build the documentation for yourself - go into ``docs/`` and run::
157 160
158 161 make html
159 162
160 163 (You need to have sphinx_ installed to build the documentation. If you don't
161 164 have sphinx_ installed you can install it via the command:
162 165 ``easy_install sphinx``)
163 166
164 167 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
165 168 .. _python: http://www.python.org/
166 169 .. _sphinx: http://sphinx.pocoo.org/
167 170 .. _mercurial: http://mercurial.selenic.com/
168 171 .. _bitbucket: http://bitbucket.org/
169 172 .. _github: http://github.com/
170 173 .. _subversion: http://subversion.tigris.org/
171 174 .. _git: http://git-scm.com/
172 175 .. _celery: http://celeryproject.org/
173 176 .. _Sphinx: http://sphinx.pocoo.org/
174 177 .. _vcs: http://pypi.python.org/pypi/vcs No newline at end of file
@@ -1,306 +1,329 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 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 ##nr of threads to spawn
33 threadpool_workers = 5
33 #threadpool_workers = 5
34 34
35 35 ##max request before thread respawn
36 threadpool_max_requests = 10
36 #threadpool_max_requests = 10
37 37
38 38 ##option to use threads of process
39 use_threadpool = true
39 #use_threadpool = true
40 40
41 use = egg:Paste#http
41 #use = egg:Paste#http
42 use = egg:waitress#main
42 43 host = 0.0.0.0
43 44 port = 5000
44 45
46 [filter:proxy-prefix]
47 # prefix middleware for rc
48 use = egg:PasteDeploy#prefix
49 prefix = /<your-prefix>
50
45 51 [app:main]
46 52 use = egg:rhodecode
53 #filter-with = proxy-prefix
47 54 full_stack = true
48 55 static_files = true
56 # Optional Languages
57 # en, fr, ja, pt_BR, zh_CN, zh_TW
49 58 lang = en
50 59 cache_dir = %(here)s/data
51 60 index_dir = %(here)s/data/index
52 61 app_instance_uuid = rc-develop
53 62 cut_off_limit = 256000
54 63 force_https = false
55 64 commit_parse_limit = 25
56 65 use_gravatar = true
66
67 ## alternative_gravatar_url allows you to use your own avatar server application
68 ## the following parts of the URL will be replaced
69 ## {email} user email
70 ## {md5email} md5 hash of the user email (like at gravatar.com)
71 ## {size} size of the image that is expected from the server application
72 #alternative_gravatar_url = http://myavatarserver.com/getbyemail/{email}/{size}
73 #alternative_gravatar_url = http://myavatarserver.com/getbymd5/{md5email}?s={size}
74
57 75 container_auth_enabled = false
58 76 proxypass_auth_enabled = false
59 77 default_encoding = utf8
60 78
61 79 ## overwrite schema of clone url
62 80 ## available vars:
63 81 ## scheme - http/https
64 82 ## user - current user
65 83 ## pass - password
66 84 ## netloc - network location
67 85 ## path - usually repo_name
68 86
69 87 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
70 88
71 89 ## issue tracking mapping for commits messages
72 90 ## comment out issue_pat, issue_server, issue_prefix to enable
73 91
74 92 ## pattern to get the issues from commit messages
75 93 ## default one used here is #<numbers> with a regex passive group for `#`
76 94 ## {id} will be all groups matched from this pattern
77 95
78 96 issue_pat = (?:\s*#)(\d+)
79 97
80 98 ## server url to the issue, each {id} will be replaced with match
81 ## fetched from the regex and {repo} is replaced with repository name
99 ## fetched from the regex and {repo} is replaced with full repository name
100 ## including groups {repo_name} is replaced with just name of repo
82 101
83 102 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
84 103
85 104 ## prefix to add to link to indicate it's an url
86 105 ## #314 will be replaced by <issue_prefix><id>
87 106
88 107 issue_prefix = #
89 108
90 109 ## instance-id prefix
91 110 ## a prefix key for this instance used for cache invalidation when running
92 111 ## multiple instances of rhodecode, make sure it's globally unique for
93 112 ## all running rhodecode instances. Leave empty if you don't use it
94 113 instance_id =
95 114
96 115 ## alternative return HTTP header for failed authentication. Default HTTP
97 116 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
98 117 ## handling that. Set this variable to 403 to return HTTPForbidden
99 118 auth_ret_code =
100 119
101 120 ####################################
102 121 ### CELERY CONFIG ####
103 122 ####################################
104 123 use_celery = false
105 124 broker.host = localhost
106 125 broker.vhost = rabbitmqhost
107 126 broker.port = 5672
108 127 broker.user = rabbitmq
109 128 broker.password = qweqwe
110 129
111 130 celery.imports = rhodecode.lib.celerylib.tasks
112 131
113 132 celery.result.backend = amqp
114 133 celery.result.dburi = amqp://
115 134 celery.result.serialier = json
116 135
117 136 #celery.send.task.error.emails = true
118 137 #celery.amqp.task.result.expires = 18000
119 138
120 139 celeryd.concurrency = 2
121 140 #celeryd.log.file = celeryd.log
122 141 celeryd.log.level = debug
123 142 celeryd.max.tasks.per.child = 1
124 143
125 144 #tasks will never be sent to the queue, but executed locally instead.
126 145 celery.always.eager = false
127 146
128 147 ####################################
129 148 ### BEAKER CACHE ####
130 149 ####################################
131 150 beaker.cache.data_dir=%(here)s/data/cache/data
132 151 beaker.cache.lock_dir=%(here)s/data/cache/lock
133 152
134 153 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
135 154
136 155 beaker.cache.super_short_term.type=memory
137 156 beaker.cache.super_short_term.expire=10
138 157 beaker.cache.super_short_term.key_length = 256
139 158
140 159 beaker.cache.short_term.type=memory
141 160 beaker.cache.short_term.expire=60
142 161 beaker.cache.short_term.key_length = 256
143 162
144 163 beaker.cache.long_term.type=memory
145 164 beaker.cache.long_term.expire=36000
146 165 beaker.cache.long_term.key_length = 256
147 166
148 167 beaker.cache.sql_cache_short.type=memory
149 168 beaker.cache.sql_cache_short.expire=10
150 169 beaker.cache.sql_cache_short.key_length = 256
151 170
152 171 beaker.cache.sql_cache_med.type=memory
153 172 beaker.cache.sql_cache_med.expire=360
154 173 beaker.cache.sql_cache_med.key_length = 256
155 174
156 175 beaker.cache.sql_cache_long.type=file
157 176 beaker.cache.sql_cache_long.expire=3600
158 177 beaker.cache.sql_cache_long.key_length = 256
159 178
160 179 ####################################
161 180 ### BEAKER SESSION ####
162 181 ####################################
163 182 ## Type of storage used for the session, current types are
164 183 ## dbm, file, memcached, database, and memory.
165 184 ## The storage uses the Container API
166 185 ## that is also used by the cache system.
167 186
168 ## db session example
169
187 ## db session ##
170 188 #beaker.session.type = ext:database
171 189 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
172 190 #beaker.session.table_name = db_session
173 191
174 ## encrypted cookie session, good for many instances
192 ## encrypted cookie client side session, good for many instances ##
175 193 #beaker.session.type = cookie
176 194
177 beaker.session.type = file
195 ## file based cookies (default) ##
196 #beaker.session.type = file
197
198
178 199 beaker.session.key = rhodecode
179 # secure cookie requires AES python libraries
200 ## secure cookie requires AES python libraries ##
180 201 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
181 202 #beaker.session.validate_key = 9712sds2212c--zxc123
182 beaker.session.timeout = 36000
203 ## sets session as invalid if it haven't been accessed for given amount of time
204 beaker.session.timeout = 2592000
183 205 beaker.session.httponly = true
206 #beaker.session.cookie_path = /<your-prefix>
184 207
185 ## uncomment for https secure cookie
208 ## uncomment for https secure cookie ##
186 209 beaker.session.secure = false
187 210
188 ##auto save the session to not to use .save()
211 ## auto save the session to not to use .save() ##
189 212 beaker.session.auto = False
190 213
191 ##true exire at browser close
214 ## default cookie expiration time in seconds `true` expire at browser close ##
192 215 #beaker.session.cookie_expires = 3600
193 216
194 217
195 218 ################################################################################
196 219 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
197 220 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
198 221 ## execute malicious code after an exception is raised. ##
199 222 ################################################################################
200 223 #set debug = false
201 224
202 225 ##################################
203 226 ### LOGVIEW CONFIG ###
204 227 ##################################
205 228 logview.sqlalchemy = #faa
206 229 logview.pylons.templating = #bfb
207 230 logview.pylons.util = #eee
208 231
209 232 #########################################################
210 233 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
211 234 #########################################################
212 235 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
213 236 sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode
214 237 sqlalchemy.db1.echo = false
215 238 sqlalchemy.db1.pool_recycle = 3600
216 239 sqlalchemy.db1.convert_unicode = true
217 240
218 241 ################################
219 242 ### LOGGING CONFIGURATION ####
220 243 ################################
221 244 [loggers]
222 245 keys = root, routes, rhodecode, sqlalchemy, beaker, templates, whoosh_indexer
223 246
224 247 [handlers]
225 248 keys = console, console_sql
226 249
227 250 [formatters]
228 251 keys = generic, color_formatter, color_formatter_sql
229 252
230 253 #############
231 254 ## LOGGERS ##
232 255 #############
233 256 [logger_root]
234 257 level = NOTSET
235 258 handlers = console
236 259
237 260 [logger_routes]
238 261 level = DEBUG
239 262 handlers =
240 263 qualname = routes.middleware
241 264 # "level = DEBUG" logs the route matched and routing variables.
242 265 propagate = 1
243 266
244 267 [logger_beaker]
245 268 level = DEBUG
246 269 handlers =
247 270 qualname = beaker.container
248 271 propagate = 1
249 272
250 273 [logger_templates]
251 274 level = INFO
252 275 handlers =
253 276 qualname = pylons.templating
254 277 propagate = 1
255 278
256 279 [logger_rhodecode]
257 280 level = DEBUG
258 281 handlers =
259 282 qualname = rhodecode
260 283 propagate = 1
261 284
262 285 [logger_sqlalchemy]
263 286 level = INFO
264 287 handlers = console_sql
265 288 qualname = sqlalchemy.engine
266 289 propagate = 0
267 290
268 291 [logger_whoosh_indexer]
269 292 level = DEBUG
270 293 handlers =
271 294 qualname = whoosh_indexer
272 295 propagate = 1
273 296
274 297 ##############
275 298 ## HANDLERS ##
276 299 ##############
277 300
278 301 [handler_console]
279 302 class = StreamHandler
280 303 args = (sys.stderr,)
281 304 level = DEBUG
282 305 formatter = color_formatter
283 306
284 307 [handler_console_sql]
285 308 class = StreamHandler
286 309 args = (sys.stderr,)
287 310 level = DEBUG
288 311 formatter = color_formatter_sql
289 312
290 313 ################
291 314 ## FORMATTERS ##
292 315 ################
293 316
294 317 [formatter_generic]
295 318 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
296 319 datefmt = %Y-%m-%d %H:%M:%S
297 320
298 321 [formatter_color_formatter]
299 322 class=rhodecode.lib.colored_formatter.ColorFormatter
300 323 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
301 324 datefmt = %Y-%m-%d %H:%M:%S
302 325
303 326 [formatter_color_formatter_sql]
304 327 class=rhodecode.lib.colored_formatter.ColorFormatterSql
305 328 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
306 329 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,659 +1,876 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 with JSON protocol both ways. An url to send API request in RhodeCode is
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
63 API CLIENT
64 ++++++++++
65
66 From version 1.4 RhodeCode adds a script that allows to easily
67 communicate with API. After installing RhodeCode a `rhodecode-api` script
68 will be available.
69
70 To get started quickly simply run::
71
72 rhodecode-api _create_config --apikey=<youapikey> --apihost=<rhodecode host>
73
74 This will create a file named .config in the directory you executed it storing
75 json config file with credentials. You can skip this step and always provide
76 both of the arguments to be able to communicate with server
77
78
79 after that simply run any api command for example get_repo::
80
81 rhodecode-api get_repo
82
83 calling {"api_key": "<apikey>", "id": 75, "args": {}, "method": "get_repo"} to http://127.0.0.1:5000
84 rhodecode said:
85 {'error': 'Missing non optional `repoid` arg in JSON DATA',
86 'id': 75,
87 'result': None}
88
89 Ups looks like we forgot to add an argument
90
91 Let's try again now giving the repoid as parameters::
92
93 rhodecode-api get_repo repoid:rhodecode
94
95 calling {"api_key": "<apikey>", "id": 39, "args": {"repoid": "rhodecode"}, "method": "get_repo"} to http://127.0.0.1:5000
96 rhodecode said:
97 {'error': None,
98 'id': 39,
99 'result': <json data...>}
100
101
102
62 103 API METHODS
63 104 +++++++++++
64 105
65 106
66 107 pull
67 108 ----
68 109
69 110 Pulls given repo from remote location. Can be used to automatically keep
70 111 remote repos up to date. This command can be executed only using api_key
71 112 belonging to user with admin rights
72 113
73 114 INPUT::
74 115
75 116 id : <id_for_response>
76 117 api_key : "<api_key>"
77 118 method : "pull"
78 119 args : {
79 "repo_name" : "<reponame>"
120 "repoid" : "<reponame or repo_id>"
121 }
122
123 OUTPUT::
124
125 id : <id_given_in_input>
126 result : "Pulled from `<reponame>`"
127 error : null
128
129
130 rescan_repos
131 ------------
132
133 Dispatch rescan repositories action. If remove_obsolete is set
134 RhodeCode will delete repos that are in database but not in the filesystem.
135 This command can be executed only using api_key belonging to user with admin
136 rights.
137
138 INPUT::
139
140 id : <id_for_response>
141 api_key : "<api_key>"
142 method : "rescan_repos"
143 args : {
144 "remove_obsolete" : "<boolean = Optional(False)>"
80 145 }
81 146
82 147 OUTPUT::
83 148
84 result : "Pulled from <reponame>"
149 id : <id_given_in_input>
150 result : "{'added': [<list of names of added repos>],
151 'removed': [<list of names of removed repos>]}"
152 error : null
153
154
155 lock
156 ----
157
158 Set locking state on given repository by given user.
159 This command can be executed only using api_key belonging to user with admin
160 rights.
161
162 INPUT::
163
164 id : <id_for_response>
165 api_key : "<api_key>"
166 method : "lock"
167 args : {
168 "repoid" : "<reponame or repo_id>"
169 "userid" : "<user_id or username>",
170 "locked" : "<bool true|false>"
171
172 }
173
174 OUTPUT::
175
176 id : <id_given_in_input>
177 result : "User `<username>` set lock state for repo `<reponame>` to `true|false`"
85 178 error : null
86 179
87 180
88 181 get_user
89 182 --------
90 183
91 184 Get's an user by username or user_id, Returns empty result if user is not found.
92 185 This command can be executed only using api_key belonging to user with admin
93 186 rights.
94 187
95 188
96 189 INPUT::
97 190
98 191 id : <id_for_response>
99 192 api_key : "<api_key>"
100 193 method : "get_user"
101 194 args : {
102 195 "userid" : "<username or user_id>"
103 196 }
104 197
105 198 OUTPUT::
106 199
200 id : <id_given_in_input>
107 201 result: None if user does not exist or
108 202 {
109 "id" : "<id>",
203 "user_id" : "<user_id>",
110 204 "username" : "<username>",
111 205 "firstname": "<firstname>",
112 206 "lastname" : "<lastname>",
113 207 "email" : "<email>",
208 "emails": "<list_of_all_additional_emails>",
114 209 "active" : "<bool>",
115 210 "admin" :  "<bool>",
116 211 "ldap_dn" : "<ldap_dn>",
117 212 "last_login": "<last_login>",
118 213 "permissions": {
119 214 "global": ["hg.create.repository",
120 215 "repository.read",
121 216 "hg.register.manual_activate"],
122 217 "repositories": {"repo1": "repository.none"},
123 218 "repositories_groups": {"Group1": "group.read"}
124 219 },
125 220 }
126 221
127 222 error: null
128 223
129 224
130 225 get_users
131 226 ---------
132 227
133 228 Lists all existing users. This command can be executed only using api_key
134 229 belonging to user with admin rights.
135 230
136 231
137 232 INPUT::
138 233
139 234 id : <id_for_response>
140 235 api_key : "<api_key>"
141 236 method : "get_users"
142 237 args : { }
143 238
144 239 OUTPUT::
145 240
241 id : <id_given_in_input>
146 242 result: [
147 243 {
148 "id" : "<id>",
244 "user_id" : "<user_id>",
149 245 "username" : "<username>",
150 246 "firstname": "<firstname>",
151 247 "lastname" : "<lastname>",
152 248 "email" : "<email>",
249 "emails": "<list_of_all_additional_emails>",
153 250 "active" : "<bool>",
154 251 "admin" :  "<bool>",
155 252 "ldap_dn" : "<ldap_dn>",
156 253 "last_login": "<last_login>",
157 254 },
158 255
159 256 ]
160 257 error: null
161 258
162 259
163 260 create_user
164 261 -----------
165 262
166 263 Creates new user. This command can
167 264 be executed only using api_key belonging to user with admin rights.
168 265
169 266
170 267 INPUT::
171 268
172 269 id : <id_for_response>
173 270 api_key : "<api_key>"
174 271 method : "create_user"
175 272 args : {
176 273 "username" : "<username>",
274 "email" : "<useremail>",
177 275 "password" : "<password>",
178 "email" : "<useremail>",
179 "firstname" : "<firstname> = None",
180 "lastname" : "<lastname> = None",
181 "active" : "<bool> = True",
182 "admin" : "<bool> = False",
183 "ldap_dn" : "<ldap_dn> = None"
276 "firstname" : "<firstname> = Optional(None)",
277 "lastname" : "<lastname> = Optional(None)",
278 "active" : "<bool> = Optional(True)",
279 "admin" : "<bool> = Optional(False)",
280 "ldap_dn" : "<ldap_dn> = Optional(None)"
184 281 }
185 282
186 283 OUTPUT::
187 284
285 id : <id_given_in_input>
188 286 result: {
189 "id" : "<new_user_id>",
190 "msg" : "created new user <username>"
287 "msg" : "created new user `<username>`",
288 "user": {
289 "user_id" : "<user_id>",
290 "username" : "<username>",
291 "firstname": "<firstname>",
292 "lastname" : "<lastname>",
293 "email" : "<email>",
294 "emails": "<list_of_all_additional_emails>",
295 "active" : "<bool>",
296 "admin" :  "<bool>",
297 "ldap_dn" : "<ldap_dn>",
298 "last_login": "<last_login>",
299 },
191 300 }
192 301 error: null
193 302
194 303
195 304 update_user
196 305 -----------
197 306
198 updates current one if such user exists. This command can
307 updates given user if such user exists. This command can
199 308 be executed only using api_key belonging to user with admin rights.
200 309
201 310
202 311 INPUT::
203 312
204 313 id : <id_for_response>
205 314 api_key : "<api_key>"
206 315 method : "update_user"
207 316 args : {
208 317 "userid" : "<user_id or username>",
209 "username" : "<username>",
210 "password" : "<password>",
211 "email" : "<useremail>",
212 "firstname" : "<firstname>",
213 "lastname" : "<lastname>",
214 "active" : "<bool>",
215 "admin" : "<bool>",
216 "ldap_dn" : "<ldap_dn>"
318 "username" : "<username> = Optional",
319 "email" : "<useremail> = Optional",
320 "password" : "<password> = Optional",
321 "firstname" : "<firstname> = Optional",
322 "lastname" : "<lastname> = Optional",
323 "active" : "<bool> = Optional",
324 "admin" : "<bool> = Optional",
325 "ldap_dn" : "<ldap_dn> = Optional"
217 326 }
218 327
219 328 OUTPUT::
220 329
330 id : <id_given_in_input>
221 331 result: {
222 "id" : "<edited_user_id>",
223 "msg" : "updated user <username>"
332 "msg" : "updated user ID:<userid> <username>",
333 "user": {
334 "user_id" : "<user_id>",
335 "username" : "<username>",
336 "firstname": "<firstname>",
337 "lastname" : "<lastname>",
338 "email" : "<email>",
339 "emails": "<list_of_all_additional_emails>",
340 "active" : "<bool>",
341 "admin" :  "<bool>",
342 "ldap_dn" : "<ldap_dn>",
343 "last_login": "<last_login>",
344 },
345 }
346 error: null
347
348
349 delete_user
350 -----------
351
352
353 deletes givenuser if such user exists. This command can
354 be executed only using api_key belonging to user with admin rights.
355
356
357 INPUT::
358
359 id : <id_for_response>
360 api_key : "<api_key>"
361 method : "delete_user"
362 args : {
363 "userid" : "<user_id or username>",
364 }
365
366 OUTPUT::
367
368 id : <id_given_in_input>
369 result: {
370 "msg" : "deleted user ID:<userid> <username>",
371 "user": null
224 372 }
225 373 error: null
226 374
227 375
228 376 get_users_group
229 377 ---------------
230 378
231 379 Gets an existing users group. This command can be executed only using api_key
232 380 belonging to user with admin rights.
233 381
234 382
235 383 INPUT::
236 384
237 385 id : <id_for_response>
238 386 api_key : "<api_key>"
239 387 method : "get_users_group"
240 388 args : {
241 "group_name" : "<name>"
389 "usersgroupid" : "<users group id or name>"
242 390 }
243 391
244 392 OUTPUT::
245 393
394 id : <id_given_in_input>
246 395 result : None if group not exist
247 396 {
248 "id" : "<id>",
397 "users_group_id" : "<id>",
249 398 "group_name" : "<groupname>",
250 399 "active": "<bool>",
251 400 "members" : [
252 { "id" : "<userid>",
401 {
402 "user_id" : "<user_id>",
253 403 "username" : "<username>",
254 404 "firstname": "<firstname>",
255 405 "lastname" : "<lastname>",
256 406 "email" : "<email>",
407 "emails": "<list_of_all_additional_emails>",
257 408 "active" : "<bool>",
258 409 "admin" :  "<bool>",
259 "ldap" : "<ldap_dn>"
410 "ldap_dn" : "<ldap_dn>",
411 "last_login": "<last_login>",
260 412 },
261 413
262 414 ]
263 415 }
264 416 error : null
265 417
266 418
267 419 get_users_groups
268 420 ----------------
269 421
270 422 Lists all existing users groups. This command can be executed only using
271 423 api_key belonging to user with admin rights.
272 424
273 425
274 426 INPUT::
275 427
276 428 id : <id_for_response>
277 429 api_key : "<api_key>"
278 430 method : "get_users_groups"
279 431 args : { }
280 432
281 433 OUTPUT::
282 434
435 id : <id_given_in_input>
283 436 result : [
284 437 {
285 "id" : "<id>",
438 "users_group_id" : "<id>",
286 439 "group_name" : "<groupname>",
287 440 "active": "<bool>",
288 441 "members" : [
289 442 {
290 "id" : "<userid>",
443 "user_id" : "<user_id>",
291 444 "username" : "<username>",
292 445 "firstname": "<firstname>",
293 446 "lastname" : "<lastname>",
294 447 "email" : "<email>",
448 "emails": "<list_of_all_additional_emails>",
295 449 "active" : "<bool>",
296 450 "admin" :  "<bool>",
297 "ldap" : "<ldap_dn>"
451 "ldap_dn" : "<ldap_dn>",
452 "last_login": "<last_login>",
298 453 },
299 454
300 455 ]
301 }
456 },
457
302 458 ]
303 459 error : null
304 460
305 461
306 462 create_users_group
307 463 ------------------
308 464
309 465 Creates new users group. This command can be executed only using api_key
310 466 belonging to user with admin rights
311 467
312 468
313 469 INPUT::
314 470
315 471 id : <id_for_response>
316 472 api_key : "<api_key>"
317 473 method : "create_users_group"
318 474 args: {
319 475 "group_name": "<groupname>",
320 "active":"<bool> = True"
476 "active":"<bool> = Optional(True)"
321 477 }
322 478
323 479 OUTPUT::
324 480
481 id : <id_given_in_input>
325 482 result: {
326 "id": "<newusersgroupid>",
327 "msg": "created new users group <groupname>"
483 "msg": "created new users group `<groupname>`",
484 "users_group": {
485 "users_group_id" : "<id>",
486 "group_name" : "<groupname>",
487 "active": "<bool>",
488 "members" : [
489 {
490 "user_id" : "<user_id>",
491 "username" : "<username>",
492 "firstname": "<firstname>",
493 "lastname" : "<lastname>",
494 "email" : "<email>",
495 "emails": "<list_of_all_additional_emails>",
496 "active" : "<bool>",
497 "admin" :  "<bool>",
498 "ldap_dn" : "<ldap_dn>",
499 "last_login": "<last_login>",
500 },
501
502 ]
503 },
328 504 }
329 505 error: null
330 506
331 507
332 508 add_user_to_users_group
333 509 -----------------------
334 510
335 511 Adds a user to a users group. If user exists in that group success will be
336 512 `false`. This command can be executed only using api_key
337 513 belonging to user with admin rights
338 514
339 515
340 516 INPUT::
341 517
342 518 id : <id_for_response>
343 519 api_key : "<api_key>"
344 520 method : "add_user_users_group"
345 521 args: {
346 "group_name" : "<groupname>",
347 "username" : "<username>"
522 "usersgroupid" : "<users group id or name>",
523 "userid" : "<user_id or username>",
348 524 }
349 525
350 526 OUTPUT::
351 527
528 id : <id_given_in_input>
352 529 result: {
353 "id": "<newusersgroupmemberid>",
354 530 "success": True|False # depends on if member is in group
355 "msg": "added member <username> to users group <groupname> |
531 "msg": "added member `<username>` to users group `<groupname>` |
356 532 User is already in that group"
357 533 }
358 534 error: null
359 535
360 536
361 537 remove_user_from_users_group
362 538 ----------------------------
363 539
364 540 Removes a user from a users group. If user is not in given group success will
365 541 be `false`. This command can be executed only
366 542 using api_key belonging to user with admin rights
367 543
368 544
369 545 INPUT::
370 546
371 547 id : <id_for_response>
372 548 api_key : "<api_key>"
373 549 method : "remove_user_from_users_group"
374 550 args: {
375 "group_name" : "<groupname>",
376 "username" : "<username>"
551 "usersgroupid" : "<users group id or name>",
552 "userid" : "<user_id or username>",
377 553 }
378 554
379 555 OUTPUT::
380 556
557 id : <id_given_in_input>
381 558 result: {
382 559 "success": True|False, # depends on if member is in group
383 560 "msg": "removed member <username> from users group <groupname> |
384 561 User wasn't in group"
385 562 }
386 563 error: null
387 564
388 565
389 566 get_repo
390 567 --------
391 568
392 569 Gets an existing repository by it's name or repository_id. Members will return
393 570 either users_group or user associated to that repository. This command can
394 571 be executed only using api_key belonging to user with admin rights.
395 572
396 573
397 574 INPUT::
398 575
399 576 id : <id_for_response>
400 577 api_key : "<api_key>"
401 578 method : "get_repo"
402 579 args: {
403 580 "repoid" : "<reponame or repo_id>"
404 581 }
405 582
406 583 OUTPUT::
407 584
585 id : <id_given_in_input>
408 586 result: None if repository does not exist or
409 587 {
410 "id" : "<id>",
588 "repo_id" : "<repo_id>",
411 589 "repo_name" : "<reponame>"
412 "type" : "<type>",
590 "repo_type" : "<repo_type>",
591 "clone_uri" : "<clone_uri>",
592 "private": : "<bool>",
593 "created_on" : "<datetimecreated>",
413 594 "description" : "<description>",
595 "landing_rev": "<landing_rev>",
596 "owner": "<repo_owner>",
597 "fork_of": "<name_of_fork_parent>",
414 598 "members" : [
415 599 {
416 600 "type": "user",
417 "id" : "<userid>",
601 "user_id" : "<user_id>",
418 602 "username" : "<username>",
419 603 "firstname": "<firstname>",
420 604 "lastname" : "<lastname>",
421 605 "email" : "<email>",
606 "emails": "<list_of_all_additional_emails>",
422 607 "active" : "<bool>",
423 608 "admin" :  "<bool>",
424 "ldap" : "<ldap_dn>",
609 "ldap_dn" : "<ldap_dn>",
610 "last_login": "<last_login>",
425 611 "permission" : "repository.(read|write|admin)"
426 612 },
427 613
428 614 {
429 615 "type": "users_group",
430 616 "id" : "<usersgroupid>",
431 617 "name" : "<usersgroupname>",
432 618 "active": "<bool>",
433 619 "permission" : "repository.(read|write|admin)"
434 620 },
435 621
436 622 ]
437 623 }
438 624 error: null
439 625
440 626
441 627 get_repos
442 628 ---------
443 629
444 630 Lists all existing repositories. This command can be executed only using api_key
445 631 belonging to user with admin rights
446 632
447 633
448 634 INPUT::
449 635
450 636 id : <id_for_response>
451 637 api_key : "<api_key>"
452 638 method : "get_repos"
453 639 args: { }
454 640
455 641 OUTPUT::
456 642
643 id : <id_given_in_input>
457 644 result: [
458 645 {
459 "id" : "<id>",
646 "repo_id" : "<repo_id>",
460 647 "repo_name" : "<reponame>"
461 "type" : "<type>",
462 "description" : "<description>"
648 "repo_type" : "<repo_type>",
649 "clone_uri" : "<clone_uri>",
650 "private": : "<bool>",
651 "created_on" : "<datetimecreated>",
652 "description" : "<description>",
653 "landing_rev": "<landing_rev>",
654 "owner": "<repo_owner>",
655 "fork_of": "<name_of_fork_parent>",
463 656 },
464 657
465 658 ]
466 659 error: null
467 660
468 661
469 662 get_repo_nodes
470 663 --------------
471 664
472 665 returns a list of nodes and it's children in a flat list for a given path
473 666 at given revision. It's possible to specify ret_type to show only `files` or
474 667 `dirs`. This command can be executed only using api_key belonging to user
475 668 with admin rights
476 669
477 670
478 671 INPUT::
479 672
480 673 id : <id_for_response>
481 674 api_key : "<api_key>"
482 675 method : "get_repo_nodes"
483 676 args: {
484 "repo_name" : "<reponame>",
677 "repoid" : "<reponame or repo_id>"
485 678 "revision" : "<revision>",
486 679 "root_path" : "<root_path>",
487 "ret_type" : "<ret_type>" = 'all'
680 "ret_type" : "<ret_type> = Optional('all')"
488 681 }
489 682
490 683 OUTPUT::
491 684
685 id : <id_given_in_input>
492 686 result: [
493 687 {
494 688 "name" : "<name>"
495 689 "type" : "<type>",
496 690 },
497 691
498 692 ]
499 693 error: null
500 694
501 695
502 696 create_repo
503 697 -----------
504 698
505 699 Creates a repository. This command can be executed only using api_key
506 700 belonging to user with admin rights.
507 701 If repository name contains "/", all needed repository groups will be created.
508 702 For example "foo/bar/baz" will create groups "foo", "bar" (with "foo" as parent),
509 703 and create "baz" repository with "bar" as group.
510 704
511 705
512 706 INPUT::
513 707
514 708 id : <id_for_response>
515 709 api_key : "<api_key>"
516 710 method : "create_repo"
517 711 args: {
518 712 "repo_name" : "<reponame>",
519 "owner_name" : "<ownername>",
520 "description" : "<description> = ''",
521 "repo_type" : "<type> = 'hg'",
522 "private" : "<bool> = False",
523 "clone_uri" : "<clone_uri> = None",
713 "owner" : "<onwer_name_or_id>",
714 "repo_type" : "<repo_type>",
715 "description" : "<description> = Optional('')",
716 "private" : "<bool> = Optional(False)",
717 "clone_uri" : "<clone_uri> = Optional(None)",
718 "landing_rev" : "<landing_rev> = Optional('tip')",
524 719 }
525 720
526 721 OUTPUT::
527 722
723 id : <id_given_in_input>
528 724 result: {
529 "id": "<newrepoid>",
530 "msg": "Created new repository <reponame>",
725 "msg": "Created new repository `<reponame>`",
726 "repo": {
727 "repo_id" : "<repo_id>",
728 "repo_name" : "<reponame>"
729 "repo_type" : "<repo_type>",
730 "clone_uri" : "<clone_uri>",
731 "private": : "<bool>",
732 "created_on" : "<datetimecreated>",
733 "description" : "<description>",
734 "landing_rev": "<landing_rev>",
735 "owner": "<repo_owner>",
736 "fork_of": "<name_of_fork_parent>",
737 },
531 738 }
532 739 error: null
533 740
534 741
535 742 delete_repo
536 743 -----------
537 744
538 745 Deletes a repository. This command can be executed only using api_key
539 746 belonging to user with admin rights.
540 747
541 748
542 749 INPUT::
543 750
544 751 id : <id_for_response>
545 752 api_key : "<api_key>"
546 753 method : "delete_repo"
547 754 args: {
548 "repo_name" : "<reponame>",
755 "repoid" : "<reponame or repo_id>"
549 756 }
550 757
551 758 OUTPUT::
552 759
760 id : <id_given_in_input>
553 761 result: {
554 "msg": "Deleted repository <reponame>",
762 "msg": "Deleted repository `<reponame>`",
763 "success": true
555 764 }
556 765 error: null
557 766
558 767
559 768 grant_user_permission
560 769 ---------------------
561 770
562 771 Grant permission for user on given repository, or update existing one
563 772 if found. This command can be executed only using api_key belonging to user
564 773 with admin rights.
565 774
566 775
567 776 INPUT::
568 777
569 778 id : <id_for_response>
570 779 api_key : "<api_key>"
571 780 method : "grant_user_permission"
572 781 args: {
573 "repo_name" : "<reponame>",
574 "username" : "<username>",
782 "repoid" : "<reponame or repo_id>"
783 "userid" : "<username or user_id>"
575 784 "perm" : "(repository.(none|read|write|admin))",
576 785 }
577 786
578 787 OUTPUT::
579 788
789 id : <id_given_in_input>
580 790 result: {
581 "msg" : "Granted perm: <perm> for user: <username> in repo: <reponame>"
791 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
792 "success": true
582 793 }
583 794 error: null
584 795
585 796
586 797 revoke_user_permission
587 798 ----------------------
588 799
589 800 Revoke permission for user on given repository. This command can be executed
590 801 only using api_key belonging to user with admin rights.
591 802
592 803
593 804 INPUT::
594 805
595 806 id : <id_for_response>
596 807 api_key : "<api_key>"
597 808 method : "revoke_user_permission"
598 809 args: {
599 "repo_name" : "<reponame>",
600 "username" : "<username>",
810 "repoid" : "<reponame or repo_id>"
811 "userid" : "<username or user_id>"
601 812 }
602 813
603 814 OUTPUT::
604 815
816 id : <id_given_in_input>
605 817 result: {
606 "msg" : "Revoked perm for user: <suername> in repo: <reponame>"
818 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
819 "success": true
607 820 }
608 821 error: null
609 822
610 823
611 824 grant_users_group_permission
612 825 ----------------------------
613 826
614 827 Grant permission for users group on given repository, or update
615 828 existing one if found. This command can be executed only using
616 829 api_key belonging to user with admin rights.
617 830
618 831
619 832 INPUT::
620 833
621 834 id : <id_for_response>
622 835 api_key : "<api_key>"
623 836 method : "grant_users_group_permission"
624 837 args: {
625 "repo_name" : "<reponame>",
626 "group_name" : "<usersgroupname>",
838 "repoid" : "<reponame or repo_id>"
839 "usersgroupid" : "<users group id or name>"
627 840 "perm" : "(repository.(none|read|write|admin))",
628 841 }
629 842
630 843 OUTPUT::
631 844
845 id : <id_given_in_input>
632 846 result: {
633 "msg" : "Granted perm: <perm> for group: <usersgroupname> in repo: <reponame>"
847 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
848 "success": true
634 849 }
635 850 error: null
636 851
637 852
638 853 revoke_users_group_permission
639 854 -----------------------------
640 855
641 856 Revoke permission for users group on given repository.This command can be
642 857 executed only using api_key belonging to user with admin rights.
643 858
644 859 INPUT::
645 860
646 861 id : <id_for_response>
647 862 api_key : "<api_key>"
648 863 method : "revoke_users_group_permission"
649 864 args: {
650 "repo_name" : "<reponame>",
651 "users_group" : "<usersgroupname>",
865 "repoid" : "<reponame or repo_id>"
866 "usersgroupid" : "<users group id or name>"
652 867 }
653 868
654 869 OUTPUT::
655 870
871 id : <id_given_in_input>
656 872 result: {
657 "msg" : "Revoked perm for group: <usersgroupname> in repo: <reponame>"
873 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
874 "success": true
658 875 }
659 876 error: null No newline at end of file
@@ -1,652 +1,712 b''
1 1 .. _changelog:
2 2
3 3 =========
4 4 Changelog
5 5 =========
6 6
7 1.4.0 (**2012-09-03**)
8 ----------------------
9
10 news
11 ++++
12
13 - new codereview system
14 - email map, allowing users to have multiple email addresses mapped into
15 their accounts
16 - improved git-hook system. Now all actions for git are logged into journal
17 including pushed revisions, user and IP address
18 - changed setup-app into setup-rhodecode and added default options to it.
19 - new git repos are created as bare now by default
20 - #464 added links to groups in permission box
21 - #465 mentions autocomplete inside comments boxes
22 - #469 added --update-only option to whoosh to re-index only given list
23 of repos in index
24 - rhodecode-api CLI client
25 - new git http protocol replaced buggy dulwich implementation.
26 Now based on pygrack & gitweb
27 - Improved RSS/ATOM feeds. Discoverable by browsers using proper headers, and
28 reformated based on user suggestions. Additional rss/atom feeds for user
29 journal
30 - various i18n improvements
31 - #478 permissions overview for admin in user edit view
32 - File view now displays small gravatars off all authors of given file
33 - Implemented landing revisions. Each repository will get landing_rev attribute
34 that defines 'default' revision/branch for generating readme files
35 - Implemented #509, RhodeCode enforces SSL for push/pulling if requested at
36 earliest possible call.
37 - Import remote svn repositories to mercurial using hgsubversion.
38 - Fixed #508 RhodeCode now has a option to explicitly set forking permissions
39 - RhodeCode can use alternative server for generating avatar icons
40 - implemented repositories locking. Pull locks, push unlocks. Also can be done
41 via API calls
42 - #538 form for permissions can handle multiple users at once
43
44 fixes
45 +++++
46
47 - improved translations
48 - fixes issue #455 Creating an archive generates an exception on Windows
49 - fixes #448 Download ZIP archive keeps file in /tmp open and results
50 in out of disk space
51 - fixes issue #454 Search results under Windows include proceeding
52 backslash
53 - fixed issue #450. Rhodecode no longer will crash when bad revision is
54 present in journal data.
55 - fix for issue #417, git execution was broken on windows for certain
56 commands.
57 - fixed #413. Don't disable .git directory for bare repos on deleting
58 - fixed issue #459. Changed the way of obtaining logger in reindex task.
59 - fixed #453 added ID field in whoosh SCHEMA that solves the issue of
60 reindexing modified files
61 - fixed #481 rhodecode emails are sent without Date header
62 - fixed #458 wrong count when no repos are present
63 - fixed issue #492 missing `\ No newline at end of file` test at the end of
64 new chunk in html diff
65 - full text search now works also for commit messages
66
7 67 1.3.6 (**2012-05-17**)
8 68 ----------------------
9 69
10 70 news
11 71 ++++
12 72
13 73 - chinese traditional translation
14 74 - changed setup-app into setup-rhodecode and added arguments for auto-setup
15 75 mode that doesn't need user interaction
16 76
17 77 fixes
18 78 +++++
19 79
20 80 - fixed no scm found warning
21 81 - fixed __future__ import error on rcextensions
22 82 - made simplejson required lib for speedup on JSON encoding
23 83 - fixes #449 bad regex could get more than revisions from parsing history
24 84 - don't clear DB session when CELERY_EAGER is turned ON
25 85
26 86 1.3.5 (**2012-05-10**)
27 87 ----------------------
28 88
29 89 news
30 90 ++++
31 91
32 92 - use ext_json for json module
33 93 - unified annotation view with file source view
34 94 - notification improvements, better inbox + css
35 95 - #419 don't strip passwords for login forms, make rhodecode
36 96 more compatible with LDAP servers
37 97 - Added HTTP_X_FORWARDED_FOR as another method of extracting
38 98 IP for pull/push logs. - moved all to base controller
39 99 - #415: Adding comment to changeset causes reload.
40 100 Comments are now added via ajax and doesn't reload the page
41 101 - #374 LDAP config is discarded when LDAP can't be activated
42 102 - limited push/pull operations are now logged for git in the journal
43 103 - bumped mercurial to 2.2.X series
44 104 - added support for displaying submodules in file-browser
45 105 - #421 added bookmarks in changelog view
46 106
47 107 fixes
48 108 +++++
49 109
50 110 - fixed dev-version marker for stable when served from source codes
51 111 - fixed missing permission checks on show forks page
52 112 - #418 cast to unicode fixes in notification objects
53 113 - #426 fixed mention extracting regex
54 114 - fixed remote-pulling for git remotes remopositories
55 115 - fixed #434: Error when accessing files or changesets of a git repository
56 116 with submodules
57 117 - fixed issue with empty APIKEYS for users after registration ref. #438
58 118 - fixed issue with getting README files from git repositories
59 119
60 120 1.3.4 (**2012-03-28**)
61 121 ----------------------
62 122
63 123 news
64 124 ++++
65 125
66 126 - Whoosh logging is now controlled by the .ini files logging setup
67 127 - added clone-url into edit form on /settings page
68 128 - added help text into repo add/edit forms
69 129 - created rcextensions module with additional mappings (ref #322) and
70 130 post push/pull/create repo hooks callbacks
71 131 - implemented #377 Users view for his own permissions on account page
72 132 - #399 added inheritance of permissions for users group on repos groups
73 133 - #401 repository group is automatically pre-selected when adding repos
74 134 inside a repository group
75 135 - added alternative HTTP 403 response when client failed to authenticate. Helps
76 136 solving issues with Mercurial and LDAP
77 137 - #402 removed group prefix from repository name when listing repositories
78 138 inside a group
79 139 - added gravatars into permission view and permissions autocomplete
80 140 - #347 when running multiple RhodeCode instances, properly invalidates cache
81 141 for all registered servers
82 142
83 143 fixes
84 144 +++++
85 145
86 146 - fixed #390 cache invalidation problems on repos inside group
87 147 - fixed #385 clone by ID url was loosing proxy prefix in URL
88 148 - fixed some unicode problems with waitress
89 149 - fixed issue with escaping < and > in changeset commits
90 150 - fixed error occurring during recursive group creation in API
91 151 create_repo function
92 152 - fixed #393 py2.5 fixes for routes url generator
93 153 - fixed #397 Private repository groups shows up before login
94 154 - fixed #396 fixed problems with revoking users in nested groups
95 155 - fixed mysql unicode issues + specified InnoDB as default engine with
96 156 utf8 charset
97 157 - #406 trim long branch/tag names in changelog to not break UI
98 158
99 159 1.3.3 (**2012-03-02**)
100 160 ----------------------
101 161
102 162 news
103 163 ++++
104 164
105 165
106 166 fixes
107 167 +++++
108 168
109 169 - fixed some python2.5 compatibility issues
110 170 - fixed issues with removed repos was accidentally added as groups, after
111 171 full rescan of paths
112 172 - fixes #376 Cannot edit user (using container auth)
113 173 - fixes #378 Invalid image urls on changeset screen with proxy-prefix
114 174 configuration
115 175 - fixed initial sorting of repos inside repo group
116 176 - fixes issue when user tried to resubmit same permission into user/user_groups
117 177 - bumped beaker version that fixes #375 leap error bug
118 178 - fixed raw_changeset for git. It was generated with hg patch headers
119 179 - fixed vcs issue with last_changeset for filenodes
120 180 - fixed missing commit after hook delete
121 181 - fixed #372 issues with git operation detection that caused a security issue
122 182 for git repos
123 183
124 184 1.3.2 (**2012-02-28**)
125 185 ----------------------
126 186
127 187 news
128 188 ++++
129 189
130 190
131 191 fixes
132 192 +++++
133 193
134 194 - fixed git protocol issues with repos-groups
135 195 - fixed git remote repos validator that prevented from cloning remote git repos
136 196 - fixes #370 ending slashes fixes for repo and groups
137 197 - fixes #368 improved git-protocol detection to handle other clients
138 198 - fixes #366 When Setting Repository Group To Blank Repo Group Wont Be
139 199 Moved To Root
140 200 - fixes #371 fixed issues with beaker/sqlalchemy and non-ascii cache keys
141 201 - fixed #373 missing cascade drop on user_group_to_perm table
142 202
143 203 1.3.1 (**2012-02-27**)
144 204 ----------------------
145 205
146 206 news
147 207 ++++
148 208
149 209
150 210 fixes
151 211 +++++
152 212
153 213 - redirection loop occurs when remember-me wasn't checked during login
154 214 - fixes issues with git blob history generation
155 215 - don't fetch branch for git in file history dropdown. Causes unneeded slowness
156 216
157 217 1.3.0 (**2012-02-26**)
158 218 ----------------------
159 219
160 220 news
161 221 ++++
162 222
163 223 - code review, inspired by github code-comments
164 224 - #215 rst and markdown README files support
165 225 - #252 Container-based and proxy pass-through authentication support
166 226 - #44 branch browser. Filtering of changelog by branches
167 227 - mercurial bookmarks support
168 228 - new hover top menu, optimized to add maximum size for important views
169 229 - configurable clone url template with possibility to specify protocol like
170 230 ssh:// or http:// and also manually alter other parts of clone_url.
171 231 - enabled largefiles extension by default
172 232 - optimized summary file pages and saved a lot of unused space in them
173 233 - #239 option to manually mark repository as fork
174 234 - #320 mapping of commit authors to RhodeCode users
175 235 - #304 hashes are displayed using monospace font
176 236 - diff configuration, toggle white lines and context lines
177 237 - #307 configurable diffs, whitespace toggle, increasing context lines
178 238 - sorting on branches, tags and bookmarks using YUI datatable
179 239 - improved file filter on files page
180 240 - implements #330 api method for listing nodes ar particular revision
181 241 - #73 added linking issues in commit messages to chosen issue tracker url
182 242 based on user defined regular expression
183 243 - added linking of changesets in commit messages
184 244 - new compact changelog with expandable commit messages
185 245 - firstname and lastname are optional in user creation
186 246 - #348 added post-create repository hook
187 247 - #212 global encoding settings is now configurable from .ini files
188 248 - #227 added repository groups permissions
189 249 - markdown gets codehilite extensions
190 250 - new API methods, delete_repositories, grante/revoke permissions for groups
191 251 and repos
192 252
193 253
194 254 fixes
195 255 +++++
196 256
197 257 - rewrote dbsession management for atomic operations, and better error handling
198 258 - fixed sorting of repo tables
199 259 - #326 escape of special html entities in diffs
200 260 - normalized user_name => username in api attributes
201 261 - fixes #298 ldap created users with mixed case emails created conflicts
202 262 on saving a form
203 263 - fixes issue when owner of a repo couldn't revoke permissions for users
204 264 and groups
205 265 - fixes #271 rare JSON serialization problem with statistics
206 266 - fixes #337 missing validation check for conflicting names of a group with a
207 267 repositories group
208 268 - #340 fixed session problem for mysql and celery tasks
209 269 - fixed #331 RhodeCode mangles repository names if the a repository group
210 270 contains the "full path" to the repositories
211 271 - #355 RhodeCode doesn't store encrypted LDAP passwords
212 272
213 273 1.2.5 (**2012-01-28**)
214 274 ----------------------
215 275
216 276 news
217 277 ++++
218 278
219 279 fixes
220 280 +++++
221 281
222 282 - #340 Celery complains about MySQL server gone away, added session cleanup
223 283 for celery tasks
224 284 - #341 "scanning for repositories in None" log message during Rescan was missing
225 285 a parameter
226 286 - fixed creating archives with subrepos. Some hooks were triggered during that
227 287 operation leading to crash.
228 288 - fixed missing email in account page.
229 289 - Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
230 290 forking on windows impossible
231 291
232 292 1.2.4 (**2012-01-19**)
233 293 ----------------------
234 294
235 295 news
236 296 ++++
237 297
238 298 - RhodeCode is bundled with mercurial series 2.0.X by default, with
239 299 full support to largefiles extension. Enabled by default in new installations
240 300 - #329 Ability to Add/Remove Groups to/from a Repository via AP
241 301 - added requires.txt file with requirements
242 302
243 303 fixes
244 304 +++++
245 305
246 306 - fixes db session issues with celery when emailing admins
247 307 - #331 RhodeCode mangles repository names if the a repository group
248 308 contains the "full path" to the repositories
249 309 - #298 Conflicting e-mail addresses for LDAP and RhodeCode users
250 310 - DB session cleanup after hg protocol operations, fixes issues with
251 311 `mysql has gone away` errors
252 312 - #333 doc fixes for get_repo api function
253 313 - #271 rare JSON serialization problem with statistics enabled
254 314 - #337 Fixes issues with validation of repository name conflicting with
255 315 a group name. A proper message is now displayed.
256 316 - #292 made ldap_dn in user edit readonly, to get rid of confusion that field
257 317 doesn't work
258 318 - #316 fixes issues with web description in hgrc files
259 319
260 320 1.2.3 (**2011-11-02**)
261 321 ----------------------
262 322
263 323 news
264 324 ++++
265 325
266 326 - added option to manage repos group for non admin users
267 327 - added following API methods for get_users, create_user, get_users_groups,
268 328 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
269 329 get_repo, create_repo, add_user_to_repo
270 330 - implements #237 added password confirmation for my account
271 331 and admin edit user.
272 332 - implements #291 email notification for global events are now sent to all
273 333 administrator users, and global config email.
274 334
275 335 fixes
276 336 +++++
277 337
278 338 - added option for passing auth method for smtp mailer
279 339 - #276 issue with adding a single user with id>10 to usergroups
280 340 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
281 341 - #288 fixes managing of repos in a group for non admin user
282 342
283 343 1.2.2 (**2011-10-17**)
284 344 ----------------------
285 345
286 346 news
287 347 ++++
288 348
289 349 - #226 repo groups are available by path instead of numerical id
290 350
291 351 fixes
292 352 +++++
293 353
294 354 - #259 Groups with the same name but with different parent group
295 355 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
296 356 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
297 357 - #265 ldap save fails sometimes on converting attributes to booleans,
298 358 added getter and setter into model that will prevent from this on db model level
299 359 - fixed problems with timestamps issues #251 and #213
300 360 - fixes #266 RhodeCode allows to create repo with the same name and in
301 361 the same parent as group
302 362 - fixes #245 Rescan of the repositories on Windows
303 363 - fixes #248 cannot edit repos inside a group on windows
304 364 - fixes #219 forking problems on windows
305 365
306 366 1.2.1 (**2011-10-08**)
307 367 ----------------------
308 368
309 369 news
310 370 ++++
311 371
312 372
313 373 fixes
314 374 +++++
315 375
316 376 - fixed problems with basic auth and push problems
317 377 - gui fixes
318 378 - fixed logger
319 379
320 380 1.2.0 (**2011-10-07**)
321 381 ----------------------
322 382
323 383 news
324 384 ++++
325 385
326 386 - implemented #47 repository groups
327 387 - implemented #89 Can setup google analytics code from settings menu
328 388 - implemented #91 added nicer looking archive urls with more download options
329 389 like tags, branches
330 390 - implemented #44 into file browsing, and added follow branch option
331 391 - implemented #84 downloads can be enabled/disabled for each repository
332 392 - anonymous repository can be cloned without having to pass default:default
333 393 into clone url
334 394 - fixed #90 whoosh indexer can index chooses repositories passed in command
335 395 line
336 396 - extended journal with day aggregates and paging
337 397 - implemented #107 source code lines highlight ranges
338 398 - implemented #93 customizable changelog on combined revision ranges -
339 399 equivalent of githubs compare view
340 400 - implemented #108 extended and more powerful LDAP configuration
341 401 - implemented #56 users groups
342 402 - major code rewrites optimized codes for speed and memory usage
343 403 - raw and diff downloads are now in git format
344 404 - setup command checks for write access to given path
345 405 - fixed many issues with international characters and unicode. It uses utf8
346 406 decode with replace to provide less errors even with non utf8 encoded strings
347 407 - #125 added API KEY access to feeds
348 408 - #109 Repository can be created from external Mercurial link (aka. remote
349 409 repository, and manually updated (via pull) from admin panel
350 410 - beta git support - push/pull server + basic view for git repos
351 411 - added followers page and forks page
352 412 - server side file creation (with binary file upload interface)
353 413 and edition with commits powered by codemirror
354 414 - #111 file browser file finder, quick lookup files on whole file tree
355 415 - added quick login sliding menu into main page
356 416 - changelog uses lazy loading of affected files details, in some scenarios
357 417 this can improve speed of changelog page dramatically especially for
358 418 larger repositories.
359 419 - implements #214 added support for downloading subrepos in download menu.
360 420 - Added basic API for direct operations on rhodecode via JSON
361 421 - Implemented advanced hook management
362 422
363 423 fixes
364 424 +++++
365 425
366 426 - fixed file browser bug, when switching into given form revision the url was
367 427 not changing
368 428 - fixed propagation to error controller on simplehg and simplegit middlewares
369 429 - fixed error when trying to make a download on empty repository
370 430 - fixed problem with '[' chars in commit messages in journal
371 431 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
372 432 - journal fork fixes
373 433 - removed issue with space inside renamed repository after deletion
374 434 - fixed strange issue on formencode imports
375 435 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
376 436 - #150 fixes for errors on repositories mapped in db but corrupted in
377 437 filesystem
378 438 - fixed problem with ascendant characters in realm #181
379 439 - fixed problem with sqlite file based database connection pool
380 440 - whoosh indexer and code stats share the same dynamic extensions map
381 441 - fixes #188 - relationship delete of repo_to_perm entry on user removal
382 442 - fixes issue #189 Trending source files shows "show more" when no more exist
383 443 - fixes issue #197 Relative paths for pidlocks
384 444 - fixes issue #198 password will require only 3 chars now for login form
385 445 - fixes issue #199 wrong redirection for non admin users after creating a repository
386 446 - fixes issues #202, bad db constraint made impossible to attach same group
387 447 more than one time. Affects only mysql/postgres
388 448 - fixes #218 os.kill patch for windows was missing sig param
389 449 - improved rendering of dag (they are not trimmed anymore when number of
390 450 heads exceeds 5)
391 451
392 452 1.1.8 (**2011-04-12**)
393 453 ----------------------
394 454
395 455 news
396 456 ++++
397 457
398 458 - improved windows support
399 459
400 460 fixes
401 461 +++++
402 462
403 463 - fixed #140 freeze of python dateutil library, since new version is python2.x
404 464 incompatible
405 465 - setup-app will check for write permission in given path
406 466 - cleaned up license info issue #149
407 467 - fixes for issues #137,#116 and problems with unicode and accented characters.
408 468 - fixes crashes on gravatar, when passed in email as unicode
409 469 - fixed tooltip flickering problems
410 470 - fixed came_from redirection on windows
411 471 - fixed logging modules, and sql formatters
412 472 - windows fixes for os.kill issue #133
413 473 - fixes path splitting for windows issues #148
414 474 - fixed issue #143 wrong import on migration to 1.1.X
415 475 - fixed problems with displaying binary files, thanks to Thomas Waldmann
416 476 - removed name from archive files since it's breaking ui for long repo names
417 477 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
418 478 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
419 479 Thomas Waldmann
420 480 - fixed issue #166 summary pager was skipping 10 revisions on second page
421 481
422 482
423 483 1.1.7 (**2011-03-23**)
424 484 ----------------------
425 485
426 486 news
427 487 ++++
428 488
429 489 fixes
430 490 +++++
431 491
432 492 - fixed (again) #136 installation support for FreeBSD
433 493
434 494
435 495 1.1.6 (**2011-03-21**)
436 496 ----------------------
437 497
438 498 news
439 499 ++++
440 500
441 501 fixes
442 502 +++++
443 503
444 504 - fixed #136 installation support for FreeBSD
445 505 - RhodeCode will check for python version during installation
446 506
447 507 1.1.5 (**2011-03-17**)
448 508 ----------------------
449 509
450 510 news
451 511 ++++
452 512
453 513 - basic windows support, by exchanging pybcrypt into sha256 for windows only
454 514 highly inspired by idea of mantis406
455 515
456 516 fixes
457 517 +++++
458 518
459 519 - fixed sorting by author in main page
460 520 - fixed crashes with diffs on binary files
461 521 - fixed #131 problem with boolean values for LDAP
462 522 - fixed #122 mysql problems thanks to striker69
463 523 - fixed problem with errors on calling raw/raw_files/annotate functions
464 524 with unknown revisions
465 525 - fixed returned rawfiles attachment names with international character
466 526 - cleaned out docs, big thanks to Jason Harris
467 527
468 528 1.1.4 (**2011-02-19**)
469 529 ----------------------
470 530
471 531 news
472 532 ++++
473 533
474 534 fixes
475 535 +++++
476 536
477 537 - fixed formencode import problem on settings page, that caused server crash
478 538 when that page was accessed as first after server start
479 539 - journal fixes
480 540 - fixed option to access repository just by entering http://server/<repo_name>
481 541
482 542 1.1.3 (**2011-02-16**)
483 543 ----------------------
484 544
485 545 news
486 546 ++++
487 547
488 548 - implemented #102 allowing the '.' character in username
489 549 - added option to access repository just by entering http://server/<repo_name>
490 550 - celery task ignores result for better performance
491 551
492 552 fixes
493 553 +++++
494 554
495 555 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
496 556 apollo13 and Johan Walles
497 557 - small fixes in journal
498 558 - fixed problems with getting setting for celery from .ini files
499 559 - registration, password reset and login boxes share the same title as main
500 560 application now
501 561 - fixed #113: to high permissions to fork repository
502 562 - fixed problem with '[' chars in commit messages in journal
503 563 - removed issue with space inside renamed repository after deletion
504 564 - db transaction fixes when filesystem repository creation failed
505 565 - fixed #106 relation issues on databases different than sqlite
506 566 - fixed static files paths links to use of url() method
507 567
508 568 1.1.2 (**2011-01-12**)
509 569 ----------------------
510 570
511 571 news
512 572 ++++
513 573
514 574
515 575 fixes
516 576 +++++
517 577
518 578 - fixes #98 protection against float division of percentage stats
519 579 - fixed graph bug
520 580 - forced webhelpers version since it was making troubles during installation
521 581
522 582 1.1.1 (**2011-01-06**)
523 583 ----------------------
524 584
525 585 news
526 586 ++++
527 587
528 588 - added force https option into ini files for easier https usage (no need to
529 589 set server headers with this options)
530 590 - small css updates
531 591
532 592 fixes
533 593 +++++
534 594
535 595 - fixed #96 redirect loop on files view on repositories without changesets
536 596 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
537 597 and server crashed with errors
538 598 - fixed large tooltips problems on main page
539 599 - fixed #92 whoosh indexer is more error proof
540 600
541 601 1.1.0 (**2010-12-18**)
542 602 ----------------------
543 603
544 604 news
545 605 ++++
546 606
547 607 - rewrite of internals for vcs >=0.1.10
548 608 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
549 609 with older clients
550 610 - anonymous access, authentication via ldap
551 611 - performance upgrade for cached repos list - each repository has its own
552 612 cache that's invalidated when needed.
553 613 - performance upgrades on repositories with large amount of commits (20K+)
554 614 - main page quick filter for filtering repositories
555 615 - user dashboards with ability to follow chosen repositories actions
556 616 - sends email to admin on new user registration
557 617 - added cache/statistics reset options into repository settings
558 618 - more detailed action logger (based on hooks) with pushed changesets lists
559 619 and options to disable those hooks from admin panel
560 620 - introduced new enhanced changelog for merges that shows more accurate results
561 621 - new improved and faster code stats (based on pygments lexers mapping tables,
562 622 showing up to 10 trending sources for each repository. Additionally stats
563 623 can be disabled in repository settings.
564 624 - gui optimizations, fixed application width to 1024px
565 625 - added cut off (for large files/changesets) limit into config files
566 626 - whoosh, celeryd, upgrade moved to paster command
567 627 - other than sqlite database backends can be used
568 628
569 629 fixes
570 630 +++++
571 631
572 632 - fixes #61 forked repo was showing only after cache expired
573 633 - fixes #76 no confirmation on user deletes
574 634 - fixes #66 Name field misspelled
575 635 - fixes #72 block user removal when he owns repositories
576 636 - fixes #69 added password confirmation fields
577 637 - fixes #87 RhodeCode crashes occasionally on updating repository owner
578 638 - fixes #82 broken annotations on files with more than 1 blank line at the end
579 639 - a lot of fixes and tweaks for file browser
580 640 - fixed detached session issues
581 641 - fixed when user had no repos he would see all repos listed in my account
582 642 - fixed ui() instance bug when global hgrc settings was loaded for server
583 643 instance and all hgrc options were merged with our db ui() object
584 644 - numerous small bugfixes
585 645
586 646 (special thanks for TkSoh for detailed feedback)
587 647
588 648
589 649 1.0.2 (**2010-11-12**)
590 650 ----------------------
591 651
592 652 news
593 653 ++++
594 654
595 655 - tested under python2.7
596 656 - bumped sqlalchemy and celery versions
597 657
598 658 fixes
599 659 +++++
600 660
601 661 - fixed #59 missing graph.js
602 662 - fixed repo_size crash when repository had broken symlinks
603 663 - fixed python2.5 crashes.
604 664
605 665
606 666 1.0.1 (**2010-11-10**)
607 667 ----------------------
608 668
609 669 news
610 670 ++++
611 671
612 672 - small css updated
613 673
614 674 fixes
615 675 +++++
616 676
617 677 - fixed #53 python2.5 incompatible enumerate calls
618 678 - fixed #52 disable mercurial extension for web
619 679 - fixed #51 deleting repositories don't delete it's dependent objects
620 680
621 681
622 682 1.0.0 (**2010-11-02**)
623 683 ----------------------
624 684
625 685 - security bugfix simplehg wasn't checking for permissions on commands
626 686 other than pull or push.
627 687 - fixed doubled messages after push or pull in admin journal
628 688 - templating and css corrections, fixed repo switcher on chrome, updated titles
629 689 - admin menu accessible from options menu on repository view
630 690 - permissions cached queries
631 691
632 692 1.0.0rc4 (**2010-10-12**)
633 693 --------------------------
634 694
635 695 - fixed python2.5 missing simplejson imports (thanks to Jens Bäckman)
636 696 - removed cache_manager settings from sqlalchemy meta
637 697 - added sqlalchemy cache settings to ini files
638 698 - validated password length and added second try of failure on paster setup-app
639 699 - fixed setup database destroy prompt even when there was no db
640 700
641 701
642 702 1.0.0rc3 (**2010-10-11**)
643 703 -------------------------
644 704
645 705 - fixed i18n during installation.
646 706
647 707 1.0.0rc2 (**2010-10-11**)
648 708 -------------------------
649 709
650 710 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
651 711 occure. After vcs is fixed it'll be put back again.
652 712 - templating/css rewrites, optimized css. No newline at end of file
@@ -1,227 +1,227 b''
1 1 # -*- coding: utf-8 -*-
2 2 #
3 3 # RhodeCode documentation build configuration file, created by
4 4 # sphinx-quickstart on Sun Oct 10 16:46:37 2010.
5 5 #
6 6 # This file is execfile()d with the current directory set to its containing dir.
7 7 #
8 8 # Note that not all possible configuration values are present in this
9 9 # autogenerated file.
10 10 #
11 11 # All configuration values have a default; values that are commented out
12 12 # serve to show the default.
13 13
14 14 import sys
15 15 import os
16 16 import datetime
17 17
18 18 # If extensions (or modules to document with autodoc) are in another directory,
19 19 # add these directories to sys.path here. If the directory is relative to the
20 20 # documentation root, use os.path.abspath to make it absolute, like shown here.
21 21 sys.path.insert(0, os.path.abspath('..'))
22 22
23 23 # -- General configuration -----------------------------------------------------
24 24
25 25 # If your documentation needs a minimal Sphinx version, state it here.
26 26 #needs_sphinx = '1.0'
27 27
28 28 # Add any Sphinx extension module names here, as strings. They can be extensions
29 29 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
30 30 extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest',
31 31 'sphinx.ext.intersphinx', 'sphinx.ext.todo',
32 32 'sphinx.ext.viewcode']
33 33
34 34 # Add any paths that contain templates here, relative to this directory.
35 35 templates_path = ['_templates']
36 36
37 37 # The suffix of source filenames.
38 38 source_suffix = '.rst'
39 39
40 40 # The encoding of source files.
41 41 #source_encoding = 'utf-8-sig'
42 42
43 43 # The master toctree document.
44 44 master_doc = 'index'
45 45
46 46 # General information about the project.
47 47 project = u'RhodeCode'
48 48 copyright = u'%s, Marcin Kuzminski' % (datetime.datetime.now().year)
49 49
50 50 # The version info for the project you're documenting, acts as replacement for
51 51 # |version| and |release|, also used in various other places throughout the
52 52 # built documents.
53 53 #
54 54 # The short X.Y version.
55 55 root = os.path.dirname(os.path.dirname(__file__))
56 56 sys.path.append(root)
57 from rhodecode import get_version, __version__
58 version = get_version()
57 from rhodecode import __version__
58 version = __version__
59 59 # The full version, including alpha/beta/rc tags.
60 60 release = __version__
61 61
62 62 # The language for content autogenerated by Sphinx. Refer to documentation
63 63 # for a list of supported languages.
64 64 #language = None
65 65
66 66 # There are two options for replacing |today|: either, you set today to some
67 67 # non-false value, then it is used:
68 68 #today = ''
69 69 # Else, today_fmt is used as the format for a strftime call.
70 70 #today_fmt = '%B %d, %Y'
71 71
72 72 # List of patterns, relative to source directory, that match files and
73 73 # directories to ignore when looking for source files.
74 74 exclude_patterns = ['_build']
75 75
76 76 # The reST default role (used for this markup: `text`) to use for all documents.
77 77 #default_role = None
78 78
79 79 # If true, '()' will be appended to :func: etc. cross-reference text.
80 80 #add_function_parentheses = True
81 81
82 82 # If true, the current module name will be prepended to all description
83 83 # unit titles (such as .. function::).
84 84 #add_module_names = True
85 85
86 86 # If true, sectionauthor and moduleauthor directives will be shown in the
87 87 # output. They are ignored by default.
88 88 #show_authors = False
89 89
90 90 # The name of the Pygments (syntax highlighting) style to use.
91 91 pygments_style = 'sphinx'
92 92
93 93 # A list of ignored prefixes for module index sorting.
94 94 #modindex_common_prefix = []
95 95
96 96
97 97 # -- Options for HTML output ---------------------------------------------------
98 98
99 99 # The theme to use for HTML and HTML Help pages. See the documentation for
100 100 # a list of builtin themes.
101 101 html_theme = 'nature'
102 102
103 103 # Theme options are theme-specific and customize the look and feel of a theme
104 104 # further. For a list of options available for each theme, see the
105 105 # documentation.
106 106 #html_theme_options = {}
107 107
108 108 # Add any paths that contain custom themes here, relative to this directory.
109 109 html_theme_path = ['theme']
110 110
111 111 # The name for this set of Sphinx documents. If None, it defaults to
112 112 # "<project> v<release> documentation".
113 113 #html_title = None
114 114
115 115 # A shorter title for the navigation bar. Default is the same as html_title.
116 116 #html_short_title = None
117 117
118 118 # The name of an image file (relative to this directory) to place at the top
119 119 # of the sidebar.
120 120 #html_logo = None
121 121
122 122 # The name of an image file (within the static path) to use as favicon of the
123 123 # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
124 124 # pixels large.
125 125 #html_favicon = None
126 126
127 127 # Add any paths that contain custom static files (such as style sheets) here,
128 128 # relative to this directory. They are copied after the builtin static files,
129 129 # so a file named "default.css" will overwrite the builtin "default.css".
130 130 #html_static_path = ['_static']
131 131
132 132 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
133 133 # using the given strftime format.
134 134 #html_last_updated_fmt = '%b %d, %Y'
135 135
136 136 # If true, SmartyPants will be used to convert quotes and dashes to
137 137 # typographically correct entities.
138 138 #html_use_smartypants = True
139 139
140 140 # Custom sidebar templates, maps document names to template names.
141 141 #html_sidebars = {}
142 142
143 143 # Additional templates that should be rendered to pages, maps page names to
144 144 # template names.
145 145 #html_additional_pages = {}
146 146
147 147 # If false, no module index is generated.
148 148 #html_domain_indices = True
149 149
150 150 # If false, no index is generated.
151 151 #html_use_index = True
152 152
153 153 # If true, the index is split into individual pages for each letter.
154 154 #html_split_index = False
155 155
156 156 # If true, links to the reST sources are added to the pages.
157 157 #html_show_sourcelink = True
158 158
159 159 # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
160 160 #html_show_sphinx = True
161 161
162 162 # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
163 163 #html_show_copyright = True
164 164
165 165 # If true, an OpenSearch description file will be output, and all pages will
166 166 # contain a <link> tag referring to it. The value of this option must be the
167 167 # base URL from which the finished HTML is served.
168 168 #html_use_opensearch = ''
169 169
170 170 # This is the file name suffix for HTML files (e.g. ".xhtml").
171 171 #html_file_suffix = None
172 172
173 173 # Output file base name for HTML help builder.
174 174 htmlhelp_basename = 'RhodeCodedoc'
175 175
176 176
177 177 # -- Options for LaTeX output --------------------------------------------------
178 178
179 179 # The paper size ('letter' or 'a4').
180 180 #latex_paper_size = 'letter'
181 181
182 182 # The font size ('10pt', '11pt' or '12pt').
183 183 #latex_font_size = '10pt'
184 184
185 185 # Grouping the document tree into LaTeX files. List of tuples
186 186 # (source start file, target name, title, author, documentclass [howto/manual]).
187 187 latex_documents = [
188 188 ('index', 'RhodeCode.tex', u'RhodeCode Documentation',
189 189 u'Marcin Kuzminski', 'manual'),
190 190 ]
191 191
192 192 # The name of an image file (relative to this directory) to place at the top of
193 193 # the title page.
194 194 #latex_logo = None
195 195
196 196 # For "manual" documents, if this is true, then toplevel headings are parts,
197 197 # not chapters.
198 198 #latex_use_parts = False
199 199
200 200 # If true, show page references after internal links.
201 201 #latex_show_pagerefs = False
202 202
203 203 # If true, show URL addresses after external links.
204 204 #latex_show_urls = False
205 205
206 206 # Additional stuff for the LaTeX preamble.
207 207 #latex_preamble = ''
208 208
209 209 # Documents to append as an appendix to all manuals.
210 210 #latex_appendices = []
211 211
212 212 # If false, no module index is generated.
213 213 #latex_domain_indices = True
214 214
215 215
216 216 # -- Options for manual page output --------------------------------------------
217 217
218 218 # One entry per manual page. List of tuples
219 219 # (source start file, name, description, authors, manual section).
220 220 man_pages = [
221 221 ('index', 'rhodecode', u'RhodeCode Documentation',
222 222 [u'Marcin Kuzminski'], 1)
223 223 ]
224 224
225 225
226 226 # Example configuration for intersphinx: refer to the Python standard library.
227 227 intersphinx_mapping = {'http://docs.python.org/': None}
@@ -1,37 +1,38 b''
1 1 .. _contributing:
2 2
3 3 =========================
4 4 Contributing to RhodeCode
5 5 =========================
6 6
7 7 If you would like to contribute to RhodeCode, please contact me, any help is
8 8 greatly appreciated!
9 9
10 10 Could I request that you make your source contributions by first forking the
11 11 RhodeCode repository on bitbucket_
12 12 https://bitbucket.org/marcinkuzminski/rhodecode and then make your changes to
13 13 your forked repository. Please post all fixes into **BETA** branch since your
14 14 fix might be already fixed there and i try to merge all fixes from beta into
15 15 stable, and not the other way. Finally, when you are finished making a change,
16 16 please send me a pull request.
17 17
18 18 To run RhodeCode in a development version you always need to install the latest
19 19 required libs from `requires.txt` file.
20 20
21 21 after downloading/pulling RhodeCode make sure you run::
22 22
23 23 python setup.py develop
24 24
25 25 command to install/verify all required packages, and prepare development
26 26 enviroment.
27 27
28 28
29 29 After finishing your changes make sure all tests passes ok. You can run
30 the testsuite running nosetest from the project root.
30 the testsuite running ``nosetest`` from the project root, or if you use tox
31 run tox for python2.5-2.7 with multiple database test.
31 32
32 33 | Thank you for any contributions!
33 34 | Marcin
34 35
35 36
36 37
37 38 .. _bitbucket: http://bitbucket.org/
@@ -1,60 +1,63 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 usage/performance
25 usage/locking
24 26 usage/statistics
25 27 usage/backup
26 28 usage/debugging
29 usage/troubleshooting
27 30
28 31 **Develop**
29 32
30 33 .. toctree::
31 34 :maxdepth: 1
32 35
33 36 contributing
34 37 changelog
35 38
36 39 **API**
37 40
38 41 .. toctree::
39 42 :maxdepth: 1
40 43
41 44 api/api
42 45 api/models
43 46
44 47
45 48 Other topics
46 49 ------------
47 50
48 51 * :ref:`genindex`
49 52 * :ref:`search`
50 53
51 54 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
52 55 .. _python: http://www.python.org/
53 56 .. _django: http://www.djangoproject.com/
54 57 .. _mercurial: http://mercurial.selenic.com/
55 58 .. _bitbucket: http://bitbucket.org/
56 59 .. _subversion: http://subversion.tigris.org/
57 60 .. _git: http://git-scm.com/
58 61 .. _celery: http://celeryproject.org/
59 62 .. _Sphinx: http://sphinx.pocoo.org/
60 63 .. _vcs: http://pypi.python.org/pypi/vcs No newline at end of file
@@ -1,124 +1,129 b''
1 1 .. _installation:
2 2
3 3 ============
4 4 Installation
5 5 ============
6 6
7 7 ``RhodeCode`` is written entirely in Python. Before posting any issues make
8 8 sure, your not missing any system libraries and using right version of
9 9 libraries required by RhodeCode. There's also restriction in terms of mercurial
10 10 clients. Minimal version of hg client known working fine with RhodeCode is
11 11 **1.6**. If you're using older client, please upgrade.
12 12
13 13
14 14 Installing RhodeCode from Cheese Shop
15 15 -------------------------------------
16 16
17 17 Rhodecode requires python version 2.5 or higher.
18 18
19 19 The easiest way to install ``rhodecode`` is to run::
20 20
21 21 easy_install rhodecode
22 22
23 23 Or::
24 24
25 25 pip install rhodecode
26 26
27 27 If you prefer to install RhodeCode manually simply grab latest release from
28 http://pypi.python.org/pypi/rhodecode, decompress the archive and run::
28 http://pypi.python.org/pypi/RhodeCode, decompress the archive and run::
29 29
30 30 python setup.py install
31 31
32 Step by step installation example for Windows
33 ---------------------------------------------
32 34
33 Step by step installation example
34 ---------------------------------
35 :ref:`installation_win`
36
37
38 Step by step installation example for Linux
39 -------------------------------------------
35 40
36 41
37 42 For installing RhodeCode i highly recommend using separate virtualenv_. This
38 43 way many required by RhodeCode libraries will remain sandboxed from your main
39 44 python and making things less problematic when doing system python updates.
40 45
41 46 - Assuming you have installed virtualenv_ create a new virtual environment
42 47 using virtualenv command::
43 48
44 virtualenv --no-site-packages /var/www/rhodecode-venv
49 virtualenv --no-site-packages /opt/rhodecode-venv
45 50
46 51
47 52 .. note:: Using ``--no-site-packages`` when generating your
48 53 virtualenv is **very important**. This flag provides the necessary
49 54 isolation for running the set of packages required by
50 55 RhodeCode. If you do not specify ``--no-site-packages``,
51 56 it's possible that RhodeCode will not install properly into
52 57 the virtualenv, or, even if it does, may not run properly,
53 58 depending on the packages you've already got installed into your
54 59 Python's "main" site-packages dir.
55 60
56 61
57 - this will install new virtualenv_ into `/var/www/rhodecode-venv`.
62 - this will install new virtualenv_ into `/opt/rhodecode-venv`.
58 63 - Activate the virtualenv_ by running::
59 64
60 source /var/www/rhodecode-venv/bin/activate
65 source /opt/rhodecode-venv/bin/activate
61 66
62 67 .. note:: If you're using UNIX, *do not* use ``sudo`` to run the
63 68 ``virtualenv`` script. It's perfectly acceptable (and desirable)
64 69 to create a virtualenv as a normal user.
65 70
66 71 - Make a folder for rhodecode data files, and configuration somewhere on the
67 72 filesystem. For example::
68 73
69 mkdir /var/www/rhodecode
74 mkdir /opt/rhodecode
70 75
71 76
72 77 - Go into the created directory run this command to install rhodecode::
73 78
74 79 easy_install rhodecode
75 80
76 81 or::
77 82
78 83 pip install rhodecode
79 84
80 85 - This will install rhodecode together with pylons and all other required
81 86 python libraries into activated virtualenv
82 87
83 88 Requirements for Celery (optional)
84 89 ----------------------------------
85 90
86 91 In order to gain maximum performance
87 92 there are some third-party you must install. When RhodeCode is used
88 93 together with celery you have to install some kind of message broker,
89 94 recommended one is rabbitmq_ to make the async tasks work.
90 95
91 96 Of course RhodeCode works in sync mode also and then you do not have to install
92 97 any third party applications. However, using Celery_ will give you a large
93 98 speed improvement when using many big repositories. If you plan to use
94 99 RhodeCode for say 7 to 10 repositories, RhodeCode will perform perfectly well
95 100 without celery running.
96 101
97 102 If you make the decision to run RhodeCode with celery make sure you run
98 103 celeryd using paster and message broker together with the application.
99 104
100 105 .. note::
101 106 Installing message broker and using celery is optional, RhodeCode will
102 107 work perfectly fine without them.
103 108
104 109
105 110 **Message Broker**
106 111
107 112 - preferred is `RabbitMq <http://www.rabbitmq.com/>`_
108 113 - A possible alternative is `Redis <http://code.google.com/p/redis/>`_
109 114
110 115 For installation instructions you can visit:
111 116 http://ask.github.com/celery/getting-started/index.html.
112 117 This is a very nice tutorial on how to start using celery_ with rabbitmq_
113 118
114 119
115 120 You can now proceed to :ref:`setup`
116 121 -----------------------------------
117 122
118 123
119 124
120 125 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
121 126 .. _python: http://www.python.org/
122 127 .. _mercurial: http://mercurial.selenic.com/
123 128 .. _celery: http://celeryproject.org/
124 129 .. _rabbitmq: http://www.rabbitmq.com/ No newline at end of file
@@ -1,732 +1,715 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 setup process can be fully automated, example for lazy::
38
39 paster setup-rhodecode production.ini --user=marcink --password=secret --email=marcin@rhodecode.org --repos=/home/marcink/my_repos
40
41
37 42 - The ``setup-rhodecode`` command will create all of the needed tables and an
38 43 admin account. When choosing a root path you can either use a new empty
39 44 location, or a location which already contains existing repositories. If you
40 45 choose a location which contains existing repositories RhodeCode will simply
41 46 add all of the repositories at the chosen location to it's database.
42 47 (Note: make sure you specify the correct path to the root).
43 48 - Note: the given path for mercurial_ repositories **must** be write accessible
44 49 for the application. It's very important since the RhodeCode web interface
45 50 will work without write access, but when trying to do a push it will
46 51 eventually fail with permission denied errors unless it has write access.
47 52
48 53 You are now ready to use RhodeCode, to run it simply execute::
49 54
50 55 paster serve production.ini
51 56
52 57 - This command runs the RhodeCode server. The web app should be available at the
53 58 127.0.0.1:5000. This ip and port is configurable via the production.ini
54 59 file created in previous step
55 60 - Use the admin account you created above when running ``setup-rhodecode``
56 61 to login to the web app.
57 62 - The default permissions on each repository is read, and the owner is admin.
58 63 Remember to update these if needed.
59 64 - In the admin panel you can toggle ldap, anonymous, permissions settings. As
60 65 well as edit more advanced options on users and repositories
61 66
62 67 Optionally users can create `rcextensions` package that extends RhodeCode
63 68 functionality. To do this simply execute::
64 69
65 70 paster make-rcext production.ini
66 71
67 72 This will create `rcextensions` package in the same place that your `ini` file
68 73 lives. With `rcextensions` it's possible to add additional mapping for whoosh,
69 74 stats and add additional code into the push/pull/create repo hooks. For example
70 75 for sending signals to build-bots such as jenkins.
71 76 Please see the `__init__.py` file inside `rcextensions` package
72 77 for more details.
73 78
74 79
75 80 Using RhodeCode with SSH
76 81 ------------------------
77 82
78 83 RhodeCode currently only hosts repositories using http and https. (The addition
79 84 of ssh hosting is a planned future feature.) However you can easily use ssh in
80 85 parallel with RhodeCode. (Repository access via ssh is a standard "out of
81 86 the box" feature of mercurial_ and you can use this to access any of the
82 87 repositories that RhodeCode is hosting. See PublishingRepositories_)
83 88
84 89 RhodeCode repository structures are kept in directories with the same name
85 90 as the project. When using repository groups, each group is a subdirectory.
86 91 This allows you to easily use ssh for accessing repositories.
87 92
88 93 In order to use ssh you need to make sure that your web-server and the users
89 94 login accounts have the correct permissions set on the appropriate directories.
90 95 (Note that these permissions are independent of any permissions you have set up
91 96 using the RhodeCode web interface.)
92 97
93 98 If your main directory (the same as set in RhodeCode settings) is for example
94 99 set to **/home/hg** and the repository you are using is named `rhodecode`, then
95 100 to clone via ssh you should run::
96 101
97 102 hg clone ssh://user@server.com/home/hg/rhodecode
98 103
99 104 Using other external tools such as mercurial-server_ or using ssh key based
100 105 authentication is fully supported.
101 106
102 107 Note: In an advanced setup, in order for your ssh access to use the same
103 108 permissions as set up via the RhodeCode web interface, you can create an
104 109 authentication hook to connect to the rhodecode db and runs check functions for
105 110 permissions against that.
106 111
107 112 Setting up Whoosh full text search
108 113 ----------------------------------
109 114
110 115 Starting from version 1.1 the whoosh index can be build by using the paster
111 116 command ``make-index``. To use ``make-index`` you must specify the configuration
112 117 file that stores the location of the index. You may specify the location of the
113 118 repositories (`--repo-location`). If not specified, this value is retrieved
114 119 from the RhodeCode database. This was required prior to 1.2. Starting from
115 120 version 1.2 it is also possible to specify a comma separated list of
116 121 repositories (`--index-only`) to build index only on chooses repositories
117 122 skipping any other found in repos location
118 123
119 124 You may optionally pass the option `-f` to enable a full index rebuild. Without
120 125 the `-f` option, indexing will run always in "incremental" mode.
121 126
122 127 For an incremental index build use::
123 128
124 129 paster make-index production.ini
125 130
126 131 For a full index rebuild use::
127 132
128 133 paster make-index production.ini -f
129 134
130 135
131 136 building index just for chosen repositories is possible with such command::
132 137
133 138 paster make-index production.ini --index-only=vcs,rhodecode
134 139
135 140
136 141 In order to do periodical index builds and keep your index always up to date.
137 142 It's recommended to do a crontab entry for incremental indexing.
138 143 An example entry might look like this::
139 144
140 145 /path/to/python/bin/paster make-index /path/to/rhodecode/production.ini
141 146
142 147 When using incremental mode (the default) whoosh will check the last
143 148 modification date of each file and add it to be reindexed if a newer file is
144 149 available. The indexing daemon checks for any removed files and removes them
145 150 from index.
146 151
147 152 If you want to rebuild index from scratch, you can use the `-f` flag as above,
148 153 or in the admin panel you can check `build from scratch` flag.
149 154
150 155
151 156 Setting up LDAP support
152 157 -----------------------
153 158
154 159 RhodeCode starting from version 1.1 supports ldap authentication. In order
155 160 to use LDAP, you have to install the python-ldap_ package. This package is
156 161 available via pypi, so you can install it by running
157 162
158 163 using easy_install::
159 164
160 165 easy_install python-ldap
161 166
162 167 using pip::
163 168
164 169 pip install python-ldap
165 170
166 171 .. note::
167 172 python-ldap requires some certain libs on your system, so before installing
168 173 it check that you have at least `openldap`, and `sasl` libraries.
169 174
170 175 LDAP settings are located in admin->ldap section,
171 176
172 177 Here's a typical ldap setup::
173 178
174 179 Connection settings
175 180 Enable LDAP = checked
176 181 Host = host.example.org
177 182 Port = 389
178 183 Account = <account>
179 184 Password = <password>
180 185 Connection Security = LDAPS connection
181 186 Certificate Checks = DEMAND
182 187
183 188 Search settings
184 189 Base DN = CN=users,DC=host,DC=example,DC=org
185 190 LDAP Filter = (&(objectClass=user)(!(objectClass=computer)))
186 191 LDAP Search Scope = SUBTREE
187 192
188 193 Attribute mappings
189 194 Login Attribute = uid
190 195 First Name Attribute = firstName
191 196 Last Name Attribute = lastName
192 197 E-mail Attribute = mail
193 198
194 199 .. _enable_ldap:
195 200
196 201 Enable LDAP : required
197 202 Whether to use LDAP for authenticating users.
198 203
199 204 .. _ldap_host:
200 205
201 206 Host : required
202 207 LDAP server hostname or IP address.
203 208
204 209 .. _Port:
205 210
206 211 Port : required
207 212 389 for un-encrypted LDAP, 636 for SSL-encrypted LDAP.
208 213
209 214 .. _ldap_account:
210 215
211 216 Account : optional
212 217 Only required if the LDAP server does not allow anonymous browsing of
213 218 records. This should be a special account for record browsing. This
214 219 will require `LDAP Password`_ below.
215 220
216 221 .. _LDAP Password:
217 222
218 223 Password : optional
219 224 Only required if the LDAP server does not allow anonymous browsing of
220 225 records.
221 226
222 227 .. _Enable LDAPS:
223 228
224 229 Connection Security : required
225 230 Defines the connection to LDAP server
226 231
227 232 No encryption
228 233 Plain non encrypted connection
229 234
230 235 LDAPS connection
231 236 Enable ldaps connection. It will likely require `Port`_ to be set to
232 237 a different value (standard LDAPS port is 636). When LDAPS is enabled
233 238 then `Certificate Checks`_ is required.
234 239
235 240 START_TLS on LDAP connection
236 241 START TLS connection
237 242
238 243 .. _Certificate Checks:
239 244
240 245 Certificate Checks : optional
241 246 How SSL certificates verification is handled - this is only useful when
242 247 `Enable LDAPS`_ is enabled. Only DEMAND or HARD offer full SSL security
243 248 while the other options are susceptible to man-in-the-middle attacks. SSL
244 249 certificates can be installed to /etc/openldap/cacerts so that the
245 250 DEMAND or HARD options can be used with self-signed certificates or
246 251 certificates that do not have traceable certificates of authority.
247 252
248 253 NEVER
249 254 A serve certificate will never be requested or checked.
250 255
251 256 ALLOW
252 257 A server certificate is requested. Failure to provide a
253 258 certificate or providing a bad certificate will not terminate the
254 259 session.
255 260
256 261 TRY
257 262 A server certificate is requested. Failure to provide a
258 263 certificate does not halt the session; providing a bad certificate
259 264 halts the session.
260 265
261 266 DEMAND
262 267 A server certificate is requested and must be provided and
263 268 authenticated for the session to proceed.
264 269
265 270 HARD
266 271 The same as DEMAND.
267 272
268 273 .. _Base DN:
269 274
270 275 Base DN : required
271 276 The Distinguished Name (DN) where searches for users will be performed.
272 277 Searches can be controlled by `LDAP Filter`_ and `LDAP Search Scope`_.
273 278
274 279 .. _LDAP Filter:
275 280
276 281 LDAP Filter : optional
277 282 A LDAP filter defined by RFC 2254. This is more useful when `LDAP
278 283 Search Scope`_ is set to SUBTREE. The filter is useful for limiting
279 284 which LDAP objects are identified as representing Users for
280 285 authentication. The filter is augmented by `Login Attribute`_ below.
281 286 This can commonly be left blank.
282 287
283 288 .. _LDAP Search Scope:
284 289
285 290 LDAP Search Scope : required
286 291 This limits how far LDAP will search for a matching object.
287 292
288 293 BASE
289 294 Only allows searching of `Base DN`_ and is usually not what you
290 295 want.
291 296
292 297 ONELEVEL
293 298 Searches all entries under `Base DN`_, but not Base DN itself.
294 299
295 300 SUBTREE
296 301 Searches all entries below `Base DN`_, but not Base DN itself.
297 302 When using SUBTREE `LDAP Filter`_ is useful to limit object
298 303 location.
299 304
300 305 .. _Login Attribute:
301 306
302 307 Login Attribute : required
303 308 The LDAP record attribute that will be matched as the USERNAME or
304 309 ACCOUNT used to connect to RhodeCode. This will be added to `LDAP
305 310 Filter`_ for locating the User object. If `LDAP Filter`_ is specified as
306 311 "LDAPFILTER", `Login Attribute`_ is specified as "uid" and the user has
307 312 connected as "jsmith" then the `LDAP Filter`_ will be augmented as below
308 313 ::
309 314
310 315 (&(LDAPFILTER)(uid=jsmith))
311 316
312 317 .. _ldap_attr_firstname:
313 318
314 319 First Name Attribute : required
315 320 The LDAP record attribute which represents the user's first name.
316 321
317 322 .. _ldap_attr_lastname:
318 323
319 324 Last Name Attribute : required
320 325 The LDAP record attribute which represents the user's last name.
321 326
322 327 .. _ldap_attr_email:
323 328
324 329 Email Attribute : required
325 330 The LDAP record attribute which represents the user's email address.
326 331
327 332 If all data are entered correctly, and python-ldap_ is properly installed
328 333 users should be granted access to RhodeCode with ldap accounts. At this
329 334 time user information is copied from LDAP into the RhodeCode user database.
330 335 This means that updates of an LDAP user object may not be reflected as a
331 336 user update in RhodeCode.
332 337
333 338 If You have problems with LDAP access and believe You entered correct
334 339 information check out the RhodeCode logs, any error messages sent from LDAP
335 340 will be saved there.
336 341
337 342 Active Directory
338 343 ''''''''''''''''
339 344
340 345 RhodeCode can use Microsoft Active Directory for user authentication. This
341 346 is done through an LDAP or LDAPS connection to Active Directory. The
342 347 following LDAP configuration settings are typical for using Active
343 348 Directory ::
344 349
345 350 Base DN = OU=SBSUsers,OU=Users,OU=MyBusiness,DC=v3sys,DC=local
346 351 Login Attribute = sAMAccountName
347 352 First Name Attribute = givenName
348 353 Last Name Attribute = sn
349 354 E-mail Attribute = mail
350 355
351 356 All other LDAP settings will likely be site-specific and should be
352 357 appropriately configured.
353 358
354 359
355 360 Authentication by container or reverse-proxy
356 361 --------------------------------------------
357 362
358 363 Starting with version 1.3, RhodeCode supports delegating the authentication
359 364 of users to its WSGI container, or to a reverse-proxy server through which all
360 365 clients access the application.
361 366
362 367 When these authentication methods are enabled in RhodeCode, it uses the
363 368 username that the container/proxy (Apache/Nginx/etc) authenticated and doesn't
364 369 perform the authentication itself. The authorization, however, is still done by
365 370 RhodeCode according to its settings.
366 371
367 372 When a user logs in for the first time using these authentication methods,
368 373 a matching user account is created in RhodeCode with default permissions. An
369 374 administrator can then modify it using RhodeCode's admin interface.
370 375 It's also possible for an administrator to create accounts and configure their
371 376 permissions before the user logs in for the first time.
372 377
373 378 Container-based authentication
374 379 ''''''''''''''''''''''''''''''
375 380
376 381 In a container-based authentication setup, RhodeCode reads the user name from
377 382 the ``REMOTE_USER`` server variable provided by the WSGI container.
378 383
379 384 After setting up your container (see `Apache's WSGI config`_), you'd need
380 385 to configure it to require authentication on the location configured for
381 386 RhodeCode.
382 387
383 388 In order for RhodeCode to start using the provided username, you should set the
384 389 following in the [app:main] section of your .ini file::
385 390
386 391 container_auth_enabled = true
387 392
388 393
389 394 Proxy pass-through authentication
390 395 '''''''''''''''''''''''''''''''''
391 396
392 397 In a proxy pass-through authentication setup, RhodeCode reads the user name
393 398 from the ``X-Forwarded-User`` request header, which should be configured to be
394 399 sent by the reverse-proxy server.
395 400
396 401 After setting up your proxy solution (see `Apache virtual host reverse proxy example`_,
397 402 `Apache as subdirectory`_ or `Nginx virtual host example`_), you'd need to
398 403 configure the authentication and add the username in a request header named
399 404 ``X-Forwarded-User``.
400 405
401 406 For example, the following config section for Apache sets a subdirectory in a
402 407 reverse-proxy setup with basic auth::
403 408
404 409 <Location /<someprefix> >
405 410 ProxyPass http://127.0.0.1:5000/<someprefix>
406 411 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
407 412 SetEnvIf X-Url-Scheme https HTTPS=1
408 413
409 414 AuthType Basic
410 415 AuthName "RhodeCode authentication"
411 416 AuthUserFile /home/web/rhodecode/.htpasswd
412 417 require valid-user
413 418
414 419 RequestHeader unset X-Forwarded-User
415 420
416 421 RewriteEngine On
417 422 RewriteCond %{LA-U:REMOTE_USER} (.+)
418 423 RewriteRule .* - [E=RU:%1]
419 424 RequestHeader set X-Forwarded-User %{RU}e
420 425 </Location>
421 426
422 427 In order for RhodeCode to start using the forwarded username, you should set
423 428 the following in the [app:main] section of your .ini file::
424 429
425 430 proxypass_auth_enabled = true
426 431
427 432 .. note::
428 433 If you enable proxy pass-through authentication, make sure your server is
429 434 only accessible through the proxy. Otherwise, any client would be able to
430 435 forge the authentication header and could effectively become authenticated
431 436 using any account of their liking.
432 437
433 438 Integration with Issue trackers
434 439 -------------------------------
435 440
436 441 RhodeCode provides a simple integration with issue trackers. It's possible
437 442 to define a regular expression that will fetch issue id stored in commit
438 443 messages and replace that with an url to this issue. To enable this simply
439 444 uncomment following variables in the ini file::
440 445
441 446 url_pat = (?:^#|\s#)(\w+)
442 447 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
443 448 issue_prefix = #
444 449
445 450 `url_pat` is the regular expression that will fetch issues from commit messages.
446 451 Default regex will match issues in format of #<number> eg. #300.
447 452
448 453 Matched issues will be replace with the link specified as `issue_server_link`
449 454 {id} will be replaced with issue id, and {repo} with repository name.
450 455 Since the # is striped `issue_prefix` is added as a prefix to url.
451 456 `issue_prefix` can be something different than # if you pass
452 457 ISSUE- as issue prefix this will generate an url in format::
453 458
454 459 <a href="https://myissueserver.com/example_repo/issue/300">ISSUE-300</a>
455 460
456 461 Hook management
457 462 ---------------
458 463
459 464 Hooks can be managed in similar way to this used in .hgrc files.
460 465 To access hooks setting click `advanced setup` on Hooks section of Mercurial
461 466 Settings in Admin.
462 467
463 468 There are 4 built in hooks that cannot be changed (only enable/disable by
464 469 checkboxes on previos section).
465 470 To add another custom hook simply fill in first section with
466 471 <name>.<hook_type> and the second one with hook path. Example hooks
467 472 can be found at *rhodecode.lib.hooks*.
468 473
469 474
470 475 Changing default encoding
471 476 -------------------------
472 477
473 478 By default RhodeCode uses utf8 encoding, starting from 1.3 series this
474 479 can be changed, simply edit default_encoding in .ini file to desired one.
475 480 This affects many parts in rhodecode including commiters names, filenames,
476 481 encoding of commit messages. In addition RhodeCode can detect if `chardet`
477 482 library is installed. If `chardet` is detected RhodeCode will fallback to it
478 483 when there are encode/decode errors.
479 484
480 485
481 486 Setting Up Celery
482 487 -----------------
483 488
484 489 Since version 1.1 celery is configured by the rhodecode ini configuration files.
485 490 Simply set use_celery=true in the ini file then add / change the configuration
486 491 variables inside the ini file.
487 492
488 493 Remember that the ini files use the format with '.' not with '_' like celery.
489 494 So for example setting `BROKER_HOST` in celery means setting `broker.host` in
490 495 the config file.
491 496
492 497 In order to start using celery run::
493 498
494 499 paster celeryd <configfile.ini>
495 500
496 501
497 502 .. note::
498 503 Make sure you run this command from the same virtualenv, and with the same
499 504 user that rhodecode runs.
500 505
501 506 HTTPS support
502 507 -------------
503 508
504 509 There are two ways to enable https:
505 510
506 511 - Set HTTP_X_URL_SCHEME in your http server headers, than rhodecode will
507 512 recognize this headers and make proper https redirections
508 513 - Alternatively, change the `force_https = true` flag in the ini configuration
509 514 to force using https, no headers are needed than to enable https
510 515
511 516
512 517 Nginx virtual host example
513 518 --------------------------
514 519
515 520 Sample config for nginx using proxy::
516 521
517 522 upstream rc {
518 523 server 127.0.0.1:5000;
519 524 # add more instances for load balancing
520 525 #server 127.0.0.1:5001;
521 526 #server 127.0.0.1:5002;
522 527 }
523 528
524 529 server {
525 530 listen 80;
526 531 server_name hg.myserver.com;
527 532 access_log /var/log/nginx/rhodecode.access.log;
528 533 error_log /var/log/nginx/rhodecode.error.log;
529 534
535 # uncomment if you have nginx with chunking module compiled
536 # fixes the issues of having to put postBuffer data for large git
537 # pushes
538 #chunkin on;
539 #error_page 411 = @my_411_error;
540 #location @my_411_error {
541 # chunkin_resume;
542 #}
543
544 # uncomment if you want to serve static files by nginx
545 #root /path/to/installation/rhodecode/public;
546
530 547 location / {
531 548 try_files $uri @rhode;
532 549 }
533 550
534 551 location @rhode {
535 552 proxy_pass http://rc;
536 553 include /etc/nginx/proxy.conf;
537 554 }
538 555
539 556 }
540 557
541 558 Here's the proxy.conf. It's tuned so it will not timeout on long
542 559 pushes or large pushes::
543 560
544 561 proxy_redirect off;
545 562 proxy_set_header Host $host;
546 563 proxy_set_header X-Url-Scheme $scheme;
547 564 proxy_set_header X-Host $http_host;
548 565 proxy_set_header X-Real-IP $remote_addr;
549 566 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
550 567 proxy_set_header Proxy-host $proxy_host;
551 568 client_max_body_size 400m;
552 569 client_body_buffer_size 128k;
553 570 proxy_buffering off;
554 571 proxy_connect_timeout 7200;
555 572 proxy_send_timeout 7200;
556 573 proxy_read_timeout 7200;
557 574 proxy_buffers 8 32k;
558 575
559 576 Also, when using root path with nginx you might set the static files to false
560 577 in the production.ini file::
561 578
562 579 [app:main]
563 580 use = egg:rhodecode
564 581 full_stack = true
565 582 static_files = false
566 583 lang=en
567 584 cache_dir = %(here)s/data
568 585
569 586 In order to not have the statics served by the application. This improves speed.
570 587
571 588
572 589 Apache virtual host reverse proxy example
573 590 -----------------------------------------
574 591
575 592 Here is a sample configuration file for apache using proxy::
576 593
577 594 <VirtualHost *:80>
578 595 ServerName hg.myserver.com
579 596 ServerAlias hg.myserver.com
580 597
581 598 <Proxy *>
582 599 Order allow,deny
583 600 Allow from all
584 601 </Proxy>
585 602
586 603 #important !
587 604 #Directive to properly generate url (clone url) for pylons
588 605 ProxyPreserveHost On
589 606
590 607 #rhodecode instance
591 608 ProxyPass / http://127.0.0.1:5000/
592 609 ProxyPassReverse / http://127.0.0.1:5000/
593 610
594 611 #to enable https use line below
595 612 #SetEnvIf X-Url-Scheme https HTTPS=1
596 613
597 614 </VirtualHost>
598 615
599 616
600 617 Additional tutorial
601 618 http://wiki.pylonshq.com/display/pylonscookbook/Apache+as+a+reverse+proxy+for+Pylons
602 619
603 620
604 621 Apache as subdirectory
605 622 ----------------------
606 623
607 624 Apache subdirectory part::
608 625
609 626 <Location /<someprefix> >
610 627 ProxyPass http://127.0.0.1:5000/<someprefix>
611 628 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
612 629 SetEnvIf X-Url-Scheme https HTTPS=1
613 630 </Location>
614 631
615 632 Besides the regular apache setup you will need to add the following line
616 633 into [app:main] section of your .ini file::
617 634
618 635 filter-with = proxy-prefix
619 636
620 637 Add the following at the end of the .ini file::
621 638
622 639 [filter:proxy-prefix]
623 640 use = egg:PasteDeploy#prefix
624 641 prefix = /<someprefix>
625 642
626 643
627 644 then change <someprefix> into your choosen prefix
628 645
629 646 Apache's WSGI config
630 647 --------------------
631 648
632 649 Alternatively, RhodeCode can be set up with Apache under mod_wsgi. For
633 650 that, you'll need to:
634 651
635 652 - Install mod_wsgi. If using a Debian-based distro, you can install
636 653 the package libapache2-mod-wsgi::
637 654
638 655 aptitude install libapache2-mod-wsgi
639 656
640 657 - Enable mod_wsgi::
641 658
642 659 a2enmod wsgi
643 660
644 661 - Create a wsgi dispatch script, like the one below. Make sure you
645 662 check the paths correctly point to where you installed RhodeCode
646 663 and its Python Virtual Environment.
647 664 - Enable the WSGIScriptAlias directive for the wsgi dispatch script,
648 665 as in the following example. Once again, check the paths are
649 666 correctly specified.
650 667
651 668 Here is a sample excerpt from an Apache Virtual Host configuration file::
652 669
653 670 WSGIDaemonProcess pylons user=www-data group=www-data processes=1 \
654 671 threads=4 \
655 672 python-path=/home/web/rhodecode/pyenv/lib/python2.6/site-packages
656 673 WSGIScriptAlias / /home/web/rhodecode/dispatch.wsgi
657 674 WSGIPassAuthorization On
658 675
659 676 Example wsgi dispatch script::
660 677
661 678 import os
662 679 os.environ["HGENCODING"] = "UTF-8"
663 680 os.environ['PYTHON_EGG_CACHE'] = '/home/web/rhodecode/.egg-cache'
664 681
665 682 # sometimes it's needed to set the curent dir
666 683 os.chdir('/home/web/rhodecode/')
667 684
668 685 import site
669 686 site.addsitedir("/home/web/rhodecode/pyenv/lib/python2.6/site-packages")
670 687
671 688 from paste.deploy import loadapp
672 689 from paste.script.util.logging_config import fileConfig
673 690
674 691 fileConfig('/home/web/rhodecode/production.ini')
675 692 application = loadapp('config:/home/web/rhodecode/production.ini')
676 693
677 694 Note: when using mod_wsgi you'll need to install the same version of
678 695 Mercurial that's inside RhodeCode's virtualenv also on the system's Python
679 696 environment.
680 697
681 698
682 699 Other configuration files
683 700 -------------------------
684 701
685 Some example init.d scripts can be found here, for debian and gentoo:
686
687 https://rhodecode.org/rhodecode/files/tip/init.d
688
689
690 Troubleshooting
691 ---------------
692
693 :Q: **Missing static files?**
694 :A: Make sure either to set the `static_files = true` in the .ini file or
695 double check the root path for your http setup. It should point to
696 for example:
697 /home/my-virtual-python/lib/python2.6/site-packages/rhodecode/public
698
699 |
700
701 :Q: **Can't install celery/rabbitmq**
702 :A: Don't worry RhodeCode works without them too. No extra setup is required.
702 Some example init.d scripts can be found in init.d directory::
703 703
704 |
705
706 :Q: **Long lasting push timeouts?**
707 :A: Make sure you set a longer timeouts in your proxy/fcgi settings, timeouts
708 are caused by https server and not RhodeCode.
709
710 |
711
712 :Q: **Large pushes timeouts?**
713 :A: Make sure you set a proper max_body_size for the http server.
714
715 |
716
717 :Q: **Apache doesn't pass basicAuth on pull/push?**
718 :A: Make sure you added `WSGIPassAuthorization true`.
719
720 For further questions search the `Issues tracker`_, or post a message in the
721 `google group rhodecode`_
704 https://secure.rhodecode.org/rhodecode/files/beta/init.d
722 705
723 706 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
724 707 .. _python: http://www.python.org/
725 708 .. _mercurial: http://mercurial.selenic.com/
726 709 .. _celery: http://celeryproject.org/
727 710 .. _rabbitmq: http://www.rabbitmq.com/
728 711 .. _python-ldap: http://www.python-ldap.org/
729 712 .. _mercurial-server: http://www.lshift.net/mercurial-server.html
730 713 .. _PublishingRepositories: http://mercurial.selenic.com/wiki/PublishingRepositories
731 714 .. _Issues tracker: https://bitbucket.org/marcinkuzminski/rhodecode/issues
732 .. _google group rhodecode: http://groups.google.com/group/rhodecode
715 .. _google group rhodecode: http://groups.google.com/group/rhodecode No newline at end of file
@@ -1,85 +1,107 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)
17 17
18 18 Follow current branch in file view
19 19 ----------------------------------
20 20
21 21 In file view when this checkbox is checked the << and >> arrows will jump
22 22 to changesets within the same branch currently viewing. So for example
23 23 if someone is viewing files at 'beta' branch and marks `follow current branch`
24 24 checkbox the << and >> buttons will only show him revisions for 'beta' branch
25 25
26 26
27 27 Compare view from changelog
28 28 ---------------------------
29 29
30 30 Checkboxes in compare view allow users to view combined compare view. You can
31 31 only show the range between the first and last checkbox (no cherry pick).
32 32 Clicking more than one checkbox will activate a link in top saying
33 33 `Show selected changes <from-rev> -> <to-rev>` clicking this will bring
34 34 compare view
35 35
36 36 Compare view is also available from the journal on pushes having more than
37 37 one changeset
38 38
39 39
40 40 Non changeable repository urls
41 41 ------------------------------
42 42
43 43 Due to complicated nature of repository grouping, often urls of repositories
44 44 can change.
45 45
46 46 example::
47 47
48 48 #before
49 49 http://server.com/repo_name
50 50 # after insertion to test_group group the url will be
51 51 http://server.com/test_group/repo_name
52 52
53 53 This can be an issue for build systems and any other hardcoded scripts, moving
54 54 repository to a group leads to a need for changing external systems. To
55 55 overcome this RhodeCode introduces a non changable replacement url. It's
56 56 simply an repository ID prefixed with `_` above urls are also accessible as::
57 57
58 58 http://server.com/_<ID>
59 59
60 60 Since ID are always the same moving the repository will not affect such url.
61 61 the _<ID> syntax can be used anywhere in the system so urls with repo_name
62 62 for changelogs, files and other can be exchanged with _<ID> syntax.
63 63
64 64
65
66 65 Mailing
67 66 -------
68 67
69 68 When administrator will fill up the mailing settings in .ini files
70 69 RhodeCode will send mails on user registration, or when RhodeCode errors occur
71 70 on errors the mails will have a detailed traceback of error.
72 71
73 72
74 73 Mails are also sent for code comments. If someone comments on a changeset
75 74 mail is sent to all participants, the person who commited the changeset
76 75 (if present in RhodeCode), and to all people mentioned with @mention system.
77 76
78 77
79 78 Trending source files
80 79 ---------------------
81 80
82 81 Trending source files are calculated based on pre defined dict of known
83 82 types and extensions. If You miss some extension or Would like to scan some
84 83 custom files it's possible to add new types in `LANGUAGES_EXTENSIONS_MAP` dict
85 located in `/rhodecode/lib/celerylib/tasks.py` No newline at end of file
84 located in `/rhodecode/lib/celerylib/tasks.py`
85
86
87 Cloning remote repositories
88 ---------------------------
89
90 RhodeCode has an ability to clone remote repos from given remote locations.
91 Currently it support following options:
92
93 - hg -> hg clone
94 - svn -> hg clone
95 - git -> git clone
96
97
98 .. note::
99
100 - *`svn -> hg` cloning requires `hgsubversion` library to be installed.*
101
102 If you need to clone repositories that are protected via basic auth, you
103 might pass the url with stored credentials inside eg.
104 `http://user:passw@remote.server/repo, RhodeCode will try to login and clone
105 using given credentials. Please take a note that they will be stored as
106 plaintext inside the database. RhodeCode will remove auth info when showing the
107 clone url in summary page.
@@ -1,49 +1,56 b''
1 1 .. _git_support:
2 2
3 3 ===========
4 4 GIT support
5 5 ===========
6 6
7 7
8 Git support in RhodeCode 1.3 was enabled by default.
8 Git support in RhodeCode 1.3 was enabled by default. You need to have a git
9 client installed on the machine to make git fully work.
10
9 11 Although There are some limitations on git usage.
10 12
11 - No hooks are runned for git push/pull actions.
12 - logs in action journals don't have git operations
13 - hooks that are executed on pull/push are not *real* hooks, they are
14 just emulating the behavior, and are executed **BEFORE** action takes place.
13 15 - large pushes needs http server with chunked encoding support.
14 16
15 17 if you plan to use git you need to run RhodeCode with some
16 18 http server that supports chunked encoding which git http protocol uses,
17 19 i recommend using waitress_ or gunicorn_ (linux only) for `paste` wsgi app
18 20 replacement.
19 21
20 To use waitress simply change change the following in the .ini file::
22 To use, simply change change the following in the .ini file::
21 23
22 24 use = egg:Paste#http
23 25
24 To::
26 to::
25 27
26 28 use = egg:waitress#main
27 29
30 or::
31
32 use = egg:gunicorn#main
33
34
28 35 And comment out bellow options::
29 36
30 37 threadpool_workers =
31 38 threadpool_max_requests =
32 39 use_threadpool =
33 40
34 41
35 42 You can simply run `paster serve` as usual.
36 43
37 44
38 45 You can always disable git/hg support by editing a
39 46 file **rhodecode/__init__.py** and commenting out backends
40 47
41 48 .. code-block:: python
42 49
43 50 BACKENDS = {
44 51 'hg': 'Mercurial repository',
45 52 #'git': 'Git repository',
46 53 }
47 54
48 55 .. _waitress: http://pypi.python.org/pypi/waitress
49 56 .. _gunicorn: http://pypi.python.org/pypi/gunicorn No newline at end of file
@@ -1,306 +1,329 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 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 ##nr of threads to spawn
33 threadpool_workers = 5
33 #threadpool_workers = 5
34 34
35 35 ##max request before thread respawn
36 threadpool_max_requests = 10
36 #threadpool_max_requests = 10
37 37
38 38 ##option to use threads of process
39 use_threadpool = true
39 #use_threadpool = true
40 40
41 use = egg:Paste#http
41 #use = egg:Paste#http
42 use = egg:waitress#main
42 43 host = 127.0.0.1
43 44 port = 8001
44 45
46 [filter:proxy-prefix]
47 # prefix middleware for rc
48 use = egg:PasteDeploy#prefix
49 prefix = /<your-prefix>
50
45 51 [app:main]
46 52 use = egg:rhodecode
53 #filter-with = proxy-prefix
47 54 full_stack = true
48 55 static_files = true
56 # Optional Languages
57 # en, fr, ja, pt_BR, zh_CN, zh_TW
49 58 lang = en
50 59 cache_dir = %(here)s/data
51 60 index_dir = %(here)s/data/index
52 61 app_instance_uuid = rc-production
53 62 cut_off_limit = 256000
54 63 force_https = false
55 64 commit_parse_limit = 50
56 65 use_gravatar = true
66
67 ## alternative_gravatar_url allows you to use your own avatar server application
68 ## the following parts of the URL will be replaced
69 ## {email} user email
70 ## {md5email} md5 hash of the user email (like at gravatar.com)
71 ## {size} size of the image that is expected from the server application
72 #alternative_gravatar_url = http://myavatarserver.com/getbyemail/{email}/{size}
73 #alternative_gravatar_url = http://myavatarserver.com/getbymd5/{md5email}?s={size}
74
57 75 container_auth_enabled = false
58 76 proxypass_auth_enabled = false
59 77 default_encoding = utf8
60 78
61 79 ## overwrite schema of clone url
62 80 ## available vars:
63 81 ## scheme - http/https
64 82 ## user - current user
65 83 ## pass - password
66 84 ## netloc - network location
67 85 ## path - usually repo_name
68 86
69 87 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
70 88
71 89 ## issue tracking mapping for commits messages
72 90 ## comment out issue_pat, issue_server, issue_prefix to enable
73 91
74 92 ## pattern to get the issues from commit messages
75 93 ## default one used here is #<numbers> with a regex passive group for `#`
76 94 ## {id} will be all groups matched from this pattern
77 95
78 96 issue_pat = (?:\s*#)(\d+)
79 97
80 98 ## server url to the issue, each {id} will be replaced with match
81 ## fetched from the regex and {repo} is replaced with repository name
99 ## fetched from the regex and {repo} is replaced with full repository name
100 ## including groups {repo_name} is replaced with just name of repo
82 101
83 102 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
84 103
85 104 ## prefix to add to link to indicate it's an url
86 105 ## #314 will be replaced by <issue_prefix><id>
87 106
88 107 issue_prefix = #
89 108
90 109 ## instance-id prefix
91 110 ## a prefix key for this instance used for cache invalidation when running
92 111 ## multiple instances of rhodecode, make sure it's globally unique for
93 112 ## all running rhodecode instances. Leave empty if you don't use it
94 113 instance_id =
95 114
96 115 ## alternative return HTTP header for failed authentication. Default HTTP
97 116 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
98 117 ## handling that. Set this variable to 403 to return HTTPForbidden
99 118 auth_ret_code =
100 119
101 120 ####################################
102 121 ### CELERY CONFIG ####
103 122 ####################################
104 123 use_celery = false
105 124 broker.host = localhost
106 125 broker.vhost = rabbitmqhost
107 126 broker.port = 5672
108 127 broker.user = rabbitmq
109 128 broker.password = qweqwe
110 129
111 130 celery.imports = rhodecode.lib.celerylib.tasks
112 131
113 132 celery.result.backend = amqp
114 133 celery.result.dburi = amqp://
115 134 celery.result.serialier = json
116 135
117 136 #celery.send.task.error.emails = true
118 137 #celery.amqp.task.result.expires = 18000
119 138
120 139 celeryd.concurrency = 2
121 140 #celeryd.log.file = celeryd.log
122 141 celeryd.log.level = debug
123 142 celeryd.max.tasks.per.child = 1
124 143
125 144 #tasks will never be sent to the queue, but executed locally instead.
126 145 celery.always.eager = false
127 146
128 147 ####################################
129 148 ### BEAKER CACHE ####
130 149 ####################################
131 150 beaker.cache.data_dir=%(here)s/data/cache/data
132 151 beaker.cache.lock_dir=%(here)s/data/cache/lock
133 152
134 153 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
135 154
136 155 beaker.cache.super_short_term.type=memory
137 156 beaker.cache.super_short_term.expire=10
138 157 beaker.cache.super_short_term.key_length = 256
139 158
140 159 beaker.cache.short_term.type=memory
141 160 beaker.cache.short_term.expire=60
142 161 beaker.cache.short_term.key_length = 256
143 162
144 163 beaker.cache.long_term.type=memory
145 164 beaker.cache.long_term.expire=36000
146 165 beaker.cache.long_term.key_length = 256
147 166
148 167 beaker.cache.sql_cache_short.type=memory
149 168 beaker.cache.sql_cache_short.expire=10
150 169 beaker.cache.sql_cache_short.key_length = 256
151 170
152 171 beaker.cache.sql_cache_med.type=memory
153 172 beaker.cache.sql_cache_med.expire=360
154 173 beaker.cache.sql_cache_med.key_length = 256
155 174
156 175 beaker.cache.sql_cache_long.type=file
157 176 beaker.cache.sql_cache_long.expire=3600
158 177 beaker.cache.sql_cache_long.key_length = 256
159 178
160 179 ####################################
161 180 ### BEAKER SESSION ####
162 181 ####################################
163 182 ## Type of storage used for the session, current types are
164 183 ## dbm, file, memcached, database, and memory.
165 184 ## The storage uses the Container API
166 185 ## that is also used by the cache system.
167 186
168 ## db session example
169
187 ## db session ##
170 188 #beaker.session.type = ext:database
171 189 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
172 190 #beaker.session.table_name = db_session
173 191
174 ## encrypted cookie session, good for many instances
192 ## encrypted cookie client side session, good for many instances ##
175 193 #beaker.session.type = cookie
176 194
177 beaker.session.type = file
195 ## file based cookies (default) ##
196 #beaker.session.type = file
197
198
178 199 beaker.session.key = rhodecode
179 # secure cookie requires AES python libraries
200 ## secure cookie requires AES python libraries ##
180 201 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
181 202 #beaker.session.validate_key = 9712sds2212c--zxc123
182 beaker.session.timeout = 36000
203 ## sets session as invalid if it haven't been accessed for given amount of time
204 beaker.session.timeout = 2592000
183 205 beaker.session.httponly = true
206 #beaker.session.cookie_path = /<your-prefix>
184 207
185 ## uncomment for https secure cookie
208 ## uncomment for https secure cookie ##
186 209 beaker.session.secure = false
187 210
188 ##auto save the session to not to use .save()
211 ## auto save the session to not to use .save() ##
189 212 beaker.session.auto = False
190 213
191 ##true exire at browser close
214 ## default cookie expiration time in seconds `true` expire at browser close ##
192 215 #beaker.session.cookie_expires = 3600
193 216
194 217
195 218 ################################################################################
196 219 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
197 220 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
198 221 ## execute malicious code after an exception is raised. ##
199 222 ################################################################################
200 223 set debug = false
201 224
202 225 ##################################
203 226 ### LOGVIEW CONFIG ###
204 227 ##################################
205 228 logview.sqlalchemy = #faa
206 229 logview.pylons.templating = #bfb
207 230 logview.pylons.util = #eee
208 231
209 232 #########################################################
210 233 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
211 234 #########################################################
212 235 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
213 236 sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode
214 237 sqlalchemy.db1.echo = false
215 238 sqlalchemy.db1.pool_recycle = 3600
216 239 sqlalchemy.db1.convert_unicode = true
217 240
218 241 ################################
219 242 ### LOGGING CONFIGURATION ####
220 243 ################################
221 244 [loggers]
222 245 keys = root, routes, rhodecode, sqlalchemy, beaker, templates, whoosh_indexer
223 246
224 247 [handlers]
225 248 keys = console, console_sql
226 249
227 250 [formatters]
228 251 keys = generic, color_formatter, color_formatter_sql
229 252
230 253 #############
231 254 ## LOGGERS ##
232 255 #############
233 256 [logger_root]
234 257 level = NOTSET
235 258 handlers = console
236 259
237 260 [logger_routes]
238 261 level = DEBUG
239 262 handlers =
240 263 qualname = routes.middleware
241 264 # "level = DEBUG" logs the route matched and routing variables.
242 265 propagate = 1
243 266
244 267 [logger_beaker]
245 268 level = DEBUG
246 269 handlers =
247 270 qualname = beaker.container
248 271 propagate = 1
249 272
250 273 [logger_templates]
251 274 level = INFO
252 275 handlers =
253 276 qualname = pylons.templating
254 277 propagate = 1
255 278
256 279 [logger_rhodecode]
257 280 level = DEBUG
258 281 handlers =
259 282 qualname = rhodecode
260 283 propagate = 1
261 284
262 285 [logger_sqlalchemy]
263 286 level = INFO
264 287 handlers = console_sql
265 288 qualname = sqlalchemy.engine
266 289 propagate = 0
267 290
268 291 [logger_whoosh_indexer]
269 292 level = DEBUG
270 293 handlers =
271 294 qualname = whoosh_indexer
272 295 propagate = 1
273 296
274 297 ##############
275 298 ## HANDLERS ##
276 299 ##############
277 300
278 301 [handler_console]
279 302 class = StreamHandler
280 303 args = (sys.stderr,)
281 304 level = INFO
282 305 formatter = generic
283 306
284 307 [handler_console_sql]
285 308 class = StreamHandler
286 309 args = (sys.stderr,)
287 310 level = WARN
288 311 formatter = generic
289 312
290 313 ################
291 314 ## FORMATTERS ##
292 315 ################
293 316
294 317 [formatter_generic]
295 318 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
296 319 datefmt = %Y-%m-%d %H:%M:%S
297 320
298 321 [formatter_color_formatter]
299 322 class=rhodecode.lib.colored_formatter.ColorFormatter
300 323 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
301 324 datefmt = %Y-%m-%d %H:%M:%S
302 325
303 326 [formatter_color_formatter_sql]
304 327 class=rhodecode.lib.colored_formatter.ColorFormatterSql
305 328 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
306 329 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,18 +1,20 b''
1 waitress==0.8.1
2 webob==1.0.8
1 3 Pylons==1.0.0
2 Beaker==1.6.3
4 Beaker==1.6.4
3 5 WebHelpers==1.3
4 6 formencode==1.2.4
5 SQLAlchemy==0.7.6
6 Mako==0.7.0
7 pygments>=1.4
7 SQLAlchemy==0.7.8
8 Mako==0.7.2
9 pygments>=1.5
8 10 whoosh>=2.4.0,<2.5
9 11 celery>=2.2.5,<2.3
10 12 babel
11 13 python-dateutil>=1.5.0,<2.0.0
12 14 dulwich>=0.8.5,<0.9.0
13 webob==1.0.8
14 15 markdown==2.1.1
15 16 docutils==0.8.1
16 17 simplejson==2.5.2
18 mock
17 19 py-bcrypt
18 mercurial>=2.2.1,<2.3 No newline at end of file
20 mercurial>=2.3.0,<2.4 No newline at end of file
@@ -1,98 +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, 3, 6)
29 VERSION = (1, 4, 0)
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__ = 5 # defines current db version for migrations
41 __dbversion__ = 6 # defines current db version for migrations
42 42 __platform__ = platform.system()
43 43 __license__ = 'GPLv3'
44 44 __py_version__ = sys.version_info
45 __author__ = 'Marcin Kuzminski'
46 __url__ = 'http://rhodecode.org'
45 47
46 48 PLATFORM_WIN = ('Windows')
47 PLATFORM_OTHERS = ('Linux', 'Darwin', 'FreeBSD', 'OpenBSD', 'SunOS')
49 PLATFORM_OTHERS = ('Linux', 'Darwin', 'FreeBSD', 'OpenBSD', 'SunOS') #depracated
48 50
49 51 is_windows = __platform__ in PLATFORM_WIN
50 is_unix = __platform__ in PLATFORM_OTHERS
52 is_unix = not is_windows
51 53
52 requirements = [
53 "Pylons==1.0.0",
54 "Beaker==1.6.3",
55 "WebHelpers==1.3",
56 "formencode==1.2.4",
57 "SQLAlchemy==0.7.6",
58 "Mako==0.7.0",
59 "pygments>=1.4",
60 "whoosh>=2.4.0,<2.5",
61 "celery>=2.2.5,<2.3",
62 "babel",
63 "python-dateutil>=1.5.0,<2.0.0",
64 "dulwich>=0.8.5,<0.9.0",
65 "webob==1.0.8",
66 "markdown==2.1.1",
67 "docutils==0.8.1",
68 "simplejson==2.5.2",
69 ]
70
71 if __py_version__ < (2, 6):
72 requirements.append("pysqlite")
73
74 if is_windows:
75 requirements.append("mercurial>=2.2.1,<2.3")
76 else:
77 requirements.append("py-bcrypt")
78 requirements.append("mercurial>=2.2.1,<2.3")
79
80
81 def get_version():
82 """Returns shorter version (digit parts only) as string."""
83
84 return '.'.join((str(each) for each in VERSION[:3]))
85 54
86 55 BACKENDS = {
87 56 'hg': 'Mercurial repository',
88 57 'git': 'Git repository',
89 58 }
90 59
91 60 CELERY_ON = False
92 61 CELERY_EAGER = False
93 62
94 63 # link to config for pylons
95 64 CONFIG = {}
96 65
97 66 # Linked module for extensions
98 67 EXTENSIONS = {}
@@ -1,102 +1,102 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 rhodecode.lib.backup_manager
3 rhodecode.bin.backup_manager
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 Mercurial repositories backup manager, it allows to backups all
6 Repositories backup manager, it allows to backups all
7 7 repositories and send it to backup server using RSA key via ssh.
8 8
9 9 :created_on: Feb 28, 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
27 27 import os
28 28 import sys
29 29
30 30 import logging
31 31 import tarfile
32 32 import datetime
33 33 import subprocess
34 34
35 35 logging.basicConfig(level=logging.DEBUG,
36 36 format="%(asctime)s %(levelname)-5.5s %(message)s")
37 37
38 38
39 39 class BackupManager(object):
40 40 def __init__(self, repos_location, rsa_key, backup_server):
41 41 today = datetime.datetime.now().weekday() + 1
42 self.backup_file_name = "mercurial_repos.%s.tar.gz" % today
42 self.backup_file_name = "rhodecode_repos.%s.tar.gz" % today
43 43
44 44 self.id_rsa_path = self.get_id_rsa(rsa_key)
45 45 self.repos_path = self.get_repos_path(repos_location)
46 46 self.backup_server = backup_server
47 47
48 48 self.backup_file_path = '/tmp'
49 49
50 50 logging.info('starting backup for %s', self.repos_path)
51 51 logging.info('backup target %s', self.backup_file_path)
52 52
53 53 def get_id_rsa(self, rsa_key):
54 54 if not os.path.isfile(rsa_key):
55 55 logging.error('Could not load id_rsa key file in %s', rsa_key)
56 56 sys.exit()
57 57 return rsa_key
58 58
59 59 def get_repos_path(self, path):
60 60 if not os.path.isdir(path):
61 61 logging.error('Wrong location for repositories in %s', path)
62 62 sys.exit()
63 63 return path
64 64
65 65 def backup_repos(self):
66 66 bckp_file = os.path.join(self.backup_file_path, self.backup_file_name)
67 67 tar = tarfile.open(bckp_file, "w:gz")
68 68
69 69 for dir_name in os.listdir(self.repos_path):
70 70 logging.info('backing up %s', dir_name)
71 71 tar.add(os.path.join(self.repos_path, dir_name), dir_name)
72 72 tar.close()
73 73 logging.info('finished backup of mercurial repositories')
74 74
75 75 def transfer_files(self):
76 76 params = {
77 77 'id_rsa_key': self.id_rsa_path,
78 78 'backup_file': os.path.join(self.backup_file_path,
79 79 self.backup_file_name),
80 80 'backup_server': self.backup_server
81 81 }
82 82 cmd = ['scp', '-l', '40000', '-i', '%(id_rsa_key)s' % params,
83 83 '%(backup_file)s' % params,
84 84 '%(backup_server)s' % params]
85 85
86 86 subprocess.call(cmd)
87 87 logging.info('Transfered file %s to %s', self.backup_file_name, cmd[4])
88 88
89 89 def rm_file(self):
90 90 logging.info('Removing file %s', self.backup_file_name)
91 91 os.remove(os.path.join(self.backup_file_path, self.backup_file_name))
92 92
93 93 if __name__ == "__main__":
94 94
95 95 repo_location = '/home/repo_path'
96 96 backup_server = 'root@192.168.1.100:/backups/mercurial'
97 97 rsa_key = '/home/id_rsa'
98 98
99 99 B_MANAGER = BackupManager(repo_location, rsa_key, backup_server)
100 100 B_MANAGER.backup_repos()
101 101 B_MANAGER.transfer_files()
102 102 B_MANAGER.rm_file()
@@ -1,316 +1,339 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 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 ##nr of threads to spawn
33 threadpool_workers = 5
33 #threadpool_workers = 5
34 34
35 35 ##max request before thread respawn
36 threadpool_max_requests = 10
36 #threadpool_max_requests = 10
37 37
38 38 ##option to use threads of process
39 use_threadpool = true
39 #use_threadpool = true
40 40
41 use = egg:Paste#http
41 #use = egg:Paste#http
42 use = egg:waitress#main
42 43 host = 127.0.0.1
43 44 port = 5000
44 45
46 [filter:proxy-prefix]
47 # prefix middleware for rc
48 use = egg:PasteDeploy#prefix
49 prefix = /<your-prefix>
50
45 51 [app:main]
46 52 use = egg:rhodecode
53 #filter-with = proxy-prefix
47 54 full_stack = true
48 55 static_files = true
56 # Optional Languages
57 # en, fr, ja, pt_BR, zh_CN, zh_TW
49 58 lang = en
50 59 cache_dir = %(here)s/data
51 60 index_dir = %(here)s/data/index
52 61 app_instance_uuid = ${app_instance_uuid}
53 62 cut_off_limit = 256000
54 63 force_https = false
55 64 commit_parse_limit = 50
56 65 use_gravatar = true
66
67 ## alternative_gravatar_url allows you to use your own avatar server application
68 ## the following parts of the URL will be replaced
69 ## {email} user email
70 ## {md5email} md5 hash of the user email (like at gravatar.com)
71 ## {size} size of the image that is expected from the server application
72 #alternative_gravatar_url = http://myavatarserver.com/getbyemail/{email}/{size}
73 #alternative_gravatar_url = http://myavatarserver.com/getbymd5/{md5email}?s={size}
74
57 75 container_auth_enabled = false
58 76 proxypass_auth_enabled = false
59 77 default_encoding = utf8
60 78
61 79 ## overwrite schema of clone url
62 80 ## available vars:
63 81 ## scheme - http/https
64 82 ## user - current user
65 83 ## pass - password
66 84 ## netloc - network location
67 85 ## path - usually repo_name
68 86
69 87 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
70 88
71 89 ## issue tracking mapping for commits messages
72 90 ## comment out issue_pat, issue_server, issue_prefix to enable
73 91
74 92 ## pattern to get the issues from commit messages
75 93 ## default one used here is #<numbers> with a regex passive group for `#`
76 94 ## {id} will be all groups matched from this pattern
77 95
78 96 issue_pat = (?:\s*#)(\d+)
79 97
80 98 ## server url to the issue, each {id} will be replaced with match
81 ## fetched from the regex and {repo} is replaced with repository name
99 ## fetched from the regex and {repo} is replaced with full repository name
100 ## including groups {repo_name} is replaced with just name of repo
82 101
83 102 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
84 103
85 104 ## prefix to add to link to indicate it's an url
86 105 ## #314 will be replaced by <issue_prefix><id>
87 106
88 107 issue_prefix = #
89 108
90 109 ## instance-id prefix
91 110 ## a prefix key for this instance used for cache invalidation when running
92 111 ## multiple instances of rhodecode, make sure it's globally unique for
93 112 ## all running rhodecode instances. Leave empty if you don't use it
94 113 instance_id =
95 114
96 115 ## alternative return HTTP header for failed authentication. Default HTTP
97 116 ## response is 401 HTTPUnauthorized. Currently HG clients have troubles with
98 117 ## handling that. Set this variable to 403 to return HTTPForbidden
99 118 auth_ret_code =
100 119
101 120 ####################################
102 121 ### CELERY CONFIG ####
103 122 ####################################
104 123 use_celery = false
105 124 broker.host = localhost
106 125 broker.vhost = rabbitmqhost
107 126 broker.port = 5672
108 127 broker.user = rabbitmq
109 128 broker.password = qweqwe
110 129
111 130 celery.imports = rhodecode.lib.celerylib.tasks
112 131
113 132 celery.result.backend = amqp
114 133 celery.result.dburi = amqp://
115 134 celery.result.serialier = json
116 135
117 136 #celery.send.task.error.emails = true
118 137 #celery.amqp.task.result.expires = 18000
119 138
120 139 celeryd.concurrency = 2
121 140 #celeryd.log.file = celeryd.log
122 141 celeryd.log.level = debug
123 142 celeryd.max.tasks.per.child = 1
124 143
125 144 #tasks will never be sent to the queue, but executed locally instead.
126 145 celery.always.eager = false
127 146
128 147 ####################################
129 148 ### BEAKER CACHE ####
130 149 ####################################
131 150 beaker.cache.data_dir=%(here)s/data/cache/data
132 151 beaker.cache.lock_dir=%(here)s/data/cache/lock
133 152
134 153 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
135 154
136 155 beaker.cache.super_short_term.type=memory
137 156 beaker.cache.super_short_term.expire=10
138 157 beaker.cache.super_short_term.key_length = 256
139 158
140 159 beaker.cache.short_term.type=memory
141 160 beaker.cache.short_term.expire=60
142 161 beaker.cache.short_term.key_length = 256
143 162
144 163 beaker.cache.long_term.type=memory
145 164 beaker.cache.long_term.expire=36000
146 165 beaker.cache.long_term.key_length = 256
147 166
148 167 beaker.cache.sql_cache_short.type=memory
149 168 beaker.cache.sql_cache_short.expire=10
150 169 beaker.cache.sql_cache_short.key_length = 256
151 170
152 171 beaker.cache.sql_cache_med.type=memory
153 172 beaker.cache.sql_cache_med.expire=360
154 173 beaker.cache.sql_cache_med.key_length = 256
155 174
156 175 beaker.cache.sql_cache_long.type=file
157 176 beaker.cache.sql_cache_long.expire=3600
158 177 beaker.cache.sql_cache_long.key_length = 256
159 178
160 179 ####################################
161 180 ### BEAKER SESSION ####
162 181 ####################################
163 182 ## Type of storage used for the session, current types are
164 183 ## dbm, file, memcached, database, and memory.
165 184 ## The storage uses the Container API
166 185 ## that is also used by the cache system.
167 186
168 ## db session example
169
187 ## db session ##
170 188 #beaker.session.type = ext:database
171 189 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
172 190 #beaker.session.table_name = db_session
173 191
174 ## encrypted cookie session, good for many instances
192 ## encrypted cookie client side session, good for many instances ##
175 193 #beaker.session.type = cookie
176 194
177 beaker.session.type = file
195 ## file based cookies (default) ##
196 #beaker.session.type = file
197
198
178 199 beaker.session.key = rhodecode
179 # secure cookie requires AES python libraries
180 #beaker.session.encrypt_key = ${app_instance_secret}
181 #beaker.session.validate_key = ${app_instance_secret}
182 beaker.session.timeout = 36000
200 ## secure cookie requires AES python libraries ##
201 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
202 #beaker.session.validate_key = 9712sds2212c--zxc123
203 ## sets session as invalid if it haven't been accessed for given amount of time
204 beaker.session.timeout = 2592000
183 205 beaker.session.httponly = true
206 #beaker.session.cookie_path = /<your-prefix>
184 207
185 ## uncomment for https secure cookie
208 ## uncomment for https secure cookie ##
186 209 beaker.session.secure = false
187 210
188 ##auto save the session to not to use .save()
211 ## auto save the session to not to use .save() ##
189 212 beaker.session.auto = False
190 213
191 ##true exire at browser close
214 ## default cookie expiration time in seconds `true` expire at browser close ##
192 215 #beaker.session.cookie_expires = 3600
193 216
194 217
195 218 ################################################################################
196 219 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
197 220 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
198 221 ## execute malicious code after an exception is raised. ##
199 222 ################################################################################
200 223 set debug = false
201 224
202 225 ##################################
203 226 ### LOGVIEW CONFIG ###
204 227 ##################################
205 228 logview.sqlalchemy = #faa
206 229 logview.pylons.templating = #bfb
207 230 logview.pylons.util = #eee
208 231
209 232 #########################################################
210 233 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
211 234 #########################################################
212 235
213 236 # SQLITE [default]
214 237 sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
215 238
216 239 # POSTGRESQL
217 240 # sqlalchemy.db1.url = postgresql://user:pass@localhost/rhodecode
218 241
219 242 # MySQL
220 243 # sqlalchemy.db1.url = mysql://user:pass@localhost/rhodecode
221 244
222 245 # see sqlalchemy docs for others
223 246
224 247 sqlalchemy.db1.echo = false
225 248 sqlalchemy.db1.pool_recycle = 3600
226 249 sqlalchemy.db1.convert_unicode = true
227 250
228 251 ################################
229 252 ### LOGGING CONFIGURATION ####
230 253 ################################
231 254 [loggers]
232 255 keys = root, routes, rhodecode, sqlalchemy, beaker, templates, whoosh_indexer
233 256
234 257 [handlers]
235 258 keys = console, console_sql
236 259
237 260 [formatters]
238 261 keys = generic, color_formatter, color_formatter_sql
239 262
240 263 #############
241 264 ## LOGGERS ##
242 265 #############
243 266 [logger_root]
244 267 level = NOTSET
245 268 handlers = console
246 269
247 270 [logger_routes]
248 271 level = DEBUG
249 272 handlers =
250 273 qualname = routes.middleware
251 274 # "level = DEBUG" logs the route matched and routing variables.
252 275 propagate = 1
253 276
254 277 [logger_beaker]
255 278 level = DEBUG
256 279 handlers =
257 280 qualname = beaker.container
258 281 propagate = 1
259 282
260 283 [logger_templates]
261 284 level = INFO
262 285 handlers =
263 286 qualname = pylons.templating
264 287 propagate = 1
265 288
266 289 [logger_rhodecode]
267 290 level = DEBUG
268 291 handlers =
269 292 qualname = rhodecode
270 293 propagate = 1
271 294
272 295 [logger_sqlalchemy]
273 296 level = INFO
274 297 handlers = console_sql
275 298 qualname = sqlalchemy.engine
276 299 propagate = 0
277 300
278 301 [logger_whoosh_indexer]
279 302 level = DEBUG
280 303 handlers =
281 304 qualname = whoosh_indexer
282 305 propagate = 1
283 306
284 307 ##############
285 308 ## HANDLERS ##
286 309 ##############
287 310
288 311 [handler_console]
289 312 class = StreamHandler
290 313 args = (sys.stderr,)
291 314 level = INFO
292 315 formatter = generic
293 316
294 317 [handler_console_sql]
295 318 class = StreamHandler
296 319 args = (sys.stderr,)
297 320 level = WARN
298 321 formatter = generic
299 322
300 323 ################
301 324 ## FORMATTERS ##
302 325 ################
303 326
304 327 [formatter_generic]
305 328 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
306 329 datefmt = %Y-%m-%d %H:%M:%S
307 330
308 331 [formatter_color_formatter]
309 332 class=rhodecode.lib.colored_formatter.ColorFormatter
310 333 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
311 334 datefmt = %Y-%m-%d %H:%M:%S
312 335
313 336 [formatter_color_formatter_sql]
314 337 class=rhodecode.lib.colored_formatter.ColorFormatterSql
315 338 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
316 339 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,97 +1,106 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
22 22 from rhodecode.lib.utils2 import engine_from_config, str2bool
23 23 from rhodecode.model import init_model
24 24 from rhodecode.model.scm import ScmModel
25 25
26 26 log = logging.getLogger(__name__)
27 27
28 28
29 29 def load_environment(global_conf, app_conf, initial=False):
30 30 """
31 31 Configure the Pylons environment via the ``pylons.config``
32 32 object
33 33 """
34 34 config = PylonsConfig()
35 35
36 36 # Pylons paths
37 37 root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
38 38 paths = dict(
39 39 root=root,
40 40 controllers=os.path.join(root, 'controllers'),
41 41 static_files=os.path.join(root, 'public'),
42 42 templates=[os.path.join(root, 'templates')]
43 43 )
44 44
45 45 # Initialize config with the basic options
46 46 config.init_app(global_conf, app_conf, package='rhodecode', paths=paths)
47 47
48 48 # store some globals into rhodecode
49 49 rhodecode.CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
50 50 rhodecode.CELERY_EAGER = str2bool(config['app_conf'].get('celery.always.eager'))
51 51
52 52 config['routes.map'] = make_map(config)
53 53 config['pylons.app_globals'] = app_globals.Globals(config)
54 54 config['pylons.h'] = helpers
55 55 rhodecode.CONFIG = config
56 56
57 57 load_rcextensions(root_path=config['here'])
58 58
59 59 # Setup cache object as early as possible
60 60 import pylons
61 61 pylons.cache._push_object(config['pylons.app_globals'].cache)
62 62
63 63 # Create the Mako TemplateLookup, with the default auto-escaping
64 64 config['pylons.app_globals'].mako_lookup = TemplateLookup(
65 65 directories=paths['templates'],
66 66 error_handler=handle_mako_error,
67 67 module_directory=os.path.join(app_conf['cache_dir'], 'templates'),
68 68 input_encoding='utf-8', default_filters=['escape'],
69 69 imports=['from webhelpers.html import escape'])
70 70
71 71 # sets the c attribute access when don't existing attribute are accessed
72 72 config['pylons.strict_tmpl_context'] = True
73 73 test = os.path.split(config['__file__'])[-1] == 'test.ini'
74 74 if test:
75 if os.environ.get('TEST_DB'):
76 # swap config if we pass enviroment variable
77 config['sqlalchemy.db1.url'] = os.environ.get('TEST_DB')
78
75 79 from rhodecode.lib.utils import create_test_env, create_test_index
76 80 from rhodecode.tests import TESTS_TMP_PATH
81 # set RC_NO_TMP_PATH=1 to disable re-creating the database and
82 # test repos
83 if not int(os.environ.get('RC_NO_TMP_PATH', 0)):
77 84 create_test_env(TESTS_TMP_PATH, config)
85 # set RC_WHOOSH_TEST_DISABLE=1 to disable whoosh index during tests
86 if not int(os.environ.get('RC_WHOOSH_TEST_DISABLE', 0)):
78 87 create_test_index(TESTS_TMP_PATH, config, True)
79 88
80 89 # MULTIPLE DB configs
81 90 # Setup the SQLAlchemy database engine
82 91 sa_engine_db1 = engine_from_config(config, 'sqlalchemy.db1.')
83
84 92 init_model(sa_engine_db1)
85 93
86 94 repos_path = make_ui('db').configitems('paths')[0][1]
87 repo2db_mapper(ScmModel().repo_scan(repos_path))
95 repo2db_mapper(ScmModel().repo_scan(repos_path),
96 remove_obsolete=False, install_git_hook=False)
88 97 set_available_permissions(config)
89 98 config['base_path'] = repos_path
90 99 set_rhodecode_config(config)
91 100 # CONFIGURATION OPTIONS HERE (note: all config options will override
92 101 # any Pylons config options)
93 102
94 103 # store config reference into our module to skip import magic of
95 104 # pylons
96 105 rhodecode.CONFIG.update(config)
97 106 return config
@@ -1,84 +1,86 b''
1 1 # Additional mappings that are not present in the pygments lexers
2 2 # used for building stats
3 # format is {'ext':'Name'} eg. {'py':'Python'}
3 # format is {'ext':['Names']} eg. {'py':['Python']} note: there can be
4 # more than one name for extension
4 5 # NOTE: that this will overide any mappings in LANGUAGES_EXTENSIONS_MAP
5 6 # build by pygments
6 7 EXTRA_MAPPINGS = {}
7 8
8 9 #==============================================================================
9 10 # WHOOSH INDEX EXTENSIONS
10 11 #==============================================================================
11 12 # if INDEX_EXTENSIONS is [] it'll use pygments lexers extensions by default.
12 13 # To set your own just add to this list extensions to index with content
13 14 INDEX_EXTENSIONS = []
14 15
15 16 # additional extensions for indexing besides the default from pygments
16 17 # those get's added to INDEX_EXTENSIONS
17 18 EXTRA_INDEX_EXTENSIONS = []
18 19
19 20
20 21 #==============================================================================
21 22 # POST CREATE REPOSITORY HOOK
22 23 #==============================================================================
23 24 # this function will be executed after each repository is created
24 25 def _crhook(*args, **kwargs):
25 26 """
26 27 Post create repository HOOK
27 28 kwargs available:
28 29 :param repo_name:
29 30 :param repo_type:
30 31 :param description:
31 32 :param private:
32 33 :param created_on:
33 34 :param enable_downloads:
34 35 :param repo_id:
35 36 :param user_id:
36 37 :param enable_statistics:
37 38 :param clone_uri:
38 39 :param fork_id:
39 40 :param group_id:
40 41 :param created_by:
41 42 """
43
42 44 return 0
43 45 CREATE_REPO_HOOK = _crhook
44 46
45 47
46 48 #==============================================================================
47 49 # POST PUSH HOOK
48 50 #==============================================================================
49 51
50 52 # this function will be executed after each push it's runned after the build-in
51 53 # hook that rhodecode uses for logging pushes
52 54 def _pushhook(*args, **kwargs):
53 55 """
54 56 Post push hook
55 57 kwargs available:
56 58
57 59 :param username: name of user who pushed
58 60 :param ip: ip of who pushed
59 61 :param action: pull
60 62 :param repository: repository name
61 63 :param pushed_revs: generator of pushed revisions
62 64 """
63 65 return 0
64 66 PUSH_HOOK = _pushhook
65 67
66 68
67 69 #==============================================================================
68 70 # POST PULL HOOK
69 71 #==============================================================================
70 72
71 73 # this function will be executed after each push it's runned after the build-in
72 74 # hook that rhodecode uses for logging pushes
73 75 def _pullhook(*args, **kwargs):
74 76 """
75 77 Post pull hook
76 78 kwargs available::
77 79
78 80 :param username: name of user who pulled
79 81 :param ip: ip of who pushed
80 82 :param action: pull
81 83 :param repository: repository name
82 84 """
83 85 return 0
84 86 PULL_HOOK = _pullhook
@@ -1,81 +1,81 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.config.rcextensions.make_rcextensions
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Whoosh indexing module for RhodeCode
7 7
8 8 :created_on: Mar 6, 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 from __future__ import with_statement
26 26
27 27 import os
28 28 import sys
29 29 import pkg_resources
30 30 import traceback
31 31 import logging
32 32 from os.path import dirname as dn, join as jn
33 33
34 34 #to get the rhodecode import
35 35 sys.path.append(dn(dn(dn(os.path.realpath(__file__)))))
36 36
37 37 from rhodecode.lib.utils import BasePasterCommand, Command, ask_ok
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class MakeRcExt(BasePasterCommand):
43 43
44 44 max_args = 1
45 45 min_args = 1
46 46
47 47 usage = "CONFIG_FILE"
48 48 summary = "Creates additional extensions for rhodecode"
49 49 group_name = "RhodeCode"
50 50 takes_config_file = -1
51 51 parser = Command.standard_parser(verbose=True)
52 52
53 53 def command(self):
54 54 logging.config.fileConfig(self.path_to_ini_file)
55 55 from pylons import config
56 56
57 def _make_file(ext_file):
57 def _make_file(ext_file, tmpl):
58 58 bdir = os.path.split(ext_file)[0]
59 59 if not os.path.isdir(bdir):
60 60 os.makedirs(bdir)
61 61 with open(ext_file, 'wb') as f:
62 62 f.write(tmpl)
63 63 log.info('Writen new extensions file to %s' % ext_file)
64 64
65 65 here = config['here']
66 66 tmpl = pkg_resources.resource_string(
67 67 'rhodecode', jn('config', 'rcextensions', '__init__.py')
68 68 )
69 69 ext_file = jn(here, 'rcextensions', '__init__.py')
70 70 if os.path.exists(ext_file):
71 71 msg = ('Extension file already exists, do you want '
72 72 'to overwrite it ? [y/n]')
73 73 if ask_ok(msg):
74 _make_file(ext_file)
74 _make_file(ext_file, tmpl)
75 75 else:
76 76 log.info('nothing done...')
77 77 else:
78 _make_file(ext_file)
78 _make_file(ext_file, tmpl)
79 79
80 80 def update_parser(self):
81 81 pass
@@ -1,507 +1,580 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 try:
36 36 by_id = repo_name.split('_')
37 37 if len(by_id) == 2 and by_id[1].isdigit():
38 38 repo_name = Repository.get(by_id[1]).repo_name
39 39 match_dict['repo_name'] = repo_name
40 40 except:
41 41 pass
42 42
43 43 return is_valid_repo(repo_name, config['base_path'])
44 44
45 45 def check_group(environ, match_dict):
46 46 """
47 47 check for valid repositories group for proper 404 handling
48 48
49 49 :param environ:
50 50 :param match_dict:
51 51 """
52 52 repos_group_name = match_dict.get('group_name')
53 53
54 54 return is_valid_repos_group(repos_group_name, config['base_path'])
55 55
56 56 def check_int(environ, match_dict):
57 57 return match_dict.get('id').isdigit()
58 58
59 59 # The ErrorController route (handles 404/500 error pages); it should
60 60 # likely stay at the top, ensuring it can always be resolved
61 61 rmap.connect('/error/{action}', controller='error')
62 62 rmap.connect('/error/{action}/{id}', controller='error')
63 63
64 64 #==========================================================================
65 65 # CUSTOM ROUTES HERE
66 66 #==========================================================================
67 67
68 68 #MAIN PAGE
69 69 rmap.connect('home', '/', controller='home', action='index')
70 70 rmap.connect('repo_switcher', '/repos', controller='home',
71 71 action='repo_switcher')
72 rmap.connect('branch_tag_switcher', '/branches-tags/{repo_name:.*}',
72 rmap.connect('branch_tag_switcher', '/branches-tags/{repo_name:.*?}',
73 73 controller='home', action='branch_tag_switcher')
74 74 rmap.connect('bugtracker',
75 75 "http://bitbucket.org/marcinkuzminski/rhodecode/issues",
76 76 _static=True)
77 77 rmap.connect('rst_help',
78 78 "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
79 79 _static=True)
80 80 rmap.connect('rhodecode_official', "http://rhodecode.org", _static=True)
81 81
82 82 #ADMIN REPOSITORY REST ROUTES
83 83 with rmap.submapper(path_prefix=ADMIN_PREFIX,
84 84 controller='admin/repos') as m:
85 85 m.connect("repos", "/repos",
86 86 action="create", conditions=dict(method=["POST"]))
87 87 m.connect("repos", "/repos",
88 88 action="index", conditions=dict(method=["GET"]))
89 89 m.connect("formatted_repos", "/repos.{format}",
90 90 action="index",
91 91 conditions=dict(method=["GET"]))
92 92 m.connect("new_repo", "/repos/new",
93 93 action="new", conditions=dict(method=["GET"]))
94 94 m.connect("formatted_new_repo", "/repos/new.{format}",
95 95 action="new", conditions=dict(method=["GET"]))
96 m.connect("/repos/{repo_name:.*}",
96 m.connect("/repos/{repo_name:.*?}",
97 97 action="update", conditions=dict(method=["PUT"],
98 98 function=check_repo))
99 m.connect("/repos/{repo_name:.*}",
99 m.connect("/repos/{repo_name:.*?}",
100 100 action="delete", conditions=dict(method=["DELETE"],
101 101 function=check_repo))
102 m.connect("edit_repo", "/repos/{repo_name:.*}/edit",
102 m.connect("edit_repo", "/repos/{repo_name:.*?}/edit",
103 103 action="edit", conditions=dict(method=["GET"],
104 104 function=check_repo))
105 m.connect("formatted_edit_repo", "/repos/{repo_name:.*}.{format}/edit",
105 m.connect("formatted_edit_repo", "/repos/{repo_name:.*?}.{format}/edit",
106 106 action="edit", conditions=dict(method=["GET"],
107 107 function=check_repo))
108 m.connect("repo", "/repos/{repo_name:.*}",
108 m.connect("repo", "/repos/{repo_name:.*?}",
109 109 action="show", conditions=dict(method=["GET"],
110 110 function=check_repo))
111 m.connect("formatted_repo", "/repos/{repo_name:.*}.{format}",
111 m.connect("formatted_repo", "/repos/{repo_name:.*?}.{format}",
112 112 action="show", conditions=dict(method=["GET"],
113 113 function=check_repo))
114 114 #ajax delete repo perm user
115 m.connect('delete_repo_user', "/repos_delete_user/{repo_name:.*}",
115 m.connect('delete_repo_user', "/repos_delete_user/{repo_name:.*?}",
116 116 action="delete_perm_user",
117 117 conditions=dict(method=["DELETE"], function=check_repo))
118 118
119 119 #ajax delete repo perm users_group
120 120 m.connect('delete_repo_users_group',
121 "/repos_delete_users_group/{repo_name:.*}",
121 "/repos_delete_users_group/{repo_name:.*?}",
122 122 action="delete_perm_users_group",
123 123 conditions=dict(method=["DELETE"], function=check_repo))
124 124
125 125 #settings actions
126 m.connect('repo_stats', "/repos_stats/{repo_name:.*}",
126 m.connect('repo_stats', "/repos_stats/{repo_name:.*?}",
127 127 action="repo_stats", conditions=dict(method=["DELETE"],
128 128 function=check_repo))
129 m.connect('repo_cache', "/repos_cache/{repo_name:.*}",
129 m.connect('repo_cache', "/repos_cache/{repo_name:.*?}",
130 130 action="repo_cache", conditions=dict(method=["DELETE"],
131 131 function=check_repo))
132 m.connect('repo_public_journal', "/repos_public_journal/{repo_name:.*}",
132 m.connect('repo_public_journal', "/repos_public_journal/{repo_name:.*?}",
133 133 action="repo_public_journal", conditions=dict(method=["PUT"],
134 134 function=check_repo))
135 m.connect('repo_pull', "/repo_pull/{repo_name:.*}",
135 m.connect('repo_pull', "/repo_pull/{repo_name:.*?}",
136 136 action="repo_pull", conditions=dict(method=["PUT"],
137 137 function=check_repo))
138 m.connect('repo_as_fork', "/repo_as_fork/{repo_name:.*}",
138 m.connect('repo_as_fork', "/repo_as_fork/{repo_name:.*?}",
139 139 action="repo_as_fork", conditions=dict(method=["PUT"],
140 140 function=check_repo))
141
141 m.connect('repo_locking', "/repo_locking/{repo_name:.*?}",
142 action="repo_locking", conditions=dict(method=["PUT"],
143 function=check_repo))
142 144 with rmap.submapper(path_prefix=ADMIN_PREFIX,
143 145 controller='admin/repos_groups') as m:
144 146 m.connect("repos_groups", "/repos_groups",
145 147 action="create", conditions=dict(method=["POST"]))
146 148 m.connect("repos_groups", "/repos_groups",
147 149 action="index", conditions=dict(method=["GET"]))
148 150 m.connect("formatted_repos_groups", "/repos_groups.{format}",
149 151 action="index", conditions=dict(method=["GET"]))
150 152 m.connect("new_repos_group", "/repos_groups/new",
151 153 action="new", conditions=dict(method=["GET"]))
152 154 m.connect("formatted_new_repos_group", "/repos_groups/new.{format}",
153 155 action="new", conditions=dict(method=["GET"]))
154 156 m.connect("update_repos_group", "/repos_groups/{id}",
155 157 action="update", conditions=dict(method=["PUT"],
156 158 function=check_int))
157 159 m.connect("delete_repos_group", "/repos_groups/{id}",
158 160 action="delete", conditions=dict(method=["DELETE"],
159 161 function=check_int))
160 m.connect("edit_repos_group", "/repos_groups/{id}/edit",
161 action="edit", conditions=dict(method=["GET"],
162 function=check_int))
162 m.connect("edit_repos_group", "/repos_groups/{id:.*?}/edit",
163 action="edit", conditions=dict(method=["GET"],))
163 164 m.connect("formatted_edit_repos_group",
164 165 "/repos_groups/{id}.{format}/edit",
165 166 action="edit", conditions=dict(method=["GET"],
166 167 function=check_int))
167 168 m.connect("repos_group", "/repos_groups/{id}",
168 169 action="show", conditions=dict(method=["GET"],
169 170 function=check_int))
170 171 m.connect("formatted_repos_group", "/repos_groups/{id}.{format}",
171 172 action="show", conditions=dict(method=["GET"],
172 173 function=check_int))
173 174 # ajax delete repos group perm user
174 175 m.connect('delete_repos_group_user_perm',
175 176 "/delete_repos_group_user_perm/{group_name:.*}",
176 177 action="delete_repos_group_user_perm",
177 178 conditions=dict(method=["DELETE"], function=check_group))
178 179
179 180 # ajax delete repos group perm users_group
180 181 m.connect('delete_repos_group_users_group_perm',
181 182 "/delete_repos_group_users_group_perm/{group_name:.*}",
182 183 action="delete_repos_group_users_group_perm",
183 184 conditions=dict(method=["DELETE"], function=check_group))
184 185
185 186 #ADMIN USER REST ROUTES
186 187 with rmap.submapper(path_prefix=ADMIN_PREFIX,
187 188 controller='admin/users') as m:
188 189 m.connect("users", "/users",
189 190 action="create", conditions=dict(method=["POST"]))
190 191 m.connect("users", "/users",
191 192 action="index", conditions=dict(method=["GET"]))
192 193 m.connect("formatted_users", "/users.{format}",
193 194 action="index", conditions=dict(method=["GET"]))
194 195 m.connect("new_user", "/users/new",
195 196 action="new", conditions=dict(method=["GET"]))
196 197 m.connect("formatted_new_user", "/users/new.{format}",
197 198 action="new", conditions=dict(method=["GET"]))
198 199 m.connect("update_user", "/users/{id}",
199 200 action="update", conditions=dict(method=["PUT"]))
200 201 m.connect("delete_user", "/users/{id}",
201 202 action="delete", conditions=dict(method=["DELETE"]))
202 203 m.connect("edit_user", "/users/{id}/edit",
203 204 action="edit", conditions=dict(method=["GET"]))
204 205 m.connect("formatted_edit_user",
205 206 "/users/{id}.{format}/edit",
206 207 action="edit", conditions=dict(method=["GET"]))
207 208 m.connect("user", "/users/{id}",
208 209 action="show", conditions=dict(method=["GET"]))
209 210 m.connect("formatted_user", "/users/{id}.{format}",
210 211 action="show", conditions=dict(method=["GET"]))
211 212
212 213 #EXTRAS USER ROUTES
213 214 m.connect("user_perm", "/users_perm/{id}",
214 215 action="update_perm", conditions=dict(method=["PUT"]))
216 m.connect("user_emails", "/users_emails/{id}",
217 action="add_email", conditions=dict(method=["PUT"]))
218 m.connect("user_emails_delete", "/users_emails/{id}",
219 action="delete_email", conditions=dict(method=["DELETE"]))
215 220
216 #ADMIN USERS REST ROUTES
221 #ADMIN USERS GROUPS REST ROUTES
217 222 with rmap.submapper(path_prefix=ADMIN_PREFIX,
218 223 controller='admin/users_groups') as m:
219 224 m.connect("users_groups", "/users_groups",
220 225 action="create", conditions=dict(method=["POST"]))
221 226 m.connect("users_groups", "/users_groups",
222 227 action="index", conditions=dict(method=["GET"]))
223 228 m.connect("formatted_users_groups", "/users_groups.{format}",
224 229 action="index", conditions=dict(method=["GET"]))
225 230 m.connect("new_users_group", "/users_groups/new",
226 231 action="new", conditions=dict(method=["GET"]))
227 232 m.connect("formatted_new_users_group", "/users_groups/new.{format}",
228 233 action="new", conditions=dict(method=["GET"]))
229 234 m.connect("update_users_group", "/users_groups/{id}",
230 235 action="update", conditions=dict(method=["PUT"]))
231 236 m.connect("delete_users_group", "/users_groups/{id}",
232 237 action="delete", conditions=dict(method=["DELETE"]))
233 238 m.connect("edit_users_group", "/users_groups/{id}/edit",
234 239 action="edit", conditions=dict(method=["GET"]))
235 240 m.connect("formatted_edit_users_group",
236 241 "/users_groups/{id}.{format}/edit",
237 242 action="edit", conditions=dict(method=["GET"]))
238 243 m.connect("users_group", "/users_groups/{id}",
239 244 action="show", conditions=dict(method=["GET"]))
240 245 m.connect("formatted_users_group", "/users_groups/{id}.{format}",
241 246 action="show", conditions=dict(method=["GET"]))
242 247
243 248 #EXTRAS USER ROUTES
244 249 m.connect("users_group_perm", "/users_groups_perm/{id}",
245 250 action="update_perm", conditions=dict(method=["PUT"]))
246 251
247 252 #ADMIN GROUP REST ROUTES
248 253 rmap.resource('group', 'groups',
249 254 controller='admin/groups', path_prefix=ADMIN_PREFIX)
250 255
251 256 #ADMIN PERMISSIONS REST ROUTES
252 257 rmap.resource('permission', 'permissions',
253 258 controller='admin/permissions', path_prefix=ADMIN_PREFIX)
254 259
255 260 ##ADMIN LDAP SETTINGS
256 261 rmap.connect('ldap_settings', '%s/ldap' % ADMIN_PREFIX,
257 262 controller='admin/ldap_settings', action='ldap_settings',
258 263 conditions=dict(method=["POST"]))
259 264
260 265 rmap.connect('ldap_home', '%s/ldap' % ADMIN_PREFIX,
261 266 controller='admin/ldap_settings')
262 267
263 268 #ADMIN SETTINGS REST ROUTES
264 269 with rmap.submapper(path_prefix=ADMIN_PREFIX,
265 270 controller='admin/settings') as m:
266 271 m.connect("admin_settings", "/settings",
267 272 action="create", conditions=dict(method=["POST"]))
268 273 m.connect("admin_settings", "/settings",
269 274 action="index", conditions=dict(method=["GET"]))
270 275 m.connect("formatted_admin_settings", "/settings.{format}",
271 276 action="index", conditions=dict(method=["GET"]))
272 277 m.connect("admin_new_setting", "/settings/new",
273 278 action="new", conditions=dict(method=["GET"]))
274 279 m.connect("formatted_admin_new_setting", "/settings/new.{format}",
275 280 action="new", conditions=dict(method=["GET"]))
276 281 m.connect("/settings/{setting_id}",
277 282 action="update", conditions=dict(method=["PUT"]))
278 283 m.connect("/settings/{setting_id}",
279 284 action="delete", conditions=dict(method=["DELETE"]))
280 285 m.connect("admin_edit_setting", "/settings/{setting_id}/edit",
281 286 action="edit", conditions=dict(method=["GET"]))
282 287 m.connect("formatted_admin_edit_setting",
283 288 "/settings/{setting_id}.{format}/edit",
284 289 action="edit", conditions=dict(method=["GET"]))
285 290 m.connect("admin_setting", "/settings/{setting_id}",
286 291 action="show", conditions=dict(method=["GET"]))
287 292 m.connect("formatted_admin_setting", "/settings/{setting_id}.{format}",
288 293 action="show", conditions=dict(method=["GET"]))
289 294 m.connect("admin_settings_my_account", "/my_account",
290 295 action="my_account", conditions=dict(method=["GET"]))
291 296 m.connect("admin_settings_my_account_update", "/my_account_update",
292 297 action="my_account_update", conditions=dict(method=["PUT"]))
293 298 m.connect("admin_settings_create_repository", "/create_repository",
294 299 action="create_repository", conditions=dict(method=["GET"]))
300 m.connect("admin_settings_my_repos", "/my_account/repos",
301 action="my_account_my_repos", conditions=dict(method=["GET"]))
302 m.connect("admin_settings_my_pullrequests", "/my_account/pull_requests",
303 action="my_account_my_pullrequests", conditions=dict(method=["GET"]))
295 304
296 305 #NOTIFICATION REST ROUTES
297 306 with rmap.submapper(path_prefix=ADMIN_PREFIX,
298 307 controller='admin/notifications') as m:
299 308 m.connect("notifications", "/notifications",
300 309 action="create", conditions=dict(method=["POST"]))
301 310 m.connect("notifications", "/notifications",
302 311 action="index", conditions=dict(method=["GET"]))
303 312 m.connect("notifications_mark_all_read", "/notifications/mark_all_read",
304 313 action="mark_all_read", conditions=dict(method=["GET"]))
305 314 m.connect("formatted_notifications", "/notifications.{format}",
306 315 action="index", conditions=dict(method=["GET"]))
307 316 m.connect("new_notification", "/notifications/new",
308 317 action="new", conditions=dict(method=["GET"]))
309 318 m.connect("formatted_new_notification", "/notifications/new.{format}",
310 319 action="new", conditions=dict(method=["GET"]))
311 320 m.connect("/notification/{notification_id}",
312 321 action="update", conditions=dict(method=["PUT"]))
313 322 m.connect("/notification/{notification_id}",
314 323 action="delete", conditions=dict(method=["DELETE"]))
315 324 m.connect("edit_notification", "/notification/{notification_id}/edit",
316 325 action="edit", conditions=dict(method=["GET"]))
317 326 m.connect("formatted_edit_notification",
318 327 "/notification/{notification_id}.{format}/edit",
319 328 action="edit", conditions=dict(method=["GET"]))
320 329 m.connect("notification", "/notification/{notification_id}",
321 330 action="show", conditions=dict(method=["GET"]))
322 331 m.connect("formatted_notification", "/notifications/{notification_id}.{format}",
323 332 action="show", conditions=dict(method=["GET"]))
324 333
325 334 #ADMIN MAIN PAGES
326 335 with rmap.submapper(path_prefix=ADMIN_PREFIX,
327 336 controller='admin/admin') as m:
328 337 m.connect('admin_home', '', action='index')
329 338 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
330 339 action='add_repo')
331 340
332 341 #==========================================================================
333 342 # API V2
334 343 #==========================================================================
335 344 with rmap.submapper(path_prefix=ADMIN_PREFIX,
336 345 controller='api/api') as m:
337 346 m.connect('api', '/api')
338 347
339 348 #USER JOURNAL
340 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX, controller='journal')
349 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX,
350 controller='journal', action='index')
351 rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX,
352 controller='journal', action='journal_rss')
353 rmap.connect('journal_atom', '%s/journal/atom' % ADMIN_PREFIX,
354 controller='journal', action='journal_atom')
341 355
342 356 rmap.connect('public_journal', '%s/public_journal' % ADMIN_PREFIX,
343 357 controller='journal', action="public_journal")
344 358
345 rmap.connect('public_journal_rss', '%s/public_journal_rss' % ADMIN_PREFIX,
359 rmap.connect('public_journal_rss', '%s/public_journal/rss' % ADMIN_PREFIX,
360 controller='journal', action="public_journal_rss")
361
362 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % ADMIN_PREFIX,
346 363 controller='journal', action="public_journal_rss")
347 364
348 365 rmap.connect('public_journal_atom',
366 '%s/public_journal/atom' % ADMIN_PREFIX, controller='journal',
367 action="public_journal_atom")
368
369 rmap.connect('public_journal_atom_old',
349 370 '%s/public_journal_atom' % ADMIN_PREFIX, controller='journal',
350 371 action="public_journal_atom")
351 372
352 373 rmap.connect('toggle_following', '%s/toggle_following' % ADMIN_PREFIX,
353 374 controller='journal', action='toggle_following',
354 375 conditions=dict(method=["POST"]))
355 376
356 377 #SEARCH
357 378 rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
358 379 rmap.connect('search_repo', '%s/search/{search_repo:.*}' % ADMIN_PREFIX,
359 380 controller='search')
360 381
361 382 #LOGIN/LOGOUT/REGISTER/SIGN IN
362 383 rmap.connect('login_home', '%s/login' % ADMIN_PREFIX, controller='login')
363 384 rmap.connect('logout_home', '%s/logout' % ADMIN_PREFIX, controller='login',
364 385 action='logout')
365 386
366 387 rmap.connect('register', '%s/register' % ADMIN_PREFIX, controller='login',
367 388 action='register')
368 389
369 390 rmap.connect('reset_password', '%s/password_reset' % ADMIN_PREFIX,
370 391 controller='login', action='password_reset')
371 392
372 393 rmap.connect('reset_password_confirmation',
373 394 '%s/password_reset_confirmation' % ADMIN_PREFIX,
374 395 controller='login', action='password_reset_confirmation')
375 396
376 397 #FEEDS
377 rmap.connect('rss_feed_home', '/{repo_name:.*}/feed/rss',
398 rmap.connect('rss_feed_home', '/{repo_name:.*?}/feed/rss',
378 399 controller='feed', action='rss',
379 400 conditions=dict(function=check_repo))
380 401
381 rmap.connect('atom_feed_home', '/{repo_name:.*}/feed/atom',
402 rmap.connect('atom_feed_home', '/{repo_name:.*?}/feed/atom',
382 403 controller='feed', action='atom',
383 404 conditions=dict(function=check_repo))
384 405
385 406 #==========================================================================
386 407 # REPOSITORY ROUTES
387 408 #==========================================================================
388 rmap.connect('summary_home', '/{repo_name:.*}',
409 rmap.connect('summary_home', '/{repo_name:.*?}',
389 410 controller='summary',
390 411 conditions=dict(function=check_repo))
391 412
392 413 rmap.connect('repos_group_home', '/{group_name:.*}',
393 414 controller='admin/repos_groups', action="show_by_name",
394 415 conditions=dict(function=check_group))
395 416
396 rmap.connect('changeset_home', '/{repo_name:.*}/changeset/{revision}',
417 rmap.connect('changeset_home', '/{repo_name:.*?}/changeset/{revision}',
397 418 controller='changeset', revision='tip',
398 419 conditions=dict(function=check_repo))
399 420
400 421 rmap.connect('changeset_comment',
401 '/{repo_name:.*}/changeset/{revision}/comment',
422 '/{repo_name:.*?}/changeset/{revision}/comment',
402 423 controller='changeset', revision='tip', action='comment',
403 424 conditions=dict(function=check_repo))
404 425
405 426 rmap.connect('changeset_comment_delete',
406 '/{repo_name:.*}/changeset/comment/{comment_id}/delete',
427 '/{repo_name:.*?}/changeset/comment/{comment_id}/delete',
407 428 controller='changeset', action='delete_comment',
408 429 conditions=dict(function=check_repo, method=["DELETE"]))
409 430
410 431 rmap.connect('raw_changeset_home',
411 '/{repo_name:.*}/raw-changeset/{revision}',
432 '/{repo_name:.*?}/raw-changeset/{revision}',
412 433 controller='changeset', action='raw_changeset',
413 434 revision='tip', conditions=dict(function=check_repo))
414 435
415 rmap.connect('summary_home', '/{repo_name:.*}/summary',
436 rmap.connect('compare_url',
437 '/{repo_name:.*?}/compare/{org_ref_type}@{org_ref}...{other_ref_type}@{other_ref}',
438 controller='compare', action='index',
439 conditions=dict(function=check_repo),
440 requirements=dict(
441 org_ref_type='(branch|book|tag|rev|org_ref_type)',
442 other_ref_type='(branch|book|tag|rev|other_ref_type)')
443 )
444
445 rmap.connect('pullrequest_home',
446 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
447 action='index', conditions=dict(function=check_repo,
448 method=["GET"]))
449
450 rmap.connect('pullrequest',
451 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
452 action='create', conditions=dict(function=check_repo,
453 method=["POST"]))
454
455 rmap.connect('pullrequest_show',
456 '/{repo_name:.*?}/pull-request/{pull_request_id}',
457 controller='pullrequests',
458 action='show', conditions=dict(function=check_repo,
459 method=["GET"]))
460 rmap.connect('pullrequest_update',
461 '/{repo_name:.*?}/pull-request/{pull_request_id}',
462 controller='pullrequests',
463 action='update', conditions=dict(function=check_repo,
464 method=["PUT"]))
465 rmap.connect('pullrequest_delete',
466 '/{repo_name:.*?}/pull-request/{pull_request_id}',
467 controller='pullrequests',
468 action='delete', conditions=dict(function=check_repo,
469 method=["DELETE"]))
470
471 rmap.connect('pullrequest_show_all',
472 '/{repo_name:.*?}/pull-request',
473 controller='pullrequests',
474 action='show_all', conditions=dict(function=check_repo,
475 method=["GET"]))
476
477 rmap.connect('pullrequest_comment',
478 '/{repo_name:.*?}/pull-request-comment/{pull_request_id}',
479 controller='pullrequests',
480 action='comment', conditions=dict(function=check_repo,
481 method=["POST"]))
482
483 rmap.connect('pullrequest_comment_delete',
484 '/{repo_name:.*?}/pull-request-comment/{comment_id}/delete',
485 controller='pullrequests', action='delete_comment',
486 conditions=dict(function=check_repo, method=["DELETE"]))
487
488 rmap.connect('summary_home', '/{repo_name:.*?}/summary',
416 489 controller='summary', conditions=dict(function=check_repo))
417 490
418 rmap.connect('shortlog_home', '/{repo_name:.*}/shortlog',
491 rmap.connect('shortlog_home', '/{repo_name:.*?}/shortlog',
419 492 controller='shortlog', conditions=dict(function=check_repo))
420 493
421 rmap.connect('branches_home', '/{repo_name:.*}/branches',
494 rmap.connect('branches_home', '/{repo_name:.*?}/branches',
422 495 controller='branches', conditions=dict(function=check_repo))
423 496
424 rmap.connect('tags_home', '/{repo_name:.*}/tags',
497 rmap.connect('tags_home', '/{repo_name:.*?}/tags',
425 498 controller='tags', conditions=dict(function=check_repo))
426 499
427 rmap.connect('bookmarks_home', '/{repo_name:.*}/bookmarks',
500 rmap.connect('bookmarks_home', '/{repo_name:.*?}/bookmarks',
428 501 controller='bookmarks', conditions=dict(function=check_repo))
429 502
430 rmap.connect('changelog_home', '/{repo_name:.*}/changelog',
503 rmap.connect('changelog_home', '/{repo_name:.*?}/changelog',
431 504 controller='changelog', conditions=dict(function=check_repo))
432 505
433 rmap.connect('changelog_details', '/{repo_name:.*}/changelog_details/{cs}',
506 rmap.connect('changelog_details', '/{repo_name:.*?}/changelog_details/{cs}',
434 507 controller='changelog', action='changelog_details',
435 508 conditions=dict(function=check_repo))
436 509
437 rmap.connect('files_home', '/{repo_name:.*}/files/{revision}/{f_path:.*}',
510 rmap.connect('files_home', '/{repo_name:.*?}/files/{revision}/{f_path:.*}',
438 511 controller='files', revision='tip', f_path='',
439 512 conditions=dict(function=check_repo))
440 513
441 rmap.connect('files_diff_home', '/{repo_name:.*}/diff/{f_path:.*}',
514 rmap.connect('files_diff_home', '/{repo_name:.*?}/diff/{f_path:.*}',
442 515 controller='files', action='diff', revision='tip', f_path='',
443 516 conditions=dict(function=check_repo))
444 517
445 518 rmap.connect('files_rawfile_home',
446 '/{repo_name:.*}/rawfile/{revision}/{f_path:.*}',
519 '/{repo_name:.*?}/rawfile/{revision}/{f_path:.*}',
447 520 controller='files', action='rawfile', revision='tip',
448 521 f_path='', conditions=dict(function=check_repo))
449 522
450 523 rmap.connect('files_raw_home',
451 '/{repo_name:.*}/raw/{revision}/{f_path:.*}',
524 '/{repo_name:.*?}/raw/{revision}/{f_path:.*}',
452 525 controller='files', action='raw', revision='tip', f_path='',
453 526 conditions=dict(function=check_repo))
454 527
455 528 rmap.connect('files_annotate_home',
456 '/{repo_name:.*}/annotate/{revision}/{f_path:.*}',
529 '/{repo_name:.*?}/annotate/{revision}/{f_path:.*}',
457 530 controller='files', action='index', revision='tip',
458 531 f_path='', annotate=True, conditions=dict(function=check_repo))
459 532
460 533 rmap.connect('files_edit_home',
461 '/{repo_name:.*}/edit/{revision}/{f_path:.*}',
534 '/{repo_name:.*?}/edit/{revision}/{f_path:.*}',
462 535 controller='files', action='edit', revision='tip',
463 536 f_path='', conditions=dict(function=check_repo))
464 537
465 538 rmap.connect('files_add_home',
466 '/{repo_name:.*}/add/{revision}/{f_path:.*}',
539 '/{repo_name:.*?}/add/{revision}/{f_path:.*}',
467 540 controller='files', action='add', revision='tip',
468 541 f_path='', conditions=dict(function=check_repo))
469 542
470 rmap.connect('files_archive_home', '/{repo_name:.*}/archive/{fname}',
543 rmap.connect('files_archive_home', '/{repo_name:.*?}/archive/{fname}',
471 544 controller='files', action='archivefile',
472 545 conditions=dict(function=check_repo))
473 546
474 547 rmap.connect('files_nodelist_home',
475 '/{repo_name:.*}/nodelist/{revision}/{f_path:.*}',
548 '/{repo_name:.*?}/nodelist/{revision}/{f_path:.*}',
476 549 controller='files', action='nodelist',
477 550 conditions=dict(function=check_repo))
478 551
479 rmap.connect('repo_settings_delete', '/{repo_name:.*}/settings',
552 rmap.connect('repo_settings_delete', '/{repo_name:.*?}/settings',
480 553 controller='settings', action="delete",
481 554 conditions=dict(method=["DELETE"], function=check_repo))
482 555
483 rmap.connect('repo_settings_update', '/{repo_name:.*}/settings',
556 rmap.connect('repo_settings_update', '/{repo_name:.*?}/settings',
484 557 controller='settings', action="update",
485 558 conditions=dict(method=["PUT"], function=check_repo))
486 559
487 rmap.connect('repo_settings_home', '/{repo_name:.*}/settings',
560 rmap.connect('repo_settings_home', '/{repo_name:.*?}/settings',
488 561 controller='settings', action='index',
489 562 conditions=dict(function=check_repo))
490 563
491 rmap.connect('repo_fork_create_home', '/{repo_name:.*}/fork',
564 rmap.connect('repo_fork_create_home', '/{repo_name:.*?}/fork',
492 565 controller='forks', action='fork_create',
493 566 conditions=dict(function=check_repo, method=["POST"]))
494 567
495 rmap.connect('repo_fork_home', '/{repo_name:.*}/fork',
568 rmap.connect('repo_fork_home', '/{repo_name:.*?}/fork',
496 569 controller='forks', action='fork',
497 570 conditions=dict(function=check_repo))
498 571
499 rmap.connect('repo_forks_home', '/{repo_name:.*}/forks',
572 rmap.connect('repo_forks_home', '/{repo_name:.*?}/forks',
500 573 controller='forks', action='forks',
501 574 conditions=dict(function=check_repo))
502 575
503 rmap.connect('repo_followers_home', '/{repo_name:.*}/followers',
576 rmap.connect('repo_followers_home', '/{repo_name:.*?}/followers',
504 577 controller='followers', action='followers',
505 578 conditions=dict(function=check_repo))
506 579
507 580 return rmap
@@ -1,59 +1,59 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
29 29 from sqlalchemy.orm import joinedload
30 30 from webhelpers.paginate import Page
31 31
32 32 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
33 33 from rhodecode.lib.base import BaseController, render
34 34 from rhodecode.model.db import UserLog
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class AdminController(BaseController):
40 40
41 41 @LoginRequired()
42 42 def __before__(self):
43 43 super(AdminController, self).__before__()
44 44
45 45 @HasPermissionAllDecorator('hg.admin')
46 46 def index(self):
47 47
48 users_log = self.sa.query(UserLog)\
48 users_log = UserLog.query()\
49 49 .options(joinedload(UserLog.user))\
50 50 .options(joinedload(UserLog.repository))\
51 51 .order_by(UserLog.action_date.desc())
52 52
53 53 p = int(request.params.get('page', 1))
54 54 c.users_log = Page(users_log, page=p, items_per_page=10)
55 55 c.log_data = render('admin/admin_log.html')
56 56
57 57 if request.environ.get('HTTP_X_PARTIAL_XHR'):
58 58 return c.log_data
59 59 return render('admin/admin.html')
@@ -1,149 +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 from rhodecode.model.meta import Session
43 44
44 45 log = logging.getLogger(__name__)
45 46
46 47
47 48 class LdapSettingsController(BaseController):
48 49
49 50 search_scope_choices = [('BASE', _('BASE'),),
50 51 ('ONELEVEL', _('ONELEVEL'),),
51 52 ('SUBTREE', _('SUBTREE'),),
52 53 ]
53 54 search_scope_default = 'SUBTREE'
54 55
55 56 tls_reqcert_choices = [('NEVER', _('NEVER'),),
56 57 ('ALLOW', _('ALLOW'),),
57 58 ('TRY', _('TRY'),),
58 59 ('DEMAND', _('DEMAND'),),
59 60 ('HARD', _('HARD'),),
60 61 ]
61 62 tls_reqcert_default = 'DEMAND'
62 63
63 64 tls_kind_choices = [('PLAIN', _('No encryption'),),
64 65 ('LDAPS', _('LDAPS connection'),),
65 66 ('START_TLS', _('START_TLS on LDAP connection'),)
66 67 ]
67 68
68 69 tls_kind_default = 'PLAIN'
69 70
70 71 @LoginRequired()
71 72 @HasPermissionAllDecorator('hg.admin')
72 73 def __before__(self):
73 74 c.admin_user = session.get('admin_user')
74 75 c.admin_username = session.get('admin_username')
75 76 c.search_scope_choices = self.search_scope_choices
76 77 c.tls_reqcert_choices = self.tls_reqcert_choices
77 78 c.tls_kind_choices = self.tls_kind_choices
78 79
79 80 c.search_scope_cur = self.search_scope_default
80 81 c.tls_reqcert_cur = self.tls_reqcert_default
81 82 c.tls_kind_cur = self.tls_kind_default
82 83
83 84 super(LdapSettingsController, self).__before__()
84 85
85 86 def index(self):
86 87 defaults = RhodeCodeSetting.get_ldap_settings()
87 88 c.search_scope_cur = defaults.get('ldap_search_scope')
88 89 c.tls_reqcert_cur = defaults.get('ldap_tls_reqcert')
89 90 c.tls_kind_cur = defaults.get('ldap_tls_kind')
90 91
91 92 return htmlfill.render(
92 93 render('admin/ldap/ldap.html'),
93 94 defaults=defaults,
94 95 encoding="UTF-8",
95 96 force_defaults=True,)
96 97
97 98 def ldap_settings(self):
98 99 """POST ldap create and store ldap settings"""
99 100
100 101 _form = LdapSettingsForm([x[0] for x in self.tls_reqcert_choices],
101 102 [x[0] for x in self.search_scope_choices],
102 103 [x[0] for x in self.tls_kind_choices])()
103 104 # check the ldap lib
104 105 ldap_active = False
105 106 try:
106 107 import ldap
107 108 ldap_active = True
108 109 except ImportError:
109 110 pass
110 111
111 112 try:
112 113 form_result = _form.to_python(dict(request.POST))
113 114
114 115 try:
115 116
116 117 for k, v in form_result.items():
117 118 if k.startswith('ldap_'):
118 119 if k == 'ldap_active':
119 120 v = ldap_active
120 121 setting = RhodeCodeSetting.get_by_name(k)
121 122 setting.app_settings_value = v
122 self.sa.add(setting)
123 Session().add(setting)
123 124
124 self.sa.commit()
125 Session().commit()
125 126 h.flash(_('Ldap settings updated successfully'),
126 127 category='success')
127 128 if not ldap_active:
128 129 #if ldap is missing send an info to user
129 130 h.flash(_('Unable to activate ldap. The "python-ldap" library '
130 131 'is missing.'), category='warning')
131 132
132 133 except (DatabaseError,):
133 134 raise
134 135
135 136 except formencode.Invalid, errors:
136 137 e = errors.error_dict or {}
137 138
138 139 return htmlfill.render(
139 140 render('admin/ldap/ldap.html'),
140 141 defaults=errors.value,
141 142 errors=e,
142 143 prefix_error=False,
143 144 encoding="UTF-8")
144 145 except Exception:
145 146 log.error(traceback.format_exc())
146 147 h.flash(_('error occurred during update of ldap settings'),
147 148 category='error')
148 149
149 150 return redirect(url('ldap_home'))
@@ -1,144 +1,170 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 31 from pylons.controllers.util import redirect
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
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class NotificationsController(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('notification', 'notifications', controller='_admin/notifications',
52 52 # path_prefix='/_admin', name_prefix='_admin_')
53 53
54 54 @LoginRequired()
55 55 @NotAnonymous()
56 56 def __before__(self):
57 57 super(NotificationsController, self).__before__()
58 58
59 59 def index(self, format='html'):
60 60 """GET /_admin/notifications: All items in the collection"""
61 61 # url('notifications')
62 62 c.user = self.rhodecode_user
63 notif = NotificationModel().get_for_user(self.rhodecode_user.user_id)
63 notif = NotificationModel().get_for_user(self.rhodecode_user.user_id,
64 filter_=request.GET.getall('type'))
64 65 p = int(request.params.get('page', 1))
65 66 c.notifications = Page(notif, page=p, items_per_page=10)
67 c.pull_request_type = Notification.TYPE_PULL_REQUEST
68 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
69 Notification.TYPE_PULL_REQUEST_COMMENT]
70
71 _current_filter = request.GET.getall('type')
72 c.current_filter = 'all'
73 if _current_filter == [c.pull_request_type]:
74 c.current_filter = 'pull_request'
75 elif _current_filter == c.comment_type:
76 c.current_filter = 'comment'
77
66 78 return render('admin/notifications/notifications.html')
67 79
68 80 def mark_all_read(self):
69 81 if request.environ.get('HTTP_X_PARTIAL_XHR'):
70 82 nm = NotificationModel()
71 83 # mark all read
72 nm.mark_all_read_for_user(self.rhodecode_user.user_id)
73 Session.commit()
84 nm.mark_all_read_for_user(self.rhodecode_user.user_id,
85 filter_=request.GET.getall('type'))
86 Session().commit()
74 87 c.user = self.rhodecode_user
75 notif = nm.get_for_user(self.rhodecode_user.user_id)
88 notif = nm.get_for_user(self.rhodecode_user.user_id,
89 filter_=request.GET.getall('type'))
76 90 c.notifications = Page(notif, page=1, items_per_page=10)
77 91 return render('admin/notifications/notifications_data.html')
78 92
79 93 def create(self):
80 94 """POST /_admin/notifications: Create a new item"""
81 95 # url('notifications')
82 96
83 97 def new(self, format='html'):
84 98 """GET /_admin/notifications/new: Form to create a new item"""
85 99 # url('new_notification')
86 100
87 101 def update(self, notification_id):
88 102 """PUT /_admin/notifications/id: Update an existing item"""
89 103 # Forms posted to this method should contain a hidden field:
90 104 # <input type="hidden" name="_method" value="PUT" />
91 105 # Or using helpers:
92 106 # h.form(url('notification', notification_id=ID),
93 107 # method='put')
94 108 # url('notification', notification_id=ID)
109 try:
110 no = Notification.get(notification_id)
111 owner = lambda: (no.notifications_to_users.user.user_id
112 == c.rhodecode_user.user_id)
113 if h.HasPermissionAny('hg.admin')() or owner:
114 NotificationModel().mark_read(c.rhodecode_user.user_id, no)
115 Session().commit()
116 return 'ok'
117 except Exception:
118 Session.rollback()
119 log.error(traceback.format_exc())
120 return 'fail'
95 121
96 122 def delete(self, notification_id):
97 123 """DELETE /_admin/notifications/id: Delete an existing item"""
98 124 # Forms posted to this method should contain a hidden field:
99 125 # <input type="hidden" name="_method" value="DELETE" />
100 126 # Or using helpers:
101 127 # h.form(url('notification', notification_id=ID),
102 128 # method='delete')
103 129 # url('notification', notification_id=ID)
104 130
105 131 try:
106 132 no = Notification.get(notification_id)
107 133 owner = lambda: (no.notifications_to_users.user.user_id
108 134 == c.rhodecode_user.user_id)
109 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
135 if h.HasPermissionAny('hg.admin')() or owner:
110 136 NotificationModel().delete(c.rhodecode_user.user_id, no)
111 Session.commit()
137 Session().commit()
112 138 return 'ok'
113 139 except Exception:
114 140 Session.rollback()
115 141 log.error(traceback.format_exc())
116 142 return 'fail'
117 143
118 144 def show(self, notification_id, format='html'):
119 145 """GET /_admin/notifications/id: Show a specific item"""
120 146 # url('notification', notification_id=ID)
121 147 c.user = self.rhodecode_user
122 148 no = Notification.get(notification_id)
123 149
124 150 owner = lambda: (no.notifications_to_users.user.user_id
125 151 == c.user.user_id)
126 152 if no and (h.HasPermissionAny('hg.admin', 'repository.admin')() or owner):
127 153 unotification = NotificationModel()\
128 154 .get_user_notification(c.user.user_id, no)
129 155
130 156 # if this association to user is not valid, we don't want to show
131 157 # this message
132 158 if unotification:
133 159 if unotification.read is False:
134 160 unotification.mark_as_read()
135 Session.commit()
161 Session().commit()
136 162 c.notification = no
137 163
138 164 return render('admin/notifications/show_notification.html')
139 165
140 166 return redirect(url('notifications'))
141 167
142 168 def edit(self, notification_id, format='html'):
143 169 """GET /_admin/notifications/id/edit: Form to edit an existing item"""
144 170 # url('edit_notification', notification_id=ID)
@@ -1,169 +1,178 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 from rhodecode.lib.base import BaseController, render
38 38 from rhodecode.model.forms import DefaultPermissionsForm
39 39 from rhodecode.model.permission import PermissionModel
40 40 from rhodecode.model.db import User
41 41 from rhodecode.model.meta import Session
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class PermissionsController(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('permission', 'permissions')
51 51
52 52 @LoginRequired()
53 53 @HasPermissionAllDecorator('hg.admin')
54 54 def __before__(self):
55 55 c.admin_user = session.get('admin_user')
56 56 c.admin_username = session.get('admin_username')
57 57 super(PermissionsController, self).__before__()
58 58
59 59 self.perms_choices = [('repository.none', _('None'),),
60 60 ('repository.read', _('Read'),),
61 61 ('repository.write', _('Write'),),
62 62 ('repository.admin', _('Admin'),)]
63 63 self.register_choices = [
64 64 ('hg.register.none',
65 65 _('disabled')),
66 66 ('hg.register.manual_activate',
67 67 _('allowed with manual account activation')),
68 68 ('hg.register.auto_activate',
69 69 _('allowed with automatic account activation')), ]
70 70
71 71 self.create_choices = [('hg.create.none', _('Disabled')),
72 72 ('hg.create.repository', _('Enabled'))]
73 73
74 self.fork_choices = [('hg.fork.none', _('Disabled')),
75 ('hg.fork.repository', _('Enabled'))]
76
77 # set the global template variables
78 c.perms_choices = self.perms_choices
79 c.register_choices = self.register_choices
80 c.create_choices = self.create_choices
81 c.fork_choices = self.fork_choices
82
74 83 def index(self, format='html'):
75 84 """GET /permissions: All items in the collection"""
76 85 # url('permissions')
77 86
78 87 def create(self):
79 88 """POST /permissions: Create a new item"""
80 89 # url('permissions')
81 90
82 91 def new(self, format='html'):
83 92 """GET /permissions/new: Form to create a new item"""
84 93 # url('new_permission')
85 94
86 95 def update(self, id):
87 96 """PUT /permissions/id: Update an existing item"""
88 97 # Forms posted to this method should contain a hidden field:
89 98 # <input type="hidden" name="_method" value="PUT" />
90 99 # Or using helpers:
91 100 # h.form(url('permission', id=ID),
92 101 # method='put')
93 102 # url('permission', id=ID)
94 103
95 104 permission_model = PermissionModel()
96 105
97 106 _form = DefaultPermissionsForm([x[0] for x in self.perms_choices],
98 107 [x[0] for x in self.register_choices],
99 [x[0] for x in self.create_choices])()
108 [x[0] for x in self.create_choices],
109 [x[0] for x in self.fork_choices])()
100 110
101 111 try:
102 112 form_result = _form.to_python(dict(request.POST))
103 113 form_result.update({'perm_user_name': id})
104 114 permission_model.update(form_result)
105 Session.commit()
115 Session().commit()
106 116 h.flash(_('Default permissions updated successfully'),
107 117 category='success')
108 118
109 119 except formencode.Invalid, errors:
110 c.perms_choices = self.perms_choices
111 c.register_choices = self.register_choices
112 c.create_choices = self.create_choices
113 120 defaults = errors.value
114 121
115 122 return htmlfill.render(
116 123 render('admin/permissions/permissions.html'),
117 124 defaults=defaults,
118 125 errors=errors.error_dict or {},
119 126 prefix_error=False,
120 127 encoding="UTF-8")
121 128 except Exception:
122 129 log.error(traceback.format_exc())
123 130 h.flash(_('error occurred during update of permissions'),
124 131 category='error')
125 132
126 133 return redirect(url('edit_permission', id=id))
127 134
128 135 def delete(self, id):
129 136 """DELETE /permissions/id: Delete an existing item"""
130 137 # Forms posted to this method should contain a hidden field:
131 138 # <input type="hidden" name="_method" value="DELETE" />
132 139 # Or using helpers:
133 140 # h.form(url('permission', id=ID),
134 141 # method='delete')
135 142 # url('permission', id=ID)
136 143
137 144 def show(self, id, format='html'):
138 145 """GET /permissions/id: Show a specific item"""
139 146 # url('permission', id=ID)
140 147
141 148 def edit(self, id, format='html'):
142 149 """GET /permissions/id/edit: Form to edit an existing item"""
143 150 #url('edit_permission', id=ID)
144 c.perms_choices = self.perms_choices
145 c.register_choices = self.register_choices
146 c.create_choices = self.create_choices
147 151
152 #this form can only edit default user permissions
148 153 if id == 'default':
149 154 default_user = User.get_by_username('default')
150 155 defaults = {'_method': 'put',
151 156 'anonymous': default_user.active}
152 157
153 158 for p in default_user.user_perms:
154 159 if p.permission.permission_name.startswith('repository.'):
155 160 defaults['default_perm'] = p.permission.permission_name
156 161
157 162 if p.permission.permission_name.startswith('hg.register.'):
158 163 defaults['default_register'] = p.permission.permission_name
159 164
160 165 if p.permission.permission_name.startswith('hg.create.'):
161 166 defaults['default_create'] = p.permission.permission_name
162 167
168 if p.permission.permission_name.startswith('hg.fork.'):
169 defaults['default_fork'] = p.permission.permission_name
170
163 171 return htmlfill.render(
164 172 render('admin/permissions/permissions.html'),
165 173 defaults=defaults,
166 174 encoding="UTF-8",
167 force_defaults=True,)
175 force_defaults=True,
176 )
168 177 else:
169 178 return redirect(url('admin_home'))
@@ -1,432 +1,509 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 paste.httpexceptions import HTTPInternalServerError
31 from webob.exc import HTTPInternalServerError
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 import rhodecode
37 38 from rhodecode.lib import helpers as h
38 39 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
39 40 HasPermissionAnyDecorator, HasRepoPermissionAllDecorator
40 41 from rhodecode.lib.base import BaseController, render
41 42 from rhodecode.lib.utils import invalidate_cache, action_logger, repo_name_slug
42 43 from rhodecode.lib.helpers import get_token
43 44 from rhodecode.model.meta import Session
44 45 from rhodecode.model.db import User, Repository, UserFollowing, RepoGroup
45 46 from rhodecode.model.forms import RepoForm
46 47 from rhodecode.model.scm import ScmModel
47 48 from rhodecode.model.repo import RepoModel
49 from rhodecode.lib.compat import json
48 50
49 51 log = logging.getLogger(__name__)
50 52
51 53
52 54 class ReposController(BaseController):
53 55 """
54 56 REST Controller styled on the Atom Publishing Protocol"""
55 57 # To properly map this controller, ensure your config/routing.py
56 58 # file has a resource setup:
57 59 # map.resource('repo', 'repos')
58 60
59 61 @LoginRequired()
60 62 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
61 63 def __before__(self):
62 64 c.admin_user = session.get('admin_user')
63 65 c.admin_username = session.get('admin_username')
64 66 super(ReposController, self).__before__()
65 67
66 68 def __load_defaults(self):
67 69 c.repo_groups = RepoGroup.groups_choices()
68 70 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
69 71
70 72 repo_model = RepoModel()
71 73 c.users_array = repo_model.get_users_js()
72 74 c.users_groups_array = repo_model.get_users_groups_js()
75 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
76 c.landing_revs_choices = choices
73 77
74 78 def __load_data(self, repo_name=None):
75 79 """
76 80 Load defaults settings for edit, and update
77 81
78 82 :param repo_name:
79 83 """
80 84 self.__load_defaults()
81 85
82 86 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
83 87 repo = db_repo.scm_instance
84 88
85 89 if c.repo_info is None:
86 90 h.flash(_('%s repository is not mapped to db perhaps'
87 91 ' it was created or renamed from the filesystem'
88 92 ' please run the application again'
89 93 ' in order to rescan repositories') % repo_name,
90 94 category='error')
91 95
92 96 return redirect(url('repos'))
93 97
98 choices, c.landing_revs = ScmModel().get_repo_landing_revs(c.repo_info)
99 c.landing_revs_choices = choices
100
94 101 c.default_user_id = User.get_by_username('default').user_id
95 102 c.in_public_journal = UserFollowing.query()\
96 103 .filter(UserFollowing.user_id == c.default_user_id)\
97 104 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
98 105
99 106 if c.repo_info.stats:
100 107 # this is on what revision we ended up so we add +1 for count
101 108 last_rev = c.repo_info.stats.stat_on_revision + 1
102 109 else:
103 110 last_rev = 0
104 111 c.stats_revision = last_rev
105 112
106 113 c.repo_last_rev = repo.count() if repo.revisions else 0
107 114
108 115 if last_rev == 0 or c.repo_last_rev == 0:
109 116 c.stats_percentage = 0
110 117 else:
111 118 c.stats_percentage = '%.2f' % ((float((last_rev)) /
112 119 c.repo_last_rev) * 100)
113 120
114 121 defaults = RepoModel()._get_defaults(repo_name)
115 122
116 123 c.repos_list = [('', _('--REMOVE FORK--'))]
117 124 c.repos_list += [(x.repo_id, x.repo_name) for x in
118 Repository.query().order_by(Repository.repo_name).all()]
125 Repository.query().order_by(Repository.repo_name).all()
126 if x.repo_id != c.repo_info.repo_id]
127
128 defaults['id_fork_of'] = db_repo.fork.repo_id if db_repo.fork else ''
119 129 return defaults
120 130
121 131 @HasPermissionAllDecorator('hg.admin')
122 132 def index(self, format='html'):
123 133 """GET /repos: All items in the collection"""
124 134 # url('repos')
125 135
126 c.repos_list = ScmModel().get_repos(Repository.query()
127 .order_by(Repository.repo_name)
128 .all(), sort_key='name_sort')
136 c.repos_list = Repository.query()\
137 .order_by(Repository.repo_name)\
138 .all()
139
140 repos_data = []
141 total_records = len(c.repos_list)
142
143 _tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
144 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
145
146 quick_menu = lambda repo_name: (template.get_def("quick_menu")
147 .render(repo_name, _=_, h=h, c=c))
148 repo_lnk = lambda name, rtype, private, fork_of: (
149 template.get_def("repo_name")
150 .render(name, rtype, private, fork_of, short_name=False,
151 admin=True, _=_, h=h, c=c))
152
153 repo_actions = lambda repo_name: (template.get_def("repo_actions")
154 .render(repo_name, _=_, h=h, c=c))
155
156 for repo in c.repos_list:
157 repos_data.append({
158 "menu": quick_menu(repo.repo_name),
159 "raw_name": repo.repo_name,
160 "name": repo_lnk(repo.repo_name, repo.repo_type,
161 repo.private, repo.fork),
162 "desc": repo.description,
163 "owner": repo.user.username,
164 "action": repo_actions(repo.repo_name),
165 })
166
167 c.data = json.dumps({
168 "totalRecords": total_records,
169 "startIndex": 0,
170 "sort": "name",
171 "dir": "asc",
172 "records": repos_data
173 })
174
129 175 return render('admin/repos/repos.html')
130 176
131 177 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
132 178 def create(self):
133 179 """
134 180 POST /repos: Create a new item"""
135 181 # url('repos')
136 182
137 183 self.__load_defaults()
138 184 form_result = {}
139 185 try:
140 form_result = RepoForm(repo_groups=c.repo_groups_choices)()\
186 form_result = RepoForm(repo_groups=c.repo_groups_choices,
187 landing_revs=c.landing_revs_choices)()\
141 188 .to_python(dict(request.POST))
142 RepoModel().create(form_result, self.rhodecode_user)
189 new_repo = RepoModel().create(form_result,
190 self.rhodecode_user.user_id)
143 191 if form_result['clone_uri']:
144 192 h.flash(_('created repository %s from %s') \
145 193 % (form_result['repo_name'], form_result['clone_uri']),
146 194 category='success')
147 195 else:
148 196 h.flash(_('created repository %s') % form_result['repo_name'],
149 197 category='success')
150 198
151 199 if request.POST.get('user_created'):
152 200 # created by regular non admin user
153 201 action_logger(self.rhodecode_user, 'user_created_repo',
154 form_result['repo_name_full'], '', self.sa)
202 form_result['repo_name_full'], self.ip_addr,
203 self.sa)
155 204 else:
156 205 action_logger(self.rhodecode_user, 'admin_created_repo',
157 form_result['repo_name_full'], '', self.sa)
158 Session.commit()
206 form_result['repo_name_full'], self.ip_addr,
207 self.sa)
208 Session().commit()
159 209 except formencode.Invalid, errors:
160 210
161 211 c.new_repo = errors.value['repo_name']
162 212
163 213 if request.POST.get('user_created'):
164 214 r = render('admin/repos/repo_add_create_repository.html')
165 215 else:
166 216 r = render('admin/repos/repo_add.html')
167 217
168 218 return htmlfill.render(
169 219 r,
170 220 defaults=errors.value,
171 221 errors=errors.error_dict or {},
172 222 prefix_error=False,
173 223 encoding="UTF-8")
174 224
175 225 except Exception:
176 226 log.error(traceback.format_exc())
177 227 msg = _('error occurred during creation of repository %s') \
178 228 % form_result.get('repo_name')
179 229 h.flash(msg, category='error')
180 if request.POST.get('user_created'):
181 return redirect(url('home'))
182 230 return redirect(url('repos'))
231 #redirect to our new repo !
232 return redirect(url('summary_home', repo_name=new_repo.repo_name))
183 233
184 234 @HasPermissionAllDecorator('hg.admin')
185 235 def new(self, format='html'):
186 236 """GET /repos/new: Form to create a new item"""
187 237 new_repo = request.GET.get('repo', '')
188 238 c.new_repo = repo_name_slug(new_repo)
189 239 self.__load_defaults()
190 240 return render('admin/repos/repo_add.html')
191 241
192 242 @HasPermissionAllDecorator('hg.admin')
193 243 def update(self, repo_name):
194 244 """
195 245 PUT /repos/repo_name: Update an existing item"""
196 246 # Forms posted to this method should contain a hidden field:
197 247 # <input type="hidden" name="_method" value="PUT" />
198 248 # Or using helpers:
199 249 # h.form(url('repo', repo_name=ID),
200 250 # method='put')
201 251 # url('repo', repo_name=ID)
202 252 self.__load_defaults()
203 253 repo_model = RepoModel()
204 254 changed_name = repo_name
255 #override the choices with extracted revisions !
256 choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo_name)
257 c.landing_revs_choices = choices
258
205 259 _form = RepoForm(edit=True, old_data={'repo_name': repo_name},
206 repo_groups=c.repo_groups_choices)()
260 repo_groups=c.repo_groups_choices,
261 landing_revs=c.landing_revs_choices)()
207 262 try:
208 263 form_result = _form.to_python(dict(request.POST))
209 264 repo = repo_model.update(repo_name, form_result)
210 265 invalidate_cache('get_repo_cached_%s' % repo_name)
211 h.flash(_('Repository %s updated successfully' % repo_name),
266 h.flash(_('Repository %s updated successfully') % repo_name,
212 267 category='success')
213 268 changed_name = repo.repo_name
214 269 action_logger(self.rhodecode_user, 'admin_updated_repo',
215 changed_name, '', self.sa)
216 Session.commit()
270 changed_name, self.ip_addr, self.sa)
271 Session().commit()
217 272 except formencode.Invalid, errors:
218 273 defaults = self.__load_data(repo_name)
219 274 defaults.update(errors.value)
220 275 return htmlfill.render(
221 276 render('admin/repos/repo_edit.html'),
222 277 defaults=defaults,
223 278 errors=errors.error_dict or {},
224 279 prefix_error=False,
225 280 encoding="UTF-8")
226 281
227 282 except Exception:
228 283 log.error(traceback.format_exc())
229 284 h.flash(_('error occurred during update of repository %s') \
230 285 % repo_name, category='error')
231 286 return redirect(url('edit_repo', repo_name=changed_name))
232 287
233 288 @HasPermissionAllDecorator('hg.admin')
234 289 def delete(self, repo_name):
235 290 """
236 291 DELETE /repos/repo_name: Delete an existing item"""
237 292 # Forms posted to this method should contain a hidden field:
238 293 # <input type="hidden" name="_method" value="DELETE" />
239 294 # Or using helpers:
240 295 # h.form(url('repo', repo_name=ID),
241 296 # method='delete')
242 297 # url('repo', repo_name=ID)
243 298
244 299 repo_model = RepoModel()
245 300 repo = repo_model.get_by_repo_name(repo_name)
246 301 if not repo:
247 302 h.flash(_('%s repository is not mapped to db perhaps'
248 303 ' it was moved or renamed from the filesystem'
249 304 ' please run the application again'
250 305 ' in order to rescan repositories') % repo_name,
251 306 category='error')
252 307
253 308 return redirect(url('repos'))
254 309 try:
255 310 action_logger(self.rhodecode_user, 'admin_deleted_repo',
256 repo_name, '', self.sa)
311 repo_name, self.ip_addr, self.sa)
257 312 repo_model.delete(repo)
258 313 invalidate_cache('get_repo_cached_%s' % repo_name)
259 314 h.flash(_('deleted repository %s') % repo_name, category='success')
260 Session.commit()
315 Session().commit()
261 316 except IntegrityError, e:
262 317 if e.message.find('repositories_fork_id_fkey') != -1:
263 318 log.error(traceback.format_exc())
264 319 h.flash(_('Cannot delete %s it still contains attached '
265 320 'forks') % repo_name,
266 321 category='warning')
267 322 else:
268 323 log.error(traceback.format_exc())
269 324 h.flash(_('An error occurred during '
270 325 'deletion of %s') % repo_name,
271 326 category='error')
272 327
273 328 except Exception, e:
274 329 log.error(traceback.format_exc())
275 330 h.flash(_('An error occurred during deletion of %s') % repo_name,
276 331 category='error')
277 332
278 333 return redirect(url('repos'))
279 334
280 335 @HasRepoPermissionAllDecorator('repository.admin')
281 336 def delete_perm_user(self, repo_name):
282 337 """
283 338 DELETE an existing repository permission user
284 339
285 340 :param repo_name:
286 341 """
287 342 try:
288 343 RepoModel().revoke_user_permission(repo=repo_name,
289 344 user=request.POST['user_id'])
290 Session.commit()
345 Session().commit()
291 346 except Exception:
292 347 log.error(traceback.format_exc())
293 348 h.flash(_('An error occurred during deletion of repository user'),
294 349 category='error')
295 350 raise HTTPInternalServerError()
296 351
297 352 @HasRepoPermissionAllDecorator('repository.admin')
298 353 def delete_perm_users_group(self, repo_name):
299 354 """
300 355 DELETE an existing repository permission users group
301 356
302 357 :param repo_name:
303 358 """
304 359
305 360 try:
306 361 RepoModel().revoke_users_group_permission(
307 362 repo=repo_name, group_name=request.POST['users_group_id']
308 363 )
309 Session.commit()
364 Session().commit()
310 365 except Exception:
311 366 log.error(traceback.format_exc())
312 367 h.flash(_('An error occurred during deletion of repository'
313 368 ' users groups'),
314 369 category='error')
315 370 raise HTTPInternalServerError()
316 371
317 372 @HasPermissionAllDecorator('hg.admin')
318 373 def repo_stats(self, repo_name):
319 374 """
320 375 DELETE an existing repository statistics
321 376
322 377 :param repo_name:
323 378 """
324 379
325 380 try:
326 381 RepoModel().delete_stats(repo_name)
327 Session.commit()
382 Session().commit()
328 383 except Exception, e:
384 log.error(traceback.format_exc())
329 385 h.flash(_('An error occurred during deletion of repository stats'),
330 386 category='error')
331 387 return redirect(url('edit_repo', repo_name=repo_name))
332 388
333 389 @HasPermissionAllDecorator('hg.admin')
334 390 def repo_cache(self, repo_name):
335 391 """
336 392 INVALIDATE existing repository cache
337 393
338 394 :param repo_name:
339 395 """
340 396
341 397 try:
342 398 ScmModel().mark_for_invalidation(repo_name)
343 Session.commit()
399 Session().commit()
344 400 except Exception, e:
401 log.error(traceback.format_exc())
345 402 h.flash(_('An error occurred during cache invalidation'),
346 403 category='error')
347 404 return redirect(url('edit_repo', repo_name=repo_name))
348 405
349 406 @HasPermissionAllDecorator('hg.admin')
407 def repo_locking(self, repo_name):
408 """
409 Unlock repository when it is locked !
410
411 :param repo_name:
412 """
413
414 try:
415 repo = Repository.get_by_repo_name(repo_name)
416 if request.POST.get('set_lock'):
417 Repository.lock(repo, c.rhodecode_user.user_id)
418 elif request.POST.get('set_unlock'):
419 Repository.unlock(repo)
420 except Exception, e:
421 log.error(traceback.format_exc())
422 h.flash(_('An error occurred during unlocking'),
423 category='error')
424 return redirect(url('edit_repo', repo_name=repo_name))
425
426 @HasPermissionAllDecorator('hg.admin')
350 427 def repo_public_journal(self, repo_name):
351 428 """
352 429 Set's this repository to be visible in public journal,
353 430 in other words assing default user to follow this repo
354 431
355 432 :param repo_name:
356 433 """
357 434
358 435 cur_token = request.POST.get('auth_token')
359 436 token = get_token()
360 437 if cur_token == token:
361 438 try:
362 439 repo_id = Repository.get_by_repo_name(repo_name).repo_id
363 440 user_id = User.get_by_username('default').user_id
364 441 self.scm_model.toggle_following_repo(repo_id, user_id)
365 442 h.flash(_('Updated repository visibility in public journal'),
366 443 category='success')
367 Session.commit()
444 Session().commit()
368 445 except:
369 446 h.flash(_('An error occurred during setting this'
370 447 ' repository in public journal'),
371 448 category='error')
372 449
373 450 else:
374 451 h.flash(_('Token mismatch'), category='error')
375 452 return redirect(url('edit_repo', repo_name=repo_name))
376 453
377 454 @HasPermissionAllDecorator('hg.admin')
378 455 def repo_pull(self, repo_name):
379 456 """
380 457 Runs task to update given repository with remote changes,
381 458 ie. make pull on remote location
382 459
383 460 :param repo_name:
384 461 """
385 462 try:
386 463 ScmModel().pull_changes(repo_name, self.rhodecode_user.username)
387 464 h.flash(_('Pulled from remote location'), category='success')
388 465 except Exception, e:
389 466 h.flash(_('An error occurred during pull from remote location'),
390 467 category='error')
391 468
392 469 return redirect(url('edit_repo', repo_name=repo_name))
393 470
394 471 @HasPermissionAllDecorator('hg.admin')
395 472 def repo_as_fork(self, repo_name):
396 473 """
397 474 Mark given repository as a fork of another
398 475
399 476 :param repo_name:
400 477 """
401 478 try:
402 479 fork_id = request.POST.get('id_fork_of')
403 480 repo = ScmModel().mark_as_fork(repo_name, fork_id,
404 481 self.rhodecode_user.username)
405 482 fork = repo.fork.repo_name if repo.fork else _('Nothing')
406 Session.commit()
407 h.flash(_('Marked repo %s as fork of %s' % (repo_name,fork)),
483 Session().commit()
484 h.flash(_('Marked repo %s as fork of %s') % (repo_name, fork),
408 485 category='success')
409 486 except Exception, e:
410 raise
487 log.error(traceback.format_exc())
411 488 h.flash(_('An error occurred during this operation'),
412 489 category='error')
413 490
414 491 return redirect(url('edit_repo', repo_name=repo_name))
415 492
416 493 @HasPermissionAllDecorator('hg.admin')
417 494 def show(self, repo_name, format='html'):
418 495 """GET /repos/repo_name: Show a specific item"""
419 496 # url('repo', repo_name=ID)
420 497
421 498 @HasPermissionAllDecorator('hg.admin')
422 499 def edit(self, repo_name, format='html'):
423 500 """GET /repos/repo_name/edit: Form to edit an existing item"""
424 501 # url('edit_repo', repo_name=ID)
425 502 defaults = self.__load_data(repo_name)
426 503
427 504 return htmlfill.render(
428 505 render('admin/repos/repo_edit.html'),
429 506 defaults=defaults,
430 507 encoding="UTF-8",
431 508 force_defaults=False
432 509 )
@@ -1,318 +1,314 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.repos_groups
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Repositories 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 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35
36 36 from sqlalchemy.exc import IntegrityError
37 37
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.auth import LoginRequired, HasPermissionAnyDecorator,\
40 40 HasReposGroupPermissionAnyDecorator
41 41 from rhodecode.lib.base import BaseController, render
42 42 from rhodecode.model.db import RepoGroup
43 43 from rhodecode.model.repos_group import ReposGroupModel
44 44 from rhodecode.model.forms import ReposGroupForm
45 45 from rhodecode.model.meta import Session
46 46 from rhodecode.model.repo import RepoModel
47 from webob.exc import HTTPInternalServerError
47 from webob.exc import HTTPInternalServerError, HTTPNotFound
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 51
52 52 class ReposGroupsController(BaseController):
53 53 """REST Controller styled on the Atom Publishing Protocol"""
54 54 # To properly map this controller, ensure your config/routing.py
55 55 # file has a resource setup:
56 56 # map.resource('repos_group', 'repos_groups')
57 57
58 58 @LoginRequired()
59 59 def __before__(self):
60 60 super(ReposGroupsController, self).__before__()
61 61
62 62 def __load_defaults(self):
63 63 c.repo_groups = RepoGroup.groups_choices()
64 64 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
65 65
66 66 repo_model = RepoModel()
67 67 c.users_array = repo_model.get_users_js()
68 68 c.users_groups_array = repo_model.get_users_groups_js()
69 69
70 70 def __load_data(self, group_id):
71 71 """
72 72 Load defaults settings for edit, and update
73 73
74 74 :param group_id:
75 75 """
76 76 self.__load_defaults()
77
78 repo_group = RepoGroup.get(group_id)
79
77 repo_group = RepoGroup.get_or_404(group_id)
80 78 data = repo_group.get_dict()
81
82 79 data['group_name'] = repo_group.name
83 80
84 81 # fill repository users
85 82 for p in repo_group.repo_group_to_perm:
86 83 data.update({'u_perm_%s' % p.user.username:
87 84 p.permission.permission_name})
88 85
89 86 # fill repository groups
90 87 for p in repo_group.users_group_to_perm:
91 88 data.update({'g_perm_%s' % p.users_group.users_group_name:
92 89 p.permission.permission_name})
93 90
94 91 return data
95 92
96 93 @HasPermissionAnyDecorator('hg.admin')
97 94 def index(self, format='html'):
98 95 """GET /repos_groups: All items in the collection"""
99 96 # url('repos_groups')
100 97 sk = lambda g: g.parents[0].group_name if g.parents else g.group_name
101 98 c.groups = sorted(RepoGroup.query().all(), key=sk)
102 99 return render('admin/repos_groups/repos_groups_show.html')
103 100
104 101 @HasPermissionAnyDecorator('hg.admin')
105 102 def create(self):
106 103 """POST /repos_groups: Create a new item"""
107 104 # url('repos_groups')
108 105 self.__load_defaults()
109 106 repos_group_form = ReposGroupForm(available_groups =
110 107 c.repo_groups_choices)()
111 108 try:
112 109 form_result = repos_group_form.to_python(dict(request.POST))
113 110 ReposGroupModel().create(
114 111 group_name=form_result['group_name'],
115 112 group_description=form_result['group_description'],
116 113 parent=form_result['group_parent_id']
117 114 )
118 Session.commit()
115 Session().commit()
119 116 h.flash(_('created repos group %s') \
120 117 % form_result['group_name'], category='success')
121 118 #TODO: in futureaction_logger(, '', '', '', self.sa)
122 119 except formencode.Invalid, errors:
123 120
124 121 return htmlfill.render(
125 122 render('admin/repos_groups/repos_groups_add.html'),
126 123 defaults=errors.value,
127 124 errors=errors.error_dict or {},
128 125 prefix_error=False,
129 126 encoding="UTF-8")
130 127 except Exception:
131 128 log.error(traceback.format_exc())
132 129 h.flash(_('error occurred during creation of repos group %s') \
133 130 % request.POST.get('group_name'), category='error')
134 131
135 132 return redirect(url('repos_groups'))
136 133
137 134 @HasPermissionAnyDecorator('hg.admin')
138 135 def new(self, format='html'):
139 136 """GET /repos_groups/new: Form to create a new item"""
140 137 # url('new_repos_group')
141 138 self.__load_defaults()
142 139 return render('admin/repos_groups/repos_groups_add.html')
143 140
144 141 @HasPermissionAnyDecorator('hg.admin')
145 142 def update(self, id):
146 143 """PUT /repos_groups/id: Update an existing item"""
147 144 # Forms posted to this method should contain a hidden field:
148 145 # <input type="hidden" name="_method" value="PUT" />
149 146 # Or using helpers:
150 147 # h.form(url('repos_group', id=ID),
151 148 # method='put')
152 149 # url('repos_group', id=ID)
153 150
154 151 self.__load_defaults()
155 152 c.repos_group = RepoGroup.get(id)
156 153
157 154 repos_group_form = ReposGroupForm(
158 155 edit=True,
159 156 old_data=c.repos_group.get_dict(),
160 157 available_groups=c.repo_groups_choices
161 158 )()
162 159 try:
163 160 form_result = repos_group_form.to_python(dict(request.POST))
164 161 ReposGroupModel().update(id, form_result)
165 Session.commit()
162 Session().commit()
166 163 h.flash(_('updated repos group %s') \
167 164 % form_result['group_name'], category='success')
168 165 #TODO: in futureaction_logger(, '', '', '', self.sa)
169 166 except formencode.Invalid, errors:
170 167
171 168 return htmlfill.render(
172 169 render('admin/repos_groups/repos_groups_edit.html'),
173 170 defaults=errors.value,
174 171 errors=errors.error_dict or {},
175 172 prefix_error=False,
176 173 encoding="UTF-8")
177 174 except Exception:
178 175 log.error(traceback.format_exc())
179 176 h.flash(_('error occurred during update of repos group %s') \
180 177 % request.POST.get('group_name'), category='error')
181 178
182 return redirect(url('repos_groups'))
179 return redirect(url('edit_repos_group', id=id))
183 180
184 181 @HasPermissionAnyDecorator('hg.admin')
185 182 def delete(self, id):
186 183 """DELETE /repos_groups/id: Delete an existing item"""
187 184 # Forms posted to this method should contain a hidden field:
188 185 # <input type="hidden" name="_method" value="DELETE" />
189 186 # Or using helpers:
190 187 # h.form(url('repos_group', id=ID),
191 188 # method='delete')
192 189 # url('repos_group', id=ID)
193 190
194 191 gr = RepoGroup.get(id)
195 192 repos = gr.repositories.all()
196 193 if repos:
197 194 h.flash(_('This group contains %s repositores and cannot be '
198 'deleted' % len(repos)),
195 'deleted') % len(repos),
199 196 category='error')
200 197 return redirect(url('repos_groups'))
201 198
202 199 try:
203 200 ReposGroupModel().delete(id)
204 Session.commit()
205 h.flash(_('removed repos group %s' % gr.group_name), category='success')
201 Session().commit()
202 h.flash(_('removed repos group %s') % gr.group_name,
203 category='success')
206 204 #TODO: in future action_logger(, '', '', '', self.sa)
207 205 except IntegrityError, e:
208 if e.message.find('groups_group_parent_id_fkey') != -1:
206 if str(e.message).find('groups_group_parent_id_fkey') != -1:
209 207 log.error(traceback.format_exc())
210 208 h.flash(_('Cannot delete this group it still contains '
211 209 'subgroups'),
212 210 category='warning')
213 211 else:
214 212 log.error(traceback.format_exc())
215 213 h.flash(_('error occurred during deletion of repos '
216 'group %s' % gr.group_name), category='error')
214 'group %s') % gr.group_name, category='error')
217 215
218 216 except Exception:
219 217 log.error(traceback.format_exc())
220 218 h.flash(_('error occurred during deletion of repos '
221 'group %s' % gr.group_name), category='error')
219 'group %s') % gr.group_name, category='error')
222 220
223 221 return redirect(url('repos_groups'))
224 222
225 223 @HasReposGroupPermissionAnyDecorator('group.admin')
226 224 def delete_repos_group_user_perm(self, group_name):
227 225 """
228 226 DELETE an existing repositories group permission user
229 227
230 228 :param group_name:
231 229 """
232 230
233 231 try:
234 232 ReposGroupModel().revoke_user_permission(
235 233 repos_group=group_name, user=request.POST['user_id']
236 234 )
237 Session.commit()
235 Session().commit()
238 236 except Exception:
239 237 log.error(traceback.format_exc())
240 238 h.flash(_('An error occurred during deletion of group user'),
241 239 category='error')
242 240 raise HTTPInternalServerError()
243 241
244 242 @HasReposGroupPermissionAnyDecorator('group.admin')
245 243 def delete_repos_group_users_group_perm(self, group_name):
246 244 """
247 245 DELETE an existing repositories group permission users group
248 246
249 247 :param group_name:
250 248 """
251 249
252 250 try:
253 251 ReposGroupModel().revoke_users_group_permission(
254 252 repos_group=group_name,
255 253 group_name=request.POST['users_group_id']
256 254 )
257 Session.commit()
255 Session().commit()
258 256 except Exception:
259 257 log.error(traceback.format_exc())
260 258 h.flash(_('An error occurred during deletion of group'
261 259 ' users groups'),
262 260 category='error')
263 261 raise HTTPInternalServerError()
264 262
265 263 def show_by_name(self, group_name):
266 264 """
267 265 This is a proxy that does a lookup group_name -> id, and shows
268 266 the group by id view instead
269 267 """
270 268 group_name = group_name.rstrip('/')
271 id_ = RepoGroup.get_by_group_name(group_name).group_id
272 return self.show(id_)
269 id_ = RepoGroup.get_by_group_name(group_name)
270 if id_:
271 return self.show(id_.group_id)
272 raise HTTPNotFound
273 273
274 274 @HasReposGroupPermissionAnyDecorator('group.read', 'group.write',
275 275 'group.admin')
276 276 def show(self, id, format='html'):
277 277 """GET /repos_groups/id: Show a specific item"""
278 278 # url('repos_group', id=ID)
279 279
280 c.group = RepoGroup.get(id)
280 c.group = RepoGroup.get_or_404(id)
281 281
282 if c.group:
283 282 c.group_repos = c.group.repositories.all()
284 else:
285 return redirect(url('home'))
286 283
287 284 #overwrite our cached list with current filter
288 285 gr_filter = c.group_repos
289 286 c.cached_repo_list = self.scm_model.get_repos(all_repos=gr_filter)
290 287
291 288 c.repos_list = c.cached_repo_list
292 289
293 290 c.repo_cnt = 0
294 291
295 c.groups = self.sa.query(RepoGroup).order_by(RepoGroup.group_name)\
292 c.groups = RepoGroup.query().order_by(RepoGroup.group_name)\
296 293 .filter(RepoGroup.group_parent_id == id).all()
297 294
298 295 return render('admin/repos_groups/repos_groups.html')
299 296
300 297 @HasPermissionAnyDecorator('hg.admin')
301 298 def edit(self, id, format='html'):
302 299 """GET /repos_groups/id/edit: Form to edit an existing item"""
303 300 # url('edit_repos_group', id=ID)
304 301
305 id_ = int(id)
306
307 c.repos_group = RepoGroup.get(id_)
308 defaults = self.__load_data(id_)
302 c.repos_group = ReposGroupModel()._get_repos_group(id)
303 defaults = self.__load_data(c.repos_group.group_id)
309 304
310 305 # we need to exclude this group from the group list for editing
311 c.repo_groups = filter(lambda x: x[0] != id_, c.repo_groups)
306 c.repo_groups = filter(lambda x: x[0] != c.repos_group.group_id,
307 c.repo_groups)
312 308
313 309 return htmlfill.render(
314 310 render('admin/repos_groups/repos_groups_edit.html'),
315 311 defaults=defaults,
316 312 encoding="UTF-8",
317 313 force_defaults=False
318 314 )
@@ -1,422 +1,482 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 40 HasPermissionAnyDecorator, NotAnonymous
41 41 from rhodecode.lib.base import BaseController, render
42 42 from rhodecode.lib.celerylib import tasks, run_task
43 43 from rhodecode.lib.utils import repo2db_mapper, invalidate_cache, \
44 44 set_rhodecode_config, repo_name_slug
45 45 from rhodecode.model.db import RhodeCodeUi, Repository, RepoGroup, \
46 RhodeCodeSetting
46 RhodeCodeSetting, PullRequest, PullRequestReviewers
47 47 from rhodecode.model.forms import UserForm, ApplicationSettingsForm, \
48 ApplicationUiSettingsForm
48 ApplicationUiSettingsForm, ApplicationVisualisationForm
49 49 from rhodecode.model.scm import ScmModel
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.db import User
52 52 from rhodecode.model.notification import EmailNotificationModel
53 53 from rhodecode.model.meta import Session
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class SettingsController(BaseController):
59 59 """REST Controller styled on the Atom Publishing Protocol"""
60 60 # To properly map this controller, ensure your config/routing.py
61 61 # file has a resource setup:
62 62 # map.resource('setting', 'settings', controller='admin/settings',
63 63 # path_prefix='/admin', name_prefix='admin_')
64 64
65 65 @LoginRequired()
66 66 def __before__(self):
67 67 c.admin_user = session.get('admin_user')
68 68 c.admin_username = session.get('admin_username')
69 69 c.modules = sorted([(p.project_name, p.version)
70 70 for p in pkg_resources.working_set],
71 71 key=lambda k: k[0].lower())
72 72 c.py_version = platform.python_version()
73 73 c.platform = platform.platform()
74 74 super(SettingsController, self).__before__()
75 75
76 76 @HasPermissionAllDecorator('hg.admin')
77 77 def index(self, format='html'):
78 78 """GET /admin/settings: All items in the collection"""
79 79 # url('admin_settings')
80 80
81 81 defaults = RhodeCodeSetting.get_app_settings()
82 defaults.update(self.get_hg_ui_settings())
82 defaults.update(self._get_hg_ui_settings())
83 83
84 84 return htmlfill.render(
85 85 render('admin/settings/settings.html'),
86 86 defaults=defaults,
87 87 encoding="UTF-8",
88 88 force_defaults=False
89 89 )
90 90
91 91 @HasPermissionAllDecorator('hg.admin')
92 92 def create(self):
93 93 """POST /admin/settings: Create a new item"""
94 94 # url('admin_settings')
95 95
96 96 @HasPermissionAllDecorator('hg.admin')
97 97 def new(self, format='html'):
98 98 """GET /admin/settings/new: Form to create a new item"""
99 99 # url('admin_new_setting')
100 100
101 101 @HasPermissionAllDecorator('hg.admin')
102 102 def update(self, setting_id):
103 103 """PUT /admin/settings/setting_id: Update an existing item"""
104 104 # Forms posted to this method should contain a hidden field:
105 105 # <input type="hidden" name="_method" value="PUT" />
106 106 # Or using helpers:
107 107 # h.form(url('admin_setting', setting_id=ID),
108 108 # method='put')
109 109 # url('admin_setting', setting_id=ID)
110
110 111 if setting_id == 'mapping':
111 112 rm_obsolete = request.POST.get('destroy', False)
112 113 log.debug('Rescanning directories with destroy=%s' % rm_obsolete)
113 114 initial = ScmModel().repo_scan()
114 115 log.debug('invalidating all repositories')
115 116 for repo_name in initial.keys():
116 117 invalidate_cache('get_repo_cached_%s' % repo_name)
117 118
118 119 added, removed = repo2db_mapper(initial, rm_obsolete)
119 120
120 121 h.flash(_('Repositories successfully'
121 122 ' rescanned added: %s,removed: %s') % (added, removed),
122 123 category='success')
123 124
124 125 if setting_id == 'whoosh':
125 repo_location = self.get_hg_ui_settings()['paths_root_path']
126 repo_location = self._get_hg_ui_settings()['paths_root_path']
126 127 full_index = request.POST.get('full_index', False)
127 128 run_task(tasks.whoosh_index, repo_location, full_index)
129 h.flash(_('Whoosh reindex task scheduled'), category='success')
128 130
129 h.flash(_('Whoosh reindex task scheduled'), category='success')
130 131 if setting_id == 'global':
131 132
132 133 application_form = ApplicationSettingsForm()()
133 134 try:
134 135 form_result = application_form.to_python(dict(request.POST))
136 except formencode.Invalid, errors:
137 return htmlfill.render(
138 render('admin/settings/settings.html'),
139 defaults=errors.value,
140 errors=errors.error_dict or {},
141 prefix_error=False,
142 encoding="UTF-8"
143 )
135 144
136 145 try:
137 hgsettings1 = RhodeCodeSetting.get_by_name('title')
138 hgsettings1.app_settings_value = \
139 form_result['rhodecode_title']
146 sett1 = RhodeCodeSetting.get_by_name_or_create('title')
147 sett1.app_settings_value = form_result['rhodecode_title']
148 Session().add(sett1)
140 149
141 hgsettings2 = RhodeCodeSetting.get_by_name('realm')
142 hgsettings2.app_settings_value = \
143 form_result['rhodecode_realm']
150 sett2 = RhodeCodeSetting.get_by_name_or_create('realm')
151 sett2.app_settings_value = form_result['rhodecode_realm']
152 Session().add(sett2)
144 153
145 hgsettings3 = RhodeCodeSetting.get_by_name('ga_code')
146 hgsettings3.app_settings_value = \
147 form_result['rhodecode_ga_code']
154 sett3 = RhodeCodeSetting.get_by_name_or_create('ga_code')
155 sett3.app_settings_value = form_result['rhodecode_ga_code']
156 Session().add(sett3)
148 157
149 self.sa.add(hgsettings1)
150 self.sa.add(hgsettings2)
151 self.sa.add(hgsettings3)
152 self.sa.commit()
158 Session().commit()
153 159 set_rhodecode_config(config)
154 h.flash(_('Updated application settings'),
155 category='success')
160 h.flash(_('Updated application settings'), category='success')
156 161
157 162 except Exception:
158 163 log.error(traceback.format_exc())
159 164 h.flash(_('error occurred during updating '
160 165 'application settings'),
161 166 category='error')
162 167
163 self.sa.rollback()
168 if setting_id == 'visual':
169
170 application_form = ApplicationVisualisationForm()()
171 try:
172 form_result = application_form.to_python(dict(request.POST))
173 except formencode.Invalid, errors:
174 return htmlfill.render(
175 render('admin/settings/settings.html'),
176 defaults=errors.value,
177 errors=errors.error_dict or {},
178 prefix_error=False,
179 encoding="UTF-8"
180 )
181
182 try:
183 sett1 = RhodeCodeSetting.get_by_name_or_create('show_public_icon')
184 sett1.app_settings_value = \
185 form_result['rhodecode_show_public_icon']
186
187 sett2 = RhodeCodeSetting.get_by_name_or_create('show_private_icon')
188 sett2.app_settings_value = \
189 form_result['rhodecode_show_private_icon']
164 190
191 sett3 = RhodeCodeSetting.get_by_name_or_create('stylify_metatags')
192 sett3.app_settings_value = \
193 form_result['rhodecode_stylify_metatags']
194
195 Session().add(sett1)
196 Session().add(sett2)
197 Session().add(sett3)
198 Session().commit()
199 set_rhodecode_config(config)
200 h.flash(_('Updated visualisation settings'),
201 category='success')
202
203 except Exception:
204 log.error(traceback.format_exc())
205 h.flash(_('error occurred during updating '
206 'visualisation settings'),
207 category='error')
208
209 if setting_id == 'vcs':
210 application_form = ApplicationUiSettingsForm()()
211 try:
212 form_result = application_form.to_python(dict(request.POST))
165 213 except formencode.Invalid, errors:
166 214 return htmlfill.render(
167 215 render('admin/settings/settings.html'),
168 216 defaults=errors.value,
169 217 errors=errors.error_dict or {},
170 218 prefix_error=False,
171 encoding="UTF-8")
172
173 if setting_id == 'mercurial':
174 application_form = ApplicationUiSettingsForm()()
175 try:
176 form_result = application_form.to_python(dict(request.POST))
219 encoding="UTF-8"
220 )
177 221
178 222 try:
223 # fix namespaces for hooks and extensions
224 _f = lambda s: s.replace('.', '_')
179 225
180 hgsettings1 = self.sa.query(RhodeCodeUi)\
181 .filter(RhodeCodeUi.ui_key == 'push_ssl').one()
182 hgsettings1.ui_value = form_result['web_push_ssl']
226 sett = RhodeCodeUi.get_by_key('push_ssl')
227 sett.ui_value = form_result['web_push_ssl']
228 Session().add(sett)
183 229
184 hgsettings2 = self.sa.query(RhodeCodeUi)\
185 .filter(RhodeCodeUi.ui_key == '/').one()
186 hgsettings2.ui_value = form_result['paths_root_path']
230 sett = RhodeCodeUi.get_by_key('/')
231 sett.ui_value = form_result['paths_root_path']
232 Session().add(sett)
187 233
188 234 #HOOKS
189 hgsettings3 = self.sa.query(RhodeCodeUi)\
190 .filter(RhodeCodeUi.ui_key == 'changegroup.update').one()
191 hgsettings3.ui_active = \
192 bool(form_result['hooks_changegroup_update'])
235 sett = RhodeCodeUi.get_by_key(RhodeCodeUi.HOOK_UPDATE)
236 sett.ui_active = form_result[_f('hooks_%s' %
237 RhodeCodeUi.HOOK_UPDATE)]
238 Session().add(sett)
193 239
194 hgsettings4 = self.sa.query(RhodeCodeUi)\
195 .filter(RhodeCodeUi.ui_key ==
196 'changegroup.repo_size').one()
197 hgsettings4.ui_active = \
198 bool(form_result['hooks_changegroup_repo_size'])
240 sett = RhodeCodeUi.get_by_key(RhodeCodeUi.HOOK_REPO_SIZE)
241 sett.ui_active = form_result[_f('hooks_%s' %
242 RhodeCodeUi.HOOK_REPO_SIZE)]
243 Session().add(sett)
199 244
200 hgsettings5 = self.sa.query(RhodeCodeUi)\
201 .filter(RhodeCodeUi.ui_key ==
202 'pretxnchangegroup.push_logger').one()
203 hgsettings5.ui_active = \
204 bool(form_result['hooks_pretxnchangegroup'
205 '_push_logger'])
245 sett = RhodeCodeUi.get_by_key(RhodeCodeUi.HOOK_PUSH)
246 sett.ui_active = form_result[_f('hooks_%s' %
247 RhodeCodeUi.HOOK_PUSH)]
248 Session().add(sett)
249
250 sett = RhodeCodeUi.get_by_key(RhodeCodeUi.HOOK_PULL)
251 sett.ui_active = form_result[_f('hooks_%s' %
252 RhodeCodeUi.HOOK_PULL)]
206 253
207 hgsettings6 = self.sa.query(RhodeCodeUi)\
208 .filter(RhodeCodeUi.ui_key ==
209 'preoutgoing.pull_logger').one()
210 hgsettings6.ui_active = \
211 bool(form_result['hooks_preoutgoing_pull_logger'])
254 Session().add(sett)
255
256 ## EXTENSIONS
257 sett = RhodeCodeUi.get_by_key('largefiles')
258 sett.ui_active = form_result[_f('extensions_largefiles')]
259 Session().add(sett)
212 260
213 self.sa.add(hgsettings1)
214 self.sa.add(hgsettings2)
215 self.sa.add(hgsettings3)
216 self.sa.add(hgsettings4)
217 self.sa.add(hgsettings5)
218 self.sa.add(hgsettings6)
219 self.sa.commit()
261 sett = RhodeCodeUi.get_by_key('hgsubversion')
262 sett.ui_active = form_result[_f('extensions_hgsubversion')]
263 Session().add(sett)
220 264
221 h.flash(_('Updated mercurial settings'),
222 category='success')
265 # sett = RhodeCodeUi.get_by_key('hggit')
266 # sett.ui_active = form_result[_f('extensions_hggit')]
267 # Session().add(sett)
223 268
224 except:
269 Session().commit()
270
271 h.flash(_('Updated VCS settings'), category='success')
272
273 except Exception:
225 274 log.error(traceback.format_exc())
226 275 h.flash(_('error occurred during updating '
227 276 'application settings'), category='error')
228 277
229 self.sa.rollback()
230
231 except formencode.Invalid, errors:
232 return htmlfill.render(
233 render('admin/settings/settings.html'),
234 defaults=errors.value,
235 errors=errors.error_dict or {},
236 prefix_error=False,
237 encoding="UTF-8")
238
239 278 if setting_id == 'hooks':
240 279 ui_key = request.POST.get('new_hook_ui_key')
241 280 ui_value = request.POST.get('new_hook_ui_value')
242 281 try:
243 282
244 283 if ui_value and ui_key:
245 284 RhodeCodeUi.create_or_update_hook(ui_key, ui_value)
246 285 h.flash(_('Added new hook'),
247 286 category='success')
248 287
249 288 # check for edits
250 289 update = False
251 290 _d = request.POST.dict_of_lists()
252 291 for k, v in zip(_d.get('hook_ui_key', []),
253 292 _d.get('hook_ui_value_new', [])):
254 293 RhodeCodeUi.create_or_update_hook(k, v)
255 294 update = True
256 295
257 296 if update:
258 297 h.flash(_('Updated hooks'), category='success')
259 self.sa.commit()
260 except:
298 Session().commit()
299 except Exception:
261 300 log.error(traceback.format_exc())
262 301 h.flash(_('error occurred during hook creation'),
263 302 category='error')
264 303
265 304 return redirect(url('admin_edit_setting', setting_id='hooks'))
266 305
267 306 if setting_id == 'email':
268 307 test_email = request.POST.get('test_email')
269 308 test_email_subj = 'RhodeCode TestEmail'
270 309 test_email_body = 'RhodeCode Email test'
271 310
272 311 test_email_html_body = EmailNotificationModel()\
273 312 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
274 313 body=test_email_body)
275 314
276 315 recipients = [test_email] if [test_email] else None
277 316
278 317 run_task(tasks.send_email, recipients, test_email_subj,
279 318 test_email_body, test_email_html_body)
280 319
281 320 h.flash(_('Email task created'), category='success')
282 321 return redirect(url('admin_settings'))
283 322
284 323 @HasPermissionAllDecorator('hg.admin')
285 324 def delete(self, setting_id):
286 325 """DELETE /admin/settings/setting_id: Delete an existing item"""
287 326 # Forms posted to this method should contain a hidden field:
288 327 # <input type="hidden" name="_method" value="DELETE" />
289 328 # Or using helpers:
290 329 # h.form(url('admin_setting', setting_id=ID),
291 330 # method='delete')
292 331 # url('admin_setting', setting_id=ID)
293 332 if setting_id == 'hooks':
294 333 hook_id = request.POST.get('hook_id')
295 334 RhodeCodeUi.delete(hook_id)
296 self.sa.commit()
335 Session().commit()
297 336
298 337 @HasPermissionAllDecorator('hg.admin')
299 338 def show(self, setting_id, format='html'):
300 339 """
301 340 GET /admin/settings/setting_id: Show a specific item"""
302 341 # url('admin_setting', setting_id=ID)
303 342
304 343 @HasPermissionAllDecorator('hg.admin')
305 344 def edit(self, setting_id, format='html'):
306 345 """
307 346 GET /admin/settings/setting_id/edit: Form to
308 347 edit an existing item"""
309 348 # url('admin_edit_setting', setting_id=ID)
310 349
311 350 c.hooks = RhodeCodeUi.get_builtin_hooks()
312 351 c.custom_hooks = RhodeCodeUi.get_custom_hooks()
313 352
314 353 return htmlfill.render(
315 354 render('admin/settings/hooks.html'),
316 355 defaults={},
317 356 encoding="UTF-8",
318 357 force_defaults=False
319 358 )
320 359
321 360 @NotAnonymous()
322 361 def my_account(self):
323 362 """
324 363 GET /_admin/my_account Displays info about my account
325 364 """
326 365 # url('admin_settings_my_account')
327 366
328 367 c.user = User.get(self.rhodecode_user.user_id)
329 all_repos = self.sa.query(Repository)\
368 all_repos = Session().query(Repository)\
330 369 .filter(Repository.user_id == c.user.user_id)\
331 370 .order_by(func.lower(Repository.repo_name)).all()
332 371
333 372 c.user_repos = ScmModel().get_repos(all_repos)
334 373
335 374 if c.user.username == 'default':
336 375 h.flash(_("You can't edit this user since it's"
337 376 " crucial for entire application"), category='warning')
338 377 return redirect(url('users'))
339 378
340 379 defaults = c.user.get_dict()
341 return htmlfill.render(
342 render('admin/users/user_edit_my_account.html'),
380
381 c.form = htmlfill.render(
382 render('admin/users/user_edit_my_account_form.html'),
343 383 defaults=defaults,
344 384 encoding="UTF-8",
345 385 force_defaults=False
346 386 )
387 return render('admin/users/user_edit_my_account.html')
347 388
389 @NotAnonymous()
348 390 def my_account_update(self):
349 391 """PUT /_admin/my_account_update: Update an existing item"""
350 392 # Forms posted to this method should contain a hidden field:
351 393 # <input type="hidden" name="_method" value="PUT" />
352 394 # Or using helpers:
353 395 # h.form(url('admin_settings_my_account_update'),
354 396 # method='put')
355 397 # url('admin_settings_my_account_update', id=ID)
356 user_model = UserModel()
357 398 uid = self.rhodecode_user.user_id
399 email = self.rhodecode_user.email
358 400 _form = UserForm(edit=True,
359 old_data={'user_id': uid,
360 'email': self.rhodecode_user.email})()
401 old_data={'user_id': uid, 'email': email})()
361 402 form_result = {}
362 403 try:
363 404 form_result = _form.to_python(dict(request.POST))
364 user_model.update_my_account(uid, form_result)
405 UserModel().update_my_account(uid, form_result)
365 406 h.flash(_('Your account was updated successfully'),
366 407 category='success')
367 Session.commit()
408 Session().commit()
368 409 except formencode.Invalid, errors:
369 410 c.user = User.get(self.rhodecode_user.user_id)
370 all_repos = self.sa.query(Repository)\
371 .filter(Repository.user_id == c.user.user_id)\
372 .order_by(func.lower(Repository.repo_name))\
373 .all()
374 c.user_repos = ScmModel().get_repos(all_repos)
375 411
376 return htmlfill.render(
377 render('admin/users/user_edit_my_account.html'),
412 c.form = htmlfill.render(
413 render('admin/users/user_edit_my_account_form.html'),
378 414 defaults=errors.value,
379 415 errors=errors.error_dict or {},
380 416 prefix_error=False,
381 417 encoding="UTF-8")
418 return render('admin/users/user_edit_my_account.html')
382 419 except Exception:
383 420 log.error(traceback.format_exc())
384 421 h.flash(_('error occurred during update of user %s') \
385 422 % form_result.get('username'), category='error')
386 423
387 424 return redirect(url('my_account'))
388 425
389 426 @NotAnonymous()
427 def my_account_my_repos(self):
428 all_repos = Session().query(Repository)\
429 .filter(Repository.user_id == self.rhodecode_user.user_id)\
430 .order_by(func.lower(Repository.repo_name))\
431 .all()
432 c.user_repos = ScmModel().get_repos(all_repos)
433 return render('admin/users/user_edit_my_account_repos.html')
434
435 @NotAnonymous()
436 def my_account_my_pullrequests(self):
437 c.my_pull_requests = PullRequest.query()\
438 .filter(PullRequest.user_id==
439 self.rhodecode_user.user_id)\
440 .all()
441 c.participate_in_pull_requests = \
442 [x.pull_request for x in PullRequestReviewers.query()\
443 .filter(PullRequestReviewers.user_id==
444 self.rhodecode_user.user_id)\
445 .all()]
446 return render('admin/users/user_edit_my_account_pullrequests.html')
447
448 @NotAnonymous()
390 449 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
391 450 def create_repository(self):
392 451 """GET /_admin/create_repository: Form to create a new item"""
393 452
394 453 c.repo_groups = RepoGroup.groups_choices()
395 454 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
455 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
396 456
397 457 new_repo = request.GET.get('repo', '')
398 458 c.new_repo = repo_name_slug(new_repo)
399 459
400 460 return render('admin/repos/repo_add_create_repository.html')
401 461
402 def get_hg_ui_settings(self):
403 ret = self.sa.query(RhodeCodeUi).all()
462 def _get_hg_ui_settings(self):
463 ret = RhodeCodeUi.query().all()
404 464
405 465 if not ret:
406 466 raise Exception('Could not get application ui settings !')
407 467 settings = {}
408 468 for each in ret:
409 469 k = each.ui_key
410 470 v = each.ui_value
411 471 if k == '/':
412 472 k = 'root_path'
413 473
414 474 if k.find('.') != -1:
415 475 k = k.replace('.', '_')
416 476
417 if each.ui_section == 'hooks':
477 if each.ui_section in ['hooks', 'extensions']:
418 478 v = each.ui_active
419 479
420 480 settings[each.ui_section + '_' + k] = v
421 481
422 482 return settings
@@ -1,211 +1,320 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 from pylons import response
29 30
30 31 from formencode import htmlfill
31 32 from pylons import request, session, tmpl_context as c, url, config
32 33 from pylons.controllers.util import redirect
33 34 from pylons.i18n.translation import _
34 35
36 import rhodecode
35 37 from rhodecode.lib.exceptions import DefaultUserException, \
36 38 UserOwnsReposException
37 39 from rhodecode.lib import helpers as h
38 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
40 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
41 AuthUser
39 42 from rhodecode.lib.base import BaseController, render
40 43
41 from rhodecode.model.db import User, Permission
44 from rhodecode.model.db import User, UserEmailMap
42 45 from rhodecode.model.forms import UserForm
43 46 from rhodecode.model.user import UserModel
44 47 from rhodecode.model.meta import Session
48 from rhodecode.lib.utils import action_logger
49 from rhodecode.lib.compat import json
50 from rhodecode.lib.utils2 import datetime_to_time, str2bool
45 51
46 52 log = logging.getLogger(__name__)
47 53
48 54
49 55 class UsersController(BaseController):
50 56 """REST Controller styled on the Atom Publishing Protocol"""
51 57 # To properly map this controller, ensure your config/routing.py
52 58 # file has a resource setup:
53 59 # map.resource('user', 'users')
54 60
55 61 @LoginRequired()
56 62 @HasPermissionAllDecorator('hg.admin')
57 63 def __before__(self):
58 64 c.admin_user = session.get('admin_user')
59 65 c.admin_username = session.get('admin_username')
60 66 super(UsersController, self).__before__()
61 67 c.available_permissions = config['available_permissions']
62 68
63 69 def index(self, format='html'):
64 70 """GET /users: All items in the collection"""
65 71 # url('users')
66 72
67 c.users_list = self.sa.query(User).all()
73 c.users_list = User.query().order_by(User.username).all()
74
75 users_data = []
76 total_records = len(c.users_list)
77 _tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
78 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
79
80 grav_tmpl = lambda user_email, size: (
81 template.get_def("user_gravatar")
82 .render(user_email, size, _=_, h=h, c=c))
83
84 user_lnk = lambda user_id, username: (
85 template.get_def("user_name")
86 .render(user_id, username, _=_, h=h, c=c))
87
88 user_actions = lambda user_id, username: (
89 template.get_def("user_actions")
90 .render(user_id, username, _=_, h=h, c=c))
91
92 for user in c.users_list:
93
94 users_data.append({
95 "gravatar": grav_tmpl(user. email, 24),
96 "raw_username": user.username,
97 "username": user_lnk(user.user_id, user.username),
98 "firstname": user.name,
99 "lastname": user.lastname,
100 "last_login": h.fmt_date(user.last_login),
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)),
105 "action": user_actions(user.user_id, user.username),
106 })
107
108 c.data = json.dumps({
109 "totalRecords": total_records,
110 "startIndex": 0,
111 "sort": None,
112 "dir": "asc",
113 "records": users_data
114 })
115
68 116 return render('admin/users/users.html')
69 117
70 118 def create(self):
71 119 """POST /users: Create a new item"""
72 120 # url('users')
73 121
74 122 user_model = UserModel()
75 123 user_form = UserForm()()
76 124 try:
77 125 form_result = user_form.to_python(dict(request.POST))
78 126 user_model.create(form_result)
79 h.flash(_('created user %s') % form_result['username'],
127 usr = form_result['username']
128 action_logger(self.rhodecode_user, 'admin_created_user:%s' % usr,
129 None, self.ip_addr, self.sa)
130 h.flash(_('created user %s') % usr,
80 131 category='success')
81 Session.commit()
82 #action_logger(self.rhodecode_user, 'new_user', '', '', self.sa)
132 Session().commit()
83 133 except formencode.Invalid, errors:
84 134 return htmlfill.render(
85 135 render('admin/users/user_add.html'),
86 136 defaults=errors.value,
87 137 errors=errors.error_dict or {},
88 138 prefix_error=False,
89 139 encoding="UTF-8")
90 140 except Exception:
91 141 log.error(traceback.format_exc())
92 142 h.flash(_('error occurred during creation of user %s') \
93 143 % request.POST.get('username'), category='error')
94 144 return redirect(url('users'))
95 145
96 146 def new(self, format='html'):
97 147 """GET /users/new: Form to create a new item"""
98 148 # url('new_user')
99 149 return render('admin/users/user_add.html')
100 150
101 151 def update(self, id):
102 152 """PUT /users/id: Update an existing item"""
103 153 # Forms posted to this method should contain a hidden field:
104 154 # <input type="hidden" name="_method" value="PUT" />
105 155 # Or using helpers:
106 156 # h.form(url('update_user', id=ID),
107 157 # method='put')
108 158 # url('user', id=ID)
109 159 user_model = UserModel()
110 160 c.user = user_model.get(id)
111
161 c.perm_user = AuthUser(user_id=id)
112 162 _form = UserForm(edit=True, old_data={'user_id': id,
113 163 'email': c.user.email})()
114 164 form_result = {}
115 165 try:
116 166 form_result = _form.to_python(dict(request.POST))
117 167 user_model.update(id, form_result)
168 usr = form_result['username']
169 action_logger(self.rhodecode_user, 'admin_updated_user:%s' % usr,
170 None, self.ip_addr, self.sa)
118 171 h.flash(_('User updated successfully'), category='success')
119 Session.commit()
172 Session().commit()
120 173 except formencode.Invalid, errors:
174 c.user_email_map = UserEmailMap.query()\
175 .filter(UserEmailMap.user == c.user).all()
176 defaults = errors.value
121 177 e = errors.error_dict or {}
122 perm = Permission.get_by_key('hg.create.repository')
123 e.update({'create_repo_perm': user_model.has_perm(id, perm)})
178 defaults.update({
179 'create_repo_perm': user_model.has_perm(id, 'hg.create.repository'),
180 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
181 '_method': 'put'
182 })
124 183 return htmlfill.render(
125 184 render('admin/users/user_edit.html'),
126 defaults=errors.value,
185 defaults=defaults,
127 186 errors=e,
128 187 prefix_error=False,
129 188 encoding="UTF-8")
130 189 except Exception:
131 190 log.error(traceback.format_exc())
132 191 h.flash(_('error occurred during update of user %s') \
133 192 % form_result.get('username'), category='error')
134
135 return redirect(url('users'))
193 return redirect(url('edit_user', id=id))
136 194
137 195 def delete(self, id):
138 196 """DELETE /users/id: Delete an existing item"""
139 197 # Forms posted to this method should contain a hidden field:
140 198 # <input type="hidden" name="_method" value="DELETE" />
141 199 # Or using helpers:
142 200 # h.form(url('delete_user', id=ID),
143 201 # method='delete')
144 202 # url('user', id=ID)
145 user_model = UserModel()
203 usr = User.get_or_404(id)
146 204 try:
147 user_model.delete(id)
148 Session.commit()
205 UserModel().delete(usr)
206 Session().commit()
149 207 h.flash(_('successfully deleted user'), category='success')
150 208 except (UserOwnsReposException, DefaultUserException), e:
151 209 h.flash(e, category='warning')
152 210 except Exception:
153 211 log.error(traceback.format_exc())
154 212 h.flash(_('An error occurred during deletion of user'),
155 213 category='error')
156 214 return redirect(url('users'))
157 215
158 216 def show(self, id, format='html'):
159 217 """GET /users/id: Show a specific item"""
160 218 # url('user', id=ID)
161 219
162 220 def edit(self, id, format='html'):
163 221 """GET /users/id/edit: Form to edit an existing item"""
164 222 # url('edit_user', id=ID)
165 c.user = User.get(id)
166 if not c.user:
167 return redirect(url('users'))
223 c.user = User.get_or_404(id)
224
168 225 if c.user.username == 'default':
169 226 h.flash(_("You can't edit this user"), category='warning')
170 227 return redirect(url('users'))
228
229 c.perm_user = AuthUser(user_id=id)
171 230 c.user.permissions = {}
172 231 c.granted_permissions = UserModel().fill_perms(c.user)\
173 232 .permissions['global']
174
233 c.user_email_map = UserEmailMap.query()\
234 .filter(UserEmailMap.user == c.user).all()
235 user_model = UserModel()
175 236 defaults = c.user.get_dict()
176 perm = Permission.get_by_key('hg.create.repository')
177 defaults.update({'create_repo_perm': UserModel().has_perm(id, perm)})
237 defaults.update({
238 'create_repo_perm': user_model.has_perm(id, 'hg.create.repository'),
239 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
240 })
178 241
179 242 return htmlfill.render(
180 243 render('admin/users/user_edit.html'),
181 244 defaults=defaults,
182 245 encoding="UTF-8",
183 246 force_defaults=False
184 247 )
185 248
186 249 def update_perm(self, id):
187 250 """PUT /users_perm/id: Update an existing item"""
188 251 # url('user_perm', id=ID, method='put')
252 usr = User.get_or_404(id)
253 grant_create_perm = str2bool(request.POST.get('create_repo_perm'))
254 grant_fork_perm = str2bool(request.POST.get('fork_repo_perm'))
255 inherit_perms = str2bool(request.POST.get('inherit_default_permissions'))
189 256
190 grant_perm = request.POST.get('create_repo_perm', False)
191 257 user_model = UserModel()
192 258
193 if grant_perm:
194 perm = Permission.get_by_key('hg.create.none')
195 user_model.revoke_perm(id, perm)
259 try:
260 usr.inherit_default_permissions = inherit_perms
261 Session().add(usr)
196 262
197 perm = Permission.get_by_key('hg.create.repository')
198 user_model.grant_perm(id, perm)
263 if grant_create_perm:
264 user_model.revoke_perm(usr, 'hg.create.none')
265 user_model.grant_perm(usr, 'hg.create.repository')
199 266 h.flash(_("Granted 'repository create' permission to user"),
200 267 category='success')
201 Session.commit()
202 268 else:
203 perm = Permission.get_by_key('hg.create.repository')
204 user_model.revoke_perm(id, perm)
205
206 perm = Permission.get_by_key('hg.create.none')
207 user_model.grant_perm(id, perm)
269 user_model.revoke_perm(usr, 'hg.create.repository')
270 user_model.grant_perm(usr, 'hg.create.none')
208 271 h.flash(_("Revoked 'repository create' permission to user"),
209 272 category='success')
210 Session.commit()
273
274 if grant_fork_perm:
275 user_model.revoke_perm(usr, 'hg.fork.none')
276 user_model.grant_perm(usr, 'hg.fork.repository')
277 h.flash(_("Granted 'repository fork' permission to user"),
278 category='success')
279 else:
280 user_model.revoke_perm(usr, 'hg.fork.repository')
281 user_model.grant_perm(usr, 'hg.fork.none')
282 h.flash(_("Revoked 'repository fork' permission to user"),
283 category='success')
284
285 Session().commit()
286 except Exception:
287 log.error(traceback.format_exc())
288 h.flash(_('An error occurred during permissions saving'),
289 category='error')
211 290 return redirect(url('edit_user', id=id))
291
292 def add_email(self, id):
293 """POST /user_emails:Add an existing item"""
294 # url('user_emails', id=ID, method='put')
295
296 #TODO: validation and form !!!
297 email = request.POST.get('new_email')
298 user_model = UserModel()
299
300 try:
301 user_model.add_extra_email(id, email)
302 Session().commit()
303 h.flash(_("Added email %s to user") % email, category='success')
304 except formencode.Invalid, error:
305 msg = error.error_dict['email']
306 h.flash(msg, category='error')
307 except Exception:
308 log.error(traceback.format_exc())
309 h.flash(_('An error occurred during email saving'),
310 category='error')
311 return redirect(url('edit_user', id=id))
312
313 def delete_email(self, id):
314 """DELETE /user_emails_delete/id: Delete an existing item"""
315 # url('user_emails_delete', id=ID, method='delete')
316 user_model = UserModel()
317 user_model.delete_extra_email(id, request.POST.get('del_email'))
318 Session().commit()
319 h.flash(_("Removed email from user"), category='success')
320 return redirect(url('edit_user', id=id))
@@ -1,228 +1,258 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.admin.users_groups
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Users 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 36 from rhodecode.lib.exceptions import UsersGroupsAssignedException
37 from rhodecode.lib.utils2 import safe_unicode
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 41 from rhodecode.model.users_group import UsersGroupModel
42 42
43 from rhodecode.model.db import User, UsersGroup, Permission, UsersGroupToPerm
43 from rhodecode.model.db import User, UsersGroup
44 44 from rhodecode.model.forms import UsersGroupForm
45 45 from rhodecode.model.meta import Session
46 from rhodecode.lib.utils import action_logger
46 47
47 48 log = logging.getLogger(__name__)
48 49
49 50
50 51 class UsersGroupsController(BaseController):
51 52 """REST Controller styled on the Atom Publishing Protocol"""
52 53 # To properly map this controller, ensure your config/routing.py
53 54 # file has a resource setup:
54 55 # map.resource('users_group', 'users_groups')
55 56
56 57 @LoginRequired()
57 58 @HasPermissionAllDecorator('hg.admin')
58 59 def __before__(self):
59 60 c.admin_user = session.get('admin_user')
60 61 c.admin_username = session.get('admin_username')
61 62 super(UsersGroupsController, self).__before__()
62 63 c.available_permissions = config['available_permissions']
63 64
64 65 def index(self, format='html'):
65 66 """GET /users_groups: All items in the collection"""
66 67 # url('users_groups')
67 c.users_groups_list = self.sa.query(UsersGroup).all()
68 c.users_groups_list = UsersGroup().query().all()
68 69 return render('admin/users_groups/users_groups.html')
69 70
70 71 def create(self):
71 72 """POST /users_groups: Create a new item"""
72 73 # url('users_groups')
73 74
74 75 users_group_form = UsersGroupForm()()
75 76 try:
76 77 form_result = users_group_form.to_python(dict(request.POST))
77 78 UsersGroupModel().create(name=form_result['users_group_name'],
78 79 active=form_result['users_group_active'])
79 h.flash(_('created users group %s') \
80 % form_result['users_group_name'], category='success')
81 #action_logger(self.rhodecode_user, 'new_user', '', '', self.sa)
82 Session.commit()
80 gr = form_result['users_group_name']
81 action_logger(self.rhodecode_user,
82 'admin_created_users_group:%s' % gr,
83 None, self.ip_addr, self.sa)
84 h.flash(_('created users group %s') % gr, category='success')
85 Session().commit()
83 86 except formencode.Invalid, errors:
84 87 return htmlfill.render(
85 88 render('admin/users_groups/users_group_add.html'),
86 89 defaults=errors.value,
87 90 errors=errors.error_dict or {},
88 91 prefix_error=False,
89 92 encoding="UTF-8")
90 93 except Exception:
91 94 log.error(traceback.format_exc())
92 95 h.flash(_('error occurred during creation of users group %s') \
93 96 % request.POST.get('users_group_name'), category='error')
94 97
95 98 return redirect(url('users_groups'))
96 99
97 100 def new(self, format='html'):
98 101 """GET /users_groups/new: Form to create a new item"""
99 102 # url('new_users_group')
100 103 return render('admin/users_groups/users_group_add.html')
101 104
102 105 def update(self, id):
103 106 """PUT /users_groups/id: Update an existing item"""
104 107 # Forms posted to this method should contain a hidden field:
105 108 # <input type="hidden" name="_method" value="PUT" />
106 109 # Or using helpers:
107 110 # h.form(url('users_group', id=ID),
108 111 # method='put')
109 112 # url('users_group', id=ID)
110 113
111 114 c.users_group = UsersGroup.get(id)
112 115 c.group_members_obj = [x.user for x in c.users_group.members]
113 116 c.group_members = [(x.user_id, x.username) for x in
114 117 c.group_members_obj]
115 118
116 119 c.available_members = [(x.user_id, x.username) for x in
117 self.sa.query(User).all()]
120 User.query().all()]
118 121
119 122 available_members = [safe_unicode(x[0]) for x in c.available_members]
120 123
121 124 users_group_form = UsersGroupForm(edit=True,
122 125 old_data=c.users_group.get_dict(),
123 126 available_members=available_members)()
124 127
125 128 try:
126 129 form_result = users_group_form.to_python(request.POST)
127 130 UsersGroupModel().update(c.users_group, form_result)
128 h.flash(_('updated users group %s') \
129 % form_result['users_group_name'],
130 category='success')
131 #action_logger(self.rhodecode_user, 'new_user', '', '', self.sa)
132 Session.commit()
131 gr = form_result['users_group_name']
132 action_logger(self.rhodecode_user,
133 'admin_updated_users_group:%s' % gr,
134 None, self.ip_addr, self.sa)
135 h.flash(_('updated users group %s') % gr, category='success')
136 Session().commit()
133 137 except formencode.Invalid, errors:
138 ug_model = UsersGroupModel()
139 defaults = errors.value
134 140 e = errors.error_dict or {}
135
136 perm = Permission.get_by_key('hg.create.repository')
137 e.update({'create_repo_perm':
138 UsersGroupModel().has_perm(id, perm)})
141 defaults.update({
142 'create_repo_perm': ug_model.has_perm(id,
143 'hg.create.repository'),
144 'fork_repo_perm': ug_model.has_perm(id,
145 'hg.fork.repository'),
146 '_method': 'put'
147 })
139 148
140 149 return htmlfill.render(
141 150 render('admin/users_groups/users_group_edit.html'),
142 defaults=errors.value,
151 defaults=defaults,
143 152 errors=e,
144 153 prefix_error=False,
145 154 encoding="UTF-8")
146 155 except Exception:
147 156 log.error(traceback.format_exc())
148 157 h.flash(_('error occurred during update of users group %s') \
149 158 % request.POST.get('users_group_name'), category='error')
150 159
151 return redirect(url('users_groups'))
160 return redirect(url('edit_users_group', id=id))
152 161
153 162 def delete(self, id):
154 163 """DELETE /users_groups/id: Delete an existing item"""
155 164 # Forms posted to this method should contain a hidden field:
156 165 # <input type="hidden" name="_method" value="DELETE" />
157 166 # Or using helpers:
158 167 # h.form(url('users_group', id=ID),
159 168 # method='delete')
160 169 # url('users_group', id=ID)
161
170 usr_gr = UsersGroup.get_or_404(id)
162 171 try:
163 UsersGroupModel().delete(id)
164 Session.commit()
172 UsersGroupModel().delete(usr_gr)
173 Session().commit()
165 174 h.flash(_('successfully deleted users group'), category='success')
166 175 except UsersGroupsAssignedException, e:
167 176 h.flash(e, category='error')
168 177 except Exception:
169 178 log.error(traceback.format_exc())
170 179 h.flash(_('An error occurred during deletion of users group'),
171 180 category='error')
172 181 return redirect(url('users_groups'))
173 182
174 183 def show(self, id, format='html'):
175 184 """GET /users_groups/id: Show a specific item"""
176 185 # url('users_group', id=ID)
177 186
178 187 def edit(self, id, format='html'):
179 188 """GET /users_groups/id/edit: Form to edit an existing item"""
180 189 # url('edit_users_group', id=ID)
181 190
182 c.users_group = self.sa.query(UsersGroup).get(id)
183 if not c.users_group:
184 return redirect(url('users_groups'))
191 c.users_group = UsersGroup.get_or_404(id)
185 192
186 193 c.users_group.permissions = {}
187 194 c.group_members_obj = [x.user for x in c.users_group.members]
188 195 c.group_members = [(x.user_id, x.username) for x in
189 196 c.group_members_obj]
190 197 c.available_members = [(x.user_id, x.username) for x in
191 self.sa.query(User).all()]
198 User.query().all()]
199 ug_model = UsersGroupModel()
192 200 defaults = c.users_group.get_dict()
193 perm = Permission.get_by_key('hg.create.repository')
194 defaults.update({'create_repo_perm':
195 UsersGroupModel().has_perm(c.users_group, perm)})
201 defaults.update({
202 'create_repo_perm': ug_model.has_perm(c.users_group,
203 'hg.create.repository'),
204 'fork_repo_perm': ug_model.has_perm(c.users_group,
205 'hg.fork.repository'),
206 })
207
196 208 return htmlfill.render(
197 209 render('admin/users_groups/users_group_edit.html'),
198 210 defaults=defaults,
199 211 encoding="UTF-8",
200 212 force_defaults=False
201 213 )
202 214
203 215 def update_perm(self, id):
204 216 """PUT /users_perm/id: Update an existing item"""
205 217 # url('users_group_perm', id=ID, method='put')
206 218
207 grant_perm = request.POST.get('create_repo_perm', False)
219 users_group = UsersGroup.get_or_404(id)
220 grant_create_perm = str2bool(request.POST.get('create_repo_perm'))
221 grant_fork_perm = str2bool(request.POST.get('fork_repo_perm'))
222 inherit_perms = str2bool(request.POST.get('inherit_default_permissions'))
223
224 usersgroup_model = UsersGroupModel()
208 225
209 if grant_perm:
210 perm = Permission.get_by_key('hg.create.none')
211 UsersGroupModel().revoke_perm(id, perm)
226 try:
227 users_group.inherit_default_permissions = inherit_perms
228 Session().add(users_group)
212 229
213 perm = Permission.get_by_key('hg.create.repository')
214 UsersGroupModel().grant_perm(id, perm)
215 h.flash(_("Granted 'repository create' permission to user"),
230 if grant_create_perm:
231 usersgroup_model.revoke_perm(id, 'hg.create.none')
232 usersgroup_model.grant_perm(id, 'hg.create.repository')
233 h.flash(_("Granted 'repository create' permission to users group"),
234 category='success')
235 else:
236 usersgroup_model.revoke_perm(id, 'hg.create.repository')
237 usersgroup_model.grant_perm(id, 'hg.create.none')
238 h.flash(_("Revoked 'repository create' permission to users group"),
216 239 category='success')
217 240
218 Session.commit()
241 if grant_fork_perm:
242 usersgroup_model.revoke_perm(id, 'hg.fork.none')
243 usersgroup_model.grant_perm(id, 'hg.fork.repository')
244 h.flash(_("Granted 'repository fork' permission to users group"),
245 category='success')
219 246 else:
220 perm = Permission.get_by_key('hg.create.repository')
221 UsersGroupModel().revoke_perm(id, perm)
247 usersgroup_model.revoke_perm(id, 'hg.fork.repository')
248 usersgroup_model.grant_perm(id, 'hg.fork.none')
249 h.flash(_("Revoked 'repository fork' permission to users group"),
250 category='success')
222 251
223 perm = Permission.get_by_key('hg.create.none')
224 UsersGroupModel().grant_perm(id, perm)
225 h.flash(_("Revoked 'repository create' permission to user"),
226 category='success')
227 Session.commit()
252 Session().commit()
253 except Exception:
254 log.error(traceback.format_exc())
255 h.flash(_('An error occurred during permissions saving'),
256 category='error')
257
228 258 return redirect(url('edit_users_group', id=id))
@@ -1,262 +1,282 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.api
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 JSON RPC controller
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 inspect
29 29 import logging
30 30 import types
31 31 import urllib
32 32 import traceback
33 import time
33 34
34 35 from rhodecode.lib.compat import izip_longest, json
35 36
36 37 from paste.response import replace_header
37 38
38 39 from pylons.controllers import WSGIController
39 40
40 41
41 42 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
42 43 HTTPBadRequest, HTTPError
43 44
44 45 from rhodecode.model.db import User
45 46 from rhodecode.lib.auth import AuthUser
47 from rhodecode.lib.base import _get_ip_addr, _get_access_path
48 from rhodecode.lib.utils2 import safe_unicode
46 49
47 50 log = logging.getLogger('JSONRPC')
48 51
49 52
50 53 class JSONRPCError(BaseException):
51 54
52 55 def __init__(self, message):
53 56 self.message = message
54 57 super(JSONRPCError, self).__init__()
55 58
56 59 def __str__(self):
57 60 return str(self.message)
58 61
59 62
60 def jsonrpc_error(message, code=None):
63 def jsonrpc_error(message, retid=None, code=None):
61 64 """
62 65 Generate a Response object with a JSON-RPC error body
63 66 """
64 67 from pylons.controllers.util import Response
65 resp = Response(body=json.dumps(dict(id=None, result=None, error=message)),
68 return Response(
69 body=json.dumps(dict(id=retid, result=None, error=message)),
66 70 status=code,
67 content_type='application/json')
68 return resp
71 content_type='application/json'
72 )
69 73
70 74
71 75 class JSONRPCController(WSGIController):
72 76 """
73 77 A WSGI-speaking JSON-RPC controller class
74 78
75 79 See the specification:
76 80 <http://json-rpc.org/wiki/specification>`.
77 81
78 82 Valid controller return values should be json-serializable objects.
79 83
80 84 Sub-classes should catch their exceptions and raise JSONRPCError
81 85 if they want to pass meaningful errors to the client.
82 86
83 87 """
84 88
85 89 def _get_method_args(self):
86 90 """
87 91 Return `self._rpc_args` to dispatched controller method
88 92 chosen by __call__
89 93 """
90 94 return self._rpc_args
91 95
92 96 def __call__(self, environ, start_response):
93 97 """
94 98 Parse the request body as JSON, look up the method on the
95 99 controller and if it exists, dispatch to it.
96 100 """
101 start = time.time()
102 self._req_id = None
97 103 if 'CONTENT_LENGTH' not in environ:
98 104 log.debug("No Content-Length")
99 return jsonrpc_error(message="No Content-Length in request")
105 return jsonrpc_error(retid=self._req_id,
106 message="No Content-Length in request")
100 107 else:
101 108 length = environ['CONTENT_LENGTH'] or 0
102 109 length = int(environ['CONTENT_LENGTH'])
103 110 log.debug('Content-Length: %s' % length)
104 111
105 112 if length == 0:
106 113 log.debug("Content-Length is 0")
107 return jsonrpc_error(message="Content-Length is 0")
114 return jsonrpc_error(retid=self._req_id,
115 message="Content-Length is 0")
108 116
109 117 raw_body = environ['wsgi.input'].read(length)
110 118
111 119 try:
112 120 json_body = json.loads(urllib.unquote_plus(raw_body))
113 121 except ValueError, e:
114 122 # catch JSON errors Here
115 return jsonrpc_error(message="JSON parse error ERR:%s RAW:%r" \
123 return jsonrpc_error(retid=self._req_id,
124 message="JSON parse error ERR:%s RAW:%r" \
116 125 % (e, urllib.unquote_plus(raw_body)))
117 126
118 127 # check AUTH based on API KEY
119 128 try:
120 129 self._req_api_key = json_body['api_key']
121 130 self._req_id = json_body['id']
122 131 self._req_method = json_body['method']
123 132 self._request_params = json_body['args']
124 133 log.debug(
125 134 'method: %s, params: %s' % (self._req_method,
126 135 self._request_params)
127 136 )
128 137 except KeyError, e:
129 return jsonrpc_error(message='Incorrect JSON query missing %s' % e)
138 return jsonrpc_error(retid=self._req_id,
139 message='Incorrect JSON query missing %s' % e)
130 140
131 141 # check if we can find this session using api_key
132 142 try:
133 143 u = User.get_by_api_key(self._req_api_key)
134 144 if u is None:
135 return jsonrpc_error(message='Invalid API KEY')
145 return jsonrpc_error(retid=self._req_id,
146 message='Invalid API KEY')
136 147 auth_u = AuthUser(u.user_id, self._req_api_key)
137 148 except Exception, e:
138 return jsonrpc_error(message='Invalid API KEY')
149 return jsonrpc_error(retid=self._req_id,
150 message='Invalid API KEY')
139 151
140 152 self._error = None
141 153 try:
142 154 self._func = self._find_method()
143 155 except AttributeError, e:
144 return jsonrpc_error(message=str(e))
156 return jsonrpc_error(retid=self._req_id,
157 message=str(e))
145 158
146 159 # now that we have a method, add self._req_params to
147 160 # self.kargs and dispatch control to WGIController
148 161 argspec = inspect.getargspec(self._func)
149 162 arglist = argspec[0][1:]
150 163 defaults = map(type, argspec[3] or [])
151 164 default_empty = types.NotImplementedType
152 165
153 166 # kw arguments required by this method
154 167 func_kwargs = dict(izip_longest(reversed(arglist), reversed(defaults),
155 168 fillvalue=default_empty))
156 169
157 170 # this is little trick to inject logged in user for
158 171 # perms decorators to work they expect the controller class to have
159 172 # rhodecode_user attribute set
160 173 self.rhodecode_user = auth_u
161 174
162 175 # This attribute will need to be first param of a method that uses
163 176 # api_key, which is translated to instance of user at that name
164 177 USER_SESSION_ATTR = 'apiuser'
165 178
166 179 if USER_SESSION_ATTR not in arglist:
167 return jsonrpc_error(message='This method [%s] does not support '
168 'authentication (missing %s param)' %
169 (self._func.__name__, USER_SESSION_ATTR))
180 return jsonrpc_error(
181 retid=self._req_id,
182 message='This method [%s] does not support '
183 'authentication (missing %s param)' % (
184 self._func.__name__, USER_SESSION_ATTR)
185 )
170 186
171 187 # get our arglist and check if we provided them as args
172 188 for arg, default in func_kwargs.iteritems():
173 189 if arg == USER_SESSION_ATTR:
174 190 # USER_SESSION_ATTR is something translated from api key and
175 191 # this is checked before so we don't need validate it
176 192 continue
177 193
178 194 # skip the required param check if it's default value is
179 195 # NotImplementedType (default_empty)
180 196 if (default == default_empty and arg not in self._request_params):
181 197 return jsonrpc_error(
198 retid=self._req_id,
182 199 message=(
183 200 'Missing non optional `%s` arg in JSON DATA' % arg
184 201 )
185 202 )
186 203
187 204 self._rpc_args = {USER_SESSION_ATTR: u}
188 205 self._rpc_args.update(self._request_params)
189 206
190 207 self._rpc_args['action'] = self._req_method
191 208 self._rpc_args['environ'] = environ
192 209 self._rpc_args['start_response'] = start_response
193 210
194 211 status = []
195 212 headers = []
196 213 exc_info = []
197 214
198 215 def change_content(new_status, new_headers, new_exc_info=None):
199 216 status.append(new_status)
200 217 headers.extend(new_headers)
201 218 exc_info.append(new_exc_info)
202 219
203 220 output = WSGIController.__call__(self, environ, change_content)
204 221 output = list(output)
205 222 headers.append(('Content-Length', str(len(output[0]))))
206 223 replace_header(headers, 'Content-Type', 'application/json')
207 224 start_response(status[0], headers, exc_info[0])
208
225 log.info('IP: %s Request to %s time: %.3fs' % (
226 _get_ip_addr(environ),
227 safe_unicode(_get_access_path(environ)), time.time() - start)
228 )
209 229 return output
210 230
211 231 def _dispatch_call(self):
212 232 """
213 233 Implement dispatch interface specified by WSGIController
214 234 """
215 235 try:
216 236 raw_response = self._inspect_call(self._func)
217 237 if isinstance(raw_response, HTTPError):
218 238 self._error = str(raw_response)
219 239 except JSONRPCError, e:
220 240 self._error = str(e)
221 241 except Exception, e:
222 242 log.error('Encountered unhandled exception: %s' \
223 243 % traceback.format_exc())
224 244 json_exc = JSONRPCError('Internal server error')
225 245 self._error = str(json_exc)
226 246
227 247 if self._error is not None:
228 248 raw_response = None
229 249
230 250 response = dict(id=self._req_id, result=raw_response,
231 251 error=self._error)
232 252
233 253 try:
234 254 return json.dumps(response)
235 255 except TypeError, e:
236 256 log.error('API FAILED. Error encoding response: %s' % e)
237 257 return json.dumps(
238 258 dict(
239 259 id=self._req_id,
240 260 result=None,
241 261 error="Error encoding response"
242 262 )
243 263 )
244 264
245 265 def _find_method(self):
246 266 """
247 267 Return method named by `self._req_method` in controller if able
248 268 """
249 269 log.debug('Trying to find JSON-RPC method: %s' % self._req_method)
250 270 if self._req_method.startswith('_'):
251 271 raise AttributeError("Method not allowed")
252 272
253 273 try:
254 274 func = getattr(self, self._req_method, None)
255 275 except UnicodeEncodeError:
256 276 raise AttributeError("Problem decoding unicode in requested "
257 277 "method name.")
258 278
259 279 if isinstance(func, types.MethodType):
260 280 return func
261 281 else:
262 282 raise AttributeError("No such method: %s" % self._req_method)
This diff has been collapsed as it changes many lines, (783 lines changed) Show them Hide them
@@ -1,657 +1,812 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 30
31 31 from rhodecode.controllers.api import JSONRPCController, JSONRPCError
32 32 from rhodecode.lib.auth import HasPermissionAllDecorator, \
33 33 HasPermissionAnyDecorator, PasswordGenerator, AuthUser
34
34 from rhodecode.lib.utils import map_groups, repo2db_mapper
35 35 from rhodecode.model.meta import Session
36 36 from rhodecode.model.scm import ScmModel
37 from rhodecode.model.db import User, UsersGroup, Repository
38 37 from rhodecode.model.repo import RepoModel
39 38 from rhodecode.model.user import UserModel
40 39 from rhodecode.model.users_group import UsersGroupModel
41 from rhodecode.lib.utils import map_groups
40 from rhodecode.model.permission import PermissionModel
41 from rhodecode.model.db import Repository
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 class Optional(object):
47 """
48 Defines an optional parameter::
49
50 param = param.getval() if isinstance(param, Optional) else param
51 param = param() if isinstance(param, Optional) else param
52
53 is equivalent of::
54
55 param = Optional.extract(param)
56
57 """
58 def __init__(self, type_):
59 self.type_ = type_
60
61 def __repr__(self):
62 return '<Optional:%s>' % self.type_.__repr__()
63
64 def __call__(self):
65 return self.getval()
66
67 def getval(self):
68 """
69 returns value from this Optional instance
70 """
71 return self.type_
72
73 @classmethod
74 def extract(cls, val):
75 if isinstance(val, cls):
76 return val.getval()
77 return val
78
79
80 def get_user_or_error(userid):
81 """
82 Get user by id or name or return JsonRPCError if not found
83
84 :param userid:
85 """
86 user = UserModel().get_user(userid)
87 if user is None:
88 raise JSONRPCError("user `%s` does not exist" % userid)
89 return user
90
91
92 def get_repo_or_error(repoid):
93 """
94 Get repo by id or name or return JsonRPCError if not found
95
96 :param userid:
97 """
98 repo = RepoModel().get_repo(repoid)
99 if repo is None:
100 raise JSONRPCError('repository `%s` does not exist' % (repoid))
101 return repo
102
103
104 def get_users_group_or_error(usersgroupid):
105 """
106 Get users group by id or name or return JsonRPCError if not found
107
108 :param userid:
109 """
110 users_group = UsersGroupModel().get_group(usersgroupid)
111 if users_group is None:
112 raise JSONRPCError('users group `%s` does not exist' % usersgroupid)
113 return users_group
114
115
116 def get_perm_or_error(permid):
117 """
118 Get permission by id or name or return JsonRPCError if not found
119
120 :param userid:
121 """
122 perm = PermissionModel().get_permission_by_name(permid)
123 if perm is None:
124 raise JSONRPCError('permission `%s` does not exist' % (permid))
125 return perm
126
127
46 128 class ApiController(JSONRPCController):
47 129 """
48 130 API Controller
49 131
50 132
51 133 Each method needs to have USER as argument this is then based on given
52 134 API_KEY propagated as instance of user object
53 135
54 136 Preferably this should be first argument also
55 137
56 138
57 139 Each function should also **raise** JSONRPCError for any
58 140 errors that happens
59 141
60 142 """
61 143
62 144 @HasPermissionAllDecorator('hg.admin')
63 def pull(self, apiuser, repo_name):
145 def pull(self, apiuser, repoid):
64 146 """
65 147 Dispatch pull action on given repo
66 148
149 :param apiuser:
150 :param repoid:
151 """
67 152
68 :param user:
69 :param repo_name:
153 repo = get_repo_or_error(repoid)
154
155 try:
156 ScmModel().pull_changes(repo.repo_name,
157 self.rhodecode_user.username)
158 return 'Pulled from `%s`' % repo.repo_name
159 except Exception:
160 log.error(traceback.format_exc())
161 raise JSONRPCError(
162 'Unable to pull changes from `%s`' % repo.repo_name
163 )
164
165 @HasPermissionAllDecorator('hg.admin')
166 def rescan_repos(self, apiuser, remove_obsolete=Optional(False)):
167 """
168 Dispatch rescan repositories action. If remove_obsolete is set
169 than also delete repos that are in database but not in the filesystem.
170 aka "clean zombies"
171
172 :param apiuser:
173 :param remove_obsolete:
70 174 """
71 175
72 if Repository.is_valid(repo_name) is False:
73 raise JSONRPCError('Unknown repo "%s"' % repo_name)
74
75 176 try:
76 ScmModel().pull_changes(repo_name, self.rhodecode_user.username)
77 return 'Pulled from %s' % repo_name
177 rm_obsolete = Optional.extract(remove_obsolete)
178 added, removed = repo2db_mapper(ScmModel().repo_scan(),
179 remove_obsolete=rm_obsolete)
180 return {'added': added, 'removed': removed}
78 181 except Exception:
79 raise JSONRPCError('Unable to pull changes from "%s"' % repo_name)
182 log.error(traceback.format_exc())
183 raise JSONRPCError(
184 'Error occurred during rescan repositories action'
185 )
186
187 @HasPermissionAllDecorator('hg.admin')
188 def lock(self, apiuser, repoid, userid, locked):
189 """
190 Set locking state on particular repository by given user
191
192 :param apiuser:
193 :param repoid:
194 :param userid:
195 :param locked:
196 """
197 repo = get_repo_or_error(repoid)
198 user = get_user_or_error(userid)
199 locked = bool(locked)
200 try:
201 if locked:
202 Repository.lock(repo, user.user_id)
203 else:
204 Repository.unlock(repo)
205
206 return ('User `%s` set lock state for repo `%s` to `%s`'
207 % (user.username, repo.repo_name, locked))
208 except Exception:
209 log.error(traceback.format_exc())
210 raise JSONRPCError(
211 'Error occurred locking repository `%s`' % repo.repo_name
212 )
80 213
81 214 @HasPermissionAllDecorator('hg.admin')
82 215 def get_user(self, apiuser, userid):
83 216 """"
84 217 Get a user by username
85 218
86 219 :param apiuser:
87 :param username:
220 :param userid:
88 221 """
89 222
90 user = UserModel().get_user(userid)
91 if user is None:
92 return user
93
94 return dict(
95 id=user.user_id,
96 username=user.username,
97 firstname=user.name,
98 lastname=user.lastname,
99 email=user.email,
100 active=user.active,
101 admin=user.admin,
102 ldap_dn=user.ldap_dn,
103 last_login=user.last_login,
104 permissions=AuthUser(user_id=user.user_id).permissions
105 )
223 user = get_user_or_error(userid)
224 data = user.get_api_data()
225 data['permissions'] = AuthUser(user_id=user.user_id).permissions
226 return data
106 227
107 228 @HasPermissionAllDecorator('hg.admin')
108 229 def get_users(self, apiuser):
109 230 """"
110 231 Get all users
111 232
112 233 :param apiuser:
113 234 """
114 235
115 236 result = []
116 for user in User.getAll():
117 result.append(
118 dict(
119 id=user.user_id,
120 username=user.username,
121 firstname=user.name,
122 lastname=user.lastname,
123 email=user.email,
124 active=user.active,
125 admin=user.admin,
126 ldap_dn=user.ldap_dn,
127 last_login=user.last_login,
128 )
129 )
237 for user in UserModel().get_all():
238 result.append(user.get_api_data())
130 239 return result
131 240
132 241 @HasPermissionAllDecorator('hg.admin')
133 def create_user(self, apiuser, username, email, password, firstname=None,
134 lastname=None, active=True, admin=False, ldap_dn=None):
242 def create_user(self, apiuser, username, email, password,
243 firstname=Optional(None), lastname=Optional(None),
244 active=Optional(True), admin=Optional(False),
245 ldap_dn=Optional(None)):
135 246 """
136 247 Create new user
137 248
138 249 :param apiuser:
139 250 :param username:
251 :param email:
140 252 :param password:
141 :param email:
142 :param name:
253 :param firstname:
143 254 :param lastname:
144 255 :param active:
145 256 :param admin:
146 257 :param ldap_dn:
147 258 """
148 if User.get_by_username(username):
149 raise JSONRPCError("user %s already exist" % username)
259
260 if UserModel().get_by_username(username):
261 raise JSONRPCError("user `%s` already exist" % username)
150 262
151 if User.get_by_email(email, case_insensitive=True):
152 raise JSONRPCError("email %s already exist" % email)
263 if UserModel().get_by_email(email, case_insensitive=True):
264 raise JSONRPCError("email `%s` already exist" % email)
153 265
154 if ldap_dn:
266 if Optional.extract(ldap_dn):
155 267 # generate temporary password if ldap_dn
156 268 password = PasswordGenerator().gen_password(length=8)
157 269
158 270 try:
159 usr = UserModel().create_or_update(
160 username, password, email, firstname,
161 lastname, active, admin, ldap_dn
271 user = UserModel().create_or_update(
272 username=Optional.extract(username),
273 password=Optional.extract(password),
274 email=Optional.extract(email),
275 firstname=Optional.extract(firstname),
276 lastname=Optional.extract(lastname),
277 active=Optional.extract(active),
278 admin=Optional.extract(admin),
279 ldap_dn=Optional.extract(ldap_dn)
162 280 )
163 Session.commit()
281 Session().commit()
164 282 return dict(
165 id=usr.user_id,
166 msg='created new user %s' % username
283 msg='created new user `%s`' % username,
284 user=user.get_api_data()
167 285 )
168 286 except Exception:
169 287 log.error(traceback.format_exc())
170 raise JSONRPCError('failed to create user %s' % username)
288 raise JSONRPCError('failed to create user `%s`' % username)
171 289
172 290 @HasPermissionAllDecorator('hg.admin')
173 def update_user(self, apiuser, userid, username, password, email,
174 firstname, lastname, active, admin, ldap_dn):
291 def update_user(self, apiuser, userid, username=Optional(None),
292 email=Optional(None), firstname=Optional(None),
293 lastname=Optional(None), active=Optional(None),
294 admin=Optional(None), ldap_dn=Optional(None),
295 password=Optional(None)):
175 296 """
176 297 Updates given user
177 298
178 299 :param apiuser:
300 :param userid:
179 301 :param username:
180 :param password:
181 302 :param email:
182 :param name:
303 :param firstname:
183 304 :param lastname:
184 305 :param active:
185 306 :param admin:
186 307 :param ldap_dn:
308 :param password:
187 309 """
188 if not UserModel().get_user(userid):
189 raise JSONRPCError("user %s does not exist" % username)
310
311 user = get_user_or_error(userid)
312
313 # call function and store only updated arguments
314 updates = {}
315
316 def store_update(attr, name):
317 if not isinstance(attr, Optional):
318 updates[name] = attr
190 319
191 320 try:
192 usr = UserModel().create_or_update(
193 username, password, email, firstname,
194 lastname, active, admin, ldap_dn
195 )
196 Session.commit()
321
322 store_update(username, 'username')
323 store_update(password, 'password')
324 store_update(email, 'email')
325 store_update(firstname, 'name')
326 store_update(lastname, 'lastname')
327 store_update(active, 'active')
328 store_update(admin, 'admin')
329 store_update(ldap_dn, 'ldap_dn')
330
331 user = UserModel().update_user(user, **updates)
332 Session().commit()
197 333 return dict(
198 id=usr.user_id,
199 msg='updated user %s' % username
334 msg='updated user ID:%s %s' % (user.user_id, user.username),
335 user=user.get_api_data()
200 336 )
201 337 except Exception:
202 338 log.error(traceback.format_exc())
203 raise JSONRPCError('failed to update user %s' % username)
339 raise JSONRPCError('failed to update user `%s`' % userid)
204 340
205 341 @HasPermissionAllDecorator('hg.admin')
206 def get_users_group(self, apiuser, group_name):
342 def delete_user(self, apiuser, userid):
207 343 """"
208 Get users group by name
344 Deletes an user
209 345
210 346 :param apiuser:
211 :param group_name:
347 :param userid:
212 348 """
349 user = get_user_or_error(userid)
213 350
214 users_group = UsersGroup.get_by_group_name(group_name)
215 if not users_group:
216 return None
351 try:
352 UserModel().delete(userid)
353 Session().commit()
354 return dict(
355 msg='deleted user ID:%s %s' % (user.user_id, user.username),
356 user=None
357 )
358 except Exception:
359 log.error(traceback.format_exc())
360 raise JSONRPCError('failed to delete ID:%s %s' % (user.user_id,
361 user.username))
362
363 @HasPermissionAllDecorator('hg.admin')
364 def get_users_group(self, apiuser, usersgroupid):
365 """"
366 Get users group by name or id
367
368 :param apiuser:
369 :param usersgroupid:
370 """
371 users_group = get_users_group_or_error(usersgroupid)
372
373 data = users_group.get_api_data()
217 374
218 375 members = []
219 376 for user in users_group.members:
220 377 user = user.user
221 members.append(dict(id=user.user_id,
222 username=user.username,
223 firstname=user.name,
224 lastname=user.lastname,
225 email=user.email,
226 active=user.active,
227 admin=user.admin,
228 ldap=user.ldap_dn))
229
230 return dict(id=users_group.users_group_id,
231 group_name=users_group.users_group_name,
232 active=users_group.users_group_active,
233 members=members)
378 members.append(user.get_api_data())
379 data['members'] = members
380 return data
234 381
235 382 @HasPermissionAllDecorator('hg.admin')
236 383 def get_users_groups(self, apiuser):
237 384 """"
238 385 Get all users groups
239 386
240 387 :param apiuser:
241 388 """
242 389
243 390 result = []
244 for users_group in UsersGroup.getAll():
245 members = []
246 for user in users_group.members:
247 user = user.user
248 members.append(dict(id=user.user_id,
249 username=user.username,
250 firstname=user.name,
251 lastname=user.lastname,
252 email=user.email,
253 active=user.active,
254 admin=user.admin,
255 ldap=user.ldap_dn))
256
257 result.append(dict(id=users_group.users_group_id,
258 group_name=users_group.users_group_name,
259 active=users_group.users_group_active,
260 members=members))
391 for users_group in UsersGroupModel().get_all():
392 result.append(users_group.get_api_data())
261 393 return result
262 394
263 395 @HasPermissionAllDecorator('hg.admin')
264 def create_users_group(self, apiuser, group_name, active=True):
396 def create_users_group(self, apiuser, group_name, active=Optional(True)):
265 397 """
266 398 Creates an new usergroup
267 399
400 :param apiuser:
268 401 :param group_name:
269 402 :param active:
270 403 """
271 404
272 if self.get_users_group(apiuser, group_name):
273 raise JSONRPCError("users group %s already exist" % group_name)
405 if UsersGroupModel().get_by_name(group_name):
406 raise JSONRPCError("users group `%s` already exist" % group_name)
274 407
275 408 try:
409 active = Optional.extract(active)
276 410 ug = UsersGroupModel().create(name=group_name, active=active)
277 Session.commit()
278 return dict(id=ug.users_group_id,
279 msg='created new users group %s' % group_name)
411 Session().commit()
412 return dict(
413 msg='created new users group `%s`' % group_name,
414 users_group=ug.get_api_data()
415 )
280 416 except Exception:
281 417 log.error(traceback.format_exc())
282 raise JSONRPCError('failed to create group %s' % group_name)
418 raise JSONRPCError('failed to create group `%s`' % group_name)
283 419
284 420 @HasPermissionAllDecorator('hg.admin')
285 def add_user_to_users_group(self, apiuser, group_name, username):
421 def add_user_to_users_group(self, apiuser, usersgroupid, userid):
286 422 """"
287 423 Add a user to a users group
288 424
289 425 :param apiuser:
290 :param group_name:
291 :param username:
426 :param usersgroupid:
427 :param userid:
292 428 """
429 user = get_user_or_error(userid)
430 users_group = get_users_group_or_error(usersgroupid)
293 431
294 432 try:
295 users_group = UsersGroup.get_by_group_name(group_name)
296 if not users_group:
297 raise JSONRPCError('unknown users group %s' % group_name)
298
299 user = User.get_by_username(username)
300 if user is None:
301 raise JSONRPCError('unknown user %s' % username)
302
303 433 ugm = UsersGroupModel().add_user_to_group(users_group, user)
304 434 success = True if ugm != True else False
305 msg = 'added member %s to users group %s' % (username, group_name)
435 msg = 'added member `%s` to users group `%s`' % (
436 user.username, users_group.users_group_name
437 )
306 438 msg = msg if success else 'User is already in that group'
307 Session.commit()
439 Session().commit()
308 440
309 441 return dict(
310 id=ugm.users_group_member_id if ugm != True else None,
311 442 success=success,
312 443 msg=msg
313 444 )
314 445 except Exception:
315 446 log.error(traceback.format_exc())
316 raise JSONRPCError('failed to add users group member')
447 raise JSONRPCError(
448 'failed to add member to users group `%s`' % (
449 users_group.users_group_name
450 )
451 )
317 452
318 453 @HasPermissionAllDecorator('hg.admin')
319 def remove_user_from_users_group(self, apiuser, group_name, username):
454 def remove_user_from_users_group(self, apiuser, usersgroupid, userid):
320 455 """
321 456 Remove user from a group
322 457
323 :param apiuser
324 :param group_name
325 :param username
458 :param apiuser:
459 :param usersgroupid:
460 :param userid:
326 461 """
462 user = get_user_or_error(userid)
463 users_group = get_users_group_or_error(usersgroupid)
327 464
328 465 try:
329 users_group = UsersGroup.get_by_group_name(group_name)
330 if not users_group:
331 raise JSONRPCError('unknown users group %s' % group_name)
332
333 user = User.get_by_username(username)
334 if user is None:
335 raise JSONRPCError('unknown user %s' % username)
336
337 success = UsersGroupModel().remove_user_from_group(users_group, user)
338 msg = 'removed member %s from users group %s' % (username, group_name)
466 success = UsersGroupModel().remove_user_from_group(users_group,
467 user)
468 msg = 'removed member `%s` from users group `%s`' % (
469 user.username, users_group.users_group_name
470 )
339 471 msg = msg if success else "User wasn't in group"
340 Session.commit()
472 Session().commit()
341 473 return dict(success=success, msg=msg)
342 474 except Exception:
343 475 log.error(traceback.format_exc())
344 raise JSONRPCError('failed to remove user from group')
476 raise JSONRPCError(
477 'failed to remove member from users group `%s`' % (
478 users_group.users_group_name
479 )
480 )
345 481
346 482 @HasPermissionAnyDecorator('hg.admin')
347 483 def get_repo(self, apiuser, repoid):
348 484 """"
349 485 Get repository by name
350 486
351 487 :param apiuser:
352 :param repo_name:
488 :param repoid:
353 489 """
354
355 repo = RepoModel().get_repo(repoid)
356 if repo is None:
357 raise JSONRPCError('unknown repository %s' % repo)
490 repo = get_repo_or_error(repoid)
358 491
359 492 members = []
360 493 for user in repo.repo_to_perm:
361 494 perm = user.permission.permission_name
362 495 user = user.user
363 members.append(
364 dict(
365 type="user",
366 id=user.user_id,
367 username=user.username,
368 firstname=user.name,
369 lastname=user.lastname,
370 email=user.email,
371 active=user.active,
372 admin=user.admin,
373 ldap=user.ldap_dn,
374 permission=perm
375 )
376 )
496 user_data = user.get_api_data()
497 user_data['type'] = "user"
498 user_data['permission'] = perm
499 members.append(user_data)
500
377 501 for users_group in repo.users_group_to_perm:
378 502 perm = users_group.permission.permission_name
379 503 users_group = users_group.users_group
380 members.append(
381 dict(
382 type="users_group",
383 id=users_group.users_group_id,
384 name=users_group.users_group_name,
385 active=users_group.users_group_active,
386 permission=perm
387 )
388 )
504 users_group_data = users_group.get_api_data()
505 users_group_data['type'] = "users_group"
506 users_group_data['permission'] = perm
507 members.append(users_group_data)
389 508
390 return dict(
391 id=repo.repo_id,
392 repo_name=repo.repo_name,
393 type=repo.repo_type,
394 description=repo.description,
395 members=members
396 )
509 data = repo.get_api_data()
510 data['members'] = members
511 return data
397 512
398 513 @HasPermissionAnyDecorator('hg.admin')
399 514 def get_repos(self, apiuser):
400 515 """"
401 516 Get all repositories
402 517
403 518 :param apiuser:
404 519 """
405 520
406 521 result = []
407 for repository in Repository.getAll():
408 result.append(
409 dict(
410 id=repository.repo_id,
411 repo_name=repository.repo_name,
412 type=repository.repo_type,
413 description=repository.description
414 )
415 )
522 for repo in RepoModel().get_all():
523 result.append(repo.get_api_data())
416 524 return result
417 525
418 526 @HasPermissionAnyDecorator('hg.admin')
419 def get_repo_nodes(self, apiuser, repo_name, revision, root_path,
527 def get_repo_nodes(self, apiuser, repoid, revision, root_path,
420 528 ret_type='all'):
421 529 """
422 530 returns a list of nodes and it's children
423 531 for a given path at given revision. It's possible to specify ret_type
424 532 to show only files or dirs
425 533
426 534 :param apiuser:
427 :param repo_name: name of repository
535 :param repoid: name or id of repository
428 536 :param revision: revision for which listing should be done
429 537 :param root_path: path from which start displaying
430 538 :param ret_type: return type 'all|files|dirs' nodes
431 539 """
540 repo = get_repo_or_error(repoid)
432 541 try:
433 _d, _f = ScmModel().get_nodes(repo_name, revision, root_path,
542 _d, _f = ScmModel().get_nodes(repo, revision, root_path,
434 543 flat=False)
435 544 _map = {
436 545 'all': _d + _f,
437 546 'files': _f,
438 547 'dirs': _d,
439 548 }
440 549 return _map[ret_type]
441 550 except KeyError:
442 551 raise JSONRPCError('ret_type must be one of %s' % _map.keys())
443 except Exception, e:
444 raise JSONRPCError(e)
552 except Exception:
553 log.error(traceback.format_exc())
554 raise JSONRPCError(
555 'failed to get repo: `%s` nodes' % repo.repo_name
556 )
445 557
446 558 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
447 def create_repo(self, apiuser, repo_name, owner_name, description='',
448 repo_type='hg', private=False, clone_uri=None):
559 def create_repo(self, apiuser, repo_name, owner, repo_type,
560 description=Optional(''), private=Optional(False),
561 clone_uri=Optional(None), landing_rev=Optional('tip')):
449 562 """
450 563 Create repository, if clone_url is given it makes a remote clone
564 if repo_name is withina group name the groups will be created
565 automatically if they aren't present
451 566
452 567 :param apiuser:
453 568 :param repo_name:
454 :param owner_name:
569 :param onwer:
570 :param repo_type:
455 571 :param description:
456 :param repo_type:
457 572 :param private:
458 573 :param clone_uri:
574 :param landing_rev:
459 575 """
576 owner = get_user_or_error(owner)
577
578 if RepoModel().get_by_repo_name(repo_name):
579 raise JSONRPCError("repo `%s` already exist" % repo_name)
580
581 private = Optional.extract(private)
582 clone_uri = Optional.extract(clone_uri)
583 description = Optional.extract(description)
584 landing_rev = Optional.extract(landing_rev)
460 585
461 586 try:
462 owner = User.get_by_username(owner_name)
463 if owner is None:
464 raise JSONRPCError('unknown user %s' % owner_name)
465
466 if Repository.get_by_repo_name(repo_name):
467 raise JSONRPCError("repo %s already exist" % repo_name)
468
469 groups = repo_name.split(Repository.url_sep())
470 real_name = groups[-1]
471 # create structure of groups
587 # create structure of groups and return the last group
472 588 group = map_groups(repo_name)
473 589
474 repo = RepoModel().create(
475 dict(
476 repo_name=real_name,
477 repo_name_full=repo_name,
478 description=description,
479 private=private,
590 repo = RepoModel().create_repo(
591 repo_name=repo_name,
480 592 repo_type=repo_type,
481 repo_group=group.group_id if group else None,
482 clone_uri=clone_uri
483 ),
484 owner
593 description=description,
594 owner=owner,
595 private=private,
596 clone_uri=clone_uri,
597 repos_group=group,
598 landing_rev=landing_rev,
485 599 )
486 Session.commit()
600
601 Session().commit()
487 602
488 603 return dict(
489 id=repo.repo_id,
490 msg="Created new repository %s" % repo.repo_name
604 msg="Created new repository `%s`" % (repo.repo_name),
605 repo=repo.get_api_data()
491 606 )
492 607
493 608 except Exception:
494 609 log.error(traceback.format_exc())
495 raise JSONRPCError('failed to create repository %s' % repo_name)
610 raise JSONRPCError('failed to create repository `%s`' % repo_name)
496 611
497 612 @HasPermissionAnyDecorator('hg.admin')
498 def delete_repo(self, apiuser, repo_name):
499 """
500 Deletes a given repository
613 def fork_repo(self, apiuser, repoid, fork_name, owner,
614 description=Optional(''), copy_permissions=Optional(False),
615 private=Optional(False), landing_rev=Optional('tip')):
616 repo = get_repo_or_error(repoid)
617 repo_name = repo.repo_name
618 owner = get_user_or_error(owner)
619
620 _repo = RepoModel().get_by_repo_name(fork_name)
621 if _repo:
622 type_ = 'fork' if _repo.fork else 'repo'
623 raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
624
625 try:
626 # create structure of groups and return the last group
627 group = map_groups(fork_name)
501 628
502 :param repo_name:
503 """
504 if not Repository.get_by_repo_name(repo_name):
505 raise JSONRPCError("repo %s does not exist" % repo_name)
506 try:
507 RepoModel().delete(repo_name)
508 Session.commit()
629 form_data = dict(
630 repo_name=fork_name,
631 repo_name_full=fork_name,
632 repo_group=group,
633 repo_type=repo.repo_type,
634 description=Optional.extract(description),
635 private=Optional.extract(private),
636 copy_permissions=Optional.extract(copy_permissions),
637 landing_rev=Optional.extract(landing_rev),
638 update_after_clone=False,
639 fork_parent_id=repo.repo_id,
640 )
641 RepoModel().create_fork(form_data, cur_user=owner)
509 642 return dict(
510 msg='Deleted repository %s' % repo_name
643 msg='Created fork of `%s` as `%s`' % (repo.repo_name,
644 fork_name),
645 success=True # cannot return the repo data here since fork
646 # cann be done async
511 647 )
512 648 except Exception:
513 649 log.error(traceback.format_exc())
514 raise JSONRPCError('failed to delete repository %s' % repo_name)
650 raise JSONRPCError(
651 'failed to fork repository `%s` as `%s`' % (repo_name,
652 fork_name)
653 )
515 654
516 655 @HasPermissionAnyDecorator('hg.admin')
517 def grant_user_permission(self, apiuser, repo_name, username, perm):
656 def delete_repo(self, apiuser, repoid):
518 657 """
519 Grant permission for user on given repository, or update existing one
520 if found
658 Deletes a given repository
521 659
522 :param repo_name:
523 :param username:
524 :param perm:
660 :param apiuser:
661 :param repoid:
525 662 """
663 repo = get_repo_or_error(repoid)
526 664
527 665 try:
528 repo = Repository.get_by_repo_name(repo_name)
529 if repo is None:
530 raise JSONRPCError('unknown repository %s' % repo)
531
532 user = User.get_by_username(username)
533 if user is None:
534 raise JSONRPCError('unknown user %s' % username)
535
536 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
537
538 Session.commit()
666 RepoModel().delete(repo)
667 Session().commit()
539 668 return dict(
540 msg='Granted perm: %s for user: %s in repo: %s' % (
541 perm, username, repo_name
542 )
669 msg='Deleted repository `%s`' % repo.repo_name,
670 success=True
543 671 )
544 672 except Exception:
545 673 log.error(traceback.format_exc())
546 674 raise JSONRPCError(
547 'failed to edit permission %(repo)s for %(user)s' % dict(
548 user=username, repo=repo_name
675 'failed to delete repository `%s`' % repo.repo_name
676 )
677
678 @HasPermissionAnyDecorator('hg.admin')
679 def grant_user_permission(self, apiuser, repoid, userid, perm):
680 """
681 Grant permission for user on given repository, or update existing one
682 if found
683
684 :param repoid:
685 :param userid:
686 :param perm:
687 """
688 repo = get_repo_or_error(repoid)
689 user = get_user_or_error(userid)
690 perm = get_perm_or_error(perm)
691
692 try:
693
694 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
695
696 Session().commit()
697 return dict(
698 msg='Granted perm: `%s` for user: `%s` in repo: `%s`' % (
699 perm.permission_name, user.username, repo.repo_name
700 ),
701 success=True
702 )
703 except Exception:
704 log.error(traceback.format_exc())
705 raise JSONRPCError(
706 'failed to edit permission for user: `%s` in repo: `%s`' % (
707 userid, repoid
549 708 )
550 709 )
551 710
552 711 @HasPermissionAnyDecorator('hg.admin')
553 def revoke_user_permission(self, apiuser, repo_name, username):
712 def revoke_user_permission(self, apiuser, repoid, userid):
554 713 """
555 714 Revoke permission for user on given repository
556 715
557 :param repo_name:
558 :param username:
716 :param apiuser:
717 :param repoid:
718 :param userid:
559 719 """
560 720
721 repo = get_repo_or_error(repoid)
722 user = get_user_or_error(userid)
561 723 try:
562 repo = Repository.get_by_repo_name(repo_name)
563 if repo is None:
564 raise JSONRPCError('unknown repository %s' % repo)
724
725 RepoModel().revoke_user_permission(repo=repo, user=user)
565 726
566 user = User.get_by_username(username)
567 if user is None:
568 raise JSONRPCError('unknown user %s' % username)
569
570 RepoModel().revoke_user_permission(repo=repo_name, user=username)
571
572 Session.commit()
727 Session().commit()
573 728 return dict(
574 msg='Revoked perm for user: %s in repo: %s' % (
575 username, repo_name
576 )
729 msg='Revoked perm for user: `%s` in repo: `%s`' % (
730 user.username, repo.repo_name
731 ),
732 success=True
577 733 )
578 734 except Exception:
579 735 log.error(traceback.format_exc())
580 736 raise JSONRPCError(
581 'failed to edit permission %(repo)s for %(user)s' % dict(
582 user=username, repo=repo_name
737 'failed to edit permission for user: `%s` in repo: `%s`' % (
738 userid, repoid
583 739 )
584 740 )
585 741
586 742 @HasPermissionAnyDecorator('hg.admin')
587 def grant_users_group_permission(self, apiuser, repo_name, group_name, perm):
743 def grant_users_group_permission(self, apiuser, repoid, usersgroupid,
744 perm):
588 745 """
589 746 Grant permission for users group on given repository, or update
590 747 existing one if found
591 748
592 :param repo_name:
593 :param group_name:
749 :param apiuser:
750 :param repoid:
751 :param usersgroupid:
594 752 :param perm:
595 753 """
754 repo = get_repo_or_error(repoid)
755 perm = get_perm_or_error(perm)
756 users_group = get_users_group_or_error(usersgroupid)
596 757
597 758 try:
598 repo = Repository.get_by_repo_name(repo_name)
599 if repo is None:
600 raise JSONRPCError('unknown repository %s' % repo)
601
602 user_group = UsersGroup.get_by_group_name(group_name)
603 if user_group is None:
604 raise JSONRPCError('unknown users group %s' % user_group)
605
606 RepoModel().grant_users_group_permission(repo=repo_name,
607 group_name=group_name,
759 RepoModel().grant_users_group_permission(repo=repo,
760 group_name=users_group,
608 761 perm=perm)
609 762
610 Session.commit()
763 Session().commit()
611 764 return dict(
612 msg='Granted perm: %s for group: %s in repo: %s' % (
613 perm, group_name, repo_name
614 )
765 msg='Granted perm: `%s` for users group: `%s` in '
766 'repo: `%s`' % (
767 perm.permission_name, users_group.users_group_name,
768 repo.repo_name
769 ),
770 success=True
615 771 )
616 772 except Exception:
773 print traceback.format_exc()
617 774 log.error(traceback.format_exc())
618 775 raise JSONRPCError(
619 'failed to edit permission %(repo)s for %(usersgr)s' % dict(
620 usersgr=group_name, repo=repo_name
776 'failed to edit permission for users group: `%s` in '
777 'repo: `%s`' % (
778 usersgroupid, repo.repo_name
621 779 )
622 780 )
623 781
624 782 @HasPermissionAnyDecorator('hg.admin')
625 def revoke_users_group_permission(self, apiuser, repo_name, group_name):
783 def revoke_users_group_permission(self, apiuser, repoid, usersgroupid):
626 784 """
627 785 Revoke permission for users group on given repository
628 786
629 :param repo_name:
630 :param group_name:
787 :param apiuser:
788 :param repoid:
789 :param usersgroupid:
631 790 """
791 repo = get_repo_or_error(repoid)
792 users_group = get_users_group_or_error(usersgroupid)
632 793
633 794 try:
634 repo = Repository.get_by_repo_name(repo_name)
635 if repo is None:
636 raise JSONRPCError('unknown repository %s' % repo)
637
638 user_group = UsersGroup.get_by_group_name(group_name)
639 if user_group is None:
640 raise JSONRPCError('unknown users group %s' % user_group)
795 RepoModel().revoke_users_group_permission(repo=repo,
796 group_name=users_group)
641 797
642 RepoModel().revoke_users_group_permission(repo=repo_name,
643 group_name=group_name)
644
645 Session.commit()
798 Session().commit()
646 799 return dict(
647 msg='Revoked perm for group: %s in repo: %s' % (
648 group_name, repo_name
649 )
800 msg='Revoked perm for users group: `%s` in repo: `%s`' % (
801 users_group.users_group_name, repo.repo_name
802 ),
803 success=True
650 804 )
651 805 except Exception:
652 806 log.error(traceback.format_exc())
653 807 raise JSONRPCError(
654 'failed to edit permission %(repo)s for %(usersgr)s' % dict(
655 usersgr=group_name, repo=repo_name
808 'failed to edit permission for users group: `%s` in '
809 'repo: `%s`' % (
810 users_group.users_group_name, repo.repo_name
656 811 )
657 812 )
@@ -1,75 +1,76 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.branches
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 branches 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 import binascii
27 28
28 29 from pylons import tmpl_context as c
29 import binascii
30 30
31 31 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
32 32 from rhodecode.lib.base import BaseRepoController, render
33 33 from rhodecode.lib.compat import OrderedDict
34 34 from rhodecode.lib.utils2 import safe_unicode
35
35 36 log = logging.getLogger(__name__)
36 37
37 38
38 39 class BranchesController(BaseRepoController):
39 40
40 41 @LoginRequired()
41 42 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
42 43 'repository.admin')
43 44 def __before__(self):
44 45 super(BranchesController, self).__before__()
45 46
46 47 def index(self):
47 48
48 49 def _branchtags(localrepo):
49 50 bt_closed = {}
50 51 for bn, heads in localrepo.branchmap().iteritems():
51 52 tip = heads[-1]
52 53 if 'close' in localrepo.changelog.read(tip)[5]:
53 54 bt_closed[bn] = tip
54 55 return bt_closed
55 56
56 57 cs_g = c.rhodecode_repo.get_changeset
57 58
58 59 c.repo_closed_branches = {}
59 60 if c.rhodecode_db_repo.repo_type == 'hg':
60 61 bt_closed = _branchtags(c.rhodecode_repo._repo)
61 62 _closed_branches = [(safe_unicode(n), cs_g(binascii.hexlify(h)),)
62 63 for n, h in bt_closed.items()]
63 64
64 65 c.repo_closed_branches = OrderedDict(sorted(_closed_branches,
65 66 key=lambda ctx: ctx[0],
66 67 reverse=False))
67 68
68 69 _branches = [(safe_unicode(n), cs_g(h))
69 70 for n, h in c.rhodecode_repo.branches.items()]
70 71 c.repo_branches = OrderedDict(sorted(_branches,
71 72 key=lambda ctx: ctx[0],
72 73 reverse=False))
73 74
74 75
75 76 return render('branches/branches.html')
@@ -1,135 +1,124 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 from mercurial import graphmod
30 29 from pylons import request, url, session, tmpl_context as c
31 30 from pylons.controllers.util import redirect
32 31 from pylons.i18n.translation import _
33 32
34 33 import rhodecode.lib.helpers as h
35 34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 35 from rhodecode.lib.base import BaseRepoController, render
37 36 from rhodecode.lib.helpers import RepoPage
38 37 from rhodecode.lib.compat import json
39
38 from rhodecode.lib.graphmod import _colored, _dagwalker
40 39 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError
41 from rhodecode.model.db import Repository
42 40
43 41 log = logging.getLogger(__name__)
44 42
45 43
46 44 class ChangelogController(BaseRepoController):
47 45
48 46 @LoginRequired()
49 47 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
50 48 'repository.admin')
51 49 def __before__(self):
52 50 super(ChangelogController, self).__before__()
53 51 c.affected_files_cut_off = 60
54 52
55 53 def index(self):
56 54 limit = 100
57 55 default = 20
58 56 if request.params.get('size'):
59 57 try:
60 58 int_size = int(request.params.get('size'))
61 59 except ValueError:
62 60 int_size = default
63 int_size = int_size if int_size <= limit else limit
64 c.size = int_size
61 c.size = max(min(int_size, limit), 1)
65 62 session['changelog_size'] = c.size
66 63 session.save()
67 64 else:
68 65 c.size = int(session.get('changelog_size', default))
69
66 # min size must be 1
67 c.size = max(c.size, 1)
70 68 p = int(request.params.get('page', 1))
71 69 branch_name = request.params.get('branch', None)
72 70 try:
73 71 if branch_name:
74 72 collection = [z for z in
75 73 c.rhodecode_repo.get_changesets(start=0,
76 74 branch_name=branch_name)]
77 75 c.total_cs = len(collection)
78 76 else:
79 77 collection = c.rhodecode_repo
80 78 c.total_cs = len(c.rhodecode_repo)
81 79
82 80 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
83 81 items_per_page=c.size, branch=branch_name)
84 82 collection = list(c.pagination)
85 83 page_revisions = [x.raw_id for x in collection]
86 c.comments = c.rhodecode_db_repo.comments(page_revisions)
87
84 c.comments = c.rhodecode_db_repo.get_comments(page_revisions)
85 c.statuses = c.rhodecode_db_repo.statuses(page_revisions)
88 86 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
89 87 log.error(traceback.format_exc())
90 88 h.flash(str(e), category='warning')
91 89 return redirect(url('home'))
92 90
93 91 self._graph(c.rhodecode_repo, collection, c.total_cs, c.size, p)
94 92
95 93 c.branch_name = branch_name
96 94 c.branch_filters = [('', _('All Branches'))] + \
97 95 [(k, k) for k in c.rhodecode_repo.branches.keys()]
98 96
99 97 return render('changelog/changelog.html')
100 98
101 99 def changelog_details(self, cs):
102 100 if request.environ.get('HTTP_X_PARTIAL_XHR'):
103 101 c.cs = c.rhodecode_repo.get_changeset(cs)
104 102 return render('changelog/changelog_details.html')
105 103
106 104 def _graph(self, repo, collection, repo_size, size, p):
107 105 """
108 106 Generates a DAG graph for mercurial
109 107
110 108 :param repo: repo instance
111 109 :param size: number of commits to show
112 110 :param p: page number
113 111 """
114 112 if not collection:
115 113 c.jsdata = json.dumps([])
116 114 return
117 115
118 116 data = []
119 117 revs = [x.revision for x in collection]
120 118
121 if repo.alias == 'git':
122 for _ in revs:
123 vtx = [0, 1]
124 edges = [[0, 0, 1]]
125 data.append(['', vtx, edges])
126
127 elif repo.alias == 'hg':
128 dag = graphmod.dagwalker(repo._repo, revs)
129 c.dag = graphmod.colored(dag, repo._repo)
130 for (id, type, ctx, vtx, edges) in c.dag:
131 if type != graphmod.CHANGESET:
132 continue
119 dag = _dagwalker(repo, revs, repo.alias)
120 dag = _colored(dag)
121 for (id, type, ctx, vtx, edges) in dag:
133 122 data.append(['', vtx, edges])
134 123
135 124 c.jsdata = json.dumps(data)
@@ -1,397 +1,443 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 29 from webob.exc import HTTPForbidden
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 pylons.decorators import jsonify
35 35
36 36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetError, \
37 37 ChangesetDoesNotExistError
38 38 from rhodecode.lib.vcs.nodes import FileNode
39 39
40 40 import rhodecode.lib.helpers as h
41 41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
42 42 from rhodecode.lib.base import BaseRepoController, render
43 from rhodecode.lib.utils import EmptyChangeset
43 from rhodecode.lib.utils import action_logger
44 44 from rhodecode.lib.compat import OrderedDict
45 45 from rhodecode.lib import diffs
46 from rhodecode.model.db import ChangesetComment
46 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 47 from rhodecode.model.comment import ChangesetCommentsModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 49 from rhodecode.model.meta import Session
49 50 from rhodecode.lib.diffs import wrapped_diff
51 from rhodecode.model.repo import RepoModel
52 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
53 from rhodecode.lib.vcs.backends.base import EmptyChangeset
50 54
51 55 log = logging.getLogger(__name__)
52 56
53 57
54 58 def _update_with_GET(params, GET):
55 59 for k in ['diff1', 'diff2', 'diff']:
56 60 params[k] += GET.getall(k)
57 61
58 62
59 63 def anchor_url(revision, path, GET):
60 64 fid = h.FID(revision, path)
61 65 return h.url.current(anchor=fid, **dict(GET))
62 66
63 67
64 68 def get_ignore_ws(fid, GET):
65 69 ig_ws_global = GET.get('ignorews')
66 70 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
67 71 if ig_ws:
68 72 try:
69 73 return int(ig_ws[0].split(':')[-1])
70 74 except:
71 75 pass
72 76 return ig_ws_global
73 77
74 78
75 79 def _ignorews_url(GET, fileid=None):
76 80 fileid = str(fileid) if fileid else None
77 81 params = defaultdict(list)
78 82 _update_with_GET(params, GET)
79 83 lbl = _('show white space')
80 84 ig_ws = get_ignore_ws(fileid, GET)
81 85 ln_ctx = get_line_ctx(fileid, GET)
82 86 # global option
83 87 if fileid is None:
84 88 if ig_ws is None:
85 89 params['ignorews'] += [1]
86 90 lbl = _('ignore white space')
87 91 ctx_key = 'context'
88 92 ctx_val = ln_ctx
89 93 # per file options
90 94 else:
91 95 if ig_ws is None:
92 96 params[fileid] += ['WS:1']
93 97 lbl = _('ignore white space')
94 98
95 99 ctx_key = fileid
96 100 ctx_val = 'C:%s' % ln_ctx
97 101 # if we have passed in ln_ctx pass it along to our params
98 102 if ln_ctx:
99 103 params[ctx_key] += [ctx_val]
100 104
101 105 params['anchor'] = fileid
102 106 img = h.image(h.url('/images/icons/text_strikethrough.png'), lbl, class_='icon')
103 107 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
104 108
105 109
106 110 def get_line_ctx(fid, GET):
107 111 ln_ctx_global = GET.get('context')
108 112 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
109 113
110 114 if ln_ctx:
111 115 retval = ln_ctx[0].split(':')[-1]
112 116 else:
113 117 retval = ln_ctx_global
114 118
115 119 try:
116 120 return int(retval)
117 121 except:
118 122 return
119 123
120 124
121 125 def _context_url(GET, fileid=None):
122 126 """
123 127 Generates url for context lines
124 128
125 129 :param fileid:
126 130 """
127 131
128 132 fileid = str(fileid) if fileid else None
129 133 ig_ws = get_ignore_ws(fileid, GET)
130 134 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
131 135
132 136 params = defaultdict(list)
133 137 _update_with_GET(params, GET)
134 138
135 139 # global option
136 140 if fileid is None:
137 141 if ln_ctx > 0:
138 142 params['context'] += [ln_ctx]
139 143
140 144 if ig_ws:
141 145 ig_ws_key = 'ignorews'
142 146 ig_ws_val = 1
143 147
144 148 # per file option
145 149 else:
146 150 params[fileid] += ['C:%s' % ln_ctx]
147 151 ig_ws_key = fileid
148 152 ig_ws_val = 'WS:%s' % 1
149 153
150 154 if ig_ws:
151 155 params[ig_ws_key] += [ig_ws_val]
152 156
153 157 lbl = _('%s line context') % ln_ctx
154 158
155 159 params['anchor'] = fileid
156 160 img = h.image(h.url('/images/icons/table_add.png'), lbl, class_='icon')
157 161 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
158 162
159 163
160 164 class ChangesetController(BaseRepoController):
161 165
162 166 @LoginRequired()
163 167 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
164 168 'repository.admin')
165 169 def __before__(self):
166 170 super(ChangesetController, self).__before__()
167 171 c.affected_files_cut_off = 60
172 repo_model = RepoModel()
173 c.users_array = repo_model.get_users_js()
174 c.users_groups_array = repo_model.get_users_groups_js()
168 175
169 176 def index(self, revision):
170 177
171 178 c.anchor_url = anchor_url
172 179 c.ignorews_url = _ignorews_url
173 180 c.context_url = _context_url
174 181 limit_off = request.GET.get('fulldiff')
175 182 #get ranges of revisions if preset
176 183 rev_range = revision.split('...')[:2]
177 184 enable_comments = True
178 185 try:
179 186 if len(rev_range) == 2:
180 187 enable_comments = False
181 188 rev_start = rev_range[0]
182 189 rev_end = rev_range[1]
183 190 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
184 191 end=rev_end)
185 192 else:
186 193 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
187 194
188 195 c.cs_ranges = list(rev_ranges)
189 196 if not c.cs_ranges:
190 197 raise RepositoryError('Changeset range returned empty result')
191 198
192 199 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
193 200 log.error(traceback.format_exc())
194 201 h.flash(str(e), category='warning')
195 202 return redirect(url('home'))
196 203
197 204 c.changes = OrderedDict()
198 205
199 206 c.lines_added = 0 # count of lines added
200 207 c.lines_deleted = 0 # count of lines removes
201 208
202 209 cumulative_diff = 0
203 210 c.cut_off = False # defines if cut off limit is reached
204
211 c.changeset_statuses = ChangesetStatus.STATUSES
205 212 c.comments = []
213 c.statuses = []
206 214 c.inline_comments = []
207 215 c.inline_cnt = 0
208 216 # Iterate over ranges (default changeset view is always one changeset)
209 217 for changeset in c.cs_ranges:
218
219 c.statuses.extend([ChangesetStatusModel()\
220 .get_status(c.rhodecode_db_repo.repo_id,
221 changeset.raw_id)])
222
210 223 c.comments.extend(ChangesetCommentsModel()\
211 224 .get_comments(c.rhodecode_db_repo.repo_id,
212 changeset.raw_id))
225 revision=changeset.raw_id))
213 226 inlines = ChangesetCommentsModel()\
214 227 .get_inline_comments(c.rhodecode_db_repo.repo_id,
215 changeset.raw_id)
228 revision=changeset.raw_id)
216 229 c.inline_comments.extend(inlines)
217 230 c.changes[changeset.raw_id] = []
218 231 try:
219 232 changeset_parent = changeset.parents[0]
220 233 except IndexError:
221 234 changeset_parent = None
222 235
223 236 #==================================================================
224 237 # ADDED FILES
225 238 #==================================================================
226 239 for node in changeset.added:
227 240 fid = h.FID(revision, node.path)
228 241 line_context_lcl = get_line_ctx(fid, request.GET)
229 242 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
230 243 lim = self.cut_off_limit
231 244 if cumulative_diff > self.cut_off_limit:
232 245 lim = -1 if limit_off is None else None
233 246 size, cs1, cs2, diff, st = wrapped_diff(
234 247 filenode_old=None,
235 248 filenode_new=node,
236 249 cut_off_limit=lim,
237 250 ignore_whitespace=ign_whitespace_lcl,
238 251 line_context=line_context_lcl,
239 252 enable_comments=enable_comments
240 253 )
241 254 cumulative_diff += size
242 255 c.lines_added += st[0]
243 256 c.lines_deleted += st[1]
244 257 c.changes[changeset.raw_id].append(
245 258 ('added', node, diff, cs1, cs2, st)
246 259 )
247 260
248 261 #==================================================================
249 262 # CHANGED FILES
250 263 #==================================================================
251 264 for node in changeset.changed:
252 265 try:
253 266 filenode_old = changeset_parent.get_node(node.path)
254 267 except ChangesetError:
255 268 log.warning('Unable to fetch parent node for diff')
256 269 filenode_old = FileNode(node.path, '', EmptyChangeset())
257 270
258 271 fid = h.FID(revision, node.path)
259 272 line_context_lcl = get_line_ctx(fid, request.GET)
260 273 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
261 274 lim = self.cut_off_limit
262 275 if cumulative_diff > self.cut_off_limit:
263 276 lim = -1 if limit_off is None else None
264 277 size, cs1, cs2, diff, st = wrapped_diff(
265 278 filenode_old=filenode_old,
266 279 filenode_new=node,
267 280 cut_off_limit=lim,
268 281 ignore_whitespace=ign_whitespace_lcl,
269 282 line_context=line_context_lcl,
270 283 enable_comments=enable_comments
271 284 )
272 285 cumulative_diff += size
273 286 c.lines_added += st[0]
274 287 c.lines_deleted += st[1]
275 288 c.changes[changeset.raw_id].append(
276 289 ('changed', node, diff, cs1, cs2, st)
277 290 )
278 291 #==================================================================
279 292 # REMOVED FILES
280 293 #==================================================================
281 294 for node in changeset.removed:
282 295 c.changes[changeset.raw_id].append(
283 296 ('removed', node, None, None, None, (0, 0))
284 297 )
285 298
286 299 # count inline comments
287 for path, lines in c.inline_comments:
300 for __, lines in c.inline_comments:
288 301 for comments in lines.values():
289 302 c.inline_cnt += len(comments)
290 303
291 304 if len(c.cs_ranges) == 1:
292 305 c.changeset = c.cs_ranges[0]
293 306 c.changes = c.changes[c.changeset.raw_id]
294 307
295 308 return render('changeset/changeset.html')
296 309 else:
297 310 return render('changeset/changeset_range.html')
298 311
299 312 def raw_changeset(self, revision):
300 313
301 314 method = request.GET.get('diff', 'show')
302 315 ignore_whitespace = request.GET.get('ignorews') == '1'
303 316 line_context = request.GET.get('context', 3)
304 317 try:
305 318 c.scm_type = c.rhodecode_repo.alias
306 319 c.changeset = c.rhodecode_repo.get_changeset(revision)
307 320 except RepositoryError:
308 321 log.error(traceback.format_exc())
309 322 return redirect(url('home'))
310 323 else:
311 324 try:
312 325 c.changeset_parent = c.changeset.parents[0]
313 326 except IndexError:
314 327 c.changeset_parent = None
315 328 c.changes = []
316 329
317 330 for node in c.changeset.added:
318 331 filenode_old = FileNode(node.path, '')
319 332 if filenode_old.is_binary or node.is_binary:
320 333 diff = _('binary file') + '\n'
321 334 else:
322 335 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
323 336 ignore_whitespace=ignore_whitespace,
324 337 context=line_context)
325 338 diff = diffs.DiffProcessor(f_gitdiff,
326 339 format='gitdiff').raw_diff()
327 340
328 341 cs1 = None
329 342 cs2 = node.changeset.raw_id
330 343 c.changes.append(('added', node, diff, cs1, cs2))
331 344
332 345 for node in c.changeset.changed:
333 346 filenode_old = c.changeset_parent.get_node(node.path)
334 347 if filenode_old.is_binary or node.is_binary:
335 348 diff = _('binary file')
336 349 else:
337 350 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
338 351 ignore_whitespace=ignore_whitespace,
339 352 context=line_context)
340 353 diff = diffs.DiffProcessor(f_gitdiff,
341 354 format='gitdiff').raw_diff()
342 355
343 356 cs1 = filenode_old.changeset.raw_id
344 357 cs2 = node.changeset.raw_id
345 358 c.changes.append(('changed', node, diff, cs1, cs2))
346 359
347 360 response.content_type = 'text/plain'
348 361
349 362 if method == 'download':
350 363 response.content_disposition = 'attachment; filename=%s.patch' \
351 364 % revision
352 365
353 366 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
354 367 for x in c.changeset.parents])
355 368
356 369 c.diffs = ''
357 370 for x in c.changes:
358 371 c.diffs += x[2]
359 372
360 373 return render('changeset/raw_changeset.html')
361 374
362 375 @jsonify
363 376 def comment(self, repo_name, revision):
377 status = request.POST.get('changeset_status')
378 change_status = request.POST.get('change_changeset_status')
379
364 380 comm = ChangesetCommentsModel().create(
365 381 text=request.POST.get('text'),
366 repo_id=c.rhodecode_db_repo.repo_id,
367 user_id=c.rhodecode_user.user_id,
382 repo=c.rhodecode_db_repo.repo_id,
383 user=c.rhodecode_user.user_id,
368 384 revision=revision,
369 385 f_path=request.POST.get('f_path'),
370 line_no=request.POST.get('line')
386 line_no=request.POST.get('line'),
387 status_change=(ChangesetStatus.get_status_lbl(status)
388 if status and change_status else None)
371 389 )
372 Session.commit()
390
391 # get status if set !
392 if status and change_status:
393 # if latest status was from pull request and it's closed
394 # disallow changing status !
395 # dont_allow_on_closed_pull_request = True !
396
397 try:
398 ChangesetStatusModel().set_status(
399 c.rhodecode_db_repo.repo_id,
400 status,
401 c.rhodecode_user.user_id,
402 comm,
403 revision=revision,
404 dont_allow_on_closed_pull_request=True
405 )
406 except StatusChangeOnClosedPullRequestError:
407 log.error(traceback.format_exc())
408 msg = _('Changing status on a changeset associated with'
409 'a closed pull request is not allowed')
410 h.flash(msg, category='warning')
411 return redirect(h.url('changeset_home', repo_name=repo_name,
412 revision=revision))
413 action_logger(self.rhodecode_user,
414 'user_commented_revision:%s' % revision,
415 c.rhodecode_db_repo, self.ip_addr, self.sa)
416
417 Session().commit()
418
373 419 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
374 420 return redirect(h.url('changeset_home', repo_name=repo_name,
375 421 revision=revision))
376 422
377 423 data = {
378 424 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
379 425 }
380 426 if comm:
381 427 c.co = comm
382 428 data.update(comm.get_dict())
383 429 data.update({'rendered_text':
384 430 render('changeset/changeset_comment_block.html')})
385 431
386 432 return data
387 433
388 434 @jsonify
389 435 def delete_comment(self, repo_name, comment_id):
390 436 co = ChangesetComment.get(comment_id)
391 437 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
392 438 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
393 439 ChangesetCommentsModel().delete(comment=co)
394 Session.commit()
440 Session().commit()
395 441 return True
396 442 else:
397 443 raise HTTPForbidden()
@@ -1,127 +1,128 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 from rhodecode.lib.utils2 import safe_unicode
31 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
32
33 from rhodecode.lib import helpers as h
32 34 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 35 from rhodecode.lib.base import BaseRepoController
34
35 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
36 from rhodecode.lib.diffs import DiffProcessor
36 37
37 38 log = logging.getLogger(__name__)
38 39
39 40
40 41 class FeedController(BaseRepoController):
41 42
42 43 @LoginRequired(api_access=True)
43 44 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
44 45 'repository.admin')
45 46 def __before__(self):
46 47 super(FeedController, self).__before__()
47 48 #common values for feeds
48 49 self.description = _('Changes on %s repository')
49 50 self.title = self.title = _('%s %s feed') % (c.rhodecode_name, '%s')
50 51 self.language = 'en-us'
51 52 self.ttl = "5"
52 self.feed_nr = 10
53 self.feed_nr = 20
53 54
54 55 def _get_title(self, cs):
55 return "R%s:%s - %s" % (
56 cs.revision, cs.short_id, cs.message
56 return "%s" % (
57 h.shorter(cs.message, 160)
57 58 )
58 59
59 60 def __changes(self, cs):
60 61 changes = []
61 62
62 a = [safe_unicode(n.path) for n in cs.added]
63 if a:
64 changes.append('\nA ' + '\nA '.join(a))
65
66 m = [safe_unicode(n.path) for n in cs.changed]
67 if m:
68 changes.append('\nM ' + '\nM '.join(m))
63 diffprocessor = DiffProcessor(cs.diff())
64 stats = diffprocessor.prepare(inline_diff=False)
65 for st in stats:
66 st.update({'added': st['stats'][0],
67 'removed': st['stats'][1]})
68 changes.append('\n %(operation)s %(filename)s '
69 '(%(added)s lines added, %(removed)s lines removed)'
70 % st)
71 return changes
69 72
70 d = [safe_unicode(n.path) for n in cs.removed]
71 if d:
72 changes.append('\nD ' + '\nD '.join(d))
73
74 changes.append('</pre>')
75
76 return ''.join(changes)
73 def __get_desc(self, cs):
74 desc_msg = []
75 desc_msg.append('%s %s %s:<br/>' % (cs.author, _('commited on'),
76 h.fmt_date(cs.date)))
77 desc_msg.append('<pre>')
78 desc_msg.append(cs.message)
79 desc_msg.append('\n')
80 desc_msg.extend(self.__changes(cs))
81 desc_msg.append('</pre>')
82 return desc_msg
77 83
78 84 def atom(self, repo_name):
79 85 """Produce an atom-1.0 feed via feedgenerator module"""
80 86 feed = Atom1Feed(
81 87 title=self.title % repo_name,
82 88 link=url('summary_home', repo_name=repo_name,
83 89 qualified=True),
84 90 description=self.description % repo_name,
85 91 language=self.language,
86 92 ttl=self.ttl
87 93 )
88 94
89 95 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
90 desc_msg = []
91 desc_msg.append('%s - %s<br/><pre>' % (cs.author, cs.date))
92 desc_msg.append(self.__changes(cs))
93
94 96 feed.add_item(title=self._get_title(cs),
95 97 link=url('changeset_home', repo_name=repo_name,
96 98 revision=cs.raw_id, qualified=True),
97 99 author_name=cs.author,
98 description=''.join(desc_msg))
100 description=''.join(self.__get_desc(cs)),
101 pubdate=cs.date,
102 )
99 103
100 104 response.content_type = feed.mime_type
101 105 return feed.writeString('utf-8')
102 106
103 107 def rss(self, repo_name):
104 108 """Produce an rss2 feed via feedgenerator module"""
105 109 feed = Rss201rev2Feed(
106 110 title=self.title % repo_name,
107 111 link=url('summary_home', repo_name=repo_name,
108 112 qualified=True),
109 113 description=self.description % repo_name,
110 114 language=self.language,
111 115 ttl=self.ttl
112 116 )
113 117
114 118 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
115 desc_msg = []
116 desc_msg.append('%s - %s<br/><pre>' % (cs.author, cs.date))
117 desc_msg.append(self.__changes(cs))
118
119 119 feed.add_item(title=self._get_title(cs),
120 120 link=url('changeset_home', repo_name=repo_name,
121 121 revision=cs.raw_id, qualified=True),
122 122 author_name=cs.author,
123 description=''.join(desc_msg),
123 description=''.join(self.__get_desc(cs)),
124 pubdate=cs.date,
124 125 )
125 126
126 127 response.content_type = feed.mime_type
127 128 return feed.writeString('utf-8')
@@ -1,490 +1,524 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 30
31 31 from pylons import request, response, tmpl_context as c, url
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from pylons.decorators import jsonify
35 from paste.fileapp import FileApp, _FileIter
36 35
37 36 from rhodecode.lib import diffs
38 37 from rhodecode.lib import helpers as h
39 38
40 39 from rhodecode.lib.compat import OrderedDict
41 40 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str
42 41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
43 42 from rhodecode.lib.base import BaseRepoController, render
44 from rhodecode.lib.utils import EmptyChangeset
43 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45 44 from rhodecode.lib.vcs.conf import settings
46 45 from rhodecode.lib.vcs.exceptions import RepositoryError, \
47 46 ChangesetDoesNotExistError, EmptyRepositoryError, \
48 47 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
49 48 from rhodecode.lib.vcs.nodes import FileNode
50 49
51 50 from rhodecode.model.repo import RepoModel
52 51 from rhodecode.model.scm import ScmModel
53 52 from rhodecode.model.db import Repository
54 53
55 54 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
56 55 _context_url, get_line_ctx, get_ignore_ws
57 56
58 57
59 58 log = logging.getLogger(__name__)
60 59
61 60
62 61 class FilesController(BaseRepoController):
63 62
64 @LoginRequired()
65 63 def __before__(self):
66 64 super(FilesController, self).__before__()
67 65 c.cut_off_limit = self.cut_off_limit
68 66
69 67 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
70 68 """
71 69 Safe way to get changeset if error occur it redirects to tip with
72 70 proper message
73 71
74 72 :param rev: revision to fetch
75 73 :param repo_name: repo name to redirect after
76 74 """
77 75
78 76 try:
79 77 return c.rhodecode_repo.get_changeset(rev)
80 78 except EmptyRepositoryError, e:
81 79 if not redirect_after:
82 80 return None
83 81 url_ = url('files_add_home',
84 82 repo_name=c.repo_name,
85 83 revision=0, f_path='')
86 add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
87 h.flash(h.literal(_('There are no files yet %s' % add_new)),
84 add_new = '<a href="%s">[%s]</a>' % (url_, _('click here to add new file'))
85 h.flash(h.literal(_('There are no files yet %s') % add_new),
88 86 category='warning')
89 87 redirect(h.url('summary_home', repo_name=repo_name))
90 88
91 89 except RepositoryError, e:
92 90 h.flash(str(e), category='warning')
93 91 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
94 92
95 93 def __get_filenode_or_redirect(self, repo_name, cs, path):
96 94 """
97 95 Returns file_node, if error occurs or given path is directory,
98 96 it'll redirect to top level path
99 97
100 98 :param repo_name: repo_name
101 99 :param cs: given changeset
102 100 :param path: path to lookup
103 101 """
104 102
105 103 try:
106 104 file_node = cs.get_node(path)
107 105 if file_node.is_dir():
108 106 raise RepositoryError('given path is a directory')
109 107 except RepositoryError, e:
110 108 h.flash(str(e), category='warning')
111 109 redirect(h.url('files_home', repo_name=repo_name,
112 110 revision=cs.raw_id))
113 111
114 112 return file_node
115 113
114 @LoginRequired()
116 115 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
117 116 'repository.admin')
118 117 def index(self, repo_name, revision, f_path, annotate=False):
119 118 # redirect to given revision from form if given
120 119 post_revision = request.POST.get('at_rev', None)
121 120 if post_revision:
122 121 cs = self.__get_cs_or_redirect(post_revision, repo_name)
123 122 redirect(url('files_home', repo_name=c.repo_name,
124 123 revision=cs.raw_id, f_path=f_path))
125 124
126 125 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
127 126 c.branch = request.GET.get('branch', None)
128 127 c.f_path = f_path
129 128 c.annotate = annotate
130 129 cur_rev = c.changeset.revision
131 130
132 131 # prev link
133 132 try:
134 133 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
135 134 c.url_prev = url('files_home', repo_name=c.repo_name,
136 135 revision=prev_rev.raw_id, f_path=f_path)
137 136 if c.branch:
138 137 c.url_prev += '?branch=%s' % c.branch
139 138 except (ChangesetDoesNotExistError, VCSError):
140 139 c.url_prev = '#'
141 140
142 141 # next link
143 142 try:
144 143 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
145 144 c.url_next = url('files_home', repo_name=c.repo_name,
146 145 revision=next_rev.raw_id, f_path=f_path)
147 146 if c.branch:
148 147 c.url_next += '?branch=%s' % c.branch
149 148 except (ChangesetDoesNotExistError, VCSError):
150 149 c.url_next = '#'
151 150
152 151 # files or dirs
153 152 try:
154 153 c.file = c.changeset.get_node(f_path)
155 154
156 155 if c.file.is_file():
157 c.file_history = self._get_node_history(c.changeset, f_path)
156 _hist = c.changeset.get_file_history(f_path)
157 c.file_history = self._get_node_history(c.changeset, f_path,
158 _hist)
159 c.authors = []
160 for a in set([x.author for x in _hist]):
161 c.authors.append((h.email(a), h.person(a)))
158 162 else:
159 c.file_history = []
163 c.authors = c.file_history = []
160 164 except RepositoryError, e:
161 165 h.flash(str(e), category='warning')
162 166 redirect(h.url('files_home', repo_name=repo_name,
163 revision=revision))
167 revision='tip'))
168
169 if request.environ.get('HTTP_X_PARTIAL_XHR'):
170 return render('files/files_ypjax.html')
164 171
165 172 return render('files/files.html')
166 173
174 @LoginRequired()
167 175 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
168 176 'repository.admin')
169 177 def rawfile(self, repo_name, revision, f_path):
170 178 cs = self.__get_cs_or_redirect(revision, repo_name)
171 179 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
172 180
173 181 response.content_disposition = 'attachment; filename=%s' % \
174 182 safe_str(f_path.split(Repository.url_sep())[-1])
175 183
176 184 response.content_type = file_node.mimetype
177 185 return file_node.content
178 186
187 @LoginRequired()
179 188 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
180 189 'repository.admin')
181 190 def raw(self, repo_name, revision, f_path):
182 191 cs = self.__get_cs_or_redirect(revision, repo_name)
183 192 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
184 193
185 194 raw_mimetype_mapping = {
186 195 # map original mimetype to a mimetype used for "show as raw"
187 196 # you can also provide a content-disposition to override the
188 197 # default "attachment" disposition.
189 198 # orig_type: (new_type, new_dispo)
190 199
191 200 # show images inline:
192 201 'image/x-icon': ('image/x-icon', 'inline'),
193 202 'image/png': ('image/png', 'inline'),
194 203 'image/gif': ('image/gif', 'inline'),
195 204 'image/jpeg': ('image/jpeg', 'inline'),
196 205 'image/svg+xml': ('image/svg+xml', 'inline'),
197 206 }
198 207
199 208 mimetype = file_node.mimetype
200 209 try:
201 210 mimetype, dispo = raw_mimetype_mapping[mimetype]
202 211 except KeyError:
203 212 # we don't know anything special about this, handle it safely
204 213 if file_node.is_binary:
205 214 # do same as download raw for binary files
206 215 mimetype, dispo = 'application/octet-stream', 'attachment'
207 216 else:
208 217 # do not just use the original mimetype, but force text/plain,
209 218 # otherwise it would serve text/html and that might be unsafe.
210 219 # Note: underlying vcs library fakes text/plain mimetype if the
211 220 # mimetype can not be determined and it thinks it is not
212 221 # binary.This might lead to erroneous text display in some
213 222 # cases, but helps in other cases, like with text files
214 223 # without extension.
215 224 mimetype, dispo = 'text/plain', 'inline'
216 225
217 226 if dispo == 'attachment':
218 227 dispo = 'attachment; filename=%s' % \
219 228 safe_str(f_path.split(os.sep)[-1])
220 229
221 230 response.content_disposition = dispo
222 231 response.content_type = mimetype
223 232 return file_node.content
224 233
234 @LoginRequired()
225 235 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
226 236 def edit(self, repo_name, revision, f_path):
237 repo = Repository.get_by_repo_name(repo_name)
238 if repo.enable_locking and repo.locked[0]:
239 h.flash(_('This repository is has been locked by %s on %s')
240 % (h.person_by_id(repo.locked[0]),
241 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
242 'warning')
243 return redirect(h.url('files_home',
244 repo_name=repo_name, revision='tip'))
245
227 246 r_post = request.POST
228 247
229 248 c.cs = self.__get_cs_or_redirect(revision, repo_name)
230 249 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
231 250
232 251 if c.file.is_binary:
233 252 return redirect(url('files_home', repo_name=c.repo_name,
234 253 revision=c.cs.raw_id, f_path=f_path))
235 254
236 255 c.f_path = f_path
237 256
238 257 if r_post:
239 258
240 259 old_content = c.file.content
241 260 sl = old_content.splitlines(1)
242 261 first_line = sl[0] if sl else ''
243 262 # modes: 0 - Unix, 1 - Mac, 2 - DOS
244 263 mode = detect_mode(first_line, 0)
245 264 content = convert_line_endings(r_post.get('content'), mode)
246 265
247 266 message = r_post.get('message') or (_('Edited %s via RhodeCode')
248 267 % (f_path))
249 268 author = self.rhodecode_user.full_contact
250 269
251 270 if content == old_content:
252 271 h.flash(_('No changes'),
253 272 category='warning')
254 273 return redirect(url('changeset_home', repo_name=c.repo_name,
255 274 revision='tip'))
256 275
257 276 try:
258 277 self.scm_model.commit_change(repo=c.rhodecode_repo,
259 278 repo_name=repo_name, cs=c.cs,
260 279 user=self.rhodecode_user,
261 280 author=author, message=message,
262 281 content=content, f_path=f_path)
263 h.flash(_('Successfully committed to %s' % f_path),
282 h.flash(_('Successfully committed to %s') % f_path,
264 283 category='success')
265 284
266 285 except Exception:
267 286 log.error(traceback.format_exc())
268 287 h.flash(_('Error occurred during commit'), category='error')
269 288 return redirect(url('changeset_home',
270 289 repo_name=c.repo_name, revision='tip'))
271 290
272 291 return render('files/files_edit.html')
273 292
293 @LoginRequired()
274 294 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
275 295 def add(self, repo_name, revision, f_path):
296
297 repo = Repository.get_by_repo_name(repo_name)
298 if repo.enable_locking and repo.locked[0]:
299 h.flash(_('This repository is has been locked by %s on %s')
300 % (h.person_by_id(repo.locked[0]),
301 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
302 'warning')
303 return redirect(h.url('files_home',
304 repo_name=repo_name, revision='tip'))
305
276 306 r_post = request.POST
277 307 c.cs = self.__get_cs_or_redirect(revision, repo_name,
278 308 redirect_after=False)
279 309 if c.cs is None:
280 310 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
281 311
282 312 c.f_path = f_path
283 313
284 314 if r_post:
285 315 unix_mode = 0
286 316 content = convert_line_endings(r_post.get('content'), unix_mode)
287 317
288 318 message = r_post.get('message') or (_('Added %s via RhodeCode')
289 319 % (f_path))
290 320 location = r_post.get('location')
291 321 filename = r_post.get('filename')
292 322 file_obj = r_post.get('upload_file', None)
293 323
294 324 if file_obj is not None and hasattr(file_obj, 'filename'):
295 325 filename = file_obj.filename
296 326 content = file_obj.file
297 327
298 328 node_path = os.path.join(location, filename)
299 329 author = self.rhodecode_user.full_contact
300 330
301 331 if not content:
302 332 h.flash(_('No content'), category='warning')
303 333 return redirect(url('changeset_home', repo_name=c.repo_name,
304 334 revision='tip'))
305 335 if not filename:
306 336 h.flash(_('No filename'), category='warning')
307 337 return redirect(url('changeset_home', repo_name=c.repo_name,
308 338 revision='tip'))
309 339
310 340 try:
311 341 self.scm_model.create_node(repo=c.rhodecode_repo,
312 342 repo_name=repo_name, cs=c.cs,
313 343 user=self.rhodecode_user,
314 344 author=author, message=message,
315 345 content=content, f_path=node_path)
316 h.flash(_('Successfully committed to %s' % node_path),
346 h.flash(_('Successfully committed to %s') % node_path,
317 347 category='success')
318 348 except NodeAlreadyExistsError, e:
319 349 h.flash(_(e), category='error')
320 350 except Exception:
321 351 log.error(traceback.format_exc())
322 352 h.flash(_('Error occurred during commit'), category='error')
323 353 return redirect(url('changeset_home',
324 354 repo_name=c.repo_name, revision='tip'))
325 355
326 356 return render('files/files_add.html')
327 357
358 @LoginRequired()
328 359 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
329 360 'repository.admin')
330 361 def archivefile(self, repo_name, fname):
331 362
332 363 fileformat = None
333 364 revision = None
334 365 ext = None
335 366 subrepos = request.GET.get('subrepos') == 'true'
336 367
337 368 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
338 369 archive_spec = fname.split(ext_data[1])
339 370 if len(archive_spec) == 2 and archive_spec[1] == '':
340 371 fileformat = a_type or ext_data[1]
341 372 revision = archive_spec[0]
342 373 ext = ext_data[1]
343 374
344 375 try:
345 376 dbrepo = RepoModel().get_by_repo_name(repo_name)
346 377 if dbrepo.enable_downloads is False:
347 378 return _('downloads disabled')
348 379
349 380 if c.rhodecode_repo.alias == 'hg':
350 381 # patch and reset hooks section of UI config to not run any
351 382 # hooks on fetching archives with subrepos
352 383 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
353 384 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
354 385
355 386 cs = c.rhodecode_repo.get_changeset(revision)
356 387 content_type = settings.ARCHIVE_SPECS[fileformat][0]
357 388 except ChangesetDoesNotExistError:
358 389 return _('Unknown revision %s') % revision
359 390 except EmptyRepositoryError:
360 391 return _('Empty repository')
361 392 except (ImproperArchiveTypeError, KeyError):
362 393 return _('Unknown archive type')
363 394
364 fd, _archive_name = tempfile.mkstemp(suffix='rcarchive')
365 with open(_archive_name, 'wb') as f:
366 cs.fill_archive(stream=f, kind=fileformat, subrepos=subrepos)
367
368 content_disposition = 'attachment; filename=%s-%s%s' \
369 % (repo_name, revision[:12], ext)
370 content_length = os.path.getsize(_archive_name)
395 fd, archive = tempfile.mkstemp()
396 t = open(archive, 'wb')
397 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
398 t.close()
371 399
372 headers = [('Content-Disposition', str(content_disposition)),
373 ('Content-Type', str(content_type)),
374 ('Content-Length', str(content_length))]
400 def get_chunked_archive(archive):
401 stream = open(archive, 'rb')
402 while True:
403 data = stream.read(16 * 1024)
404 if not data:
405 stream.close()
406 os.close(fd)
407 os.remove(archive)
408 break
409 yield data
375 410
376 class _DestroyingFileWrapper(_FileIter):
377 def close(self):
378 self.file.close
379 os.remove(self.file.name)
411 response.content_disposition = str('attachment; filename=%s-%s%s' \
412 % (repo_name, revision[:12], ext))
413 response.content_type = str(content_type)
414 return get_chunked_archive(archive)
380 415
381 request.environ['wsgi.file_wrapper'] = _DestroyingFileWrapper
382 fapp = FileApp(_archive_name, headers=headers)
383 return fapp(request.environ, self.start_response)
384
416 @LoginRequired()
385 417 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
386 418 'repository.admin')
387 419 def diff(self, repo_name, f_path):
388 420 ignore_whitespace = request.GET.get('ignorews') == '1'
389 421 line_context = request.GET.get('context', 3)
390 422 diff1 = request.GET.get('diff1', '')
391 423 diff2 = request.GET.get('diff2', '')
392 424 c.action = request.GET.get('diff')
393 425 c.no_changes = diff1 == diff2
394 426 c.f_path = f_path
395 427 c.big_diff = False
396 428 c.anchor_url = anchor_url
397 429 c.ignorews_url = _ignorews_url
398 430 c.context_url = _context_url
399 431 c.changes = OrderedDict()
400 432 c.changes[diff2] = []
401 433 try:
402 434 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
403 435 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
404 436 node1 = c.changeset_1.get_node(f_path)
405 437 else:
406 438 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
407 439 node1 = FileNode('.', '', changeset=c.changeset_1)
408 440
409 441 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
410 442 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
411 443 node2 = c.changeset_2.get_node(f_path)
412 444 else:
413 445 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
414 446 node2 = FileNode('.', '', changeset=c.changeset_2)
415 447 except RepositoryError:
416 448 return redirect(url('files_home', repo_name=c.repo_name,
417 449 f_path=f_path))
418 450
419 451 if c.action == 'download':
420 452 _diff = diffs.get_gitdiff(node1, node2,
421 453 ignore_whitespace=ignore_whitespace,
422 454 context=line_context)
423 455 diff = diffs.DiffProcessor(_diff, format='gitdiff')
424 456
425 457 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
426 458 response.content_type = 'text/plain'
427 459 response.content_disposition = (
428 460 'attachment; filename=%s' % diff_name
429 461 )
430 462 return diff.raw_diff()
431 463
432 464 elif c.action == 'raw':
433 465 _diff = diffs.get_gitdiff(node1, node2,
434 466 ignore_whitespace=ignore_whitespace,
435 467 context=line_context)
436 468 diff = diffs.DiffProcessor(_diff, format='gitdiff')
437 469 response.content_type = 'text/plain'
438 470 return diff.raw_diff()
439 471
440 472 else:
441 473 fid = h.FID(diff2, node2.path)
442 474 line_context_lcl = get_line_ctx(fid, request.GET)
443 475 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
444 476
445 477 lim = request.GET.get('fulldiff') or self.cut_off_limit
446 478 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
447 479 filenode_new=node2,
448 480 cut_off_limit=lim,
449 481 ignore_whitespace=ign_whitespace_lcl,
450 482 line_context=line_context_lcl,
451 483 enable_comments=False)
452 484
453 485 c.changes = [('', node2, diff, cs1, cs2, st,)]
454 486
455 487 return render('files/file_diff.html')
456 488
457 def _get_node_history(self, cs, f_path):
489 def _get_node_history(self, cs, f_path, changesets=None):
490 if changesets is None:
458 491 changesets = cs.get_file_history(f_path)
459 492 hist_l = []
460 493
461 494 changesets_group = ([], _("Changesets"))
462 495 branches_group = ([], _("Branches"))
463 496 tags_group = ([], _("Tags"))
464 497 _hg = cs.repository.alias == 'hg'
465 498 for chs in changesets:
466 499 _branch = '(%s)' % chs.branch if _hg else ''
467 500 n_desc = 'r%s:%s %s' % (chs.revision, chs.short_id, _branch)
468 501 changesets_group[0].append((chs.raw_id, n_desc,))
469 502
470 503 hist_l.append(changesets_group)
471 504
472 505 for name, chs in c.rhodecode_repo.branches.items():
473 506 branches_group[0].append((chs, name),)
474 507 hist_l.append(branches_group)
475 508
476 509 for name, chs in c.rhodecode_repo.tags.items():
477 510 tags_group[0].append((chs, name),)
478 511 hist_l.append(tags_group)
479 512
480 513 return hist_l
481 514
482 @jsonify
515 @LoginRequired()
483 516 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
484 517 'repository.admin')
518 @jsonify
485 519 def nodelist(self, repo_name, revision, f_path):
486 520 if request.environ.get('HTTP_X_PARTIAL_XHR'):
487 521 cs = self.__get_cs_or_redirect(revision, repo_name)
488 522 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
489 523 flat=False)
490 return _d + _f
524 return {'nodes': _d + _f}
@@ -1,180 +1,184 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 NotAnonymous, HasRepoPermissionAny
38 NotAnonymous, HasRepoPermissionAny, HasPermissionAllDecorator,\
39 HasPermissionAnyDecorator
39 40 from rhodecode.lib.base import BaseRepoController, render
40 41 from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User
41 42 from rhodecode.model.repo import RepoModel
42 43 from rhodecode.model.forms import RepoForkForm
44 from rhodecode.model.scm import ScmModel
43 45
44 46 log = logging.getLogger(__name__)
45 47
46 48
47 49 class ForksController(BaseRepoController):
48 50
49 51 @LoginRequired()
50 52 def __before__(self):
51 53 super(ForksController, self).__before__()
52 54
53 55 def __load_defaults(self):
54 56 c.repo_groups = RepoGroup.groups_choices()
55 57 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
58 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
59 c.landing_revs_choices = choices
56 60
57 61 def __load_data(self, repo_name=None):
58 62 """
59 63 Load defaults settings for edit, and update
60 64
61 65 :param repo_name:
62 66 """
63 67 self.__load_defaults()
64 68
65 69 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
66 70 repo = db_repo.scm_instance
67 71
68 72 if c.repo_info is None:
69 73 h.flash(_('%s repository is not mapped to db perhaps'
70 74 ' it was created or renamed from the filesystem'
71 75 ' please run the application again'
72 76 ' in order to rescan repositories') % repo_name,
73 77 category='error')
74 78
75 79 return redirect(url('repos'))
76 80
77 81 c.default_user_id = User.get_by_username('default').user_id
78 82 c.in_public_journal = UserFollowing.query()\
79 83 .filter(UserFollowing.user_id == c.default_user_id)\
80 84 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
81 85
82 86 if c.repo_info.stats:
83 87 last_rev = c.repo_info.stats.stat_on_revision+1
84 88 else:
85 89 last_rev = 0
86 90 c.stats_revision = last_rev
87 91
88 92 c.repo_last_rev = repo.count() if repo.revisions else 0
89 93
90 94 if last_rev == 0 or c.repo_last_rev == 0:
91 95 c.stats_percentage = 0
92 96 else:
93 97 c.stats_percentage = '%.2f' % ((float((last_rev)) /
94 98 c.repo_last_rev) * 100)
95 99
96 100 defaults = RepoModel()._get_defaults(repo_name)
97 101 # add prefix to fork
98 102 defaults['repo_name'] = 'fork-' + defaults['repo_name']
99 103 return defaults
100 104
101 105 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
102 106 'repository.admin')
103 107 def forks(self, repo_name):
104 108 p = int(request.params.get('page', 1))
105 109 repo_id = c.rhodecode_db_repo.repo_id
106 110 d = []
107 111 for r in Repository.get_repo_forks(repo_id):
108 112 if not HasRepoPermissionAny(
109 113 'repository.read', 'repository.write', 'repository.admin'
110 114 )(r.repo_name, 'get forks check'):
111 115 continue
112 116 d.append(r)
113 117 c.forks_pager = Page(d, page=p, items_per_page=20)
114 118
115 119 c.forks_data = render('/forks/forks_data.html')
116 120
117 121 if request.environ.get('HTTP_X_PARTIAL_XHR'):
118 122 return c.forks_data
119 123
120 124 return render('/forks/forks.html')
121 125
122 126 @NotAnonymous()
127 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
123 128 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
124 129 'repository.admin')
125 130 def fork(self, repo_name):
126 131 c.repo_info = Repository.get_by_repo_name(repo_name)
127 132 if not c.repo_info:
128 133 h.flash(_('%s repository is not mapped to db perhaps'
129 134 ' it was created or renamed from the file system'
130 135 ' please run the application again'
131 136 ' in order to rescan repositories') % repo_name,
132 137 category='error')
133 138
134 139 return redirect(url('home'))
135 140
136 141 defaults = self.__load_data(repo_name)
137 142
138 143 return htmlfill.render(
139 144 render('forks/fork.html'),
140 145 defaults=defaults,
141 146 encoding="UTF-8",
142 147 force_defaults=False
143 148 )
144 149
145
146 150 @NotAnonymous()
151 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
147 152 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
148 153 'repository.admin')
149 154 def fork_create(self, repo_name):
150 155 self.__load_defaults()
151 156 c.repo_info = Repository.get_by_repo_name(repo_name)
152 157 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
153 repo_groups=c.repo_groups_choices,)()
158 repo_groups=c.repo_groups_choices,
159 landing_revs=c.landing_revs_choices)()
154 160 form_result = {}
155 161 try:
156 162 form_result = _form.to_python(dict(request.POST))
157 # add org_path of repo so we can do a clone from it later
158 form_result['org_path'] = c.repo_info.repo_name
159 163
160 164 # create fork is done sometimes async on celery, db transaction
161 165 # management is handled there.
162 RepoModel().create_fork(form_result, self.rhodecode_user)
166 RepoModel().create_fork(form_result, self.rhodecode_user.user_id)
163 167 h.flash(_('forked %s repository as %s') \
164 168 % (repo_name, form_result['repo_name']),
165 169 category='success')
166 170 except formencode.Invalid, errors:
167 171 c.new_repo = errors.value['repo_name']
168 172
169 173 return htmlfill.render(
170 174 render('forks/fork.html'),
171 175 defaults=errors.value,
172 176 errors=errors.error_dict or {},
173 177 prefix_error=False,
174 178 encoding="UTF-8")
175 179 except Exception:
176 180 log.error(traceback.format_exc())
177 181 h.flash(_('An error occurred during repository forking %s') %
178 182 repo_name, category='error')
179 183
180 184 return redirect(url('home'))
@@ -1,65 +1,66 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 from paste.httpexceptions import HTTPBadRequest
29 from webob.exc import HTTPBadRequest
30 30
31 31 from rhodecode.lib.auth import LoginRequired
32 32 from rhodecode.lib.base import BaseController, render
33 33 from rhodecode.model.db import Repository
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class HomeController(BaseController):
39 39
40 40 @LoginRequired()
41 41 def __before__(self):
42 42 super(HomeController, self).__before__()
43 43
44 44 def index(self):
45 45 c.repos_list = self.scm_model.get_repos()
46 46 c.groups = self.scm_model.get_repos_groups()
47 47 c.group = None
48 48 return render('/index.html')
49 49
50 50 def repo_switcher(self):
51 51 if request.is_xhr:
52 52 all_repos = Repository.query().order_by(Repository.repo_name).all()
53 53 c.repos_list = self.scm_model.get_repos(all_repos,
54 sort_key='name_sort')
54 sort_key='name_sort',
55 simple=True)
55 56 return render('/repo_switcher_list.html')
56 57 else:
57 58 return HTTPBadRequest()
58 59
59 60 def branch_tag_switcher(self, repo_name):
60 61 if request.is_xhr:
61 62 c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
62 63 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
63 64 return render('/switch_to_list.html')
64 65 else:
65 66 return HTTPBadRequest()
@@ -1,242 +1,294 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.journal
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Journal controller for pylons
7 7
8 8 :created_on: Nov 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 import logging
26 26 from itertools import groupby
27 27
28 28 from sqlalchemy import or_
29 29 from sqlalchemy.orm import joinedload
30 30 from webhelpers.paginate import Page
31 31 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
32 32
33 from paste.httpexceptions import HTTPBadRequest
33 from webob.exc import HTTPBadRequest
34 34 from pylons import request, tmpl_context as c, response, url
35 35 from pylons.i18n.translation import _
36 36
37 37 import rhodecode.lib.helpers as h
38 38 from rhodecode.lib.auth import LoginRequired, NotAnonymous
39 39 from rhodecode.lib.base import BaseController, render
40 40 from rhodecode.model.db import UserLog, UserFollowing, Repository, User
41 41 from rhodecode.model.meta import Session
42 42 from sqlalchemy.sql.expression import func
43 43 from rhodecode.model.scm import ScmModel
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class JournalController(BaseController):
49 49
50 50 def __before__(self):
51 51 super(JournalController, self).__before__()
52 self.rhodecode_user = self.rhodecode_user
53 self.title = _('%s public journal %s feed') % (c.rhodecode_name, '%s')
54 52 self.language = 'en-us'
55 53 self.ttl = "5"
56 54 self.feed_nr = 20
57 55
58 56 @LoginRequired()
59 57 @NotAnonymous()
60 58 def index(self):
61 59 # Return a rendered template
62 60 p = int(request.params.get('page', 1))
63 61
64 62 c.user = User.get(self.rhodecode_user.user_id)
65 63 all_repos = self.sa.query(Repository)\
66 64 .filter(Repository.user_id == c.user.user_id)\
67 65 .order_by(func.lower(Repository.repo_name)).all()
68 66
69 67 c.user_repos = ScmModel().get_repos(all_repos)
70 68
71 69 c.following = self.sa.query(UserFollowing)\
72 70 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
73 71 .options(joinedload(UserFollowing.follows_repository))\
74 72 .all()
75 73
76 74 journal = self._get_journal_data(c.following)
77 75
78 76 c.journal_pager = Page(journal, page=p, items_per_page=20)
79 77
80 78 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
81 79
82 80 c.journal_data = render('journal/journal_data.html')
83 81 if request.environ.get('HTTP_X_PARTIAL_XHR'):
84 82 return c.journal_data
85 83 return render('journal/journal.html')
86 84
85 @LoginRequired(api_access=True)
86 @NotAnonymous()
87 def journal_atom(self):
88 """
89 Produce an atom-1.0 feed via feedgenerator module
90 """
91 following = self.sa.query(UserFollowing)\
92 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
93 .options(joinedload(UserFollowing.follows_repository))\
94 .all()
95 return self._atom_feed(following, public=False)
96
97 @LoginRequired(api_access=True)
98 @NotAnonymous()
99 def journal_rss(self):
100 """
101 Produce an rss feed via feedgenerator module
102 """
103 following = self.sa.query(UserFollowing)\
104 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
105 .options(joinedload(UserFollowing.follows_repository))\
106 .all()
107 return self._rss_feed(following, public=False)
108
87 109 def _get_daily_aggregate(self, journal):
88 110 groups = []
89 111 for k, g in groupby(journal, lambda x: x.action_as_day):
90 112 user_group = []
91 113 for k2, g2 in groupby(list(g), lambda x: x.user.email):
92 114 l = list(g2)
93 115 user_group.append((l[0].user, l))
94 116
95 117 groups.append((k, user_group,))
96 118
97 119 return groups
98 120
99 121 def _get_journal_data(self, following_repos):
100 122 repo_ids = [x.follows_repository.repo_id for x in following_repos
101 123 if x.follows_repository is not None]
102 124 user_ids = [x.follows_user.user_id for x in following_repos
103 125 if x.follows_user is not None]
104 126
105 127 filtering_criterion = None
106 128
107 129 if repo_ids and user_ids:
108 130 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
109 131 UserLog.user_id.in_(user_ids))
110 132 if repo_ids and not user_ids:
111 133 filtering_criterion = UserLog.repository_id.in_(repo_ids)
112 134 if not repo_ids and user_ids:
113 135 filtering_criterion = UserLog.user_id.in_(user_ids)
114 136 if filtering_criterion is not None:
115 137 journal = self.sa.query(UserLog)\
116 138 .options(joinedload(UserLog.user))\
117 139 .options(joinedload(UserLog.repository))\
118 140 .filter(filtering_criterion)\
119 141 .order_by(UserLog.action_date.desc())
120 142 else:
121 143 journal = []
122 144
123 145 return journal
124 146
125 147 @LoginRequired()
126 148 @NotAnonymous()
127 149 def toggle_following(self):
128 150 cur_token = request.POST.get('auth_token')
129 151 token = h.get_token()
130 152 if cur_token == token:
131 153
132 154 user_id = request.POST.get('follows_user_id')
133 155 if user_id:
134 156 try:
135 157 self.scm_model.toggle_following_user(user_id,
136 158 self.rhodecode_user.user_id)
137 159 Session.commit()
138 160 return 'ok'
139 161 except:
140 162 raise HTTPBadRequest()
141 163
142 164 repo_id = request.POST.get('follows_repo_id')
143 165 if repo_id:
144 166 try:
145 167 self.scm_model.toggle_following_repo(repo_id,
146 168 self.rhodecode_user.user_id)
147 169 Session.commit()
148 170 return 'ok'
149 171 except:
150 172 raise HTTPBadRequest()
151 173
152 174 log.debug('token mismatch %s vs %s' % (cur_token, token))
153 175 raise HTTPBadRequest()
154 176
155 177 @LoginRequired()
156 178 def public_journal(self):
157 179 # Return a rendered template
158 180 p = int(request.params.get('page', 1))
159 181
160 182 c.following = self.sa.query(UserFollowing)\
161 183 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
162 184 .options(joinedload(UserFollowing.follows_repository))\
163 185 .all()
164 186
165 187 journal = self._get_journal_data(c.following)
166 188
167 189 c.journal_pager = Page(journal, page=p, items_per_page=20)
168 190
169 191 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
170 192
171 193 c.journal_data = render('journal/journal_data.html')
172 194 if request.environ.get('HTTP_X_PARTIAL_XHR'):
173 195 return c.journal_data
174 196 return render('journal/public_journal.html')
175 197
198 def _atom_feed(self, repos, public=True):
199 journal = self._get_journal_data(repos)
200 if public:
201 _link = url('public_journal_atom', qualified=True)
202 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
203 'atom feed')
204 else:
205 _link = url('journal_atom', qualified=True)
206 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'atom feed')
207
208 feed = Atom1Feed(title=_desc,
209 link=_link,
210 description=_desc,
211 language=self.language,
212 ttl=self.ttl)
213
214 for entry in journal[:self.feed_nr]:
215 action, action_extra, ico = h.action_parser(entry, feed=True)
216 title = "%s - %s %s" % (entry.user.short_contact, action(),
217 entry.repository.repo_name)
218 desc = action_extra()
219 _url = None
220 if entry.repository is not None:
221 _url = url('changelog_home',
222 repo_name=entry.repository.repo_name,
223 qualified=True)
224
225 feed.add_item(title=title,
226 pubdate=entry.action_date,
227 link=_url or url('', qualified=True),
228 author_email=entry.user.email,
229 author_name=entry.user.full_contact,
230 description=desc)
231
232 response.content_type = feed.mime_type
233 return feed.writeString('utf-8')
234
235 def _rss_feed(self, repos, public=True):
236 journal = self._get_journal_data(repos)
237 if public:
238 _link = url('public_journal_atom', qualified=True)
239 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
240 'rss feed')
241 else:
242 _link = url('journal_atom', qualified=True)
243 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'rss feed')
244
245 feed = Rss201rev2Feed(title=_desc,
246 link=_link,
247 description=_desc,
248 language=self.language,
249 ttl=self.ttl)
250
251 for entry in journal[:self.feed_nr]:
252 action, action_extra, ico = h.action_parser(entry, feed=True)
253 title = "%s - %s %s" % (entry.user.short_contact, action(),
254 entry.repository.repo_name)
255 desc = action_extra()
256 _url = None
257 if entry.repository is not None:
258 _url = url('changelog_home',
259 repo_name=entry.repository.repo_name,
260 qualified=True)
261
262 feed.add_item(title=title,
263 pubdate=entry.action_date,
264 link=_url or url('', qualified=True),
265 author_email=entry.user.email,
266 author_name=entry.user.full_contact,
267 description=desc)
268
269 response.content_type = feed.mime_type
270 return feed.writeString('utf-8')
271
176 272 @LoginRequired(api_access=True)
177 273 def public_journal_atom(self):
178 274 """
179 275 Produce an atom-1.0 feed via feedgenerator module
180 276 """
181 277 c.following = self.sa.query(UserFollowing)\
182 278 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
183 279 .options(joinedload(UserFollowing.follows_repository))\
184 280 .all()
185 281
186 journal = self._get_journal_data(c.following)
187
188 feed = Atom1Feed(title=self.title % 'atom',
189 link=url('public_journal_atom', qualified=True),
190 description=_('Public journal'),
191 language=self.language,
192 ttl=self.ttl)
193
194 for entry in journal[:self.feed_nr]:
195 #tmpl = h.action_parser(entry)[0]
196 action, action_extra = h.action_parser(entry, feed=True)
197 title = "%s - %s %s" % (entry.user.short_contact, action,
198 entry.repository.repo_name)
199 desc = action_extra()
200 feed.add_item(title=title,
201 pubdate=entry.action_date,
202 link=url('', qualified=True),
203 author_email=entry.user.email,
204 author_name=entry.user.full_contact,
205 description=desc)
206
207 response.content_type = feed.mime_type
208 return feed.writeString('utf-8')
282 return self._atom_feed(c.following)
209 283
210 284 @LoginRequired(api_access=True)
211 285 def public_journal_rss(self):
212 286 """
213 287 Produce an rss2 feed via feedgenerator module
214 288 """
215 289 c.following = self.sa.query(UserFollowing)\
216 290 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
217 291 .options(joinedload(UserFollowing.follows_repository))\
218 292 .all()
219 293
220 journal = self._get_journal_data(c.following)
221
222 feed = Rss201rev2Feed(title=self.title % 'rss',
223 link=url('public_journal_rss', qualified=True),
224 description=_('Public journal'),
225 language=self.language,
226 ttl=self.ttl)
227
228 for entry in journal[:self.feed_nr]:
229 #tmpl = h.action_parser(entry)[0]
230 action, action_extra = h.action_parser(entry, feed=True)
231 title = "%s - %s %s" % (entry.user.short_contact, action,
232 entry.repository.repo_name)
233 desc = action_extra()
234 feed.add_item(title=title,
235 pubdate=entry.action_date,
236 link=url('', qualified=True),
237 author_email=entry.user.email,
238 author_name=entry.user.full_contact,
239 description=desc)
240
241 response.content_type = feed.mime_type
242 return feed.writeString('utf-8')
294 return self._rss_feed(c.following)
@@ -1,169 +1,196 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.login
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Login controller for rhodeocode
7 7
8 8 :created_on: Apr 22, 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 formencode
28 import datetime
29 import urlparse
28 30
29 31 from formencode import htmlfill
30
32 from webob.exc import HTTPFound
31 33 from pylons.i18n.translation import _
32 34 from pylons.controllers.util import abort, redirect
33 35 from pylons import request, response, session, tmpl_context as c, url
34 36
35 37 import rhodecode.lib.helpers as h
36 38 from rhodecode.lib.auth import AuthUser, HasPermissionAnyDecorator
37 39 from rhodecode.lib.base import BaseController, render
38 40 from rhodecode.model.db import User
39 41 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
40 42 from rhodecode.model.user import UserModel
41 43 from rhodecode.model.meta import Session
42 44
43 45
44 46 log = logging.getLogger(__name__)
45 47
46 48
47 49 class LoginController(BaseController):
48 50
49 51 def __before__(self):
50 52 super(LoginController, self).__before__()
51 53
52 54 def index(self):
53 55 # redirect if already logged in
54 c.came_from = request.GET.get('came_from', None)
56 c.came_from = request.GET.get('came_from')
55 57
56 58 if self.rhodecode_user.is_authenticated \
57 59 and self.rhodecode_user.username != 'default':
58 60
59 61 return redirect(url('home'))
60 62
61 63 if request.POST:
62 64 # import Login Form validator class
63 65 login_form = LoginForm()
64 66 try:
67 session.invalidate()
65 68 c.form_result = login_form.to_python(dict(request.POST))
66 69 # form checks for username/password, now we're authenticated
67 70 username = c.form_result['username']
68 71 user = User.get_by_username(username, case_insensitive=True)
69 72 auth_user = AuthUser(user.user_id)
70 73 auth_user.set_authenticated()
71 74 cs = auth_user.get_cookie_store()
72 75 session['rhodecode_user'] = cs
76 user.update_lastlogin()
77 Session().commit()
78
73 79 # If they want to be remembered, update the cookie
74 80 if c.form_result['remember'] is not False:
75 session.cookie_expires = False
76 session._set_cookie_values()
77 session._update_cookie_out()
81 _year = (datetime.datetime.now() +
82 datetime.timedelta(seconds=60 * 60 * 24 * 365))
83 session._set_cookie_expires(_year)
84
78 85 session.save()
79 86
80 87 log.info('user %s is now authenticated and stored in '
81 88 'session, session attrs %s' % (username, cs))
82 user.update_lastlogin()
83 Session.commit()
89
90 # dumps session attrs back to cookie
91 session._update_cookie_out()
84 92
93 # we set new cookie
94 headers = None
95 if session.request['set_cookie']:
96 # send set-cookie headers back to response to update cookie
97 headers = [('Set-Cookie', session.request['cookie_out'])]
98
99 allowed_schemes = ['http', 'https']
85 100 if c.came_from:
86 return redirect(c.came_from)
101 parsed = urlparse.urlparse(c.came_from)
102 server_parsed = urlparse.urlparse(url.current())
103 if parsed.scheme and parsed.scheme not in allowed_schemes:
104 log.error(
105 'Suspicious URL scheme detected %s for url %s' %
106 (parsed.scheme, parsed))
107 c.came_from = url('home')
108 elif server_parsed.netloc != parsed.netloc:
109 log.error('Suspicious NETLOC detected %s for url %s'
110 'server url is: %s' %
111 (parsed.netloc, parsed, server_parsed))
112 c.came_from = url('home')
113 raise HTTPFound(location=c.came_from, headers=headers)
87 114 else:
88 return redirect(url('home'))
115 raise HTTPFound(location=url('home'), headers=headers)
89 116
90 117 except formencode.Invalid, errors:
91 118 return htmlfill.render(
92 119 render('/login.html'),
93 120 defaults=errors.value,
94 121 errors=errors.error_dict or {},
95 122 prefix_error=False,
96 123 encoding="UTF-8")
97 124
98 125 return render('/login.html')
99 126
100 127 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
101 128 'hg.register.manual_activate')
102 129 def register(self):
103 130 c.auto_active = False
104 131 for perm in User.get_by_username('default').user_perms:
105 132 if perm.permission.permission_name == 'hg.register.auto_activate':
106 133 c.auto_active = True
107 134 break
108 135
109 136 if request.POST:
110 137
111 138 register_form = RegisterForm()()
112 139 try:
113 140 form_result = register_form.to_python(dict(request.POST))
114 141 form_result['active'] = c.auto_active
115 142 UserModel().create_registration(form_result)
116 143 h.flash(_('You have successfully registered into rhodecode'),
117 144 category='success')
118 Session.commit()
145 Session().commit()
119 146 return redirect(url('login_home'))
120 147
121 148 except formencode.Invalid, errors:
122 149 return htmlfill.render(
123 150 render('/register.html'),
124 151 defaults=errors.value,
125 152 errors=errors.error_dict or {},
126 153 prefix_error=False,
127 154 encoding="UTF-8")
128 155
129 156 return render('/register.html')
130 157
131 158 def password_reset(self):
132 159 if request.POST:
133 160 password_reset_form = PasswordResetForm()()
134 161 try:
135 162 form_result = password_reset_form.to_python(dict(request.POST))
136 163 UserModel().reset_password_link(form_result)
137 164 h.flash(_('Your password reset link was sent'),
138 165 category='success')
139 166 return redirect(url('login_home'))
140 167
141 168 except formencode.Invalid, errors:
142 169 return htmlfill.render(
143 170 render('/password_reset.html'),
144 171 defaults=errors.value,
145 172 errors=errors.error_dict or {},
146 173 prefix_error=False,
147 174 encoding="UTF-8")
148 175
149 176 return render('/password_reset.html')
150 177
151 178 def password_reset_confirmation(self):
152 179 if request.GET and request.GET.get('key'):
153 180 try:
154 181 user = User.get_by_api_key(request.GET.get('key'))
155 182 data = dict(email=user.email)
156 183 UserModel().reset_password(data)
157 184 h.flash(_('Your password reset was successful, '
158 185 'new password has been sent to your email'),
159 186 category='success')
160 187 except Exception, e:
161 188 log.error(e)
162 189 return redirect(url('reset_password'))
163 190
164 191 return redirect(url('login_home'))
165 192
166 193 def logout(self):
167 194 session.delete()
168 195 log.info('Logging out and deleting session for user')
169 196 redirect(url('home'))
@@ -1,126 +1,143 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.search
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 Search controller for rhodecode
6 Search controller for RhodeCode
7 7
8 8 :created_on: Aug 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 import logging
26 26 import traceback
27 27
28 28 from pylons.i18n.translation import _
29 29 from pylons import request, config, tmpl_context as c
30 30
31 31 from rhodecode.lib.auth import LoginRequired
32 32 from rhodecode.lib.base import BaseController, render
33 from rhodecode.lib.indexers import SCHEMA, IDX_NAME, ResultWrapper
33 from rhodecode.lib.indexers import CHGSETS_SCHEMA, SCHEMA, CHGSET_IDX_NAME, \
34 IDX_NAME, WhooshResultWrapper
34 35
35 36 from webhelpers.paginate import Page
36 37 from webhelpers.util import update_params
37 38
38 39 from whoosh.index import open_dir, EmptyIndexError
39 40 from whoosh.qparser import QueryParser, QueryParserError
40 41 from whoosh.query import Phrase, Wildcard, Term, Prefix
42 from rhodecode.model.repo import RepoModel
41 43
42 44 log = logging.getLogger(__name__)
43 45
44 46
45 47 class SearchController(BaseController):
46 48
47 49 @LoginRequired()
48 50 def __before__(self):
49 51 super(SearchController, self).__before__()
50 52
51 53 def index(self, search_repo=None):
52 54 c.repo_name = search_repo
53 55 c.formated_results = []
54 56 c.runtime = ''
55 57 c.cur_query = request.GET.get('q', None)
56 c.cur_type = request.GET.get('type', 'source')
58 c.cur_type = request.GET.get('type', 'content')
57 59 c.cur_search = search_type = {'content': 'content',
58 'commit': 'content',
60 'commit': 'message',
59 61 'path': 'path',
60 'repository': 'repository'}\
61 .get(c.cur_type, 'content')
62 'repository': 'repository'
63 }.get(c.cur_type, 'content')
64
65 index_name = {
66 'content': IDX_NAME,
67 'commit': CHGSET_IDX_NAME,
68 'path': IDX_NAME
69 }.get(c.cur_type, IDX_NAME)
70
71 schema_defn = {
72 'content': SCHEMA,
73 'commit': CHGSETS_SCHEMA,
74 'path': SCHEMA
75 }.get(c.cur_type, SCHEMA)
76
77 log.debug('IDX: %s' % index_name)
78 log.debug('SCHEMA: %s' % schema_defn)
62 79
63 80 if c.cur_query:
64 81 cur_query = c.cur_query.lower()
82 log.debug(cur_query)
65 83
66 84 if c.cur_query:
67 85 p = int(request.params.get('page', 1))
68 86 highlight_items = set()
69 87 try:
70 88 idx = open_dir(config['app_conf']['index_dir'],
71 indexname=IDX_NAME)
89 indexname=index_name)
72 90 searcher = idx.searcher()
73 91
74 qp = QueryParser(search_type, schema=SCHEMA)
92 qp = QueryParser(search_type, schema=schema_defn)
75 93 if c.repo_name:
76 94 cur_query = u'repository:%s %s' % (c.repo_name, cur_query)
77 95 try:
78 96 query = qp.parse(unicode(cur_query))
79 97 # extract words for highlight
80 98 if isinstance(query, Phrase):
81 99 highlight_items.update(query.words)
82 100 elif isinstance(query, Prefix):
83 101 highlight_items.add(query.text)
84 102 else:
85 103 for i in query.all_terms():
86 if i[0] == 'content':
104 if i[0] in ['content', 'message']:
87 105 highlight_items.add(i[1])
88 106
89 107 matcher = query.matcher(searcher)
90 108
91 log.debug(query)
92 log.debug(highlight_items)
109 log.debug('query: %s' % query)
110 log.debug('hl terms: %s' % highlight_items)
93 111 results = searcher.search(query)
94 112 res_ln = len(results)
95 113 c.runtime = '%s results (%.3f seconds)' % (
96 114 res_ln, results.runtime
97 115 )
98 116
99 117 def url_generator(**kw):
100 118 return update_params("?q=%s&type=%s" \
101 % (c.cur_query, c.cur_search), **kw)
102
119 % (c.cur_query, c.cur_type), **kw)
120 repo_location = RepoModel().repos_path
103 121 c.formated_results = Page(
104 ResultWrapper(search_type, searcher, matcher,
105 highlight_items),
122 WhooshResultWrapper(search_type, searcher, matcher,
123 highlight_items, repo_location),
106 124 page=p,
107 125 item_count=res_ln,
108 126 items_per_page=10,
109 127 url=url_generator
110 128 )
111 129
112 130 except QueryParserError:
113 131 c.runtime = _('Invalid search query. Try quoting it.')
114 132 searcher.close()
115 133 except (EmptyIndexError, IOError):
116 134 log.error(traceback.format_exc())
117 135 log.error('Empty Index data')
118 136 c.runtime = _('There is no index to search in. '
119 137 'Please run whoosh indexer')
120 138 except (Exception):
121 139 log.error(traceback.format_exc())
122 140 c.runtime = _('An error occurred during this search operation')
123 141
124
125 142 # Return a rendered template
126 143 return render('/search/search.html')
@@ -1,158 +1,162 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.settings
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Settings controller for rhodecode
7 7
8 8 :created_on: Jun 30, 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 tmpl_context as c, request, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35
36 36 import rhodecode.lib.helpers as h
37 37
38 38 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAllDecorator
39 39 from rhodecode.lib.base import BaseRepoController, render
40 40 from rhodecode.lib.utils import invalidate_cache, action_logger
41 41
42 42 from rhodecode.model.forms import RepoSettingsForm
43 43 from rhodecode.model.repo import RepoModel
44 44 from rhodecode.model.db import RepoGroup
45 45 from rhodecode.model.meta import Session
46 from rhodecode.model.scm import ScmModel
46 47
47 48 log = logging.getLogger(__name__)
48 49
49 50
50 51 class SettingsController(BaseRepoController):
51 52
52 53 @LoginRequired()
53 54 def __before__(self):
54 55 super(SettingsController, self).__before__()
55 56
56 57 def __load_defaults(self):
57 58 c.repo_groups = RepoGroup.groups_choices()
58 59 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
59 60
60 61 repo_model = RepoModel()
61 62 c.users_array = repo_model.get_users_js()
62 63 c.users_groups_array = repo_model.get_users_groups_js()
64 choices, c.landing_revs = ScmModel().get_repo_landing_revs()
65 c.landing_revs_choices = choices
63 66
64 67 @HasRepoPermissionAllDecorator('repository.admin')
65 68 def index(self, repo_name):
66 69 repo_model = RepoModel()
67 70 c.repo_info = repo = repo_model.get_by_repo_name(repo_name)
68 71 if not repo:
69 72 h.flash(_('%s repository is not mapped to db perhaps'
70 73 ' it was created or renamed from the file system'
71 74 ' please run the application again'
72 75 ' in order to rescan repositories') % repo_name,
73 76 category='error')
74 77
75 78 return redirect(url('home'))
76 79
77 80 self.__load_defaults()
78 81
79 82 defaults = RepoModel()._get_defaults(repo_name)
80 83
81 84 return htmlfill.render(
82 85 render('settings/repo_settings.html'),
83 86 defaults=defaults,
84 87 encoding="UTF-8",
85 88 force_defaults=False
86 89 )
87 90
88 91 @HasRepoPermissionAllDecorator('repository.admin')
89 92 def update(self, repo_name):
90 93 repo_model = RepoModel()
91 94 changed_name = repo_name
92 95
93 96 self.__load_defaults()
94 97
95 98 _form = RepoSettingsForm(edit=True,
96 99 old_data={'repo_name': repo_name},
97 repo_groups=c.repo_groups_choices)()
100 repo_groups=c.repo_groups_choices,
101 landing_revs=c.landing_revs_choices)()
98 102 try:
99 103 form_result = _form.to_python(dict(request.POST))
100 104
101 105 repo_model.update(repo_name, form_result)
102 106 invalidate_cache('get_repo_cached_%s' % repo_name)
103 h.flash(_('Repository %s updated successfully' % repo_name),
107 h.flash(_('Repository %s updated successfully') % repo_name,
104 108 category='success')
105 109 changed_name = form_result['repo_name_full']
106 110 action_logger(self.rhodecode_user, 'user_updated_repo',
107 changed_name, '', self.sa)
111 changed_name, self.ip_addr, self.sa)
108 112 Session.commit()
109 113 except formencode.Invalid, errors:
110 114 c.repo_info = repo_model.get_by_repo_name(repo_name)
111 115 c.users_array = repo_model.get_users_js()
112 116 errors.value.update({'user': c.repo_info.user.username})
113 117 return htmlfill.render(
114 118 render('settings/repo_settings.html'),
115 119 defaults=errors.value,
116 120 errors=errors.error_dict or {},
117 121 prefix_error=False,
118 122 encoding="UTF-8")
119 123 except Exception:
120 124 log.error(traceback.format_exc())
121 125 h.flash(_('error occurred during update of repository %s') \
122 126 % repo_name, category='error')
123 127
124 128 return redirect(url('repo_settings_home', repo_name=changed_name))
125 129
126 130 @HasRepoPermissionAllDecorator('repository.admin')
127 131 def delete(self, repo_name):
128 132 """DELETE /repos/repo_name: Delete an existing item"""
129 133 # Forms posted to this method should contain a hidden field:
130 134 # <input type="hidden" name="_method" value="DELETE" />
131 135 # Or using helpers:
132 136 # h.form(url('repo_settings_delete', repo_name=ID),
133 137 # method='delete')
134 138 # url('repo_settings_delete', repo_name=ID)
135 139
136 140 repo_model = RepoModel()
137 141 repo = repo_model.get_by_repo_name(repo_name)
138 142 if not repo:
139 143 h.flash(_('%s repository is not mapped to db perhaps'
140 144 ' it was moved or renamed from the filesystem'
141 145 ' please run the application again'
142 146 ' in order to rescan repositories') % repo_name,
143 147 category='error')
144 148
145 149 return redirect(url('home'))
146 150 try:
147 151 action_logger(self.rhodecode_user, 'user_deleted_repo',
148 repo_name, '', self.sa)
152 repo_name, self.ip_addr, self.sa)
149 153 repo_model.delete(repo)
150 154 invalidate_cache('get_repo_cached_%s' % repo_name)
151 155 h.flash(_('deleted repository %s') % repo_name, category='success')
152 156 Session.commit()
153 157 except Exception:
154 158 log.error(traceback.format_exc())
155 159 h.flash(_('An error occurred during deletion of %s') % repo_name,
156 160 category='error')
157 161
158 162 return redirect(url('home'))
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
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
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: file renamed from rhodecode/tests/test_models.py to rhodecode/tests/models/test_notifications.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rhodecode/tests/mem_watch to rhodecode/tests/scripts/mem_watch
1 NO CONTENT: file renamed from rhodecode/tests/_test_concurency.py to rhodecode/tests/scripts/test_concurency.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rhodecode/tests/rhodecode_crawler.py to rhodecode/tests/scripts/test_crawler.py
1 NO CONTENT: file renamed from rhodecode/tests/test_hg_operations.py to rhodecode/tests/scripts/test_vcs_operations.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, 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: 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