##// END OF EJS Templates
merge upstream
Aras Pranckevicius -
r1861:3f5be4db merge beta
parent child Browse files
Show More
@@ -1,150 +1,149 b''
1 =================================================
2 Welcome to RhodeCode (RhodiumCode) documentation!
3 =================================================
1 ========================
2 RhodeCode documentation!
3 ========================
4 4
5 5 ``RhodeCode`` is a fast and powerful management tool for Mercurial_ and GIT_
6 6 with a built in push/pull server and full text search.
7 7 It works on http/https and has a built in permission/authentication system with
8 8 the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also supports
9 9 simple API so it's easy integrable with existing external systems.
10 10
11 11 RhodeCode is similar in some respects to github or bitbucket_,
12 12 however RhodeCode can be run as standalone hosted application on your own server.
13 13 It is open source and donation ware and focuses more on providing a customized,
14 14 self administered interface for Mercurial and GIT repositories.
15 15 RhodeCode is powered by a vcs_ library that Lukasz Balcerzak and I created to
16 16 handle multiple different version control systems.
17 17
18 18 RhodeCode uses `Semantic Versioning <http://semver.org/>`_
19 19
20 20 RhodeCode demo
21 21 --------------
22 22
23 23 http://demo.rhodecode.org
24 24
25 25 The default access is anonymous but you can login to an administrative account
26 26 using the following credentials:
27 27
28 28 - username: demo
29 29 - password: demo12
30 30
31 31 Source code
32 32 -----------
33 33
34 34 The latest sources can be obtained from official RhodeCode instance
35 35 https://secure.rhodecode.org
36 36
37 37
38 38 MIRRORS:
39 39
40 40 Issue tracker and sources at bitbucket_
41 41
42 42 http://bitbucket.org/marcinkuzminski/rhodecode
43 43
44 44 Sources at github_
45 45
46 46 https://github.com/marcinkuzminski/rhodecode
47 47
48 48 Installation
49 49 ------------
50 50
51 51 Please visit http://packages.python.org/RhodeCode/installation.html
52 52
53 53
54 54 RhodeCode Features
55 55 ------------------
56 56
57 57 - Has its own middleware to handle mercurial_ protocol requests.
58 58 Each request can be logged and authenticated.
59 59 - Runs on threads unlike hgweb. You can make multiple pulls/pushes simultaneous.
60 60 Supports http/https and LDAP
61 61 - Full permissions (private/read/write/admin) and authentication per project.
62 62 One account for web interface and mercurial_ push/pull/clone operations.
63 63 - Have built in users groups for easier permission management
64 64 - Repository groups let you group repos and manage them easier.
65 65 - Users can fork other users repo. RhodeCode have also compare view to see
66 66 combined changeset for all changeset made within single push.
67 67 - Build in commit-api let's you add, edit and commit files right from RhodeCode
68 68 interface using simple editor or upload form for binaries.
69 69 - Mako templates let's you customize the look and feel of the application.
70 70 - Beautiful diffs, annotations and source code browsing all colored by pygments.
71 71 Raw diffs are made in git-diff format, including git_ binary-patches
72 72 - Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics
73 73 - Admin interface with user/permission management. Admin activity journal, logs
74 74 pulls, pushes, forks, registrations and other actions made by all users.
75 75 - Server side forks. It is possible to fork a project and modify it freely
76 76 without breaking the main repository. You can even write Your own hooks
77 77 and install them
78 78 - code review with notification system, inline commenting, all parsed using
79 79 rst syntax
80 80 - rst and markdown README support for repositories
81 81 - Full text search powered by Whoosh on the source files, and file names.
82 82 Build in indexing daemons, with optional incremental index build
83 83 (no external search servers required all in one application)
84 84 - Setup project descriptions and info inside built in db for easy, non
85 85 file-system operations
86 86 - Intelligent cache with invalidation after push or project change, provides
87 87 high performance and always up to date data.
88 88 - Rss / atom feeds, gravatar support, download sources as zip/tar/gz
89 89 - Async tasks for speed and performance using celery_ (works without them too)
90 90 - Backup scripts can do backup of whole app and send it over scp to desired
91 91 location
92 92 - Based on pylons / sqlalchemy / sqlite / whoosh / vcs
93 93
94 94
95 95 .. include:: ./docs/screenshots.rst
96 96
97 97
98 98 Incoming / Plans
99 99 ----------------
100 100
101 101 - Finer granular permissions per branch, repo group or subrepo
102 102 - pull requests and web based merges
103 103 - per line file history
104 104 - SSH based authentication with server side key management
105 - Redmine and other bugtrackers integration
106 105 - Commit based built in wiki system
107 106 - More statistics and graph (global annotation + some more statistics)
108 107 - Other advancements as development continues (or you can of course make
109 108 additions and or requests)
110 109
111 110 License
112 111 -------
113 112
114 113 ``RhodeCode`` is released under the GPLv3 license.
115 114
116 115
117 116 Mailing group Q&A
118 117 -----------------
119 118
120 119 Join the `Google group <http://groups.google.com/group/rhodecode>`_
121 120
122 121 Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
123 122
124 123 Join #rhodecode on FreeNode (irc.freenode.net)
125 124 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
126 125
127 126 Online documentation
128 127 --------------------
129 128
130 129 Online documentation for the current version of RhodeCode is available at
131 130 http://packages.python.org/RhodeCode/.
132 131 You may also build the documentation for yourself - go into ``docs/`` and run::
133 132
134 133 make html
135 134
136 135 (You need to have sphinx_ installed to build the documentation. If you don't
137 136 have sphinx_ installed you can install it via the command:
138 137 ``easy_install sphinx``)
139 138
140 139 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
141 140 .. _python: http://www.python.org/
142 141 .. _sphinx: http://sphinx.pocoo.org/
143 142 .. _mercurial: http://mercurial.selenic.com/
144 143 .. _bitbucket: http://bitbucket.org/
145 144 .. _github: http://github.com/
146 145 .. _subversion: http://subversion.tigris.org/
147 146 .. _git: http://git-scm.com/
148 147 .. _celery: http://celeryproject.org/
149 148 .. _Sphinx: http://sphinx.pocoo.org/
150 149 .. _vcs: http://pypi.python.org/pypi/vcs No newline at end of file
@@ -1,267 +1,288 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 33 threadpool_workers = 5
34 34
35 35 ##max request before thread respawn
36 36 threadpool_max_requests = 10
37 37
38 38 ##option to use threads of process
39 39 use_threadpool = true
40 40
41 41 use = egg:Paste#http
42 42 host = 0.0.0.0
43 43 port = 5000
44 44
45 45 [app:main]
46 46 use = egg:rhodecode
47 47 full_stack = true
48 48 static_files = true
49 49 lang=en
50 50 cache_dir = %(here)s/data
51 51 index_dir = %(here)s/data/index
52 52 app_instance_uuid = develop
53 53 cut_off_limit = 256000
54 54 force_https = false
55 55 commit_parse_limit = 25
56 56 use_gravatar = true
57 57 container_auth_enabled = false
58 58 proxypass_auth_enabled = false
59 59
60 60 ## overwrite schema of clone url
61 # available vars:
62 # scheme - http/https
63 # user - current user
64 # pass - password
65 # netloc - network location
66 # path - usually repo_name
67 # clone_uri = {scheme}://{user}{pass}{netloc}{path}
61 ## available vars:
62 ## scheme - http/https
63 ## user - current user
64 ## pass - password
65 ## netloc - network location
66 ## path - usually repo_name
67
68 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
69
70 ## issue tracking mapping for commits messages
71 ## uncomment url_pat, issue_server, issue_prefix to enable
72
73
74 ## pattern to get the issues from commit messages
75 ## default one used here is #1234
76
77 #url_pat = (?:^#|\s#)(\w+)
78
79 ## server url to the issue, each {id} will be replaced with id
80 ## fetched from the regex
81
82 #issue_server = https://myissueserver.com/issue/{id}
83
84 ## prefix to add to link to indicate it's an url
85 ## #314 will be replaced by <issue_prefix><id>
86
87 #issue_prefix = #
88
68 89
69 90 ####################################
70 91 ### CELERY CONFIG ####
71 92 ####################################
72 93 use_celery = false
73 94 broker.host = localhost
74 95 broker.vhost = rabbitmqhost
75 96 broker.port = 5672
76 97 broker.user = rabbitmq
77 98 broker.password = qweqwe
78 99
79 100 celery.imports = rhodecode.lib.celerylib.tasks
80 101
81 102 celery.result.backend = amqp
82 103 celery.result.dburi = amqp://
83 104 celery.result.serialier = json
84 105
85 106 #celery.send.task.error.emails = true
86 107 #celery.amqp.task.result.expires = 18000
87 108
88 109 celeryd.concurrency = 2
89 110 #celeryd.log.file = celeryd.log
90 111 celeryd.log.level = debug
91 112 celeryd.max.tasks.per.child = 1
92 113
93 114 #tasks will never be sent to the queue, but executed locally instead.
94 115 celery.always.eager = false
95 116
96 117 ####################################
97 118 ### BEAKER CACHE ####
98 119 ####################################
99 120 beaker.cache.data_dir=%(here)s/data/cache/data
100 121 beaker.cache.lock_dir=%(here)s/data/cache/lock
101 122
102 123 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
103 124
104 125 beaker.cache.super_short_term.type=memory
105 126 beaker.cache.super_short_term.expire=10
106 127 beaker.cache.super_short_term.key_length = 256
107 128
108 129 beaker.cache.short_term.type=memory
109 130 beaker.cache.short_term.expire=60
110 131 beaker.cache.short_term.key_length = 256
111 132
112 133 beaker.cache.long_term.type=memory
113 134 beaker.cache.long_term.expire=36000
114 135 beaker.cache.long_term.key_length = 256
115 136
116 137 beaker.cache.sql_cache_short.type=memory
117 138 beaker.cache.sql_cache_short.expire=10
118 139 beaker.cache.sql_cache_short.key_length = 256
119 140
120 141 beaker.cache.sql_cache_med.type=memory
121 142 beaker.cache.sql_cache_med.expire=360
122 143 beaker.cache.sql_cache_med.key_length = 256
123 144
124 145 beaker.cache.sql_cache_long.type=file
125 146 beaker.cache.sql_cache_long.expire=3600
126 147 beaker.cache.sql_cache_long.key_length = 256
127 148
128 149 ####################################
129 150 ### BEAKER SESSION ####
130 151 ####################################
131 152 ## Type of storage used for the session, current types are
132 153 ## dbm, file, memcached, database, and memory.
133 154 ## The storage uses the Container API
134 155 ## that is also used by the cache system.
135 156
136 157 ## db session example
137 158
138 159 #beaker.session.type = ext:database
139 160 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
140 161 #beaker.session.table_name = db_session
141 162
142 163 ## encrypted cookie session, good for many instances
143 164 #beaker.session.type = cookie
144 165
145 166 beaker.session.type = file
146 167 beaker.session.key = rhodecode
147 168 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
148 169 #beaker.session.validate_key = 9712sds2212c--zxc123
149 170 beaker.session.timeout = 36000
150 171 beaker.session.httponly = true
151 172
152 173 ## uncomment for https secure cookie
153 174 beaker.session.secure = false
154 175
155 176 ##auto save the session to not to use .save()
156 177 beaker.session.auto = False
157 178
158 179 ##true exire at browser close
159 180 #beaker.session.cookie_expires = 3600
160 181
161 182
162 183 ################################################################################
163 184 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
164 185 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
165 186 ## execute malicious code after an exception is raised. ##
166 187 ################################################################################
167 188 #set debug = false
168 189
169 190 ##################################
170 191 ### LOGVIEW CONFIG ###
171 192 ##################################
172 193 logview.sqlalchemy = #faa
173 194 logview.pylons.templating = #bfb
174 195 logview.pylons.util = #eee
175 196
176 197 #########################################################
177 198 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
178 199 #########################################################
179 200 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
180 201 sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode
181 202 sqlalchemy.db1.echo = false
182 203 sqlalchemy.db1.pool_recycle = 3600
183 204 sqlalchemy.convert_unicode = true
184 205
185 206 ################################
186 207 ### LOGGING CONFIGURATION ####
187 208 ################################
188 209 [loggers]
189 210 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
190 211
191 212 [handlers]
192 213 keys = console, console_sql
193 214
194 215 [formatters]
195 216 keys = generic, color_formatter, color_formatter_sql
196 217
197 218 #############
198 219 ## LOGGERS ##
199 220 #############
200 221 [logger_root]
201 222 level = NOTSET
202 223 handlers = console
203 224
204 225 [logger_routes]
205 226 level = DEBUG
206 227 handlers =
207 228 qualname = routes.middleware
208 229 # "level = DEBUG" logs the route matched and routing variables.
209 230 propagate = 1
210 231
211 232 [logger_beaker]
212 233 level = DEBUG
213 234 handlers =
214 235 qualname = beaker.container
215 236 propagate = 1
216 237
217 238 [logger_templates]
218 239 level = INFO
219 240 handlers =
220 241 qualname = pylons.templating
221 242 propagate = 1
222 243
223 244 [logger_rhodecode]
224 245 level = DEBUG
225 246 handlers =
226 247 qualname = rhodecode
227 248 propagate = 1
228 249
229 250 [logger_sqlalchemy]
230 251 level = INFO
231 252 handlers = console_sql
232 253 qualname = sqlalchemy.engine
233 254 propagate = 0
234 255
235 256 ##############
236 257 ## HANDLERS ##
237 258 ##############
238 259
239 260 [handler_console]
240 261 class = StreamHandler
241 262 args = (sys.stderr,)
242 263 level = DEBUG
243 264 formatter = color_formatter
244 265
245 266 [handler_console_sql]
246 267 class = StreamHandler
247 268 args = (sys.stderr,)
248 269 level = DEBUG
249 270 formatter = color_formatter_sql
250 271
251 272 ################
252 273 ## FORMATTERS ##
253 274 ################
254 275
255 276 [formatter_generic]
256 277 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
257 278 datefmt = %Y-%m-%d %H:%M:%S
258 279
259 280 [formatter_color_formatter]
260 281 class=rhodecode.lib.colored_formatter.ColorFormatter
261 282 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
262 283 datefmt = %Y-%m-%d %H:%M:%S
263 284
264 285 [formatter_color_formatter_sql]
265 286 class=rhodecode.lib.colored_formatter.ColorFormatterSql
266 287 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
267 288 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,421 +1,472 b''
1 1 .. _api:
2 2
3 3
4 4 API
5 5 ===
6 6
7 7
8 8 Starting from RhodeCode version 1.2 a simple API was implemented.
9 9 There's a single schema for calling all api methods. API is implemented
10 10 with JSON protocol both ways. An url to send API request in RhodeCode is
11 11 <your_server>/_admin/api
12 12
13 API ACCESS FOR WEB VIEWS
14 ++++++++++++++++++++++++
13 15
14 16 API access can also be turned on for each view decorated with `@LoginRequired`
15 17 decorator. To enable API access simple change standard login decorator into
16 18 `@LoginRequired(api_access=True)`. After such a change view can be accessed
17 19 by adding a GET parameter to url `?api_key=<api_key>`. By default it's only
18 20 enabled on RSS/ATOM feed views.
19 21
20 22
23 API ACCESS
24 ++++++++++
25
21 26 All clients are required to send JSON-RPC spec JSON data::
22 27
23 28 {
24 29 "id:<id>,
25 30 "api_key":"<api_key>",
26 31 "method":"<method_name>",
27 32 "args":{"<arg_key>":"<arg_val>"}
28 33 }
29 34
30 35 Example call for autopulling remotes repos using curl::
31 36 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"}}'
32 37
33 38 Simply provide
34 39 - *id* A value of any type, which is used to match the response with the request that it is replying to.
35 40 - *api_key* for access and permission validation.
36 41 - *method* is name of method to call
37 42 - *args* is an key:value list of arguments to pass to method
38 43
39 44 .. note::
40 45
41 46 api_key can be found in your user account page
42 47
43 48
44 49 RhodeCode API will return always a JSON-RPC response::
45 50
46 51 {
47 52 "id":<id>,
48 53 "result": "<result>",
49 54 "error": null
50 55 }
51 56
52 57 All responses from API will be `HTTP/1.0 200 OK`, if there's an error while
53 58 calling api *error* key from response will contain failure description
54 59 and result will be null.
55 60
56 61 API METHODS
57 62 +++++++++++
58 63
59 64
60 65 pull
61 66 ----
62 67
63 68 Pulls given repo from remote location. Can be used to automatically keep
64 69 remote repos up to date. This command can be executed only using api_key
65 70 belonging to user with admin rights
66 71
67 72 INPUT::
68 73
69 74 api_key : "<api_key>"
70 75 method : "pull"
71 76 args : {
72 "repo" : "<repo_name>"
77 "repo_name" : "<reponame>"
73 78 }
74 79
75 80 OUTPUT::
76 81
77 result : "Pulled from <repo_name>"
82 result : "Pulled from <reponame>"
78 83 error : null
79 84
80 85
86 get_user
87 --------
88
89 Get's an user by username, Returns empty result if user is not found.
90 This command can be executed only using api_key belonging to user with admin
91 rights.
92
93 INPUT::
94
95 api_key : "<api_key>"
96 method : "get_user"
97 args : {
98 "username" : "<username>"
99 }
100
101 OUTPUT::
102
103 result: None if user does not exist or
104 {
105 "id" : "<id>",
106 "username" : "<username>",
107 "firstname": "<firstname>",
108 "lastname" : "<lastname>",
109 "email" : "<email>",
110 "active" : "<bool>",
111 "admin" :  "<bool>",
112 "ldap" : "<ldap_dn>"
113 }
114
115 error: null
116
117
81 118 get_users
82 119 ---------
83 120
84 121 Lists all existing users. This command can be executed only using api_key
85 122 belonging to user with admin rights.
86 123
87 124 INPUT::
88 125
89 126 api_key : "<api_key>"
90 127 method : "get_users"
91 128 args : { }
92 129
93 130 OUTPUT::
94 131
95 132 result: [
96 133 {
97 134 "id" : "<id>",
98 135 "username" : "<username>",
99 136 "firstname": "<firstname>",
100 137 "lastname" : "<lastname>",
101 138 "email" : "<email>",
102 139 "active" : "<bool>",
103 140 "admin" :  "<bool>",
104 141 "ldap" : "<ldap_dn>"
105 142 },
106 143
107 144 ]
108 145 error: null
109 146
110 147 create_user
111 148 -----------
112 149
113 150 Creates new user in RhodeCode. This command can be executed only using api_key
114 151 belonging to user with admin rights.
115 152
116 153 INPUT::
117 154
118 155 api_key : "<api_key>"
119 156 method : "create_user"
120 157 args : {
121 158 "username" : "<username>",
122 159 "password" : "<password>",
123 160 "firstname" : "<firstname>",
124 161 "lastname" : "<lastname>",
125 162 "email" : "<useremail>"
126 163 "active" : "<bool> = True",
127 164 "admin" : "<bool> = False",
128 165 "ldap_dn" : "<ldap_dn> = None"
129 166 }
130 167
131 168 OUTPUT::
132 169
133 170 result: {
171 "id" : "<new_user_id>",
134 172 "msg" : "created new user <username>"
135 173 }
136 174 error: null
137 175
138 get_users_groups
139 ----------------
140
141 Lists all existing users groups. This command can be executed only using api_key
142 belonging to user with admin rights.
143
144 INPUT::
145
146 api_key : "<api_key>"
147 method : "get_users_groups"
148 args : { }
149
150 OUTPUT::
151
152 result : [
153 {
154 "id" : "<id>",
155 "name" : "<name>",
156 "active": "<bool>",
157 "members" : [
158 {
159 "id" : "<userid>",
160 "username" : "<username>",
161 "firstname": "<firstname>",
162 "lastname" : "<lastname>",
163 "email" : "<email>",
164 "active" : "<bool>",
165 "admin" :  "<bool>",
166 "ldap" : "<ldap_dn>"
167 },
168
169 ]
170 }
171 ]
172 error : null
173
174 176 get_users_group
175 177 ---------------
176 178
177 179 Gets an existing users group. This command can be executed only using api_key
178 180 belonging to user with admin rights.
179 181
180 182 INPUT::
181 183
182 184 api_key : "<api_key>"
183 185 method : "get_users_group"
184 186 args : {
185 187 "group_name" : "<name>"
186 188 }
187 189
188 190 OUTPUT::
189 191
190 192 result : None if group not exist
191 193 {
192 "id" : "<id>",
193 "name" : "<name>",
194 "active": "<bool>",
194 "id" : "<id>",
195 "group_name" : "<groupname>",
196 "active": "<bool>",
195 197 "members" : [
196 { "id" : "<userid>",
197 "username" : "<username>",
198 "firstname": "<firstname>",
199 "lastname" : "<lastname>",
200 "email" : "<email>",
201 "active" : "<bool>",
202 "admin" :  "<bool>",
203 "ldap" : "<ldap_dn>"
204 },
205
206 ]
198 { "id" : "<userid>",
199 "username" : "<username>",
200 "firstname": "<firstname>",
201 "lastname" : "<lastname>",
202 "email" : "<email>",
203 "active" : "<bool>",
204 "admin" :  "<bool>",
205 "ldap" : "<ldap_dn>"
206 },
207
208 ]
207 209 }
208 210 error : null
209 211
212 get_users_groups
213 ----------------
214
215 Lists all existing users groups. This command can be executed only using
216 api_key belonging to user with admin rights.
217
218 INPUT::
219
220 api_key : "<api_key>"
221 method : "get_users_groups"
222 args : { }
223
224 OUTPUT::
225
226 result : [
227 {
228 "id" : "<id>",
229 "group_name" : "<groupname>",
230 "active": "<bool>",
231 "members" : [
232 {
233 "id" : "<userid>",
234 "username" : "<username>",
235 "firstname": "<firstname>",
236 "lastname" : "<lastname>",
237 "email" : "<email>",
238 "active" : "<bool>",
239 "admin" :  "<bool>",
240 "ldap" : "<ldap_dn>"
241 },
242
243 ]
244 }
245 ]
246 error : null
247
248
210 249 create_users_group
211 250 ------------------
212 251
213 252 Creates new users group. This command can be executed only using api_key
214 253 belonging to user with admin rights
215 254
216 255 INPUT::
217 256
218 257 api_key : "<api_key>"
219 258 method : "create_users_group"
220 259 args: {
221 "name": "<name>",
260 "group_name": "<groupname>",
222 261 "active":"<bool> = True"
223 262 }
224 263
225 264 OUTPUT::
226 265
227 266 result: {
228 267 "id": "<newusersgroupid>",
229 "msg": "created new users group <name>"
268 "msg": "created new users group <groupname>"
230 269 }
231 270 error: null
232 271
233 272 add_user_to_users_group
234 273 -----------------------
235 274
236 275 Adds a user to a users group. This command can be executed only using api_key
237 276 belonging to user with admin rights
238 277
239 278 INPUT::
240 279
241 280 api_key : "<api_key>"
242 281 method : "add_user_users_group"
243 282 args: {
244 283 "group_name" : "<groupname>",
245 284 "username" : "<username>"
246 285 }
247 286
248 287 OUTPUT::
249 288
250 289 result: {
251 290 "id": "<newusersgroupmemberid>",
252 291 "msg": "created new users group member"
253 292 }
254 293 error: null
255 294
295 get_repo
296 --------
297
298 Gets an existing repository. This command can be executed only using api_key
299 belonging to user with admin rights
300
301 INPUT::
302
303 api_key : "<api_key>"
304 method : "get_repo"
305 args: {
306 "repo_name" : "<reponame>"
307 }
308
309 OUTPUT::
310
311 result: None if repository does not exist or
312 {
313 "id" : "<id>",
314 "repo_name" : "<reponame>"
315 "type" : "<type>",
316 "description" : "<description>",
317 "members" : [
318 { "id" : "<userid>",
319 "username" : "<username>",
320 "firstname": "<firstname>",
321 "lastname" : "<lastname>",
322 "email" : "<email>",
323 "active" : "<bool>",
324 "admin" :  "<bool>",
325 "ldap" : "<ldap_dn>",
326 "permission" : "repository.(read|write|admin)"
327 },
328
329 {
330 "id" : "<usersgroupid>",
331 "name" : "<usersgroupname>",
332 "active": "<bool>",
333 "permission" : "repository.(read|write|admin)"
334 },
335
336 ]
337 }
338 error: null
339
256 340 get_repos
257 341 ---------
258 342
259 343 Lists all existing repositories. This command can be executed only using api_key
260 344 belonging to user with admin rights
261 345
262 346 INPUT::
263 347
264 348 api_key : "<api_key>"
265 349 method : "get_repos"
266 350 args: { }
267 351
268 352 OUTPUT::
269 353
270 354 result: [
271 355 {
272 356 "id" : "<id>",
273 "name" : "<name>"
357 "repo_name" : "<reponame>"
274 358 "type" : "<type>",
275 359 "description" : "<description>"
276 360 },
277 361
278 362 ]
279 363 error: null
280 364
281 get_repo
282 --------
283
284 Gets an existing repository. This command can be executed only using api_key
285 belonging to user with admin rights
286
287 INPUT::
288
289 api_key : "<api_key>"
290 method : "get_repo"
291 args: {
292 "name" : "<name>"
293 }
294
295 OUTPUT::
296
297 result: None if repository not exist
298 {
299 "id" : "<id>",
300 "name" : "<name>"
301 "type" : "<type>",
302 "description" : "<description>",
303 "members" : [
304 { "id" : "<userid>",
305 "username" : "<username>",
306 "firstname": "<firstname>",
307 "lastname" : "<lastname>",
308 "email" : "<email>",
309 "active" : "<bool>",
310 "admin" :  "<bool>",
311 "ldap" : "<ldap_dn>",
312 "permission" : "repository.(read|write|admin)"
313 },
314
315 {
316 "id" : "<usersgroupid>",
317 "name" : "<usersgroupname>",
318 "active": "<bool>",
319 "permission" : "repository.(read|write|admin)"
320 },
321
322 ]
323 }
324 error: null
325 365
326 366 get_repo_nodes
327 367 --------------
328 368
329 369 returns a list of nodes and it's children in a flat list for a given path
330 at given revision. It's possible to specify ret_type to show only files or
331 dirs. This command can be executed only using api_key belonging to user
370 at given revision. It's possible to specify ret_type to show only `files` or
371 `dirs`. This command can be executed only using api_key belonging to user
332 372 with admin rights
333 373
334 374 INPUT::
335 375
336 376 api_key : "<api_key>"
337 377 method : "get_repo_nodes"
338 378 args: {
339 "repo_name" : "<name>",
379 "repo_name" : "<reponame>",
340 380 "revision" : "<revision>",
341 381 "root_path" : "<root_path>",
342 382 "ret_type" : "<ret_type>" = 'all'
343 383 }
344 384
345 385 OUTPUT::
346 386
347 387 result: [
348 388 {
349 389 "name" : "<name>"
350 390 "type" : "<type>",
351 391 },
352 392
353 393 ]
354 394 error: null
355 395
356 396
357 397
358 398 create_repo
359 399 -----------
360 400
361 401 Creates a repository. This command can be executed only using api_key
362 402 belonging to user with admin rights.
363 403 If repository name contains "/", all needed repository groups will be created.
364 404 For example "foo/bar/baz" will create groups "foo", "bar" (with "foo" as parent),
365 405 and create "baz" repository with "bar" as group.
366 406
367 407 INPUT::
368 408
369 409 api_key : "<api_key>"
370 410 method : "create_repo"
371 411 args: {
372 "name" : "<name>",
412 "repo_name" : "<reponame>",
373 413 "owner_name" : "<ownername>",
374 414 "description" : "<description> = ''",
375 415 "repo_type" : "<type> = 'hg'",
376 416 "private" : "<bool> = False"
377 417 }
378 418
379 419 OUTPUT::
380 420
381 result: None
421 result: {
422 "id": "<newrepoid>",
423 "msg": "Created new repository <reponame>",
424 }
382 425 error: null
383 426
384 427 add_user_to_repo
385 428 ----------------
386 429
387 430 Add a user to a repository. This command can be executed only using api_key
388 431 belonging to user with admin rights.
389 432 If "perm" is None, user will be removed from the repository.
390 433
391 434 INPUT::
392 435
393 436 api_key : "<api_key>"
394 437 method : "add_user_to_repo"
395 438 args: {
396 439 "repo_name" : "<reponame>",
397 "username" : "<username>",
440 "username" : "<username>",
398 441 "perm" : "(None|repository.(read|write|admin))",
399 442 }
400 443
401 444 OUTPUT::
402 445
403 result: None
446 result: {
447 "msg" : "Added perm: <perm> for <username> in repo: <reponame>"
448 }
404 449 error: null
405 450
406 451 add_users_group_to_repo
407 452 -----------------------
408 453
409 454 Add a users group to a repository. This command can be executed only using
410 455 api_key belonging to user with admin rights. If "perm" is None, group will
411 456 be removed from the repository.
412 457
413 458 INPUT::
414 459
415 460 api_key : "<api_key>"
416 461 method : "add_users_group_to_repo"
417 462 args: {
418 463 "repo_name" : "<reponame>",
419 "group_name" : "<groupname>",
464 "group_name" : "<groupname>",
420 465 "perm" : "(None|repository.(read|write|admin))",
421 } No newline at end of file
466 }
467 OUTPUT::
468
469 result: {
470 "msg" : Added perm: <perm> for <groupname> in repo: <reponame>"
471 }
472
@@ -1,10 +1,10 b''
1 .. _api:
1 .. _indexapi:
2 2
3 3 API Reference
4 4 =============
5 5
6 6 .. toctree::
7 7 :maxdepth: 3
8 8
9 9 models
10 10 api No newline at end of file
@@ -1,19 +1,34 b''
1 1 .. _models:
2 2
3 3 The :mod:`models` Module
4 4 ========================
5 5
6 6 .. automodule:: rhodecode.model
7 7 :members:
8 8
9 .. automodule:: rhodecode.model.comment
10 :members:
11
12 .. automodule:: rhodecode.model.notification
13 :members:
14
9 15 .. automodule:: rhodecode.model.permission
10 16 :members:
11
17
18 .. automodule:: rhodecode.model.repo_permission
19 :members:
20
12 21 .. automodule:: rhodecode.model.repo
13 22 :members:
14 23
24 .. automodule:: rhodecode.model.repos_group
25 :members:
26
15 27 .. automodule:: rhodecode.model.scm
16 28 :members:
17
29
18 30 .. automodule:: rhodecode.model.user
19 31 :members:
32
33 .. automodule:: rhodecode.model.users_group
34 :members: No newline at end of file
@@ -1,441 +1,443 b''
1 1 .. _changelog:
2 2
3 3 Changelog
4 4 =========
5 5
6 6
7 7 1.3.0 (**XXXX-XX-XX**)
8 8 ======================
9 9
10 10 :status: in-progress
11 11 :branch: beta
12 12
13 13 news
14 14 ----
15 15
16 16 - code review, inspired by github code-comments
17 17 - #215 rst and markdown README files support
18 18 - #252 Container-based and proxy pass-through authentication support
19 19 - #44 branch browser. Filtering of changelog by branches
20 20 - mercurial bookmarks support
21 21 - hover top menu
22 22 - configurable clone url template with possibility to specify protocol like
23 23 ssh:// or http:// and also manually alter other parts of clone_url.
24 24 - enabled largefiles extension by default
25 25 - optimized summary file pages and saved a lot of unused space in them
26 26 - #239 option to manually mark repository as fork
27 27 - #320 mapping of commit authors to RhodeCode users
28 28 - #304 hashes are displayed using monospace font
29 29 - diff configuration, toggle white lines and context lines
30 30 - #307 configurable diffs, whitespace toggle, increasing context lines
31 31 - sorting on branches, tags and bookmarks using YUI datatable
32 32 - improved file filter on files page
33 33 - implements #330 api method for listing nodes ar particular revision
34 34 - fixed #331 RhodeCode mangles repository names if the a repository group
35 35 contains the "full path" to the repositories
36
36 - #73 added linking issues in commit messages to choosen issue tracker url
37 based on user defined regular expression
38
37 39 fixes
38 40 -----
39 41
40 42 - rewrote dbsession management for atomic operations, and better error handling
41 43 - fixed sorting of repo tables
42 44 - #326 escape of special html entities in diffs
43 45 - normalized user_name => username in api attributes
44 46 - fixes #298 ldap created users with mixed case emails created conflicts
45 47 on saving a form
46 48 - fixes issue when owner of a repo couldn't revoke permissions for users
47 49 and groups
48 50
49 51 1.2.3 (**2011-11-02**)
50 52 ======================
51 53
52 54 news
53 55 ----
54 56
55 57 - added option to manage repos group for non admin users
56 58 - added following API methods for get_users, create_user, get_users_groups,
57 59 get_users_group, create_users_group, add_user_to_users_groups, get_repos,
58 60 get_repo, create_repo, add_user_to_repo
59 61 - implements #237 added password confirmation for my account
60 62 and admin edit user.
61 63 - implements #291 email notification for global events are now sent to all
62 64 administrator users, and global config email.
63 65
64 66 fixes
65 67 -----
66 68
67 69 - added option for passing auth method for smtp mailer
68 70 - #276 issue with adding a single user with id>10 to usergroups
69 71 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
70 72 - #288 fixes managing of repos in a group for non admin user
71 73
72 74 1.2.2 (**2011-10-17**)
73 75 ======================
74 76
75 77 news
76 78 ----
77 79
78 80 - #226 repo groups are available by path instead of numerical id
79 81
80 82 fixes
81 83 -----
82 84
83 85 - #259 Groups with the same name but with different parent group
84 86 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
85 87 - #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
86 88 - #265 ldap save fails sometimes on converting attributes to booleans,
87 89 added getter and setter into model that will prevent from this on db model level
88 90 - fixed problems with timestamps issues #251 and #213
89 91 - fixes #266 RhodeCode allows to create repo with the same name and in
90 92 the same parent as group
91 93 - fixes #245 Rescan of the repositories on Windows
92 94 - fixes #248 cannot edit repos inside a group on windows
93 95 - fixes #219 forking problems on windows
94 96
95 97 1.2.1 (**2011-10-08**)
96 98 ======================
97 99
98 100 news
99 101 ----
100 102
101 103
102 104 fixes
103 105 -----
104 106
105 107 - fixed problems with basic auth and push problems
106 108 - gui fixes
107 109 - fixed logger
108 110
109 111 1.2.0 (**2011-10-07**)
110 112 ======================
111 113
112 114 news
113 115 ----
114 116
115 117 - implemented #47 repository groups
116 118 - implemented #89 Can setup google analytics code from settings menu
117 119 - implemented #91 added nicer looking archive urls with more download options
118 120 like tags, branches
119 121 - implemented #44 into file browsing, and added follow branch option
120 122 - implemented #84 downloads can be enabled/disabled for each repository
121 123 - anonymous repository can be cloned without having to pass default:default
122 124 into clone url
123 125 - fixed #90 whoosh indexer can index chooses repositories passed in command
124 126 line
125 127 - extended journal with day aggregates and paging
126 128 - implemented #107 source code lines highlight ranges
127 129 - implemented #93 customizable changelog on combined revision ranges -
128 130 equivalent of githubs compare view
129 131 - implemented #108 extended and more powerful LDAP configuration
130 132 - implemented #56 users groups
131 133 - major code rewrites optimized codes for speed and memory usage
132 134 - raw and diff downloads are now in git format
133 135 - setup command checks for write access to given path
134 136 - fixed many issues with international characters and unicode. It uses utf8
135 137 decode with replace to provide less errors even with non utf8 encoded strings
136 138 - #125 added API KEY access to feeds
137 139 - #109 Repository can be created from external Mercurial link (aka. remote
138 140 repository, and manually updated (via pull) from admin panel
139 141 - beta git support - push/pull server + basic view for git repos
140 142 - added followers page and forks page
141 143 - server side file creation (with binary file upload interface)
142 144 and edition with commits powered by codemirror
143 145 - #111 file browser file finder, quick lookup files on whole file tree
144 146 - added quick login sliding menu into main page
145 147 - changelog uses lazy loading of affected files details, in some scenarios
146 148 this can improve speed of changelog page dramatically especially for
147 149 larger repositories.
148 150 - implements #214 added support for downloading subrepos in download menu.
149 151 - Added basic API for direct operations on rhodecode via JSON
150 152 - Implemented advanced hook management
151 153
152 154 fixes
153 155 -----
154 156
155 157 - fixed file browser bug, when switching into given form revision the url was
156 158 not changing
157 159 - fixed propagation to error controller on simplehg and simplegit middlewares
158 160 - fixed error when trying to make a download on empty repository
159 161 - fixed problem with '[' chars in commit messages in journal
160 162 - fixed #99 Unicode errors, on file node paths with non utf-8 characters
161 163 - journal fork fixes
162 164 - removed issue with space inside renamed repository after deletion
163 165 - fixed strange issue on formencode imports
164 166 - fixed #126 Deleting repository on Windows, rename used incompatible chars.
165 167 - #150 fixes for errors on repositories mapped in db but corrupted in
166 168 filesystem
167 169 - fixed problem with ascendant characters in realm #181
168 170 - fixed problem with sqlite file based database connection pool
169 171 - whoosh indexer and code stats share the same dynamic extensions map
170 172 - fixes #188 - relationship delete of repo_to_perm entry on user removal
171 173 - fixes issue #189 Trending source files shows "show more" when no more exist
172 174 - fixes issue #197 Relative paths for pidlocks
173 175 - fixes issue #198 password will require only 3 chars now for login form
174 176 - fixes issue #199 wrong redirection for non admin users after creating a repository
175 177 - fixes issues #202, bad db constraint made impossible to attach same group
176 178 more than one time. Affects only mysql/postgres
177 179 - fixes #218 os.kill patch for windows was missing sig param
178 180 - improved rendering of dag (they are not trimmed anymore when number of
179 181 heads exceeds 5)
180 182
181 183 1.1.8 (**2011-04-12**)
182 184 ======================
183 185
184 186 news
185 187 ----
186 188
187 189 - improved windows support
188 190
189 191 fixes
190 192 -----
191 193
192 194 - fixed #140 freeze of python dateutil library, since new version is python2.x
193 195 incompatible
194 196 - setup-app will check for write permission in given path
195 197 - cleaned up license info issue #149
196 198 - fixes for issues #137,#116 and problems with unicode and accented characters.
197 199 - fixes crashes on gravatar, when passed in email as unicode
198 200 - fixed tooltip flickering problems
199 201 - fixed came_from redirection on windows
200 202 - fixed logging modules, and sql formatters
201 203 - windows fixes for os.kill issue #133
202 204 - fixes path splitting for windows issues #148
203 205 - fixed issue #143 wrong import on migration to 1.1.X
204 206 - fixed problems with displaying binary files, thanks to Thomas Waldmann
205 207 - removed name from archive files since it's breaking ui for long repo names
206 208 - fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
207 209 - fixed compatibility for 1024px displays, and larger dpi settings, thanks to
208 210 Thomas Waldmann
209 211 - fixed issue #166 summary pager was skipping 10 revisions on second page
210 212
211 213
212 214 1.1.7 (**2011-03-23**)
213 215 ======================
214 216
215 217 news
216 218 ----
217 219
218 220 fixes
219 221 -----
220 222
221 223 - fixed (again) #136 installation support for FreeBSD
222 224
223 225
224 226 1.1.6 (**2011-03-21**)
225 227 ======================
226 228
227 229 news
228 230 ----
229 231
230 232 fixes
231 233 -----
232 234
233 235 - fixed #136 installation support for FreeBSD
234 236 - RhodeCode will check for python version during installation
235 237
236 238 1.1.5 (**2011-03-17**)
237 239 ======================
238 240
239 241 news
240 242 ----
241 243
242 244 - basic windows support, by exchanging pybcrypt into sha256 for windows only
243 245 highly inspired by idea of mantis406
244 246
245 247 fixes
246 248 -----
247 249
248 250 - fixed sorting by author in main page
249 251 - fixed crashes with diffs on binary files
250 252 - fixed #131 problem with boolean values for LDAP
251 253 - fixed #122 mysql problems thanks to striker69
252 254 - fixed problem with errors on calling raw/raw_files/annotate functions
253 255 with unknown revisions
254 256 - fixed returned rawfiles attachment names with international character
255 257 - cleaned out docs, big thanks to Jason Harris
256 258
257 259 1.1.4 (**2011-02-19**)
258 260 ======================
259 261
260 262 news
261 263 ----
262 264
263 265 fixes
264 266 -----
265 267
266 268 - fixed formencode import problem on settings page, that caused server crash
267 269 when that page was accessed as first after server start
268 270 - journal fixes
269 271 - fixed option to access repository just by entering http://server/<repo_name>
270 272
271 273 1.1.3 (**2011-02-16**)
272 274 ======================
273 275
274 276 news
275 277 ----
276 278
277 279 - implemented #102 allowing the '.' character in username
278 280 - added option to access repository just by entering http://server/<repo_name>
279 281 - celery task ignores result for better performance
280 282
281 283 fixes
282 284 -----
283 285
284 286 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
285 287 apollo13 and Johan Walles
286 288 - small fixes in journal
287 289 - fixed problems with getting setting for celery from .ini files
288 290 - registration, password reset and login boxes share the same title as main
289 291 application now
290 292 - fixed #113: to high permissions to fork repository
291 293 - fixed problem with '[' chars in commit messages in journal
292 294 - removed issue with space inside renamed repository after deletion
293 295 - db transaction fixes when filesystem repository creation failed
294 296 - fixed #106 relation issues on databases different than sqlite
295 297 - fixed static files paths links to use of url() method
296 298
297 299 1.1.2 (**2011-01-12**)
298 300 ======================
299 301
300 302 news
301 303 ----
302 304
303 305
304 306 fixes
305 307 -----
306 308
307 309 - fixes #98 protection against float division of percentage stats
308 310 - fixed graph bug
309 311 - forced webhelpers version since it was making troubles during installation
310 312
311 313 1.1.1 (**2011-01-06**)
312 314 ======================
313 315
314 316 news
315 317 ----
316 318
317 319 - added force https option into ini files for easier https usage (no need to
318 320 set server headers with this options)
319 321 - small css updates
320 322
321 323 fixes
322 324 -----
323 325
324 326 - fixed #96 redirect loop on files view on repositories without changesets
325 327 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
326 328 and server crashed with errors
327 329 - fixed large tooltips problems on main page
328 330 - fixed #92 whoosh indexer is more error proof
329 331
330 332 1.1.0 (**2010-12-18**)
331 333 ======================
332 334
333 335 news
334 336 ----
335 337
336 338 - rewrite of internals for vcs >=0.1.10
337 339 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
338 340 with older clients
339 341 - anonymous access, authentication via ldap
340 342 - performance upgrade for cached repos list - each repository has its own
341 343 cache that's invalidated when needed.
342 344 - performance upgrades on repositories with large amount of commits (20K+)
343 345 - main page quick filter for filtering repositories
344 346 - user dashboards with ability to follow chosen repositories actions
345 347 - sends email to admin on new user registration
346 348 - added cache/statistics reset options into repository settings
347 349 - more detailed action logger (based on hooks) with pushed changesets lists
348 350 and options to disable those hooks from admin panel
349 351 - introduced new enhanced changelog for merges that shows more accurate results
350 352 - new improved and faster code stats (based on pygments lexers mapping tables,
351 353 showing up to 10 trending sources for each repository. Additionally stats
352 354 can be disabled in repository settings.
353 355 - gui optimizations, fixed application width to 1024px
354 356 - added cut off (for large files/changesets) limit into config files
355 357 - whoosh, celeryd, upgrade moved to paster command
356 358 - other than sqlite database backends can be used
357 359
358 360 fixes
359 361 -----
360 362
361 363 - fixes #61 forked repo was showing only after cache expired
362 364 - fixes #76 no confirmation on user deletes
363 365 - fixes #66 Name field misspelled
364 366 - fixes #72 block user removal when he owns repositories
365 367 - fixes #69 added password confirmation fields
366 368 - fixes #87 RhodeCode crashes occasionally on updating repository owner
367 369 - fixes #82 broken annotations on files with more than 1 blank line at the end
368 370 - a lot of fixes and tweaks for file browser
369 371 - fixed detached session issues
370 372 - fixed when user had no repos he would see all repos listed in my account
371 373 - fixed ui() instance bug when global hgrc settings was loaded for server
372 374 instance and all hgrc options were merged with our db ui() object
373 375 - numerous small bugfixes
374 376
375 377 (special thanks for TkSoh for detailed feedback)
376 378
377 379
378 380 1.0.2 (**2010-11-12**)
379 381 ======================
380 382
381 383 news
382 384 ----
383 385
384 386 - tested under python2.7
385 387 - bumped sqlalchemy and celery versions
386 388
387 389 fixes
388 390 -----
389 391
390 392 - fixed #59 missing graph.js
391 393 - fixed repo_size crash when repository had broken symlinks
392 394 - fixed python2.5 crashes.
393 395
394 396
395 397 1.0.1 (**2010-11-10**)
396 398 ======================
397 399
398 400 news
399 401 ----
400 402
401 403 - small css updated
402 404
403 405 fixes
404 406 -----
405 407
406 408 - fixed #53 python2.5 incompatible enumerate calls
407 409 - fixed #52 disable mercurial extension for web
408 410 - fixed #51 deleting repositories don't delete it's dependent objects
409 411
410 412
411 413 1.0.0 (**2010-11-02**)
412 414 ======================
413 415
414 416 - security bugfix simplehg wasn't checking for permissions on commands
415 417 other than pull or push.
416 418 - fixed doubled messages after push or pull in admin journal
417 419 - templating and css corrections, fixed repo switcher on chrome, updated titles
418 420 - admin menu accessible from options menu on repository view
419 421 - permissions cached queries
420 422
421 423 1.0.0rc4 (**2010-10-12**)
422 424 ==========================
423 425
424 426 - fixed python2.5 missing simplejson imports (thanks to Jens Bäckman)
425 427 - removed cache_manager settings from sqlalchemy meta
426 428 - added sqlalchemy cache settings to ini files
427 429 - validated password length and added second try of failure on paster setup-app
428 430 - fixed setup database destroy prompt even when there was no db
429 431
430 432
431 433 1.0.0rc3 (**2010-10-11**)
432 434 =========================
433 435
434 436 - fixed i18n during installation.
435 437
436 438 1.0.0rc2 (**2010-10-11**)
437 439 =========================
438 440
439 441 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
440 442 occure. After vcs is fixed it'll be put back again.
441 443 - templating/css rewrites, optimized css. No newline at end of file
@@ -1,59 +1,58 b''
1 1 .. _index:
2 2
3 3 .. include:: ./../README.rst
4 4
5 Documentation
6 -------------
5 Users Guide
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/enable_git
24 24 usage/statistics
25 25 usage/backup
26 usage/api_key_access
27 26
28 27 **Develop**
29 28
30 29 .. toctree::
31 30 :maxdepth: 1
32 31
33 32 contributing
34 33 changelog
35 34
36 35 **API**
37 36
38 37 .. toctree::
39 :maxdepth: 2
38 :maxdepth: 1
40 39
41 40 api/index
42 41
43 42
44 43 Other topics
45 44 ------------
46 45
47 46 * :ref:`genindex`
48 47 * :ref:`search`
49 48
50 49 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
51 50 .. _python: http://www.python.org/
52 51 .. _django: http://www.djangoproject.com/
53 52 .. _mercurial: http://mercurial.selenic.com/
54 53 .. _bitbucket: http://bitbucket.org/
55 54 .. _subversion: http://subversion.tigris.org/
56 55 .. _git: http://git-scm.com/
57 56 .. _celery: http://celeryproject.org/
58 57 .. _Sphinx: http://sphinx.pocoo.org/
59 58 .. _vcs: http://pypi.python.org/pypi/vcs No newline at end of file
@@ -1,694 +1,712 b''
1 1 .. _setup:
2 2
3 3 Setup
4 4 =====
5 5
6 6
7 7 Setting up RhodeCode
8 8 --------------------
9 9
10 10 First, you will need to create a RhodeCode configuration file. Run the
11 11 following command to do this::
12 12
13 13 paster make-config RhodeCode production.ini
14 14
15 15 - This will create the file `production.ini` in the current directory. This
16 16 configuration file contains the various settings for RhodeCode, e.g proxy
17 17 port, email settings, usage of static files, cache, celery settings and
18 18 logging.
19 19
20 20
21 21 Next, you need to create the databases used by RhodeCode. I recommend that you
22 22 use sqlite (default) or postgresql. If you choose a database other than the
23 23 default ensure you properly adjust the db url in your production.ini
24 24 configuration file to use this other database. Create the databases by running
25 25 the following command::
26 26
27 27 paster setup-app production.ini
28 28
29 29 This will prompt you for a "root" path. This "root" path is the location where
30 30 RhodeCode will store all of its repositories on the current machine. After
31 31 entering this "root" path ``setup-app`` will also prompt you for a username
32 32 and password for the initial admin account which ``setup-app`` sets up for you.
33 33
34 34 - The ``setup-app`` command will create all of the needed tables and an admin
35 35 account. When choosing a root path you can either use a new empty location,
36 36 or a location which already contains existing repositories. If you choose a
37 37 location which contains existing repositories RhodeCode will simply add all
38 38 of the repositories at the chosen location to it's database. (Note: make
39 39 sure you specify the correct path to the root).
40 40 - Note: the given path for mercurial_ repositories **must** be write accessible
41 41 for the application. It's very important since the RhodeCode web interface
42 42 will work without write access, but when trying to do a push it will
43 43 eventually fail with permission denied errors unless it has write access.
44 44
45 45 You are now ready to use RhodeCode, to run it simply execute::
46 46
47 47 paster serve production.ini
48 48
49 49 - This command runs the RhodeCode server. The web app should be available at the
50 50 127.0.0.1:5000. This ip and port is configurable via the production.ini
51 51 file created in previous step
52 52 - Use the admin account you created above when running ``setup-app`` to login
53 53 to the web app.
54 54 - The default permissions on each repository is read, and the owner is admin.
55 55 Remember to update these if needed.
56 56 - In the admin panel you can toggle ldap, anonymous, permissions settings. As
57 57 well as edit more advanced options on users and repositories
58 58
59 59 Try copying your own mercurial repository into the "root" directory you are
60 60 using, then from within the RhodeCode web application choose Admin >
61 61 repositories. Then choose Add New Repository. Add the repository you copied
62 62 into the root. Test that you can browse your repository from within RhodeCode
63 63 and then try cloning your repository from RhodeCode with::
64 64
65 65 hg clone http://127.0.0.1:5000/<repository name>
66 66
67 67 where *repository name* is replaced by the name of your repository.
68 68
69 69 Using RhodeCode with SSH
70 70 ------------------------
71 71
72 72 RhodeCode currently only hosts repositories using http and https. (The addition
73 73 of ssh hosting is a planned future feature.) However you can easily use ssh in
74 74 parallel with RhodeCode. (Repository access via ssh is a standard "out of
75 75 the box" feature of mercurial_ and you can use this to access any of the
76 76 repositories that RhodeCode is hosting. See PublishingRepositories_)
77 77
78 78 RhodeCode repository structures are kept in directories with the same name
79 79 as the project. When using repository groups, each group is a subdirectory.
80 80 This allows you to easily use ssh for accessing repositories.
81 81
82 82 In order to use ssh you need to make sure that your web-server and the users
83 83 login accounts have the correct permissions set on the appropriate directories.
84 84 (Note that these permissions are independent of any permissions you have set up
85 85 using the RhodeCode web interface.)
86 86
87 87 If your main directory (the same as set in RhodeCode settings) is for example
88 88 set to **/home/hg** and the repository you are using is named `rhodecode`, then
89 89 to clone via ssh you should run::
90 90
91 91 hg clone ssh://user@server.com/home/hg/rhodecode
92 92
93 93 Using other external tools such as mercurial-server_ or using ssh key based
94 94 authentication is fully supported.
95 95
96 96 Note: In an advanced setup, in order for your ssh access to use the same
97 97 permissions as set up via the RhodeCode web interface, you can create an
98 98 authentication hook to connect to the rhodecode db and runs check functions for
99 99 permissions against that.
100 100
101 101 Setting up Whoosh full text search
102 102 ----------------------------------
103 103
104 104 Starting from version 1.1 the whoosh index can be build by using the paster
105 105 command ``make-index``. To use ``make-index`` you must specify the configuration
106 106 file that stores the location of the index. You may specify the location of the
107 107 repositories (`--repo-location`). If not specified, this value is retrieved
108 108 from the RhodeCode database. This was required prior to 1.2. Starting from
109 109 version 1.2 it is also possible to specify a comma separated list of
110 110 repositories (`--index-only`) to build index only on chooses repositories
111 111 skipping any other found in repos location
112 112
113 113 You may optionally pass the option `-f` to enable a full index rebuild. Without
114 114 the `-f` option, indexing will run always in "incremental" mode.
115 115
116 116 For an incremental index build use::
117 117
118 118 paster make-index production.ini
119 119
120 120 For a full index rebuild use::
121 121
122 122 paster make-index production.ini -f
123 123
124 124
125 125 building index just for chosen repositories is possible with such command::
126 126
127 127 paster make-index production.ini --index-only=vcs,rhodecode
128 128
129 129
130 130 In order to do periodical index builds and keep your index always up to date.
131 131 It's recommended to do a crontab entry for incremental indexing.
132 132 An example entry might look like this::
133 133
134 134 /path/to/python/bin/paster make-index /path/to/rhodecode/production.ini
135 135
136 136 When using incremental mode (the default) whoosh will check the last
137 137 modification date of each file and add it to be reindexed if a newer file is
138 138 available. The indexing daemon checks for any removed files and removes them
139 139 from index.
140 140
141 141 If you want to rebuild index from scratch, you can use the `-f` flag as above,
142 142 or in the admin panel you can check `build from scratch` flag.
143 143
144 144
145 145 Setting up LDAP support
146 146 -----------------------
147 147
148 148 RhodeCode starting from version 1.1 supports ldap authentication. In order
149 149 to use LDAP, you have to install the python-ldap_ package. This package is
150 150 available via pypi, so you can install it by running
151 151
152 152 using easy_install::
153 153
154 154 easy_install python-ldap
155 155
156 156 using pip::
157 157
158 158 pip install python-ldap
159 159
160 160 .. note::
161 161 python-ldap requires some certain libs on your system, so before installing
162 162 it check that you have at least `openldap`, and `sasl` libraries.
163 163
164 164 LDAP settings are located in admin->ldap section,
165 165
166 166 Here's a typical ldap setup::
167 167
168 168 Connection settings
169 169 Enable LDAP = checked
170 170 Host = host.example.org
171 171 Port = 389
172 172 Account = <account>
173 173 Password = <password>
174 174 Connection Security = LDAPS connection
175 175 Certificate Checks = DEMAND
176 176
177 177 Search settings
178 178 Base DN = CN=users,DC=host,DC=example,DC=org
179 179 LDAP Filter = (&(objectClass=user)(!(objectClass=computer)))
180 180 LDAP Search Scope = SUBTREE
181 181
182 182 Attribute mappings
183 183 Login Attribute = uid
184 184 First Name Attribute = firstName
185 185 Last Name Attribute = lastName
186 186 E-mail Attribute = mail
187 187
188 188 .. _enable_ldap:
189 189
190 190 Enable LDAP : required
191 191 Whether to use LDAP for authenticating users.
192 192
193 193 .. _ldap_host:
194 194
195 195 Host : required
196 196 LDAP server hostname or IP address.
197 197
198 198 .. _Port:
199 199
200 200 Port : required
201 201 389 for un-encrypted LDAP, 636 for SSL-encrypted LDAP.
202 202
203 203 .. _ldap_account:
204 204
205 205 Account : optional
206 206 Only required if the LDAP server does not allow anonymous browsing of
207 207 records. This should be a special account for record browsing. This
208 208 will require `LDAP Password`_ below.
209 209
210 210 .. _LDAP Password:
211 211
212 212 Password : optional
213 213 Only required if the LDAP server does not allow anonymous browsing of
214 214 records.
215 215
216 216 .. _Enable LDAPS:
217 217
218 218 Connection Security : required
219 219 Defines the connection to LDAP server
220 220
221 221 No encryption
222 222 Plain non encrypted connection
223 223
224 224 LDAPS connection
225 225 Enable ldaps connection. It will likely require `Port`_ to be set to
226 226 a different value (standard LDAPS port is 636). When LDAPS is enabled
227 227 then `Certificate Checks`_ is required.
228 228
229 229 START_TLS on LDAP connection
230 230 START TLS connection
231 231
232 232 .. _Certificate Checks:
233 233
234 234 Certificate Checks : optional
235 235 How SSL certificates verification is handled - this is only useful when
236 236 `Enable LDAPS`_ is enabled. Only DEMAND or HARD offer full SSL security
237 237 while the other options are susceptible to man-in-the-middle attacks. SSL
238 238 certificates can be installed to /etc/openldap/cacerts so that the
239 239 DEMAND or HARD options can be used with self-signed certificates or
240 240 certificates that do not have traceable certificates of authority.
241 241
242 242 NEVER
243 243 A serve certificate will never be requested or checked.
244 244
245 245 ALLOW
246 246 A server certificate is requested. Failure to provide a
247 247 certificate or providing a bad certificate will not terminate the
248 248 session.
249 249
250 250 TRY
251 251 A server certificate is requested. Failure to provide a
252 252 certificate does not halt the session; providing a bad certificate
253 253 halts the session.
254 254
255 255 DEMAND
256 256 A server certificate is requested and must be provided and
257 257 authenticated for the session to proceed.
258 258
259 259 HARD
260 260 The same as DEMAND.
261 261
262 262 .. _Base DN:
263 263
264 264 Base DN : required
265 265 The Distinguished Name (DN) where searches for users will be performed.
266 266 Searches can be controlled by `LDAP Filter`_ and `LDAP Search Scope`_.
267 267
268 268 .. _LDAP Filter:
269 269
270 270 LDAP Filter : optional
271 271 A LDAP filter defined by RFC 2254. This is more useful when `LDAP
272 272 Search Scope`_ is set to SUBTREE. The filter is useful for limiting
273 273 which LDAP objects are identified as representing Users for
274 274 authentication. The filter is augmented by `Login Attribute`_ below.
275 275 This can commonly be left blank.
276 276
277 277 .. _LDAP Search Scope:
278 278
279 279 LDAP Search Scope : required
280 280 This limits how far LDAP will search for a matching object.
281 281
282 282 BASE
283 283 Only allows searching of `Base DN`_ and is usually not what you
284 284 want.
285 285
286 286 ONELEVEL
287 287 Searches all entries under `Base DN`_, but not Base DN itself.
288 288
289 289 SUBTREE
290 290 Searches all entries below `Base DN`_, but not Base DN itself.
291 291 When using SUBTREE `LDAP Filter`_ is useful to limit object
292 292 location.
293 293
294 294 .. _Login Attribute:
295 295
296 296 Login Attribute : required
297 297 The LDAP record attribute that will be matched as the USERNAME or
298 298 ACCOUNT used to connect to RhodeCode. This will be added to `LDAP
299 299 Filter`_ for locating the User object. If `LDAP Filter`_ is specified as
300 300 "LDAPFILTER", `Login Attribute`_ is specified as "uid" and the user has
301 301 connected as "jsmith" then the `LDAP Filter`_ will be augmented as below
302 302 ::
303 303
304 304 (&(LDAPFILTER)(uid=jsmith))
305 305
306 306 .. _ldap_attr_firstname:
307 307
308 308 First Name Attribute : required
309 309 The LDAP record attribute which represents the user's first name.
310 310
311 311 .. _ldap_attr_lastname:
312 312
313 313 Last Name Attribute : required
314 314 The LDAP record attribute which represents the user's last name.
315 315
316 316 .. _ldap_attr_email:
317 317
318 318 Email Attribute : required
319 319 The LDAP record attribute which represents the user's email address.
320 320
321 321 If all data are entered correctly, and python-ldap_ is properly installed
322 322 users should be granted access to RhodeCode with ldap accounts. At this
323 323 time user information is copied from LDAP into the RhodeCode user database.
324 324 This means that updates of an LDAP user object may not be reflected as a
325 325 user update in RhodeCode.
326 326
327 327 If You have problems with LDAP access and believe You entered correct
328 328 information check out the RhodeCode logs, any error messages sent from LDAP
329 329 will be saved there.
330 330
331 331 Active Directory
332 332 ''''''''''''''''
333 333
334 334 RhodeCode can use Microsoft Active Directory for user authentication. This
335 335 is done through an LDAP or LDAPS connection to Active Directory. The
336 336 following LDAP configuration settings are typical for using Active
337 337 Directory ::
338 338
339 339 Base DN = OU=SBSUsers,OU=Users,OU=MyBusiness,DC=v3sys,DC=local
340 340 Login Attribute = sAMAccountName
341 341 First Name Attribute = givenName
342 342 Last Name Attribute = sn
343 343 E-mail Attribute = mail
344 344
345 345 All other LDAP settings will likely be site-specific and should be
346 346 appropriately configured.
347 347
348 348
349 349
350 350 Authentication by container or reverse-proxy
351 351 --------------------------------------------
352 352
353 353 Starting with version 1.3, RhodeCode supports delegating the authentication
354 354 of users to its WSGI container, or to a reverse-proxy server through which all
355 355 clients access the application.
356 356
357 357 When these authentication methods are enabled in RhodeCode, it uses the
358 358 username that the container/proxy (Apache/Nginx/etc) authenticated and doesn't
359 359 perform the authentication itself. The authorization, however, is still done by
360 360 RhodeCode according to its settings.
361 361
362 362 When a user logs in for the first time using these authentication methods,
363 363 a matching user account is created in RhodeCode with default permissions. An
364 364 administrator can then modify it using RhodeCode's admin interface.
365 365 It's also possible for an administrator to create accounts and configure their
366 366 permissions before the user logs in for the first time.
367 367
368 368 Container-based authentication
369 369 ''''''''''''''''''''''''''''''
370 370
371 371 In a container-based authentication setup, RhodeCode reads the user name from
372 372 the ``REMOTE_USER`` server variable provided by the WSGI container.
373 373
374 374 After setting up your container (see `Apache's WSGI config`_), you'd need
375 375 to configure it to require authentication on the location configured for
376 376 RhodeCode.
377 377
378 378 In order for RhodeCode to start using the provided username, you should set the
379 379 following in the [app:main] section of your .ini file::
380 380
381 381 container_auth_enabled = true
382 382
383 383
384 384 Proxy pass-through authentication
385 385 '''''''''''''''''''''''''''''''''
386 386
387 387 In a proxy pass-through authentication setup, RhodeCode reads the user name
388 388 from the ``X-Forwarded-User`` request header, which should be configured to be
389 389 sent by the reverse-proxy server.
390 390
391 391 After setting up your proxy solution (see `Apache virtual host reverse proxy example`_,
392 392 `Apache as subdirectory`_ or `Nginx virtual host example`_), you'd need to
393 393 configure the authentication and add the username in a request header named
394 394 ``X-Forwarded-User``.
395 395
396 396 For example, the following config section for Apache sets a subdirectory in a
397 397 reverse-proxy setup with basic auth::
398 398
399 399 <Location /<someprefix> >
400 400 ProxyPass http://127.0.0.1:5000/<someprefix>
401 401 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
402 402 SetEnvIf X-Url-Scheme https HTTPS=1
403 403
404 404 AuthType Basic
405 405 AuthName "RhodeCode authentication"
406 406 AuthUserFile /home/web/rhodecode/.htpasswd
407 407 require valid-user
408 408
409 409 RequestHeader unset X-Forwarded-User
410 410
411 411 RewriteEngine On
412 412 RewriteCond %{LA-U:REMOTE_USER} (.+)
413 413 RewriteRule .* - [E=RU:%1]
414 414 RequestHeader set X-Forwarded-User %{RU}e
415 415 </Location>
416 416
417 417 In order for RhodeCode to start using the forwarded username, you should set
418 418 the following in the [app:main] section of your .ini file::
419 419
420 420 proxypass_auth_enabled = true
421 421
422 422 .. note::
423 423 If you enable proxy pass-through authentication, make sure your server is
424 424 only accessible through the proxy. Otherwise, any client would be able to
425 425 forge the authentication header and could effectively become authenticated
426 426 using any account of their liking.
427 427
428 Integration with Issue trackers
429 -------------------------------
428 430
431 RhodeCode provides a simple integration with issue trackers. It's possible
432 to define a regular expression that will fetch issue id stored in commit
433 messages and replace that with an url to this issue. To enable this simply
434 uncomment following variables in the ini file::
435
436 url_pat = (?:^#|\s#)(\w+)
437 issue_server = https://myissueserver.com/issue/{id}
438 issue_prefix = #
439
440 `url_pat` is the regular expression that will match issues, default given regex
441 will match issues in format of #<number> eg. #300.
442 Matched issues will be replace with the `issue_server` url replacing {id} with
443 id fetched from regex. Since the # is striped `issue_prefix` is added as a
444 prefix to url. `issue_prefix` can be something different than # if you pass
445 ISSUE- as issue prefix this will generate an url in format
446 `<a href="https://myissueserver.com/issue/300">ISSUE-300</a>`
429 447
430 448 Hook management
431 449 ---------------
432 450
433 451 Hooks can be managed in similar way to this used in .hgrc files.
434 452 To access hooks setting click `advanced setup` on Hooks section of Mercurial
435 453 Settings in Admin.
436 454
437 455 There are 4 built in hooks that cannot be changed (only enable/disable by
438 456 checkboxes on previos section).
439 457 To add another custom hook simply fill in first section with
440 458 <name>.<hook_type> and the second one with hook path. Example hooks
441 459 can be found at *rhodecode.lib.hooks*.
442 460
443 461
444 462 Setting Up Celery
445 463 -----------------
446 464
447 465 Since version 1.1 celery is configured by the rhodecode ini configuration files.
448 466 Simply set use_celery=true in the ini file then add / change the configuration
449 467 variables inside the ini file.
450 468
451 469 Remember that the ini files use the format with '.' not with '_' like celery.
452 470 So for example setting `BROKER_HOST` in celery means setting `broker.host` in
453 471 the config file.
454 472
455 473 In order to start using celery run::
456 474
457 475 paster celeryd <configfile.ini>
458 476
459 477
460 478 .. note::
461 479 Make sure you run this command from the same virtualenv, and with the same
462 480 user that rhodecode runs.
463 481
464 482 HTTPS support
465 483 -------------
466 484
467 485 There are two ways to enable https:
468 486
469 487 - Set HTTP_X_URL_SCHEME in your http server headers, than rhodecode will
470 488 recognize this headers and make proper https redirections
471 489 - Alternatively, change the `force_https = true` flag in the ini configuration
472 490 to force using https, no headers are needed than to enable https
473 491
474 492
475 493 Nginx virtual host example
476 494 --------------------------
477 495
478 496 Sample config for nginx using proxy::
479 497
480 498 upstream rc {
481 499 server 127.0.0.1:5000;
482 500 # add more instances for load balancing
483 501 #server 127.0.0.1:5001;
484 502 #server 127.0.0.1:5002;
485 503 }
486 504
487 505 server {
488 506 listen 80;
489 507 server_name hg.myserver.com;
490 508 access_log /var/log/nginx/rhodecode.access.log;
491 509 error_log /var/log/nginx/rhodecode.error.log;
492 510
493 511 location / {
494 512 try_files $uri @rhode;
495 513 }
496 514
497 515 location @rhode {
498 516 proxy_pass http://rc;
499 517 include /etc/nginx/proxy.conf;
500 518 }
501 519
502 520 }
503 521
504 522 Here's the proxy.conf. It's tuned so it will not timeout on long
505 523 pushes or large pushes::
506 524
507 525 proxy_redirect off;
508 526 proxy_set_header Host $host;
509 527 proxy_set_header X-Url-Scheme $scheme;
510 528 proxy_set_header X-Host $http_host;
511 529 proxy_set_header X-Real-IP $remote_addr;
512 530 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
513 531 proxy_set_header Proxy-host $proxy_host;
514 532 client_max_body_size 400m;
515 533 client_body_buffer_size 128k;
516 534 proxy_buffering off;
517 535 proxy_connect_timeout 7200;
518 536 proxy_send_timeout 7200;
519 537 proxy_read_timeout 7200;
520 538 proxy_buffers 8 32k;
521 539
522 540 Also, when using root path with nginx you might set the static files to false
523 541 in the production.ini file::
524 542
525 543 [app:main]
526 544 use = egg:rhodecode
527 545 full_stack = true
528 546 static_files = false
529 547 lang=en
530 548 cache_dir = %(here)s/data
531 549
532 550 In order to not have the statics served by the application. This improves speed.
533 551
534 552
535 553 Apache virtual host reverse proxy example
536 554 -----------------------------------------
537 555
538 556 Here is a sample configuration file for apache using proxy::
539 557
540 558 <VirtualHost *:80>
541 559 ServerName hg.myserver.com
542 560 ServerAlias hg.myserver.com
543 561
544 562 <Proxy *>
545 563 Order allow,deny
546 564 Allow from all
547 565 </Proxy>
548 566
549 567 #important !
550 568 #Directive to properly generate url (clone url) for pylons
551 569 ProxyPreserveHost On
552 570
553 571 #rhodecode instance
554 572 ProxyPass / http://127.0.0.1:5000/
555 573 ProxyPassReverse / http://127.0.0.1:5000/
556 574
557 575 #to enable https use line below
558 576 #SetEnvIf X-Url-Scheme https HTTPS=1
559 577
560 578 </VirtualHost>
561 579
562 580
563 581 Additional tutorial
564 582 http://wiki.pylonshq.com/display/pylonscookbook/Apache+as+a+reverse+proxy+for+Pylons
565 583
566 584
567 585 Apache as subdirectory
568 586 ----------------------
569 587
570 588 Apache subdirectory part::
571 589
572 590 <Location /<someprefix> >
573 591 ProxyPass http://127.0.0.1:5000/<someprefix>
574 592 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
575 593 SetEnvIf X-Url-Scheme https HTTPS=1
576 594 </Location>
577 595
578 596 Besides the regular apache setup you will need to add the following line
579 597 into [app:main] section of your .ini file::
580 598
581 599 filter-with = proxy-prefix
582 600
583 601 Add the following at the end of the .ini file::
584 602
585 603 [filter:proxy-prefix]
586 604 use = egg:PasteDeploy#prefix
587 605 prefix = /<someprefix>
588 606
589 607
590 608 then change <someprefix> into your choosen prefix
591 609
592 610 Apache's WSGI config
593 611 --------------------
594 612
595 613 Alternatively, RhodeCode can be set up with Apache under mod_wsgi. For
596 614 that, you'll need to:
597 615
598 616 - Install mod_wsgi. If using a Debian-based distro, you can install
599 617 the package libapache2-mod-wsgi::
600 618
601 619 aptitude install libapache2-mod-wsgi
602 620
603 621 - Enable mod_wsgi::
604 622
605 623 a2enmod wsgi
606 624
607 625 - Create a wsgi dispatch script, like the one below. Make sure you
608 626 check the paths correctly point to where you installed RhodeCode
609 627 and its Python Virtual Environment.
610 628 - Enable the WSGIScriptAlias directive for the wsgi dispatch script,
611 629 as in the following example. Once again, check the paths are
612 630 correctly specified.
613 631
614 632 Here is a sample excerpt from an Apache Virtual Host configuration file::
615 633
616 634 WSGIDaemonProcess pylons user=www-data group=www-data processes=1 \
617 635 threads=4 \
618 636 python-path=/home/web/rhodecode/pyenv/lib/python2.6/site-packages
619 637 WSGIScriptAlias / /home/web/rhodecode/dispatch.wsgi
620 638
621 639 Example wsgi dispatch script::
622 640
623 641 import os
624 642 os.environ["HGENCODING"] = "UTF-8"
625 643 os.environ['PYTHON_EGG_CACHE'] = '/home/web/rhodecode/.egg-cache'
626 644
627 645 # sometimes it's needed to set the curent dir
628 646 os.chdir('/home/web/rhodecode/')
629 647
630 648 import site
631 649 site.addsitedir("/home/web/rhodecode/pyenv/lib/python2.6/site-packages")
632 650
633 651 from paste.deploy import loadapp
634 652 from paste.script.util.logging_config import fileConfig
635 653
636 654 fileConfig('/home/web/rhodecode/production.ini')
637 655 application = loadapp('config:/home/web/rhodecode/production.ini')
638 656
639 657 Note: when using mod_wsgi you'll need to install the same version of
640 658 Mercurial that's inside RhodeCode's virtualenv also on the system's Python
641 659 environment.
642 660
643 661
644 662 Other configuration files
645 663 -------------------------
646 664
647 665 Some example init.d scripts can be found here, for debian and gentoo:
648 666
649 667 https://rhodecode.org/rhodecode/files/tip/init.d
650 668
651 669
652 670 Troubleshooting
653 671 ---------------
654 672
655 673 :Q: **Missing static files?**
656 674 :A: Make sure either to set the `static_files = true` in the .ini file or
657 675 double check the root path for your http setup. It should point to
658 676 for example:
659 677 /home/my-virtual-python/lib/python2.6/site-packages/rhodecode/public
660 678
661 679 |
662 680
663 681 :Q: **Can't install celery/rabbitmq**
664 682 :A: Don't worry RhodeCode works without them too. No extra setup is required.
665 683
666 684 |
667 685
668 686 :Q: **Long lasting push timeouts?**
669 687 :A: Make sure you set a longer timeouts in your proxy/fcgi settings, timeouts
670 688 are caused by https server and not RhodeCode.
671 689
672 690 |
673 691
674 692 :Q: **Large pushes timeouts?**
675 693 :A: Make sure you set a proper max_body_size for the http server.
676 694
677 695 |
678 696
679 697 :Q: **Apache doesn't pass basicAuth on pull/push?**
680 698 :A: Make sure you added `WSGIPassAuthorization true`.
681 699
682 700 For further questions search the `Issues tracker`_, or post a message in the
683 701 `google group rhodecode`_
684 702
685 703 .. _virtualenv: http://pypi.python.org/pypi/virtualenv
686 704 .. _python: http://www.python.org/
687 705 .. _mercurial: http://mercurial.selenic.com/
688 706 .. _celery: http://celeryproject.org/
689 707 .. _rabbitmq: http://www.rabbitmq.com/
690 708 .. _python-ldap: http://www.python-ldap.org/
691 709 .. _mercurial-server: http://www.lshift.net/mercurial-server.html
692 710 .. _PublishingRepositories: http://mercurial.selenic.com/wiki/PublishingRepositories
693 711 .. _Issues tracker: https://bitbucket.org/marcinkuzminski/rhodecode/issues
694 712 .. _google group rhodecode: http://groups.google.com/group/rhodecode
@@ -1,267 +1,288 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 33 threadpool_workers = 5
34 34
35 35 ##max request before thread respawn
36 36 threadpool_max_requests = 10
37 37
38 38 ##option to use threads of process
39 39 use_threadpool = true
40 40
41 41 use = egg:Paste#http
42 42 host = 127.0.0.1
43 43 port = 8001
44 44
45 45 [app:main]
46 46 use = egg:rhodecode
47 47 full_stack = true
48 48 static_files = true
49 49 lang=en
50 50 cache_dir = %(here)s/data
51 51 index_dir = %(here)s/data/index
52 52 app_instance_uuid = prod1234
53 53 cut_off_limit = 256000
54 54 force_https = false
55 55 commit_parse_limit = 50
56 56 use_gravatar = true
57 57 container_auth_enabled = false
58 58 proxypass_auth_enabled = false
59 59
60 60 ## overwrite schema of clone url
61 # available vars:
62 # scheme - http/https
63 # user - current user
64 # pass - password
65 # netloc - network location
66 # path - usually repo_name
67 # clone_uri = {scheme}://{user}{pass}{netloc}{path}
61 ## available vars:
62 ## scheme - http/https
63 ## user - current user
64 ## pass - password
65 ## netloc - network location
66 ## path - usually repo_name
67
68 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
69
70 ## issue tracking mapping for commits messages
71 ## uncomment url_pat, issue_server, issue_prefix to enable
72
73
74 ## pattern to get the issues from commit messages
75 ## default one used here is #1234
76
77 #url_pat = (?:^#|\s#)(\w+)
78
79 ## server url to the issue, each {id} will be replaced with id
80 ## fetched from the regex
81
82 #issue_server = https://myissueserver.com/issue/{id}
83
84 ## prefix to add to link to indicate it's an url
85 ## #314 will be replaced by <issue_prefix><id>
86
87 #issue_prefix = #
88
68 89
69 90 ####################################
70 91 ### CELERY CONFIG ####
71 92 ####################################
72 93 use_celery = false
73 94 broker.host = localhost
74 95 broker.vhost = rabbitmqhost
75 96 broker.port = 5672
76 97 broker.user = rabbitmq
77 98 broker.password = qweqwe
78 99
79 100 celery.imports = rhodecode.lib.celerylib.tasks
80 101
81 102 celery.result.backend = amqp
82 103 celery.result.dburi = amqp://
83 104 celery.result.serialier = json
84 105
85 106 #celery.send.task.error.emails = true
86 107 #celery.amqp.task.result.expires = 18000
87 108
88 109 celeryd.concurrency = 2
89 110 #celeryd.log.file = celeryd.log
90 111 celeryd.log.level = debug
91 112 celeryd.max.tasks.per.child = 1
92 113
93 114 #tasks will never be sent to the queue, but executed locally instead.
94 115 celery.always.eager = false
95 116
96 117 ####################################
97 118 ### BEAKER CACHE ####
98 119 ####################################
99 120 beaker.cache.data_dir=%(here)s/data/cache/data
100 121 beaker.cache.lock_dir=%(here)s/data/cache/lock
101 122
102 123 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
103 124
104 125 beaker.cache.super_short_term.type=memory
105 126 beaker.cache.super_short_term.expire=10
106 127 beaker.cache.super_short_term.key_length = 256
107 128
108 129 beaker.cache.short_term.type=memory
109 130 beaker.cache.short_term.expire=60
110 131 beaker.cache.short_term.key_length = 256
111 132
112 133 beaker.cache.long_term.type=memory
113 134 beaker.cache.long_term.expire=36000
114 135 beaker.cache.long_term.key_length = 256
115 136
116 137 beaker.cache.sql_cache_short.type=memory
117 138 beaker.cache.sql_cache_short.expire=10
118 139 beaker.cache.sql_cache_short.key_length = 256
119 140
120 141 beaker.cache.sql_cache_med.type=memory
121 142 beaker.cache.sql_cache_med.expire=360
122 143 beaker.cache.sql_cache_med.key_length = 256
123 144
124 145 beaker.cache.sql_cache_long.type=file
125 146 beaker.cache.sql_cache_long.expire=3600
126 147 beaker.cache.sql_cache_long.key_length = 256
127 148
128 149 ####################################
129 150 ### BEAKER SESSION ####
130 151 ####################################
131 152 ## Type of storage used for the session, current types are
132 153 ## dbm, file, memcached, database, and memory.
133 154 ## The storage uses the Container API
134 155 ## that is also used by the cache system.
135 156
136 157 ## db session example
137 158
138 159 #beaker.session.type = ext:database
139 160 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
140 161 #beaker.session.table_name = db_session
141 162
142 163 ## encrypted cookie session, good for many instances
143 164 #beaker.session.type = cookie
144 165
145 166 beaker.session.type = file
146 167 beaker.session.key = rhodecode
147 168 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
148 169 #beaker.session.validate_key = 9712sds2212c--zxc123
149 170 beaker.session.timeout = 36000
150 171 beaker.session.httponly = true
151 172
152 173 ## uncomment for https secure cookie
153 174 beaker.session.secure = false
154 175
155 176 ##auto save the session to not to use .save()
156 177 beaker.session.auto = False
157 178
158 179 ##true exire at browser close
159 180 #beaker.session.cookie_expires = 3600
160 181
161 182
162 183 ################################################################################
163 184 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
164 185 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
165 186 ## execute malicious code after an exception is raised. ##
166 187 ################################################################################
167 188 set debug = false
168 189
169 190 ##################################
170 191 ### LOGVIEW CONFIG ###
171 192 ##################################
172 193 logview.sqlalchemy = #faa
173 194 logview.pylons.templating = #bfb
174 195 logview.pylons.util = #eee
175 196
176 197 #########################################################
177 198 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
178 199 #########################################################
179 200 #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
180 201 sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode
181 202 sqlalchemy.db1.echo = false
182 203 sqlalchemy.db1.pool_recycle = 3600
183 204 sqlalchemy.convert_unicode = true
184 205
185 206 ################################
186 207 ### LOGGING CONFIGURATION ####
187 208 ################################
188 209 [loggers]
189 210 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
190 211
191 212 [handlers]
192 213 keys = console, console_sql
193 214
194 215 [formatters]
195 216 keys = generic, color_formatter, color_formatter_sql
196 217
197 218 #############
198 219 ## LOGGERS ##
199 220 #############
200 221 [logger_root]
201 222 level = NOTSET
202 223 handlers = console
203 224
204 225 [logger_routes]
205 226 level = DEBUG
206 227 handlers =
207 228 qualname = routes.middleware
208 229 # "level = DEBUG" logs the route matched and routing variables.
209 230 propagate = 1
210 231
211 232 [logger_beaker]
212 233 level = DEBUG
213 234 handlers =
214 235 qualname = beaker.container
215 236 propagate = 1
216 237
217 238 [logger_templates]
218 239 level = INFO
219 240 handlers =
220 241 qualname = pylons.templating
221 242 propagate = 1
222 243
223 244 [logger_rhodecode]
224 245 level = DEBUG
225 246 handlers =
226 247 qualname = rhodecode
227 248 propagate = 1
228 249
229 250 [logger_sqlalchemy]
230 251 level = INFO
231 252 handlers = console_sql
232 253 qualname = sqlalchemy.engine
233 254 propagate = 0
234 255
235 256 ##############
236 257 ## HANDLERS ##
237 258 ##############
238 259
239 260 [handler_console]
240 261 class = StreamHandler
241 262 args = (sys.stderr,)
242 263 level = INFO
243 264 formatter = generic
244 265
245 266 [handler_console_sql]
246 267 class = StreamHandler
247 268 args = (sys.stderr,)
248 269 level = WARN
249 270 formatter = generic
250 271
251 272 ################
252 273 ## FORMATTERS ##
253 274 ################
254 275
255 276 [formatter_generic]
256 277 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
257 278 datefmt = %Y-%m-%d %H:%M:%S
258 279
259 280 [formatter_color_formatter]
260 281 class=rhodecode.lib.colored_formatter.ColorFormatter
261 282 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
262 283 datefmt = %Y-%m-%d %H:%M:%S
263 284
264 285 [formatter_color_formatter_sql]
265 286 class=rhodecode.lib.colored_formatter.ColorFormatterSql
266 287 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
267 288 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,277 +1,298 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 33 threadpool_workers = 5
34 34
35 35 ##max request before thread respawn
36 36 threadpool_max_requests = 10
37 37
38 38 ##option to use threads of process
39 39 use_threadpool = true
40 40
41 41 use = egg:Paste#http
42 42 host = 127.0.0.1
43 43 port = 5000
44 44
45 45 [app:main]
46 46 use = egg:rhodecode
47 47 full_stack = true
48 48 static_files = true
49 49 lang=en
50 50 cache_dir = %(here)s/data
51 51 index_dir = %(here)s/data/index
52 52 app_instance_uuid = ${app_instance_uuid}
53 53 cut_off_limit = 256000
54 54 force_https = false
55 55 commit_parse_limit = 50
56 56 use_gravatar = true
57 57 container_auth_enabled = false
58 58 proxypass_auth_enabled = false
59 59
60 60 ## overwrite schema of clone url
61 # available vars:
62 # scheme - http/https
63 # user - current user
64 # pass - password
65 # netloc - network location
66 # path - usually repo_name
61 ## available vars:
62 ## scheme - http/https
63 ## user - current user
64 ## pass - password
65 ## netloc - network location
66 ## path - usually repo_name
67
67 68 # clone_uri = {scheme}://{user}{pass}{netloc}{path}
68 69
70 ## issue tracking mapping for commits messages
71 ## uncomment url_pat, issue_server, issue_prefix to enable
72
73
74 ## pattern to get the issues from commit messages
75 ## default one used here is #1234
76
77 #url_pat = (?:^#|\s#)(\w+)
78
79 ## server url to the issue, each {id} will be replaced with id
80 ## fetched from the regex
81
82 #issue_server = https://myissueserver.com/issue/{id}
83
84 ## prefix to add to link to indicate it's an url
85 ## #314 will be replaced by <issue_prefix><id>
86
87 #issue_prefix = #
88
89
69 90 ####################################
70 91 ### CELERY CONFIG ####
71 92 ####################################
72 93 use_celery = false
73 94 broker.host = localhost
74 95 broker.vhost = rabbitmqhost
75 96 broker.port = 5672
76 97 broker.user = rabbitmq
77 98 broker.password = qweqwe
78 99
79 100 celery.imports = rhodecode.lib.celerylib.tasks
80 101
81 102 celery.result.backend = amqp
82 103 celery.result.dburi = amqp://
83 104 celery.result.serialier = json
84 105
85 106 #celery.send.task.error.emails = true
86 107 #celery.amqp.task.result.expires = 18000
87 108
88 109 celeryd.concurrency = 2
89 110 #celeryd.log.file = celeryd.log
90 111 celeryd.log.level = debug
91 112 celeryd.max.tasks.per.child = 1
92 113
93 114 #tasks will never be sent to the queue, but executed locally instead.
94 115 celery.always.eager = false
95 116
96 117 ####################################
97 118 ### BEAKER CACHE ####
98 119 ####################################
99 120 beaker.cache.data_dir=%(here)s/data/cache/data
100 121 beaker.cache.lock_dir=%(here)s/data/cache/lock
101 122
102 123 beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
103 124
104 125 beaker.cache.super_short_term.type=memory
105 126 beaker.cache.super_short_term.expire=10
106 127 beaker.cache.super_short_term.key_length = 256
107 128
108 129 beaker.cache.short_term.type=memory
109 130 beaker.cache.short_term.expire=60
110 131 beaker.cache.short_term.key_length = 256
111 132
112 133 beaker.cache.long_term.type=memory
113 134 beaker.cache.long_term.expire=36000
114 135 beaker.cache.long_term.key_length = 256
115 136
116 137 beaker.cache.sql_cache_short.type=memory
117 138 beaker.cache.sql_cache_short.expire=10
118 139 beaker.cache.sql_cache_short.key_length = 256
119 140
120 141 beaker.cache.sql_cache_med.type=memory
121 142 beaker.cache.sql_cache_med.expire=360
122 143 beaker.cache.sql_cache_med.key_length = 256
123 144
124 145 beaker.cache.sql_cache_long.type=file
125 146 beaker.cache.sql_cache_long.expire=3600
126 147 beaker.cache.sql_cache_long.key_length = 256
127 148
128 149 ####################################
129 150 ### BEAKER SESSION ####
130 151 ####################################
131 152 ## Type of storage used for the session, current types are
132 153 ## dbm, file, memcached, database, and memory.
133 154 ## The storage uses the Container API
134 155 ## that is also used by the cache system.
135 156
136 157 ## db session example
137 158
138 159 #beaker.session.type = ext:database
139 160 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
140 161 #beaker.session.table_name = db_session
141 162
142 163 ## encrypted cookie session, good for many instances
143 164 #beaker.session.type = cookie
144 165
145 166 beaker.session.type = file
146 167 beaker.session.key = rhodecode
147 168 beaker.session.encrypt_key = ${app_instance_secret}
148 169 beaker.session.validate_key = ${app_instance_secret}
149 170 beaker.session.timeout = 36000
150 171 beaker.session.httponly = true
151 172
152 173 ## uncomment for https secure cookie
153 174 beaker.session.secure = false
154 175
155 176 ##auto save the session to not to use .save()
156 177 beaker.session.auto = False
157 178
158 179 ##true exire at browser close
159 180 #beaker.session.cookie_expires = 3600
160 181
161 182
162 183 ################################################################################
163 184 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
164 185 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
165 186 ## execute malicious code after an exception is raised. ##
166 187 ################################################################################
167 188 set debug = false
168 189
169 190 ##################################
170 191 ### LOGVIEW CONFIG ###
171 192 ##################################
172 193 logview.sqlalchemy = #faa
173 194 logview.pylons.templating = #bfb
174 195 logview.pylons.util = #eee
175 196
176 197 #########################################################
177 198 ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ###
178 199 #########################################################
179 200
180 201 # SQLITE [default]
181 202 sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db
182 203
183 204 # POSTGRESQL
184 205 # sqlalchemy.db1.url = postgresql://user:pass@localhost/rhodecode
185 206
186 207 # MySQL
187 208 # sqlalchemy.db1.url = mysql://user:pass@localhost/rhodecode
188 209
189 210 # see sqlalchemy docs for others
190 211
191 212 sqlalchemy.db1.echo = false
192 213 sqlalchemy.db1.pool_recycle = 3600
193 214 sqlalchemy.convert_unicode = true
194 215
195 216 ################################
196 217 ### LOGGING CONFIGURATION ####
197 218 ################################
198 219 [loggers]
199 220 keys = root, routes, rhodecode, sqlalchemy, beaker, templates
200 221
201 222 [handlers]
202 223 keys = console, console_sql
203 224
204 225 [formatters]
205 226 keys = generic, color_formatter, color_formatter_sql
206 227
207 228 #############
208 229 ## LOGGERS ##
209 230 #############
210 231 [logger_root]
211 232 level = NOTSET
212 233 handlers = console
213 234
214 235 [logger_routes]
215 236 level = DEBUG
216 237 handlers =
217 238 qualname = routes.middleware
218 239 # "level = DEBUG" logs the route matched and routing variables.
219 240 propagate = 1
220 241
221 242 [logger_beaker]
222 243 level = DEBUG
223 244 handlers =
224 245 qualname = beaker.container
225 246 propagate = 1
226 247
227 248 [logger_templates]
228 249 level = INFO
229 250 handlers =
230 251 qualname = pylons.templating
231 252 propagate = 1
232 253
233 254 [logger_rhodecode]
234 255 level = DEBUG
235 256 handlers =
236 257 qualname = rhodecode
237 258 propagate = 1
238 259
239 260 [logger_sqlalchemy]
240 261 level = INFO
241 262 handlers = console_sql
242 263 qualname = sqlalchemy.engine
243 264 propagate = 0
244 265
245 266 ##############
246 267 ## HANDLERS ##
247 268 ##############
248 269
249 270 [handler_console]
250 271 class = StreamHandler
251 272 args = (sys.stderr,)
252 273 level = INFO
253 274 formatter = color_formatter
254 275
255 276 [handler_console_sql]
256 277 class = StreamHandler
257 278 args = (sys.stderr,)
258 279 level = WARN
259 280 formatter = color_formatter_sql
260 281
261 282 ################
262 283 ## FORMATTERS ##
263 284 ################
264 285
265 286 [formatter_generic]
266 287 format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
267 288 datefmt = %Y-%m-%d %H:%M:%S
268 289
269 290 [formatter_color_formatter]
270 291 class=rhodecode.lib.colored_formatter.ColorFormatter
271 292 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
272 293 datefmt = %Y-%m-%d %H:%M:%S
273 294
274 295 [formatter_color_formatter_sql]
275 296 class=rhodecode.lib.colored_formatter.ColorFormatterSql
276 297 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
277 298 datefmt = %Y-%m-%d %H:%M:%S No newline at end of file
@@ -1,499 +1,510 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 sqlalchemy.orm.exc import NoResultFound
32 32
33 33 from rhodecode.controllers.api import JSONRPCController, JSONRPCError
34 34 from rhodecode.lib.auth import HasPermissionAllDecorator, \
35 35 HasPermissionAnyDecorator
36 36
37 37 from rhodecode.model.meta import Session
38 38 from rhodecode.model.scm import ScmModel
39 39 from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository
40 40 from rhodecode.model.repo import RepoModel
41 41 from rhodecode.model.user import UserModel
42 42 from rhodecode.model.repo_permission import RepositoryPermissionModel
43 43 from rhodecode.model.users_group import UsersGroupModel
44 44 from rhodecode.model.repos_group import ReposGroupModel
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class ApiController(JSONRPCController):
51 51 """
52 52 API Controller
53 53
54 54
55 55 Each method needs to have USER as argument this is then based on given
56 56 API_KEY propagated as instance of user object
57 57
58 58 Preferably this should be first argument also
59 59
60 60
61 61 Each function should also **raise** JSONRPCError for any
62 62 errors that happens
63 63
64 64 """
65 65
66 66 @HasPermissionAllDecorator('hg.admin')
67 def pull(self, apiuser, repo):
67 def pull(self, apiuser, repo_name):
68 68 """
69 69 Dispatch pull action on given repo
70 70
71 71
72 72 :param user:
73 :param repo:
73 :param repo_name:
74 74 """
75 75
76 if Repository.is_valid(repo) is False:
77 raise JSONRPCError('Unknown repo "%s"' % repo)
76 if Repository.is_valid(repo_name) is False:
77 raise JSONRPCError('Unknown repo "%s"' % repo_name)
78 78
79 79 try:
80 ScmModel().pull_changes(repo, self.rhodecode_user.username)
81 return 'Pulled from %s' % repo
80 ScmModel().pull_changes(repo_name, self.rhodecode_user.username)
81 return 'Pulled from %s' % repo_name
82 82 except Exception:
83 raise JSONRPCError('Unable to pull changes from "%s"' % repo)
83 raise JSONRPCError('Unable to pull changes from "%s"' % repo_name)
84 84
85 85 @HasPermissionAllDecorator('hg.admin')
86 86 def get_user(self, apiuser, username):
87 87 """"
88 88 Get a user by username
89 89
90 90 :param apiuser:
91 91 :param username:
92 92 """
93 93
94 94 user = User.get_by_username(username)
95 95 if not user:
96 96 return None
97 97
98 98 return dict(
99 99 id=user.user_id,
100 100 username=user.username,
101 101 firstname=user.name,
102 102 lastname=user.lastname,
103 103 email=user.email,
104 104 active=user.active,
105 105 admin=user.admin,
106 106 ldap=user.ldap_dn
107 107 )
108 108
109 109 @HasPermissionAllDecorator('hg.admin')
110 110 def get_users(self, apiuser):
111 111 """"
112 112 Get all users
113 113
114 114 :param apiuser:
115 115 """
116 116
117 117 result = []
118 118 for user in User.getAll():
119 119 result.append(
120 120 dict(
121 121 id=user.user_id,
122 122 username=user.username,
123 123 firstname=user.name,
124 124 lastname=user.lastname,
125 125 email=user.email,
126 126 active=user.active,
127 127 admin=user.admin,
128 128 ldap=user.ldap_dn
129 129 )
130 130 )
131 131 return result
132 132
133 133 @HasPermissionAllDecorator('hg.admin')
134 134 def create_user(self, apiuser, username, password, firstname,
135 135 lastname, email, active=True, admin=False, ldap_dn=None):
136 136 """
137 137 Create new user
138 138
139 139 :param apiuser:
140 140 :param username:
141 141 :param password:
142 142 :param name:
143 143 :param lastname:
144 144 :param email:
145 145 :param active:
146 146 :param admin:
147 147 :param ldap_dn:
148 148 """
149 149
150 150 if User.get_by_username(username):
151 151 raise JSONRPCError("user %s already exist" % username)
152 152
153 153 try:
154 UserModel().create_or_update(username, password, email, firstname,
155 lastname, active, admin, ldap_dn)
154 usr = UserModel().create_or_update(
155 username, password, email, firstname,
156 lastname, active, admin, ldap_dn
157 )
156 158 Session.commit()
157 return dict(msg='created new user %s' % username)
159 return dict(
160 id=usr.user_id,
161 msg='created new user %s' % username
162 )
158 163 except Exception:
159 164 log.error(traceback.format_exc())
160 165 raise JSONRPCError('failed to create user %s' % username)
161 166
162 167 @HasPermissionAllDecorator('hg.admin')
163 168 def get_users_group(self, apiuser, group_name):
164 169 """"
165 170 Get users group by name
166 171
167 172 :param apiuser:
168 173 :param group_name:
169 174 """
170 175
171 176 users_group = UsersGroup.get_by_group_name(group_name)
172 177 if not users_group:
173 178 return None
174 179
175 180 members = []
176 181 for user in users_group.members:
177 182 user = user.user
178 183 members.append(dict(id=user.user_id,
179 184 username=user.username,
180 185 firstname=user.name,
181 186 lastname=user.lastname,
182 187 email=user.email,
183 188 active=user.active,
184 189 admin=user.admin,
185 190 ldap=user.ldap_dn))
186 191
187 192 return dict(id=users_group.users_group_id,
188 name=users_group.users_group_name,
193 group_name=users_group.users_group_name,
189 194 active=users_group.users_group_active,
190 195 members=members)
191 196
192 197 @HasPermissionAllDecorator('hg.admin')
193 198 def get_users_groups(self, apiuser):
194 199 """"
195 200 Get all users groups
196 201
197 202 :param apiuser:
198 203 """
199 204
200 205 result = []
201 206 for users_group in UsersGroup.getAll():
202 207 members = []
203 208 for user in users_group.members:
204 209 user = user.user
205 210 members.append(dict(id=user.user_id,
206 211 username=user.username,
207 212 firstname=user.name,
208 213 lastname=user.lastname,
209 214 email=user.email,
210 215 active=user.active,
211 216 admin=user.admin,
212 217 ldap=user.ldap_dn))
213 218
214 219 result.append(dict(id=users_group.users_group_id,
215 name=users_group.users_group_name,
220 group_name=users_group.users_group_name,
216 221 active=users_group.users_group_active,
217 222 members=members))
218 223 return result
219 224
220 225 @HasPermissionAllDecorator('hg.admin')
221 def create_users_group(self, apiuser, name, active=True):
226 def create_users_group(self, apiuser, group_name, active=True):
222 227 """
223 228 Creates an new usergroup
224 229
225 :param name:
230 :param group_name:
226 231 :param active:
227 232 """
228 233
229 if self.get_users_group(apiuser, name):
230 raise JSONRPCError("users group %s already exist" % name)
234 if self.get_users_group(apiuser, group_name):
235 raise JSONRPCError("users group %s already exist" % group_name)
231 236
232 237 try:
233 ug = UsersGroupModel().create(name=name, active=active)
238 ug = UsersGroupModel().create(name=group_name, active=active)
234 239 Session.commit()
235 240 return dict(id=ug.users_group_id,
236 msg='created new users group %s' % name)
241 msg='created new users group %s' % group_name)
237 242 except Exception:
238 243 log.error(traceback.format_exc())
239 raise JSONRPCError('failed to create group %s' % name)
244 raise JSONRPCError('failed to create group %s' % group_name)
240 245
241 246 @HasPermissionAllDecorator('hg.admin')
242 247 def add_user_to_users_group(self, apiuser, group_name, username):
243 248 """"
244 249 Add a user to a group
245 250
246 251 :param apiuser:
247 252 :param group_name:
248 253 :param username:
249 254 """
250 255
251 256 try:
252 257 users_group = UsersGroup.get_by_group_name(group_name)
253 258 if not users_group:
254 259 raise JSONRPCError('unknown users group %s' % group_name)
255 260
256 261 try:
257 262 user = User.get_by_username(username)
258 263 except NoResultFound:
259 264 raise JSONRPCError('unknown user %s' % username)
260 265
261 266 ugm = UsersGroupModel().add_user_to_group(users_group, user)
262 267 Session.commit()
263 268 return dict(id=ugm.users_group_member_id,
264 269 msg='created new users group member')
265 270 except Exception:
266 271 log.error(traceback.format_exc())
267 272 raise JSONRPCError('failed to create users group member')
268 273
269 274 @HasPermissionAnyDecorator('hg.admin')
270 275 def get_repo(self, apiuser, repo_name):
271 276 """"
272 277 Get repository by name
273 278
274 279 :param apiuser:
275 280 :param repo_name:
276 281 """
277 282
278 283 repo = Repository.get_by_repo_name(repo_name)
279 284 if repo is None:
280 285 raise JSONRPCError('unknown repository %s' % repo)
281 286
282 287 members = []
283 288 for user in repo.repo_to_perm:
284 289 perm = user.permission.permission_name
285 290 user = user.user
286 291 members.append(
287 292 dict(
288 293 type_="user",
289 294 id=user.user_id,
290 295 username=user.username,
291 296 firstname=user.name,
292 297 lastname=user.lastname,
293 298 email=user.email,
294 299 active=user.active,
295 300 admin=user.admin,
296 301 ldap=user.ldap_dn,
297 302 permission=perm
298 303 )
299 304 )
300 305 for users_group in repo.users_group_to_perm:
301 306 perm = users_group.permission.permission_name
302 307 users_group = users_group.users_group
303 308 members.append(
304 309 dict(
305 310 type_="users_group",
306 311 id=users_group.users_group_id,
307 312 name=users_group.users_group_name,
308 313 active=users_group.users_group_active,
309 314 permission=perm
310 315 )
311 316 )
312 317
313 318 return dict(
314 319 id=repo.repo_id,
315 name=repo.repo_name,
320 repo_name=repo.repo_name,
316 321 type=repo.repo_type,
317 322 description=repo.description,
318 323 members=members
319 324 )
320 325
321 326 @HasPermissionAnyDecorator('hg.admin')
322 327 def get_repos(self, apiuser):
323 328 """"
324 329 Get all repositories
325 330
326 331 :param apiuser:
327 332 """
328 333
329 334 result = []
330 335 for repository in Repository.getAll():
331 336 result.append(
332 337 dict(
333 338 id=repository.repo_id,
334 name=repository.repo_name,
339 repo_name=repository.repo_name,
335 340 type=repository.repo_type,
336 341 description=repository.description
337 342 )
338 343 )
339 344 return result
340 345
341 346 @HasPermissionAnyDecorator('hg.admin')
342 347 def get_repo_nodes(self, apiuser, repo_name, revision, root_path,
343 348 ret_type='all'):
344 349 """
345 350 returns a list of nodes and it's children
346 351 for a given path at given revision. It's possible to specify ret_type
347 352 to show only files or dirs
348 353
349 354 :param apiuser:
350 355 :param repo_name: name of repository
351 356 :param revision: revision for which listing should be done
352 357 :param root_path: path from which start displaying
353 358 :param ret_type: return type 'all|files|dirs' nodes
354 359 """
355 360 try:
356 361 _d, _f = ScmModel().get_nodes(repo_name, revision, root_path,
357 362 flat=False)
358 363 _map = {
359 364 'all': _d + _f,
360 365 'files': _f,
361 366 'dirs': _d,
362 367 }
363 368 return _map[ret_type]
364 369 except KeyError:
365 370 raise JSONRPCError('ret_type must be one of %s' % _map.keys())
366 371 except Exception, e:
367 372 raise JSONRPCError(e)
368 373
369 374 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
370 def create_repo(self, apiuser, name, owner_name, description='',
375 def create_repo(self, apiuser, repo_name, owner_name, description='',
371 376 repo_type='hg', private=False):
372 377 """
373 378 Create a repository
374 379
375 380 :param apiuser:
376 :param name:
381 :param repo_name:
377 382 :param description:
378 383 :param type:
379 384 :param private:
380 385 :param owner_name:
381 386 """
382 387
383 388 try:
384 389 try:
385 390 owner = User.get_by_username(owner_name)
386 391 except NoResultFound:
387 392 raise JSONRPCError('unknown user %s' % owner)
388 393
389 if self.get_repo(apiuser, name):
390 raise JSONRPCError("repo %s already exist" % name)
394 if Repository.get_by_repo_name(repo_name):
395 raise JSONRPCError("repo %s already exist" % repo_name)
391 396
392 groups = name.split('/')
397 groups = repo_name.split('/')
393 398 real_name = groups[-1]
394 399 groups = groups[:-1]
395 400 parent_id = None
396 401 for g in groups:
397 402 group = RepoGroup.get_by_group_name(g)
398 403 if not group:
399 404 group = ReposGroupModel().create(
400 405 dict(
401 406 group_name=g,
402 407 group_description='',
403 408 group_parent_id=parent_id
404 409 )
405 410 )
406 411 parent_id = group.group_id
407 412
408 RepoModel().create(
413 repo = RepoModel().create(
409 414 dict(
410 415 repo_name=real_name,
411 repo_name_full=name,
416 repo_name_full=repo_name,
412 417 description=description,
413 418 private=private,
414 419 repo_type=repo_type,
415 420 repo_group=parent_id,
416 421 clone_uri=None
417 422 ),
418 423 owner
419 424 )
420 425 Session.commit()
426
427 return dict(
428 id=repo.repo_id,
429 msg="Created new repository %s" % repo.repo_name
430 )
431
421 432 except Exception:
422 433 log.error(traceback.format_exc())
423 raise JSONRPCError('failed to create repository %s' % name)
434 raise JSONRPCError('failed to create repository %s' % repo_name)
424 435
425 436 @HasPermissionAnyDecorator('hg.admin')
426 437 def add_user_to_repo(self, apiuser, repo_name, username, perm):
427 438 """
428 439 Add permission for a user to a repository
429 440
430 441 :param apiuser:
431 442 :param repo_name:
432 443 :param username:
433 444 :param perm:
434 445 """
435 446
436 447 try:
437 448 repo = Repository.get_by_repo_name(repo_name)
438 449 if repo is None:
439 450 raise JSONRPCError('unknown repository %s' % repo)
440 451
441 452 try:
442 453 user = User.get_by_username(username)
443 454 except NoResultFound:
444 455 raise JSONRPCError('unknown user %s' % user)
445 456
446 457 RepositoryPermissionModel()\
447 458 .update_or_delete_user_permission(repo, user, perm)
448 459 Session.commit()
449 460
450 461 return dict(
451 462 msg='Added perm: %s for %s in repo: %s' % (
452 463 perm, username, repo_name
453 464 )
454 465 )
455 466 except Exception:
456 467 log.error(traceback.format_exc())
457 468 raise JSONRPCError(
458 469 'failed to edit permission %(repo)s for %(user)s' % dict(
459 470 user=username, repo=repo_name
460 471 )
461 472 )
462 473
463 474 @HasPermissionAnyDecorator('hg.admin')
464 475 def add_users_group_to_repo(self, apiuser, repo_name, group_name, perm):
465 476 """
466 477 Add permission for a users group to a repository
467 478
468 479 :param apiuser:
469 480 :param repo_name:
470 481 :param group_name:
471 482 :param perm:
472 483 """
473 484
474 485 try:
475 486 repo = Repository.get_by_repo_name(repo_name)
476 487 if repo is None:
477 488 raise JSONRPCError('unknown repository %s' % repo)
478 489
479 490 try:
480 491 user_group = UsersGroup.get_by_group_name(group_name)
481 492 except NoResultFound:
482 493 raise JSONRPCError('unknown users group %s' % user_group)
483 494
484 495 RepositoryPermissionModel()\
485 496 .update_or_delete_users_group_permission(repo, user_group,
486 497 perm)
487 498 Session.commit()
488 499 return dict(
489 500 msg='Added perm: %s for %s in repo: %s' % (
490 501 perm, group_name, repo_name
491 502 )
492 503 )
493 504 except Exception:
494 505 log.error(traceback.format_exc())
495 506 raise JSONRPCError(
496 507 'failed to edit permission %(repo)s for %(usergr)s' % dict(
497 508 usergr=group_name, repo=repo_name
498 509 )
499 510 )
@@ -1,482 +1,492 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.db_manage
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Database creation, and setup module for RhodeCode. Used for creation
7 7 of database as well as for migration operations
8 8
9 9 :created_on: Apr 10, 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 import uuid
30 30 import logging
31 31 from os.path import dirname as dn, join as jn
32 32
33 33 from rhodecode import __dbversion__
34 34 from rhodecode.model import meta
35 35
36 36 from rhodecode.model.user import UserModel
37 37 from rhodecode.lib.utils import ask_ok
38 38 from rhodecode.model import init_model
39 39 from rhodecode.model.db import User, Permission, RhodeCodeUi, \
40 40 RhodeCodeSetting, UserToPerm, DbMigrateVersion
41 41
42 42 from sqlalchemy.engine import create_engine
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class DbManage(object):
48 48 def __init__(self, log_sql, dbconf, root, tests=False):
49 49 self.dbname = dbconf.split('/')[-1]
50 50 self.tests = tests
51 51 self.root = root
52 52 self.dburi = dbconf
53 53 self.log_sql = log_sql
54 54 self.db_exists = False
55 55 self.init_db()
56 56
57 57 def init_db(self):
58 58 engine = create_engine(self.dburi, echo=self.log_sql)
59 59 init_model(engine)
60 60 self.sa = meta.Session
61 61
62 62 def create_tables(self, override=False):
63 63 """
64 64 Create a auth database
65 65 """
66 66
67 67 log.info("Any existing database is going to be destroyed")
68 68 if self.tests:
69 69 destroy = True
70 70 else:
71 71 destroy = ask_ok('Are you sure to destroy old database ? [y/n]')
72 72 if not destroy:
73 73 sys.exit()
74 74 if destroy:
75 75 meta.Base.metadata.drop_all()
76 76
77 77 checkfirst = not override
78 78 meta.Base.metadata.create_all(checkfirst=checkfirst)
79 79 log.info('Created tables for %s', self.dbname)
80 80
81 81 def set_db_version(self):
82 82 ver = DbMigrateVersion()
83 83 ver.version = __dbversion__
84 84 ver.repository_id = 'rhodecode_db_migrations'
85 85 ver.repository_path = 'versions'
86 86 self.sa.add(ver)
87 87 log.info('db version set to: %s', __dbversion__)
88 88
89 89 def upgrade(self):
90 90 """
91 91 Upgrades given database schema to given revision following
92 92 all needed steps, to perform the upgrade
93 93
94 94 """
95 95
96 96 from rhodecode.lib.dbmigrate.migrate.versioning import api
97 97 from rhodecode.lib.dbmigrate.migrate.exceptions import \
98 98 DatabaseNotControlledError
99 99
100 if 'sqlite' in self.dburi:
101 print (
102 '********************** WARNING **********************\n'
103 'Make sure your version of sqlite is at least 3.7.X. \n'
104 'Earlier versions are known to fail on some migrations\n'
105 '*****************************************************\n'
106 )
100 107 upgrade = ask_ok('You are about to perform database upgrade, make '
101 108 'sure You backed up your database before. '
102 109 'Continue ? [y/n]')
103 110 if not upgrade:
104 111 sys.exit('Nothing done')
105 112
106 113 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
107 114 'rhodecode/lib/dbmigrate')
108 115 db_uri = self.dburi
109 116
110 117 try:
111 118 curr_version = api.db_version(db_uri, repository_path)
112 119 msg = ('Found current database under version'
113 120 ' control with version %s' % curr_version)
114 121
115 122 except (RuntimeError, DatabaseNotControlledError):
116 123 curr_version = 1
117 124 msg = ('Current database is not under version control. Setting'
118 125 ' as version %s' % curr_version)
119 126 api.version_control(db_uri, repository_path, curr_version)
120 127
121 128 print (msg)
122 129
123 130 if curr_version == __dbversion__:
124 131 sys.exit('This database is already at the newest version')
125 132
126 133 #======================================================================
127 134 # UPGRADE STEPS
128 135 #======================================================================
129 136 class UpgradeSteps(object):
130 137 """
131 138 Those steps follow schema versions so for example schema
132 139 for example schema with seq 002 == step_2 and so on.
133 140 """
134 141
135 142 def __init__(self, klass):
136 143 self.klass = klass
137 144
138 145 def step_0(self):
139 146 # step 0 is the schema upgrade, and than follow proper upgrades
140 147 print ('attempting to do database upgrade to version %s' \
141 148 % __dbversion__)
142 149 api.upgrade(db_uri, repository_path, __dbversion__)
143 150 print ('Schema upgrade completed')
144 151
145 152 def step_1(self):
146 153 pass
147 154
148 155 def step_2(self):
149 156 print ('Patching repo paths for newer version of RhodeCode')
150 157 self.klass.fix_repo_paths()
151 158
152 159 print ('Patching default user of RhodeCode')
153 160 self.klass.fix_default_user()
154 161
155 162 log.info('Changing ui settings')
156 163 self.klass.create_ui_settings()
157 164
158 165 def step_3(self):
159 166 print ('Adding additional settings into RhodeCode db')
160 167 self.klass.fix_settings()
161 168 print ('Adding ldap defaults')
162 169 self.klass.create_ldap_options(skip_existing=True)
163 170
171 def step_4(self):
172 print ('TODO:')
173
164 174 upgrade_steps = [0] + range(curr_version + 1, __dbversion__ + 1)
165 175
166 176 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
167 177 for step in upgrade_steps:
168 178 print ('performing upgrade step %s' % step)
169 179 getattr(UpgradeSteps(self), 'step_%s' % step)()
170 180
171 181 def fix_repo_paths(self):
172 182 """
173 183 Fixes a old rhodecode version path into new one without a '*'
174 184 """
175 185
176 186 paths = self.sa.query(RhodeCodeUi)\
177 187 .filter(RhodeCodeUi.ui_key == '/')\
178 188 .scalar()
179 189
180 190 paths.ui_value = paths.ui_value.replace('*', '')
181 191
182 192 try:
183 193 self.sa.add(paths)
184 194 self.sa.commit()
185 195 except:
186 196 self.sa.rollback()
187 197 raise
188 198
189 199 def fix_default_user(self):
190 200 """
191 201 Fixes a old default user with some 'nicer' default values,
192 202 used mostly for anonymous access
193 203 """
194 204 def_user = self.sa.query(User)\
195 205 .filter(User.username == 'default')\
196 206 .one()
197 207
198 208 def_user.name = 'Anonymous'
199 209 def_user.lastname = 'User'
200 210 def_user.email = 'anonymous@rhodecode.org'
201 211
202 212 try:
203 213 self.sa.add(def_user)
204 214 self.sa.commit()
205 215 except:
206 216 self.sa.rollback()
207 217 raise
208 218
209 219 def fix_settings(self):
210 220 """
211 221 Fixes rhodecode settings adds ga_code key for google analytics
212 222 """
213 223
214 224 hgsettings3 = RhodeCodeSetting('ga_code', '')
215 225
216 226 try:
217 227 self.sa.add(hgsettings3)
218 228 self.sa.commit()
219 229 except:
220 230 self.sa.rollback()
221 231 raise
222 232
223 233 def admin_prompt(self, second=False):
224 234 if not self.tests:
225 235 import getpass
226 236
227 237 def get_password():
228 238 password = getpass.getpass('Specify admin password '
229 239 '(min 6 chars):')
230 240 confirm = getpass.getpass('Confirm password:')
231 241
232 242 if password != confirm:
233 243 log.error('passwords mismatch')
234 244 return False
235 245 if len(password) < 6:
236 246 log.error('password is to short use at least 6 characters')
237 247 return False
238 248
239 249 return password
240 250
241 251 username = raw_input('Specify admin username:')
242 252
243 253 password = get_password()
244 254 if not password:
245 255 #second try
246 256 password = get_password()
247 257 if not password:
248 258 sys.exit()
249 259
250 260 email = raw_input('Specify admin email:')
251 261 self.create_user(username, password, email, True)
252 262 else:
253 263 log.info('creating admin and regular test users')
254 264 from rhodecode.tests import TEST_USER_ADMIN_LOGIN,\
255 265 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL,\
256 266 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,\
257 267 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
258 268 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
259 269
260 270 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
261 271 TEST_USER_ADMIN_EMAIL, True)
262 272
263 273 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
264 274 TEST_USER_REGULAR_EMAIL, False)
265 275
266 276 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
267 277 TEST_USER_REGULAR2_EMAIL, False)
268 278
269 279 def create_ui_settings(self):
270 280 """
271 281 Creates ui settings, fills out hooks
272 282 and disables dotencode
273 283 """
274 284
275 285 #HOOKS
276 286 hooks1_key = RhodeCodeUi.HOOK_UPDATE
277 287 hooks1_ = self.sa.query(RhodeCodeUi)\
278 288 .filter(RhodeCodeUi.ui_key == hooks1_key).scalar()
279 289
280 290 hooks1 = RhodeCodeUi() if hooks1_ is None else hooks1_
281 291 hooks1.ui_section = 'hooks'
282 292 hooks1.ui_key = hooks1_key
283 293 hooks1.ui_value = 'hg update >&2'
284 294 hooks1.ui_active = False
285 295
286 296 hooks2_key = RhodeCodeUi.HOOK_REPO_SIZE
287 297 hooks2_ = self.sa.query(RhodeCodeUi)\
288 298 .filter(RhodeCodeUi.ui_key == hooks2_key).scalar()
289 299
290 300 hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_
291 301 hooks2.ui_section = 'hooks'
292 302 hooks2.ui_key = hooks2_key
293 303 hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size'
294 304
295 305 hooks3 = RhodeCodeUi()
296 306 hooks3.ui_section = 'hooks'
297 307 hooks3.ui_key = RhodeCodeUi.HOOK_PUSH
298 308 hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action'
299 309
300 310 hooks4 = RhodeCodeUi()
301 311 hooks4.ui_section = 'hooks'
302 312 hooks4.ui_key = RhodeCodeUi.HOOK_PULL
303 313 hooks4.ui_value = 'python:rhodecode.lib.hooks.log_pull_action'
304 314
305 315 # For mercurial 1.7 set backward comapatibility with format
306 316 dotencode_disable = RhodeCodeUi()
307 317 dotencode_disable.ui_section = 'format'
308 318 dotencode_disable.ui_key = 'dotencode'
309 319 dotencode_disable.ui_value = 'false'
310 320
311 321 # enable largefiles
312 322 largefiles = RhodeCodeUi()
313 323 largefiles.ui_section = 'extensions'
314 324 largefiles.ui_key = 'largefiles'
315 325 largefiles.ui_value = ''
316 326
317 327 self.sa.add(hooks1)
318 328 self.sa.add(hooks2)
319 329 self.sa.add(hooks3)
320 330 self.sa.add(hooks4)
321 331 self.sa.add(largefiles)
322 332
323 333 def create_ldap_options(self, skip_existing=False):
324 334 """Creates ldap settings"""
325 335
326 336 for k, v in [('ldap_active', 'false'), ('ldap_host', ''),
327 337 ('ldap_port', '389'), ('ldap_tls_kind', 'PLAIN'),
328 338 ('ldap_tls_reqcert', ''), ('ldap_dn_user', ''),
329 339 ('ldap_dn_pass', ''), ('ldap_base_dn', ''),
330 340 ('ldap_filter', ''), ('ldap_search_scope', ''),
331 341 ('ldap_attr_login', ''), ('ldap_attr_firstname', ''),
332 342 ('ldap_attr_lastname', ''), ('ldap_attr_email', '')]:
333 343
334 344 if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
335 345 log.debug('Skipping option %s' % k)
336 346 continue
337 347 setting = RhodeCodeSetting(k, v)
338 348 self.sa.add(setting)
339 349
340 350 def config_prompt(self, test_repo_path='', retries=3):
341 351 if retries == 3:
342 352 log.info('Setting up repositories config')
343 353
344 354 if not self.tests and not test_repo_path:
345 355 path = raw_input('Specify valid full path to your repositories'
346 356 ' you can change this later in application settings:')
347 357 else:
348 358 path = test_repo_path
349 359 path_ok = True
350 360
351 361 # check proper dir
352 362 if not os.path.isdir(path):
353 363 path_ok = False
354 364 log.error('Given path %s is not a valid directory', path)
355 365
356 366 # check write access
357 367 if not os.access(path, os.W_OK) and path_ok:
358 368 path_ok = False
359 369 log.error('No write permission to given path %s', path)
360 370
361 371 if retries == 0:
362 372 sys.exit('max retries reached')
363 373 if path_ok is False:
364 374 retries -= 1
365 375 return self.config_prompt(test_repo_path, retries)
366 376
367 377 return path
368 378
369 379 def create_settings(self, path):
370 380
371 381 self.create_ui_settings()
372 382
373 383 #HG UI OPTIONS
374 384 web1 = RhodeCodeUi()
375 385 web1.ui_section = 'web'
376 386 web1.ui_key = 'push_ssl'
377 387 web1.ui_value = 'false'
378 388
379 389 web2 = RhodeCodeUi()
380 390 web2.ui_section = 'web'
381 391 web2.ui_key = 'allow_archive'
382 392 web2.ui_value = 'gz zip bz2'
383 393
384 394 web3 = RhodeCodeUi()
385 395 web3.ui_section = 'web'
386 396 web3.ui_key = 'allow_push'
387 397 web3.ui_value = '*'
388 398
389 399 web4 = RhodeCodeUi()
390 400 web4.ui_section = 'web'
391 401 web4.ui_key = 'baseurl'
392 402 web4.ui_value = '/'
393 403
394 404 paths = RhodeCodeUi()
395 405 paths.ui_section = 'paths'
396 406 paths.ui_key = '/'
397 407 paths.ui_value = path
398 408
399 409 hgsettings1 = RhodeCodeSetting('realm', 'RhodeCode authentication')
400 410 hgsettings2 = RhodeCodeSetting('title', 'RhodeCode')
401 411 hgsettings3 = RhodeCodeSetting('ga_code', '')
402 412
403 413 self.sa.add(web1)
404 414 self.sa.add(web2)
405 415 self.sa.add(web3)
406 416 self.sa.add(web4)
407 417 self.sa.add(paths)
408 418 self.sa.add(hgsettings1)
409 419 self.sa.add(hgsettings2)
410 420 self.sa.add(hgsettings3)
411 421
412 422 self.create_ldap_options()
413 423
414 424 log.info('created ui config')
415 425
416 426 def create_user(self, username, password, email='', admin=False):
417 427 log.info('creating user %s', username)
418 428 UserModel().create_or_update(username, password, email,
419 429 name='RhodeCode', lastname='Admin',
420 430 active=True, admin=admin)
421 431
422 432 def create_default_user(self):
423 433 log.info('creating default user')
424 434 # create default user for handling default permissions.
425 435 UserModel().create_or_update(username='default',
426 436 password=str(uuid.uuid1())[:8],
427 437 email='anonymous@rhodecode.org',
428 438 name='Anonymous', lastname='User')
429 439
430 440 def create_permissions(self):
431 441 # module.(access|create|change|delete)_[name]
432 442 # module.(read|write|owner)
433 443 perms = [('repository.none', 'Repository no access'),
434 444 ('repository.read', 'Repository read access'),
435 445 ('repository.write', 'Repository write access'),
436 446 ('repository.admin', 'Repository admin access'),
437 447 ('hg.admin', 'Hg Administrator'),
438 448 ('hg.create.repository', 'Repository create'),
439 449 ('hg.create.none', 'Repository creation disabled'),
440 450 ('hg.register.none', 'Register disabled'),
441 451 ('hg.register.manual_activate', 'Register new user with '
442 452 'RhodeCode without manual'
443 453 'activation'),
444 454
445 455 ('hg.register.auto_activate', 'Register new user with '
446 456 'RhodeCode without auto '
447 457 'activation'),
448 458 ]
449 459
450 460 for p in perms:
451 461 new_perm = Permission()
452 462 new_perm.permission_name = p[0]
453 463 new_perm.permission_longname = p[1]
454 464 self.sa.add(new_perm)
455 465
456 466 def populate_default_permissions(self):
457 467 log.info('creating default user permissions')
458 468
459 469 default_user = self.sa.query(User)\
460 470 .filter(User.username == 'default').scalar()
461 471
462 472 reg_perm = UserToPerm()
463 473 reg_perm.user = default_user
464 474 reg_perm.permission = self.sa.query(Permission)\
465 475 .filter(Permission.permission_name == 'hg.register.manual_activate')\
466 476 .scalar()
467 477
468 478 create_repo_perm = UserToPerm()
469 479 create_repo_perm.user = default_user
470 480 create_repo_perm.permission = self.sa.query(Permission)\
471 481 .filter(Permission.permission_name == 'hg.create.repository')\
472 482 .scalar()
473 483
474 484 default_repo_perm = UserToPerm()
475 485 default_repo_perm.user = default_user
476 486 default_repo_perm.permission = self.sa.query(Permission)\
477 487 .filter(Permission.permission_name == 'repository.read')\
478 488 .scalar()
479 489
480 490 self.sa.add(reg_perm)
481 491 self.sa.add(create_repo_perm)
482 492 self.sa.add(default_repo_perm)
@@ -1,755 +1,792 b''
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 import StringIO
9 9 import urllib
10 10 import math
11 import logging
11 12
12 13 from datetime import datetime
13 14 from pygments.formatters.html import HtmlFormatter
14 15 from pygments import highlight as code_highlight
15 16 from pylons import url, request, config
16 17 from pylons.i18n.translation import _, ungettext
17 18 from hashlib import md5
18 19
19 20 from webhelpers.html import literal, HTML, escape
20 21 from webhelpers.html.tools import *
21 22 from webhelpers.html.builder import make_tag
22 23 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
23 24 end_form, file, form, hidden, image, javascript_link, link_to, \
24 25 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
25 26 submit, text, password, textarea, title, ul, xml_declaration, radio
26 27 from webhelpers.html.tools import auto_link, button_to, highlight, \
27 28 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
28 29 from webhelpers.number import format_byte_size, format_bit_size
29 30 from webhelpers.pylonslib import Flash as _Flash
30 31 from webhelpers.pylonslib.secure_form import secure_form
31 32 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
32 33 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
33 34 replace_whitespace, urlify, truncate, wrap_paragraphs
34 35 from webhelpers.date import time_ago_in_words
35 36 from webhelpers.paginate import Page
36 37 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
37 38 convert_boolean_attrs, NotGiven, _make_safe_id_component
38 39
39 40 from rhodecode.lib.annotate import annotate_highlight
40 41 from rhodecode.lib.utils import repo_name_slug
41 42 from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
42 43 from rhodecode.lib.markup_renderer import MarkupRenderer
43 44
45 log = logging.getLogger(__name__)
46
44 47
45 48 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
46 49 """
47 50 Reset button
48 51 """
49 52 _set_input_attrs(attrs, type, name, value)
50 53 _set_id_attr(attrs, id, name)
51 54 convert_boolean_attrs(attrs, ["disabled"])
52 55 return HTML.input(**attrs)
53 56
54 57 reset = _reset
55 58 safeid = _make_safe_id_component
56 59
57 60
58 61 def FID(raw_id, path):
59 62 """
60 63 Creates a uniqe ID for filenode based on it's hash of path and revision
61 64 it's safe to use in urls
62 65
63 66 :param raw_id:
64 67 :param path:
65 68 """
66 69
67 70 return 'C-%s-%s' % (short_id(raw_id), md5(path).hexdigest()[:12])
68 71
69 72
70 73 def get_token():
71 74 """Return the current authentication token, creating one if one doesn't
72 75 already exist.
73 76 """
74 77 token_key = "_authentication_token"
75 78 from pylons import session
76 79 if not token_key in session:
77 80 try:
78 81 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
79 82 except AttributeError: # Python < 2.4
80 83 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
81 84 session[token_key] = token
82 85 if hasattr(session, 'save'):
83 86 session.save()
84 87 return session[token_key]
85 88
86 89 class _GetError(object):
87 90 """Get error from form_errors, and represent it as span wrapped error
88 91 message
89 92
90 93 :param field_name: field to fetch errors for
91 94 :param form_errors: form errors dict
92 95 """
93 96
94 97 def __call__(self, field_name, form_errors):
95 98 tmpl = """<span class="error_msg">%s</span>"""
96 99 if form_errors and form_errors.has_key(field_name):
97 100 return literal(tmpl % form_errors.get(field_name))
98 101
99 102 get_error = _GetError()
100 103
101 104 class _ToolTip(object):
102 105
103 106 def __call__(self, tooltip_title, trim_at=50):
104 107 """Special function just to wrap our text into nice formatted
105 108 autowrapped text
106 109
107 110 :param tooltip_title:
108 111 """
109 112 return escape(tooltip_title)
110 113 tooltip = _ToolTip()
111 114
112 115 class _FilesBreadCrumbs(object):
113 116
114 117 def __call__(self, repo_name, rev, paths):
115 118 if isinstance(paths, str):
116 119 paths = safe_unicode(paths)
117 120 url_l = [link_to(repo_name, url('files_home',
118 121 repo_name=repo_name,
119 122 revision=rev, f_path=''))]
120 123 paths_l = paths.split('/')
121 124 for cnt, p in enumerate(paths_l):
122 125 if p != '':
123 126 url_l.append(link_to(p,
124 127 url('files_home',
125 128 repo_name=repo_name,
126 129 revision=rev,
127 130 f_path='/'.join(paths_l[:cnt + 1])
128 131 )
129 132 )
130 133 )
131 134
132 135 return literal('/'.join(url_l))
133 136
134 137 files_breadcrumbs = _FilesBreadCrumbs()
135 138
136 139 class CodeHtmlFormatter(HtmlFormatter):
137 140 """My code Html Formatter for source codes
138 141 """
139 142
140 143 def wrap(self, source, outfile):
141 144 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
142 145
143 146 def _wrap_code(self, source):
144 147 for cnt, it in enumerate(source):
145 148 i, t = it
146 149 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
147 150 yield i, t
148 151
149 152 def _wrap_tablelinenos(self, inner):
150 153 dummyoutfile = StringIO.StringIO()
151 154 lncount = 0
152 155 for t, line in inner:
153 156 if t:
154 157 lncount += 1
155 158 dummyoutfile.write(line)
156 159
157 160 fl = self.linenostart
158 161 mw = len(str(lncount + fl - 1))
159 162 sp = self.linenospecial
160 163 st = self.linenostep
161 164 la = self.lineanchors
162 165 aln = self.anchorlinenos
163 166 nocls = self.noclasses
164 167 if sp:
165 168 lines = []
166 169
167 170 for i in range(fl, fl + lncount):
168 171 if i % st == 0:
169 172 if i % sp == 0:
170 173 if aln:
171 174 lines.append('<a href="#%s%d" class="special">%*d</a>' %
172 175 (la, i, mw, i))
173 176 else:
174 177 lines.append('<span class="special">%*d</span>' % (mw, i))
175 178 else:
176 179 if aln:
177 180 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
178 181 else:
179 182 lines.append('%*d' % (mw, i))
180 183 else:
181 184 lines.append('')
182 185 ls = '\n'.join(lines)
183 186 else:
184 187 lines = []
185 188 for i in range(fl, fl + lncount):
186 189 if i % st == 0:
187 190 if aln:
188 191 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
189 192 else:
190 193 lines.append('%*d' % (mw, i))
191 194 else:
192 195 lines.append('')
193 196 ls = '\n'.join(lines)
194 197
195 198 # in case you wonder about the seemingly redundant <div> here: since the
196 199 # content in the other cell also is wrapped in a div, some browsers in
197 200 # some configurations seem to mess up the formatting...
198 201 if nocls:
199 202 yield 0, ('<table class="%stable">' % self.cssclass +
200 203 '<tr><td><div class="linenodiv" '
201 204 'style="background-color: #f0f0f0; padding-right: 10px">'
202 205 '<pre style="line-height: 125%">' +
203 206 ls + '</pre></div></td><td id="hlcode" class="code">')
204 207 else:
205 208 yield 0, ('<table class="%stable">' % self.cssclass +
206 209 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
207 210 ls + '</pre></div></td><td id="hlcode" class="code">')
208 211 yield 0, dummyoutfile.getvalue()
209 212 yield 0, '</td></tr></table>'
210 213
211 214
212 215 def pygmentize(filenode, **kwargs):
213 216 """pygmentize function using pygments
214 217
215 218 :param filenode:
216 219 """
217 220
218 221 return literal(code_highlight(filenode.content,
219 222 filenode.lexer, CodeHtmlFormatter(**kwargs)))
220 223
221 224
222 225 def pygmentize_annotation(repo_name, filenode, **kwargs):
223 226 """
224 227 pygmentize function for annotation
225 228
226 229 :param filenode:
227 230 """
228 231
229 232 color_dict = {}
230 233
231 234 def gen_color(n=10000):
232 235 """generator for getting n of evenly distributed colors using
233 236 hsv color and golden ratio. It always return same order of colors
234 237
235 238 :returns: RGB tuple
236 239 """
237 240
238 241 def hsv_to_rgb(h, s, v):
239 242 if s == 0.0:
240 243 return v, v, v
241 244 i = int(h * 6.0) # XXX assume int() truncates!
242 245 f = (h * 6.0) - i
243 246 p = v * (1.0 - s)
244 247 q = v * (1.0 - s * f)
245 248 t = v * (1.0 - s * (1.0 - f))
246 249 i = i % 6
247 250 if i == 0:
248 251 return v, t, p
249 252 if i == 1:
250 253 return q, v, p
251 254 if i == 2:
252 255 return p, v, t
253 256 if i == 3:
254 257 return p, q, v
255 258 if i == 4:
256 259 return t, p, v
257 260 if i == 5:
258 261 return v, p, q
259 262
260 263 golden_ratio = 0.618033988749895
261 264 h = 0.22717784590367374
262 265
263 266 for _ in xrange(n):
264 267 h += golden_ratio
265 268 h %= 1
266 269 HSV_tuple = [h, 0.95, 0.95]
267 270 RGB_tuple = hsv_to_rgb(*HSV_tuple)
268 271 yield map(lambda x: str(int(x * 256)), RGB_tuple)
269 272
270 273 cgenerator = gen_color()
271 274
272 275 def get_color_string(cs):
273 276 if cs in color_dict:
274 277 col = color_dict[cs]
275 278 else:
276 279 col = color_dict[cs] = cgenerator.next()
277 280 return "color: rgb(%s)! important;" % (', '.join(col))
278 281
279 282 def url_func(repo_name):
280 283
281 284 def _url_func(changeset):
282 285 author = changeset.author
283 286 date = changeset.date
284 287 message = tooltip(changeset.message)
285 288
286 289 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
287 290 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
288 291 "</b> %s<br/></div>")
289 292
290 293 tooltip_html = tooltip_html % (author, date, message)
291 294 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
292 295 short_id(changeset.raw_id))
293 296 uri = link_to(
294 297 lnk_format,
295 298 url('changeset_home', repo_name=repo_name,
296 299 revision=changeset.raw_id),
297 300 style=get_color_string(changeset.raw_id),
298 301 class_='tooltip',
299 302 title=tooltip_html
300 303 )
301 304
302 305 uri += '\n'
303 306 return uri
304 307 return _url_func
305 308
306 309 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
307 310
308 311
309 312 def is_following_repo(repo_name, user_id):
310 313 from rhodecode.model.scm import ScmModel
311 314 return ScmModel().is_following_repo(repo_name, user_id)
312 315
313 316 flash = _Flash()
314 317
315 318 #==============================================================================
316 319 # SCM FILTERS available via h.
317 320 #==============================================================================
318 321 from vcs.utils import author_name, author_email
319 322 from rhodecode.lib import credentials_filter, age as _age
320 323 from rhodecode.model.db import User
321 324
322 325 age = lambda x: _age(x)
323 326 capitalize = lambda x: x.capitalize()
324 327 email = author_email
325 328 short_id = lambda x: x[:12]
326 329 hide_credentials = lambda x: ''.join(credentials_filter(x))
327 330
328 331
329 332 def email_or_none(author):
330 333 _email = email(author)
331 334 if _email != '':
332 335 return _email
333 336
334 337 # See if it contains a username we can get an email from
335 338 user = User.get_by_username(author_name(author), case_insensitive=True,
336 339 cache=True)
337 340 if user is not None:
338 341 return user.email
339 342
340 343 # No valid email, not a valid user in the system, none!
341 344 return None
342 345
343 346
344 347 def person(author):
345 348 # attr to return from fetched user
346 349 person_getter = lambda usr: usr.username
347 350
348 351 # Valid email in the attribute passed, see if they're in the system
349 352 _email = email(author)
350 353 if _email != '':
351 354 user = User.get_by_email(_email, case_insensitive=True, cache=True)
352 355 if user is not None:
353 356 return person_getter(user)
354 357 return _email
355 358
356 359 # Maybe it's a username?
357 360 _author = author_name(author)
358 361 user = User.get_by_username(_author, case_insensitive=True,
359 362 cache=True)
360 363 if user is not None:
361 364 return person_getter(user)
362 365
363 366 # Still nothing? Just pass back the author name then
364 367 return _author
365 368
366 369 def bool2icon(value):
367 370 """Returns True/False values represented as small html image of true/false
368 371 icons
369 372
370 373 :param value: bool value
371 374 """
372 375
373 376 if value is True:
374 377 return HTML.tag('img', src=url("/images/icons/accept.png"),
375 378 alt=_('True'))
376 379
377 380 if value is False:
378 381 return HTML.tag('img', src=url("/images/icons/cancel.png"),
379 382 alt=_('False'))
380 383
381 384 return value
382 385
383 386
384 387 def action_parser(user_log, feed=False):
385 388 """This helper will action_map the specified string action into translated
386 389 fancy names with icons and links
387 390
388 391 :param user_log: user log instance
389 392 :param feed: use output for feeds (no html and fancy icons)
390 393 """
391 394
392 395 action = user_log.action
393 396 action_params = ' '
394 397
395 398 x = action.split(':')
396 399
397 400 if len(x) > 1:
398 401 action, action_params = x
399 402
400 403 def get_cs_links():
401 404 revs_limit = 3 #display this amount always
402 405 revs_top_limit = 50 #show upto this amount of changesets hidden
403 406 revs = action_params.split(',')
404 407 repo_name = user_log.repository.repo_name
405 408
406 409 from rhodecode.model.scm import ScmModel
407 410 repo = user_log.repository.scm_instance
408 411
409 412 message = lambda rev: get_changeset_safe(repo, rev).message
410 413 cs_links = []
411 414 cs_links.append(" " + ', '.join ([link_to(rev,
412 415 url('changeset_home',
413 416 repo_name=repo_name,
414 417 revision=rev), title=tooltip(message(rev)),
415 418 class_='tooltip') for rev in revs[:revs_limit] ]))
416 419
417 420 compare_view = (' <div class="compare_view tooltip" title="%s">'
418 421 '<a href="%s">%s</a> '
419 422 '</div>' % (_('Show all combined changesets %s->%s' \
420 423 % (revs[0], revs[-1])),
421 424 url('changeset_home', repo_name=repo_name,
422 425 revision='%s...%s' % (revs[0], revs[-1])
423 426 ),
424 427 _('compare view'))
425 428 )
426 429
427 430 if len(revs) > revs_limit:
428 431 uniq_id = revs[0]
429 432 html_tmpl = ('<span> %s '
430 433 '<a class="show_more" id="_%s" href="#more">%s</a> '
431 434 '%s</span>')
432 435 if not feed:
433 436 cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
434 437 % (len(revs) - revs_limit),
435 438 _('revisions')))
436 439
437 440 if not feed:
438 441 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
439 442 else:
440 443 html_tmpl = '<span id="%s"> %s </span>'
441 444
442 445 cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
443 446 url('changeset_home',
444 447 repo_name=repo_name, revision=rev),
445 448 title=message(rev), class_='tooltip')
446 449 for rev in revs[revs_limit:revs_top_limit]])))
447 450 if len(revs) > 1:
448 451 cs_links.append(compare_view)
449 452 return ''.join(cs_links)
450 453
451 454 def get_fork_name():
452 455 repo_name = action_params
453 456 return _('fork name ') + str(link_to(action_params, url('summary_home',
454 457 repo_name=repo_name,)))
455 458
456 459 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
457 460 'user_created_repo':(_('[created] repository'), None),
458 461 'user_created_fork':(_('[created] repository as fork'), None),
459 462 'user_forked_repo':(_('[forked] repository'), get_fork_name),
460 463 'user_updated_repo':(_('[updated] repository'), None),
461 464 'admin_deleted_repo':(_('[delete] repository'), None),
462 465 'admin_created_repo':(_('[created] repository'), None),
463 466 'admin_forked_repo':(_('[forked] repository'), None),
464 467 'admin_updated_repo':(_('[updated] repository'), None),
465 468 'push':(_('[pushed] into'), get_cs_links),
466 469 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
467 470 'push_remote':(_('[pulled from remote] into'), get_cs_links),
468 471 'pull':(_('[pulled] from'), None),
469 472 'started_following_repo':(_('[started following] repository'), None),
470 473 'stopped_following_repo':(_('[stopped following] repository'), None),
471 474 }
472 475
473 476 action_str = action_map.get(action, action)
474 477 if feed:
475 478 action = action_str[0].replace('[', '').replace(']', '')
476 479 else:
477 480 action = action_str[0].replace('[', '<span class="journal_highlight">')\
478 481 .replace(']', '</span>')
479 482
480 483 action_params_func = lambda :""
481 484
482 485 if callable(action_str[1]):
483 486 action_params_func = action_str[1]
484 487
485 488 return [literal(action), action_params_func]
486 489
487 490 def action_parser_icon(user_log):
488 491 action = user_log.action
489 492 action_params = None
490 493 x = action.split(':')
491 494
492 495 if len(x) > 1:
493 496 action, action_params = x
494 497
495 498 tmpl = """<img src="%s%s" alt="%s"/>"""
496 499 map = {'user_deleted_repo':'database_delete.png',
497 500 'user_created_repo':'database_add.png',
498 501 'user_created_fork':'arrow_divide.png',
499 502 'user_forked_repo':'arrow_divide.png',
500 503 'user_updated_repo':'database_edit.png',
501 504 'admin_deleted_repo':'database_delete.png',
502 505 'admin_created_repo':'database_add.png',
503 506 'admin_forked_repo':'arrow_divide.png',
504 507 'admin_updated_repo':'database_edit.png',
505 508 'push':'script_add.png',
506 509 'push_local':'script_edit.png',
507 510 'push_remote':'connect.png',
508 511 'pull':'down_16.png',
509 512 'started_following_repo':'heart_add.png',
510 513 'stopped_following_repo':'heart_delete.png',
511 514 }
512 515 return literal(tmpl % ((url('/images/icons/')),
513 516 map.get(action, action), action))
514 517
515 518
516 519 #==============================================================================
517 520 # PERMS
518 521 #==============================================================================
519 522 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
520 523 HasRepoPermissionAny, HasRepoPermissionAll
521 524
522 525 #==============================================================================
523 526 # GRAVATAR URL
524 527 #==============================================================================
525 528
526 529 def gravatar_url(email_address, size=30):
527 530 if (not str2bool(config['app_conf'].get('use_gravatar')) or
528 531 not email_address or email_address == 'anonymous@rhodecode.org'):
529 532 return url("/images/user%s.png" % size)
530 533
531 534 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
532 535 default = 'identicon'
533 536 baseurl_nossl = "http://www.gravatar.com/avatar/"
534 537 baseurl_ssl = "https://secure.gravatar.com/avatar/"
535 538 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
536 539
537 540 if isinstance(email_address, unicode):
538 541 #hashlib crashes on unicode items
539 542 email_address = safe_str(email_address)
540 543 # construct the url
541 544 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
542 545 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
543 546
544 547 return gravatar_url
545 548
546 549
547 550 #==============================================================================
548 551 # REPO PAGER, PAGER FOR REPOSITORY
549 552 #==============================================================================
550 553 class RepoPage(Page):
551 554
552 555 def __init__(self, collection, page=1, items_per_page=20,
553 556 item_count=None, url=None, **kwargs):
554 557
555 558 """Create a "RepoPage" instance. special pager for paging
556 559 repository
557 560 """
558 561 self._url_generator = url
559 562
560 563 # Safe the kwargs class-wide so they can be used in the pager() method
561 564 self.kwargs = kwargs
562 565
563 566 # Save a reference to the collection
564 567 self.original_collection = collection
565 568
566 569 self.collection = collection
567 570
568 571 # The self.page is the number of the current page.
569 572 # The first page has the number 1!
570 573 try:
571 574 self.page = int(page) # make it int() if we get it as a string
572 575 except (ValueError, TypeError):
573 576 self.page = 1
574 577
575 578 self.items_per_page = items_per_page
576 579
577 580 # Unless the user tells us how many items the collections has
578 581 # we calculate that ourselves.
579 582 if item_count is not None:
580 583 self.item_count = item_count
581 584 else:
582 585 self.item_count = len(self.collection)
583 586
584 587 # Compute the number of the first and last available page
585 588 if self.item_count > 0:
586 589 self.first_page = 1
587 590 self.page_count = int(math.ceil(float(self.item_count) /
588 591 self.items_per_page))
589 592 self.last_page = self.first_page + self.page_count - 1
590 593
591 594 # Make sure that the requested page number is the range of
592 595 # valid pages
593 596 if self.page > self.last_page:
594 597 self.page = self.last_page
595 598 elif self.page < self.first_page:
596 599 self.page = self.first_page
597 600
598 601 # Note: the number of items on this page can be less than
599 602 # items_per_page if the last page is not full
600 603 self.first_item = max(0, (self.item_count) - (self.page *
601 604 items_per_page))
602 605 self.last_item = ((self.item_count - 1) - items_per_page *
603 606 (self.page - 1))
604 607
605 608 self.items = list(self.collection[self.first_item:self.last_item + 1])
606 609
607 610
608 611 # Links to previous and next page
609 612 if self.page > self.first_page:
610 613 self.previous_page = self.page - 1
611 614 else:
612 615 self.previous_page = None
613 616
614 617 if self.page < self.last_page:
615 618 self.next_page = self.page + 1
616 619 else:
617 620 self.next_page = None
618 621
619 622 # No items available
620 623 else:
621 624 self.first_page = None
622 625 self.page_count = 0
623 626 self.last_page = None
624 627 self.first_item = None
625 628 self.last_item = None
626 629 self.previous_page = None
627 630 self.next_page = None
628 631 self.items = []
629 632
630 633 # This is a subclass of the 'list' type. Initialise the list now.
631 634 list.__init__(self, reversed(self.items))
632 635
633 636
634 637 def changed_tooltip(nodes):
635 638 """
636 639 Generates a html string for changed nodes in changeset page.
637 640 It limits the output to 30 entries
638 641
639 642 :param nodes: LazyNodesGenerator
640 643 """
641 644 if nodes:
642 645 pref = ': <br/> '
643 646 suf = ''
644 647 if len(nodes) > 30:
645 648 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
646 649 return literal(pref + '<br/> '.join([safe_unicode(x.path)
647 650 for x in nodes[:30]]) + suf)
648 651 else:
649 652 return ': ' + _('No Files')
650 653
651 654
652 655
653 656 def repo_link(groups_and_repos):
654 657 """
655 658 Makes a breadcrumbs link to repo within a group
656 659 joins &raquo; on each group to create a fancy link
657 660
658 661 ex::
659 662 group >> subgroup >> repo
660 663
661 664 :param groups_and_repos:
662 665 """
663 666 groups, repo_name = groups_and_repos
664 667
665 668 if not groups:
666 669 return repo_name
667 670 else:
668 671 def make_link(group):
669 672 return link_to(group.name, url('repos_group_home',
670 673 group_name=group.group_name))
671 674 return literal(' &raquo; '.join(map(make_link, groups)) + \
672 675 " &raquo; " + repo_name)
673 676
674 677 def fancy_file_stats(stats):
675 678 """
676 679 Displays a fancy two colored bar for number of added/deleted
677 680 lines of code on file
678 681
679 682 :param stats: two element list of added/deleted lines of code
680 683 """
681 684
682 685 a, d, t = stats[0], stats[1], stats[0] + stats[1]
683 686 width = 100
684 687 unit = float(width) / (t or 1)
685 688
686 689 # needs > 9% of width to be visible or 0 to be hidden
687 690 a_p = max(9, unit * a) if a > 0 else 0
688 691 d_p = max(9, unit * d) if d > 0 else 0
689 692 p_sum = a_p + d_p
690 693
691 694 if p_sum > width:
692 695 #adjust the percentage to be == 100% since we adjusted to 9
693 696 if a_p > d_p:
694 697 a_p = a_p - (p_sum - width)
695 698 else:
696 699 d_p = d_p - (p_sum - width)
697 700
698 701 a_v = a if a > 0 else ''
699 702 d_v = d if d > 0 else ''
700 703
701 704
702 705 def cgen(l_type):
703 706 mapping = {'tr':'top-right-rounded-corner',
704 707 'tl':'top-left-rounded-corner',
705 708 'br':'bottom-right-rounded-corner',
706 709 'bl':'bottom-left-rounded-corner'}
707 710 map_getter = lambda x:mapping[x]
708 711
709 712 if l_type == 'a' and d_v:
710 713 #case when added and deleted are present
711 714 return ' '.join(map(map_getter, ['tl', 'bl']))
712 715
713 716 if l_type == 'a' and not d_v:
714 717 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
715 718
716 719 if l_type == 'd' and a_v:
717 720 return ' '.join(map(map_getter, ['tr', 'br']))
718 721
719 722 if l_type == 'd' and not a_v:
720 723 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
721 724
722 725
723 726
724 727 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
725 728 a_p, a_v)
726 729 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
727 730 d_p, d_v)
728 731 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
729 732
730 733
731 def urlify_text(text):
734 def urlify_text(text_):
732 735 import re
733 736
734 737 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
735 738 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
736 739
737 740 def url_func(match_obj):
738 741 url_full = match_obj.groups()[0]
739 742 return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
740 743
741 return literal(url_pat.sub(url_func, text))
744 return literal(url_pat.sub(url_func, text_))
742 745
746 def urlify_commit(text_):
747 import re
748 import traceback
749
750 try:
751 conf = config['app_conf']
752
753 URL_PAT = re.compile(r'%s' % conf.get('url_pat'))
754
755 if URL_PAT:
756 ISSUE_SERVER = conf.get('issue_server')
757 ISSUE_PREFIX = conf.get('issue_prefix')
758 def url_func(match_obj):
759 issue_id = match_obj.groups()[0]
760 tmpl = (
761 '<a class="%(cls)s" href="%(url)s">'
762 ' %(issue-prefix)s%(id-repr)s'
763 '</a>'
764 )
765 return tmpl % (
766 {
767 'cls':'issue-tracker-link',
768 'url':ISSUE_SERVER.replace('{id}',issue_id),
769 'id-repr':issue_id,
770 'issue-prefix':ISSUE_PREFIX,
771 'serv':ISSUE_SERVER,
772 }
773 )
774 return literal(URL_PAT.sub(url_func, text_))
775 except:
776 log.error(traceback.format_exc())
777 pass
778
779 return text_
743 780
744 781 def rst(source):
745 782 return literal('<div class="rst-block">%s</div>' %
746 783 MarkupRenderer.rst(source))
747 784
748 785 def rst_w_mentions(source):
749 786 """
750 787 Wrapped rst renderer with @mention highlighting
751 788
752 789 :param source:
753 790 """
754 791 return literal('<div class="rst-block">%s</div>' %
755 792 MarkupRenderer.rst_with_mentions(source))
@@ -1,11 +1,13 b''
1
1 2
2 3 class InvalidMessage(RuntimeError):
3 4 """
4 5 Raised if message is missing vital headers, such
5 6 as recipients or sender address.
6 7 """
7 8
9
8 10 class BadHeaders(RuntimeError):
9 11 """
10 12 Raised if message contains newlines in headers.
11 13 """
@@ -1,180 +1,185 b''
1 1 from rhodecode.lib.rcmail.response import MailResponse
2 2
3 3 from rhodecode.lib.rcmail.exceptions import BadHeaders
4 4 from rhodecode.lib.rcmail.exceptions import InvalidMessage
5 5
6 6 class Attachment(object):
7 7 """
8 8 Encapsulates file attachment information.
9 9
10 10 :param filename: filename of attachment
11 11 :param content_type: file mimetype
12 12 :param data: the raw file data, either as string or file obj
13 13 :param disposition: content-disposition (if any)
14 14 """
15 15
16 16 def __init__(self,
17 17 filename=None,
18 18 content_type=None,
19 19 data=None,
20 20 disposition=None):
21 21
22 22 self.filename = filename
23 23 self.content_type = content_type
24 24 self.disposition = disposition or 'attachment'
25 25 self._data = data
26 26
27 27 @property
28 28 def data(self):
29 29 if isinstance(self._data, basestring):
30 30 return self._data
31 31 self._data = self._data.read()
32 32 return self._data
33 33
34 34
35 35 class Message(object):
36 36 """
37 37 Encapsulates an email message.
38 38
39 39 :param subject: email subject header
40 40 :param recipients: list of email addresses
41 41 :param body: plain text message
42 42 :param html: HTML message
43 43 :param sender: email sender address
44 44 :param cc: CC list
45 45 :param bcc: BCC list
46 46 :param extra_headers: dict of extra email headers
47 47 :param attachments: list of Attachment instances
48 :param recipients_separator: alternative separator for any of
49 'From', 'To', 'Delivered-To', 'Cc', 'Bcc' fields
48 50 """
49 51
50 52 def __init__(self,
51 53 subject=None,
52 54 recipients=None,
53 55 body=None,
54 56 html=None,
55 57 sender=None,
56 58 cc=None,
57 59 bcc=None,
58 60 extra_headers=None,
59 attachments=None):
60
61 attachments=None,
62 recipients_separator="; "):
61 63
62 64 self.subject = subject or ''
63 65 self.sender = sender
64 66 self.body = body
65 67 self.html = html
66 68
67 69 self.recipients = recipients or []
68 70 self.attachments = attachments or []
69 71 self.cc = cc or []
70 72 self.bcc = bcc or []
71 73 self.extra_headers = extra_headers or {}
72 74
75 self.recipients_separator = recipients_separator
76
73 77 @property
74 78 def send_to(self):
75 79 return set(self.recipients) | set(self.bcc or ()) | set(self.cc or ())
76 80
77 81 def to_message(self):
78 82 """
79 83 Returns raw email.Message instance.Validates message first.
80 84 """
81 85
82 86 self.validate()
83 87
84 88 return self.get_response().to_message()
85 89
86 90 def get_response(self):
87 91 """
88 92 Creates a Lamson MailResponse instance
89 93 """
90 94
91 95 response = MailResponse(Subject=self.subject,
92 96 To=self.recipients,
93 97 From=self.sender,
94 98 Body=self.body,
95 Html=self.html)
99 Html=self.html,
100 separator=self.recipients_separator)
96 101
97 102 if self.bcc:
98 103 response.base['Bcc'] = self.bcc
99 104
100 105 if self.cc:
101 106 response.base['Cc'] = self.cc
102 107
103 108 for attachment in self.attachments:
104 109
105 110 response.attach(attachment.filename,
106 111 attachment.content_type,
107 112 attachment.data,
108 113 attachment.disposition)
109 114
110 115 response.update(self.extra_headers)
111 116
112 117 return response
113 118
114 119 def is_bad_headers(self):
115 120 """
116 121 Checks for bad headers i.e. newlines in subject, sender or recipients.
117 122 """
118 123
119 124 headers = [self.subject, self.sender]
120 125 headers += list(self.send_to)
121 126 headers += self.extra_headers.values()
122 127
123 128 for val in headers:
124 129 for c in '\r\n':
125 130 if c in val:
126 131 return True
127 132 return False
128 133
129 134 def validate(self):
130 135 """
131 136 Checks if message is valid and raises appropriate exception.
132 137 """
133 138
134 139 if not self.recipients:
135 140 raise InvalidMessage, "No recipients have been added"
136 141
137 142 if not self.body and not self.html:
138 143 raise InvalidMessage, "No body has been set"
139 144
140 145 if not self.sender:
141 146 raise InvalidMessage, "No sender address has been set"
142 147
143 148 if self.is_bad_headers():
144 149 raise BadHeaders
145 150
146 151 def add_recipient(self, recipient):
147 152 """
148 153 Adds another recipient to the message.
149 154
150 155 :param recipient: email address of recipient.
151 156 """
152 157
153 158 self.recipients.append(recipient)
154 159
155 160 def add_cc(self, recipient):
156 161 """
157 162 Adds an email address to the CC list.
158 163
159 164 :param recipient: email address of recipient.
160 165 """
161 166
162 167 self.cc.append(recipient)
163 168
164 169 def add_bcc(self, recipient):
165 170 """
166 171 Adds an email address to the BCC list.
167 172
168 173 :param recipient: email address of recipient.
169 174 """
170 175
171 176 self.bcc.append(recipient)
172 177
173 178 def attach(self, attachment):
174 179 """
175 180 Adds an attachment to the message.
176 181
177 182 :param attachment: an **Attachment** instance.
178 183 """
179 184
180 185 self.attachments.append(attachment)
@@ -1,438 +1,446 b''
1 1 # The code in this module is entirely lifted from the Lamson project
2 2 # (http://lamsonproject.org/). Its copyright is:
3 3
4 4 # Copyright (c) 2008, Zed A. Shaw
5 5 # All rights reserved.
6 6
7 7 # It is provided under this license:
8 8
9 9 # Redistribution and use in source and binary forms, with or without
10 10 # modification, are permitted provided that the following conditions are met:
11 11
12 12 # * Redistributions of source code must retain the above copyright notice, this
13 13 # list of conditions and the following disclaimer.
14 14
15 15 # * Redistributions in binary form must reproduce the above copyright notice,
16 16 # this list of conditions and the following disclaimer in the documentation
17 17 # and/or other materials provided with the distribution.
18 18
19 19 # * Neither the name of the Zed A. Shaw nor the names of its contributors may
20 20 # be used to endorse or promote products derived from this software without
21 21 # specific prior written permission.
22 22
23 23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24 24 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25 25 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
26 26 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
27 27 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
28 28 # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29 29 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30 30 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
31 31 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
32 32 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 33 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 34 # POSSIBILITY OF SUCH DAMAGE.
35 35
36 36 import os
37 37 import mimetypes
38 38 import string
39 39 from email import encoders
40 40 from email.charset import Charset
41 41 from email.utils import parseaddr
42 42 from email.mime.base import MIMEBase
43 43
44 44 ADDRESS_HEADERS_WHITELIST = ['From', 'To', 'Delivered-To', 'Cc', 'Bcc']
45 45 DEFAULT_ENCODING = "utf-8"
46 46 VALUE_IS_EMAIL_ADDRESS = lambda v: '@' in v
47 47
48 48 def normalize_header(header):
49 49 return string.capwords(header.lower(), '-')
50 50
51 51 class EncodingError(Exception):
52 52 """Thrown when there is an encoding error."""
53 53 pass
54 54
55 55 class MailBase(object):
56 56 """MailBase is used as the basis of lamson.mail and contains the basics of
57 57 encoding an email. You actually can do all your email processing with this
58 58 class, but it's more raw.
59 59 """
60 60 def __init__(self, items=()):
61 61 self.headers = dict(items)
62 62 self.parts = []
63 63 self.body = None
64 64 self.content_encoding = {'Content-Type': (None, {}),
65 65 'Content-Disposition': (None, {}),
66 66 'Content-Transfer-Encoding': (None, {})}
67 67
68 68 def __getitem__(self, key):
69 69 return self.headers.get(normalize_header(key), None)
70 70
71 71 def __len__(self):
72 72 return len(self.headers)
73 73
74 74 def __iter__(self):
75 75 return iter(self.headers)
76 76
77 77 def __contains__(self, key):
78 78 return normalize_header(key) in self.headers
79 79
80 80 def __setitem__(self, key, value):
81 81 self.headers[normalize_header(key)] = value
82 82
83 83 def __delitem__(self, key):
84 84 del self.headers[normalize_header(key)]
85 85
86 86 def __nonzero__(self):
87 87 return self.body != None or len(self.headers) > 0 or len(self.parts) > 0
88 88
89 89 def keys(self):
90 90 """Returns the sorted keys."""
91 91 return sorted(self.headers.keys())
92 92
93 93 def attach_file(self, filename, data, ctype, disposition):
94 94 """
95 95 A file attachment is a raw attachment with a disposition that
96 96 indicates the file name.
97 97 """
98 98 assert filename, "You can't attach a file without a filename."
99 99 ctype = ctype.lower()
100 100
101 101 part = MailBase()
102 102 part.body = data
103 103 part.content_encoding['Content-Type'] = (ctype, {'name': filename})
104 104 part.content_encoding['Content-Disposition'] = (disposition,
105 105 {'filename': filename})
106 106 self.parts.append(part)
107 107
108 108
109 109 def attach_text(self, data, ctype):
110 110 """
111 111 This attaches a simpler text encoded part, which doesn't have a
112 112 filename.
113 113 """
114 114 ctype = ctype.lower()
115 115
116 116 part = MailBase()
117 117 part.body = data
118 118 part.content_encoding['Content-Type'] = (ctype, {})
119 119 self.parts.append(part)
120 120
121 121 def walk(self):
122 122 for p in self.parts:
123 123 yield p
124 124 for x in p.walk():
125 125 yield x
126 126
127 127 class MailResponse(object):
128 128 """
129 129 You are given MailResponse objects from the lamson.view methods, and
130 130 whenever you want to generate an email to send to someone. It has the
131 131 same basic functionality as MailRequest, but it is designed to be written
132 132 to, rather than read from (although you can do both).
133 133
134 134 You can easily set a Body or Html during creation or after by passing it
135 135 as __init__ parameters, or by setting those attributes.
136 136
137 137 You can initially set the From, To, and Subject, but they are headers so
138 138 use the dict notation to change them: msg['From'] = 'joe@test.com'.
139 139
140 140 The message is not fully crafted until right when you convert it with
141 141 MailResponse.to_message. This lets you change it and work with it, then
142 142 send it out when it's ready.
143 143 """
144 def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None):
144 def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None,
145 separator="; "):
145 146 self.Body = Body
146 147 self.Html = Html
147 148 self.base = MailBase([('To', To), ('From', From), ('Subject', Subject)])
148 149 self.multipart = self.Body and self.Html
149 150 self.attachments = []
151 self.separator = separator
150 152
151 153 def __contains__(self, key):
152 154 return self.base.__contains__(key)
153 155
154 156 def __getitem__(self, key):
155 157 return self.base.__getitem__(key)
156 158
157 159 def __setitem__(self, key, val):
158 160 return self.base.__setitem__(key, val)
159 161
160 162 def __delitem__(self, name):
161 163 del self.base[name]
162 164
163 165 def attach(self, filename=None, content_type=None, data=None,
164 166 disposition=None):
165 167 """
166 168
167 169 Simplifies attaching files from disk or data as files. To attach
168 170 simple text simple give data and a content_type. To attach a file,
169 171 give the data/content_type/filename/disposition combination.
170 172
171 173 For convenience, if you don't give data and only a filename, then it
172 174 will read that file's contents when you call to_message() later. If
173 175 you give data and filename then it will assume you've filled data
174 176 with what the file's contents are and filename is just the name to
175 177 use.
176 178 """
177 179
178 180 assert filename or data, ("You must give a filename or some data to "
179 181 "attach.")
180 182 assert data or os.path.exists(filename), ("File doesn't exist, and no "
181 183 "data given.")
182 184
183 185 self.multipart = True
184 186
185 187 if filename and not content_type:
186 188 content_type, encoding = mimetypes.guess_type(filename)
187 189
188 190 assert content_type, ("No content type given, and couldn't guess "
189 191 "from the filename: %r" % filename)
190 192
191 193 self.attachments.append({'filename': filename,
192 194 'content_type': content_type,
193 195 'data': data,
194 196 'disposition': disposition,})
195 197 def attach_part(self, part):
196 198 """
197 199 Attaches a raw MailBase part from a MailRequest (or anywhere)
198 200 so that you can copy it over.
199 201 """
200 202 self.multipart = True
201 203
202 204 self.attachments.append({'filename': None,
203 205 'content_type': None,
204 206 'data': None,
205 207 'disposition': None,
206 208 'part': part,
207 209 })
208 210
209 211 def attach_all_parts(self, mail_request):
210 212 """
211 213 Used for copying the attachment parts of a mail.MailRequest
212 214 object for mailing lists that need to maintain attachments.
213 215 """
214 216 for part in mail_request.all_parts():
215 217 self.attach_part(part)
216 218
217 219 self.base.content_encoding = mail_request.base.content_encoding.copy()
218 220
219 221 def clear(self):
220 222 """
221 223 Clears out the attachments so you can redo them. Use this to keep the
222 224 headers for a series of different messages with different attachments.
223 225 """
224 226 del self.attachments[:]
225 227 del self.base.parts[:]
226 228 self.multipart = False
227 229
228 230
229 231 def update(self, message):
230 232 """
231 233 Used to easily set a bunch of heading from another dict
232 234 like object.
233 235 """
234 236 for k in message.keys():
235 237 self.base[k] = message[k]
236 238
237 239 def __str__(self):
238 240 """
239 241 Converts to a string.
240 242 """
241 243 return self.to_message().as_string()
242 244
243 245 def _encode_attachment(self, filename=None, content_type=None, data=None,
244 246 disposition=None, part=None):
245 247 """
246 248 Used internally to take the attachments mentioned in self.attachments
247 249 and do the actual encoding in a lazy way when you call to_message.
248 250 """
249 251 if part:
250 252 self.base.parts.append(part)
251 253 elif filename:
252 254 if not data:
253 255 data = open(filename).read()
254 256
255 257 self.base.attach_file(filename, data, content_type,
256 258 disposition or 'attachment')
257 259 else:
258 260 self.base.attach_text(data, content_type)
259 261
260 262 ctype = self.base.content_encoding['Content-Type'][0]
261 263
262 264 if ctype and not ctype.startswith('multipart'):
263 265 self.base.content_encoding['Content-Type'] = ('multipart/mixed', {})
264 266
265 267 def to_message(self):
266 268 """
267 269 Figures out all the required steps to finally craft the
268 270 message you need and return it. The resulting message
269 271 is also available as a self.base attribute.
270 272
271 273 What is returned is a Python email API message you can
272 274 use with those APIs. The self.base attribute is the raw
273 275 lamson.encoding.MailBase.
274 276 """
275 277 del self.base.parts[:]
276 278
277 279 if self.Body and self.Html:
278 280 self.multipart = True
279 281 self.base.content_encoding['Content-Type'] = (
280 282 'multipart/alternative', {})
281 283
282 284 if self.multipart:
283 285 self.base.body = None
284 286 if self.Body:
285 287 self.base.attach_text(self.Body, 'text/plain')
286 288
287 289 if self.Html:
288 290 self.base.attach_text(self.Html, 'text/html')
289 291
290 292 for args in self.attachments:
291 293 self._encode_attachment(**args)
292 294
293 295 elif self.Body:
294 296 self.base.body = self.Body
295 297 self.base.content_encoding['Content-Type'] = ('text/plain', {})
296 298
297 299 elif self.Html:
298 300 self.base.body = self.Html
299 301 self.base.content_encoding['Content-Type'] = ('text/html', {})
300 302
301 return to_message(self.base)
303 return to_message(self.base, separator=self.separator)
302 304
303 305 def all_parts(self):
304 306 """
305 307 Returns all the encoded parts. Only useful for debugging
306 308 or inspecting after calling to_message().
307 309 """
308 310 return self.base.parts
309 311
310 312 def keys(self):
311 313 return self.base.keys()
312 314
313 def to_message(mail):
315 def to_message(mail, separator="; "):
314 316 """
315 317 Given a MailBase message, this will construct a MIMEPart
316 318 that is canonicalized for use with the Python email API.
317 319 """
318 320 ctype, params = mail.content_encoding['Content-Type']
319 321
320 322 if not ctype:
321 323 if mail.parts:
322 324 ctype = 'multipart/mixed'
323 325 else:
324 326 ctype = 'text/plain'
325 327 else:
326 328 if mail.parts:
327 329 assert ctype.startswith(("multipart", "message")), \
328 330 "Content type should be multipart or message, not %r" % ctype
329 331
330 332 # adjust the content type according to what it should be now
331 333 mail.content_encoding['Content-Type'] = (ctype, params)
332 334
333 335 try:
334 336 out = MIMEPart(ctype, **params)
335 337 except TypeError, exc: # pragma: no cover
336 338 raise EncodingError("Content-Type malformed, not allowed: %r; "
337 339 "%r (Python ERROR: %s" %
338 340 (ctype, params, exc.message))
339 341
340 342 for k in mail.keys():
341 343 if k in ADDRESS_HEADERS_WHITELIST:
342 out[k.encode('ascii')] = header_to_mime_encoding(mail[k])
344 out[k.encode('ascii')] = header_to_mime_encoding(
345 mail[k],
346 not_email=False,
347 separator=separator
348 )
343 349 else:
344 out[k.encode('ascii')] = header_to_mime_encoding(mail[k],
345 not_email=True)
350 out[k.encode('ascii')] = header_to_mime_encoding(
351 mail[k],
352 not_email=True
353 )
346 354
347 355 out.extract_payload(mail)
348 356
349 357 # go through the children
350 358 for part in mail.parts:
351 359 out.attach(to_message(part))
352 360
353 361 return out
354 362
355 363 class MIMEPart(MIMEBase):
356 364 """
357 365 A reimplementation of nearly everything in email.mime to be more useful
358 366 for actually attaching things. Rather than one class for every type of
359 367 thing you'd encode, there's just this one, and it figures out how to
360 368 encode what you ask it.
361 369 """
362 370 def __init__(self, type, **params):
363 371 self.maintype, self.subtype = type.split('/')
364 372 MIMEBase.__init__(self, self.maintype, self.subtype, **params)
365 373
366 374 def add_text(self, content):
367 375 # this is text, so encode it in canonical form
368 376 try:
369 377 encoded = content.encode('ascii')
370 378 charset = 'ascii'
371 379 except UnicodeError:
372 380 encoded = content.encode('utf-8')
373 381 charset = 'utf-8'
374 382
375 383 self.set_payload(encoded, charset=charset)
376 384
377 385
378 386 def extract_payload(self, mail):
379 387 if mail.body == None: return # only None, '' is still ok
380 388
381 389 ctype, ctype_params = mail.content_encoding['Content-Type']
382 390 cdisp, cdisp_params = mail.content_encoding['Content-Disposition']
383 391
384 392 assert ctype, ("Extract payload requires that mail.content_encoding "
385 393 "have a valid Content-Type.")
386 394
387 395 if ctype.startswith("text/"):
388 396 self.add_text(mail.body)
389 397 else:
390 398 if cdisp:
391 399 # replicate the content-disposition settings
392 400 self.add_header('Content-Disposition', cdisp, **cdisp_params)
393 401
394 402 self.set_payload(mail.body)
395 403 encoders.encode_base64(self)
396 404
397 405 def __repr__(self):
398 406 return "<MIMEPart '%s/%s': %r, %r, multipart=%r>" % (
399 407 self.subtype,
400 408 self.maintype,
401 409 self['Content-Type'],
402 410 self['Content-Disposition'],
403 411 self.is_multipart())
404 412
405 413
406 def header_to_mime_encoding(value, not_email=False):
414 def header_to_mime_encoding(value, not_email=False, separator=", "):
407 415 if not value: return ""
408 416
409 417 encoder = Charset(DEFAULT_ENCODING)
410 418 if type(value) == list:
411 return "; ".join(properly_encode_header(
419 return separator.join(properly_encode_header(
412 420 v, encoder, not_email) for v in value)
413 421 else:
414 422 return properly_encode_header(value, encoder, not_email)
415 423
416 424 def properly_encode_header(value, encoder, not_email):
417 425 """
418 426 The only thing special (weird) about this function is that it tries
419 427 to do a fast check to see if the header value has an email address in
420 428 it. Since random headers could have an email address, and email addresses
421 429 have weird special formatting rules, we have to check for it.
422 430
423 431 Normally this works fine, but in Librelist, we need to "obfuscate" email
424 432 addresses by changing the '@' to '-AT-'. This is where
425 433 VALUE_IS_EMAIL_ADDRESS exists. It's a simple lambda returning True/False
426 434 to check if a header value has an email address. If you need to make this
427 435 check different, then change this.
428 436 """
429 437 try:
430 438 return value.encode("ascii")
431 439 except UnicodeEncodeError:
432 440 if not_email is False and VALUE_IS_EMAIL_ADDRESS(value):
433 441 # this could have an email address, make sure we don't screw it up
434 442 name, address = parseaddr(value)
435 443 return '"%s" <%s>' % (
436 444 encoder.header_encode(name.encode("utf-8")), address)
437 445
438 446 return encoder.header_encode(value.encode("utf-8"))
@@ -1,93 +1,94 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.rcmail.smtp_mailer
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Simple smtp mailer used in RhodeCode
7 7
8 8 :created_on: Sep 13, 2010
9 9 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 10 :license: GPLv3, see COPYING for more details.
11 11 """
12 12 # This program is free software: you can redistribute it and/or modify
13 13 # it under the terms of the GNU General Public License as published by
14 14 # the Free Software Foundation, either version 3 of the License, or
15 15 # (at your option) any later version.
16 16 #
17 17 # This program is distributed in the hope that it will be useful,
18 18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 20 # GNU General Public License for more details.
21 21 #
22 22 # You should have received a copy of the GNU General Public License
23 23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 24
25 25 import logging
26 26 import smtplib
27 27 from socket import sslerror
28 28 from rhodecode.lib.rcmail.message import Message
29 29
30 30
31 31 class SmtpMailer(object):
32 32 """SMTP mailer class
33 33
34 34 mailer = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth
35 35 mail_port, ssl, tls)
36 36 mailer.send(recipients, subject, body, attachment_files)
37 37
38 38 :param recipients might be a list of string or single string
39 39 :param attachment_files is a dict of {filename:location}
40 40 it tries to guess the mimetype and attach the file
41 41
42 42 """
43 43
44 44 def __init__(self, mail_from, user, passwd, mail_server, smtp_auth=None,
45 45 mail_port=None, ssl=False, tls=False, debug=False):
46 46
47 47 self.mail_from = mail_from
48 48 self.mail_server = mail_server
49 49 self.mail_port = mail_port
50 50 self.user = user
51 51 self.passwd = passwd
52 52 self.ssl = ssl
53 53 self.tls = tls
54 54 self.debug = debug
55 55 self.auth = smtp_auth
56 56
57 57 def send(self, recipients=[], subject='', body='', html='',
58 58 attachment_files=None):
59 59
60 60 if isinstance(recipients, basestring):
61 61 recipients = [recipients]
62 msg = Message(subject, recipients, body, html, self.mail_from)
62 msg = Message(subject, recipients, body, html, self.mail_from,
63 recipients_separator=", ")
63 64 raw_msg = msg.to_message()
64 65
65 66 if self.ssl:
66 67 smtp_serv = smtplib.SMTP_SSL(self.mail_server, self.mail_port)
67 68 else:
68 69 smtp_serv = smtplib.SMTP(self.mail_server, self.mail_port)
69 70
70 71 if self.tls:
71 72 smtp_serv.ehlo()
72 73 smtp_serv.starttls()
73 74
74 75 if self.debug:
75 76 smtp_serv.set_debuglevel(1)
76 77
77 78 smtp_serv.ehlo()
78 79 if self.auth:
79 80 smtp_serv.esmtp_features["auth"] = self.auth
80 81
81 82 # if server requires authorization you must provide login and password
82 83 # but only if we have them
83 84 if self.user and self.passwd:
84 85 smtp_serv.login(self.user, self.passwd)
85 86
86 87 smtp_serv.sendmail(msg.sender, msg.send_to, raw_msg.as_string())
87 88 logging.info('MAIL SEND TO: %s' % recipients)
88 89
89 90 try:
90 91 smtp_serv.quit()
91 92 except sslerror:
92 93 # sslerror is raised in tls connections on closing sometimes
93 94 pass
@@ -1,599 +1,599 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Utilities library for RhodeCode
7 7
8 8 :created_on: Apr 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 os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 import paste
31 31 import beaker
32 32 import tarfile
33 33 import shutil
34 34 from os.path import abspath
35 35 from os.path import dirname as dn, join as jn
36 36
37 37 from paste.script.command import Command, BadCommand
38 38
39 39 from mercurial import ui, config
40 40
41 41 from webhelpers.text import collapse, remove_formatting, strip_tags
42 42
43 43 from vcs import get_backend
44 44 from vcs.backends.base import BaseChangeset
45 45 from vcs.utils.lazy import LazyProperty
46 46 from vcs.utils.helpers import get_scm
47 47 from vcs.exceptions import VCSError
48 48
49 49 from rhodecode.lib.caching_query import FromCache
50 50
51 51 from rhodecode.model import meta
52 52 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
53 53 UserLog, RepoGroup, RhodeCodeSetting
54 54 from rhodecode.model.meta import Session
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def recursive_replace(str_, replace=' '):
60 60 """Recursive replace of given sign to just one instance
61 61
62 62 :param str_: given string
63 63 :param replace: char to find and replace multiple instances
64 64
65 65 Examples::
66 66 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
67 67 'Mighty-Mighty-Bo-sstones'
68 68 """
69 69
70 70 if str_.find(replace * 2) == -1:
71 71 return str_
72 72 else:
73 73 str_ = str_.replace(replace * 2, replace)
74 74 return recursive_replace(str_, replace)
75 75
76 76
77 77 def repo_name_slug(value):
78 78 """Return slug of name of repository
79 79 This function is called on each creation/modification
80 80 of repository to prevent bad names in repo
81 81 """
82 82
83 83 slug = remove_formatting(value)
84 84 slug = strip_tags(slug)
85 85
86 86 for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """:
87 87 slug = slug.replace(c, '-')
88 88 slug = recursive_replace(slug, '-')
89 89 slug = collapse(slug, '-')
90 90 return slug
91 91
92 92
93 93 def get_repo_slug(request):
94 94 return request.environ['pylons.routes_dict'].get('repo_name')
95 95
96 96
97 97 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
98 98 """
99 99 Action logger for various actions made by users
100 100
101 101 :param user: user that made this action, can be a unique username string or
102 102 object containing user_id attribute
103 103 :param action: action to log, should be on of predefined unique actions for
104 104 easy translations
105 105 :param repo: string name of repository or object containing repo_id,
106 106 that action was made on
107 107 :param ipaddr: optional ip address from what the action was made
108 108 :param sa: optional sqlalchemy session
109 109
110 110 """
111 111
112 112 if not sa:
113 113 sa = meta.Session
114 114
115 115 try:
116 116 if hasattr(user, 'user_id'):
117 117 user_obj = user
118 118 elif isinstance(user, basestring):
119 119 user_obj = User.get_by_username(user)
120 120 else:
121 121 raise Exception('You have to provide user object or username')
122 122
123 123 if hasattr(repo, 'repo_id'):
124 124 repo_obj = Repository.get(repo.repo_id)
125 125 repo_name = repo_obj.repo_name
126 126 elif isinstance(repo, basestring):
127 127 repo_name = repo.lstrip('/')
128 128 repo_obj = Repository.get_by_repo_name(repo_name)
129 129 else:
130 130 raise Exception('You have to provide repository to action logger')
131 131
132 132 user_log = UserLog()
133 133 user_log.user_id = user_obj.user_id
134 134 user_log.action = action
135 135
136 136 user_log.repository_id = repo_obj.repo_id
137 137 user_log.repository_name = repo_name
138 138
139 139 user_log.action_date = datetime.datetime.now()
140 140 user_log.user_ip = ipaddr
141 141 sa.add(user_log)
142 142
143 143 log.info('Adding user %s, action %s on %s', user_obj, action, repo)
144 144 if commit:
145 145 sa.commit()
146 146 except:
147 147 log.error(traceback.format_exc())
148 148 raise
149 149
150 150
151 151 def get_repos(path, recursive=False):
152 152 """
153 153 Scans given path for repos and return (name,(type,path)) tuple
154 154
155 :param path: path to scann for repositories
155 :param path: path to scan for repositories
156 156 :param recursive: recursive search and return names with subdirs in front
157 157 """
158 158
159 159 # remove ending slash for better results
160 path = path.rstrip('/')
160 path = path.rstrip(os.sep)
161 161
162 162 def _get_repos(p):
163 163 if not os.access(p, os.W_OK):
164 164 return
165 165 for dirpath in os.listdir(p):
166 166 if os.path.isfile(os.path.join(p, dirpath)):
167 167 continue
168 168 cur_path = os.path.join(p, dirpath)
169 169 try:
170 170 scm_info = get_scm(cur_path)
171 171 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
172 172 except VCSError:
173 173 if not recursive:
174 174 continue
175 175 #check if this dir containts other repos for recursive scan
176 176 rec_path = os.path.join(p, dirpath)
177 177 if os.path.isdir(rec_path):
178 178 for inner_scm in _get_repos(rec_path):
179 179 yield inner_scm
180 180
181 181 return _get_repos(path)
182 182
183 183
184 184 def is_valid_repo(repo_name, base_path):
185 185 """
186 186 Returns True if given path is a valid repository False otherwise
187 187 :param repo_name:
188 188 :param base_path:
189 189
190 190 :return True: if given path is a valid repository
191 191 """
192 192 full_path = os.path.join(base_path, repo_name)
193 193
194 194 try:
195 195 get_scm(full_path)
196 196 return True
197 197 except VCSError:
198 198 return False
199 199
200 200 def is_valid_repos_group(repos_group_name, base_path):
201 201 """
202 202 Returns True if given path is a repos group False otherwise
203 203
204 204 :param repo_name:
205 205 :param base_path:
206 206 """
207 207 full_path = os.path.join(base_path, repos_group_name)
208 208
209 209 # check if it's not a repo
210 210 if is_valid_repo(repos_group_name, base_path):
211 211 return False
212 212
213 213 # check if it's a valid path
214 214 if os.path.isdir(full_path):
215 215 return True
216 216
217 217 return False
218 218
219 219 def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
220 220 while True:
221 221 ok = raw_input(prompt)
222 222 if ok in ('y', 'ye', 'yes'):
223 223 return True
224 224 if ok in ('n', 'no', 'nop', 'nope'):
225 225 return False
226 226 retries = retries - 1
227 227 if retries < 0:
228 228 raise IOError
229 229 print complaint
230 230
231 231 #propagated from mercurial documentation
232 232 ui_sections = ['alias', 'auth',
233 233 'decode/encode', 'defaults',
234 234 'diff', 'email',
235 235 'extensions', 'format',
236 236 'merge-patterns', 'merge-tools',
237 237 'hooks', 'http_proxy',
238 238 'smtp', 'patch',
239 239 'paths', 'profiling',
240 240 'server', 'trusted',
241 241 'ui', 'web', ]
242 242
243 243
244 244 def make_ui(read_from='file', path=None, checkpaths=True):
245 245 """A function that will read python rc files or database
246 246 and make an mercurial ui object from read options
247 247
248 248 :param path: path to mercurial config file
249 249 :param checkpaths: check the path
250 250 :param read_from: read from 'file' or 'db'
251 251 """
252 252
253 253 baseui = ui.ui()
254 254
255 255 #clean the baseui object
256 256 baseui._ocfg = config.config()
257 257 baseui._ucfg = config.config()
258 258 baseui._tcfg = config.config()
259 259
260 260 if read_from == 'file':
261 261 if not os.path.isfile(path):
262 262 log.warning('Unable to read config file %s' % path)
263 263 return False
264 264 log.debug('reading hgrc from %s', path)
265 265 cfg = config.config()
266 266 cfg.read(path)
267 267 for section in ui_sections:
268 268 for k, v in cfg.items(section):
269 269 log.debug('settings ui from file[%s]%s:%s', section, k, v)
270 270 baseui.setconfig(section, k, v)
271 271
272 272 elif read_from == 'db':
273 273 sa = meta.Session
274 274 ret = sa.query(RhodeCodeUi)\
275 275 .options(FromCache("sql_cache_short",
276 276 "get_hg_ui_settings")).all()
277 277
278 278 hg_ui = ret
279 279 for ui_ in hg_ui:
280 280 if ui_.ui_active:
281 281 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
282 282 ui_.ui_key, ui_.ui_value)
283 283 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
284 284
285 285 meta.Session.remove()
286 286 return baseui
287 287
288 288
289 289 def set_rhodecode_config(config):
290 290 """
291 291 Updates pylons config with new settings from database
292 292
293 293 :param config:
294 294 """
295 295 hgsettings = RhodeCodeSetting.get_app_settings()
296 296
297 297 for k, v in hgsettings.items():
298 298 config[k] = v
299 299
300 300
301 301 def invalidate_cache(cache_key, *args):
302 302 """
303 303 Puts cache invalidation task into db for
304 304 further global cache invalidation
305 305 """
306 306
307 307 from rhodecode.model.scm import ScmModel
308 308
309 309 if cache_key.startswith('get_repo_cached_'):
310 310 name = cache_key.split('get_repo_cached_')[-1]
311 311 ScmModel().mark_for_invalidation(name)
312 312
313 313
314 314 class EmptyChangeset(BaseChangeset):
315 315 """
316 316 An dummy empty changeset. It's possible to pass hash when creating
317 317 an EmptyChangeset
318 318 """
319 319
320 320 def __init__(self, cs='0' * 40, repo=None, requested_revision=None, alias=None):
321 321 self._empty_cs = cs
322 322 self.revision = -1
323 323 self.message = ''
324 324 self.author = ''
325 325 self.date = ''
326 326 self.repository = repo
327 327 self.requested_revision = requested_revision
328 328 self.alias = alias
329 329
330 330 @LazyProperty
331 331 def raw_id(self):
332 332 """
333 333 Returns raw string identifying this changeset, useful for web
334 334 representation.
335 335 """
336 336
337 337 return self._empty_cs
338 338
339 339 @LazyProperty
340 340 def branch(self):
341 341 return get_backend(self.alias).DEFAULT_BRANCH_NAME
342 342
343 343 @LazyProperty
344 344 def short_id(self):
345 345 return self.raw_id[:12]
346 346
347 347 def get_file_changeset(self, path):
348 348 return self
349 349
350 350 def get_file_content(self, path):
351 351 return u''
352 352
353 353 def get_file_size(self, path):
354 354 return 0
355 355
356 356
357 357 def map_groups(groups):
358 358 """
359 359 Checks for groups existence, and creates groups structures.
360 360 It returns last group in structure
361 361
362 362 :param groups: list of groups structure
363 363 """
364 364 sa = meta.Session
365 365
366 366 parent = None
367 367 group = None
368 368
369 369 # last element is repo in nested groups structure
370 370 groups = groups[:-1]
371 371
372 372 for lvl, group_name in enumerate(groups):
373 373 group_name = '/'.join(groups[:lvl] + [group_name])
374 374 group = sa.query(RepoGroup).filter(RepoGroup.group_name == group_name).scalar()
375 375
376 376 if group is None:
377 377 group = RepoGroup(group_name, parent)
378 378 sa.add(group)
379 379 sa.commit()
380 380 parent = group
381 381 return group
382 382
383 383
384 384 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
385 385 """
386 386 maps all repos given in initial_repo_list, non existing repositories
387 387 are created, if remove_obsolete is True it also check for db entries
388 388 that are not in initial_repo_list and removes them.
389 389
390 390 :param initial_repo_list: list of repositories found by scanning methods
391 391 :param remove_obsolete: check for obsolete entries in database
392 392 """
393 393 from rhodecode.model.repo import RepoModel
394 394 sa = meta.Session
395 395 rm = RepoModel()
396 396 user = sa.query(User).filter(User.admin == True).first()
397 397 if user is None:
398 398 raise Exception('Missing administrative account !')
399 399 added = []
400 400
401 401 for name, repo in initial_repo_list.items():
402 402 group = map_groups(name.split(Repository.url_sep()))
403 403 if not rm.get_by_repo_name(name, cache=False):
404 404 log.info('repository %s not found creating default', name)
405 405 added.append(name)
406 406 form_data = {
407 407 'repo_name': name,
408 408 'repo_name_full': name,
409 409 'repo_type': repo.alias,
410 410 'description': repo.description \
411 411 if repo.description != 'unknown' else \
412 412 '%s repository' % name,
413 413 'private': False,
414 414 'group_id': getattr(group, 'group_id', None)
415 415 }
416 416 rm.create(form_data, user, just_db=True)
417 417 sa.commit()
418 418 removed = []
419 419 if remove_obsolete:
420 420 #remove from database those repositories that are not in the filesystem
421 421 for repo in sa.query(Repository).all():
422 422 if repo.repo_name not in initial_repo_list.keys():
423 423 removed.append(repo.repo_name)
424 424 sa.delete(repo)
425 425 sa.commit()
426 426
427 427 return added, removed
428 428
429 429 # set cache regions for beaker so celery can utilise it
430 430 def add_cache(settings):
431 431 cache_settings = {'regions': None}
432 432 for key in settings.keys():
433 433 for prefix in ['beaker.cache.', 'cache.']:
434 434 if key.startswith(prefix):
435 435 name = key.split(prefix)[1].strip()
436 436 cache_settings[name] = settings[key].strip()
437 437 if cache_settings['regions']:
438 438 for region in cache_settings['regions'].split(','):
439 439 region = region.strip()
440 440 region_settings = {}
441 441 for key, value in cache_settings.items():
442 442 if key.startswith(region):
443 443 region_settings[key.split('.')[1]] = value
444 444 region_settings['expire'] = int(region_settings.get('expire',
445 445 60))
446 446 region_settings.setdefault('lock_dir',
447 447 cache_settings.get('lock_dir'))
448 448 region_settings.setdefault('data_dir',
449 449 cache_settings.get('data_dir'))
450 450
451 451 if 'type' not in region_settings:
452 452 region_settings['type'] = cache_settings.get('type',
453 453 'memory')
454 454 beaker.cache.cache_regions[region] = region_settings
455 455
456 456
457 457 #==============================================================================
458 458 # TEST FUNCTIONS AND CREATORS
459 459 #==============================================================================
460 460 def create_test_index(repo_location, config, full_index):
461 461 """
462 462 Makes default test index
463 463
464 464 :param config: test config
465 465 :param full_index:
466 466 """
467 467
468 468 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
469 469 from rhodecode.lib.pidlock import DaemonLock, LockHeld
470 470
471 471 repo_location = repo_location
472 472
473 473 index_location = os.path.join(config['app_conf']['index_dir'])
474 474 if not os.path.exists(index_location):
475 475 os.makedirs(index_location)
476 476
477 477 try:
478 478 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
479 479 WhooshIndexingDaemon(index_location=index_location,
480 480 repo_location=repo_location)\
481 481 .run(full_index=full_index)
482 482 l.release()
483 483 except LockHeld:
484 484 pass
485 485
486 486
487 487 def create_test_env(repos_test_path, config):
488 488 """
489 489 Makes a fresh database and
490 490 install test repository into tmp dir
491 491 """
492 492 from rhodecode.lib.db_manage import DbManage
493 493 from rhodecode.tests import HG_REPO, TESTS_TMP_PATH
494 494
495 495 # PART ONE create db
496 496 dbconf = config['sqlalchemy.db1.url']
497 497 log.debug('making test db %s', dbconf)
498 498
499 499 # create test dir if it doesn't exist
500 500 if not os.path.isdir(repos_test_path):
501 501 log.debug('Creating testdir %s' % repos_test_path)
502 502 os.makedirs(repos_test_path)
503 503
504 504 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
505 505 tests=True)
506 506 dbmanage.create_tables(override=True)
507 507 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
508 508 dbmanage.create_default_user()
509 509 dbmanage.admin_prompt()
510 510 dbmanage.create_permissions()
511 511 dbmanage.populate_default_permissions()
512 512 Session.commit()
513 513 # PART TWO make test repo
514 514 log.debug('making test vcs repositories')
515 515
516 516 idx_path = config['app_conf']['index_dir']
517 517 data_path = config['app_conf']['cache_dir']
518 518
519 519 #clean index and data
520 520 if idx_path and os.path.exists(idx_path):
521 521 log.debug('remove %s' % idx_path)
522 522 shutil.rmtree(idx_path)
523 523
524 524 if data_path and os.path.exists(data_path):
525 525 log.debug('remove %s' % data_path)
526 526 shutil.rmtree(data_path)
527 527
528 528 #CREATE DEFAULT HG REPOSITORY
529 529 cur_dir = dn(dn(abspath(__file__)))
530 530 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
531 531 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
532 532 tar.close()
533 533
534 534
535 535 #==============================================================================
536 536 # PASTER COMMANDS
537 537 #==============================================================================
538 538 class BasePasterCommand(Command):
539 539 """
540 540 Abstract Base Class for paster commands.
541 541
542 542 The celery commands are somewhat aggressive about loading
543 543 celery.conf, and since our module sets the `CELERY_LOADER`
544 544 environment variable to our loader, we have to bootstrap a bit and
545 545 make sure we've had a chance to load the pylons config off of the
546 546 command line, otherwise everything fails.
547 547 """
548 548 min_args = 1
549 549 min_args_error = "Please provide a paster config file as an argument."
550 550 takes_config_file = 1
551 551 requires_config_file = True
552 552
553 553 def notify_msg(self, msg, log=False):
554 554 """Make a notification to user, additionally if logger is passed
555 555 it logs this action using given logger
556 556
557 557 :param msg: message that will be printed to user
558 558 :param log: logging instance, to use to additionally log this message
559 559
560 560 """
561 561 if log and isinstance(log, logging):
562 562 log(msg)
563 563
564 564 def run(self, args):
565 565 """
566 566 Overrides Command.run
567 567
568 568 Checks for a config file argument and loads it.
569 569 """
570 570 if len(args) < self.min_args:
571 571 raise BadCommand(
572 572 self.min_args_error % {'min_args': self.min_args,
573 573 'actual_args': len(args)})
574 574
575 575 # Decrement because we're going to lob off the first argument.
576 576 # @@ This is hacky
577 577 self.min_args -= 1
578 578 self.bootstrap_config(args[0])
579 579 self.update_parser()
580 580 return super(BasePasterCommand, self).run(args[1:])
581 581
582 582 def update_parser(self):
583 583 """
584 584 Abstract method. Allows for the class's parser to be updated
585 585 before the superclass's `run` method is called. Necessary to
586 586 allow options/arguments to be passed through to the underlying
587 587 celery command.
588 588 """
589 589 raise NotImplementedError("Abstract Method.")
590 590
591 591 def bootstrap_config(self, conf):
592 592 """
593 593 Loads the pylons configuration.
594 594 """
595 595 from pylons import config as pylonsconfig
596 596
597 597 path_to_ini_file = os.path.realpath(conf)
598 598 conf = paste.deploy.appconfig('config:' + path_to_ini_file)
599 599 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
@@ -1,219 +1,219 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.notification
4 ~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Model for notifications
7 7
8 8
9 9 :created_on: Nov 20, 2011
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 logging
29 29 import traceback
30 30 import datetime
31 31
32 32 from pylons.i18n.translation import _
33 33
34 34 import rhodecode
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.model import BaseModel
37 37 from rhodecode.model.db import Notification, User, UserNotification
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class NotificationModel(BaseModel):
43 43
44 44 def __get_user(self, user):
45 45 if isinstance(user, basestring):
46 46 return User.get_by_username(username=user)
47 47 else:
48 48 return self._get_instance(User, user)
49 49
50 50 def __get_notification(self, notification):
51 51 if isinstance(notification, Notification):
52 52 return notification
53 53 elif isinstance(notification, int):
54 54 return Notification.get(notification)
55 55 else:
56 56 if notification:
57 57 raise Exception('notification must be int or Instance'
58 58 ' of Notification got %s' % type(notification))
59 59
60 60 def create(self, created_by, subject, body, recipients=None,
61 61 type_=Notification.TYPE_MESSAGE, with_email=True,
62 62 email_kwargs={}):
63 63 """
64 64
65 65 Creates notification of given type
66 66
67 67 :param created_by: int, str or User instance. User who created this
68 68 notification
69 69 :param subject:
70 70 :param body:
71 71 :param recipients: list of int, str or User objects, when None
72 72 is given send to all admins
73 73 :param type_: type of notification
74 74 :param with_email: send email with this notification
75 75 :param email_kwargs: additional dict to pass as args to email template
76 76 """
77 77 from rhodecode.lib.celerylib import tasks, run_task
78 78
79 79 if recipients and not getattr(recipients, '__iter__', False):
80 80 raise Exception('recipients must be a list of iterable')
81 81
82 82 created_by_obj = self.__get_user(created_by)
83 83
84 84 if recipients:
85 85 recipients_objs = []
86 86 for u in recipients:
87 87 obj = self.__get_user(u)
88 88 if obj:
89 89 recipients_objs.append(obj)
90 90 recipients_objs = set(recipients_objs)
91 91 else:
92 92 # empty recipients means to all admins
93 93 recipients_objs = User.query().filter(User.admin == True).all()
94 94
95 95 notif = Notification.create(created_by=created_by_obj, subject=subject,
96 96 body=body, recipients=recipients_objs,
97 97 type_=type_)
98 98
99 99 if with_email is False:
100 100 return notif
101 101
102 102 # send email with notification
103 103 for rec in recipients_objs:
104 104 email_subject = NotificationModel().make_description(notif, False)
105 105 type_ = type_
106 106 email_body = body
107 107 kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
108 108 kwargs.update(email_kwargs)
109 109 email_body_html = EmailNotificationModel()\
110 110 .get_email_tmpl(type_, **kwargs)
111 111 run_task(tasks.send_email, rec.email, email_subject, email_body,
112 112 email_body_html)
113 113
114 114 return notif
115 115
116 116 def delete(self, user, notification):
117 117 # we don't want to remove actual notification just the assignment
118 118 try:
119 119 notification = self.__get_notification(notification)
120 120 user = self.__get_user(user)
121 121 if notification and user:
122 122 obj = UserNotification.query()\
123 123 .filter(UserNotification.user == user)\
124 124 .filter(UserNotification.notification
125 125 == notification)\
126 126 .one()
127 127 self.sa.delete(obj)
128 128 return True
129 129 except Exception:
130 130 log.error(traceback.format_exc())
131 131 raise
132 132
133 133 def get_for_user(self, user):
134 134 user = self.__get_user(user)
135 135 return user.notifications
136 136
137 137 def mark_all_read_for_user(self, user):
138 138 user = self.__get_user(user)
139 139 UserNotification.query()\
140 140 .filter(UserNotification.read==False)\
141 141 .update({'read': True})
142 142
143 143 def get_unread_cnt_for_user(self, user):
144 144 user = self.__get_user(user)
145 145 return UserNotification.query()\
146 146 .filter(UserNotification.read == False)\
147 147 .filter(UserNotification.user == user).count()
148 148
149 149 def get_unread_for_user(self, user):
150 150 user = self.__get_user(user)
151 151 return [x.notification for x in UserNotification.query()\
152 152 .filter(UserNotification.read == False)\
153 153 .filter(UserNotification.user == user).all()]
154 154
155 155 def get_user_notification(self, user, notification):
156 156 user = self.__get_user(user)
157 157 notification = self.__get_notification(notification)
158 158
159 159 return UserNotification.query()\
160 160 .filter(UserNotification.notification == notification)\
161 161 .filter(UserNotification.user == user).scalar()
162 162
163 163 def make_description(self, notification, show_age=True):
164 164 """
165 165 Creates a human readable description based on properties
166 166 of notification object
167 167 """
168 168
169 169 _map = {notification.TYPE_CHANGESET_COMMENT:_('commented on commit'),
170 170 notification.TYPE_MESSAGE:_('sent message'),
171 171 notification.TYPE_MENTION:_('mentioned you'),
172 172 notification.TYPE_REGISTRATION:_('registered in RhodeCode')}
173 173
174 174 DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
175 175
176 176 tmpl = "%(user)s %(action)s %(when)s"
177 177 if show_age:
178 178 when = h.age(notification.created_on)
179 179 else:
180 180 DTF = lambda d: datetime.datetime.strftime(d, DATETIME_FORMAT)
181 181 when = DTF(notification.created_on)
182 182 data = dict(user=notification.created_by_user.username,
183 183 action=_map[notification.type_],
184 184 when=when)
185 185 return tmpl % data
186 186
187 187
188 188 class EmailNotificationModel(BaseModel):
189 189
190 190 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
191 191 TYPE_PASSWORD_RESET = 'passoword_link'
192 192 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
193 193 TYPE_DEFAULT = 'default'
194 194
195 195 def __init__(self):
196 196 self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0]
197 197 self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
198 198
199 199 self.email_types = {
200 200 self.TYPE_CHANGESET_COMMENT:'email_templates/changeset_comment.html',
201 201 self.TYPE_PASSWORD_RESET:'email_templates/password_reset.html',
202 202 self.TYPE_REGISTRATION:'email_templates/registration.html',
203 203 self.TYPE_DEFAULT:'email_templates/default.html'
204 204 }
205 205
206 206 def get_email_tmpl(self, type_, **kwargs):
207 207 """
208 208 return generated template for email based on given type
209 209
210 210 :param type_:
211 211 """
212 212
213 213 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
214 214 email_template = self._tmpl_lookup.get_template(base)
215 215 # translator inject
216 216 _kwargs = {'_':_}
217 217 _kwargs.update(kwargs)
218 218 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
219 219 return email_template.render(**_kwargs)
@@ -1,192 +1,192 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.html"/>
4 4
5 5 <%def name="title()">
6 6 ${c.repo_name} ${_('Changelog')} - ${c.rhodecode_name}
7 7 </%def>
8 8
9 9 <%def name="breadcrumbs_links()">
10 10 ${h.link_to(u'Home',h.url('/'))}
11 11 &raquo;
12 12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 13 &raquo;
14 14 ${_('Changelog')} - ${_('showing ')} ${c.size if c.size <= c.total_cs else c.total_cs} ${_('out of')} ${c.total_cs} ${_('revisions')}
15 15 </%def>
16 16
17 17 <%def name="page_nav()">
18 18 ${self.menu('changelog')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <div class="box">
23 23 <!-- box / title -->
24 24 <div class="title">
25 25 ${self.breadcrumbs()}
26 26 </div>
27 27 <div class="table">
28 28 % if c.pagination:
29 29 <div id="graph">
30 30 <div id="graph_nodes">
31 31 <canvas id="graph_canvas"></canvas>
32 32 </div>
33 33 <div id="graph_content">
34 34 <div class="container_header">
35 35 ${h.form(h.url.current(),method='get')}
36 36 <div class="info_box" style="float:left">
37 37 ${h.submit('set',_('Show'),class_="ui-btn")}
38 38 ${h.text('size',size=1,value=c.size)}
39 39 ${_('revisions')}
40 40 </div>
41 41 ${h.end_form()}
42 42 <div id="rev_range_container" style="display:none"></div>
43 43 <div style="float:right">${h.select('branch_filter',c.branch_name,c.branch_filters)}</div>
44 44 </div>
45 45
46 46 %for cnt,cs in enumerate(c.pagination):
47 47 <div id="chg_${cnt+1}" class="container ${'tablerow1' if cnt%2==0 else 'tablerow2'}">
48 48 <div class="left">
49 49 <div>
50 50 ${h.checkbox(cs.short_id,class_="changeset_range")}
51 <span class="changeset_id">${cs.revision}:<span class="changeset_hash">${h.short_id(cs.raw_id)}</span></span>
51 <span class="tooltip" title="${cs.date}"><a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}"><span class="changeset_id">${cs.revision}:<span class="changeset_hash">${h.short_id(cs.raw_id)}</span></span></a></span>
52 52 </div>
53 53 <div class="author">
54 54 <div class="gravatar">
55 55 <img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),16)}"/>
56 56 </div>
57 57 <div title="${cs.author}" class="user">${h.person(cs.author)}</div>
58 58 </div>
59 59 <div class="date">${cs.date}</div>
60 60 </div>
61 61 <div class="mid">
62 62 <div class="message">${h.link_to(h.wrap_paragraphs(cs.message),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
63 63 </div>
64 64 <div class="right">
65 65 <div id="${cs.raw_id}_changes_info" class="changes">
66 66 <span id="${cs.raw_id}" class="changed_total tooltip" title="${_('Affected number of files, click to show more details')}">${len(cs.affected_files)}</span>
67 67 </div>
68 68 %if cs.parents:
69 69 %for p_cs in reversed(cs.parents):
70 70 <div class="parent">${_('Parent')}
71 71 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
72 72 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
73 73 </div>
74 74 %endfor
75 75 %else:
76 76 <div class="parent">${_('No parents')}</div>
77 77 %endif
78 78
79 79 <span class="logtags">
80 80 %if len(cs.parents)>1:
81 81 <span class="merge">${_('merge')}</span>
82 82 %endif
83 83 %if cs.branch:
84 84 <span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
85 85 ${h.link_to(cs.branch,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
86 86 %endif
87 87 %for tag in cs.tags:
88 88 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
89 89 ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
90 90 %endfor
91 91 </span>
92 92 </div>
93 93 </div>
94 94
95 95 %endfor
96 96 <div class="pagination-wh pagination-left">
97 97 ${c.pagination.pager('$link_previous ~2~ $link_next')}
98 98 </div>
99 99 </div>
100 100 </div>
101 101
102 102 <script type="text/javascript" src="${h.url('/js/graph.js')}"></script>
103 103 <script type="text/javascript">
104 104 YAHOO.util.Event.onDOMReady(function(){
105 105
106 106 //Monitor range checkboxes and build a link to changesets
107 107 //ranges
108 108 var checkboxes = YUD.getElementsByClassName('changeset_range');
109 109 var url_tmpl = "${h.url('changeset_home',repo_name=c.repo_name,revision='__REVRANGE__')}";
110 110 YUE.on(checkboxes,'click',function(e){
111 111 var checked_checkboxes = [];
112 112 for (pos in checkboxes){
113 113 if(checkboxes[pos].checked){
114 114 checked_checkboxes.push(checkboxes[pos]);
115 115 }
116 116 }
117 117 if(checked_checkboxes.length>1){
118 118 var rev_end = checked_checkboxes[0].name;
119 119 var rev_start = checked_checkboxes[checked_checkboxes.length-1].name;
120 120
121 121 var url = url_tmpl.replace('__REVRANGE__',
122 122 rev_start+'...'+rev_end);
123 123
124 124 var link = "<a href="+url+">${_('Show selected changes __S -> __E')}</a>"
125 125 link = link.replace('__S',rev_start);
126 126 link = link.replace('__E',rev_end);
127 127 YUD.get('rev_range_container').innerHTML = link;
128 128 YUD.setStyle('rev_range_container','display','');
129 129 }
130 130 else{
131 131 YUD.setStyle('rev_range_container','display','none');
132 132
133 133 }
134 134 });
135 135
136 136 // Fetch changeset details
137 137 YUE.on(YUD.getElementsByClassName('changed_total'),'click',function(e){
138 138 var id = e.currentTarget.id
139 139 var url = "${h.url('changelog_details',repo_name=c.repo_name,cs='__CS__')}"
140 140 var url = url.replace('__CS__',id);
141 141 ypjax(url,id+'_changes_info',function(){tooltip_activate()});
142 142 });
143 143
144 144 // change branch filter
145 145 YUE.on(YUD.get('branch_filter'),'change',function(e){
146 146 var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
147 147 console.log(selected_branch);
148 148 var url_main = "${h.url('changelog_home',repo_name=c.repo_name)}";
149 149 var url = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}";
150 150 var url = url.replace('__BRANCH__',selected_branch);
151 151 if(selected_branch != ''){
152 152 window.location = url;
153 153 }else{
154 154 window.location = url_main;
155 155 }
156 156
157 157 });
158 158
159 159 function set_canvas(heads) {
160 160 var c = document.getElementById('graph_nodes');
161 161 var t = document.getElementById('graph_content');
162 162 canvas = document.getElementById('graph_canvas');
163 163 var div_h = t.clientHeight;
164 164 c.style.height=div_h+'px';
165 165 canvas.setAttribute('height',div_h);
166 166 c.style.height=max_w+'px';
167 167 canvas.setAttribute('width',max_w);
168 168 };
169 169 var heads = 1;
170 170 var max_heads = 0;
171 171 var jsdata = ${c.jsdata|n};
172 172
173 173 for( var i=0;i<jsdata.length;i++){
174 174 var m = Math.max.apply(Math, jsdata[i][1]);
175 175 if (m>max_heads){
176 176 max_heads = m;
177 177 }
178 178 }
179 179 var max_w = Math.max(100,max_heads*25);
180 180 set_canvas(max_w);
181 181
182 182 var r = new BranchRenderer();
183 183 r.render(jsdata,max_w);
184 184
185 185 });
186 186 </script>
187 187 %else:
188 188 ${_('There are no changes yet')}
189 189 %endif
190 190 </div>
191 191 </div>
192 192 </%def> No newline at end of file
@@ -1,189 +1,193 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.html"/>
4 4
5 5 <%def name="title()">
6 6 ${c.repo_name} ${_('Changeset')} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)} - ${c.rhodecode_name}
7 7 </%def>
8 8
9 9 <%def name="breadcrumbs_links()">
10 10 ${h.link_to(u'Home',h.url('/'))}
11 11 &raquo;
12 12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 13 &raquo;
14 14 ${_('Changeset')} - r${c.changeset.revision}:${h.short_id(c.changeset.raw_id)}
15 15 </%def>
16 16
17 17 <%def name="page_nav()">
18 18 ${self.menu('changelog')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22 <div class="box">
23 23 <!-- box / title -->
24 24 <div class="title">
25 25 ${self.breadcrumbs()}
26 26 </div>
27 27 <div class="table">
28 28 <div class="diffblock">
29 29 <div class="code-header">
30 30 <div class="date">${c.changeset.revision}:
31 31 ${h.link_to(h.short_id(c.changeset.raw_id),h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}
32 32 ${c.changeset.date}</div>
33 33 <span class="diff-actions">
34 34 <a href="${h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='show')}" title="${_('raw diff')}"><img class="icon" src="${h.url('/images/icons/page_white_text.png')}"/></a>
35 35 <a href="${h.url('raw_changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='download')}" title="${_('download diff')}"><img class="icon" src="${h.url('/images/icons/down_16.png')}"/></a>
36 36 ${c.ignorews_url()}
37 37 ${c.context_url()}
38 38 </span>
39 39 <div class="comments-number" style="float:right;padding-right:5px">${len(c.comments)} comment(s) (${c.inline_cnt} ${_('inline')})</div>
40 40 </div>
41 41 </div>
42 42 <div id="changeset_content">
43 43 <div class="container">
44 44 <div class="left">
45 45 <div class="author">
46 46 <div class="gravatar">
47 47 <img alt="gravatar" src="${h.gravatar_url(h.email(c.changeset.author),20)}"/>
48 48 </div>
49 49 <span>${h.person(c.changeset.author)}</span><br/>
50 50 <span><a href="mailto:${h.email_or_none(c.changeset.author)}">${h.email_or_none(c.changeset.author)}</a></span><br/>
51 51 </div>
52 <div class="message">${h.wrap_paragraphs(c.changeset.message)}</div>
52 <div class="message">${h.urlify_commit(h.wrap_paragraphs(c.changeset.message))}</div>
53 53 </div>
54 54 <div class="right">
55 55 <div class="changes">
56 56 % if len(c.changeset.affected_files) <= c.affected_files_cut_off:
57 57 <span class="removed" title="${_('removed')}">${len(c.changeset.removed)}</span>
58 58 <span class="changed" title="${_('changed')}">${len(c.changeset.changed)}</span>
59 59 <span class="added" title="${_('added')}">${len(c.changeset.added)}</span>
60 60 % else:
61 61 <span class="removed" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
62 62 <span class="changed" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
63 63 <span class="added" title="${_('affected %s files') % len(c.changeset.affected_files)}">!</span>
64 64 % endif
65 65 </div>
66 66
67 67 %if c.changeset.parents:
68 68 %for p_cs in reversed(c.changeset.parents):
69 69 <div class="parent">${_('Parent')}
70 70 <span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
71 71 h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
72 72 </div>
73 73 %endfor
74 74 %else:
75 75 <div class="parent">${_('No parents')}</div>
76 76 %endif
77 77 <span class="logtags">
78 78 %if len(c.changeset.parents)>1:
79 79 <span class="merge">${_('merge')}</span>
80 80 %endif
81 81 <span class="branchtag" title="${'%s %s' % (_('branch'),c.changeset.branch)}">
82 82 ${h.link_to(c.changeset.branch,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
83 83 %for tag in c.changeset.tags:
84 84 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
85 85 ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</span>
86 86 %endfor
87 87 </span>
88 88 </div>
89 89 </div>
90 90 <span>
91 91 ${_('%s files affected with %s additions and %s deletions:') % (len(c.changeset.affected_files),c.lines_added,c.lines_deleted)}
92 92 </span>
93 93 <div class="cs_files">
94 94 %for change,filenode,diff,cs1,cs2,stat in c.changes:
95 95 <div class="cs_${change}">
96 96 <div class="node">
97 97 %if change != 'removed':
98 ${h.link_to(h.safe_unicode(filenode.path),c.anchor_url(filenode.changeset.raw_id,filenode.path))}
98 ${h.link_to(h.safe_unicode(filenode.path),c.anchor_url(filenode.changeset.raw_id,filenode.path)+"_target")}
99 99 %else:
100 100 ${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID('',filenode.path)))}
101 101 %endif
102 102 </div>
103 103 <div class="changes">${h.fancy_file_stats(stat)}</div>
104 104 </div>
105 105 %endfor
106 106 % if c.cut_off:
107 107 ${_('Changeset was too big and was cut off...')}
108 108 % endif
109 109 </div>
110 110 </div>
111 111
112 112 </div>
113 113
114 114 ## diff block
115 115 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
116 116 ${diff_block.diff_block(c.changes)}
117 117
118 118 ## template for inline comment form
119 119 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
120 120 ${comment.comment_inline_form(c.changeset)}
121 121
122 122 ${comment.comments(c.changeset)}
123 123
124 124 <script type="text/javascript">
125 125 var deleteComment = function(comment_id){
126 126
127 127 var url = "${url('changeset_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}".replace('__COMMENT_ID__',comment_id);
128 128 var postData = '_method=delete';
129 129 var success = function(o){
130 130 var n = YUD.get('comment-'+comment_id);
131 131 n.parentNode.removeChild(n);
132 132 }
133 133 ajaxPOST(url,postData,success);
134 134 }
135 135
136 136 YUE.onDOMReady(function(){
137 137
138 138 YUE.on(YUQ('.show-inline-comments'),'change',function(e){
139 139 var show = 'none';
140 140 var target = e.currentTarget;
141 console.log(target);
141 142 if(target.checked){
142 143 var show = ''
143 144 }
145 console.log('aa')
144 146 var boxid = YUD.getAttribute(target,'id_for');
147 console.log(boxid);
145 148 var comments = YUQ('#{0} .inline-comments'.format(boxid));
149 console.log(comments)
146 150 for(c in comments){
147 151 YUD.setStyle(comments[c],'display',show);
148 152 }
149 153 var btns = YUQ('#{0} .inline-comments-button'.format(boxid));
150 154 for(c in btns){
151 155 YUD.setStyle(btns[c],'display',show);
152 156 }
153 157 })
154 158
155 159 YUE.on(YUQ('.line'),'click',function(e){
156 160 var tr = e.currentTarget;
157 161 injectInlineForm(tr);
158 162 });
159 163
160 164 // inject comments into they proper positions
161 165 var file_comments = YUQ('.inline-comment-placeholder');
162 166
163 167 for (f in file_comments){
164 168 var box = file_comments[f];
165 169 var inlines = box.children;
166 170 for(var i=0; i<inlines.length; i++){
167 171 try{
168 172
169 173 var inline = inlines[i];
170 174 var lineno = YUD.getAttribute(inlines[i],'line');
171 175 var lineid = "{0}_{1}".format(YUD.getAttribute(inline,'target_id'),lineno);
172 176 var target_line = YUD.get(lineid);
173 177
174 178 var add = createInlineAddButton(target_line.parentNode,'${_("add another comment")}');
175 179 YUD.insertAfter(add,target_line.parentNode);
176 180
177 181 var comment = new YAHOO.util.Element(tableTr('inline-comments',inline.innerHTML))
178 182 YUD.insertAfter(comment,target_line.parentNode);
179 183 }catch(e){
180 184 console.log(e);
181 185 }
182 186 }
183 187 }
184 188 })
185 189
186 190 </script>
187 191
188 192 </div>
189 193 </%def>
@@ -1,89 +1,89 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${c.repo_name} ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)} - ${c.rhodecode_name}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 ${h.link_to(u'Home',h.url('/'))}
10 10 &raquo;
11 11 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
12 12 &raquo;
13 13 ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)}
14 14 </%def>
15 15
16 16 <%def name="page_nav()">
17 17 ${self.menu('changelog')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21 <div class="box">
22 22 <!-- box / title -->
23 23 <div class="title">
24 24 ${self.breadcrumbs()}
25 25 </div>
26 26 <div class="table">
27 27 <div id="body" class="diffblock">
28 28 <div class="code-header cv">
29 29 <h3 class="code-header-title">${_('Compare View')}</h3>
30 30 <div>
31 31 ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)}
32 32 </div>
33 33 </div>
34 34 </div>
35 35 <div id="changeset_compare_view_content">
36 36 <div class="container">
37 37 <table class="compare_view_commits noborder">
38 38 %for cs in c.cs_ranges:
39 39 <tr>
40 40 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),14)}"/></div></td>
41 41 <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</td>
42 42 <td><div class="author">${h.person(cs.author)}</div></td>
43 43 <td><span class="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
44 <td><div class="message">${h.link_to(h.wrap_paragraphs(cs.message),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div></td>
44 <td><div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message))}</div></td>
45 45 </tr>
46 46 %endfor
47 47 </table>
48 48 </div>
49 49 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
50 50 <div class="cs_files">
51 51 %for cs in c.cs_ranges:
52 52 <div class="cur_cs">r${cs}</div>
53 53 %for change,filenode,diff,cs1,cs2,st in c.changes[cs.raw_id]:
54 54 <div class="cs_${change}">${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID(cs.raw_id,filenode.path)))}</div>
55 55 %endfor
56 56 %endfor
57 57 </div>
58 58 </div>
59 59
60 60 </div>
61 61 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
62 62 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
63 63 %for cs in c.cs_ranges:
64 64 ##${comment.comment_inline_form(cs)}
65 65 ## diff block
66 66 <h3 style="border:none;padding-top:8px;">${'r%s:%s' % (cs.revision,h.short_id(cs.raw_id))}</h3>
67 67 ${diff_block.diff_block(c.changes[cs.raw_id])}
68 68 ##${comment.comments(cs)}
69 69
70 70 %endfor
71 71 <script type="text/javascript">
72 72
73 73 YUE.onDOMReady(function(){
74 74
75 75 YUE.on(YUQ('.diff-menu-activate'),'click',function(e){
76 76 var act = e.currentTarget.nextElementSibling;
77 77
78 78 if(YUD.hasClass(act,'active')){
79 79 YUD.removeClass(act,'active');
80 80 YUD.setStyle(act,'display','none');
81 81 }else{
82 82 YUD.addClass(act,'active');
83 83 YUD.setStyle(act,'display','');
84 84 }
85 85 });
86 86 })
87 87 </script>
88 88 </div>
89 89 </%def> No newline at end of file
@@ -1,41 +1,41 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ##usage:
3 3 ## <%namespace name="diff_block" file="/changeset/diff_block.html"/>
4 4 ## ${diff_block.diff_block(changes)}
5 5 ##
6 6 <%def name="diff_block(changes)">
7 7
8 8 %for change,filenode,diff,cs1,cs2,stat in changes:
9 9 %if change !='removed':
10 <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}" style="clear:both;height:90px;margin-top:-60px"></div>
11 <div class="diffblock margined comm">
10 <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}_target" style="clear:both;height:90px;margin-top:-60px"></div>
11 <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}" class="diffblock margined comm">
12 12 <div class="code-header">
13 13 <div class="changeset_header">
14 14 <div class="changeset_file">
15 15 ${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name,
16 16 revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))}
17 17 </div>
18 18 <span class="diff-actions">
19 19 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='diff',fulldiff=1)}" title="${_('diff')}"><img class="icon" src="${h.url('/images/icons/page_white_text.png')}"/></a>
20 20 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='raw')}" title="${_('raw diff')}"><img class="icon" src="${h.url('/images/icons/page_white_text.png')}"/></a>
21 21 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='download')}" title="${_('download diff')}"><img class="icon" src="${h.url('/images/icons/down_16.png')}"/></a>
22 22 ${c.ignorews_url(h.FID(filenode.changeset.raw_id,filenode.path))}
23 23 ${c.context_url(h.FID(filenode.changeset.raw_id,filenode.path))}
24 24 </span>
25 25 <span style="float:right;margin-top:-3px">
26 26 <label>
27 27 ${_('show inline comments')}
28 28 ${h.checkbox('',checked="checked",class_="show-inline-comments",id_for=h.FID(filenode.changeset.raw_id,filenode.path))}
29 29 </label>
30 30 </span>
31 31 </div>
32 32 </div>
33 33 <div class="code-body">
34 34 <div class="full_f_path" path="${h.safe_unicode(filenode.path)}"></div>
35 35 ${diff|n}
36 36 </div>
37 37 </div>
38 38 %endif
39 39 %endfor
40 40
41 41 </%def> No newline at end of file
@@ -1,77 +1,79 b''
1 1 ## -*- coding: utf-8 -*-
2 2 %if c.repo_changesets:
3 3 <table class="table_disp">
4 4 <tr>
5 <th class="left">${_('commit message')}</th>
5 <th class="left">${_('revision')}</th>
6 <th class="left">${_('commit message')}</th>
6 7 <th class="left">${_('age')}</th>
7 8 <th class="left">${_('author')}</th>
8 <th class="left">${_('revision')}</th>
9 9 <th class="left">${_('branch')}</th>
10 10 <th class="left">${_('tags')}</th>
11 11 </tr>
12 12 %for cnt,cs in enumerate(c.repo_changesets):
13 13 <tr class="parity${cnt%2}">
14 14 <td>
15 <div><pre><a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}">r${cs.revision}:${h.short_id(cs.raw_id)}</a></pre></div>
16 </td>
17 <td>
15 18 ${h.link_to(h.truncate(cs.message,50),
16 19 h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id),
17 20 title=cs.message)}
18 21 </td>
19 22 <td><span class="tooltip" title="${cs.date}">
20 23 ${h.age(cs.date)}</span>
21 24 </td>
22 25 <td title="${cs.author}">${h.person(cs.author)}</td>
23 <td><div><pre><a href="${h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id)}">r${cs.revision}:${h.short_id(cs.raw_id)}</a></pre></div></td>
24 26 <td>
25 27 <span class="logtags">
26 28 <span class="branchtag">${cs.branch}</span>
27 29 </span>
28 30 </td>
29 31 <td>
30 32 <span class="logtags">
31 33 %for tag in cs.tags:
32 34 <span class="tagtag">${tag}</span>
33 35 %endfor
34 36 </span>
35 37 </td>
36 38 </tr>
37 39 %endfor
38 40
39 41 </table>
40 42
41 43 <script type="text/javascript">
42 44 YUE.onDOMReady(function(){
43 45 YUE.delegate("shortlog_data","click",function(e, matchedEl, container){
44 46 ypjax(e.target.href,"shortlog_data",function(){tooltip_activate();});
45 47 YUE.preventDefault(e);
46 48 },'.pager_link');
47 49 });
48 50 </script>
49 51
50 52 <div class="pagination-wh pagination-left">
51 53 ${c.repo_changesets.pager('$link_previous ~2~ $link_next')}
52 54 </div>
53 55 %else:
54 56
55 57 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
56 58 <h4>${_('Add or upload files directly via RhodeCode')}</h4>
57 59 <div style="margin: 20px 30px;">
58 60 <div id="add_node_id" class="add_node">
59 61 <a class="ui-btn" href="${h.url('files_add_home',repo_name=c.repo_name,revision=0,f_path='')}">${_('add new file')}</a>
60 62 </div>
61 63 </div>
62 64 %endif
63 65
64 66
65 67 <h4>${_('Push new repo')}</h4>
66 68 <pre>
67 69 ${c.rhodecode_repo.alias} clone ${c.clone_repo_url}
68 70 ${c.rhodecode_repo.alias} add README # add first file
69 71 ${c.rhodecode_repo.alias} commit -m "Initial" # commit with message
70 72 ${c.rhodecode_repo.alias} push # push changes back
71 73 </pre>
72 74
73 75 <h4>${_('Existing repository?')}</h4>
74 76 <pre>
75 77 ${c.rhodecode_repo.alias} push ${c.clone_repo_url}
76 78 </pre>
77 79 %endif
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now