diff --git a/.bumpversion.cfg b/.bumpversion.cfg --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.2.1 +current_version = 4.3.0 message = release: Bump version {current_version} to {new_version} [bumpversion:file:rhodecode/VERSION] diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -25,6 +25,7 @@ syntax: regexp ^build/ ^coverage\.xml$ ^data$ +^\.eggs/ ^configs/data$ ^dev.ini$ ^acceptance_tests/dev.*\.ini$ diff --git a/.release.cfg b/.release.cfg --- a/.release.cfg +++ b/.release.cfg @@ -4,26 +4,21 @@ done = false [task:bump_version] done = true -[task:rc_tools_pinned] -done = true - [task:fixes_on_stable] -done = true [task:pip2nix_generated] -done = true [task:changelog_updated] -done = true [task:generate_api_docs] -done = true + +[task:updated_translation] [release] -state = prepared -version = 4.2.1 +state = in_progress +version = 4.3.0 -[task:updated_translation] +[task:rc_tools_pinned] [task:generate_js_routes] diff --git a/Gruntfile.js b/Gruntfile.js --- a/Gruntfile.js +++ b/Gruntfile.js @@ -20,6 +20,7 @@ module.exports = function(grunt) { '<%= dirs.js.src %>/moment.js', '<%= dirs.js.src %>/appenlight-client-0.4.1.min.js', '<%= dirs.js.src %>/i18n_utils.js', + '<%= dirs.js.src %>/deform.js', // Plugins '<%= dirs.js.src %>/plugins/jquery.pjax.js', @@ -31,6 +32,7 @@ module.exports = function(grunt) { '<%= dirs.js.src %>/plugins/jquery.mark.js', '<%= dirs.js.src %>/plugins/jquery.timeago.js', '<%= dirs.js.src %>/plugins/jquery.timeago-extension.js', + '<%= dirs.js.src %>/plugins/toastr.js', // Select2 '<%= dirs.js.src %>/select2/select2.js', @@ -56,12 +58,14 @@ module.exports = function(grunt) { '<%= dirs.js.src %>/rhodecode/utils/colorgenerator.js', '<%= dirs.js.src %>/rhodecode/utils/ie.js', '<%= dirs.js.src %>/rhodecode/utils/os.js', + '<%= dirs.js.src %>/rhodecode/utils/topics.js', // Rhodecode widgets '<%= dirs.js.src %>/rhodecode/widgets/multiselect.js', // Rhodecode components '<%= dirs.js.src %>/rhodecode/init.js', + '<%= dirs.js.src %>/rhodecode/connection_controller.js', '<%= dirs.js.src %>/rhodecode/codemirror.js', '<%= dirs.js.src %>/rhodecode/comments.js', '<%= dirs.js.src %>/rhodecode/constants.js', @@ -76,6 +80,7 @@ module.exports = function(grunt) { '<%= dirs.js.src %>/rhodecode/select2_widgets.js', '<%= dirs.js.src %>/rhodecode/tooltips.js', '<%= dirs.js.src %>/rhodecode/users.js', + '<%= dirs.js.src %>/rhodecode/utils/notifications.js', '<%= dirs.js.src %>/rhodecode/appenlight.js', // Rhodecode main module diff --git a/LICENSE.txt b/LICENSE.txt --- a/LICENSE.txt +++ b/LICENSE.txt @@ -10,8 +10,10 @@ permission notice: file: Copyright (c) 2008-2011 - msgpack-python file:licenses/msgpack_license.txt + Copyright (c) 2009 - tornado + file:licenses/tornado_license.txt -Both licensed under the Apache License, Version 2.0 (the "License"); +All licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at diff --git a/MANIFEST.in b/MANIFEST.in --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,20 +2,20 @@ include test.ini include MANIFEST.in include README.rst +include CHANGES.rst +include LICENSE.txt + include rhodecode/VERSION # docs recursive-include docs * -# init.d -recursive-include init.d * +# all config files +recursive-include configs * # translations recursive-include rhodecode/i18n * -# bin stuff -recursive-include rhodecode/bin * - # hook templates recursive-include rhodecode/config/hook_templates * diff --git a/Makefile b/Makefile --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ ci-docs: docs; clean: test-clean find . -type f \( -iname '*.c' -o -iname '*.pyc' -o -iname '*.so' \) -exec rm '{}' ';' -test: test-clean test-lint test-only +test: test-clean test-only test-clean: rm -rf coverage.xml htmlcov junit.xml pylint.log result diff --git a/acceptance_tests/README.rst b/acceptance_tests/README.rst deleted file mode 100644 --- a/acceptance_tests/README.rst +++ /dev/null @@ -1,47 +0,0 @@ -README - Quickstart -=================== - -This folder contains the functional tests and automation of specification -examples. Details about testing can be found in -`/docs-internal/testing/index.rst`. - - -Setting up your Rhodecode Enterprise instance ---------------------------------------------- - -The tests will create users and repositories as needed, so you can start with a -new and empty instance. - -Use the following example call for the database setup of Enterprise:: - - paster setup-rhodecode \ - --user=admin \ - --email=admin@example.com \ - --password=secret \ - --api-key=9999999999999999999999999999999999999999 \ - your-enterprise-config.ini - -This way the username, password, and auth token of the admin user will match the -defaults from the test run. - - -Usage ------ - -1. Make sure your Rhodecode Enterprise instance is running at - http://localhost:5000. - -2. Enter `nix-shell` from the acceptance_tests folder:: - - cd acceptance_tests - nix-shell - - Make sure that `rcpkgs` and `rcnixpkgs` are available on the nix path. - -3. Run the tests:: - - py.test -c example.ini -vs - - The parameter ``-vs`` allows you to see debugging output during the test - run. Check ``py.test --help`` and the documentation at http://pytest.org to - learn all details about the test runner. diff --git a/configs/development.ini b/configs/development.ini --- a/configs/development.ini +++ b/configs/development.ini @@ -1,23 +1,36 @@ -################################################################################ + + ################################################################################ -# RhodeCode Enterprise - configuration file # -# Built-in functions and variables # +## RHODECODE ENTERPRISE CONFIGURATION ## # The %(here)s variable will be replaced with the parent directory of this file# -# # ################################################################################ [DEFAULT] debug = true + ################################################################################ +## EMAIL CONFIGURATION ## ## Uncomment and replace with the email address which should receive ## ## any error reports after an application crash ## ## Additionally these settings will be used by the RhodeCode mailing system ## ################################################################################ -#email_to = admin@localhost -#error_email_from = paste_error@localhost + +## prefix all emails subjects with given prefix, helps filtering out emails +#email_prefix = [RhodeCode] + +## email FROM address all mails will be sent #app_email_from = rhodecode-noreply@localhost + +## Uncomment and replace with the address which should receive any error report +## note: using appenlight for error handling doesn't need this to be uncommented +#email_to = admin@localhost + +## in case of Application errors, sent an error email form +#error_email_from = rhodecode_error@localhost + +## additional error message to be send in case of server crash #error_message = -#email_prefix = [RhodeCode] + #smtp_server = mail.server.com #smtp_username = @@ -37,6 +50,7 @@ port = 5000 ## WAITRESS WSGI SERVER ## ## Recommended for Development ## ################################## + use = egg:waitress#main ## number of worker threads threads = 5 @@ -51,6 +65,7 @@ asyncore_use_poll = true ## GUNICORN WSGI SERVER ## ########################## ## run with gunicorn --log-config --paste + #use = egg:gunicorn#main ## Sets the number of process workers. You must set `instance_id = *` ## when this option is set to more than one worker, recommended @@ -77,15 +92,18 @@ asyncore_use_poll = true ## prefix middleware for RhodeCode, disables force_https flag. +## recommended when using proxy setup. ## allows to set RhodeCode under a prefix in server. ## eg https://server.com/. Enable `filter-with =` option below as well. -#[filter:proxy-prefix] -#use = egg:PasteDeploy#prefix -#prefix = / +## optionally set prefix like: `prefix = /` +[filter:proxy-prefix] +use = egg:PasteDeploy#prefix +prefix = / [app:main] use = egg:rhodecode-enterprise-ce -## enable proxy prefix middleware, defined below + +## enable proxy prefix middleware, defined above #filter-with = proxy-prefix # During development the we want to have the debug toolbar enabled @@ -123,12 +141,10 @@ rhodecode.api.url = /_admin/api ## `SignatureVerificationError` in case of wrong key, or damaged encryption data. #rhodecode.encrypted_values.strict = false -full_stack = true +## return gzipped responses from Rhodecode (static files/application) +gzip_responses = false -## Serve static files via RhodeCode, disable to serve them via HTTP server -static_files = true - -# autogenerate javascript routes file on startup +## autogenerate javascript routes file on startup generate_js_files = false ## Optional Languages @@ -317,8 +333,8 @@ beaker.cache.sql_cache_short.type = memo beaker.cache.sql_cache_short.expire = 10 beaker.cache.sql_cache_short.key_length = 256 -# default is memory cache, configure only if required -# using multi-node or multi-worker setup +## default is memory cache, configure only if required +## using multi-node or multi-worker setup #beaker.cache.auth_plugins.type = ext:database #beaker.cache.auth_plugins.lock_dir = %(here)s/data/cache/auth_plugin_lock #beaker.cache.auth_plugins.url = postgresql://postgres:secret@localhost/rhodecode @@ -331,8 +347,8 @@ beaker.cache.repo_cache_long.type = memo beaker.cache.repo_cache_long.max_items = 4096 beaker.cache.repo_cache_long.expire = 2592000 -# default is memorylru_base cache, configure only if required -# using multi-node or multi-worker setup +## default is memorylru_base cache, configure only if required +## using multi-node or multi-worker setup #beaker.cache.repo_cache_long.type = ext:memcached #beaker.cache.repo_cache_long.url = localhost:11211 #beaker.cache.repo_cache_long.expire = 1209600 @@ -347,7 +363,7 @@ beaker.cache.repo_cache_long.expire = 25 beaker.session.type = file beaker.session.data_dir = %(here)s/data/sessions/data -## db based session, fast, and allows easy management over logged in users ## +## db based session, fast, and allows easy management over logged in users #beaker.session.type = ext:database #beaker.session.table_name = db_session #beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode @@ -368,6 +384,7 @@ beaker.session.lock_dir = %(here)s/data/ ## accessed for given amount of time in seconds beaker.session.timeout = 2592000 beaker.session.httponly = true +## Path to use for the cookie. #beaker.session.cookie_path = / ## uncomment for https secure cookie @@ -391,6 +408,23 @@ beaker.session.auto = false search.module = rhodecode.lib.index.whoosh search.location = %(here)s/data/index +######################################## +### CHANNELSTREAM CONFIG #### +######################################## +## channelstream enables persistent connections and live notification +## in the system. It's also used by the chat system + +channelstream.enabled = true +## location of channelstream server on the backend +channelstream.server = 127.0.0.1:9800 +## location of the channelstream server from outside world +## most likely this would be an http server special backend URL, that handles +## websocket connections see nginx example for config +channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream +channelstream.secret = secret +channelstream.history.location = %(here)s/channelstream_history + + ################################### ## APPENLIGHT CONFIG ## ################################### @@ -466,9 +500,10 @@ debug_style = true ######################################################### ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ### ######################################################### -sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30 +#sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30 #sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode #sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode +sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30 # see sqlalchemy docs for other advanced settings @@ -498,28 +533,53 @@ vcs.server = localhost:9900 ## Available protocols are: ## `pyro4` - using pyro4 server ## `http` - using http-rpc backend -#vcs.server.protocol = http +vcs.server.protocol = http ## Push/Pull operations protocol, available options are: ## `pyro4` - using pyro4 server ## `rhodecode.lib.middleware.utils.scm_app_http` - Http based, recommended ## `vcsserver.scm_app` - internal app (EE only) -#vcs.scm_app_implementation = rhodecode.lib.middleware.utils.scm_app_http +vcs.scm_app_implementation = rhodecode.lib.middleware.utils.scm_app_http ## Push/Pull operations hooks protocol, available options are: ## `pyro4` - using pyro4 server ## `http` - using http-rpc backend -#vcs.hooks.protocol = http +vcs.hooks.protocol = http vcs.server.log_level = debug ## Start VCSServer with this instance as a subprocess, usefull for development vcs.start_server = true + +## List of enabled VCS backends, available options are: +## `hg` - mercurial +## `git` - git +## `svn` - subversion vcs.backends = hg, git, svn + vcs.connection_timeout = 3600 ## Compatibility version when creating SVN repositories. Defaults to newest version when commented out. ## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible #vcs.svn.compatible_version = pre-1.8-compatible + +############################################################ +### Subversion proxy support (mod_dav_svn) ### +### Maps RhodeCode repo groups into SVN paths for Apache ### +############################################################ +## Enable or disable the config file generation. +svn.proxy.generate_config = false +## Generate config file with `SVNListParentPath` set to `On`. +svn.proxy.list_parent_path = true +## Set location and file name of generated config file. +svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf +## File system path to the directory containing the repositories served by +## RhodeCode. +svn.proxy.parent_path_root = /path/to/repo_store +## Used as a prefix to the block in the generated config file. In +## most cases it should be set to `/`. +svn.proxy.location_root = / + + ################################ ### LOGGING CONFIGURATION #### ################################ diff --git a/configs/production.ini b/configs/production.ini --- a/configs/production.ini +++ b/configs/production.ini @@ -1,23 +1,36 @@ -################################################################################ + + ################################################################################ -# RhodeCode Enterprise - configuration file # -# Built-in functions and variables # +## RHODECODE ENTERPRISE CONFIGURATION ## # The %(here)s variable will be replaced with the parent directory of this file# -# # ################################################################################ [DEFAULT] debug = true + ################################################################################ +## EMAIL CONFIGURATION ## ## Uncomment and replace with the email address which should receive ## ## any error reports after an application crash ## ## Additionally these settings will be used by the RhodeCode mailing system ## ################################################################################ -#email_to = admin@localhost -#error_email_from = paste_error@localhost + +## prefix all emails subjects with given prefix, helps filtering out emails +#email_prefix = [RhodeCode] + +## email FROM address all mails will be sent #app_email_from = rhodecode-noreply@localhost + +## Uncomment and replace with the address which should receive any error report +## note: using appenlight for error handling doesn't need this to be uncommented +#email_to = admin@localhost + +## in case of Application errors, sent an error email form +#error_email_from = rhodecode_error@localhost + +## additional error message to be send in case of server crash #error_message = -#email_prefix = [RhodeCode] + #smtp_server = mail.server.com #smtp_username = @@ -37,6 +50,7 @@ port = 5000 ## WAITRESS WSGI SERVER ## ## Recommended for Development ## ################################## + #use = egg:waitress#main ## number of worker threads #threads = 5 @@ -51,6 +65,7 @@ port = 5000 ## GUNICORN WSGI SERVER ## ########################## ## run with gunicorn --log-config --paste + use = egg:gunicorn#main ## Sets the number of process workers. You must set `instance_id = *` ## when this option is set to more than one worker, recommended @@ -77,15 +92,18 @@ timeout = 21600 ## prefix middleware for RhodeCode, disables force_https flag. +## recommended when using proxy setup. ## allows to set RhodeCode under a prefix in server. ## eg https://server.com/. Enable `filter-with =` option below as well. -#[filter:proxy-prefix] -#use = egg:PasteDeploy#prefix -#prefix = / +## optionally set prefix like: `prefix = /` +[filter:proxy-prefix] +use = egg:PasteDeploy#prefix +prefix = / [app:main] use = egg:rhodecode-enterprise-ce -## enable proxy prefix middleware, defined below + +## enable proxy prefix middleware, defined above #filter-with = proxy-prefix ## encryption key used to encrypt social plugin tokens, @@ -97,12 +115,10 @@ use = egg:rhodecode-enterprise-ce ## `SignatureVerificationError` in case of wrong key, or damaged encryption data. #rhodecode.encrypted_values.strict = false -full_stack = true +## return gzipped responses from Rhodecode (static files/application) +gzip_responses = false -## Serve static files via RhodeCode, disable to serve them via HTTP server -static_files = true - -# autogenerate javascript routes file on startup +## autogenerate javascript routes file on startup generate_js_files = false ## Optional Languages @@ -291,8 +307,8 @@ beaker.cache.sql_cache_short.type = memo beaker.cache.sql_cache_short.expire = 10 beaker.cache.sql_cache_short.key_length = 256 -# default is memory cache, configure only if required -# using multi-node or multi-worker setup +## default is memory cache, configure only if required +## using multi-node or multi-worker setup #beaker.cache.auth_plugins.type = ext:database #beaker.cache.auth_plugins.lock_dir = %(here)s/data/cache/auth_plugin_lock #beaker.cache.auth_plugins.url = postgresql://postgres:secret@localhost/rhodecode @@ -305,8 +321,8 @@ beaker.cache.repo_cache_long.type = memo beaker.cache.repo_cache_long.max_items = 4096 beaker.cache.repo_cache_long.expire = 2592000 -# default is memorylru_base cache, configure only if required -# using multi-node or multi-worker setup +## default is memorylru_base cache, configure only if required +## using multi-node or multi-worker setup #beaker.cache.repo_cache_long.type = ext:memcached #beaker.cache.repo_cache_long.url = localhost:11211 #beaker.cache.repo_cache_long.expire = 1209600 @@ -321,7 +337,7 @@ beaker.cache.repo_cache_long.expire = 25 beaker.session.type = file beaker.session.data_dir = %(here)s/data/sessions/data -## db based session, fast, and allows easy management over logged in users ## +## db based session, fast, and allows easy management over logged in users #beaker.session.type = ext:database #beaker.session.table_name = db_session #beaker.session.sa.url = postgresql://postgres:secret@localhost/rhodecode @@ -342,6 +358,7 @@ beaker.session.lock_dir = %(here)s/data/ ## accessed for given amount of time in seconds beaker.session.timeout = 2592000 beaker.session.httponly = true +## Path to use for the cookie. #beaker.session.cookie_path = / ## uncomment for https secure cookie @@ -365,6 +382,23 @@ beaker.session.auto = false search.module = rhodecode.lib.index.whoosh search.location = %(here)s/data/index +######################################## +### CHANNELSTREAM CONFIG #### +######################################## +## channelstream enables persistent connections and live notification +## in the system. It's also used by the chat system + +channelstream.enabled = true +## location of channelstream server on the backend +channelstream.server = 127.0.0.1:9800 +## location of the channelstream server from outside world +## most likely this would be an http server special backend URL, that handles +## websocket connections see nginx example for config +channelstream.ws_url = ws://rhodecode.yourserver.com/_channelstream +channelstream.secret = secret +channelstream.history.location = %(here)s/channelstream_history + + ################################### ## APPENLIGHT CONFIG ## ################################### @@ -436,8 +470,9 @@ set debug = false ### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG ### ######################################################### #sqlalchemy.db1.url = sqlite:///%(here)s/rhodecode.db?timeout=30 +#sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode +#sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode sqlalchemy.db1.url = postgresql://postgres:qweqwe@localhost/rhodecode -#sqlalchemy.db1.url = mysql://root:qweqwe@localhost/rhodecode # see sqlalchemy docs for other advanced settings @@ -483,12 +518,37 @@ vcs.server = localhost:9900 vcs.server.log_level = info ## Start VCSServer with this instance as a subprocess, usefull for development vcs.start_server = false + +## List of enabled VCS backends, available options are: +## `hg` - mercurial +## `git` - git +## `svn` - subversion vcs.backends = hg, git, svn + vcs.connection_timeout = 3600 ## Compatibility version when creating SVN repositories. Defaults to newest version when commented out. ## Available options are: pre-1.4-compatible, pre-1.5-compatible, pre-1.6-compatible, pre-1.8-compatible #vcs.svn.compatible_version = pre-1.8-compatible + +############################################################ +### Subversion proxy support (mod_dav_svn) ### +### Maps RhodeCode repo groups into SVN paths for Apache ### +############################################################ +## Enable or disable the config file generation. +svn.proxy.generate_config = false +## Generate config file with `SVNListParentPath` set to `On`. +svn.proxy.list_parent_path = true +## Set location and file name of generated config file. +svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf +## File system path to the directory containing the repositories served by +## RhodeCode. +svn.proxy.parent_path_root = /path/to/repo_store +## Used as a prefix to the block in the generated config file. In +## most cases it should be set to `/`. +svn.proxy.location_root = / + + ################################ ### LOGGING CONFIGURATION #### ################################ diff --git a/default.nix b/default.nix --- a/default.nix +++ b/default.nix @@ -123,8 +123,9 @@ let # pkgs/default.nix? passthru = { inherit - pythonLocalOverrides - myPythonPackagesUnfix; + linkNodeModules + myPythonPackagesUnfix + pythonLocalOverrides; pythonPackages = self; }; @@ -165,6 +166,7 @@ let ln -s ${self.supervisor}/bin/supervisor* $out/bin/ ln -s ${self.gunicorn}/bin/gunicorn $out/bin/ ln -s ${self.PasteScript}/bin/paster $out/bin/ + ln -s ${self.channelstream}/bin/channelstream $out/bin/ ln -s ${self.pyramid}/bin/* $out/bin/ #*/ # rhodecode-tools diff --git a/docs/_templates/footer.html b/docs/_templates/footer.html new file mode 100644 --- /dev/null +++ b/docs/_templates/footer.html @@ -0,0 +1,6 @@ +{% extends "!footer.html" %} + +{% block extrafooter %} +
+Documentation defects and suggestions can be submitted here +{% endblock %} diff --git a/docs/admin/apache-reverse-proxy.rst b/docs/admin/apache-reverse-proxy.rst --- a/docs/admin/apache-reverse-proxy.rst +++ b/docs/admin/apache-reverse-proxy.rst @@ -9,9 +9,9 @@ Here is a sample configuration file for ServerName hg.myserver.com ServerAlias hg.myserver.com - ## uncomment root directive if you want to serve static files by - ## Apache requires static_files = false in .ini file - #DocumentRoot /path/to/rhodecode/installation/public + ## uncomment to serve static files by Apache + ## ProxyPass /_static/rhodecode ! + ## Alias /_static/rhodecode /path/to/.rccontrol/enterprise-1/static Order allow,deny diff --git a/docs/admin/apache-subdirectory.rst b/docs/admin/apache-subdirectory.rst --- a/docs/admin/apache-subdirectory.rst +++ b/docs/admin/apache-subdirectory.rst @@ -16,17 +16,18 @@ Use the following example to configure A In addition to the regular Apache setup you will need to add the following lines into the ``rhodecode.ini`` file. +* Above ``[app:main]`` section of the ``rhodecode.ini`` file add the + following section if it doesn't exist yet. + +.. code-block:: ini + + [filter:proxy-prefix] + use = egg:PasteDeploy#prefix + prefix = / # Change into your chosen prefix + * In the the ``[app:main]`` section of your ``rhodecode.ini`` file add the following line. .. code-block:: ini filter-with = proxy-prefix - -* At the end of the ``rhodecode.ini`` file add the following section. - -.. code-block:: ini - - [filter:proxy-prefix] - use = egg:PasteDeploy#prefix - prefix = / # Change into your chosen prefix diff --git a/docs/admin/backup-restore.rst b/docs/admin/backup-restore.rst --- a/docs/admin/backup-restore.rst +++ b/docs/admin/backup-restore.rst @@ -121,7 +121,7 @@ then work on restoring any specific setu :ref:`indexing-ref` section for details. * To reconfigure any extensions, copy the backed up extensions into the :file:`/home/{user}/.rccontrol/{instance-id}/rcextensions` and also specify - any custom hooks if necessary. See the :ref:`integrations-ref` section for + any custom hooks if necessary. See the :ref:`extensions-hooks-ref` section for details. .. _Schrödinger's Backup: http://novabackup.novastor.com/blog/schrodingers-backup-good-bad-backup/ diff --git a/docs/admin/nginx-config-example.rst b/docs/admin/nginx-config-example.rst --- a/docs/admin/nginx-config-example.rst +++ b/docs/admin/nginx-config-example.rst @@ -5,6 +5,11 @@ Use the following example to configure N .. code-block:: nginx + log_format log_custom '$remote_addr - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent" ' + '$request_time $upstream_response_time $pipe'; + upstream rc { server 127.0.0.1:10002; @@ -14,12 +19,12 @@ Use the following example to configure N # server 127.0.0.1:10004; } - ## gist alias + ## gist alias server, for serving nicer GIST urls server { listen 443; server_name gist.myserver.com; - access_log /var/log/nginx/gist.access.log; + access_log /var/log/nginx/gist.access.log log_custom; error_log /var/log/nginx/gist.error.log; ssl on; @@ -28,23 +33,36 @@ Use the following example to configure N ssl_session_timeout 5m; - ssl_protocols SSLv3 TLSv1; - ssl_ciphers DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:EDH-RSA-DES-CBC3-SHA:AES256-SHA:DES-CBC3-SHA:AES128-SHA:RC4-SHA:RC4-MD5; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; + add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;"; # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits - ssl_dhparam /etc/nginx/ssl/dhparam.pem; + #ssl_dhparam /etc/nginx/ssl/dhparam.pem; rewrite ^/(.+)$ https://rhodecode.myserver.com/_admin/gists/$1; rewrite (.*) https://rhodecode.myserver.com/_admin/gists; } + ## HTTP to HTTPS rewrite server { - listen 443; + listen 80; server_name rhodecode.myserver.com; - access_log /var/log/nginx/rhodecode.access.log; - error_log /var/log/nginx/rhodecode.error.log; + + if ($http_host = rhodecode.myserver.com) { + rewrite (.*) https://rhodecode.myserver.com$1 permanent; + } + } + + ## MAIN SSL enabled server + server { + listen 443 ssl; + server_name rhodecode.myserver.com; + + access_log /var/log/nginx/rhodecode.access.log log_custom; + error_log /var/log/nginx/rhodecode.error.log; ssl on; ssl_certificate rhodecode.myserver.com.crt; @@ -52,21 +70,51 @@ Use the following example to configure N ssl_session_timeout 5m; - ssl_protocols SSLv3 TLSv1; - ssl_ciphers DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:EDH-RSA-DES-CBC3-SHA:AES256-SHA:DES-CBC3-SHA:AES128-SHA:RC4-SHA:RC4-MD5; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; + + # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits + #ssl_dhparam /etc/nginx/ssl/dhparam.pem; + + include /etc/nginx/proxy.conf; + + ## serve static files by nginx, recommended + # location /_static/rhodecode { + # alias /path/to/.rccontrol/enterprise-1/static; + # } - ## uncomment root directive if you want to serve static files by nginx - ## requires static_files = false in .ini file - # root /path/to/rhodecode/installation/public; + ## channel stream live components + location /_channelstream { + rewrite /_channelstream/(.*) /$1 break; + proxy_pass http://127.0.0.1:9800; - include /etc/nginx/proxy.conf; + proxy_connect_timeout 10; + proxy_send_timeout 10m; + proxy_read_timeout 10m; + tcp_nodelay off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Url-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + gzip off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } location / { try_files $uri @rhode; } - location @rhode { - proxy_pass http://rc; - } - } + location @rhode { + proxy_pass http://rc; + } + + ## custom 502 error page + error_page 502 /502.html; + location = /502.html { + root /path/to/.rccontrol/enterprise-1/static; + } + } \ No newline at end of file diff --git a/docs/admin/nginx-url-prefix.rst b/docs/admin/nginx-url-prefix.rst --- a/docs/admin/nginx-url-prefix.rst +++ b/docs/admin/nginx-url-prefix.rst @@ -15,7 +15,16 @@ Use the following example to configure N } In addition to the Nginx configuration you will need to add the following -lines into the ``rhodecode.ini`` file. +lines (if they not exist) into the ``rhodecode.ini`` file. + +* Above ``[app:main]`` section of the ``rhodecode.ini`` file add the + following section if it doesn't exist yet. + +.. code-block:: ini + + [filter:proxy-prefix] + use = egg:PasteDeploy#prefix + prefix = / # Change into your chosen prefix * In the the ``[app:main]`` section of your ``rhodecode.ini`` file add the following line. @@ -24,10 +33,4 @@ lines into the ``rhodecode.ini`` file. filter-with = proxy-prefix -* At the end of the ``rhodecode.ini`` file add the following section. -.. code-block:: ini - - [filter:proxy-prefix] - use = egg:PasteDeploy#prefix - prefix = / # Change into your chosen prefix diff --git a/docs/admin/repo-extra-fields.rst b/docs/admin/repo-extra-fields.rst --- a/docs/admin/repo-extra-fields.rst +++ b/docs/admin/repo-extra-fields.rst @@ -32,7 +32,7 @@ Example Usage ------------- To use the extra fields in an extension, see the example below. For more -information and examples, see the :ref:`integrations-ref` section. +information and examples, see the :ref:`extensions-hooks-ref` section. .. code-block:: python diff --git a/docs/admin/reset-information.rst b/docs/admin/reset-information.rst --- a/docs/admin/reset-information.rst +++ b/docs/admin/reset-information.rst @@ -30,7 +30,7 @@ account permissions. # Use this example to change user permissions In [1]: adminuser = User.get_by_username('username') In [2]: adminuser.admin = True - In [3]: Session.add(adminuser);Session().commit() + In [3]: Session().add(adminuser);Session().commit() In [4]: exit() Set to read global ``.hgrc`` file diff --git a/docs/admin/svn-http.rst b/docs/admin/svn-http.rst --- a/docs/admin/svn-http.rst +++ b/docs/admin/svn-http.rst @@ -76,7 +76,7 @@ following examples. For more |svn| infor .. code-block:: bash # To clone a repository - svn clone http://my-svn-server.example.com/my-svn-repo + svn checkout http://my-svn-server.example.com/my-svn-repo # svn commit svn commit diff --git a/docs/api/api.rst b/docs/api/api.rst --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -194,2631 +194,14 @@ are not required in args. ApiController. .. --- API DEFS MARKER --- - -pull ----- - -.. py:function:: pull(apiuser, repoid) - - Triggers a pull on the given repository from a remote location. You - can use this to keep remote repositories up-to-date. - - This command can only be run using an |authtoken| with admin - rights to the specified repository. For more information, - see :ref:`config-token-ref`. - - This command takes the following options: - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: The repository name or repository ID. - :type repoid: str or int - - Example output: - - .. code-block:: bash - - id : - result : { - "msg": "Pulled from ``" - "repository": "" - } - error : null - - Example error output: - - .. code-block:: bash - - id : - result : null - error : { - "Unable to pull changes from ``" - } - - -strip ------ - -.. py:function:: strip(apiuser, repoid, revision, branch) - - Strips the given revision from the specified repository. - - * This will remove the revision and all of its decendants. - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - This command takes the following options: - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: The repository name or repository ID. - :type repoid: str or int - :param revision: The revision you wish to strip. - :type revision: str - :param branch: The branch from which to strip the revision. - :type branch: str - - Example output: - - .. code-block:: bash - - id : - result : { - "msg": "'Stripped commit from repo ``'" - "repository": "" - } - error : null - - Example error output: - - .. code-block:: bash - - id : - result : null - error : { - "Unable to strip commit from repo ``" - } - - -rescan_repos ------------- - -.. py:function:: rescan_repos(apiuser, remove_obsolete=) - - Triggers a rescan of the specified repositories. - - * If the ``remove_obsolete`` option is set, it also deletes repositories - that are found in the database but not on the file system, so called - "clean zombies". - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - This command takes the following options: - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param remove_obsolete: Deletes repositories from the database that - are not found on the filesystem. - :type remove_obsolete: Optional(``True`` | ``False``) - - Example output: - - .. code-block:: bash - - id : - result : { - 'added': [,...] - 'removed': [,...] - } - error : null - - Example error output: - - .. code-block:: bash - - id : - result : null - error : { - 'Error occurred during rescan repositories action' - } - - -invalidate_cache ----------------- - -.. py:function:: invalidate_cache(apiuser, repoid, delete_keys=) - - Invalidates the cache for the specified repository. - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - This command takes the following options: - - :param apiuser: This is filled automatically from |authtoken|. - :type apiuser: AuthUser - :param repoid: Sets the repository name or repository ID. - :type repoid: str or int - :param delete_keys: This deletes the invalidated keys instead of - just flagging them. - :type delete_keys: Optional(``True`` | ``False``) - - Example output: - - .. code-block:: bash - - id : - result : { - 'msg': Cache for repository `` was invalidated, - 'repository': - } - error : null - - Example error output: - - .. code-block:: bash - - id : - result : null - error : { - 'Error occurred during cache invalidation action' - } - - -lock ----- - -.. py:function:: lock(apiuser, repoid, locked=, userid=>) - - Sets the lock state of the specified |repo| by the given user. - From more information, see :ref:`repo-locking`. - - * If the ``userid`` option is not set, the repository is locked to the - user who called the method. - * If the ``locked`` parameter is not set, the current lock state of the - repository is displayed. - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - This command takes the following options: - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: Sets the repository name or repository ID. - :type repoid: str or int - :param locked: Sets the lock state. - :type locked: Optional(``True`` | ``False``) - :param userid: Set the repository lock to this user. - :type userid: Optional(str or int) - - Example error output: - - .. code-block:: bash - - id : - result : { - 'repo': '', - 'locked': , - 'locked_since': , - 'locked_by': , - 'lock_reason': , - 'lock_state_changed': , - 'msg': 'Repo `` locked by `` on .' - or - 'msg': 'Repo `` not locked.' - or - 'msg': 'User `` set lock state for repo `` to ``' - } - error : null - - Example error output: - - .. code-block:: bash - - id : - result : null - error : { - 'Error occurred locking repository `` - } - - -get_locks ---------- - -.. py:function:: get_locks(apiuser, userid=>) - - Displays all repositories locked by the specified user. - - * If this command is run by a non-admin user, it returns - a list of |repos| locked by that user. - - This command takes the following options: - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param userid: Sets the userid whose list of locked |repos| will be - displayed. - :type userid: Optional(str or int) - - Example output: - - .. code-block:: bash - - id : - result : { - [repo_object, repo_object,...] - } - error : null - - -get_ip ------- - -.. py:function:: get_ip(apiuser, userid=>) - - Displays the IP Address as seen from the |RCE| server. - - * This command displays the IP Address, as well as all the defined IP - addresses for the specified user. If the ``userid`` is not set, the - data returned is for the user calling the method. - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - This command takes the following options: - - :param apiuser: This is filled automatically from |authtoken|. - :type apiuser: AuthUser - :param userid: Sets the userid for which associated IP Address data - is returned. - :type userid: Optional(str or int) - - Example output: - - .. code-block:: bash - - id : - result : { - "server_ip_addr": "", - "user_ips": [ - { - "ip_addr": "", - "ip_range": ["", ""], - }, - ... - ] - } - - -show_ip -------- - -.. py:function:: show_ip(apiuser, userid=>) - - Displays the IP Address as seen from the |RCE| server. - - * This command displays the IP Address, as well as all the defined IP - addresses for the specified user. If the ``userid`` is not set, the - data returned is for the user calling the method. - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - This command takes the following options: - - :param apiuser: This is filled automatically from |authtoken|. - :type apiuser: AuthUser - :param userid: Sets the userid for which associated IP Address data - is returned. - :type userid: Optional(str or int) - - Example output: - - .. code-block:: bash - - id : - result : { - "server_ip_addr": "", - "user_ips": [ - { - "ip_addr": "", - "ip_range": ["", ""], - }, - ... - ] - } - - -get_license_info ----------------- - -.. py:function:: get_license_info(apiuser) - - Returns the |RCE| license information. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - - Example output: - - .. code-block:: bash - - id : - result : { - 'rhodecode_version': , - 'token': , - 'issued_to': , - 'issued_on': , - 'expires_on': , - 'type': , - 'users_limit': , - 'key': - } - error : null - - -set_license_key ---------------- - -.. py:function:: set_license_key(apiuser, key) - - Sets the |RCE| license key. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param key: This is the license key to be set. - :type key: str - - Example output: - - .. code-block:: bash - - id : - result: { - "msg" : "updated license information", - "key": - } - error: null - - Example error output: - - .. code-block:: bash - - id : - result : null - error : { - "license key is not valid" - or - "trial licenses cannot be uploaded" - or - "error occurred while updating license" - } - - -get_server_info ---------------- - -.. py:function:: get_server_info(apiuser) - - Returns the |RCE| server information. - - This includes the running version of |RCE| and all installed - packages. This command takes the following options: - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - - Example output: - - .. code-block:: bash - - id : - result : { - 'modules': [,...] - 'py_version': , - 'platform': , - 'rhodecode_version': - } - error : null - - -get_user --------- - -.. py:function:: get_user(apiuser, userid=>) - - Returns the information associated with a username or userid. - - * If the ``userid`` is not set, this command returns the information - for the ``userid`` calling the method. - - .. note:: - - Normal users may only run this command against their ``userid``. For - full privileges you must run this command using an |authtoken| with - admin rights. - - This command takes the following options: - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param userid: Sets the userid for which data will be returned. - :type userid: Optional(str or int) - - Example output: - - .. code-block:: bash - - { - "error": null, - "id": , - "result": { - "active": true, - "admin": false, - "api_key": "api-key", - "api_keys": [ list of keys ], - "email": "user@example.com", - "emails": [ - "user@example.com" - ], - "extern_name": "rhodecode", - "extern_type": "rhodecode", - "firstname": "username", - "ip_addresses": [], - "language": null, - "last_login": "Timestamp", - "lastname": "surnae", - "permissions": { - "global": [ - "hg.inherit_default_perms.true", - "usergroup.read", - "hg.repogroup.create.false", - "hg.create.none", - "hg.extern_activate.manual", - "hg.create.write_on_repogroup.false", - "hg.usergroup.create.false", - "group.none", - "repository.none", - "hg.register.none", - "hg.fork.repository" - ], - "repositories": { "username/example": "repository.write"}, - "repositories_groups": { "user-group/repo": "group.none" }, - "user_groups": { "user_group_name": "usergroup.read" } - }, - "user_id": 32, - "username": "username" - } - } - - -get_users ---------- - -.. py:function:: get_users(apiuser) - - Lists all users in the |RCE| user database. - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - This command takes the following options: - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - - Example output: - - .. code-block:: bash - - id : - result: [, ...] - error: null - - -create_user ------------ - -.. py:function:: create_user(apiuser, username, email, password=, firstname=, lastname=, active=, admin=, extern_name=, extern_type=, force_password_change=) - - Creates a new user and returns the new user object. - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - This command takes the following options: - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param username: Set the new username. - :type username: str or int - :param email: Set the user email address. - :type email: str - :param password: Set the new user password. - :type password: Optional(str) - :param firstname: Set the new user firstname. - :type firstname: Optional(str) - :param lastname: Set the new user surname. - :type lastname: Optional(str) - :param active: Set the user as active. - :type active: Optional(``True`` | ``False``) - :param admin: Give the new user admin rights. - :type admin: Optional(``True`` | ``False``) - :param extern_name: Set the authentication plugin name. - Using LDAP this is filled with LDAP UID. - :type extern_name: Optional(str) - :param extern_type: Set the new user authentication plugin. - :type extern_type: Optional(str) - :param force_password_change: Force the new user to change password - on next login. - :type force_password_change: Optional(``True`` | ``False``) - - Example output: - - .. code-block:: bash - - id : - result: { - "msg" : "created new user ``", - "user": - } - error: null - - Example error output: - - .. code-block:: bash - - id : - result : null - error : { - "user `` already exist" - or - "email `` already exist" - or - "failed to create user ``" - } - - -update_user ------------ - -.. py:function:: update_user(apiuser, userid, username=, email=, password=, firstname=, lastname=, active=, admin=, extern_type=, extern_name=) - - Updates the details for the specified user, if that user exists. - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - This command takes the following options: - - :param apiuser: This is filled automatically from |authtoken|. - :type apiuser: AuthUser - :param userid: Set the ``userid`` to update. - :type userid: str or int - :param username: Set the new username. - :type username: str or int - :param email: Set the new email. - :type email: str - :param password: Set the new password. - :type password: Optional(str) - :param firstname: Set the new first name. - :type firstname: Optional(str) - :param lastname: Set the new surname. - :type lastname: Optional(str) - :param active: Set the new user as active. - :type active: Optional(``True`` | ``False``) - :param admin: Give the user admin rights. - :type admin: Optional(``True`` | ``False``) - :param extern_name: Set the authentication plugin user name. - Using LDAP this is filled with LDAP UID. - :type extern_name: Optional(str) - :param extern_type: Set the authentication plugin type. - :type extern_type: Optional(str) - - - Example output: - - .. code-block:: bash - - id : - result: { - "msg" : "updated user ID: ", - "user": , - } - error: null - - Example error output: - - .. code-block:: bash - - id : - result : null - error : { - "failed to update user ``" - } - - -delete_user ------------ - -.. py:function:: delete_user(apiuser, userid) - - Deletes the specified user from the |RCE| user database. - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - .. important:: - - Ensure all open pull requests and open code review - requests to this user are close. - - Also ensure all repositories, or repository groups owned by this - user are reassigned before deletion. - - This command takes the following options: - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param userid: Set the user to delete. - :type userid: str or int - - Example output: - - .. code-block:: bash - - id : - result: { - "msg" : "deleted user ID: ", - "user": null - } - error: null - - Example error output: - - .. code-block:: bash - - id : - result : null - error : { - "failed to delete user ID: " - } - - -get_user_group --------------- - -.. py:function:: get_user_group(apiuser, usergroupid) - - Returns the data of an existing user group. - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param usergroupid: Set the user group from which to return data. - :type usergroupid: str or int - - Example error output: - - .. code-block:: bash - - { - "error": null, - "id": , - "result": { - "active": true, - "group_description": "group description", - "group_name": "group name", - "members": [ - { - "name": "owner-name", - "origin": "owner", - "permission": "usergroup.admin", - "type": "user" - }, - { - { - "name": "user name", - "origin": "permission", - "permission": "usergroup.admin", - "type": "user" - }, - { - "name": "user group name", - "origin": "permission", - "permission": "usergroup.write", - "type": "user_group" - } - ], - "owner": "owner name", - "users": [], - "users_group_id": 2 - } - } - - -get_user_groups ---------------- - -.. py:function:: get_user_groups(apiuser) - - Lists all the existing user groups within RhodeCode. - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - This command takes the following options: - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - - Example error output: - - .. code-block:: bash - - id : - result : [,...] - error : null - - -create_user_group ------------------ - -.. py:function:: create_user_group(apiuser, group_name, description=, owner=>, active=) - - Creates a new user group. - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - This command takes the following options: - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param group_name: Set the name of the new user group. - :type group_name: str - :param description: Give a description of the new user group. - :type description: str - :param owner: Set the owner of the new user group. - If not set, the owner is the |authtoken| user. - :type owner: Optional(str or int) - :param active: Set this group as active. - :type active: Optional(``True`` | ``False``) - - Example output: - - .. code-block:: bash - - id : - result: { - "msg": "created new user group ``", - "user_group": - } - error: null - - Example error output: - - .. code-block:: bash - - id : - result : null - error : { - "user group `` already exist" - or - "failed to create group ``" - } - - -update_user_group ------------------ - -.. py:function:: update_user_group(apiuser, usergroupid, group_name=, description=, owner=, active=) - - Updates the specified `user group` with the details provided. - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param usergroupid: Set the id of the `user group` to update. - :type usergroupid: str or int - :param group_name: Set the new name the `user group` - :type group_name: str - :param description: Give a description for the `user group` - :type description: str - :param owner: Set the owner of the `user group`. - :type owner: Optional(str or int) - :param active: Set the group as active. - :type active: Optional(``True`` | ``False``) - - Example output: - - .. code-block:: bash - - id : - result : { - "msg": 'updated user group ID: ', - "user_group": - } - error : null - - Example error output: - - .. code-block:: bash - - id : - result : null - error : { - "failed to update user group ``" - } - - -delete_user_group ------------------ - -.. py:function:: delete_user_group(apiuser, usergroupid) - - Deletes the specified `user group`. - - This command can only be run using an |authtoken| with admin rights to - the specified repository. - - This command takes the following options: - - :param apiuser: filled automatically from apikey - :type apiuser: AuthUser - :param usergroupid: - :type usergroupid: int - - Example output: - - .. code-block:: bash - - id : - result : { - "msg": "deleted user group ID: " - } - error : null - - Example error output: - - .. code-block:: bash - - id : - result : null - error : { - "failed to delete user group ID: " - or - "RepoGroup assigned to " - } - - -add_user_to_user_group ----------------------- - -.. py:function:: add_user_to_user_group(apiuser, usergroupid, userid) - - Adds a user to a `user group`. If the user already exists in the group - this command will return false. - - This command can only be run using an |authtoken| with admin rights to - the specified user group. - - This command takes the following options: - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param usergroupid: Set the name of the `user group` to which a - user will be added. - :type usergroupid: int - :param userid: Set the `user_id` of the user to add to the group. - :type userid: int - - Example output: - - .. code-block:: bash - - id : - result : { - "success": True|False # depends on if member is in group - "msg": "added member `` to user group `` | - User is already in that group" - - } - error : null - - Example error output: - - .. code-block:: bash - - id : - result : null - error : { - "failed to add member to user group ``" - } - - -remove_user_from_user_group ---------------------------- - -.. py:function:: remove_user_from_user_group(apiuser, usergroupid, userid) - - Removes a user from a user group. - - * If the specified user is not in the group, this command will return - `false`. - - This command can only be run using an |authtoken| with admin rights to - the specified user group. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param usergroupid: Sets the user group name. - :type usergroupid: str or int - :param userid: The user you wish to remove from |RCE|. - :type userid: str or int - - Example output: - - .. code-block:: bash - - id : - result: { - "success": True|False, # depends on if member is in group - "msg": "removed member from user group | - User wasn't in group" - } - error: null - - -grant_user_permission_to_user_group ------------------------------------ - -.. py:function:: grant_user_permission_to_user_group(apiuser, usergroupid, userid, perm) - - Set permissions for a user in a user group. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param usergroupid: Set the user group to edit permissions on. - :type usergroupid: str or int - :param userid: Set the user from whom you wish to set permissions. - :type userid: str - :param perm: (usergroup.(none|read|write|admin)) - :type perm: str - - Example output: - - .. code-block:: bash - - id : - result : { - "msg": "Granted perm: `` for user: `` in user group: ``", - "success": true - } - error : null - - -revoke_user_permission_from_user_group --------------------------------------- - -.. py:function:: revoke_user_permission_from_user_group(apiuser, usergroupid, userid) - - Revoke a users permissions in a user group. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param usergroupid: Set the user group from which to revoke the user - permissions. - :type: usergroupid: str or int - :param userid: Set the userid of the user whose permissions will be - revoked. - :type userid: str - - Example output: - - .. code-block:: bash - - id : - result : { - "msg": "Revoked perm for user: `` in user group: ``", - "success": true - } - error : null - - -grant_user_group_permission_to_user_group ------------------------------------------ - -.. py:function:: grant_user_group_permission_to_user_group(apiuser, usergroupid, sourceusergroupid, perm) - - Give one user group permissions to another user group. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param usergroupid: Set the user group on which to edit permissions. - :type usergroupid: str or int - :param sourceusergroupid: Set the source user group to which - access/permissions will be granted. - :type sourceusergroupid: str or int - :param perm: (usergroup.(none|read|write|admin)) - :type perm: str - - Example output: - - .. code-block:: bash - - id : - result : { - "msg": "Granted perm: `` for user group: `` in user group: ``", - "success": true - } - error : null - - -revoke_user_group_permission_from_user_group --------------------------------------------- - -.. py:function:: revoke_user_group_permission_from_user_group(apiuser, usergroupid, sourceusergroupid) - - Revoke the permissions that one user group has to another. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param usergroupid: Set the user group on which to edit permissions. - :type usergroupid: str or int - :param sourceusergroupid: Set the user group from which permissions - are revoked. - :type sourceusergroupid: str or int - - Example output: - - .. code-block:: bash - - id : - result : { - "msg": "Revoked perm for user group: `` in user group: ``", - "success": true - } - error : null - - -get_pull_request ----------------- - -.. py:function:: get_pull_request(apiuser, repoid, pullrequestid) - - Get a pull request based on the given ID. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: Repository name or repository ID from where the pull - request was opened. - :type repoid: str or int - :param pullrequestid: ID of the requested pull request. - :type pullrequestid: int - - Example output: - - .. code-block:: bash - - "id": , - "result": - { - "pull_request_id": "", - "url": "", - "title": "", - "description": "<description>", - "status" : "<status>", - "created_on": "<date_time_created>", - "updated_on": "<date_time_updated>", - "commit_ids": [ - ... - "<commit_id>", - "<commit_id>", - ... - ], - "review_status": "<review_status>", - "mergeable": { - "status": "<bool>", - "message": "<message>", - }, - "source": { - "clone_url": "<clone_url>", - "repository": "<repository_name>", - "reference": - { - "name": "<name>", - "type": "<type>", - "commit_id": "<commit_id>", - } - }, - "target": { - "clone_url": "<clone_url>", - "repository": "<repository_name>", - "reference": - { - "name": "<name>", - "type": "<type>", - "commit_id": "<commit_id>", - } - }, - "author": <user_obj>, - "reviewers": [ - ... - { - "user": "<user_obj>", - "review_status": "<review_status>", - } - ... - ] - }, - "error": null - - -get_pull_requests ------------------ - -.. py:function:: get_pull_requests(apiuser, repoid, status=<Optional:'new'>) - - Get all pull requests from the repository specified in `repoid`. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: Repository name or repository ID. - :type repoid: str or int - :param status: Only return pull requests with the specified status. - Valid options are. - * ``new`` (default) - * ``open`` - * ``closed`` - :type status: str - - Example output: - - .. code-block:: bash - - "id": <id_given_in_input>, - "result": - [ - ... - { - "pull_request_id": "<pull_request_id>", - "url": "<url>", - "title" : "<title>", - "description": "<description>", - "status": "<status>", - "created_on": "<date_time_created>", - "updated_on": "<date_time_updated>", - "commit_ids": [ - ... - "<commit_id>", - "<commit_id>", - ... - ], - "review_status": "<review_status>", - "mergeable": { - "status": "<bool>", - "message: "<message>", - }, - "source": { - "clone_url": "<clone_url>", - "reference": - { - "name": "<name>", - "type": "<type>", - "commit_id": "<commit_id>", - } - }, - "target": { - "clone_url": "<clone_url>", - "reference": - { - "name": "<name>", - "type": "<type>", - "commit_id": "<commit_id>", - } - }, - "author": <user_obj>, - "reviewers": [ - ... - { - "user": "<user_obj>", - "review_status": "<review_status>", - } - ... - ] - } - ... - ], - "error": null - - -merge_pull_request ------------------- - -.. py:function:: merge_pull_request(apiuser, repoid, pullrequestid, userid=<Optional:<OptionalAttr:apiuser>>) - - Merge the pull request specified by `pullrequestid` into its target - repository. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: The Repository name or repository ID of the - target repository to which the |pr| is to be merged. - :type repoid: str or int - :param pullrequestid: ID of the pull request which shall be merged. - :type pullrequestid: int - :param userid: Merge the pull request as this user. - :type userid: Optional(str or int) - - Example output: - - .. code-block:: bash - - "id": <id_given_in_input>, - "result": - { - "executed": "<bool>", - "failure_reason": "<int>", - "merge_commit_id": "<merge_commit_id>", - "possible": "<bool>" - }, - "error": null - - -close_pull_request ------------------- - -.. py:function:: close_pull_request(apiuser, repoid, pullrequestid, userid=<Optional:<OptionalAttr:apiuser>>) - - Close the pull request specified by `pullrequestid`. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: Repository name or repository ID to which the pull - request belongs. - :type repoid: str or int - :param pullrequestid: ID of the pull request to be closed. - :type pullrequestid: int - :param userid: Close the pull request as this user. - :type userid: Optional(str or int) - - Example output: - - .. code-block:: bash - - "id": <id_given_in_input>, - "result": - { - "pull_request_id": "<int>", - "closed": "<bool>" - }, - "error": null - - -comment_pull_request --------------------- - -.. py:function:: comment_pull_request(apiuser, repoid, pullrequestid, message=<Optional:None>, status=<Optional:None>, userid=<Optional:<OptionalAttr:apiuser>>) +.. toctree:: - Comment on the pull request specified with the `pullrequestid`, - in the |repo| specified by the `repoid`, and optionally change the - review status. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: The repository name or repository ID. - :type repoid: str or int - :param pullrequestid: The pull request ID. - :type pullrequestid: int - :param message: The text content of the comment. - :type message: str - :param status: (**Optional**) Set the approval status of the pull - request. Valid options are: - * not_reviewed - * approved - * rejected - * under_review - :type status: str - :param userid: Comment on the pull request as this user - :type userid: Optional(str or int) - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result : - { - "pull_request_id": "<Integer>", - "comment_id": "<Integer>" - } - error : null - - -create_pull_request -------------------- - -.. py:function:: create_pull_request(apiuser, source_repo, target_repo, source_ref, target_ref, title, description=<Optional:''>, reviewers=<Optional:None>) - - Creates a new pull request. - - Accepts refs in the following formats: - - * branch:<branch_name>:<sha> - * branch:<branch_name> - * bookmark:<bookmark_name>:<sha> (Mercurial only) - * bookmark:<bookmark_name> (Mercurial only) - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param source_repo: Set the source repository name. - :type source_repo: str - :param target_repo: Set the target repository name. - :type target_repo: str - :param source_ref: Set the source ref name. - :type source_ref: str - :param target_ref: Set the target ref name. - :type target_ref: str - :param title: Set the pull request title. - :type title: str - :param description: Set the pull request description. - :type description: Optional(str) - :param reviewers: Set the new pull request reviewers list. - :type reviewers: Optional(list) - - -update_pull_request -------------------- - -.. py:function:: update_pull_request(apiuser, repoid, pullrequestid, title=<Optional:''>, description=<Optional:''>, reviewers=<Optional:None>, update_commits=<Optional:None>, close_pull_request=<Optional:None>) - - Updates a pull request. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: The repository name or repository ID. - :type repoid: str or int - :param pullrequestid: The pull request ID. - :type pullrequestid: int - :param title: Set the pull request title. - :type title: str - :param description: Update pull request description. - :type description: Optional(str) - :param reviewers: Update pull request reviewers list with new value. - :type reviewers: Optional(list) - :param update_commits: Trigger update of commits for this pull request - :type: update_commits: Optional(bool) - :param close_pull_request: Close this pull request with rejected state - :type: close_pull_request: Optional(bool) - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result : - { - "msg": "Updated pull request `63`", - "pull_request": <pull_request_object>, - "updated_reviewers": { - "added": [ - "username" - ], - "removed": [] - }, - "updated_commits": { - "added": [ - "<sha1_hash>" - ], - "common": [ - "<sha1_hash>", - "<sha1_hash>", - ], - "removed": [] - } - } - error : null - - -get_repo --------- - -.. py:function:: get_repo(apiuser, repoid, cache=<Optional:True>) - - Gets an existing repository by its name or repository_id. - - The members section so the output returns users groups or users - associated with that repository. - - This command can only be run using an |authtoken| with admin rights, - or users with at least read rights to the |repo|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: The repository name or repository id. - :type repoid: str or int - :param cache: use the cached value for last changeset - :type: cache: Optional(bool) - - Example output: - - .. code-block:: bash - - { - "error": null, - "id": <repo_id>, - "result": { - "clone_uri": null, - "created_on": "timestamp", - "description": "repo description", - "enable_downloads": false, - "enable_locking": false, - "enable_statistics": false, - "followers": [ - { - "active": true, - "admin": false, - "api_key": "****************************************", - "api_keys": [ - "****************************************" - ], - "email": "user@example.com", - "emails": [ - "user@example.com" - ], - "extern_name": "rhodecode", - "extern_type": "rhodecode", - "firstname": "username", - "ip_addresses": [], - "language": null, - "last_login": "2015-09-16T17:16:35.854", - "lastname": "surname", - "user_id": <user_id>, - "username": "name" - } - ], - "fork_of": "parent-repo", - "landing_rev": [ - "rev", - "tip" - ], - "last_changeset": { - "author": "User <user@example.com>", - "branch": "default", - "date": "timestamp", - "message": "last commit message", - "parents": [ - { - "raw_id": "commit-id" - } - ], - "raw_id": "commit-id", - "revision": <revision number>, - "short_id": "short id" - }, - "lock_reason": null, - "locked_by": null, - "locked_date": null, - "members": [ - { - "name": "super-admin-name", - "origin": "super-admin", - "permission": "repository.admin", - "type": "user" - }, - { - "name": "owner-name", - "origin": "owner", - "permission": "repository.admin", - "type": "user" - }, - { - "name": "user-group-name", - "origin": "permission", - "permission": "repository.write", - "type": "user_group" - } - ], - "owner": "owner-name", - "permissions": [ - { - "name": "super-admin-name", - "origin": "super-admin", - "permission": "repository.admin", - "type": "user" - }, - { - "name": "owner-name", - "origin": "owner", - "permission": "repository.admin", - "type": "user" - }, - { - "name": "user-group-name", - "origin": "permission", - "permission": "repository.write", - "type": "user_group" - } - ], - "private": true, - "repo_id": 676, - "repo_name": "user-group/repo-name", - "repo_type": "hg" - } - } - - -get_repos ---------- - -.. py:function:: get_repos(apiuser) - - Lists all existing repositories. - - This command can only be run using an |authtoken| with admin rights, - or users with at least read rights to |repos|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result: [ - { - "repo_id" : "<repo_id>", - "repo_name" : "<reponame>" - "repo_type" : "<repo_type>", - "clone_uri" : "<clone_uri>", - "private": : "<bool>", - "created_on" : "<datetimecreated>", - "description" : "<description>", - "landing_rev": "<landing_rev>", - "owner": "<repo_owner>", - "fork_of": "<name_of_fork_parent>", - "enable_downloads": "<bool>", - "enable_locking": "<bool>", - "enable_statistics": "<bool>", - }, - ... - ] - error: null - - -get_repo_changeset ------------------- - -.. py:function:: get_repo_changeset(apiuser, repoid, revision, details=<Optional:'basic'>) - - Returns information about a changeset. - - Additionally parameters define the amount of details returned by - this function. - - This command can only be run using an |authtoken| with admin rights, - or users with at least read rights to the |repo|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: The repository name or repository id - :type repoid: str or int - :param revision: revision for which listing should be done - :type revision: str - :param details: details can be 'basic|extended|full' full gives diff - info details like the diff itself, and number of changed files etc. - :type details: Optional(str) - - -get_repo_changesets -------------------- - -.. py:function:: get_repo_changesets(apiuser, repoid, start_rev, limit, details=<Optional:'basic'>) - - Returns a set of changesets limited by the number of commits starting - from the `start_rev` option. - - Additional parameters define the amount of details returned by this - function. - - This command can only be run using an |authtoken| with admin rights, - or users with at least read rights to |repos|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: The repository name or repository ID. - :type repoid: str or int - :param start_rev: The starting revision from where to get changesets. - :type start_rev: str - :param limit: Limit the number of changesets to this amount - :type limit: str or int - :param details: Set the level of detail returned. Valid option are: - ``basic``, ``extended`` and ``full``. - :type details: Optional(str) - - .. note:: - - Setting the parameter `details` to the value ``full`` is extensive - and returns details like the diff itself, and the number - of changed files. - - -get_repo_nodes --------------- - -.. py:function:: get_repo_nodes(apiuser, repoid, revision, root_path, ret_type=<Optional:'all'>, details=<Optional:'basic'>) - - Returns a list of nodes and children in a flat list for a given - path at given revision. - - It's possible to specify ret_type to show only `files` or `dirs`. - - This command can only be run using an |authtoken| with admin rights, - or users with at least read rights to |repos|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: The repository name or repository ID. - :type repoid: str or int - :param revision: The revision for which listing should be done. - :type revision: str - :param root_path: The path from which to start displaying. - :type root_path: str - :param ret_type: Set the return type. Valid options are - ``all`` (default), ``files`` and ``dirs``. - :type ret_type: Optional(str) - :param details: Returns extended information about nodes, such as - md5, binary, and or content. The valid options are ``basic`` and - ``full``. - :type details: Optional(str) - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result: [ - { - "name" : "<name>" - "type" : "<type>", - "binary": "<true|false>" (only in extended mode) - "md5" : "<md5 of file content>" (only in extended mode) - }, - ... - ] - error: null - - -create_repo ------------ - -.. py:function:: create_repo(apiuser, repo_name, repo_type, owner=<Optional:<OptionalAttr:apiuser>>, description=<Optional:''>, private=<Optional:False>, clone_uri=<Optional:None>, landing_rev=<Optional:'rev:tip'>, enable_statistics=<Optional:False>, enable_locking=<Optional:False>, enable_downloads=<Optional:False>, copy_permissions=<Optional:False>) - - Creates a repository. - - * If the repository name contains "/", all the required repository - groups will be created. - - For example "foo/bar/baz" will create |repo| groups "foo" and "bar" - (with "foo" as parent). It will also create the "baz" repository - with "bar" as |repo| group. - - This command can only be run using an |authtoken| with at least - write permissions to the |repo|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repo_name: Set the repository name. - :type repo_name: str - :param repo_type: Set the repository type; 'hg','git', or 'svn'. - :type repo_type: str - :param owner: user_id or username - :type owner: Optional(str) - :param description: Set the repository description. - :type description: Optional(str) - :param private: - :type private: bool - :param clone_uri: - :type clone_uri: str - :param landing_rev: <rev_type>:<rev> - :type landing_rev: str - :param enable_locking: - :type enable_locking: bool - :param enable_downloads: - :type enable_downloads: bool - :param enable_statistics: - :type enable_statistics: bool - :param copy_permissions: Copy permission from group in which the - repository is being created. - :type copy_permissions: bool - - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result: { - "msg": "Created new repository `<reponame>`", - "success": true, - "task": "<celery task id or None if done sync>" - } - error: null - - - Example error output: - - .. code-block:: bash - - id : <id_given_in_input> - result : null - error : { - 'failed to create repository `<repo_name>` - } - - -add_field_to_repo ------------------ - -.. py:function:: add_field_to_repo(apiuser, repoid, key, label=<Optional:''>, description=<Optional:''>) - - Adds an extra field to a repository. - - This command can only be run using an |authtoken| with at least - write permissions to the |repo|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: Set the repository name or repository id. - :type repoid: str or int - :param key: Create a unique field key for this repository. - :type key: str - :param label: - :type label: Optional(str) - :param description: - :type description: Optional(str) - - -remove_field_from_repo ----------------------- - -.. py:function:: remove_field_from_repo(apiuser, repoid, key) - - Removes an extra field from a repository. - - This command can only be run using an |authtoken| with at least - write permissions to the |repo|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: Set the repository name or repository ID. - :type repoid: str or int - :param key: Set the unique field key for this repository. - :type key: str - - -update_repo ------------ - -.. py:function:: update_repo(apiuser, repoid, name=<Optional:None>, owner=<Optional:<OptionalAttr:apiuser>>, group=<Optional:None>, fork_of=<Optional:None>, description=<Optional:''>, private=<Optional:False>, clone_uri=<Optional:None>, landing_rev=<Optional:'rev:tip'>, enable_statistics=<Optional:False>, enable_locking=<Optional:False>, enable_downloads=<Optional:False>, fields=<Optional:''>) - - Updates a repository with the given information. - - This command can only be run using an |authtoken| with at least - write permissions to the |repo|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: repository name or repository ID. - :type repoid: str or int - :param name: Update the |repo| name. - :type name: str - :param owner: Set the |repo| owner. - :type owner: str - :param group: Set the |repo| group the |repo| belongs to. - :type group: str - :param fork_of: Set the master |repo| name. - :type fork_of: str - :param description: Update the |repo| description. - :type description: str - :param private: Set the |repo| as private. (True | False) - :type private: bool - :param clone_uri: Update the |repo| clone URI. - :type clone_uri: str - :param landing_rev: Set the |repo| landing revision. Default is - ``tip``. - :type landing_rev: str - :param enable_statistics: Enable statistics on the |repo|, - (True | False). - :type enable_statistics: bool - :param enable_locking: Enable |repo| locking. - :type enable_locking: bool - :param enable_downloads: Enable downloads from the |repo|, - (True | False). - :type enable_downloads: bool - :param fields: Add extra fields to the |repo|. Use the following - example format: ``field_key=field_val,field_key2=fieldval2``. - Escape ', ' with \, - :type fields: str - - -fork_repo ---------- - -.. py:function:: fork_repo(apiuser, repoid, fork_name, owner=<Optional:<OptionalAttr:apiuser>>, description=<Optional:''>, copy_permissions=<Optional:False>, private=<Optional:False>, landing_rev=<Optional:'rev:tip'>) - - Creates a fork of the specified |repo|. - - * If using |RCE| with Celery this will immediately return a success - message, even though the fork will be created asynchronously. - - This command can only be run using an |authtoken| with fork - permissions on the |repo|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: Set repository name or repository ID. - :type repoid: str or int - :param fork_name: Set the fork name. - :type fork_name: str - :param owner: Set the fork owner. - :type owner: str - :param description: Set the fork descripton. - :type description: str - :param copy_permissions: Copy permissions from parent |repo|. The - default is False. - :type copy_permissions: bool - :param private: Make the fork private. The default is False. - :type private: bool - :param landing_rev: Set the landing revision. The default is tip. - - Example output: - - .. code-block:: bash - - id : <id_for_response> - api_key : "<api_key>" - args: { - "repoid" : "<reponame or repo_id>", - "fork_name": "<forkname>", - "owner": "<username or user_id = Optional(=apiuser)>", - "description": "<description>", - "copy_permissions": "<bool>", - "private": "<bool>", - "landing_rev": "<landing_rev>" - } - - Example error output: - - .. code-block:: bash - - id : <id_given_in_input> - result: { - "msg": "Created fork of `<reponame>` as `<forkname>`", - "success": true, - "task": "<celery task id or None if done sync>" - } - error: null - - -delete_repo ------------ - -.. py:function:: delete_repo(apiuser, repoid, forks=<Optional:''>) - - Deletes a repository. - - * When the `forks` parameter is set it's possible to detach or delete - forks of deleted repository. - - This command can only be run using an |authtoken| with admin - permissions on the |repo|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: Set the repository name or repository ID. - :type repoid: str or int - :param forks: Set to `detach` or `delete` forks from the |repo|. - :type forks: Optional(str) - - Example error output: - - .. code-block:: bash - - id : <id_given_in_input> - result: { - "msg": "Deleted repository `<reponame>`", - "success": true - } - error: null - - -comment_commit --------------- - -.. py:function:: comment_commit(apiuser, repoid, commit_id, message, userid=<Optional:<OptionalAttr:apiuser>>, status=<Optional:None>) - - Set a commit comment, and optionally change the status of the commit. - This command can be executed only using api_key belonging to user - with admin rights, or repository administrator. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: Set the repository name or repository ID. - :type repoid: str or int - :param commit_id: Specify the commit_id for which to set a comment. - :type commit_id: str - :param message: The comment text. - :type message: str - :param userid: Set the user name of the comment creator. - :type userid: Optional(str or int) - :param status: status, one of 'not_reviewed', 'approved', 'rejected', - 'under_review' - :type status: str - - Example error output: - - .. code-block:: json - - { - "id" : <id_given_in_input>, - "result" : { - "msg": "Commented on commit `<commit_id>` for repository `<repoid>`", - "status_change": null or <status>, - "success": true - }, - "error" : null - } - - -changeset_comment ------------------ - -.. py:function:: changeset_comment(apiuser, repoid, revision, message, userid=<Optional:<OptionalAttr:apiuser>>, status=<Optional:None>) - - .. deprecated:: 3.4.0 - - Please use method `comment_commit` instead. - - - Set a changeset comment, and optionally change the status of the - changeset. - - This command can only be run using an |authtoken| with admin - permissions on the |repo|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: Set the repository name or repository ID. - :type repoid: str or int - :param revision: Specify the revision for which to set a comment. - :type revision: str - :param message: The comment text. - :type message: str - :param userid: Set the user name of the comment creator. - :type userid: Optional(str or int) - :param status: Set the comment status. The following are valid options: - * not_reviewed - * approved - * rejected - * under_review - :type status: str - - Example error output: - - .. code-block:: json - - { - "id" : <id_given_in_input>, - "result" : { - "msg": "Commented on commit `<revision>` for repository `<repoid>`", - "status_change": null or <status>, - "success": true - }, - "error" : null - } - - -grant_user_permission ---------------------- - -.. py:function:: grant_user_permission(apiuser, repoid, userid, perm) - - Grant permissions for the specified user on the given repository, - or update existing permissions if found. - - This command can only be run using an |authtoken| with admin - permissions on the |repo|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: Set the repository name or repository ID. - :type repoid: str or int - :param userid: Set the user name. - :type userid: str - :param perm: Set the user permissions, using the following format - ``(repository.(none|read|write|admin))`` - :type perm: str - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result: { - "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`", - "success": true - } - error: null - - -revoke_user_permission ----------------------- - -.. py:function:: revoke_user_permission(apiuser, repoid, userid) - - Revoke permission for a user on the specified repository. - - This command can only be run using an |authtoken| with admin - permissions on the |repo|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: Set the repository name or repository ID. - :type repoid: str or int - :param userid: Set the user name of revoked user. - :type userid: str or int - - Example error output: - - .. code-block:: bash - - id : <id_given_in_input> - result: { - "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`", - "success": true - } - error: null - - -grant_user_group_permission ---------------------------- - -.. py:function:: grant_user_group_permission(apiuser, repoid, usergroupid, perm) - - Grant permission for a user group on the specified repository, - or update existing permissions. - - This command can only be run using an |authtoken| with admin - permissions on the |repo|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: Set the repository name or repository ID. - :type repoid: str or int - :param usergroupid: Specify the ID of the user group. - :type usergroupid: str or int - :param perm: Set the user group permissions using the following - format: (repository.(none|read|write|admin)) - :type perm: str - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result : { - "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`", - "success": true - - } - error : null - - Example error output: - - .. code-block:: bash - - id : <id_given_in_input> - result : null - error : { - "failed to edit permission for user group: `<usergroup>` in repo `<repo>`' - } - - -revoke_user_group_permission ----------------------------- - -.. py:function:: revoke_user_group_permission(apiuser, repoid, usergroupid) - - Revoke the permissions of a user group on a given repository. - - This command can only be run using an |authtoken| with admin - permissions on the |repo|. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repoid: Set the repository name or repository ID. - :type repoid: str or int - :param usergroupid: Specify the user group ID. - :type usergroupid: str or int - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result: { - "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`", - "success": true - } - error: null - - -get_repo_group --------------- - -.. py:function:: get_repo_group(apiuser, repogroupid) - - Return the specified |repo| group, along with permissions, - and repositories inside the group - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repogroupid: Specify the name of ID of the repository group. - :type repogroupid: str or int - - - Example output: - - .. code-block:: bash - - { - "error": null, - "id": repo-group-id, - "result": { - "group_description": "repo group description", - "group_id": 14, - "group_name": "group name", - "members": [ - { - "name": "super-admin-username", - "origin": "super-admin", - "permission": "group.admin", - "type": "user" - }, - { - "name": "owner-name", - "origin": "owner", - "permission": "group.admin", - "type": "user" - }, - { - "name": "user-group-name", - "origin": "permission", - "permission": "group.write", - "type": "user_group" - } - ], - "owner": "owner-name", - "parent_group": null, - "repositories": [ repo-list ] - } - } - - -get_repo_groups ---------------- - -.. py:function:: get_repo_groups(apiuser) - - Returns all repository groups. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - - -create_repo_group ------------------ - -.. py:function:: create_repo_group(apiuser, group_name, description=<Optional:''>, owner=<Optional:<OptionalAttr:apiuser>>, copy_permissions=<Optional:False>) - - Creates a repository group. - - * If the repository group name contains "/", all the required repository - groups will be created. - - For example "foo/bar/baz" will create |repo| groups "foo" and "bar" - (with "foo" as parent). It will also create the "baz" repository - with "bar" as |repo| group. - - This command can only be run using an |authtoken| with admin - permissions. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param group_name: Set the repository group name. - :type group_name: str - :param description: Set the |repo| group description. - :type description: str - :param owner: Set the |repo| group owner. - :type owner: str - :param copy_permissions: - :type copy_permissions: - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result : { - "msg": "Created new repo group `<repo_group_name>`" - "repo_group": <repogroup_object> - } - error : null - - - Example error output: - - .. code-block:: bash - - id : <id_given_in_input> - result : null - error : { - failed to create repo group `<repogroupid>` - } - - -update_repo_group ------------------ - -.. py:function:: update_repo_group(apiuser, repogroupid, group_name=<Optional:''>, description=<Optional:''>, owner=<Optional:<OptionalAttr:apiuser>>, parent=<Optional:None>, enable_locking=<Optional:False>) - - Updates repository group with the details given. - - This command can only be run using an |authtoken| with admin - permissions. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repogroupid: Set the ID of repository group. - :type repogroupid: str or int - :param group_name: Set the name of the |repo| group. - :type group_name: str - :param description: Set a description for the group. - :type description: str - :param owner: Set the |repo| group owner. - :type owner: str - :param parent: Set the |repo| group parent. - :type parent: str or int - :param enable_locking: Enable |repo| locking. The default is false. - :type enable_locking: bool - - -delete_repo_group ------------------ - -.. py:function:: delete_repo_group(apiuser, repogroupid) - - Deletes a |repo| group. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repogroupid: Set the name or ID of repository group to be - deleted. - :type repogroupid: str or int - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result : { - 'msg': 'deleted repo group ID:<repogroupid> <repogroupname> - 'repo_group': null - } - error : null - - Example error output: - - .. code-block:: bash - - id : <id_given_in_input> - result : null - error : { - "failed to delete repo group ID:<repogroupid> <repogroupname>" - } - - -grant_user_permission_to_repo_group ------------------------------------ - -.. py:function:: grant_user_permission_to_repo_group(apiuser, repogroupid, userid, perm, apply_to_children=<Optional:'none'>) - - Grant permission for a user on the given repository group, or update - existing permissions if found. - - This command can only be run using an |authtoken| with admin - permissions. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repogroupid: Set the name or ID of repository group. - :type repogroupid: str or int - :param userid: Set the user name. - :type userid: str - :param perm: (group.(none|read|write|admin)) - :type perm: str - :param apply_to_children: 'none', 'repos', 'groups', 'all' - :type apply_to_children: str - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result: { - "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`", - "success": true - } - error: null - - Example error output: - - .. code-block:: bash - - id : <id_given_in_input> - result : null - error : { - "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`" - } - - -revoke_user_permission_from_repo_group --------------------------------------- - -.. py:function:: revoke_user_permission_from_repo_group(apiuser, repogroupid, userid, apply_to_children=<Optional:'none'>) - - Revoke permission for a user in a given repository group. - - This command can only be run using an |authtoken| with admin - permissions on the |repo| group. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repogroupid: Set the name or ID of the repository group. - :type repogroupid: str or int - :param userid: Set the user name to revoke. - :type userid: str - :param apply_to_children: 'none', 'repos', 'groups', 'all' - :type apply_to_children: str - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result: { - "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`", - "success": true - } - error: null - - Example error output: - - .. code-block:: bash - - id : <id_given_in_input> - result : null - error : { - "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`" - } - - -grant_user_group_permission_to_repo_group ------------------------------------------ - -.. py:function:: grant_user_group_permission_to_repo_group(apiuser, repogroupid, usergroupid, perm, apply_to_children=<Optional:'none'>) - - Grant permission for a user group on given repository group, or update - existing permissions if found. - - This command can only be run using an |authtoken| with admin - permissions on the |repo| group. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repogroupid: Set the name or id of repository group - :type repogroupid: str or int - :param usergroupid: id of usergroup - :type usergroupid: str or int - :param perm: (group.(none|read|write|admin)) - :type perm: str - :param apply_to_children: 'none', 'repos', 'groups', 'all' - :type apply_to_children: str - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result : { - "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`", - "success": true - - } - error : null - - Example error output: - - .. code-block:: bash - - id : <id_given_in_input> - result : null - error : { - "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`" - } - - -revoke_user_group_permission_from_repo_group --------------------------------------------- - -.. py:function:: revoke_user_group_permission_from_repo_group(apiuser, repogroupid, usergroupid, apply_to_children=<Optional:'none'>) - - Revoke permission for user group on given repository. - - This command can only be run using an |authtoken| with admin - permissions on the |repo| group. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param repogroupid: name or id of repository group - :type repogroupid: str or int - :param usergroupid: - :param apply_to_children: 'none', 'repos', 'groups', 'all' - :type apply_to_children: str - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result: { - "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`", - "success": true - } - error: null - - Example error output: - - .. code-block:: bash - - id : <id_given_in_input> - result : null - error : { - "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`" - } - - -get_gist --------- - -.. py:function:: get_gist(apiuser, gistid, content=<Optional:False>) - - Get the specified gist, based on the gist ID. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param gistid: Set the id of the private or public gist - :type gistid: str - :param content: Return the gist content. Default is false. - :type content: Optional(bool) - - -get_gists ---------- - -.. py:function:: get_gists(apiuser, userid=<Optional:<OptionalAttr:apiuser>>) - - Get all gists for given user. If userid is empty returned gists - are for user who called the api - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param userid: user to get gists for - :type userid: Optional(str or int) - - -create_gist ------------ - -.. py:function:: create_gist(apiuser, files, owner=<Optional:<OptionalAttr:apiuser>>, gist_type=<Optional:u'public'>, lifetime=<Optional:-1>, acl_level=<Optional:u'acl_public'>, description=<Optional:''>) - - Creates a new Gist. - - :param apiuser: This is filled automatically from the |authtoken|. - :type apiuser: AuthUser - :param files: files to be added to the gist. The data structure has - to match the following example:: - - {'filename': {'content':'...', 'lexer': null}, - 'filename2': {'content':'...', 'lexer': null}} - - :type files: dict - :param owner: Set the gist owner, defaults to api method caller - :type owner: Optional(str or int) - :param gist_type: type of gist ``public`` or ``private`` - :type gist_type: Optional(str) - :param lifetime: time in minutes of gist lifetime - :type lifetime: Optional(int) - :param acl_level: acl level for this gist, can be - ``acl_public`` or ``acl_private`` If the value is set to - ``acl_private`` only logged in users are able to access this gist. - If not set it defaults to ``acl_public``. - :type acl_level: Optional(str) - :param description: gist description - :type description: Optional(str) - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result : { - "msg": "created new gist", - "gist": {} - } - error : null - - Example error output: - - .. code-block:: bash - - id : <id_given_in_input> - result : null - error : { - "failed to create gist" - } - - -delete_gist ------------ - -.. py:function:: delete_gist(apiuser, gistid) - - Deletes existing gist - - :param apiuser: filled automatically from apikey - :type apiuser: AuthUser - :param gistid: id of gist to delete - :type gistid: str - - Example output: - - .. code-block:: bash - - id : <id_given_in_input> - result : { - "deleted gist ID: <gist_id>", - "gist": null - } - error : null - - Example error output: - - .. code-block:: bash - - id : <id_given_in_input> - result : null - error : { - "failed to delete gist ID:<gist_id>" - } - + methods/license-methods + methods/deprecated-methods + methods/gist-methods + methods/pull-request-methods + methods/repo-methods + methods/repo-group-methods + methods/server-methods + methods/user-methods + methods/user-group-methods diff --git a/docs/api/methods/deprecated-methods.rst b/docs/api/methods/deprecated-methods.rst new file mode 100644 --- /dev/null +++ b/docs/api/methods/deprecated-methods.rst @@ -0,0 +1,77 @@ +.. _deprecated-methods-ref: + +deprecated methods +================= + +changeset_comment +----------------- + +.. py:function:: changeset_comment(apiuser, repoid, revision, message, userid=<Optional:<OptionalAttr:apiuser>>, status=<Optional:None>) + + .. deprecated:: 3.4.0 + + Please use method `comment_commit` instead. + + + Set a changeset comment, and optionally change the status of the + changeset. + + This command can only be run using an |authtoken| with admin + permissions on the |repo|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: Set the repository name or repository ID. + :type repoid: str or int + :param revision: Specify the revision for which to set a comment. + :type revision: str + :param message: The comment text. + :type message: str + :param userid: Set the user name of the comment creator. + :type userid: Optional(str or int) + :param status: Set the comment status. The following are valid options: + * not_reviewed + * approved + * rejected + * under_review + :type status: str + + Example error output: + + .. code-block:: json + + { + "id" : <id_given_in_input>, + "result" : { + "msg": "Commented on commit `<revision>` for repository `<repoid>`", + "status_change": null or <status>, + "success": true + }, + "error" : null + } + + +get_locks +--------- + +.. py:function:: get_locks(apiuser, userid=<Optional:<OptionalAttr:apiuser>>) + + .. deprecated:: 4.0.0 + + Please use method `get_user_locks` instead. + + None + + +show_ip +------- + +.. py:function:: show_ip(apiuser, userid=<Optional:<OptionalAttr:apiuser>>) + + .. deprecated:: 4.0.0 + + Please use method `get_ip` instead. + + None + + diff --git a/docs/api/methods/gist-methods.rst b/docs/api/methods/gist-methods.rst new file mode 100644 --- /dev/null +++ b/docs/api/methods/gist-methods.rst @@ -0,0 +1,121 @@ +.. _gist-methods-ref: + +gist methods +================= + +create_gist +----------- + +.. py:function:: create_gist(apiuser, files, gistid=<Optional:None>, owner=<Optional:<OptionalAttr:apiuser>>, gist_type=<Optional:u'public'>, lifetime=<Optional:-1>, acl_level=<Optional:u'acl_public'>, description=<Optional:''>) + + Creates a new Gist. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param files: files to be added to the gist. The data structure has + to match the following example:: + + {'filename1': {'content':'...'}, 'filename2': {'content':'...'}} + + :type files: dict + :param gistid: Set a custom id for the gist + :type gistid: Optional(str) + :param owner: Set the gist owner, defaults to api method caller + :type owner: Optional(str or int) + :param gist_type: type of gist ``public`` or ``private`` + :type gist_type: Optional(str) + :param lifetime: time in minutes of gist lifetime + :type lifetime: Optional(int) + :param acl_level: acl level for this gist, can be + ``acl_public`` or ``acl_private`` If the value is set to + ``acl_private`` only logged in users are able to access this gist. + If not set it defaults to ``acl_public``. + :type acl_level: Optional(str) + :param description: gist description + :type description: Optional(str) + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "msg": "created new gist", + "gist": {} + } + error : null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "failed to create gist" + } + + +delete_gist +----------- + +.. py:function:: delete_gist(apiuser, gistid) + + Deletes existing gist + + :param apiuser: filled automatically from apikey + :type apiuser: AuthUser + :param gistid: id of gist to delete + :type gistid: str + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "deleted gist ID: <gist_id>", + "gist": null + } + error : null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "failed to delete gist ID:<gist_id>" + } + + +get_gist +-------- + +.. py:function:: get_gist(apiuser, gistid, content=<Optional:False>) + + Get the specified gist, based on the gist ID. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param gistid: Set the id of the private or public gist + :type gistid: str + :param content: Return the gist content. Default is false. + :type content: Optional(bool) + + +get_gists +--------- + +.. py:function:: get_gists(apiuser, userid=<Optional:<OptionalAttr:apiuser>>) + + Get all gists for given user. If userid is empty returned gists + are for user who called the api + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param userid: user to get gists for + :type userid: Optional(str or int) + + diff --git a/docs/api/methods/license-methods.rst b/docs/api/methods/license-methods.rst new file mode 100644 --- /dev/null +++ b/docs/api/methods/license-methods.rst @@ -0,0 +1,71 @@ +.. _license-methods-ref: + +license methods +================= + +get_license_info (EE only) +---------------- + +.. py:function:: get_license_info(apiuser) + + Returns the |RCE| license information. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + 'rhodecode_version': <rhodecode version>, + 'token': <license token>, + 'issued_to': <license owner>, + 'issued_on': <license issue date>, + 'expires_on': <license expiration date>, + 'type': <license type>, + 'users_limit': <license users limit>, + 'key': <license key> + } + error : null + + +set_license_key (EE only) +--------------- + +.. py:function:: set_license_key(apiuser, key) + + Sets the |RCE| license key. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param key: This is the license key to be set. + :type key: str + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "msg" : "updated license information", + "key": <key> + } + error: null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "license key is not valid" + or + "trial licenses cannot be uploaded" + or + "error occurred while updating license" + } + + diff --git a/docs/api/methods/pull-request-methods.rst b/docs/api/methods/pull-request-methods.rst new file mode 100644 --- /dev/null +++ b/docs/api/methods/pull-request-methods.rst @@ -0,0 +1,344 @@ +.. _pull-request-methods-ref: + +pull_request methods +================= + +close_pull_request +------------------ + +.. py:function:: close_pull_request(apiuser, repoid, pullrequestid, userid=<Optional:<OptionalAttr:apiuser>>) + + Close the pull request specified by `pullrequestid`. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: Repository name or repository ID to which the pull + request belongs. + :type repoid: str or int + :param pullrequestid: ID of the pull request to be closed. + :type pullrequestid: int + :param userid: Close the pull request as this user. + :type userid: Optional(str or int) + + Example output: + + .. code-block:: bash + + "id": <id_given_in_input>, + "result": + { + "pull_request_id": "<int>", + "closed": "<bool>" + }, + "error": null + + +comment_pull_request +-------------------- + +.. py:function:: comment_pull_request(apiuser, repoid, pullrequestid, message=<Optional:None>, status=<Optional:None>, userid=<Optional:<OptionalAttr:apiuser>>) + + Comment on the pull request specified with the `pullrequestid`, + in the |repo| specified by the `repoid`, and optionally change the + review status. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: The repository name or repository ID. + :type repoid: str or int + :param pullrequestid: The pull request ID. + :type pullrequestid: int + :param message: The text content of the comment. + :type message: str + :param status: (**Optional**) Set the approval status of the pull + request. Valid options are: + * not_reviewed + * approved + * rejected + * under_review + :type status: str + :param userid: Comment on the pull request as this user + :type userid: Optional(str or int) + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : + { + "pull_request_id": "<Integer>", + "comment_id": "<Integer>" + } + error : null + + +create_pull_request +------------------- + +.. py:function:: create_pull_request(apiuser, source_repo, target_repo, source_ref, target_ref, title, description=<Optional:''>, reviewers=<Optional:None>) + + Creates a new pull request. + + Accepts refs in the following formats: + + * branch:<branch_name>:<sha> + * branch:<branch_name> + * bookmark:<bookmark_name>:<sha> (Mercurial only) + * bookmark:<bookmark_name> (Mercurial only) + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param source_repo: Set the source repository name. + :type source_repo: str + :param target_repo: Set the target repository name. + :type target_repo: str + :param source_ref: Set the source ref name. + :type source_ref: str + :param target_ref: Set the target ref name. + :type target_ref: str + :param title: Set the pull request title. + :type title: str + :param description: Set the pull request description. + :type description: Optional(str) + :param reviewers: Set the new pull request reviewers list. + :type reviewers: Optional(list) + + +get_pull_request +---------------- + +.. py:function:: get_pull_request(apiuser, repoid, pullrequestid) + + Get a pull request based on the given ID. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: Repository name or repository ID from where the pull + request was opened. + :type repoid: str or int + :param pullrequestid: ID of the requested pull request. + :type pullrequestid: int + + Example output: + + .. code-block:: bash + + "id": <id_given_in_input>, + "result": + { + "pull_request_id": "<pull_request_id>", + "url": "<url>", + "title": "<title>", + "description": "<description>", + "status" : "<status>", + "created_on": "<date_time_created>", + "updated_on": "<date_time_updated>", + "commit_ids": [ + ... + "<commit_id>", + "<commit_id>", + ... + ], + "review_status": "<review_status>", + "mergeable": { + "status": "<bool>", + "message": "<message>", + }, + "source": { + "clone_url": "<clone_url>", + "repository": "<repository_name>", + "reference": + { + "name": "<name>", + "type": "<type>", + "commit_id": "<commit_id>", + } + }, + "target": { + "clone_url": "<clone_url>", + "repository": "<repository_name>", + "reference": + { + "name": "<name>", + "type": "<type>", + "commit_id": "<commit_id>", + } + }, + "author": <user_obj>, + "reviewers": [ + ... + { + "user": "<user_obj>", + "review_status": "<review_status>", + } + ... + ] + }, + "error": null + + +get_pull_requests +----------------- + +.. py:function:: get_pull_requests(apiuser, repoid, status=<Optional:'new'>) + + Get all pull requests from the repository specified in `repoid`. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: Repository name or repository ID. + :type repoid: str or int + :param status: Only return pull requests with the specified status. + Valid options are. + * ``new`` (default) + * ``open`` + * ``closed`` + :type status: str + + Example output: + + .. code-block:: bash + + "id": <id_given_in_input>, + "result": + [ + ... + { + "pull_request_id": "<pull_request_id>", + "url": "<url>", + "title" : "<title>", + "description": "<description>", + "status": "<status>", + "created_on": "<date_time_created>", + "updated_on": "<date_time_updated>", + "commit_ids": [ + ... + "<commit_id>", + "<commit_id>", + ... + ], + "review_status": "<review_status>", + "mergeable": { + "status": "<bool>", + "message: "<message>", + }, + "source": { + "clone_url": "<clone_url>", + "reference": + { + "name": "<name>", + "type": "<type>", + "commit_id": "<commit_id>", + } + }, + "target": { + "clone_url": "<clone_url>", + "reference": + { + "name": "<name>", + "type": "<type>", + "commit_id": "<commit_id>", + } + }, + "author": <user_obj>, + "reviewers": [ + ... + { + "user": "<user_obj>", + "review_status": "<review_status>", + } + ... + ] + } + ... + ], + "error": null + + +merge_pull_request +------------------ + +.. py:function:: merge_pull_request(apiuser, repoid, pullrequestid, userid=<Optional:<OptionalAttr:apiuser>>) + + Merge the pull request specified by `pullrequestid` into its target + repository. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: The Repository name or repository ID of the + target repository to which the |pr| is to be merged. + :type repoid: str or int + :param pullrequestid: ID of the pull request which shall be merged. + :type pullrequestid: int + :param userid: Merge the pull request as this user. + :type userid: Optional(str or int) + + Example output: + + .. code-block:: bash + + "id": <id_given_in_input>, + "result": + { + "executed": "<bool>", + "failure_reason": "<int>", + "merge_commit_id": "<merge_commit_id>", + "possible": "<bool>" + }, + "error": null + + +update_pull_request +------------------- + +.. py:function:: update_pull_request(apiuser, repoid, pullrequestid, title=<Optional:''>, description=<Optional:''>, reviewers=<Optional:None>, update_commits=<Optional:None>, close_pull_request=<Optional:None>) + + Updates a pull request. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: The repository name or repository ID. + :type repoid: str or int + :param pullrequestid: The pull request ID. + :type pullrequestid: int + :param title: Set the pull request title. + :type title: str + :param description: Update pull request description. + :type description: Optional(str) + :param reviewers: Update pull request reviewers list with new value. + :type reviewers: Optional(list) + :param update_commits: Trigger update of commits for this pull request + :type: update_commits: Optional(bool) + :param close_pull_request: Close this pull request with rejected state + :type: close_pull_request: Optional(bool) + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : + { + "msg": "Updated pull request `63`", + "pull_request": <pull_request_object>, + "updated_reviewers": { + "added": [ + "username" + ], + "removed": [] + }, + "updated_commits": { + "added": [ + "<sha1_hash>" + ], + "common": [ + "<sha1_hash>", + "<sha1_hash>", + ], + "removed": [] + } + } + error : null + + diff --git a/docs/api/methods/repo-group-methods.rst b/docs/api/methods/repo-group-methods.rst new file mode 100644 --- /dev/null +++ b/docs/api/methods/repo-group-methods.rst @@ -0,0 +1,350 @@ +.. _repo-group-methods-ref: + +repo_group methods +================= + +create_repo_group +----------------- + +.. py:function:: create_repo_group(apiuser, group_name, description=<Optional:''>, owner=<Optional:<OptionalAttr:apiuser>>, copy_permissions=<Optional:False>) + + Creates a repository group. + + * If the repository group name contains "/", all the required repository + groups will be created. + + For example "foo/bar/baz" will create |repo| groups "foo" and "bar" + (with "foo" as parent). It will also create the "baz" repository + with "bar" as |repo| group. + + This command can only be run using an |authtoken| with admin + permissions. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param group_name: Set the repository group name. + :type group_name: str + :param description: Set the |repo| group description. + :type description: str + :param owner: Set the |repo| group owner. + :type owner: str + :param copy_permissions: + :type copy_permissions: + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "msg": "Created new repo group `<repo_group_name>`" + "repo_group": <repogroup_object> + } + error : null + + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + failed to create repo group `<repogroupid>` + } + + +delete_repo_group +----------------- + +.. py:function:: delete_repo_group(apiuser, repogroupid) + + Deletes a |repo| group. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repogroupid: Set the name or ID of repository group to be + deleted. + :type repogroupid: str or int + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + 'msg': 'deleted repo group ID:<repogroupid> <repogroupname> + 'repo_group': null + } + error : null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "failed to delete repo group ID:<repogroupid> <repogroupname>" + } + + +get_repo_group +-------------- + +.. py:function:: get_repo_group(apiuser, repogroupid) + + Return the specified |repo| group, along with permissions, + and repositories inside the group + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repogroupid: Specify the name of ID of the repository group. + :type repogroupid: str or int + + + Example output: + + .. code-block:: bash + + { + "error": null, + "id": repo-group-id, + "result": { + "group_description": "repo group description", + "group_id": 14, + "group_name": "group name", + "members": [ + { + "name": "super-admin-username", + "origin": "super-admin", + "permission": "group.admin", + "type": "user" + }, + { + "name": "owner-name", + "origin": "owner", + "permission": "group.admin", + "type": "user" + }, + { + "name": "user-group-name", + "origin": "permission", + "permission": "group.write", + "type": "user_group" + } + ], + "owner": "owner-name", + "parent_group": null, + "repositories": [ repo-list ] + } + } + + +get_repo_groups +--------------- + +.. py:function:: get_repo_groups(apiuser) + + Returns all repository groups. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + + +grant_user_group_permission_to_repo_group +----------------------------------------- + +.. py:function:: grant_user_group_permission_to_repo_group(apiuser, repogroupid, usergroupid, perm, apply_to_children=<Optional:'none'>) + + Grant permission for a user group on given repository group, or update + existing permissions if found. + + This command can only be run using an |authtoken| with admin + permissions on the |repo| group. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repogroupid: Set the name or id of repository group + :type repogroupid: str or int + :param usergroupid: id of usergroup + :type usergroupid: str or int + :param perm: (group.(none|read|write|admin)) + :type perm: str + :param apply_to_children: 'none', 'repos', 'groups', 'all' + :type apply_to_children: str + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`", + "success": true + + } + error : null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`" + } + + +grant_user_permission_to_repo_group +----------------------------------- + +.. py:function:: grant_user_permission_to_repo_group(apiuser, repogroupid, userid, perm, apply_to_children=<Optional:'none'>) + + Grant permission for a user on the given repository group, or update + existing permissions if found. + + This command can only be run using an |authtoken| with admin + permissions. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repogroupid: Set the name or ID of repository group. + :type repogroupid: str or int + :param userid: Set the user name. + :type userid: str + :param perm: (group.(none|read|write|admin)) + :type perm: str + :param apply_to_children: 'none', 'repos', 'groups', 'all' + :type apply_to_children: str + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`", + "success": true + } + error: null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`" + } + + +revoke_user_group_permission_from_repo_group +-------------------------------------------- + +.. py:function:: revoke_user_group_permission_from_repo_group(apiuser, repogroupid, usergroupid, apply_to_children=<Optional:'none'>) + + Revoke permission for user group on given repository. + + This command can only be run using an |authtoken| with admin + permissions on the |repo| group. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repogroupid: name or id of repository group + :type repogroupid: str or int + :param usergroupid: + :param apply_to_children: 'none', 'repos', 'groups', 'all' + :type apply_to_children: str + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`", + "success": true + } + error: null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`" + } + + +revoke_user_permission_from_repo_group +-------------------------------------- + +.. py:function:: revoke_user_permission_from_repo_group(apiuser, repogroupid, userid, apply_to_children=<Optional:'none'>) + + Revoke permission for a user in a given repository group. + + This command can only be run using an |authtoken| with admin + permissions on the |repo| group. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repogroupid: Set the name or ID of the repository group. + :type repogroupid: str or int + :param userid: Set the user name to revoke. + :type userid: str + :param apply_to_children: 'none', 'repos', 'groups', 'all' + :type apply_to_children: str + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`", + "success": true + } + error: null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`" + } + + +update_repo_group +----------------- + +.. py:function:: update_repo_group(apiuser, repogroupid, group_name=<Optional:''>, description=<Optional:''>, owner=<Optional:<OptionalAttr:apiuser>>, parent=<Optional:None>, enable_locking=<Optional:False>) + + Updates repository group with the details given. + + This command can only be run using an |authtoken| with admin + permissions. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repogroupid: Set the ID of repository group. + :type repogroupid: str or int + :param group_name: Set the name of the |repo| group. + :type group_name: str + :param description: Set a description for the group. + :type description: str + :param owner: Set the |repo| group owner. + :type owner: str + :param parent: Set the |repo| group parent. + :type parent: str or int + :param enable_locking: Enable |repo| locking. The default is false. + :type enable_locking: bool + + diff --git a/docs/api/methods/repo-methods.rst b/docs/api/methods/repo-methods.rst new file mode 100644 --- /dev/null +++ b/docs/api/methods/repo-methods.rst @@ -0,0 +1,967 @@ +.. _repo-methods-ref: + +repo methods +================= + +add_field_to_repo +----------------- + +.. py:function:: add_field_to_repo(apiuser, repoid, key, label=<Optional:''>, description=<Optional:''>) + + Adds an extra field to a repository. + + This command can only be run using an |authtoken| with at least + write permissions to the |repo|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: Set the repository name or repository id. + :type repoid: str or int + :param key: Create a unique field key for this repository. + :type key: str + :param label: + :type label: Optional(str) + :param description: + :type description: Optional(str) + + +comment_commit +-------------- + +.. py:function:: comment_commit(apiuser, repoid, commit_id, message, userid=<Optional:<OptionalAttr:apiuser>>, status=<Optional:None>) + + Set a commit comment, and optionally change the status of the commit. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: Set the repository name or repository ID. + :type repoid: str or int + :param commit_id: Specify the commit_id for which to set a comment. + :type commit_id: str + :param message: The comment text. + :type message: str + :param userid: Set the user name of the comment creator. + :type userid: Optional(str or int) + :param status: status, one of 'not_reviewed', 'approved', 'rejected', + 'under_review' + :type status: str + + Example error output: + + .. code-block:: json + + { + "id" : <id_given_in_input>, + "result" : { + "msg": "Commented on commit `<commit_id>` for repository `<repoid>`", + "status_change": null or <status>, + "success": true + }, + "error" : null + } + + +create_repo +----------- + +.. py:function:: create_repo(apiuser, repo_name, repo_type, owner=<Optional:<OptionalAttr:apiuser>>, description=<Optional:''>, private=<Optional:False>, clone_uri=<Optional:None>, landing_rev=<Optional:'rev:tip'>, enable_statistics=<Optional:False>, enable_locking=<Optional:False>, enable_downloads=<Optional:False>, copy_permissions=<Optional:False>) + + Creates a repository. + + * If the repository name contains "/", all the required repository + groups will be created. + + For example "foo/bar/baz" will create |repo| groups "foo" and "bar" + (with "foo" as parent). It will also create the "baz" repository + with "bar" as |repo| group. + + This command can only be run using an |authtoken| with at least + write permissions to the |repo|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repo_name: Set the repository name. + :type repo_name: str + :param repo_type: Set the repository type; 'hg','git', or 'svn'. + :type repo_type: str + :param owner: user_id or username + :type owner: Optional(str) + :param description: Set the repository description. + :type description: Optional(str) + :param private: + :type private: bool + :param clone_uri: + :type clone_uri: str + :param landing_rev: <rev_type>:<rev> + :type landing_rev: str + :param enable_locking: + :type enable_locking: bool + :param enable_downloads: + :type enable_downloads: bool + :param enable_statistics: + :type enable_statistics: bool + :param copy_permissions: Copy permission from group in which the + repository is being created. + :type copy_permissions: bool + + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "msg": "Created new repository `<reponame>`", + "success": true, + "task": "<celery task id or None if done sync>" + } + error: null + + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + 'failed to create repository `<repo_name>` + } + + +delete_repo +----------- + +.. py:function:: delete_repo(apiuser, repoid, forks=<Optional:''>) + + Deletes a repository. + + * When the `forks` parameter is set it's possible to detach or delete + forks of deleted repository. + + This command can only be run using an |authtoken| with admin + permissions on the |repo|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: Set the repository name or repository ID. + :type repoid: str or int + :param forks: Set to `detach` or `delete` forks from the |repo|. + :type forks: Optional(str) + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "msg": "Deleted repository `<reponame>`", + "success": true + } + error: null + + +fork_repo +--------- + +.. py:function:: fork_repo(apiuser, repoid, fork_name, owner=<Optional:<OptionalAttr:apiuser>>, description=<Optional:''>, copy_permissions=<Optional:False>, private=<Optional:False>, landing_rev=<Optional:'rev:tip'>) + + Creates a fork of the specified |repo|. + + * If using |RCE| with Celery this will immediately return a success + message, even though the fork will be created asynchronously. + + This command can only be run using an |authtoken| with fork + permissions on the |repo|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: Set repository name or repository ID. + :type repoid: str or int + :param fork_name: Set the fork name. + :type fork_name: str + :param owner: Set the fork owner. + :type owner: str + :param description: Set the fork descripton. + :type description: str + :param copy_permissions: Copy permissions from parent |repo|. The + default is False. + :type copy_permissions: bool + :param private: Make the fork private. The default is False. + :type private: bool + :param landing_rev: Set the landing revision. The default is tip. + + Example output: + + .. code-block:: bash + + id : <id_for_response> + api_key : "<api_key>" + args: { + "repoid" : "<reponame or repo_id>", + "fork_name": "<forkname>", + "owner": "<username or user_id = Optional(=apiuser)>", + "description": "<description>", + "copy_permissions": "<bool>", + "private": "<bool>", + "landing_rev": "<landing_rev>" + } + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "msg": "Created fork of `<reponame>` as `<forkname>`", + "success": true, + "task": "<celery task id or None if done sync>" + } + error: null + + +get_repo +-------- + +.. py:function:: get_repo(apiuser, repoid, cache=<Optional:True>) + + Gets an existing repository by its name or repository_id. + + The members section so the output returns users groups or users + associated with that repository. + + This command can only be run using an |authtoken| with admin rights, + or users with at least read rights to the |repo|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: The repository name or repository id. + :type repoid: str or int + :param cache: use the cached value for last changeset + :type: cache: Optional(bool) + + Example output: + + .. code-block:: bash + + { + "error": null, + "id": <repo_id>, + "result": { + "clone_uri": null, + "created_on": "timestamp", + "description": "repo description", + "enable_downloads": false, + "enable_locking": false, + "enable_statistics": false, + "followers": [ + { + "active": true, + "admin": false, + "api_key": "****************************************", + "api_keys": [ + "****************************************" + ], + "email": "user@example.com", + "emails": [ + "user@example.com" + ], + "extern_name": "rhodecode", + "extern_type": "rhodecode", + "firstname": "username", + "ip_addresses": [], + "language": null, + "last_login": "2015-09-16T17:16:35.854", + "lastname": "surname", + "user_id": <user_id>, + "username": "name" + } + ], + "fork_of": "parent-repo", + "landing_rev": [ + "rev", + "tip" + ], + "last_changeset": { + "author": "User <user@example.com>", + "branch": "default", + "date": "timestamp", + "message": "last commit message", + "parents": [ + { + "raw_id": "commit-id" + } + ], + "raw_id": "commit-id", + "revision": <revision number>, + "short_id": "short id" + }, + "lock_reason": null, + "locked_by": null, + "locked_date": null, + "members": [ + { + "name": "super-admin-name", + "origin": "super-admin", + "permission": "repository.admin", + "type": "user" + }, + { + "name": "owner-name", + "origin": "owner", + "permission": "repository.admin", + "type": "user" + }, + { + "name": "user-group-name", + "origin": "permission", + "permission": "repository.write", + "type": "user_group" + } + ], + "owner": "owner-name", + "permissions": [ + { + "name": "super-admin-name", + "origin": "super-admin", + "permission": "repository.admin", + "type": "user" + }, + { + "name": "owner-name", + "origin": "owner", + "permission": "repository.admin", + "type": "user" + }, + { + "name": "user-group-name", + "origin": "permission", + "permission": "repository.write", + "type": "user_group" + } + ], + "private": true, + "repo_id": 676, + "repo_name": "user-group/repo-name", + "repo_type": "hg" + } + } + + +get_repo_changeset +------------------ + +.. py:function:: get_repo_changeset(apiuser, repoid, revision, details=<Optional:'basic'>) + + Returns information about a changeset. + + Additionally parameters define the amount of details returned by + this function. + + This command can only be run using an |authtoken| with admin rights, + or users with at least read rights to the |repo|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: The repository name or repository id + :type repoid: str or int + :param revision: revision for which listing should be done + :type revision: str + :param details: details can be 'basic|extended|full' full gives diff + info details like the diff itself, and number of changed files etc. + :type details: Optional(str) + + +get_repo_changesets +------------------- + +.. py:function:: get_repo_changesets(apiuser, repoid, start_rev, limit, details=<Optional:'basic'>) + + Returns a set of commits limited by the number starting + from the `start_rev` option. + + Additional parameters define the amount of details returned by this + function. + + This command can only be run using an |authtoken| with admin rights, + or users with at least read rights to |repos|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: The repository name or repository ID. + :type repoid: str or int + :param start_rev: The starting revision from where to get changesets. + :type start_rev: str + :param limit: Limit the number of commits to this amount + :type limit: str or int + :param details: Set the level of detail returned. Valid option are: + ``basic``, ``extended`` and ``full``. + :type details: Optional(str) + + .. note:: + + Setting the parameter `details` to the value ``full`` is extensive + and returns details like the diff itself, and the number + of changed files. + + +get_repo_nodes +-------------- + +.. py:function:: get_repo_nodes(apiuser, repoid, revision, root_path, ret_type=<Optional:'all'>, details=<Optional:'basic'>, max_file_bytes=<Optional:None>) + + Returns a list of nodes and children in a flat list for a given + path at given revision. + + It's possible to specify ret_type to show only `files` or `dirs`. + + This command can only be run using an |authtoken| with admin rights, + or users with at least read rights to |repos|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: The repository name or repository ID. + :type repoid: str or int + :param revision: The revision for which listing should be done. + :type revision: str + :param root_path: The path from which to start displaying. + :type root_path: str + :param ret_type: Set the return type. Valid options are + ``all`` (default), ``files`` and ``dirs``. + :type ret_type: Optional(str) + :param details: Returns extended information about nodes, such as + md5, binary, and or content. The valid options are ``basic`` and + ``full``. + :type details: Optional(str) + :param max_file_bytes: Only return file content under this file size bytes + :type details: Optional(int) + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: [ + { + "name" : "<name>" + "type" : "<type>", + "binary": "<true|false>" (only in extended mode) + "md5" : "<md5 of file content>" (only in extended mode) + }, + ... + ] + error: null + + +get_repo_refs +------------- + +.. py:function:: get_repo_refs(apiuser, repoid) + + Returns a dictionary of current references. It returns + bookmarks, branches, closed_branches, and tags for given repository + + It's possible to specify ret_type to show only `files` or `dirs`. + + This command can only be run using an |authtoken| with admin rights, + or users with at least read rights to |repos|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: The repository name or repository ID. + :type repoid: str or int + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: [ + TODO... + ] + error: null + + +get_repo_settings +----------------- + +.. py:function:: get_repo_settings(apiuser, repoid, key=<Optional:None>) + + Returns all settings for a repository. If key is given it only returns the + setting identified by the key or null. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: The repository name or repository id. + :type repoid: str or int + :param key: Key of the setting to return. + :type: key: Optional(str) + + Example output: + + .. code-block:: bash + + { + "error": null, + "id": 237, + "result": { + "extensions_largefiles": true, + "hooks_changegroup_push_logger": true, + "hooks_changegroup_repo_size": false, + "hooks_outgoing_pull_logger": true, + "phases_publish": "True", + "rhodecode_hg_use_rebase_for_merging": true, + "rhodecode_pr_merge_enabled": true, + "rhodecode_use_outdated_comments": true + } + } + + +get_repos +--------- + +.. py:function:: get_repos(apiuser) + + Lists all existing repositories. + + This command can only be run using an |authtoken| with admin rights, + or users with at least read rights to |repos|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: [ + { + "repo_id" : "<repo_id>", + "repo_name" : "<reponame>" + "repo_type" : "<repo_type>", + "clone_uri" : "<clone_uri>", + "private": : "<bool>", + "created_on" : "<datetimecreated>", + "description" : "<description>", + "landing_rev": "<landing_rev>", + "owner": "<repo_owner>", + "fork_of": "<name_of_fork_parent>", + "enable_downloads": "<bool>", + "enable_locking": "<bool>", + "enable_statistics": "<bool>", + }, + ... + ] + error: null + + +grant_user_group_permission +--------------------------- + +.. py:function:: grant_user_group_permission(apiuser, repoid, usergroupid, perm) + + Grant permission for a user group on the specified repository, + or update existing permissions. + + This command can only be run using an |authtoken| with admin + permissions on the |repo|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: Set the repository name or repository ID. + :type repoid: str or int + :param usergroupid: Specify the ID of the user group. + :type usergroupid: str or int + :param perm: Set the user group permissions using the following + format: (repository.(none|read|write|admin)) + :type perm: str + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`", + "success": true + + } + error : null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "failed to edit permission for user group: `<usergroup>` in repo `<repo>`' + } + + +grant_user_permission +--------------------- + +.. py:function:: grant_user_permission(apiuser, repoid, userid, perm) + + Grant permissions for the specified user on the given repository, + or update existing permissions if found. + + This command can only be run using an |authtoken| with admin + permissions on the |repo|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: Set the repository name or repository ID. + :type repoid: str or int + :param userid: Set the user name. + :type userid: str + :param perm: Set the user permissions, using the following format + ``(repository.(none|read|write|admin))`` + :type perm: str + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`", + "success": true + } + error: null + + +invalidate_cache +---------------- + +.. py:function:: invalidate_cache(apiuser, repoid, delete_keys=<Optional:False>) + + Invalidates the cache for the specified repository. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + This command takes the following options: + + :param apiuser: This is filled automatically from |authtoken|. + :type apiuser: AuthUser + :param repoid: Sets the repository name or repository ID. + :type repoid: str or int + :param delete_keys: This deletes the invalidated keys instead of + just flagging them. + :type delete_keys: Optional(``True`` | ``False``) + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + 'msg': Cache for repository `<repository name>` was invalidated, + 'repository': <repository name> + } + error : null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + 'Error occurred during cache invalidation action' + } + + +lock +---- + +.. py:function:: lock(apiuser, repoid, locked=<Optional:None>, userid=<Optional:<OptionalAttr:apiuser>>) + + Sets the lock state of the specified |repo| by the given user. + From more information, see :ref:`repo-locking`. + + * If the ``userid`` option is not set, the repository is locked to the + user who called the method. + * If the ``locked`` parameter is not set, the current lock state of the + repository is displayed. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + This command takes the following options: + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: Sets the repository name or repository ID. + :type repoid: str or int + :param locked: Sets the lock state. + :type locked: Optional(``True`` | ``False``) + :param userid: Set the repository lock to this user. + :type userid: Optional(str or int) + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + 'repo': '<reponame>', + 'locked': <bool: lock state>, + 'locked_since': <int: lock timestamp>, + 'locked_by': <username of person who made the lock>, + 'lock_reason': <str: reason for locking>, + 'lock_state_changed': <bool: True if lock state has been changed in this request>, + 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.' + or + 'msg': 'Repo `<repository name>` not locked.' + or + 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`' + } + error : null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + 'Error occurred locking repository `<reponame>` + } + + +pull +---- + +.. py:function:: pull(apiuser, repoid) + + Triggers a pull on the given repository from a remote location. You + can use this to keep remote repositories up-to-date. + + This command can only be run using an |authtoken| with admin + rights to the specified repository. For more information, + see :ref:`config-token-ref`. + + This command takes the following options: + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: The repository name or repository ID. + :type repoid: str or int + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "msg": "Pulled from `<repository name>`" + "repository": "<repository name>" + } + error : null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "Unable to pull changes from `<reponame>`" + } + + +remove_field_from_repo +---------------------- + +.. py:function:: remove_field_from_repo(apiuser, repoid, key) + + Removes an extra field from a repository. + + This command can only be run using an |authtoken| with at least + write permissions to the |repo|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: Set the repository name or repository ID. + :type repoid: str or int + :param key: Set the unique field key for this repository. + :type key: str + + +revoke_user_group_permission +---------------------------- + +.. py:function:: revoke_user_group_permission(apiuser, repoid, usergroupid) + + Revoke the permissions of a user group on a given repository. + + This command can only be run using an |authtoken| with admin + permissions on the |repo|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: Set the repository name or repository ID. + :type repoid: str or int + :param usergroupid: Specify the user group ID. + :type usergroupid: str or int + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`", + "success": true + } + error: null + + +revoke_user_permission +---------------------- + +.. py:function:: revoke_user_permission(apiuser, repoid, userid) + + Revoke permission for a user on the specified repository. + + This command can only be run using an |authtoken| with admin + permissions on the |repo|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: Set the repository name or repository ID. + :type repoid: str or int + :param userid: Set the user name of revoked user. + :type userid: str or int + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`", + "success": true + } + error: null + + +set_repo_settings +----------------- + +.. py:function:: set_repo_settings(apiuser, repoid, settings) + + Update repository settings. Returns true on success. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: The repository name or repository id. + :type repoid: str or int + :param settings: The new settings for the repository. + :type: settings: dict + + Example output: + + .. code-block:: bash + + { + "error": null, + "id": 237, + "result": true + } + + +strip +----- + +.. py:function:: strip(apiuser, repoid, revision, branch) + + Strips the given revision from the specified repository. + + * This will remove the revision and all of its decendants. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + This command takes the following options: + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: The repository name or repository ID. + :type repoid: str or int + :param revision: The revision you wish to strip. + :type revision: str + :param branch: The branch from which to strip the revision. + :type branch: str + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "msg": "'Stripped commit <commit_hash> from repo `<repository name>`'" + "repository": "<repository name>" + } + error : null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "Unable to strip commit <commit_hash> from repo `<repository name>`" + } + + +update_repo +----------- + +.. py:function:: update_repo(apiuser, repoid, name=<Optional:None>, owner=<Optional:<OptionalAttr:apiuser>>, group=<Optional:None>, fork_of=<Optional:None>, description=<Optional:''>, private=<Optional:False>, clone_uri=<Optional:None>, landing_rev=<Optional:'rev:tip'>, enable_statistics=<Optional:False>, enable_locking=<Optional:False>, enable_downloads=<Optional:False>, fields=<Optional:''>) + + Updates a repository with the given information. + + This command can only be run using an |authtoken| with at least + write permissions to the |repo|. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: repository name or repository ID. + :type repoid: str or int + :param name: Update the |repo| name. + :type name: str + :param owner: Set the |repo| owner. + :type owner: str + :param group: Set the |repo| group the |repo| belongs to. + :type group: str + :param fork_of: Set the master |repo| name. + :type fork_of: str + :param description: Update the |repo| description. + :type description: str + :param private: Set the |repo| as private. (True | False) + :type private: bool + :param clone_uri: Update the |repo| clone URI. + :type clone_uri: str + :param landing_rev: Set the |repo| landing revision. Default is + ``tip``. + :type landing_rev: str + :param enable_statistics: Enable statistics on the |repo|, + (True | False). + :type enable_statistics: bool + :param enable_locking: Enable |repo| locking. + :type enable_locking: bool + :param enable_downloads: Enable downloads from the |repo|, + (True | False). + :type enable_downloads: bool + :param fields: Add extra fields to the |repo|. Use the following + example format: ``field_key=field_val,field_key2=fieldval2``. + Escape ', ' with \, + :type fields: str + + diff --git a/docs/api/methods/server-methods.rst b/docs/api/methods/server-methods.rst new file mode 100644 --- /dev/null +++ b/docs/api/methods/server-methods.rst @@ -0,0 +1,115 @@ +.. _server-methods-ref: + +server methods +================= + +get_ip +------ + +.. py:function:: get_ip(apiuser, userid=<Optional:<OptionalAttr:apiuser>>) + + Displays the IP Address as seen from the |RCE| server. + + * This command displays the IP Address, as well as all the defined IP + addresses for the specified user. If the ``userid`` is not set, the + data returned is for the user calling the method. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + This command takes the following options: + + :param apiuser: This is filled automatically from |authtoken|. + :type apiuser: AuthUser + :param userid: Sets the userid for which associated IP Address data + is returned. + :type userid: Optional(str or int) + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "server_ip_addr": "<ip_from_clien>", + "user_ips": [ + { + "ip_addr": "<ip_with_mask>", + "ip_range": ["<start_ip>", "<end_ip>"], + }, + ... + ] + } + + +get_server_info +--------------- + +.. py:function:: get_server_info(apiuser) + + Returns the |RCE| server information. + + This includes the running version of |RCE| and all installed + packages. This command takes the following options: + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + 'modules': [<module name>,...] + 'py_version': <python version>, + 'platform': <platform type>, + 'rhodecode_version': <rhodecode version> + } + error : null + + +rescan_repos +------------ + +.. py:function:: rescan_repos(apiuser, remove_obsolete=<Optional:False>) + + Triggers a rescan of the specified repositories. + + * If the ``remove_obsolete`` option is set, it also deletes repositories + that are found in the database but not on the file system, so called + "clean zombies". + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + This command takes the following options: + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param remove_obsolete: Deletes repositories from the database that + are not found on the filesystem. + :type remove_obsolete: Optional(``True`` | ``False``) + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + 'added': [<added repository name>,...] + 'removed': [<removed repository name>,...] + } + error : null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + 'Error occurred during rescan repositories action' + } + + diff --git a/docs/api/methods/user-group-methods.rst b/docs/api/methods/user-group-methods.rst new file mode 100644 --- /dev/null +++ b/docs/api/methods/user-group-methods.rst @@ -0,0 +1,406 @@ +.. _user-group-methods-ref: + +user_group methods +================= + +add_user_to_user_group +---------------------- + +.. py:function:: add_user_to_user_group(apiuser, usergroupid, userid) + + Adds a user to a `user group`. If the user already exists in the group + this command will return false. + + This command can only be run using an |authtoken| with admin rights to + the specified user group. + + This command takes the following options: + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param usergroupid: Set the name of the `user group` to which a + user will be added. + :type usergroupid: int + :param userid: Set the `user_id` of the user to add to the group. + :type userid: int + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "success": True|False # depends on if member is in group + "msg": "added member `<username>` to user group `<groupname>` | + User is already in that group" + + } + error : null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "failed to add member to user group `<user_group_name>`" + } + + +create_user_group +----------------- + +.. py:function:: create_user_group(apiuser, group_name, description=<Optional:''>, owner=<Optional:<OptionalAttr:apiuser>>, active=<Optional:True>) + + Creates a new user group. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + This command takes the following options: + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param group_name: Set the name of the new user group. + :type group_name: str + :param description: Give a description of the new user group. + :type description: str + :param owner: Set the owner of the new user group. + If not set, the owner is the |authtoken| user. + :type owner: Optional(str or int) + :param active: Set this group as active. + :type active: Optional(``True`` | ``False``) + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "msg": "created new user group `<groupname>`", + "user_group": <user_group_object> + } + error: null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "user group `<group name>` already exist" + or + "failed to create group `<group name>`" + } + + +delete_user_group +----------------- + +.. py:function:: delete_user_group(apiuser, usergroupid) + + Deletes the specified `user group`. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + This command takes the following options: + + :param apiuser: filled automatically from apikey + :type apiuser: AuthUser + :param usergroupid: + :type usergroupid: int + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "msg": "deleted user group ID:<user_group_id> <user_group_name>" + } + error : null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "failed to delete user group ID:<user_group_id> <user_group_name>" + or + "RepoGroup assigned to <repo_groups_list>" + } + + +get_user_group +-------------- + +.. py:function:: get_user_group(apiuser, usergroupid) + + Returns the data of an existing user group. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param usergroupid: Set the user group from which to return data. + :type usergroupid: str or int + + Example error output: + + .. code-block:: bash + + { + "error": null, + "id": <id>, + "result": { + "active": true, + "group_description": "group description", + "group_name": "group name", + "members": [ + { + "name": "owner-name", + "origin": "owner", + "permission": "usergroup.admin", + "type": "user" + }, + { + { + "name": "user name", + "origin": "permission", + "permission": "usergroup.admin", + "type": "user" + }, + { + "name": "user group name", + "origin": "permission", + "permission": "usergroup.write", + "type": "user_group" + } + ], + "owner": "owner name", + "users": [], + "users_group_id": 2 + } + } + + +get_user_groups +--------------- + +.. py:function:: get_user_groups(apiuser) + + Lists all the existing user groups within RhodeCode. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + This command takes the following options: + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : [<user_group_obj>,...] + error : null + + +grant_user_group_permission_to_user_group +----------------------------------------- + +.. py:function:: grant_user_group_permission_to_user_group(apiuser, usergroupid, sourceusergroupid, perm) + + Give one user group permissions to another user group. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param usergroupid: Set the user group on which to edit permissions. + :type usergroupid: str or int + :param sourceusergroupid: Set the source user group to which + access/permissions will be granted. + :type sourceusergroupid: str or int + :param perm: (usergroup.(none|read|write|admin)) + :type perm: str + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "msg": "Granted perm: `<perm_name>` for user group: `<source_user_group_name>` in user group: `<user_group_name>`", + "success": true + } + error : null + + +grant_user_permission_to_user_group +----------------------------------- + +.. py:function:: grant_user_permission_to_user_group(apiuser, usergroupid, userid, perm) + + Set permissions for a user in a user group. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param usergroupid: Set the user group to edit permissions on. + :type usergroupid: str or int + :param userid: Set the user from whom you wish to set permissions. + :type userid: str + :param perm: (usergroup.(none|read|write|admin)) + :type perm: str + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "msg": "Granted perm: `<perm_name>` for user: `<username>` in user group: `<user_group_name>`", + "success": true + } + error : null + + +remove_user_from_user_group +--------------------------- + +.. py:function:: remove_user_from_user_group(apiuser, usergroupid, userid) + + Removes a user from a user group. + + * If the specified user is not in the group, this command will return + `false`. + + This command can only be run using an |authtoken| with admin rights to + the specified user group. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param usergroupid: Sets the user group name. + :type usergroupid: str or int + :param userid: The user you wish to remove from |RCE|. + :type userid: str or int + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "success": True|False, # depends on if member is in group + "msg": "removed member <username> from user group <groupname> | + User wasn't in group" + } + error: null + + +revoke_user_group_permission_from_user_group +-------------------------------------------- + +.. py:function:: revoke_user_group_permission_from_user_group(apiuser, usergroupid, sourceusergroupid) + + Revoke the permissions that one user group has to another. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param usergroupid: Set the user group on which to edit permissions. + :type usergroupid: str or int + :param sourceusergroupid: Set the user group from which permissions + are revoked. + :type sourceusergroupid: str or int + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "msg": "Revoked perm for user group: `<user_group_name>` in user group: `<target_user_group_name>`", + "success": true + } + error : null + + +revoke_user_permission_from_user_group +-------------------------------------- + +.. py:function:: revoke_user_permission_from_user_group(apiuser, usergroupid, userid) + + Revoke a users permissions in a user group. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param usergroupid: Set the user group from which to revoke the user + permissions. + :type: usergroupid: str or int + :param userid: Set the userid of the user whose permissions will be + revoked. + :type userid: str + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "msg": "Revoked perm for user: `<username>` in user group: `<user_group_name>`", + "success": true + } + error : null + + +update_user_group +----------------- + +.. py:function:: update_user_group(apiuser, usergroupid, group_name=<Optional:''>, description=<Optional:''>, owner=<Optional:None>, active=<Optional:True>) + + Updates the specified `user group` with the details provided. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param usergroupid: Set the id of the `user group` to update. + :type usergroupid: str or int + :param group_name: Set the new name the `user group` + :type group_name: str + :param description: Give a description for the `user group` + :type description: str + :param owner: Set the owner of the `user group`. + :type owner: Optional(str or int) + :param active: Set the group as active. + :type active: Optional(``True`` | ``False``) + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + "msg": 'updated user group ID:<user group id> <user group name>', + "user_group": <user_group_object> + } + error : null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "failed to update user group `<user group name>`" + } + + diff --git a/docs/api/methods/user-methods.rst b/docs/api/methods/user-methods.rst new file mode 100644 --- /dev/null +++ b/docs/api/methods/user-methods.rst @@ -0,0 +1,295 @@ +.. _user-methods-ref: + +user methods +================= + +create_user +----------- + +.. py:function:: create_user(apiuser, username, email, password=<Optional:''>, firstname=<Optional:''>, lastname=<Optional:''>, active=<Optional:True>, admin=<Optional:False>, extern_name=<Optional:'rhodecode'>, extern_type=<Optional:'rhodecode'>, force_password_change=<Optional:False>) + + Creates a new user and returns the new user object. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + This command takes the following options: + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param username: Set the new username. + :type username: str or int + :param email: Set the user email address. + :type email: str + :param password: Set the new user password. + :type password: Optional(str) + :param firstname: Set the new user firstname. + :type firstname: Optional(str) + :param lastname: Set the new user surname. + :type lastname: Optional(str) + :param active: Set the user as active. + :type active: Optional(``True`` | ``False``) + :param admin: Give the new user admin rights. + :type admin: Optional(``True`` | ``False``) + :param extern_name: Set the authentication plugin name. + Using LDAP this is filled with LDAP UID. + :type extern_name: Optional(str) + :param extern_type: Set the new user authentication plugin. + :type extern_type: Optional(str) + :param force_password_change: Force the new user to change password + on next login. + :type force_password_change: Optional(``True`` | ``False``) + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "msg" : "created new user `<username>`", + "user": <user_obj> + } + error: null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "user `<username>` already exist" + or + "email `<email>` already exist" + or + "failed to create user `<username>`" + } + + +delete_user +----------- + +.. py:function:: delete_user(apiuser, userid) + + Deletes the specified user from the |RCE| user database. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + .. important:: + + Ensure all open pull requests and open code review + requests to this user are close. + + Also ensure all repositories, or repository groups owned by this + user are reassigned before deletion. + + This command takes the following options: + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param userid: Set the user to delete. + :type userid: str or int + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "msg" : "deleted user ID:<userid> <username>", + "user": null + } + error: null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "failed to delete user ID:<userid> <username>" + } + + +get_user +-------- + +.. py:function:: get_user(apiuser, userid=<Optional:<OptionalAttr:apiuser>>) + + Returns the information associated with a username or userid. + + * If the ``userid`` is not set, this command returns the information + for the ``userid`` calling the method. + + .. note:: + + Normal users may only run this command against their ``userid``. For + full privileges you must run this command using an |authtoken| with + admin rights. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param userid: Sets the userid for which data will be returned. + :type userid: Optional(str or int) + + Example output: + + .. code-block:: bash + + { + "error": null, + "id": <id>, + "result": { + "active": true, + "admin": false, + "api_key": "api-key", + "api_keys": [ list of keys ], + "email": "user@example.com", + "emails": [ + "user@example.com" + ], + "extern_name": "rhodecode", + "extern_type": "rhodecode", + "firstname": "username", + "ip_addresses": [], + "language": null, + "last_login": "Timestamp", + "lastname": "surnae", + "permissions": { + "global": [ + "hg.inherit_default_perms.true", + "usergroup.read", + "hg.repogroup.create.false", + "hg.create.none", + "hg.extern_activate.manual", + "hg.create.write_on_repogroup.false", + "hg.usergroup.create.false", + "group.none", + "repository.none", + "hg.register.none", + "hg.fork.repository" + ], + "repositories": { "username/example": "repository.write"}, + "repositories_groups": { "user-group/repo": "group.none" }, + "user_groups": { "user_group_name": "usergroup.read" } + }, + "user_id": 32, + "username": "username" + } + } + + +get_user_locks +-------------- + +.. py:function:: get_user_locks(apiuser, userid=<Optional:<OptionalAttr:apiuser>>) + + Displays all repositories locked by the specified user. + + * If this command is run by a non-admin user, it returns + a list of |repos| locked by that user. + + This command takes the following options: + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param userid: Sets the userid whose list of locked |repos| will be + displayed. + :type userid: Optional(str or int) + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result : { + [repo_object, repo_object,...] + } + error : null + + +get_users +--------- + +.. py:function:: get_users(apiuser) + + Lists all users in the |RCE| user database. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + This command takes the following options: + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: [<user_object>, ...] + error: null + + +update_user +----------- + +.. py:function:: update_user(apiuser, userid, username=<Optional:None>, email=<Optional:None>, password=<Optional:None>, firstname=<Optional:None>, lastname=<Optional:None>, active=<Optional:None>, admin=<Optional:None>, extern_type=<Optional:None>, extern_name=<Optional:None>) + + Updates the details for the specified user, if that user exists. + + This command can only be run using an |authtoken| with admin rights to + the specified repository. + + This command takes the following options: + + :param apiuser: This is filled automatically from |authtoken|. + :type apiuser: AuthUser + :param userid: Set the ``userid`` to update. + :type userid: str or int + :param username: Set the new username. + :type username: str or int + :param email: Set the new email. + :type email: str + :param password: Set the new password. + :type password: Optional(str) + :param firstname: Set the new first name. + :type firstname: Optional(str) + :param lastname: Set the new surname. + :type lastname: Optional(str) + :param active: Set the new user as active. + :type active: Optional(``True`` | ``False``) + :param admin: Give the user admin rights. + :type admin: Optional(``True`` | ``False``) + :param extern_name: Set the authentication plugin user name. + Using LDAP this is filled with LDAP UID. + :type extern_name: Optional(str) + :param extern_type: Set the authentication plugin type. + :type extern_type: Optional(str) + + + Example output: + + .. code-block:: bash + + id : <id_given_in_input> + result: { + "msg" : "updated user ID:<userid> <username>", + "user": <user_object>, + } + error: null + + Example error output: + + .. code-block:: bash + + id : <id_given_in_input> + result : null + error : { + "failed to update user `<username>`" + } + + diff --git a/docs/code-review/code-review.rst b/docs/code-review/code-review.rst --- a/docs/code-review/code-review.rst +++ b/docs/code-review/code-review.rst @@ -14,7 +14,7 @@ code review matters, see these posts on You can also use the |RCE| API set up continuous integration servers to leave comments from a test suite. See the :ref:`api` and -:ref:`integrations-ref` sections for examples on how to set this up. +:ref:`extensions-hooks-ref` sections for examples on how to set this up. .. toctree:: diff --git a/docs/common.py b/docs/common.py --- a/docs/common.py +++ b/docs/common.py @@ -6,6 +6,8 @@ rst_epilog = ''' .. |AE| replace:: Appenlight .. |authtoken| replace:: Authentication Token .. |authtokens| replace:: **Auth Tokens** +.. |RCCEshort| replace:: Community +.. |RCEEshort| replace:: Enterprise .. |git| replace:: Git .. |hg| replace:: Mercurial .. |svn| replace:: Subversion diff --git a/docs/contributing/dev-setup.rst b/docs/contributing/dev-setup.rst --- a/docs/contributing/dev-setup.rst +++ b/docs/contributing/dev-setup.rst @@ -78,7 +78,7 @@ following command from inside the cloned On the first run, this will take a while to download and optionally compile a few things. The following runs will be faster. The development shell works - fine on MacOS and Linux platforms. + fine on both MacOS and Linux platforms. @@ -91,9 +91,9 @@ use the following steps: 1. Create a copy of `~/rhodecode-enterprise-ce/configs/development.ini` 2. Adjust the configuration settings to your needs - .. note:: +.. note:: - It is recommended to use the name `dev.ini`. + It is recommended to use the name `dev.ini`. Setup the Development Database @@ -108,32 +108,37 @@ time operation:: --repos=~/my_dev_repos +Compile CSS and JavaScript +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To use the application's frontend, you will need to compile the CSS and +JavaScript with Grunt. This is easily done from within the nix-shell using the +following command:: + + make web-build + +You will need to recompile following any changes made to the CSS or JavaScript +files. + + Start the Development Server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -When starting the development server, you should start the vcsserver as a -separate process. To do this, use one of the following examples: +From the rhodecode-vcsserver directory, start the development server in another +nix-shell, using the following command:: -1. Set the `start.vcs_server` flag in the ``dev.ini`` file to true. For example: + pserve configs/development.ini http_port=9900 - .. code-block:: python +In the adjacent nix-shell which you created for your development server, you may +now start CE with the following command:: - ### VCS CONFIG ### - ################## - vcs.start_server = true - vcs.server = localhost:9900 - vcs.server.log_level = debug - Then start the server using the following command: ``rcserver dev.ini`` + rcserver dev.ini -2. Start the development server using the following example:: - - rcserver --with-vcsserver dev.ini +.. note:: -3. Start the development server in a different terminal using the following - example:: - - vcsserver + To automatically refresh - and recompile the frontend assets - when changes + are made in the source code, you can use the option `--reload`. Run the Environment Tests @@ -141,4 +146,12 @@ Run the Environment Tests Please make sure that the tests are passing to verify that your environment is set up correctly. RhodeCode uses py.test to run tests. -Please simply run ``make test`` to run the basic test suite. +While your instance is running, start a new nix-shell and simply run +``make test`` to run the basic test suite. + + +Need Help? +^^^^^^^^^^ + +Join us on Slack via https://rhodecode.com/join or post questions in our +Community Portal at https://community.rhodecode.com diff --git a/docs/contributing/overview.rst b/docs/contributing/overview.rst --- a/docs/contributing/overview.rst +++ b/docs/contributing/overview.rst @@ -86,14 +86,11 @@ Sign the Contributor License Agreement ====================================== If your contribution is approved, you will need to virtually sign the license -agreement in order for it to be merged into the project's codebase. You can read -it on our website here: https://rhodecode.com/static/pdf/RhodeCode-CLA.pdf +agreement in order for it to be merged into the project's codebase. -To sign, go to code.rhodecode.com -and clone the CLA repository. Add your name and make a pull request to add it to -the contributor agreement; this serves as your virtual signature. Once your -signature is merged, add a link to the relevant commit to your contribution -pull request. +You can read it on our website at https://rhodecode.com/rhodecode-cla + +To sign electronically, go to https://rhodecode.com/sign-cla diff --git a/docs/default.nix b/docs/default.nix --- a/docs/default.nix +++ b/docs/default.nix @@ -28,10 +28,11 @@ let }; Pygments = buildPythonPackage rec { - name = "Pygments-2.0.2"; + name = "Pygments-2.1.3"; + doCheck = false; src = fetchurl { - url = "https://pypi.python.org/packages/source/P/Pygments/${name}.tar.gz"; - md5 = "238587a1370d62405edabd0794b3ec4a"; + url = "https://pypi.python.org/packages/b8/67/ab177979be1c81bc99c8d0592ef22d547e70bb4c6815c383286ed5dec504/Pygments-2.1.3.tar.gz"; + md5 = "ed3fba2467c8afcda4d317e4ef2c6150"; }; }; @@ -78,11 +79,19 @@ let ]; }; - Sphinx = buildPythonPackage (rec { - name = "Sphinx-1.3.1"; + imagesize = buildPythonPackage rec { + name = "imagesize-0.7.1"; src = fetchurl { - url = "http://pypi.python.org/packages/source/S/Sphinx/${name}.tar.gz"; - md5 = "8786a194acf9673464c5455b11fd4332"; + url = "https://pypi.python.org/packages/53/72/6c6f1e787d9cab2cc733cf042f125abec07209a58308831c9f292504e826/${name}.tar.gz"; + md5 = "976148283286a6ba5f69b0f81aef8052"; + }; + }; + + Sphinx = buildPythonPackage (rec { + name = "Sphinx-1.4.4"; + src = fetchurl { + url = "https://pypi.python.org/packages/20/a2/72f44c84f6c4115e3fef58d36d657ec311d80196eab9fd5ec7bcde76143b/${name}.tar.gz"; + md5 = "64ce2ec08d37ed56313a98232cbe2aee"; }; propagatedBuildInputs = [ docutils @@ -93,6 +102,7 @@ let snowballstemmer pytz babel + imagesize # TODO: johbo: Had to include it here so that can be imported sphinx_rtd_theme diff --git a/docs/integrations/config-ext.rst b/docs/extensions/config-ext.rst rename from docs/integrations/config-ext.rst rename to docs/extensions/config-ext.rst diff --git a/docs/integrations/example-ext.py b/docs/extensions/example-ext.py rename from docs/integrations/example-ext.py rename to docs/extensions/example-ext.py diff --git a/docs/extensions/extensions-hooks.rst b/docs/extensions/extensions-hooks.rst new file mode 100644 --- /dev/null +++ b/docs/extensions/extensions-hooks.rst @@ -0,0 +1,25 @@ +.. _extensions-hooks-ref: + +Extensions & Hooks +================== + +The extensions & hooks section references three concepts regularly, +so to clarify what is meant each time, read the following definitions: + +* **Plugin**: A Plugin is software that adds a specific feature to + an existing software application. +* **Extension**: An extension extends the capabilities of, + or the data available to, an existing software application. +* **Hook**: A hook intercepts function calls, messages, or events passed + between software components and can be used to trigger plugins, or their + extensions. + +.. toctree:: + + rcx + install-ext + config-ext + extensions + hooks + full-blown-example + int-slack diff --git a/docs/integrations/extensions.rst b/docs/extensions/extensions.rst rename from docs/integrations/extensions.rst rename to docs/extensions/extensions.rst diff --git a/docs/integrations/full-blown-example.rst b/docs/extensions/full-blown-example.rst rename from docs/integrations/full-blown-example.rst rename to docs/extensions/full-blown-example.rst diff --git a/docs/integrations/hooks.rst b/docs/extensions/hooks.rst rename from docs/integrations/hooks.rst rename to docs/extensions/hooks.rst diff --git a/docs/integrations/install-ext.rst b/docs/extensions/install-ext.rst rename from docs/integrations/install-ext.rst rename to docs/extensions/install-ext.rst diff --git a/docs/integrations/int-slack.rst b/docs/extensions/int-slack.rst rename from docs/integrations/int-slack.rst rename to docs/extensions/int-slack.rst diff --git a/docs/integrations/rcx.rst b/docs/extensions/rcx.rst rename from docs/integrations/rcx.rst rename to docs/extensions/rcx.rst diff --git a/docs/index.rst b/docs/index.rst --- a/docs/index.rst +++ b/docs/index.rst @@ -58,6 +58,7 @@ and commit files and |repos| while manag collaboration/review-notifications collaboration/pull-requests code-review/code-review + integrations/integrations .. toctree:: :maxdepth: 1 @@ -65,7 +66,7 @@ and commit files and |repos| while manag api/api tools/rhodecode-tools - integrations/integrations + extensions/extensions-hooks contributing/contributing .. toctree:: diff --git a/docs/install/quick-start.rst b/docs/install/quick-start.rst --- a/docs/install/quick-start.rst +++ b/docs/install/quick-start.rst @@ -1,7 +1,7 @@ .. _quick-start: -Quick Start Guide -================= +Quick Start Installation Guide +============================== .. important:: diff --git a/docs/integrations/email.rst b/docs/integrations/email.rst new file mode 100644 --- /dev/null +++ b/docs/integrations/email.rst @@ -0,0 +1,23 @@ +.. _integrations-email: + +Email integration +================= + +The email integration allows you to send the summary of repo pushes to a +list of email recipients in the format: + +An example:: + + User: johndoe + Branches: default + Repository: http://rhodecode.company.com/repo + Commit: 8eab60a44a612e331edfcd59b8d96b2f6a935cd9 + URL: http://rhodecode.company.com/repo/changeset/8eab60a44a612e331edfcd59b8d96b2f6a935cd9 + Author: John Doe + Date: 2016-03-01 11:20:44 + Commit Message: + + fixed bug with thing + + +To create one, create a ``email`` integration in `creating-integrations`. diff --git a/docs/integrations/hipchat.rst b/docs/integrations/hipchat.rst new file mode 100644 --- /dev/null +++ b/docs/integrations/hipchat.rst @@ -0,0 +1,14 @@ +.. _integrations-hipchat: + +Hipchat integration +=================== + +In order to set a Hipchat integration up, it is necessary to obtain the Hipchat +service url by: + +1. Log into Hipchat (https://your-hipchat.hipchat.com/) +2. Go to *Integrations* -> *Build your own* +3. Select a room to post notifications to and save it + +Hipchat will create a URL for you to use in your integration as outlined in +:ref:`creating-integrations`. \ No newline at end of file diff --git a/docs/integrations/integrations.rst b/docs/integrations/integrations.rst --- a/docs/integrations/integrations.rst +++ b/docs/integrations/integrations.rst @@ -1,25 +1,52 @@ -.. _integrations-ref: +.. _integrations: + +Integrations +------------ -Integrations and Extensions -=========================== +Rhodecode supports integrations with external services for various events, +such as commit pushes and pull requests. Multiple integrations of the same type +can be added at the same time; this is useful for posting different events to +different Slack channels, for example. -The integrations section references three concepts regularly, -so to clarify what is meant each time, read the following definitions: +Supported integrations +^^^^^^^^^^^^^^^^^^^^^^ -* **Plugin**: A Plugin is software that adds a specific feature to - an existing software application. -* **Extension**: An extension extends the capabilities of, - or the data available to, an existing software application. -* **Hook**: A hook intercepts function calls, messages, or events passed - between software components and can be used to trigger plugins, or their - extensions. +============================ ============ ===================================== +Type/Name |RC| Edition Description +============================ ============ ===================================== +:ref:`integrations-slack` |RCCEshort| https://slack.com/ +:ref:`integrations-hipchat` |RCCEshort| https://www.hipchat.com/ +:ref:`integrations-webhook` |RCCEshort| POST events as `json` to a custom url +:ref:`integrations-email` |RCEEshort| Send repo push commits by email +:ref:`integrations-redmine` |RCEEshort| Close/Resolve/Reference redmine issues +:ref:`integrations-jira` |RCEEshort| Close/Resolve/Reference JIRA issues +============================ ============ ===================================== + +.. _creating-integrations: + +Creating an Integration +^^^^^^^^^^^^^^^^^^^^^^^ + +Integrations can be added globally via the admin UI: + +:menuselection:`Admin --> Integrations` + +or per repository in each repository's settings: + +:menuselection:`Admin --> Repositories --> Edit --> Integrations` + +To create an integration, select the type from the list in the *Create New +Integration* section. + +The *Current Integrations* section shows existing integrations that have been +created along with their type (eg. Slack) and enabled status. + +See pages specific to each type of integration for more instructions: .. toctree:: - rcx - install-ext - config-ext - extensions - hooks - full-blown-example - int-slack + slack + hipchat + redmine + jira + webhook diff --git a/docs/integrations/jira.rst b/docs/integrations/jira.rst new file mode 100644 --- /dev/null +++ b/docs/integrations/jira.rst @@ -0,0 +1,27 @@ +.. _integrations-jira: + +JIRA integration +================ + +.. important:: + + JIRA integration is only available in |RCEE|. + + +.. important:: + + In order to make issue numbers clickable in commit messages, see the + :ref:`rhodecode-issue-trackers-ref` section. The JIRA integration + only deals with altering JIRA issues. + + +The JIRA integration allows you to reference and change issue statuses in +JIRA directly from commit messages using commit message patterns such as +``fixes #JIRA-235`` in order to change the status of issue JIRA-235 to +eg. "Resolved". + +In order to apply a status to a JIRA issue, it is necessary to find the +transition status id in the *Workflow* section of JIRA. + +Once you have the transition status id, you can create a JIRA integration +as outlined in :ref:`creating-integrations`. diff --git a/docs/integrations/redmine.rst b/docs/integrations/redmine.rst new file mode 100644 --- /dev/null +++ b/docs/integrations/redmine.rst @@ -0,0 +1,28 @@ +.. _integrations-redmine: + +Redmine integration +=================== + +.. important:: + + Redmine integration is only available in |RCEE|. + + +.. important:: + + In order to make issue numbers clickable in commit messages, see the section + :ref:`rhodecode-issue-trackers-ref`. Redmine integration is specifically for + altering Redmine issues. + + +Redmine integration allows you to reference and change issue statuses in +Redmine directly from commit messages, using commit message patterns such as +``fixes #235`` in order to change the status of issue 235 to eg. "Resolved". + +To set a Redmine integration up, it is first necessary to obtain a Redmine API +key. This can be found under *My Account* in the Redmine application. +You may have to enable API Access in Redmine settings if it is not already +available. + +Once you have the API key, create a Redmine integration as outlined in +:ref:`creating-integrations`. diff --git a/docs/integrations/slack.rst b/docs/integrations/slack.rst new file mode 100644 --- /dev/null +++ b/docs/integrations/slack.rst @@ -0,0 +1,21 @@ +.. _integrations-slack: + +Slack integration +================= + +To set a Slack integration up, it is first necessary to set up a Slack webhook +API endpoint for your Slack channel. This can be done at: + +https://my.slack.com/services/new/incoming-webhook/ + +Select the channel you would like to use, and Slack will provide you with the +webhook URL for configuration. + +You can now create a Slack integration as outlined in +:ref:`creating-integrations`. + +.. note:: + Some settings in the RhodeCode admin are identical to the options within the + Slack integration. For example, if notifications are to be sent in a private + chat, leave the "Channel" field blank. Likewise, the Emoji option within + RhodeCode can override the one set in the Slack admin. \ No newline at end of file diff --git a/docs/integrations/webhook.rst b/docs/integrations/webhook.rst new file mode 100644 --- /dev/null +++ b/docs/integrations/webhook.rst @@ -0,0 +1,12 @@ +.. _integrations-webhook: + +Webhook integration +=================== + +The Webhook integration allows you to POST events such as repository pushes +or pull requests to a custom http endpoint as a json dict with details of the +event. + +To create a webhook integration, select "webhook" in the integration settings +and use the url and key from your custom webhook. See +:ref:`creating-integrations` for additional instructions. \ No newline at end of file diff --git a/docs/issue-trackers/issue-trackers.rst b/docs/issue-trackers/issue-trackers.rst --- a/docs/issue-trackers/issue-trackers.rst +++ b/docs/issue-trackers/issue-trackers.rst @@ -5,12 +5,12 @@ Issue Tracker Integration You can set an issue tracker connection in two ways with |RCE|. -* At instance level you can set a default issue tracker. -* At |repo| level you can configure an integration with a different issue +* At the instance level, you can set a default issue tracker. +* At the |repo| level, you can configure an integration with a different issue tracker. -To integrate |RCM| with an issue tracker you need to define a regular -expression that will fetch the issue ID stored in commit messages and replace +To integrate |RCM| with an issue tracker, you need to define a regular +expression that will fetch the issue ID stored in commit messages, and replace it with a URL. This enables |RCE| to generate a link matching each issue to the target |repo|. @@ -33,9 +33,8 @@ 3. Select **Add** so save the rule to yo Repository Issue Tracker Configuration -------------------------------------- -You can configure specific |repos| to use a different issue tracker if -you need to connect to a non-default one. See the instructions in -:ref:`repo-it` +You can configure specific |repos| to use a different issue tracker than the +default one. See the instructions in :ref:`repo-it` .. _issue-tr-eg-ref: diff --git a/docs/release-notes/release-notes-4.2.1.rst b/docs/release-notes/release-notes-4.2.1.rst --- a/docs/release-notes/release-notes-4.2.1.rst +++ b/docs/release-notes/release-notes-4.2.1.rst @@ -9,6 +9,6 @@ Release Date Fixes ^^^^^ -- ui: fixed empty labels caused by missing translation of JS components -- login: fixed bad routing URL in comments when user is not logged in. -- celery: make sure to run tasks in sync mode if connection to celery is lost. +- UI: fixed empty labels caused by missing translation of JS components. +- Login: fixed bad routing URL in comments when user is not logged in. +- Celery: make sure to run tasks in sync mode if connection to celery is lost. diff --git a/docs/release-notes/release-notes-4.3.0.rst b/docs/release-notes/release-notes-4.3.0.rst new file mode 100644 --- /dev/null +++ b/docs/release-notes/release-notes-4.3.0.rst @@ -0,0 +1,121 @@ +|RCE| 4.3.0 |RNS| +----------------- + +Release Date +^^^^^^^^^^^^ + +- 2016-08-12 + + +General +^^^^^^^ + +- Subversion: detect requests also based on magic path. + This adds subversion 1.9 support for SVN backend. +- Summary/changelog: unified how data is displayed for those pages. + * use consistent order of columns + * fix the link to commit status + * fix order of displaying comments +- Live-chat: refactor live chat system for code review based on + latest channelstream changes. +- SVN: Add template to generate the apache mod_dav_svn config for all + repository groups. Repository groups can now be automatically mapped to be + supported by SVN backend. Set `svn.proxy.generate_config = true` and similar + options found inside .ini config. +- Readme/markup: improved order of generating readme files. Fixes #4050 + * we now use order based on default system renderer + * having multiple readme files will pick correct one as set renderer +- Api: add a max_file_bytes parameter to get_nodes so that large files + can be skipped. +- Auth-ldap: added flag to set debug mode for LDAP connections. +- Labs: moved rebase-merge option from labs settings into VCS settings. +- System: send platform type and version to upgrade endpoint when checking + for new versions. +- Packaging: update rhodecode-tools from 0.8.3 to 0.10.0 +- Packaging: update codemirror from 5.4.0 to 5.11.0 +- Packaging: updated pygments to 2.1.3 +- Packaging: bumped supervisor to 3.3.0 +- Packaging: bumped psycopg2 to 2.6.1 +- Packaging: bumped mercurial to 3.8.4 + + +New Features +^^^^^^^^^^^^ + +- Integrations: created new event based integration framework. + Allows to configure global, or per repo: Slack, Hipchat, Webhooks, Email + integrations. This also deprecated usage of rcextensions for those. +- Integrations (EE only): added smart commits for Jira and Redmine with + ability to map keywords into issue tracker actions. + `Fixes #123 -> resolves issues`, `Closes #123 -> closes issue` etc. +- Markdown: added improved support for Github flavored markdown. +- Labs: enable labs setting by default. Labs are new experimental features in + RhodeCode that can be used to test new upcomming features. +- Api: Add api methods to get/set repository settings, implements #4021. +- Gravatars: commit emails are now displayed based on the actual email + used inside commit rather then the main one of associated account + inside RhodeCode, #4037. +- Emails: All emails got new styling. They look now consistent + to UI of application. We also added bunch of usefull information into + email body, #4087. +- Pull requests: add show/hide comment functionality inside diffs, #4106. +- Notifications: added real-time notifications with via channelstream + about new comments when reviewing the code. Never miss someone replies + onto comments you submitted while doing a code-review. + + +Security +^^^^^^^^ + +- Api: make `comment_commits` api call have consistent permissions + with web interface. +- Files: fixes display of "Add File" button missing or present despite + permissions, because of cached version of the page was rendered, fixes #4083. +- Login/Registration: fixed flash message problem on login/registration + pages, fixes #4043. +- Auth-token: allow other authentication types to use auth-token. + Accounts associated with other types like LDAP, or PAM can + now use auth-tokens to authenticate via RhodeCode. + + +Performance +^^^^^^^^^^^ + +- Core: made all RhodeCode components gevent compatible. RhodeCode can now make + use of async workers. You can handle dozens of concurrent operations using a + single worker. This works only with new HTTP backend. +- Core: added new very efficient HTTP backend can be used to replace pyro4. +- Core: Set 'gzip_responses' to false by default. We no longer require any + gzip computations on backed, thus speeding up large file transfers. +- UI: optimized events system for JavaScript to boost performance on + large html pages. +- VCS: moved VCSMiddleware up to pyramid layer as wrapper around pylons app. + Skips few calls, and allows slightly faster clones/pulls and pushes. + + +Fixes +^^^^^ + +- VCS: add vcsserver cache invalidation to mercurial backend. + Fixes multi-process problems after Mercurial 3.8.X release with server + side merges. +- VCS: clear caches on remap-and-rescan option. +- VCS: improved logic of updating commit caches in cases of rebases. +- Caches: Add an argument to make the cache context thread scoped. Brings + support to gevent compatible handling. +- Diff2way: fixed unicode problem on non-ascii files. +- Full text search: whoosh schema uses now bigger ints, fixes #4035 +- File-browser: optimized cached tree calculation, reduced load times by + 50% on complex file trees. +- Styling: #4086 fixing bug where long commit messages did not wrap in file view. +- SVN: Ignore the content length header from response, fixes #4112. + Fixes the "svn: E120106: ra_serf: The server sent a truncated HTTP response body." +- Auth: Fix password_changed function, fixes #4043. +- UI/tables: better message when tables are empty #685 #1832. +- UX: put gravatar and username together in user list #3203. +- Gists: use colander schema to validate input data. + * brings consistent validation acros API and web + * use nicer and stricter schemas to validate data + * fixes #4118 +- Appenlight: error reporting can now also report VCSMiddleware errors. +- Users: hash email key for User.get_by_email() fixes #4132 diff --git a/docs/release-notes/release-notes.rst b/docs/release-notes/release-notes.rst --- a/docs/release-notes/release-notes.rst +++ b/docs/release-notes/release-notes.rst @@ -9,6 +9,7 @@ Release Notes .. toctree:: :maxdepth: 1 + release-notes-4.3.0.rst release-notes-4.2.1.rst release-notes-4.2.0.rst release-notes-4.1.2.rst diff --git a/docs/tools/tools-cli.rst b/docs/tools/tools-cli.rst --- a/docs/tools/tools-cli.rst +++ b/docs/tools/tools-cli.rst @@ -301,7 +301,7 @@ used to send signals to build-bots such \ - -plugins Add plugins to your |RCE| installation. See the - :ref:`integrations-ref` section for more details. + :ref:`extensions-hooks-ref` section for more details. \ - -version Display your |RCT| version. diff --git a/licenses/tornado_license.txt b/licenses/tornado_license.txt new file mode 100644 --- /dev/null +++ b/licenses/tornado_license.txt @@ -0,0 +1,13 @@ +# Copyright 2009 Facebook +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/pkgs/patch-rhodecode-tools-setup.diff b/pkgs/patch-rhodecode-tools-setup.diff --- a/pkgs/patch-rhodecode-tools-setup.diff +++ b/pkgs/patch-rhodecode-tools-setup.diff @@ -1,12 +1,12 @@ diff --git a/requirements.txt b/requirements.txt --- a/requirements.txt +++ b/requirements.txt -@@ -3,7 +3,7 @@future==0.14.3 +@@ -3,7 +3,7 @@ future==0.14.3 six==1.9.0 mako==1.0.1 markupsafe==0.23 -requests==2.5.1 +requests - #responses whoosh==2.7.0 - elasticsearch==2.3.0 \ No newline at end of file + elasticsearch==2.3.0 + elasticsearch-dsl==2.0.0 \ No newline at end of file diff --git a/pkgs/python-packages-overrides.nix b/pkgs/python-packages-overrides.nix --- a/pkgs/python-packages-overrides.nix +++ b/pkgs/python-packages-overrides.nix @@ -95,6 +95,14 @@ self: super: { }; }); + py-gfm = super.py-gfm.override { + src = pkgs.fetchgit { + url = "https://code.rhodecode.com/upstream/py-gfm"; + rev = "0d66a19bc16e3d49de273c0f797d4e4781e8c0f2"; + sha256 = "0ryp74jyihd3ckszq31bml5jr3bciimhfp7va7kw6ld92930ksv3"; + }; + }; + pycurl = super.pycurl.override (attrs: { propagatedBuildInputs = attrs.propagatedBuildInputs ++ [ pkgs.curl diff --git a/pkgs/python-packages.nix b/pkgs/python-packages.nix --- a/pkgs/python-packages.nix +++ b/pkgs/python-packages.nix @@ -38,6 +38,19 @@ license = [ pkgs.lib.licenses.mit ]; }; }; + Chameleon = super.buildPythonPackage { + name = "Chameleon-2.24"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/5a/9e/637379ffa13c5172b5c0e704833ffea6bf51cec7567f93fd6e903d53ed74/Chameleon-2.24.tar.gz"; + md5 = "1b01f1f6533a8a11d0d2f2366dec5342"; + }; + meta = { + license = [ { fullName = "BSD-like (http://repoze.org/license.html)"; } ]; + }; + }; Fabric = super.buildPythonPackage { name = "Fabric-1.10.0"; buildInputs = with self; []; @@ -169,13 +182,13 @@ }; }; Pygments = super.buildPythonPackage { - name = "Pygments-2.0.2"; + name = "Pygments-2.1.3"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/f4/c6/bdbc5a8a112256b2b6136af304dbae93d8b1ef8738ff2d12a51018800e46/Pygments-2.0.2.tar.gz"; - md5 = "238587a1370d62405edabd0794b3ec4a"; + url = "https://pypi.python.org/packages/b8/67/ab177979be1c81bc99c8d0592ef22d547e70bb4c6815c383286ed5dec504/Pygments-2.1.3.tar.gz"; + md5 = "ed3fba2467c8afcda4d317e4ef2c6150"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal ]; @@ -467,6 +480,19 @@ license = [ pkgs.lib.licenses.bsdOriginal ]; }; }; + channelstream = super.buildPythonPackage { + name = "channelstream-0.5.2"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; [gevent ws4py pyramid pyramid-jinja2 itsdangerous requests six]; + src = fetchurl { + url = "https://pypi.python.org/packages/2b/31/29a8e085cf5bf97fa88e7b947adabfc581a18a3463adf77fb6dada34a65f/channelstream-0.5.2.tar.gz"; + md5 = "1c5eb2a8a405be6f1073da94da6d81d3"; + }; + meta = { + license = [ pkgs.lib.licenses.bsdOriginal ]; + }; + }; click = super.buildPythonPackage { name = "click-5.1"; buildInputs = with self; []; @@ -558,6 +584,19 @@ license = [ pkgs.lib.licenses.bsdOriginal ]; }; }; + deform = super.buildPythonPackage { + name = "deform-2.0a2"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; [Chameleon colander peppercorn translationstring zope.deprecation]; + src = fetchurl { + url = "https://pypi.python.org/packages/8d/b3/aab57e81da974a806dc9c5fa024a6404720f890a6dcf2e80885e3cb4609a/deform-2.0a2.tar.gz"; + md5 = "7a90d41f7fbc18002ce74f39bd90a5e4"; + }; + meta = { + license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; + }; + }; docutils = super.buildPythonPackage { name = "docutils-0.12"; buildInputs = with self; []; @@ -572,13 +611,13 @@ }; }; dogpile.cache = super.buildPythonPackage { - name = "dogpile.cache-0.5.7"; + name = "dogpile.cache-0.6.1"; buildInputs = with self; []; doCheck = false; - propagatedBuildInputs = with self; [dogpile.core]; + propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/07/74/2a83bedf758156d9c95d112691bbad870d3b77ccbcfb781b4ef836ea7d96/dogpile.cache-0.5.7.tar.gz"; - md5 = "3e58ce41af574aab41d78e9c4190f194"; + url = "https://pypi.python.org/packages/f6/a0/6f2142c58c6588d17c734265b103ae1cd0741e1681dd9483a63f22033375/dogpile.cache-0.6.1.tar.gz"; + md5 = "35d7fb30f22bbd0685763d894dd079a9"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal ]; @@ -688,6 +727,19 @@ license = [ pkgs.lib.licenses.bsdOriginal ]; }; }; + gevent = super.buildPythonPackage { + name = "gevent-1.1.1"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; [greenlet]; + src = fetchurl { + url = "https://pypi.python.org/packages/12/dc/0b2e57823225de86f6e111a65d212c9e3b64847dddaa19691a6cb94b0b2e/gevent-1.1.1.tar.gz"; + md5 = "1532f5396ab4d07a231f1935483be7c3"; + }; + meta = { + license = [ pkgs.lib.licenses.mit ]; + }; + }; gnureadline = super.buildPythonPackage { name = "gnureadline-6.3.3"; buildInputs = with self; []; @@ -702,7 +754,7 @@ }; }; gprof2dot = super.buildPythonPackage { - name = "gprof2dot-2015.12.1"; + name = "gprof2dot-2015.12.01"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; @@ -714,6 +766,19 @@ license = [ { fullName = "LGPL"; } ]; }; }; + greenlet = super.buildPythonPackage { + name = "greenlet-0.4.9"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/4e/3d/9d421539b74e33608b245092870156b2e171fb49f2b51390aa4641eecb4a/greenlet-0.4.9.zip"; + md5 = "c6659cdb2a5e591723e629d2eef22e82"; + }; + meta = { + license = [ pkgs.lib.licenses.mit ]; + }; + }; gunicorn = super.buildPythonPackage { name = "gunicorn-19.6.0"; buildInputs = with self; []; @@ -948,6 +1013,19 @@ license = [ { fullName = "Expat license"; } pkgs.lib.licenses.mit ]; }; }; + peppercorn = super.buildPythonPackage { + name = "peppercorn-0.5"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/45/ec/a62ec317d1324a01567c5221b420742f094f05ee48097e5157d32be3755c/peppercorn-0.5.tar.gz"; + md5 = "f08efbca5790019ab45d76b7244abd40"; + }; + meta = { + license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; + }; + }; psutil = super.buildPythonPackage { name = "psutil-2.2.1"; buildInputs = with self; []; @@ -962,13 +1040,13 @@ }; }; psycopg2 = super.buildPythonPackage { - name = "psycopg2-2.6"; + name = "psycopg2-2.6.1"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/dd/c7/9016ff8ff69da269b1848276eebfb264af5badf6b38caad805426771f04d/psycopg2-2.6.tar.gz"; - md5 = "fbbb039a8765d561a1c04969bbae7c74"; + url = "https://pypi.python.org/packages/86/fd/cc8315be63a41fe000cce20482a917e874cdc1151e62cb0141f5e55f711e/psycopg2-2.6.1.tar.gz"; + md5 = "842b44f8c95517ed5b792081a2370da1"; }; meta = { license = [ pkgs.lib.licenses.zpt21 { fullName = "GNU Library or Lesser General Public License (LGPL)"; } { fullName = "LGPL with exceptions or ZPL"; } ]; @@ -1000,6 +1078,19 @@ license = [ pkgs.lib.licenses.bsdOriginal ]; }; }; + py-gfm = super.buildPythonPackage { + name = "py-gfm-0.1.3"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; [setuptools Markdown]; + src = fetchurl { + url = "https://pypi.python.org/packages/12/e4/6b3d8678da04f97d7490d8264d8de51c2dc9fb91209ccee9c515c95e14c5/py-gfm-0.1.3.tar.gz"; + md5 = "e588d9e69640a241b97e2c59c22527a6"; + }; + meta = { + license = [ pkgs.lib.licenses.bsdOriginal ]; + }; + }; pycrypto = super.buildPythonPackage { name = "pycrypto-2.6.1"; buildInputs = with self; []; @@ -1339,23 +1430,23 @@ }; }; rhodecode-enterprise-ce = super.buildPythonPackage { - name = "rhodecode-enterprise-ce-4.2.1"; + name = "rhodecode-enterprise-ce-4.3.0"; buildInputs = with self; [WebTest configobj cssselect flake8 lxml mock pytest pytest-cov pytest-runner]; doCheck = true; - propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments Pylons Pyro4 Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress celery colander decorator docutils gunicorn infrae.cache ipython iso8601 kombu msgpack-python packaging psycopg2 pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson waitress zope.cachedescriptors psutil py-bcrypt]; + propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments Pylons Pyro4 Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu msgpack-python packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt]; src = ./.; meta = { license = [ { fullName = "AGPLv3, and Commercial License"; } ]; }; }; rhodecode-tools = super.buildPythonPackage { - name = "rhodecode-tools-0.8.3"; + name = "rhodecode-tools-0.10.0"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [click future six Mako MarkupSafe requests Whoosh elasticsearch elasticsearch-dsl]; src = fetchurl { - url = "https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.8.3.zip"; - md5 = "9acdfd71b8ddf4056057065f37ab9ccb"; + url = "https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.10.0.zip"; + md5 = "4762391473ded761bead3aa58c748044"; }; meta = { license = [ { fullName = "AGPLv3 and Proprietary"; } ]; @@ -1453,13 +1544,13 @@ }; }; supervisor = super.buildPythonPackage { - name = "supervisor-3.1.3"; + name = "supervisor-3.3.0"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [meld3]; src = fetchurl { - url = "https://pypi.python.org/packages/a6/41/65ad5bd66230b173eb4d0b8810230f3a9c59ef52ae066e540b6b99895db7/supervisor-3.1.3.tar.gz"; - md5 = "aad263c4fbc070de63dd354864d5e552"; + url = "https://pypi.python.org/packages/44/80/d28047d120bfcc8158b4e41127706731ee6a3419c661e0a858fb0e7c4b2d/supervisor-3.3.0.tar.gz"; + md5 = "46bac00378d1eddb616752b990c67416"; }; meta = { license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; @@ -1556,6 +1647,19 @@ license = [ pkgs.lib.licenses.zpt21 ]; }; }; + ws4py = super.buildPythonPackage { + name = "ws4py-0.3.5"; + buildInputs = with self; []; + doCheck = false; + propagatedBuildInputs = with self; []; + src = fetchurl { + url = "https://pypi.python.org/packages/b6/4f/34af703be86939629479e74d6e650e39f3bd73b3b09212c34e5125764cbc/ws4py-0.3.5.zip"; + md5 = "a261b75c20b980e55ce7451a3576a867"; + }; + meta = { + license = [ pkgs.lib.licenses.bsdOriginal ]; + }; + }; wsgiref = super.buildPythonPackage { name = "wsgiref-0.1.2"; buildInputs = with self; []; diff --git a/requirements.txt b/requirements.txt --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ MySQL-python==1.2.5 Paste==2.0.2 PasteDeploy==1.5.2 PasteScript==1.7.5 -Pygments==2.0.2 +Pygments==2.1.3 # TODO: This version is not available on PyPI # Pylons==1.0.2.dev20160108 @@ -24,10 +24,6 @@ Pyro4==4.41 # TODO: This should probably not be in here # -e hg+https://johbo@code.rhodecode.com/johbo/rhodecode-fork@3a454bd1f17c0b2b2a951cf2b111e0320d7942a9#egg=RhodeCodeEnterprise-dev -# TODO: This is not really a dependency, we should add it only -# into the development environment, since there it is useful. -# RhodeCodeVCSServer==3.9.0 - Routes==1.13 SQLAlchemy==0.9.9 Sphinx==1.2.2 @@ -53,6 +49,7 @@ backport-ipaddress==0.1 bottle==0.12.8 bumpversion==0.5.3 celery==2.2.10 +channelstream==0.5.2 click==5.1 colander==1.2 configobj==5.0.6 @@ -60,15 +57,18 @@ cov-core==1.15.0 coverage==3.7.1 cssselect==0.9.1 decorator==3.4.2 +deform==2.0a2 docutils==0.12 -dogpile.cache==0.5.7 +dogpile.cache==0.6.1 dogpile.core==0.4.1 dulwich==0.12.0 ecdsa==0.11 flake8==2.4.1 future==0.14.3 futures==3.0.2 +gevent==1.1.1 gprof2dot==2015.12.1 +greenlet==0.4.9 gunicorn==19.6.0 # TODO: Needs subvertpy and blows up without Subversion headers, @@ -94,9 +94,10 @@ packaging==15.2 paramiko==1.15.1 pep8==1.5.7 psutil==2.2.1 -psycopg2==2.6 +psycopg2==2.6.1 py==1.4.29 py-bcrypt==0.4 +py-gfm==0.1.3 pycrypto==2.6.1 pycurl==7.19.5 pyflakes==0.8.1 @@ -123,7 +124,7 @@ pyzmq==14.6.0 # TODO: This is not available in public # rc-testdata==0.2.0 -https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.8.3.zip#md5=9acdfd71b8ddf4056057065f37ab9ccb +https://code.rhodecode.com/rhodecode-tools-ce/archive/v0.10.0.zip#md5=4762391473ded761bead3aa58c748044 recaptcha-client==1.0.6 @@ -136,7 +137,7 @@ setuptools-scm==1.11.0 simplejson==3.7.2 six==1.9.0 subprocess32==3.2.6 -supervisor==3.1.3 +supervisor==3.3.0 transifex-client==0.10 translationstring==1.3 trollius==1.0.4 diff --git a/rhodecode/VERSION b/rhodecode/VERSION --- a/rhodecode/VERSION +++ b/rhodecode/VERSION @@ -1,1 +1,1 @@ -4.2.1 \ No newline at end of file +4.3.0 \ No newline at end of file diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py --- a/rhodecode/__init__.py +++ b/rhodecode/__init__.py @@ -43,11 +43,15 @@ CELERY_EAGER = False # link to config for pylons CONFIG = {} +# Populated with the settings dictionary from application init in +# rhodecode.conf.environment.load_pyramid_environment +PYRAMID_SETTINGS = {} + # Linked module for extensions EXTENSIONS = {} __version__ = ('.'.join((str(each) for each in VERSION[:3]))) -__dbversion__ = 54 # defines current db version for migrations +__dbversion__ = 55 # defines current db version for migrations __platform__ = platform.system() __license__ = 'AGPLv3, and Commercial License' __author__ = 'RhodeCode GmbH' diff --git a/rhodecode/admin/navigation.py b/rhodecode/admin/navigation.py --- a/rhodecode/admin/navigation.py +++ b/rhodecode/admin/navigation.py @@ -80,6 +80,8 @@ class NavigationRegistry(object): NavEntry('email', _('Email'), 'admin_settings_email'), NavEntry('hooks', _('Hooks'), 'admin_settings_hooks'), NavEntry('search', _('Full Text Search'), 'admin_settings_search'), + NavEntry('integrations', _('Integrations'), + 'global_integrations_home', pyramid=True), NavEntry('system', _('System Info'), 'admin_settings_system'), NavEntry('open_source', _('Open Source Licenses'), 'admin_settings_open_source', pyramid=True), diff --git a/rhodecode/api/__init__.py b/rhodecode/api/__init__.py --- a/rhodecode/api/__init__.py +++ b/rhodecode/api/__init__.py @@ -25,12 +25,15 @@ import types import decorator import venusian +from collections import OrderedDict + from pyramid.exceptions import ConfigurationError from pyramid.renderers import render from pyramid.response import Response from pyramid.httpexceptions import HTTPNotFound -from rhodecode.api.exc import JSONRPCBaseError, JSONRPCError, JSONRPCForbidden +from rhodecode.api.exc import ( + JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError) from rhodecode.lib.auth import AuthUser from rhodecode.lib.base import get_ip_addr from rhodecode.lib.ext_json import json @@ -127,6 +130,11 @@ def exception_view(exc, request): if isinstance(exc, JSONRPCError): fault_message = exc.message log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message) + elif isinstance(exc, JSONRPCValidationError): + colander_exc = exc.colander_exception + #TODO: think maybe of nicer way to serialize errors ? + fault_message = colander_exc.asdict() + log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message) elif isinstance(exc, JSONRPCForbidden): fault_message = 'Access was denied to this resource.' log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message) @@ -474,7 +482,7 @@ def includeme(config): plugin_module, config.registry.settings) if not hasattr(config.registry, 'jsonrpc_methods'): - config.registry.jsonrpc_methods = {} + config.registry.jsonrpc_methods = OrderedDict() # match filter by given method only config.add_view_predicate( diff --git a/rhodecode/api/exc.py b/rhodecode/api/exc.py --- a/rhodecode/api/exc.py +++ b/rhodecode/api/exc.py @@ -27,5 +27,13 @@ class JSONRPCError(JSONRPCBaseError): pass +class JSONRPCValidationError(JSONRPCBaseError): + + def __init__(self, *args, **kwargs): + self.colander_exception = kwargs.pop('colander_exc') + super(JSONRPCValidationError, self).__init__(*args, **kwargs) + + class JSONRPCForbidden(JSONRPCBaseError): pass + diff --git a/rhodecode/api/tests/test_create_gist.py b/rhodecode/api/tests/test_create_gist.py --- a/rhodecode/api/tests/test_create_gist.py +++ b/rhodecode/api/tests/test_create_gist.py @@ -43,7 +43,7 @@ class TestApiCreateGist(object): description='foobar-gist', gist_type=gist_type, acl_level=gist_acl_level, - files={'foobar': {'content': 'foo'}}) + files={'foobar_ąć': {'content': 'foo'}}) response = api_call(self.app, params) response_json = response.json gist = response_json['result']['gist'] @@ -68,6 +68,32 @@ class TestApiCreateGist(object): finally: Fixture().destroy_gists() + @pytest.mark.parametrize("expected, lifetime, gist_type, gist_acl_level, files", [ + ({'gist_type': '"ups" is not one of private, public'}, + 10, 'ups', Gist.ACL_LEVEL_PUBLIC, {'f': {'content': 'f'}}), + + ({'lifetime': '-120 is less than minimum value -1'}, + -120, Gist.GIST_PUBLIC, Gist.ACL_LEVEL_PUBLIC, {'f': {'content': 'f'}}), + + ({'0.content': 'Required'}, + 10, Gist.GIST_PUBLIC, Gist.ACL_LEVEL_PUBLIC, {'f': {'x': 'f'}}), + ]) + def test_api_try_create_gist( + self, expected, lifetime, gist_type, gist_acl_level, files): + id_, params = build_data( + self.apikey_regular, 'create_gist', + lifetime=lifetime, + description='foobar-gist', + gist_type=gist_type, + acl_level=gist_acl_level, + files=files) + response = api_call(self.app, params) + + try: + assert_error(id_, expected, given=response.body) + finally: + Fixture().destroy_gists() + @mock.patch.object(GistModel, 'create', crash) def test_api_create_gist_exception_occurred(self): id_, params = build_data(self.apikey_regular, 'create_gist', files={}) diff --git a/rhodecode/api/tests/test_deprecated_api.py b/rhodecode/api/tests/test_deprecated_api.py --- a/rhodecode/api/tests/test_deprecated_api.py +++ b/rhodecode/api/tests/test_deprecated_api.py @@ -21,7 +21,7 @@ import pytest -from rhodecode.api.views import depracated_api +from rhodecode.api.views import deprecated_api from rhodecode.lib.ext_json import json from rhodecode.api.tests.utils import ( build_data, api_call) @@ -30,7 +30,7 @@ from rhodecode.api.tests.utils import ( @pytest.mark.usefixtures("testuser_api", "app") class TestCommitComment(object): def test_deprecated_message_in_docstring(self): - docstring = depracated_api.changeset_comment.__doc__ + docstring = deprecated_api.changeset_comment.__doc__ assert '.. deprecated:: 3.4.0' in docstring assert 'Please use method `comment_commit` instead.' in docstring diff --git a/rhodecode/api/tests/test_get_repo_nodes.py b/rhodecode/api/tests/test_get_repo_nodes.py --- a/rhodecode/api/tests/test_get_repo_nodes.py +++ b/rhodecode/api/tests/test_get_repo_nodes.py @@ -73,6 +73,29 @@ class TestGetRepoNodes(object): expected = 'failed to get repo: `%s` nodes' % (backend.repo_name,) assert_error(id_, expected, given=response.body) + def test_api_get_repo_nodes_max_file_bytes(self, backend): + commit_id = 'tip' + path = '/' + max_file_bytes = 500 + + id_, params = build_data( + self.apikey, 'get_repo_nodes', + repoid=backend.repo_name, revision=commit_id, details='full', + root_path=path) + response = api_call(self.app, params) + assert any(file['content'] and len(file['content']) > max_file_bytes + for file in response.json['result']) + + id_, params = build_data( + self.apikey, 'get_repo_nodes', + repoid=backend.repo_name, revision=commit_id, + root_path=path, details='full', + max_file_bytes=max_file_bytes) + response = api_call(self.app, params) + assert all( + file['content'] is None if file['size'] > max_file_bytes else True + for file in response.json['result']) + def test_api_get_repo_nodes_bad_ret_type(self, backend): commit_id = 'tip' path = '/' diff --git a/rhodecode/api/tests/test_update_repo.py b/rhodecode/api/tests/test_update_repo.py --- a/rhodecode/api/tests/test_update_repo.py +++ b/rhodecode/api/tests/test_update_repo.py @@ -47,9 +47,14 @@ class TestApiUpdateRepo(object): ({'enable_statistics': True}, SAME_AS_UPDATES), ({'enable_locking': True}, SAME_AS_UPDATES), ({'enable_downloads': True}, SAME_AS_UPDATES), - ({'name': 'new_repo_name'}, {'repo_name': 'new_repo_name'}), - ({'group': 'test_group_for_update'}, - {'repo_name': 'test_group_for_update/%s' % UPDATE_REPO_NAME}), + ({'name': 'new_repo_name'}, { + 'repo_name': 'new_repo_name', + 'url': 'http://test.example.com:80/new_repo_name', + }), + ({'group': 'test_group_for_update'}, { + 'repo_name': 'test_group_for_update/%s' % UPDATE_REPO_NAME, + 'url': 'http://test.example.com:80/test_group_for_update/%s' % UPDATE_REPO_NAME + }), ]) def test_api_update_repo(self, updates, expected, backend): repo_name = UPDATE_REPO_NAME diff --git a/rhodecode/api/utils.py b/rhodecode/api/utils.py --- a/rhodecode/api/utils.py +++ b/rhodecode/api/utils.py @@ -34,8 +34,6 @@ from rhodecode.lib.vcs.exceptions import log = logging.getLogger(__name__) - - class OAttr(object): """ Special Option that defines other attribute, and can default to them diff --git a/rhodecode/api/views/depracated_api.py b/rhodecode/api/views/deprecated_api.py rename from rhodecode/api/views/depracated_api.py rename to rhodecode/api/views/deprecated_api.py diff --git a/rhodecode/api/views/gist_api.py b/rhodecode/api/views/gist_api.py --- a/rhodecode/api/views/gist_api.py +++ b/rhodecode/api/views/gist_api.py @@ -23,6 +23,7 @@ import logging import time from rhodecode.api import jsonrpc_method, JSONRPCError +from rhodecode.api.exc import JSONRPCValidationError from rhodecode.api.utils import ( Optional, OAttr, get_gist_or_error, get_user_or_error, has_superadmin_permission) @@ -96,7 +97,8 @@ def get_gists(request, apiuser, userid=O @jsonrpc_method() def create_gist( - request, apiuser, files, owner=Optional(OAttr('apiuser')), + request, apiuser, files, gistid=Optional(None), + owner=Optional(OAttr('apiuser')), gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1), acl_level=Optional(Gist.ACL_LEVEL_PUBLIC), description=Optional('')): @@ -108,10 +110,11 @@ def create_gist( :param files: files to be added to the gist. The data structure has to match the following example:: - {'filename': {'content':'...', 'lexer': null}, - 'filename2': {'content':'...', 'lexer': null}} + {'filename1': {'content':'...'}, 'filename2': {'content':'...'}} :type files: dict + :param gistid: Set a custom id for the gist + :type gistid: Optional(str) :param owner: Set the gist owner, defaults to api method caller :type owner: Optional(str or int) :param gist_type: type of gist ``public`` or ``private`` @@ -148,23 +151,49 @@ def create_gist( } """ + from rhodecode.model import validation_schema + from rhodecode.model.validation_schema.schemas import gist_schema + + if isinstance(owner, Optional): + owner = apiuser.user_id + + owner = get_user_or_error(owner) + + lifetime = Optional.extract(lifetime) + schema = gist_schema.GistSchema().bind( + # bind the given values if it's allowed, however the deferred + # validator will still validate it according to other rules + lifetime_options=[lifetime]) try: - if isinstance(owner, Optional): - owner = apiuser.user_id + nodes = gist_schema.nodes_to_sequence( + files, colander_node=schema.get('nodes')) + + schema_data = schema.deserialize(dict( + gistid=Optional.extract(gistid), + description=Optional.extract(description), + gist_type=Optional.extract(gist_type), + lifetime=lifetime, + gist_acl_level=Optional.extract(acl_level), + nodes=nodes + )) - owner = get_user_or_error(owner) - description = Optional.extract(description) - gist_type = Optional.extract(gist_type) - lifetime = Optional.extract(lifetime) - acl_level = Optional.extract(acl_level) + # convert to safer format with just KEYs so we sure no duplicates + schema_data['nodes'] = gist_schema.sequence_to_nodes( + schema_data['nodes'], colander_node=schema.get('nodes')) + + except validation_schema.Invalid as err: + raise JSONRPCValidationError(colander_exc=err) - gist = GistModel().create(description=description, - owner=owner, - gist_mapping=files, - gist_type=gist_type, - lifetime=lifetime, - gist_acl_level=acl_level) + try: + gist = GistModel().create( + owner=owner, + gist_id=schema_data['gistid'], + description=schema_data['description'], + gist_mapping=schema_data['nodes'], + gist_type=schema_data['gist_type'], + lifetime=schema_data['lifetime'], + gist_acl_level=schema_data['gist_acl_level']) Session().commit() return { 'msg': 'created new gist', diff --git a/rhodecode/api/views/pull_request_api.py b/rhodecode/api/views/pull_request_api.py --- a/rhodecode/api/views/pull_request_api.py +++ b/rhodecode/api/views/pull_request_api.py @@ -265,7 +265,7 @@ def merge_pull_request(request, apiuser, PullRequestModel().close_pull_request( pull_request.pull_request_id, apiuser) - Session.commit() + Session().commit() return data @@ -319,7 +319,7 @@ def close_pull_request(request, apiuser, PullRequestModel().close_pull_request( pull_request.pull_request_id, apiuser) - Session.commit() + Session().commit() data = { 'pull_request_id': pull_request.pull_request_id, 'closed': True, @@ -408,6 +408,8 @@ def comment_pull_request(request, apiuse line_no=None, status_change=(ChangesetStatus.get_status_lbl(status) if status and allowed_to_change_status else None), + status_change_type=(status + if status and allowed_to_change_status else None), closing_pr=False, renderer=renderer ) diff --git a/rhodecode/api/views/repo_api.py b/rhodecode/api/views/repo_api.py --- a/rhodecode/api/views/repo_api.py +++ b/rhodecode/api/views/repo_api.py @@ -43,8 +43,8 @@ from rhodecode.model.db import ( from rhodecode.model.repo import RepoModel from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.scm import ScmModel, RepoList -from rhodecode.model.settings import SettingsModel -from rhodecode.model.validation_schema import RepoSchema +from rhodecode.model.settings import SettingsModel, VcsSettingsModel +from rhodecode.model.validation_schema.schemas import repo_schema log = logging.getLogger(__name__) @@ -400,7 +400,8 @@ def get_repo_changesets(request, apiuser @jsonrpc_method() def get_repo_nodes(request, apiuser, repoid, revision, root_path, - ret_type=Optional('all'), details=Optional('basic')): + ret_type=Optional('all'), details=Optional('basic'), + max_file_bytes=Optional(None)): """ Returns a list of nodes and children in a flat list for a given path at given revision. @@ -425,6 +426,8 @@ def get_repo_nodes(request, apiuser, rep md5, binary, and or content. The valid options are ``basic`` and ``full``. :type details: Optional(str) + :param max_file_bytes: Only return file content under this file size bytes + :type details: Optional(int) Example output: @@ -472,7 +475,8 @@ def get_repo_nodes(request, apiuser, rep _d, _f = ScmModel().get_nodes( repo, revision, root_path, flat=False, - extended_info=extended_info, content=content) + extended_info=extended_info, content=content, + max_file_bytes=max_file_bytes) _map = { 'all': _d + _f, 'files': _f, @@ -606,7 +610,7 @@ def create_repo(request, apiuser, repo_n } """ - schema = RepoSchema() + schema = repo_schema.RepoSchema() try: data = schema.deserialize({ 'repo_name': repo_name @@ -1310,8 +1314,6 @@ def comment_commit( userid=Optional(OAttr('apiuser')), status=Optional(None)): """ Set a commit comment, and optionally change the status of the commit. - This command can be executed only using api_key belonging to user - with admin rights, or repository administrator. :param apiuser: This is filled automatically from the |authtoken|. :type apiuser: AuthUser @@ -1344,7 +1346,7 @@ def comment_commit( """ repo = get_repo_or_error(repoid) if not has_superadmin_permission(apiuser): - _perms = ('repository.admin',) + _perms = ('repository.read', 'repository.write', 'repository.admin') has_repo_permissions(apiuser, repoid, repo, _perms) if isinstance(userid, Optional): @@ -1361,9 +1363,11 @@ def comment_commit( try: rc_config = SettingsModel().get_all_settings() renderer = rc_config.get('rhodecode_markup_renderer', 'rst') - + status_change_label = ChangesetStatus.get_status_lbl(status) comm = ChangesetCommentsModel().create( - message, repo, user, revision=commit_id, status_change=status, + message, repo, user, revision=commit_id, + status_change=status_change_label, + status_change_type=status, renderer=renderer) if status: # also do a status change @@ -1775,3 +1779,110 @@ def strip(request, apiuser, repoid, revi 'Unable to strip commit %s from repo `%s`' % ( revision, repo.repo_name) ) + + +@jsonrpc_method() +def get_repo_settings(request, apiuser, repoid, key=Optional(None)): + """ + Returns all settings for a repository. If key is given it only returns the + setting identified by the key or null. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: The repository name or repository id. + :type repoid: str or int + :param key: Key of the setting to return. + :type: key: Optional(str) + + Example output: + + .. code-block:: bash + + { + "error": null, + "id": 237, + "result": { + "extensions_largefiles": true, + "hooks_changegroup_push_logger": true, + "hooks_changegroup_repo_size": false, + "hooks_outgoing_pull_logger": true, + "phases_publish": "True", + "rhodecode_hg_use_rebase_for_merging": true, + "rhodecode_pr_merge_enabled": true, + "rhodecode_use_outdated_comments": true + } + } + """ + + # Restrict access to this api method to admins only. + if not has_superadmin_permission(apiuser): + raise JSONRPCForbidden() + + try: + repo = get_repo_or_error(repoid) + settings_model = VcsSettingsModel(repo=repo) + settings = settings_model.get_global_settings() + settings.update(settings_model.get_repo_settings()) + + # If only a single setting is requested fetch it from all settings. + key = Optional.extract(key) + if key is not None: + settings = settings.get(key, None) + except Exception: + msg = 'Failed to fetch settings for repository `{}`'.format(repoid) + log.exception(msg) + raise JSONRPCError(msg) + + return settings + + +@jsonrpc_method() +def set_repo_settings(request, apiuser, repoid, settings): + """ + Update repository settings. Returns true on success. + + :param apiuser: This is filled automatically from the |authtoken|. + :type apiuser: AuthUser + :param repoid: The repository name or repository id. + :type repoid: str or int + :param settings: The new settings for the repository. + :type: settings: dict + + Example output: + + .. code-block:: bash + + { + "error": null, + "id": 237, + "result": true + } + """ + # Restrict access to this api method to admins only. + if not has_superadmin_permission(apiuser): + raise JSONRPCForbidden() + + if type(settings) is not dict: + raise JSONRPCError('Settings have to be a JSON Object.') + + try: + settings_model = VcsSettingsModel(repo=repoid) + + # Merge global, repo and incoming settings. + new_settings = settings_model.get_global_settings() + new_settings.update(settings_model.get_repo_settings()) + new_settings.update(settings) + + # Update the settings. + inherit_global_settings = new_settings.get( + 'inherit_global_settings', False) + settings_model.create_or_update_repo_settings( + new_settings, inherit_global_settings=inherit_global_settings) + Session().commit() + except Exception: + msg = 'Failed to update settings for repository `{}`'.format(repoid) + log.exception(msg) + raise JSONRPCError(msg) + + # Indicate success. + return True diff --git a/rhodecode/api/views/repo_group_api.py b/rhodecode/api/views/repo_group_api.py --- a/rhodecode/api/views/repo_group_api.py +++ b/rhodecode/api/views/repo_group_api.py @@ -34,7 +34,7 @@ from rhodecode.lib.auth import ( from rhodecode.model.db import Session, RepoGroup from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.scm import RepoGroupList -from rhodecode.model.validation_schema import RepoGroupSchema +from rhodecode.model.validation_schema.schemas import repo_group_schema log = logging.getLogger(__name__) @@ -193,7 +193,7 @@ def create_repo_group(request, apiuser, """ - schema = RepoGroupSchema() + schema = repo_group_schema.RepoGroupSchema() try: data = schema.deserialize({ 'group_name': group_name diff --git a/rhodecode/authentication/base.py b/rhodecode/authentication/base.py --- a/rhodecode/authentication/base.py +++ b/rhodecode/authentication/base.py @@ -490,20 +490,26 @@ def loadplugin(plugin_id): or None on failure. """ # TODO: Disusing pyramids thread locals to retrieve the registry. - authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry) + authn_registry = get_authn_registry() plugin = authn_registry.get_plugin(plugin_id) if plugin is None: log.error('Authentication plugin not found: "%s"', plugin_id) return plugin +def get_authn_registry(registry=None): + registry = registry or get_current_registry() + authn_registry = registry.getUtility(IAuthnPluginRegistry) + return authn_registry + + def get_auth_cache_manager(custom_ttl=None): return caches.get_cache_manager( 'auth_plugins', 'rhodecode.authentication', custom_ttl) def authenticate(username, password, environ=None, auth_type=None, - skip_missing=False): + skip_missing=False, registry=None): """ Authentication function used for access control, It tries to authenticate based on enabled authentication modules. @@ -520,7 +526,7 @@ def authenticate(username, password, env % auth_type) headers_only = environ and not (username and password) - authn_registry = get_current_registry().getUtility(IAuthnPluginRegistry) + authn_registry = get_authn_registry(registry) for plugin in authn_registry.get_plugins_for_authentication(): plugin.set_auth_type(auth_type) user = plugin.get_user(username) @@ -559,16 +565,16 @@ def authenticate(username, password, env if isinstance(plugin.AUTH_CACHE_TTL, (int, long)): # plugin cache set inside is more important than the settings value _cache_ttl = plugin.AUTH_CACHE_TTL - elif plugin_settings.get('auth_cache_ttl'): - _cache_ttl = safe_int(plugin_settings.get('auth_cache_ttl'), 0) + elif plugin_settings.get('cache_ttl'): + _cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0) plugin_cache_active = bool(_cache_ttl and _cache_ttl > 0) # get instance of cache manager configured for a namespace cache_manager = get_auth_cache_manager(custom_ttl=_cache_ttl) - log.debug('Cache for plugin `%s` active: %s', plugin.get_id(), - plugin_cache_active) + log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)', + plugin.get_id(), plugin_cache_active, _cache_ttl) # for environ based password can be empty, but then the validation is # on the server that fills in the env data needed for authentication @@ -581,8 +587,7 @@ def authenticate(username, password, env # to RhodeCode database. If this function returns data # then auth is correct. start = time.time() - log.debug('Running plugin `%s` _authenticate method', - plugin.get_id()) + log.debug('Running plugin `%s` _authenticate method', plugin.get_id()) def auth_func(): """ diff --git a/rhodecode/authentication/plugins/auth_ldap.py b/rhodecode/authentication/plugins/auth_ldap.py --- a/rhodecode/authentication/plugins/auth_ldap.py +++ b/rhodecode/authentication/plugins/auth_ldap.py @@ -141,9 +141,9 @@ class LdapSettingsSchema(AuthnPluginSett colander.String(), default='', description=_('LDAP Attribute to map to user name'), - missing_msg=_('The LDAP Login attribute of the CN must be specified'), preparer=strip_whitespace, title=_('Login Attribute'), + missing_msg=_('The LDAP Login attribute of the CN must be specified'), widget='string') attr_firstname = colander.SchemaNode( colander.String(), @@ -186,6 +186,7 @@ class AuthLdap(object): if ldap == Missing: raise LdapImportError("Missing or incompatible ldap library") + self.debug = False self.ldap_version = ldap_version self.ldap_server_type = 'ldap' @@ -213,6 +214,8 @@ class AuthLdap(object): self.LDAP_FILTER = safe_str(ldap_filter) def _get_ldap_server(self): + if self.debug: + ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255) if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'): ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, '/etc/openldap/cacerts') diff --git a/rhodecode/authentication/plugins/auth_rhodecode.py b/rhodecode/authentication/plugins/auth_rhodecode.py --- a/rhodecode/authentication/plugins/auth_rhodecode.py +++ b/rhodecode/authentication/plugins/auth_rhodecode.py @@ -137,5 +137,7 @@ class RhodeCodeAuthPlugin(RhodeCodeAuthP "authenticating on this plugin", userobj) return None else: - log.warning('user %s tried auth but is disabled', userobj) + log.warning( + 'user `%s` failed to authenticate via %s, reason: account not ' + 'active.', username, self.name) return None diff --git a/rhodecode/authentication/plugins/auth_token.py b/rhodecode/authentication/plugins/auth_token.py --- a/rhodecode/authentication/plugins/auth_token.py +++ b/rhodecode/authentication/plugins/auth_token.py @@ -83,13 +83,17 @@ class RhodeCodeAuthPlugin(RhodeCodeAuthP allowed_auth_plugins=None, allowed_auth_sources=None): """ Custom method for this auth that doesn't accept empty users. And also - allows rhodecode and authtoken extern_type to auth with this. But only - via vcs mode + allows users from all other active plugins to use it and also + authenticate against it. But only via vcs mode """ - # only this and rhodecode plugins can use this type - from rhodecode.authentication.plugins import auth_rhodecode - allowed_auth_plugins = [ - self.name, auth_rhodecode.RhodeCodeAuthPlugin.name] + from rhodecode.authentication.base import get_authn_registry + authn_registry = get_authn_registry() + + active_plugins = set( + [x.name for x in authn_registry.get_plugins_for_authentication()]) + active_plugins.discard(self.name) + + allowed_auth_plugins = [self.name] + list(active_plugins) # only for vcs operations allowed_auth_sources = [VCS_TYPE] diff --git a/rhodecode/authentication/views.py b/rhodecode/authentication/views.py --- a/rhodecode/authentication/views.py +++ b/rhodecode/authentication/views.py @@ -26,8 +26,8 @@ from pyramid.httpexceptions import HTTPF from pyramid.renderers import render from pyramid.response import Response -from rhodecode.authentication.base import get_auth_cache_manager -from rhodecode.authentication.interface import IAuthnPluginRegistry +from rhodecode.authentication.base import ( + get_auth_cache_manager, get_authn_registry) from rhodecode.lib import auth from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator from rhodecode.model.forms import AuthSettingsForm @@ -96,7 +96,7 @@ class AuthnPluginViewBase(object): # Store validated data. for name, value in valid_data.items(): self.plugin.create_or_update_setting(name, value) - Session.commit() + Session().commit() # Display success message and redirect. self.request.session.flash( @@ -125,7 +125,7 @@ class AuthSettingsView(object): @HasPermissionAllDecorator('hg.admin') def index(self, defaults=None, errors=None, prefix_error=False): defaults = defaults or {} - authn_registry = self.request.registry.getUtility(IAuthnPluginRegistry) + authn_registry = get_authn_registry(self.request.registry) enabled_plugins = SettingsModel().get_auth_plugins() # Create template context and render it. diff --git a/rhodecode/channelstream/__init__.py b/rhodecode/channelstream/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/channelstream/__init__.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import os + +from pyramid.settings import asbool + +from rhodecode.config.routing import ADMIN_PREFIX +from rhodecode.lib.ext_json import json + + +def url_gen(request): + urls = { + 'connect': request.route_url('channelstream_connect'), + 'subscribe': request.route_url('channelstream_subscribe') + } + return json.dumps(urls) + + +PLUGIN_DEFINITION = { + 'name': 'channelstream', + 'config': { + 'javascript': [], + 'css': [], + 'template_hooks': { + 'plugin_init_template': 'rhodecode:templates/channelstream/plugin_init.html' + }, + 'url_gen': url_gen, + 'static': None, + 'enabled': False, + 'server': '', + 'secret': '' + } +} + + +def includeme(config): + settings = config.registry.settings + PLUGIN_DEFINITION['config']['enabled'] = asbool( + settings.get('channelstream.enabled')) + PLUGIN_DEFINITION['config']['server'] = settings.get( + 'channelstream.server', '') + PLUGIN_DEFINITION['config']['secret'] = settings.get( + 'channelstream.secret', '') + PLUGIN_DEFINITION['config']['history.location'] = settings.get( + 'channelstream.history.location', '') + config.register_rhodecode_plugin( + PLUGIN_DEFINITION['name'], + PLUGIN_DEFINITION['config'] + ) + # create plugin history location + history_dir = PLUGIN_DEFINITION['config']['history.location'] + if history_dir and not os.path.exists(history_dir): + os.makedirs(history_dir, 0750) + + config.add_route( + name='channelstream_connect', + pattern=ADMIN_PREFIX + '/channelstream/connect') + config.add_route( + name='channelstream_subscribe', + pattern=ADMIN_PREFIX + '/channelstream/subscribe') + config.scan('rhodecode.channelstream') diff --git a/rhodecode/channelstream/views.py b/rhodecode/channelstream/views.py new file mode 100644 --- /dev/null +++ b/rhodecode/channelstream/views.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +""" +Channel Stream controller for rhodecode + +:created_on: Oct 10, 2015 +:author: marcinl +:copyright: (c) 2013-2015 RhodeCode GmbH. +:license: Commercial License, see LICENSE for more details. +""" + +import logging +import uuid + +from pylons import tmpl_context as c +from pyramid.settings import asbool +from pyramid.view import view_config +from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPBadGateway + +from rhodecode.lib.channelstream import ( + channelstream_request, + ChannelstreamConnectionException, + ChannelstreamPermissionException, + check_channel_permissions, + get_connection_validators, + get_user_data, + parse_channels_info, + update_history_from_logs, + STATE_PUBLIC_KEYS) +from rhodecode.lib.auth import NotAnonymous +from rhodecode.lib.utils2 import str2bool + +log = logging.getLogger(__name__) + + +class ChannelstreamView(object): + def __init__(self, context, request): + self.context = context + self.request = request + + # Some of the decorators rely on this attribute to be present + # on the class of the decorated method. + self._rhodecode_user = request.user + registry = request.registry + self.channelstream_config = registry.rhodecode_plugins['channelstream'] + if not self.channelstream_config.get('enabled'): + log.exception('Channelstream plugin is disabled') + raise HTTPBadRequest() + + @NotAnonymous() + @view_config(route_name='channelstream_connect', renderer='json') + def connect(self): + """ handle authorization of users trying to connect """ + try: + json_body = self.request.json_body + except Exception: + log.exception('Failed to decode json from request') + raise HTTPBadRequest() + try: + channels = check_channel_permissions( + json_body.get('channels'), + get_connection_validators(self.request.registry)) + except ChannelstreamPermissionException: + log.error('Incorrect permissions for requested channels') + raise HTTPForbidden() + + user = c.rhodecode_user + if user.user_id: + user_data = get_user_data(user.user_id) + else: + user_data = { + 'id': None, + 'username': None, + 'first_name': None, + 'last_name': None, + 'icon_link': None, + 'display_name': None, + 'display_link': None, + } + payload = { + 'username': user.username, + 'user_state': user_data, + 'conn_id': str(uuid.uuid4()), + 'channels': channels, + 'channel_configs': {}, + 'state_public_keys': STATE_PUBLIC_KEYS, + 'info': { + 'exclude_channels': ['broadcast'] + } + } + filtered_channels = [channel for channel in channels + if channel != 'broadcast'] + for channel in filtered_channels: + payload['channel_configs'][channel] = { + 'notify_presence': True, + 'history_size': 100, + 'store_history': True, + 'broadcast_presence_with_user_lists': True + } + # connect user to server + try: + connect_result = channelstream_request(self.channelstream_config, + payload, '/connect') + except ChannelstreamConnectionException: + log.exception('Channelstream service is down') + return HTTPBadGateway() + + connect_result['channels'] = channels + connect_result['channels_info'] = parse_channels_info( + connect_result['channels_info'], + include_channel_info=filtered_channels) + update_history_from_logs(self.channelstream_config, + filtered_channels, connect_result) + return connect_result + + @NotAnonymous() + @view_config(route_name='channelstream_subscribe', renderer='json') + def subscribe(self): + """ can be used to subscribe specific connection to other channels """ + try: + json_body = self.request.json_body + except Exception: + log.exception('Failed to decode json from request') + raise HTTPBadRequest() + try: + channels = check_channel_permissions( + json_body.get('channels'), + get_connection_validators(self.request.registry)) + except ChannelstreamPermissionException: + log.error('Incorrect permissions for requested channels') + raise HTTPForbidden() + payload = {'conn_id': json_body.get('conn_id', ''), + 'channels': channels, + 'channel_configs': {}, + 'info': { + 'exclude_channels': ['broadcast']} + } + filtered_channels = [chan for chan in channels if chan != 'broadcast'] + for channel in filtered_channels: + payload['channel_configs'][channel] = { + 'notify_presence': True, + 'history_size': 100, + 'store_history': True, + 'broadcast_presence_with_user_lists': True + } + try: + connect_result = channelstream_request( + self.channelstream_config, payload, '/subscribe') + except ChannelstreamConnectionException: + log.exception('Channelstream service is down') + return HTTPBadGateway() + # include_channel_info will limit history only to new channel + # to not overwrite histories on other channels in client + connect_result['channels_info'] = parse_channels_info( + connect_result['channels_info'], + include_channel_info=filtered_channels) + update_history_from_logs(self.channelstream_config, + filtered_channels, connect_result) + return connect_result diff --git a/rhodecode/config/conf.py b/rhodecode/config/conf.py --- a/rhodecode/config/conf.py +++ b/rhodecode/config/conf.py @@ -30,36 +30,6 @@ from rhodecode.lib.utils2 import __get_l # extensions will index it's content LANGUAGES_EXTENSIONS_MAP = __get_lem() -# list of readme files to search in file tree and display in summary -# attached weights defines the search order lower is first -ALL_READMES = [ - ('readme', 0), ('README', 0), ('Readme', 0), - ('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1), - ('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2), - ('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2), - ('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2), -] - -# extension together with weights to search lower is first -RST_EXTS = [ - ('', 0), ('.rst', 1), ('.rest', 1), - ('.RST', 2), ('.REST', 2) -] - -MARKDOWN_EXTS = [ - ('.md', 1), ('.MD', 1), - ('.mkdn', 2), ('.MKDN', 2), - ('.mdown', 3), ('.MDOWN', 3), - ('.markdown', 4), ('.MARKDOWN', 4) -] - -PLAIN_EXTS = [ - ('.text', 2), ('.TEXT', 2), - ('.txt', 3), ('.TXT', 3) -] - -ALL_EXTS = MARKDOWN_EXTS + RST_EXTS + PLAIN_EXTS - DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" DATE_FORMAT = "%Y-%m-%d" diff --git a/rhodecode/config/environment.py b/rhodecode/config/environment.py --- a/rhodecode/config/environment.py +++ b/rhodecode/config/environment.py @@ -34,11 +34,16 @@ from pylons.configuration import PylonsC from pylons.error import handle_mako_error from pyramid.settings import asbool -# don't remove this import it does magic for celery -from rhodecode.lib import celerypylons # noqa +# ------------------------------------------------------------------------------ +# CELERY magic until refactor - issue #4163 - import order matters here: +from rhodecode.lib import celerypylons # this must be first, celerypylons + # sets config settings upon import -import rhodecode.lib.app_globals as app_globals +import rhodecode.integrations # any modules using celery task + # decorators should be added afterwards: +# ------------------------------------------------------------------------------ +from rhodecode.lib import app_globals from rhodecode.config import utils from rhodecode.config.routing import make_map from rhodecode.config.jsroutes import generate_jsroutes_content @@ -112,25 +117,12 @@ def load_environment(global_conf, app_co # sets the c attribute access when don't existing attribute are accessed config['pylons.strict_tmpl_context'] = True - # Limit backends to "vcs.backends" from configuration - backends = config['vcs.backends'] = aslist( - config.get('vcs.backends', 'hg,git'), sep=',') - for alias in rhodecode.BACKENDS.keys(): - if alias not in backends: - del rhodecode.BACKENDS[alias] - log.info("Enabled backends: %s", backends) - - # initialize vcs client and optionally run the server if enabled - vcs_server_uri = config.get('vcs.server', '') - vcs_server_enabled = str2bool(config.get('vcs.server.enable', 'true')) - start_server = ( - str2bool(config.get('vcs.start_server', 'false')) and - not int(os.environ.get('RC_VCSSERVER_TEST_DISABLE', '0'))) - if vcs_server_enabled and start_server: - log.info("Starting vcsserver") - start_vcs_server(server_and_port=vcs_server_uri, - protocol=utils.get_vcs_server_protocol(config), - log_level=config['vcs.server.log_level']) + # configure channelstream + config['channelstream_config'] = { + 'enabled': asbool(config.get('channelstream.enabled', False)), + 'server': config.get('channelstream.server'), + 'secret': config.get('channelstream.secret') + } set_available_permissions(config) db_cfg = make_db_config(clear_session=True) @@ -138,9 +130,6 @@ def load_environment(global_conf, app_co repos_path = list(db_cfg.items('paths'))[0][1] config['base_path'] = repos_path - config['vcs.hooks.direct_calls'] = _use_direct_hook_calls(config) - config['vcs.hooks.protocol'] = _get_vcs_hooks_protocol(config) - # store db config also in main global CONFIG set_rhodecode_config(config) @@ -153,34 +142,17 @@ def load_environment(global_conf, app_co # store config reference into our module to skip import magic of pylons rhodecode.CONFIG.update(config) - utils.configure_pyro4(config) - utils.configure_vcs(config) - if vcs_server_enabled: - connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(config)) - - import_on_startup = str2bool(config.get('startup.import_repos', False)) - if vcs_server_enabled and import_on_startup: - repo2db_mapper(ScmModel().repo_scan(repos_path), remove_obsolete=False) return config -def _use_direct_hook_calls(config): - default_direct_hook_calls = 'false' - direct_hook_calls = str2bool( - config.get('vcs.hooks.direct_calls', default_direct_hook_calls)) - return direct_hook_calls - - -def _get_vcs_hooks_protocol(config): - protocol = config.get('vcs.hooks.protocol', 'pyro4').lower() - return protocol - - def load_pyramid_environment(global_config, settings): # Some parts of the code expect a merge of global and app settings. settings_merged = global_config.copy() settings_merged.update(settings) + # Store the settings to make them available to other modules. + rhodecode.PYRAMID_SETTINGS = settings_merged + # If this is a test run we prepare the test environment like # creating a test database, test search index and test repositories. # This has to be done before the database connection is initialized. @@ -190,3 +162,27 @@ def load_pyramid_environment(global_conf # Initialize the database connection. utils.initialize_database(settings_merged) + + # Limit backends to `vcs.backends` from configuration + for alias in rhodecode.BACKENDS.keys(): + if alias not in settings['vcs.backends']: + del rhodecode.BACKENDS[alias] + log.info('Enabled VCS backends: %s', rhodecode.BACKENDS.keys()) + + # initialize vcs client and optionally run the server if enabled + vcs_server_uri = settings['vcs.server'] + vcs_server_enabled = settings['vcs.server.enable'] + start_server = ( + settings['vcs.start_server'] and + not int(os.environ.get('RC_VCSSERVER_TEST_DISABLE', '0'))) + + if vcs_server_enabled and start_server: + log.info("Starting vcsserver") + start_vcs_server(server_and_port=vcs_server_uri, + protocol=utils.get_vcs_server_protocol(settings), + log_level=settings['vcs.server.log_level']) + + utils.configure_pyro4(settings) + utils.configure_vcs(settings) + if vcs_server_enabled: + connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings)) diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -22,23 +22,25 @@ Pylons middleware initialization """ import logging +from collections import OrderedDict from paste.registry import RegistryManager from paste.gzipper import make_gzip_middleware from pylons.wsgiapp import PylonsApp from pyramid.authorization import ACLAuthorizationPolicy from pyramid.config import Configurator -from pyramid.static import static_view from pyramid.settings import asbool, aslist from pyramid.wsgi import wsgiapp -from pyramid.httpexceptions import HTTPError, HTTPInternalServerError +from pyramid.httpexceptions import HTTPError, HTTPInternalServerError, HTTPFound +from pyramid.events import ApplicationCreated import pyramid.httpexceptions as httpexceptions -from pyramid.renderers import render_to_response, render +from pyramid.renderers import render_to_response from routes.middleware import RoutesMiddleware import routes.util import rhodecode from rhodecode.config import patches +from rhodecode.config.routing import STATIC_FILE_PREFIX from rhodecode.config.environment import ( load_environment, load_pyramid_environment) from rhodecode.lib.middleware import csrf @@ -47,24 +49,45 @@ from rhodecode.lib.middleware.disable_vc from rhodecode.lib.middleware.https_fixup import HttpsFixup from rhodecode.lib.middleware.vcs import VCSMiddleware from rhodecode.lib.plugins.utils import register_rhodecode_plugin +from rhodecode.lib.utils2 import aslist as rhodecode_aslist +from rhodecode.subscribers import scan_repositories_if_enabled log = logging.getLogger(__name__) -def make_app(global_conf, full_stack=True, static_files=True, **app_conf): +# this is used to avoid avoid the route lookup overhead in routesmiddleware +# for certain routes which won't go to pylons to - eg. static files, debugger +# it is only needed for the pylons migration and can be removed once complete +class SkippableRoutesMiddleware(RoutesMiddleware): + """ Routes middleware that allows you to skip prefixes """ + + def __init__(self, *args, **kw): + self.skip_prefixes = kw.pop('skip_prefixes', []) + super(SkippableRoutesMiddleware, self).__init__(*args, **kw) + + def __call__(self, environ, start_response): + for prefix in self.skip_prefixes: + if environ['PATH_INFO'].startswith(prefix): + # added to avoid the case when a missing /_static route falls + # through to pylons and causes an exception as pylons is + # expecting wsgiorg.routingargs to be set in the environ + # by RoutesMiddleware. + if 'wsgiorg.routing_args' not in environ: + environ['wsgiorg.routing_args'] = (None, {}) + return self.app(environ, start_response) + + return super(SkippableRoutesMiddleware, self).__call__( + environ, start_response) + + +def make_app(global_conf, static_files=True, **app_conf): """Create a Pylons WSGI application and return it ``global_conf`` The inherited configuration for this application. Normally from the [DEFAULT] section of the Paste ini file. - ``full_stack`` - Whether or not this application provides a full WSGI stack (by - default, meaning it handles its own exceptions and errors). - Disable full_stack when this application is "managed" by - another WSGI middleware. - ``app_conf`` The application's local configuration. Normally specified in the [app:<name>] section of the Paste ini file (where <name> @@ -89,16 +112,6 @@ def make_app(global_conf, full_stack=Tru app = csrf.OriginChecker(app, expected_origin, skip_urls=[routes.util.url_for('api')]) - - if asbool(full_stack): - - # Appenlight monitoring and error handler - app, appenlight_client = wrap_in_appenlight_if_enabled(app, config) - - # we want our low level middleware to get to the request ASAP. We don't - # need any pylons stack middleware in them - app = VCSMiddleware(app, config, appenlight_client) - # Establish the Registry for this application app = RegistryManager(app) @@ -140,13 +153,78 @@ def make_pyramid_app(global_config, **se load_pyramid_environment(global_config, settings) + includeme_first(config) includeme(config) - includeme_last(config) pyramid_app = config.make_wsgi_app() pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config) + pyramid_app.config = config return pyramid_app +def make_not_found_view(config): + """ + This creates the view which should be registered as not-found-view to + pyramid. Basically it contains of the old pylons app, converted to a view. + Additionally it is wrapped by some other middlewares. + """ + settings = config.registry.settings + vcs_server_enabled = settings['vcs.server.enable'] + + # Make pylons app from unprepared settings. + pylons_app = make_app( + config.registry._pylons_compat_global_config, + **config.registry._pylons_compat_settings) + config.registry._pylons_compat_config = pylons_app.config + + # Appenlight monitoring. + pylons_app, appenlight_client = wrap_in_appenlight_if_enabled( + pylons_app, settings) + + # The VCSMiddleware shall operate like a fallback if pyramid doesn't find + # a view to handle the request. Therefore we wrap it around the pylons app. + if vcs_server_enabled: + pylons_app = VCSMiddleware( + pylons_app, settings, appenlight_client, registry=config.registry) + + pylons_app_as_view = wsgiapp(pylons_app) + + # Protect from VCS Server error related pages when server is not available + if not vcs_server_enabled: + pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view) + + def pylons_app_with_error_handler(context, request): + """ + Handle exceptions from rc pylons app: + + - old webob type exceptions get converted to pyramid exceptions + - pyramid exceptions are passed to the error handler view + """ + def is_vcs_response(response): + return 'X-RhodeCode-Backend' in response.headers + + def is_http_error(response): + # webob type error responses + return (400 <= response.status_int <= 599) + + def is_error_handling_needed(response): + return is_http_error(response) and not is_vcs_response(response) + + try: + response = pylons_app_as_view(context, request) + if is_error_handling_needed(response): + response = webob_to_pyramid_http_response(response) + return error_handler(response, request) + except HTTPError as e: # pyramid type exceptions + return error_handler(e, request) + except Exception: + if settings.get('debugtoolbar.enabled', False): + raise + return error_handler(HTTPInternalServerError(), request) + return response + + return pylons_app_with_error_handler + + def add_pylons_compat_data(registry, global_config, settings): """ Attach data to the registry to support the Pylons integration. @@ -205,20 +283,32 @@ def error_handler(exception, request): def includeme(config): settings = config.registry.settings + # plugin information + config.registry.rhodecode_plugins = OrderedDict() + + config.add_directive( + 'register_rhodecode_plugin', register_rhodecode_plugin) + if asbool(settings.get('appenlight', 'false')): config.include('appenlight_client.ext.pyramid_tween') # Includes which are required. The application would fail without them. config.include('pyramid_mako') config.include('pyramid_beaker') + config.include('rhodecode.channelstream') config.include('rhodecode.admin') config.include('rhodecode.authentication') + config.include('rhodecode.integrations') config.include('rhodecode.login') config.include('rhodecode.tweens') config.include('rhodecode.api') + config.include('rhodecode.svn_support') config.add_route( 'rhodecode_support', 'https://rhodecode.com/help/', static=True) + # Add subscribers. + config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated) + # Set the authorization policy. authz_policy = ACLAuthorizationPolicy() config.set_authorization_policy(authz_policy) @@ -226,86 +316,37 @@ def includeme(config): # Set the default renderer for HTML templates to mako. config.add_mako_renderer('.html') - # plugin information - config.registry.rhodecode_plugins = {} - - config.add_directive( - 'register_rhodecode_plugin', register_rhodecode_plugin) # include RhodeCode plugins includes = aslist(settings.get('rhodecode.includes', [])) for inc in includes: config.include(inc) - pylons_app = make_app( - config.registry._pylons_compat_global_config, - **config.registry._pylons_compat_settings) - config.registry._pylons_compat_config = pylons_app.config - - pylons_app_as_view = wsgiapp(pylons_app) - - # Protect from VCS Server error related pages when server is not available - vcs_server_enabled = asbool(settings.get('vcs.server.enable', 'true')) - if not vcs_server_enabled: - pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view) - - - def pylons_app_with_error_handler(context, request): - """ - Handle exceptions from rc pylons app: - - - old webob type exceptions get converted to pyramid exceptions - - pyramid exceptions are passed to the error handler view - """ - try: - response = pylons_app_as_view(context, request) - if 400 <= response.status_int <= 599: # webob type error responses - return error_handler( - webob_to_pyramid_http_response(response), request) - except HTTPError as e: # pyramid type exceptions - return error_handler(e, request) - except Exception: - if settings.get('debugtoolbar.enabled', False): - raise - return error_handler(HTTPInternalServerError(), request) - return response - # This is the glue which allows us to migrate in chunks. By registering the # pylons based application as the "Not Found" view in Pyramid, we will # fallback to the old application each time the new one does not yet know # how to handle a request. - config.add_notfound_view(pylons_app_with_error_handler) + config.add_notfound_view(make_not_found_view(config)) - if settings.get('debugtoolbar.enabled', False): - # if toolbar, then only http type exceptions get caught and rendered - ExcClass = HTTPError - else: + if not settings.get('debugtoolbar.enabled', False): # if no toolbar, then any exception gets caught and rendered - ExcClass = Exception - config.add_view(error_handler, context=ExcClass) + config.add_view(error_handler, context=Exception) + + config.add_view(error_handler, context=HTTPError) -def includeme_last(config): - """ - The static file catchall needs to be last in the view configuration. - """ - settings = config.registry.settings +def includeme_first(config): + # redirect automatic browser favicon.ico requests to correct place + def favicon_redirect(context, request): + return HTTPFound( + request.static_path('rhodecode:public/images/favicon.ico')) - # Note: johbo: I would prefer to register a prefix for static files at some - # point, e.g. move them under '_static/'. This would fully avoid that we - # can have name clashes with a repository name. Imaging someone calling his - # repo "css" ;-) Also having an external web server to serve out the static - # files seems to be easier to set up if they have a common prefix. - # - # Example: config.add_static_view('_static', path='rhodecode:public') - # - # It might be an option to register both paths for a while and then migrate - # over to the new location. + config.add_view(favicon_redirect, route_name='favicon') + config.add_route('favicon', '/favicon.ico') - # Serving static files with a catchall. - if settings['static_files']: - config.add_route('catchall_static', '/*subpath') - config.add_view( - static_view('rhodecode:public'), route_name='catchall_static') + config.add_static_view( + '_static/deform', 'deform:static') + config.add_static_view( + '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24) def wrap_app_in_wsgi_middlewares(pyramid_app, config): @@ -322,19 +363,14 @@ def wrap_app_in_wsgi_middlewares(pyramid pyramid_app = HttpsFixup(pyramid_app, settings) # Add RoutesMiddleware to support the pylons compatibility tween during - # migration to pyramid. - pyramid_app = RoutesMiddleware( - pyramid_app, config.registry._pylons_compat_config['routes.map']) + pyramid_app = SkippableRoutesMiddleware( + pyramid_app, config.registry._pylons_compat_config['routes.map'], + skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar')) - if asbool(settings.get('appenlight', 'false')): - pyramid_app, _ = wrap_in_appenlight_if_enabled( - pyramid_app, config.registry._pylons_compat_config) + pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings) - # TODO: johbo: Don't really see why we enable the gzip middleware when - # serving static files, might be something that should have its own setting - # as well? - if settings['static_files']: + if settings['gzip_responses']: pyramid_app = make_gzip_middleware( pyramid_app, settings, compress_level=1) @@ -376,12 +412,63 @@ def sanitize_settings_and_apply_defaults # should allow to pass in a prefix. settings.setdefault('rhodecode.api.url', '/_admin/api') - _bool_setting(settings, 'vcs.server.enable', 'true') - _bool_setting(settings, 'static_files', 'true') + # Sanitize generic settings. + _list_setting(settings, 'default_encoding', 'UTF-8') _bool_setting(settings, 'is_test', 'false') + _bool_setting(settings, 'gzip_responses', 'false') + + # Call split out functions that sanitize settings for each topic. + _sanitize_appenlight_settings(settings) + _sanitize_vcs_settings(settings) return settings +def _sanitize_appenlight_settings(settings): + _bool_setting(settings, 'appenlight', 'false') + + +def _sanitize_vcs_settings(settings): + """ + Applies settings defaults and does type conversion for all VCS related + settings. + """ + _string_setting(settings, 'vcs.svn.compatible_version', '') + _string_setting(settings, 'git_rev_filter', '--all') + _string_setting(settings, 'vcs.hooks.protocol', 'pyro4') + _string_setting(settings, 'vcs.server', '') + _string_setting(settings, 'vcs.server.log_level', 'debug') + _string_setting(settings, 'vcs.server.protocol', 'pyro4') + _bool_setting(settings, 'startup.import_repos', 'false') + _bool_setting(settings, 'vcs.hooks.direct_calls', 'false') + _bool_setting(settings, 'vcs.server.enable', 'true') + _bool_setting(settings, 'vcs.start_server', 'false') + _list_setting(settings, 'vcs.backends', 'hg, git, svn') + _int_setting(settings, 'vcs.connection_timeout', 3600) + + +def _int_setting(settings, name, default): + settings[name] = int(settings.get(name, default)) + + def _bool_setting(settings, name, default): - settings[name] = asbool(settings.get(name, default)) + input = settings.get(name, default) + if isinstance(input, unicode): + input = input.encode('utf8') + settings[name] = asbool(input) + + +def _list_setting(settings, name, default): + raw_value = settings.get(name, default) + + old_separator = ',' + if old_separator in raw_value: + # If we get a comma separated list, pass it to our own function. + settings[name] = rhodecode_aslist(raw_value, sep=old_separator) + else: + # Otherwise we assume it uses pyramids space/newline separation. + settings[name] = aslist(raw_value) + + +def _string_setting(settings, name, default): + settings[name] = settings.get(name, default).lower() diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -36,6 +36,7 @@ from rhodecode.config import routing_lin # prefix for non repository related links needs to be prefixed with `/` ADMIN_PREFIX = '/_admin' +STATIC_FILE_PREFIX = '/_static' # Default requirements for URL parts URL_NAME_REQUIREMENTS = { @@ -51,6 +52,19 @@ URL_NAME_REQUIREMENTS = { } +def add_route_requirements(route_path, requirements): + """ + Adds regex requirements to pyramid routes using a mapping dict + + >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'}) + '/{action}/{id:\d+}' + + """ + for key, regex in requirements.items(): + route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex)) + return route_path + + class JSRoutesMapper(Mapper): """ Wrapper for routes.Mapper to make pyroutes compatible url definitions @@ -546,6 +560,13 @@ def make_map(config): action='my_account_auth_tokens_add', conditions={'method': ['POST']}) m.connect('my_account_auth_tokens', '/my_account/auth_tokens', action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']}) + m.connect('my_account_notifications', '/my_account/notifications', + action='my_notifications', + conditions={'method': ['GET']}) + m.connect('my_account_notifications_toggle_visibility', + '/my_account/toggle_visibility', + action='my_notifications_toggle_visibility', + conditions={'method': ['POST']}) # NOTIFICATION REST ROUTES with rmap.submapper(path_prefix=ADMIN_PREFIX, @@ -554,7 +575,6 @@ def make_map(config): action='index', conditions={'method': ['GET']}) m.connect('notifications_mark_all_read', '/notifications/mark_all_read', action='mark_all_read', conditions={'method': ['POST']}) - m.connect('/notifications/{notification_id}', action='update', conditions={'method': ['PUT']}) m.connect('/notifications/{notification_id}', @@ -850,7 +870,7 @@ def make_map(config): conditions={'function': check_repo, 'method': ['DELETE']}, requirements=URL_NAME_REQUIREMENTS, jsroute=True) - rmap.connect('changeset_info', '/changeset_info/{repo_name}/{revision}', + rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}', controller='changeset', action='changeset_info', requirements=URL_NAME_REQUIREMENTS, jsroute=True) @@ -1090,9 +1110,9 @@ def make_map(config): conditions={'function': check_repo}, requirements=URL_NAME_REQUIREMENTS, jsroute=True) - rmap.connect('files_metadata_list_home', - '/{repo_name}/metadata_list/{revision}/{f_path}', - controller='files', action='metadata_list', + rmap.connect('files_nodetree_full', + '/{repo_name}/nodetree_full/{commit_id}/{f_path}', + controller='files', action='nodetree_full', conditions={'function': check_repo}, requirements=URL_NAME_REQUIREMENTS, jsroute=True) diff --git a/rhodecode/config/utils.py b/rhodecode/config/utils.py --- a/rhodecode/config/utils.py +++ b/rhodecode/config/utils.py @@ -49,22 +49,18 @@ def configure_vcs(config): Patch VCS config with some RhodeCode specific stuff """ from rhodecode.lib.vcs import conf - from rhodecode.lib.utils2 import aslist conf.settings.BACKENDS = { 'hg': 'rhodecode.lib.vcs.backends.hg.MercurialRepository', 'git': 'rhodecode.lib.vcs.backends.git.GitRepository', 'svn': 'rhodecode.lib.vcs.backends.svn.SubversionRepository', } - conf.settings.HG_USE_REBASE_FOR_MERGING = config.get( - 'rhodecode_hg_use_rebase_for_merging', False) - conf.settings.GIT_REV_FILTER = shlex.split( - config.get('git_rev_filter', '--all').strip()) - conf.settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding', - 'UTF-8'), sep=',') - conf.settings.ALIASES[:] = config.get('vcs.backends') - conf.settings.SVN_COMPATIBLE_VERSION = config.get( - 'vcs.svn.compatible_version') + conf.settings.HOOKS_PROTOCOL = config['vcs.hooks.protocol'] + conf.settings.HOOKS_DIRECT_CALLS = config['vcs.hooks.direct_calls'] + conf.settings.GIT_REV_FILTER = shlex.split(config['git_rev_filter']) + conf.settings.DEFAULT_ENCODINGS = config['default_encoding'] + conf.settings.ALIASES[:] = config['vcs.backends'] + conf.settings.SVN_COMPATIBLE_VERSION = config['vcs.svn.compatible_version'] def initialize_database(config): @@ -90,8 +86,7 @@ def initialize_test_environment(settings def get_vcs_server_protocol(config): - protocol = config.get('vcs.server.protocol', 'pyro4') - return protocol + return config['vcs.server.protocol'] def set_instance_id(config): diff --git a/rhodecode/controllers/admin/gists.py b/rhodecode/controllers/admin/gists.py --- a/rhodecode/controllers/admin/gists.py +++ b/rhodecode/controllers/admin/gists.py @@ -25,15 +25,18 @@ gist controller for RhodeCode import time import logging -import traceback + import formencode +import peppercorn from formencode import htmlfill from pylons import request, response, tmpl_context as c, url from pylons.controllers.util import abort, redirect from pylons.i18n.translation import _ +from webob.exc import HTTPNotFound, HTTPForbidden +from sqlalchemy.sql.expression import or_ -from rhodecode.model.forms import GistForm + from rhodecode.model.gist import GistModel from rhodecode.model.meta import Session from rhodecode.model.db import Gist, User @@ -44,9 +47,10 @@ from rhodecode.lib.auth import LoginRequ from rhodecode.lib.utils import jsonify from rhodecode.lib.utils2 import safe_str, safe_int, time_to_datetime from rhodecode.lib.ext_json import json -from webob.exc import HTTPNotFound, HTTPForbidden -from sqlalchemy.sql.expression import or_ from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError +from rhodecode.model import validation_schema +from rhodecode.model.validation_schema.schemas import gist_schema + log = logging.getLogger(__name__) @@ -56,11 +60,11 @@ class GistsController(BaseController): def __load_defaults(self, extra_values=None): c.lifetime_values = [ - (str(-1), _('forever')), - (str(5), _('5 minutes')), - (str(60), _('1 hour')), - (str(60 * 24), _('1 day')), - (str(60 * 24 * 30), _('1 month')), + (-1, _('forever')), + (5, _('5 minutes')), + (60, _('1 hour')), + (60 * 24, _('1 day')), + (60 * 24 * 30, _('1 month')), ] if extra_values: c.lifetime_values.append(extra_values) @@ -136,40 +140,56 @@ class GistsController(BaseController): """POST /admin/gists: Create a new item""" # url('gists') self.__load_defaults() - gist_form = GistForm([x[0] for x in c.lifetime_values], - [x[0] for x in c.acl_options])() + + data = dict(request.POST) + data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME + data['nodes'] = [{ + 'filename': data['filename'], + 'content': data.get('content'), + 'mimetype': data.get('mimetype') # None is autodetect + }] + + data['gist_type'] = ( + Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE) + data['gist_acl_level'] = ( + data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE) + + schema = gist_schema.GistSchema().bind( + lifetime_options=[x[0] for x in c.lifetime_values]) + try: - form_result = gist_form.to_python(dict(request.POST)) - # TODO: multiple files support, from the form - filename = form_result['filename'] or Gist.DEFAULT_FILENAME - nodes = { - filename: { - 'content': form_result['content'], - 'lexer': form_result['mimetype'] # None is autodetect - } - } - _public = form_result['public'] - gist_type = Gist.GIST_PUBLIC if _public else Gist.GIST_PRIVATE - gist_acl_level = form_result.get( - 'acl_level', Gist.ACL_LEVEL_PRIVATE) + + schema_data = schema.deserialize(data) + # convert to safer format with just KEYs so we sure no duplicates + schema_data['nodes'] = gist_schema.sequence_to_nodes( + schema_data['nodes']) + gist = GistModel().create( - description=form_result['description'], + gist_id=schema_data['gistid'], # custom access id not real ID + description=schema_data['description'], owner=c.rhodecode_user.user_id, - gist_mapping=nodes, - gist_type=gist_type, - lifetime=form_result['lifetime'], - gist_id=form_result['gistid'], - gist_acl_level=gist_acl_level + gist_mapping=schema_data['nodes'], + gist_type=schema_data['gist_type'], + lifetime=schema_data['lifetime'], + gist_acl_level=schema_data['gist_acl_level'] ) Session().commit() new_gist_id = gist.gist_access_id - except formencode.Invalid as errors: - defaults = errors.value + except validation_schema.Invalid as errors: + defaults = data + errors = errors.asdict() + + if 'nodes.0.content' in errors: + errors['content'] = errors['nodes.0.content'] + del errors['nodes.0.content'] + if 'nodes.0.filename' in errors: + errors['filename'] = errors['nodes.0.filename'] + del errors['nodes.0.filename'] return formencode.htmlfill.render( render('admin/gists/new.html'), defaults=defaults, - errors=errors.error_dict or {}, + errors=errors, prefix_error=False, encoding="UTF-8", force_defaults=False @@ -243,7 +263,8 @@ class GistsController(BaseController): log.exception("Exception in gist show") raise HTTPNotFound() if format == 'raw': - content = '\n\n'.join([f.content for f in c.files if (f_path is None or f.path == f_path)]) + content = '\n\n'.join([f.content for f in c.files + if (f_path is None or f.path == f_path)]) response.content_type = 'text/plain' return content return render('admin/gists/show.html') @@ -252,32 +273,35 @@ class GistsController(BaseController): @NotAnonymous() @auth.CSRFRequired() def edit(self, gist_id): + self.__load_defaults() self._add_gist_to_context(gist_id) owner = c.gist.gist_owner == c.rhodecode_user.user_id if not (h.HasPermissionAny('hg.admin')() or owner): raise HTTPForbidden() - rpost = request.POST - nodes = {} - _file_data = zip(rpost.getall('org_files'), rpost.getall('files'), - rpost.getall('mimetypes'), rpost.getall('contents')) - for org_filename, filename, mimetype, content in _file_data: - nodes[org_filename] = { - 'org_filename': org_filename, - 'filename': filename, - 'content': content, - 'lexer': mimetype, - } + data = peppercorn.parse(request.POST.items()) + + schema = gist_schema.GistSchema() + schema = schema.bind( + # '0' is special value to leave lifetime untouched + lifetime_options=[x[0] for x in c.lifetime_values] + [0], + ) + try: + schema_data = schema.deserialize(data) + # convert to safer format with just KEYs so we sure no duplicates + schema_data['nodes'] = gist_schema.sequence_to_nodes( + schema_data['nodes']) + GistModel().update( gist=c.gist, - description=rpost['description'], + description=schema_data['description'], owner=c.gist.owner, - gist_mapping=nodes, - gist_type=c.gist.gist_type, - lifetime=rpost['lifetime'], - gist_acl_level=rpost['acl_level'] + gist_mapping=schema_data['nodes'], + gist_type=schema_data['gist_type'], + lifetime=schema_data['lifetime'], + gist_acl_level=schema_data['gist_acl_level'] ) Session().commit() @@ -287,6 +311,10 @@ class GistsController(BaseController): # store only DB stuff for gist Session().commit() h.flash(_('Successfully updated gist data'), category='success') + except validation_schema.Invalid as errors: + errors = errors.asdict() + h.flash(_('Error occurred during update of gist {}: {}').format( + gist_id, errors), category='error') except Exception: log.exception("Exception in gist edit") h.flash(_('Error occurred during update of gist %s') % gist_id, @@ -317,7 +345,7 @@ class GistsController(BaseController): # this cannot use timeago, since it's used in select2 as a value expiry = h.age(h.time_to_datetime(c.gist.gist_expires)) self.__load_defaults( - extra_values=('0', _('%(expiry)s - current value') % {'expiry': expiry})) + extra_values=(0, _('%(expiry)s - current value') % {'expiry': expiry})) return render('admin/gists/edit.html') @LoginRequired() diff --git a/rhodecode/controllers/admin/my_account.py b/rhodecode/controllers/admin/my_account.py --- a/rhodecode/controllers/admin/my_account.py +++ b/rhodecode/controllers/admin/my_account.py @@ -346,3 +346,17 @@ class MyAccountController(BaseController h.flash(_("Auth token successfully deleted"), category='success') return redirect(url('my_account_auth_tokens')) + + def my_notifications(self): + c.active = 'notifications' + return render('admin/my_account/my_account.html') + + @auth.CSRFRequired() + def my_notifications_toggle_visibility(self): + user = c.rhodecode_user.get_instance() + user_data = user.user_data + status = user_data.get('notification_status', False) + user_data['notification_status'] = not status + user.user_data = user_data + Session().commit() + return redirect(url('my_account_notifications')) diff --git a/rhodecode/controllers/admin/notifications.py b/rhodecode/controllers/admin/notifications.py --- a/rhodecode/controllers/admin/notifications.py +++ b/rhodecode/controllers/admin/notifications.py @@ -86,6 +86,7 @@ class NotificationsController(BaseContro return render('admin/notifications/notifications.html') + @auth.CSRFRequired() def mark_all_read(self): if request.is_xhr: diff --git a/rhodecode/controllers/admin/repos.py b/rhodecode/controllers/admin/repos.py --- a/rhodecode/controllers/admin/repos.py +++ b/rhodecode/controllers/admin/repos.py @@ -33,6 +33,7 @@ from pylons.controllers.util import redi from pylons.i18n.translation import _ from webob.exc import HTTPForbidden, HTTPNotFound, HTTPBadRequest +import rhodecode from rhodecode.lib import auth, helpers as h from rhodecode.lib.auth import ( LoginRequired, HasPermissionAllDecorator, @@ -42,7 +43,7 @@ from rhodecode.lib.base import BaseRepoC from rhodecode.lib.ext_json import json from rhodecode.lib.exceptions import AttachedForksError from rhodecode.lib.utils import action_logger, repo_name_slug, jsonify -from rhodecode.lib.utils2 import safe_int +from rhodecode.lib.utils2 import safe_int, str2bool from rhodecode.lib.vcs import RepositoryError from rhodecode.model.db import ( User, Repository, UserFollowing, RepoGroup, RepositoryField) @@ -779,6 +780,8 @@ class ReposController(BaseRepoController c.repo_info = self._load_repo(repo_name) defaults = self._vcs_form_defaults(repo_name) c.inherit_global_settings = defaults['inherit_global_settings'] + c.labs_active = str2bool( + rhodecode.CONFIG.get('labs_settings_active', 'true')) return htmlfill.render( render('admin/repos/repo_edit.html'), diff --git a/rhodecode/controllers/admin/settings.py b/rhodecode/controllers/admin/settings.py --- a/rhodecode/controllers/admin/settings.py +++ b/rhodecode/controllers/admin/settings.py @@ -79,7 +79,7 @@ class SettingsController(BaseController) def __before__(self): super(SettingsController, self).__before__() c.labs_active = str2bool( - rhodecode.CONFIG.get('labs_settings_active', 'false')) + rhodecode.CONFIG.get('labs_settings_active', 'true')) c.navlist = navigation_list(request) def _get_hg_ui_settings(self): @@ -790,13 +790,6 @@ LabSetting = collections.namedtuple( # rhodecode.model.forms.LabsSettingsForm. _LAB_SETTINGS = [ LabSetting( - key='rhodecode_hg_use_rebase_for_merging', - type='bool', - group=lazy_ugettext('Mercurial server-side merge'), - label=lazy_ugettext('Use rebase instead of creating a merge commit when merging via web interface'), - help='' # Do not translate the empty string! - ), - LabSetting( key='rhodecode_proxy_subversion_http_requests', type='bool', group=lazy_ugettext('Subversion HTTP Support'), diff --git a/rhodecode/controllers/admin/users.py b/rhodecode/controllers/admin/users.py --- a/rhodecode/controllers/admin/users.py +++ b/rhodecode/controllers/admin/users.py @@ -83,9 +83,6 @@ class UsersController(BaseController): from rhodecode.lib.utils import PartialRenderer _render = PartialRenderer('data_table/_dt_elements.html') - def grav_tmpl(user_email, size): - return _render("user_gravatar", user_email, size) - def username(user_id, username): return _render("user_name", user_id, username) @@ -100,9 +97,7 @@ class UsersController(BaseController): users_data = [] for user in c.users_list: users_data.append({ - "gravatar": grav_tmpl(user.email, 20), - "username": h.link_to( - user.username, h.url('user_profile', username=user.username)), + "username": h.gravatar_with_user(user.username), "username_raw": user.username, "email": user.email, "first_name": h.escape(user.name), diff --git a/rhodecode/controllers/changeset.py b/rhodecode/controllers/changeset.py --- a/rhodecode/controllers/changeset.py +++ b/rhodecode/controllers/changeset.py @@ -351,7 +351,8 @@ class ChangesetController(BaseRepoContro f_path=request.POST.get('f_path'), line_no=request.POST.get('line'), status_change=(ChangesetStatus.get_status_lbl(status) - if status else None) + if status else None), + status_change_type=status ) # get status if set ! if status: diff --git a/rhodecode/controllers/files.py b/rhodecode/controllers/files.py --- a/rhodecode/controllers/files.py +++ b/rhodecode/controllers/files.py @@ -136,11 +136,13 @@ class FilesController(BaseRepoController _namespace = caches.get_repo_namespace_key(namespace_type, repo_name) return caches.get_cache_manager('repo_cache_long', _namespace) - def _get_tree_at_commit(self, repo_name, commit_id, f_path): + def _get_tree_at_commit(self, repo_name, commit_id, f_path, + full_load=False, force=False): def _cached_tree(): log.debug('Generating cached file tree for %s, %s, %s', repo_name, commit_id, f_path) - return render('files/files_browser.html') + c.full_load = full_load + return render('files/files_browser_tree.html') cache_manager = self.__get_tree_cache_manager( repo_name, caches.FILE_TREE) @@ -148,6 +150,10 @@ class FilesController(BaseRepoController cache_key = caches.compute_key_from_params( repo_name, commit_id, f_path) + if force: + # we want to force recompute of caches + cache_manager.remove_value(cache_key) + return cache_manager.get(cache_key, createfunc=_cached_tree) def _get_nodelist_at_commit(self, repo_name, commit_id, f_path): @@ -165,22 +171,6 @@ class FilesController(BaseRepoController repo_name, commit_id, f_path) return cache_manager.get(cache_key, createfunc=_cached_nodes) - def _get_metadata_at_commit(self, repo_name, commit, dir_node): - def _cached_metadata(): - log.debug('Generating cached metadata for %s, %s, %s', - repo_name, commit.raw_id, safe_str(dir_node.path)) - - data = ScmModel().get_dirnode_metadata(commit, dir_node) - return data - - cache_manager = self.__get_tree_cache_manager( - repo_name, caches.FILE_TREE_META) - - cache_key = caches.compute_key_from_params( - repo_name, commit.raw_id, safe_str(dir_node.path)) - - return cache_manager.get(cache_key, createfunc=_cached_metadata) - @LoginRequired() @HasRepoPermissionAnyDecorator( 'repository.read', 'repository.write', 'repository.admin') @@ -246,6 +236,7 @@ class FilesController(BaseRepoController c.authors = [] c.file_tree = self._get_tree_at_commit( repo_name, c.commit.raw_id, f_path) + except RepositoryError as e: h.flash(safe_str(e), category='error') raise HTTPNotFound() @@ -1092,23 +1083,32 @@ class FilesController(BaseRepoController @XHRRequired() @HasRepoPermissionAnyDecorator( 'repository.read', 'repository.write', 'repository.admin') - @jsonify - def metadata_list(self, repo_name, revision, f_path): + def nodetree_full(self, repo_name, commit_id, f_path): """ - Returns a json dict that contains commit date, author, revision - and id for the specified repo, revision and file path + Returns rendered html of file tree that contains commit date, + author, revision for the specified combination of + repo, commit_id and file path :param repo_name: name of the repository - :param revision: revision of files + :param commit_id: commit_id of file tree :param f_path: file path of the requested directory """ - commit = self.__get_commit_or_redirect(revision, repo_name) + commit = self.__get_commit_or_redirect(commit_id, repo_name) try: - file_node = commit.get_node(f_path) + dir_node = commit.get_node(f_path) except RepositoryError as e: - return {'error': safe_str(e)} + return 'error {}'.format(safe_str(e)) + + if dir_node.is_file(): + return '' - metadata = self._get_metadata_at_commit( - repo_name, commit, file_node) - return {'metadata': metadata} + c.file = dir_node + c.commit = commit + + # using force=True here, make a little trick. We flush the cache and + # compute it using the same key as without full_load, so the fully + # loaded cached tree is now returned instead of partial + return self._get_tree_at_commit( + repo_name, commit.raw_id, dir_node.path, full_load=True, + force=True) diff --git a/rhodecode/controllers/journal.py b/rhodecode/controllers/journal.py --- a/rhodecode/controllers/journal.py +++ b/rhodecode/controllers/journal.py @@ -244,7 +244,7 @@ class JournalController(BaseController): try: self.scm_model.toggle_following_user( user_id, c.rhodecode_user.user_id) - Session.commit() + Session().commit() return 'ok' except Exception: raise HTTPBadRequest() @@ -254,7 +254,7 @@ class JournalController(BaseController): try: self.scm_model.toggle_following_repo( repo_id, c.rhodecode_user.user_id) - Session.commit() + Session().commit() return 'ok' except Exception: raise HTTPBadRequest() diff --git a/rhodecode/controllers/pullrequests.py b/rhodecode/controllers/pullrequests.py --- a/rhodecode/controllers/pullrequests.py +++ b/rhodecode/controllers/pullrequests.py @@ -32,6 +32,7 @@ from pylons.i18n.translation import _ from sqlalchemy.sql import func from sqlalchemy.sql.expression import or_ +from rhodecode import events from rhodecode.lib import auth, diffs, helpers as h from rhodecode.lib.ext_json import json from rhodecode.lib.base import ( @@ -640,6 +641,9 @@ class PullrequestsController(BaseRepoCon pull_request_id = safe_int(pull_request_id) c.pull_request = PullRequest.get_or_404(pull_request_id) + c.template_context['pull_request_data']['pull_request_id'] = \ + pull_request_id + # pull_requests repo_name we opened it against # ie. target_repo must match if repo_name != c.pull_request.target_repo.repo_name: @@ -758,9 +762,13 @@ class PullrequestsController(BaseRepoCon line_no=request.POST.get('line'), status_change=(ChangesetStatus.get_status_lbl(status) if status and allowed_to_change_status else None), + status_change_type=(status + if status and allowed_to_change_status else None), closing_pr=close_pr ) + + if allowed_to_change_status: old_calculated_status = pull_request.calculated_review_status() # get status if set ! @@ -774,6 +782,7 @@ class PullrequestsController(BaseRepoCon ) Session().flush() + events.trigger(events.PullRequestCommentEvent(pull_request, comm)) # we now calculate the status of pull request, and based on that # calculation we set the commits status calculated_status = pull_request.calculated_review_status() diff --git a/rhodecode/controllers/search.py b/rhodecode/controllers/search.py --- a/rhodecode/controllers/search.py +++ b/rhodecode/controllers/search.py @@ -35,6 +35,7 @@ from rhodecode.lib.helpers import Page from rhodecode.lib.utils2 import safe_str, safe_int from rhodecode.lib.index import searcher_from_config from rhodecode.model import validation_schema +from rhodecode.model.validation_schema.schemas import search_schema log = logging.getLogger(__name__) @@ -48,7 +49,7 @@ class SearchController(BaseRepoControlle formatted_results = [] execution_time = '' - schema = validation_schema.SearchParamsSchema() + schema = search_schema.SearchParamsSchema() search_params = {} errors = [] @@ -75,7 +76,6 @@ class SearchController(BaseRepoControlle page_limit = search_params['page_limit'] requested_page = search_params['requested_page'] - c.perm_user = AuthUser(user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr) diff --git a/rhodecode/controllers/summary.py b/rhodecode/controllers/summary.py --- a/rhodecode/controllers/summary.py +++ b/rhodecode/controllers/summary.py @@ -24,14 +24,12 @@ Summary controller for RhodeCode Enterpr import logging from string import lower -from itertools import product from pylons import tmpl_context as c, request from pylons.i18n.translation import _ from beaker.cache import cache_region, region_invalidate -from rhodecode.config.conf import ( - ALL_READMES, ALL_EXTS, LANGUAGES_EXTENSIONS_MAP) +from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP) from rhodecode.controllers import utils from rhodecode.controllers.changelog import _load_changelog_summary from rhodecode.lib import caches, helpers as h @@ -49,10 +47,6 @@ from rhodecode.model.db import Statistic log = logging.getLogger(__name__) -README_FILES = [''.join([x[0][0], x[1][0]]) - for x in sorted(list(product(ALL_READMES, ALL_EXTS)), - key=lambda y:y[0][1] + y[1][1])] - class SummaryController(BaseRepoController): @@ -62,6 +56,7 @@ class SummaryController(BaseRepoControll def __get_readme_data(self, db_repo): repo_name = db_repo.repo_name log.debug('Looking for README file') + default_renderer = c.visual.default_renderer @cache_region('long_term') def _generate_readme(cache_key): @@ -73,7 +68,7 @@ class SummaryController(BaseRepoControll if isinstance(commit, EmptyCommit): raise EmptyRepositoryError() renderer = MarkupRenderer() - for f in README_FILES: + for f in renderer.pick_readme_order(default_renderer): try: node = commit.get_node(f) except NodeDoesNotExistError: @@ -241,7 +236,7 @@ class SummaryController(BaseRepoControll (_("Tag"), repo.tags, 'tag'), (_("Bookmark"), repo.bookmarks, 'book'), ] - res = self._create_reference_data(repo, refs_to_create) + res = self._create_reference_data(repo, repo_name, refs_to_create) data = { 'more': False, 'results': res @@ -258,14 +253,14 @@ class SummaryController(BaseRepoControll # TODO: enable when vcs can handle bookmarks filters # (_("Bookmarks"), repo.bookmarks, "book"), ] - res = self._create_reference_data(repo, refs_to_create) + res = self._create_reference_data(repo, repo_name, refs_to_create) data = { 'more': False, 'results': res } return data - def _create_reference_data(self, repo, refs_to_create): + def _create_reference_data(self, repo, full_repo_name, refs_to_create): format_ref_id = utils.get_format_ref_id(repo) result = [] @@ -274,28 +269,32 @@ class SummaryController(BaseRepoControll result.append({ 'text': title, 'children': self._create_reference_items( - repo, refs, ref_type, format_ref_id), + repo, full_repo_name, refs, ref_type, format_ref_id), }) return result - def _create_reference_items(self, repo, refs, ref_type, format_ref_id): + def _create_reference_items(self, repo, full_repo_name, refs, ref_type, + format_ref_id): result = [] is_svn = h.is_svn(repo) - for name, raw_id in refs.iteritems(): + for ref_name, raw_id in refs.iteritems(): + files_url = self._create_files_url( + repo, full_repo_name, ref_name, raw_id, is_svn) result.append({ - 'text': name, - 'id': format_ref_id(name, raw_id), + 'text': ref_name, + 'id': format_ref_id(ref_name, raw_id), 'raw_id': raw_id, 'type': ref_type, - 'files_url': self._create_files_url(repo, name, raw_id, is_svn) + 'files_url': files_url, }) return result - def _create_files_url(self, repo, name, raw_id, is_svn): - use_commit_id = '/' in name or is_svn + def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, + is_svn): + use_commit_id = '/' in ref_name or is_svn return h.url( 'files_home', - repo_name=repo.name, - f_path=name if is_svn else '', - revision=raw_id if use_commit_id else name, - at=name) + repo_name=full_repo_name, + f_path=ref_name if is_svn else '', + revision=raw_id if use_commit_id else ref_name, + at=ref_name) diff --git a/rhodecode/events/__init__.py b/rhodecode/events/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/events/__init__.py @@ -0,0 +1,78 @@ +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging +from pyramid.threadlocal import get_current_registry + +log = logging.getLogger(__name__) + + +def trigger(event, registry=None): + """ + Helper method to send an event. This wraps the pyramid logic to send an + event. + """ + # For the first step we are using pyramids thread locals here. If the + # event mechanism works out as a good solution we should think about + # passing the registry as an argument to get rid of it. + registry = registry or get_current_registry() + registry.notify(event) + log.debug('event %s triggered', event) + + # Until we can work around the problem that VCS operations do not have a + # pyramid context to work with, we send the events to integrations directly + + # Later it will be possible to use regular pyramid subscribers ie: + # config.add_subscriber(integrations_event_handler, RhodecodeEvent) + from rhodecode.integrations import integrations_event_handler + if isinstance(event, RhodecodeEvent): + integrations_event_handler(event) + + +from rhodecode.events.base import RhodecodeEvent + +from rhodecode.events.user import ( # noqa + UserPreCreate, + UserPreUpdate, + UserRegistered +) + +from rhodecode.events.repo import ( # noqa + RepoEvent, + RepoPreCreateEvent, RepoCreateEvent, + RepoPreDeleteEvent, RepoDeleteEvent, + RepoPrePushEvent, RepoPushEvent, + RepoPrePullEvent, RepoPullEvent, +) + +from rhodecode.events.repo_group import ( # noqa + RepoGroupEvent, + RepoGroupCreateEvent, + RepoGroupUpdateEvent, + RepoGroupDeleteEvent, +) + +from rhodecode.events.pullrequest import ( # noqa + PullRequestEvent, + PullRequestCreateEvent, + PullRequestUpdateEvent, + PullRequestCommentEvent, + PullRequestReviewEvent, + PullRequestMergeEvent, + PullRequestCloseEvent, +) diff --git a/rhodecode/events/base.py b/rhodecode/events/base.py new file mode 100644 --- /dev/null +++ b/rhodecode/events/base.py @@ -0,0 +1,69 @@ +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +from datetime import datetime +from pyramid.threadlocal import get_current_request +from rhodecode.lib.utils2 import AttributeDict + + +# this is a user object to be used for events caused by the system (eg. shell) +SYSTEM_USER = AttributeDict(dict( + username='__SYSTEM__' +)) + + +class RhodecodeEvent(object): + """ + Base event class for all Rhodecode events + """ + name = "RhodeCodeEvent" + + def __init__(self): + self.request = get_current_request() + self.utc_timestamp = datetime.utcnow() + + @property + def actor(self): + if self.request: + return self.request.user.get_instance() + return SYSTEM_USER + + @property + def actor_ip(self): + if self.request: + return self.request.user.ip_addr + return '<no ip available>' + + @property + def server_url(self): + if self.request: + from rhodecode.lib import helpers as h + return h.url('home', qualified=True) + return '<no server_url available>' + + def as_dict(self): + data = { + 'name': self.name, + 'utc_timestamp': self.utc_timestamp, + 'actor_ip': self.actor_ip, + 'actor': { + 'username': self.actor.username + }, + 'server_url': self.server_url + } + return data diff --git a/rhodecode/interfaces.py b/rhodecode/events/interfaces.py rename from rhodecode/interfaces.py rename to rhodecode/events/interfaces.py diff --git a/rhodecode/events/pullrequest.py b/rhodecode/events/pullrequest.py new file mode 100644 --- /dev/null +++ b/rhodecode/events/pullrequest.py @@ -0,0 +1,131 @@ +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +from rhodecode.translation import lazy_ugettext +from rhodecode.events.repo import ( + RepoEvent, _commits_as_dict, _issues_as_dict) + + +class PullRequestEvent(RepoEvent): + """ + Base class for pull request events. + + :param pullrequest: a :class:`PullRequest` instance + """ + + def __init__(self, pullrequest): + super(PullRequestEvent, self).__init__(pullrequest.target_repo) + self.pullrequest = pullrequest + + def as_dict(self): + from rhodecode.model.pull_request import PullRequestModel + data = super(PullRequestEvent, self).as_dict() + + commits = _commits_as_dict( + commit_ids=self.pullrequest.revisions, + repos=[self.pullrequest.source_repo] + ) + issues = _issues_as_dict(commits) + + data.update({ + 'pullrequest': { + 'title': self.pullrequest.title, + 'issues': issues, + 'pull_request_id': self.pullrequest.pull_request_id, + 'url': PullRequestModel().get_url(self.pullrequest), + 'status': self.pullrequest.calculated_review_status(), + 'commits': commits, + } + }) + return data + + +class PullRequestCreateEvent(PullRequestEvent): + """ + An instance of this class is emitted as an :term:`event` after a pull + request is created. + """ + name = 'pullrequest-create' + display_name = lazy_ugettext('pullrequest created') + + +class PullRequestCloseEvent(PullRequestEvent): + """ + An instance of this class is emitted as an :term:`event` after a pull + request is closed. + """ + name = 'pullrequest-close' + display_name = lazy_ugettext('pullrequest closed') + + +class PullRequestUpdateEvent(PullRequestEvent): + """ + An instance of this class is emitted as an :term:`event` after a pull + request's commits have been updated. + """ + name = 'pullrequest-update' + display_name = lazy_ugettext('pullrequest commits updated') + + +class PullRequestReviewEvent(PullRequestEvent): + """ + An instance of this class is emitted as an :term:`event` after a pull + request review has changed. + """ + name = 'pullrequest-review' + display_name = lazy_ugettext('pullrequest review changed') + + +class PullRequestMergeEvent(PullRequestEvent): + """ + An instance of this class is emitted as an :term:`event` after a pull + request is merged. + """ + name = 'pullrequest-merge' + display_name = lazy_ugettext('pullrequest merged') + + +class PullRequestCommentEvent(PullRequestEvent): + """ + An instance of this class is emitted as an :term:`event` after a pull + request comment is created. + """ + name = 'pullrequest-comment' + display_name = lazy_ugettext('pullrequest commented') + + def __init__(self, pullrequest, comment): + super(PullRequestCommentEvent, self).__init__(pullrequest) + self.comment = comment + + def as_dict(self): + from rhodecode.model.comment import ChangesetCommentsModel + data = super(PullRequestCommentEvent, self).as_dict() + + status = None + if self.comment.status_change: + status = self.comment.status_change[0].status + + data.update({ + 'comment': { + 'status': status, + 'text': self.comment.text, + 'url': ChangesetCommentsModel().get_url(self.comment) + } + }) + return data diff --git a/rhodecode/events/repo.py b/rhodecode/events/repo.py new file mode 100644 --- /dev/null +++ b/rhodecode/events/repo.py @@ -0,0 +1,257 @@ +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging + +from rhodecode.translation import lazy_ugettext +from rhodecode.model.db import User, Repository, Session +from rhodecode.events.base import RhodecodeEvent +from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError + +log = logging.getLogger(__name__) + +def _commits_as_dict(commit_ids, repos): + """ + Helper function to serialize commit_ids + + :param commit_ids: commits to get + :param repos: list of repos to check + """ + from rhodecode.lib.utils2 import extract_mentioned_users + from rhodecode.model.db import Repository + from rhodecode.lib import helpers as h + from rhodecode.lib.helpers import process_patterns + from rhodecode.lib.helpers import urlify_commit_message + + if not repos: + raise Exception('no repo defined') + + if not isinstance(repos, (tuple, list)): + repos = [repos] + + if not commit_ids: + return [] + + needed_commits = set(commit_ids) + + commits = [] + reviewers = [] + for repo in repos: + if not needed_commits: + return commits # return early if we have the commits we need + + vcs_repo = repo.scm_instance(cache=False) + try: + for commit_id in list(needed_commits): + try: + cs = vcs_repo.get_changeset(commit_id) + except CommitDoesNotExistError: + continue # maybe its in next repo + + cs_data = cs.__json__() + cs_data['mentions'] = extract_mentioned_users(cs_data['message']) + cs_data['reviewers'] = reviewers + cs_data['url'] = h.url('changeset_home', + repo_name=repo.repo_name, + revision=cs_data['raw_id'], + qualified=True + ) + urlified_message, issues_data = process_patterns( + cs_data['message'], repo.repo_name) + cs_data['issues'] = issues_data + cs_data['message_html'] = urlify_commit_message(cs_data['message'], + repo.repo_name) + commits.append(cs_data) + + needed_commits.discard(commit_id) + + except Exception as e: + log.exception(e) + # we don't send any commits when crash happens, only full list matters + # we short circuit then. + return [] + + missing_commits = set(commit_ids) - set(c['raw_id'] for c in commits) + if missing_commits: + log.error('missing commits: %s' % ', '.join(missing_commits)) + + return commits + + +def _issues_as_dict(commits): + """ Helper function to serialize issues from commits """ + issues = {} + for commit in commits: + for issue in commit['issues']: + issues[issue['id']] = issue + return issues + +class RepoEvent(RhodecodeEvent): + """ + Base class for events acting on a repository. + + :param repo: a :class:`Repository` instance + """ + + def __init__(self, repo): + super(RepoEvent, self).__init__() + self.repo = repo + + def as_dict(self): + from rhodecode.model.repo import RepoModel + data = super(RepoEvent, self).as_dict() + data.update({ + 'repo': { + 'repo_id': self.repo.repo_id, + 'repo_name': self.repo.repo_name, + 'repo_type': self.repo.repo_type, + 'url': RepoModel().get_url(self.repo) + } + }) + return data + + +class RepoPreCreateEvent(RepoEvent): + """ + An instance of this class is emitted as an :term:`event` before a repo is + created. + """ + name = 'repo-pre-create' + display_name = lazy_ugettext('repository pre create') + + +class RepoCreateEvent(RepoEvent): + """ + An instance of this class is emitted as an :term:`event` whenever a repo is + created. + """ + name = 'repo-create' + display_name = lazy_ugettext('repository created') + + +class RepoPreDeleteEvent(RepoEvent): + """ + An instance of this class is emitted as an :term:`event` whenever a repo is + created. + """ + name = 'repo-pre-delete' + display_name = lazy_ugettext('repository pre delete') + + +class RepoDeleteEvent(RepoEvent): + """ + An instance of this class is emitted as an :term:`event` whenever a repo is + created. + """ + name = 'repo-delete' + display_name = lazy_ugettext('repository deleted') + + +class RepoVCSEvent(RepoEvent): + """ + Base class for events triggered by the VCS + """ + def __init__(self, repo_name, extras): + self.repo = Repository.get_by_repo_name(repo_name) + if not self.repo: + raise Exception('repo by this name %s does not exist' % repo_name) + self.extras = extras + super(RepoVCSEvent, self).__init__(self.repo) + + @property + def actor(self): + if self.extras.get('username'): + return User.get_by_username(self.extras['username']) + + @property + def actor_ip(self): + if self.extras.get('ip'): + return self.extras['ip'] + + @property + def server_url(self): + if self.extras.get('server_url'): + return self.extras['server_url'] + + +class RepoPrePullEvent(RepoVCSEvent): + """ + An instance of this class is emitted as an :term:`event` before commits + are pulled from a repo. + """ + name = 'repo-pre-pull' + display_name = lazy_ugettext('repository pre pull') + + +class RepoPullEvent(RepoVCSEvent): + """ + An instance of this class is emitted as an :term:`event` after commits + are pulled from a repo. + """ + name = 'repo-pull' + display_name = lazy_ugettext('repository pull') + + +class RepoPrePushEvent(RepoVCSEvent): + """ + An instance of this class is emitted as an :term:`event` before commits + are pushed to a repo. + """ + name = 'repo-pre-push' + display_name = lazy_ugettext('repository pre push') + + +class RepoPushEvent(RepoVCSEvent): + """ + An instance of this class is emitted as an :term:`event` after commits + are pushed to a repo. + + :param extras: (optional) dict of data from proxied VCS actions + """ + name = 'repo-push' + display_name = lazy_ugettext('repository push') + + def __init__(self, repo_name, pushed_commit_ids, extras): + super(RepoPushEvent, self).__init__(repo_name, extras) + self.pushed_commit_ids = pushed_commit_ids + + def as_dict(self): + data = super(RepoPushEvent, self).as_dict() + branch_url = repo_url = data['repo']['url'] + + commits = _commits_as_dict( + commit_ids=self.pushed_commit_ids, repos=[self.repo]) + issues = _issues_as_dict(commits) + + branches = set( + commit['branch'] for commit in commits if commit['branch']) + branches = [ + { + 'name': branch, + 'url': '{}/changelog?branch={}'.format( + data['repo']['url'], branch) + } + for branch in branches + ] + + data['push'] = { + 'commits': commits, + 'issues': issues, + 'branches': branches, + } + return data diff --git a/rhodecode/events/repo_group.py b/rhodecode/events/repo_group.py new file mode 100644 --- /dev/null +++ b/rhodecode/events/repo_group.py @@ -0,0 +1,80 @@ +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging + +from rhodecode.translation import lazy_ugettext +from rhodecode.events.base import RhodecodeEvent + + +log = logging.getLogger(__name__) + + +class RepoGroupEvent(RhodecodeEvent): + """ + Base class for events acting on a repository group. + + :param repo: a :class:`RepositoryGroup` instance + """ + + def __init__(self, repo_group): + super(RepoGroupEvent, self).__init__() + self.repo_group = repo_group + + def as_dict(self): + data = super(RepoGroupEvent, self).as_dict() + data.update({ + 'repo_group': { + 'group_id': self.repo_group.group_id, + 'group_name': self.repo_group.group_name, + 'group_parent_id': self.repo_group.group_parent_id, + 'group_description': self.repo_group.group_description, + 'user_id': self.repo_group.user_id, + 'created_by': self.repo_group.user.username, + 'created_on': self.repo_group.created_on, + 'enable_locking': self.repo_group.enable_locking, + } + }) + return data + + +class RepoGroupCreateEvent(RepoGroupEvent): + """ + An instance of this class is emitted as an :term:`event` whenever a + repository group is created. + """ + name = 'repo-group-create' + display_name = lazy_ugettext('repository group created') + + +class RepoGroupDeleteEvent(RepoGroupEvent): + """ + An instance of this class is emitted as an :term:`event` whenever a + repository group is deleted. + """ + name = 'repo-group-delete' + display_name = lazy_ugettext('repository group deleted') + + +class RepoGroupUpdateEvent(RepoGroupEvent): + """ + An instance of this class is emitted as an :term:`event` whenever a + repository group is updated. + """ + name = 'repo-group-update' + display_name = lazy_ugettext('repository group update') diff --git a/rhodecode/events.py b/rhodecode/events/user.py rename from rhodecode/events.py rename to rhodecode/events/user.py --- a/rhodecode/events.py +++ b/rhodecode/events/user.py @@ -17,37 +17,49 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ from zope.interface import implementer -from rhodecode.interfaces import ( + +from rhodecode.translation import lazy_ugettext +from rhodecode.events.base import RhodecodeEvent +from rhodecode.events.interfaces import ( IUserRegistered, IUserPreCreate, IUserPreUpdate) @implementer(IUserRegistered) -class UserRegistered(object): +class UserRegistered(RhodecodeEvent): """ An instance of this class is emitted as an :term:`event` whenever a user account is registered. """ + name = 'user-register' + display_name = lazy_ugettext('user registered') + def __init__(self, user, session): self.user = user self.session = session @implementer(IUserPreCreate) -class UserPreCreate(object): +class UserPreCreate(RhodecodeEvent): """ An instance of this class is emitted as an :term:`event` before a new user object is created. """ + name = 'user-pre-create' + display_name = lazy_ugettext('user pre create') + def __init__(self, user_data): self.user_data = user_data @implementer(IUserPreUpdate) -class UserPreUpdate(object): +class UserPreUpdate(RhodecodeEvent): """ An instance of this class is emitted as an :term:`event` before a user object is updated. """ + name = 'user-pre-update' + display_name = lazy_ugettext('user pre update') + def __init__(self, user, user_data): self.user = user self.user_data = user_data diff --git a/rhodecode/i18n/rhodecode.pot b/rhodecode/i18n/rhodecode.pot --- a/rhodecode/i18n/rhodecode.pot +++ b/rhodecode/i18n/rhodecode.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: rhodecode-enterprise-ce 4.2.0\n" +"Project-Id-Version: rhodecode-enterprise-ce 4.3.0\n" "Report-Msgid-Bugs-To: marcin@rhodecode.com\n" -"POT-Creation-Date: 2016-06-30 17:18+0000\n" +"POT-Creation-Date: 2016-08-02 20:55+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -18,12 +18,13 @@ msgstr "" "Generated-By: Babel 1.3\n" #: rhodecode/admin/navigation.py:74 rhodecode/authentication/routes.py:60 +#: rhodecode/integrations/views.py:126 #: rhodecode/templates/admin/permissions/permissions.html:36 msgid "Global" msgstr "" #: rhodecode/admin/navigation.py:75 -#: rhodecode/templates/admin/repos/repo_edit.html:48 +#: rhodecode/templates/admin/repos/repo_edit.html:52 msgid "VCS" msgstr "" @@ -36,7 +37,7 @@ msgid "Remap and Rescan" msgstr "" #: rhodecode/admin/navigation.py:78 -#: rhodecode/templates/admin/repos/repo_edit.html:54 +#: rhodecode/templates/admin/repos/repo_edit.html:58 msgid "Issue Tracker" msgstr "" @@ -45,7 +46,8 @@ msgstr "" #: rhodecode/templates/admin/my_account/my_account_profile_edit.html:94 #: rhodecode/templates/admin/users/user_add.html:86 #: rhodecode/templates/admin/users/user_edit_profile.html:65 -#: rhodecode/templates/admin/users/users.html:91 +#: rhodecode/templates/admin/users/users.html:90 +#: rhodecode/templates/email_templates/user_registration.mako:25 #: rhodecode/templates/users/user_profile.html:51 msgid "Email" msgstr "" @@ -59,15 +61,27 @@ msgid "Full Text Search" msgstr "" #: rhodecode/admin/navigation.py:83 +#: rhodecode/templates/admin/integrations/base.html:21 +#: rhodecode/templates/admin/integrations/edit.html:8 +#: rhodecode/templates/admin/integrations/edit.html:19 +#: rhodecode/templates/admin/integrations/list.html:15 +#: rhodecode/templates/admin/integrations/list.html:19 +#: rhodecode/templates/admin/integrations/list.html:26 +#: rhodecode/templates/admin/repos/repo_edit.html:72 +#: rhodecode/templates/base/base.html:84 +msgid "Integrations" +msgstr "" + +#: rhodecode/admin/navigation.py:85 #: rhodecode/templates/admin/settings/settings_system.html:47 msgid "System Info" msgstr "" -#: rhodecode/admin/navigation.py:84 +#: rhodecode/admin/navigation.py:86 msgid "Open Source Licenses" msgstr "" -#: rhodecode/admin/navigation.py:91 +#: rhodecode/admin/navigation.py:93 msgid "Labs" msgstr "" @@ -75,7 +89,9 @@ msgstr "" msgid "Enable or disable this authentication plugin." msgstr "" -#: rhodecode/authentication/schema.py:37 +#: rhodecode/authentication/schema.py:37 rhodecode/integrations/schema.py:37 +#: rhodecode/templates/admin/integrations/list.html:62 +#: rhodecode/templates/admin/my_account/my_account_notifications.html:14 msgid "Enabled" msgstr "" @@ -217,7 +233,7 @@ msgstr "" #: rhodecode/templates/login.html:50 rhodecode/templates/register.html:48 #: rhodecode/templates/admin/my_account/my_account.html:30 #: rhodecode/templates/admin/users/user_add.html:44 -#: rhodecode/templates/base/base.html:314 +#: rhodecode/templates/base/base.html:315 #: rhodecode/templates/debug_style/login.html:45 msgid "Password" msgstr "" @@ -266,12 +282,12 @@ msgstr "" msgid "LDAP Attribute to map to user name" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:144 -msgid "The LDAP Login attribute of the CN must be specified" +#: rhodecode/authentication/plugins/auth_ldap.py:145 +msgid "Login Attribute" msgstr "" #: rhodecode/authentication/plugins/auth_ldap.py:146 -msgid "Login Attribute" +msgid "The LDAP Login attribute of the CN must be specified" msgstr "" #: rhodecode/authentication/plugins/auth_ldap.py:151 @@ -298,7 +314,7 @@ msgstr "" msgid "Email Attribute" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:348 +#: rhodecode/authentication/plugins/auth_ldap.py:351 msgid "LDAP" msgstr "" @@ -331,7 +347,7 @@ msgid "Rhodecode Token Auth" msgstr "" #: rhodecode/controllers/changelog.py:90 rhodecode/controllers/compare.py:63 -#: rhodecode/controllers/pullrequests.py:279 +#: rhodecode/controllers/pullrequests.py:280 msgid "There are no commits yet" msgstr "" @@ -367,8 +383,8 @@ msgid "No such commit exists for this re msgstr "" #: rhodecode/controllers/changeset.py:335 -#: rhodecode/controllers/pullrequests.py:746 -#: rhodecode/model/pull_request.py:836 +#: rhodecode/controllers/pullrequests.py:750 +#: rhodecode/model/pull_request.py:843 #, python-format msgid "Status change %(transition_icon)s %(status)s" msgstr "" @@ -423,100 +439,100 @@ msgstr "" msgid "There are no files yet. %s" msgstr "" -#: rhodecode/controllers/files.py:390 rhodecode/controllers/files.py:443 -#: rhodecode/controllers/files.py:474 rhodecode/controllers/files.py:549 -#: rhodecode/controllers/files.py:594 rhodecode/controllers/files.py:685 +#: rhodecode/controllers/files.py:381 rhodecode/controllers/files.py:434 +#: rhodecode/controllers/files.py:465 rhodecode/controllers/files.py:540 +#: rhodecode/controllers/files.py:585 rhodecode/controllers/files.py:676 #, python-format msgid "This repository has been locked by %s on %s" msgstr "" +#: rhodecode/controllers/files.py:389 rhodecode/controllers/files.py:442 +msgid "You can only delete files with revision being a valid branch " +msgstr "" + #: rhodecode/controllers/files.py:398 rhodecode/controllers/files.py:451 -msgid "You can only delete files with revision being a valid branch " -msgstr "" - -#: rhodecode/controllers/files.py:407 rhodecode/controllers/files.py:460 #, python-format msgid "Deleted file %s via RhodeCode Enterprise" msgstr "" -#: rhodecode/controllers/files.py:427 +#: rhodecode/controllers/files.py:418 #, python-format msgid "Successfully deleted file %s" msgstr "" -#: rhodecode/controllers/files.py:430 rhodecode/controllers/files.py:536 -#: rhodecode/controllers/files.py:673 +#: rhodecode/controllers/files.py:421 rhodecode/controllers/files.py:527 +#: rhodecode/controllers/files.py:664 msgid "Error occurred during commit" msgstr "" -#: rhodecode/controllers/files.py:482 rhodecode/controllers/files.py:557 +#: rhodecode/controllers/files.py:473 rhodecode/controllers/files.py:548 msgid "You can only edit files with revision being a valid branch " msgstr "" -#: rhodecode/controllers/files.py:494 rhodecode/controllers/files.py:569 +#: rhodecode/controllers/files.py:485 rhodecode/controllers/files.py:560 #, python-format msgid "Edited file %s via RhodeCode Enterprise" msgstr "" -#: rhodecode/controllers/files.py:511 +#: rhodecode/controllers/files.py:502 msgid "No changes" msgstr "" -#: rhodecode/controllers/files.py:533 rhodecode/controllers/files.py:662 +#: rhodecode/controllers/files.py:524 rhodecode/controllers/files.py:653 #, python-format msgid "Successfully committed to %s" msgstr "" -#: rhodecode/controllers/files.py:607 rhodecode/controllers/files.py:696 +#: rhodecode/controllers/files.py:598 rhodecode/controllers/files.py:687 msgid "Added file via RhodeCode Enterprise" msgstr "" -#: rhodecode/controllers/files.py:632 +#: rhodecode/controllers/files.py:623 msgid "No filename" msgstr "" -#: rhodecode/controllers/files.py:665 +#: rhodecode/controllers/files.py:656 msgid "The location specified must be a relative path and must not contain .. in the path" msgstr "" -#: rhodecode/controllers/files.py:719 +#: rhodecode/controllers/files.py:710 msgid "Downloads disabled" msgstr "" -#: rhodecode/controllers/files.py:725 +#: rhodecode/controllers/files.py:716 #, python-format msgid "Unknown revision %s" msgstr "" -#: rhodecode/controllers/files.py:727 +#: rhodecode/controllers/files.py:718 msgid "Empty repository" msgstr "" -#: rhodecode/controllers/files.py:729 rhodecode/controllers/files.py:763 +#: rhodecode/controllers/files.py:720 rhodecode/controllers/files.py:754 msgid "Unknown archive type" msgstr "" -#: rhodecode/controllers/files.py:930 +#: rhodecode/controllers/files.py:921 #, python-format msgid "Commit %(commit)s does not exist." msgstr "" -#: rhodecode/controllers/files.py:947 +#: rhodecode/controllers/files.py:938 #, python-format msgid "%(file_path)s has not changed between %(commit_1)s and %(commit_2)s." msgstr "" -#: rhodecode/controllers/files.py:1014 +#: rhodecode/controllers/files.py:1005 msgid "Changesets" msgstr "" -#: rhodecode/controllers/files.py:1035 rhodecode/controllers/summary.py:256 -#: rhodecode/model/pull_request.py:1051 rhodecode/model/scm.py:783 +#: rhodecode/controllers/files.py:1026 rhodecode/controllers/summary.py:251 +#: rhodecode/model/pull_request.py:1059 rhodecode/model/scm.py:780 #: rhodecode/templates/base/vcs_settings.html:138 msgid "Branches" msgstr "" -#: rhodecode/controllers/files.py:1039 rhodecode/model/scm.py:798 +#: rhodecode/controllers/files.py:1030 rhodecode/model/scm.py:795 #: rhodecode/templates/base/vcs_settings.html:163 msgid "Tags" msgstr "" @@ -531,13 +547,13 @@ msgid "Groups" msgstr "" #: rhodecode/controllers/home.py:212 rhodecode/controllers/home.py:247 -#: rhodecode/controllers/pullrequests.py:382 +#: rhodecode/controllers/pullrequests.py:383 #: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.html:128 #: rhodecode/templates/admin/repos/repo_add.html:15 #: rhodecode/templates/admin/repos/repo_add.html:19 #: rhodecode/templates/admin/users/user_edit_advanced.html:11 -#: rhodecode/templates/base/base.html:79 rhodecode/templates/base/base.html:149 -#: rhodecode/templates/base/base.html:626 +#: rhodecode/templates/base/base.html:78 rhodecode/templates/base/base.html:150 +#: rhodecode/templates/base/base.html:627 msgid "Repositories" msgstr "" @@ -554,93 +570,93 @@ msgstr "" msgid "journal" msgstr "" -#: rhodecode/controllers/pullrequests.py:293 +#: rhodecode/controllers/pullrequests.py:294 msgid "Commit does not exist" msgstr "" -#: rhodecode/controllers/pullrequests.py:405 +#: rhodecode/controllers/pullrequests.py:406 msgid "Pull request requires a title with min. 3 chars" msgstr "" -#: rhodecode/controllers/pullrequests.py:407 +#: rhodecode/controllers/pullrequests.py:408 msgid "Error creating pull request: {}" msgstr "" -#: rhodecode/controllers/pullrequests.py:454 +#: rhodecode/controllers/pullrequests.py:455 msgid "Successfully opened new pull request" msgstr "" -#: rhodecode/controllers/pullrequests.py:457 +#: rhodecode/controllers/pullrequests.py:458 msgid "Error occurred during sending pull request" msgstr "" -#: rhodecode/controllers/pullrequests.py:497 +#: rhodecode/controllers/pullrequests.py:498 msgid "Cannot update closed pull requests." msgstr "" -#: rhodecode/controllers/pullrequests.py:503 +#: rhodecode/controllers/pullrequests.py:504 msgid "Pull request title & description updated." msgstr "" -#: rhodecode/controllers/pullrequests.py:513 +#: rhodecode/controllers/pullrequests.py:514 msgid "Pull request updated to \"{source_commit_id}\" with {count_added} added, {count_removed} removed commits." msgstr "" -#: rhodecode/controllers/pullrequests.py:523 +#: rhodecode/controllers/pullrequests.py:524 msgid "Nothing changed in pull request." msgstr "" -#: rhodecode/controllers/pullrequests.py:526 +#: rhodecode/controllers/pullrequests.py:527 msgid "Skipping update of pull request due to reference type: {reference_type}" msgstr "" -#: rhodecode/controllers/pullrequests.py:533 +#: rhodecode/controllers/pullrequests.py:534 msgid "Update failed due to missing commits." msgstr "" -#: rhodecode/controllers/pullrequests.py:579 +#: rhodecode/controllers/pullrequests.py:580 msgid "Pull request reviewer approval is pending." msgstr "" -#: rhodecode/controllers/pullrequests.py:593 +#: rhodecode/controllers/pullrequests.py:594 msgid "Pull request was successfully merged and closed." msgstr "" -#: rhodecode/controllers/pullrequests.py:631 +#: rhodecode/controllers/pullrequests.py:632 msgid "Successfully deleted pull request" msgstr "" -#: rhodecode/controllers/pullrequests.py:664 +#: rhodecode/controllers/pullrequests.py:668 msgid "Reviewer approval is pending." msgstr "" -#: rhodecode/controllers/pullrequests.py:706 +#: rhodecode/controllers/pullrequests.py:710 msgid "Close Pull Request" msgstr "" -#: rhodecode/controllers/pullrequests.py:750 -#: rhodecode/model/pull_request.py:840 +#: rhodecode/controllers/pullrequests.py:754 +#: rhodecode/model/pull_request.py:847 msgid "Closing with" msgstr "" -#: rhodecode/controllers/pullrequests.py:795 +#: rhodecode/controllers/pullrequests.py:802 #, python-format msgid "Closing pull request on other statuses than rejected or approved is forbidden. Calculated status from all reviewers is currently: %s" msgstr "" -#: rhodecode/controllers/summary.py:240 +#: rhodecode/controllers/summary.py:235 msgid "Branch" msgstr "" -#: rhodecode/controllers/summary.py:241 +#: rhodecode/controllers/summary.py:236 msgid "Tag" msgstr "" -#: rhodecode/controllers/summary.py:242 +#: rhodecode/controllers/summary.py:237 msgid "Bookmark" msgstr "" -#: rhodecode/controllers/summary.py:257 +#: rhodecode/controllers/summary.py:252 msgid "Closed branches" msgstr "" @@ -652,83 +668,87 @@ msgstr "" msgid "Error occurred during update of default values" msgstr "" -#: rhodecode/controllers/admin/gists.py:59 +#: rhodecode/controllers/admin/gists.py:63 #: rhodecode/controllers/admin/my_account.py:307 -#: rhodecode/controllers/admin/users.py:436 +#: rhodecode/controllers/admin/users.py:431 msgid "forever" msgstr "" -#: rhodecode/controllers/admin/gists.py:60 +#: rhodecode/controllers/admin/gists.py:64 #: rhodecode/controllers/admin/my_account.py:308 -#: rhodecode/controllers/admin/users.py:437 +#: rhodecode/controllers/admin/users.py:432 msgid "5 minutes" msgstr "" -#: rhodecode/controllers/admin/gists.py:61 +#: rhodecode/controllers/admin/gists.py:65 #: rhodecode/controllers/admin/my_account.py:309 -#: rhodecode/controllers/admin/users.py:438 +#: rhodecode/controllers/admin/users.py:433 msgid "1 hour" msgstr "" -#: rhodecode/controllers/admin/gists.py:62 +#: rhodecode/controllers/admin/gists.py:66 #: rhodecode/controllers/admin/my_account.py:310 -#: rhodecode/controllers/admin/users.py:439 +#: rhodecode/controllers/admin/users.py:434 msgid "1 day" msgstr "" -#: rhodecode/controllers/admin/gists.py:63 -#: rhodecode/controllers/admin/my_account.py:311 -#: rhodecode/controllers/admin/users.py:440 -msgid "1 month" -msgstr "" - #: rhodecode/controllers/admin/gists.py:67 +#: rhodecode/controllers/admin/my_account.py:311 +#: rhodecode/controllers/admin/users.py:435 +msgid "1 month" +msgstr "" + +#: rhodecode/controllers/admin/gists.py:71 #: rhodecode/controllers/admin/my_account.py:313 -#: rhodecode/controllers/admin/users.py:442 +#: rhodecode/controllers/admin/users.py:437 msgid "Lifetime" msgstr "" -#: rhodecode/controllers/admin/gists.py:69 +#: rhodecode/controllers/admin/gists.py:73 msgid "Requires registered account" msgstr "" -#: rhodecode/controllers/admin/gists.py:70 +#: rhodecode/controllers/admin/gists.py:74 msgid "Can be accessed by anonymous users" msgstr "" -#: rhodecode/controllers/admin/gists.py:180 +#: rhodecode/controllers/admin/gists.py:200 msgid "Error occurred during gist creation" msgstr "" -#: rhodecode/controllers/admin/gists.py:211 +#: rhodecode/controllers/admin/gists.py:231 #, python-format msgid "Deleted gist %s" msgstr "" -#: rhodecode/controllers/admin/gists.py:284 +#: rhodecode/controllers/admin/gists.py:308 msgid "Successfully updated gist content" msgstr "" -#: rhodecode/controllers/admin/gists.py:289 +#: rhodecode/controllers/admin/gists.py:313 msgid "Successfully updated gist data" msgstr "" -#: rhodecode/controllers/admin/gists.py:292 +#: rhodecode/controllers/admin/gists.py:316 +msgid "Error occurred during update of gist {}: {}" +msgstr "" + +#: rhodecode/controllers/admin/gists.py:320 #, python-format msgid "Error occurred during update of gist %s" msgstr "" -#: rhodecode/controllers/admin/gists.py:315 +#: rhodecode/controllers/admin/gists.py:343 #: rhodecode/templates/admin/gists/show.html:67 #: rhodecode/templates/admin/my_account/my_account_auth_tokens.html:19 #: rhodecode/templates/admin/my_account/my_account_auth_tokens.html:42 #: rhodecode/templates/admin/users/user_edit_auth_tokens.html:16 #: rhodecode/templates/admin/users/user_edit_auth_tokens.html:38 -#: rhodecode/templates/data_table/_dt_elements.html:253 +#: rhodecode/templates/data_table/_dt_elements.html:255 msgid "never" msgstr "" -#: rhodecode/controllers/admin/gists.py:320 +#: rhodecode/controllers/admin/gists.py:348 #, python-format msgid "%(expiry)s - current value" msgstr "" @@ -742,7 +762,7 @@ msgid "Your account was updated successf msgstr "" #: rhodecode/controllers/admin/my_account.py:143 -#: rhodecode/controllers/admin/users.py:223 +#: rhodecode/controllers/admin/users.py:218 #, python-format msgid "Error occurred during update of user %s" msgstr "" @@ -756,38 +776,38 @@ msgid "Error occurred during update of u msgstr "" #: rhodecode/controllers/admin/my_account.py:261 -#: rhodecode/controllers/admin/users.py:616 +#: rhodecode/controllers/admin/users.py:611 #, python-format msgid "Added new email address `%s` for user account" msgstr "" #: rhodecode/controllers/admin/my_account.py:268 -#: rhodecode/controllers/admin/users.py:623 +#: rhodecode/controllers/admin/users.py:618 msgid "An error occurred during email saving" msgstr "" #: rhodecode/controllers/admin/my_account.py:278 -#: rhodecode/controllers/admin/users.py:638 +#: rhodecode/controllers/admin/users.py:633 msgid "Removed email address from user account" msgstr "" #: rhodecode/controllers/admin/my_account.py:316 -#: rhodecode/controllers/admin/users.py:445 +#: rhodecode/controllers/admin/users.py:440 msgid "Role" msgstr "" #: rhodecode/controllers/admin/my_account.py:329 -#: rhodecode/controllers/admin/users.py:469 +#: rhodecode/controllers/admin/users.py:464 msgid "Auth token successfully created" msgstr "" #: rhodecode/controllers/admin/my_account.py:342 -#: rhodecode/controllers/admin/users.py:488 +#: rhodecode/controllers/admin/users.py:483 msgid "Auth token successfully reset" msgstr "" #: rhodecode/controllers/admin/my_account.py:346 -#: rhodecode/controllers/admin/users.py:492 +#: rhodecode/controllers/admin/users.py:487 msgid "Auth token successfully deleted" msgstr "" @@ -862,170 +882,170 @@ msgstr "" msgid "Repository Group permissions updated" msgstr "" -#: rhodecode/controllers/admin/repos.py:128 +#: rhodecode/controllers/admin/repos.py:129 #, python-format msgid "Error creating repository %s: invalid certificate" msgstr "" -#: rhodecode/controllers/admin/repos.py:132 +#: rhodecode/controllers/admin/repos.py:133 #, python-format msgid "Error creating repository %s" msgstr "" -#: rhodecode/controllers/admin/repos.py:264 +#: rhodecode/controllers/admin/repos.py:265 #, python-format msgid "Created repository %s from %s" msgstr "" -#: rhodecode/controllers/admin/repos.py:273 +#: rhodecode/controllers/admin/repos.py:274 #, python-format msgid "Forked repository %s as %s" msgstr "" -#: rhodecode/controllers/admin/repos.py:276 +#: rhodecode/controllers/admin/repos.py:277 #, python-format msgid "Created repository %s" msgstr "" -#: rhodecode/controllers/admin/repos.py:318 +#: rhodecode/controllers/admin/repos.py:319 #, python-format msgid "Repository %s updated successfully" msgstr "" -#: rhodecode/controllers/admin/repos.py:337 +#: rhodecode/controllers/admin/repos.py:338 #, python-format msgid "Error occurred during update of repository %s" msgstr "" -#: rhodecode/controllers/admin/repos.py:365 +#: rhodecode/controllers/admin/repos.py:366 #, python-format msgid "Detached %s forks" msgstr "" -#: rhodecode/controllers/admin/repos.py:368 +#: rhodecode/controllers/admin/repos.py:369 #, python-format msgid "Deleted %s forks" msgstr "" -#: rhodecode/controllers/admin/repos.py:373 +#: rhodecode/controllers/admin/repos.py:374 #, python-format msgid "Deleted repository %s" msgstr "" -#: rhodecode/controllers/admin/repos.py:376 +#: rhodecode/controllers/admin/repos.py:377 #, python-format msgid "Cannot delete %s it still contains attached forks" msgstr "" -#: rhodecode/controllers/admin/repos.py:381 +#: rhodecode/controllers/admin/repos.py:382 #, python-format msgid "An error occurred during deletion of %s" msgstr "" -#: rhodecode/controllers/admin/repos.py:435 +#: rhodecode/controllers/admin/repos.py:436 msgid "Repository permissions updated" msgstr "" -#: rhodecode/controllers/admin/repos.py:466 +#: rhodecode/controllers/admin/repos.py:467 msgid "An error occurred during creation of field" msgstr "" -#: rhodecode/controllers/admin/repos.py:481 +#: rhodecode/controllers/admin/repos.py:482 msgid "An error occurred during removal of field" msgstr "" -#: rhodecode/controllers/admin/repos.py:520 +#: rhodecode/controllers/admin/repos.py:521 msgid "Updated repository visibility in public journal" msgstr "" -#: rhodecode/controllers/admin/repos.py:524 +#: rhodecode/controllers/admin/repos.py:525 msgid "An error occurred during setting this repository in public journal" msgstr "" -#: rhodecode/controllers/admin/repos.py:548 +#: rhodecode/controllers/admin/repos.py:549 msgid "Nothing" msgstr "" -#: rhodecode/controllers/admin/repos.py:550 +#: rhodecode/controllers/admin/repos.py:551 #, python-format msgid "Marked repo %s as fork of %s" msgstr "" -#: rhodecode/controllers/admin/repos.py:557 +#: rhodecode/controllers/admin/repos.py:558 msgid "An error occurred during this operation" msgstr "" -#: rhodecode/controllers/admin/repos.py:575 +#: rhodecode/controllers/admin/repos.py:576 msgid "Locked repository" msgstr "" -#: rhodecode/controllers/admin/repos.py:578 +#: rhodecode/controllers/admin/repos.py:579 msgid "Unlocked repository" msgstr "" -#: rhodecode/controllers/admin/repos.py:581 -#: rhodecode/controllers/admin/repos.py:610 +#: rhodecode/controllers/admin/repos.py:582 +#: rhodecode/controllers/admin/repos.py:611 msgid "An error occurred during unlocking" msgstr "" -#: rhodecode/controllers/admin/repos.py:600 +#: rhodecode/controllers/admin/repos.py:601 msgid "Unlocked" msgstr "" -#: rhodecode/controllers/admin/repos.py:604 +#: rhodecode/controllers/admin/repos.py:605 msgid "Locked" msgstr "" -#: rhodecode/controllers/admin/repos.py:606 +#: rhodecode/controllers/admin/repos.py:607 #, python-format msgid "Repository has been %s" msgstr "" -#: rhodecode/controllers/admin/repos.py:621 +#: rhodecode/controllers/admin/repos.py:622 msgid "Cache invalidation successful" msgstr "" -#: rhodecode/controllers/admin/repos.py:625 +#: rhodecode/controllers/admin/repos.py:626 msgid "An error occurred during cache invalidation" msgstr "" -#: rhodecode/controllers/admin/repos.py:645 +#: rhodecode/controllers/admin/repos.py:646 msgid "Pulled from remote location" msgstr "" -#: rhodecode/controllers/admin/repos.py:648 +#: rhodecode/controllers/admin/repos.py:649 msgid "An error occurred during pull from remote location" msgstr "" -#: rhodecode/controllers/admin/repos.py:670 +#: rhodecode/controllers/admin/repos.py:671 msgid "An error occurred during deletion of repository stats" msgstr "" -#: rhodecode/controllers/admin/repos.py:717 +#: rhodecode/controllers/admin/repos.py:718 msgid "Error occurred during deleting issue tracker entry" msgstr "" -#: rhodecode/controllers/admin/repos.py:720 +#: rhodecode/controllers/admin/repos.py:721 #: rhodecode/controllers/admin/settings.py:363 msgid "Removed issue tracker entry" msgstr "" -#: rhodecode/controllers/admin/repos.py:750 +#: rhodecode/controllers/admin/repos.py:751 #: rhodecode/controllers/admin/settings.py:409 msgid "Updated issue tracker entries" msgstr "" -#: rhodecode/controllers/admin/repos.py:809 +#: rhodecode/controllers/admin/repos.py:812 #: rhodecode/controllers/admin/settings.py:142 #: rhodecode/controllers/admin/settings.py:719 msgid "Some form inputs contain invalid data." msgstr "" -#: rhodecode/controllers/admin/repos.py:827 +#: rhodecode/controllers/admin/repos.py:830 msgid "Error occurred during updating repository VCS settings" msgstr "" -#: rhodecode/controllers/admin/repos.py:831 +#: rhodecode/controllers/admin/repos.py:834 #: rhodecode/controllers/admin/settings.py:168 msgid "Updated VCS settings" msgstr "" @@ -1091,26 +1111,18 @@ msgid "Updated Labs settings" msgstr "" #: rhodecode/controllers/admin/settings.py:795 -msgid "Mercurial server-side merge" +msgid "Subversion HTTP Support" msgstr "" #: rhodecode/controllers/admin/settings.py:796 -msgid "Use rebase instead of creating a merge commit when merging via web interface" +msgid "Proxy subversion HTTP requests" msgstr "" #: rhodecode/controllers/admin/settings.py:802 -msgid "Subversion HTTP Support" -msgstr "" - -#: rhodecode/controllers/admin/settings.py:803 -msgid "Proxy subversion HTTP requests" -msgstr "" - -#: rhodecode/controllers/admin/settings.py:809 msgid "Subversion HTTP Server URL" msgstr "" -#: rhodecode/controllers/admin/settings.py:811 +#: rhodecode/controllers/admin/settings.py:804 msgid "e.g. http://localhost:8080/" msgstr "" @@ -1155,121 +1167,281 @@ msgid "User Group global permissions upd msgstr "" #: rhodecode/controllers/admin/user_groups.py:440 -#: rhodecode/controllers/admin/users.py:566 +#: rhodecode/controllers/admin/users.py:561 msgid "An error occurred during permissions saving" msgstr "" -#: rhodecode/controllers/admin/users.py:147 +#: rhodecode/controllers/admin/users.py:142 #, python-format msgid "Created user %(user_link)s" msgstr "" -#: rhodecode/controllers/admin/users.py:162 +#: rhodecode/controllers/admin/users.py:157 #, python-format msgid "Error occurred during creation of user %s" msgstr "" -#: rhodecode/controllers/admin/users.py:206 +#: rhodecode/controllers/admin/users.py:201 msgid "User updated successfully" msgstr "" +#: rhodecode/controllers/admin/users.py:252 +#, python-format +msgid "Detached %s repositories" +msgstr "" + #: rhodecode/controllers/admin/users.py:257 #, python-format -msgid "Detached %s repositories" -msgstr "" - -#: rhodecode/controllers/admin/users.py:262 -#, python-format msgid "Deleted %s repositories" msgstr "" +#: rhodecode/controllers/admin/users.py:265 +#, python-format +msgid "Detached %s repository groups" +msgstr "" + #: rhodecode/controllers/admin/users.py:270 #, python-format -msgid "Detached %s repository groups" -msgstr "" - -#: rhodecode/controllers/admin/users.py:275 -#, python-format msgid "Deleted %s repository groups" msgstr "" +#: rhodecode/controllers/admin/users.py:278 +#, python-format +msgid "Detached %s user groups" +msgstr "" + #: rhodecode/controllers/admin/users.py:283 #, python-format -msgid "Detached %s user groups" -msgstr "" - -#: rhodecode/controllers/admin/users.py:288 -#, python-format msgid "Deleted %s user groups" msgstr "" -#: rhodecode/controllers/admin/users.py:299 +#: rhodecode/controllers/admin/users.py:294 msgid "Successfully deleted user" msgstr "" -#: rhodecode/controllers/admin/users.py:305 +#: rhodecode/controllers/admin/users.py:300 msgid "An error occurred during deletion of user" msgstr "" -#: rhodecode/controllers/admin/users.py:324 +#: rhodecode/controllers/admin/users.py:319 msgid "Force password change disabled for user" msgstr "" -#: rhodecode/controllers/admin/users.py:326 +#: rhodecode/controllers/admin/users.py:321 msgid "Force password change enabled for user" msgstr "" -#: rhodecode/controllers/admin/users.py:330 +#: rhodecode/controllers/admin/users.py:325 msgid "An error occurred during password reset for user" msgstr "" -#: rhodecode/controllers/admin/users.py:356 +#: rhodecode/controllers/admin/users.py:351 #, python-format msgid "Created repository group `%s`" msgstr "" -#: rhodecode/controllers/admin/users.py:360 +#: rhodecode/controllers/admin/users.py:355 msgid "An error occurred during repository group creation for user" msgstr "" -#: rhodecode/controllers/admin/users.py:379 -#: rhodecode/controllers/admin/users.py:400 -#: rhodecode/controllers/admin/users.py:430 -#: rhodecode/controllers/admin/users.py:461 -#: rhodecode/controllers/admin/users.py:478 -#: rhodecode/controllers/admin/users.py:501 -#: rhodecode/controllers/admin/users.py:575 -#: rhodecode/controllers/admin/users.py:588 -#: rhodecode/controllers/admin/users.py:646 +#: rhodecode/controllers/admin/users.py:374 +#: rhodecode/controllers/admin/users.py:395 +#: rhodecode/controllers/admin/users.py:425 +#: rhodecode/controllers/admin/users.py:456 +#: rhodecode/controllers/admin/users.py:473 +#: rhodecode/controllers/admin/users.py:496 +#: rhodecode/controllers/admin/users.py:570 +#: rhodecode/controllers/admin/users.py:583 +#: rhodecode/controllers/admin/users.py:641 msgid "You can't edit this user" msgstr "" -#: rhodecode/controllers/admin/users.py:414 +#: rhodecode/controllers/admin/users.py:409 msgid "The user participates as reviewer in pull requests and cannot be deleted. You can set the user to \"inactive\" instead of deleting it." msgstr "" -#: rhodecode/controllers/admin/users.py:550 +#: rhodecode/controllers/admin/users.py:545 msgid "User global permissions updated successfully" msgstr "" -#: rhodecode/controllers/admin/users.py:678 +#: rhodecode/controllers/admin/users.py:673 #, python-format msgid "An error occurred during ip saving:%s" msgstr "" -#: rhodecode/controllers/admin/users.py:693 +#: rhodecode/controllers/admin/users.py:688 msgid "An error occurred during ip saving" msgstr "" -#: rhodecode/controllers/admin/users.py:697 +#: rhodecode/controllers/admin/users.py:692 #, python-format msgid "Added ips %s to user whitelist" msgstr "" -#: rhodecode/controllers/admin/users.py:715 +#: rhodecode/controllers/admin/users.py:710 msgid "Removed ip address from user whitelist" msgstr "" +#: rhodecode/events/pullrequest.py:65 +msgid "pullrequest created" +msgstr "" + +#: rhodecode/events/pullrequest.py:74 +msgid "pullrequest closed" +msgstr "" + +#: rhodecode/events/pullrequest.py:83 +msgid "pullrequest commits updated" +msgstr "" + +#: rhodecode/events/pullrequest.py:92 +msgid "pullrequest review changed" +msgstr "" + +#: rhodecode/events/pullrequest.py:101 +msgid "pullrequest merged" +msgstr "" + +#: rhodecode/events/pullrequest.py:110 +msgid "pullrequest commented" +msgstr "" + +#: rhodecode/events/repo.py:135 +msgid "repository pre create" +msgstr "" + +#: rhodecode/events/repo.py:144 +msgid "repository created" +msgstr "" + +#: rhodecode/events/repo.py:153 +msgid "repository pre delete" +msgstr "" + +#: rhodecode/events/repo.py:162 +msgid "repository deleted" +msgstr "" + +#: rhodecode/events/repo.py:193 +msgid "repository pre pull" +msgstr "" + +#: rhodecode/events/repo.py:202 +msgid "repository pull" +msgstr "" + +#: rhodecode/events/repo.py:211 +msgid "repository pre push" +msgstr "" + +#: rhodecode/events/repo.py:222 +msgid "repository push" +msgstr "" + +#: rhodecode/events/user.py:34 +msgid "user registered" +msgstr "" + +#: rhodecode/events/user.py:48 +msgid "user pre create" +msgstr "" + +#: rhodecode/events/user.py:61 +msgid "user pre update" +msgstr "" + +#: rhodecode/integrations/schema.py:35 +msgid "Enable or disable this integration." +msgstr "" + +#: rhodecode/integrations/schema.py:42 +msgid "Short name for this integration." +msgstr "" + +#: rhodecode/integrations/schema.py:44 +msgid "Integration name" +msgstr "" + +#: rhodecode/integrations/views.py:172 +msgid "Integration {integration_name} deleted successfully." +msgstr "" + +#: rhodecode/integrations/views.py:200 +msgid "Errors exist when saving integration settings. Please check the form inputs." +msgstr "" + +#: rhodecode/integrations/views.py:220 +msgid "Integration {integration_name} updated successfully." +msgstr "" + +#: rhodecode/integrations/types/slack.py:45 +msgid "Slack service URL" +msgstr "" + +#: rhodecode/integrations/types/slack.py:46 +msgid "This can be setup at the <a href=\"https://my.slack.com/services/new/incoming-webhook/\">slack app manager</a>" +msgstr "" + +#: rhodecode/integrations/types/slack.py:59 rhodecode/templates/login.html:43 +#: rhodecode/templates/register.html:41 +#: rhodecode/templates/admin/admin_log.html:7 +#: rhodecode/templates/admin/my_account/my_account_profile.html:24 +#: rhodecode/templates/admin/my_account/my_account_profile_edit.html:21 +#: rhodecode/templates/admin/my_account/my_account_profile_edit.html:66 +#: rhodecode/templates/admin/users/user_add.html:35 +#: rhodecode/templates/admin/users/user_edit_profile.html:39 +#: rhodecode/templates/admin/users/users.html:88 +#: rhodecode/templates/base/base.html:306 +#: rhodecode/templates/debug_style/login.html:36 +#: rhodecode/templates/email_templates/user_registration.mako:23 +#: rhodecode/templates/users/user_profile.html:27 +msgid "Username" +msgstr "" + +#: rhodecode/integrations/types/slack.py:60 +msgid "Username to show notifications coming from." +msgstr "" + +#: rhodecode/integrations/types/slack.py:69 +msgid "Channel" +msgstr "" + +#: rhodecode/integrations/types/slack.py:70 +msgid "Channel to send notifications to." +msgstr "" + +#: rhodecode/integrations/types/slack.py:79 +msgid "Emoji" +msgstr "" + +#: rhodecode/integrations/types/slack.py:80 +msgid "Emoji to use eg. :studio_microphone:" +msgstr "" + +#: rhodecode/integrations/types/slack.py:107 +msgid "Slack" +msgstr "" + +#: rhodecode/integrations/types/webhook.py:41 +msgid "Webhook URL" +msgstr "" + +#: rhodecode/integrations/types/webhook.py:42 +msgid "URL of the webhook to receive POST event." +msgstr "" + +#: rhodecode/integrations/types/webhook.py:51 +msgid "Secret Token" +msgstr "" + +#: rhodecode/integrations/types/webhook.py:52 +msgid "String used to validate received payloads." +msgstr "" + +#: rhodecode/integrations/types/webhook.py:62 +msgid "Webhook" +msgstr "" + #: rhodecode/lib/action_parser.py:89 msgid "[deleted] repository" msgstr "" @@ -1411,11 +1583,15 @@ msgstr "" msgid "You need to be signed in to view this page" msgstr "" -#: rhodecode/lib/base.py:511 +#: rhodecode/lib/base.py:545 #, python-format msgid "The repository at %(repo_name)s cannot be located." msgstr "" +#: rhodecode/lib/diffs.py:56 +msgid "Click to comment" +msgstr "" + #: rhodecode/lib/diffs.py:71 msgid "Binary file" msgstr "" @@ -1428,36 +1604,40 @@ msgstr "" msgid "No changes detected" msgstr "" -#: rhodecode/lib/helpers.py:1434 +#: rhodecode/lib/diffs.py:631 +msgid "Click to select line" +msgstr "" + +#: rhodecode/lib/helpers.py:1481 #, python-format msgid " and %s more" msgstr "" -#: rhodecode/lib/helpers.py:1438 +#: rhodecode/lib/helpers.py:1485 msgid "No Files" msgstr "" -#: rhodecode/lib/helpers.py:1511 +#: rhodecode/lib/helpers.py:1558 msgid "new file" msgstr "" -#: rhodecode/lib/helpers.py:1514 +#: rhodecode/lib/helpers.py:1561 msgid "mod" msgstr "" -#: rhodecode/lib/helpers.py:1517 +#: rhodecode/lib/helpers.py:1564 msgid "del" msgstr "" -#: rhodecode/lib/helpers.py:1520 +#: rhodecode/lib/helpers.py:1567 msgid "rename" msgstr "" -#: rhodecode/lib/helpers.py:1525 +#: rhodecode/lib/helpers.py:1572 msgid "chmod" msgstr "" -#: rhodecode/lib/helpers.py:1767 +#: rhodecode/lib/helpers.py:1819 msgid "" "Example filter terms:\n" " repository:vcs\n" @@ -1476,89 +1656,91 @@ msgid "" " \"username:test AND repository:test*\"\n" msgstr "" -#: rhodecode/lib/helpers.py:1787 +#: rhodecode/lib/helpers.py:1839 #, python-format msgid "%s repository is not mapped to db perhaps it was created or renamed from the filesystem please run the application again in order to rescan repositories" msgstr "" -#: rhodecode/lib/utils2.py:453 +#: rhodecode/lib/utils2.py:454 #, python-format msgid "%d year" msgid_plural "%d years" msgstr[0] "" msgstr[1] "" -#: rhodecode/lib/utils2.py:454 +#: rhodecode/lib/utils2.py:455 #, python-format msgid "%d month" msgid_plural "%d months" msgstr[0] "" msgstr[1] "" -#: rhodecode/lib/utils2.py:455 +#: rhodecode/lib/utils2.py:456 #, python-format msgid "%d day" msgid_plural "%d days" msgstr[0] "" msgstr[1] "" -#: rhodecode/lib/utils2.py:456 +#: rhodecode/lib/utils2.py:457 #, python-format msgid "%d hour" msgid_plural "%d hours" msgstr[0] "" msgstr[1] "" -#: rhodecode/lib/utils2.py:457 +#: rhodecode/lib/utils2.py:458 #, python-format msgid "%d minute" msgid_plural "%d minutes" msgstr[0] "" msgstr[1] "" -#: rhodecode/lib/utils2.py:458 +#: rhodecode/lib/utils2.py:459 #, python-format msgid "%d second" msgid_plural "%d seconds" msgstr[0] "" msgstr[1] "" -#: rhodecode/lib/utils2.py:476 +#: rhodecode/lib/utils2.py:477 #, python-format msgid "in %s" msgstr "" -#: rhodecode/lib/utils2.py:482 +#: rhodecode/lib/utils2.py:483 #, python-format msgid "%s ago" msgstr "" -#: rhodecode/lib/utils2.py:492 +#: rhodecode/lib/utils2.py:493 #, python-format msgid "%s, %s ago" msgstr "" -#: rhodecode/lib/utils2.py:494 +#: rhodecode/lib/utils2.py:495 #, python-format msgid "in %s, %s" msgstr "" -#: rhodecode/lib/utils2.py:496 +#: rhodecode/lib/utils2.py:497 #, python-format msgid "%s and %s" msgstr "" -#: rhodecode/lib/utils2.py:498 +#: rhodecode/lib/utils2.py:499 #, python-format msgid "%s and %s ago" msgstr "" -#: rhodecode/lib/utils2.py:500 +#: rhodecode/lib/utils2.py:501 #, python-format msgid "in %s and %s" msgstr "" -#: rhodecode/lib/utils2.py:504 +#: rhodecode/lib/utils2.py:505 rhodecode/public/js/scripts.js:25035 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:49 +#: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:174 msgid "just now" msgstr "" @@ -1584,7 +1766,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:818 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:824 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:946 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:971 rhodecode/model/db.py:2291 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:971 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2293 rhodecode/model/db.py:2285 msgid "Repository no access" msgstr "" @@ -1610,7 +1793,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:819 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:825 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:947 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:972 rhodecode/model/db.py:2292 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:972 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2294 rhodecode/model/db.py:2286 msgid "Repository read access" msgstr "" @@ -1636,7 +1820,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:820 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:826 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:948 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:973 rhodecode/model/db.py:2293 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:973 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2295 rhodecode/model/db.py:2287 msgid "Repository write access" msgstr "" @@ -1662,7 +1847,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:821 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:827 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:949 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:974 rhodecode/model/db.py:2294 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:974 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2296 rhodecode/model/db.py:2288 msgid "Repository admin access" msgstr "" @@ -1728,7 +1914,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:839 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:845 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:967 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:992 rhodecode/model/db.py:2312 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:992 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2314 rhodecode/model/db.py:2306 msgid "Repository creation disabled" msgstr "" @@ -1754,7 +1941,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:840 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:846 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:968 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:993 rhodecode/model/db.py:2313 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:993 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2315 rhodecode/model/db.py:2307 msgid "Repository creation enabled" msgstr "" @@ -1780,7 +1968,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:844 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:850 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:972 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:997 rhodecode/model/db.py:2317 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:997 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2319 rhodecode/model/db.py:2311 msgid "Repository forking disabled" msgstr "" @@ -1806,7 +1995,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:845 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:851 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:973 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:998 rhodecode/model/db.py:2318 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:998 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2320 rhodecode/model/db.py:2312 msgid "Repository forking enabled" msgstr "" @@ -1853,7 +2043,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:1186 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:1196 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:1318 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1343 rhodecode/model/db.py:2950 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1343 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2952 rhodecode/model/db.py:2944 msgid "Not Reviewed" msgstr "" @@ -1879,7 +2070,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:1187 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:1197 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:1319 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1344 rhodecode/model/db.py:2951 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1344 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2953 rhodecode/model/db.py:2945 msgid "Approved" msgstr "" @@ -1905,7 +2097,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:1188 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:1198 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:1320 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1345 rhodecode/model/db.py:2952 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1345 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2954 rhodecode/model/db.py:2946 msgid "Rejected" msgstr "" @@ -1931,7 +2124,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:1189 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:1199 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:1321 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1346 rhodecode/model/db.py:2953 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1346 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2955 rhodecode/model/db.py:2947 msgid "Under Review" msgstr "" @@ -1954,7 +2148,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:823 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:829 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:951 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:976 rhodecode/model/db.py:2296 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:976 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2298 rhodecode/model/db.py:2290 msgid "Repository group no access" msgstr "" @@ -1977,7 +2172,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:824 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:830 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:952 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:977 rhodecode/model/db.py:2297 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:977 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2299 rhodecode/model/db.py:2291 msgid "Repository group read access" msgstr "" @@ -2000,7 +2196,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:825 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:831 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:953 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:978 rhodecode/model/db.py:2298 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:978 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2300 rhodecode/model/db.py:2292 msgid "Repository group write access" msgstr "" @@ -2023,7 +2220,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:826 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:832 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:954 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:979 rhodecode/model/db.py:2299 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:979 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2301 rhodecode/model/db.py:2293 msgid "Repository group admin access" msgstr "" @@ -2045,7 +2243,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:828 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:834 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:956 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:981 rhodecode/model/db.py:2301 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:981 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2303 rhodecode/model/db.py:2295 msgid "User group no access" msgstr "" @@ -2067,7 +2266,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:829 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:835 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:957 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:982 rhodecode/model/db.py:2302 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:982 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2304 rhodecode/model/db.py:2296 msgid "User group read access" msgstr "" @@ -2089,7 +2289,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:830 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:836 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:958 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:983 rhodecode/model/db.py:2303 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:983 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2305 rhodecode/model/db.py:2297 msgid "User group write access" msgstr "" @@ -2111,7 +2312,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:831 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:837 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:959 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:984 rhodecode/model/db.py:2304 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:984 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2306 rhodecode/model/db.py:2298 msgid "User group admin access" msgstr "" @@ -2133,7 +2335,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:833 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:839 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:961 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:986 rhodecode/model/db.py:2306 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:986 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2308 rhodecode/model/db.py:2300 msgid "Repository Group creation disabled" msgstr "" @@ -2155,7 +2358,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:834 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:840 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:962 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:987 rhodecode/model/db.py:2307 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:987 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2309 rhodecode/model/db.py:2301 msgid "Repository Group creation enabled" msgstr "" @@ -2177,7 +2381,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:836 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:842 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:964 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:989 rhodecode/model/db.py:2309 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:989 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2311 rhodecode/model/db.py:2303 msgid "User Group creation disabled" msgstr "" @@ -2199,7 +2404,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:837 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:843 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:965 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:990 rhodecode/model/db.py:2310 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:990 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2312 rhodecode/model/db.py:2304 msgid "User Group creation enabled" msgstr "" @@ -2221,7 +2427,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:847 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:853 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:975 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1000 rhodecode/model/db.py:2320 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1000 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2322 rhodecode/model/db.py:2314 msgid "Registration disabled" msgstr "" @@ -2243,7 +2450,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:848 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:854 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:976 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1001 rhodecode/model/db.py:2321 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1001 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2323 rhodecode/model/db.py:2315 msgid "User Registration with manual account activation" msgstr "" @@ -2265,7 +2473,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:849 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:855 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:977 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1002 rhodecode/model/db.py:2322 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1002 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2324 rhodecode/model/db.py:2316 msgid "User Registration with automatic account activation" msgstr "" @@ -2287,7 +2496,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:851 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:857 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:979 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1004 rhodecode/model/db.py:2324 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1004 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2326 rhodecode/model/db.py:2318 msgid "Manual activation of external account" msgstr "" @@ -2309,7 +2519,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:852 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:858 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:980 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1005 rhodecode/model/db.py:2325 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1005 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2327 rhodecode/model/db.py:2319 msgid "Automatic activation of external account" msgstr "" @@ -2325,7 +2536,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:841 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:847 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:969 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:994 rhodecode/model/db.py:2314 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:994 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2316 rhodecode/model/db.py:2308 msgid "Repository creation enabled with write permission to a repository group" msgstr "" @@ -2341,7 +2553,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:842 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:848 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:970 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:995 rhodecode/model/db.py:2315 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:995 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2317 rhodecode/model/db.py:2309 msgid "Repository creation disabled with write permission to a repository group" msgstr "" @@ -2354,7 +2567,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:816 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:822 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:944 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:969 rhodecode/model/db.py:2289 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:969 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2291 rhodecode/model/db.py:2283 msgid "RhodeCode Super Administrator" msgstr "" @@ -2365,7 +2579,8 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:854 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:860 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:982 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1007 rhodecode/model/db.py:2327 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1007 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2329 rhodecode/model/db.py:2321 msgid "Inherit object permissions from default user disabled" msgstr "" @@ -2376,10 +2591,35 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_3_2_0_0.py:855 #: rhodecode/lib/dbmigrate/schema/db_3_3_0_0.py:861 #: rhodecode/lib/dbmigrate/schema/db_3_5_0_0.py:983 -#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1008 rhodecode/model/db.py:2328 +#: rhodecode/lib/dbmigrate/schema/db_3_7_0_0.py:1008 +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2330 rhodecode/model/db.py:2322 msgid "Inherit object permissions from default user enabled" msgstr "" +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:909 rhodecode/model/db.py:910 +msgid "all" +msgstr "" + +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:910 rhodecode/model/db.py:911 +msgid "http/web interface" +msgstr "" + +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:911 rhodecode/model/db.py:912 +msgid "vcs (git/hg/svn protocol)" +msgstr "" + +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:912 rhodecode/model/db.py:913 +msgid "api calls" +msgstr "" + +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:913 rhodecode/model/db.py:914 +msgid "feed access" +msgstr "" + +#: rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py:2069 rhodecode/model/db.py:2061 +msgid "No parent" +msgstr "" + #: rhodecode/lib/index/whoosh.py:148 msgid "Invalid search query. Try quoting it." msgstr "" @@ -2424,48 +2664,32 @@ msgstr "" msgid "Your password reset link was sent" msgstr "" -#: rhodecode/login/views.py:333 +#: rhodecode/login/views.py:336 msgid "Your password reset was successful, a new password has been sent to your email" msgstr "" -#: rhodecode/model/db.py:909 -msgid "all" -msgstr "" - -#: rhodecode/model/db.py:910 -msgid "http/web interface" -msgstr "" - -#: rhodecode/model/db.py:911 -msgid "vcs (git/hg protocol)" -msgstr "" - -#: rhodecode/model/db.py:912 -msgid "api calls" -msgstr "" - -#: rhodecode/model/db.py:913 -msgid "feed access" -msgstr "" - -#: rhodecode/model/db.py:2067 -msgid "No parent" -msgstr "" - -#: rhodecode/model/forms.py:66 +#: rhodecode/model/comment.py:263 +msgid "made a comment" +msgstr "" + +#: rhodecode/model/comment.py:264 +msgid "Refresh page" +msgstr "" + +#: rhodecode/model/forms.py:85 msgid "Please enter a login" msgstr "" -#: rhodecode/model/forms.py:67 +#: rhodecode/model/forms.py:86 #, python-format msgid "Enter a value %(min)i characters long or more" msgstr "" -#: rhodecode/model/forms.py:76 +#: rhodecode/model/forms.py:95 msgid "Please enter a password" msgstr "" -#: rhodecode/model/forms.py:77 +#: rhodecode/model/forms.py:96 #, python-format msgid "Enter %(min)i characters or more" msgstr "" @@ -2574,43 +2798,43 @@ msgid "" " %(pr_title)s" msgstr "" -#: rhodecode/model/pull_request.py:448 +#: rhodecode/model/pull_request.py:449 msgid "Pull request merged and closed" msgstr "" -#: rhodecode/model/pull_request.py:867 +#: rhodecode/model/pull_request.py:874 msgid "Server-side pull request merging is disabled." msgstr "" -#: rhodecode/model/pull_request.py:869 +#: rhodecode/model/pull_request.py:876 msgid "This pull request is closed." msgstr "" -#: rhodecode/model/pull_request.py:880 +#: rhodecode/model/pull_request.py:887 msgid "Pull request merging is not supported." msgstr "" -#: rhodecode/model/pull_request.py:898 +#: rhodecode/model/pull_request.py:905 msgid "Target repository large files support is disabled." msgstr "" -#: rhodecode/model/pull_request.py:901 +#: rhodecode/model/pull_request.py:908 msgid "Source repository large files support is disabled." msgstr "" -#: rhodecode/model/pull_request.py:1050 rhodecode/model/scm.py:791 +#: rhodecode/model/pull_request.py:1058 rhodecode/model/scm.py:788 msgid "Bookmarks" msgstr "" -#: rhodecode/model/pull_request.py:1055 +#: rhodecode/model/pull_request.py:1063 msgid "Commit IDs" msgstr "" -#: rhodecode/model/pull_request.py:1058 +#: rhodecode/model/pull_request.py:1066 msgid "Closed Branches" msgstr "" -#: rhodecode/model/scm.py:773 +#: rhodecode/model/scm.py:770 msgid "latest tip" msgstr "" @@ -2811,6 +3035,7 @@ msgid "Revisions %(revs)s are already pa msgstr "" #: rhodecode/model/validators.py:933 +#: rhodecode/model/validation_schema/validators.py:14 msgid "Please enter a valid IPv4 or IpV6 address" msgstr "" @@ -2824,31 +3049,389 @@ msgid "Key name can only consist of lett msgstr "" #: rhodecode/model/validators.py:976 -msgid "Filename cannot be inside a directory" -msgstr "" - -#: rhodecode/model/validators.py:992 #, python-format msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name" msgstr "" -#: rhodecode/model/validators.py:995 +#: rhodecode/model/validators.py:979 #, python-format msgid "The plugin \"%(plugin_id)s\" is missing an includeme function." msgstr "" -#: rhodecode/model/validators.py:998 +#: rhodecode/model/validators.py:982 #, python-format msgid "Can not load plugin \"%(plugin_id)s\"" msgstr "" -#: rhodecode/model/validators.py:1000 +#: rhodecode/model/validators.py:984 #, python-format msgid "No plugin available with ID \"%(plugin_id)s\"" msgstr "" -#: rhodecode/model/validators.py:1067 -msgid "This gistid is already in use" +#: rhodecode/model/validation_schema/schemas/gist_schema.py:89 +msgid "Gist with name {} already exists" +msgstr "" + +#: rhodecode/model/validation_schema/schemas/gist_schema.py:95 +msgid "Filename {} cannot be inside a directory" +msgstr "" + +#: rhodecode/model/validation_schema/schemas/gist_schema.py:132 +msgid "Duplicated value for filename found: `{}`" +msgstr "" + +#: rhodecode/public/js/scripts.js:23039 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:16 +#: rhodecode/public/js/src/plugins/jquery.autocomplete.js:87 +msgid "No results" +msgstr "" + +#: rhodecode/public/js/scripts.js:24970 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:66 +#: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:109 +msgid "{0} year" +msgstr "" + +#: rhodecode/public/js/scripts.js:24971 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:62 +#: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:110 +msgid "{0} month" +msgstr "" + +#: rhodecode/public/js/scripts.js:24972 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:57 +#: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:111 +msgid "{0} day" +msgstr "" + +#: rhodecode/public/js/scripts.js:24973 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:59 +#: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:112 +msgid "{0} hour" +msgstr "" + +#: rhodecode/public/js/scripts.js:24974 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:61 +#: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:113 +msgid "{0} min" +msgstr "" + +#: rhodecode/public/js/scripts.js:24975 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:65 +#: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:114 +msgid "{0} sec" +msgstr "" + +#: rhodecode/public/js/scripts.js:24995 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:46 +#: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:134 +msgid "in {0}" +msgstr "" + +#: rhodecode/public/js/scripts.js:25003 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:54 +#: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:142 +msgid "{0} ago" +msgstr "" + +#: rhodecode/public/js/scripts.js:25015 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:68 +#: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:154 +msgid "{0}, {1} ago" +msgstr "" + +#: rhodecode/public/js/scripts.js:25017 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:48 +#: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:156 +msgid "in {0}, {1}" +msgstr "" + +#: rhodecode/public/js/scripts.js:25021 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:55 +#: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:160 +msgid "{0} and {1}" +msgstr "" + +#: rhodecode/public/js/scripts.js:25023 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:56 +#: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:162 +msgid "{0} and {1} ago" +msgstr "" + +#: rhodecode/public/js/scripts.js:25025 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:47 +#: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:164 +msgid "in {0} and {1}" +msgstr "" + +#: rhodecode/public/js/scripts.js:39304 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:7 +#: rhodecode/public/js/rhodecode/i18n/select2/translations.js:4 +msgid "Loading more results..." +msgstr "" + +#: rhodecode/public/js/scripts.js:39307 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:27 +#: rhodecode/public/js/rhodecode/i18n/select2/translations.js:7 +msgid "Searching..." +msgstr "" + +#: rhodecode/public/js/scripts.js:39310 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:11 +#: rhodecode/public/js/rhodecode/i18n/select2/translations.js:10 +msgid "No matches found" +msgstr "" + +#: rhodecode/public/js/scripts.js:39313 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:6 +#: rhodecode/public/js/rhodecode/i18n/select2/translations.js:13 +msgid "Loading failed" +msgstr "" + +#: rhodecode/public/js/scripts.js:39317 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:20 +#: rhodecode/public/js/rhodecode/i18n/select2/translations.js:17 +msgid "One result is available, press enter to select it." +msgstr "" + +#: rhodecode/public/js/scripts.js:39319 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:64 +#: rhodecode/public/js/rhodecode/i18n/select2/translations.js:19 +msgid "{0} results are available, use up and down arrow keys to navigate." +msgstr "" + +#: rhodecode/public/js/scripts.js:39324 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:25 +#: rhodecode/public/js/rhodecode/i18n/select2/translations.js:24 +msgid "Please enter {0} or more character" +msgstr "" + +#: rhodecode/public/js/scripts.js:39326 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:26 +#: rhodecode/public/js/rhodecode/i18n/select2/translations.js:26 +msgid "Please enter {0} or more characters" +msgstr "" + +#: rhodecode/public/js/scripts.js:39331 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:23 +#: rhodecode/public/js/rhodecode/i18n/select2/translations.js:31 +msgid "Please delete {0} character" +msgstr "" + +#: rhodecode/public/js/scripts.js:39333 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:24 +#: rhodecode/public/js/rhodecode/i18n/select2/translations.js:33 +msgid "Please delete {0} characters" +msgstr "" + +#: rhodecode/public/js/scripts.js:39337 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:40 +#: rhodecode/public/js/rhodecode/i18n/select2/translations.js:37 +msgid "You can only select {0} item" +msgstr "" + +#: rhodecode/public/js/scripts.js:39339 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:41 +#: rhodecode/public/js/rhodecode/i18n/select2/translations.js:39 +msgid "You can only select {0} items" +msgstr "" + +#: rhodecode/public/js/scripts.js:40911 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:29 +#: rhodecode/public/js/src/rhodecode/codemirror.js:369 +msgid "Set status to Approved" +msgstr "" + +#: rhodecode/public/js/scripts.js:40929 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:30 +#: rhodecode/public/js/src/rhodecode/codemirror.js:387 +msgid "Set status to Rejected" +msgstr "" + +#: rhodecode/public/js/scripts.js:41308 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:2 +#: rhodecode/public/js/src/rhodecode/comments.js:235 +msgid "Add another comment" +msgstr "" + +#: rhodecode/public/js/scripts.js:41526 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:35 +#: rhodecode/public/js/src/rhodecode/comments.js:453 +msgid "Status Review" +msgstr "" + +#: rhodecode/public/js/scripts.js:41540 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:3 +#: rhodecode/public/js/src/rhodecode/comments.js:467 +msgid "Comment text will be set automatically based on currently selected status ({0}) ..." +msgstr "" + +#: rhodecode/public/js/scripts.js:41653 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:37 +#: rhodecode/public/js/src/rhodecode/comments.js:580 +msgid "Submitting..." +msgstr "" + +#: rhodecode/public/js/scripts.js:41703 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:5 +#: rhodecode/public/js/src/rhodecode/comments.js:630 +#: rhodecode/templates/files/files_browser_tree.html:47 +msgid "Loading ..." +msgstr "" + +#: rhodecode/public/js/scripts.js:41903 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:51 +#: rhodecode/public/js/src/rhodecode/files.js:150 +msgid "truncated result" +msgstr "" + +#: rhodecode/public/js/scripts.js:41905 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:52 +#: rhodecode/public/js/src/rhodecode/files.js:152 +msgid "truncated results" +msgstr "" + +#: rhodecode/public/js/scripts.js:41914 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:12 +#: rhodecode/public/js/src/rhodecode/files.js:161 +msgid "No matching files" +msgstr "" + +#: rhodecode/public/js/scripts.js:42049 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:28 +#: rhodecode/public/js/src/rhodecode/files.js:296 +msgid "Selection link" +msgstr "" + +#: rhodecode/public/js/scripts.js:42089 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:36 +#: rhodecode/public/js/src/rhodecode/followers.js:26 +msgid "Stop following this repository" +msgstr "" + +#: rhodecode/public/js/scripts.js:42090 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:38 +#: rhodecode/public/js/src/rhodecode/followers.js:27 +msgid "Unfollow" +msgstr "" + +#: rhodecode/public/js/scripts.js:42099 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:34 +#: rhodecode/public/js/src/rhodecode/followers.js:36 +msgid "Start following this repository" +msgstr "" + +#: rhodecode/public/js/scripts.js:42100 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:4 +#: rhodecode/public/js/src/rhodecode/followers.js:37 +msgid "Follow" +msgstr "" + +#: rhodecode/public/js/scripts.js:43049 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:44 +#: rhodecode/public/js/src/rhodecode.js:142 +msgid "file" +msgstr "" + +#: rhodecode/public/js/scripts.js:43069 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:31 +#: rhodecode/public/js/src/rhodecode.js:162 +msgid "Show more" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:8 +msgid "No bookmarks available yet." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:9 +msgid "No branches available yet." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:10 +msgid "No gists available yet." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:13 +msgid "No pull requests available yet." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:14 +msgid "No repositories available yet." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:15 +msgid "No repository groups available yet." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:17 +msgid "No tags available yet." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:18 +msgid "No user groups available yet." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:19 +msgid "No users available yet." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:21 +#: rhodecode/templates/changelog/changelog.html:62 +msgid "Open new pull request" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:22 +msgid "Open new pull request for selected commit" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:32 +msgid "Show selected commit __S" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:33 +msgid "Show selected commits __S ... __E" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:39 +msgid "Updating..." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:42 +#: rhodecode/templates/admin/auth/auth_settings.html:71 +msgid "disabled" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:43 +#: rhodecode/templates/admin/auth/auth_settings.html:71 +msgid "enabled" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:45 +msgid "files" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:50 +msgid "specify commit" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:53 +msgid "{0} active out of {1} users" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:58 +msgid "{0} days" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:60 +msgid "{0} hours" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:63 +msgid "{0} months" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:67 +msgid "{0} years" msgstr "" #: rhodecode/templates/index.html:5 @@ -2909,7 +3492,7 @@ msgstr "" #: rhodecode/templates/base/perms_summary.html:102 #: rhodecode/templates/bookmarks/bookmarks.html:59 #: rhodecode/templates/branches/branches.html:58 -#: rhodecode/templates/files/files_browser.html:49 +#: rhodecode/templates/files/files_browser_tree.html:5 #: rhodecode/templates/pullrequests/pullrequests.html:100 #: rhodecode/templates/tags/tags.html:59 msgid "Name" @@ -2918,6 +3501,7 @@ msgstr "" #: rhodecode/templates/index_base.html:100 #: rhodecode/templates/index_base.html:125 #: rhodecode/templates/admin/gists/index.html:114 +#: rhodecode/templates/admin/integrations/list.html:63 #: rhodecode/templates/admin/my_account/my_account_auth_tokens.html:77 #: rhodecode/templates/admin/repo_groups/repo_group_add.html:45 #: rhodecode/templates/admin/repo_groups/repo_group_edit_settings.html:42 @@ -2933,8 +3517,9 @@ msgstr "" #: rhodecode/templates/base/issue_tracker_settings.html:10 #: rhodecode/templates/changeset/changeset.html:53 #: rhodecode/templates/compare/compare_commits.html:24 +#: rhodecode/templates/email_templates/commit_comment.mako:82 #: rhodecode/templates/email_templates/pull_request_review.mako:30 -#: rhodecode/templates/email_templates/pull_request_review.mako:67 +#: rhodecode/templates/email_templates/pull_request_review.mako:51 #: rhodecode/templates/files/file_tree_detail.html:5 #: rhodecode/templates/files/file_tree_detail.html:12 #: rhodecode/templates/forks/fork.html:48 @@ -2972,12 +3557,12 @@ msgstr "" #: rhodecode/templates/admin/repos/repos.html:63 #: rhodecode/templates/bookmarks/bookmarks.html:66 #: rhodecode/templates/branches/branches.html:65 -#: rhodecode/templates/changelog/changelog.html:106 -#: rhodecode/templates/changelog/changelog_summary_data.html:6 +#: rhodecode/templates/changelog/changelog.html:104 +#: rhodecode/templates/changelog/changelog_summary_data.html:8 #: rhodecode/templates/changeset/changeset.html:36 #: rhodecode/templates/compare/compare_commits.html:22 -#: rhodecode/templates/email_templates/commit_comment.mako:16 #: rhodecode/templates/email_templates/commit_comment.mako:45 +#: rhodecode/templates/email_templates/commit_comment.mako:81 #: rhodecode/templates/search/search_commit.html:6 #: rhodecode/templates/tags/tags.html:66 msgid "Commit" @@ -2993,7 +3578,7 @@ msgid "Home" msgstr "" #: rhodecode/templates/login.html:5 rhodecode/templates/login.html:35 -#: rhodecode/templates/login.html:64 rhodecode/templates/base/base.html:328 +#: rhodecode/templates/login.html:64 rhodecode/templates/base/base.html:329 #: rhodecode/templates/debug_style/login.html:60 msgid "Sign In" msgstr "" @@ -3002,20 +3587,6 @@ msgstr "" msgid "Go to the registration page to create a new account." msgstr "" -#: rhodecode/templates/login.html:43 rhodecode/templates/register.html:41 -#: rhodecode/templates/admin/admin_log.html:5 -#: rhodecode/templates/admin/my_account/my_account_profile.html:24 -#: rhodecode/templates/admin/my_account/my_account_profile_edit.html:21 -#: rhodecode/templates/admin/my_account/my_account_profile_edit.html:66 -#: rhodecode/templates/admin/users/user_add.html:35 -#: rhodecode/templates/admin/users/user_edit_profile.html:39 -#: rhodecode/templates/admin/users/users.html:89 -#: rhodecode/templates/base/base.html:305 -#: rhodecode/templates/debug_style/login.html:36 -#: rhodecode/templates/users/user_profile.html:27 -msgid "Username" -msgstr "" - #: rhodecode/templates/login.html:58 msgid "Remember me" msgstr "" @@ -3051,7 +3622,7 @@ msgid "Send password reset email" msgstr "" #: rhodecode/templates/password_reset.html:60 -msgid "Password reset link will be send to matching email address" +msgid "Password reset link will be sent to matching email address" msgstr "" #: rhodecode/templates/register.html:35 @@ -3072,7 +3643,7 @@ msgstr "" #: rhodecode/templates/admin/my_account/my_account_profile_edit.html:76 #: rhodecode/templates/admin/users/user_add.html:68 #: rhodecode/templates/admin/users/user_edit_profile.html:47 -#: rhodecode/templates/admin/users/users.html:93 +#: rhodecode/templates/admin/users/users.html:92 msgid "First Name" msgstr "" @@ -3082,7 +3653,7 @@ msgstr "" #: rhodecode/templates/admin/my_account/my_account_profile_edit.html:85 #: rhodecode/templates/admin/users/user_add.html:77 #: rhodecode/templates/admin/users/user_edit_profile.html:56 -#: rhodecode/templates/admin/users/users.html:95 +#: rhodecode/templates/admin/users/users.html:94 msgid "Last Name" msgstr "" @@ -3096,7 +3667,7 @@ msgstr "" #: rhodecode/templates/admin/admin.html:5 #: rhodecode/templates/admin/admin.html:15 -#: rhodecode/templates/base/base.html:78 +#: rhodecode/templates/base/base.html:77 msgid "Admin journal" msgstr "" @@ -3121,17 +3692,17 @@ msgstr[1] "" msgid "Example Queries" msgstr "" -#: rhodecode/templates/admin/admin_log.html:6 +#: rhodecode/templates/admin/admin_log.html:8 #: rhodecode/templates/admin/my_account/my_account_repos.html:37 #: rhodecode/templates/admin/repo_groups/repo_groups.html:62 #: rhodecode/templates/admin/repos/repo_edit_fields.html:13 #: rhodecode/templates/admin/repos/repos.html:69 #: rhodecode/templates/admin/user_groups/user_groups.html:66 -#: rhodecode/templates/admin/users/users.html:106 +#: rhodecode/templates/admin/users/users.html:105 msgid "Action" msgstr "" -#: rhodecode/templates/admin/admin_log.html:7 +#: rhodecode/templates/admin/admin_log.html:9 #: rhodecode/templates/admin/defaults/defaults.html:31 #: rhodecode/templates/admin/permissions/permissions_objects.html:13 #: rhodecode/templates/search/search_commit.html:5 @@ -3139,18 +3710,18 @@ msgstr "" msgid "Repository" msgstr "" -#: rhodecode/templates/admin/admin_log.html:8 +#: rhodecode/templates/admin/admin_log.html:10 #: rhodecode/templates/bookmarks/bookmarks.html:61 #: rhodecode/templates/branches/branches.html:60 #: rhodecode/templates/tags/tags.html:61 msgid "Date" msgstr "" -#: rhodecode/templates/admin/admin_log.html:9 +#: rhodecode/templates/admin/admin_log.html:11 msgid "From IP" msgstr "" -#: rhodecode/templates/admin/admin_log.html:44 +#: rhodecode/templates/admin/admin_log.html:46 msgid "No actions yet" msgstr "" @@ -3162,6 +3733,9 @@ msgstr "" #: rhodecode/templates/admin/auth/auth_settings.html:12 #: rhodecode/templates/admin/auth/plugin_settings.html:12 #: rhodecode/templates/admin/defaults/defaults.html:12 +#: rhodecode/templates/admin/integrations/base.html:19 +#: rhodecode/templates/admin/integrations/edit.html:15 +#: rhodecode/templates/admin/integrations/list.html:8 #: rhodecode/templates/admin/permissions/permissions.html:12 #: rhodecode/templates/admin/repo_groups/repo_group_add.html:12 #: rhodecode/templates/admin/repo_groups/repo_group_edit.html:12 @@ -3179,9 +3753,9 @@ msgstr "" #: rhodecode/templates/admin/users/user_add.html:11 #: rhodecode/templates/admin/users/user_edit.html:12 #: rhodecode/templates/admin/users/users.html:13 -#: rhodecode/templates/admin/users/users.html:102 -#: rhodecode/templates/base/base.html:405 -#: rhodecode/templates/base/base.html:412 +#: rhodecode/templates/admin/users/users.html:101 +#: rhodecode/templates/base/base.html:406 +#: rhodecode/templates/base/base.html:413 msgid "Admin" msgstr "" @@ -3206,14 +3780,6 @@ msgstr "" msgid "Available Built-in Plugins" msgstr "" -#: rhodecode/templates/admin/auth/auth_settings.html:71 -msgid "enabled" -msgstr "" - -#: rhodecode/templates/admin/auth/auth_settings.html:71 -msgid "disabled" -msgstr "" - #: rhodecode/templates/admin/auth/auth_settings.html:81 #: rhodecode/templates/admin/auth/plugin_settings.html:87 #: rhodecode/templates/admin/defaults/defaults_repositories.html:63 @@ -3254,6 +3820,7 @@ msgstr "" #: rhodecode/templates/admin/defaults/defaults_repositories.html:14 #: rhodecode/templates/admin/gists/index.html:110 +#: rhodecode/templates/admin/integrations/list.html:64 #: rhodecode/templates/admin/repos/repo_add_base.html:62 #: rhodecode/templates/admin/repos/repo_edit_fields.html:12 msgid "Type" @@ -3320,18 +3887,18 @@ msgstr "" msgid "Gist access level" msgstr "" -#: rhodecode/templates/admin/gists/edit.html:59 +#: rhodecode/templates/admin/gists/edit.html:62 #: rhodecode/templates/admin/gists/new.html:50 #: rhodecode/templates/files/files_add.html:74 #: rhodecode/templates/files/files_edit.html:78 msgid "plain" msgstr "" -#: rhodecode/templates/admin/gists/edit.html:103 +#: rhodecode/templates/admin/gists/edit.html:107 msgid "Update Gist" msgstr "" -#: rhodecode/templates/admin/gists/edit.html:104 +#: rhodecode/templates/admin/gists/edit.html:108 #: rhodecode/templates/base/issue_tracker_settings.html:74 #: rhodecode/templates/changeset/changeset_file_comment.html:139 #: rhodecode/templates/files/files_add.html:102 @@ -3392,14 +3959,14 @@ msgstr "" #: rhodecode/templates/admin/gists/index.html:108 #: rhodecode/templates/admin/my_account/my_account_pullrequests.html:24 -#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:87 +#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:89 #: rhodecode/templates/bookmarks/bookmarks.html:63 #: rhodecode/templates/branches/branches.html:62 -#: rhodecode/templates/changelog/changelog.html:102 -#: rhodecode/templates/changelog/changelog_summary_data.html:10 +#: rhodecode/templates/changelog/changelog.html:110 +#: rhodecode/templates/changelog/changelog_summary_data.html:11 #: rhodecode/templates/changeset/changeset.html:164 #: rhodecode/templates/compare/compare_commits.html:21 -#: rhodecode/templates/files/files_browser.html:53 +#: rhodecode/templates/files/files_browser_tree.html:9 #: rhodecode/templates/pullrequests/pullrequest_show.html:169 #: rhodecode/templates/pullrequests/pullrequests.html:102 #: rhodecode/templates/search/search_commit.html:16 @@ -3482,9 +4049,10 @@ msgid "Gist" msgstr "" #: rhodecode/templates/admin/gists/show.html:49 +#: rhodecode/templates/admin/integrations/list.html:110 #: rhodecode/templates/admin/my_account/my_account_auth_tokens.html:56 #: rhodecode/templates/admin/my_account/my_account_emails.html:32 -#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:61 +#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:63 #: rhodecode/templates/admin/permissions/permissions_ips.html:26 #: rhodecode/templates/admin/repos/repo_edit_fields.html:25 #: rhodecode/templates/admin/settings/settings_hooks.html:46 @@ -3496,10 +4064,10 @@ msgstr "" #: rhodecode/templates/base/vcs_settings.html:172 #: rhodecode/templates/changeset/changeset_file_comment.html:49 #: rhodecode/templates/changeset/changeset_file_comment.html:99 -#: rhodecode/templates/data_table/_dt_elements.html:117 -#: rhodecode/templates/data_table/_dt_elements.html:174 -#: rhodecode/templates/data_table/_dt_elements.html:188 -#: rhodecode/templates/data_table/_dt_elements.html:200 +#: rhodecode/templates/data_table/_dt_elements.html:119 +#: rhodecode/templates/data_table/_dt_elements.html:176 +#: rhodecode/templates/data_table/_dt_elements.html:190 +#: rhodecode/templates/data_table/_dt_elements.html:202 #: rhodecode/templates/debug_style/buttons.html:132 #: rhodecode/templates/files/files_source.html:33 #: rhodecode/templates/files/files_source.html:37 @@ -3512,14 +4080,15 @@ msgid "Confirm to delete this Gist" msgstr "" #: rhodecode/templates/admin/gists/show.html:56 +#: rhodecode/templates/admin/integrations/list.html:103 #: rhodecode/templates/admin/my_account/my_account_profile.html:5 #: rhodecode/templates/base/issue_tracker_settings.html:61 #: rhodecode/templates/changeset/changeset_file_comment.html:145 #: rhodecode/templates/changeset/changeset_file_comment.html:292 -#: rhodecode/templates/data_table/_dt_elements.html:112 -#: rhodecode/templates/data_table/_dt_elements.html:170 -#: rhodecode/templates/data_table/_dt_elements.html:183 -#: rhodecode/templates/data_table/_dt_elements.html:196 +#: rhodecode/templates/data_table/_dt_elements.html:114 +#: rhodecode/templates/data_table/_dt_elements.html:172 +#: rhodecode/templates/data_table/_dt_elements.html:185 +#: rhodecode/templates/data_table/_dt_elements.html:198 #: rhodecode/templates/debug_style/buttons.html:128 #: rhodecode/templates/files/files_add.html:204 #: rhodecode/templates/files/files_edit.html:165 @@ -3549,8 +4118,44 @@ msgstr "" msgid "Show as raw" msgstr "" +#: rhodecode/templates/admin/integrations/base.html:12 +msgid "Integrations settings" +msgstr "" + +#: rhodecode/templates/admin/integrations/edit.html:17 +#: rhodecode/templates/admin/integrations/list.html:10 +#: rhodecode/templates/admin/repo_groups/repo_group_edit.html:44 +#: rhodecode/templates/admin/repos/repo_edit.html:15 +#: rhodecode/templates/admin/repos/repo_edit.html:43 +#: rhodecode/templates/admin/settings/settings.html:14 +#: rhodecode/templates/admin/user_groups/user_group_edit.html:33 +#: rhodecode/templates/base/base.html:86 rhodecode/templates/base/base.html:251 +msgid "Settings" +msgstr "" + +#: rhodecode/templates/admin/integrations/edit.html:36 +#, python-format +msgid "Create new %(integration_type)s integration" +msgstr "" + +#: rhodecode/templates/admin/integrations/list.html:31 +msgid "Create new integration" +msgstr "" + +#: rhodecode/templates/admin/integrations/list.html:56 +msgid "Current integrations" +msgstr "" + +#: rhodecode/templates/admin/integrations/list.html:65 +msgid "Actions" +msgstr "" + +#: rhodecode/templates/admin/integrations/list.html:89 +msgid "unknown integration" +msgstr "" + #: rhodecode/templates/admin/my_account/my_account.html:5 -#: rhodecode/templates/base/base.html:342 +#: rhodecode/templates/base/base.html:343 msgid "My account" msgstr "" @@ -3586,7 +4191,7 @@ msgstr "" #: rhodecode/templates/admin/my_account/my_account.html:40 #: rhodecode/templates/admin/notifications/notifications.html:33 -#: rhodecode/templates/base/base.html:242 +#: rhodecode/templates/base/base.html:243 msgid "Pull Requests" msgstr "" @@ -3594,6 +4199,10 @@ msgstr "" msgid "My Permissions" msgstr "" +#: rhodecode/templates/admin/my_account/my_account.html:42 +msgid "My Live Notifications" +msgstr "" + #: rhodecode/templates/admin/my_account/my_account_auth_tokens.html:3 msgid "Authentication Tokens" msgstr "" @@ -3603,7 +4212,7 @@ msgid "Built-in tokens can be used to au msgstr "" #: rhodecode/templates/admin/my_account/my_account_auth_tokens.html:8 -msgid "Each token can have a role. VCS tokens can be used together with the authtoken auth plugin for git/hg operations." +msgid "Each token can have a role. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations." msgstr "" #: rhodecode/templates/admin/my_account/my_account_auth_tokens.html:14 @@ -3680,6 +4289,19 @@ msgstr "" msgid "New email address" msgstr "" +#: rhodecode/templates/admin/my_account/my_account_notifications.html:3 +msgid "Your live notification settings" +msgstr "" + +#: rhodecode/templates/admin/my_account/my_account_notifications.html:14 +#: rhodecode/templates/admin/notifications/show_notification.html:12 +msgid "Notifications" +msgstr "" + +#: rhodecode/templates/admin/my_account/my_account_notifications.html:14 +msgid "Disabled" +msgstr "" + #: rhodecode/templates/admin/my_account/my_account_password.html:3 msgid "Change Your Account Password" msgstr "" @@ -3734,35 +4356,35 @@ msgid "Pull Requests You Opened" msgstr "" #: rhodecode/templates/admin/my_account/my_account_pullrequests.html:23 -#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:86 +#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:88 msgid "Target Repo" msgstr "" #: rhodecode/templates/admin/my_account/my_account_pullrequests.html:26 -#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:89 +#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:91 #: rhodecode/templates/admin/settings/settings_global.html:9 #: rhodecode/templates/email_templates/pull_request_review.mako:28 -#: rhodecode/templates/email_templates/pull_request_review.mako:65 +#: rhodecode/templates/email_templates/pull_request_review.mako:48 #: rhodecode/templates/pullrequests/pullrequest.html:38 #: rhodecode/templates/pullrequests/pullrequests.html:104 msgid "Title" msgstr "" #: rhodecode/templates/admin/my_account/my_account_pullrequests.html:27 -#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:90 +#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:92 msgid "Opened On" msgstr "" -#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:41 -#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:103 -#: rhodecode/templates/changelog/changelog.html:141 +#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:43 +#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:107 +#: rhodecode/templates/changelog/changelog.html:153 #: rhodecode/templates/compare/compare_commits.html:49 #: rhodecode/templates/search/search_commit.html:36 msgid "Expand commit message" msgstr "" -#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:50 -#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:112 +#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:52 +#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:116 #: rhodecode/templates/changeset/changeset_file_comment.html:284 #: rhodecode/templates/pullrequests/pullrequest_show.html:14 #: rhodecode/templates/pullrequests/pullrequest_show.html:112 @@ -3770,19 +4392,19 @@ msgstr "" msgid "Closed" msgstr "" -#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:62 +#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:64 msgid "Confirm to delete this pull request" msgstr "" -#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:69 +#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:71 msgid "You currently have no open pull requests." msgstr "" -#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:77 +#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:79 msgid "Pull Requests You Participate In" msgstr "" -#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:125 +#: rhodecode/templates/admin/my_account/my_account_pullrequests.html:129 msgid "There are currently no open pull requests requiring your participation." msgstr "" @@ -3822,19 +4444,15 @@ msgstr "" msgid "Show notification" msgstr "" -#: rhodecode/templates/admin/notifications/show_notification.html:12 -msgid "Notifications" -msgstr "" - #: rhodecode/templates/admin/permissions/permissions.html:5 msgid "Permissions Administration" msgstr "" #: rhodecode/templates/admin/permissions/permissions.html:14 #: rhodecode/templates/admin/repo_groups/repo_group_edit.html:45 -#: rhodecode/templates/admin/repos/repo_edit.html:42 +#: rhodecode/templates/admin/repos/repo_edit.html:46 #: rhodecode/templates/admin/user_groups/user_group_edit.html:34 -#: rhodecode/templates/base/base.html:83 +#: rhodecode/templates/base/base.html:82 msgid "Permissions" msgstr "" @@ -3963,7 +4581,7 @@ msgstr "" #: rhodecode/templates/admin/repo_groups/repo_group_add.html:14 #: rhodecode/templates/admin/users/user_edit_advanced.html:12 -#: rhodecode/templates/base/base.html:80 rhodecode/templates/base/base.html:152 +#: rhodecode/templates/base/base.html:79 rhodecode/templates/base/base.html:153 msgid "Repository groups" msgstr "" @@ -3994,17 +4612,8 @@ msgstr "" msgid "Add Child Group" msgstr "" -#: rhodecode/templates/admin/repo_groups/repo_group_edit.html:44 -#: rhodecode/templates/admin/repos/repo_edit.html:15 -#: rhodecode/templates/admin/repos/repo_edit.html:39 -#: rhodecode/templates/admin/settings/settings.html:14 -#: rhodecode/templates/admin/user_groups/user_group_edit.html:33 -#: rhodecode/templates/base/base.html:86 rhodecode/templates/base/base.html:250 -msgid "Settings" -msgstr "" - #: rhodecode/templates/admin/repo_groups/repo_group_edit.html:46 -#: rhodecode/templates/admin/repos/repo_edit.html:45 +#: rhodecode/templates/admin/repos/repo_edit.html:49 #: rhodecode/templates/admin/user_groups/user_group_edit.html:35 #: rhodecode/templates/admin/users/user_edit.html:35 msgid "Advanced" @@ -4177,7 +4786,7 @@ msgid "Import Existing Repository ?" msgstr "" #: rhodecode/templates/admin/repos/repo_add_base.html:23 -#: rhodecode/templates/base/base.html:197 +#: rhodecode/templates/base/base.html:198 msgid "Clone from" msgstr "" @@ -4244,19 +4853,19 @@ msgstr "" msgid "%s repository settings" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit.html:51 +#: rhodecode/templates/admin/repos/repo_edit.html:55 msgid "Extra Fields" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit.html:57 -msgid "Caches" -msgstr "" - #: rhodecode/templates/admin/repos/repo_edit.html:61 -msgid "Remote" +msgid "Caches" msgstr "" #: rhodecode/templates/admin/repos/repo_edit.html:65 +msgid "Remote" +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit.html:69 #: rhodecode/templates/summary/components.html:135 msgid "Statistics" msgstr "" @@ -4358,7 +4967,7 @@ msgid "Delete forks" msgstr "" #: rhodecode/templates/admin/repos/repo_edit_advanced.html:139 -#: rhodecode/templates/data_table/_dt_elements.html:118 +#: rhodecode/templates/data_table/_dt_elements.html:120 #, python-format msgid "Confirm to delete this repository: %s" msgstr "" @@ -4419,7 +5028,7 @@ msgstr "" #: rhodecode/templates/admin/user_groups/user_groups.html:62 #: rhodecode/templates/admin/users/user_add.html:97 #: rhodecode/templates/admin/users/user_edit_profile.html:90 -#: rhodecode/templates/admin/users/users.html:100 +#: rhodecode/templates/admin/users/users.html:99 msgid "Active" msgstr "" @@ -4575,7 +5184,7 @@ msgid "http[s] url where from repository msgstr "" #: rhodecode/templates/admin/repos/repo_edit_settings.html:56 -#: rhodecode/templates/data_table/_dt_elements.html:158 +#: rhodecode/templates/data_table/_dt_elements.html:160 #: rhodecode/templates/forks/fork.html:58 msgid "Repository group" msgstr "" @@ -4823,12 +5432,11 @@ msgid "Server Announcement" msgstr "" #: rhodecode/templates/admin/settings/settings_global.html:80 -msgid "Custom js/css code added at the end of the <header> tag." +msgid "Custom js/css code added at the end of the <header/> tag." msgstr "" #: rhodecode/templates/admin/settings/settings_global.html:81 -#: rhodecode/templates/admin/settings/settings_global.html:103 -msgid "Use <script> or <css> tags to define custom styling or scripting" +msgid "Use <script/> or <css/> tags to define custom styling or scripting" msgstr "" #: rhodecode/templates/admin/settings/settings_global.html:88 @@ -4839,6 +5447,10 @@ msgstr "" msgid "Custom js/css code added at the end of the <body> tag." msgstr "" +#: rhodecode/templates/admin/settings/settings_global.html:103 +msgid "Use <script> or <css> tags to define custom styling or scripting" +msgstr "" + #: rhodecode/templates/admin/settings/settings_hooks.html:3 msgid "Built in Mercurial hooks - read only" msgstr "" @@ -5209,7 +5821,7 @@ msgstr "" #: rhodecode/templates/admin/user_groups/user_group_add.html:13 #: rhodecode/templates/admin/users/user_edit_advanced.html:13 -#: rhodecode/templates/base/base.html:82 rhodecode/templates/base/base.html:155 +#: rhodecode/templates/base/base.html:81 rhodecode/templates/base/base.html:156 msgid "User groups" msgstr "" @@ -5300,8 +5912,8 @@ msgid "Change owner of this user group." msgstr "" #: rhodecode/templates/admin/user_groups/user_group_edit_settings.html:59 -#: rhodecode/templates/base/base.html:257 -#: rhodecode/templates/base/base.html:399 +#: rhodecode/templates/base/base.html:258 +#: rhodecode/templates/base/base.html:400 #: rhodecode/templates/search/search.html:64 msgid "Search" msgstr "" @@ -5345,7 +5957,7 @@ msgstr "" #: rhodecode/templates/admin/users/user_add.html:13 #: rhodecode/templates/admin/users/user_edit.html:14 -#: rhodecode/templates/base/base.html:81 +#: rhodecode/templates/base/base.html:80 msgid "Users" msgstr "" @@ -5408,7 +6020,7 @@ msgid "Source of Record" msgstr "" #: rhodecode/templates/admin/users/user_edit_advanced.html:8 -#: rhodecode/templates/admin/users/users.html:98 +#: rhodecode/templates/admin/users/users.html:97 msgid "Last login" msgstr "" @@ -5505,7 +6117,7 @@ msgid "Detach user groups" msgstr "" #: rhodecode/templates/admin/users/user_edit_advanced.html:135 -#: rhodecode/templates/data_table/_dt_elements.html:189 +#: rhodecode/templates/data_table/_dt_elements.html:191 #, python-format msgid "Confirm to delete this user: %s" msgstr "" @@ -5585,21 +6197,21 @@ msgstr "" msgid "Users administration" msgstr "" -#: rhodecode/templates/admin/users/users.html:104 +#: rhodecode/templates/admin/users/users.html:103 msgid "Authentication type" msgstr "" -#: rhodecode/templates/base/base.html:45 +#: rhodecode/templates/base/base.html:44 #: rhodecode/templates/errors/error_document.html:51 msgid "Support" msgstr "" -#: rhodecode/templates/base/base.html:52 +#: rhodecode/templates/base/base.html:51 #, python-format msgid "RhodeCode instance id: %s" msgstr "" -#: rhodecode/templates/base/base.html:84 +#: rhodecode/templates/base/base.html:83 msgid "Authentication" msgstr "" @@ -5613,40 +6225,40 @@ msgstr "" msgid "Show More" msgstr "" -#: rhodecode/templates/base/base.html:189 +#: rhodecode/templates/base/base.html:190 msgid "Fork of" msgstr "" -#: rhodecode/templates/base/base.html:206 +#: rhodecode/templates/base/base.html:207 #, python-format msgid "Repository locked by %(user)s" msgstr "" -#: rhodecode/templates/base/base.html:211 +#: rhodecode/templates/base/base.html:212 msgid "Repository not locked. Pull repository to lock it." msgstr "" -#: rhodecode/templates/base/base.html:229 -#: rhodecode/templates/data_table/_dt_elements.html:12 -#: rhodecode/templates/data_table/_dt_elements.html:13 -#: rhodecode/templates/data_table/_dt_elements.html:147 -msgid "Summary" -msgstr "" - #: rhodecode/templates/base/base.html:230 +#: rhodecode/templates/data_table/_dt_elements.html:12 +#: rhodecode/templates/data_table/_dt_elements.html:13 +#: rhodecode/templates/data_table/_dt_elements.html:149 +msgid "Summary" +msgstr "" + +#: rhodecode/templates/base/base.html:231 #: rhodecode/templates/data_table/_dt_elements.html:17 #: rhodecode/templates/data_table/_dt_elements.html:18 msgid "Changelog" msgstr "" -#: rhodecode/templates/base/base.html:231 +#: rhodecode/templates/base/base.html:232 #: rhodecode/templates/data_table/_dt_elements.html:22 #: rhodecode/templates/data_table/_dt_elements.html:23 #: rhodecode/templates/files/files.html:15 msgid "Files" msgstr "" -#: rhodecode/templates/base/base.html:233 +#: rhodecode/templates/base/base.html:234 #: rhodecode/templates/bookmarks/bookmarks.html:68 #: rhodecode/templates/branches/branches.html:67 #: rhodecode/templates/files/file_diff.html:11 @@ -5655,29 +6267,29 @@ msgstr "" msgid "Compare" msgstr "" -#: rhodecode/templates/base/base.html:238 +#: rhodecode/templates/base/base.html:239 #, python-format msgid "Show Pull Requests for %s" msgstr "" -#: rhodecode/templates/base/base.html:247 +#: rhodecode/templates/base/base.html:248 msgid "Options" msgstr "" -#: rhodecode/templates/base/base.html:254 +#: rhodecode/templates/base/base.html:255 #: rhodecode/templates/forks/forks_data.html:30 msgid "Compare fork" msgstr "" -#: rhodecode/templates/base/base.html:261 +#: rhodecode/templates/base/base.html:262 msgid "Unlock" msgstr "" -#: rhodecode/templates/base/base.html:263 +#: rhodecode/templates/base/base.html:264 msgid "Lock" msgstr "" -#: rhodecode/templates/base/base.html:268 +#: rhodecode/templates/base/base.html:269 #: rhodecode/templates/data_table/_dt_elements.html:27 #: rhodecode/templates/data_table/_dt_elements.html:28 #: rhodecode/templates/forks/forks_data.html:8 @@ -5687,73 +6299,73 @@ msgid_plural "Forks" msgstr[0] "" msgstr[1] "" -#: rhodecode/templates/base/base.html:269 +#: rhodecode/templates/base/base.html:270 msgid "Create Pull Request" msgstr "" -#: rhodecode/templates/base/base.html:291 +#: rhodecode/templates/base/base.html:292 msgid "Sign in" msgstr "" -#: rhodecode/templates/base/base.html:299 +#: rhodecode/templates/base/base.html:300 #: rhodecode/templates/debug_style/login.html:28 msgid "Sign in to your account" msgstr "" -#: rhodecode/templates/base/base.html:315 +#: rhodecode/templates/base/base.html:316 #: rhodecode/templates/debug_style/login.html:46 msgid "(Forgot password?)" msgstr "" -#: rhodecode/templates/base/base.html:324 +#: rhodecode/templates/base/base.html:325 #: rhodecode/templates/debug_style/login.html:56 msgid "Don't have an account ?" msgstr "" -#: rhodecode/templates/base/base.html:345 +#: rhodecode/templates/base/base.html:346 msgid "Sign Out" msgstr "" -#: rhodecode/templates/base/base.html:381 -msgid "Show activity journal" -msgstr "" - #: rhodecode/templates/base/base.html:382 +msgid "Show activity journal" +msgstr "" + +#: rhodecode/templates/base/base.html:383 #: rhodecode/templates/journal/journal.html:4 #: rhodecode/templates/journal/journal.html:14 msgid "Journal" msgstr "" -#: rhodecode/templates/base/base.html:387 +#: rhodecode/templates/base/base.html:388 msgid "Show Public activity journal" msgstr "" -#: rhodecode/templates/base/base.html:388 +#: rhodecode/templates/base/base.html:389 msgid "Public journal" msgstr "" -#: rhodecode/templates/base/base.html:393 -msgid "Show Gists" -msgstr "" - #: rhodecode/templates/base/base.html:394 +msgid "Show Gists" +msgstr "" + +#: rhodecode/templates/base/base.html:395 msgid "Gists" msgstr "" -#: rhodecode/templates/base/base.html:398 +#: rhodecode/templates/base/base.html:399 msgid "Search in repositories you have access to" msgstr "" -#: rhodecode/templates/base/base.html:404 +#: rhodecode/templates/base/base.html:405 msgid "Admin settings" msgstr "" -#: rhodecode/templates/base/base.html:411 +#: rhodecode/templates/base/base.html:412 msgid "Delegated Admin settings" msgstr "" -#: rhodecode/templates/base/base.html:421 #: rhodecode/templates/base/base.html:422 +#: rhodecode/templates/base/base.html:423 #: rhodecode/templates/debug_style/buttons.html:5 #: rhodecode/templates/debug_style/code-block.html:6 #: rhodecode/templates/debug_style/collapsable-content.html:5 @@ -5774,15 +6386,15 @@ msgstr "" msgid "Style" msgstr "" -#: rhodecode/templates/base/base.html:479 +#: rhodecode/templates/base/base.html:480 msgid "Go to" msgstr "" -#: rhodecode/templates/base/base.html:590 +#: rhodecode/templates/base/base.html:591 msgid "Keyboard shortcuts" msgstr "" -#: rhodecode/templates/base/base.html:598 +#: rhodecode/templates/base/base.html:599 msgid "Site-wide shortcuts" msgstr "" @@ -5957,7 +6569,7 @@ msgstr "" msgid "No permission defined" msgstr "" -#: rhodecode/templates/base/root.html:151 +#: rhodecode/templates/base/root.html:120 msgid "Please enable JavaScript to use RhodeCode Enterprise" msgstr "" @@ -6077,6 +6689,22 @@ msgstr "" msgid "During the update of a pull request, the position of inline comments will be updated and outdated inline comments will be hidden." msgstr "" +#: rhodecode/templates/base/vcs_settings.html:222 +msgid "Labs settings" +msgstr "" + +#: rhodecode/templates/base/vcs_settings.html:222 +msgid "These features are considered experimental and may not work as expected." +msgstr "" + +#: rhodecode/templates/base/vcs_settings.html:229 +msgid "Mercurial server-side merge" +msgstr "" + +#: rhodecode/templates/base/vcs_settings.html:234 +msgid "Use rebase instead of creating a merge commit when merging via web interface" +msgstr "" + #: rhodecode/templates/bookmarks/bookmarks.html:5 #, python-format msgid "%s Bookmarks" @@ -6091,8 +6719,8 @@ msgid "Compare Selected Bookmarks" msgstr "" #: rhodecode/templates/bookmarks/bookmarks_data.html:13 -#: rhodecode/templates/changelog/changelog.html:180 -#: rhodecode/templates/changelog/changelog_summary_data.html:53 +#: rhodecode/templates/changelog/changelog.html:183 +#: rhodecode/templates/changelog/changelog_summary_data.html:62 #: rhodecode/templates/changeset/changeset.html:92 #: rhodecode/templates/files/base.html:10 #, python-format @@ -6113,8 +6741,8 @@ msgid "Compare Selected Branches" msgstr "" #: rhodecode/templates/branches/branches_data.html:12 -#: rhodecode/templates/changelog/changelog.html:172 -#: rhodecode/templates/changelog/changelog_summary_data.html:67 +#: rhodecode/templates/changelog/changelog.html:175 +#: rhodecode/templates/changelog/changelog_summary_data.html:76 #: rhodecode/templates/changeset/changeset.html:105 #: rhodecode/templates/files/base.html:23 #, python-format @@ -6144,10 +6772,6 @@ msgstr "" msgid "Compare fork with Parent (%s)" msgstr "" -#: rhodecode/templates/changelog/changelog.html:62 -msgid "Open new pull request" -msgstr "" - #: rhodecode/templates/changelog/changelog.html:68 #: rhodecode/templates/changelog/changelog.html:69 msgid "Clear selection" @@ -6157,43 +6781,49 @@ msgstr "" msgid "Clear filter" msgstr "" -#: rhodecode/templates/changelog/changelog.html:103 -#: rhodecode/templates/changelog/changelog_summary_data.html:9 -msgid "Age" -msgstr "" - -#: rhodecode/templates/changelog/changelog.html:105 +#: rhodecode/templates/changelog/changelog.html:107 #: rhodecode/templates/files/files_add.html:93 #: rhodecode/templates/files/files_delete.html:60 #: rhodecode/templates/files/files_edit.html:96 msgid "Commit Message" msgstr "" -#: rhodecode/templates/changelog/changelog.html:108 -#: rhodecode/templates/changelog/changelog_summary_data.html:11 +#: rhodecode/templates/changelog/changelog.html:109 +#: rhodecode/templates/changelog/changelog_summary_data.html:10 +msgid "Age" +msgstr "" + +#: rhodecode/templates/changelog/changelog.html:112 +#: rhodecode/templates/changelog/changelog_summary_data.html:12 msgid "Refs" msgstr "" -#: rhodecode/templates/changelog/changelog.html:122 -#: rhodecode/templates/changelog/changelog_summary_data.html:22 +#: rhodecode/templates/changelog/changelog.html:126 +#: rhodecode/templates/changelog/changelog_summary_data.html:21 #, python-format msgid "" "Commit status: %s\n" "Click to open associated pull request #%s" msgstr "" -#: rhodecode/templates/changelog/changelog.html:126 +#: rhodecode/templates/changelog/changelog.html:130 +#: rhodecode/templates/changelog/changelog_summary_data.html:25 #, python-format msgid "Commit status: %s" msgstr "" -#: rhodecode/templates/changelog/changelog.html:162 -#: rhodecode/templates/changelog/changelog_summary_data.html:33 +#: rhodecode/templates/changelog/changelog.html:136 +#: rhodecode/templates/changelog/changelog_summary_data.html:31 +msgid "Commit status: Not Reviewed" +msgstr "" + +#: rhodecode/templates/changelog/changelog.html:141 +#: rhodecode/templates/changelog/changelog_summary_data.html:36 msgid "Commit has comments" msgstr "" -#: rhodecode/templates/changelog/changelog.html:188 -#: rhodecode/templates/changelog/changelog_summary_data.html:60 +#: rhodecode/templates/changelog/changelog.html:191 +#: rhodecode/templates/changelog/changelog_summary_data.html:69 #: rhodecode/templates/changeset/changeset.html:99 #: rhodecode/templates/files/base.html:17 #: rhodecode/templates/tags/tags_data.html:12 @@ -6201,16 +6831,16 @@ msgstr "" msgid "Tag %s" msgstr "" -#: rhodecode/templates/changelog/changelog.html:338 +#: rhodecode/templates/changelog/changelog.html:341 msgid "Filter changelog" msgstr "" -#: rhodecode/templates/changelog/changelog.html:411 +#: rhodecode/templates/changelog/changelog.html:414 msgid "There are no changes yet" msgstr "" #: rhodecode/templates/changelog/changelog_details.html:4 -#: rhodecode/templates/pullrequests/pullrequest_show.html:358 +#: rhodecode/templates/pullrequests/pullrequest_show.html:362 msgid "Removed" msgstr "" @@ -6241,25 +6871,25 @@ msgstr "" msgid "Show File" msgstr "" -#: rhodecode/templates/changelog/changelog_summary_data.html:8 +#: rhodecode/templates/changelog/changelog_summary_data.html:9 #: rhodecode/templates/search/search_commit.html:8 msgid "Commit message" msgstr "" -#: rhodecode/templates/changelog/changelog_summary_data.html:91 +#: rhodecode/templates/changelog/changelog_summary_data.html:100 msgid "Add or upload files directly via RhodeCode:" msgstr "" -#: rhodecode/templates/changelog/changelog_summary_data.html:94 +#: rhodecode/templates/changelog/changelog_summary_data.html:103 #: rhodecode/templates/files/files_browser.html:25 msgid "Add New File" msgstr "" -#: rhodecode/templates/changelog/changelog_summary_data.html:102 +#: rhodecode/templates/changelog/changelog_summary_data.html:111 msgid "Push new repo:" msgstr "" -#: rhodecode/templates/changelog/changelog_summary_data.html:113 +#: rhodecode/templates/changelog/changelog_summary_data.html:122 msgid "Existing repository?" msgstr "" @@ -6335,7 +6965,7 @@ msgstr "" #: rhodecode/templates/changeset/changeset.html:145 #: rhodecode/templates/changeset/changeset.html:147 -#: rhodecode/tests/functional/test_changeset_comments.py:217 +#: rhodecode/tests/functional/test_commit_comments.py:263 #, python-format msgid "%d Commit comment" msgid_plural "%d Commit comments" @@ -6346,22 +6976,22 @@ msgstr[1] "" #: rhodecode/templates/changeset/changeset.html:153 #: rhodecode/templates/pullrequests/pullrequest_show.html:145 #: rhodecode/templates/pullrequests/pullrequest_show.html:147 -#: rhodecode/tests/functional/test_changeset_comments.py:224 +#: rhodecode/tests/functional/test_commit_comments.py:270 #, python-format msgid "%d Inline Comment" msgid_plural "%d Inline Comments" msgstr[0] "" msgstr[1] "" -#: rhodecode/templates/changeset/changeset.html:177 +#: rhodecode/templates/changeset/changeset.html:175 msgid "Browse files at current commit" msgstr "" +#: rhodecode/templates/changeset/changeset.html:175 +msgid "Browse files" +msgstr "" + #: rhodecode/templates/changeset/changeset.html:177 -msgid "Browse files" -msgstr "" - -#: rhodecode/templates/changeset/changeset.html:179 #: rhodecode/templates/changeset/changeset_range.html:59 #: rhodecode/templates/compare/compare_diff.html:255 #: rhodecode/templates/files/file_diff.html:77 @@ -6369,7 +6999,7 @@ msgstr "" msgid "Expand All" msgstr "" -#: rhodecode/templates/changeset/changeset.html:179 +#: rhodecode/templates/changeset/changeset.html:177 #: rhodecode/templates/changeset/changeset_range.html:59 #: rhodecode/templates/compare/compare_diff.html:255 #: rhodecode/templates/files/file_diff.html:77 @@ -6377,30 +7007,32 @@ msgstr "" msgid "Collapse All" msgstr "" -#: rhodecode/templates/changeset/changeset.html:190 +#: rhodecode/templates/changeset/changeset.html:188 #: rhodecode/templates/compare/compare_diff.html:263 #: rhodecode/templates/pullrequests/pullrequest_show.html:274 msgid "No files" msgstr "" -#: rhodecode/templates/changeset/changeset.html:227 +#: rhodecode/templates/changeset/changeset.html:225 #: rhodecode/templates/files/file_diff.html:128 +#: rhodecode/templates/pullrequests/pullrequest_show.html:315 msgid "Show comments" msgstr "" -#: rhodecode/templates/changeset/changeset.html:228 +#: rhodecode/templates/changeset/changeset.html:226 #: rhodecode/templates/files/file_diff.html:129 +#: rhodecode/templates/pullrequests/pullrequest_show.html:316 msgid "Hide comments" msgstr "" -#: rhodecode/templates/changeset/changeset.html:245 +#: rhodecode/templates/changeset/changeset.html:243 #: rhodecode/templates/changeset/diff_block.html:25 #: rhodecode/templates/changeset/diff_block.html:46 #: rhodecode/templates/files/file_diff.html:146 msgid "Diff was truncated. File content available only in full diff." msgstr "" -#: rhodecode/templates/changeset/changeset.html:245 +#: rhodecode/templates/changeset/changeset.html:243 #: rhodecode/templates/changeset/diff_block.html:7 #: rhodecode/templates/changeset/diff_block.html:10 #: rhodecode/templates/changeset/diff_block.html:25 @@ -6410,22 +7042,22 @@ msgstr "" msgid "Showing a big diff might take some time and resources, continue?" msgstr "" -#: rhodecode/templates/changeset/changeset.html:245 +#: rhodecode/templates/changeset/changeset.html:243 #: rhodecode/templates/changeset/diff_block.html:7 #: rhodecode/templates/changeset/diff_block.html:10 #: rhodecode/templates/changeset/diff_block.html:25 #: rhodecode/templates/changeset/diff_block.html:46 #: rhodecode/templates/files/file_diff.html:146 -#: rhodecode/templates/pullrequests/pullrequest_show.html:386 -#: rhodecode/templates/pullrequests/pullrequest_show.html:392 +#: rhodecode/templates/pullrequests/pullrequest_show.html:390 +#: rhodecode/templates/pullrequests/pullrequest_show.html:396 msgid "Show full diff" msgstr "" -#: rhodecode/templates/changeset/changeset.html:314 +#: rhodecode/templates/changeset/changeset.html:312 msgid "No Child Commits" msgstr "" -#: rhodecode/templates/changeset/changeset.html:350 +#: rhodecode/templates/changeset/changeset.html:348 msgid "No Parent Commits" msgstr "" @@ -6478,6 +7110,8 @@ msgstr "" #: rhodecode/templates/changeset/changeset_file_comment.html:146 #: rhodecode/templates/changeset/changeset_file_comment.html:293 #: rhodecode/templates/compare/compare_diff.html:57 +#: rhodecode/templates/email_templates/commit_comment.mako:87 +#: rhodecode/templates/email_templates/pull_request_comment.mako:93 msgid "Comment" msgstr "" @@ -6624,12 +7258,15 @@ msgid "Compare Commits" msgstr "" #: rhodecode/templates/compare/compare_diff.html:46 +#: rhodecode/templates/email_templates/pull_request_review.mako:50 #: rhodecode/templates/files/file_diff.html:56 #: rhodecode/templates/pullrequests/pullrequest_show.html:85 msgid "Target" msgstr "" #: rhodecode/templates/compare/compare_diff.html:47 +#: rhodecode/templates/email_templates/pull_request_comment.mako:92 +#: rhodecode/templates/email_templates/pull_request_review.mako:49 #: rhodecode/templates/files/file_diff.html:62 #: rhodecode/templates/files/files_source.html:18 msgid "Source" @@ -6679,36 +7316,36 @@ msgstr "" msgid "Subscribe to %s atom feed" msgstr "" -#: rhodecode/templates/data_table/_dt_elements.html:127 +#: rhodecode/templates/data_table/_dt_elements.html:129 msgid "Creating" msgstr "" -#: rhodecode/templates/data_table/_dt_elements.html:129 +#: rhodecode/templates/data_table/_dt_elements.html:131 msgid "Created" msgstr "" -#: rhodecode/templates/data_table/_dt_elements.html:175 +#: rhodecode/templates/data_table/_dt_elements.html:177 #, python-format msgid "Confirm to delete this group: %s with %s repository" msgid_plural "Confirm to delete this group: %s with %s repositories" msgstr[0] "" msgstr[1] "" -#: rhodecode/templates/data_table/_dt_elements.html:201 +#: rhodecode/templates/data_table/_dt_elements.html:203 #, python-format msgid "Confirm to delete this user group: %s" msgstr "" -#: rhodecode/templates/data_table/_dt_elements.html:218 +#: rhodecode/templates/data_table/_dt_elements.html:220 msgid "User group" msgstr "" -#: rhodecode/templates/data_table/_dt_elements.html:262 +#: rhodecode/templates/data_table/_dt_elements.html:264 #: rhodecode/templates/forks/fork.html:81 msgid "Private" msgstr "" -#: rhodecode/templates/data_table/_dt_elements.html:287 +#: rhodecode/templates/data_table/_dt_elements.html:289 #, python-format msgid "Pull request #%(pr_number)s" msgstr "" @@ -6812,85 +7449,130 @@ msgstr "" msgid "Form vertical" msgstr "" -#: rhodecode/templates/email_templates/base.mako:16 +#: rhodecode/templates/email_templates/base.mako:7 #, python-format msgid "This is a notification from RhodeCode. %(instance_url)s" msgstr "" -#: rhodecode/templates/email_templates/commit_comment.mako:5 -#: rhodecode/templates/email_templates/pull_request_comment.mako:5 +#: rhodecode/templates/email_templates/base.mako:90 +msgid "RhodeCode" +msgstr "" + +#: rhodecode/templates/email_templates/commit_comment.mako:16 +#: rhodecode/templates/email_templates/pull_request_comment.mako:17 msgid "[mention]" msgstr "" -#: rhodecode/templates/email_templates/commit_comment.mako:5 -#, python-format -msgid "%(user)s commented on commit of %(repo_name)s" -msgstr "" - -#: rhodecode/templates/email_templates/commit_comment.mako:14 -#: rhodecode/templates/email_templates/commit_comment.mako:41 -#: rhodecode/templates/email_templates/pull_request_comment.mako:15 -#: rhodecode/templates/email_templates/pull_request_comment.mako:51 -msgid "Comment link" +#: rhodecode/templates/email_templates/commit_comment.mako:19 +#, python-format +msgid "%(user)s commented on commit `%(commit_id)s` (file: `%(comment_file)s`)" msgstr "" #: rhodecode/templates/email_templates/commit_comment.mako:19 +#: rhodecode/templates/email_templates/commit_comment.mako:22 +#: rhodecode/templates/email_templates/commit_comment.mako:24 +#, python-format +msgid "in the %(repo_name)s repository" +msgstr "" + +#: rhodecode/templates/email_templates/commit_comment.mako:22 +#, python-format +msgid "%(user)s commented on commit `%(commit_id)s` (status: %(status)s)" +msgstr "" + +#: rhodecode/templates/email_templates/commit_comment.mako:24 +#: rhodecode/templates/email_templates/commit_comment.mako:78 +#, python-format +msgid "%(user)s commented on commit `%(commit_id)s`" +msgstr "" + #: rhodecode/templates/email_templates/commit_comment.mako:43 -#: rhodecode/templates/email_templates/pull_request_comment.mako:20 -#: rhodecode/templates/email_templates/pull_request_comment.mako:54 +#: rhodecode/templates/email_templates/pull_request_comment.mako:43 +msgid "Comment link" +msgstr "" + +#: rhodecode/templates/email_templates/commit_comment.mako:48 +#: rhodecode/templates/email_templates/pull_request_comment.mako:48 #, python-format msgid "File: %(comment_file)s on line %(comment_line)s" msgstr "" -#: rhodecode/templates/email_templates/commit_comment.mako:28 -#: rhodecode/templates/email_templates/commit_comment.mako:56 +#: rhodecode/templates/email_templates/commit_comment.mako:54 msgid "Commit status was changed to" msgstr "" -#: rhodecode/templates/email_templates/commit_comment.mako:35 -#, python-format -msgid "%(user)s commented on a file in commit of %(repo_url)s." -msgstr "" - -#: rhodecode/templates/email_templates/commit_comment.mako:37 -#, python-format -msgid "%(user)s commented on a commit of %(repo_url)s." -msgstr "" - -#: rhodecode/templates/email_templates/commit_comment.mako:47 -#: rhodecode/templates/files/files_detail.html:5 -#: rhodecode/templates/files/files_detail.html:12 -msgid "Commit Description" -msgstr "" - -#: rhodecode/templates/email_templates/pull_request_comment.mako:5 -#, python-format -msgid "%(user)s commented on pull request #%(pr_id)s: \"%(pr_title)s\"" -msgstr "" - -#: rhodecode/templates/email_templates/pull_request_comment.mako:17 -#: rhodecode/templates/email_templates/pull_request_comment.mako:52 +#: rhodecode/templates/email_templates/commit_comment.mako:76 +#, python-format +msgid "%(user)s commented on commit `%(commit_id)s` (file:`%(comment_file)s`)" +msgstr "" + +#: rhodecode/templates/email_templates/commit_comment.mako:76 +#: rhodecode/templates/email_templates/commit_comment.mako:78 +#, python-format +msgid "in the %(repo)s repository" +msgstr "" + +#: rhodecode/templates/email_templates/commit_comment.mako:85 +msgid "Status" +msgstr "" + +#: rhodecode/templates/email_templates/commit_comment.mako:85 +msgid "The commit status was changed to" +msgstr "" + +#: rhodecode/templates/email_templates/commit_comment.mako:87 +#: rhodecode/templates/email_templates/pull_request_comment.mako:93 +#, python-format +msgid "Comment on line: %(comment_line)s" +msgstr "" + +#: rhodecode/templates/email_templates/password_reset.mako:30 +msgid "Generate new password here" +msgstr "" + +#: rhodecode/templates/email_templates/pull_request_comment.mako:20 +#, python-format +msgid "%(user)s commented on pull request #%(pr_id)s \"%(pr_title)s\" (file: `%(comment_file)s`)" +msgstr "" + +#: rhodecode/templates/email_templates/pull_request_comment.mako:23 +#, python-format +msgid "%(user)s commented on pull request #%(pr_id)s \"%(pr_title)s\" (status: %(status)s)" +msgstr "" + +#: rhodecode/templates/email_templates/pull_request_comment.mako:25 +#: rhodecode/templates/email_templates/pull_request_comment.mako:82 +#, python-format +msgid "%(user)s commented on pull request #%(pr_id)s \"%(pr_title)s\"" +msgstr "" + +#: rhodecode/templates/email_templates/pull_request_comment.mako:45 msgid "Source repository" msgstr "" -#: rhodecode/templates/email_templates/pull_request_comment.mako:29 -#: rhodecode/templates/email_templates/pull_request_comment.mako:63 -msgid "Pull request status was changed to" -msgstr "" - -#: rhodecode/templates/email_templates/pull_request_comment.mako:31 -#: rhodecode/templates/email_templates/pull_request_comment.mako:65 -msgid "Pull request was closed with status" -msgstr "" - -#: rhodecode/templates/email_templates/pull_request_comment.mako:37 -#, python-format -msgid "%(user)s commented on a file on pull request #%(pr_id)s: \"%(pr_title)s\"." -msgstr "" - -#: rhodecode/templates/email_templates/pull_request_comment.mako:43 -#, python-format -msgid "%(user)s commented on a pull request #%(pr_id)s \"%(pr_title)s\"." +#: rhodecode/templates/email_templates/pull_request_comment.mako:54 +#, python-format +msgid "%(user)s submitted pull request #%(pr_id)s status: *%(status)s*" +msgstr "" + +#: rhodecode/templates/email_templates/pull_request_comment.mako:56 +#, python-format +msgid "%(user)s submitted pull request #%(pr_id)s status: *%(status)s and closed*" +msgstr "" + +#: rhodecode/templates/email_templates/pull_request_comment.mako:80 +#, python-format +msgid "%(user)s commented on pull request #%(pr_id)s \"%(pr_title)s\" (file:`%(comment_file)s`)" +msgstr "" + +#: rhodecode/templates/email_templates/pull_request_comment.mako:86 +#, python-format +msgid "submitted pull request status: %(status)s" +msgstr "" + +#: rhodecode/templates/email_templates/pull_request_comment.mako:88 +#, python-format +msgid "submitted pull request status: %(status)s and closed" msgstr "" #: rhodecode/templates/email_templates/pull_request_review.mako:5 @@ -6899,18 +7581,15 @@ msgid "%(user)s wants you to review pull msgstr "" #: rhodecode/templates/email_templates/pull_request_review.mako:17 -#: rhodecode/templates/email_templates/pull_request_review.mako:54 #, python-format msgid "Pull request from %(source_ref_type)s:%(source_ref_name)s of %(repo_url)s into %(target_ref_type)s:%(target_ref_name)s" msgstr "" #: rhodecode/templates/email_templates/pull_request_review.mako:26 -#: rhodecode/templates/email_templates/pull_request_review.mako:63 msgid "Link" msgstr "" #: rhodecode/templates/email_templates/pull_request_review.mako:35 -#: rhodecode/templates/email_templates/pull_request_review.mako:72 #, python-format msgid "Commit (%(num)s)" msgid_plural "Commits (%(num)s)" @@ -6922,6 +7601,25 @@ msgstr[1] "" msgid "%(user)s wants you to review pull request #%(pr_id)s: \"%(pr_title)s\"." msgstr "" +#: rhodecode/templates/email_templates/pull_request_review.mako:49 +#, python-format +msgid "%(source_ref_type)s of %(source_repo_url)s" +msgstr "" + +#: rhodecode/templates/email_templates/pull_request_review.mako:50 +#, python-format +msgid "%(target_ref_type)s of %(target_repo_url)s" +msgstr "" + +#: rhodecode/templates/email_templates/pull_request_review.mako:52 +#: rhodecode/templates/summary/components.html:95 +#: rhodecode/templates/summary/components.html:98 +#, python-format +msgid "%(num)s Commit" +msgid_plural "%(num)s Commits" +msgstr[0] "" +msgstr[1] "" + #: rhodecode/templates/email_templates/test.mako:5 msgid "hello \"world\"" msgstr "" @@ -6930,6 +7628,21 @@ msgstr "" msgid "Translation" msgstr "" +#: rhodecode/templates/email_templates/user_registration.mako:22 +#, python-format +msgid "New user %(user)s has registered on %(date)s" +msgstr "" + +#: rhodecode/templates/email_templates/user_registration.mako:24 +msgid "Full Name" +msgstr "" + +#: rhodecode/templates/email_templates/user_registration.mako:26 +#: rhodecode/templates/users/user.html:29 +#: rhodecode/templates/users/user_profile.html:5 +msgid "Profile" +msgstr "" + #: rhodecode/templates/errors/error_document.html:39 #, python-format msgid "You will be redirected to %s in %s seconds" @@ -6941,8 +7654,8 @@ msgid "%(user)s commited on %(date)s UTC msgstr "" #: rhodecode/templates/feed/atom_feed_entry.mako:26 -#: rhodecode/templates/pullrequests/pullrequest_show.html:386 -#: rhodecode/templates/pullrequests/pullrequest_show.html:392 +#: rhodecode/templates/pullrequests/pullrequest_show.html:390 +#: rhodecode/templates/pullrequests/pullrequest_show.html:396 msgid "Commit was too big and was cut off..." msgstr "" @@ -6986,7 +7699,7 @@ msgstr[1] "" msgid "Show All" msgstr "" -#: rhodecode/templates/files/file_authors_box.html:26 +#: rhodecode/templates/files/file_authors_box.html:25 msgid "last author" msgstr "" @@ -7021,7 +7734,7 @@ msgstr "" msgid "%s Files" msgstr "" -#: rhodecode/templates/files/files.html:143 +#: rhodecode/templates/files/files.html:131 msgid "Switch To Commit" msgstr "" @@ -7117,22 +7830,18 @@ msgstr "" msgid "Loading file list..." msgstr "" -#: rhodecode/templates/files/files_browser.html:50 +#: rhodecode/templates/files/files_browser_tree.html:6 msgid "Size" msgstr "" -#: rhodecode/templates/files/files_browser.html:51 +#: rhodecode/templates/files/files_browser_tree.html:7 msgid "Modified" msgstr "" -#: rhodecode/templates/files/files_browser.html:52 +#: rhodecode/templates/files/files_browser_tree.html:8 msgid "Last Commit" msgstr "" -#: rhodecode/templates/files/files_browser.html:89 -msgid "Loading..." -msgstr "" - #: rhodecode/templates/files/files_delete.html:4 #, python-format msgid "%s Files Delete" @@ -7157,6 +7866,11 @@ msgstr "" msgid "Delete File" msgstr "" +#: rhodecode/templates/files/files_detail.html:5 +#: rhodecode/templates/files/files_detail.html:12 +msgid "Commit Description" +msgstr "" + #: rhodecode/templates/files/files_detail.html:35 msgid "File last commit" msgstr "" @@ -7315,7 +8029,7 @@ msgstr "" msgid "Forked" msgstr "" -#: rhodecode/templates/forks/forks_data.html:48 +#: rhodecode/templates/forks/forks_data.html:46 msgid "There are no forks yet" msgstr "" @@ -7331,7 +8045,7 @@ msgstr "" msgid "RSS journal feed" msgstr "" -#: rhodecode/templates/journal/journal_data.html:53 +#: rhodecode/templates/journal/journal_data.html:51 msgid "No entries yet" msgstr "" @@ -7503,13 +8217,13 @@ msgid_plural "Compare View: %s commits" msgstr[0] "" msgstr[1] "" -#: rhodecode/templates/pullrequests/pullrequest_show.html:330 -#: rhodecode/templates/pullrequests/pullrequest_show.html:365 +#: rhodecode/templates/pullrequests/pullrequest_show.html:334 +#: rhodecode/templates/pullrequests/pullrequest_show.html:369 msgid "Outdated Inline Comments" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.html:386 -#: rhodecode/templates/pullrequests/pullrequest_show.html:392 +#: rhodecode/templates/pullrequests/pullrequest_show.html:390 +#: rhodecode/templates/pullrequests/pullrequest_show.html:396 msgid "Showing a huge diff might take some time and resources" msgstr "" @@ -7688,14 +8402,6 @@ msgstr "" msgid "Information" msgstr "" -#: rhodecode/templates/summary/components.html:95 -#: rhodecode/templates/summary/components.html:98 -#, python-format -msgid "%(num)s Commit" -msgid_plural "%(num)s Commits" -msgstr[0] "" -msgstr[1] "" - #: rhodecode/templates/summary/components.html:102 msgid "Number of Repository Forks" msgstr "" @@ -7759,11 +8465,6 @@ msgstr "" msgid "Compare Selected Tags" msgstr "" -#: rhodecode/templates/users/user.html:29 -#: rhodecode/templates/users/user_profile.html:5 -msgid "Profile" -msgstr "" - #: rhodecode/templates/users/user_profile.html:35 msgid "First name" msgstr "" diff --git a/rhodecode/integrations/__init__.py b/rhodecode/integrations/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/integrations/__init__.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2012-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging + +from rhodecode.integrations.registry import IntegrationTypeRegistry +from rhodecode.integrations.types import webhook, slack, hipchat, email + +log = logging.getLogger(__name__) + + +# TODO: dan: This is currently global until we figure out what to do about +# VCS's not having a pyramid context - move it to pyramid app configuration +# includeme level later to allow per instance integration setup +integration_type_registry = IntegrationTypeRegistry() + +integration_type_registry.register_integration_type( + webhook.WebhookIntegrationType) +integration_type_registry.register_integration_type( + slack.SlackIntegrationType) +integration_type_registry.register_integration_type( + hipchat.HipchatIntegrationType) +integration_type_registry.register_integration_type( + email.EmailIntegrationType) + + +def integrations_event_handler(event): + """ + Takes an event and passes it to all enabled integrations + """ + from rhodecode.model.integration import IntegrationModel + + integration_model = IntegrationModel() + integrations = integration_model.get_for_event(event) + for integration in integrations: + try: + integration_model.send_event(integration, event) + except Exception: + log.exception( + 'failure occured when sending event %s to integration %s' % ( + event, integration)) + + +def includeme(config): + config.include('rhodecode.integrations.routes') diff --git a/rhodecode/integrations/registry.py b/rhodecode/integrations/registry.py new file mode 100644 --- /dev/null +++ b/rhodecode/integrations/registry.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2012-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging + +log = logging.getLogger(__name__) + + +class IntegrationTypeRegistry(dict): + """ + Registry Class to hold IntegrationTypes + """ + def register_integration_type(self, IntegrationType): + key = IntegrationType.key + if key in self: + log.warning( + 'Overriding existing integration type %s (%s) with %s' % ( + self[key], key, IntegrationType)) + + self[key] = IntegrationType + diff --git a/rhodecode/integrations/routes.py b/rhodecode/integrations/routes.py new file mode 100644 --- /dev/null +++ b/rhodecode/integrations/routes.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2012-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging + +from rhodecode.model.db import Repository, Integration +from rhodecode.config.routing import ( + ADMIN_PREFIX, add_route_requirements, URL_NAME_REQUIREMENTS) +from rhodecode.integrations import integration_type_registry + +log = logging.getLogger(__name__) + + +def includeme(config): + config.add_route('global_integrations_home', + ADMIN_PREFIX + '/integrations') + config.add_route('global_integrations_list', + ADMIN_PREFIX + '/integrations/{integration}') + for route_name in ['global_integrations_home', 'global_integrations_list']: + config.add_view('rhodecode.integrations.views.GlobalIntegrationsView', + attr='index', + renderer='rhodecode:templates/admin/integrations/list.html', + request_method='GET', + route_name=route_name) + + config.add_route('global_integrations_create', + ADMIN_PREFIX + '/integrations/{integration}/new', + custom_predicates=(valid_integration,)) + config.add_route('global_integrations_edit', + ADMIN_PREFIX + '/integrations/{integration}/{integration_id}', + custom_predicates=(valid_integration,)) + for route_name in ['global_integrations_create', 'global_integrations_edit']: + config.add_view('rhodecode.integrations.views.GlobalIntegrationsView', + attr='settings_get', + renderer='rhodecode:templates/admin/integrations/edit.html', + request_method='GET', + route_name=route_name) + config.add_view('rhodecode.integrations.views.GlobalIntegrationsView', + attr='settings_post', + renderer='rhodecode:templates/admin/integrations/edit.html', + request_method='POST', + route_name=route_name) + + config.add_route('repo_integrations_home', + add_route_requirements( + '{repo_name}/settings/integrations', + URL_NAME_REQUIREMENTS + ), + custom_predicates=(valid_repo,)) + config.add_route('repo_integrations_list', + add_route_requirements( + '{repo_name}/settings/integrations/{integration}', + URL_NAME_REQUIREMENTS + ), + custom_predicates=(valid_repo, valid_integration)) + for route_name in ['repo_integrations_home', 'repo_integrations_list']: + config.add_view('rhodecode.integrations.views.RepoIntegrationsView', + attr='index', + request_method='GET', + route_name=route_name) + + config.add_route('repo_integrations_create', + add_route_requirements( + '{repo_name}/settings/integrations/{integration}/new', + URL_NAME_REQUIREMENTS + ), + custom_predicates=(valid_repo, valid_integration)) + config.add_route('repo_integrations_edit', + add_route_requirements( + '{repo_name}/settings/integrations/{integration}/{integration_id}', + URL_NAME_REQUIREMENTS + ), + custom_predicates=(valid_repo, valid_integration)) + for route_name in ['repo_integrations_edit', 'repo_integrations_create']: + config.add_view('rhodecode.integrations.views.RepoIntegrationsView', + attr='settings_get', + renderer='rhodecode:templates/admin/integrations/edit.html', + request_method='GET', + route_name=route_name) + config.add_view('rhodecode.integrations.views.RepoIntegrationsView', + attr='settings_post', + renderer='rhodecode:templates/admin/integrations/edit.html', + request_method='POST', + route_name=route_name) + + +def valid_repo(info, request): + repo = Repository.get_by_repo_name(info['match']['repo_name']) + if repo: + return True + + +def valid_integration(info, request): + integration_type = info['match']['integration'] + integration_id = info['match'].get('integration_id') + repo_name = info['match'].get('repo_name') + + if integration_type not in integration_type_registry: + return False + + repo = None + if repo_name: + repo = Repository.get_by_repo_name(info['match']['repo_name']) + if not repo: + return False + + if integration_id: + integration = Integration.get(integration_id) + if not integration: + return False + if integration.integration_type != integration_type: + return False + if repo and repo.repo_id != integration.repo_id: + return False + + return True diff --git a/rhodecode/integrations/schema.py b/rhodecode/integrations/schema.py new file mode 100644 --- /dev/null +++ b/rhodecode/integrations/schema.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2012-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import colander + +from rhodecode.translation import lazy_ugettext + + +class IntegrationSettingsSchemaBase(colander.MappingSchema): + """ + This base schema is intended for use in integrations. + It adds a few default settings (e.g., "enabled"), so that integration + authors don't have to maintain a bunch of boilerplate. + """ + enabled = colander.SchemaNode( + colander.Bool(), + default=True, + description=lazy_ugettext('Enable or disable this integration.'), + missing=False, + title=lazy_ugettext('Enabled'), + ) + + name = colander.SchemaNode( + colander.String(), + description=lazy_ugettext('Short name for this integration.'), + missing=colander.required, + title=lazy_ugettext('Integration name'), + ) diff --git a/rhodecode/integrations/types/__init__.py b/rhodecode/integrations/types/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/integrations/types/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2012-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ diff --git a/rhodecode/integrations/types/base.py b/rhodecode/integrations/types/base.py new file mode 100644 --- /dev/null +++ b/rhodecode/integrations/types/base.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2012-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +from rhodecode.integrations.schema import IntegrationSettingsSchemaBase + + +class IntegrationTypeBase(object): + """ Base class for IntegrationType plugins """ + + def __init__(self, settings): + """ + :param settings: dict of settings to be used for the integration + """ + self.settings = settings + + + def settings_schema(self): + """ + A colander schema of settings for the integration type + + Subclasses can return their own schema but should always + inherit from IntegrationSettingsSchemaBase + """ + return IntegrationSettingsSchemaBase() + diff --git a/rhodecode/integrations/types/email.py b/rhodecode/integrations/types/email.py new file mode 100644 --- /dev/null +++ b/rhodecode/integrations/types/email.py @@ -0,0 +1,222 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2012-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +from __future__ import unicode_literals +import deform +import logging +import colander + +from mako.template import Template + +from rhodecode import events +from rhodecode.translation import _, lazy_ugettext +from rhodecode.lib.celerylib import run_task +from rhodecode.lib.celerylib import tasks +from rhodecode.integrations.types.base import IntegrationTypeBase +from rhodecode.integrations.schema import IntegrationSettingsSchemaBase + + +log = logging.getLogger(__name__) + +repo_push_template_plaintext = Template(''' +Commits: + +% for commit in data['push']['commits']: +${commit['url']} by ${commit['author']} at ${commit['date']} +${commit['message']} +---- + +% endfor +''') + +## TODO (marcink): think about putting this into a file, or use base.mako email template + +repo_push_template_html = Template(''' +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0"/> + <title>${subject} + + + + + + + + + + + + + +
+ + + + + +
+ + ${'RhodeCode'} + +
+ % for commit in data['push']['commits']: + ${commit['short_id']} by ${commit['author']} at ${commit['date']}
+ ${commit['message_html']}
+
+ % endfor +
+
+ +

+ ${'This is a notification from RhodeCode. %(instance_url)s' % {'instance_url': instance_url}} +

+ + +''') + + +class EmailSettingsSchema(IntegrationSettingsSchemaBase): + @colander.instantiate(validator=colander.Length(min=1)) + class recipients(colander.SequenceSchema): + title = lazy_ugettext('Recipients') + description = lazy_ugettext('Email addresses to send push events to') + widget = deform.widget.SequenceWidget(min_len=1) + + recipient = colander.SchemaNode( + colander.String(), + title=lazy_ugettext('Email address'), + description=lazy_ugettext('Email address'), + default='', + validator=colander.Email(), + widget=deform.widget.TextInputWidget( + placeholder='user@domain.com', + ), + ) + + +class EmailIntegrationType(IntegrationTypeBase): + key = 'email' + display_name = lazy_ugettext('Email') + SettingsSchema = EmailSettingsSchema + + def settings_schema(self): + schema = EmailSettingsSchema() + return schema + + def send_event(self, event): + data = event.as_dict() + log.debug('got event: %r', event) + + if isinstance(event, events.RepoPushEvent): + repo_push_handler(data, self.settings) + else: + log.debug('ignoring event: %r', event) + + +def repo_push_handler(data, settings): + commit_num = len(data['push']['commits']) + server_url = data['server_url'] + + if commit_num == 0: + subject = '[{repo_name}] {author} pushed {commit_num} commit on branches: {branches}'.format( + author=data['actor']['username'], + repo_name=data['repo']['repo_name'], + commit_num=commit_num, + branches=', '.join( + branch['name'] for branch in data['push']['branches']) + ) + else: + subject = '[{repo_name}] {author} pushed {commit_num} commits on branches: {branches}'.format( + author=data['actor']['username'], + repo_name=data['repo']['repo_name'], + commit_num=commit_num, + branches=', '.join( + branch['name'] for branch in data['push']['branches'])) + + email_body_plaintext = repo_push_template_plaintext.render( + data=data, + subject=subject, + instance_url=server_url) + + email_body_html = repo_push_template_html.render( + data=data, + subject=subject, + instance_url=server_url) + + for email_address in settings['recipients']: + run_task( + tasks.send_email, email_address, subject, + email_body_plaintext, email_body_html) diff --git a/rhodecode/integrations/types/hipchat.py b/rhodecode/integrations/types/hipchat.py new file mode 100644 --- /dev/null +++ b/rhodecode/integrations/types/hipchat.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2012-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +from __future__ import unicode_literals +import deform +import re +import logging +import requests +import colander +import textwrap +from celery.task import task +from mako.template import Template + +from rhodecode import events +from rhodecode.translation import lazy_ugettext +from rhodecode.lib import helpers as h +from rhodecode.lib.celerylib import run_task +from rhodecode.lib.colander_utils import strip_whitespace +from rhodecode.integrations.types.base import IntegrationTypeBase +from rhodecode.integrations.schema import IntegrationSettingsSchemaBase + +log = logging.getLogger(__name__) + + +class HipchatSettingsSchema(IntegrationSettingsSchemaBase): + color_choices = [ + ('yellow', lazy_ugettext('Yellow')), + ('red', lazy_ugettext('Red')), + ('green', lazy_ugettext('Green')), + ('purple', lazy_ugettext('Purple')), + ('gray', lazy_ugettext('Gray')), + ] + + server_url = colander.SchemaNode( + colander.String(), + title=lazy_ugettext('Hipchat server URL'), + description=lazy_ugettext('Hipchat integration url.'), + default='', + preparer=strip_whitespace, + validator=colander.url, + widget=deform.widget.TextInputWidget( + placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?', + ), + ) + notify = colander.SchemaNode( + colander.Bool(), + title=lazy_ugettext('Notify'), + description=lazy_ugettext('Make a notification to the users in room.'), + missing=False, + default=False, + ) + color = colander.SchemaNode( + colander.String(), + title=lazy_ugettext('Color'), + description=lazy_ugettext('Background color of message.'), + missing='', + validator=colander.OneOf([x[0] for x in color_choices]), + widget=deform.widget.Select2Widget( + values=color_choices, + ), + ) + + +repo_push_template = Template(''' +${data['actor']['username']} pushed to +%if data['push']['branches']: +${len(data['push']['branches']) > 1 and 'branches' or 'branch'} +${', '.join('%s' % (branch['url'], branch['name']) for branch in data['push']['branches'])} +%else: +unknown branch +%endif +in ${data['repo']['repo_name']} +
+
    +%for commit in data['push']['commits']: +
  • + ${commit['short_id']} - ${commit['message_html']} +
  • +%endfor +
+''') + + + +class HipchatIntegrationType(IntegrationTypeBase): + key = 'hipchat' + display_name = lazy_ugettext('Hipchat') + valid_events = [ + events.PullRequestCloseEvent, + events.PullRequestMergeEvent, + events.PullRequestUpdateEvent, + events.PullRequestCommentEvent, + events.PullRequestReviewEvent, + events.PullRequestCreateEvent, + events.RepoPushEvent, + events.RepoCreateEvent, + ] + + def send_event(self, event): + if event.__class__ not in self.valid_events: + log.debug('event not valid: %r' % event) + return + + if event.name not in self.settings['events']: + log.debug('event ignored: %r' % event) + return + + data = event.as_dict() + + text = '%s caused a %s event' % ( + data['actor']['username'], event.name) + + log.debug('handling hipchat event for %s' % event.name) + + if isinstance(event, events.PullRequestCommentEvent): + text = self.format_pull_request_comment_event(event, data) + elif isinstance(event, events.PullRequestReviewEvent): + text = self.format_pull_request_review_event(event, data) + elif isinstance(event, events.PullRequestEvent): + text = self.format_pull_request_event(event, data) + elif isinstance(event, events.RepoPushEvent): + text = self.format_repo_push_event(data) + elif isinstance(event, events.RepoCreateEvent): + text = self.format_repo_create_event(data) + else: + log.error('unhandled event type: %r' % event) + + run_task(post_text_to_hipchat, self.settings, text) + + def settings_schema(self): + schema = HipchatSettingsSchema() + schema.add(colander.SchemaNode( + colander.Set(), + widget=deform.widget.CheckboxChoiceWidget( + values=sorted( + [(e.name, e.display_name) for e in self.valid_events] + ) + ), + description="Events activated for this integration", + name='events' + )) + + return schema + + def format_pull_request_comment_event(self, event, data): + comment_text = data['comment']['text'] + if len(comment_text) > 200: + comment_text = '{comment_text}...'.format( + comment_text=comment_text[:200], + comment_url=data['comment']['url'], + ) + + comment_status = '' + if data['comment']['status']: + comment_status = '[{}]: '.format(data['comment']['status']) + + return (textwrap.dedent( + ''' + {user} commented on pull request {number} - {pr_title}: + >>> {comment_status}{comment_text} + ''').format( + comment_status=comment_status, + user=data['actor']['username'], + number=data['pullrequest']['pull_request_id'], + pr_url=data['pullrequest']['url'], + pr_status=data['pullrequest']['status'], + pr_title=data['pullrequest']['title'], + comment_text=comment_text + ) + ) + + def format_pull_request_review_event(self, event, data): + return (textwrap.dedent( + ''' + Status changed to {pr_status} for pull request #{number} - {pr_title} + ''').format( + user=data['actor']['username'], + number=data['pullrequest']['pull_request_id'], + pr_url=data['pullrequest']['url'], + pr_status=data['pullrequest']['status'], + pr_title=data['pullrequest']['title'], + ) + ) + + def format_pull_request_event(self, event, data): + action = { + events.PullRequestCloseEvent: 'closed', + events.PullRequestMergeEvent: 'merged', + events.PullRequestUpdateEvent: 'updated', + events.PullRequestCreateEvent: 'created', + }.get(event.__class__, str(event.__class__)) + + return ('Pull request #{number} - {title} ' + '{action} by {user}').format( + user=data['actor']['username'], + number=data['pullrequest']['pull_request_id'], + url=data['pullrequest']['url'], + title=data['pullrequest']['title'], + action=action + ) + + def format_repo_push_event(self, data): + result = repo_push_template.render( + data=data, + ) + return result + + def format_repo_create_event(self, data): + return '{} ({}) repository created by {}'.format( + data['repo']['url'], + data['repo']['repo_name'], + data['repo']['repo_type'], + data['actor']['username'], + ) + + +@task(ignore_result=True) +def post_text_to_hipchat(settings, text): + log.debug('sending %s to hipchat %s' % (text, settings['server_url'])) + resp = requests.post(settings['server_url'], json={ + "message": text, + "color": settings.get('color', 'yellow'), + "notify": settings.get('notify', False), + }) + resp.raise_for_status() # raise exception on a failed request diff --git a/rhodecode/integrations/types/slack.py b/rhodecode/integrations/types/slack.py new file mode 100644 --- /dev/null +++ b/rhodecode/integrations/types/slack.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2012-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +from __future__ import unicode_literals +import deform +import re +import logging +import requests +import colander +import textwrap +from celery.task import task +from mako.template import Template + +from rhodecode import events +from rhodecode.translation import lazy_ugettext +from rhodecode.lib import helpers as h +from rhodecode.lib.celerylib import run_task +from rhodecode.lib.colander_utils import strip_whitespace +from rhodecode.integrations.types.base import IntegrationTypeBase +from rhodecode.integrations.schema import IntegrationSettingsSchemaBase + +log = logging.getLogger(__name__) + + +class SlackSettingsSchema(IntegrationSettingsSchemaBase): + service = colander.SchemaNode( + colander.String(), + title=lazy_ugettext('Slack service URL'), + description=h.literal(lazy_ugettext( + 'This can be setup at the ' + '' + 'slack app manager')), + default='', + preparer=strip_whitespace, + validator=colander.url, + widget=deform.widget.TextInputWidget( + placeholder='https://hooks.slack.com/services/...', + ), + ) + username = colander.SchemaNode( + colander.String(), + title=lazy_ugettext('Username'), + description=lazy_ugettext('Username to show notifications coming from.'), + missing='Rhodecode', + preparer=strip_whitespace, + widget=deform.widget.TextInputWidget( + placeholder='Rhodecode' + ), + ) + channel = colander.SchemaNode( + colander.String(), + title=lazy_ugettext('Channel'), + description=lazy_ugettext('Channel to send notifications to.'), + missing='', + preparer=strip_whitespace, + widget=deform.widget.TextInputWidget( + placeholder='#general' + ), + ) + icon_emoji = colander.SchemaNode( + colander.String(), + title=lazy_ugettext('Emoji'), + description=lazy_ugettext('Emoji to use eg. :studio_microphone:'), + missing='', + preparer=strip_whitespace, + widget=deform.widget.TextInputWidget( + placeholder=':studio_microphone:' + ), + ) + + +repo_push_template = Template(r''' +*${data['actor']['username']}* pushed to \ +%if data['push']['branches']: +${len(data['push']['branches']) > 1 and 'branches' or 'branch'} \ +${', '.join('<%s|%s>' % (branch['url'], branch['name']) for branch in data['push']['branches'])} \ +%else: +unknown branch \ +%endif +in <${data['repo']['url']}|${data['repo']['repo_name']}> +>>> +%for commit in data['push']['commits']: +<${commit['url']}|${commit['short_id']}> - ${commit['message_html']|html_to_slack_links} +%endfor +''') + + +class SlackIntegrationType(IntegrationTypeBase): + key = 'slack' + display_name = lazy_ugettext('Slack') + SettingsSchema = SlackSettingsSchema + valid_events = [ + events.PullRequestCloseEvent, + events.PullRequestMergeEvent, + events.PullRequestUpdateEvent, + events.PullRequestCommentEvent, + events.PullRequestReviewEvent, + events.PullRequestCreateEvent, + events.RepoPushEvent, + events.RepoCreateEvent, + ] + + def send_event(self, event): + if event.__class__ not in self.valid_events: + log.debug('event not valid: %r' % event) + return + + if event.name not in self.settings['events']: + log.debug('event ignored: %r' % event) + return + + data = event.as_dict() + + text = '*%s* caused a *%s* event' % ( + data['actor']['username'], event.name) + + log.debug('handling slack event for %s' % event.name) + + if isinstance(event, events.PullRequestCommentEvent): + text = self.format_pull_request_comment_event(event, data) + elif isinstance(event, events.PullRequestReviewEvent): + text = self.format_pull_request_review_event(event, data) + elif isinstance(event, events.PullRequestEvent): + text = self.format_pull_request_event(event, data) + elif isinstance(event, events.RepoPushEvent): + text = self.format_repo_push_event(data) + elif isinstance(event, events.RepoCreateEvent): + text = self.format_repo_create_event(data) + else: + log.error('unhandled event type: %r' % event) + + run_task(post_text_to_slack, self.settings, text) + + def settings_schema(self): + schema = SlackSettingsSchema() + schema.add(colander.SchemaNode( + colander.Set(), + widget=deform.widget.CheckboxChoiceWidget( + values=sorted( + [(e.name, e.display_name) for e in self.valid_events] + ) + ), + description="Events activated for this integration", + name='events' + )) + + return schema + + def format_pull_request_comment_event(self, event, data): + comment_text = data['comment']['text'] + if len(comment_text) > 200: + comment_text = '<{comment_url}|{comment_text}...>'.format( + comment_text=comment_text[:200], + comment_url=data['comment']['url'], + ) + + comment_status = '' + if data['comment']['status']: + comment_status = '[{}]: '.format(data['comment']['status']) + + return (textwrap.dedent( + ''' + {user} commented on pull request <{pr_url}|#{number}> - {pr_title}: + >>> {comment_status}{comment_text} + ''').format( + comment_status=comment_status, + user=data['actor']['username'], + number=data['pullrequest']['pull_request_id'], + pr_url=data['pullrequest']['url'], + pr_status=data['pullrequest']['status'], + pr_title=data['pullrequest']['title'], + comment_text=comment_text + ) + ) + + def format_pull_request_review_event(self, event, data): + return (textwrap.dedent( + ''' + Status changed to {pr_status} for pull request <{pr_url}|#{number}> - {pr_title} + ''').format( + user=data['actor']['username'], + number=data['pullrequest']['pull_request_id'], + pr_url=data['pullrequest']['url'], + pr_status=data['pullrequest']['status'], + pr_title=data['pullrequest']['title'], + ) + ) + + def format_pull_request_event(self, event, data): + action = { + events.PullRequestCloseEvent: 'closed', + events.PullRequestMergeEvent: 'merged', + events.PullRequestUpdateEvent: 'updated', + events.PullRequestCreateEvent: 'created', + }.get(event.__class__, str(event.__class__)) + + return ('Pull request <{url}|#{number}> - {title} ' + '{action} by {user}').format( + user=data['actor']['username'], + number=data['pullrequest']['pull_request_id'], + url=data['pullrequest']['url'], + title=data['pullrequest']['title'], + action=action + ) + + def format_repo_push_event(self, data): + result = repo_push_template.render( + data=data, + html_to_slack_links=html_to_slack_links, + ) + return result + + def format_repo_create_event(self, data): + return '<{}|{}> ({}) repository created by *{}*'.format( + data['repo']['url'], + data['repo']['repo_name'], + data['repo']['repo_type'], + data['actor']['username'], + ) + + +def html_to_slack_links(message): + return re.compile(r'(.+?)').sub( + r'<\1|\2>', message) + + +@task(ignore_result=True) +def post_text_to_slack(settings, text): + log.debug('sending %s to slack %s' % (text, settings['service'])) + resp = requests.post(settings['service'], json={ + "channel": settings.get('channel', ''), + "username": settings.get('username', 'Rhodecode'), + "text": text, + "icon_emoji": settings.get('icon_emoji', ':studio_microphone:') + }) + resp.raise_for_status() # raise exception on a failed request diff --git a/rhodecode/integrations/types/webhook.py b/rhodecode/integrations/types/webhook.py new file mode 100644 --- /dev/null +++ b/rhodecode/integrations/types/webhook.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2012-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +from __future__ import unicode_literals + +import deform +import logging +import requests +import colander +from celery.task import task +from mako.template import Template + +from rhodecode import events +from rhodecode.translation import lazy_ugettext +from rhodecode.integrations.types.base import IntegrationTypeBase +from rhodecode.integrations.schema import IntegrationSettingsSchemaBase + +log = logging.getLogger(__name__) + + +class WebhookSettingsSchema(IntegrationSettingsSchemaBase): + url = colander.SchemaNode( + colander.String(), + title=lazy_ugettext('Webhook URL'), + description=lazy_ugettext('URL of the webhook to receive POST event.'), + default='', + validator=colander.url, + widget=deform.widget.TextInputWidget( + placeholder='https://www.example.com/webhook' + ), + ) + secret_token = colander.SchemaNode( + colander.String(), + title=lazy_ugettext('Secret Token'), + description=lazy_ugettext('String used to validate received payloads.'), + default='', + widget=deform.widget.TextInputWidget( + placeholder='secret_token' + ), + ) + + +class WebhookIntegrationType(IntegrationTypeBase): + key = 'webhook' + display_name = lazy_ugettext('Webhook') + valid_events = [ + events.PullRequestCloseEvent, + events.PullRequestMergeEvent, + events.PullRequestUpdateEvent, + events.PullRequestCommentEvent, + events.PullRequestReviewEvent, + events.PullRequestCreateEvent, + events.RepoPushEvent, + events.RepoCreateEvent, + ] + + def settings_schema(self): + schema = WebhookSettingsSchema() + schema.add(colander.SchemaNode( + colander.Set(), + widget=deform.widget.CheckboxChoiceWidget( + values=sorted( + [(e.name, e.display_name) for e in self.valid_events] + ) + ), + description="Events activated for this integration", + name='events' + )) + return schema + + def send_event(self, event): + log.debug('handling event %s with webhook integration %s', + event.name, self) + + if event.__class__ not in self.valid_events: + log.debug('event not valid: %r' % event) + return + + if event.name not in self.settings['events']: + log.debug('event ignored: %r' % event) + return + + data = event.as_dict() + post_to_webhook(data, self.settings) + + +@task(ignore_result=True) +def post_to_webhook(data, settings): + log.debug('sending event:%s to webhook %s', data['name'], settings['url']) + resp = requests.post(settings['url'], json={ + 'token': settings['secret_token'], + 'event': data + }) + resp.raise_for_status() # raise exception on a failed request diff --git a/rhodecode/integrations/views.py b/rhodecode/integrations/views.py new file mode 100644 --- /dev/null +++ b/rhodecode/integrations/views.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2012-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import colander +import logging +import pylons +import deform + +from pyramid.httpexceptions import HTTPFound, HTTPForbidden +from pyramid.renderers import render +from pyramid.response import Response + +from rhodecode.lib import auth +from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator +from rhodecode.model.db import Repository, Session, Integration +from rhodecode.model.scm import ScmModel +from rhodecode.model.integration import IntegrationModel +from rhodecode.admin.navigation import navigation_list +from rhodecode.translation import _ +from rhodecode.integrations import integration_type_registry + +log = logging.getLogger(__name__) + + +class IntegrationSettingsViewBase(object): + """ Base Integration settings view used by both repo / global settings """ + + def __init__(self, context, request): + self.context = context + self.request = request + self._load_general_context() + + if not self.perm_check(request.user): + raise HTTPForbidden() + + def _load_general_context(self): + """ + This avoids boilerplate for repo/global+list/edit+views/templates + by doing all possible contexts at the same time however it should + be split up into separate functions once more "contexts" exist + """ + + self.IntegrationType = None + self.repo = None + self.integration = None + self.integrations = {} + + request = self.request + + if 'repo_name' in request.matchdict: # we're in a repo context + repo_name = request.matchdict['repo_name'] + self.repo = Repository.get_by_repo_name(repo_name) + + if 'integration' in request.matchdict: # we're in integration context + integration_type = request.matchdict['integration'] + self.IntegrationType = integration_type_registry[integration_type] + + if 'integration_id' in request.matchdict: # single integration context + integration_id = request.matchdict['integration_id'] + self.integration = Integration.get(integration_id) + else: # list integrations context + for integration in IntegrationModel().get_integrations(self.repo): + self.integrations.setdefault(integration.integration_type, [] + ).append(integration) + + self.settings = self.integration and self.integration.settings or {} + + def _template_c_context(self): + # TODO: dan: this is a stopgap in order to inherit from current pylons + # based admin/repo settings templates - this should be removed entirely + # after port to pyramid + + c = pylons.tmpl_context + c.active = 'integrations' + c.rhodecode_user = self.request.user + c.repo = self.repo + c.repo_name = self.repo and self.repo.repo_name or None + if self.repo: + c.repo_info = self.repo + c.rhodecode_db_repo = self.repo + c.repository_pull_requests = ScmModel().get_pull_requests(self.repo) + else: + c.navlist = navigation_list(self.request) + + return c + + def _form_schema(self): + if self.integration: + settings = self.integration.settings + else: + settings = {} + return self.IntegrationType(settings=settings).settings_schema() + + def settings_get(self, defaults=None, errors=None, form=None): + """ + View that displays the plugin settings as a form. + """ + defaults = defaults or {} + errors = errors or {} + + if self.integration: + defaults = self.integration.settings or {} + defaults['name'] = self.integration.name + defaults['enabled'] = self.integration.enabled + else: + if self.repo: + scope = self.repo.repo_name + else: + scope = _('Global') + + defaults['name'] = '{} {} integration'.format(scope, + self.IntegrationType.display_name) + defaults['enabled'] = True + + schema = self._form_schema().bind(request=self.request) + + if self.integration: + buttons = ('submit', 'delete') + else: + buttons = ('submit',) + + form = form or deform.Form(schema, appstruct=defaults, buttons=buttons) + + for node in schema: + setting = self.settings.get(node.name) + if setting is not None: + defaults.setdefault(node.name, setting) + else: + if node.default: + defaults.setdefault(node.name, node.default) + + template_context = { + 'form': form, + 'defaults': defaults, + 'errors': errors, + 'schema': schema, + 'current_IntegrationType': self.IntegrationType, + 'integration': self.integration, + 'settings': self.settings, + 'resource': self.context, + 'c': self._template_c_context(), + } + + return template_context + + @auth.CSRFRequired() + def settings_post(self): + """ + View that validates and stores the plugin settings. + """ + if self.request.params.get('delete'): + Session().delete(self.integration) + Session().commit() + self.request.session.flash( + _('Integration {integration_name} deleted successfully.').format( + integration_name=self.integration.name), + queue='success') + if self.repo: + redirect_to = self.request.route_url( + 'repo_integrations_home', repo_name=self.repo.repo_name) + else: + redirect_to = self.request.route_url('global_integrations_home') + raise HTTPFound(redirect_to) + + schema = self._form_schema().bind(request=self.request) + + form = deform.Form(schema, buttons=('submit', 'delete')) + + params = {} + for node in schema.children: + if type(node.typ) in (colander.Set, colander.List): + val = self.request.params.getall(node.name) + else: + val = self.request.params.get(node.name) + if val: + params[node.name] = val + + controls = self.request.POST.items() + try: + valid_data = form.validate(controls) + except deform.ValidationFailure as e: + self.request.session.flash( + _('Errors exist when saving integration settings. ' + 'Please check the form inputs.'), + queue='error') + return self.settings_get(errors={}, defaults=params, form=e) + + if not self.integration: + self.integration = Integration() + self.integration.integration_type = self.IntegrationType.key + if self.repo: + self.integration.repo = self.repo + Session().add(self.integration) + + self.integration.enabled = valid_data.pop('enabled', False) + self.integration.name = valid_data.pop('name') + self.integration.settings = valid_data + + Session().commit() + + # Display success message and redirect. + self.request.session.flash( + _('Integration {integration_name} updated successfully.').format( + integration_name=self.IntegrationType.display_name), + queue='success') + + if self.repo: + redirect_to = self.request.route_url( + 'repo_integrations_edit', repo_name=self.repo.repo_name, + integration=self.integration.integration_type, + integration_id=self.integration.integration_id) + else: + redirect_to = self.request.route_url( + 'global_integrations_edit', + integration=self.integration.integration_type, + integration_id=self.integration.integration_id) + + return HTTPFound(redirect_to) + + def index(self): + current_integrations = self.integrations + if self.IntegrationType: + current_integrations = { + self.IntegrationType.key: self.integrations.get( + self.IntegrationType.key, []) + } + + template_context = { + 'current_IntegrationType': self.IntegrationType, + 'current_integrations': current_integrations, + 'available_integrations': integration_type_registry, + 'c': self._template_c_context() + } + + if self.repo: + html = render('rhodecode:templates/admin/integrations/list.html', + template_context, + request=self.request) + else: + html = render('rhodecode:templates/admin/integrations/list.html', + template_context, + request=self.request) + + return Response(html) + + +class GlobalIntegrationsView(IntegrationSettingsViewBase): + def perm_check(self, user): + return auth.HasPermissionAll('hg.admin').check_permissions(user=user) + + +class RepoIntegrationsView(IntegrationSettingsViewBase): + def perm_check(self, user): + return auth.HasRepoPermissionAll('repository.admin' + )(repo_name=self.repo.repo_name, user=user) diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -205,11 +205,12 @@ def vcs_operation_context( class BasicAuth(AuthBasicAuthenticator): - def __init__(self, realm, authfunc, auth_http_code=None, + def __init__(self, realm, authfunc, registry, auth_http_code=None, initial_call_detection=False): self.realm = realm self.initial_call = initial_call_detection self.authfunc = authfunc + self.registry = registry self._rc_auth_http_code = auth_http_code def _get_response_from_code(self, http_code): @@ -242,7 +243,8 @@ class BasicAuth(AuthBasicAuthenticator): if len(_parts) == 2: username, password = _parts if self.authfunc( - username, password, environ, VCS_TYPE): + username, password, environ, VCS_TYPE, + registry=self.registry): return username if username and password: # we mark that we actually executed authentication once, at @@ -254,7 +256,11 @@ class BasicAuth(AuthBasicAuthenticator): __call__ = authenticate -def attach_context_attributes(context): +def attach_context_attributes(context, request): + """ + Attach variables into template context called `c`, please note that + request could be pylons or pyramid request in here. + """ rc_config = SettingsModel().get_all_settings(cache=True) context.rhodecode_version = rhodecode.__version__ @@ -320,6 +326,36 @@ def attach_context_attributes(context): 'appenlight.api_public_key', '') context.appenlight_server_url = config.get('appenlight.server_url', '') + # JS template context + context.template_context = { + 'repo_name': None, + 'repo_type': None, + 'repo_landing_commit': None, + 'rhodecode_user': { + 'username': None, + 'email': None, + 'notification_status': False + }, + 'visual': { + 'default_renderer': None + }, + 'commit_data': { + 'commit_id': None + }, + 'pull_request_data': {'pull_request_id': None}, + 'timeago': { + 'refresh_time': 120 * 1000, + 'cutoff_limit': 1000 * 60 * 60 * 24 * 7 + }, + 'pylons_dispatch': { + # 'controller': request.environ['pylons.routes_dict']['controller'], + # 'action': request.environ['pylons.routes_dict']['action'], + }, + 'pyramid_dispatch': { + + }, + 'extra': {'plugins': {}} + } # END CONFIG VARS # TODO: This dosn't work when called from pylons compatibility tween. @@ -380,7 +416,7 @@ class BaseController(WSGIController): """ # on each call propagate settings calls into global settings. set_rhodecode_config(config) - attach_context_attributes(c) + attach_context_attributes(c, request) # TODO: Remove this when fixed in attach_context_attributes() c.repo_name = get_repo_slug(request) # can be empty diff --git a/rhodecode/lib/caches.py b/rhodecode/lib/caches.py --- a/rhodecode/lib/caches.py +++ b/rhodecode/lib/caches.py @@ -21,6 +21,7 @@ import beaker import logging +import threading from beaker.cache import _cache_decorate, cache_regions, region_invalidate @@ -35,7 +36,7 @@ FILE_SEARCH_TREE_META = 'cache_file_sear SUMMARY_STATS = 'cache_summary_stats' # This list of caches gets purged when invalidation happens -USED_REPO_CACHES = (FILE_TREE, FILE_TREE_META, FILE_TREE_META) +USED_REPO_CACHES = (FILE_TREE, FILE_SEARCH_TREE_META) DEFAULT_CACHE_MANAGER_CONFIG = { 'type': 'memorylru_base', @@ -170,7 +171,7 @@ class InvalidationContext(object): safe_str(self.repo_name), safe_str(self.cache_type)) def __init__(self, compute_func, repo_name, cache_type, - raise_exception=False): + raise_exception=False, thread_scoped=False): self.compute_func = compute_func self.repo_name = repo_name self.cache_type = cache_type @@ -178,6 +179,13 @@ class InvalidationContext(object): repo_name, cache_type) self.raise_exception = raise_exception + # Append the thread id to the cache key if this invalidation context + # should be scoped to the current thread. + if thread_scoped: + thread_id = threading.current_thread().ident + self.cache_key = '{cache_key}_{thread_id}'.format( + cache_key=self.cache_key, thread_id=thread_id) + def get_cache_obj(self): cache_key = CacheKey.get_cache_key( self.repo_name, self.cache_type) diff --git a/rhodecode/lib/celerylib/__init__.py b/rhodecode/lib/celerylib/__init__.py --- a/rhodecode/lib/celerylib/__init__.py +++ b/rhodecode/lib/celerylib/__init__.py @@ -22,6 +22,7 @@ celery libs for RhodeCode """ +import pylons import socket import logging @@ -29,16 +30,23 @@ import rhodecode from os.path import join as jn from pylons import config +from celery.task import Task +from pyramid.request import Request +from pyramid.scripting import prepare +from pyramid.threadlocal import get_current_request from decorator import decorator from zope.cachedescriptors.property import Lazy as LazyProperty from rhodecode.config import utils -from rhodecode.lib.utils2 import safe_str, md5_safe, aslist +from rhodecode.lib.utils2 import ( + safe_str, md5_safe, aslist, get_routes_generator_for_server_url, + get_server_url) from rhodecode.lib.pidlock import DaemonLock, LockHeld from rhodecode.lib.vcs import connect_vcs from rhodecode.model import meta +from rhodecode.lib.auth import AuthUser log = logging.getLogger(__name__) @@ -52,6 +60,76 @@ class ResultWrapper(object): return self.task +class RhodecodeCeleryTask(Task): + """ + This is a celery task which will create a rhodecode app instance context + for the task, patch pyramid + pylons threadlocals with the original request + that created the task and also add the user to the context. + + This class as a whole should be removed once the pylons port is complete + and a pyramid only solution for celery is implemented as per issue #4139 + """ + + def apply_async(self, args=None, kwargs=None, task_id=None, producer=None, + link=None, link_error=None, **options): + """ queue the job to run (we are in web request context here) """ + + request = get_current_request() + + if request: + # we hook into kwargs since it is the only way to pass our data to + # the celery worker in celery 2.2 + kwargs.update({ + '_rhodecode_proxy_data': { + 'environ': { + 'PATH_INFO': request.environ['PATH_INFO'], + 'SCRIPT_NAME': request.environ['SCRIPT_NAME'], + 'HTTP_HOST': request.environ.get('HTTP_HOST', + request.environ['SERVER_NAME']), + 'SERVER_NAME': request.environ['SERVER_NAME'], + 'SERVER_PORT': request.environ['SERVER_PORT'], + 'wsgi.url_scheme': request.environ['wsgi.url_scheme'], + }, + 'auth_user': { + 'ip_addr': request.user.ip_addr, + 'user_id': request.user.user_id + }, + } + }) + return super(RhodecodeCeleryTask, self).apply_async( + args, kwargs, task_id, producer, link, link_error, **options) + + def __call__(self, *args, **kwargs): + """ rebuild the context and then run task on celery worker """ + proxy_data = kwargs.pop('_rhodecode_proxy_data', {}) + + if not proxy_data: + return super(RhodecodeCeleryTask, self).__call__(*args, **kwargs) + + log.debug('using celery proxy data to run task: %r', proxy_data) + + from rhodecode.config.routing import make_map + + request = Request.blank('/', environ=proxy_data['environ']) + request.user = AuthUser(user_id=proxy_data['auth_user']['user_id'], + ip_addr=proxy_data['auth_user']['ip_addr']) + + pyramid_request = prepare(request) # set pyramid threadlocal request + + # pylons routing + if not rhodecode.CONFIG.get('routes.map'): + rhodecode.CONFIG['routes.map'] = make_map(config) + pylons.url._push_object(get_routes_generator_for_server_url( + get_server_url(request.environ) + )) + + try: + return super(RhodecodeCeleryTask, self).__call__(*args, **kwargs) + finally: + pyramid_request['closer']() + pylons.url._pop_object() + + def run_task(task, *args, **kwargs): if rhodecode.CELERY_ENABLED: celery_is_up = False @@ -131,16 +209,16 @@ def dbsession(func): def vcsconnection(func): def __wrapper(func, *fargs, **fkwargs): if rhodecode.CELERY_ENABLED and not rhodecode.CELERY_EAGER: - backends = config['vcs.backends'] = aslist( - config.get('vcs.backends', 'hg,git'), sep=',') + settings = rhodecode.PYRAMID_SETTINGS + backends = settings['vcs.backends'] for alias in rhodecode.BACKENDS.keys(): if alias not in backends: del rhodecode.BACKENDS[alias] - utils.configure_pyro4(config) - utils.configure_vcs(config) + utils.configure_pyro4(settings) + utils.configure_vcs(settings) connect_vcs( - config['vcs.server'], - utils.get_vcs_server_protocol(config)) + settings['vcs.server'], + utils.get_vcs_server_protocol(settings)) ret = func(*fargs, **fkwargs) return ret diff --git a/rhodecode/lib/celerylib/tasks.py b/rhodecode/lib/celerylib/tasks.py --- a/rhodecode/lib/celerylib/tasks.py +++ b/rhodecode/lib/celerylib/tasks.py @@ -33,7 +33,7 @@ from pylons import config import rhodecode from rhodecode.lib.celerylib import ( run_task, dbsession, __get_lockkey, LockHeld, DaemonLock, - get_session, vcsconnection) + get_session, vcsconnection, RhodecodeCeleryTask) from rhodecode.lib.hooks_base import log_create_repository from rhodecode.lib.rcmail.smtp_mailer import SmtpMailer from rhodecode.lib.utils import add_cache, action_logger @@ -56,7 +56,7 @@ def get_logger(cls): return log -@task(ignore_result=True) +@task(ignore_result=True, base=RhodecodeCeleryTask) @dbsession def send_email(recipients, subject, body='', html_body='', email_config=None): """ @@ -70,7 +70,7 @@ def send_email(recipients, subject, body """ log = get_logger(send_email) - email_config = email_config or config + email_config = email_config or rhodecode.CONFIG subject = "%s %s" % (email_config.get('email_prefix', ''), subject) if not recipients: # if recipients are not defined we send to email_config + all admins @@ -104,7 +104,7 @@ def send_email(recipients, subject, body return True -@task(ignore_result=False) +@task(ignore_result=True, base=RhodecodeCeleryTask) @dbsession @vcsconnection def create_repo(form_data, cur_user): @@ -197,7 +197,7 @@ def create_repo(form_data, cur_user): return True -@task(ignore_result=False) +@task(ignore_result=True, base=RhodecodeCeleryTask) @dbsession @vcsconnection def create_repo_fork(form_data, cur_user): diff --git a/rhodecode/lib/celerypylons/loader.py b/rhodecode/lib/celerypylons/loader.py --- a/rhodecode/lib/celerypylons/loader.py +++ b/rhodecode/lib/celerypylons/loader.py @@ -18,15 +18,16 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ +import pylons +import rhodecode + from celery.loaders.base import BaseLoader -from pylons import config to_pylons = lambda x: x.replace('_', '.').lower() to_celery = lambda x: x.replace('.', '_').upper() LIST_PARAMS = """CELERY_IMPORTS ADMINS ROUTES""".split() - class PylonsSettingsProxy(object): """Pylons Settings Proxy @@ -35,9 +36,11 @@ class PylonsSettingsProxy(object): """ def __getattr__(self, key): pylons_key = to_pylons(key) + proxy_config = rhodecode.PYRAMID_SETTINGS or pylons.config try: - value = config[pylons_key] - if key in LIST_PARAMS:return value.split() + value = proxy_config[pylons_key] + if key in LIST_PARAMS: + return value.split() return self.type_converter(value) except KeyError: raise AttributeError(pylons_key) @@ -56,7 +59,8 @@ class PylonsSettingsProxy(object): def __setattr__(self, key, value): pylons_key = to_pylons(key) - config[pylons_key] = value + proxy_config = rhodecode.PYRAMID_SETTINGS or pylons.config + proxy_config[pylons_key] = value def __setitem__(self, key, value): self.__setattr__(key, value) @@ -86,3 +90,8 @@ class PylonsLoader(BaseLoader): Import task modules. """ self.import_default_modules() + from rhodecode.config.middleware import make_pyramid_app + + # adding to self to keep a reference around + self.pyramid_app = make_pyramid_app( + pylons.config, **pylons.config['app_conf']) diff --git a/rhodecode/lib/channelstream.py b/rhodecode/lib/channelstream.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/channelstream.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging +import os + +import itsdangerous +import requests + +from dogpile.core import ReadWriteMutex + +import rhodecode.lib.helpers as h + +from rhodecode.lib.auth import HasRepoPermissionAny +from rhodecode.lib.ext_json import json +from rhodecode.model.db import User + +log = logging.getLogger(__name__) + +LOCK = ReadWriteMutex() + +STATE_PUBLIC_KEYS = ['id', 'username', 'first_name', 'last_name', + 'icon_link', 'display_name', 'display_link'] + + +class ChannelstreamException(Exception): + pass + + +class ChannelstreamConnectionException(Exception): + pass + + +class ChannelstreamPermissionException(Exception): + pass + + +def channelstream_request(config, payload, endpoint, raise_exc=True): + signer = itsdangerous.TimestampSigner(config['secret']) + sig_for_server = signer.sign(endpoint) + secret_headers = {'x-channelstream-secret': sig_for_server, + 'x-channelstream-endpoint': endpoint, + 'Content-Type': 'application/json'} + req_url = 'http://{}{}'.format(config['server'], endpoint) + response = None + try: + response = requests.post(req_url, data=json.dumps(payload), + headers=secret_headers).json() + except requests.ConnectionError: + log.exception('ConnectionError happened') + if raise_exc: + raise ChannelstreamConnectionException() + except Exception: + log.exception('Exception related to channelstream happened') + if raise_exc: + raise ChannelstreamConnectionException() + return response + + +def get_user_data(user_id): + user = User.get(user_id) + return { + 'id': user.user_id, + 'username': user.username, + 'first_name': user.name, + 'last_name': user.lastname, + 'icon_link': h.gravatar_url(user.email, 14), + 'display_name': h.person(user, 'username_or_name_or_email'), + 'display_link': h.link_to_user(user), + } + + +def broadcast_validator(channel_name): + """ checks if user can access the broadcast channel """ + if channel_name == 'broadcast': + return True + + +def repo_validator(channel_name): + """ checks if user can access the broadcast channel """ + channel_prefix = '/repo$' + if channel_name.startswith(channel_prefix): + elements = channel_name[len(channel_prefix):].split('$') + repo_name = elements[0] + can_access = HasRepoPermissionAny( + 'repository.read', + 'repository.write', + 'repository.admin')(repo_name) + log.debug('permission check for {} channel ' + 'resulted in {}'.format(repo_name, can_access)) + if can_access: + return True + return False + + +def check_channel_permissions(channels, plugin_validators, should_raise=True): + valid_channels = [] + + validators = [broadcast_validator, repo_validator] + if plugin_validators: + validators.extend(plugin_validators) + for channel_name in channels: + is_valid = False + for validator in validators: + if validator(channel_name): + is_valid = True + break + if is_valid: + valid_channels.append(channel_name) + else: + if should_raise: + raise ChannelstreamPermissionException() + return valid_channels + + +def get_channels_info(self, channels): + payload = {'channels': channels} + # gather persistence info + return channelstream_request(self._config(), payload, '/info') + + +def parse_channels_info(info_result, include_channel_info=None): + """ + Returns data that contains only secure information that can be + presented to clients + """ + include_channel_info = include_channel_info or [] + + user_state_dict = {} + for userinfo in info_result['users']: + user_state_dict[userinfo['user']] = { + k: v for k, v in userinfo['state'].items() + if k in STATE_PUBLIC_KEYS + } + + channels_info = {} + + for c_name, c_info in info_result['channels'].items(): + if c_name not in include_channel_info: + continue + connected_list = [] + for userinfo in c_info['users']: + connected_list.append({ + 'user': userinfo['user'], + 'state': user_state_dict[userinfo['user']] + }) + channels_info[c_name] = {'users': connected_list, + 'history': c_info['history']} + + return channels_info + + +def log_filepath(history_location, channel_name): + filename = '{}.log'.format(channel_name.encode('hex')) + filepath = os.path.join(history_location, filename) + return filepath + + +def read_history(history_location, channel_name): + filepath = log_filepath(history_location, channel_name) + if not os.path.exists(filepath): + return [] + history_lines_limit = -100 + history = [] + with open(filepath, 'rb') as f: + for line in f.readlines()[history_lines_limit:]: + try: + history.append(json.loads(line)) + except Exception: + log.exception('Failed to load history') + return history + + +def update_history_from_logs(config, channels, payload): + history_location = config.get('history.location') + for channel in channels: + history = read_history(history_location, channel) + payload['channels_info'][channel]['history'] = history + + +def write_history(config, message): + """ writes a messge to a base64encoded filename """ + history_location = config.get('history.location') + if not os.path.exists(history_location): + return + try: + LOCK.acquire_write_lock() + filepath = log_filepath(history_location, message['channel']) + with open(filepath, 'ab') as f: + json.dump(message, f) + f.write('\n') + finally: + LOCK.release_write_lock() + + +def get_connection_validators(registry): + validators = [] + for k, config in registry.rhodecode_plugins.iteritems(): + validator = config.get('channelstream', {}).get('connect_validator') + if validator: + validators.append(validator) + return validators diff --git a/rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py b/rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/schema/db_4_3_0_0.py @@ -0,0 +1,3516 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +""" +Database Models for RhodeCode Enterprise +""" + +import os +import sys +import time +import hashlib +import logging +import datetime +import warnings +import ipaddress +import functools +import traceback +import collections + + +from sqlalchemy import * +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import ( + relationship, joinedload, class_mapper, validates, aliased) +from sqlalchemy.sql.expression import true +from beaker.cache import cache_region, region_invalidate +from webob.exc import HTTPNotFound +from zope.cachedescriptors.property import Lazy as LazyProperty + +from pylons import url +from pylons.i18n.translation import lazy_ugettext as _ + +from rhodecode.lib.vcs import get_backend +from rhodecode.lib.vcs.utils.helpers import get_scm +from rhodecode.lib.vcs.exceptions import VCSError +from rhodecode.lib.vcs.backends.base import ( + EmptyCommit, Reference, MergeFailureReason) +from rhodecode.lib.utils2 import ( + str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe, + time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict) +from rhodecode.lib.ext_json import json +from rhodecode.lib.caching_query import FromCache +from rhodecode.lib.encrypt import AESCipher + +from rhodecode.model.meta import Base, Session + +URL_SEP = '/' +log = logging.getLogger(__name__) + +# ============================================================================= +# BASE CLASSES +# ============================================================================= + +# this is propagated from .ini file rhodecode.encrypted_values.secret or +# beaker.session.secret if first is not set. +# and initialized at environment.py +ENCRYPTION_KEY = None + +# used to sort permissions by types, '#' used here is not allowed to be in +# usernames, and it's very early in sorted string.printable table. +PERMISSION_TYPE_SORT = { + 'admin': '####', + 'write': '###', + 'read': '##', + 'none': '#', +} + + +def display_sort(obj): + """ + Sort function used to sort permissions in .permissions() function of + Repository, RepoGroup, UserGroup. Also it put the default user in front + of all other resources + """ + + if obj.username == User.DEFAULT_USER: + return '#####' + prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '') + return prefix + obj.username + + +def _hash_key(k): + return md5_safe(k) + + +class EncryptedTextValue(TypeDecorator): + """ + Special column for encrypted long text data, use like:: + + value = Column("encrypted_value", EncryptedValue(), nullable=False) + + This column is intelligent so if value is in unencrypted form it return + unencrypted form, but on save it always encrypts + """ + impl = Text + + def process_bind_param(self, value, dialect): + if not value: + return value + if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'): + # protect against double encrypting if someone manually starts + # doing + raise ValueError('value needs to be in unencrypted format, ie. ' + 'not starting with enc$aes') + return 'enc$aes_hmac$%s' % AESCipher( + ENCRYPTION_KEY, hmac=True).encrypt(value) + + def process_result_value(self, value, dialect): + import rhodecode + + if not value: + return value + + parts = value.split('$', 3) + if not len(parts) == 3: + # probably not encrypted values + return value + else: + if parts[0] != 'enc': + # parts ok but without our header ? + return value + enc_strict_mode = str2bool(rhodecode.CONFIG.get( + 'rhodecode.encrypted_values.strict') or True) + # at that stage we know it's our encryption + if parts[1] == 'aes': + decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2]) + elif parts[1] == 'aes_hmac': + decrypted_data = AESCipher( + ENCRYPTION_KEY, hmac=True, + strict_verification=enc_strict_mode).decrypt(parts[2]) + else: + raise ValueError( + 'Encryption type part is wrong, must be `aes` ' + 'or `aes_hmac`, got `%s` instead' % (parts[1])) + return decrypted_data + + +class BaseModel(object): + """ + Base Model for all classes + """ + + @classmethod + def _get_keys(cls): + """return column names for this model """ + return class_mapper(cls).c.keys() + + def get_dict(self): + """ + return dict with keys and values corresponding + to this model data """ + + d = {} + for k in self._get_keys(): + d[k] = getattr(self, k) + + # also use __json__() if present to get additional fields + _json_attr = getattr(self, '__json__', None) + if _json_attr: + # update with attributes from __json__ + if callable(_json_attr): + _json_attr = _json_attr() + for k, val in _json_attr.iteritems(): + d[k] = val + return d + + def get_appstruct(self): + """return list with keys and values tuples corresponding + to this model data """ + + l = [] + for k in self._get_keys(): + l.append((k, getattr(self, k),)) + return l + + def populate_obj(self, populate_dict): + """populate model with data from given populate_dict""" + + for k in self._get_keys(): + if k in populate_dict: + setattr(self, k, populate_dict[k]) + + @classmethod + def query(cls): + return Session().query(cls) + + @classmethod + def get(cls, id_): + if id_: + return cls.query().get(id_) + + @classmethod + def get_or_404(cls, id_): + try: + id_ = int(id_) + except (TypeError, ValueError): + raise HTTPNotFound + + res = cls.query().get(id_) + if not res: + raise HTTPNotFound + return res + + @classmethod + def getAll(cls): + # deprecated and left for backward compatibility + return cls.get_all() + + @classmethod + def get_all(cls): + return cls.query().all() + + @classmethod + def delete(cls, id_): + obj = cls.query().get(id_) + Session().delete(obj) + + @classmethod + def identity_cache(cls, session, attr_name, value): + exist_in_session = [] + for (item_cls, pkey), instance in session.identity_map.items(): + if cls == item_cls and getattr(instance, attr_name) == value: + exist_in_session.append(instance) + if exist_in_session: + if len(exist_in_session) == 1: + return exist_in_session[0] + log.exception( + 'multiple objects with attr %s and ' + 'value %s found with same name: %r', + attr_name, value, exist_in_session) + + def __repr__(self): + if hasattr(self, '__unicode__'): + # python repr needs to return str + try: + return safe_str(self.__unicode__()) + except UnicodeDecodeError: + pass + return '' % (self.__class__.__name__) + + +class RhodeCodeSetting(Base, BaseModel): + __tablename__ = 'rhodecode_settings' + __table_args__ = ( + UniqueConstraint('app_settings_name'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + SETTINGS_TYPES = { + 'str': safe_str, + 'int': safe_int, + 'unicode': safe_unicode, + 'bool': str2bool, + 'list': functools.partial(aslist, sep=',') + } + DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions' + GLOBAL_CONF_KEY = 'app_settings' + + app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None) + _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None) + _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None) + + def __init__(self, key='', val='', type='unicode'): + self.app_settings_name = key + self.app_settings_type = type + self.app_settings_value = val + + @validates('_app_settings_value') + def validate_settings_value(self, key, val): + assert type(val) == unicode + return val + + @hybrid_property + def app_settings_value(self): + v = self._app_settings_value + _type = self.app_settings_type + if _type: + _type = self.app_settings_type.split('.')[0] + # decode the encrypted value + if 'encrypted' in self.app_settings_type: + cipher = EncryptedTextValue() + v = safe_unicode(cipher.process_result_value(v, None)) + + converter = self.SETTINGS_TYPES.get(_type) or \ + self.SETTINGS_TYPES['unicode'] + return converter(v) + + @app_settings_value.setter + def app_settings_value(self, val): + """ + Setter that will always make sure we use unicode in app_settings_value + + :param val: + """ + val = safe_unicode(val) + # encode the encrypted value + if 'encrypted' in self.app_settings_type: + cipher = EncryptedTextValue() + val = safe_unicode(cipher.process_bind_param(val, None)) + self._app_settings_value = val + + @hybrid_property + def app_settings_type(self): + return self._app_settings_type + + @app_settings_type.setter + def app_settings_type(self, val): + if val.split('.')[0] not in self.SETTINGS_TYPES: + raise Exception('type must be one of %s got %s' + % (self.SETTINGS_TYPES.keys(), val)) + self._app_settings_type = val + + def __unicode__(self): + return u"<%s('%s:%s[%s]')>" % ( + self.__class__.__name__, + self.app_settings_name, self.app_settings_value, + self.app_settings_type + ) + + +class RhodeCodeUi(Base, BaseModel): + __tablename__ = 'rhodecode_ui' + __table_args__ = ( + UniqueConstraint('ui_key'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + HOOK_REPO_SIZE = 'changegroup.repo_size' + # HG + HOOK_PRE_PULL = 'preoutgoing.pre_pull' + HOOK_PULL = 'outgoing.pull_logger' + HOOK_PRE_PUSH = 'prechangegroup.pre_push' + HOOK_PUSH = 'changegroup.push_logger' + + # TODO: johbo: Unify way how hooks are configured for git and hg, + # git part is currently hardcoded. + + # SVN PATTERNS + SVN_BRANCH_ID = 'vcs_svn_branch' + SVN_TAG_ID = 'vcs_svn_tag' + + ui_id = Column( + "ui_id", Integer(), nullable=False, unique=True, default=None, + primary_key=True) + ui_section = Column( + "ui_section", String(255), nullable=True, unique=None, default=None) + ui_key = Column( + "ui_key", String(255), nullable=True, unique=None, default=None) + ui_value = Column( + "ui_value", String(255), nullable=True, unique=None, default=None) + ui_active = Column( + "ui_active", Boolean(), nullable=True, unique=None, default=True) + + def __repr__(self): + return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section, + self.ui_key, self.ui_value) + + +class RepoRhodeCodeSetting(Base, BaseModel): + __tablename__ = 'repo_rhodecode_settings' + __table_args__ = ( + UniqueConstraint( + 'app_settings_name', 'repository_id', + name='uq_repo_rhodecode_setting_name_repo_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + repository_id = Column( + "repository_id", Integer(), ForeignKey('repositories.repo_id'), + nullable=False) + app_settings_id = Column( + "app_settings_id", Integer(), nullable=False, unique=True, + default=None, primary_key=True) + app_settings_name = Column( + "app_settings_name", String(255), nullable=True, unique=None, + default=None) + _app_settings_value = Column( + "app_settings_value", String(4096), nullable=True, unique=None, + default=None) + _app_settings_type = Column( + "app_settings_type", String(255), nullable=True, unique=None, + default=None) + + repository = relationship('Repository') + + def __init__(self, repository_id, key='', val='', type='unicode'): + self.repository_id = repository_id + self.app_settings_name = key + self.app_settings_type = type + self.app_settings_value = val + + @validates('_app_settings_value') + def validate_settings_value(self, key, val): + assert type(val) == unicode + return val + + @hybrid_property + def app_settings_value(self): + v = self._app_settings_value + type_ = self.app_settings_type + SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES + converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode'] + return converter(v) + + @app_settings_value.setter + def app_settings_value(self, val): + """ + Setter that will always make sure we use unicode in app_settings_value + + :param val: + """ + self._app_settings_value = safe_unicode(val) + + @hybrid_property + def app_settings_type(self): + return self._app_settings_type + + @app_settings_type.setter + def app_settings_type(self, val): + SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES + if val not in SETTINGS_TYPES: + raise Exception('type must be one of %s got %s' + % (SETTINGS_TYPES.keys(), val)) + self._app_settings_type = val + + def __unicode__(self): + return u"<%s('%s:%s:%s[%s]')>" % ( + self.__class__.__name__, self.repository.repo_name, + self.app_settings_name, self.app_settings_value, + self.app_settings_type + ) + + +class RepoRhodeCodeUi(Base, BaseModel): + __tablename__ = 'repo_rhodecode_ui' + __table_args__ = ( + UniqueConstraint( + 'repository_id', 'ui_section', 'ui_key', + name='uq_repo_rhodecode_ui_repository_id_section_key'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + repository_id = Column( + "repository_id", Integer(), ForeignKey('repositories.repo_id'), + nullable=False) + ui_id = Column( + "ui_id", Integer(), nullable=False, unique=True, default=None, + primary_key=True) + ui_section = Column( + "ui_section", String(255), nullable=True, unique=None, default=None) + ui_key = Column( + "ui_key", String(255), nullable=True, unique=None, default=None) + ui_value = Column( + "ui_value", String(255), nullable=True, unique=None, default=None) + ui_active = Column( + "ui_active", Boolean(), nullable=True, unique=None, default=True) + + repository = relationship('Repository') + + def __repr__(self): + return '<%s[%s:%s]%s=>%s]>' % ( + self.__class__.__name__, self.repository.repo_name, + self.ui_section, self.ui_key, self.ui_value) + + +class User(Base, BaseModel): + __tablename__ = 'users' + __table_args__ = ( + UniqueConstraint('username'), UniqueConstraint('email'), + Index('u_username_idx', 'username'), + Index('u_email_idx', 'email'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + DEFAULT_USER = 'default' + DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org' + DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}' + + user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + username = Column("username", String(255), nullable=True, unique=None, default=None) + password = Column("password", String(255), nullable=True, unique=None, default=None) + active = Column("active", Boolean(), nullable=True, unique=None, default=True) + admin = Column("admin", Boolean(), nullable=True, unique=None, default=False) + name = Column("firstname", String(255), nullable=True, unique=None, default=None) + lastname = Column("lastname", String(255), nullable=True, unique=None, default=None) + _email = Column("email", String(255), nullable=True, unique=None, default=None) + last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None) + extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None) + extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None) + api_key = Column("api_key", String(255), nullable=True, unique=None, default=None) + inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data + + user_log = relationship('UserLog') + user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all') + + repositories = relationship('Repository') + repository_groups = relationship('RepoGroup') + user_groups = relationship('UserGroup') + + user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all') + followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all') + + repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all') + repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all') + user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all') + + group_member = relationship('UserGroupMember', cascade='all') + + notifications = relationship('UserNotification', cascade='all') + # notifications assigned to this user + user_created_notifications = relationship('Notification', cascade='all') + # comments created by this user + user_comments = relationship('ChangesetComment', cascade='all') + # user profile extra info + user_emails = relationship('UserEmailMap', cascade='all') + user_ip_map = relationship('UserIpMap', cascade='all') + user_auth_tokens = relationship('UserApiKeys', cascade='all') + # gists + user_gists = relationship('Gist', cascade='all') + # user pull requests + user_pull_requests = relationship('PullRequest', cascade='all') + # external identities + extenal_identities = relationship( + 'ExternalIdentity', + primaryjoin="User.user_id==ExternalIdentity.local_user_id", + cascade='all') + + def __unicode__(self): + return u"<%s('id:%s:%s')>" % (self.__class__.__name__, + self.user_id, self.username) + + @hybrid_property + def email(self): + return self._email + + @email.setter + def email(self, val): + self._email = val.lower() if val else None + + @property + def firstname(self): + # alias for future + return self.name + + @property + def emails(self): + other = UserEmailMap.query().filter(UserEmailMap.user==self).all() + return [self.email] + [x.email for x in other] + + @property + def auth_tokens(self): + return [self.api_key] + [x.api_key for x in self.extra_auth_tokens] + + @property + def extra_auth_tokens(self): + return UserApiKeys.query().filter(UserApiKeys.user == self).all() + + @property + def feed_token(self): + feed_tokens = UserApiKeys.query()\ + .filter(UserApiKeys.user == self)\ + .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\ + .all() + if feed_tokens: + return feed_tokens[0].api_key + else: + # use the main token so we don't end up with nothing... + return self.api_key + + @classmethod + def extra_valid_auth_tokens(cls, user, role=None): + tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\ + .filter(or_(UserApiKeys.expires == -1, + UserApiKeys.expires >= time.time())) + if role: + tokens = tokens.filter(or_(UserApiKeys.role == role, + UserApiKeys.role == UserApiKeys.ROLE_ALL)) + return tokens.all() + + @property + def ip_addresses(self): + ret = UserIpMap.query().filter(UserIpMap.user == self).all() + return [x.ip_addr for x in ret] + + @property + def username_and_name(self): + return '%s (%s %s)' % (self.username, self.firstname, self.lastname) + + @property + def username_or_name_or_email(self): + full_name = self.full_name if self.full_name is not ' ' else None + return self.username or full_name or self.email + + @property + def full_name(self): + return '%s %s' % (self.firstname, self.lastname) + + @property + def full_name_or_username(self): + return ('%s %s' % (self.firstname, self.lastname) + if (self.firstname and self.lastname) else self.username) + + @property + def full_contact(self): + return '%s %s <%s>' % (self.firstname, self.lastname, self.email) + + @property + def short_contact(self): + return '%s %s' % (self.firstname, self.lastname) + + @property + def is_admin(self): + return self.admin + + @property + def AuthUser(self): + """ + Returns instance of AuthUser for this user + """ + from rhodecode.lib.auth import AuthUser + return AuthUser(user_id=self.user_id, api_key=self.api_key, + username=self.username) + + @hybrid_property + def user_data(self): + if not self._user_data: + return {} + + try: + return json.loads(self._user_data) + except TypeError: + return {} + + @user_data.setter + def user_data(self, val): + if not isinstance(val, dict): + raise Exception('user_data must be dict, got %s' % type(val)) + try: + self._user_data = json.dumps(val) + except Exception: + log.error(traceback.format_exc()) + + @classmethod + def get_by_username(cls, username, case_insensitive=False, + cache=False, identity_cache=False): + session = Session() + + if case_insensitive: + q = cls.query().filter( + func.lower(cls.username) == func.lower(username)) + else: + q = cls.query().filter(cls.username == username) + + if cache: + if identity_cache: + val = cls.identity_cache(session, 'username', username) + if val: + return val + else: + q = q.options( + FromCache("sql_cache_short", + "get_user_by_name_%s" % _hash_key(username))) + + return q.scalar() + + @classmethod + def get_by_auth_token(cls, auth_token, cache=False, fallback=True): + q = cls.query().filter(cls.api_key == auth_token) + + if cache: + q = q.options(FromCache("sql_cache_short", + "get_auth_token_%s" % auth_token)) + res = q.scalar() + + if fallback and not res: + #fallback to additional keys + _res = UserApiKeys.query()\ + .filter(UserApiKeys.api_key == auth_token)\ + .filter(or_(UserApiKeys.expires == -1, + UserApiKeys.expires >= time.time()))\ + .first() + if _res: + res = _res.user + return res + + @classmethod + def get_by_email(cls, email, case_insensitive=False, cache=False): + + if case_insensitive: + q = cls.query().filter(func.lower(cls.email) == func.lower(email)) + + else: + q = cls.query().filter(cls.email == email) + + if cache: + q = q.options(FromCache("sql_cache_short", + "get_email_key_%s" % email)) + + ret = q.scalar() + if ret is None: + q = UserEmailMap.query() + # try fetching in alternate email map + if case_insensitive: + q = q.filter(func.lower(UserEmailMap.email) == func.lower(email)) + else: + q = q.filter(UserEmailMap.email == email) + q = q.options(joinedload(UserEmailMap.user)) + if cache: + q = q.options(FromCache("sql_cache_short", + "get_email_map_key_%s" % email)) + ret = getattr(q.scalar(), 'user', None) + + return ret + + @classmethod + def get_from_cs_author(cls, author): + """ + Tries to get User objects out of commit author string + + :param author: + """ + from rhodecode.lib.helpers import email, author_name + # Valid email in the attribute passed, see if they're in the system + _email = email(author) + if _email: + user = cls.get_by_email(_email, case_insensitive=True) + if user: + return user + # Maybe we can match by username? + _author = author_name(author) + user = cls.get_by_username(_author, case_insensitive=True) + if user: + return user + + def update_userdata(self, **kwargs): + usr = self + old = usr.user_data + old.update(**kwargs) + usr.user_data = old + Session().add(usr) + log.debug('updated userdata with ', kwargs) + + def update_lastlogin(self): + """Update user lastlogin""" + self.last_login = datetime.datetime.now() + Session().add(self) + log.debug('updated user %s lastlogin', self.username) + + def update_lastactivity(self): + """Update user lastactivity""" + usr = self + old = usr.user_data + old.update({'last_activity': time.time()}) + usr.user_data = old + Session().add(usr) + log.debug('updated user %s lastactivity', usr.username) + + def update_password(self, new_password, change_api_key=False): + from rhodecode.lib.auth import get_crypt_password,generate_auth_token + + self.password = get_crypt_password(new_password) + if change_api_key: + self.api_key = generate_auth_token(self.username) + Session().add(self) + + @classmethod + def get_first_super_admin(cls): + user = User.query().filter(User.admin == true()).first() + if user is None: + raise Exception('FATAL: Missing administrative account!') + return user + + @classmethod + def get_all_super_admins(cls): + """ + Returns all admin accounts sorted by username + """ + return User.query().filter(User.admin == true())\ + .order_by(User.username.asc()).all() + + @classmethod + def get_default_user(cls, cache=False): + user = User.get_by_username(User.DEFAULT_USER, cache=cache) + if user is None: + raise Exception('FATAL: Missing default account!') + return user + + def _get_default_perms(self, user, suffix=''): + from rhodecode.model.permission import PermissionModel + return PermissionModel().get_default_perms(user.user_perms, suffix) + + def get_default_perms(self, suffix=''): + return self._get_default_perms(self, suffix) + + def get_api_data(self, include_secrets=False, details='full'): + """ + Common function for generating user related data for API + + :param include_secrets: By default secrets in the API data will be replaced + by a placeholder value to prevent exposing this data by accident. In case + this data shall be exposed, set this flag to ``True``. + + :param details: details can be 'basic|full' basic gives only a subset of + the available user information that includes user_id, name and emails. + """ + user = self + user_data = self.user_data + data = { + 'user_id': user.user_id, + 'username': user.username, + 'firstname': user.name, + 'lastname': user.lastname, + 'email': user.email, + 'emails': user.emails, + } + if details == 'basic': + return data + + api_key_length = 40 + api_key_replacement = '*' * api_key_length + + extras = { + 'api_key': api_key_replacement, + 'api_keys': [api_key_replacement], + 'active': user.active, + 'admin': user.admin, + 'extern_type': user.extern_type, + 'extern_name': user.extern_name, + 'last_login': user.last_login, + 'ip_addresses': user.ip_addresses, + 'language': user_data.get('language') + } + data.update(extras) + + if include_secrets: + data['api_key'] = user.api_key + data['api_keys'] = user.auth_tokens + return data + + def __json__(self): + data = { + 'full_name': self.full_name, + 'full_name_or_username': self.full_name_or_username, + 'short_contact': self.short_contact, + 'full_contact': self.full_contact, + } + data.update(self.get_api_data()) + return data + + +class UserApiKeys(Base, BaseModel): + __tablename__ = 'user_api_keys' + __table_args__ = ( + Index('uak_api_key_idx', 'api_key'), + Index('uak_api_key_expires_idx', 'api_key', 'expires'), + UniqueConstraint('api_key'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + __mapper_args__ = {} + + # ApiKey role + ROLE_ALL = 'token_role_all' + ROLE_HTTP = 'token_role_http' + ROLE_VCS = 'token_role_vcs' + ROLE_API = 'token_role_api' + ROLE_FEED = 'token_role_feed' + ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED] + + user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + api_key = Column("api_key", String(255), nullable=False, unique=True) + description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql')) + expires = Column('expires', Float(53), nullable=False) + role = Column('role', String(255), nullable=True) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + + user = relationship('User', lazy='joined') + + @classmethod + def _get_role_name(cls, role): + return { + cls.ROLE_ALL: _('all'), + cls.ROLE_HTTP: _('http/web interface'), + cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'), + cls.ROLE_API: _('api calls'), + cls.ROLE_FEED: _('feed access'), + }.get(role, role) + + @property + def expired(self): + if self.expires == -1: + return False + return time.time() > self.expires + + @property + def role_humanized(self): + return self._get_role_name(self.role) + + +class UserEmailMap(Base, BaseModel): + __tablename__ = 'user_email_map' + __table_args__ = ( + Index('uem_email_idx', 'email'), + UniqueConstraint('email'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + __mapper_args__ = {} + + email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + _email = Column("email", String(255), nullable=True, unique=False, default=None) + user = relationship('User', lazy='joined') + + @validates('_email') + def validate_email(self, key, email): + # check if this email is not main one + main_email = Session().query(User).filter(User.email == email).scalar() + if main_email is not None: + raise AttributeError('email %s is present is user table' % email) + return email + + @hybrid_property + def email(self): + return self._email + + @email.setter + def email(self, val): + self._email = val.lower() if val else None + + +class UserIpMap(Base, BaseModel): + __tablename__ = 'user_ip_map' + __table_args__ = ( + UniqueConstraint('user_id', 'ip_addr'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + __mapper_args__ = {} + + ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None) + active = Column("active", Boolean(), nullable=True, unique=None, default=True) + description = Column("description", String(10000), nullable=True, unique=None, default=None) + user = relationship('User', lazy='joined') + + @classmethod + def _get_ip_range(cls, ip_addr): + net = ipaddress.ip_network(ip_addr, strict=False) + return [str(net.network_address), str(net.broadcast_address)] + + def __json__(self): + return { + 'ip_addr': self.ip_addr, + 'ip_range': self._get_ip_range(self.ip_addr), + } + + def __unicode__(self): + return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__, + self.user_id, self.ip_addr) + +class UserLog(Base, BaseModel): + __tablename__ = 'user_logs' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + username = Column("username", String(255), nullable=True, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True) + repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None) + user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None) + action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None) + action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None) + + def __unicode__(self): + return u"<%s('id:%s:%s')>" % (self.__class__.__name__, + self.repository_name, + self.action) + + @property + def action_as_day(self): + return datetime.date(*self.action_date.timetuple()[:3]) + + user = relationship('User') + repository = relationship('Repository', cascade='') + + +class UserGroup(Base, BaseModel): + __tablename__ = 'users_groups' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + + users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None) + user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None) + users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None) + inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data + + members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined") + users_group_to_perm = relationship('UserGroupToPerm', cascade='all') + users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all') + users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all') + user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all') + user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all') + + user = relationship('User') + + @hybrid_property + def group_data(self): + if not self._group_data: + return {} + + try: + return json.loads(self._group_data) + except TypeError: + return {} + + @group_data.setter + def group_data(self, val): + try: + self._group_data = json.dumps(val) + except Exception: + log.error(traceback.format_exc()) + + def __unicode__(self): + return u"<%s('id:%s:%s')>" % (self.__class__.__name__, + self.users_group_id, + self.users_group_name) + + @classmethod + def get_by_group_name(cls, group_name, cache=False, + case_insensitive=False): + if case_insensitive: + q = cls.query().filter(func.lower(cls.users_group_name) == + func.lower(group_name)) + + else: + q = cls.query().filter(cls.users_group_name == group_name) + if cache: + q = q.options(FromCache( + "sql_cache_short", + "get_group_%s" % _hash_key(group_name))) + return q.scalar() + + @classmethod + def get(cls, user_group_id, cache=False): + user_group = cls.query() + if cache: + user_group = user_group.options(FromCache("sql_cache_short", + "get_users_group_%s" % user_group_id)) + return user_group.get(user_group_id) + + def permissions(self, with_admins=True, with_owner=True): + q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self) + q = q.options(joinedload(UserUserGroupToPerm.user_group), + joinedload(UserUserGroupToPerm.user), + joinedload(UserUserGroupToPerm.permission),) + + # get owners and admins and permissions. We do a trick of re-writing + # objects from sqlalchemy to named-tuples due to sqlalchemy session + # has a global reference and changing one object propagates to all + # others. This means if admin is also an owner admin_row that change + # would propagate to both objects + perm_rows = [] + for _usr in q.all(): + usr = AttributeDict(_usr.user.get_dict()) + usr.permission = _usr.permission.permission_name + perm_rows.append(usr) + + # filter the perm rows by 'default' first and then sort them by + # admin,write,read,none permissions sorted again alphabetically in + # each group + perm_rows = sorted(perm_rows, key=display_sort) + + _admin_perm = 'usergroup.admin' + owner_row = [] + if with_owner: + usr = AttributeDict(self.user.get_dict()) + usr.owner_row = True + usr.permission = _admin_perm + owner_row.append(usr) + + super_admin_rows = [] + if with_admins: + for usr in User.get_all_super_admins(): + # if this admin is also owner, don't double the record + if usr.user_id == owner_row[0].user_id: + owner_row[0].admin_row = True + else: + usr = AttributeDict(usr.get_dict()) + usr.admin_row = True + usr.permission = _admin_perm + super_admin_rows.append(usr) + + return super_admin_rows + owner_row + perm_rows + + def permission_user_groups(self): + q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self) + q = q.options(joinedload(UserGroupUserGroupToPerm.user_group), + joinedload(UserGroupUserGroupToPerm.target_user_group), + joinedload(UserGroupUserGroupToPerm.permission),) + + perm_rows = [] + for _user_group in q.all(): + usr = AttributeDict(_user_group.user_group.get_dict()) + usr.permission = _user_group.permission.permission_name + perm_rows.append(usr) + + return perm_rows + + def _get_default_perms(self, user_group, suffix=''): + from rhodecode.model.permission import PermissionModel + return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix) + + def get_default_perms(self, suffix=''): + return self._get_default_perms(self, suffix) + + def get_api_data(self, with_group_members=True, include_secrets=False): + """ + :param include_secrets: See :meth:`User.get_api_data`, this parameter is + basically forwarded. + + """ + user_group = self + + data = { + 'users_group_id': user_group.users_group_id, + 'group_name': user_group.users_group_name, + 'group_description': user_group.user_group_description, + 'active': user_group.users_group_active, + 'owner': user_group.user.username, + } + if with_group_members: + users = [] + for user in user_group.members: + user = user.user + users.append(user.get_api_data(include_secrets=include_secrets)) + data['users'] = users + + return data + + +class UserGroupMember(Base, BaseModel): + __tablename__ = 'users_groups_members' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + + users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + + user = relationship('User', lazy='joined') + users_group = relationship('UserGroup') + + def __init__(self, gr_id='', u_id=''): + self.users_group_id = gr_id + self.user_id = u_id + + +class RepositoryField(Base, BaseModel): + __tablename__ = 'repositories_fields' + __table_args__ = ( + UniqueConstraint('repository_id', 'field_key'), # no-multi field + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields + + repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) + field_key = Column("field_key", String(250)) + field_label = Column("field_label", String(1024), nullable=False) + field_value = Column("field_value", String(10000), nullable=False) + field_desc = Column("field_desc", String(1024), nullable=False) + field_type = Column("field_type", String(255), nullable=False, unique=None) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + + repository = relationship('Repository') + + @property + def field_key_prefixed(self): + return 'ex_%s' % self.field_key + + @classmethod + def un_prefix_key(cls, key): + if key.startswith(cls.PREFIX): + return key[len(cls.PREFIX):] + return key + + @classmethod + def get_by_key_name(cls, key, repo): + row = cls.query()\ + .filter(cls.repository == repo)\ + .filter(cls.field_key == key).scalar() + return row + + +class Repository(Base, BaseModel): + __tablename__ = 'repositories' + __table_args__ = ( + Index('r_repo_name_idx', 'repo_name', mysql_length=255), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}' + DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}' + + STATE_CREATED = 'repo_state_created' + STATE_PENDING = 'repo_state_pending' + STATE_ERROR = 'repo_state_error' + + LOCK_AUTOMATIC = 'lock_auto' + LOCK_API = 'lock_api' + LOCK_WEB = 'lock_web' + LOCK_PULL = 'lock_pull' + + NAME_SEP = URL_SEP + + repo_id = Column( + "repo_id", Integer(), nullable=False, unique=True, default=None, + primary_key=True) + _repo_name = Column( + "repo_name", Text(), nullable=False, default=None) + _repo_name_hash = Column( + "repo_name_hash", String(255), nullable=False, unique=True) + repo_state = Column("repo_state", String(255), nullable=True) + + clone_uri = Column( + "clone_uri", EncryptedTextValue(), nullable=True, unique=False, + default=None) + repo_type = Column( + "repo_type", String(255), nullable=False, unique=False, default=None) + user_id = Column( + "user_id", Integer(), ForeignKey('users.user_id'), nullable=False, + unique=False, default=None) + private = Column( + "private", Boolean(), nullable=True, unique=None, default=None) + enable_statistics = Column( + "statistics", Boolean(), nullable=True, unique=None, default=True) + enable_downloads = Column( + "downloads", Boolean(), nullable=True, unique=None, default=True) + description = Column( + "description", String(10000), nullable=True, unique=None, default=None) + created_on = Column( + 'created_on', DateTime(timezone=False), nullable=True, unique=None, + default=datetime.datetime.now) + updated_on = Column( + 'updated_on', DateTime(timezone=False), nullable=True, unique=None, + default=datetime.datetime.now) + _landing_revision = Column( + "landing_revision", String(255), nullable=False, unique=False, + default=None) + enable_locking = Column( + "enable_locking", Boolean(), nullable=False, unique=None, + default=False) + _locked = Column( + "locked", String(255), nullable=True, unique=False, default=None) + _changeset_cache = Column( + "changeset_cache", LargeBinary(), nullable=True) # JSON data + + fork_id = Column( + "fork_id", Integer(), ForeignKey('repositories.repo_id'), + nullable=True, unique=False, default=None) + group_id = Column( + "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, + unique=False, default=None) + + user = relationship('User', lazy='joined') + fork = relationship('Repository', remote_side=repo_id, lazy='joined') + group = relationship('RepoGroup', lazy='joined') + repo_to_perm = relationship( + 'UserRepoToPerm', cascade='all', + order_by='UserRepoToPerm.repo_to_perm_id') + users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all') + stats = relationship('Statistics', cascade='all', uselist=False) + + followers = relationship( + 'UserFollowing', + primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', + cascade='all') + extra_fields = relationship( + 'RepositoryField', cascade="all, delete, delete-orphan") + logs = relationship('UserLog') + comments = relationship( + 'ChangesetComment', cascade="all, delete, delete-orphan") + pull_requests_source = relationship( + 'PullRequest', + primaryjoin='PullRequest.source_repo_id==Repository.repo_id', + cascade="all, delete, delete-orphan") + pull_requests_target = relationship( + 'PullRequest', + primaryjoin='PullRequest.target_repo_id==Repository.repo_id', + cascade="all, delete, delete-orphan") + ui = relationship('RepoRhodeCodeUi', cascade="all") + settings = relationship('RepoRhodeCodeSetting', cascade="all") + + def __unicode__(self): + return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id, + safe_unicode(self.repo_name)) + + @hybrid_property + def landing_rev(self): + # always should return [rev_type, rev] + if self._landing_revision: + _rev_info = self._landing_revision.split(':') + if len(_rev_info) < 2: + _rev_info.insert(0, 'rev') + return [_rev_info[0], _rev_info[1]] + return [None, None] + + @landing_rev.setter + def landing_rev(self, val): + if ':' not in val: + raise ValueError('value must be delimited with `:` and consist ' + 'of :, got %s instead' % val) + self._landing_revision = val + + @hybrid_property + def locked(self): + if self._locked: + user_id, timelocked, reason = self._locked.split(':') + lock_values = int(user_id), timelocked, reason + else: + lock_values = [None, None, None] + return lock_values + + @locked.setter + def locked(self, val): + if val and isinstance(val, (list, tuple)): + self._locked = ':'.join(map(str, val)) + else: + self._locked = None + + @hybrid_property + def changeset_cache(self): + from rhodecode.lib.vcs.backends.base import EmptyCommit + dummy = EmptyCommit().__json__() + if not self._changeset_cache: + return dummy + try: + return json.loads(self._changeset_cache) + except TypeError: + return dummy + except Exception: + log.error(traceback.format_exc()) + return dummy + + @changeset_cache.setter + def changeset_cache(self, val): + try: + self._changeset_cache = json.dumps(val) + except Exception: + log.error(traceback.format_exc()) + + @hybrid_property + def repo_name(self): + return self._repo_name + + @repo_name.setter + def repo_name(self, value): + self._repo_name = value + self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest() + + @classmethod + def normalize_repo_name(cls, repo_name): + """ + Normalizes os specific repo_name to the format internally stored inside + database using URL_SEP + + :param cls: + :param repo_name: + """ + return cls.NAME_SEP.join(repo_name.split(os.sep)) + + @classmethod + def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False): + session = Session() + q = session.query(cls).filter(cls.repo_name == repo_name) + + if cache: + if identity_cache: + val = cls.identity_cache(session, 'repo_name', repo_name) + if val: + return val + else: + q = q.options( + FromCache("sql_cache_short", + "get_repo_by_name_%s" % _hash_key(repo_name))) + + return q.scalar() + + @classmethod + def get_by_full_path(cls, repo_full_path): + repo_name = repo_full_path.split(cls.base_path(), 1)[-1] + repo_name = cls.normalize_repo_name(repo_name) + return cls.get_by_repo_name(repo_name.strip(URL_SEP)) + + @classmethod + def get_repo_forks(cls, repo_id): + return cls.query().filter(Repository.fork_id == repo_id) + + @classmethod + def base_path(cls): + """ + Returns base path when all repos are stored + + :param cls: + """ + q = Session().query(RhodeCodeUi)\ + .filter(RhodeCodeUi.ui_key == cls.NAME_SEP) + q = q.options(FromCache("sql_cache_short", "repository_repo_path")) + return q.one().ui_value + + @classmethod + def is_valid(cls, repo_name): + """ + returns True if given repo name is a valid filesystem repository + + :param cls: + :param repo_name: + """ + from rhodecode.lib.utils import is_valid_repo + + return is_valid_repo(repo_name, cls.base_path()) + + @classmethod + def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None), + case_insensitive=True): + q = Repository.query() + + if not isinstance(user_id, Optional): + q = q.filter(Repository.user_id == user_id) + + if not isinstance(group_id, Optional): + q = q.filter(Repository.group_id == group_id) + + if case_insensitive: + q = q.order_by(func.lower(Repository.repo_name)) + else: + q = q.order_by(Repository.repo_name) + return q.all() + + @property + def forks(self): + """ + Return forks of this repo + """ + return Repository.get_repo_forks(self.repo_id) + + @property + def parent(self): + """ + Returns fork parent + """ + return self.fork + + @property + def just_name(self): + return self.repo_name.split(self.NAME_SEP)[-1] + + @property + def groups_with_parents(self): + groups = [] + if self.group is None: + return groups + + cur_gr = self.group + groups.insert(0, cur_gr) + while 1: + gr = getattr(cur_gr, 'parent_group', None) + cur_gr = cur_gr.parent_group + if gr is None: + break + groups.insert(0, gr) + + return groups + + @property + def groups_and_repo(self): + return self.groups_with_parents, self + + @LazyProperty + def repo_path(self): + """ + Returns base full path for that repository means where it actually + exists on a filesystem + """ + q = Session().query(RhodeCodeUi).filter( + RhodeCodeUi.ui_key == self.NAME_SEP) + q = q.options(FromCache("sql_cache_short", "repository_repo_path")) + return q.one().ui_value + + @property + def repo_full_path(self): + p = [self.repo_path] + # we need to split the name by / since this is how we store the + # names in the database, but that eventually needs to be converted + # into a valid system path + p += self.repo_name.split(self.NAME_SEP) + return os.path.join(*map(safe_unicode, p)) + + @property + def cache_keys(self): + """ + Returns associated cache keys for that repo + """ + return CacheKey.query()\ + .filter(CacheKey.cache_args == self.repo_name)\ + .order_by(CacheKey.cache_key)\ + .all() + + def get_new_name(self, repo_name): + """ + returns new full repository name based on assigned group and new new + + :param group_name: + """ + path_prefix = self.group.full_path_splitted if self.group else [] + return self.NAME_SEP.join(path_prefix + [repo_name]) + + @property + def _config(self): + """ + Returns db based config object. + """ + from rhodecode.lib.utils import make_db_config + return make_db_config(clear_session=False, repo=self) + + def permissions(self, with_admins=True, with_owner=True): + q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self) + q = q.options(joinedload(UserRepoToPerm.repository), + joinedload(UserRepoToPerm.user), + joinedload(UserRepoToPerm.permission),) + + # get owners and admins and permissions. We do a trick of re-writing + # objects from sqlalchemy to named-tuples due to sqlalchemy session + # has a global reference and changing one object propagates to all + # others. This means if admin is also an owner admin_row that change + # would propagate to both objects + perm_rows = [] + for _usr in q.all(): + usr = AttributeDict(_usr.user.get_dict()) + usr.permission = _usr.permission.permission_name + perm_rows.append(usr) + + # filter the perm rows by 'default' first and then sort them by + # admin,write,read,none permissions sorted again alphabetically in + # each group + perm_rows = sorted(perm_rows, key=display_sort) + + _admin_perm = 'repository.admin' + owner_row = [] + if with_owner: + usr = AttributeDict(self.user.get_dict()) + usr.owner_row = True + usr.permission = _admin_perm + owner_row.append(usr) + + super_admin_rows = [] + if with_admins: + for usr in User.get_all_super_admins(): + # if this admin is also owner, don't double the record + if usr.user_id == owner_row[0].user_id: + owner_row[0].admin_row = True + else: + usr = AttributeDict(usr.get_dict()) + usr.admin_row = True + usr.permission = _admin_perm + super_admin_rows.append(usr) + + return super_admin_rows + owner_row + perm_rows + + def permission_user_groups(self): + q = UserGroupRepoToPerm.query().filter( + UserGroupRepoToPerm.repository == self) + q = q.options(joinedload(UserGroupRepoToPerm.repository), + joinedload(UserGroupRepoToPerm.users_group), + joinedload(UserGroupRepoToPerm.permission),) + + perm_rows = [] + for _user_group in q.all(): + usr = AttributeDict(_user_group.users_group.get_dict()) + usr.permission = _user_group.permission.permission_name + perm_rows.append(usr) + + return perm_rows + + def get_api_data(self, include_secrets=False): + """ + Common function for generating repo api data + + :param include_secrets: See :meth:`User.get_api_data`. + + """ + # TODO: mikhail: Here there is an anti-pattern, we probably need to + # move this methods on models level. + from rhodecode.model.settings import SettingsModel + + repo = self + _user_id, _time, _reason = self.locked + + data = { + 'repo_id': repo.repo_id, + 'repo_name': repo.repo_name, + 'repo_type': repo.repo_type, + 'clone_uri': repo.clone_uri or '', + 'url': url('summary_home', repo_name=self.repo_name, qualified=True), + 'private': repo.private, + 'created_on': repo.created_on, + 'description': repo.description, + 'landing_rev': repo.landing_rev, + 'owner': repo.user.username, + 'fork_of': repo.fork.repo_name if repo.fork else None, + 'enable_statistics': repo.enable_statistics, + 'enable_locking': repo.enable_locking, + 'enable_downloads': repo.enable_downloads, + 'last_changeset': repo.changeset_cache, + 'locked_by': User.get(_user_id).get_api_data( + include_secrets=include_secrets) if _user_id else None, + 'locked_date': time_to_datetime(_time) if _time else None, + 'lock_reason': _reason if _reason else None, + } + + # TODO: mikhail: should be per-repo settings here + rc_config = SettingsModel().get_all_settings() + repository_fields = str2bool( + rc_config.get('rhodecode_repository_fields')) + if repository_fields: + for f in self.extra_fields: + data[f.field_key_prefixed] = f.field_value + + return data + + @classmethod + def lock(cls, repo, user_id, lock_time=None, lock_reason=None): + if not lock_time: + lock_time = time.time() + if not lock_reason: + lock_reason = cls.LOCK_AUTOMATIC + repo.locked = [user_id, lock_time, lock_reason] + Session().add(repo) + Session().commit() + + @classmethod + def unlock(cls, repo): + repo.locked = None + Session().add(repo) + Session().commit() + + @classmethod + def getlock(cls, repo): + return repo.locked + + def is_user_lock(self, user_id): + if self.lock[0]: + lock_user_id = safe_int(self.lock[0]) + user_id = safe_int(user_id) + # both are ints, and they are equal + return all([lock_user_id, user_id]) and lock_user_id == user_id + + return False + + def get_locking_state(self, action, user_id, only_when_enabled=True): + """ + Checks locking on this repository, if locking is enabled and lock is + present returns a tuple of make_lock, locked, locked_by. + make_lock can have 3 states None (do nothing) True, make lock + False release lock, This value is later propagated to hooks, which + do the locking. Think about this as signals passed to hooks what to do. + + """ + # TODO: johbo: This is part of the business logic and should be moved + # into the RepositoryModel. + + if action not in ('push', 'pull'): + raise ValueError("Invalid action value: %s" % repr(action)) + + # defines if locked error should be thrown to user + currently_locked = False + # defines if new lock should be made, tri-state + make_lock = None + repo = self + user = User.get(user_id) + + lock_info = repo.locked + + if repo and (repo.enable_locking or not only_when_enabled): + if action == 'push': + # check if it's already locked !, if it is compare users + locked_by_user_id = lock_info[0] + if user.user_id == locked_by_user_id: + log.debug( + 'Got `push` action from user %s, now unlocking', user) + # unlock if we have push from user who locked + make_lock = False + else: + # we're not the same user who locked, ban with + # code defined in settings (default is 423 HTTP Locked) ! + log.debug('Repo %s is currently locked by %s', repo, user) + currently_locked = True + elif action == 'pull': + # [0] user [1] date + if lock_info[0] and lock_info[1]: + log.debug('Repo %s is currently locked by %s', repo, user) + currently_locked = True + else: + log.debug('Setting lock on repo %s by %s', repo, user) + make_lock = True + + else: + log.debug('Repository %s do not have locking enabled', repo) + + log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s', + make_lock, currently_locked, lock_info) + + from rhodecode.lib.auth import HasRepoPermissionAny + perm_check = HasRepoPermissionAny('repository.write', 'repository.admin') + if make_lock and not perm_check(repo_name=repo.repo_name, user=user): + # if we don't have at least write permission we cannot make a lock + log.debug('lock state reset back to FALSE due to lack ' + 'of at least read permission') + make_lock = False + + return make_lock, currently_locked, lock_info + + @property + def last_db_change(self): + return self.updated_on + + @property + def clone_uri_hidden(self): + clone_uri = self.clone_uri + if clone_uri: + import urlobject + url_obj = urlobject.URLObject(clone_uri) + if url_obj.password: + clone_uri = url_obj.with_password('*****') + return clone_uri + + def clone_url(self, **override): + qualified_home_url = url('home', qualified=True) + + uri_tmpl = None + if 'with_id' in override: + uri_tmpl = self.DEFAULT_CLONE_URI_ID + del override['with_id'] + + if 'uri_tmpl' in override: + uri_tmpl = override['uri_tmpl'] + del override['uri_tmpl'] + + # we didn't override our tmpl from **overrides + if not uri_tmpl: + uri_tmpl = self.DEFAULT_CLONE_URI + try: + from pylons import tmpl_context as c + uri_tmpl = c.clone_uri_tmpl + except Exception: + # in any case if we call this outside of request context, + # ie, not having tmpl_context set up + pass + + return get_clone_url(uri_tmpl=uri_tmpl, + qualifed_home_url=qualified_home_url, + repo_name=self.repo_name, + repo_id=self.repo_id, **override) + + def set_state(self, state): + self.repo_state = state + Session().add(self) + #========================================================================== + # SCM PROPERTIES + #========================================================================== + + def get_commit(self, commit_id=None, commit_idx=None, pre_load=None): + return get_commit_safe( + self.scm_instance(), commit_id, commit_idx, pre_load=pre_load) + + def get_changeset(self, rev=None, pre_load=None): + warnings.warn("Use get_commit", DeprecationWarning) + commit_id = None + commit_idx = None + if isinstance(rev, basestring): + commit_id = rev + else: + commit_idx = rev + return self.get_commit(commit_id=commit_id, commit_idx=commit_idx, + pre_load=pre_load) + + def get_landing_commit(self): + """ + Returns landing commit, or if that doesn't exist returns the tip + """ + _rev_type, _rev = self.landing_rev + commit = self.get_commit(_rev) + if isinstance(commit, EmptyCommit): + return self.get_commit() + return commit + + def update_commit_cache(self, cs_cache=None, config=None): + """ + Update cache of last changeset for repository, keys should be:: + + short_id + raw_id + revision + parents + message + date + author + + :param cs_cache: + """ + from rhodecode.lib.vcs.backends.base import BaseChangeset + if cs_cache is None: + # use no-cache version here + scm_repo = self.scm_instance(cache=False, config=config) + if scm_repo: + cs_cache = scm_repo.get_commit( + pre_load=["author", "date", "message", "parents"]) + else: + cs_cache = EmptyCommit() + + if isinstance(cs_cache, BaseChangeset): + cs_cache = cs_cache.__json__() + + def is_outdated(new_cs_cache): + if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or + new_cs_cache['revision'] != self.changeset_cache['revision']): + return True + return False + + # check if we have maybe already latest cached revision + if is_outdated(cs_cache) or not self.changeset_cache: + _default = datetime.datetime.fromtimestamp(0) + last_change = cs_cache.get('date') or _default + log.debug('updated repo %s with new cs cache %s', + self.repo_name, cs_cache) + self.updated_on = last_change + self.changeset_cache = cs_cache + Session().add(self) + Session().commit() + else: + log.debug('Skipping update_commit_cache for repo:`%s` ' + 'commit already with latest changes', self.repo_name) + + @property + def tip(self): + return self.get_commit('tip') + + @property + def author(self): + return self.tip.author + + @property + def last_change(self): + return self.scm_instance().last_change + + def get_comments(self, revisions=None): + """ + Returns comments for this repository grouped by revisions + + :param revisions: filter query by revisions only + """ + cmts = ChangesetComment.query()\ + .filter(ChangesetComment.repo == self) + if revisions: + cmts = cmts.filter(ChangesetComment.revision.in_(revisions)) + grouped = collections.defaultdict(list) + for cmt in cmts.all(): + grouped[cmt.revision].append(cmt) + return grouped + + def statuses(self, revisions=None): + """ + Returns statuses for this repository + + :param revisions: list of revisions to get statuses for + """ + statuses = ChangesetStatus.query()\ + .filter(ChangesetStatus.repo == self)\ + .filter(ChangesetStatus.version == 0) + + if revisions: + # Try doing the filtering in chunks to avoid hitting limits + size = 500 + status_results = [] + for chunk in xrange(0, len(revisions), size): + status_results += statuses.filter( + ChangesetStatus.revision.in_( + revisions[chunk: chunk+size]) + ).all() + else: + status_results = statuses.all() + + grouped = {} + + # maybe we have open new pullrequest without a status? + stat = ChangesetStatus.STATUS_UNDER_REVIEW + status_lbl = ChangesetStatus.get_status_lbl(stat) + for pr in PullRequest.query().filter(PullRequest.source_repo == self).all(): + for rev in pr.revisions: + pr_id = pr.pull_request_id + pr_repo = pr.target_repo.repo_name + grouped[rev] = [stat, status_lbl, pr_id, pr_repo] + + for stat in status_results: + pr_id = pr_repo = None + if stat.pull_request: + pr_id = stat.pull_request.pull_request_id + pr_repo = stat.pull_request.target_repo.repo_name + grouped[stat.revision] = [str(stat.status), stat.status_lbl, + pr_id, pr_repo] + return grouped + + # ========================================================================== + # SCM CACHE INSTANCE + # ========================================================================== + + def scm_instance(self, **kwargs): + import rhodecode + + # Passing a config will not hit the cache currently only used + # for repo2dbmapper + config = kwargs.pop('config', None) + cache = kwargs.pop('cache', None) + full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache')) + # if cache is NOT defined use default global, else we have a full + # control over cache behaviour + if cache is None and full_cache and not config: + return self._get_instance_cached() + return self._get_instance(cache=bool(cache), config=config) + + def _get_instance_cached(self): + @cache_region('long_term') + def _get_repo(cache_key): + return self._get_instance() + + invalidator_context = CacheKey.repo_context_cache( + _get_repo, self.repo_name, None) + + with invalidator_context as context: + context.invalidate() + repo = context.compute() + + return repo + + def _get_instance(self, cache=True, config=None): + repo_full_path = self.repo_full_path + try: + vcs_alias = get_scm(repo_full_path)[0] + log.debug( + 'Creating instance of %s repository from %s', + vcs_alias, repo_full_path) + backend = get_backend(vcs_alias) + except VCSError: + log.exception( + 'Perhaps this repository is in db and not in ' + 'filesystem run rescan repositories with ' + '"destroy old data" option from admin panel') + return + + config = config or self._config + custom_wire = { + 'cache': cache # controls the vcs.remote cache + } + repo = backend( + safe_str(repo_full_path), config=config, create=False, + with_wire=custom_wire) + + return repo + + def __json__(self): + return {'landing_rev': self.landing_rev} + + def get_dict(self): + + # Since we transformed `repo_name` to a hybrid property, we need to + # keep compatibility with the code which uses `repo_name` field. + + result = super(Repository, self).get_dict() + result['repo_name'] = result.pop('_repo_name', None) + return result + + +class RepoGroup(Base, BaseModel): + __tablename__ = 'groups' + __table_args__ = ( + UniqueConstraint('group_name', 'group_parent_id'), + CheckConstraint('group_id != group_parent_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + __mapper_args__ = {'order_by': 'group_name'} + + CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups + + group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + group_name = Column("group_name", String(255), nullable=False, unique=True, default=None) + group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None) + group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None) + enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + + repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id') + users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all') + parent_group = relationship('RepoGroup', remote_side=group_id) + user = relationship('User') + + def __init__(self, group_name='', parent_group=None): + self.group_name = group_name + self.parent_group = parent_group + + def __unicode__(self): + return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id, + self.group_name) + + @classmethod + def _generate_choice(cls, repo_group): + from webhelpers.html import literal as _literal + _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k)) + return repo_group.group_id, _name(repo_group.full_path_splitted) + + @classmethod + def groups_choices(cls, groups=None, show_empty_group=True): + if not groups: + groups = cls.query().all() + + repo_groups = [] + if show_empty_group: + repo_groups = [('-1', u'-- %s --' % _('No parent'))] + + repo_groups.extend([cls._generate_choice(x) for x in groups]) + + repo_groups = sorted( + repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0]) + return repo_groups + + @classmethod + def url_sep(cls): + return URL_SEP + + @classmethod + def get_by_group_name(cls, group_name, cache=False, case_insensitive=False): + if case_insensitive: + gr = cls.query().filter(func.lower(cls.group_name) + == func.lower(group_name)) + else: + gr = cls.query().filter(cls.group_name == group_name) + if cache: + gr = gr.options(FromCache( + "sql_cache_short", + "get_group_%s" % _hash_key(group_name))) + return gr.scalar() + + @classmethod + def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None), + case_insensitive=True): + q = RepoGroup.query() + + if not isinstance(user_id, Optional): + q = q.filter(RepoGroup.user_id == user_id) + + if not isinstance(group_id, Optional): + q = q.filter(RepoGroup.group_parent_id == group_id) + + if case_insensitive: + q = q.order_by(func.lower(RepoGroup.group_name)) + else: + q = q.order_by(RepoGroup.group_name) + return q.all() + + @property + def parents(self): + parents_recursion_limit = 10 + groups = [] + if self.parent_group is None: + return groups + cur_gr = self.parent_group + groups.insert(0, cur_gr) + cnt = 0 + while 1: + cnt += 1 + gr = getattr(cur_gr, 'parent_group', None) + cur_gr = cur_gr.parent_group + if gr is None: + break + if cnt == parents_recursion_limit: + # this will prevent accidental infinit loops + log.error(('more than %s parents found for group %s, stopping ' + 'recursive parent fetching' % (parents_recursion_limit, self))) + break + + groups.insert(0, gr) + return groups + + @property + def children(self): + return RepoGroup.query().filter(RepoGroup.parent_group == self) + + @property + def name(self): + return self.group_name.split(RepoGroup.url_sep())[-1] + + @property + def full_path(self): + return self.group_name + + @property + def full_path_splitted(self): + return self.group_name.split(RepoGroup.url_sep()) + + @property + def repositories(self): + return Repository.query()\ + .filter(Repository.group == self)\ + .order_by(Repository.repo_name) + + @property + def repositories_recursive_count(self): + cnt = self.repositories.count() + + def children_count(group): + cnt = 0 + for child in group.children: + cnt += child.repositories.count() + cnt += children_count(child) + return cnt + + return cnt + children_count(self) + + def _recursive_objects(self, include_repos=True): + all_ = [] + + def _get_members(root_gr): + if include_repos: + for r in root_gr.repositories: + all_.append(r) + childs = root_gr.children.all() + if childs: + for gr in childs: + all_.append(gr) + _get_members(gr) + + _get_members(self) + return [self] + all_ + + def recursive_groups_and_repos(self): + """ + Recursive return all groups, with repositories in those groups + """ + return self._recursive_objects() + + def recursive_groups(self): + """ + Returns all children groups for this group including children of children + """ + return self._recursive_objects(include_repos=False) + + def get_new_name(self, group_name): + """ + returns new full group name based on parent and new name + + :param group_name: + """ + path_prefix = (self.parent_group.full_path_splitted if + self.parent_group else []) + return RepoGroup.url_sep().join(path_prefix + [group_name]) + + def permissions(self, with_admins=True, with_owner=True): + q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self) + q = q.options(joinedload(UserRepoGroupToPerm.group), + joinedload(UserRepoGroupToPerm.user), + joinedload(UserRepoGroupToPerm.permission),) + + # get owners and admins and permissions. We do a trick of re-writing + # objects from sqlalchemy to named-tuples due to sqlalchemy session + # has a global reference and changing one object propagates to all + # others. This means if admin is also an owner admin_row that change + # would propagate to both objects + perm_rows = [] + for _usr in q.all(): + usr = AttributeDict(_usr.user.get_dict()) + usr.permission = _usr.permission.permission_name + perm_rows.append(usr) + + # filter the perm rows by 'default' first and then sort them by + # admin,write,read,none permissions sorted again alphabetically in + # each group + perm_rows = sorted(perm_rows, key=display_sort) + + _admin_perm = 'group.admin' + owner_row = [] + if with_owner: + usr = AttributeDict(self.user.get_dict()) + usr.owner_row = True + usr.permission = _admin_perm + owner_row.append(usr) + + super_admin_rows = [] + if with_admins: + for usr in User.get_all_super_admins(): + # if this admin is also owner, don't double the record + if usr.user_id == owner_row[0].user_id: + owner_row[0].admin_row = True + else: + usr = AttributeDict(usr.get_dict()) + usr.admin_row = True + usr.permission = _admin_perm + super_admin_rows.append(usr) + + return super_admin_rows + owner_row + perm_rows + + def permission_user_groups(self): + q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self) + q = q.options(joinedload(UserGroupRepoGroupToPerm.group), + joinedload(UserGroupRepoGroupToPerm.users_group), + joinedload(UserGroupRepoGroupToPerm.permission),) + + perm_rows = [] + for _user_group in q.all(): + usr = AttributeDict(_user_group.users_group.get_dict()) + usr.permission = _user_group.permission.permission_name + perm_rows.append(usr) + + return perm_rows + + def get_api_data(self): + """ + Common function for generating api data + + """ + group = self + data = { + 'group_id': group.group_id, + 'group_name': group.group_name, + 'group_description': group.group_description, + 'parent_group': group.parent_group.group_name if group.parent_group else None, + 'repositories': [x.repo_name for x in group.repositories], + 'owner': group.user.username, + } + return data + + +class Permission(Base, BaseModel): + __tablename__ = 'permissions' + __table_args__ = ( + Index('p_perm_name_idx', 'permission_name'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + PERMS = [ + ('hg.admin', _('RhodeCode Super Administrator')), + + ('repository.none', _('Repository no access')), + ('repository.read', _('Repository read access')), + ('repository.write', _('Repository write access')), + ('repository.admin', _('Repository admin access')), + + ('group.none', _('Repository group no access')), + ('group.read', _('Repository group read access')), + ('group.write', _('Repository group write access')), + ('group.admin', _('Repository group admin access')), + + ('usergroup.none', _('User group no access')), + ('usergroup.read', _('User group read access')), + ('usergroup.write', _('User group write access')), + ('usergroup.admin', _('User group admin access')), + + ('hg.repogroup.create.false', _('Repository Group creation disabled')), + ('hg.repogroup.create.true', _('Repository Group creation enabled')), + + ('hg.usergroup.create.false', _('User Group creation disabled')), + ('hg.usergroup.create.true', _('User Group creation enabled')), + + ('hg.create.none', _('Repository creation disabled')), + ('hg.create.repository', _('Repository creation enabled')), + ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')), + ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')), + + ('hg.fork.none', _('Repository forking disabled')), + ('hg.fork.repository', _('Repository forking enabled')), + + ('hg.register.none', _('Registration disabled')), + ('hg.register.manual_activate', _('User Registration with manual account activation')), + ('hg.register.auto_activate', _('User Registration with automatic account activation')), + + ('hg.extern_activate.manual', _('Manual activation of external account')), + ('hg.extern_activate.auto', _('Automatic activation of external account')), + + ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')), + ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')), + ] + + # definition of system default permissions for DEFAULT user + DEFAULT_USER_PERMISSIONS = [ + 'repository.read', + 'group.read', + 'usergroup.read', + 'hg.create.repository', + 'hg.repogroup.create.false', + 'hg.usergroup.create.false', + 'hg.create.write_on_repogroup.true', + 'hg.fork.repository', + 'hg.register.manual_activate', + 'hg.extern_activate.auto', + 'hg.inherit_default_perms.true', + ] + + # defines which permissions are more important higher the more important + # Weight defines which permissions are more important. + # The higher number the more important. + PERM_WEIGHTS = { + 'repository.none': 0, + 'repository.read': 1, + 'repository.write': 3, + 'repository.admin': 4, + + 'group.none': 0, + 'group.read': 1, + 'group.write': 3, + 'group.admin': 4, + + 'usergroup.none': 0, + 'usergroup.read': 1, + 'usergroup.write': 3, + 'usergroup.admin': 4, + + 'hg.repogroup.create.false': 0, + 'hg.repogroup.create.true': 1, + + 'hg.usergroup.create.false': 0, + 'hg.usergroup.create.true': 1, + + 'hg.fork.none': 0, + 'hg.fork.repository': 1, + 'hg.create.none': 0, + 'hg.create.repository': 1 + } + + permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None) + permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None) + + def __unicode__(self): + return u"<%s('%s:%s')>" % ( + self.__class__.__name__, self.permission_id, self.permission_name + ) + + @classmethod + def get_by_key(cls, key): + return cls.query().filter(cls.permission_name == key).scalar() + + @classmethod + def get_default_repo_perms(cls, user_id, repo_id=None): + q = Session().query(UserRepoToPerm, Repository, Permission)\ + .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\ + .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\ + .filter(UserRepoToPerm.user_id == user_id) + if repo_id: + q = q.filter(UserRepoToPerm.repository_id == repo_id) + return q.all() + + @classmethod + def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None): + q = Session().query(UserGroupRepoToPerm, Repository, Permission)\ + .join( + Permission, + UserGroupRepoToPerm.permission_id == Permission.permission_id)\ + .join( + Repository, + UserGroupRepoToPerm.repository_id == Repository.repo_id)\ + .join( + UserGroup, + UserGroupRepoToPerm.users_group_id == + UserGroup.users_group_id)\ + .join( + UserGroupMember, + UserGroupRepoToPerm.users_group_id == + UserGroupMember.users_group_id)\ + .filter( + UserGroupMember.user_id == user_id, + UserGroup.users_group_active == true()) + if repo_id: + q = q.filter(UserGroupRepoToPerm.repository_id == repo_id) + return q.all() + + @classmethod + def get_default_group_perms(cls, user_id, repo_group_id=None): + q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\ + .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\ + .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\ + .filter(UserRepoGroupToPerm.user_id == user_id) + if repo_group_id: + q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id) + return q.all() + + @classmethod + def get_default_group_perms_from_user_group( + cls, user_id, repo_group_id=None): + q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\ + .join( + Permission, + UserGroupRepoGroupToPerm.permission_id == + Permission.permission_id)\ + .join( + RepoGroup, + UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\ + .join( + UserGroup, + UserGroupRepoGroupToPerm.users_group_id == + UserGroup.users_group_id)\ + .join( + UserGroupMember, + UserGroupRepoGroupToPerm.users_group_id == + UserGroupMember.users_group_id)\ + .filter( + UserGroupMember.user_id == user_id, + UserGroup.users_group_active == true()) + if repo_group_id: + q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id) + return q.all() + + @classmethod + def get_default_user_group_perms(cls, user_id, user_group_id=None): + q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\ + .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\ + .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\ + .filter(UserUserGroupToPerm.user_id == user_id) + if user_group_id: + q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id) + return q.all() + + @classmethod + def get_default_user_group_perms_from_user_group( + cls, user_id, user_group_id=None): + TargetUserGroup = aliased(UserGroup, name='target_user_group') + q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\ + .join( + Permission, + UserGroupUserGroupToPerm.permission_id == + Permission.permission_id)\ + .join( + TargetUserGroup, + UserGroupUserGroupToPerm.target_user_group_id == + TargetUserGroup.users_group_id)\ + .join( + UserGroup, + UserGroupUserGroupToPerm.user_group_id == + UserGroup.users_group_id)\ + .join( + UserGroupMember, + UserGroupUserGroupToPerm.user_group_id == + UserGroupMember.users_group_id)\ + .filter( + UserGroupMember.user_id == user_id, + UserGroup.users_group_active == true()) + if user_group_id: + q = q.filter( + UserGroupUserGroupToPerm.user_group_id == user_group_id) + + return q.all() + + +class UserRepoToPerm(Base, BaseModel): + __tablename__ = 'repo_to_perm' + __table_args__ = ( + UniqueConstraint('user_id', 'repository_id', 'permission_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) + + user = relationship('User') + repository = relationship('Repository') + permission = relationship('Permission') + + @classmethod + def create(cls, user, repository, permission): + n = cls() + n.user = user + n.repository = repository + n.permission = permission + Session().add(n) + return n + + def __unicode__(self): + return u'<%s => %s >' % (self.user, self.repository) + + +class UserUserGroupToPerm(Base, BaseModel): + __tablename__ = 'user_user_group_to_perm' + __table_args__ = ( + UniqueConstraint('user_id', 'user_group_id', 'permission_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + + user = relationship('User') + user_group = relationship('UserGroup') + permission = relationship('Permission') + + @classmethod + def create(cls, user, user_group, permission): + n = cls() + n.user = user + n.user_group = user_group + n.permission = permission + Session().add(n) + return n + + def __unicode__(self): + return u'<%s => %s >' % (self.user, self.user_group) + + +class UserToPerm(Base, BaseModel): + __tablename__ = 'user_to_perm' + __table_args__ = ( + UniqueConstraint('user_id', 'permission_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + user = relationship('User') + permission = relationship('Permission', lazy='joined') + + def __unicode__(self): + return u'<%s => %s >' % (self.user, self.permission) + + +class UserGroupRepoToPerm(Base, BaseModel): + __tablename__ = 'users_group_repo_to_perm' + __table_args__ = ( + UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) + + users_group = relationship('UserGroup') + permission = relationship('Permission') + repository = relationship('Repository') + + @classmethod + def create(cls, users_group, repository, permission): + n = cls() + n.users_group = users_group + n.repository = repository + n.permission = permission + Session().add(n) + return n + + def __unicode__(self): + return u' %s >' % (self.users_group, self.repository) + + +class UserGroupUserGroupToPerm(Base, BaseModel): + __tablename__ = 'user_group_user_group_to_perm' + __table_args__ = ( + UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'), + CheckConstraint('target_user_group_id != user_group_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + + target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id') + user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id') + permission = relationship('Permission') + + @classmethod + def create(cls, target_user_group, user_group, permission): + n = cls() + n.target_user_group = target_user_group + n.user_group = user_group + n.permission = permission + Session().add(n) + return n + + def __unicode__(self): + return u' %s >' % (self.target_user_group, self.user_group) + + +class UserGroupToPerm(Base, BaseModel): + __tablename__ = 'users_group_to_perm' + __table_args__ = ( + UniqueConstraint('users_group_id', 'permission_id',), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + users_group = relationship('UserGroup') + permission = relationship('Permission') + + +class UserRepoGroupToPerm(Base, BaseModel): + __tablename__ = 'user_repo_group_to_perm' + __table_args__ = ( + UniqueConstraint('user_id', 'group_id', 'permission_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + user = relationship('User') + group = relationship('RepoGroup') + permission = relationship('Permission') + + @classmethod + def create(cls, user, repository_group, permission): + n = cls() + n.user = user + n.group = repository_group + n.permission = permission + Session().add(n) + return n + + +class UserGroupRepoGroupToPerm(Base, BaseModel): + __tablename__ = 'users_group_repo_group_to_perm' + __table_args__ = ( + UniqueConstraint('users_group_id', 'group_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) + group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None) + permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) + + users_group = relationship('UserGroup') + permission = relationship('Permission') + group = relationship('RepoGroup') + + @classmethod + def create(cls, user_group, repository_group, permission): + n = cls() + n.users_group = user_group + n.group = repository_group + n.permission = permission + Session().add(n) + return n + + def __unicode__(self): + return u' %s >' % (self.users_group, self.group) + + +class Statistics(Base, BaseModel): + __tablename__ = 'statistics' + __table_args__ = ( + UniqueConstraint('repository_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None) + stat_on_revision = Column("stat_on_revision", Integer(), nullable=False) + commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data + commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data + languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data + + repository = relationship('Repository', single_parent=True) + + +class UserFollowing(Base, BaseModel): + __tablename__ = 'user_followings' + __table_args__ = ( + UniqueConstraint('user_id', 'follows_repository_id'), + UniqueConstraint('user_id', 'follows_user_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) + follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None) + follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) + follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) + + user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id') + + follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id') + follows_repository = relationship('Repository', order_by='Repository.repo_name') + + @classmethod + def get_repo_followers(cls, repo_id): + return cls.query().filter(cls.follows_repo_id == repo_id) + + +class CacheKey(Base, BaseModel): + __tablename__ = 'cache_invalidation' + __table_args__ = ( + UniqueConstraint('cache_key'), + Index('key_idx', 'cache_key'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + CACHE_TYPE_ATOM = 'ATOM' + CACHE_TYPE_RSS = 'RSS' + CACHE_TYPE_README = 'README' + + cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) + cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None) + cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None) + cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False) + + def __init__(self, cache_key, cache_args=''): + self.cache_key = cache_key + self.cache_args = cache_args + self.cache_active = False + + def __unicode__(self): + return u"<%s('%s:%s[%s]')>" % ( + self.__class__.__name__, + self.cache_id, self.cache_key, self.cache_active) + + def _cache_key_partition(self): + prefix, repo_name, suffix = self.cache_key.partition(self.cache_args) + return prefix, repo_name, suffix + + def get_prefix(self): + """ + Try to extract prefix from existing cache key. The key could consist + of prefix, repo_name, suffix + """ + # this returns prefix, repo_name, suffix + return self._cache_key_partition()[0] + + def get_suffix(self): + """ + get suffix that might have been used in _get_cache_key to + generate self.cache_key. Only used for informational purposes + in repo_edit.html. + """ + # prefix, repo_name, suffix + return self._cache_key_partition()[2] + + @classmethod + def delete_all_cache(cls): + """ + Delete all cache keys from database. + Should only be run when all instances are down and all entries + thus stale. + """ + cls.query().delete() + Session().commit() + + @classmethod + def get_cache_key(cls, repo_name, cache_type): + """ + + Generate a cache key for this process of RhodeCode instance. + Prefix most likely will be process id or maybe explicitly set + instance_id from .ini file. + """ + import rhodecode + prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '') + + repo_as_unicode = safe_unicode(repo_name) + key = u'{}_{}'.format(repo_as_unicode, cache_type) \ + if cache_type else repo_as_unicode + + return u'{}{}'.format(prefix, key) + + @classmethod + def set_invalidate(cls, repo_name, delete=False): + """ + Mark all caches of a repo as invalid in the database. + """ + + try: + qry = Session().query(cls).filter(cls.cache_args == repo_name) + if delete: + log.debug('cache objects deleted for repo %s', + safe_str(repo_name)) + qry.delete() + else: + log.debug('cache objects marked as invalid for repo %s', + safe_str(repo_name)) + qry.update({"cache_active": False}) + + Session().commit() + except Exception: + log.exception( + 'Cache key invalidation failed for repository %s', + safe_str(repo_name)) + Session().rollback() + + @classmethod + def get_active_cache(cls, cache_key): + inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar() + if inv_obj: + return inv_obj + return None + + @classmethod + def repo_context_cache(cls, compute_func, repo_name, cache_type): + """ + @cache_region('long_term') + def _heavy_calculation(cache_key): + return 'result' + + cache_context = CacheKey.repo_context_cache( + _heavy_calculation, repo_name, cache_type) + + with cache_context as context: + context.invalidate() + computed = context.compute() + + assert computed == 'result' + """ + from rhodecode.lib import caches + return caches.InvalidationContext(compute_func, repo_name, cache_type) + + +class ChangesetComment(Base, BaseModel): + __tablename__ = 'changeset_comments' + __table_args__ = ( + Index('cc_revision_idx', 'revision'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + + COMMENT_OUTDATED = u'comment_outdated' + + comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True) + repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False) + revision = Column('revision', String(40), nullable=True) + pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True) + pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True) + line_no = Column('line_no', Unicode(10), nullable=True) + hl_lines = Column('hl_lines', Unicode(512), nullable=True) + f_path = Column('f_path', Unicode(1000), nullable=True) + user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False) + text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + renderer = Column('renderer', Unicode(64), nullable=True) + display_state = Column('display_state', Unicode(128), nullable=True) + + author = relationship('User', lazy='joined') + repo = relationship('Repository') + status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan") + pull_request = relationship('PullRequest', lazy='joined') + pull_request_version = relationship('PullRequestVersion') + + @classmethod + def get_users(cls, revision=None, pull_request_id=None): + """ + Returns user associated with this ChangesetComment. ie those + who actually commented + + :param cls: + :param revision: + """ + q = Session().query(User)\ + .join(ChangesetComment.author) + if revision: + q = q.filter(cls.revision == revision) + elif pull_request_id: + q = q.filter(cls.pull_request_id == pull_request_id) + return q.all() + + def render(self, mentions=False): + from rhodecode.lib import helpers as h + return h.render(self.text, renderer=self.renderer, mentions=mentions) + + def __repr__(self): + if self.comment_id: + return '' % self.comment_id + else: + return '' % id(self) + + +class ChangesetStatus(Base, BaseModel): + __tablename__ = 'changeset_statuses' + __table_args__ = ( + Index('cs_revision_idx', 'revision'), + Index('cs_version_idx', 'version'), + UniqueConstraint('repo_id', 'revision', 'version'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed' + STATUS_APPROVED = 'approved' + STATUS_REJECTED = 'rejected' + STATUS_UNDER_REVIEW = 'under_review' + + STATUSES = [ + (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default + (STATUS_APPROVED, _("Approved")), + (STATUS_REJECTED, _("Rejected")), + (STATUS_UNDER_REVIEW, _("Under Review")), + ] + + changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True) + repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None) + revision = Column('revision', String(40), nullable=False) + status = Column('status', String(128), nullable=False, default=DEFAULT) + changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id')) + modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now) + version = Column('version', Integer(), nullable=False, default=0) + pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True) + + author = relationship('User', lazy='joined') + repo = relationship('Repository') + comment = relationship('ChangesetComment', lazy='joined') + pull_request = relationship('PullRequest', lazy='joined') + + def __unicode__(self): + return u"<%s('%s[%s]:%s')>" % ( + self.__class__.__name__, + self.status, self.version, self.author + ) + + @classmethod + def get_status_lbl(cls, value): + return dict(cls.STATUSES).get(value) + + @property + def status_lbl(self): + return ChangesetStatus.get_status_lbl(self.status) + + +class _PullRequestBase(BaseModel): + """ + Common attributes of pull request and version entries. + """ + + # .status values + STATUS_NEW = u'new' + STATUS_OPEN = u'open' + STATUS_CLOSED = u'closed' + + title = Column('title', Unicode(255), nullable=True) + description = Column( + 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), + nullable=True) + # new/open/closed status of pull request (not approve/reject/etc) + status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW) + created_on = Column( + 'created_on', DateTime(timezone=False), nullable=False, + default=datetime.datetime.now) + updated_on = Column( + 'updated_on', DateTime(timezone=False), nullable=False, + default=datetime.datetime.now) + + @declared_attr + def user_id(cls): + return Column( + "user_id", Integer(), ForeignKey('users.user_id'), nullable=False, + unique=None) + + # 500 revisions max + _revisions = Column( + 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql')) + + @declared_attr + def source_repo_id(cls): + # TODO: dan: rename column to source_repo_id + return Column( + 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'), + nullable=False) + + source_ref = Column('org_ref', Unicode(255), nullable=False) + + @declared_attr + def target_repo_id(cls): + # TODO: dan: rename column to target_repo_id + return Column( + 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'), + nullable=False) + + target_ref = Column('other_ref', Unicode(255), nullable=False) + + # TODO: dan: rename column to last_merge_source_rev + _last_merge_source_rev = Column( + 'last_merge_org_rev', String(40), nullable=True) + # TODO: dan: rename column to last_merge_target_rev + _last_merge_target_rev = Column( + 'last_merge_other_rev', String(40), nullable=True) + _last_merge_status = Column('merge_status', Integer(), nullable=True) + merge_rev = Column('merge_rev', String(40), nullable=True) + + @hybrid_property + def revisions(self): + return self._revisions.split(':') if self._revisions else [] + + @revisions.setter + def revisions(self, val): + self._revisions = ':'.join(val) + + @declared_attr + def author(cls): + return relationship('User', lazy='joined') + + @declared_attr + def source_repo(cls): + return relationship( + 'Repository', + primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__) + + @property + def source_ref_parts(self): + refs = self.source_ref.split(':') + return Reference(refs[0], refs[1], refs[2]) + + @declared_attr + def target_repo(cls): + return relationship( + 'Repository', + primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__) + + @property + def target_ref_parts(self): + refs = self.target_ref.split(':') + return Reference(refs[0], refs[1], refs[2]) + + +class PullRequest(Base, _PullRequestBase): + __tablename__ = 'pull_requests' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + + pull_request_id = Column( + 'pull_request_id', Integer(), nullable=False, primary_key=True) + + def __repr__(self): + if self.pull_request_id: + return '' % self.pull_request_id + else: + return '' % id(self) + + reviewers = relationship('PullRequestReviewers', + cascade="all, delete, delete-orphan") + statuses = relationship('ChangesetStatus') + comments = relationship('ChangesetComment', + cascade="all, delete, delete-orphan") + versions = relationship('PullRequestVersion', + cascade="all, delete, delete-orphan") + + def is_closed(self): + return self.status == self.STATUS_CLOSED + + def get_api_data(self): + from rhodecode.model.pull_request import PullRequestModel + pull_request = self + merge_status = PullRequestModel().merge_status(pull_request) + data = { + 'pull_request_id': pull_request.pull_request_id, + 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name, + pull_request_id=self.pull_request_id, + qualified=True), + 'title': pull_request.title, + 'description': pull_request.description, + 'status': pull_request.status, + 'created_on': pull_request.created_on, + 'updated_on': pull_request.updated_on, + 'commit_ids': pull_request.revisions, + 'review_status': pull_request.calculated_review_status(), + 'mergeable': { + 'status': merge_status[0], + 'message': unicode(merge_status[1]), + }, + 'source': { + 'clone_url': pull_request.source_repo.clone_url(), + 'repository': pull_request.source_repo.repo_name, + 'reference': { + 'name': pull_request.source_ref_parts.name, + 'type': pull_request.source_ref_parts.type, + 'commit_id': pull_request.source_ref_parts.commit_id, + }, + }, + 'target': { + 'clone_url': pull_request.target_repo.clone_url(), + 'repository': pull_request.target_repo.repo_name, + 'reference': { + 'name': pull_request.target_ref_parts.name, + 'type': pull_request.target_ref_parts.type, + 'commit_id': pull_request.target_ref_parts.commit_id, + }, + }, + 'author': pull_request.author.get_api_data(include_secrets=False, + details='basic'), + 'reviewers': [ + { + 'user': reviewer.get_api_data(include_secrets=False, + details='basic'), + 'review_status': st[0][1].status if st else 'not_reviewed', + } + for reviewer, st in pull_request.reviewers_statuses() + ] + } + + return data + + def __json__(self): + return { + 'revisions': self.revisions, + } + + def calculated_review_status(self): + # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html + # because it's tricky on how to use ChangesetStatusModel from there + warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning) + from rhodecode.model.changeset_status import ChangesetStatusModel + return ChangesetStatusModel().calculated_review_status(self) + + def reviewers_statuses(self): + warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning) + from rhodecode.model.changeset_status import ChangesetStatusModel + return ChangesetStatusModel().reviewers_statuses(self) + + +class PullRequestVersion(Base, _PullRequestBase): + __tablename__ = 'pull_request_versions' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + + pull_request_version_id = Column( + 'pull_request_version_id', Integer(), nullable=False, primary_key=True) + pull_request_id = Column( + 'pull_request_id', Integer(), + ForeignKey('pull_requests.pull_request_id'), nullable=False) + pull_request = relationship('PullRequest') + + def __repr__(self): + if self.pull_request_version_id: + return '' % self.pull_request_version_id + else: + return '' % id(self) + + +class PullRequestReviewers(Base, BaseModel): + __tablename__ = 'pull_request_reviewers' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + + def __init__(self, user=None, pull_request=None): + self.user = user + self.pull_request = pull_request + + pull_requests_reviewers_id = Column( + 'pull_requests_reviewers_id', Integer(), nullable=False, + primary_key=True) + pull_request_id = Column( + "pull_request_id", Integer(), + ForeignKey('pull_requests.pull_request_id'), nullable=False) + user_id = Column( + "user_id", Integer(), ForeignKey('users.user_id'), nullable=True) + + user = relationship('User') + pull_request = relationship('PullRequest') + + +class Notification(Base, BaseModel): + __tablename__ = 'notifications' + __table_args__ = ( + Index('notification_type_idx', 'type'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + + TYPE_CHANGESET_COMMENT = u'cs_comment' + TYPE_MESSAGE = u'message' + TYPE_MENTION = u'mention' + TYPE_REGISTRATION = u'registration' + TYPE_PULL_REQUEST = u'pull_request' + TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment' + + notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True) + subject = Column('subject', Unicode(512), nullable=True) + body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True) + created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + type_ = Column('type', Unicode(255)) + + created_by_user = relationship('User') + notifications_to_users = relationship('UserNotification', lazy='joined', + cascade="all, delete, delete-orphan") + + @property + def recipients(self): + return [x.user for x in UserNotification.query()\ + .filter(UserNotification.notification == self)\ + .order_by(UserNotification.user_id.asc()).all()] + + @classmethod + def create(cls, created_by, subject, body, recipients, type_=None): + if type_ is None: + type_ = Notification.TYPE_MESSAGE + + notification = cls() + notification.created_by_user = created_by + notification.subject = subject + notification.body = body + notification.type_ = type_ + notification.created_on = datetime.datetime.now() + + for u in recipients: + assoc = UserNotification() + assoc.notification = notification + + # if created_by is inside recipients mark his notification + # as read + if u.user_id == created_by.user_id: + assoc.read = True + + u.notifications.append(assoc) + Session().add(notification) + + return notification + + @property + def description(self): + from rhodecode.model.notification import NotificationModel + return NotificationModel().make_description(self) + + +class UserNotification(Base, BaseModel): + __tablename__ = 'user_to_notification' + __table_args__ = ( + UniqueConstraint('user_id', 'notification_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True) + notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True) + read = Column('read', Boolean, default=False) + sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None) + + user = relationship('User', lazy="joined") + notification = relationship('Notification', lazy="joined", + order_by=lambda: Notification.created_on.desc(),) + + def mark_as_read(self): + self.read = True + Session().add(self) + + +class Gist(Base, BaseModel): + __tablename__ = 'gists' + __table_args__ = ( + Index('g_gist_access_id_idx', 'gist_access_id'), + Index('g_created_on_idx', 'created_on'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + GIST_PUBLIC = u'public' + GIST_PRIVATE = u'private' + DEFAULT_FILENAME = u'gistfile1.txt' + + ACL_LEVEL_PUBLIC = u'acl_public' + ACL_LEVEL_PRIVATE = u'acl_private' + + gist_id = Column('gist_id', Integer(), primary_key=True) + gist_access_id = Column('gist_access_id', Unicode(250)) + gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql')) + gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True) + gist_expires = Column('gist_expires', Float(53), nullable=False) + gist_type = Column('gist_type', Unicode(128), nullable=False) + created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) + acl_level = Column('acl_level', Unicode(128), nullable=True) + + owner = relationship('User') + + def __repr__(self): + return '' % (self.gist_type, self.gist_access_id) + + @classmethod + def get_or_404(cls, id_): + res = cls.query().filter(cls.gist_access_id == id_).scalar() + if not res: + raise HTTPNotFound + return res + + @classmethod + def get_by_access_id(cls, gist_access_id): + return cls.query().filter(cls.gist_access_id == gist_access_id).scalar() + + def gist_url(self): + import rhodecode + alias_url = rhodecode.CONFIG.get('gist_alias_url') + if alias_url: + return alias_url.replace('{gistid}', self.gist_access_id) + + return url('gist', gist_id=self.gist_access_id, qualified=True) + + @classmethod + def base_path(cls): + """ + Returns base path when all gists are stored + + :param cls: + """ + from rhodecode.model.gist import GIST_STORE_LOC + q = Session().query(RhodeCodeUi)\ + .filter(RhodeCodeUi.ui_key == URL_SEP) + q = q.options(FromCache("sql_cache_short", "repository_repo_path")) + return os.path.join(q.one().ui_value, GIST_STORE_LOC) + + def get_api_data(self): + """ + Common function for generating gist related data for API + """ + gist = self + data = { + 'gist_id': gist.gist_id, + 'type': gist.gist_type, + 'access_id': gist.gist_access_id, + 'description': gist.gist_description, + 'url': gist.gist_url(), + 'expires': gist.gist_expires, + 'created_on': gist.created_on, + 'modified_at': gist.modified_at, + 'content': None, + 'acl_level': gist.acl_level, + } + return data + + def __json__(self): + data = dict( + ) + data.update(self.get_api_data()) + return data + # SCM functions + + def scm_instance(self, **kwargs): + from rhodecode.lib.vcs import get_repo + base_path = self.base_path() + return get_repo(os.path.join(*map(safe_str, + [base_path, self.gist_access_id]))) + + +class DbMigrateVersion(Base, BaseModel): + __tablename__ = 'db_migrate_version' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, + ) + repository_id = Column('repository_id', String(250), primary_key=True) + repository_path = Column('repository_path', Text) + version = Column('version', Integer) + + +class ExternalIdentity(Base, BaseModel): + __tablename__ = 'external_identities' + __table_args__ = ( + Index('local_user_id_idx', 'local_user_id'), + Index('external_id_idx', 'external_id'), + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8'}) + + external_id = Column('external_id', Unicode(255), default=u'', + primary_key=True) + external_username = Column('external_username', Unicode(1024), default=u'') + local_user_id = Column('local_user_id', Integer(), + ForeignKey('users.user_id'), primary_key=True) + provider_name = Column('provider_name', Unicode(255), default=u'', + primary_key=True) + access_token = Column('access_token', String(1024), default=u'') + alt_token = Column('alt_token', String(1024), default=u'') + token_secret = Column('token_secret', String(1024), default=u'') + + @classmethod + def by_external_id_and_provider(cls, external_id, provider_name, + local_user_id=None): + """ + Returns ExternalIdentity instance based on search params + + :param external_id: + :param provider_name: + :return: ExternalIdentity + """ + query = cls.query() + query = query.filter(cls.external_id == external_id) + query = query.filter(cls.provider_name == provider_name) + if local_user_id: + query = query.filter(cls.local_user_id == local_user_id) + return query.first() + + @classmethod + def user_by_external_id_and_provider(cls, external_id, provider_name): + """ + Returns User instance based on search params + + :param external_id: + :param provider_name: + :return: User + """ + query = User.query() + query = query.filter(cls.external_id == external_id) + query = query.filter(cls.provider_name == provider_name) + query = query.filter(User.user_id == cls.local_user_id) + return query.first() + + @classmethod + def by_local_user_id(cls, local_user_id): + """ + Returns all tokens for user + + :param local_user_id: + :return: ExternalIdentity + """ + query = cls.query() + query = query.filter(cls.local_user_id == local_user_id) + return query + + +class Integration(Base, BaseModel): + __tablename__ = 'integrations' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + integration_id = Column('integration_id', Integer(), primary_key=True) + integration_type = Column('integration_type', String(255)) + enabled = Column("enabled", Boolean(), nullable=False) + name = Column('name', String(255), nullable=False) + settings_json = Column('settings_json', + UnicodeText().with_variant(UnicodeText(16384), 'mysql')) + repo_id = Column( + "repo_id", Integer(), ForeignKey('repositories.repo_id'), + nullable=True, unique=None, default=None) + repo = relationship('Repository', lazy='joined') + + @hybrid_property + def settings(self): + data = json.loads(self.settings_json or '{}') + return data + + @settings.setter + def settings(self, dct): + self.settings_json = json.dumps(dct, indent=2) + + def __repr__(self): + if self.repo: + scope = 'repo=%r' % self.repo + else: + scope = 'global' + + return '' % (self.integration_type, scope) + + def settings_as_dict(self): + return json.loads(self.settings_json) diff --git a/rhodecode/lib/dbmigrate/versions/055_version_4_3_0.py b/rhodecode/lib/dbmigrate/versions/055_version_4_3_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/055_version_4_3_0.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +import logging +import sqlalchemy as sa + +from alembic.migration import MigrationContext +from alembic.operations import Operations + +from rhodecode.lib.dbmigrate.versions import _reset_base + +log = logging.getLogger(__name__) + + +def upgrade(migrate_engine): + """ + Upgrade operations go here. + Don't create your own engine; bind migrate_engine to your metadata + """ + _reset_base(migrate_engine) + from rhodecode.lib.dbmigrate.schema import db_4_3_0_0 + + integrations_table = db_4_3_0_0.Integration.__table__ + integrations_table.create() + + +def downgrade(migrate_engine): + pass diff --git a/rhodecode/lib/diffs.py b/rhodecode/lib/diffs.py --- a/rhodecode/lib/diffs.py +++ b/rhodecode/lib/diffs.py @@ -49,11 +49,11 @@ class OPS(object): def wrap_to_table(str_): return ''' - + -
%s
''' % str_ + ''' % (_('Click to comment'), str_) def wrapped_diff(filenode_old, filenode_new, diff_limit=None, file_limit=None, @@ -626,7 +626,9 @@ class DiffProcessor(object): """ if condition: - return '''%(label)s''' % { + return '''%(label)s''' % { + 'title': _('Click to select line'), 'url': url, 'label': label } diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -44,7 +44,7 @@ from pygments.formatters.html import Htm from pygments import highlight as code_highlight from pygments.lexers import ( get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype) -from pylons import url +from pylons import url as pylons_url from pylons.i18n.translation import _, ungettext from pyramid.threadlocal import get_current_request @@ -69,6 +69,7 @@ from webhelpers2.number import format_by from rhodecode.lib.annotate import annotate_highlight from rhodecode.lib.action_parser import action_parser +from rhodecode.lib.ext_json import json from rhodecode.lib.utils import repo_name_slug, get_custom_lexer from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \ get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \ @@ -84,10 +85,46 @@ from rhodecode.model.settings import Iss log = logging.getLogger(__name__) + DEFAULT_USER = User.DEFAULT_USER DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL +def url(*args, **kw): + return pylons_url(*args, **kw) + + +def pylons_url_current(*args, **kw): + """ + This function overrides pylons.url.current() which returns the current + path so that it will also work from a pyramid only context. This + should be removed once port to pyramid is complete. + """ + if not args and not kw: + request = get_current_request() + return request.path + return pylons_url.current(*args, **kw) + +url.current = pylons_url_current + + +def asset(path, ver=None): + """ + Helper to generate a static asset file path for rhodecode assets + + eg. h.asset('images/image.png', ver='3923') + + :param path: path of asset + :param ver: optional version query param to append as ?ver= + """ + request = get_current_request() + query = {} + if ver: + query = {'ver': ver} + return request.static_path( + 'rhodecode:public/{}'.format(path), _query=query) + + def html_escape(text, html_escape_table=None): """Produce entities within text.""" if not html_escape_table: @@ -771,19 +808,17 @@ def discover_user(author): def email_or_none(author): # extract email from the commit string _email = author_email(author) + + # If we have an email, use it, otherwise + # see if it contains a username we can get an email from if _email != '': - # check it against RhodeCode database, and use the MAIN email for this - # user - user = User.get_by_email(_email, case_insensitive=True, cache=True) - if user is not None: - return user.email return _email + else: + user = User.get_by_username( + author_name(author), case_insensitive=True, cache=True) - # See if it contains a username we can get an email from - user = User.get_by_username(author_name(author), case_insensitive=True, - cache=True) if user is not None: - return user.email + return user.email # No valid email, not a valid user in the system, none! return None @@ -819,6 +854,20 @@ def person(author, show_attr="username_a return _author or _email +def author_string(email): + if email: + user = User.get_by_email(email, case_insensitive=True, cache=True) + if user: + if user.firstname or user.lastname: + return '%s %s <%s>' % (user.firstname, user.lastname, email) + else: + return email + else: + return email + else: + return None + + def person_by_id(id_, show_attr="username_and_name"): # attr to return from fetched user person_getter = lambda usr: getattr(usr, show_attr) @@ -905,7 +954,8 @@ def bool2icon(value): #============================================================================== from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \ HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \ -HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token +HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \ +csrf_token_key #============================================================================== @@ -1601,7 +1651,7 @@ def urlify_commits(text_, repository): 'pref': pref, 'cls': 'revision-link', 'url': url('changeset_home', repo_name=repository, - revision=commit_id), + revision=commit_id, qualified=True), 'commit_id': commit_id, 'suf': suf } @@ -1611,7 +1661,8 @@ def urlify_commits(text_, repository): return newtext -def _process_url_func(match_obj, repo_name, uid, entry): +def _process_url_func(match_obj, repo_name, uid, entry, + return_raw_data=False): pref = '' if match_obj.group().startswith(' '): pref = ' ' @@ -1637,7 +1688,7 @@ def _process_url_func(match_obj, repo_na named_vars.update(match_obj.groupdict()) _url = string.Template(entry['url']).safe_substitute(**named_vars) - return tmpl % { + data = { 'pref': pref, 'cls': 'issue-tracker-link', 'url': _url, @@ -1645,9 +1696,15 @@ def _process_url_func(match_obj, repo_na 'issue-prefix': entry['pref'], 'serv': entry['url'], } + if return_raw_data: + return { + 'id': issue_id, + 'url': _url + } + return tmpl % data -def process_patterns(text_string, repo_name, config): +def process_patterns(text_string, repo_name, config=None): repo = None if repo_name: # Retrieving repo_name to avoid invalid repo_name to explode on @@ -1657,11 +1714,9 @@ def process_patterns(text_string, repo_n settings_model = IssueTrackerSettingsModel(repo=repo) active_entries = settings_model.get_settings(cache=True) + issues_data = [] newtext = text_string for uid, entry in active_entries.items(): - url_func = partial( - _process_url_func, repo_name=repo_name, entry=entry, uid=uid) - log.debug('found issue tracker entry with uid %s' % (uid,)) if not (entry['pat'] and entry['url']): @@ -1679,10 +1734,20 @@ def process_patterns(text_string, repo_n entry['pat']) continue + data_func = partial( + _process_url_func, repo_name=repo_name, entry=entry, uid=uid, + return_raw_data=True) + + for match_obj in pattern.finditer(text_string): + issues_data.append(data_func(match_obj)) + + url_func = partial( + _process_url_func, repo_name=repo_name, entry=entry, uid=uid) + newtext = pattern.sub(url_func, newtext) log.debug('processed prefix:uid `%s`' % (uid,)) - return newtext + return newtext, issues_data def urlify_commit_message(commit_text, repository=None): @@ -1694,22 +1759,22 @@ def urlify_commit_message(commit_text, r :param repository: """ from pylons import url # doh, we need to re-import url to mock it later - from rhodecode import CONFIG def escaper(string): return string.replace('<', '<').replace('>', '>') newtext = escaper(commit_text) + + # extract http/https links and make them real urls + newtext = urlify_text(newtext, safe=False) + # urlify commits - extract commit ids and make link out of them, if we have # the scope of repository present. if repository: newtext = urlify_commits(newtext, repository) - # extract http/https links and make them real urls - newtext = urlify_text(newtext, safe=False) - # process issue tracker patterns - newtext = process_patterns(newtext, repository or '', CONFIG) + newtext, issues = process_patterns(newtext, repository or '') return literal(newtext) @@ -1721,21 +1786,11 @@ def rst(source, mentions=False): def markdown(source, mentions=False): return literal('
%s
' % - MarkupRenderer.markdown(source, flavored=False, + MarkupRenderer.markdown(source, flavored=True, mentions=mentions)) def renderer_from_filename(filename, exclude=None): - from rhodecode.config.conf import MARKDOWN_EXTS, RST_EXTS - - def _filter(elements): - if isinstance(exclude, (list, tuple)): - return [x for x in elements if x not in exclude] - return elements - - if filename.endswith(tuple(_filter([x[0] for x in MARKDOWN_EXTS if x[0]]))): - return 'markdown' - if filename.endswith(tuple(_filter([x[0] for x in RST_EXTS if x[0]]))): - return 'rst' + return MarkupRenderer.renderer_from_filename(filename, exclude=exclude) def render(source, renderer='rst', mentions=False): @@ -1827,11 +1882,15 @@ def secure_form(url, method="POST", mult """ from webhelpers.pylonslib.secure_form import insecure_form - from rhodecode.lib.auth import get_csrf_token, csrf_token_key form = insecure_form(url, method, multipart, **attrs) - token = HTML.div(hidden(csrf_token_key, get_csrf_token()), style="display: none;") + token = csrf_input() return literal("%s\n%s" % (form, token)) +def csrf_input(): + return literal( + ''.format( + csrf_token_key, csrf_token_key, get_csrf_token())) + def dropdownmenu(name, selected, options, enable_filter=False, **attrs): select_html = select(name, selected, options, **attrs) select2 = """ @@ -1887,6 +1946,16 @@ def route_path(*args, **kwds): return req.route_path(*args, **kwds) +def static_url(*args, **kwds): + """ + Wrapper around pyramids `route_path` function. It is used to generate + URLs from within pylons views or templates. This will be removed when + pyramid migration if finished. + """ + req = get_current_request() + return req.static_url(*args, **kwds) + + def resource_path(*args, **kwds): """ Wrapper around pyramids `route_path` function. It is used to generate diff --git a/rhodecode/lib/hooks_base.py b/rhodecode/lib/hooks_base.py --- a/rhodecode/lib/hooks_base.py +++ b/rhodecode/lib/hooks_base.py @@ -27,6 +27,7 @@ import os import collections import rhodecode +from rhodecode import events from rhodecode.lib import helpers as h from rhodecode.lib.utils import action_logger from rhodecode.lib.utils2 import safe_str @@ -102,6 +103,9 @@ def pre_push(extras): # Calling hooks after checking the lock, for consistent behavior pre_push_extension(repo_store_path=Repository.base_path(), **extras) + events.trigger(events.RepoPrePushEvent(repo_name=extras.repository, + extras=extras)) + return HookResponse(0, output) @@ -128,6 +132,8 @@ def pre_pull(extras): # Calling hooks after checking the lock, for consistent behavior pre_pull_extension(**extras) + events.trigger(events.RepoPrePullEvent(repo_name=extras.repository, + extras=extras)) return HookResponse(0, output) @@ -138,6 +144,8 @@ def post_pull(extras): action = 'pull' action_logger(user, action, extras.repository, extras.ip, commit=True) + events.trigger(events.RepoPullEvent(repo_name=extras.repository, + extras=extras)) # extension hook call post_pull_extension(**extras) @@ -171,6 +179,10 @@ def post_push(extras): action_logger( extras.username, action, extras.repository, extras.ip, commit=True) + events.trigger(events.RepoPushEvent(repo_name=extras.repository, + pushed_commit_ids=commit_ids, + extras=extras)) + # extension hook call post_push_extension( repo_store_path=Repository.base_path(), diff --git a/rhodecode/lib/hooks_daemon.py b/rhodecode/lib/hooks_daemon.py --- a/rhodecode/lib/hooks_daemon.py +++ b/rhodecode/lib/hooks_daemon.py @@ -20,14 +20,19 @@ import json import logging +import urlparse import threading from BaseHTTPServer import BaseHTTPRequestHandler from SocketServer import TCPServer +from routes.util import URLGenerator import Pyro4 +import pylons +import rhodecode from rhodecode.lib import hooks_base -from rhodecode.lib.utils2 import AttributeDict +from rhodecode.lib.utils2 import ( + AttributeDict, safe_str, get_routes_generator_for_server_url) log = logging.getLogger(__name__) @@ -194,10 +199,14 @@ def prepare_callback_daemon(extras, prot callback_daemon = DummyHooksCallbackDaemon() extras['hooks_module'] = callback_daemon.hooks_module else: - callback_daemon = ( - Pyro4HooksCallbackDaemon() - if protocol == 'pyro4' - else HttpHooksCallbackDaemon()) + if protocol == 'pyro4': + callback_daemon = Pyro4HooksCallbackDaemon() + elif protocol == 'http': + callback_daemon = HttpHooksCallbackDaemon() + else: + log.error('Unsupported callback daemon protocol "%s"', protocol) + raise Exception('Unsupported callback daemon protocol.') + extras['hooks_uri'] = callback_daemon.hooks_uri extras['hooks_protocol'] = protocol @@ -236,6 +245,8 @@ class Hooks(object): def _call_hook(self, hook, extras): extras = AttributeDict(extras) + pylons_router = get_routes_generator_for_server_url(extras.server_url) + pylons.url._push_object(pylons_router) try: result = hook(extras) @@ -248,6 +259,9 @@ class Hooks(object): 'exception': type(error).__name__, 'exception_args': error_args, } + finally: + pylons.url._pop_object() + return { 'status': result.status, 'output': result.output, diff --git a/rhodecode/lib/hooks_utils.py b/rhodecode/lib/hooks_utils.py --- a/rhodecode/lib/hooks_utils.py +++ b/rhodecode/lib/hooks_utils.py @@ -21,6 +21,7 @@ import pylons import webob +from rhodecode import events from rhodecode.lib import hooks_base from rhodecode.lib import utils2 @@ -76,6 +77,7 @@ def trigger_log_create_pull_request_hook extras = _get_rc_scm_extras(username, repo_name, repo_alias, 'create_pull_request') + events.trigger(events.PullRequestCreateEvent(pull_request)) extras.update(pull_request.get_api_data()) hooks_base.log_create_pull_request(**extras) @@ -95,6 +97,7 @@ def trigger_log_merge_pull_request_hook( extras = _get_rc_scm_extras(username, repo_name, repo_alias, 'merge_pull_request') + events.trigger(events.PullRequestMergeEvent(pull_request)) extras.update(pull_request.get_api_data()) hooks_base.log_merge_pull_request(**extras) @@ -114,6 +117,7 @@ def trigger_log_close_pull_request_hook( extras = _get_rc_scm_extras(username, repo_name, repo_alias, 'close_pull_request') + events.trigger(events.PullRequestCloseEvent(pull_request)) extras.update(pull_request.get_api_data()) hooks_base.log_close_pull_request(**extras) @@ -133,6 +137,7 @@ def trigger_log_review_pull_request_hook extras = _get_rc_scm_extras(username, repo_name, repo_alias, 'review_pull_request') + events.trigger(events.PullRequestReviewEvent(pull_request)) extras.update(pull_request.get_api_data()) hooks_base.log_review_pull_request(**extras) @@ -152,5 +157,6 @@ def trigger_log_update_pull_request_hook extras = _get_rc_scm_extras(username, repo_name, repo_alias, 'update_pull_request') + events.trigger(events.PullRequestUpdateEvent(pull_request)) extras.update(pull_request.get_api_data()) hooks_base.log_update_pull_request(**extras) diff --git a/rhodecode/lib/index/whoosh_fallback_schema.py b/rhodecode/lib/index/whoosh_fallback_schema.py --- a/rhodecode/lib/index/whoosh_fallback_schema.py +++ b/rhodecode/lib/index/whoosh_fallback_schema.py @@ -49,9 +49,9 @@ FILE_SCHEMA = Schema( extension=ID(stored=True), commit_id=TEXT(stored=True), - size=NUMERIC(stored=True), + size=NUMERIC(int, 64, signed=False, stored=True), mimetype=TEXT(stored=True), - lines=NUMERIC(stored=True), + lines=NUMERIC(int, 64, signed=False, stored=True), ) @@ -63,7 +63,7 @@ COMMIT_SCHEMA = Schema( repository_id=NUMERIC(unique=True, stored=True), commit_idx=NUMERIC(stored=True, sortable=True), commit_idx_sort=ID(), - date=NUMERIC(stored=True, sortable=True), + date=NUMERIC(int, 64, signed=False, stored=True, sortable=True), owner=TEXT(stored=True), author=TEXT(stored=True), message=FieldType(format=Characters(), analyzer=ANALYZER, diff --git a/rhodecode/lib/jsonalchemy.py b/rhodecode/lib/jsonalchemy.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/jsonalchemy.py @@ -0,0 +1,265 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import collections + +import sqlalchemy +from sqlalchemy import UnicodeText +from sqlalchemy.ext.mutable import Mutable + +from rhodecode.lib.ext_json import json + + +class JsonRaw(unicode): + """ + Allows interacting with a JSON types field using a raw string. + + For example:: + db_instance = JsonTable() + db_instance.enabled = True + db_instance.json_data = JsonRaw('{"a": 4}') + + This will bypass serialization/checks, and allow storing + raw values + """ + pass + + +# Set this to the standard dict if Order is not required +DictClass = collections.OrderedDict + + +class JSONEncodedObj(sqlalchemy.types.TypeDecorator): + """ + Represents an immutable structure as a json-encoded string. + + If default is, for example, a dict, then a NULL value in the + database will be exposed as an empty dict. + """ + + impl = UnicodeText + safe = True + + def __init__(self, *args, **kwargs): + self.default = kwargs.pop('default', None) + self.safe = kwargs.pop('safe_json', self.safe) + self.dialect_map = kwargs.pop('dialect_map', {}) + super(JSONEncodedObj, self).__init__(*args, **kwargs) + + def load_dialect_impl(self, dialect): + if dialect.name in self.dialect_map: + return dialect.type_descriptor(self.dialect_map[dialect.name]) + return dialect.type_descriptor(self.impl) + + def process_bind_param(self, value, dialect): + if isinstance(value, JsonRaw): + value = value + elif value is not None: + value = json.dumps(value) + return value + + def process_result_value(self, value, dialect): + if self.default is not None and (not value or value == '""'): + return self.default() + + if value is not None: + try: + value = json.loads(value, object_pairs_hook=DictClass) + except Exception as e: + if self.safe: + return self.default() + else: + raise + return value + + +class MutationObj(Mutable): + @classmethod + def coerce(cls, key, value): + if isinstance(value, dict) and not isinstance(value, MutationDict): + return MutationDict.coerce(key, value) + if isinstance(value, list) and not isinstance(value, MutationList): + return MutationList.coerce(key, value) + return value + + @classmethod + def _listen_on_attribute(cls, attribute, coerce, parent_cls): + key = attribute.key + if parent_cls is not attribute.class_: + return + + # rely on "propagate" here + parent_cls = attribute.class_ + + def load(state, *args): + val = state.dict.get(key, None) + if coerce: + val = cls.coerce(key, val) + state.dict[key] = val + if isinstance(val, cls): + val._parents[state.obj()] = key + + def set(target, value, oldvalue, initiator): + if not isinstance(value, cls): + value = cls.coerce(key, value) + if isinstance(value, cls): + value._parents[target.obj()] = key + if isinstance(oldvalue, cls): + oldvalue._parents.pop(target.obj(), None) + return value + + def pickle(state, state_dict): + val = state.dict.get(key, None) + if isinstance(val, cls): + if 'ext.mutable.values' not in state_dict: + state_dict['ext.mutable.values'] = [] + state_dict['ext.mutable.values'].append(val) + + def unpickle(state, state_dict): + if 'ext.mutable.values' in state_dict: + for val in state_dict['ext.mutable.values']: + val._parents[state.obj()] = key + + sqlalchemy.event.listen(parent_cls, 'load', load, raw=True, + propagate=True) + sqlalchemy.event.listen(parent_cls, 'refresh', load, raw=True, + propagate=True) + sqlalchemy.event.listen(parent_cls, 'pickle', pickle, raw=True, + propagate=True) + sqlalchemy.event.listen(attribute, 'set', set, raw=True, retval=True, + propagate=True) + sqlalchemy.event.listen(parent_cls, 'unpickle', unpickle, raw=True, + propagate=True) + + +class MutationDict(MutationObj, DictClass): + @classmethod + def coerce(cls, key, value): + """Convert plain dictionary to MutationDict""" + self = MutationDict( + (k, MutationObj.coerce(key, v)) for (k, v) in value.items()) + self._key = key + return self + + def __setitem__(self, key, value): + # Due to the way OrderedDict works, this is called during __init__. + # At this time we don't have a key set, but what is more, the value + # being set has already been coerced. So special case this and skip. + if hasattr(self, '_key'): + value = MutationObj.coerce(self._key, value) + DictClass.__setitem__(self, key, value) + self.changed() + + def __delitem__(self, key): + DictClass.__delitem__(self, key) + self.changed() + + def __setstate__(self, state): + self.__dict__ = state + + def __reduce_ex__(self, proto): + # support pickling of MutationDicts + d = dict(self) + return (self.__class__, (d, )) + + +class MutationList(MutationObj, list): + @classmethod + def coerce(cls, key, value): + """Convert plain list to MutationList""" + self = MutationList((MutationObj.coerce(key, v) for v in value)) + self._key = key + return self + + def __setitem__(self, idx, value): + list.__setitem__(self, idx, MutationObj.coerce(self._key, value)) + self.changed() + + def __setslice__(self, start, stop, values): + list.__setslice__(self, start, stop, + (MutationObj.coerce(self._key, v) for v in values)) + self.changed() + + def __delitem__(self, idx): + list.__delitem__(self, idx) + self.changed() + + def __delslice__(self, start, stop): + list.__delslice__(self, start, stop) + self.changed() + + def append(self, value): + list.append(self, MutationObj.coerce(self._key, value)) + self.changed() + + def insert(self, idx, value): + list.insert(self, idx, MutationObj.coerce(self._key, value)) + self.changed() + + def extend(self, values): + list.extend(self, (MutationObj.coerce(self._key, v) for v in values)) + self.changed() + + def pop(self, *args, **kw): + value = list.pop(self, *args, **kw) + self.changed() + return value + + def remove(self, value): + list.remove(self, value) + self.changed() + + +def JsonType(impl=None, **kwargs): + """ + Helper for using a mutation obj, it allows to use .with_variant easily. + example:: + + settings = Column('settings_json', + MutationObj.as_mutable( + JsonType(dialect_map=dict(mysql=UnicodeText(16384)))) + """ + + if impl == 'list': + return JSONEncodedObj(default=list, **kwargs) + elif impl == 'dict': + return JSONEncodedObj(default=DictClass, **kwargs) + else: + return JSONEncodedObj(**kwargs) + + +JSON = MutationObj.as_mutable(JsonType()) +""" +A type to encode/decode JSON on the fly + +sqltype is the string type for the underlying DB column:: + + Column(JSON) (defaults to UnicodeText) +""" + +JSONDict = MutationObj.as_mutable(JsonType('dict')) +""" +A type to encode/decode JSON dictionaries on the fly +""" + +JSONList = MutationObj.as_mutable(JsonType('list')) +""" +A type to encode/decode JSON lists` on the fly +""" diff --git a/rhodecode/lib/markdown_ext.py b/rhodecode/lib/markdown_ext.py --- a/rhodecode/lib/markdown_ext.py +++ b/rhodecode/lib/markdown_ext.py @@ -22,6 +22,8 @@ import re import markdown +from mdx_gfm import GithubFlavoredMarkdownExtension # noqa + class FlavoredCheckboxExtension(markdown.Extension): @@ -65,8 +67,6 @@ class FlavoredCheckboxPostprocessor(mark return html.replace(before, after) - - # Global Vars URLIZE_RE = '(%s)' % '|'.join([ r'<(?:f|ht)tps?://[^>]*>', @@ -75,6 +75,7 @@ URLIZE_RE = '(%s)' % '|'.join([ r'[^(<\s]+\.(?:com|net|org)\b', ]) + class UrlizePattern(markdown.inlinepatterns.Pattern): """ Return a link Element given an autolink (`http://example/com`). """ def handleMatch(self, m): diff --git a/rhodecode/lib/markup_renderer.py b/rhodecode/lib/markup_renderer.py --- a/rhodecode/lib/markup_renderer.py +++ b/rhodecode/lib/markup_renderer.py @@ -23,17 +23,19 @@ Renderer for markup languages with ability to parse using rst or markdown """ - import re import os import logging +import itertools + from mako.lookup import TemplateLookup from docutils.core import publish_parts from docutils.parsers.rst import directives import markdown -from rhodecode.lib.markdown_ext import FlavoredCheckboxExtension, UrlizeExtension +from rhodecode.lib.markdown_ext import ( + UrlizeExtension, GithubFlavoredMarkdownExtension) from rhodecode.lib.utils2 import safe_unicode, md5_safe, MENTIONS_REGEX log = logging.getLogger(__name__) @@ -49,6 +51,37 @@ class MarkupRenderer(object): RST_PAT = re.compile(r'\.re?st$', re.IGNORECASE) PLAIN_PAT = re.compile(r'^readme$', re.IGNORECASE) + # list of readme files to search in file tree and display in summary + # attached weights defines the search order lower is first + ALL_READMES = [ + ('readme', 0), ('README', 0), ('Readme', 0), + ('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1), + ('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2), + ('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2), + ('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2), + ] + # extension together with weights. Lower is first means we control how + # extensions are attached to readme names with those. + PLAIN_EXTS = [ + ('', 0), # special case that renders READMES names without extension + ('.text', 2), ('.TEXT', 2), + ('.txt', 3), ('.TXT', 3) + ] + + RST_EXTS = [ + ('.rst', 1), ('.rest', 1), + ('.RST', 2), ('.REST', 2) + ] + + MARKDOWN_EXTS = [ + ('.md', 1), ('.MD', 1), + ('.mkdn', 2), ('.MKDN', 2), + ('.mdown', 3), ('.MDOWN', 3), + ('.markdown', 4), ('.MARKDOWN', 4) + ] + + ALL_EXTS = PLAIN_EXTS + MARKDOWN_EXTS + RST_EXTS + def _detect_renderer(self, source, filename=None): """ runs detection of what renderer should be used for generating html @@ -71,6 +104,49 @@ class MarkupRenderer(object): return getattr(MarkupRenderer, detected_renderer) + @classmethod + def renderer_from_filename(cls, filename, exclude): + """ + Detect renderer markdown/rst from filename and optionally use exclude + list to remove some options. This is mostly used in helpers. + Returns None when no renderer can be detected. + """ + def _filter(elements): + if isinstance(exclude, (list, tuple)): + return [x for x in elements if x not in exclude] + return elements + + if filename.endswith( + tuple(_filter([x[0] for x in cls.MARKDOWN_EXTS if x[0]]))): + return 'markdown' + if filename.endswith(tuple(_filter([x[0] for x in cls.RST_EXTS if x[0]]))): + return 'rst' + + return None + + @classmethod + def generate_readmes(cls, all_readmes, extensions): + combined = itertools.product(all_readmes, extensions) + # sort by filename weight(y[0][1]) + extensions weight(y[1][1]) + prioritized_readmes = sorted(combined, key=lambda y: y[0][1] + y[1][1]) + # filename, extension + return [''.join([x[0][0], x[1][0]]) for x in prioritized_readmes] + + def pick_readme_order(self, default_renderer): + + if default_renderer == 'markdown': + markdown = self.generate_readmes(self.ALL_READMES, self.MARKDOWN_EXTS) + readme_order = markdown + self.generate_readmes( + self.ALL_READMES, self.RST_EXTS + self.PLAIN_EXTS) + elif default_renderer == 'rst': + markdown = self.generate_readmes(self.ALL_READMES, self.RST_EXTS) + readme_order = markdown + self.generate_readmes( + self.ALL_READMES, self.MARKDOWN_EXTS + self.PLAIN_EXTS) + else: + readme_order = self.generate_readmes(self.ALL_READMES, self.ALL_EXTS) + + return readme_order + def render(self, source, filename=None): """ Renders a given filename using detected renderer @@ -141,15 +217,13 @@ class MarkupRenderer(object): return '
' + source.replace("\n", '
') @classmethod - def markdown(cls, source, safe=True, flavored=False, mentions=False): + def markdown(cls, source, safe=True, flavored=True, mentions=False): # It does not allow to insert inline HTML. In presence of HTML tags, it # will replace them instead with [HTML_REMOVED]. This is controlled by # the safe_mode=True parameter of the markdown method. extensions = ['codehilite', 'extra', 'def_list', 'sane_lists'] if flavored: - extensions.append('nl2br') - extensions.append(FlavoredCheckboxExtension()) - extensions.append(UrlizeExtension()) + extensions.append(GithubFlavoredMarkdownExtension()) if mentions: mention_pat = re.compile(MENTIONS_REGEX) @@ -171,7 +245,7 @@ class MarkupRenderer(object): except Exception: log.exception('Error when rendering Markdown') if safe: - log.debug('Fallbacking to render in plain mode') + log.debug('Fallback to render in plain mode') return cls.plain(source) else: raise @@ -190,8 +264,9 @@ class MarkupRenderer(object): source = safe_unicode(source) try: - docutils_settings = dict([(alias, None) for alias in - cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES]) + docutils_settings = dict( + [(alias, None) for alias in + cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES]) docutils_settings.update({'input_encoding': 'unicode', 'report_level': 4}) diff --git a/rhodecode/lib/middleware/appenlight.py b/rhodecode/lib/middleware/appenlight.py --- a/rhodecode/lib/middleware/appenlight.py +++ b/rhodecode/lib/middleware/appenlight.py @@ -25,7 +25,6 @@ middleware to handle appenlight publishi from appenlight_client import make_appenlight_middleware from appenlight_client.exceptions import get_current_traceback from appenlight_client.wsgi import AppenlightWSGIWrapper -from paste.deploy.converters import asbool def track_exception(environ): @@ -50,7 +49,7 @@ def track_extra_information(environ, sec environ['appenlight.extra'][section] = value -def wrap_in_appenlight_if_enabled(app, config, appenlight_client=None): +def wrap_in_appenlight_if_enabled(app, settings, appenlight_client=None): """ Wraps the given `app` for appenlight support. @@ -64,10 +63,10 @@ def wrap_in_appenlight_if_enabled(app, c This is in use to support our setup of the vcs related middlewares. """ - if asbool(config['app_conf'].get('appenlight')): + if settings['appenlight']: app = RemoteTracebackTracker(app) if not appenlight_client: - app = make_appenlight_middleware(app, config) + app = make_appenlight_middleware(app, settings) appenlight_client = app.appenlight_client else: app = AppenlightWSGIWrapper(app, appenlight_client) diff --git a/rhodecode/lib/middleware/simplesvn.py b/rhodecode/lib/middleware/simplesvn.py --- a/rhodecode/lib/middleware/simplesvn.py +++ b/rhodecode/lib/middleware/simplesvn.py @@ -30,7 +30,7 @@ from rhodecode.lib.utils import is_valid class SimpleSvnApp(object): IGNORED_HEADERS = [ 'connection', 'keep-alive', 'content-encoding', - 'transfer-encoding'] + 'transfer-encoding', 'content-length'] def __init__(self, config): self.config = config @@ -79,12 +79,18 @@ class SimpleSvnApp(object): return headers def _get_response_headers(self, headers): - return [ + headers = [ (h, headers[h]) for h in headers if h.lower() not in self.IGNORED_HEADERS ] + # Add custom response header to indicate that this is a VCS response + # and which backend is used. + headers.append(('X-RhodeCode-Backend', 'svn')) + + return headers + class SimpleSvn(simplevcs.SimpleVCS): diff --git a/rhodecode/lib/middleware/simplevcs.py b/rhodecode/lib/middleware/simplevcs.py --- a/rhodecode/lib/middleware/simplevcs.py +++ b/rhodecode/lib/middleware/simplevcs.py @@ -42,8 +42,10 @@ from rhodecode.lib.exceptions import ( from rhodecode.lib.hooks_daemon import prepare_callback_daemon from rhodecode.lib.middleware import appenlight from rhodecode.lib.middleware.utils import scm_app -from rhodecode.lib.utils import is_valid_repo +from rhodecode.lib.utils import ( + is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path) from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool +from rhodecode.lib.vcs.conf import settings as vcs_settings from rhodecode.model import meta from rhodecode.model.db import User, Repository from rhodecode.model.scm import ScmModel @@ -80,23 +82,24 @@ class SimpleVCS(object): SCM = 'unknown' - def __init__(self, application, config): + def __init__(self, application, config, registry): + self.registry = registry self.application = application self.config = config # base path of repo locations - self.basepath = self.config['base_path'] + self.basepath = get_rhodecode_base_path() # authenticate this VCS request using authfunc auth_ret_code_detection = \ str2bool(self.config.get('auth_ret_code_detection', False)) - self.authenticate = BasicAuth('', authenticate, - config.get('auth_ret_code'), - auth_ret_code_detection) + self.authenticate = BasicAuth( + '', authenticate, registry, config.get('auth_ret_code'), + auth_ret_code_detection) self.ip_addr = '0.0.0.0' @property def scm_app(self): custom_implementation = self.config.get('vcs.scm_app_implementation') - if custom_implementation: + if custom_implementation and custom_implementation != 'pyro4': log.info( "Using custom implementation of scm_app: %s", custom_implementation) @@ -282,15 +285,15 @@ class SimpleVCS(object): # try to auth based on environ, container auth methods log.debug('Running PRE-AUTH for container based authentication') - pre_auth = authenticate('', '', environ,VCS_TYPE) + pre_auth = authenticate( + '', '', environ, VCS_TYPE, registry=self.registry) if pre_auth and pre_auth.get('username'): username = pre_auth['username'] log.debug('PRE-AUTH got %s as username', username) # If not authenticated by the container, running basic auth if not username: - self.authenticate.realm = \ - safe_str(self.config['rhodecode_realm']) + self.authenticate.realm = get_rhodecode_realm() try: result = self.authenticate(environ) @@ -349,6 +352,7 @@ class SimpleVCS(object): log.info( '%s action on %s repo "%s" by "%s" from %s', action, self.SCM, str_repo_name, safe_str(username), ip_addr) + return self._generate_vcs_response( environ, start_response, repo_path, repo_name, extras, action) @@ -429,8 +433,8 @@ class SimpleVCS(object): def _prepare_callback_daemon(self, extras): return prepare_callback_daemon( - extras, protocol=self.config.get('vcs.hooks.protocol'), - use_direct_calls=self.config.get('vcs.hooks.direct_calls')) + extras, protocol=vcs_settings.HOOKS_PROTOCOL, + use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS) def _should_check_locking(query_string): diff --git a/rhodecode/lib/middleware/utils/scm_app_http.py b/rhodecode/lib/middleware/utils/scm_app_http.py --- a/rhodecode/lib/middleware/utils/scm_app_http.py +++ b/rhodecode/lib/middleware/utils/scm_app_http.py @@ -39,12 +39,12 @@ log = logging.getLogger(__name__) def create_git_wsgi_app(repo_path, repo_name, config): url = _vcs_streaming_url() + 'git/' - return VcsHttpProxy(url, repo_path, repo_name, config) + return VcsHttpProxy(url, repo_path, repo_name, config, 'git') def create_hg_wsgi_app(repo_path, repo_name, config): url = _vcs_streaming_url() + 'hg/' - return VcsHttpProxy(url, repo_path, repo_name, config) + return VcsHttpProxy(url, repo_path, repo_name, config, 'hg') def _vcs_streaming_url(): @@ -67,7 +67,7 @@ class VcsHttpProxy(object): server as well. """ - def __init__(self, url, repo_path, repo_name, config): + def __init__(self, url, repo_path, repo_name, config, backend): """ :param str url: The URL of the VCSServer to call. """ @@ -75,6 +75,7 @@ class VcsHttpProxy(object): self._repo_name = repo_name self._repo_path = repo_path self._config = config + self._backend = backend log.debug( "Creating VcsHttpProxy for repo %s, url %s", repo_name, url) @@ -115,6 +116,10 @@ class VcsHttpProxy(object): if not wsgiref.util.is_hop_by_hop(h) ] + # Add custom response header to indicate that this is a VCS response + # and which backend is used. + response_headers.append(('X-RhodeCode-Backend', self._backend)) + # TODO: johbo: Better way to get the status including text? status = str(response.status_code) start_response(status, response_headers) diff --git a/rhodecode/lib/middleware/vcs.py b/rhodecode/lib/middleware/vcs.py --- a/rhodecode/lib/middleware/vcs.py +++ b/rhodecode/lib/middleware/vcs.py @@ -72,7 +72,11 @@ def is_svn(environ): Returns True if requests target is Subversion server """ http_dav = environ.get('HTTP_DAV', '') - is_svn_path = 'subversion' in http_dav + magic_path_segment = rhodecode.CONFIG.get( + 'rhodecode_subversion_magic_path', '/!svn') + is_svn_path = ( + 'subversion' in http_dav or + magic_path_segment in environ['PATH_INFO']) log.debug( 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'], is_svn_path) @@ -122,23 +126,24 @@ class GunzipMiddleware(object): class VCSMiddleware(object): - def __init__(self, app, config, appenlight_client): + def __init__(self, app, config, appenlight_client, registry): self.application = app self.config = config self.appenlight_client = appenlight_client + self.registry = registry def _get_handler_app(self, environ): app = None if is_hg(environ): - app = SimpleHg(self.application, self.config) + app = SimpleHg(self.application, self.config, self.registry) if is_git(environ): - app = SimpleGit(self.application, self.config) + app = SimpleGit(self.application, self.config, self.registry) proxy_svn = rhodecode.CONFIG.get( 'rhodecode_proxy_subversion_http_requests', False) if proxy_svn and is_svn(environ): - app = SimpleSvn(self.application, self.config) + app = SimpleSvn(self.application, self.config, self.registry) if app: app = GunzipMiddleware(app) diff --git a/rhodecode/lib/paster_commands/make_config.py b/rhodecode/lib/paster_commands/make_config.py --- a/rhodecode/lib/paster_commands/make_config.py +++ b/rhodecode/lib/paster_commands/make_config.py @@ -19,7 +19,7 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ """ -depracated make-config paster command for RhodeCode +deprecated make-config paster command for RhodeCode """ import os diff --git a/rhodecode/lib/plugins/utils.py b/rhodecode/lib/plugins/utils.py --- a/rhodecode/lib/plugins/utils.py +++ b/rhodecode/lib/plugins/utils.py @@ -23,8 +23,6 @@ def get_plugin_settings(prefix, settings """ Returns plugin settings. Use:: - - :param prefix: :param settings: :return: @@ -45,21 +43,15 @@ def register_rhodecode_plugin(config, pl 'javascript': None, 'static': None, 'css': None, - 'top_nav': None, + 'nav': None, 'fulltext_indexer': None, 'sqlalchemy_migrations': None, 'default_values_setter': None, - 'resource_types': [], - 'url_gen': None + 'url_gen': None, + 'template_hooks': {} } config.registry.rhodecode_plugins[plugin_name].update( plugin_config) - # inform RC what kind of resource types we have available - # so we can avoid failing when a plugin is removed but data - # is still present in the db - if plugin_config.get('resource_types'): - config.registry.resource_types.extend( - plugin_config['resource_types']) config.action( 'register_rhodecode_plugin={}'.format(plugin_name), register) diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -352,17 +352,17 @@ def is_valid_repo_group(repo_group_name, return False -def ask_ok(prompt, retries=4, complaint='Yes or no please!'): +def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'): while True: ok = raw_input(prompt) - if ok in ('y', 'ye', 'yes'): + if ok.lower() in ('y', 'ye', 'yes'): return True - if ok in ('n', 'no', 'nop', 'nope'): + if ok.lower() in ('n', 'no', 'nop', 'nope'): return False retries = retries - 1 if retries < 0: raise IOError - print complaint + print(complaint) # propagated from mercurial documentation ui_sections = [ @@ -475,6 +475,25 @@ def set_rhodecode_config(config): config[k] = v +def get_rhodecode_realm(): + """ + Return the rhodecode realm from database. + """ + from rhodecode.model.settings import SettingsModel + realm = SettingsModel().get_setting_by_name('realm') + return safe_str(realm.app_settings_value) + + +def get_rhodecode_base_path(): + """ + Returns the base path. The base path is the filesystem path which points + to the repository store. + """ + from rhodecode.model.settings import SettingsModel + paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/') + return safe_str(paths_ui.ui_value) + + def map_groups(path): """ Given a full path to a repository, create all nested groups that this @@ -958,8 +977,10 @@ class PartialRenderer(object): def password_changed(auth_user, session): - if auth_user.username == User.DEFAULT_USER: + # Never report password change in case of default user or anonymous user. + if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None: return False + password_hash = md5(auth_user.password) if auth_user.password else None rhodecode_user = session.get('rhodecode_user', {}) session_password_hash = rhodecode_user.get('password', '') diff --git a/rhodecode/lib/utils2.py b/rhodecode/lib/utils2.py --- a/rhodecode/lib/utils2.py +++ b/rhodecode/lib/utils2.py @@ -41,6 +41,7 @@ import pygments.lexers import sqlalchemy import sqlalchemy.engine.url import webob +import routes.util import rhodecode @@ -858,3 +859,28 @@ class Optional(object): if isinstance(val, cls): return val.getval() return val + + +def get_routes_generator_for_server_url(server_url): + parsed_url = urlobject.URLObject(server_url) + netloc = safe_str(parsed_url.netloc) + script_name = safe_str(parsed_url.path) + + if ':' in netloc: + server_name, server_port = netloc.split(':') + else: + server_name = netloc + server_port = (parsed_url.scheme == 'https' and '443' or '80') + + environ = { + 'REQUEST_METHOD': 'GET', + 'PATH_INFO': '/', + 'SERVER_NAME': server_name, + 'SERVER_PORT': server_port, + 'SCRIPT_NAME': script_name, + } + if parsed_url.scheme == 'https': + environ['HTTPS'] = 'on' + environ['wsgi.url_scheme'] = 'https' + + return routes.util.URLGenerator(rhodecode.CONFIG['routes.map'], environ) diff --git a/rhodecode/lib/vcs/__init__.py b/rhodecode/lib/vcs/__init__.py --- a/rhodecode/lib/vcs/__init__.py +++ b/rhodecode/lib/vcs/__init__.py @@ -29,7 +29,7 @@ VERSION = (0, 5, 0, 'dev') __version__ = '.'.join((str(each) for each in VERSION[:4])) __all__ = [ - 'get_version', 'get_repo', 'get_backend', + 'get_version', 'get_vcs_instance', 'get_backend', 'VCSError', 'RepositoryError', 'CommitError' ] @@ -40,17 +40,29 @@ import time import urlparse from cStringIO import StringIO -import pycurl import Pyro4 from Pyro4.errors import CommunicationError from rhodecode.lib.vcs.conf import settings -from rhodecode.lib.vcs.backends import get_repo, get_backend +from rhodecode.lib.vcs.backends import get_vcs_instance, get_backend from rhodecode.lib.vcs.exceptions import ( VCSError, RepositoryError, CommitError) +log = logging.getLogger(__name__) -log = logging.getLogger(__name__) +# The pycurl library directly accesses C API functions and is not patched by +# gevent. This will potentially lead to deadlocks due to incompatibility to +# gevent. Therefore we check if gevent is active and import a gevent compatible +# wrapper in that case. +try: + from gevent import monkey + if monkey.is_module_patched('__builtin__'): + import geventcurl as pycurl + log.debug('Using gevent comapatible pycurl: %s', pycurl) + else: + import pycurl +except ImportError: + import pycurl def get_version(): @@ -64,11 +76,11 @@ def connect_pyro4(server_and_port): from rhodecode.lib.vcs import connection, client from rhodecode.lib.middleware.utils import scm_app - git_remote = client.ThreadlocalProxyFactory( + git_remote = client.RequestScopeProxyFactory( settings.pyro_remote(settings.PYRO_GIT, server_and_port)) - hg_remote = client.ThreadlocalProxyFactory( + hg_remote = client.RequestScopeProxyFactory( settings.pyro_remote(settings.PYRO_HG, server_and_port)) - svn_remote = client.ThreadlocalProxyFactory( + svn_remote = client.RequestScopeProxyFactory( settings.pyro_remote(settings.PYRO_SVN, server_and_port)) connection.Git = client.RepoMaker(proxy_factory=git_remote) diff --git a/rhodecode/lib/vcs/backends/__init__.py b/rhodecode/lib/vcs/backends/__init__.py --- a/rhodecode/lib/vcs/backends/__init__.py +++ b/rhodecode/lib/vcs/backends/__init__.py @@ -22,7 +22,8 @@ VCS Backends module """ -import os +import logging + from pprint import pformat from rhodecode.lib.vcs.conf import settings @@ -31,31 +32,29 @@ from rhodecode.lib.vcs.utils.helpers imp from rhodecode.lib.vcs.utils.imports import import_class -def get_repo(path=None, alias=None, create=False): +log = logging.getLogger(__name__) + + +def get_vcs_instance(repo_path, *args, **kwargs): """ - Returns ``Repository`` object of type linked with given ``alias`` at - the specified ``path``. If ``alias`` is not given it will try to guess it - using get_scm method + Given a path to a repository an instance of the corresponding vcs backend + repository class is created and returned. If no repository can be found + for the path it returns None. Arguments and keyword arguments are passed + to the vcs backend repository class. """ - if create: - if not (path or alias): - raise TypeError( - "If create is specified, we need path and scm type") - return get_backend(alias)(path, create=True) - if path is None: - path = os.path.abspath(os.path.curdir) try: - scm, path = get_scm(path, search_path_up=True) - path = os.path.abspath(path) - alias = scm + vcs_alias = get_scm(repo_path)[0] + log.debug( + 'Creating instance of %s repository from %s', vcs_alias, repo_path) + backend = get_backend(vcs_alias) except VCSError: - raise VCSError("No scm found at %s" % path) - if alias is None: - alias = get_scm(path)[0] + log.exception( + 'Perhaps this repository is in db and not in ' + 'filesystem run rescan repositories with ' + '"destroy old data" option from admin panel') + return None - backend = get_backend(alias) - repo = backend(path, create=create) - return repo + return backend(repo_path=repo_path, *args, **kwargs) def get_backend(alias): diff --git a/rhodecode/lib/vcs/backends/base.py b/rhodecode/lib/vcs/backends/base.py --- a/rhodecode/lib/vcs/backends/base.py +++ b/rhodecode/lib/vcs/backends/base.py @@ -365,7 +365,8 @@ class BaseRepository(object): raise NotImplementedError def merge(self, target_ref, source_repo, source_ref, workspace_id, - user_name='', user_email='', message='', dry_run=False): + user_name='', user_email='', message='', dry_run=False, + use_rebase=False): """ Merge the revisions specified in `source_ref` from `source_repo` onto the `target_ref` of this repository. @@ -388,6 +389,8 @@ class BaseRepository(object): :param user_email: Merge commit `user_email`. :param message: Merge commit `message`. :param dry_run: If `True` the merge will not take place. + :param use_rebase: If `True` commits from the source will be rebased + on top of the target instead of being merged. """ if dry_run: message = message or 'sample_message' @@ -407,7 +410,8 @@ class BaseRepository(object): try: return self._merge_repo( shadow_repository_path, target_ref, source_repo, - source_ref, message, user_name, user_email, dry_run=dry_run) + source_ref, message, user_name, user_email, dry_run=dry_run, + use_rebase=use_rebase) except RepositoryError: log.exception( 'Unexpected failure when running merge, dry-run=%s', @@ -864,7 +868,7 @@ class BaseCommit(object): for f in files: f_path = os.path.join(prefix, f.path) file_info.append( - (f_path, f.mode, f.is_link(), f._get_content())) + (f_path, f.mode, f.is_link(), f.raw_bytes)) if write_metadata: metadata = [ @@ -1328,6 +1332,10 @@ class EmptyCommit(BaseCommit): def short_id(self): return self.raw_id[:12] + @LazyProperty + def id(self): + return self.raw_id + def get_file_commit(self, path): return self diff --git a/rhodecode/lib/vcs/backends/git/repository.py b/rhodecode/lib/vcs/backends/git/repository.py --- a/rhodecode/lib/vcs/backends/git/repository.py +++ b/rhodecode/lib/vcs/backends/git/repository.py @@ -833,7 +833,8 @@ class GitRepository(BaseRepository): def _merge_repo(self, shadow_repository_path, target_ref, source_repo, source_ref, merge_message, - merger_name, merger_email, dry_run=False): + merger_name, merger_email, dry_run=False, + use_rebase=False): if target_ref.commit_id != self.branches[target_ref.name]: return MergeResponse( False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD) diff --git a/rhodecode/lib/vcs/backends/hg/repository.py b/rhodecode/lib/vcs/backends/hg/repository.py --- a/rhodecode/lib/vcs/backends/hg/repository.py +++ b/rhodecode/lib/vcs/backends/hg/repository.py @@ -25,15 +25,15 @@ HG repository module import logging import binascii import os -import re import shutil import urllib from zope.cachedescriptors.property import Lazy as LazyProperty from rhodecode.lib.compat import OrderedDict -from rhodecode.lib.datelib import (date_to_timestamp_plus_offset, - utcdate_fromtimestamp, makedate, date_astimestamp) +from rhodecode.lib.datelib import ( + date_to_timestamp_plus_offset, utcdate_fromtimestamp, makedate, + date_astimestamp) from rhodecode.lib.utils import safe_unicode, safe_str from rhodecode.lib.vcs import connection from rhodecode.lib.vcs.backends.base import ( @@ -42,7 +42,6 @@ from rhodecode.lib.vcs.backends.base imp from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit -from rhodecode.lib.vcs.conf import settings from rhodecode.lib.vcs.exceptions import ( EmptyRepositoryError, RepositoryError, TagAlreadyExistError, TagDoesNotExistError, CommitDoesNotExistError) @@ -176,6 +175,7 @@ class MercurialRepository(BaseRepository self._remote.tag( name, commit.raw_id, message, local, user, date, tz) + self._remote.invalidate_vcs_cache() # Reinitialize tags self.tags = self._get_tags() @@ -203,6 +203,7 @@ class MercurialRepository(BaseRepository date, tz = date_to_timestamp_plus_offset(date) self._remote.tag(name, nullid, message, local, user, date, tz) + self._remote.invalidate_vcs_cache() self.tags = self._get_tags() @LazyProperty @@ -262,6 +263,7 @@ class MercurialRepository(BaseRepository def strip(self, commit_id, branch=None): self._remote.strip(commit_id, update=False, backup="none") + self._remote.invalidate_vcs_cache() self.commit_ids = self._get_all_commit_ids() self._rebuild_cache(self.commit_ids) @@ -531,6 +533,7 @@ class MercurialRepository(BaseRepository """ url = self._get_url(url) self._remote.pull(url, commit_ids=commit_ids) + self._remote.invalidate_vcs_cache() def _local_clone(self, clone_path): """ @@ -577,7 +580,7 @@ class MercurialRepository(BaseRepository push_branches=push_branches) def _local_merge(self, target_ref, merge_message, user_name, user_email, - source_ref): + source_ref, use_rebase=False): """ Merge the given source_revision into the checked out revision. @@ -597,13 +600,14 @@ class MercurialRepository(BaseRepository # In this case we should force a commit message return source_ref.commit_id, True - if settings.HG_USE_REBASE_FOR_MERGING: + if use_rebase: try: bookmark_name = 'rcbook%s%s' % (source_ref.commit_id, target_ref.commit_id) self.bookmark(bookmark_name, revision=source_ref.commit_id) self._remote.rebase( source=source_ref.commit_id, dest=target_ref.commit_id) + self._remote.invalidate_vcs_cache() self._update(bookmark_name) return self._identify(), True except RepositoryError: @@ -612,15 +616,19 @@ class MercurialRepository(BaseRepository log.exception('Error while rebasing shadow repo during merge.') # Cleanup any rebase leftovers + self._remote.invalidate_vcs_cache() self._remote.rebase(abort=True) + self._remote.invalidate_vcs_cache() self._remote.update(clean=True) raise else: try: self._remote.merge(source_ref.commit_id) + self._remote.invalidate_vcs_cache() self._remote.commit( message=safe_str(merge_message), username=safe_str('%s <%s>' % (user_name, user_email))) + self._remote.invalidate_vcs_cache() return self._identify(), True except RepositoryError: # Cleanup any merge leftovers @@ -659,7 +667,8 @@ class MercurialRepository(BaseRepository def _merge_repo(self, shadow_repository_path, target_ref, source_repo, source_ref, merge_message, - merger_name, merger_email, dry_run=False): + merger_name, merger_email, dry_run=False, + use_rebase=False): if target_ref.commit_id not in self._heads(): return MergeResponse( False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD) @@ -690,7 +699,7 @@ class MercurialRepository(BaseRepository try: merge_commit_id, needs_push = shadow_repo._local_merge( target_ref, merge_message, merger_name, merger_email, - source_ref) + source_ref, use_rebase=use_rebase) merge_possible = True except RepositoryError as e: log.exception('Failure when doing local merge on hg shadow repo') @@ -771,11 +780,13 @@ class MercurialRepository(BaseRepository options = {option_name: [ref]} self._remote.pull_cmd(repository_path, hooks=False, **options) + self._remote.invalidate_vcs_cache() def bookmark(self, bookmark, revision=None): if isinstance(bookmark, unicode): bookmark = safe_str(bookmark) self._remote.bookmark(bookmark, revision=revision) + self._remote.invalidate_vcs_cache() class MercurialIndexBasedCollectionGenerator(CollectionGenerator): diff --git a/rhodecode/lib/vcs/backends/svn/commit.py b/rhodecode/lib/vcs/backends/svn/commit.py --- a/rhodecode/lib/vcs/backends/svn/commit.py +++ b/rhodecode/lib/vcs/backends/svn/commit.py @@ -203,6 +203,10 @@ class SubversionCommit(base.BaseCommit): changed_files.update(files) return list(changed_files) + @LazyProperty + def id(self): + return self.raw_id + @property def added(self): return nodes.AddedFileNodesGenerator( diff --git a/rhodecode/lib/vcs/client.py b/rhodecode/lib/vcs/client.py --- a/rhodecode/lib/vcs/client.py +++ b/rhodecode/lib/vcs/client.py @@ -34,6 +34,7 @@ from urllib2 import URLError import msgpack import Pyro4 import requests +from pyramid.threadlocal import get_current_request from Pyro4.errors import CommunicationError, ConnectionClosedError, DaemonError from rhodecode.lib.vcs import exceptions @@ -190,19 +191,67 @@ class RepoMaker(object): return _wrap_remote_call(remote_proxy, func) -class ThreadlocalProxyFactory(object): +class RequestScopeProxyFactory(object): """ - Creates one Pyro4 proxy per thread on demand. + This factory returns pyro proxy instances based on a per request scope. + It returns the same instance if called from within the same request and + different instances if called from different requests. """ def __init__(self, remote_uri): self._remote_uri = remote_uri - self._thread_local = threading.local() + self._proxy_pool = [] + self._borrowed_proxies = {} + + def __call__(self, request=None): + """ + Wrapper around `getProxy`. + """ + request = request or get_current_request() + return self.getProxy(request) + + def getProxy(self, request): + """ + Call this to get the pyro proxy instance for the request. + """ + + # If called without a request context we return new proxy instances + # on every call. This allows to run e.g. invoke tasks. + if request is None: + log.info('Creating pyro proxy without request context for ' + 'remote_uri=%s', self._remote_uri) + return Pyro4.Proxy(self._remote_uri) - def __call__(self): - if not hasattr(self._thread_local, 'proxy'): - self._thread_local.proxy = Pyro4.Proxy(self._remote_uri) - return self._thread_local.proxy + # If there is an already borrowed proxy for the request context we + # return that instance instead of creating a new one. + if request in self._borrowed_proxies: + return self._borrowed_proxies[request] + + # Get proxy from pool or create new instance. + try: + proxy = self._proxy_pool.pop() + except IndexError: + log.info('Creating pyro proxy for remote_uri=%s', self._remote_uri) + proxy = Pyro4.Proxy(self._remote_uri) + + # Mark proxy as borrowed for the request context and add a callback + # that returns it when the request processing is finished. + self._borrowed_proxies[request] = proxy + request.add_finished_callback(self._returnProxy) + + return proxy + + def _returnProxy(self, request): + """ + Callback that gets called by pyramid when the request is finished. + It puts the proxy back into the pool. + """ + if request in self._borrowed_proxies: + proxy = self._borrowed_proxies.pop(request) + self._proxy_pool.append(proxy) + else: + log.warn('Return proxy for remote_uri=%s but no proxy borrowed ' + 'for this request.', self._remote_uri) class RemoteRepo(object): @@ -211,7 +260,7 @@ class RemoteRepo(object): self._wire = { "path": path, "config": config, - "context": uuid.uuid4(), + "context": self._create_vcs_cache_context(), } if with_wire: self._wire.update(with_wire) @@ -238,6 +287,19 @@ class RemoteRepo(object): def __getitem__(self, key): return self.revision(key) + def _create_vcs_cache_context(self): + """ + Creates a unique string which is passed to the VCSServer on every + remote call. It is used as cache key in the VCSServer. + """ + return str(uuid.uuid4()) + + def invalidate_vcs_cache(self): + """ + This is a no-op method for the pyro4 backend but we want to have the + same API for client.RemoteRepo and client_http.RemoteRepo classes. + """ + def _get_proxy_method(proxy, name): try: diff --git a/rhodecode/lib/vcs/client_http.py b/rhodecode/lib/vcs/client_http.py --- a/rhodecode/lib/vcs/client_http.py +++ b/rhodecode/lib/vcs/client_http.py @@ -90,7 +90,7 @@ class RemoteRepo(object): self._wire = { "path": path, "config": config, - "context": str(uuid.uuid4()), + "context": self._create_vcs_cache_context(), } if with_wire: self._wire.update(with_wire) @@ -125,6 +125,21 @@ class RemoteRepo(object): def __getitem__(self, key): return self.revision(key) + def _create_vcs_cache_context(self): + """ + Creates a unique string which is passed to the VCSServer on every + remote call. It is used as cache key in the VCSServer. + """ + return str(uuid.uuid4()) + + def invalidate_vcs_cache(self): + """ + This invalidates the context which is sent to the VCSServer on every + call to a remote method. It forces the VCSServer to create a fresh + repository instance on the next call to a remote method. + """ + self._wire['context'] = self._create_vcs_cache_context() + class RemoteObject(object): diff --git a/rhodecode/lib/vcs/conf/settings.py b/rhodecode/lib/vcs/conf/settings.py --- a/rhodecode/lib/vcs/conf/settings.py +++ b/rhodecode/lib/vcs/conf/settings.py @@ -29,8 +29,6 @@ DEFAULT_ENCODINGS = ['utf8'] # It can also be ['--branches', '--tags'] GIT_REV_FILTER = ['--all'] -HG_USE_REBASE_FOR_MERGING = False - # Compatibility version when creating SVN repositories. None means newest. # Other available options are: pre-1.4-compatible, pre-1.5-compatible, # pre-1.6-compatible, pre-1.8-compatible @@ -51,6 +49,9 @@ ARCHIVE_SPECS = { 'zip': ('application/zip', '.zip'), } +HOOKS_PROTOCOL = None +HOOKS_DIRECT_CALLS = False + PYRO_PORT = 9900 PYRO_GIT = 'git_remote' diff --git a/rhodecode/lib/vcs/geventcurl.py b/rhodecode/lib/vcs/geventcurl.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/vcs/geventcurl.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +""" +This serves as a drop in replacement for pycurl. It implements the pycurl Curl +class in a way that is compatible with gevent. +""" + + +import logging +import gevent +import pycurl + +# Import everything from pycurl. +# This allows us to use this module as a drop in replacement of pycurl. +from pycurl import * # noqa + +from gevent import core +from gevent.hub import Waiter + + +log = logging.getLogger(__name__) + + +class GeventCurlMulti(object): + """ + Wrapper around pycurl.CurlMulti that integrates it into gevent's event + loop. + + Parts of this class are a modified version of code copied from the Tornado + Web Server project which is licensed under the Apache License, Version 2.0 + (the "License"). To be more specific the code originates from this file: + https://github.com/tornadoweb/tornado/blob/stable/tornado/curl_httpclient.py + + This is the original license header of the origin: + + Copyright 2009 Facebook + + Licensed under the Apache License, Version 2.0 (the "License"); you may + not use this file except in compliance with the License. You may obtain + a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. See the License for the specific language governing + permissions and limitations under the License. + """ + + def __init__(self, loop=None): + self._watchers = {} + self._timeout = None + self.loop = loop or gevent.get_hub().loop + + # Setup curl's multi instance. + self._curl_multi = pycurl.CurlMulti() + self.setopt(pycurl.M_TIMERFUNCTION, self._set_timeout) + self.setopt(pycurl.M_SOCKETFUNCTION, self._handle_socket) + + def __getattr__(self, item): + """ + The pycurl.CurlMulti class is final and we cannot subclass it. + Therefore we are wrapping it and forward everything to it here. + """ + return getattr(self._curl_multi, item) + + def add_handle(self, curl): + """ + Add handle variant that also takes care about the initial invocation of + socket action method. This is done by setting an immediate timeout. + """ + result = self._curl_multi.add_handle(curl) + self._set_timeout(0) + return result + + def _handle_socket(self, event, fd, multi, data): + """ + Called by libcurl when it wants to change the file descriptors it cares + about. + """ + event_map = { + pycurl.POLL_NONE: core.NONE, + pycurl.POLL_IN: core.READ, + pycurl.POLL_OUT: core.WRITE, + pycurl.POLL_INOUT: core.READ | core.WRITE + } + + if event == pycurl.POLL_REMOVE: + watcher = self._watchers.pop(fd, None) + if watcher is not None: + watcher.stop() + else: + gloop_event = event_map[event] + watcher = self._watchers.get(fd) + if watcher is None: + watcher = self.loop.io(fd, gloop_event) + watcher.start(self._handle_events, fd, pass_events=True) + self._watchers[fd] = watcher + else: + if watcher.events != gloop_event: + watcher.stop() + watcher.events = gloop_event + watcher.start(self._handle_events, fd, pass_events=True) + + def _set_timeout(self, msecs): + """ + Called by libcurl to schedule a timeout. + """ + if self._timeout is not None: + self._timeout.stop() + self._timeout = self.loop.timer(msecs/1000.0) + self._timeout.start(self._handle_timeout) + + def _handle_events(self, events, fd): + action = 0 + if events & core.READ: + action |= pycurl.CSELECT_IN + if events & core.WRITE: + action |= pycurl.CSELECT_OUT + while True: + try: + ret, num_handles = self._curl_multi.socket_action(fd, action) + except pycurl.error, e: + ret = e.args[0] + if ret != pycurl.E_CALL_MULTI_PERFORM: + break + self._finish_pending_requests() + + def _handle_timeout(self): + """ + Called by IOLoop when the requested timeout has passed. + """ + if self._timeout is not None: + self._timeout.stop() + self._timeout = None + while True: + try: + ret, num_handles = self._curl_multi.socket_action( + pycurl.SOCKET_TIMEOUT, 0) + except pycurl.error, e: + ret = e.args[0] + if ret != pycurl.E_CALL_MULTI_PERFORM: + break + self._finish_pending_requests() + + # In theory, we shouldn't have to do this because curl will call + # _set_timeout whenever the timeout changes. However, sometimes after + # _handle_timeout we will need to reschedule immediately even though + # nothing has changed from curl's perspective. This is because when + # socket_action is called with SOCKET_TIMEOUT, libcurl decides + # internally which timeouts need to be processed by using a monotonic + # clock (where available) while tornado uses python's time.time() to + # decide when timeouts have occurred. When those clocks disagree on + # elapsed time (as they will whenever there is an NTP adjustment), + # tornado might call _handle_timeout before libcurl is ready. After + # each timeout, resync the scheduled timeout with libcurl's current + # state. + new_timeout = self._curl_multi.timeout() + if new_timeout >= 0: + self._set_timeout(new_timeout) + + def _finish_pending_requests(self): + """ + Process any requests that were completed by the last call to + multi.socket_action. + """ + while True: + num_q, ok_list, err_list = self._curl_multi.info_read() + for curl in ok_list: + curl.waiter.switch() + for curl, errnum, errmsg in err_list: + curl.waiter.throw(Exception('%s %s' % (errnum, errmsg))) + if num_q == 0: + break + + +class GeventCurl(object): + """ + Gevent compatible implementation of the pycurl.Curl class. Essentially a + wrapper around pycurl.Curl with a customized perform method. It uses the + GeventCurlMulti class to implement a blocking API to libcurl's "easy" + interface. + """ + + # Reference to the GeventCurlMulti instance. + _multi_instance = None + + def __init__(self): + self._curl = pycurl.Curl() + + def __getattr__(self, item): + """ + The pycurl.Curl class is final and we cannot subclass it. Therefore we + are wrapping it and forward everything to it here. + """ + return getattr(self._curl, item) + + @property + def _multi(self): + """ + Lazy property that returns the GeventCurlMulti instance. The value is + cached as a class attribute. Therefore only one instance per process + exists. + """ + if GeventCurl._multi_instance is None: + GeventCurl._multi_instance = GeventCurlMulti() + return GeventCurl._multi_instance + + def perform(self): + """ + This perform method is compatible with gevent because it uses gevent + synchronization mechanisms to wait for the request to finish. + """ + waiter = self._curl.waiter = Waiter() + try: + self._multi.add_handle(self._curl) + response = waiter.get() + finally: + self._multi.remove_handle(self._curl) + del self._curl.waiter + + return response + +# Curl is originally imported from pycurl. At this point we override it with +# our custom implementation. +Curl = GeventCurl diff --git a/rhodecode/lib/vcs/nodes.py b/rhodecode/lib/vcs/nodes.py --- a/rhodecode/lib/vcs/nodes.py +++ b/rhodecode/lib/vcs/nodes.py @@ -28,6 +28,7 @@ import stat from zope.cachedescriptors.property import Lazy as LazyProperty from rhodecode.lib.utils import safe_unicode, safe_str +from rhodecode.lib.utils2 import md5 from rhodecode.lib.vcs import path as vcspath from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db @@ -318,22 +319,35 @@ class FileNode(Node): mode = self._mode return mode - def _get_content(self): + @LazyProperty + def raw_bytes(self): + """ + Returns lazily the raw bytes of the FileNode. + """ if self.commit: - content = self.commit.get_file_content(self.path) + if self._content is None: + self._content = self.commit.get_file_content(self.path) + content = self._content else: content = self._content return content - @property + @LazyProperty + def md5(self): + """ + Returns md5 of the file node. + """ + return md5(self.raw_bytes) + + @LazyProperty def content(self): """ Returns lazily content of the FileNode. If possible, would try to decode content from UTF-8. """ - content = self._get_content() + content = self.raw_bytes - if bool(content and '\0' in content): + if self.is_binary: return content return safe_unicode(content) @@ -467,12 +481,12 @@ class FileNode(Node): else: return NodeState.NOT_CHANGED - @property + @LazyProperty def is_binary(self): """ Returns True if file has binary content. """ - _bin = '\0' in self._get_content() + _bin = self.raw_bytes and '\0' in self.raw_bytes return _bin @LazyProperty @@ -502,7 +516,7 @@ class FileNode(Node): all_lines, empty_lines = 0, 0 if not self.is_binary: - content = self._get_content() + content = self.content if count_empty: all_lines = 0 empty_lines = 0 @@ -717,22 +731,10 @@ class LargeFileNode(FileNode): we override check since the LargeFileNode path is system absolute """ - def _get_content(self): + def raw_bytes(self): if self.commit: with open(self.path, 'rb') as f: content = f.read() else: content = self._content - return content - - @property - def content(self): - """ - Returns lazily content of the `FileNode`. If possible, would try to - decode content from UTF-8. - """ - content = self._get_content() - - if bool(content and '\0' in content): - return content - return safe_unicode(content) + return content \ No newline at end of file diff --git a/rhodecode/lib/vcs/utils/helpers.py b/rhodecode/lib/vcs/utils/helpers.py --- a/rhodecode/lib/vcs/utils/helpers.py +++ b/rhodecode/lib/vcs/utils/helpers.py @@ -38,39 +38,19 @@ from rhodecode.lib.vcs.exceptions import log = logging.getLogger(__name__) -def get_scm(path, search_path_up=False, explicit_alias=None): +def get_scm(path): """ Returns one of alias from ``ALIASES`` (in order of precedence same as - shortcuts given in ``ALIASES``) and top working dir path for the given + shortcuts given in ``ALIASES``) and working dir path for the given argument. If no scm-specific directory is found or more than one scm is found at that directory, ``VCSError`` is raised. - - :param search_path_up: if set to ``True``, this function would try to - move up to parent directory every time no scm is recognized for the - currently checked path. Default: ``False``. - :param explicit_alias: can be one of available backend aliases, when given - it will return given explicit alias in repositories under more than one - version control, if explicit_alias is different than found it will raise - VCSError """ if not os.path.isdir(path): raise VCSError("Given path %s is not a directory" % path) - def get_scms(path): - return [(scm, path) for scm in get_scms_for_path(path)] - - found_scms = get_scms(path) - while not found_scms and search_path_up: - newpath = os.path.abspath(os.path.join(path, os.pardir)) - if newpath == path: - break - path = newpath - found_scms = get_scms(path) + found_scms = [(scm, path) for scm in get_scms_for_path(path)] if len(found_scms) > 1: - for scm in found_scms: - if scm[0] == explicit_alias: - return scm found = ', '.join((x[0] for x in found_scms)) raise VCSError( 'More than one [%s] scm found at given path %s' % (found, path)) diff --git a/rhodecode/login/views.py b/rhodecode/login/views.py --- a/rhodecode/login/views.py +++ b/rhodecode/login/views.py @@ -327,8 +327,11 @@ class LoginView(object): if self.request.GET and self.request.GET.get('key'): try: user = User.get_by_auth_token(self.request.GET.get('key')) + password_reset_url = self.request.route_url( + 'reset_password_confirmation', + _query={'key': user.api_key}) data = {'email': user.email} - UserModel().reset_password(data) + UserModel().reset_password(data, password_reset_url) self.session.flash( _('Your password reset was successful, ' 'a new password has been sent to your email'), diff --git a/rhodecode/model/__init__.py b/rhodecode/model/__init__.py --- a/rhodecode/model/__init__.py +++ b/rhodecode/model/__init__.py @@ -145,17 +145,6 @@ class BaseModel(object): return self._get_instance( db.Permission, permission, callback=db.Permission.get_by_key) - def send_event(self, event): - """ - Helper method to send an event. This wraps the pyramid logic to send an - event. - """ - # For the first step we are using pyramids thread locals here. If the - # event mechanism works out as a good solution we should think about - # passing the registry into the constructor to get rid of it. - registry = get_current_registry() - registry.notify(event) - @classmethod def get_all(cls): """ diff --git a/rhodecode/model/comment.py b/rhodecode/model/comment.py --- a/rhodecode/model/comment.py +++ b/rhodecode/model/comment.py @@ -26,10 +26,15 @@ import logging import traceback import collections +from datetime import datetime + +from pylons.i18n.translation import _ +from pyramid.threadlocal import get_current_registry from sqlalchemy.sql.expression import null from sqlalchemy.sql.functions import coalesce from rhodecode.lib import helpers as h, diffs +from rhodecode.lib.channelstream import channelstream_request from rhodecode.lib.utils import action_logger from rhodecode.lib.utils2 import extract_mentioned_users from rhodecode.model import BaseModel @@ -77,7 +82,8 @@ class ChangesetCommentsModel(BaseModel): return global_renderer def create(self, text, repo, user, revision=None, pull_request=None, - f_path=None, line_no=None, status_change=None, closing_pr=False, + f_path=None, line_no=None, status_change=None, + status_change_type=None, closing_pr=False, send_email=True, renderer=None): """ Creates new comment for commit or pull request. @@ -91,7 +97,8 @@ class ChangesetCommentsModel(BaseModel): :param pull_request: :param f_path: :param line_no: - :param status_change: + :param status_change: Label for status change + :param status_change_type: type of status change :param closing_pr: :param send_email: """ @@ -134,89 +141,83 @@ class ChangesetCommentsModel(BaseModel): Session().add(comment) Session().flush() - - if send_email: - kwargs = { - 'user': user, - 'renderer_type': renderer, - 'repo_name': repo.repo_name, - 'status_change': status_change, - 'comment_body': text, - 'comment_file': f_path, - 'comment_line': line_no, - } + kwargs = { + 'user': user, + 'renderer_type': renderer, + 'repo_name': repo.repo_name, + 'status_change': status_change, + 'status_change_type': status_change_type, + 'comment_body': text, + 'comment_file': f_path, + 'comment_line': line_no, + } - if commit_obj: - recipients = ChangesetComment.get_users( - revision=commit_obj.raw_id) - # add commit author if it's in RhodeCode system - cs_author = User.get_from_cs_author(commit_obj.author) - if not cs_author: - # use repo owner if we cannot extract the author correctly - cs_author = repo.user - recipients += [cs_author] + if commit_obj: + recipients = ChangesetComment.get_users( + revision=commit_obj.raw_id) + # add commit author if it's in RhodeCode system + cs_author = User.get_from_cs_author(commit_obj.author) + if not cs_author: + # use repo owner if we cannot extract the author correctly + cs_author = repo.user + recipients += [cs_author] + + commit_comment_url = self.get_url(comment) - commit_comment_url = h.url( - 'changeset_home', - repo_name=repo.repo_name, - revision=commit_obj.raw_id, - anchor='comment-%s' % comment.comment_id, - qualified=True,) + target_repo_url = h.link_to( + repo.repo_name, + h.url('summary_home', + repo_name=repo.repo_name, qualified=True)) - target_repo_url = h.link_to( - repo.repo_name, - h.url('summary_home', - repo_name=repo.repo_name, qualified=True)) - - # commit specifics - kwargs.update({ - 'commit': commit_obj, - 'commit_message': commit_obj.message, - 'commit_target_repo': target_repo_url, - 'commit_comment_url': commit_comment_url, - }) + # commit specifics + kwargs.update({ + 'commit': commit_obj, + 'commit_message': commit_obj.message, + 'commit_target_repo': target_repo_url, + 'commit_comment_url': commit_comment_url, + }) - elif pull_request_obj: - # get the current participants of this pull request - recipients = ChangesetComment.get_users( - pull_request_id=pull_request_obj.pull_request_id) - # add pull request author - recipients += [pull_request_obj.author] + elif pull_request_obj: + # get the current participants of this pull request + recipients = ChangesetComment.get_users( + pull_request_id=pull_request_obj.pull_request_id) + # add pull request author + recipients += [pull_request_obj.author] - # add the reviewers to notification - recipients += [x.user for x in pull_request_obj.reviewers] + # add the reviewers to notification + recipients += [x.user for x in pull_request_obj.reviewers] - pr_target_repo = pull_request_obj.target_repo - pr_source_repo = pull_request_obj.source_repo + pr_target_repo = pull_request_obj.target_repo + pr_source_repo = pull_request_obj.source_repo - pr_comment_url = h.url( - 'pullrequest_show', - repo_name=pr_target_repo.repo_name, - pull_request_id=pull_request_obj.pull_request_id, - anchor='comment-%s' % comment.comment_id, - qualified=True,) + pr_comment_url = h.url( + 'pullrequest_show', + repo_name=pr_target_repo.repo_name, + pull_request_id=pull_request_obj.pull_request_id, + anchor='comment-%s' % comment.comment_id, + qualified=True,) - # set some variables for email notification - pr_target_repo_url = h.url( - 'summary_home', repo_name=pr_target_repo.repo_name, - qualified=True) + # set some variables for email notification + pr_target_repo_url = h.url( + 'summary_home', repo_name=pr_target_repo.repo_name, + qualified=True) - pr_source_repo_url = h.url( - 'summary_home', repo_name=pr_source_repo.repo_name, - qualified=True) + pr_source_repo_url = h.url( + 'summary_home', repo_name=pr_source_repo.repo_name, + qualified=True) - # pull request specifics - kwargs.update({ - 'pull_request': pull_request_obj, - 'pr_id': pull_request_obj.pull_request_id, - 'pr_target_repo': pr_target_repo, - 'pr_target_repo_url': pr_target_repo_url, - 'pr_source_repo': pr_source_repo, - 'pr_source_repo_url': pr_source_repo_url, - 'pr_comment_url': pr_comment_url, - 'pr_closing': closing_pr, - }) - + # pull request specifics + kwargs.update({ + 'pull_request': pull_request_obj, + 'pr_id': pull_request_obj.pull_request_id, + 'pr_target_repo': pr_target_repo, + 'pr_target_repo_url': pr_target_repo_url, + 'pr_source_repo': pr_source_repo, + 'pr_source_repo_url': pr_source_repo_url, + 'pr_comment_url': pr_comment_url, + 'pr_closing': closing_pr, + }) + if send_email: # pre-generate the subject for notification itself (subject, _h, _e, # we don't care about those @@ -245,6 +246,44 @@ class ChangesetCommentsModel(BaseModel): ) action_logger(user, action, comment.repo) + registry = get_current_registry() + rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {}) + channelstream_config = rhodecode_plugins.get('channelstream', {}) + msg_url = '' + if commit_obj: + msg_url = commit_comment_url + repo_name = repo.repo_name + elif pull_request_obj: + msg_url = pr_comment_url + repo_name = pr_target_repo.repo_name + + if channelstream_config.get('enabled'): + message = '{} {} - ' \ + '' \ + '{}' + message = message.format( + user.username, _('made a comment'), msg_url, + _('Show it now')) + channel = '/repo${}$/pr/{}'.format( + repo_name, + pull_request_id + ) + payload = { + 'type': 'message', + 'timestamp': datetime.utcnow(), + 'user': 'system', + 'exclude_users': [user.username], + 'channel': channel, + 'message': { + 'message': message, + 'level': 'info', + 'topic': '/notifications' + } + } + channelstream_request(channelstream_config, [payload], + '/message', raise_exc=False) + return comment def delete(self, comment): @@ -271,6 +310,23 @@ class ChangesetCommentsModel(BaseModel): q = q.order_by(ChangesetComment.created_on) return q.all() + def get_url(self, comment): + comment = self.__get_commit_comment(comment) + if comment.pull_request: + return h.url( + 'pullrequest_show', + repo_name=comment.pull_request.target_repo.repo_name, + pull_request_id=comment.pull_request.pull_request_id, + anchor='comment-%s' % comment.comment_id, + qualified=True,) + else: + return h.url( + 'changeset_home', + repo_name=comment.repo.repo_name, + revision=comment.revision, + anchor='comment-%s' % comment.comment_id, + qualified=True,) + def get_comments(self, repo_id, revision=None, pull_request=None): """ Gets main comments based on revision or pull_request_id diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -49,7 +49,7 @@ from zope.cachedescriptors.property impo from pylons import url from pylons.i18n.translation import lazy_ugettext as _ -from rhodecode.lib.vcs import get_backend +from rhodecode.lib.vcs import get_backend, get_vcs_instance from rhodecode.lib.vcs.utils.helpers import get_scm from rhodecode.lib.vcs.exceptions import VCSError from rhodecode.lib.vcs.backends.base import ( @@ -57,6 +57,7 @@ from rhodecode.lib.vcs.backends.base imp from rhodecode.lib.utils2 import ( str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe, time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict) +from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict from rhodecode.lib.ext_json import json from rhodecode.lib.caching_query import FromCache from rhodecode.lib.encrypt import AESCipher @@ -720,7 +721,7 @@ class User(Base, BaseModel): if cache: q = q.options(FromCache("sql_cache_short", - "get_email_key_%s" % email)) + "get_email_key_%s" % _hash_key(email))) ret = q.scalar() if ret is None: @@ -908,7 +909,7 @@ class UserApiKeys(Base, BaseModel): return { cls.ROLE_ALL: _('all'), cls.ROLE_HTTP: _('http/web interface'), - cls.ROLE_VCS: _('vcs (git/hg protocol)'), + cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'), cls.ROLE_API: _('api calls'), cls.ROLE_FEED: _('feed access'), }.get(role, role) @@ -1330,6 +1331,8 @@ class Repository(Base, BaseModel): cascade="all, delete, delete-orphan") ui = relationship('RepoRhodeCodeUi', cascade="all") settings = relationship('RepoRhodeCodeSetting', cascade="all") + integrations = relationship('Integration', + cascade="all, delete, delete-orphan") def __unicode__(self): return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id, @@ -1641,6 +1644,7 @@ class Repository(Base, BaseModel): 'repo_name': repo.repo_name, 'repo_type': repo.repo_type, 'clone_uri': repo.clone_uri or '', + 'url': url('summary_home', repo_name=self.repo_name, qualified=True), 'private': repo.private, 'created_on': repo.created_on, 'description': repo.description, @@ -1861,7 +1865,8 @@ class Repository(Base, BaseModel): cs_cache = cs_cache.__json__() def is_outdated(new_cs_cache): - if new_cs_cache['raw_id'] != self.changeset_cache['raw_id']: + if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or + new_cs_cache['revision'] != self.changeset_cache['revision']): return True return False @@ -1972,7 +1977,7 @@ class Repository(Base, BaseModel): return self._get_instance() invalidator_context = CacheKey.repo_context_cache( - _get_repo, self.repo_name, None) + _get_repo, self.repo_name, None, thread_scoped=True) with invalidator_context as context: context.invalidate() @@ -1981,27 +1986,16 @@ class Repository(Base, BaseModel): return repo def _get_instance(self, cache=True, config=None): - repo_full_path = self.repo_full_path - try: - vcs_alias = get_scm(repo_full_path)[0] - log.debug( - 'Creating instance of %s repository from %s', - vcs_alias, repo_full_path) - backend = get_backend(vcs_alias) - except VCSError: - log.exception( - 'Perhaps this repository is in db and not in ' - 'filesystem run rescan repositories with ' - '"destroy old data" option from admin panel') - return - config = config or self._config custom_wire = { 'cache': cache # controls the vcs.remote cache } - repo = backend( - safe_str(repo_full_path), config=config, create=False, - with_wire=custom_wire) + + repo = get_vcs_instance( + repo_path=safe_str(self.repo_full_path), + config=config, + with_wire=custom_wire, + create=False) return repo @@ -2854,7 +2848,8 @@ class CacheKey(Base, BaseModel): return None @classmethod - def repo_context_cache(cls, compute_func, repo_name, cache_type): + def repo_context_cache(cls, compute_func, repo_name, cache_type, + thread_scoped=False): """ @cache_region('long_term') def _heavy_calculation(cache_key): @@ -2870,7 +2865,8 @@ class CacheKey(Base, BaseModel): assert computed == 'result' """ from rhodecode.lib import caches - return caches.InvalidationContext(compute_func, repo_name, cache_type) + return caches.InvalidationContext( + compute_func, repo_name, cache_type, thread_scoped=thread_scoped) class ChangesetComment(Base, BaseModel): @@ -3111,10 +3107,9 @@ class PullRequest(Base, _PullRequestBase merge_status = PullRequestModel().merge_status(pull_request) data = { 'pull_request_id': pull_request.pull_request_id, - 'url': url('pullrequest_show', - repo_name=pull_request.target_repo.repo_name, - pull_request_id=pull_request.pull_request_id, - qualified=True), + 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name, + pull_request_id=self.pull_request_id, + qualified=True), 'title': pull_request.title, 'description': pull_request.description, 'status': pull_request.status, @@ -3395,10 +3390,9 @@ class Gist(Base, BaseModel): # SCM functions def scm_instance(self, **kwargs): - from rhodecode.lib.vcs import get_repo - base_path = self.base_path() - return get_repo(os.path.join(*map(safe_str, - [base_path, self.gist_access_id]))) + full_repo_path = os.path.join(self.base_path(), self.gist_access_id) + return get_vcs_instance( + repo_path=safe_str(full_repo_path), create=False) class DbMigrateVersion(Base, BaseModel): @@ -3474,3 +3468,32 @@ class ExternalIdentity(Base, BaseModel): query = cls.query() query = query.filter(cls.local_user_id == local_user_id) return query + + +class Integration(Base, BaseModel): + __tablename__ = 'integrations' + __table_args__ = ( + {'extend_existing': True, 'mysql_engine': 'InnoDB', + 'mysql_charset': 'utf8', 'sqlite_autoincrement': True} + ) + + integration_id = Column('integration_id', Integer(), primary_key=True) + integration_type = Column('integration_type', String(255)) + enabled = Column('enabled', Boolean(), nullable=False) + name = Column('name', String(255), nullable=False) + + settings = Column( + 'settings_json', MutationObj.as_mutable( + JsonType(dialect_map=dict(mysql=UnicodeText(16384))))) + repo_id = Column( + 'repo_id', Integer(), ForeignKey('repositories.repo_id'), + nullable=True, unique=None, default=None) + repo = relationship('Repository', lazy='joined') + + def __repr__(self): + if self.repo: + scope = 'repo=%r' % self.repo + else: + scope = 'global' + + return '' % (self.integration_type, scope) diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -41,19 +41,38 @@ for SELECT use formencode.All(OneOf(list """ +import deform import logging +import formencode -import formencode +from pkg_resources import resource_filename from formencode import All, Pipe from pylons.i18n.translation import _ from rhodecode import BACKENDS +from rhodecode.lib import helpers from rhodecode.model import validators as v log = logging.getLogger(__name__) +deform_templates = resource_filename('deform', 'templates') +rhodecode_templates = resource_filename('rhodecode', 'templates/forms') +search_path = (rhodecode_templates, deform_templates) + + +class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory): + """ Subclass of ZPTRendererFactory to add rhodecode context variables """ + def __call__(self, template_name, **kw): + kw['h'] = helpers + return self.load(template_name)(**kw) + + +form_renderer = RhodecodeFormZPTRendererFactory(search_path) +deform.Form.set_default_renderer(form_renderer) + + def LoginForm(): class _LoginForm(formencode.Schema): allow_extra_fields = True @@ -382,6 +401,7 @@ class _BaseVcsSettingsForm(formencode.Sc rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False) rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False) + rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False) def ApplicationUiSettingsForm(): @@ -415,7 +435,6 @@ def LabsSettingsForm(): allow_extra_fields = True filter_extra_fields = False - rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False) rhodecode_proxy_subversion_http_requests = v.StringBoolean( if_missing=False) rhodecode_subversion_http_server_url = v.UnicodeString( @@ -536,23 +555,6 @@ def PullRequestForm(repo_id): return _PullRequestForm -def GistForm(lifetime_options, acl_level_options): - class _GistForm(formencode.Schema): - - gistid = All(v.UniqGistId(), v.UnicodeString(strip=True, min=3, not_empty=False, if_missing=None)) - filename = All(v.BasePath()(), - v.UnicodeString(strip=True, required=False)) - description = v.UnicodeString(required=False, if_missing=u'') - lifetime = v.OneOf(lifetime_options) - mimetype = v.UnicodeString(required=False, if_missing=None) - content = v.UnicodeString(required=True, not_empty=True) - public = v.UnicodeString(required=False, if_missing=u'') - private = v.UnicodeString(required=False, if_missing=u'') - acl_level = v.OneOf(acl_level_options) - - return _GistForm - - def IssueTrackerPatternsForm(): class _IssueTrackerPatternsForm(formencode.Schema): allow_extra_fields = True diff --git a/rhodecode/model/gist.py b/rhodecode/model/gist.py --- a/rhodecode/model/gist.py +++ b/rhodecode/model/gist.py @@ -107,7 +107,7 @@ class GistModel(BaseModel): :param description: description of the gist :param owner: user who created this gist - :param gist_mapping: mapping {filename:{'content':content},...} + :param gist_mapping: mapping [{'filename': 'file1.txt', 'content': content}, ...}] :param gist_type: type of gist private/public :param lifetime: in minutes, -1 == forever :param gist_acl_level: acl level for this gist @@ -141,25 +141,10 @@ class GistModel(BaseModel): repo_name=gist_id, repo_type='hg', repo_group=GIST_STORE_LOC, use_global_config=True) - processed_mapping = {} - for filename in gist_mapping: - if filename != os.path.basename(filename): - raise Exception('Filename cannot be inside a directory') - - content = gist_mapping[filename]['content'] - # TODO: expand support for setting explicit lexers -# if lexer is None: -# try: -# guess_lexer = pygments.lexers.guess_lexer_for_filename -# lexer = guess_lexer(filename,content) -# except pygments.util.ClassNotFound: -# lexer = 'text' - processed_mapping[filename] = {'content': content} - # now create single multifile commit message = 'added file' - message += 's: ' if len(processed_mapping) > 1 else ': ' - message += ', '.join([x for x in processed_mapping]) + message += 's: ' if len(gist_mapping) > 1 else ': ' + message += ', '.join([x for x in gist_mapping]) # fake RhodeCode Repository object fake_repo = AttributeDict({ @@ -170,7 +155,7 @@ class GistModel(BaseModel): ScmModel().create_nodes( user=owner.user_id, repo=fake_repo, message=message, - nodes=processed_mapping, + nodes=gist_mapping, trigger_push_hook=False ) @@ -196,7 +181,6 @@ class GistModel(BaseModel): gist = self._get_gist(gist) gist_repo = gist.scm_instance() - lifetime = safe_int(lifetime, -1) if lifetime == 0: # preserve old value gist_expires = gist.gist_expires else: @@ -207,9 +191,9 @@ class GistModel(BaseModel): gist_mapping_op = {} for k, v in gist_mapping.items(): # add, mod, del - if not v['org_filename'] and v['filename']: + if not v['filename_org'] and v['filename']: op = 'add' - elif v['org_filename'] and not v['filename']: + elif v['filename_org'] and not v['filename']: op = 'del' else: op = 'mod' diff --git a/rhodecode/model/integration.py b/rhodecode/model/integration.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/integration.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +""" +Model for integrations +""" + + +import logging +import traceback + +from pylons import tmpl_context as c +from pylons.i18n.translation import _, ungettext +from sqlalchemy import or_ +from sqlalchemy.sql.expression import false, true +from mako import exceptions + +import rhodecode +from rhodecode import events +from rhodecode.lib import helpers as h +from rhodecode.lib.caching_query import FromCache +from rhodecode.lib.utils import PartialRenderer +from rhodecode.model import BaseModel +from rhodecode.model.db import Integration, User +from rhodecode.model.meta import Session +from rhodecode.integrations import integration_type_registry +from rhodecode.integrations.types.base import IntegrationTypeBase + +log = logging.getLogger(__name__) + + +class IntegrationModel(BaseModel): + + cls = Integration + + def __get_integration(self, integration): + if isinstance(integration, Integration): + return integration + elif isinstance(integration, (int, long)): + return self.sa.query(Integration).get(integration) + else: + if integration: + raise Exception('integration must be int, long or Instance' + ' of Integration got %s' % type(integration)) + + def create(self, IntegrationType, enabled, name, settings, repo=None): + """ Create an IntegrationType integration """ + integration = Integration() + integration.integration_type = IntegrationType.key + integration.settings = {} + integration.repo = repo + integration.enabled = enabled + integration.name = name + + self.sa.add(integration) + self.sa.commit() + return integration + + def delete(self, integration): + try: + integration = self.__get_integration(integration) + if integration: + self.sa.delete(integration) + return True + except Exception: + log.error(traceback.format_exc()) + raise + return False + + def get_integration_handler(self, integration): + TypeClass = integration_type_registry.get(integration.integration_type) + if not TypeClass: + log.error('No class could be found for integration type: {}'.format( + integration.integration_type)) + return None + + return TypeClass(integration.settings) + + def send_event(self, integration, event): + """ Send an event to an integration """ + handler = self.get_integration_handler(integration) + if handler: + handler.send_event(event) + + def get_integrations(self, repo=None): + if repo: + return self.sa.query(Integration).filter( + Integration.repo_id==repo.repo_id).all() + + # global integrations + return self.sa.query(Integration).filter( + Integration.repo_id==None).all() + + def get_for_event(self, event, cache=False): + """ + Get integrations that match an event + """ + query = self.sa.query(Integration).filter(Integration.enabled==True) + + if isinstance(event, events.RepoEvent): # global + repo integrations + query = query.filter( + or_(Integration.repo_id==None, + Integration.repo_id==event.repo.repo_id)) + if cache: + query = query.options(FromCache( + "sql_cache_short", + "get_enabled_repo_integrations_%i" % event.repo.repo_id)) + else: # only global integrations + query = query.filter(Integration.repo_id==None) + if cache: + query = query.options(FromCache( + "sql_cache_short", "get_enabled_global_integrations")) + + return query.all() diff --git a/rhodecode/model/notification.py b/rhodecode/model/notification.py --- a/rhodecode/model/notification.py +++ b/rhodecode/model/notification.py @@ -327,7 +327,8 @@ class EmailNotificationModel(BaseModel): :return: """ _kwargs = { - 'instance_url': h.url('home', qualified=True) + 'instance_url': h.url('home', qualified=True), + 'rhodecode_instance_name': getattr(c, 'rhodecode_name', '') } _kwargs.update(kwargs) return _kwargs @@ -339,7 +340,7 @@ class EmailNotificationModel(BaseModel): def render_email(self, type_, **kwargs): """ renders template for email, and returns a tuple of - (subject, email_headers, email_body) + (subject, email_headers, email_html_body, email_plaintext_body) """ # translator and helpers inject _kwargs = self._update_kwargs_for_render(kwargs) diff --git a/rhodecode/model/pull_request.py b/rhodecode/model/pull_request.py --- a/rhodecode/model/pull_request.py +++ b/rhodecode/model/pull_request.py @@ -31,7 +31,6 @@ import datetime from pylons.i18n.translation import _ from pylons.i18n.translation import lazy_ugettext -import rhodecode from rhodecode.lib import helpers as h, hooks_utils, diffs from rhodecode.lib.compat import OrderedDict from rhodecode.lib.hooks_daemon import prepare_callback_daemon @@ -41,13 +40,14 @@ from rhodecode.lib.utils import action_l from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe from rhodecode.lib.vcs.backends.base import ( Reference, MergeResponse, MergeFailureReason) +from rhodecode.lib.vcs.conf import settings as vcs_settings from rhodecode.lib.vcs.exceptions import ( CommitDoesNotExistError, EmptyRepositoryError) from rhodecode.model import BaseModel from rhodecode.model.changeset_status import ChangesetStatusModel from rhodecode.model.comment import ChangesetCommentsModel from rhodecode.model.db import ( - PullRequest, PullRequestReviewers, Notification, ChangesetStatus, + PullRequest, PullRequestReviewers, ChangesetStatus, PullRequestVersion, ChangesetComment) from rhodecode.model.meta import Session from rhodecode.model.notification import NotificationModel, \ @@ -423,11 +423,11 @@ class PullRequestModel(BaseModel): } workspace_id = self._workspace_id(pull_request) - protocol = rhodecode.CONFIG.get('vcs.hooks.protocol') - use_direct_calls = rhodecode.CONFIG.get('vcs.hooks.direct_calls') + use_rebase = self._use_rebase_for_merging(pull_request) callback_daemon, extras = prepare_callback_daemon( - extras, protocol=protocol, use_direct_calls=use_direct_calls) + extras, protocol=vcs_settings.HOOKS_PROTOCOL, + use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS) with callback_daemon: # TODO: johbo: Implement a clean way to run a config_override @@ -437,7 +437,7 @@ class PullRequestModel(BaseModel): merge_state = target_vcs.merge( target_ref, source_vcs, pull_request.source_ref_parts, workspace_id, user_name=user.username, - user_email=user.email, message=message) + user_email=user.email, message=message, use_rebase=use_rebase) return merge_state def _comment_and_close_pr(self, pull_request, user, merge_state): @@ -747,6 +747,12 @@ class PullRequestModel(BaseModel): return ids_to_add, ids_to_remove + def get_url(self, pull_request): + return h.url('pullrequest_show', + repo_name=safe_str(pull_request.target_repo.repo_name), + pull_request_id=pull_request.pull_request_id, + qualified=True) + def notify_reviewers(self, pull_request, reviewers_ids): # notification to reviewers if not reviewers_ids: @@ -847,6 +853,7 @@ class PullRequestModel(BaseModel): f_path=None, line_no=None, status_change=ChangesetStatus.get_status_lbl(status), + status_change_type=status, closing_pr=True ) @@ -955,9 +962,10 @@ class PullRequestModel(BaseModel): def _refresh_merge_state(self, pull_request, target_vcs, target_reference): workspace_id = self._workspace_id(pull_request) source_vcs = pull_request.source_repo.scm_instance() + use_rebase = self._use_rebase_for_merging(pull_request) merge_state = target_vcs.merge( target_reference, source_vcs, pull_request.source_ref_parts, - workspace_id, dry_run=True) + workspace_id, dry_run=True, use_rebase=use_rebase) # Do not store the response if there was an unknown error. if merge_state.failure_reason != MergeFailureReason.UNKNOWN: @@ -1126,6 +1134,11 @@ class PullRequestModel(BaseModel): settings = settings_model.get_general_settings() return settings.get('rhodecode_pr_merge_enabled', False) + def _use_rebase_for_merging(self, pull_request): + settings_model = VcsSettingsModel(repo=pull_request.target_repo) + settings = settings_model.get_general_settings() + return settings.get('rhodecode_hg_use_rebase_for_merging', False) + def _log_action(self, action, user, pull_request): action_logger( user, diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -34,6 +34,7 @@ from sqlalchemy.sql import func from sqlalchemy.sql.expression import true, or_ from zope.cachedescriptors.property import Lazy as LazyProperty +from rhodecode import events from rhodecode.lib import helpers as h from rhodecode.lib.auth import HasUserGroupPermissionAny from rhodecode.lib.caching_query import FromCache @@ -140,6 +141,10 @@ class RepoModel(BaseModel): return None + def get_url(self, repo): + return h.url('summary_home', repo_name=safe_str(repo.repo_name), + qualified=True) + def get_users(self, name_contains=None, limit=20, only_active=True): # TODO: mikhail: move this method to the UserModel. query = self.sa.query(User) @@ -470,6 +475,8 @@ class RepoModel(BaseModel): parent_repo = fork_of new_repo.fork = parent_repo + events.trigger(events.RepoPreCreateEvent(new_repo)) + self.sa.add(new_repo) EMPTY_PERM = 'repository.none' @@ -525,11 +532,13 @@ class RepoModel(BaseModel): # now automatically start following this repository as owner ScmModel(self.sa).toggle_following_repo(new_repo.repo_id, owner.user_id) + # we need to flush here, in order to check if database won't # throw any exceptions, create filesystem dirs at the very end self.sa.flush() + events.trigger(events.RepoCreateEvent(new_repo)) + return new_repo - return new_repo except Exception: log.error(traceback.format_exc()) raise @@ -633,6 +642,7 @@ class RepoModel(BaseModel): raise AttachedForksError() old_repo_dict = repo.get_dict() + events.trigger(events.RepoPreDeleteEvent(repo)) try: self.sa.delete(repo) if fs_remove: @@ -644,6 +654,7 @@ class RepoModel(BaseModel): 'deleted_on': time.time(), }) log_delete_repository(**old_repo_dict) + events.trigger(events.RepoDeleteEvent(repo)) except Exception: log.error(traceback.format_exc()) raise diff --git a/rhodecode/model/repo_group.py b/rhodecode/model/repo_group.py --- a/rhodecode/model/repo_group.py +++ b/rhodecode/model/repo_group.py @@ -33,6 +33,7 @@ import traceback from zope.cachedescriptors.property import Lazy as LazyProperty +from rhodecode import events from rhodecode.model import BaseModel from rhodecode.model.db import ( RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm, @@ -257,6 +258,9 @@ class RepoGroupModel(BaseModel): log_create_repository_group( created_by=user.username, **repo_group.get_dict()) + # Trigger create event. + events.trigger(events.RepoGroupCreateEvent(repo_group)) + return new_repo_group except Exception: self.sa.rollback() @@ -455,6 +459,9 @@ class RepoGroupModel(BaseModel): self._rename_group(old_path, new_path) + # Trigger update event. + events.trigger(events.RepoGroupUpdateEvent(repo_group)) + return repo_group except Exception: log.error(traceback.format_exc()) @@ -469,6 +476,9 @@ class RepoGroupModel(BaseModel): else: log.debug('skipping removal from filesystem') + # Trigger delete event. + events.trigger(events.RepoGroupDeleteEvent(repo_group)) + except Exception: log.error('Error removing repo_group %s', repo_group) raise diff --git a/rhodecode/model/scm.py b/rhodecode/model/scm.py --- a/rhodecode/model/scm.py +++ b/rhodecode/model/scm.py @@ -279,11 +279,7 @@ class ScmModel(BaseModel): if repo: config = repo._config config.set('extensions', 'largefiles', '') - cs_cache = None - if delete: - # if we do a hard clear, reset last-commit to Empty - cs_cache = EmptyCommit() - repo.update_commit_cache(config=config, cs_cache=cs_cache) + repo.update_commit_cache(config=config, cs_cache=None) caches.clear_repo_caches(repo_name) def toggle_following_repo(self, follow_repo_id, user_id): @@ -482,7 +478,7 @@ class ScmModel(BaseModel): return data def get_nodes(self, repo_name, commit_id, root_path='/', flat=True, - extended_info=False, content=False): + extended_info=False, content=False, max_file_bytes=None): """ recursive walk in root dir and return a set of all path in that dir based on repository walk function @@ -490,7 +486,8 @@ class ScmModel(BaseModel): :param repo_name: name of repository :param commit_id: commit id for which to list nodes :param root_path: root path to list - :param flat: return as a list, if False returns a dict with decription + :param flat: return as a list, if False returns a dict with description + :param max_file_bytes: will not return file contents over this limit """ _files = list() @@ -503,31 +500,31 @@ class ScmModel(BaseModel): for f in files: _content = None _data = f.unicode_path + over_size_limit = (max_file_bytes is not None + and f.size > max_file_bytes) + if not flat: _data = { "name": f.unicode_path, "type": "file", } if extended_info: - _content = safe_str(f.content) _data.update({ - "md5": md5(_content), + "md5": f.md5, "binary": f.is_binary, "size": f.size, "extension": f.extension, - "mimetype": f.mimetype, "lines": f.lines()[0] }) + if content: full_content = None - if not f.is_binary: - # in case we loaded the _content already - # re-use it, or load from f[ile] - full_content = _content or safe_str(f.content) + if not f.is_binary and not over_size_limit: + full_content = safe_str(f.content) _data.update({ - "content": full_content + "content": full_content, }) _files.append(_data) for d in dirs: diff --git a/rhodecode/model/settings.py b/rhodecode/model/settings.py --- a/rhodecode/model/settings.py +++ b/rhodecode/model/settings.py @@ -198,7 +198,7 @@ class SettingsModel(BaseModel): # update if set res.app_settings_value = val - Session.add(res) + Session().add(res) return res def invalidate_settings_cache(self): @@ -401,7 +401,9 @@ class IssueTrackerSettingsModel(object): class VcsSettingsModel(object): INHERIT_SETTINGS = 'inherit_vcs_settings' - GENERAL_SETTINGS = ('use_outdated_comments', 'pr_merge_enabled') + GENERAL_SETTINGS = ( + 'use_outdated_comments', 'pr_merge_enabled', + 'hg_use_rebase_for_merging') HOOKS_SETTINGS = ( ('hooks', 'changegroup.repo_size'), ('hooks', 'changegroup.push_logger'), diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -32,7 +32,7 @@ import ipaddress from sqlalchemy.exc import DatabaseError from sqlalchemy.sql.expression import true, false -from rhodecode.events import UserPreCreate, UserPreUpdate +from rhodecode import events from rhodecode.lib.utils2 import ( safe_unicode, get_current_rhodecode_user, action_logger_generic, AttributeDict) @@ -270,12 +270,12 @@ class UserModel(BaseModel): # raises UserCreationError if it's not allowed for any reason to # create new active user, this also executes pre-create hooks check_allowed_create_user(user_data, cur_user, strict_check=True) - self.send_event(UserPreCreate(user_data)) + events.trigger(events.UserPreCreate(user_data)) new_user = User() edit = False else: log.debug('updating user %s', username) - self.send_event(UserPreUpdate(user, user_data)) + events.trigger(events.UserPreUpdate(user, user_data)) new_user = user edit = True @@ -532,7 +532,7 @@ class UserModel(BaseModel): return True - def reset_password(self, data): + def reset_password(self, data, pwd_reset_url): from rhodecode.lib.celerylib import tasks, run_task from rhodecode.model.notification import EmailNotificationModel from rhodecode.lib import auth @@ -557,6 +557,7 @@ class UserModel(BaseModel): email_kwargs = { 'new_password': new_passwd, + 'password_reset_url': pwd_reset_url, 'user': user, 'email': user_email, 'date': datetime.datetime.now() diff --git a/rhodecode/model/validation_schema.py b/rhodecode/model/validation_schema.py deleted file mode 100644 --- a/rhodecode/model/validation_schema.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2016-2016 RhodeCode GmbH -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License, version 3 -# (only), as published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -# This program is dual-licensed. If you wish to learn more about the -# RhodeCode Enterprise Edition, including its added features, Support services, -# and proprietary license terms, please see https://rhodecode.com/licenses/ - -import colander -from colander import Invalid # noqa - - -class GroupNameType(colander.String): - SEPARATOR = '/' - - def deserialize(self, node, cstruct): - result = super(GroupNameType, self).deserialize(node, cstruct) - return self._replace_extra_slashes(result) - - def _replace_extra_slashes(self, path): - path = path.split(self.SEPARATOR) - path = [item for item in path if item] - return self.SEPARATOR.join(path) - - -class RepoGroupSchema(colander.Schema): - group_name = colander.SchemaNode(GroupNameType()) - - -class RepoSchema(colander.Schema): - repo_name = colander.SchemaNode(GroupNameType()) - - -class SearchParamsSchema(colander.MappingSchema): - search_query = colander.SchemaNode( - colander.String(), - missing='') - search_type = colander.SchemaNode( - colander.String(), - missing='content', - validator=colander.OneOf(['content', 'path', 'commit', 'repository'])) - search_sort = colander.SchemaNode( - colander.String(), - missing='newfirst', - validator=colander.OneOf( - ['oldfirst', 'newfirst'])) - page_limit = colander.SchemaNode( - colander.Integer(), - missing=10, - validator=colander.Range(1, 500)) - requested_page = colander.SchemaNode( - colander.Integer(), - missing=1) - diff --git a/rhodecode/model/validation_schema/__init__.py b/rhodecode/model/validation_schema/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/validation_schema/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import colander + +from colander import Invalid # noqa, don't remove this + diff --git a/rhodecode/model/validation_schema/preparers.py b/rhodecode/model/validation_schema/preparers.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/validation_schema/preparers.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import unicodedata + + + +def strip_preparer(value): + """ + strips given values using .strip() function + """ + + if value: + value = value.strip() + return value + + +def slugify_preparer(value): + """ + Slugify given value to a safe representation for url/id + """ + from rhodecode.lib.utils import repo_name_slug + if value: + value = repo_name_slug(value.lower()) + return value + + +def non_ascii_strip_preparer(value): + """ + trie to replace non-ascii letters to their ascii representation + eg:: + + `żołw` converts into `zolw` + """ + if value: + value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') + return value + + +def unique_list_preparer(value): + """ + Converts an list to a list with only unique values + """ + + def make_unique(value): + seen = [] + return [c for c in value if + not (c in seen or seen.append(c))] + + if isinstance(value, list): + ret_val = make_unique(value) + elif isinstance(value, set): + ret_val = list(value) + elif isinstance(value, tuple): + ret_val = make_unique(value) + elif value is None: + ret_val = [] + else: + ret_val = [value] + + return ret_val + + +def unique_list_from_str_preparer(value): + """ + Converts an list to a list with only unique values + """ + from rhodecode.lib.utils2 import aslist + + if isinstance(value, basestring): + value = aslist(value, ',') + return unique_list_preparer(value) \ No newline at end of file diff --git a/rhodecode/model/validation_schema/schemas/__init__.py b/rhodecode/model/validation_schema/schemas/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/validation_schema/schemas/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +""" +Colander Schema nodes +http://docs.pylonsproject.org/projects/colander/en/latest/basics.html#schema-node-objects +""" + diff --git a/rhodecode/model/validation_schema/schemas/gist_schema.py b/rhodecode/model/validation_schema/schemas/gist_schema.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/validation_schema/schemas/gist_schema.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import os + +import colander + +from rhodecode.translation import _ +from rhodecode.model.validation_schema import validators, preparers + + +def nodes_to_sequence(nodes, colander_node=None): + """ + Converts old style dict nodes to new list of dicts + + :param nodes: dict with key beeing name of the file + + """ + if not isinstance(nodes, dict): + msg = 'Nodes needs to be a dict, got {}'.format(type(nodes)) + raise colander.Invalid(colander_node, msg) + out = [] + + for key, val in nodes.items(): + val = (isinstance(val, dict) and val) or {} + out.append(dict( + filename=key, + content=val.get('content'), + mimetype=val.get('mimetype') + )) + + out = Nodes().deserialize(out) + return out + + +def sequence_to_nodes(nodes, colander_node=None): + if not isinstance(nodes, list): + msg = 'Nodes needs to be a list, got {}'.format(type(nodes)) + raise colander.Invalid(colander_node, msg) + nodes = Nodes().deserialize(nodes) + + out = {} + try: + for file_data in nodes: + file_data_skip = file_data.copy() + # if we got filename_org we use it as a key so we keep old + # name as input and rename is-reflected inside the values as + # filename and filename_org differences. + filename_org = file_data.get('filename_org') + filename = filename_org or file_data['filename'] + out[filename] = {} + out[filename].update(file_data_skip) + + except Exception as e: + msg = 'Invalid data format org_exc:`{}`'.format(repr(e)) + raise colander.Invalid(colander_node, msg) + return out + + +@colander.deferred +def deferred_lifetime_validator(node, kw): + options = kw.get('lifetime_options', []) + return colander.All( + colander.Range(min=-1, max=60 * 24 * 30 * 12), + colander.OneOf([x for x in options])) + + +def unique_gist_validator(node, value): + from rhodecode.model.db import Gist + existing = Gist.get_by_access_id(value) + if existing: + msg = _(u'Gist with name {} already exists').format(value) + raise colander.Invalid(node, msg) + + +def filename_validator(node, value): + if value != os.path.basename(value): + msg = _(u'Filename {} cannot be inside a directory').format(value) + raise colander.Invalid(node, msg) + + +class NodeSchema(colander.MappingSchema): + # if we perform rename this will be org filename + filename_org = colander.SchemaNode( + colander.String(), + preparer=[preparers.strip_preparer, + preparers.non_ascii_strip_preparer], + validator=filename_validator, + missing=None) + + filename = colander.SchemaNode( + colander.String(), + preparer=[preparers.strip_preparer, + preparers.non_ascii_strip_preparer], + validator=filename_validator) + + content = colander.SchemaNode( + colander.String()) + mimetype = colander.SchemaNode( + colander.String(), + missing=None) + + +class Nodes(colander.SequenceSchema): + filenames = NodeSchema() + + def validator(self, node, cstruct): + if not isinstance(cstruct, list): + return + + found_filenames = [] + for data in cstruct: + filename = data['filename'] + if filename in found_filenames: + msg = _('Duplicated value for filename found: `{}`').format( + filename) + raise colander.Invalid(node, msg) + found_filenames.append(filename) + + +class GistSchema(colander.MappingSchema): + """ + schema = GistSchema() + schema.bind( + lifetime_options = [1,2,3] + ) + out = schema.deserialize(dict( + nodes=[ + {'filename': 'x', 'content': 'xxx', }, + {'filename': 'docs/Z', 'content': 'xxx', 'mimetype': 'x'}, + ] + )) + """ + + from rhodecode.model.db import Gist + + gistid = colander.SchemaNode( + colander.String(), + missing=None, + preparer=[preparers.strip_preparer, + preparers.non_ascii_strip_preparer, + preparers.slugify_preparer], + validator=colander.All( + colander.Length(min=3), + unique_gist_validator + )) + + description = colander.SchemaNode( + colander.String(), + missing=u'') + + lifetime = colander.SchemaNode( + colander.Integer(), + validator=deferred_lifetime_validator) + + gist_acl_level = colander.SchemaNode( + colander.String(), + validator=colander.OneOf([Gist.ACL_LEVEL_PUBLIC, + Gist.ACL_LEVEL_PRIVATE])) + + gist_type = colander.SchemaNode( + colander.String(), + missing=Gist.ACL_LEVEL_PUBLIC, + validator=colander.OneOf([Gist.GIST_PRIVATE, Gist.GIST_PUBLIC])) + + nodes = Nodes() + + diff --git a/rhodecode/model/validation_schema/schemas/repo_group_schema.py b/rhodecode/model/validation_schema/schemas/repo_group_schema.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/validation_schema/schemas/repo_group_schema.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +import colander + + +from rhodecode.model.validation_schema import validators, preparers, types + + +class RepoGroupSchema(colander.Schema): + group_name = colander.SchemaNode(types.GroupNameType()) diff --git a/rhodecode/model/validation_schema/schemas/repo_schema.py b/rhodecode/model/validation_schema/schemas/repo_schema.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/validation_schema/schemas/repo_schema.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import colander + +from rhodecode.model.validation_schema import validators, preparers, types + + +class RepoSchema(colander.Schema): + repo_name = colander.SchemaNode(types.GroupNameType()) diff --git a/rhodecode/model/validation_schema/schemas/search_schema.py b/rhodecode/model/validation_schema/schemas/search_schema.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/validation_schema/schemas/search_schema.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +import colander + + +class SearchParamsSchema(colander.MappingSchema): + search_query = colander.SchemaNode( + colander.String(), + missing='') + search_type = colander.SchemaNode( + colander.String(), + missing='content', + validator=colander.OneOf(['content', 'path', 'commit', 'repository'])) + search_sort = colander.SchemaNode( + colander.String(), + missing='newfirst', + validator=colander.OneOf( + ['oldfirst', 'newfirst'])) + page_limit = colander.SchemaNode( + colander.Integer(), + missing=10, + validator=colander.Range(1, 500)) + requested_page = colander.SchemaNode( + colander.Integer(), + missing=1) diff --git a/rhodecode/model/validation_schema/types.py b/rhodecode/model/validation_schema/types.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/validation_schema/types.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import colander + + +class GroupNameType(colander.String): + SEPARATOR = '/' + + def deserialize(self, node, cstruct): + result = super(GroupNameType, self).deserialize(node, cstruct) + return self._replace_extra_slashes(result) + + def _replace_extra_slashes(self, path): + path = path.split(self.SEPARATOR) + path = [item for item in path if item] + return self.SEPARATOR.join(path) diff --git a/rhodecode/model/validation_schema/validators.py b/rhodecode/model/validation_schema/validators.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/validation_schema/validators.py @@ -0,0 +1,19 @@ +import os + +import ipaddress +import colander + +from rhodecode.translation import _ + + +def ip_addr_validator(node, value): + try: + # this raises an ValueError if address is not IpV4 or IpV6 + ipaddress.ip_network(value, strict=False) + except ValueError: + msg = _(u'Please enter a valid IPv4 or IpV6 address') + raise colander.Invalid(node, msg) + + + + diff --git a/rhodecode/model/validators.py b/rhodecode/model/validators.py --- a/rhodecode/model/validators.py +++ b/rhodecode/model/validators.py @@ -970,22 +970,6 @@ def FieldKey(): return _validator -def BasePath(): - class _validator(formencode.validators.FancyValidator): - messages = { - 'badPath': _(u'Filename cannot be inside a directory'), - } - - def _to_python(self, value, state): - return value - - def validate_python(self, value, state): - if value != os.path.basename(value): - raise formencode.Invalid(self.message('badPath', state), - value, state) - return _validator - - def ValidAuthPlugins(): class _validator(formencode.validators.FancyValidator): messages = { @@ -1061,26 +1045,6 @@ def ValidAuthPlugins(): return _validator -def UniqGistId(): - class _validator(formencode.validators.FancyValidator): - messages = { - 'gistid_taken': _(u'This gistid is already in use') - } - - def _to_python(self, value, state): - return repo_name_slug(value.lower()) - - def validate_python(self, value, state): - existing = Gist.get_by_access_id(value) - if existing: - msg = M(self, 'gistid_taken', state) - raise formencode.Invalid( - msg, value, state, error_dict={'gistid': msg} - ) - - return _validator - - def ValidPattern(): class _Validator(formencode.validators.FancyValidator): diff --git a/rhodecode/public/502.html b/rhodecode/public/502.html new file mode 100644 --- /dev/null +++ b/rhodecode/public/502.html @@ -0,0 +1,44 @@ + + + + Error - 502 Bad Gateway + + + + + + + + + + + +
+
+

+ 502 Bad Gateway | Backend server is unreachable +

+
+

Possible Cause

+
    +
  • The server is beeing restarted.
  • +
  • The server is overloaded.
  • +
  • The link may be incorrect.
  • +
+
+
+

Support

+

For support, go to Support. + It may be useful to include your log file; see the log file locations here. +

+
+
+

Documentation

+

For more information, see docs.rhodecode.com.

+
+
+
+ + + + diff --git a/rhodecode/public/css/code-block.less b/rhodecode/public/css/code-block.less --- a/rhodecode/public/css/code-block.less +++ b/rhodecode/public/css/code-block.less @@ -494,16 +494,33 @@ div.codeblock { } .code-highlighttable, -div.codeblock .code-body table { - width: 0 !important; - border: 0px !important; - margin: 0; - letter-spacing: normal; +div.codeblock { + + &.readme { + background-color: white; + } + + .markdown-block table { + border-collapse: collapse; + th, + td { + padding: .5em !important; + border: @border-thickness solid @border-default-color !important; + } + } - td { + table { + width: 0 !important; border: 0px !important; - vertical-align: top; + margin: 0; + letter-spacing: normal; + + + td { + border: 0px !important; + vertical-align: top; + } } } diff --git a/rhodecode/public/css/codemirror.less b/rhodecode/public/css/codemirror.less --- a/rhodecode/public/css/codemirror.less +++ b/rhodecode/public/css/codemirror.less @@ -165,7 +165,7 @@ div.CodeMirror span.CodeMirror-nonmatchi } /* The fake, visible scrollbars. Used to force redraw during scrolling - before actuall scrolling happens, thus preventing shaking and + before actual scrolling happens, thus preventing shaking and flickering artifacts. */ .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { position: absolute; @@ -207,6 +207,11 @@ div.CodeMirror span.CodeMirror-nonmatchi z-index: 4; height: 100%; } +.CodeMirror-gutter-background { + position: absolute; + top: 0; bottom: 0; + z-index: 4; +} .CodeMirror-gutter-elt { position: absolute; cursor: default; @@ -280,7 +285,7 @@ div.CodeMirror span.CodeMirror-nonmatchi overflow: hidden; visibility: hidden; } -.CodeMirror-measure pre { position: static; } + .CodeMirror div.CodeMirror-cursor { position: absolute; @@ -288,11 +293,17 @@ div.CodeMirror span.CodeMirror-nonmatchi width: 0; } +.CodeMirror-measure pre { position: static; } + div.CodeMirror-cursors { visibility: hidden; position: relative; z-index: 3; } +div.CodeMirror-dragcursors { + visibility: visible; +} + .CodeMirror-focused div.CodeMirror-cursors { visibility: visible; } @@ -300,8 +311,8 @@ div.CodeMirror-cursors { .CodeMirror-selected { background: #d9d9d9; } .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } .CodeMirror-crosshair { cursor: crosshair; } -.CodeMirror ::selection { background: #d7d4f0; } -.CodeMirror ::-moz-selection { background: #d7d4f0; } +.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } +.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } .cm-searching { background: #ffa; diff --git a/rhodecode/public/css/deform.less b/rhodecode/public/css/deform.less new file mode 100644 --- /dev/null +++ b/rhodecode/public/css/deform.less @@ -0,0 +1,91 @@ +.deform { + + * { + box-sizing: border-box; + } + + .required:after { + color: #e32; + content: '*'; + display:inline; + } + + .control-label { + width: 200px; + float: left; + } + .control-inputs { + width: 400px; + float: left; + } + .form-group .radio, .form-group .checkbox { + position: relative; + display: block; + /* margin-bottom: 10px; */ + } + + .form-group { + clear: left; + } + + .form-control { + width: 100%; + } + + .error-block { + color: red; + } + + .deform-seq-container .control-inputs { + width: 100%; + } + + .deform-seq-container .deform-seq-item-handle { + width: 8.3%; + float: left; + } + + .deform-seq-container .deform-seq-item-group { + width: 91.6%; + float: left; + } + + .form-control { + input { + height: 40px; + } + input[type=checkbox], input[type=radio] { + height: auto; + } + select { + height: 40px; + } + } + + .form-control.select2-container { height: 40px; } + + .deform-two-field-sequence .deform-seq-container .deform-seq-item label { + display: none; + } + .deform-two-field-sequence .deform-seq-container .deform-seq-item:first-child label { + display: block; + } + .deform-two-field-sequence .deform-seq-container .deform-seq-item .panel-heading { + display: none; + } + .deform-two-field-sequence .deform-seq-container .deform-seq-item.form-group { + background: red; + } + .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group .form-group { + width: 45%; padding: 0 2px; float: left; clear: none; + } + .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel { + padding: 0; + margin: 5px 0; + border: none; + } + .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel > .panel-body { + padding: 0; + } + +} diff --git a/rhodecode/public/css/legacy_code_styles.less b/rhodecode/public/css/legacy_code_styles.less --- a/rhodecode/public/css/legacy_code_styles.less +++ b/rhodecode/public/css/legacy_code_styles.less @@ -177,8 +177,7 @@ div.markdown-block p, div.markdown-block blockquote, div.markdown-block dl, div.markdown-block li, -div.markdown-block table, -div.markdown-block pre { +div.markdown-block table { margin: 15px 0 !important; margin: 3px 0px 13px 0px !important; color: #424242 !important; @@ -189,6 +188,17 @@ div.markdown-block pre { line-height: 140% !important; } +div.markdown-block pre { + margin: 15px 0 !important; + margin: 3px 0px 13px 0px !important; + padding: .5em; + color: #424242 !important; + font-size: 13px !important; + overflow: visible !important; + line-height: 140% !important; + background-color: @grey7; +} + div.markdown-block img { padding: 4px; border: @border-thickness solid @grey5; @@ -265,7 +275,8 @@ div.markdown-block code { div.markdown-block pre { border: @border-thickness solid @grey5; overflow: auto; - padding: 4px 8px; + padding: .5em; + background-color: @grey7; } div.markdown-block pre > code { diff --git a/rhodecode/public/css/main.less b/rhodecode/public/css/main.less --- a/rhodecode/public/css/main.less +++ b/rhodecode/public/css/main.less @@ -25,6 +25,8 @@ @import 'comments'; @import 'panels-bootstrap'; @import 'panels'; +@import 'toastr'; +@import 'deform'; //--- BASE ------------------// @@ -141,7 +143,7 @@ input.inline[type="file"] { h1 { color: @grey2; } - + .error-branding { font-family: @text-semibold; color: @grey4; @@ -310,13 +312,19 @@ ul.auth_plugins { .td-status { padding-left: .5em; } - .truncate { + .log-container .truncate { height: 2.75em; white-space: pre-line; } table.rctable .user { padding-left: 0; } + table.rctable { + td.td-description, + .rc-user { + min-width: auto; + } + } } // Pull Requests @@ -670,6 +678,13 @@ label { } } +.user-inline-data { + display: inline-block; + float: left; + padding-left: .5em; + line-height: 1.3em; +} + .rc-user { // gravatar + user wrapper float: left; position: relative; @@ -1009,9 +1024,9 @@ label { padding: .9em; color: @grey3; background-color: @grey7; - border-right: @border-thickness solid @border-default-color; - border-bottom: @border-thickness solid @border-default-color; - border-left: @border-thickness solid @border-default-color; + border-right: @border-thickness solid @border-default-color; + border-bottom: @border-thickness solid @border-default-color; + border-left: @border-thickness solid @border-default-color; } #repo_vcs_settings { @@ -1601,6 +1616,10 @@ BIN_FILENODE = 7 float: right; } +#notification-status{ + display: inline; +} + // Repositories #summary.fields{ @@ -1864,15 +1883,6 @@ h3.files_location{ } } -.file_author{ - margin-bottom: @padding; - - div{ - display: inline-block; - margin-right: 0.5em; - } -} - .browser-cur-rev{ margin-bottom: @textmargin; } @@ -1949,7 +1959,7 @@ div.search-feedback-items { padding:0px 0px 0px 96px; } -div.search-code-body { +div.search-code-body { background-color: #ffffff; padding: 5px 0 5px 10px; pre { .match { background-color: #faffa6;} diff --git a/rhodecode/public/css/mergerly.css b/rhodecode/public/css/mergerly.css --- a/rhodecode/public/css/mergerly.css +++ b/rhodecode/public/css/mergerly.css @@ -1,4 +1,3 @@ - /* required */ .mergely-column textarea { width: 80px; height: 200px; } .mergely-column { float: left; } @@ -12,17 +11,19 @@ .mergely-column { border: 1px solid #ccc; } .mergely-active { border: 1px solid #a3d1ff; } -.mergely.a.rhs.start { border-top: 1px solid #ddffdd; } -.mergely.a.lhs.start.end, -.mergely.a.rhs.end { border-bottom: 1px solid #ddffdd; } -.mergely.a.rhs { background-color: #ddffdd; } -.mergely.a.lhs.start.end.first { border-bottom: 0; border-top: 1px solid #ddffdd; } +.mergely.a,.mergely.d,.mergely.c { color: #000; } -.mergely.d.lhs { background-color: #edc0c0; } +.mergely.a.rhs.start { border-top: 1px solid #a3d1ff; } +.mergely.a.lhs.start.end, +.mergely.a.rhs.end { border-bottom: 1px solid #a3d1ff; } +.mergely.a.rhs { background-color: #ddeeff; } +.mergely.a.lhs.start.end.first { border-bottom: 0; border-top: 1px solid #a3d1ff; } + +.mergely.d.lhs { background-color: #ffe9e9; } .mergely.d.lhs.end, -.mergely.d.rhs.start.end { border-bottom: 1px solid #ffdddd; } -.mergely.d.rhs.start.end.first { border-bottom: 0; border-top: 1px solid #ffdddd; } -.mergely.d.lhs.start { border-top: 1px solid #ffdddd; } +.mergely.d.rhs.start.end { border-bottom: 1px solid #f8e8e8; } +.mergely.d.rhs.start.end.first { border-bottom: 0; border-top: 1px solid #f8e8e8; } +.mergely.d.lhs.start { border-top: 1px solid #f8e8e8; } .mergely.c.lhs, .mergely.c.rhs { background-color: #fafafa; } @@ -31,11 +32,19 @@ .mergely.c.lhs.end, .mergely.c.rhs.end { border-bottom: 1px solid #a3a3a3; } -.mergely.ch.a.rhs { background-color: #ddffdd; } -.mergely.ch.d.lhs { background-color: #ffdddd; } - +.mergely.ch.a.rhs { background-color: #ddeeff; } +.mergely.ch.d.lhs { background-color: #ffe9e9; text-decoration: line-through; color: red !important; } .mergely-margin #compare-lhs-margin, .mergely-margin #compare-rhs-margin { cursor: pointer } + +.mergely.current.start { border-top: 1px solid #000 !important; } +.mergely.current.end { border-bottom: 1px solid #000 !important; } +.mergely.current.lhs.a.start.end, +.mergely.current.rhs.d.start.end { border-top: 0 !important; } +.mergely.current.CodeMirror-linenumber { color: #F9F9F9; font-weight: bold; background-color: #777; } + +.CodeMirror-linenumber { cursor: pointer; } +.CodeMirror-code { color: #717171; } \ No newline at end of file diff --git a/rhodecode/public/css/summary.less b/rhodecode/public/css/summary.less --- a/rhodecode/public/css/summary.less +++ b/rhodecode/public/css/summary.less @@ -138,6 +138,10 @@ .summary .sidebar-right-content { margin-bottom: @space; + + .rc-user { + min-width: 0; + } } .fieldset { @@ -168,6 +172,10 @@ overflow-x: auto; } } + .commit.truncate-wrap { + overflow:hidden; + text-overflow: ellipsis; + } } // expand commit message @@ -254,15 +262,3 @@ } -#readme { - width: 100%; - - .readme { - overflow-x: auto; - border: @border-thickness solid @border-default-color; - .border-radius(@border-radius); - } - .readme_box { - margin: 15px; - } -} diff --git a/rhodecode/public/css/tables.less b/rhodecode/public/css/tables.less --- a/rhodecode/public/css/tables.less +++ b/rhodecode/public/css/tables.less @@ -123,11 +123,6 @@ table.dataTable { } } - &.td-gravatar { - width: 16px; - padding: 0 5px; - } - &.tags-col { padding-right: 0; } diff --git a/rhodecode/public/css/toastr.less b/rhodecode/public/css/toastr.less new file mode 100644 --- /dev/null +++ b/rhodecode/public/css/toastr.less @@ -0,0 +1,268 @@ +// Mix-ins +.borderRadius(@radius) { + -moz-border-radius: @radius; + -webkit-border-radius: @radius; + border-radius: @radius; +} + +.boxShadow(@boxShadow) { + -moz-box-shadow: @boxShadow; + -webkit-box-shadow: @boxShadow; + box-shadow: @boxShadow; +} + +.opacity(@opacity) { + @opacityPercent: @opacity * 100; + opacity: @opacity; + -ms-filter: ~"progid:DXImageTransform.Microsoft.Alpha(Opacity=@{opacityPercent})"; + filter: ~"alpha(opacity=@{opacityPercent})"; +} + +.wordWrap(@wordWrap: break-word) { + -ms-word-wrap: @wordWrap; + word-wrap: @wordWrap; +} + +// Variables +@black: #000000; +@grey: #999999; +@light-grey: #CCCCCC; +@white: #FFFFFF; +@near-black: #030303; +@green: #51A351; +@red: #BD362F; +@blue: #2F96B4; +@orange: #F89406; +@default-container-opacity: .8; + +// Styles +.toast-title { + font-weight: bold; +} + +.toast-message { + .wordWrap(); + + a, + label { + color: @near-black; + } + + a:hover { + color: @light-grey; + text-decoration: none; + } +} + +.toast-close-button { + position: relative; + right: -0.3em; + top: -0.3em; + float: right; + font-size: 20px; + font-weight: bold; + color: @black; + -webkit-text-shadow: 0 1px 0 rgba(255,255,255,1); + text-shadow: 0 1px 0 rgba(255,255,255,1); + .opacity(0.8); + + &:hover, + &:focus { + color: @black; + text-decoration: none; + cursor: pointer; + .opacity(0.4); + } +} + +/*Additional properties for button version + iOS requires the button element instead of an anchor tag. + If you want the anchor version, it requires `href="#"`.*/ +button.toast-close-button { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +//#endregion + +.toast-top-center { + top: 0; + right: 0; + width: 100%; +} + +.toast-bottom-center { + bottom: 0; + right: 0; + width: 100%; +} + +.toast-top-full-width { + top: 0; + right: 0; + width: 100%; +} + +.toast-bottom-full-width { + bottom: 0; + right: 0; + width: 100%; +} + +.toast-top-left { + top: 12px; + left: 12px; +} + +.toast-top-right { + top: 12px; + right: 12px; +} + +.toast-bottom-right { + right: 12px; + bottom: 12px; +} + +.toast-bottom-left { + bottom: 12px; + left: 12px; +} + +#toast-container { + position: fixed; + z-index: 999999; + // The container should not be clickable. + pointer-events: none; + * { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + } + + > div { + position: relative; + // The toast itself should be clickable. + pointer-events: auto; + overflow: hidden; + margin: 0 0 6px; + padding: 15px; + width: 300px; + .borderRadius(1px 1px 1px 1px); + background-position: 15px center; + background-repeat: no-repeat; + color: @near-black; + .opacity(@default-container-opacity); + } + + > :hover { + .opacity(1); + cursor: pointer; + } + + > .toast-info { + //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important; + } + + > .toast-error { + //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important; + } + + > .toast-success { + //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important; + } + + > .toast-warning { + //background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important; + } + + /*overrides*/ + &.toast-top-center > div, + &.toast-bottom-center > div { + width: 400px; + margin-left: auto; + margin-right: auto; + } + + &.toast-top-full-width > div, + &.toast-bottom-full-width > div { + width: 96%; + margin-left: auto; + margin-right: auto; + } +} + +.toast { + border-color: @near-black; + border-style: solid; + border-width: 2px 2px 2px 25px; + background-color: @white; +} + +.toast-success { + border-color: @green; +} + +.toast-error { + border-color: @red; +} + +.toast-info { + border-color: @blue; +} + +.toast-warning { + border-color: @orange; +} + +.toast-progress { + position: absolute; + left: 0; + bottom: 0; + height: 4px; + background-color: @black; + .opacity(0.4); +} + +/*Responsive Design*/ + +@media all and (max-width: 240px) { + #toast-container { + + > div { + padding: 8px; + width: 11em; + } + + & .toast-close-button { + right: -0.2em; + top: -0.2em; + } + } +} + +@media all and (min-width: 241px) and (max-width: 480px) { + #toast-container { + > div { + padding: 8px; + width: 18em; + } + + & .toast-close-button { + right: -0.2em; + top: -0.2em; + } + } +} + +@media all and (min-width: 481px) and (max-width: 768px) { + #toast-container { + > div { + padding: 15px; + width: 25em; + } + } +} diff --git a/rhodecode/public/js/mergerly.js b/rhodecode/public/js/mergerly.js --- a/rhodecode/public/js/mergerly.js +++ b/rhodecode/public/js/mergerly.js @@ -297,15 +297,15 @@ jQuery.extend(Mgly.diff.prototype, { }, _optimize: function(ctx) { var start = 0, end = 0; - while (start < ctx.length) { - while ((start < ctx.length) && (ctx.modified[start] == undefined || ctx.modified[start] == false)) { + while (start < ctx.codes.length) { + while ((start < ctx.codes.length) && (ctx.modified[start] == undefined || ctx.modified[start] == false)) { start++; } end = start; - while ((end < ctx.length) && (ctx.modified[end] == true)) { + while ((end < ctx.codes.length) && (ctx.modified[end] == true)) { end++; } - if ((end < ctx.length) && (ctx.ctx[start] == ctx.codes[end])) { + if ((end < ctx.codes.length) && (ctx.codes[start] == ctx.codes[end])) { ctx.modified[start] = false; ctx.modified[end] = true; } @@ -438,8 +438,7 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr if (this.resized) this.resized(); }, _debug: '', //scroll,draw,calc,diff,markup,change - resized: function() { }, - finished: function () { } + resized: function() { } }; var cmsettings = { mode: 'text/plain', @@ -497,7 +496,7 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr if (direction == 'next') { this._current_diff = Math.min(++this._current_diff, this.changes.length - 1); } - else { + else if (direction == 'prev') { this._current_diff = Math.max(--this._current_diff, 0); } this._scroll_to_change(this.changes[this._current_diff]); @@ -540,10 +539,10 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr if (this.settings.hasOwnProperty('sidebar')) { // dynamically enable sidebars if (this.settings.sidebar) { - jQuery(this.element).find('.mergely-margin').css({display: 'block'}); + this.element.find('.mergely-margin').css({display: 'block'}); } else { - jQuery(this.element).find('.mergely-margin').css({display: 'none'}); + this.element.find('.mergely-margin').css({display: 'none'}); } } var le, re; @@ -690,12 +689,12 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr jQuery('').appendTo('head'); //bind - var rhstx = jQuery('#' + this.id + '-rhs').get(0); + var rhstx = this.element.find('#' + this.id + '-rhs').get(0); if (!rhstx) { console.error('rhs textarea not defined - Mergely not initialized properly'); return; } - var lhstx = jQuery('#' + this.id + '-lhs').get(0); + var lhstx = this.element.find('#' + this.id + '-lhs').get(0); if (!rhstx) { console.error('lhs textarea not defined - Mergely not initialized properly'); return; @@ -728,6 +727,38 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr ); sz(true); } + + // scrollToDiff() from gutter + function gutterClicked(side, line, ev) { + // The "Merge left/right" buttons are also located in the gutter. + // Don't interfere with them: + if (ev.target && (jQuery(ev.target).closest('.merge-button').length > 0)) { + return; + } + + // See if the user clicked the line number of a difference: + var i, change; + for (i = 0; i < this.changes.length; i++) { + change = this.changes[i]; + if (line >= change[side+'-line-from'] && line <= change[side+'-line-to']) { + this._current_diff = i; + // I really don't like this here - something about gutterClick does not + // like mutating editor here. Need to trigger the scroll to diff from + // a timeout. + setTimeout(function() { this.scrollToDiff(); }.bind(this), 10); + break; + } + } + } + + this.editor[this.id + '-lhs'].on('gutterClick', function(cm, n, gutterClass, ev) { + gutterClicked.call(this, 'lhs', n, ev); + }.bind(this)); + + this.editor[this.id + '-rhs'].on('gutterClick', function(cm, n, gutterClass, ev) { + gutterClicked.call(this, 'rhs', n, ev); + }.bind(this)); + //bind var setv; if (this.settings.lhs) { @@ -745,23 +776,10 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr var self = this; var led = self.editor[self.id+'-lhs']; var red = self.editor[self.id+'-rhs']; - - var yref = led.getScrollerElement().offsetHeight * 0.5; // center between >0 and 1/2 - // set cursors led.setCursor(Math.max(change["lhs-line-from"],0), 0); // use led.getCursor().ch ? red.setCursor(Math.max(change["rhs-line-from"],0), 0); - - // using directly CodeMirror breaks canvas alignment - // var ly = led.charCoords({line: Math.max(change["lhs-line-from"],0), ch: 0}, "local").top; - - // calculate scroll offset for current change. Warning: returns relative y position so we scroll to 0 first. - led.scrollTo(null, 0); - red.scrollTo(null, 0); - self._calculate_offsets(self.id+'-lhs', self.id+'-rhs', [change]); - led.scrollTo(null, Math.max(change["lhs-y-start"]-yref, 0)); - red.scrollTo(null, Math.max(change["rhs-y-start"]-yref, 0)); - // right pane should simply follows + led.scrollIntoView({line: change["lhs-line-to"]}); }, _scrolling: function(editor_name) { @@ -922,13 +940,11 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr this.trace('change', 'diff time', timer.stop()); this.changes = Mgly.DiffParser(d.normal_form()); this.trace('change', 'parse time', timer.stop()); - if (this._current_diff === undefined && this.changes.length) { // go to first difference on start-up this._current_diff = 0; this._scroll_to_change(this.changes[0]); } - this.trace('change', 'scroll_to_change time', timer.stop()); this._calculate_offsets(editor_name1, editor_name2, this.changes); this.trace('change', 'offsets time', timer.stop()); @@ -992,7 +1008,7 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr // this is the distance from the top of the screen to the top of the // content of the first codemirror editor - var topnode = jQuery('#' + this.id + ' .CodeMirror-measure').first(); + var topnode = this.element.find('.CodeMirror-measure').first(); var top_offset = topnode.offset().top - 4; if(!top_offset) return false; @@ -1121,11 +1137,12 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr return changes; }, _markup_changes: function (editor_name1, editor_name2, changes) { - jQuery('.merge-button').remove(); // clear + this.element.find('.merge-button').remove(); //clear var self = this; var led = this.editor[editor_name1]; var red = this.editor[editor_name2]; + var current_diff = this._current_diff; var timer = new Mgly.Timer(); led.operation(function() { @@ -1140,6 +1157,12 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr led.addLineClass(llf, 'background', 'start'); led.addLineClass(llt, 'background', 'end'); + if (current_diff == i) { + if (llf != llt) { + led.addLineClass(llf, 'background', 'current'); + } + led.addLineClass(llt, 'background', 'current'); + } if (llf == 0 && llt == 0 && rlf == 0) { led.addLineClass(llf, 'background', clazz.join(' ')); led.addLineClass(llf, 'background', 'first'); @@ -1186,6 +1209,12 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr red.addLineClass(rlf, 'background', 'start'); red.addLineClass(rlt, 'background', 'end'); + if (current_diff == i) { + if (rlf != rlt) { + red.addLineClass(rlf, 'background', 'current'); + } + red.addLineClass(rlt, 'background', 'current'); + } if (rlf == 0 && rlt == 0 && llf == 0) { red.addLineClass(rlf, 'background', clazz.join(' ')); red.addLineClass(rlf, 'background', 'first'); @@ -1286,11 +1315,12 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr self.chfns[self.id + '-rhs'].push(m[0].markText(m[1], m[2], m[3])); } }); + this.trace('change', 'LCS markup time', timer.stop()); // merge buttons var ed = {lhs:led, rhs:red}; - jQuery('.merge-button').on('click', function(ev){ + this.element.find('.merge-button').on('click', function(ev){ // side of mouseenter var side = 'rhs'; var oside = 'lhs'; @@ -1314,6 +1344,35 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr self._merge_change(change, side, oside); return false; }); + + // gutter markup + var lhsLineNumbers = $('#mergely-lhs ~ .CodeMirror').find('.CodeMirror-linenumber'); + var rhsLineNumbers = $('#mergely-rhs ~ .CodeMirror').find('.CodeMirror-linenumber'); + rhsLineNumbers.removeClass('mergely current'); + lhsLineNumbers.removeClass('mergely current'); + for (var i = 0; i < changes.length; ++i) { + if (current_diff == i && change.op !== 'd') { + var change = changes[i]; + var j, jf = change['rhs-line-from'], jt = change['rhs-line-to'] + 1; + for (j = jf; j < jt; j++) { + var n = (j + 1).toString(); + rhsLineNumbers + .filter(function(i, node) { return $(node).text() === n; }) + .addClass('mergely current'); + } + } + if (current_diff == i && change.op !== 'a') { + var change = changes[i]; + jf = change['lhs-line-from'], jt = change['lhs-line-to'] + 1; + for (j = jf; j < jt; j++) { + var n = (j + 1).toString(); + lhsLineNumbers + .filter(function(i, node) { return $(node).text() === n; }) + .addClass('mergely current'); + } + } + } + this.trace('change', 'markup buttons time', timer.stop()); }, _merge_change : function(change, side, oside) { @@ -1373,8 +1432,8 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr var gutter_height = jQuery(this.editor[editor_name1].getScrollerElement()).children(':first-child').height(); var dcanvas = document.getElementById(editor_name1 + '-' + editor_name2 + '-canvas'); if (dcanvas == undefined) throw 'Failed to find: ' + editor_name1 + '-' + editor_name2 + '-canvas'; - var clhs = jQuery('#' + this.id + '-lhs-margin'); - var crhs = jQuery('#' + this.id + '-rhs-margin'); + var clhs = this.element.find('#' + this.id + '-lhs-margin'); + var crhs = this.element.find('#' + this.id + '-rhs-margin'); return { visible_page_height: visible_page_height, gutter_height: gutter_height, @@ -1405,7 +1464,7 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr this.trace('draw', 'lhs-scroller-top', ex.lhs_scroller.scrollTop()); this.trace('draw', 'rhs-scroller-top', ex.rhs_scroller.scrollTop()); - jQuery.each(jQuery.find('#' + this.id + ' canvas'), function () { + jQuery.each(this.element.find('canvas'), function () { jQuery(this).get(0).height = ex.visible_page_height; }); @@ -1427,6 +1486,10 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr var vp = this._get_viewport(editor_name1, editor_name2); for (var i = 0; i < changes.length; ++i) { var change = changes[i]; + var fill = this.settings.fgcolor[change['op']]; + if (this._current_diff==i) { + fill = '#000'; + } this.trace('draw', change); // margin indicators @@ -1437,14 +1500,14 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr this.trace('draw', 'marker calculated', lhs_y_start, lhs_y_end, rhs_y_start, rhs_y_end); ctx_lhs.beginPath(); - ctx_lhs.fillStyle = this.settings.fgcolor[(this._current_diff==i?'c':'')+change['op']]; + ctx_lhs.fillStyle = fill; ctx_lhs.strokeStyle = '#000'; ctx_lhs.lineWidth = 0.5; ctx_lhs.fillRect(1.5, lhs_y_start, 4.5, Math.max(lhs_y_end - lhs_y_start, 5)); ctx_lhs.strokeRect(1.5, lhs_y_start, 4.5, Math.max(lhs_y_end - lhs_y_start, 5)); ctx_rhs.beginPath(); - ctx_rhs.fillStyle = this.settings.fgcolor[(this._current_diff==i?'c':'')+change['op']]; + ctx_rhs.fillStyle = fill; ctx_rhs.strokeStyle = '#000'; ctx_rhs.lineWidth = 0.5; ctx_rhs.fillRect(1.5, rhs_y_start, 4.5, Math.max(rhs_y_end - rhs_y_start, 5)); @@ -1463,7 +1526,7 @@ jQuery.extend(Mgly.CodeMirrorDiffView.pr // draw left box ctx.beginPath(); - ctx.strokeStyle = this.settings.fgcolor[(this._current_diff==i?'c':'')+change['op']]; + ctx.strokeStyle = fill; ctx.lineWidth = (this._current_diff==i) ? 1.5 : 1; var rectWidth = this.draw_lhs_width; diff --git a/rhodecode/public/js/mode/clike/clike.js b/rhodecode/public/js/mode/clike/clike.js --- a/rhodecode/public/js/mode/clike/clike.js +++ b/rhodecode/public/js/mode/clike/clike.js @@ -25,8 +25,12 @@ CodeMirror.defineMode("clike", function( multiLineStrings = parserConfig.multiLineStrings, indentStatements = parserConfig.indentStatements !== false, indentSwitch = parserConfig.indentSwitch !== false, - namespaceSeparator = parserConfig.namespaceSeparator; - var isOperatorChar = /[+\-*&%=<>!?|\/]/; + namespaceSeparator = parserConfig.namespaceSeparator, + isPunctuationChar = parserConfig.isPunctuationChar || /[\[\]{}\(\),;\:\.]/, + numberStart = parserConfig.numberStart || /[\d\.]/, + number = parserConfig.number || /^(?:0x[a-f\d]+|0b[01]+|(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)(u|ll?|l|f)?/i, + isOperatorChar = parserConfig.isOperatorChar || /[+\-*&%=<>!?|\/]/, + endStatement = parserConfig.endStatement || /^[;:,]$/; var curPunc, isDefKeyword; @@ -40,13 +44,14 @@ CodeMirror.defineMode("clike", function( state.tokenize = tokenString(ch); return state.tokenize(stream, state); } - if (/[\[\]{}\(\),;\:\.]/.test(ch)) { + if (isPunctuationChar.test(ch)) { curPunc = ch; return null; } - if (/\d/.test(ch)) { - stream.eatWhile(/[\w\.]/); - return "number"; + if (numberStart.test(ch)) { + stream.backUp(1) + if (stream.match(number)) return "number" + stream.next() } if (ch == "/") { if (stream.eat("*")) { @@ -67,17 +72,17 @@ CodeMirror.defineMode("clike", function( stream.eatWhile(/[\w\$_\xa1-\uffff]/); var cur = stream.current(); - if (keywords.propertyIsEnumerable(cur)) { - if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement"; - if (defKeywords.propertyIsEnumerable(cur)) isDefKeyword = true; + if (contains(keywords, cur)) { + if (contains(blockKeywords, cur)) curPunc = "newstatement"; + if (contains(defKeywords, cur)) isDefKeyword = true; return "keyword"; } - if (types.propertyIsEnumerable(cur)) return "variable-3"; - if (builtin.propertyIsEnumerable(cur)) { - if (blockKeywords.propertyIsEnumerable(cur)) curPunc = "newstatement"; + if (contains(types, cur)) return "variable-3"; + if (contains(builtin, cur)) { + if (contains(blockKeywords, cur)) curPunc = "newstatement"; return "builtin"; } - if (atoms.propertyIsEnumerable(cur)) return "atom"; + if (contains(atoms, cur)) return "atom"; return "variable"; } @@ -168,8 +173,7 @@ CodeMirror.defineMode("clike", function( if (style == "comment" || style == "meta") return style; if (ctx.align == null) ctx.align = true; - if ((curPunc == ";" || curPunc == ":" || curPunc == ",")) - while (isStatement(state.context.type)) popContext(state); + if (endStatement.test(curPunc)) while (isStatement(state.context.type)) popContext(state); else if (curPunc == "{") pushContext(state, stream.column(), "}"); else if (curPunc == "[") pushContext(state, stream.column(), "]"); else if (curPunc == "(") pushContext(state, stream.column(), ")"); @@ -212,8 +216,16 @@ CodeMirror.defineMode("clike", function( if (state.tokenize != tokenBase && state.tokenize != null) return CodeMirror.Pass; var ctx = state.context, firstChar = textAfter && textAfter.charAt(0); if (isStatement(ctx.type) && firstChar == "}") ctx = ctx.prev; + if (hooks.indent) { + var hook = hooks.indent(state, ctx, textAfter); + if (typeof hook == "number") return hook + } var closing = firstChar == ctx.type; var switchBlock = ctx.prev && ctx.prev.type == "switchstatement"; + if (parserConfig.allmanIndentation && /[{(]/.test(firstChar)) { + while (ctx.type != "top" && ctx.type != "}") ctx = ctx.prev + return ctx.indented + } if (isStatement(ctx.type)) return ctx.indented + (firstChar == "{" ? 0 : statementIndentUnit); if (ctx.align && (!dontAlignCalls || ctx.type != ")")) @@ -238,27 +250,30 @@ CodeMirror.defineMode("clike", function( for (var i = 0; i < words.length; ++i) obj[words[i]] = true; return obj; } + function contains(words, word) { + if (typeof words === "function") { + return words(word); + } else { + return words.propertyIsEnumerable(word); + } + } var cKeywords = "auto if break case register continue return default do sizeof " + - "static else struct switch extern typedef float union for " + - "goto while enum const volatile"; + "static else struct switch extern typedef union for goto while enum const volatile"; var cTypes = "int long char short double float unsigned signed void size_t ptrdiff_t"; function cppHook(stream, state) { - if (!state.startOfLine) return false; - for (;;) { - if (stream.skipTo("\\")) { - stream.next(); - if (stream.eol()) { - state.tokenize = cppHook; - break; - } - } else { - stream.skipToEnd(); - state.tokenize = null; - break; + if (!state.startOfLine) return false + for (var ch, next = null; ch = stream.peek();) { + if (ch == "\\" && stream.match(/^.$/)) { + next = cppHook + break + } else if (ch == "/" && stream.match(/^\/[\/\*]/, false)) { + break } + stream.next() } - return "meta"; + state.tokenize = next + return "meta" } function pointerHook(_stream, state) { @@ -266,6 +281,11 @@ CodeMirror.defineMode("clike", function( return false; } + function cpp14Literal(stream) { + stream.eatWhile(/[\w\.']/); + return "number"; + } + function cpp11StringHook(stream, state) { stream.backUp(1); // Raw strings. @@ -373,6 +393,16 @@ CodeMirror.defineMode("clike", function( "U": cpp11StringHook, "L": cpp11StringHook, "R": cpp11StringHook, + "0": cpp14Literal, + "1": cpp14Literal, + "2": cpp14Literal, + "3": cpp14Literal, + "4": cpp14Literal, + "5": cpp14Literal, + "6": cpp14Literal, + "7": cpp14Literal, + "8": cpp14Literal, + "9": cpp14Literal, token: function(stream, state, style) { if (style == "variable" && stream.peek() == "(" && (state.prevToken == ";" || state.prevToken == null || @@ -398,6 +428,7 @@ CodeMirror.defineMode("clike", function( defKeywords: words("class interface package enum"), typeFirstDefinitions: true, atoms: words("true false null"), + endStatement: /^[;:]$/, hooks: { "@": function(stream) { stream.eatWhile(/[\w\$_]/); @@ -453,7 +484,7 @@ CodeMirror.defineMode("clike", function( keywords: words( /* scala */ - "abstract case catch class def do else extends false final finally for forSome if " + + "abstract case catch class def do else extends final finally for forSome if " + "implicit import lazy match new null object override package private protected return " + "sealed super this throw trait try type val var while with yield _ : = => <- <: " + "<% >: # @ " + @@ -501,6 +532,59 @@ CodeMirror.defineMode("clike", function( modeProps: {closeBrackets: {triples: '"'}} }); + function tokenKotlinString(tripleString){ + return function (stream, state) { + var escaped = false, next, end = false; + while (!stream.eol()) { + if (!tripleString && !escaped && stream.match('"') ) {end = true; break;} + if (tripleString && stream.match('"""')) {end = true; break;} + next = stream.next(); + if(!escaped && next == "$" && stream.match('{')) + stream.skipTo("}"); + escaped = !escaped && next == "\\" && !tripleString; + } + if (end || !tripleString) + state.tokenize = null; + return "string"; + } + } + + def("text/x-kotlin", { + name: "clike", + keywords: words( + /*keywords*/ + "package as typealias class interface this super val " + + "var fun for is in This throw return " + + "break continue object if else while do try when !in !is as? " + + + /*soft keywords*/ + "file import where by get set abstract enum open inner override private public internal " + + "protected catch finally out final vararg reified dynamic companion constructor init " + + "sealed field property receiver param sparam lateinit data inline noinline tailrec " + + "external annotation crossinline const operator infix" + ), + types: words( + /* package java.lang */ + "Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable " + + "Compiler Double Exception Float Integer Long Math Number Object Package Pair Process " + + "Runtime Runnable SecurityManager Short StackTraceElement StrictMath String " + + "StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void" + ), + intendSwitch: false, + indentStatements: false, + multiLineStrings: true, + blockKeywords: words("catch class do else finally for if where try while enum"), + defKeywords: words("class val var object package interface fun"), + atoms: words("true false null this"), + hooks: { + '"': function(stream, state) { + state.tokenize = tokenKotlinString(stream.match('""')); + return state.tokenize(stream, state); + } + }, + modeProps: {closeBrackets: {triples: '"'}} + }); + def(["x-shader/x-vertex", "x-shader/x-fragment"], { name: "clike", keywords: words("sampler1D sampler2D sampler3D samplerCube " + @@ -583,9 +667,106 @@ CodeMirror.defineMode("clike", function( stream.eatWhile(/[\w\$]/); return "keyword"; }, - "#": cppHook + "#": cppHook, + indent: function(_state, ctx, textAfter) { + if (ctx.type == "statement" && /^@\w/.test(textAfter)) return ctx.indented + } }, modeProps: {fold: "brace"} }); + def("text/x-squirrel", { + name: "clike", + keywords: words("base break clone continue const default delete enum extends function in class" + + " foreach local resume return this throw typeof yield constructor instanceof static"), + types: words(cTypes), + blockKeywords: words("case catch class else for foreach if switch try while"), + defKeywords: words("function local class"), + typeFirstDefinitions: true, + atoms: words("true false null"), + hooks: {"#": cppHook}, + modeProps: {fold: ["brace", "include"]} + }); + + // Ceylon Strings need to deal with interpolation + var stringTokenizer = null; + function tokenCeylonString(type) { + return function(stream, state) { + var escaped = false, next, end = false; + while (!stream.eol()) { + if (!escaped && stream.match('"') && + (type == "single" || stream.match('""'))) { + end = true; + break; + } + if (!escaped && stream.match('``')) { + stringTokenizer = tokenCeylonString(type); + end = true; + break; + } + next = stream.next(); + escaped = type == "single" && !escaped && next == "\\"; + } + if (end) + state.tokenize = null; + return "string"; + } + } + + def("text/x-ceylon", { + name: "clike", + keywords: words("abstracts alias assembly assert assign break case catch class continue dynamic else" + + " exists extends finally for function given if import in interface is let module new" + + " nonempty object of out outer package return satisfies super switch then this throw" + + " try value void while"), + types: function(word) { + // In Ceylon all identifiers that start with an uppercase are types + var first = word.charAt(0); + return (first === first.toUpperCase() && first !== first.toLowerCase()); + }, + blockKeywords: words("case catch class dynamic else finally for function if interface module new object switch try while"), + defKeywords: words("class dynamic function interface module object package value"), + builtin: words("abstract actual aliased annotation by default deprecated doc final formal late license" + + " native optional sealed see serializable shared suppressWarnings tagged throws variable"), + isPunctuationChar: /[\[\]{}\(\),;\:\.`]/, + isOperatorChar: /[+\-*&%=<>!?|^~:\/]/, + numberStart: /[\d#$]/, + number: /^(?:#[\da-fA-F_]+|\$[01_]+|[\d_]+[kMGTPmunpf]?|[\d_]+\.[\d_]+(?:[eE][-+]?\d+|[kMGTPmunpf]|)|)/i, + multiLineStrings: true, + typeFirstDefinitions: true, + atoms: words("true false null larger smaller equal empty finished"), + indentSwitch: false, + styleDefs: false, + hooks: { + "@": function(stream) { + stream.eatWhile(/[\w\$_]/); + return "meta"; + }, + '"': function(stream, state) { + state.tokenize = tokenCeylonString(stream.match('""') ? "triple" : "single"); + return state.tokenize(stream, state); + }, + '`': function(stream, state) { + if (!stringTokenizer || !stream.match('`')) return false; + state.tokenize = stringTokenizer; + stringTokenizer = null; + return state.tokenize(stream, state); + }, + "'": function(stream) { + stream.eatWhile(/[\w\$_\xa1-\uffff]/); + return "atom"; + }, + token: function(_stream, state, style) { + if ((style == "variable" || style == "variable-3") && + state.prevToken == ".") { + return "variable-2"; + } + } + }, + modeProps: { + fold: ["brace", "import"], + closeBrackets: {triples: '"'} + } + }); + }); diff --git a/rhodecode/public/js/mode/clojure/clojure.js b/rhodecode/public/js/mode/clojure/clojure.js --- a/rhodecode/public/js/mode/clojure/clojure.js +++ b/rhodecode/public/js/mode/clojure/clojure.js @@ -59,7 +59,8 @@ CodeMirror.defineMode("clojure", functio sign: /[+-]/, exponent: /e/i, keyword_char: /[^\s\(\[\;\)\]]/, - symbol: /[\w*+!\-\._?:<>\/\xa1-\uffff]/ + symbol: /[\w*+!\-\._?:<>\/\xa1-\uffff]/, + block_indent: /^(?:def|with)[^\/]+$|\/(?:def|with)/ }; function stateStack(indent, type, prev) { // represents a state stack object @@ -96,6 +97,9 @@ CodeMirror.defineMode("clojure", functio if ( '.' == stream.peek() ) { stream.eat('.'); stream.eatWhile(tests.digit); + } else if ('/' == stream.peek() ) { + stream.eat('/'); + stream.eatWhile(tests.digit); } if ( stream.eat(tests.exponent) ) { @@ -139,7 +143,7 @@ CodeMirror.defineMode("clojure", functio } // skip spaces - if (stream.eatSpace()) { + if (state.mode != "string" && stream.eatSpace()) { return null; } var returnType = null; @@ -187,7 +191,7 @@ CodeMirror.defineMode("clojure", functio } if (keyWord.length > 0 && (indentKeys.propertyIsEnumerable(keyWord) || - /^(?:def|with)/.test(keyWord))) { // indent-word + tests.block_indent.test(keyWord))) { // indent-word pushStack(state, indentTemp + INDENT_WORD_SKIP, ch); } else { // non-indent word // we continue eating the spaces @@ -240,5 +244,6 @@ CodeMirror.defineMode("clojure", functio }); CodeMirror.defineMIME("text/x-clojure", "clojure"); +CodeMirror.defineMIME("text/x-clojurescript", "clojure"); }); diff --git a/rhodecode/public/js/mode/coffeescript/coffeescript.js b/rhodecode/public/js/mode/coffeescript/coffeescript.js --- a/rhodecode/public/js/mode/coffeescript/coffeescript.js +++ b/rhodecode/public/js/mode/coffeescript/coffeescript.js @@ -25,7 +25,7 @@ CodeMirror.defineMode("coffeescript", fu var operators = /^(?:->|=>|\+[+=]?|-[\-=]?|\*[\*=]?|\/[\/=]?|[=!]=|<[><]?=?|>>?=?|%=?|&=?|\|=?|\^=?|\~|!|\?|(or|and|\|\||&&|\?)=)/; var delimiters = /^(?:[()\[\]{},:`=;]|\.\.?\.?)/; var identifiers = /^[_A-Za-z$][_A-Za-z$0-9]*/; - var properties = /^(@|this\.)[_A-Za-z$][_A-Za-z$0-9]*/; + var atProp = /^@[_A-Za-z$][_A-Za-z$0-9]*/; var wordOperators = wordRegexp(["and", "or", "not", "is", "isnt", "in", @@ -145,6 +145,8 @@ CodeMirror.defineMode("coffeescript", fu } } + + // Handle operators and delimiters if (stream.match(operators) || stream.match(wordOperators)) { return "operator"; @@ -157,6 +159,10 @@ CodeMirror.defineMode("coffeescript", fu return "atom"; } + if (stream.match(atProp) || state.prop && stream.match(identifiers)) { + return "property"; + } + if (stream.match(keywords)) { return "keyword"; } @@ -165,10 +171,6 @@ CodeMirror.defineMode("coffeescript", fu return "variable"; } - if (stream.match(properties)) { - return "property"; - } - // Handle non-detected items stream.next(); return ERRORCLASS; @@ -265,24 +267,11 @@ CodeMirror.defineMode("coffeescript", fu var style = state.tokenize(stream, state); var current = stream.current(); - // Handle "." connected identifiers - if (current === ".") { - style = state.tokenize(stream, state); - current = stream.current(); - if (/^\.[\w$]+$/.test(current)) { - return "variable"; - } else { - return ERRORCLASS; - } - } - // Handle scope changes. if (current === "return") { state.dedent = true; } - if (((current === "->" || current === "=>") && - !state.lambda && - !stream.peek()) + if (((current === "->" || current === "=>") && stream.eol()) || style === "indent") { indent(stream, state); } @@ -324,8 +313,7 @@ CodeMirror.defineMode("coffeescript", fu return { tokenize: tokenBase, scope: {offset:basecolumn || 0, type:"coffee", prev: null, align: false}, - lastToken: null, - lambda: false, + prop: false, dedent: 0 }; }, @@ -335,12 +323,9 @@ CodeMirror.defineMode("coffeescript", fu if (fillAlign && stream.sol()) fillAlign.align = false; var style = tokenLexer(stream, state); - if (fillAlign && style && style != "comment") fillAlign.align = true; - - state.lastToken = {style:style, content: stream.current()}; - - if (stream.eol() && stream.lambda) { - state.lambda = false; + if (style && style != "comment") { + if (fillAlign) fillAlign.align = true; + state.prop = style == "punctuation" && stream.current() == "." } return style; @@ -365,5 +350,6 @@ CodeMirror.defineMode("coffeescript", fu }); CodeMirror.defineMIME("text/x-coffeescript", "coffeescript"); +CodeMirror.defineMIME("text/coffeescript", "coffeescript"); }); diff --git a/rhodecode/public/js/mode/css/css.js b/rhodecode/public/js/mode/css/css.js --- a/rhodecode/public/js/mode/css/css.js +++ b/rhodecode/public/js/mode/css/css.js @@ -12,6 +12,7 @@ "use strict"; CodeMirror.defineMode("css", function(config, parserConfig) { + var inline = parserConfig.inline if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css"); var indentUnit = config.indentUnit, @@ -19,13 +20,15 @@ CodeMirror.defineMode("css", function(co documentTypes = parserConfig.documentTypes || {}, mediaTypes = parserConfig.mediaTypes || {}, mediaFeatures = parserConfig.mediaFeatures || {}, + mediaValueKeywords = parserConfig.mediaValueKeywords || {}, propertyKeywords = parserConfig.propertyKeywords || {}, nonStandardPropertyKeywords = parserConfig.nonStandardPropertyKeywords || {}, fontProperties = parserConfig.fontProperties || {}, counterDescriptors = parserConfig.counterDescriptors || {}, colorKeywords = parserConfig.colorKeywords || {}, valueKeywords = parserConfig.valueKeywords || {}, - allowNested = parserConfig.allowNested; + allowNested = parserConfig.allowNested, + supportsAtComponent = parserConfig.supportsAtComponent === true; var type, override; function ret(style, tp) { type = tp; return style; } @@ -119,13 +122,14 @@ CodeMirror.defineMode("css", function(co this.prev = prev; } - function pushContext(state, stream, type) { - state.context = new Context(type, stream.indentation() + indentUnit, state.context); + function pushContext(state, stream, type, indent) { + state.context = new Context(type, stream.indentation() + (indent === false ? 0 : indentUnit), state.context); return type; } function popContext(state) { - state.context = state.context.prev; + if (state.context.prev) + state.context = state.context.prev; return state.context.type; } @@ -157,9 +161,13 @@ CodeMirror.defineMode("css", function(co return pushContext(state, stream, "block"); } else if (type == "}" && state.context.prev) { return popContext(state); - } else if (/@(media|supports|(-moz-)?document)/.test(type)) { + } else if (supportsAtComponent && /@component/.test(type)) { + return pushContext(state, stream, "atComponentBlock"); + } else if (/^@(-moz-)?document$/.test(type)) { + return pushContext(state, stream, "documentTypes"); + } else if (/^@(media|supports|(-moz-)?document|import)$/.test(type)) { return pushContext(state, stream, "atBlock"); - } else if (/@(font-face|counter-style)/.test(type)) { + } else if (/^@(font-face|counter-style)/.test(type)) { state.stateArg = type; return "restricted_atBlock_before"; } else if (/^@(-(moz|ms|o|webkit)-)?keyframes$/.test(type)) { @@ -219,7 +227,7 @@ CodeMirror.defineMode("css", function(co if (type == "}" || type == "{") return popAndPass(type, stream, state); if (type == "(") return pushContext(state, stream, "parens"); - if (type == "hash" && !/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(stream.current())) { + if (type == "hash" && !/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(stream.current())) { override += " error"; } else if (type == "word") { wordAsValue(stream); @@ -252,33 +260,56 @@ CodeMirror.defineMode("css", function(co return pass(type, stream, state); }; + states.documentTypes = function(type, stream, state) { + if (type == "word" && documentTypes.hasOwnProperty(stream.current())) { + override = "tag"; + return state.context.type; + } else { + return states.atBlock(type, stream, state); + } + }; + states.atBlock = function(type, stream, state) { if (type == "(") return pushContext(state, stream, "atBlock_parens"); - if (type == "}") return popAndPass(type, stream, state); + if (type == "}" || type == ";") return popAndPass(type, stream, state); if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top"); + if (type == "interpolation") return pushContext(state, stream, "interpolation"); + if (type == "word") { var word = stream.current().toLowerCase(); if (word == "only" || word == "not" || word == "and" || word == "or") override = "keyword"; - else if (documentTypes.hasOwnProperty(word)) - override = "tag"; else if (mediaTypes.hasOwnProperty(word)) override = "attribute"; else if (mediaFeatures.hasOwnProperty(word)) override = "property"; + else if (mediaValueKeywords.hasOwnProperty(word)) + override = "keyword"; else if (propertyKeywords.hasOwnProperty(word)) override = "property"; else if (nonStandardPropertyKeywords.hasOwnProperty(word)) override = "string-2"; else if (valueKeywords.hasOwnProperty(word)) override = "atom"; + else if (colorKeywords.hasOwnProperty(word)) + override = "keyword"; else override = "error"; } return state.context.type; }; + states.atComponentBlock = function(type, stream, state) { + if (type == "}") + return popAndPass(type, stream, state); + if (type == "{") + return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top", false); + if (type == "word") + override = "error"; + return state.context.type; + }; + states.atBlock_parens = function(type, stream, state) { if (type == ")") return popContext(state); if (type == "{" || type == "}") return popAndPass(type, stream, state, 2); @@ -336,9 +367,9 @@ CodeMirror.defineMode("css", function(co return { startState: function(base) { return {tokenize: null, - state: "top", + state: inline ? "block" : "top", stateArg: null, - context: new Context("top", base || 0, null)}; + context: new Context(inline ? "block" : "top", base || 0, null)}; }, token: function(stream, state) { @@ -357,12 +388,18 @@ CodeMirror.defineMode("css", function(co var cx = state.context, ch = textAfter && textAfter.charAt(0); var indent = cx.indent; if (cx.type == "prop" && (ch == "}" || ch == ")")) cx = cx.prev; - if (cx.prev && - (ch == "}" && (cx.type == "block" || cx.type == "top" || cx.type == "interpolation" || cx.type == "restricted_atBlock") || - ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") || - ch == "{" && (cx.type == "at" || cx.type == "atBlock"))) { - indent = cx.indent - indentUnit; - cx = cx.prev; + if (cx.prev) { + if (ch == "}" && (cx.type == "block" || cx.type == "top" || + cx.type == "interpolation" || cx.type == "restricted_atBlock")) { + // Resume indentation from parent context. + cx = cx.prev; + indent = cx.indent; + } else if (ch == ")" && (cx.type == "parens" || cx.type == "atBlock_parens") || + ch == "{" && (cx.type == "at" || cx.type == "atBlock")) { + // Dedent relative to current context. + indent = Math.max(0, cx.indent - indentUnit); + cx = cx.prev; + } } return indent; }, @@ -399,17 +436,24 @@ CodeMirror.defineMode("css", function(co "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color", "max-color", "color-index", "min-color-index", "max-color-index", "monochrome", "min-monochrome", "max-monochrome", "resolution", - "min-resolution", "max-resolution", "scan", "grid" + "min-resolution", "max-resolution", "scan", "grid", "orientation", + "device-pixel-ratio", "min-device-pixel-ratio", "max-device-pixel-ratio", + "pointer", "any-pointer", "hover", "any-hover" ], mediaFeatures = keySet(mediaFeatures_); + var mediaValueKeywords_ = [ + "landscape", "portrait", "none", "coarse", "fine", "on-demand", "hover", + "interlace", "progressive" + ], mediaValueKeywords = keySet(mediaValueKeywords_); + var propertyKeywords_ = [ "align-content", "align-items", "align-self", "alignment-adjust", "alignment-baseline", "anchor-point", "animation", "animation-delay", "animation-direction", "animation-duration", "animation-fill-mode", "animation-iteration-count", "animation-name", "animation-play-state", "animation-timing-function", "appearance", "azimuth", "backface-visibility", - "background", "background-attachment", "background-clip", "background-color", - "background-image", "background-origin", "background-position", + "background", "background-attachment", "background-blend-mode", "background-clip", + "background-color", "background-image", "background-origin", "background-position", "background-repeat", "background-size", "baseline-shift", "binding", "bleed", "bookmark-label", "bookmark-level", "bookmark-state", "bookmark-target", "border", "border-bottom", "border-bottom-color", @@ -553,11 +597,12 @@ CodeMirror.defineMode("css", function(co "capitalize", "caps-lock-indicator", "caption", "captiontext", "caret", "cell", "center", "checkbox", "circle", "cjk-decimal", "cjk-earthly-branch", "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote", - "col-resize", "collapse", "column", "compact", "condensed", "contain", "content", + "col-resize", "collapse", "color", "color-burn", "color-dodge", "column", "column-reverse", + "compact", "condensed", "contain", "content", "content-box", "context-menu", "continuous", "copy", "counter", "counters", "cover", "crop", - "cross", "crosshair", "currentcolor", "cursive", "cyclic", "dashed", "decimal", + "cross", "crosshair", "currentcolor", "cursive", "cyclic", "darken", "dashed", "decimal", "decimal-leading-zero", "default", "default-button", "destination-atop", - "destination-in", "destination-out", "destination-over", "devanagari", + "destination-in", "destination-out", "destination-over", "devanagari", "difference", "disc", "discard", "disclosure-closed", "disclosure-open", "document", "dot-dash", "dot-dot-dash", "dotted", "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out", @@ -568,23 +613,23 @@ CodeMirror.defineMode("css", function(co "ethiopic-halehame-gez", "ethiopic-halehame-om-et", "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et", "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", "ethiopic-halehame-tig", - "ethiopic-numeric", "ew-resize", "expanded", "extends", "extra-condensed", - "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "flex", "footnotes", + "ethiopic-numeric", "ew-resize", "exclusion", "expanded", "extends", "extra-condensed", + "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "flex", "flex-end", "flex-start", "footnotes", "forwards", "from", "geometricPrecision", "georgian", "graytext", "groove", - "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hebrew", + "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hard-light", "hebrew", "help", "hidden", "hide", "higher", "highlight", "highlighttext", - "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "icon", "ignore", + "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "hue", "icon", "ignore", "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite", "infobackground", "infotext", "inherit", "initial", "inline", "inline-axis", "inline-block", "inline-flex", "inline-table", "inset", "inside", "intrinsic", "invert", "italic", "japanese-formal", "japanese-informal", "justify", "kannada", "katakana", "katakana-iroha", "keep-all", "khmer", "korean-hangul-formal", "korean-hanja-formal", "korean-hanja-informal", - "landscape", "lao", "large", "larger", "left", "level", "lighter", + "landscape", "lao", "large", "larger", "left", "level", "lighter", "lighten", "line-through", "linear", "linear-gradient", "lines", "list-item", "listbox", "listitem", "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian", "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian", - "lower-roman", "lowercase", "ltr", "malayalam", "match", "matrix", "matrix3d", + "lower-roman", "lowercase", "ltr", "luminosity", "malayalam", "match", "matrix", "matrix3d", "media-controls-background", "media-current-time-display", "media-fullscreen-button", "media-mute-button", "media-play-button", "media-return-to-realtime-button", "media-rewind-button", @@ -593,7 +638,7 @@ CodeMirror.defineMode("css", function(co "media-volume-slider-container", "media-volume-sliderthumb", "medium", "menu", "menulist", "menulist-button", "menulist-text", "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic", - "mix", "mongolian", "monospace", "move", "multiple", "myanmar", "n-resize", + "mix", "mongolian", "monospace", "move", "multiple", "multiply", "myanmar", "n-resize", "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop", "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap", "ns-resize", "numbers", "numeric", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote", @@ -606,8 +651,8 @@ CodeMirror.defineMode("css", function(co "relative", "repeat", "repeating-linear-gradient", "repeating-radial-gradient", "repeat-x", "repeat-y", "reset", "reverse", "rgb", "rgba", "ridge", "right", "rotate", "rotate3d", "rotateX", "rotateY", - "rotateZ", "round", "row-resize", "rtl", "run-in", "running", - "s-resize", "sans-serif", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", + "rotateZ", "round", "row", "row-resize", "row-reverse", "rtl", "run-in", "running", + "s-resize", "sans-serif", "saturation", "scale", "scale3d", "scaleX", "scaleY", "scaleZ", "screen", "scroll", "scrollbar", "se-resize", "searchfield", "searchfield-cancel-button", "searchfield-decoration", "searchfield-results-button", "searchfield-results-decoration", @@ -615,8 +660,8 @@ CodeMirror.defineMode("css", function(co "simp-chinese-formal", "simp-chinese-informal", "single", "skew", "skewX", "skewY", "skip-white-space", "slide", "slider-horizontal", "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow", - "small", "small-caps", "small-caption", "smaller", "solid", "somali", - "source-atop", "source-in", "source-out", "source-over", "space", "spell-out", "square", + "small", "small-caps", "small-caption", "smaller", "soft-light", "solid", "somali", + "source-atop", "source-in", "source-out", "source-over", "space", "space-around", "space-between", "spell-out", "square", "square-button", "start", "static", "status-bar", "stretch", "stroke", "sub", "subpixel-antialiased", "super", "sw-resize", "symbolic", "symbols", "table", "table-caption", "table-cell", "table-column", "table-column-group", @@ -633,12 +678,13 @@ CodeMirror.defineMode("css", function(co "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url", "var", "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted", "visibleStroke", "visual", "w-resize", "wait", "wave", "wider", - "window", "windowframe", "windowtext", "words", "x-large", "x-small", "xor", + "window", "windowframe", "windowtext", "words", "wrap", "wrap-reverse", "x-large", "x-small", "xor", "xx-large", "xx-small" ], valueKeywords = keySet(valueKeywords_); - var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(propertyKeywords_) - .concat(nonStandardPropertyKeywords_).concat(colorKeywords_).concat(valueKeywords_); + var allWords = documentTypes_.concat(mediaTypes_).concat(mediaFeatures_).concat(mediaValueKeywords_) + .concat(propertyKeywords_).concat(nonStandardPropertyKeywords_).concat(colorKeywords_) + .concat(valueKeywords_); CodeMirror.registerHelper("hintWords", "css", allWords); function tokenCComment(stream, state) { @@ -657,6 +703,7 @@ CodeMirror.defineMode("css", function(co documentTypes: documentTypes, mediaTypes: mediaTypes, mediaFeatures: mediaFeatures, + mediaValueKeywords: mediaValueKeywords, propertyKeywords: propertyKeywords, nonStandardPropertyKeywords: nonStandardPropertyKeywords, fontProperties: fontProperties, @@ -676,6 +723,7 @@ CodeMirror.defineMode("css", function(co CodeMirror.defineMIME("text/x-scss", { mediaTypes: mediaTypes, mediaFeatures: mediaFeatures, + mediaValueKeywords: mediaValueKeywords, propertyKeywords: propertyKeywords, nonStandardPropertyKeywords: nonStandardPropertyKeywords, colorKeywords: colorKeywords, @@ -717,6 +765,7 @@ CodeMirror.defineMode("css", function(co CodeMirror.defineMIME("text/x-less", { mediaTypes: mediaTypes, mediaFeatures: mediaFeatures, + mediaValueKeywords: mediaValueKeywords, propertyKeywords: propertyKeywords, nonStandardPropertyKeywords: nonStandardPropertyKeywords, colorKeywords: colorKeywords, @@ -751,4 +800,26 @@ CodeMirror.defineMode("css", function(co helperType: "less" }); + CodeMirror.defineMIME("text/x-gss", { + documentTypes: documentTypes, + mediaTypes: mediaTypes, + mediaFeatures: mediaFeatures, + propertyKeywords: propertyKeywords, + nonStandardPropertyKeywords: nonStandardPropertyKeywords, + fontProperties: fontProperties, + counterDescriptors: counterDescriptors, + colorKeywords: colorKeywords, + valueKeywords: valueKeywords, + supportsAtComponent: true, + tokenHooks: { + "/": function(stream, state) { + if (!stream.eat("*")) return false; + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } + }, + name: "css", + helperType: "gss" + }); + }); diff --git a/rhodecode/public/js/mode/css/gss_test.js b/rhodecode/public/js/mode/css/gss_test.js deleted file mode 100644 --- a/rhodecode/public/js/mode/css/gss_test.js +++ /dev/null @@ -1,17 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function() { - "use strict"; - - var mode = CodeMirror.getMode({indentUnit: 2}, "text/x-gss"); - function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1), "gss"); } - - MT("atComponent", - "[def @component] {", - "[tag foo] {", - " [property color]: [keyword black];", - "}", - "}"); - -})(); diff --git a/rhodecode/public/js/mode/css/less_test.js b/rhodecode/public/js/mode/css/less_test.js deleted file mode 100644 --- a/rhodecode/public/js/mode/css/less_test.js +++ /dev/null @@ -1,54 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function() { - "use strict"; - - var mode = CodeMirror.getMode({indentUnit: 2}, "text/x-less"); - function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1), "less"); } - - MT("variable", - "[variable-2 @base]: [atom #f04615];", - "[qualifier .class] {", - " [property width]: [variable percentage]([number 0.5]); [comment // returns `50%`]", - " [property color]: [variable saturate]([variable-2 @base], [number 5%]);", - "}"); - - MT("amp", - "[qualifier .child], [qualifier .sibling] {", - " [qualifier .parent] [atom &] {", - " [property color]: [keyword black];", - " }", - " [atom &] + [atom &] {", - " [property color]: [keyword red];", - " }", - "}"); - - MT("mixin", - "[qualifier .mixin] ([variable dark]; [variable-2 @color]) {", - " [property color]: [variable darken]([variable-2 @color], [number 10%]);", - "}", - "[qualifier .mixin] ([variable light]; [variable-2 @color]) {", - " [property color]: [variable lighten]([variable-2 @color], [number 10%]);", - "}", - "[qualifier .mixin] ([variable-2 @_]; [variable-2 @color]) {", - " [property display]: [atom block];", - "}", - "[variable-2 @switch]: [variable light];", - "[qualifier .class] {", - " [qualifier .mixin]([variable-2 @switch]; [atom #888]);", - "}"); - - MT("nest", - "[qualifier .one] {", - " [def @media] ([property width]: [number 400px]) {", - " [property font-size]: [number 1.2em];", - " [def @media] [attribute print] [keyword and] [property color] {", - " [property color]: [keyword blue];", - " }", - " }", - "}"); - - - MT("interpolation", ".@{[variable foo]} { [property font-weight]: [atom bold]; }"); -})(); diff --git a/rhodecode/public/js/mode/css/scss_test.js b/rhodecode/public/js/mode/css/scss_test.js deleted file mode 100644 --- a/rhodecode/public/js/mode/css/scss_test.js +++ /dev/null @@ -1,110 +0,0 @@ -// CodeMirror, copyright (c) by Marijn Haverbeke and others -// Distributed under an MIT license: http://codemirror.net/LICENSE - -(function() { - var mode = CodeMirror.getMode({indentUnit: 2}, "text/x-scss"); - function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1), "scss"); } - - MT('url_with_quotation', - "[tag foo] { [property background]:[atom url]([string test.jpg]) }"); - - MT('url_with_double_quotes', - "[tag foo] { [property background]:[atom url]([string \"test.jpg\"]) }"); - - MT('url_with_single_quotes', - "[tag foo] { [property background]:[atom url]([string \'test.jpg\']) }"); - - MT('string', - "[def @import] [string \"compass/css3\"]"); - - MT('important_keyword', - "[tag foo] { [property background]:[atom url]([string \'test.jpg\']) [keyword !important] }"); - - MT('variable', - "[variable-2 $blue]:[atom #333]"); - - MT('variable_as_attribute', - "[tag foo] { [property color]:[variable-2 $blue] }"); - - MT('numbers', - "[tag foo] { [property padding]:[number 10px] [number 10] [number 10em] [number 8in] }"); - - MT('number_percentage', - "[tag foo] { [property width]:[number 80%] }"); - - MT('selector', - "[builtin #hello][qualifier .world]{}"); - - MT('singleline_comment', - "[comment // this is a comment]"); - - MT('multiline_comment', - "[comment /*foobar*/]"); - - MT('attribute_with_hyphen', - "[tag foo] { [property font-size]:[number 10px] }"); - - MT('string_after_attribute', - "[tag foo] { [property content]:[string \"::\"] }"); - - MT('directives', - "[def @include] [qualifier .mixin]"); - - MT('basic_structure', - "[tag p] { [property background]:[keyword red]; }"); - - MT('nested_structure', - "[tag p] { [tag a] { [property color]:[keyword red]; } }"); - - MT('mixin', - "[def @mixin] [tag table-base] {}"); - - MT('number_without_semicolon', - "[tag p] {[property width]:[number 12]}", - "[tag a] {[property color]:[keyword red];}"); - - MT('atom_in_nested_block', - "[tag p] { [tag a] { [property color]:[atom #000]; } }"); - - MT('interpolation_in_property', - "[tag foo] { #{[variable-2 $hello]}:[number 2]; }"); - - MT('interpolation_in_selector', - "[tag foo]#{[variable-2 $hello]} { [property color]:[atom #000]; }"); - - MT('interpolation_error', - "[tag foo]#{[variable foo]} { [property color]:[atom #000]; }"); - - MT("divide_operator", - "[tag foo] { [property width]:[number 4] [operator /] [number 2] }"); - - MT('nested_structure_with_id_selector', - "[tag p] { [builtin #hello] { [property color]:[keyword red]; } }"); - - MT('indent_mixin', - "[def @mixin] [tag container] (", - " [variable-2 $a]: [number 10],", - " [variable-2 $b]: [number 10])", - "{}"); - - MT('indent_nested', - "[tag foo] {", - " [tag bar] {", - " }", - "}"); - - MT('indent_parentheses', - "[tag foo] {", - " [property color]: [variable darken]([variable-2 $blue],", - " [number 9%]);", - "}"); - - MT('indent_vardef', - "[variable-2 $name]:", - " [string 'val'];", - "[tag tag] {", - " [tag inner] {", - " [property margin]: [number 3px];", - " }", - "}"); -})(); diff --git a/rhodecode/public/js/mode/cypher/cypher.js b/rhodecode/public/js/mode/cypher/cypher.js --- a/rhodecode/public/js/mode/cypher/cypher.js +++ b/rhodecode/public/js/mode/cypher/cypher.js @@ -60,9 +60,9 @@ }; var indentUnit = config.indentUnit; var curPunc; - var funcs = wordRegexp(["abs", "acos", "allShortestPaths", "asin", "atan", "atan2", "avg", "ceil", "coalesce", "collect", "cos", "cot", "count", "degrees", "e", "endnode", "exp", "extract", "filter", "floor", "haversin", "head", "id", "keys", "labels", "last", "left", "length", "log", "log10", "lower", "ltrim", "max", "min", "node", "nodes", "percentileCont", "percentileDisc", "pi", "radians", "rand", "range", "reduce", "rel", "relationship", "relationships", "replace", "right", "round", "rtrim", "shortestPath", "sign", "sin", "split", "sqrt", "startnode", "stdev", "stdevp", "str", "substring", "sum", "tail", "tan", "timestamp", "toFloat", "toInt", "trim", "type", "upper"]); - var preds = wordRegexp(["all", "and", "any", "has", "in", "none", "not", "or", "single", "xor"]); - var keywords = wordRegexp(["as", "asc", "ascending", "assert", "by", "case", "commit", "constraint", "create", "csv", "cypher", "delete", "desc", "descending", "distinct", "drop", "else", "end", "explain", "false", "fieldterminator", "foreach", "from", "headers", "in", "index", "is", "limit", "load", "match", "merge", "null", "on", "optional", "order", "periodic", "profile", "remove", "return", "scan", "set", "skip", "start", "then", "true", "union", "unique", "unwind", "using", "when", "where", "with"]); + var funcs = wordRegexp(["abs", "acos", "allShortestPaths", "asin", "atan", "atan2", "avg", "ceil", "coalesce", "collect", "cos", "cot", "count", "degrees", "e", "endnode", "exp", "extract", "filter", "floor", "haversin", "head", "id", "keys", "labels", "last", "left", "length", "log", "log10", "lower", "ltrim", "max", "min", "node", "nodes", "percentileCont", "percentileDisc", "pi", "radians", "rand", "range", "reduce", "rel", "relationship", "relationships", "replace", "reverse", "right", "round", "rtrim", "shortestPath", "sign", "sin", "size", "split", "sqrt", "startnode", "stdev", "stdevp", "str", "substring", "sum", "tail", "tan", "timestamp", "toFloat", "toInt", "toString", "trim", "type", "upper"]); + var preds = wordRegexp(["all", "and", "any", "contains", "exists", "has", "in", "none", "not", "or", "single", "xor"]); + var keywords = wordRegexp(["as", "asc", "ascending", "assert", "by", "case", "commit", "constraint", "create", "csv", "cypher", "delete", "desc", "descending", "detach", "distinct", "drop", "else", "end", "ends", "explain", "false", "fieldterminator", "foreach", "from", "headers", "in", "index", "is", "join", "limit", "load", "match", "merge", "null", "on", "optional", "order", "periodic", "profile", "remove", "return", "scan", "set", "skip", "start", "starts", "then", "true", "union", "unique", "unwind", "using", "when", "where", "with"]); var operatorChars = /[*+\-<>=&|~%^]/; return { diff --git a/rhodecode/public/js/mode/dart/dart.js b/rhodecode/public/js/mode/dart/dart.js --- a/rhodecode/public/js/mode/dart/dart.js +++ b/rhodecode/public/js/mode/dart/dart.js @@ -15,7 +15,7 @@ "implements get native operator set typedef with enum throw rethrow " + "assert break case continue default in return new deferred async await " + "try catch finally do else for if switch while import library export " + - "part of show hide is").split(" "); + "part of show hide is as").split(" "); var blockKeywords = "try catch finally do else for if switch while".split(" "); var atoms = "true false null".split(" "); var builtins = "void bool num int double dynamic var String".split(" "); @@ -26,21 +26,101 @@ return obj; } + function pushInterpolationStack(state) { + (state.interpolationStack || (state.interpolationStack = [])).push(state.tokenize); + } + + function popInterpolationStack(state) { + return (state.interpolationStack || (state.interpolationStack = [])).pop(); + } + + function sizeInterpolationStack(state) { + return state.interpolationStack ? state.interpolationStack.length : 0; + } + CodeMirror.defineMIME("application/dart", { name: "clike", keywords: set(keywords), - multiLineStrings: true, blockKeywords: set(blockKeywords), builtin: set(builtins), atoms: set(atoms), hooks: { "@": function(stream) { - stream.eatWhile(/[\w\$_]/); + stream.eatWhile(/[\w\$_\.]/); return "meta"; + }, + + // custom string handling to deal with triple-quoted strings and string interpolation + "'": function(stream, state) { + return tokenString("'", stream, state, false); + }, + "\"": function(stream, state) { + return tokenString("\"", stream, state, false); + }, + "r": function(stream, state) { + var peek = stream.peek(); + if (peek == "'" || peek == "\"") { + return tokenString(stream.next(), stream, state, true); + } + return false; + }, + + "}": function(_stream, state) { + // "}" is end of interpolation, if interpolation stack is non-empty + if (sizeInterpolationStack(state) > 0) { + state.tokenize = popInterpolationStack(state); + return null; + } + return false; } } }); + function tokenString(quote, stream, state, raw) { + var tripleQuoted = false; + if (stream.eat(quote)) { + if (stream.eat(quote)) tripleQuoted = true; + else return "string"; //empty string + } + function tokenStringHelper(stream, state) { + var escaped = false; + while (!stream.eol()) { + if (!raw && !escaped && stream.peek() == "$") { + pushInterpolationStack(state); + state.tokenize = tokenInterpolation; + return "string"; + } + var next = stream.next(); + if (next == quote && !escaped && (!tripleQuoted || stream.match(quote + quote))) { + state.tokenize = null; + break; + } + escaped = !raw && !escaped && next == "\\"; + } + return "string"; + } + state.tokenize = tokenStringHelper; + return tokenStringHelper(stream, state); + } + + function tokenInterpolation(stream, state) { + stream.eat("$"); + if (stream.eat("{")) { + // let clike handle the content of ${...}, + // we take over again when "}" appears (see hooks). + state.tokenize = null; + } else { + state.tokenize = tokenInterpolationIdentifier; + } + return null; + } + + function tokenInterpolationIdentifier(stream, state) { + stream.eatWhile(/[\w_]/); + state.tokenize = popInterpolationStack(state); + return "variable"; + } + CodeMirror.registerHelper("hintWords", "application/dart", keywords.concat(atoms).concat(builtins)); // This is needed to make loading through meta.js work. diff --git a/rhodecode/public/js/mode/django/django.js b/rhodecode/public/js/mode/django/django.js --- a/rhodecode/public/js/mode/django/django.js +++ b/rhodecode/public/js/mode/django/django.js @@ -14,14 +14,14 @@ "use strict"; CodeMirror.defineMode("django:inner", function() { - var keywords = ["block", "endblock", "for", "endfor", "true", "false", - "loop", "none", "self", "super", "if", "endif", "as", - "else", "import", "with", "endwith", "without", "context", "ifequal", "endifequal", - "ifnotequal", "endifnotequal", "extends", "include", "load", "comment", - "endcomment", "empty", "url", "static", "trans", "blocktrans", "now", "regroup", - "lorem", "ifchanged", "endifchanged", "firstof", "debug", "cycle", "csrf_token", - "autoescape", "endautoescape", "spaceless", "ssi", "templatetag", - "verbatim", "endverbatim", "widthratio"], + var keywords = ["block", "endblock", "for", "endfor", "true", "false", "filter", "endfilter", + "loop", "none", "self", "super", "if", "elif", "endif", "as", "else", "import", + "with", "endwith", "without", "context", "ifequal", "endifequal", "ifnotequal", + "endifnotequal", "extends", "include", "load", "comment", "endcomment", + "empty", "url", "static", "trans", "blocktrans", "endblocktrans", "now", + "regroup", "lorem", "ifchanged", "endifchanged", "firstof", "debug", "cycle", + "csrf_token", "autoescape", "endautoescape", "spaceless", "endspaceless", + "ssi", "templatetag", "verbatim", "endverbatim", "widthratio"], filters = ["add", "addslashes", "capfirst", "center", "cut", "date", "default", "default_if_none", "dictsort", "dictsortreversed", "divisibleby", "escape", "escapejs", @@ -35,11 +35,13 @@ "truncatechars_html", "truncatewords", "truncatewords_html", "unordered_list", "upper", "urlencode", "urlize", "urlizetrunc", "wordcount", "wordwrap", "yesno"], - operators = ["==", "!=", "<", ">", "<=", ">=", "in", "not", "or", "and"]; + operators = ["==", "!=", "<", ">", "<=", ">="], + wordOperators = ["in", "not", "or", "and"]; keywords = new RegExp("^\\b(" + keywords.join("|") + ")\\b"); filters = new RegExp("^\\b(" + filters.join("|") + ")\\b"); operators = new RegExp("^\\b(" + operators.join("|") + ")\\b"); + wordOperators = new RegExp("^\\b(" + wordOperators.join("|") + ")\\b"); // We have to return "null" instead of null, in order to avoid string // styling as the default, when using Django templates inside HTML @@ -59,7 +61,7 @@ // Ignore completely any stream series that do not match the // Django template opening tags. - while (stream.next() != null && !stream.match("{{", false) && !stream.match("{%", false)) {} + while (stream.next() != null && !stream.match(/\{[{%#]/, false)) {} return null; } @@ -270,6 +272,11 @@ return "operator"; } + // Attempt to match a word operator + if (stream.match(wordOperators)) { + return "keyword"; + } + // Attempt to match a keyword var keywordMatch = stream.match(keywords); if (keywordMatch) { @@ -310,9 +317,8 @@ // Mark everything as comment inside the tag and the tag itself. function inComment (stream, state) { - if (stream.match("#}")) { - state.tokenize = tokenBase; - } + if (stream.match(/^.*?#\}/)) state.tokenize = tokenBase + else stream.skipToEnd() return "comment"; } diff --git a/rhodecode/public/js/mode/dockerfile/dockerfile.js b/rhodecode/public/js/mode/dockerfile/dockerfile.js --- a/rhodecode/public/js/mode/dockerfile/dockerfile.js +++ b/rhodecode/public/js/mode/dockerfile/dockerfile.js @@ -69,7 +69,10 @@ token: null, next: "start" } - ] + ], + meta: { + lineComment: "#" + } }); CodeMirror.defineMIME("text/x-dockerfile", "dockerfile"); diff --git a/rhodecode/public/js/mode/elm/elm.js b/rhodecode/public/js/mode/elm/elm.js --- a/rhodecode/public/js/mode/elm/elm.js +++ b/rhodecode/public/js/mode/elm/elm.js @@ -202,4 +202,4 @@ }); CodeMirror.defineMIME("text/x-elm", "elm"); -})(); +}); diff --git a/rhodecode/public/js/mode/erlang/erlang.js b/rhodecode/public/js/mode/erlang/erlang.js --- a/rhodecode/public/js/mode/erlang/erlang.js +++ b/rhodecode/public/js/mode/erlang/erlang.js @@ -220,8 +220,6 @@ CodeMirror.defineMode("erlang", function }else{ return rval(state,stream,"function"); } - }else if (is_member(w,operatorAtomWords)) { - return rval(state,stream,"operator"); }else if (lookahead(stream) == ":") { if (w == "erlang") { return rval(state,stream,"builtin"); @@ -230,8 +228,6 @@ CodeMirror.defineMode("erlang", function } }else if (is_member(w,["true","false"])) { return rval(state,stream,"boolean"); - }else if (is_member(w,["true","false"])) { - return rval(state,stream,"boolean"); }else{ return rval(state,stream,"atom"); } diff --git a/rhodecode/public/js/mode/gfm/gfm.js b/rhodecode/public/js/mode/gfm/gfm.js --- a/rhodecode/public/js/mode/gfm/gfm.js +++ b/rhodecode/public/js/mode/gfm/gfm.js @@ -11,6 +11,8 @@ })(function(CodeMirror) { "use strict"; +var urlRE = /^((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|tag|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/i + CodeMirror.defineMode("gfm", function(config, modeConfig) { var codeDepth = 0; function blankLine(state) { @@ -37,7 +39,7 @@ CodeMirror.defineMode("gfm", function(co // Hack to prevent formatting override inside code blocks (block and inline) if (state.codeBlock) { - if (stream.match(/^```/)) { + if (stream.match(/^```+/)) { state.codeBlock = false; return null; } @@ -47,7 +49,7 @@ CodeMirror.defineMode("gfm", function(co if (stream.sol()) { state.code = false; } - if (stream.sol() && stream.match(/^```/)) { + if (stream.sol() && stream.match(/^```+/)) { stream.skipToEnd(); state.codeBlock = true; return null; @@ -78,25 +80,29 @@ CodeMirror.defineMode("gfm", function(co } if (stream.sol() || state.ateSpace) { state.ateSpace = false; - if(stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40}\b)/)) { - // User/Project@SHA - // User@SHA - // SHA - state.combineTokens = true; - return "link"; - } else if (stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/)) { - // User/Project#Num - // User#Num - // #Num - state.combineTokens = true; - return "link"; + if (modeConfig.gitHubSpice !== false) { + if(stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40}\b)/)) { + // User/Project@SHA + // User@SHA + // SHA + state.combineTokens = true; + return "link"; + } else if (stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/)) { + // User/Project#Num + // User#Num + // #Num + state.combineTokens = true; + return "link"; + } } } - if (stream.match(/^((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/i) && - stream.string.slice(stream.start - 2, stream.start) != "](") { + if (stream.match(urlRE) && + stream.string.slice(stream.start - 2, stream.start) != "](" && + (stream.start == 0 || /\W/.test(stream.string.charAt(stream.start - 1)))) { // URLs // Taken from http://daringfireball.net/2010/07/improved_regex_for_matching_urls // And then (issue #1160) simplified to make it not crash the Chrome Regexp engine + // And then limited url schemes to the CommonMark list, so foo:bar isn't matched as a URL state.combineTokens = true; return "link"; } @@ -109,15 +115,16 @@ CodeMirror.defineMode("gfm", function(co var markdownConfig = { underscoresBreakWords: false, taskLists: true, - fencedCodeBlocks: true, + fencedCodeBlocks: '```', strikethrough: true }; for (var attr in modeConfig) { markdownConfig[attr] = modeConfig[attr]; } markdownConfig.name = "markdown"; - CodeMirror.defineMIME("gfmBase", markdownConfig); - return CodeMirror.overlayMode(CodeMirror.getMode(config, "gfmBase"), gfmOverlay); + return CodeMirror.overlayMode(CodeMirror.getMode(config, markdownConfig), gfmOverlay); + }, "markdown"); + CodeMirror.defineMIME("text/x-gfm", "gfm"); }); diff --git a/rhodecode/public/js/mode/go/go.js b/rhodecode/public/js/mode/go/go.js --- a/rhodecode/public/js/mode/go/go.js +++ b/rhodecode/public/js/mode/go/go.js @@ -86,7 +86,7 @@ CodeMirror.defineMode("go", function(con var escaped = false, next, end = false; while ((next = stream.next()) != null) { if (next == quote && !escaped) {end = true; break;} - escaped = !escaped && next == "\\"; + escaped = !escaped && quote != "`" && next == "\\"; } if (end || !(escaped || quote == "`")) state.tokenize = tokenBase; diff --git a/rhodecode/public/js/mode/haml/haml.js b/rhodecode/public/js/mode/haml/haml.js --- a/rhodecode/public/js/mode/haml/haml.js +++ b/rhodecode/public/js/mode/haml/haml.js @@ -85,8 +85,10 @@ state.tokenize = rubyInQuote(")"); return state.tokenize(stream, state); } else if (ch == "{") { - state.tokenize = rubyInQuote("}"); - return state.tokenize(stream, state); + if (!stream.match(/^\{%.*/)) { + state.tokenize = rubyInQuote("}"); + return state.tokenize(stream, state); + } } } diff --git a/rhodecode/public/js/mode/handlebars/handlebars.js b/rhodecode/public/js/mode/handlebars/handlebars.js --- a/rhodecode/public/js/mode/handlebars/handlebars.js +++ b/rhodecode/public/js/mode/handlebars/handlebars.js @@ -3,15 +3,15 @@ (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror"), require("../../addon/mode/simple")); + mod(require("../../lib/codemirror"), require("../../addon/mode/simple"), require("../../addon/mode/multiplex")); else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror", "../../addon/mode/simple"], mod); + define(["../../lib/codemirror", "../../addon/mode/simple", "../../addon/mode/multiplex"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; - CodeMirror.defineSimpleMode("handlebars", { + CodeMirror.defineSimpleMode("handlebars-tags", { start: [ { regex: /\{\{!--/, push: "dash_comment", token: "comment" }, { regex: /\{\{!/, push: "comment", token: "comment" }, @@ -21,8 +21,8 @@ { regex: /\}\}/, pop: true, token: "tag" }, // Double and single quotes - { regex: /"(?:[^\\]|\\.)*?"/, token: "string" }, - { regex: /'(?:[^\\]|\\.)*?'/, token: "string" }, + { regex: /"(?:[^\\"]|\\.)*"?/, token: "string" }, + { regex: /'(?:[^\\']|\\.)*'?/, token: "string" }, // Handlebars keywords { regex: />|[#\/]([A-Za-z_]\w*)/, token: "keyword" }, @@ -49,5 +49,14 @@ ] }); + CodeMirror.defineMode("handlebars", function(config, parserConfig) { + var handlebars = CodeMirror.getMode(config, "handlebars-tags"); + if (!parserConfig || !parserConfig.base) return handlebars; + return CodeMirror.multiplexingMode( + CodeMirror.getMode(config, parserConfig.base), + {open: "{{", close: "}}", mode: handlebars, parseDelimiters: true} + ); + }); + CodeMirror.defineMIME("text/x-handlebars-template", "handlebars"); }); diff --git a/rhodecode/public/js/mode/haxe/haxe.js b/rhodecode/public/js/mode/haxe/haxe.js --- a/rhodecode/public/js/mode/haxe/haxe.js +++ b/rhodecode/public/js/mode/haxe/haxe.js @@ -16,23 +16,21 @@ CodeMirror.defineMode("haxe", function(c // Tokenizer - var keywords = function(){ - function kw(type) {return {type: type, style: "keyword"};} - var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"); - var operator = kw("operator"), atom = {type: "atom", style: "atom"}, attribute = {type:"attribute", style: "attribute"}; + function kw(type) {return {type: type, style: "keyword"};} + var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"); + var operator = kw("operator"), atom = {type: "atom", style: "atom"}, attribute = {type:"attribute", style: "attribute"}; var type = kw("typedef"); - return { - "if": A, "while": A, "else": B, "do": B, "try": B, - "return": C, "break": C, "continue": C, "new": C, "throw": C, - "var": kw("var"), "inline":attribute, "static": attribute, "using":kw("import"), + var keywords = { + "if": A, "while": A, "else": B, "do": B, "try": B, + "return": C, "break": C, "continue": C, "new": C, "throw": C, + "var": kw("var"), "inline":attribute, "static": attribute, "using":kw("import"), "public": attribute, "private": attribute, "cast": kw("cast"), "import": kw("import"), "macro": kw("macro"), - "function": kw("function"), "catch": kw("catch"), "untyped": kw("untyped"), "callback": kw("cb"), - "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), - "in": operator, "never": kw("property_access"), "trace":kw("trace"), + "function": kw("function"), "catch": kw("catch"), "untyped": kw("untyped"), "callback": kw("cb"), + "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), + "in": operator, "never": kw("property_access"), "trace":kw("trace"), "class": type, "abstract":type, "enum":type, "interface":type, "typedef":type, "extends":type, "implements":type, "dynamic":type, - "true": atom, "false": atom, "null": atom - }; - }(); + "true": atom, "false": atom, "null": atom + }; var isOperatorChar = /[+\-*&%=<>!?|]/; @@ -41,14 +39,13 @@ CodeMirror.defineMode("haxe", function(c return f(stream, state); } - function nextUntilUnescaped(stream, end) { + function toUnescaped(stream, end) { var escaped = false, next; while ((next = stream.next()) != null) { if (next == end && !escaped) - return false; + return true; escaped = !escaped && next == "\\"; } - return escaped; } // Used as scratch variables to communicate multiple values without @@ -61,70 +58,58 @@ CodeMirror.defineMode("haxe", function(c function haxeTokenBase(stream, state) { var ch = stream.next(); - if (ch == '"' || ch == "'") + if (ch == '"' || ch == "'") { return chain(stream, state, haxeTokenString(ch)); - else if (/[\[\]{}\(\),;\:\.]/.test(ch)) + } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { return ret(ch); - else if (ch == "0" && stream.eat(/x/i)) { + } else if (ch == "0" && stream.eat(/x/i)) { stream.eatWhile(/[\da-f]/i); return ret("number", "number"); - } - else if (/\d/.test(ch) || ch == "-" && stream.eat(/\d/)) { - stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/); + } else if (/\d/.test(ch) || ch == "-" && stream.eat(/\d/)) { + stream.match(/^\d*(?:\.\d*(?!\.))?(?:[eE][+\-]?\d+)?/); return ret("number", "number"); - } - else if (state.reAllowed && (ch == "~" && stream.eat(/\//))) { - nextUntilUnescaped(stream, "/"); + } else if (state.reAllowed && (ch == "~" && stream.eat(/\//))) { + toUnescaped(stream, "/"); stream.eatWhile(/[gimsu]/); return ret("regexp", "string-2"); - } - else if (ch == "/") { + } else if (ch == "/") { if (stream.eat("*")) { return chain(stream, state, haxeTokenComment); - } - else if (stream.eat("/")) { + } else if (stream.eat("/")) { stream.skipToEnd(); return ret("comment", "comment"); - } - else { + } else { stream.eatWhile(isOperatorChar); return ret("operator", null, stream.current()); } - } - else if (ch == "#") { + } else if (ch == "#") { stream.skipToEnd(); return ret("conditional", "meta"); - } - else if (ch == "@") { + } else if (ch == "@") { stream.eat(/:/); stream.eatWhile(/[\w_]/); return ret ("metadata", "meta"); - } - else if (isOperatorChar.test(ch)) { + } else if (isOperatorChar.test(ch)) { stream.eatWhile(isOperatorChar); return ret("operator", null, stream.current()); - } - else { - var word; - if(/[A-Z]/.test(ch)) - { - stream.eatWhile(/[\w_<>]/); - word = stream.current(); - return ret("type", "variable-3", word); - } - else - { + } else { + var word; + if(/[A-Z]/.test(ch)) { + stream.eatWhile(/[\w_<>]/); + word = stream.current(); + return ret("type", "variable-3", word); + } else { stream.eatWhile(/[\w_]/); var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word]; return (known && state.kwAllowed) ? ret(known.type, known.style, word) : ret("variable", "variable", word); - } + } } } function haxeTokenString(quote) { return function(stream, state) { - if (!nextUntilUnescaped(stream, quote)) + if (toUnescaped(stream, quote)) state.tokenize = haxeTokenBase; return ret("string", "string"); }; @@ -176,27 +161,25 @@ CodeMirror.defineMode("haxe", function(c cc.pop()(); if (cx.marked) return cx.marked; if (type == "variable" && inScope(state, content)) return "variable-2"; - if (type == "variable" && imported(state, content)) return "variable-3"; + if (type == "variable" && imported(state, content)) return "variable-3"; return style; } } } - function imported(state, typename) - { - if (/[a-z]/.test(typename.charAt(0))) - return false; - var len = state.importedtypes.length; - for (var i = 0; i") { - // Script block: mode to change to depends on type attribute - var scriptType = stream.string.slice(Math.max(0, stream.pos - 100), stream.pos).match(/\btype\s*=\s*("[^"]+"|'[^']+'|\S+)[^<]*$/i); - scriptType = scriptType ? scriptType[1] : ""; - if (scriptType && /[\"\']/.test(scriptType.charAt(0))) scriptType = scriptType.slice(1, scriptType.length - 1); - for (var i = 0; i < scriptTypes.length; ++i) { - var tp = scriptTypes[i]; - if (typeof tp.matches == "string" ? scriptType == tp.matches : tp.matches.test(scriptType)) { - if (tp.mode) { - state.token = script; - state.localMode = tp.mode; - state.localState = tp.mode.startState && tp.mode.startState(htmlMode.indent(state.htmlState, "")); - } - break; - } - } - } else if (tagName == "style" && /\btag\b/.test(style) && stream.current() == ">") { - state.token = css; - state.localMode = cssMode; - state.localState = cssMode.startState(htmlMode.indent(state.htmlState, "")); - } - return style; - } + var defaultTags = { + script: [ + ["lang", /(javascript|babel)/i, "javascript"], + ["type", /^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^$/i, "javascript"], + ["type", /./, "text/plain"], + [null, null, "javascript"] + ], + style: [ + ["lang", /^css$/i, "css"], + ["type", /^(text\/)?(x-)?(stylesheet|css)$/i, "css"], + ["type", /./, "text/plain"], + [null, null, "css"] + ] + }; + function maybeBackup(stream, pat, style) { - var cur = stream.current(); - var close = cur.search(pat); - if (close > -1) stream.backUp(cur.length - close); - else if (cur.match(/<\/?$/)) { + var cur = stream.current(), close = cur.search(pat); + if (close > -1) { + stream.backUp(cur.length - close); + } else if (cur.match(/<\/?$/)) { stream.backUp(cur.length); if (!stream.match(pat, false)) stream.match(cur); } return style; } - function script(stream, state) { - if (stream.match(/^<\/\s*script\s*>/i, false)) { - state.token = html; - state.localState = state.localMode = null; - return null; - } - return maybeBackup(stream, /<\/\s*script\s*>/, - state.localMode.token(stream, state.localState)); + + var attrRegexpCache = {}; + function getAttrRegexp(attr) { + var regexp = attrRegexpCache[attr]; + if (regexp) return regexp; + return attrRegexpCache[attr] = new RegExp("\\s+" + attr + "\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*"); + } + + function getAttrValue(stream, attr) { + var pos = stream.pos, match; + while (pos >= 0 && stream.string.charAt(pos) !== "<") pos--; + if (pos < 0) return pos; + if (match = stream.string.slice(pos, stream.pos).match(getAttrRegexp(attr))) + return match[2]; + return ""; } - function css(stream, state) { - if (stream.match(/^<\/\s*style\s*>/i, false)) { - state.token = html; - state.localState = state.localMode = null; - return null; + + function getTagRegexp(tagName, anchored) { + return new RegExp((anchored ? "^" : "") + "<\/\s*" + tagName + "\s*>", "i"); + } + + function addTags(from, to) { + for (var tag in from) { + var dest = to[tag] || (to[tag] = []); + var source = from[tag]; + for (var i = source.length - 1; i >= 0; i--) + dest.unshift(source[i]) } - return maybeBackup(stream, /<\/\s*style\s*>/, - cssMode.token(stream, state.localState)); + } + + function findMatchingMode(tagInfo, stream) { + for (var i = 0; i < tagInfo.length; i++) { + var spec = tagInfo[i]; + if (!spec[0] || spec[1].test(getAttrValue(stream, spec[0]))) return spec[2]; + } } - return { - startState: function() { - var state = htmlMode.startState(); - return {token: html, localMode: null, localState: null, htmlState: state}; - }, + CodeMirror.defineMode("htmlmixed", function (config, parserConfig) { + var htmlMode = CodeMirror.getMode(config, { + name: "xml", + htmlMode: true, + multilineTagIndentFactor: parserConfig.multilineTagIndentFactor, + multilineTagIndentPastTag: parserConfig.multilineTagIndentPastTag + }); - copyState: function(state) { - if (state.localState) - var local = CodeMirror.copyState(state.localMode, state.localState); - return {token: state.token, localMode: state.localMode, localState: local, - htmlState: CodeMirror.copyState(htmlMode, state.htmlState)}; - }, + var tags = {}; + var configTags = parserConfig && parserConfig.tags, configScript = parserConfig && parserConfig.scriptTypes; + addTags(defaultTags, tags); + if (configTags) addTags(configTags, tags); + if (configScript) for (var i = configScript.length - 1; i >= 0; i--) + tags.script.unshift(["type", configScript[i].matches, configScript[i].mode]) - token: function(stream, state) { - return state.token(stream, state); - }, + function html(stream, state) { + var tagName = state.htmlState.tagName && state.htmlState.tagName.toLowerCase(); + var tagInfo = tagName && tags.hasOwnProperty(tagName) && tags[tagName]; + + var style = htmlMode.token(stream, state.htmlState), modeSpec; - indent: function(state, textAfter) { - if (!state.localMode || /^\s*<\//.test(textAfter)) - return htmlMode.indent(state.htmlState, textAfter); - else if (state.localMode.indent) - return state.localMode.indent(state.localState, textAfter); - else - return CodeMirror.Pass; - }, + if (tagInfo && /\btag\b/.test(style) && stream.current() === ">" && + (modeSpec = findMatchingMode(tagInfo, stream))) { + var mode = CodeMirror.getMode(config, modeSpec); + var endTagA = getTagRegexp(tagName, true), endTag = getTagRegexp(tagName, false); + state.token = function (stream, state) { + if (stream.match(endTagA, false)) { + state.token = html; + state.localState = state.localMode = null; + return null; + } + return maybeBackup(stream, endTag, state.localMode.token(stream, state.localState)); + }; + state.localMode = mode; + state.localState = CodeMirror.startState(mode, htmlMode.indent(state.htmlState, "")); + } + return style; + }; + + return { + startState: function () { + var state = htmlMode.startState(); + return {token: html, localMode: null, localState: null, htmlState: state}; + }, - innerMode: function(state) { - return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode}; - } - }; -}, "xml", "javascript", "css"); + copyState: function (state) { + var local; + if (state.localState) { + local = CodeMirror.copyState(state.localMode, state.localState); + } + return {token: state.token, localMode: state.localMode, localState: local, + htmlState: CodeMirror.copyState(htmlMode, state.htmlState)}; + }, + + token: function (stream, state) { + return state.token(stream, state); + }, -CodeMirror.defineMIME("text/html", "htmlmixed"); + indent: function (state, textAfter) { + if (!state.localMode || /^\s*<\//.test(textAfter)) + return htmlMode.indent(state.htmlState, textAfter); + else if (state.localMode.indent) + return state.localMode.indent(state.localState, textAfter); + else + return CodeMirror.Pass; + }, + innerMode: function (state) { + return {state: state.localState || state.htmlState, mode: state.localMode || htmlMode}; + } + }; + }, "xml", "javascript", "css"); + + CodeMirror.defineMIME("text/html", "htmlmixed"); }); diff --git a/rhodecode/public/js/mode/jade/jade.js b/rhodecode/public/js/mode/jade/jade.js --- a/rhodecode/public/js/mode/jade/jade.js +++ b/rhodecode/public/js/mode/jade/jade.js @@ -74,7 +74,7 @@ CodeMirror.defineMode('jade', function ( res.javaScriptArguments = this.javaScriptArguments; res.javaScriptArgumentsDepth = this.javaScriptArgumentsDepth; res.isInterpolating = this.isInterpolating; - res.interpolationNesting = this.intpolationNesting; + res.interpolationNesting = this.interpolationNesting; res.jsState = CodeMirror.copyState(jsMode, this.jsState); @@ -167,7 +167,7 @@ CodeMirror.defineMode('jade', function ( if (state.interpolationNesting < 0) { stream.next(); state.isInterpolating = false; - return 'puncutation'; + return 'punctuation'; } } else if (stream.peek() === '{') { state.interpolationNesting++; @@ -583,7 +583,7 @@ CodeMirror.defineMode('jade', function ( copyState: copyState, token: nextToken }; -}); +}, 'javascript', 'css', 'htmlmixed'); CodeMirror.defineMIME('text/x-jade', 'jade'); diff --git a/rhodecode/public/js/mode/javascript/javascript.js b/rhodecode/public/js/mode/javascript/javascript.js --- a/rhodecode/public/js/mode/javascript/javascript.js +++ b/rhodecode/public/js/mode/javascript/javascript.js @@ -13,6 +13,11 @@ })(function(CodeMirror) { "use strict"; +function expressionAllowed(stream, state, backUp) { + return /^(?:operator|sof|keyword c|case|new|[\[{}\(,;:]|=>)$/.test(state.lastType) || + (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0)))) +} + CodeMirror.defineMode("javascript", function(config, parserConfig) { var indentUnit = config.indentUnit; var statementIndent = parserConfig.statementIndent; @@ -30,13 +35,13 @@ CodeMirror.defineMode("javascript", func var jsKeywords = { "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, - "return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C, "debugger": C, + "return": C, "break": C, "continue": C, "new": kw("new"), "delete": C, "throw": C, "debugger": C, "var": kw("var"), "const": kw("var"), "let": kw("var"), "function": kw("function"), "catch": kw("catch"), "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), "in": operator, "typeof": operator, "instanceof": operator, "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, - "this": kw("this"), "module": kw("module"), "class": kw("class"), "super": kw("atom"), + "this": kw("this"), "class": kw("class"), "super": kw("atom"), "yield": C, "export": kw("export"), "import": kw("import"), "extends": C }; @@ -45,18 +50,23 @@ CodeMirror.defineMode("javascript", func var type = {type: "variable", style: "variable-3"}; var tsKeywords = { // object-like things - "interface": kw("interface"), - "extends": kw("extends"), - "constructor": kw("constructor"), + "interface": kw("class"), + "implements": C, + "namespace": C, + "module": kw("module"), + "enum": kw("module"), // scope modifiers - "public": kw("public"), - "private": kw("private"), - "protected": kw("protected"), - "static": kw("static"), + "public": kw("modifier"), + "private": kw("modifier"), + "protected": kw("modifier"), + "abstract": kw("modifier"), + + // operators + "as": operator, // types - "string": type, "number": type, "bool": type, "any": type + "string": type, "number": type, "boolean": type, "any": type }; for (var attr in tsKeywords) { @@ -105,6 +115,12 @@ CodeMirror.defineMode("javascript", func } else if (ch == "0" && stream.eat(/x/i)) { stream.eatWhile(/[\da-f]/i); return ret("number", "number"); + } else if (ch == "0" && stream.eat(/o/i)) { + stream.eatWhile(/[0-7]/i); + return ret("number", "number"); + } else if (ch == "0" && stream.eat(/b/i)) { + stream.eatWhile(/[01]/i); + return ret("number", "number"); } else if (/\d/.test(ch)) { stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/); return ret("number", "number"); @@ -115,8 +131,7 @@ CodeMirror.defineMode("javascript", func } else if (stream.eat("/")) { stream.skipToEnd(); return ret("comment", "comment"); - } else if (state.lastType == "operator" || state.lastType == "keyword c" || - state.lastType == "sof" || /^[\[{}\(,;:]$/.test(state.lastType)) { + } else if (expressionAllowed(stream, state, 1)) { readRegexp(stream); stream.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/); return ret("regexp", "string-2"); @@ -275,8 +290,8 @@ CodeMirror.defineMode("javascript", func return false; } var state = cx.state; + cx.marked = "def"; if (state.context) { - cx.marked = "def"; if (inList(state.localVars)) return; state.localVars = {name: varname, next: state.localVars}; } else { @@ -347,10 +362,10 @@ CodeMirror.defineMode("javascript", func if (type == "default") return cont(expect(":")); if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"), statement, poplex, popcontext); - if (type == "module") return cont(pushlex("form"), pushcontext, afterModule, popcontext, poplex); if (type == "class") return cont(pushlex("form"), className, poplex); - if (type == "export") return cont(pushlex("form"), afterExport, poplex); - if (type == "import") return cont(pushlex("form"), afterImport, poplex); + if (type == "export") return cont(pushlex("stat"), afterExport, poplex); + if (type == "import") return cont(pushlex("stat"), afterImport, poplex); + if (type == "module") return cont(pushlex("form"), pattern, pushlex("}"), expect("{"), block, poplex, poplex) return pass(pushlex("stat"), expression, expect(";"), poplex); } function expression(type) { @@ -374,7 +389,8 @@ CodeMirror.defineMode("javascript", func if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); if (type == "{") return contCommasep(objprop, "}", null, maybeop); - if (type == "quasi") { return pass(quasi, maybeop); } + if (type == "quasi") return pass(quasi, maybeop); + if (type == "new") return cont(maybeTarget(noComma)); return cont(); } function maybeexpression(type) { @@ -425,6 +441,18 @@ CodeMirror.defineMode("javascript", func findFatArrow(cx.stream, cx.state); return pass(type == "{" ? statement : expressionNoComma); } + function maybeTarget(noComma) { + return function(type) { + if (type == ".") return cont(noComma ? targetNoComma : target); + else return pass(noComma ? expressionNoComma : expression); + }; + } + function target(_, value) { + if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); } + } + function targetNoComma(_, value) { + if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); } + } function maybelabel(type) { if (type == ":") return cont(poplex, statement); return pass(maybeoperatorComma, expect(";"), poplex); @@ -442,8 +470,12 @@ CodeMirror.defineMode("javascript", func return cont(afterprop); } else if (type == "jsonld-keyword") { return cont(afterprop); + } else if (type == "modifier") { + return cont(objprop) } else if (type == "[") { return cont(expression, expect("]"), afterprop); + } else if (type == "spread") { + return cont(expression); } } function getterSetter(type) { @@ -492,7 +524,9 @@ CodeMirror.defineMode("javascript", func return pass(pattern, maybetype, maybeAssign, vardefCont); } function pattern(type, value) { + if (type == "modifier") return cont(pattern) if (type == "variable") { register(value); return cont(); } + if (type == "spread") return cont(pattern); if (type == "[") return contCommasep(pattern, "]"); if (type == "{") return contCommasep(proppattern, "}"); } @@ -502,6 +536,8 @@ CodeMirror.defineMode("javascript", func return cont(maybeAssign); } if (type == "variable") cx.marked = "property"; + if (type == "spread") return cont(pattern); + if (type == "}") return pass(); return cont(expect(":"), pattern, maybeAssign); } function maybeAssign(_type, value) { @@ -572,10 +608,6 @@ CodeMirror.defineMode("javascript", func cx.marked = "property"; return cont(); } - function afterModule(type, value) { - if (type == "string") return cont(statement); - if (type == "variable") { register(value); return cont(maybeFrom); } - } function afterExport(_type, value) { if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } @@ -628,7 +660,7 @@ CodeMirror.defineMode("javascript", func lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), localVars: parserConfig.localVars, context: parserConfig.localVars && {vars: parserConfig.localVars}, - indented: 0 + indented: basecolumn || 0 }; if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") state.globalVars = parserConfig.globalVars; @@ -684,7 +716,13 @@ CodeMirror.defineMode("javascript", func helperType: jsonMode ? "json" : "javascript", jsonldMode: jsonldMode, - jsonMode: jsonMode + jsonMode: jsonMode, + + expressionAllowed: expressionAllowed, + skipExpression: function(state) { + var top = state.cc[state.cc.length - 1] + if (top == expression || top == expressionNoComma) state.cc.pop() + } }; }); diff --git a/rhodecode/public/js/mode/julia/julia.js b/rhodecode/public/js/mode/julia/julia.js --- a/rhodecode/public/js/mode/julia/julia.js +++ b/rhodecode/public/js/mode/julia/julia.js @@ -18,35 +18,34 @@ CodeMirror.defineMode("julia", function( return new RegExp("^((" + words.join(")|(") + "))\\b"); } - var operators = parserConf.operators || /^\.?[|&^\\%*+\-<>!=\/]=?|\?|~|:|\$|\.[<>]|<<=?|>>>?=?|\.[<>=]=|->?|\/\/|\bin\b/; + var operators = parserConf.operators || /^\.?[|&^\\%*+\-<>!=\/]=?|\?|~|:|\$|\.[<>]|<<=?|>>>?=?|\.[<>=]=|->?|\/\/|\bin\b(?!\()|[\u2208\u2209](?!\()/; var delimiters = parserConf.delimiters || /^[;,()[\]{}]/; - var identifiers = parserConf.identifiers|| /^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*!*/; + var identifiers = parserConf.identifiers || /^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*!*/; var blockOpeners = ["begin", "function", "type", "immutable", "let", "macro", "for", "while", "quote", "if", "else", "elseif", "try", "finally", "catch", "do"]; var blockClosers = ["end", "else", "elseif", "catch", "finally"]; - var keywordList = ['if', 'else', 'elseif', 'while', 'for', 'begin', 'let', 'end', 'do', 'try', 'catch', 'finally', 'return', 'break', 'continue', 'global', 'local', 'const', 'export', 'import', 'importall', 'using', 'function', 'macro', 'module', 'baremodule', 'type', 'immutable', 'quote', 'typealias', 'abstract', 'bitstype', 'ccall']; - var builtinList = ['true', 'false', 'enumerate', 'open', 'close', 'nothing', 'NaN', 'Inf', 'print', 'println', 'Int', 'Int8', 'Uint8', 'Int16', 'Uint16', 'Int32', 'Uint32', 'Int64', 'Uint64', 'Int128', 'Uint128', 'Bool', 'Char', 'Float16', 'Float32', 'Float64', 'Array', 'Vector', 'Matrix', 'String', 'UTF8String', 'ASCIIString', 'error', 'warn', 'info', '@printf']; + var keywordList = ['if', 'else', 'elseif', 'while', 'for', 'begin', 'let', 'end', 'do', 'try', 'catch', 'finally', 'return', 'break', 'continue', 'global', 'local', 'const', 'export', 'import', 'importall', 'using', 'function', 'macro', 'module', 'baremodule', 'type', 'immutable', 'quote', 'typealias', 'abstract', 'bitstype']; + var builtinList = ['true', 'false', 'nothing', 'NaN', 'Inf']; //var stringPrefixes = new RegExp("^[br]?('|\")") - var stringPrefixes = /^(`|'|"{3}|([br]?"))/; + var stringPrefixes = /^(`|'|"{3}|([brv]?"))/; var keywords = wordRegexp(keywordList); var builtins = wordRegexp(builtinList); var openers = wordRegexp(blockOpeners); var closers = wordRegexp(blockClosers); var macro = /^@[_A-Za-z][_A-Za-z0-9]*/; - var symbol = /^:[_A-Za-z][_A-Za-z0-9]*/; + var symbol = /^:[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*!*/; + var typeAnnotation = /^::[^.,;"{()=$\s]+({[^}]*}+)*/; - function in_array(state) { - var ch = cur_scope(state); - if(ch=="[" || ch=="{") { + function inArray(state) { + var ch = currentScope(state); + if (ch == '[') { return true; } - else { - return false; - } + return false; } - function cur_scope(state) { - if(state.scopes.length==0) { + function currentScope(state) { + if (state.scopes.length == 0) { return null; } return state.scopes[state.scopes.length - 1]; @@ -54,20 +53,34 @@ CodeMirror.defineMode("julia", function( // tokenizers function tokenBase(stream, state) { + //Handle multiline comments + if (stream.match(/^#=\s*/)) { + state.scopes.push('#='); + } + if (currentScope(state) == '#=' && stream.match(/^=#/)) { + state.scopes.pop(); + return 'comment'; + } + if (state.scopes.indexOf('#=') >= 0) { + if (!stream.match(/.*?(?=(#=|=#))/)) { + stream.skipToEnd(); + } + return 'comment'; + } + // Handle scope changes - var leaving_expr = state.leaving_expr; - if(stream.sol()) { - leaving_expr = false; + var leavingExpr = state.leavingExpr; + if (stream.sol()) { + leavingExpr = false; } - state.leaving_expr = false; - if(leaving_expr) { - if(stream.match(/^'+/)) { + state.leavingExpr = false; + if (leavingExpr) { + if (stream.match(/^'+/)) { return 'operator'; } - } - if(stream.match(/^\.{2,3}/)) { + if (stream.match(/^\.{2,3}/)) { return 'operator'; } @@ -76,56 +89,51 @@ CodeMirror.defineMode("julia", function( } var ch = stream.peek(); - // Handle Comments + + // Handle single line comments if (ch === '#') { - stream.skipToEnd(); - return 'comment'; - } - if(ch==='[') { - state.scopes.push("["); + stream.skipToEnd(); + return 'comment'; } - if(ch==='{') { - state.scopes.push("{"); + if (ch === '[') { + state.scopes.push('['); } - var scope=cur_scope(state); + var scope = currentScope(state); - if(scope==='[' && ch===']') { + if (scope == '[' && ch === ']') { state.scopes.pop(); - state.leaving_expr=true; + state.leavingExpr = true; } - if(scope==='{' && ch==='}') { + if (scope == '(' && ch === ')') { state.scopes.pop(); - state.leaving_expr=true; - } - - if(ch===')') { - state.leaving_expr = true; + state.leavingExpr = true; } var match; - if(!in_array(state) && (match=stream.match(openers, false))) { + if (!inArray(state) && (match=stream.match(openers, false))) { state.scopes.push(match); } - if(!in_array(state) && stream.match(closers, false)) { + if (!inArray(state) && stream.match(closers, false)) { state.scopes.pop(); } - if(in_array(state)) { - if(stream.match(/^end/)) { + if (inArray(state)) { + if (state.lastToken == 'end' && stream.match(/^:/)) { + return 'operator'; + } + if (stream.match(/^end/)) { return 'number'; } - } - if(stream.match(/^=>/)) { + if (stream.match(/^=>/)) { return 'operator'; } - // Handle Number Literals if (stream.match(/^[0-9\.]/, false)) { var imMatcher = RegExp(/^im\b/); @@ -134,10 +142,11 @@ CodeMirror.defineMode("julia", function( if (stream.match(/^\d*\.(?!\.)\d+([ef][\+\-]?\d+)?/i)) { floatLiteral = true; } if (stream.match(/^\d+\.(?!\.)\d*/)) { floatLiteral = true; } if (stream.match(/^\.\d+/)) { floatLiteral = true; } + if (stream.match(/^0x\.[0-9a-f]+p[\+\-]?\d+/i)) { floatLiteral = true; } if (floatLiteral) { // Float literals may be "imaginary" stream.match(imMatcher); - state.leaving_expr = true; + state.leavingExpr = true; return 'number'; } // Integers @@ -157,18 +166,27 @@ CodeMirror.defineMode("julia", function( if (intLiteral) { // Integer literals may be "long" stream.match(imMatcher); - state.leaving_expr = true; + state.leavingExpr = true; return 'number'; } } - if(stream.match(/^(::)|(<:)/)) { + if (stream.match(/^<:/)) { return 'operator'; } + if (stream.match(typeAnnotation)) { + return 'builtin'; + } + // Handle symbols - if(!leaving_expr && stream.match(symbol)) { - return 'string'; + if (!leavingExpr && stream.match(symbol) || stream.match(/:\./)) { + return 'builtin'; + } + + // Handle parametric types + if (stream.match(/^{[^}]*}(?=\()/)) { + return 'builtin'; } // Handle operators and Delimiters @@ -176,7 +194,6 @@ CodeMirror.defineMode("julia", function( return 'operator'; } - // Handle Strings if (stream.match(stringPrefixes)) { state.tokenize = tokenStringFactory(stream.current()); @@ -187,7 +204,6 @@ CodeMirror.defineMode("julia", function( return 'meta'; } - if (stream.match(delimiters)) { return null; } @@ -200,21 +216,74 @@ CodeMirror.defineMode("julia", function( return 'builtin'; } + var isDefinition = state.isDefinition || + state.lastToken == 'function' || + state.lastToken == 'macro' || + state.lastToken == 'type' || + state.lastToken == 'immutable'; if (stream.match(identifiers)) { - state.leaving_expr=true; + if (isDefinition) { + if (stream.peek() === '.') { + state.isDefinition = true; + return 'variable'; + } + state.isDefinition = false; + return 'def'; + } + if (stream.match(/^({[^}]*})*\(/, false)) { + return callOrDef(stream, state); + } + state.leavingExpr = true; return 'variable'; } + // Handle non-detected items stream.next(); return ERRORCLASS; } + function callOrDef(stream, state) { + var match = stream.match(/^(\(\s*)/); + if (match) { + if (state.firstParenPos < 0) + state.firstParenPos = state.scopes.length; + state.scopes.push('('); + state.charsAdvanced += match[1].length; + } + if (currentScope(state) == '(' && stream.match(/^\)/)) { + state.scopes.pop(); + state.charsAdvanced += 1; + if (state.scopes.length <= state.firstParenPos) { + var isDefinition = stream.match(/^\s*?=(?!=)/, false); + stream.backUp(state.charsAdvanced); + state.firstParenPos = -1; + state.charsAdvanced = 0; + if (isDefinition) + return 'def'; + return 'builtin'; + } + } + // Unfortunately javascript does not support multiline strings, so we have + // to undo anything done upto here if a function call or definition splits + // over two or more lines. + if (stream.match(/^$/g, false)) { + stream.backUp(state.charsAdvanced); + while (state.scopes.length > state.firstParenPos + 1) + state.scopes.pop(); + state.firstParenPos = -1; + state.charsAdvanced = 0; + return 'builtin'; + } + state.charsAdvanced += stream.match(/^([^()]*)/)[1].length; + return callOrDef(stream, state); + } + function tokenStringFactory(delimiter) { - while ('rub'.indexOf(delimiter.charAt(0).toLowerCase()) >= 0) { + while ('bruv'.indexOf(delimiter.charAt(0).toLowerCase()) >= 0) { delimiter = delimiter.substr(1); } - var singleline = delimiter.length == 1; + var singleline = delimiter == "'"; var OUTCLASS = 'string'; function tokenString(stream, state) { @@ -245,45 +314,41 @@ CodeMirror.defineMode("julia", function( return tokenString; } - function tokenLexer(stream, state) { - var style = state.tokenize(stream, state); - var current = stream.current(); - - // Handle '.' connected identifiers - if (current === '.') { - style = stream.match(identifiers, false) ? null : ERRORCLASS; - if (style === null && state.lastStyle === 'meta') { - // Apply 'meta' style to '.' connected identifiers when - // appropriate. - style = 'meta'; - } - return style; - } - - return style; - } - var external = { startState: function() { return { tokenize: tokenBase, scopes: [], - leaving_expr: false + lastToken: null, + leavingExpr: false, + isDefinition: false, + charsAdvanced: 0, + firstParenPos: -1 }; }, token: function(stream, state) { - var style = tokenLexer(stream, state); - state.lastStyle = style; + var style = state.tokenize(stream, state); + var current = stream.current(); + + if (current && style) { + state.lastToken = current; + } + + // Handle '.' connected identifiers + if (current === '.') { + style = stream.match(identifiers, false) || stream.match(macro, false) || + stream.match(/\(/, false) ? 'operator' : ERRORCLASS; + } return style; }, indent: function(state, textAfter) { var delta = 0; - if(textAfter=="end" || textAfter=="]" || textAfter=="}" || textAfter=="else" || textAfter=="elseif" || textAfter=="catch" || textAfter=="finally") { + if (textAfter == "end" || textAfter == "]" || textAfter == "}" || textAfter == "else" || textAfter == "elseif" || textAfter == "catch" || textAfter == "finally") { delta = -1; } - return (state.scopes.length + delta) * 4; + return (state.scopes.length + delta) * _conf.indentUnit; }, lineComment: "#", diff --git a/rhodecode/public/js/mode/markdown/markdown.js b/rhodecode/public/js/mode/markdown/markdown.js --- a/rhodecode/public/js/mode/markdown/markdown.js +++ b/rhodecode/public/js/mode/markdown/markdown.js @@ -39,8 +39,10 @@ CodeMirror.defineMode("markdown", functi if (modeCfg.underscoresBreakWords === undefined) modeCfg.underscoresBreakWords = true; - // Turn on fenced code blocks? ("```" to start/end) - if (modeCfg.fencedCodeBlocks === undefined) modeCfg.fencedCodeBlocks = false; + // Use `fencedCodeBlocks` to configure fenced code blocks. false to + // disable, string to specify a precise regexp that the fence should + // match, and true to allow three or more backticks or tildes (as + // per CommonMark). // Turn on task lists? ("- [ ] " and "- [x] ") if (modeCfg.taskLists === undefined) modeCfg.taskLists = false; @@ -49,32 +51,46 @@ CodeMirror.defineMode("markdown", functi if (modeCfg.strikethrough === undefined) modeCfg.strikethrough = false; + // Allow token types to be overridden by user-provided token types. + if (modeCfg.tokenTypeOverrides === undefined) + modeCfg.tokenTypeOverrides = {}; + var codeDepth = 0; - var header = 'header' - , code = 'comment' - , quote = 'quote' - , list1 = 'variable-2' - , list2 = 'variable-3' - , list3 = 'keyword' - , hr = 'hr' - , image = 'tag' - , formatting = 'formatting' - , linkinline = 'link' - , linkemail = 'link' - , linktext = 'link' - , linkhref = 'string' - , em = 'em' - , strong = 'strong' - , strikethrough = 'strikethrough'; + var tokenTypes = { + header: "header", + code: "comment", + quote: "quote", + list1: "variable-2", + list2: "variable-3", + list3: "keyword", + hr: "hr", + image: "tag", + formatting: "formatting", + linkInline: "link", + linkEmail: "link", + linkText: "link", + linkHref: "string", + em: "em", + strong: "strong", + strikethrough: "strikethrough" + }; + + for (var tokenType in tokenTypes) { + if (tokenTypes.hasOwnProperty(tokenType) && modeCfg.tokenTypeOverrides[tokenType]) { + tokenTypes[tokenType] = modeCfg.tokenTypeOverrides[tokenType]; + } + } var hrRE = /^([*\-_])(?:\s*\1){2,}\s*$/ , ulRE = /^[*\-+]\s+/ , olRE = /^[0-9]+([.)])\s+/ , taskListRE = /^\[(x| )\](?=\s)/ // Must follow ulRE or olRE - , atxHeaderRE = /^(#+)(?: |$)/ + , atxHeaderRE = modeCfg.allowAtxHeaderWithoutSpace ? /^(#+)/ : /^(#+)(?: |$)/ , setextHeaderRE = /^ *(?:\={1,}|-{1,})\s*$/ - , textRE = /^[^#!\[\]*_\\<>` "'(~]+/; + , textRE = /^[^#!\[\]*_\\<>` "'(~]+/ + , fencedCodeRE = new RegExp("^(" + (modeCfg.fencedCodeBlocks === true ? "~~~+|```+" : modeCfg.fencedCodeBlocks) + + ")[ \\t]*([\\w+#]*)"); function switchInline(stream, state, f) { state.f = state.inline = f; @@ -86,6 +102,9 @@ CodeMirror.defineMode("markdown", functi return f(stream, state); } + function lineIsEmpty(line) { + return !line || !/\S/.test(line.string) + } // Blocks @@ -110,7 +129,8 @@ CodeMirror.defineMode("markdown", functi state.trailingSpace = 0; state.trailingSpaceNewLine = false; // Mark this line as blank - state.thisLineHasContent = false; + state.prevLine = state.thisLine + state.thisLine = null return null; } @@ -141,10 +161,10 @@ CodeMirror.defineMode("markdown", functi var match = null; if (state.indentationDiff >= 4) { stream.skipToEnd(); - if (prevLineIsIndentedCode || !state.prevLineHasContent) { + if (prevLineIsIndentedCode || lineIsEmpty(state.prevLine)) { state.indentation -= 4; state.indentedCode = true; - return code; + return tokenTypes.code; } else { return null; } @@ -155,7 +175,8 @@ CodeMirror.defineMode("markdown", functi if (modeCfg.highlightFormatting) state.formatting = "header"; state.f = state.inline; return getType(state); - } else if (state.prevLineHasContent && !state.quote && !prevLineIsList && !prevLineIsIndentedCode && (match = stream.match(setextHeaderRE))) { + } else if (!lineIsEmpty(state.prevLine) && !state.quote && !prevLineIsList && + !prevLineIsIndentedCode && (match = stream.match(setextHeaderRE))) { state.header = match[0].charAt(0) == '=' ? 1 : 2; if (modeCfg.highlightFormatting) state.formatting = "header"; state.f = state.inline; @@ -169,8 +190,8 @@ CodeMirror.defineMode("markdown", functi return switchInline(stream, state, footnoteLink); } else if (stream.match(hrRE, true)) { state.hr = true; - return hr; - } else if ((!state.prevLineHasContent || prevLineIsList) && (stream.match(ulRE, false) || stream.match(olRE, false))) { + return tokenTypes.hr; + } else if ((lineIsEmpty(state.prevLine) || prevLineIsList) && (stream.match(ulRE, false) || stream.match(olRE, false))) { var listType = null; if (stream.match(ulRE, true)) { listType = 'ul'; @@ -178,7 +199,7 @@ CodeMirror.defineMode("markdown", functi stream.match(olRE, true); listType = 'ol'; } - state.indentation += 4; + state.indentation = stream.column() + stream.current().length; state.list = true; state.listDepth++; if (modeCfg.taskLists && stream.match(taskListRE, false)) { @@ -187,9 +208,10 @@ CodeMirror.defineMode("markdown", functi state.f = state.inline; if (modeCfg.highlightFormatting) state.formatting = ["list", "list-" + listType]; return getType(state); - } else if (modeCfg.fencedCodeBlocks && stream.match(/^```[ \t]*([\w+#]*)/, true)) { + } else if (modeCfg.fencedCodeBlocks && (match = stream.match(fencedCodeRE, true))) { + state.fencedChars = match[1] // try switching mode - state.localMode = getMode(RegExp.$1); + state.localMode = getMode(match[2]); if (state.localMode) state.localState = state.localMode.startState(); state.f = state.block = local; if (modeCfg.highlightFormatting) state.formatting = "code-block"; @@ -202,7 +224,8 @@ CodeMirror.defineMode("markdown", functi function htmlBlock(stream, state) { var style = htmlMode.token(stream, state.htmlState); - if ((htmlFound && state.htmlState.tagStart === null && !state.htmlState.context) || + if ((htmlFound && state.htmlState.tagStart === null && + (!state.htmlState.context && state.htmlState.tokenize.isInText)) || (state.md_inside && stream.current().indexOf(">") > -1)) { state.f = inlineNormal; state.block = blockNormal; @@ -212,7 +235,7 @@ CodeMirror.defineMode("markdown", functi } function local(stream, state) { - if (stream.sol() && stream.match("```", false)) { + if (state.fencedChars && stream.match(state.fencedChars, false)) { state.localMode = state.localState = null; state.f = state.block = leavingLocal; return null; @@ -220,14 +243,15 @@ CodeMirror.defineMode("markdown", functi return state.localMode.token(stream, state.localState); } else { stream.skipToEnd(); - return code; + return tokenTypes.code; } } function leavingLocal(stream, state) { - stream.match("```"); + stream.match(state.fencedChars); state.block = blockNormal; state.f = inlineNormal; + state.fencedChars = null; if (modeCfg.highlightFormatting) state.formatting = "code-block"; state.code = true; var returnType = getType(state); @@ -240,22 +264,22 @@ CodeMirror.defineMode("markdown", functi var styles = []; if (state.formatting) { - styles.push(formatting); + styles.push(tokenTypes.formatting); if (typeof state.formatting === "string") state.formatting = [state.formatting]; for (var i = 0; i < state.formatting.length; i++) { - styles.push(formatting + "-" + state.formatting[i]); + styles.push(tokenTypes.formatting + "-" + state.formatting[i]); if (state.formatting[i] === "header") { - styles.push(formatting + "-" + state.formatting[i] + "-" + state.header); + styles.push(tokenTypes.formatting + "-" + state.formatting[i] + "-" + state.header); } // Add `formatting-quote` and `formatting-quote-#` for blockquotes // Add `error` instead if the maximum blockquote nesting depth is passed if (state.formatting[i] === "quote") { if (!modeCfg.maxBlockquoteDepth || modeCfg.maxBlockquoteDepth >= state.quote) { - styles.push(formatting + "-" + state.formatting[i] + "-" + state.quote); + styles.push(tokenTypes.formatting + "-" + state.formatting[i] + "-" + state.quote); } else { styles.push("error"); } @@ -273,38 +297,36 @@ CodeMirror.defineMode("markdown", functi } if (state.linkHref) { - styles.push(linkhref, "url"); + styles.push(tokenTypes.linkHref, "url"); } else { // Only apply inline styles to non-url text - if (state.strong) { styles.push(strong); } - if (state.em) { styles.push(em); } - if (state.strikethrough) { styles.push(strikethrough); } - - if (state.linkText) { styles.push(linktext); } - - if (state.code) { styles.push(code); } + if (state.strong) { styles.push(tokenTypes.strong); } + if (state.em) { styles.push(tokenTypes.em); } + if (state.strikethrough) { styles.push(tokenTypes.strikethrough); } + if (state.linkText) { styles.push(tokenTypes.linkText); } + if (state.code) { styles.push(tokenTypes.code); } } - if (state.header) { styles.push(header); styles.push(header + "-" + state.header); } + if (state.header) { styles.push(tokenTypes.header, tokenTypes.header + "-" + state.header); } if (state.quote) { - styles.push(quote); + styles.push(tokenTypes.quote); // Add `quote-#` where the maximum for `#` is modeCfg.maxBlockquoteDepth if (!modeCfg.maxBlockquoteDepth || modeCfg.maxBlockquoteDepth >= state.quote) { - styles.push(quote + "-" + state.quote); + styles.push(tokenTypes.quote + "-" + state.quote); } else { - styles.push(quote + "-" + modeCfg.maxBlockquoteDepth); + styles.push(tokenTypes.quote + "-" + modeCfg.maxBlockquoteDepth); } } if (state.list !== false) { var listMod = (state.listDepth - 1) % 3; if (!listMod) { - styles.push(list1); + styles.push(tokenTypes.list1); } else if (listMod === 1) { - styles.push(list2); + styles.push(tokenTypes.list2); } else { - styles.push(list3); + styles.push(tokenTypes.list3); } } @@ -360,7 +382,8 @@ CodeMirror.defineMode("markdown", functi stream.next(); if (modeCfg.highlightFormatting) { var type = getType(state); - return type ? type + " formatting-escape" : "formatting-escape"; + var formattingEscape = tokenTypes.formatting + "-escape"; + return type ? type + " " + formattingEscape : formattingEscape; } } @@ -374,7 +397,7 @@ CodeMirror.defineMode("markdown", functi matchCh = (matchCh+'').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); var regex = '^\\s*(?:[^' + matchCh + '\\\\]+|\\\\\\\\|\\\\.)' + matchCh; if (stream.match(new RegExp(regex), true)) { - return linkhref; + return tokenTypes.linkHref; } } @@ -405,7 +428,7 @@ CodeMirror.defineMode("markdown", functi if (ch === '!' && stream.match(/\[[^\]]*\] ?(?:\(|\[)/, false)) { stream.match(/\[[^\]]*\]/); state.inline = state.f = linkHref; - return image; + return tokenTypes.image; } if (ch === '[' && stream.match(/.*\](\(.*\)| ?\[.*\])/, false)) { @@ -431,7 +454,7 @@ CodeMirror.defineMode("markdown", functi } else { type = ""; } - return type + linkinline; + return type + tokenTypes.linkInline; } if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, false)) { @@ -443,15 +466,14 @@ CodeMirror.defineMode("markdown", functi } else { type = ""; } - return type + linkemail; + return type + tokenTypes.linkEmail; } - if (ch === '<' && stream.match(/^\w/, false)) { - if (stream.string.indexOf(">") != -1) { - var atts = stream.string.substring(1,stream.string.indexOf(">")); - if (/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(atts)) { - state.md_inside = true; - } + if (ch === '<' && stream.match(/^(!--|\w)/, false)) { + var end = stream.string.indexOf(">", stream.pos); + if (end != -1) { + var atts = stream.string.substring(stream.start, end); + if (/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(atts)) state.md_inside = true; } stream.backUp(1); state.htmlState = CodeMirror.startState(htmlMode); @@ -553,12 +575,12 @@ CodeMirror.defineMode("markdown", functi } else { type = ""; } - return type + linkinline; + return type + tokenTypes.linkInline; } stream.match(/^[^>]+/, true); - return linkinline; + return tokenTypes.linkInline; } function linkHref(stream, state) { @@ -598,7 +620,7 @@ CodeMirror.defineMode("markdown", functi } function footnoteLink(stream, state) { - if (stream.match(/^[^\]]*\]:/, false)) { + if (stream.match(/^([^\]\\]|\\.)*\]:/, false)) { state.f = footnoteLinkInside; stream.next(); // Consume [ if (modeCfg.highlightFormatting) state.formatting = "link"; @@ -617,9 +639,9 @@ CodeMirror.defineMode("markdown", functi return returnType; } - stream.match(/^[^\]]+/, true); + stream.match(/^([^\]\\]|\\.)+/, true); - return linktext; + return tokenTypes.linkText; } function footnoteUrl(stream, state) { @@ -636,7 +658,7 @@ CodeMirror.defineMode("markdown", functi stream.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/, true); } state.f = state.inline = inlineNormal; - return linkhref + " url"; + return tokenTypes.linkHref + " url"; } var savedInlineRE = []; @@ -656,8 +678,8 @@ CodeMirror.defineMode("markdown", functi return { f: blockNormal, - prevLineHasContent: false, - thisLineHasContent: false, + prevLine: null, + thisLine: null, block: blockNormal, htmlState: null, @@ -680,7 +702,8 @@ CodeMirror.defineMode("markdown", functi quote: 0, trailingSpace: 0, trailingSpaceNewLine: false, - strikethrough: false + strikethrough: false, + fencedChars: null }; }, @@ -688,8 +711,8 @@ CodeMirror.defineMode("markdown", functi return { f: s.f, - prevLineHasContent: s.prevLineHasContent, - thisLineHasContent: s.thisLineHasContent, + prevLine: s.prevLine, + thisLine: s.thisLine, block: s.block, htmlState: s.htmlState && CodeMirror.copyState(htmlMode, s.htmlState), @@ -702,6 +725,7 @@ CodeMirror.defineMode("markdown", functi text: s.text, formatting: false, linkTitle: s.linkTitle, + code: s.code, em: s.em, strong: s.strong, strikethrough: s.strikethrough, @@ -714,7 +738,8 @@ CodeMirror.defineMode("markdown", functi indentedCode: s.indentedCode, trailingSpace: s.trailingSpace, trailingSpaceNewLine: s.trailingSpaceNewLine, - md_inside: s.md_inside + md_inside: s.md_inside, + fencedChars: s.fencedChars }; }, @@ -723,28 +748,25 @@ CodeMirror.defineMode("markdown", functi // Reset state.formatting state.formatting = false; - if (stream.sol()) { - var forceBlankLine = !!state.header || state.hr; + if (stream != state.thisLine) { + var forceBlankLine = state.header || state.hr; // Reset state.header and state.hr state.header = 0; state.hr = false; if (stream.match(/^\s*$/, true) || forceBlankLine) { - state.prevLineHasContent = false; blankLine(state); - return forceBlankLine ? this.token(stream, state) : null; - } else { - state.prevLineHasContent = state.thisLineHasContent; - state.thisLineHasContent = true; + if (!forceBlankLine) return null + state.prevLine = null } + state.prevLine = state.thisLine + state.thisLine = stream + // Reset state.taskList state.taskList = false; - // Reset state.code - state.code = false; - // Reset state.trailingSpace state.trailingSpace = 0; state.trailingSpaceNewLine = false; diff --git a/rhodecode/public/js/mode/meta.js b/rhodecode/public/js/mode/meta.js --- a/rhodecode/public/js/mode/meta.js +++ b/rhodecode/public/js/mode/meta.js @@ -14,18 +14,22 @@ CodeMirror.modeInfo = [ {name: "APL", mime: "text/apl", mode: "apl", ext: ["dyalog", "apl"]}, {name: "PGP", mimes: ["application/pgp", "application/pgp-keys", "application/pgp-signature"], mode: "asciiarmor", ext: ["pgp"]}, - {name: "ASN.1", mime: "text/x-ttcn-asn", mode: "asn.1", ext: ["asn, asn1"]}, + {name: "ASN.1", mime: "text/x-ttcn-asn", mode: "asn.1", ext: ["asn", "asn1"]}, {name: "Asterisk", mime: "text/x-asterisk", mode: "asterisk", file: /^extensions\.conf$/i}, + {name: "Brainfuck", mime: "text/x-brainfuck", mode: "brainfuck", ext: ["b", "bf"]}, {name: "C", mime: "text/x-csrc", mode: "clike", ext: ["c", "h"]}, {name: "C++", mime: "text/x-c++src", mode: "clike", ext: ["cpp", "c++", "cc", "cxx", "hpp", "h++", "hh", "hxx"], alias: ["cpp"]}, {name: "Cobol", mime: "text/x-cobol", mode: "cobol", ext: ["cob", "cpy"]}, {name: "C#", mime: "text/x-csharp", mode: "clike", ext: ["cs"], alias: ["csharp"]}, {name: "Clojure", mime: "text/x-clojure", mode: "clojure", ext: ["clj"]}, + {name: "ClojureScript", mime: "text/x-clojurescript", mode: "clojure", ext: ["cljs"]}, + {name: "Closure Stylesheets (GSS)", mime: "text/x-gss", mode: "css", ext: ["gss"]}, {name: "CMake", mime: "text/x-cmake", mode: "cmake", ext: ["cmake", "cmake.in"], file: /^CMakeLists.txt$/}, {name: "CoffeeScript", mime: "text/x-coffeescript", mode: "coffeescript", ext: ["coffee"], alias: ["coffee", "coffee-script"]}, {name: "Common Lisp", mime: "text/x-common-lisp", mode: "commonlisp", ext: ["cl", "lisp", "el"], alias: ["lisp"]}, {name: "Cypher", mime: "application/x-cypher-query", mode: "cypher", ext: ["cyp", "cypher"]}, {name: "Cython", mime: "text/x-cython", mode: "python", ext: ["pyx", "pxd", "pxi"]}, + {name: "Crystal", mime: "text/x-crystal", mode: "crystal", ext: ["cr"]}, {name: "CSS", mime: "text/css", mode: "css", ext: ["css"]}, {name: "CQL", mime: "text/x-cassandra", mode: "sql", ext: ["cql"]}, {name: "D", mime: "text/x-d", mode: "d", ext: ["d"]}, @@ -53,6 +57,7 @@ {name: "Groovy", mime: "text/x-groovy", mode: "groovy", ext: ["groovy"]}, {name: "HAML", mime: "text/x-haml", mode: "haml", ext: ["haml"]}, {name: "Haskell", mime: "text/x-haskell", mode: "haskell", ext: ["hs"]}, + {name: "Haskell (Literate)", mime: "text/x-literate-haskell", mode: "haskell-literate", ext: ["lhs"]}, {name: "Haxe", mime: "text/x-haxe", mode: "haxe", ext: ["hx"]}, {name: "HXML", mime: "text/x-hxml", mode: "haxe", ext: ["hxml"]}, {name: "ASP.NET", mime: "application/x-aspx", mode: "htmlembedded", ext: ["aspx"], alias: ["asp", "aspx"]}, @@ -66,9 +71,10 @@ mode: "javascript", ext: ["js"], alias: ["ecmascript", "js", "node"]}, {name: "JSON", mimes: ["application/json", "application/x-json"], mode: "javascript", ext: ["json", "map"], alias: ["json5"]}, {name: "JSON-LD", mime: "application/ld+json", mode: "javascript", ext: ["jsonld"], alias: ["jsonld"]}, + {name: "JSX", mime: "text/jsx", mode: "jsx", ext: ["jsx"]}, {name: "Jinja2", mime: "null", mode: "jinja2"}, {name: "Julia", mime: "text/x-julia", mode: "julia", ext: ["jl"]}, - {name: "Kotlin", mime: "text/x-kotlin", mode: "kotlin", ext: ["kt"]}, + {name: "Kotlin", mime: "text/x-kotlin", mode: "clike", ext: ["kt"]}, {name: "LESS", mime: "text/x-less", mode: "css", ext: ["less"]}, {name: "LiveScript", mime: "text/x-livescript", mode: "livescript", ext: ["ls"], alias: ["ls"]}, {name: "Lua", mime: "text/x-lua", mode: "lua", ext: ["lua"]}, @@ -81,10 +87,12 @@ {name: "MS SQL", mime: "text/x-mssql", mode: "sql"}, {name: "MySQL", mime: "text/x-mysql", mode: "sql"}, {name: "Nginx", mime: "text/x-nginx-conf", mode: "nginx", file: /nginx.*\.conf$/i}, + {name: "NSIS", mime: "text/x-nsis", mode: "nsis", ext: ["nsh", "nsi"]}, {name: "NTriples", mime: "text/n-triples", mode: "ntriples", ext: ["nt"]}, {name: "Objective C", mime: "text/x-objectivec", mode: "clike", ext: ["m", "mm"]}, {name: "OCaml", mime: "text/x-ocaml", mode: "mllike", ext: ["ml", "mli", "mll", "mly"]}, {name: "Octave", mime: "text/x-octave", mode: "octave", ext: ["m"]}, + {name: "Oz", mime: "text/x-oz", mode: "oz", ext: ["oz"]}, {name: "Pascal", mime: "text/x-pascal", mode: "pascal", ext: ["p", "pas"]}, {name: "PEG.js", mime: "null", mode: "pegjs", ext: ["jsonld"]}, {name: "Perl", mime: "text/x-perl", mode: "perl", ext: ["pl", "pm"]}, @@ -106,7 +114,7 @@ {name: "Scala", mime: "text/x-scala", mode: "clike", ext: ["scala"]}, {name: "Scheme", mime: "text/x-scheme", mode: "scheme", ext: ["scm", "ss"]}, {name: "SCSS", mime: "text/x-scss", mode: "css", ext: ["scss"]}, - {name: "Shell", mime: "text/x-sh", mode: "shell", ext: ["sh", "ksh", "bash"], alias: ["bash", "sh", "zsh"]}, + {name: "Shell", mime: "text/x-sh", mode: "shell", ext: ["sh", "ksh", "bash"], alias: ["bash", "sh", "zsh"], file: /^PKGBUILD$/}, {name: "Sieve", mime: "application/sieve", mode: "sieve", ext: ["siv", "sieve"]}, {name: "Slim", mimes: ["text/x-slim", "application/x-slim"], mode: "slim", ext: ["slim"]}, {name: "Smalltalk", mime: "text/x-stsrc", mode: "smalltalk", ext: ["st"]}, @@ -116,6 +124,7 @@ {name: "SPARQL", mime: "application/sparql-query", mode: "sparql", ext: ["rq", "sparql"], alias: ["sparul"]}, {name: "Spreadsheet", mime: "text/x-spreadsheet", mode: "spreadsheet", alias: ["excel", "formula"]}, {name: "SQL", mime: "text/x-sql", mode: "sql", ext: ["sql"]}, + {name: "Squirrel", mime: "text/x-squirrel", mode: "clike", ext: ["nut"]}, {name: "Swift", mime: "text/x-swift", mode: "swift", ext: ["swift"]}, {name: "MariaDB", mime: "text/x-mariadb", mode: "sql"}, {name: "sTeX", mime: "text/x-stex", mode: "stex"}, @@ -137,10 +146,14 @@ {name: "VBScript", mime: "text/vbscript", mode: "vbscript", ext: ["vbs"]}, {name: "Velocity", mime: "text/velocity", mode: "velocity", ext: ["vtl"]}, {name: "Verilog", mime: "text/x-verilog", mode: "verilog", ext: ["v"]}, + {name: "VHDL", mime: "text/x-vhdl", mode: "vhdl", ext: ["vhd", "vhdl"]}, {name: "XML", mimes: ["application/xml", "text/xml"], mode: "xml", ext: ["xml", "xsl", "xsd"], alias: ["rss", "wsdl", "xsd"]}, {name: "XQuery", mime: "application/xquery", mode: "xquery", ext: ["xy", "xquery"]}, {name: "YAML", mime: "text/x-yaml", mode: "yaml", ext: ["yaml", "yml"], alias: ["yml"]}, - {name: "Z80", mime: "text/x-z80", mode: "z80", ext: ["z80"]} + {name: "Z80", mime: "text/x-z80", mode: "z80", ext: ["z80"]}, + {name: "mscgen", mime: "text/x-mscgen", mode: "mscgen", ext: ["mscgen", "mscin", "msc"]}, + {name: "xu", mime: "text/x-xu", mode: "mscgen", ext: ["xu"]}, + {name: "msgenny", mime: "text/x-msgenny", mode: "mscgen", ext: ["msgenny"]} ]; // Ensure all modes have a mime property for backwards compatibility for (var i = 0; i < CodeMirror.modeInfo.length; i++) { diff --git a/rhodecode/public/js/mode/meta_ext.js b/rhodecode/public/js/mode/meta_ext.js --- a/rhodecode/public/js/mode/meta_ext.js +++ b/rhodecode/public/js/mode/meta_ext.js @@ -113,7 +113,7 @@ MIME_TO_EXT = { "text/x-fortran": {"exts": ["*.f","*.f90","*.F","*.F90","*.for","*.f77"], "mode": "fortran"}, "text/x-fsharp": {"exts": ["*.fs","*.fsi"], "mode": "mllike"}, "text/x-gas": {"exts": ["*.s","*.S"], "mode": "gas"}, -"text/x-gfm": {"exts": ["*.md","*.MD"], "mode": "markdown"}, +"text/x-gfm": {"exts": ["*.md","*.MD"], "mode": "gfm"}, "text/x-gherkin": {"exts": ["*.feature"], "mode": ""}, "text/x-glslsrc": {"exts": ["*.vert","*.frag","*.geo"], "mode": ""}, "text/x-gnuplot": {"exts": ["*.plot","*.plt"], "mode": ""}, @@ -137,11 +137,11 @@ MIME_TO_EXT = { "text/x-julia": {"exts": ["*.jl"], "mode": "julia"}, "text/x-kconfig": {"exts": ["Kconfig","*Config.in*","external.in*","standard-modules.in"], "mode": ""}, "text/x-koka": {"exts": ["*.kk","*.kki"], "mode": ""}, -"text/x-kotlin": {"exts": ["*.kt"], "mode": "kotlin"}, +"text/x-kotlin": {"exts": ["*.kt"], "mode": "clike"}, "text/x-lasso": {"exts": ["*.lasso","*.lasso[89]"], "mode": ""}, "text/x-latex": {"exts": ["*.ltx","*.text"], "mode": "stex"}, "text/x-less": {"exts": ["*.less"], "mode": "css"}, -"text/x-literate-haskell": {"exts": ["*.lhs"], "mode": ""}, +"text/x-literate-haskell": {"exts": ["*.lhs"], "mode": "haskell-literate"}, "text/x-livescript": {"exts": ["*.ls"], "mode": "livescript"}, "text/x-llvm": {"exts": ["*.ll"], "mode": ""}, "text/x-logos": {"exts": ["*.x","*.xi","*.xm","*.xmi"], "mode": ""}, @@ -162,7 +162,7 @@ MIME_TO_EXT = { "text/x-newspeak": {"exts": ["*.ns2"], "mode": ""}, "text/x-nginx-conf": {"exts": ["*.conf"], "mode": "nginx"}, "text/x-nimrod": {"exts": ["*.nim","*.nimrod"], "mode": ""}, -"text/x-nsis": {"exts": ["*.nsi","*.nsh"], "mode": ""}, +"text/x-nsis": {"exts": ["*.nsi","*.nsh"], "mode": "nsis"}, "text/x-objdump": {"exts": ["*.objdump"], "mode": ""}, "text/x-objective-c": {"exts": ["*.m","*.h"], "mode": ""}, "text/x-objective-c++": {"exts": ["*.mm","*.hh"], "mode": ""}, @@ -217,7 +217,7 @@ MIME_TO_EXT = { "text/x-vb": {"exts": ["*.vb"], "mode": "vb"}, "text/x-vbnet": {"exts": ["*.vb","*.bas"], "mode": ""}, "text/x-verilog": {"exts": ["*.v"], "mode": "verilog"}, -"text/x-vhdl": {"exts": ["*.vhdl","*.vhd"], "mode": ""}, +"text/x-vhdl": {"exts": ["*.vhdl","*.vhd"], "mode": "vhdl"}, "text/x-vim": {"exts": ["*.vim",".vimrc",".exrc",".gvimrc","_vimrc","_exrc","_gvimrc","vimrc","gvimrc"], "mode": ""}, "text/x-windows-registry": {"exts": ["*.reg"], "mode": ""}, "text/x-xtend": {"exts": ["*.xtend"], "mode": ""}, diff --git a/rhodecode/public/js/mode/nginx/nginx.js b/rhodecode/public/js/mode/nginx/nginx.js --- a/rhodecode/public/js/mode/nginx/nginx.js +++ b/rhodecode/public/js/mode/nginx/nginx.js @@ -173,6 +173,6 @@ CodeMirror.defineMode("nginx", function( }; }); -CodeMirror.defineMIME("text/nginx", "text/x-nginx-conf"); +CodeMirror.defineMIME("text/x-nginx-conf", "nginx"); }); diff --git a/rhodecode/public/js/mode/php/php.js b/rhodecode/public/js/mode/php/php.js --- a/rhodecode/public/js/mode/php/php.js +++ b/rhodecode/public/js/mode/php/php.js @@ -86,7 +86,7 @@ "die echo empty exit eval include include_once isset list require require_once return " + "print unset __halt_compiler self static parent yield insteadof finally"; var phpAtoms = "true false null TRUE FALSE NULL __CLASS__ __DIR__ __FILE__ __LINE__ __METHOD__ __FUNCTION__ __NAMESPACE__ __TRAIT__"; - var phpBuiltin = "func_num_args func_get_arg func_get_args strlen strcmp strncmp strcasecmp strncasecmp each error_reporting define defined trigger_error user_error set_error_handler restore_error_handler get_declared_classes get_loaded_extensions extension_loaded get_extension_funcs debug_backtrace constant bin2hex hex2bin sleep usleep time mktime gmmktime strftime gmstrftime strtotime date gmdate getdate localtime checkdate flush wordwrap htmlspecialchars htmlentities html_entity_decode md5 md5_file crc32 getimagesize image_type_to_mime_type phpinfo phpversion phpcredits strnatcmp strnatcasecmp substr_count strspn strcspn strtok strtoupper strtolower strpos strrpos strrev hebrev hebrevc nl2br basename dirname pathinfo stripslashes stripcslashes strstr stristr strrchr str_shuffle str_word_count strcoll substr substr_replace quotemeta ucfirst ucwords strtr addslashes addcslashes rtrim str_replace str_repeat count_chars chunk_split trim ltrim strip_tags similar_text explode implode setlocale localeconv parse_str str_pad chop strchr sprintf printf vprintf vsprintf sscanf fscanf parse_url urlencode urldecode rawurlencode rawurldecode readlink linkinfo link unlink exec system escapeshellcmd escapeshellarg passthru shell_exec proc_open proc_close rand srand getrandmax mt_rand mt_srand mt_getrandmax base64_decode base64_encode abs ceil floor round is_finite is_nan is_infinite bindec hexdec octdec decbin decoct dechex base_convert number_format fmod ip2long long2ip getenv putenv getopt microtime gettimeofday getrusage uniqid quoted_printable_decode set_time_limit get_cfg_var magic_quotes_runtime set_magic_quotes_runtime get_magic_quotes_gpc get_magic_quotes_runtime import_request_variables error_log serialize unserialize memory_get_usage var_dump var_export debug_zval_dump print_r highlight_file show_source highlight_string ini_get ini_get_all ini_set ini_alter ini_restore get_include_path set_include_path restore_include_path setcookie header headers_sent connection_aborted connection_status ignore_user_abort parse_ini_file is_uploaded_file move_uploaded_file intval floatval doubleval strval gettype settype is_null is_resource is_bool is_long is_float is_int is_integer is_double is_real is_numeric is_string is_array is_object is_scalar ereg ereg_replace eregi eregi_replace split spliti join sql_regcase dl pclose popen readfile rewind rmdir umask fclose feof fgetc fgets fgetss fread fopen fpassthru ftruncate fstat fseek ftell fflush fwrite fputs mkdir rename copy tempnam tmpfile file file_get_contents stream_select stream_context_create stream_context_set_params stream_context_set_option stream_context_get_options stream_filter_prepend stream_filter_append fgetcsv flock get_meta_tags stream_set_write_buffer set_file_buffer set_socket_blocking stream_set_blocking socket_set_blocking stream_get_meta_data stream_register_wrapper stream_wrapper_register stream_set_timeout socket_set_timeout socket_get_status realpath fnmatch fsockopen pfsockopen pack unpack get_browser crypt opendir closedir chdir getcwd rewinddir readdir dir glob fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype file_exists is_writable is_writeable is_readable is_executable is_file is_dir is_link stat lstat chown touch clearstatcache mail ob_start ob_flush ob_clean ob_end_flush ob_end_clean ob_get_flush ob_get_clean ob_get_length ob_get_level ob_get_status ob_get_contents ob_implicit_flush ob_list_handlers ksort krsort natsort natcasesort asort arsort sort rsort usort uasort uksort shuffle array_walk count end prev next reset current key min max in_array array_search extract compact array_fill range array_multisort array_push array_pop array_shift array_unshift array_splice array_slice array_merge array_merge_recursive array_keys array_values array_count_values array_reverse array_reduce array_pad array_flip array_change_key_case array_rand array_unique array_intersect array_intersect_assoc array_diff array_diff_assoc array_sum array_filter array_map array_chunk array_key_exists pos sizeof key_exists assert assert_options version_compare ftok str_rot13 aggregate session_name session_module_name session_save_path session_id session_regenerate_id session_decode session_register session_unregister session_is_registered session_encode session_start session_destroy session_unset session_set_save_handler session_cache_limiter session_cache_expire session_set_cookie_params session_get_cookie_params session_write_close preg_match preg_match_all preg_replace preg_replace_callback preg_split preg_quote preg_grep overload ctype_alnum ctype_alpha ctype_cntrl ctype_digit ctype_lower ctype_graph ctype_print ctype_punct ctype_space ctype_upper ctype_xdigit virtual apache_request_headers apache_note apache_lookup_uri apache_child_terminate apache_setenv apache_response_headers apache_get_version getallheaders mysql_connect mysql_pconnect mysql_close mysql_select_db mysql_create_db mysql_drop_db mysql_query mysql_unbuffered_query mysql_db_query mysql_list_dbs mysql_list_tables mysql_list_fields mysql_list_processes mysql_error mysql_errno mysql_affected_rows mysql_insert_id mysql_result mysql_num_rows mysql_num_fields mysql_fetch_row mysql_fetch_array mysql_fetch_assoc mysql_fetch_object mysql_data_seek mysql_fetch_lengths mysql_fetch_field mysql_field_seek mysql_free_result mysql_field_name mysql_field_table mysql_field_len mysql_field_type mysql_field_flags mysql_escape_string mysql_real_escape_string mysql_stat mysql_thread_id mysql_client_encoding mysql_get_client_info mysql_get_host_info mysql_get_proto_info mysql_get_server_info mysql_info mysql mysql_fieldname mysql_fieldtable mysql_fieldlen mysql_fieldtype mysql_fieldflags mysql_selectdb mysql_createdb mysql_dropdb mysql_freeresult mysql_numfields mysql_numrows mysql_listdbs mysql_listtables mysql_listfields mysql_db_name mysql_dbname mysql_tablename mysql_table_name pg_connect pg_pconnect pg_close pg_connection_status pg_connection_busy pg_connection_reset pg_host pg_dbname pg_port pg_tty pg_options pg_ping pg_query pg_send_query pg_cancel_query pg_fetch_result pg_fetch_row pg_fetch_assoc pg_fetch_array pg_fetch_object pg_fetch_all pg_affected_rows pg_get_result pg_result_seek pg_result_status pg_free_result pg_last_oid pg_num_rows pg_num_fields pg_field_name pg_field_num pg_field_size pg_field_type pg_field_prtlen pg_field_is_null pg_get_notify pg_get_pid pg_result_error pg_last_error pg_last_notice pg_put_line pg_end_copy pg_copy_to pg_copy_from pg_trace pg_untrace pg_lo_create pg_lo_unlink pg_lo_open pg_lo_close pg_lo_read pg_lo_write pg_lo_read_all pg_lo_import pg_lo_export pg_lo_seek pg_lo_tell pg_escape_string pg_escape_bytea pg_unescape_bytea pg_client_encoding pg_set_client_encoding pg_meta_data pg_convert pg_insert pg_update pg_delete pg_select pg_exec pg_getlastoid pg_cmdtuples pg_errormessage pg_numrows pg_numfields pg_fieldname pg_fieldsize pg_fieldtype pg_fieldnum pg_fieldprtlen pg_fieldisnull pg_freeresult pg_result pg_loreadall pg_locreate pg_lounlink pg_loopen pg_loclose pg_loread pg_lowrite pg_loimport pg_loexport http_response_code get_declared_traits getimagesizefromstring socket_import_stream stream_set_chunk_size trait_exists header_register_callback class_uses session_status session_register_shutdown echo print global static exit array empty eval isset unset die include require include_once require_once json_decode json_encode json_last_error json_last_error_msg curl_close curl_copy_handle curl_errno curl_error curl_escape curl_exec curl_file_create curl_getinfo curl_init curl_multi_add_handle curl_multi_close curl_multi_exec curl_multi_getcontent curl_multi_info_read curl_multi_init curl_multi_remove_handle curl_multi_select curl_multi_setopt curl_multi_strerror curl_pause curl_reset curl_setopt_array curl_setopt curl_share_close curl_share_init curl_share_setopt curl_strerror curl_unescape curl_version mysqli_affected_rows mysqli_autocommit mysqli_change_user mysqli_character_set_name mysqli_close mysqli_commit mysqli_connect_errno mysqli_connect_error mysqli_connect mysqli_data_seek mysqli_debug mysqli_dump_debug_info mysqli_errno mysqli_error_list mysqli_error mysqli_fetch_all mysqli_fetch_array mysqli_fetch_assoc mysqli_fetch_field_direct mysqli_fetch_field mysqli_fetch_fields mysqli_fetch_lengths mysqli_fetch_object mysqli_fetch_row mysqli_field_count mysqli_field_seek mysqli_field_tell mysqli_free_result mysqli_get_charset mysqli_get_client_info mysqli_get_client_stats mysqli_get_client_version mysqli_get_connection_stats mysqli_get_host_info mysqli_get_proto_info mysqli_get_server_info mysqli_get_server_version mysqli_info mysqli_init mysqli_insert_id mysqli_kill mysqli_more_results mysqli_multi_query mysqli_next_result mysqli_num_fields mysqli_num_rows mysqli_options mysqli_ping mysqli_prepare mysqli_query mysqli_real_connect mysqli_real_escape_string mysqli_real_query mysqli_reap_async_query mysqli_refresh mysqli_rollback mysqli_select_db mysqli_set_charset mysqli_set_local_infile_default mysqli_set_local_infile_handler mysqli_sqlstate mysqli_ssl_set mysqli_stat mysqli_stmt_init mysqli_store_result mysqli_thread_id mysqli_thread_safe mysqli_use_result mysqli_warning_count"; + var phpBuiltin = "func_num_args func_get_arg func_get_args strlen strcmp strncmp strcasecmp strncasecmp each error_reporting define defined trigger_error user_error set_error_handler restore_error_handler get_declared_classes get_loaded_extensions extension_loaded get_extension_funcs debug_backtrace constant bin2hex hex2bin sleep usleep time mktime gmmktime strftime gmstrftime strtotime date gmdate getdate localtime checkdate flush wordwrap htmlspecialchars htmlentities html_entity_decode md5 md5_file crc32 getimagesize image_type_to_mime_type phpinfo phpversion phpcredits strnatcmp strnatcasecmp substr_count strspn strcspn strtok strtoupper strtolower strpos strrpos strrev hebrev hebrevc nl2br basename dirname pathinfo stripslashes stripcslashes strstr stristr strrchr str_shuffle str_word_count strcoll substr substr_replace quotemeta ucfirst ucwords strtr addslashes addcslashes rtrim str_replace str_repeat count_chars chunk_split trim ltrim strip_tags similar_text explode implode setlocale localeconv parse_str str_pad chop strchr sprintf printf vprintf vsprintf sscanf fscanf parse_url urlencode urldecode rawurlencode rawurldecode readlink linkinfo link unlink exec system escapeshellcmd escapeshellarg passthru shell_exec proc_open proc_close rand srand getrandmax mt_rand mt_srand mt_getrandmax base64_decode base64_encode abs ceil floor round is_finite is_nan is_infinite bindec hexdec octdec decbin decoct dechex base_convert number_format fmod ip2long long2ip getenv putenv getopt microtime gettimeofday getrusage uniqid quoted_printable_decode set_time_limit get_cfg_var magic_quotes_runtime set_magic_quotes_runtime get_magic_quotes_gpc get_magic_quotes_runtime import_request_variables error_log serialize unserialize memory_get_usage var_dump var_export debug_zval_dump print_r highlight_file show_source highlight_string ini_get ini_get_all ini_set ini_alter ini_restore get_include_path set_include_path restore_include_path setcookie header headers_sent connection_aborted connection_status ignore_user_abort parse_ini_file is_uploaded_file move_uploaded_file intval floatval doubleval strval gettype settype is_null is_resource is_bool is_long is_float is_int is_integer is_double is_real is_numeric is_string is_array is_object is_scalar ereg ereg_replace eregi eregi_replace split spliti join sql_regcase dl pclose popen readfile rewind rmdir umask fclose feof fgetc fgets fgetss fread fopen fpassthru ftruncate fstat fseek ftell fflush fwrite fputs mkdir rename copy tempnam tmpfile file file_get_contents file_put_contents stream_select stream_context_create stream_context_set_params stream_context_set_option stream_context_get_options stream_filter_prepend stream_filter_append fgetcsv flock get_meta_tags stream_set_write_buffer set_file_buffer set_socket_blocking stream_set_blocking socket_set_blocking stream_get_meta_data stream_register_wrapper stream_wrapper_register stream_set_timeout socket_set_timeout socket_get_status realpath fnmatch fsockopen pfsockopen pack unpack get_browser crypt opendir closedir chdir getcwd rewinddir readdir dir glob fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype file_exists is_writable is_writeable is_readable is_executable is_file is_dir is_link stat lstat chown touch clearstatcache mail ob_start ob_flush ob_clean ob_end_flush ob_end_clean ob_get_flush ob_get_clean ob_get_length ob_get_level ob_get_status ob_get_contents ob_implicit_flush ob_list_handlers ksort krsort natsort natcasesort asort arsort sort rsort usort uasort uksort shuffle array_walk count end prev next reset current key min max in_array array_search extract compact array_fill range array_multisort array_push array_pop array_shift array_unshift array_splice array_slice array_merge array_merge_recursive array_keys array_values array_count_values array_reverse array_reduce array_pad array_flip array_change_key_case array_rand array_unique array_intersect array_intersect_assoc array_diff array_diff_assoc array_sum array_filter array_map array_chunk array_key_exists pos sizeof key_exists assert assert_options version_compare ftok str_rot13 aggregate session_name session_module_name session_save_path session_id session_regenerate_id session_decode session_register session_unregister session_is_registered session_encode session_start session_destroy session_unset session_set_save_handler session_cache_limiter session_cache_expire session_set_cookie_params session_get_cookie_params session_write_close preg_match preg_match_all preg_replace preg_replace_callback preg_split preg_quote preg_grep overload ctype_alnum ctype_alpha ctype_cntrl ctype_digit ctype_lower ctype_graph ctype_print ctype_punct ctype_space ctype_upper ctype_xdigit virtual apache_request_headers apache_note apache_lookup_uri apache_child_terminate apache_setenv apache_response_headers apache_get_version getallheaders mysql_connect mysql_pconnect mysql_close mysql_select_db mysql_create_db mysql_drop_db mysql_query mysql_unbuffered_query mysql_db_query mysql_list_dbs mysql_list_tables mysql_list_fields mysql_list_processes mysql_error mysql_errno mysql_affected_rows mysql_insert_id mysql_result mysql_num_rows mysql_num_fields mysql_fetch_row mysql_fetch_array mysql_fetch_assoc mysql_fetch_object mysql_data_seek mysql_fetch_lengths mysql_fetch_field mysql_field_seek mysql_free_result mysql_field_name mysql_field_table mysql_field_len mysql_field_type mysql_field_flags mysql_escape_string mysql_real_escape_string mysql_stat mysql_thread_id mysql_client_encoding mysql_get_client_info mysql_get_host_info mysql_get_proto_info mysql_get_server_info mysql_info mysql mysql_fieldname mysql_fieldtable mysql_fieldlen mysql_fieldtype mysql_fieldflags mysql_selectdb mysql_createdb mysql_dropdb mysql_freeresult mysql_numfields mysql_numrows mysql_listdbs mysql_listtables mysql_listfields mysql_db_name mysql_dbname mysql_tablename mysql_table_name pg_connect pg_pconnect pg_close pg_connection_status pg_connection_busy pg_connection_reset pg_host pg_dbname pg_port pg_tty pg_options pg_ping pg_query pg_send_query pg_cancel_query pg_fetch_result pg_fetch_row pg_fetch_assoc pg_fetch_array pg_fetch_object pg_fetch_all pg_affected_rows pg_get_result pg_result_seek pg_result_status pg_free_result pg_last_oid pg_num_rows pg_num_fields pg_field_name pg_field_num pg_field_size pg_field_type pg_field_prtlen pg_field_is_null pg_get_notify pg_get_pid pg_result_error pg_last_error pg_last_notice pg_put_line pg_end_copy pg_copy_to pg_copy_from pg_trace pg_untrace pg_lo_create pg_lo_unlink pg_lo_open pg_lo_close pg_lo_read pg_lo_write pg_lo_read_all pg_lo_import pg_lo_export pg_lo_seek pg_lo_tell pg_escape_string pg_escape_bytea pg_unescape_bytea pg_client_encoding pg_set_client_encoding pg_meta_data pg_convert pg_insert pg_update pg_delete pg_select pg_exec pg_getlastoid pg_cmdtuples pg_errormessage pg_numrows pg_numfields pg_fieldname pg_fieldsize pg_fieldtype pg_fieldnum pg_fieldprtlen pg_fieldisnull pg_freeresult pg_result pg_loreadall pg_locreate pg_lounlink pg_loopen pg_loclose pg_loread pg_lowrite pg_loimport pg_loexport http_response_code get_declared_traits getimagesizefromstring socket_import_stream stream_set_chunk_size trait_exists header_register_callback class_uses session_status session_register_shutdown echo print global static exit array empty eval isset unset die include require include_once require_once json_decode json_encode json_last_error json_last_error_msg curl_close curl_copy_handle curl_errno curl_error curl_escape curl_exec curl_file_create curl_getinfo curl_init curl_multi_add_handle curl_multi_close curl_multi_exec curl_multi_getcontent curl_multi_info_read curl_multi_init curl_multi_remove_handle curl_multi_select curl_multi_setopt curl_multi_strerror curl_pause curl_reset curl_setopt_array curl_setopt curl_share_close curl_share_init curl_share_setopt curl_strerror curl_unescape curl_version mysqli_affected_rows mysqli_autocommit mysqli_change_user mysqli_character_set_name mysqli_close mysqli_commit mysqli_connect_errno mysqli_connect_error mysqli_connect mysqli_data_seek mysqli_debug mysqli_dump_debug_info mysqli_errno mysqli_error_list mysqli_error mysqli_fetch_all mysqli_fetch_array mysqli_fetch_assoc mysqli_fetch_field_direct mysqli_fetch_field mysqli_fetch_fields mysqli_fetch_lengths mysqli_fetch_object mysqli_fetch_row mysqli_field_count mysqli_field_seek mysqli_field_tell mysqli_free_result mysqli_get_charset mysqli_get_client_info mysqli_get_client_stats mysqli_get_client_version mysqli_get_connection_stats mysqli_get_host_info mysqli_get_proto_info mysqli_get_server_info mysqli_get_server_version mysqli_info mysqli_init mysqli_insert_id mysqli_kill mysqli_more_results mysqli_multi_query mysqli_next_result mysqli_num_fields mysqli_num_rows mysqli_options mysqli_ping mysqli_prepare mysqli_query mysqli_real_connect mysqli_real_escape_string mysqli_real_query mysqli_reap_async_query mysqli_refresh mysqli_rollback mysqli_select_db mysqli_set_charset mysqli_set_local_infile_default mysqli_set_local_infile_handler mysqli_sqlstate mysqli_ssl_set mysqli_stat mysqli_stmt_init mysqli_store_result mysqli_thread_id mysqli_thread_safe mysqli_use_result mysqli_warning_count"; CodeMirror.registerHelper("hintWords", "php", [phpKeywords, phpAtoms, phpBuiltin].join(" ").split(" ")); CodeMirror.registerHelper("wordChars", "php", /[\w$]/); @@ -105,14 +105,15 @@ return "variable-2"; }, "<": function(stream, state) { - if (stream.match(/<")) { state.curMode = htmlMode; state.curState = state.html; + if (!state.php.context.prev) state.php = null; return "meta"; } else { return phpMode.token(stream, state.curState); @@ -190,7 +193,8 @@ return { startState: function() { - var html = CodeMirror.startState(htmlMode), php = CodeMirror.startState(phpMode); + var html = CodeMirror.startState(htmlMode) + var php = parserConfig.startOpen ? CodeMirror.startState(phpMode) : null return {html: html, php: php, curMode: parserConfig.startOpen ? phpMode : htmlMode, @@ -200,7 +204,7 @@ copyState: function(state) { var html = state.html, htmlNew = CodeMirror.copyState(htmlMode, html), - php = state.php, phpNew = CodeMirror.copyState(phpMode, php), cur; + php = state.php, phpNew = php && CodeMirror.copyState(phpMode, php), cur; if (state.curMode == htmlMode) cur = htmlNew; else cur = phpNew; return {html: htmlNew, php: phpNew, curMode: state.curMode, curState: cur, diff --git a/rhodecode/public/js/mode/python/python.js b/rhodecode/public/js/mode/python/python.js --- a/rhodecode/public/js/mode/python/python.js +++ b/rhodecode/public/js/mode/python/python.js @@ -48,18 +48,18 @@ CodeMirror.defineMode("python", function(conf, parserConf) { var ERRORCLASS = "error"; - var singleDelimiters = parserConf.singleDelimiters || new RegExp("^[\\(\\)\\[\\]\\{\\}@,:`=;\\.]"); - var doubleOperators = parserConf.doubleOperators || new RegExp("^((==)|(!=)|(<=)|(>=)|(<>)|(<<)|(>>)|(//)|(\\*\\*))"); - var doubleDelimiters = parserConf.doubleDelimiters || new RegExp("^((\\+=)|(\\-=)|(\\*=)|(%=)|(/=)|(&=)|(\\|=)|(\\^=))"); - var tripleDelimiters = parserConf.tripleDelimiters || new RegExp("^((//=)|(>>=)|(<<=)|(\\*\\*=))"); + var singleDelimiters = parserConf.singleDelimiters || /^[\(\)\[\]\{\}@,:`=;\.]/; + var doubleOperators = parserConf.doubleOperators || /^([!<>]==|<>|<<|>>|\/\/|\*\*)/; + var doubleDelimiters = parserConf.doubleDelimiters || /^(\+=|\-=|\*=|%=|\/=|&=|\|=|\^=)/; + var tripleDelimiters = parserConf.tripleDelimiters || /^(\/\/=|>>=|<<=|\*\*=)/; if (parserConf.version && parseInt(parserConf.version, 10) == 3){ // since http://legacy.python.org/dev/peps/pep-0465/ @ is also an operator - var singleOperators = parserConf.singleOperators || new RegExp("^[\\+\\-\\*/%&|\\^~<>!@]"); - var identifiers = parserConf.identifiers|| new RegExp("^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*"); + var singleOperators = parserConf.singleOperators || /^[\+\-\*\/%&|\^~<>!@]/; + var identifiers = parserConf.identifiers|| /^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*/; } else { - var singleOperators = parserConf.singleOperators || new RegExp("^[\\+\\-\\*/%&|\\^~<>!]"); - var identifiers = parserConf.identifiers|| new RegExp("^[_A-Za-z][_A-Za-z0-9]*"); + var singleOperators = parserConf.singleOperators || /^[\+\-\*\/%&|\^~<>!]/; + var identifiers = parserConf.identifiers|| /^[_A-Za-z][_A-Za-z0-9]*/; } var hangingIndent = parserConf.hangingIndent || conf.indentUnit; @@ -160,13 +160,16 @@ // Handle operators and Delimiters if (stream.match(tripleDelimiters) || stream.match(doubleDelimiters)) - return null; + return "punctuation"; if (stream.match(doubleOperators) || stream.match(singleOperators)) return "operator"; if (stream.match(singleDelimiters)) - return null; + return "punctuation"; + + if (state.lastToken == "." && stream.match(identifiers)) + return "property"; if (stream.match(keywords) || stream.match(wordOperators)) return "keyword"; @@ -246,17 +249,6 @@ var style = state.tokenize(stream, state); var current = stream.current(); - // Handle '.' connected identifiers - if (current == ".") { - style = stream.match(identifiers, false) ? null : ERRORCLASS; - if (style == null && state.lastStyle == "meta") { - // Apply 'meta' style to '.' connected identifiers when - // appropriate. - style = "meta"; - } - return style; - } - // Handle decorators if (current == "@"){ if(parserConf.version && parseInt(parserConf.version, 10) == 3){ @@ -267,7 +259,7 @@ } if ((style == "variable" || style == "builtin") - && state.lastStyle == "meta") + && state.lastToken == "meta") style = "meta"; // Handle scope changes. @@ -300,7 +292,6 @@ return { tokenize: tokenBase, scopes: [{offset: basecolumn || 0, type: "py", align: null}], - lastStyle: null, lastToken: null, lambda: false, dedent: 0 @@ -312,11 +303,9 @@ if (addErr) state.errorToken = false; var style = tokenLexer(stream, state); - state.lastStyle = style; - - var current = stream.current(); - if (current && style) - state.lastToken = current; + if (style && style != "comment") + state.lastToken = (style == "keyword" || style == "punctuation") ? stream.current() : style; + if (style == "punctuation") style = null; if (stream.eol() && state.lambda) state.lambda = false; diff --git a/rhodecode/public/js/mode/rpm/rpm.js b/rhodecode/public/js/mode/rpm/rpm.js --- a/rhodecode/public/js/mode/rpm/rpm.js +++ b/rhodecode/public/js/mode/rpm/rpm.js @@ -34,10 +34,10 @@ CodeMirror.defineMIME("text/x-rpm-change // Quick and dirty spec file highlighting CodeMirror.defineMode("rpm-spec", function() { - var arch = /^(i386|i586|i686|x86_64|ppc64|ppc|ia64|s390x|s390|sparc64|sparcv9|sparc|noarch|alphaev6|alpha|hppa|mipsel)/; + var arch = /^(i386|i586|i686|x86_64|ppc64le|ppc64|ppc|ia64|s390x|s390|sparc64|sparcv9|sparc|noarch|alphaev6|alpha|hppa|mipsel)/; - var preamble = /^(Name|Version|Release|License|Summary|Url|Group|Source|BuildArch|BuildRequires|BuildRoot|AutoReqProv|Provides|Requires(\(\w+\))?|Obsoletes|Conflicts|Recommends|Source\d*|Patch\d*|ExclusiveArch|NoSource|Supplements):/; - var section = /^%(debug_package|package|description|prep|build|install|files|clean|changelog|preinstall|preun|postinstall|postun|pre|post|triggerin|triggerun|pretrans|posttrans|verifyscript|check|triggerpostun|triggerprein|trigger)/; + var preamble = /^[a-zA-Z0-9()]+:/; + var section = /^%(debug_package|package|description|prep|build|install|files|clean|changelog|preinstall|preun|postinstall|postun|pretrans|posttrans|pre|post|triggerin|triggerun|verifyscript|check|triggerpostun|triggerprein|trigger)/; var control_flow_complex = /^%(ifnarch|ifarch|if)/; // rpm control flow macros var control_flow_simple = /^%(else|endif)/; // rpm control flow macros var operators = /^(\!|\?|\<\=|\<|\>\=|\>|\=\=|\&\&|\|\|)/; // operators in control flow macros @@ -55,8 +55,8 @@ CodeMirror.defineMode("rpm-spec", functi if (ch == "#") { stream.skipToEnd(); return "comment"; } if (stream.sol()) { - if (stream.match(preamble)) { return "preamble"; } - if (stream.match(section)) { return "section"; } + if (stream.match(preamble)) { return "header"; } + if (stream.match(section)) { return "atom"; } } if (stream.match(/^\$\w+/)) { return "def"; } // Variables like '$RPM_BUILD_ROOT' @@ -73,21 +73,29 @@ CodeMirror.defineMode("rpm-spec", functi if (stream.eol()) { state.controlFlow = false; } } - if (stream.match(arch)) { return "number"; } + if (stream.match(arch)) { + if (stream.eol()) { state.controlFlow = false; } + return "number"; + } // Macros like '%make_install' or '%attr(0775,root,root)' if (stream.match(/^%[\w]+/)) { if (stream.match(/^\(/)) { state.macroParameters = true; } - return "macro"; + return "keyword"; } if (state.macroParameters) { if (stream.match(/^\d+/)) { return "number";} if (stream.match(/^\)/)) { state.macroParameters = false; - return "macro"; + return "keyword"; } } - if (stream.match(/^%\{\??[\w \-]+\}/)) { return "macro"; } // Macros like '%{defined fedora}' + + // Macros like '%{defined fedora}' + if (stream.match(/^%\{\??[\w \-\:\!]+\}/)) { + if (stream.eol()) { state.controlFlow = false; } + return "def"; + } //TODO: Include bash script sub-parser (CodeMirror supports that) stream.next(); diff --git a/rhodecode/public/js/mode/ruby/ruby.js b/rhodecode/public/js/mode/ruby/ruby.js --- a/rhodecode/public/js/mode/ruby/ruby.js +++ b/rhodecode/public/js/mode/ruby/ruby.js @@ -25,7 +25,7 @@ CodeMirror.defineMode("ruby", function(c "caller", "lambda", "proc", "public", "protected", "private", "require", "load", "require_relative", "extend", "autoload", "__END__", "__FILE__", "__LINE__", "__dir__" ]); - var indentWords = wordObj(["def", "class", "case", "for", "while", "module", "then", + var indentWords = wordObj(["def", "class", "case", "for", "while", "until", "module", "then", "catch", "loop", "proc", "begin"]); var dedentWords = wordObj(["end", "until"]); var matching = {"[": "]", "{": "}", "(": ")"}; @@ -37,7 +37,6 @@ CodeMirror.defineMode("ruby", function(c } function tokenBase(stream, state) { - curPunc = null; if (stream.sol() && stream.match("=begin") && stream.eol()) { state.tokenize.push(readBlockComment); return "comment"; @@ -232,6 +231,7 @@ CodeMirror.defineMode("ruby", function(c }, token: function(stream, state) { + curPunc = null; if (stream.sol()) state.indented = stream.indentation(); var style = state.tokenize[state.tokenize.length-1](stream, state), kwtype; var thisTok = curPunc; @@ -275,7 +275,7 @@ CodeMirror.defineMode("ruby", function(c (state.continuedLine ? config.indentUnit : 0); }, - electricChars: "}de", // enD and rescuE + electricInput: /^\s*(?:end|rescue|\})$/, lineComment: "#" }; }); diff --git a/rhodecode/public/js/mode/rust/rust.js b/rhodecode/public/js/mode/rust/rust.js --- a/rhodecode/public/js/mode/rust/rust.js +++ b/rhodecode/public/js/mode/rust/rust.js @@ -3,449 +3,69 @@ (function(mod) { if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../../lib/codemirror")); + mod(require("../../lib/codemirror"), require("../../addon/mode/simple")); else if (typeof define == "function" && define.amd) // AMD - define(["../../lib/codemirror"], mod); + define(["../../lib/codemirror", "../../addon/mode/simple"], mod); else // Plain browser env mod(CodeMirror); })(function(CodeMirror) { "use strict"; -CodeMirror.defineMode("rust", function() { - var indentUnit = 4, altIndentUnit = 2; - var valKeywords = { - "if": "if-style", "while": "if-style", "loop": "else-style", "else": "else-style", - "do": "else-style", "ret": "else-style", "fail": "else-style", - "break": "atom", "cont": "atom", "const": "let", "resource": "fn", - "let": "let", "fn": "fn", "for": "for", "alt": "alt", "iface": "iface", - "impl": "impl", "type": "type", "enum": "enum", "mod": "mod", - "as": "op", "true": "atom", "false": "atom", "assert": "op", "check": "op", - "claim": "op", "native": "ignore", "unsafe": "ignore", "import": "else-style", - "export": "else-style", "copy": "op", "log": "op", "log_err": "op", - "use": "op", "bind": "op", "self": "atom", "struct": "enum" - }; - var typeKeywords = function() { - var keywords = {"fn": "fn", "block": "fn", "obj": "obj"}; - var atoms = "bool uint int i8 i16 i32 i64 u8 u16 u32 u64 float f32 f64 str char".split(" "); - for (var i = 0, e = atoms.length; i < e; ++i) keywords[atoms[i]] = "atom"; - return keywords; - }(); - var operatorChar = /[+\-*&%=<>!?|\.@]/; - - // Tokenizer - - // Used as scratch variable to communicate multiple values without - // consing up tons of objects. - var tcat, content; - function r(tc, style) { - tcat = tc; - return style; - } - - function tokenBase(stream, state) { - var ch = stream.next(); - if (ch == '"') { - state.tokenize = tokenString; - return state.tokenize(stream, state); - } - if (ch == "'") { - tcat = "atom"; - if (stream.eat("\\")) { - if (stream.skipTo("'")) { stream.next(); return "string"; } - else { return "error"; } - } else { - stream.next(); - return stream.eat("'") ? "string" : "error"; - } - } - if (ch == "/") { - if (stream.eat("/")) { stream.skipToEnd(); return "comment"; } - if (stream.eat("*")) { - state.tokenize = tokenComment(1); - return state.tokenize(stream, state); - } - } - if (ch == "#") { - if (stream.eat("[")) { tcat = "open-attr"; return null; } - stream.eatWhile(/\w/); - return r("macro", "meta"); - } - if (ch == ":" && stream.match(":<")) { - return r("op", null); - } - if (ch.match(/\d/) || (ch == "." && stream.eat(/\d/))) { - var flp = false; - if (!stream.match(/^x[\da-f]+/i) && !stream.match(/^b[01]+/)) { - stream.eatWhile(/\d/); - if (stream.eat(".")) { flp = true; stream.eatWhile(/\d/); } - if (stream.match(/^e[+\-]?\d+/i)) { flp = true; } - } - if (flp) stream.match(/^f(?:32|64)/); - else stream.match(/^[ui](?:8|16|32|64)/); - return r("atom", "number"); - } - if (ch.match(/[()\[\]{}:;,]/)) return r(ch, null); - if (ch == "-" && stream.eat(">")) return r("->", null); - if (ch.match(operatorChar)) { - stream.eatWhile(operatorChar); - return r("op", null); - } - stream.eatWhile(/\w/); - content = stream.current(); - if (stream.match(/^::\w/)) { - stream.backUp(1); - return r("prefix", "variable-2"); - } - if (state.keywords.propertyIsEnumerable(content)) - return r(state.keywords[content], content.match(/true|false/) ? "atom" : "keyword"); - return r("name", "variable"); - } - - function tokenString(stream, state) { - var ch, escaped = false; - while (ch = stream.next()) { - if (ch == '"' && !escaped) { - state.tokenize = tokenBase; - return r("atom", "string"); - } - escaped = !escaped && ch == "\\"; - } - // Hack to not confuse the parser when a string is split in - // pieces. - return r("op", "string"); - } - - function tokenComment(depth) { - return function(stream, state) { - var lastCh = null, ch; - while (ch = stream.next()) { - if (ch == "/" && lastCh == "*") { - if (depth == 1) { - state.tokenize = tokenBase; - break; - } else { - state.tokenize = tokenComment(depth - 1); - return state.tokenize(stream, state); - } - } - if (ch == "*" && lastCh == "/") { - state.tokenize = tokenComment(depth + 1); - return state.tokenize(stream, state); - } - lastCh = ch; - } - return "comment"; - }; - } - - // Parser - - var cx = {state: null, stream: null, marked: null, cc: null}; - function pass() { - for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); - } - function cont() { - pass.apply(null, arguments); - return true; - } - - function pushlex(type, info) { - var result = function() { - var state = cx.state; - state.lexical = {indented: state.indented, column: cx.stream.column(), - type: type, prev: state.lexical, info: info}; - }; - result.lex = true; - return result; - } - function poplex() { - var state = cx.state; - if (state.lexical.prev) { - if (state.lexical.type == ")") - state.indented = state.lexical.indented; - state.lexical = state.lexical.prev; - } - } - function typecx() { cx.state.keywords = typeKeywords; } - function valcx() { cx.state.keywords = valKeywords; } - poplex.lex = typecx.lex = valcx.lex = true; - - function commasep(comb, end) { - function more(type) { - if (type == ",") return cont(comb, more); - if (type == end) return cont(); - return cont(more); - } - return function(type) { - if (type == end) return cont(); - return pass(comb, more); - }; - } +CodeMirror.defineSimpleMode("rust",{ + start: [ + // string and byte string + {regex: /b?"/, token: "string", next: "string"}, + // raw string and raw byte string + {regex: /b?r"/, token: "string", next: "string_raw"}, + {regex: /b?r#+"/, token: "string", next: "string_raw_hash"}, + // character + {regex: /'(?:[^'\\]|\\(?:[nrt0'"]|x[\da-fA-F]{2}|u\{[\da-fA-F]{6}\}))'/, token: "string-2"}, + // byte + {regex: /b'(?:[^']|\\(?:['\\nrt0]|x[\da-fA-F]{2}))'/, token: "string-2"}, - function stat_of(comb, tag) { - return cont(pushlex("stat", tag), comb, poplex, block); - } - function block(type) { - if (type == "}") return cont(); - if (type == "let") return stat_of(letdef1, "let"); - if (type == "fn") return stat_of(fndef); - if (type == "type") return cont(pushlex("stat"), tydef, endstatement, poplex, block); - if (type == "enum") return stat_of(enumdef); - if (type == "mod") return stat_of(mod); - if (type == "iface") return stat_of(iface); - if (type == "impl") return stat_of(impl); - if (type == "open-attr") return cont(pushlex("]"), commasep(expression, "]"), poplex); - if (type == "ignore" || type.match(/[\]\);,]/)) return cont(block); - return pass(pushlex("stat"), expression, poplex, endstatement, block); - } - function endstatement(type) { - if (type == ";") return cont(); - return pass(); - } - function expression(type) { - if (type == "atom" || type == "name") return cont(maybeop); - if (type == "{") return cont(pushlex("}"), exprbrace, poplex); - if (type.match(/[\[\(]/)) return matchBrackets(type, expression); - if (type.match(/[\]\)\};,]/)) return pass(); - if (type == "if-style") return cont(expression, expression); - if (type == "else-style" || type == "op") return cont(expression); - if (type == "for") return cont(pattern, maybetype, inop, expression, expression); - if (type == "alt") return cont(expression, altbody); - if (type == "fn") return cont(fndef); - if (type == "macro") return cont(macro); - return cont(); - } - function maybeop(type) { - if (content == ".") return cont(maybeprop); - if (content == "::<"){return cont(typarams, maybeop);} - if (type == "op" || content == ":") return cont(expression); - if (type == "(" || type == "[") return matchBrackets(type, expression); - return pass(); - } - function maybeprop() { - if (content.match(/^\w+$/)) {cx.marked = "variable"; return cont(maybeop);} - return pass(expression); - } - function exprbrace(type) { - if (type == "op") { - if (content == "|") return cont(blockvars, poplex, pushlex("}", "block"), block); - if (content == "||") return cont(poplex, pushlex("}", "block"), block); - } - if (content == "mutable" || (content.match(/^\w+$/) && cx.stream.peek() == ":" - && !cx.stream.match("::", false))) - return pass(record_of(expression)); - return pass(block); - } - function record_of(comb) { - function ro(type) { - if (content == "mutable" || content == "with") {cx.marked = "keyword"; return cont(ro);} - if (content.match(/^\w*$/)) {cx.marked = "variable"; return cont(ro);} - if (type == ":") return cont(comb, ro); - if (type == "}") return cont(); - return cont(ro); - } - return ro; - } - function blockvars(type) { - if (type == "name") {cx.marked = "def"; return cont(blockvars);} - if (type == "op" && content == "|") return cont(); - return cont(blockvars); - } - - function letdef1(type) { - if (type.match(/[\]\)\};]/)) return cont(); - if (content == "=") return cont(expression, letdef2); - if (type == ",") return cont(letdef1); - return pass(pattern, maybetype, letdef1); - } - function letdef2(type) { - if (type.match(/[\]\)\};,]/)) return pass(letdef1); - else return pass(expression, letdef2); - } - function maybetype(type) { - if (type == ":") return cont(typecx, rtype, valcx); - return pass(); - } - function inop(type) { - if (type == "name" && content == "in") {cx.marked = "keyword"; return cont();} - return pass(); - } - function fndef(type) { - if (content == "@" || content == "~") {cx.marked = "keyword"; return cont(fndef);} - if (type == "name") {cx.marked = "def"; return cont(fndef);} - if (content == "<") return cont(typarams, fndef); - if (type == "{") return pass(expression); - if (type == "(") return cont(pushlex(")"), commasep(argdef, ")"), poplex, fndef); - if (type == "->") return cont(typecx, rtype, valcx, fndef); - if (type == ";") return cont(); - return cont(fndef); - } - function tydef(type) { - if (type == "name") {cx.marked = "def"; return cont(tydef);} - if (content == "<") return cont(typarams, tydef); - if (content == "=") return cont(typecx, rtype, valcx); - return cont(tydef); - } - function enumdef(type) { - if (type == "name") {cx.marked = "def"; return cont(enumdef);} - if (content == "<") return cont(typarams, enumdef); - if (content == "=") return cont(typecx, rtype, valcx, endstatement); - if (type == "{") return cont(pushlex("}"), typecx, enumblock, valcx, poplex); - return cont(enumdef); - } - function enumblock(type) { - if (type == "}") return cont(); - if (type == "(") return cont(pushlex(")"), commasep(rtype, ")"), poplex, enumblock); - if (content.match(/^\w+$/)) cx.marked = "def"; - return cont(enumblock); - } - function mod(type) { - if (type == "name") {cx.marked = "def"; return cont(mod);} - if (type == "{") return cont(pushlex("}"), block, poplex); - return pass(); - } - function iface(type) { - if (type == "name") {cx.marked = "def"; return cont(iface);} - if (content == "<") return cont(typarams, iface); - if (type == "{") return cont(pushlex("}"), block, poplex); - return pass(); - } - function impl(type) { - if (content == "<") return cont(typarams, impl); - if (content == "of" || content == "for") {cx.marked = "keyword"; return cont(rtype, impl);} - if (type == "name") {cx.marked = "def"; return cont(impl);} - if (type == "{") return cont(pushlex("}"), block, poplex); - return pass(); - } - function typarams() { - if (content == ">") return cont(); - if (content == ",") return cont(typarams); - if (content == ":") return cont(rtype, typarams); - return pass(rtype, typarams); - } - function argdef(type) { - if (type == "name") {cx.marked = "def"; return cont(argdef);} - if (type == ":") return cont(typecx, rtype, valcx); - return pass(); - } - function rtype(type) { - if (type == "name") {cx.marked = "variable-3"; return cont(rtypemaybeparam); } - if (content == "mutable") {cx.marked = "keyword"; return cont(rtype);} - if (type == "atom") return cont(rtypemaybeparam); - if (type == "op" || type == "obj") return cont(rtype); - if (type == "fn") return cont(fntype); - if (type == "{") return cont(pushlex("{"), record_of(rtype), poplex); - return matchBrackets(type, rtype); - } - function rtypemaybeparam() { - if (content == "<") return cont(typarams); - return pass(); - } - function fntype(type) { - if (type == "(") return cont(pushlex("("), commasep(rtype, ")"), poplex, fntype); - if (type == "->") return cont(rtype); - return pass(); - } - function pattern(type) { - if (type == "name") {cx.marked = "def"; return cont(patternmaybeop);} - if (type == "atom") return cont(patternmaybeop); - if (type == "op") return cont(pattern); - if (type.match(/[\]\)\};,]/)) return pass(); - return matchBrackets(type, pattern); - } - function patternmaybeop(type) { - if (type == "op" && content == ".") return cont(); - if (content == "to") {cx.marked = "keyword"; return cont(pattern);} - else return pass(); - } - function altbody(type) { - if (type == "{") return cont(pushlex("}", "alt"), altblock1, poplex); - return pass(); - } - function altblock1(type) { - if (type == "}") return cont(); - if (type == "|") return cont(altblock1); - if (content == "when") {cx.marked = "keyword"; return cont(expression, altblock2);} - if (type.match(/[\]\);,]/)) return cont(altblock1); - return pass(pattern, altblock2); - } - function altblock2(type) { - if (type == "{") return cont(pushlex("}", "alt"), block, poplex, altblock1); - else return pass(altblock1); - } - - function macro(type) { - if (type.match(/[\[\(\{]/)) return matchBrackets(type, expression); - return pass(); - } - function matchBrackets(type, comb) { - if (type == "[") return cont(pushlex("]"), commasep(comb, "]"), poplex); - if (type == "(") return cont(pushlex(")"), commasep(comb, ")"), poplex); - if (type == "{") return cont(pushlex("}"), commasep(comb, "}"), poplex); - return cont(); - } - - function parse(state, stream, style) { - var cc = state.cc; - // Communicate our context to the combinators. - // (Less wasteful than consing up a hundred closures on every call.) - cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; - - while (true) { - var combinator = cc.length ? cc.pop() : block; - if (combinator(tcat)) { - while(cc.length && cc[cc.length - 1].lex) - cc.pop()(); - return cx.marked || style; - } - } - } - - return { - startState: function() { - return { - tokenize: tokenBase, - cc: [], - lexical: {indented: -indentUnit, column: 0, type: "top", align: false}, - keywords: valKeywords, - indented: 0 - }; - }, - - token: function(stream, state) { - if (stream.sol()) { - if (!state.lexical.hasOwnProperty("align")) - state.lexical.align = false; - state.indented = stream.indentation(); - } - if (stream.eatSpace()) return null; - tcat = content = null; - var style = state.tokenize(stream, state); - if (style == "comment") return style; - if (!state.lexical.hasOwnProperty("align")) - state.lexical.align = true; - if (tcat == "prefix") return style; - if (!content) content = stream.current(); - return parse(state, stream, style); - }, - - indent: function(state, textAfter) { - if (state.tokenize != tokenBase) return 0; - var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, - type = lexical.type, closing = firstChar == type; - if (type == "stat") return lexical.indented + indentUnit; - if (lexical.align) return lexical.column + (closing ? 0 : 1); - return lexical.indented + (closing ? 0 : (lexical.info == "alt" ? altIndentUnit : indentUnit)); - }, - - electricChars: "{}", + {regex: /(?:(?:[0-9][0-9_]*)(?:(?:[Ee][+-]?[0-9_]+)|\.[0-9_]+(?:[Ee][+-]?[0-9_]+)?)(?:f32|f64)?)|(?:0(?:b[01_]+|(?:o[0-7_]+)|(?:x[0-9a-fA-F_]+))|(?:[0-9][0-9_]*))(?:u8|u16|u32|u64|i8|i16|i32|i64|isize|usize)?/, + token: "number"}, + {regex: /(let(?:\s+mut)?|fn|enum|mod|struct|type)(\s+)([a-zA-Z_][a-zA-Z0-9_]*)/, token: ["keyword", null, "def"]}, + {regex: /(?:abstract|alignof|as|box|break|continue|const|crate|do|else|enum|extern|fn|for|final|if|impl|in|loop|macro|match|mod|move|offsetof|override|priv|proc|pub|pure|ref|return|self|sizeof|static|struct|super|trait|type|typeof|unsafe|unsized|use|virtual|where|while|yield)\b/, token: "keyword"}, + {regex: /\b(?:Self|isize|usize|char|bool|u8|u16|u32|u64|f16|f32|f64|i8|i16|i32|i64|str|Option)\b/, token: "atom"}, + {regex: /\b(?:true|false|Some|None|Ok|Err)\b/, token: "builtin"}, + {regex: /\b(fn)(\s+)([a-zA-Z_][a-zA-Z0-9_]*)/, + token: ["keyword", null ,"def"]}, + {regex: /#!?\[.*\]/, token: "meta"}, + {regex: /\/\/.*/, token: "comment"}, + {regex: /\/\*/, token: "comment", next: "comment"}, + {regex: /[-+\/*=<>!]+/, token: "operator"}, + {regex: /[a-zA-Z_]\w*!/,token: "variable-3"}, + {regex: /[a-zA-Z_]\w*/, token: "variable"}, + {regex: /[\{\[\(]/, indent: true}, + {regex: /[\}\]\)]/, dedent: true} + ], + string: [ + {regex: /"/, token: "string", next: "start"}, + {regex: /(?:[^\\"]|\\(?:.|$))*/, token: "string"} + ], + string_raw: [ + {regex: /"/, token: "string", next: "start"}, + {regex: /[^"]*/, token: "string"} + ], + string_raw_hash: [ + {regex: /"#+/, token: "string", next: "start"}, + {regex: /(?:[^"]|"(?!#))*/, token: "string"} + ], + comment: [ + {regex: /.*?\*\//, token: "comment", next: "start"}, + {regex: /.*/, token: "comment"} + ], + meta: { + dontIndentStates: ["comment"], + electricInput: /^\s*\}$/, blockCommentStart: "/*", blockCommentEnd: "*/", lineComment: "//", fold: "brace" - }; + } }); + CodeMirror.defineMIME("text/x-rustsrc", "rust"); - }); diff --git a/rhodecode/public/js/mode/sparql/sparql.js b/rhodecode/public/js/mode/sparql/sparql.js --- a/rhodecode/public/js/mode/sparql/sparql.js +++ b/rhodecode/public/js/mode/sparql/sparql.js @@ -165,7 +165,9 @@ CodeMirror.defineMode("sparql", function return context.col + (closing ? 0 : 1); else return context.indent + (closing ? 0 : indentUnit); - } + }, + + lineComment: "#" }; }); diff --git a/rhodecode/public/js/mode/sql/sql.js b/rhodecode/public/js/mode/sql/sql.js --- a/rhodecode/public/js/mode/sql/sql.js +++ b/rhodecode/public/js/mode/sql/sql.js @@ -257,7 +257,7 @@ CodeMirror.defineMode("sql", function(co } // these keywords are used by all SQL dialects (however, a mode can still overwrite it) - var sqlKeywords = "alter and as asc between by count create delete desc distinct drop from group having in insert into is join like not on or order select set table union update values where "; + var sqlKeywords = "alter and as asc between by count create delete desc distinct drop from group having in insert into is join like not on or order select set table union update values where limit"; // turn a space-separated list into an array function set(str) { diff --git a/rhodecode/public/js/mode/stylus/stylus.js b/rhodecode/public/js/mode/stylus/stylus.js --- a/rhodecode/public/js/mode/stylus/stylus.js +++ b/rhodecode/public/js/mode/stylus/stylus.js @@ -126,19 +126,16 @@ if (stream.match(/^&{1}\s*$/)) { return ["variable-3", "reference"]; } - // Variable - if (ch == "$" && stream.match(/^\$[\w-]+/i)) { - return ["variable-2", "variable-name"]; - } // Word operator if (stream.match(wordOperatorKeywordsRegexp)) { return ["operator", "operator"]; } // Word - if (stream.match(/^[-_]*[a-z0-9]+[\w-]*/i)) { + if (stream.match(/^\$?[-_]*[a-z0-9]+[\w-]*/i)) { + // Variable if (stream.match(/^(\.|\[)[\w-\'\"\]]+/i, false)) { if (!wordIsTag(stream.current())) { - stream.match(/[\w-]+/); + stream.match(/\./); return ["variable-2", "variable-name"]; } } @@ -323,7 +320,7 @@ return pushContext(state, stream, "block", 0); } if (type == "variable-name") { - if ((stream.indentation() == 0 && startOfLine(stream)) || wordIsBlock(firstWordOfLine(stream))) { + if (stream.string.match(/^\s?\$[\w-\.\[\]\'\"]+$/) || wordIsBlock(firstWordOfLine(stream))) { return pushContext(state, stream, "variableName"); } else { @@ -429,6 +426,11 @@ return pushContext(state, stream, "block"); } if (word == "return") return pushContext(state, stream, "block", 0); + + // Placeholder selector + if (override == "variable-2" && stream.string.match(/^\s?\$[\w-\.\[\]\'\"]+$/)) { + return pushContext(state, stream, "block"); + } } return state.context.type; }; @@ -639,7 +641,6 @@ states.variableName = function(type, stream, state) { if (type == "string" || type == "[" || type == "]" || stream.current().match(/^(\.|\$)/)) { if (stream.current().match(/^\.[\w-]+/i)) override = "variable-2"; - if (endOfLine(stream)) return popContext(state); return "variableName"; } return popAndPass(type, stream, state); @@ -735,7 +736,7 @@ var nonStandardPropertyKeywords_ = ["scrollbar-arrow-color","scrollbar-base-color","scrollbar-dark-shadow-color","scrollbar-face-color","scrollbar-highlight-color","scrollbar-shadow-color","scrollbar-3d-light-color","scrollbar-track-color","shape-inside","searchfield-cancel-button","searchfield-decoration","searchfield-results-button","searchfield-results-decoration","zoom"]; var fontProperties_ = ["font-family","src","unicode-range","font-variant","font-feature-settings","font-stretch","font-weight","font-style"]; var colorKeywords_ = ["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen"]; - var valueKeywords_ = ["above","absolute","activeborder","additive","activecaption","afar","after-white-space","ahead","alias","all","all-scroll","alphabetic","alternate","always","amharic","amharic-abegede","antialiased","appworkspace","arabic-indic","armenian","asterisks","attr","auto","avoid","avoid-column","avoid-page","avoid-region","background","backwards","baseline","below","bidi-override","binary","bengali","blink","block","block-axis","bold","bolder","border","border-box","both","bottom","break","break-all","break-word","bullets","button","button-bevel","buttonface","buttonhighlight","buttonshadow","buttontext","calc","cambodian","capitalize","caps-lock-indicator","caption","captiontext","caret","cell","center","checkbox","circle","cjk-decimal","cjk-earthly-branch","cjk-heavenly-stem","cjk-ideographic","clear","clip","close-quote","col-resize","collapse","column","compact","condensed","contain","content","content-box","context-menu","continuous","copy","counter","counters","cover","crop","cross","crosshair","currentcolor","cursive","cyclic","dashed","decimal","decimal-leading-zero","default","default-button","destination-atop","destination-in","destination-out","destination-over","devanagari","disc","discard","disclosure-closed","disclosure-open","document","dot-dash","dot-dot-dash","dotted","double","down","e-resize","ease","ease-in","ease-in-out","ease-out","element","ellipse","ellipsis","embed","end","ethiopic","ethiopic-abegede","ethiopic-abegede-am-et","ethiopic-abegede-gez","ethiopic-abegede-ti-er","ethiopic-abegede-ti-et","ethiopic-halehame-aa-er","ethiopic-halehame-aa-et","ethiopic-halehame-am-et","ethiopic-halehame-gez","ethiopic-halehame-om-et","ethiopic-halehame-sid-et","ethiopic-halehame-so-et","ethiopic-halehame-ti-er","ethiopic-halehame-ti-et","ethiopic-halehame-tig","ethiopic-numeric","ew-resize","expanded","extends","extra-condensed","extra-expanded","fantasy","fast","fill","fixed","flat","flex","footnotes","forwards","from","geometricPrecision","georgian","graytext","groove","gujarati","gurmukhi","hand","hangul","hangul-consonant","hebrew","help","hidden","hide","higher","highlight","highlighttext","hiragana","hiragana-iroha","horizontal","hsl","hsla","icon","ignore","inactiveborder","inactivecaption","inactivecaptiontext","infinite","infobackground","infotext","inherit","initial","inline","inline-axis","inline-block","inline-flex","inline-table","inset","inside","intrinsic","invert","italic","japanese-formal","japanese-informal","justify","kannada","katakana","katakana-iroha","keep-all","khmer","korean-hangul-formal","korean-hanja-formal","korean-hanja-informal","landscape","lao","large","larger","left","level","lighter","line-through","linear","linear-gradient","lines","list-item","listbox","listitem","local","logical","loud","lower","lower-alpha","lower-armenian","lower-greek","lower-hexadecimal","lower-latin","lower-norwegian","lower-roman","lowercase","ltr","malayalam","match","matrix","matrix3d","media-controls-background","media-current-time-display","media-fullscreen-button","media-mute-button","media-play-button","media-return-to-realtime-button","media-rewind-button","media-seek-back-button","media-seek-forward-button","media-slider","media-sliderthumb","media-time-remaining-display","media-volume-slider","media-volume-slider-container","media-volume-sliderthumb","medium","menu","menulist","menulist-button","menulist-text","menulist-textfield","menutext","message-box","middle","min-intrinsic","mix","mongolian","monospace","move","multiple","myanmar","n-resize","narrower","ne-resize","nesw-resize","no-close-quote","no-drop","no-open-quote","no-repeat","none","normal","not-allowed","nowrap","ns-resize","numbers","numeric","nw-resize","nwse-resize","oblique","octal","open-quote","optimizeLegibility","optimizeSpeed","oriya","oromo","outset","outside","outside-shape","overlay","overline","padding","padding-box","painted","page","paused","persian","perspective","plus-darker","plus-lighter","pointer","polygon","portrait","pre","pre-line","pre-wrap","preserve-3d","progress","push-button","radial-gradient","radio","read-only","read-write","read-write-plaintext-only","rectangle","region","relative","repeat","repeating-linear-gradient","repeating-radial-gradient","repeat-x","repeat-y","reset","reverse","rgb","rgba","ridge","right","rotate","rotate3d","rotateX","rotateY","rotateZ","round","row-resize","rtl","run-in","running","s-resize","sans-serif","scale","scale3d","scaleX","scaleY","scaleZ","scroll","scrollbar","se-resize","searchfield","searchfield-cancel-button","searchfield-decoration","searchfield-results-button","searchfield-results-decoration","semi-condensed","semi-expanded","separate","serif","show","sidama","simp-chinese-formal","simp-chinese-informal","single","skew","skewX","skewY","skip-white-space","slide","slider-horizontal","slider-vertical","sliderthumb-horizontal","sliderthumb-vertical","slow","small","small-caps","small-caption","smaller","solid","somali","source-atop","source-in","source-out","source-over","space","spell-out","square","square-button","start","static","status-bar","stretch","stroke","sub","subpixel-antialiased","super","sw-resize","symbolic","symbols","table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row","table-row-group","tamil","telugu","text","text-bottom","text-top","textarea","textfield","thai","thick","thin","threeddarkshadow","threedface","threedhighlight","threedlightshadow","threedshadow","tibetan","tigre","tigrinya-er","tigrinya-er-abegede","tigrinya-et","tigrinya-et-abegede","to","top","trad-chinese-formal","trad-chinese-informal","translate","translate3d","translateX","translateY","translateZ","transparent","ultra-condensed","ultra-expanded","underline","up","upper-alpha","upper-armenian","upper-greek","upper-hexadecimal","upper-latin","upper-norwegian","upper-roman","uppercase","urdu","url","var","vertical","vertical-text","visible","visibleFill","visiblePainted","visibleStroke","visual","w-resize","wait","wave","wider","window","windowframe","windowtext","words","x-large","x-small","xor","xx-large","xx-small","bicubic","optimizespeed","grayscale"]; + var valueKeywords_ = ["above","absolute","activeborder","additive","activecaption","afar","after-white-space","ahead","alias","all","all-scroll","alphabetic","alternate","always","amharic","amharic-abegede","antialiased","appworkspace","arabic-indic","armenian","asterisks","attr","auto","avoid","avoid-column","avoid-page","avoid-region","background","backwards","baseline","below","bidi-override","binary","bengali","blink","block","block-axis","bold","bolder","border","border-box","both","bottom","break","break-all","break-word","bullets","button","button-bevel","buttonface","buttonhighlight","buttonshadow","buttontext","calc","cambodian","capitalize","caps-lock-indicator","caption","captiontext","caret","cell","center","checkbox","circle","cjk-decimal","cjk-earthly-branch","cjk-heavenly-stem","cjk-ideographic","clear","clip","close-quote","col-resize","collapse","column","compact","condensed","contain","content","content-box","context-menu","continuous","copy","counter","counters","cover","crop","cross","crosshair","currentcolor","cursive","cyclic","dashed","decimal","decimal-leading-zero","default","default-button","destination-atop","destination-in","destination-out","destination-over","devanagari","disc","discard","disclosure-closed","disclosure-open","document","dot-dash","dot-dot-dash","dotted","double","down","e-resize","ease","ease-in","ease-in-out","ease-out","element","ellipse","ellipsis","embed","end","ethiopic","ethiopic-abegede","ethiopic-abegede-am-et","ethiopic-abegede-gez","ethiopic-abegede-ti-er","ethiopic-abegede-ti-et","ethiopic-halehame-aa-er","ethiopic-halehame-aa-et","ethiopic-halehame-am-et","ethiopic-halehame-gez","ethiopic-halehame-om-et","ethiopic-halehame-sid-et","ethiopic-halehame-so-et","ethiopic-halehame-ti-er","ethiopic-halehame-ti-et","ethiopic-halehame-tig","ethiopic-numeric","ew-resize","expanded","extends","extra-condensed","extra-expanded","fantasy","fast","fill","fixed","flat","flex","footnotes","forwards","from","geometricPrecision","georgian","graytext","groove","gujarati","gurmukhi","hand","hangul","hangul-consonant","hebrew","help","hidden","hide","higher","highlight","highlighttext","hiragana","hiragana-iroha","horizontal","hsl","hsla","icon","ignore","inactiveborder","inactivecaption","inactivecaptiontext","infinite","infobackground","infotext","inherit","initial","inline","inline-axis","inline-block","inline-flex","inline-table","inset","inside","intrinsic","invert","italic","japanese-formal","japanese-informal","justify","kannada","katakana","katakana-iroha","keep-all","khmer","korean-hangul-formal","korean-hanja-formal","korean-hanja-informal","landscape","lao","large","larger","left","level","lighter","line-through","linear","linear-gradient","lines","list-item","listbox","listitem","local","logical","loud","lower","lower-alpha","lower-armenian","lower-greek","lower-hexadecimal","lower-latin","lower-norwegian","lower-roman","lowercase","ltr","malayalam","match","matrix","matrix3d","media-controls-background","media-current-time-display","media-fullscreen-button","media-mute-button","media-play-button","media-return-to-realtime-button","media-rewind-button","media-seek-back-button","media-seek-forward-button","media-slider","media-sliderthumb","media-time-remaining-display","media-volume-slider","media-volume-slider-container","media-volume-sliderthumb","medium","menu","menulist","menulist-button","menulist-text","menulist-textfield","menutext","message-box","middle","min-intrinsic","mix","mongolian","monospace","move","multiple","myanmar","n-resize","narrower","ne-resize","nesw-resize","no-close-quote","no-drop","no-open-quote","no-repeat","none","normal","not-allowed","nowrap","ns-resize","numbers","numeric","nw-resize","nwse-resize","oblique","octal","open-quote","optimizeLegibility","optimizeSpeed","oriya","oromo","outset","outside","outside-shape","overlay","overline","padding","padding-box","painted","page","paused","persian","perspective","plus-darker","plus-lighter","pointer","polygon","portrait","pre","pre-line","pre-wrap","preserve-3d","progress","push-button","radial-gradient","radio","read-only","read-write","read-write-plaintext-only","rectangle","region","relative","repeat","repeating-linear-gradient","repeating-radial-gradient","repeat-x","repeat-y","reset","reverse","rgb","rgba","ridge","right","rotate","rotate3d","rotateX","rotateY","rotateZ","round","row-resize","rtl","run-in","running","s-resize","sans-serif","scale","scale3d","scaleX","scaleY","scaleZ","scroll","scrollbar","se-resize","searchfield","searchfield-cancel-button","searchfield-decoration","searchfield-results-button","searchfield-results-decoration","semi-condensed","semi-expanded","separate","serif","show","sidama","simp-chinese-formal","simp-chinese-informal","single","skew","skewX","skewY","skip-white-space","slide","slider-horizontal","slider-vertical","sliderthumb-horizontal","sliderthumb-vertical","slow","small","small-caps","small-caption","smaller","solid","somali","source-atop","source-in","source-out","source-over","space","spell-out","square","square-button","start","static","status-bar","stretch","stroke","sub","subpixel-antialiased","super","sw-resize","symbolic","symbols","table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row","table-row-group","tamil","telugu","text","text-bottom","text-top","textarea","textfield","thai","thick","thin","threeddarkshadow","threedface","threedhighlight","threedlightshadow","threedshadow","tibetan","tigre","tigrinya-er","tigrinya-er-abegede","tigrinya-et","tigrinya-et-abegede","to","top","trad-chinese-formal","trad-chinese-informal","translate","translate3d","translateX","translateY","translateZ","transparent","ultra-condensed","ultra-expanded","underline","up","upper-alpha","upper-armenian","upper-greek","upper-hexadecimal","upper-latin","upper-norwegian","upper-roman","uppercase","urdu","url","var","vertical","vertical-text","visible","visibleFill","visiblePainted","visibleStroke","visual","w-resize","wait","wave","wider","window","windowframe","windowtext","words","x-large","x-small","xor","xx-large","xx-small","bicubic","optimizespeed","grayscale","row","row-reverse","wrap","wrap-reverse","column-reverse","flex-start","flex-end","space-between","space-around"]; var wordOperatorKeywords_ = ["in","and","or","not","is not","is a","is","isnt","defined","if unless"], blockKeywords_ = ["for","if","else","unless", "from", "to"], diff --git a/rhodecode/public/js/mode/swift/swift.js b/rhodecode/public/js/mode/swift/swift.js --- a/rhodecode/public/js/mode/swift/swift.js +++ b/rhodecode/public/js/mode/swift/swift.js @@ -13,189 +13,188 @@ })(function(CodeMirror) { "use strict" - function trim(str) { return /^\s*(.*?)\s*$/.exec(str)[1] } - - var separators = [" ","\\\+","\\\-","\\\(","\\\)","\\\*","/",":","\\\?","\\\<","\\\>"," ","\\\."] - var tokens = new RegExp(separators.join("|"),"g") - - function getWord(string, pos) { - var index = -1, count = 1 - var words = string.split(tokens) - for (var i = 0; i < words.length; i++) { - for(var j = 1; j <= words[i].length; j++) { - if (count==pos) index = i - count++ - } - count++ - } - var ret = ["", ""] - if (pos == 0) { - ret[1] = words[0] - ret[0] = null - } else { - ret[1] = words[index] - ret[0] = words[index-1] - } - return ret + function wordSet(words) { + var set = {} + for (var i = 0; i < words.length; i++) set[words[i]] = true + return set } - CodeMirror.defineMode("swift", function() { - var keywords=["var","let","class","deinit","enum","extension","func","import","init","let","protocol","static","struct","subscript","typealias","var","as","dynamicType","is","new","super","self","Self","Type","__COLUMN__","__FILE__","__FUNCTION__","__LINE__","break","case","continue","default","do","else","fallthrough","if","in","for","return","switch","where","while","associativity","didSet","get","infix","inout","left","mutating","none","nonmutating","operator","override","postfix","precedence","prefix","right","set","unowned","unowned(safe)","unowned(unsafe)","weak","willSet"] - var commonConstants=["Infinity","NaN","undefined","null","true","false","on","off","yes","no","nil","null","this","super"] - var types=["String","bool","int","string","double","Double","Int","Float","float","public","private","extension"] - var numbers=["0","1","2","3","4","5","6","7","8","9"] - var operators=["+","-","/","*","%","=","|","&","<",">"] - var punc=[";",",",".","(",")","{","}","[","]"] - var delimiters=/^(?:[()\[\]{},:`=;]|\.\.?\.?)/ - var identifiers=/^[_A-Za-z$][_A-Za-z$0-9]*/ - var properties=/^(@|this\.)[_A-Za-z$][_A-Za-z$0-9]*/ - var regexPrefixes=/^(\/{3}|\/)/ + var keywords = wordSet(["var","let","class","deinit","enum","extension","func","import","init","protocol", + "static","struct","subscript","typealias","as","dynamicType","is","new","super", + "self","Self","Type","__COLUMN__","__FILE__","__FUNCTION__","__LINE__","break","case", + "continue","default","do","else","fallthrough","if","in","for","return","switch", + "where","while","associativity","didSet","get","infix","inout","left","mutating", + "none","nonmutating","operator","override","postfix","precedence","prefix","right", + "set","unowned","weak","willSet"]) + var definingKeywords = wordSet(["var","let","class","enum","extension","func","import","protocol","struct", + "typealias","dynamicType","for"]) + var atoms = wordSet(["Infinity","NaN","undefined","null","true","false","on","off","yes","no","nil","null", + "this","super"]) + var types = wordSet(["String","bool","int","string","double","Double","Int","Float","float","public", + "private","extension"]) + var operators = "+-/*%=|&<>#" + var punc = ";,.(){}[]" + var number = /^-?(?:(?:[\d_]+\.[_\d]*|\.[_\d]+|0o[0-7_\.]+|0b[01_\.]+)(?:e-?[\d_]+)?|0x[\d_a-f\.]+(?:p-?[\d_]+)?)/i + var identifier = /^[_A-Za-z$][_A-Za-z$0-9]*/ + var property = /^[@\.][_A-Za-z$][_A-Za-z$0-9]*/ + var regexp = /^\/(?!\s)(?:\/\/)?(?:\\.|[^\/])+\// + + function tokenBase(stream, state, prev) { + if (stream.sol()) state.indented = stream.indentation() + if (stream.eatSpace()) return null + + var ch = stream.peek() + if (ch == "/") { + if (stream.match("//")) { + stream.skipToEnd() + return "comment" + } + if (stream.match("/*")) { + state.tokenize.push(tokenComment) + return tokenComment(stream, state) + } + if (stream.match(regexp)) return "string-2" + } + if (operators.indexOf(ch) > -1) { + stream.next() + return "operator" + } + if (punc.indexOf(ch) > -1) { + stream.next() + stream.match("..") + return "punctuation" + } + if (ch == '"' || ch == "'") { + stream.next() + var tokenize = tokenString(ch) + state.tokenize.push(tokenize) + return tokenize(stream, state) + } + + if (stream.match(number)) return "number" + if (stream.match(property)) return "property" + + if (stream.match(identifier)) { + var ident = stream.current() + if (keywords.hasOwnProperty(ident)) { + if (definingKeywords.hasOwnProperty(ident)) + state.prev = "define" + return "keyword" + } + if (types.hasOwnProperty(ident)) return "variable-2" + if (atoms.hasOwnProperty(ident)) return "atom" + if (prev == "define") return "def" + return "variable" + } + stream.next() + return null + } + + function tokenUntilClosingParen() { + var depth = 0 + return function(stream, state, prev) { + var inner = tokenBase(stream, state, prev) + if (inner == "punctuation") { + if (stream.current() == "(") ++depth + else if (stream.current() == ")") { + if (depth == 0) { + stream.backUp(1) + state.tokenize.pop() + return state.tokenize[state.tokenize.length - 1](stream, state) + } + else --depth + } + } + return inner + } + } + + function tokenString(quote) { + return function(stream, state) { + var ch, escaped = false + while (ch = stream.next()) { + if (escaped) { + if (ch == "(") { + state.tokenize.push(tokenUntilClosingParen()) + return "string" + } + escaped = false + } else if (ch == quote) { + break + } else { + escaped = ch == "\\" + } + } + state.tokenize.pop() + return "string" + } + } + + function tokenComment(stream, state) { + stream.match(/^(?:[^*]|\*(?!\/))*/) + if (stream.match("*/")) state.tokenize.pop() + return "comment" + } + + function Context(prev, align, indented) { + this.prev = prev + this.align = align + this.indented = indented + } + + function pushContext(state, stream) { + var align = stream.match(/^\s*($|\/[\/\*])/, false) ? null : stream.column() + 1 + state.context = new Context(state.context, align, state.indented) + } + + function popContext(state) { + if (state.context) { + state.indented = state.context.indented + state.context = state.context.prev + } + } + + CodeMirror.defineMode("swift", function(config) { return { startState: function() { return { - prev: false, - string: false, - escape: false, - inner: false, - comment: false, - num_left: 0, - num_right: 0, - doubleString: false, - singleString: false + prev: null, + context: null, + indented: 0, + tokenize: [] } }, - token: function(stream, state) { - if (stream.eatSpace()) return null - var ch = stream.next() - if (state.string) { - if (state.escape) { - state.escape = false - return "string" - } else { - if ((ch == "\"" && (state.doubleString && !state.singleString) || - (ch == "'" && (!state.doubleString && state.singleString))) && - !state.escape) { - state.string = false - state.doubleString = false - state.singleString = false - return "string" - } else if (ch == "\\" && stream.peek() == "(") { - state.inner = true - state.string = false - return "keyword" - } else if (ch == "\\" && stream.peek() != "(") { - state.escape = true - state.string = true - return "string" - } else { - return "string" - } - } - } else if (state.comment) { - if (ch == "*" && stream.peek() == "/") { - state.prev = "*" - return "comment" - } else if (ch == "/" && state.prev == "*") { - state.prev = false - state.comment = false - return "comment" - } - return "comment" - } else { - if (ch == "/") { - if (stream.peek() == "/") { - stream.skipToEnd() - return "comment" - } - if (stream.peek() == "*") { - state.comment = true - return "comment" - } - } - if (ch == "(" && state.inner) { - state.num_left++ - return null - } - if (ch == ")" && state.inner) { - state.num_right++ - if (state.num_left == state.num_right) { - state.inner=false - state.string=true - } - return null - } + token: function(stream, state) { + var prev = state.prev + state.prev = null + var tokenize = state.tokenize[state.tokenize.length - 1] || tokenBase + var style = tokenize(stream, state, prev) + if (!style || style == "comment") state.prev = prev + else if (!state.prev) state.prev = style - var ret = getWord(stream.string, stream.pos) - var the_word = ret[1] - var prev_word = ret[0] + if (style == "punctuation") { + var bracket = /[\(\[\{]|([\]\)\}])/.exec(stream.current()) + if (bracket) (bracket[1] ? popContext : pushContext)(state, stream) + } - if (operators.indexOf(ch + "") > -1) return "operator" - if (punc.indexOf(ch) > -1) return "punctuation" - - if (typeof the_word != "undefined") { - the_word = trim(the_word) - if (typeof prev_word != "undefined") prev_word = trim(prev_word) - if (the_word.charAt(0) == "#") return null - - if (types.indexOf(the_word) > -1) return "def" - if (commonConstants.indexOf(the_word) > -1) return "atom" - if (numbers.indexOf(the_word) > -1) return "number" - - if ((numbers.indexOf(the_word.charAt(0) + "") > -1 || - operators.indexOf(the_word.charAt(0) + "") > -1) && - numbers.indexOf(ch) > -1) { - return "number" - } + return style + }, - if (keywords.indexOf(the_word) > -1 || - keywords.indexOf(the_word.split(tokens)[0]) > -1) - return "keyword" - if (keywords.indexOf(prev_word) > -1) return "def" - } - if (ch == '"' && !state.doubleString) { - state.string = true - state.doubleString = true - return "string" - } - if (ch == "'" && !state.singleString) { - state.string = true - state.singleString = true - return "string" - } - if (ch == "(" && state.inner) - state.num_left++ - if (ch == ")" && state.inner) { - state.num_right++ - if (state.num_left == state.num_right) { - state.inner = false - state.string = true - } - return null - } - if (stream.match(/^-?[0-9\.]/, false)) { - if (stream.match(/^-?\d*\.\d+(e[\+\-]?\d+)?/i) || - stream.match(/^-?\d+\.\d*/) || - stream.match(/^-?\.\d+/)) { - if (stream.peek() == ".") stream.backUp(1) - return "number" - } - if (stream.match(/^-?0x[0-9a-f]+/i) || - stream.match(/^-?[1-9]\d*(e[\+\-]?\d+)?/) || - stream.match(/^-?0(?![\dx])/i)) - return "number" - } - if (stream.match(regexPrefixes)) { - if (stream.current()!="/" || stream.match(/^.*\//,false)) return "string" - else stream.backUp(1) - } - if (stream.match(delimiters)) return "punctuation" - if (stream.match(identifiers)) return "variable" - if (stream.match(properties)) return "property" - return "variable" - } - } + indent: function(state, textAfter) { + var cx = state.context + if (!cx) return 0 + var closing = /^[\]\}\)]/.test(textAfter) + if (cx.align != null) return cx.align - (closing ? 1 : 0) + return cx.indented + (closing ? 0 : config.indentUnit) + }, + + electricInput: /^\s*[\)\}\]]$/, + + lineComment: "//", + blockCommentStart: "/*", + blockCommentEnd: "*/" } }) diff --git a/rhodecode/public/js/mode/xml/xml.js b/rhodecode/public/js/mode/xml/xml.js --- a/rhodecode/public/js/mode/xml/xml.js +++ b/rhodecode/public/js/mode/xml/xml.js @@ -11,54 +11,56 @@ })(function(CodeMirror) { "use strict"; -CodeMirror.defineMode("xml", function(config, parserConfig) { - var indentUnit = config.indentUnit; - var multilineTagIndentFactor = parserConfig.multilineTagIndentFactor || 1; - var multilineTagIndentPastTag = parserConfig.multilineTagIndentPastTag; - if (multilineTagIndentPastTag == null) multilineTagIndentPastTag = true; +var htmlConfig = { + autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true, + 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true, + 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true, + 'track': true, 'wbr': true, 'menuitem': true}, + implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true, + 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true, + 'th': true, 'tr': true}, + contextGrabbers: { + 'dd': {'dd': true, 'dt': true}, + 'dt': {'dd': true, 'dt': true}, + 'li': {'li': true}, + 'option': {'option': true, 'optgroup': true}, + 'optgroup': {'optgroup': true}, + 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true, + 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true, + 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, + 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true, + 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true}, + 'rp': {'rp': true, 'rt': true}, + 'rt': {'rp': true, 'rt': true}, + 'tbody': {'tbody': true, 'tfoot': true}, + 'td': {'td': true, 'th': true}, + 'tfoot': {'tbody': true}, + 'th': {'td': true, 'th': true}, + 'thead': {'tbody': true, 'tfoot': true}, + 'tr': {'tr': true} + }, + doNotIndent: {"pre": true}, + allowUnquoted: true, + allowMissing: true, + caseFold: true +} - var Kludges = parserConfig.htmlMode ? { - autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true, - 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true, - 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true, - 'track': true, 'wbr': true, 'menuitem': true}, - implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true, - 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true, - 'th': true, 'tr': true}, - contextGrabbers: { - 'dd': {'dd': true, 'dt': true}, - 'dt': {'dd': true, 'dt': true}, - 'li': {'li': true}, - 'option': {'option': true, 'optgroup': true}, - 'optgroup': {'optgroup': true}, - 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true, - 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true, - 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, - 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true, - 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true}, - 'rp': {'rp': true, 'rt': true}, - 'rt': {'rp': true, 'rt': true}, - 'tbody': {'tbody': true, 'tfoot': true}, - 'td': {'td': true, 'th': true}, - 'tfoot': {'tbody': true}, - 'th': {'td': true, 'th': true}, - 'thead': {'tbody': true, 'tfoot': true}, - 'tr': {'tr': true} - }, - doNotIndent: {"pre": true}, - allowUnquoted: true, - allowMissing: true, - caseFold: true - } : { - autoSelfClosers: {}, - implicitlyClosed: {}, - contextGrabbers: {}, - doNotIndent: {}, - allowUnquoted: false, - allowMissing: false, - caseFold: false - }; - var alignCDATA = parserConfig.alignCDATA; +var xmlConfig = { + autoSelfClosers: {}, + implicitlyClosed: {}, + contextGrabbers: {}, + doNotIndent: {}, + allowUnquoted: false, + allowMissing: false, + caseFold: false +} + +CodeMirror.defineMode("xml", function(editorConf, config_) { + var indentUnit = editorConf.indentUnit + var config = {} + var defaults = config_.htmlMode ? htmlConfig : xmlConfig + for (var prop in defaults) config[prop] = defaults[prop] + for (var prop in config_) config[prop] = config_[prop] // Return variables for tokenizers var type, setStyle; @@ -109,6 +111,7 @@ CodeMirror.defineMode("xml", function(co return null; } } + inText.isInText = true; function inTag(stream, state) { var ch = stream.next(); @@ -187,7 +190,7 @@ CodeMirror.defineMode("xml", function(co this.tagName = tagName; this.indent = state.indented; this.startOfLine = startOfLine; - if (Kludges.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent)) + if (config.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent)) this.noIndent = true; } function popContext(state) { @@ -200,8 +203,8 @@ CodeMirror.defineMode("xml", function(co return; } parentTagName = state.context.tagName; - if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName) || - !Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) { + if (!config.contextGrabbers.hasOwnProperty(parentTagName) || + !config.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) { return; } popContext(state); @@ -232,7 +235,7 @@ CodeMirror.defineMode("xml", function(co if (type == "word") { var tagName = stream.current(); if (state.context && state.context.tagName != tagName && - Kludges.implicitlyClosed.hasOwnProperty(state.context.tagName)) + config.implicitlyClosed.hasOwnProperty(state.context.tagName)) popContext(state); if (state.context && state.context.tagName == tagName) { setStyle = "tag"; @@ -268,7 +271,7 @@ CodeMirror.defineMode("xml", function(co var tagName = state.tagName, tagStart = state.tagStart; state.tagName = state.tagStart = null; if (type == "selfcloseTag" || - Kludges.autoSelfClosers.hasOwnProperty(tagName)) { + config.autoSelfClosers.hasOwnProperty(tagName)) { maybePopContext(state, tagName); } else { maybePopContext(state, tagName); @@ -281,12 +284,12 @@ CodeMirror.defineMode("xml", function(co } function attrEqState(type, stream, state) { if (type == "equals") return attrValueState; - if (!Kludges.allowMissing) setStyle = "error"; + if (!config.allowMissing) setStyle = "error"; return attrState(type, stream, state); } function attrValueState(type, stream, state) { if (type == "string") return attrContinuedState; - if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return attrState;} + if (type == "word" && config.allowUnquoted) {setStyle = "string"; return attrState;} setStyle = "error"; return attrState(type, stream, state); } @@ -296,12 +299,14 @@ CodeMirror.defineMode("xml", function(co } return { - startState: function() { - return {tokenize: inText, - state: baseState, - indented: 0, - tagName: null, tagStart: null, - context: null}; + startState: function(baseIndent) { + var state = {tokenize: inText, + state: baseState, + indented: baseIndent || 0, + tagName: null, tagStart: null, + context: null} + if (baseIndent != null) state.baseIndent = baseIndent + return state }, token: function(stream, state) { @@ -334,19 +339,19 @@ CodeMirror.defineMode("xml", function(co return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0; // Indent the starts of attribute names. if (state.tagName) { - if (multilineTagIndentPastTag) + if (config.multilineTagIndentPastTag !== false) return state.tagStart + state.tagName.length + 2; else - return state.tagStart + indentUnit * multilineTagIndentFactor; + return state.tagStart + indentUnit * (config.multilineTagIndentFactor || 1); } - if (alignCDATA && /$/, blockCommentStart: "", - configuration: parserConfig.htmlMode ? "html" : "xml", - helperType: parserConfig.htmlMode ? "html" : "xml" + configuration: config.htmlMode ? "html" : "xml", + helperType: config.htmlMode ? "html" : "xml", + + skipAttribute: function(state) { + if (state.state == attrValueState) + state.state = attrState + } }; }); diff --git a/rhodecode/public/js/rhodecode/i18n/be.js b/rhodecode/public/js/rhodecode/i18n/be.js --- a/rhodecode/public/js/rhodecode/i18n/be.js +++ b/rhodecode/public/js/rhodecode/i18n/be.js @@ -5,35 +5,71 @@ */ //JS translations map var _TM = { - '{0} active out of {1} users': '{0} active out of {1} users', - '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', 'Add another comment': 'Add another comment', - 'disabled': 'disabled', - 'enabled': 'enabled', + 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', 'Follow': 'Follow', - 'loading ...': 'loading ...', 'Loading ...': 'Loading ...', 'Loading failed': 'Loading failed', 'Loading more results...': 'Loading more results...', + 'No bookmarks available yet.': 'No bookmarks available yet.', + 'No branches available yet.': 'No branches available yet.', + 'No gists available yet.': 'No gists available yet.', 'No matches found': 'No matches found', 'No matching files': 'No matching files', + 'No pull requests available yet.': 'No pull requests available yet.', + 'No repositories available yet.': 'No repositories available yet.', + 'No repository groups available yet.': 'No repository groups available yet.', 'No results': 'No results', + 'No tags available yet.': 'No tags available yet.', + 'No user groups available yet.': 'No user groups available yet.', + 'No users available yet.': 'No users available yet.', 'One result is available, press enter to select it.': 'One result is available, press enter to select it.', 'Open new pull request': 'Open new pull request', 'Open new pull request for selected commit': 'Open new pull request for selected commit', + 'Please delete {0} character': 'Please delete {0} character', + 'Please delete {0} characters': 'Please delete {0} characters', + 'Please enter {0} or more character': 'Please enter {0} or more character', + 'Please enter {0} or more characters': 'Please enter {0} or more characters', 'Searching...': 'Searching...', - 'Select changeset': 'Select changeset', - 'Select commit': 'Select commit', 'Selection link': 'Selection link', 'Set status to Approved': 'Set status to Approved', 'Set status to Rejected': 'Set status to Rejected', + 'Show more': 'Show more', 'Show selected commit __S': 'Show selected commit __S', 'Show selected commits __S ... __E': 'Show selected commits __S ... __E', + 'Start following this repository': 'Start following this repository', + 'Status Review': 'Status Review', + 'Stop following this repository': 'Stop following this repository', + 'Submitting...': 'Submitting...', + 'Unfollow': 'Unfollow', + 'Updating...': 'Updating...', + 'You can only select {0} item': 'You can only select {0} item', + 'You can only select {0} items': 'You can only select {0} items', + 'disabled': 'disabled', + 'enabled': 'enabled', + 'file': 'file', + 'files': 'files', + 'in {0}': 'in {0}', + 'in {0} and {1}': 'in {0} and {1}', + 'in {0}, {1}': 'in {0}, {1}', + 'just now': 'just now', 'specify commit': 'specify commit', - 'Start following this repository': 'Start following this repository', - 'Stop following this repository': 'Stop following this repository', 'truncated result': 'truncated result', 'truncated results': 'truncated results', - 'Unfollow': 'Unfollow', - 'Updating...': 'Updating...' + '{0} active out of {1} users': '{0} active out of {1} users', + '{0} ago': '{0} ago', + '{0} and {1}': '{0} and {1}', + '{0} and {1} ago': '{0} and {1} ago', + '{0} day': '{0} day', + '{0} days': '{0} days', + '{0} hour': '{0} hour', + '{0} hours': '{0} hours', + '{0} min': '{0} min', + '{0} month': '{0} month', + '{0} months': '{0} months', + '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', + '{0} sec': '{0} sec', + '{0} year': '{0} year', + '{0} years': '{0} years', + '{0}, {1} ago': '{0}, {1} ago' }; \ No newline at end of file diff --git a/rhodecode/public/js/rhodecode/i18n/de.js b/rhodecode/public/js/rhodecode/i18n/de.js --- a/rhodecode/public/js/rhodecode/i18n/de.js +++ b/rhodecode/public/js/rhodecode/i18n/de.js @@ -5,35 +5,71 @@ */ //JS translations map var _TM = { - '{0} active out of {1} users': '{0} active out of {1} users', - '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', 'Add another comment': 'Add another comment', - 'disabled': 'disabled', - 'enabled': 'enabled', + 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', 'Follow': 'Follow', - 'loading ...': 'loading ...', 'Loading ...': 'Loading ...', 'Loading failed': 'Loading failed', 'Loading more results...': 'Loading more results...', + 'No bookmarks available yet.': 'No bookmarks available yet.', + 'No branches available yet.': 'No branches available yet.', + 'No gists available yet.': 'No gists available yet.', 'No matches found': 'No matches found', 'No matching files': 'No matching files', + 'No pull requests available yet.': 'No pull requests available yet.', + 'No repositories available yet.': 'No repositories available yet.', + 'No repository groups available yet.': 'No repository groups available yet.', 'No results': 'No results', + 'No tags available yet.': 'No tags available yet.', + 'No user groups available yet.': 'No user groups available yet.', + 'No users available yet.': 'No users available yet.', 'One result is available, press enter to select it.': 'One result is available, press enter to select it.', 'Open new pull request': 'Open new pull request', 'Open new pull request for selected commit': 'Open new pull request for selected commit', + 'Please delete {0} character': 'Please delete {0} character', + 'Please delete {0} characters': 'Please delete {0} characters', + 'Please enter {0} or more character': 'Please enter {0} or more character', + 'Please enter {0} or more characters': 'Please enter {0} or more characters', 'Searching...': 'Searching...', - 'Select changeset': 'Select changeset', - 'Select commit': 'Select commit', 'Selection link': 'Selection link', 'Set status to Approved': 'Set status to Approved', 'Set status to Rejected': 'Set status to Rejected', + 'Show more': 'Show more', 'Show selected commit __S': 'Show selected commit __S', 'Show selected commits __S ... __E': 'Show selected commits __S ... __E', + 'Start following this repository': 'Start following this repository', + 'Status Review': 'Status Review', + 'Stop following this repository': 'Stop following this repository', + 'Submitting...': 'Submitting...', + 'Unfollow': 'Unfollow', + 'Updating...': 'Updating...', + 'You can only select {0} item': 'You can only select {0} item', + 'You can only select {0} items': 'You can only select {0} items', + 'disabled': 'disabled', + 'enabled': 'enabled', + 'file': 'file', + 'files': 'files', + 'in {0}': 'in {0}', + 'in {0} and {1}': 'in {0} and {1}', + 'in {0}, {1}': 'in {0}, {1}', + 'just now': 'jetzt gerade', 'specify commit': 'specify commit', - 'Start following this repository': 'Start following this repository', - 'Stop following this repository': 'Stop following this repository', 'truncated result': 'truncated result', 'truncated results': 'truncated results', - 'Unfollow': 'Unfollow', - 'Updating...': 'Updating...' + '{0} active out of {1} users': '{0} active out of {1} users', + '{0} ago': '{0} ago', + '{0} and {1}': '{0} and {1}', + '{0} and {1} ago': '{0} and {1} ago', + '{0} day': '{0} day', + '{0} days': '{0} days', + '{0} hour': '{0} hour', + '{0} hours': '{0} hours', + '{0} min': '{0} min', + '{0} month': '{0} month', + '{0} months': '{0} months', + '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', + '{0} sec': '{0} sec', + '{0} year': '{0} year', + '{0} years': '{0} years', + '{0}, {1} ago': '{0}, {1} ago' }; \ No newline at end of file diff --git a/rhodecode/public/js/rhodecode/i18n/en.js b/rhodecode/public/js/rhodecode/i18n/en.js --- a/rhodecode/public/js/rhodecode/i18n/en.js +++ b/rhodecode/public/js/rhodecode/i18n/en.js @@ -5,35 +5,71 @@ */ //JS translations map var _TM = { - '{0} active out of {1} users': '{0} active out of {1} users', - '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', 'Add another comment': 'Add another comment', - 'disabled': 'disabled', - 'enabled': 'enabled', + 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', 'Follow': 'Follow', - 'loading ...': 'loading ...', 'Loading ...': 'Loading ...', 'Loading failed': 'Loading failed', 'Loading more results...': 'Loading more results...', + 'No bookmarks available yet.': 'No bookmarks available yet.', + 'No branches available yet.': 'No branches available yet.', + 'No gists available yet.': 'No gists available yet.', 'No matches found': 'No matches found', 'No matching files': 'No matching files', + 'No pull requests available yet.': 'No pull requests available yet.', + 'No repositories available yet.': 'No repositories available yet.', + 'No repository groups available yet.': 'No repository groups available yet.', 'No results': 'No results', + 'No tags available yet.': 'No tags available yet.', + 'No user groups available yet.': 'No user groups available yet.', + 'No users available yet.': 'No users available yet.', 'One result is available, press enter to select it.': 'One result is available, press enter to select it.', 'Open new pull request': 'Open new pull request', 'Open new pull request for selected commit': 'Open new pull request for selected commit', + 'Please delete {0} character': 'Please delete {0} character', + 'Please delete {0} characters': 'Please delete {0} characters', + 'Please enter {0} or more character': 'Please enter {0} or more character', + 'Please enter {0} or more characters': 'Please enter {0} or more characters', 'Searching...': 'Searching...', - 'Select changeset': 'Select changeset', - 'Select commit': 'Select commit', 'Selection link': 'Selection link', 'Set status to Approved': 'Set status to Approved', 'Set status to Rejected': 'Set status to Rejected', + 'Show more': 'Show more', 'Show selected commit __S': 'Show selected commit __S', 'Show selected commits __S ... __E': 'Show selected commits __S ... __E', + 'Start following this repository': 'Start following this repository', + 'Status Review': 'Status Review', + 'Stop following this repository': 'Stop following this repository', + 'Submitting...': 'Submitting...', + 'Unfollow': 'Unfollow', + 'Updating...': 'Updating...', + 'You can only select {0} item': 'You can only select {0} item', + 'You can only select {0} items': 'You can only select {0} items', + 'disabled': 'disabled', + 'enabled': 'enabled', + 'file': 'file', + 'files': 'files', + 'in {0}': 'in {0}', + 'in {0} and {1}': 'in {0} and {1}', + 'in {0}, {1}': 'in {0}, {1}', + 'just now': 'just now', 'specify commit': 'specify commit', - 'Start following this repository': 'Start following this repository', - 'Stop following this repository': 'Stop following this repository', 'truncated result': 'truncated result', 'truncated results': 'truncated results', - 'Unfollow': 'Unfollow', - 'Updating...': 'Updating...' + '{0} active out of {1} users': '{0} active out of {1} users', + '{0} ago': '{0} ago', + '{0} and {1}': '{0} and {1}', + '{0} and {1} ago': '{0} and {1} ago', + '{0} day': '{0} day', + '{0} days': '{0} days', + '{0} hour': '{0} hour', + '{0} hours': '{0} hours', + '{0} min': '{0} min', + '{0} month': '{0} month', + '{0} months': '{0} months', + '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', + '{0} sec': '{0} sec', + '{0} year': '{0} year', + '{0} years': '{0} years', + '{0}, {1} ago': '{0}, {1} ago' }; \ No newline at end of file diff --git a/rhodecode/public/js/rhodecode/i18n/es.js b/rhodecode/public/js/rhodecode/i18n/es.js --- a/rhodecode/public/js/rhodecode/i18n/es.js +++ b/rhodecode/public/js/rhodecode/i18n/es.js @@ -5,35 +5,71 @@ */ //JS translations map var _TM = { - '{0} active out of {1} users': '{0} active out of {1} users', - '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', 'Add another comment': 'Add another comment', - 'disabled': 'disabled', - 'enabled': 'enabled', + 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', 'Follow': 'Follow', - 'loading ...': 'loading ...', 'Loading ...': 'Loading ...', 'Loading failed': 'Loading failed', 'Loading more results...': 'Loading more results...', + 'No bookmarks available yet.': 'No bookmarks available yet.', + 'No branches available yet.': 'No branches available yet.', + 'No gists available yet.': 'No gists available yet.', 'No matches found': 'No matches found', 'No matching files': 'No matching files', + 'No pull requests available yet.': 'No pull requests available yet.', + 'No repositories available yet.': 'No repositories available yet.', + 'No repository groups available yet.': 'No repository groups available yet.', 'No results': 'No results', + 'No tags available yet.': 'No tags available yet.', + 'No user groups available yet.': 'No user groups available yet.', + 'No users available yet.': 'No users available yet.', 'One result is available, press enter to select it.': 'One result is available, press enter to select it.', 'Open new pull request': 'Open new pull request', 'Open new pull request for selected commit': 'Open new pull request for selected commit', + 'Please delete {0} character': 'Please delete {0} character', + 'Please delete {0} characters': 'Please delete {0} characters', + 'Please enter {0} or more character': 'Please enter {0} or more character', + 'Please enter {0} or more characters': 'Please enter {0} or more characters', 'Searching...': 'Searching...', - 'Select changeset': 'Select changeset', - 'Select commit': 'Select commit', 'Selection link': 'Selection link', 'Set status to Approved': 'Set status to Approved', 'Set status to Rejected': 'Set status to Rejected', + 'Show more': 'Show more', 'Show selected commit __S': 'Show selected commit __S', 'Show selected commits __S ... __E': 'Show selected commits __S ... __E', + 'Start following this repository': 'Start following this repository', + 'Status Review': 'Status Review', + 'Stop following this repository': 'Stop following this repository', + 'Submitting...': 'Submitting...', + 'Unfollow': 'Unfollow', + 'Updating...': 'Updating...', + 'You can only select {0} item': 'You can only select {0} item', + 'You can only select {0} items': 'You can only select {0} items', + 'disabled': 'disabled', + 'enabled': 'enabled', + 'file': 'file', + 'files': 'files', + 'in {0}': 'in {0}', + 'in {0} and {1}': 'in {0} and {1}', + 'in {0}, {1}': 'in {0}, {1}', + 'just now': 'just now', 'specify commit': 'specify commit', - 'Start following this repository': 'Start following this repository', - 'Stop following this repository': 'Stop following this repository', 'truncated result': 'truncated result', 'truncated results': 'truncated results', - 'Unfollow': 'Unfollow', - 'Updating...': 'Updating...' + '{0} active out of {1} users': '{0} active out of {1} users', + '{0} ago': '{0} ago', + '{0} and {1}': '{0} and {1}', + '{0} and {1} ago': '{0} and {1} ago', + '{0} day': '{0} day', + '{0} days': '{0} days', + '{0} hour': '{0} hour', + '{0} hours': '{0} hours', + '{0} min': '{0} min', + '{0} month': '{0} month', + '{0} months': '{0} months', + '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', + '{0} sec': '{0} sec', + '{0} year': '{0} year', + '{0} years': '{0} years', + '{0}, {1} ago': '{0}, {1} ago' }; \ No newline at end of file diff --git a/rhodecode/public/js/rhodecode/i18n/fr.js b/rhodecode/public/js/rhodecode/i18n/fr.js --- a/rhodecode/public/js/rhodecode/i18n/fr.js +++ b/rhodecode/public/js/rhodecode/i18n/fr.js @@ -5,35 +5,71 @@ */ //JS translations map var _TM = { - '{0} active out of {1} users': '{0} active out of {1} users', - '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', 'Add another comment': 'Add another comment', - 'disabled': 'Désactivé', - 'enabled': 'enabled', + 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', 'Follow': 'Follow', - 'loading ...': 'loading ...', 'Loading ...': 'Loading ...', 'Loading failed': 'Loading failed', 'Loading more results...': 'Loading more results...', + 'No bookmarks available yet.': 'No bookmarks available yet.', + 'No branches available yet.': 'No branches available yet.', + 'No gists available yet.': 'No gists available yet.', 'No matches found': 'No matches found', 'No matching files': 'No matching files', + 'No pull requests available yet.': 'No pull requests available yet.', + 'No repositories available yet.': 'No repositories available yet.', + 'No repository groups available yet.': 'No repository groups available yet.', 'No results': 'No results', + 'No tags available yet.': 'No tags available yet.', + 'No user groups available yet.': 'No user groups available yet.', + 'No users available yet.': 'No users available yet.', 'One result is available, press enter to select it.': 'One result is available, press enter to select it.', 'Open new pull request': 'Nouvelle requête de pull', 'Open new pull request for selected commit': 'Open new pull request for selected commit', + 'Please delete {0} character': 'Please delete {0} character', + 'Please delete {0} characters': 'Please delete {0} characters', + 'Please enter {0} or more character': 'Please enter {0} or more character', + 'Please enter {0} or more characters': 'Please enter {0} or more characters', 'Searching...': 'Searching...', - 'Select changeset': 'Select changeset', - 'Select commit': 'Select commit', 'Selection link': 'Selection link', 'Set status to Approved': 'Set status to Approved', 'Set status to Rejected': 'Set status to Rejected', + 'Show more': 'Show more', 'Show selected commit __S': 'Show selected commit __S', 'Show selected commits __S ... __E': 'Show selected commits __S ... __E', + 'Start following this repository': 'Start following this repository', + 'Status Review': 'Status Review', + 'Stop following this repository': 'Stop following this repository', + 'Submitting...': 'Submitting...', + 'Unfollow': 'Unfollow', + 'Updating...': 'Updating...', + 'You can only select {0} item': 'You can only select {0} item', + 'You can only select {0} items': 'You can only select {0} items', + 'disabled': 'Désactivé', + 'enabled': 'enabled', + 'file': 'file', + 'files': 'files', + 'in {0}': 'in {0}', + 'in {0} and {1}': 'in {0} and {1}', + 'in {0}, {1}': 'in {0}, {1}', + 'just now': 'à l’instant', 'specify commit': 'specify commit', - 'Start following this repository': 'Start following this repository', - 'Stop following this repository': 'Stop following this repository', 'truncated result': 'truncated result', 'truncated results': 'truncated results', - 'Unfollow': 'Unfollow', - 'Updating...': 'Updating...' + '{0} active out of {1} users': '{0} active out of {1} users', + '{0} ago': '{0} ago', + '{0} and {1}': '{0} and {1}', + '{0} and {1} ago': '{0} and {1} ago', + '{0} day': '{0} day', + '{0} days': '{0} days', + '{0} hour': '{0} hour', + '{0} hours': '{0} hours', + '{0} min': '{0} min', + '{0} month': '{0} month', + '{0} months': '{0} months', + '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', + '{0} sec': '{0} sec', + '{0} year': '{0} year', + '{0} years': '{0} years', + '{0}, {1} ago': '{0}, {1} ago' }; \ No newline at end of file diff --git a/rhodecode/public/js/rhodecode/i18n/it.js b/rhodecode/public/js/rhodecode/i18n/it.js --- a/rhodecode/public/js/rhodecode/i18n/it.js +++ b/rhodecode/public/js/rhodecode/i18n/it.js @@ -5,35 +5,71 @@ */ //JS translations map var _TM = { - '{0} active out of {1} users': '{0} active out of {1} users', - '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', 'Add another comment': 'Add another comment', - 'disabled': 'disabilitato', - 'enabled': 'abilitato', + 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', 'Follow': 'Follow', - 'loading ...': 'loading ...', 'Loading ...': 'Loading ...', 'Loading failed': 'Loading failed', 'Loading more results...': 'Loading more results...', + 'No bookmarks available yet.': 'No bookmarks available yet.', + 'No branches available yet.': 'No branches available yet.', + 'No gists available yet.': 'No gists available yet.', 'No matches found': 'No matches found', 'No matching files': 'No matching files', + 'No pull requests available yet.': 'No pull requests available yet.', + 'No repositories available yet.': 'No repositories available yet.', + 'No repository groups available yet.': 'No repository groups available yet.', 'No results': 'No results', + 'No tags available yet.': 'No tags available yet.', + 'No user groups available yet.': 'No user groups available yet.', + 'No users available yet.': 'No users available yet.', 'One result is available, press enter to select it.': 'One result is available, press enter to select it.', 'Open new pull request': 'Apri una richiesta PULL', 'Open new pull request for selected commit': 'Open new pull request for selected commit', + 'Please delete {0} character': 'Please delete {0} character', + 'Please delete {0} characters': 'Please delete {0} characters', + 'Please enter {0} or more character': 'Please enter {0} or more character', + 'Please enter {0} or more characters': 'Please enter {0} or more characters', 'Searching...': 'Searching...', - 'Select changeset': 'Select changeset', - 'Select commit': 'Seleziona una commit', 'Selection link': 'Selection link', 'Set status to Approved': 'Set status to Approved', 'Set status to Rejected': 'Set status to Rejected', + 'Show more': 'Show more', 'Show selected commit __S': 'Show selected commit __S', 'Show selected commits __S ... __E': 'Show selected commits __S ... __E', + 'Start following this repository': 'Start following this repository', + 'Status Review': 'Status Review', + 'Stop following this repository': 'Stop following this repository', + 'Submitting...': 'Submitting...', + 'Unfollow': 'Unfollow', + 'Updating...': 'Updating...', + 'You can only select {0} item': 'You can only select {0} item', + 'You can only select {0} items': 'You can only select {0} items', + 'disabled': 'disabilitato', + 'enabled': 'abilitato', + 'file': 'file', + 'files': 'files', + 'in {0}': 'in {0}', + 'in {0} and {1}': 'in {0} and {1}', + 'in {0}, {1}': 'in {0}, {1}', + 'just now': 'proprio ora', 'specify commit': 'specify commit', - 'Start following this repository': 'Start following this repository', - 'Stop following this repository': 'Stop following this repository', 'truncated result': 'truncated result', 'truncated results': 'truncated results', - 'Unfollow': 'Unfollow', - 'Updating...': 'Updating...' + '{0} active out of {1} users': '{0} active out of {1} users', + '{0} ago': '{0} ago', + '{0} and {1}': '{0} and {1}', + '{0} and {1} ago': '{0} and {1} ago', + '{0} day': '{0} day', + '{0} days': '{0} days', + '{0} hour': '{0} hour', + '{0} hours': '{0} hours', + '{0} min': '{0} min', + '{0} month': '{0} month', + '{0} months': '{0} months', + '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', + '{0} sec': '{0} sec', + '{0} year': '{0} year', + '{0} years': '{0} years', + '{0}, {1} ago': '{0}, {1} ago' }; \ No newline at end of file diff --git a/rhodecode/public/js/rhodecode/i18n/ja.js b/rhodecode/public/js/rhodecode/i18n/ja.js --- a/rhodecode/public/js/rhodecode/i18n/ja.js +++ b/rhodecode/public/js/rhodecode/i18n/ja.js @@ -5,35 +5,71 @@ */ //JS translations map var _TM = { - '{0} active out of {1} users': '{0} active out of {1} users', - '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', 'Add another comment': 'Add another comment', - 'disabled': '無効', - 'enabled': '有効', + 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', 'Follow': 'Follow', - 'loading ...': 'loading ...', 'Loading ...': 'Loading ...', 'Loading failed': 'Loading failed', 'Loading more results...': 'Loading more results...', + 'No bookmarks available yet.': 'No bookmarks available yet.', + 'No branches available yet.': 'No branches available yet.', + 'No gists available yet.': 'No gists available yet.', 'No matches found': 'No matches found', 'No matching files': 'No matching files', + 'No pull requests available yet.': 'No pull requests available yet.', + 'No repositories available yet.': 'No repositories available yet.', + 'No repository groups available yet.': 'No repository groups available yet.', 'No results': 'No results', + 'No tags available yet.': 'No tags available yet.', + 'No user groups available yet.': 'No user groups available yet.', + 'No users available yet.': 'No users available yet.', 'One result is available, press enter to select it.': 'One result is available, press enter to select it.', 'Open new pull request': '新しいプルリクエストを作成', 'Open new pull request for selected commit': 'Open new pull request for selected commit', + 'Please delete {0} character': 'Please delete {0} character', + 'Please delete {0} characters': 'Please delete {0} characters', + 'Please enter {0} or more character': 'Please enter {0} or more character', + 'Please enter {0} or more characters': 'Please enter {0} or more characters', 'Searching...': 'Searching...', - 'Select changeset': 'Select changeset', - 'Select commit': 'Select commit', 'Selection link': 'Selection link', 'Set status to Approved': 'Set status to Approved', 'Set status to Rejected': 'Set status to Rejected', + 'Show more': 'Show more', 'Show selected commit __S': 'Show selected commit __S', 'Show selected commits __S ... __E': 'Show selected commits __S ... __E', + 'Start following this repository': 'Start following this repository', + 'Status Review': 'Status Review', + 'Stop following this repository': 'Stop following this repository', + 'Submitting...': 'Submitting...', + 'Unfollow': 'Unfollow', + 'Updating...': 'Updating...', + 'You can only select {0} item': 'You can only select {0} item', + 'You can only select {0} items': 'You can only select {0} items', + 'disabled': '無効', + 'enabled': '有効', + 'file': 'file', + 'files': 'files', + 'in {0}': 'in {0}', + 'in {0} and {1}': 'in {0} and {1}', + 'in {0}, {1}': 'in {0}, {1}', + 'just now': 'たったいま', 'specify commit': 'specify commit', - 'Start following this repository': 'Start following this repository', - 'Stop following this repository': 'Stop following this repository', 'truncated result': 'truncated result', 'truncated results': 'truncated results', - 'Unfollow': 'Unfollow', - 'Updating...': 'Updating...' + '{0} active out of {1} users': '{0} active out of {1} users', + '{0} ago': '{0} ago', + '{0} and {1}': '{0} and {1}', + '{0} and {1} ago': '{0} and {1} ago', + '{0} day': '{0} day', + '{0} days': '{0} days', + '{0} hour': '{0} hour', + '{0} hours': '{0} hours', + '{0} min': '{0} min', + '{0} month': '{0} month', + '{0} months': '{0} months', + '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', + '{0} sec': '{0} sec', + '{0} year': '{0} year', + '{0} years': '{0} years', + '{0}, {1} ago': '{0}, {1} ago' }; \ No newline at end of file diff --git a/rhodecode/public/js/rhodecode/i18n/js_translations.js b/rhodecode/public/js/rhodecode/i18n/js_translations.js new file mode 100644 --- /dev/null +++ b/rhodecode/public/js/rhodecode/i18n/js_translations.js @@ -0,0 +1,68 @@ +// AUTO GENERATED FILE FOR Babel JS-GETTEXT EXTRACTORS, DO NOT CHANGE +_gettext('Add another comment'); +_gettext('Comment text will be set automatically based on currently selected status ({0}) ...'); +_gettext('Follow'); +_gettext('Loading ...'); +_gettext('Loading failed'); +_gettext('Loading more results...'); +_gettext('No bookmarks available yet.'); +_gettext('No branches available yet.'); +_gettext('No gists available yet.'); +_gettext('No matches found'); +_gettext('No matching files'); +_gettext('No pull requests available yet.'); +_gettext('No repositories available yet.'); +_gettext('No repository groups available yet.'); +_gettext('No results'); +_gettext('No tags available yet.'); +_gettext('No user groups available yet.'); +_gettext('No users available yet.'); +_gettext('One result is available, press enter to select it.'); +_gettext('Open new pull request'); +_gettext('Open new pull request for selected commit'); +_gettext('Please delete {0} character'); +_gettext('Please delete {0} characters'); +_gettext('Please enter {0} or more character'); +_gettext('Please enter {0} or more characters'); +_gettext('Searching...'); +_gettext('Selection link'); +_gettext('Set status to Approved'); +_gettext('Set status to Rejected'); +_gettext('Show more'); +_gettext('Show selected commit __S'); +_gettext('Show selected commits __S ... __E'); +_gettext('Start following this repository'); +_gettext('Status Review'); +_gettext('Stop following this repository'); +_gettext('Submitting...'); +_gettext('Unfollow'); +_gettext('Updating...'); +_gettext('You can only select {0} item'); +_gettext('You can only select {0} items'); +_gettext('disabled'); +_gettext('enabled'); +_gettext('file'); +_gettext('files'); +_gettext('in {0}'); +_gettext('in {0} and {1}'); +_gettext('in {0}, {1}'); +_gettext('just now'); +_gettext('specify commit'); +_gettext('truncated result'); +_gettext('truncated results'); +_gettext('{0} active out of {1} users'); +_gettext('{0} ago'); +_gettext('{0} and {1}'); +_gettext('{0} and {1} ago'); +_gettext('{0} day'); +_gettext('{0} days'); +_gettext('{0} hour'); +_gettext('{0} hours'); +_gettext('{0} min'); +_gettext('{0} month'); +_gettext('{0} months'); +_gettext('{0} results are available, use up and down arrow keys to navigate.'); +_gettext('{0} sec'); +_gettext('{0} year'); +_gettext('{0} years'); +_gettext('{0}, {1} ago'); diff --git a/rhodecode/public/js/rhodecode/i18n/pl.js b/rhodecode/public/js/rhodecode/i18n/pl.js --- a/rhodecode/public/js/rhodecode/i18n/pl.js +++ b/rhodecode/public/js/rhodecode/i18n/pl.js @@ -5,35 +5,71 @@ */ //JS translations map var _TM = { - '{0} active out of {1} users': '{0} active out of {1} users', - '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', 'Add another comment': 'Add another comment', - 'disabled': 'disabled', - 'enabled': 'enabled', + 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', 'Follow': 'Follow', - 'loading ...': 'loading ...', 'Loading ...': 'Loading ...', 'Loading failed': 'Loading failed', 'Loading more results...': 'Loading more results...', + 'No bookmarks available yet.': 'No bookmarks available yet.', + 'No branches available yet.': 'No branches available yet.', + 'No gists available yet.': 'No gists available yet.', 'No matches found': 'No matches found', 'No matching files': 'No matching files', + 'No pull requests available yet.': 'No pull requests available yet.', + 'No repositories available yet.': 'No repositories available yet.', + 'No repository groups available yet.': 'No repository groups available yet.', 'No results': 'No results', + 'No tags available yet.': 'No tags available yet.', + 'No user groups available yet.': 'No user groups available yet.', + 'No users available yet.': 'No users available yet.', 'One result is available, press enter to select it.': 'One result is available, press enter to select it.', 'Open new pull request': 'Otwórz nową prośbę o połączenie gałęzi', 'Open new pull request for selected commit': 'Open new pull request for selected commit', + 'Please delete {0} character': 'Please delete {0} character', + 'Please delete {0} characters': 'Please delete {0} characters', + 'Please enter {0} or more character': 'Please enter {0} or more character', + 'Please enter {0} or more characters': 'Please enter {0} or more characters', 'Searching...': 'Searching...', - 'Select changeset': 'Select changeset', - 'Select commit': 'Select commit', 'Selection link': 'Selection link', 'Set status to Approved': 'Set status to Approved', 'Set status to Rejected': 'Set status to Rejected', + 'Show more': 'Show more', 'Show selected commit __S': 'Show selected commit __S', 'Show selected commits __S ... __E': 'Show selected commits __S ... __E', + 'Start following this repository': 'Start following this repository', + 'Status Review': 'Status Review', + 'Stop following this repository': 'Stop following this repository', + 'Submitting...': 'Submitting...', + 'Unfollow': 'Unfollow', + 'Updating...': 'Updating...', + 'You can only select {0} item': 'You can only select {0} item', + 'You can only select {0} items': 'You can only select {0} items', + 'disabled': 'disabled', + 'enabled': 'enabled', + 'file': 'file', + 'files': 'files', + 'in {0}': 'in {0}', + 'in {0} and {1}': 'in {0} and {1}', + 'in {0}, {1}': 'in {0}, {1}', + 'just now': 'przed chwilą', 'specify commit': 'specify commit', - 'Start following this repository': 'Start following this repository', - 'Stop following this repository': 'Stop following this repository', 'truncated result': 'truncated result', 'truncated results': 'truncated results', - 'Unfollow': 'Unfollow', - 'Updating...': 'Updating...' + '{0} active out of {1} users': '{0} active out of {1} users', + '{0} ago': '{0} ago', + '{0} and {1}': '{0} and {1}', + '{0} and {1} ago': '{0} and {1} ago', + '{0} day': '{0} day', + '{0} days': '{0} days', + '{0} hour': '{0} hour', + '{0} hours': '{0} hours', + '{0} min': '{0} min', + '{0} month': '{0} month', + '{0} months': '{0} months', + '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', + '{0} sec': '{0} sec', + '{0} year': '{0} year', + '{0} years': '{0} years', + '{0}, {1} ago': '{0}, {1} ago' }; \ No newline at end of file diff --git a/rhodecode/public/js/rhodecode/i18n/pt.js b/rhodecode/public/js/rhodecode/i18n/pt.js --- a/rhodecode/public/js/rhodecode/i18n/pt.js +++ b/rhodecode/public/js/rhodecode/i18n/pt.js @@ -5,35 +5,71 @@ */ //JS translations map var _TM = { - '{0} active out of {1} users': '{0} active out of {1} users', - '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', 'Add another comment': 'Add another comment', - 'disabled': 'desabilitado', - 'enabled': 'enabled', + 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', 'Follow': 'Follow', - 'loading ...': 'loading ...', 'Loading ...': 'Loading ...', 'Loading failed': 'Loading failed', 'Loading more results...': 'Loading more results...', + 'No bookmarks available yet.': 'No bookmarks available yet.', + 'No branches available yet.': 'No branches available yet.', + 'No gists available yet.': 'No gists available yet.', 'No matches found': 'No matches found', 'No matching files': 'No matching files', + 'No pull requests available yet.': 'No pull requests available yet.', + 'No repositories available yet.': 'No repositories available yet.', + 'No repository groups available yet.': 'No repository groups available yet.', 'No results': 'No results', + 'No tags available yet.': 'No tags available yet.', + 'No user groups available yet.': 'No user groups available yet.', + 'No users available yet.': 'No users available yet.', 'One result is available, press enter to select it.': 'One result is available, press enter to select it.', 'Open new pull request': 'Crie novo pull request', 'Open new pull request for selected commit': 'Open new pull request for selected commit', + 'Please delete {0} character': 'Please delete {0} character', + 'Please delete {0} characters': 'Please delete {0} characters', + 'Please enter {0} or more character': 'Please enter {0} or more character', + 'Please enter {0} or more characters': 'Please enter {0} or more characters', 'Searching...': 'Searching...', - 'Select changeset': 'Select changeset', - 'Select commit': 'Select commit', 'Selection link': 'Selection link', 'Set status to Approved': 'Set status to Approved', 'Set status to Rejected': 'Set status to Rejected', + 'Show more': 'Show more', 'Show selected commit __S': 'Show selected commit __S', 'Show selected commits __S ... __E': 'Show selected commits __S ... __E', + 'Start following this repository': 'Start following this repository', + 'Status Review': 'Status Review', + 'Stop following this repository': 'Stop following this repository', + 'Submitting...': 'Submitting...', + 'Unfollow': 'Unfollow', + 'Updating...': 'Updating...', + 'You can only select {0} item': 'You can only select {0} item', + 'You can only select {0} items': 'You can only select {0} items', + 'disabled': 'desabilitado', + 'enabled': 'enabled', + 'file': 'file', + 'files': 'files', + 'in {0}': 'in {0}', + 'in {0} and {1}': 'in {0} and {1}', + 'in {0}, {1}': 'in {0}, {1}', + 'just now': 'agora há pouco', 'specify commit': 'specify commit', - 'Start following this repository': 'Start following this repository', - 'Stop following this repository': 'Stop following this repository', 'truncated result': 'truncated result', 'truncated results': 'truncated results', - 'Unfollow': 'Unfollow', - 'Updating...': 'Updating...' + '{0} active out of {1} users': '{0} active out of {1} users', + '{0} ago': '{0} ago', + '{0} and {1}': '{0} and {1}', + '{0} and {1} ago': '{0} and {1} ago', + '{0} day': '{0} day', + '{0} days': '{0} days', + '{0} hour': '{0} hour', + '{0} hours': '{0} hours', + '{0} min': '{0} min', + '{0} month': '{0} month', + '{0} months': '{0} months', + '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', + '{0} sec': '{0} sec', + '{0} year': '{0} year', + '{0} years': '{0} years', + '{0}, {1} ago': '{0}, {1} ago' }; \ No newline at end of file diff --git a/rhodecode/public/js/rhodecode/i18n/ru.js b/rhodecode/public/js/rhodecode/i18n/ru.js --- a/rhodecode/public/js/rhodecode/i18n/ru.js +++ b/rhodecode/public/js/rhodecode/i18n/ru.js @@ -5,35 +5,71 @@ */ //JS translations map var _TM = { - '{0} active out of {1} users': '{0} active out of {1} users', - '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', 'Add another comment': 'Add another comment', - 'disabled': 'отключено', - 'enabled': 'enabled', + 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', 'Follow': 'Follow', - 'loading ...': 'loading ...', 'Loading ...': 'Loading ...', 'Loading failed': 'Loading failed', 'Loading more results...': 'Loading more results...', + 'No bookmarks available yet.': 'No bookmarks available yet.', + 'No branches available yet.': 'No branches available yet.', + 'No gists available yet.': 'No gists available yet.', 'No matches found': 'No matches found', 'No matching files': 'No matching files', + 'No pull requests available yet.': 'No pull requests available yet.', + 'No repositories available yet.': 'No repositories available yet.', + 'No repository groups available yet.': 'No repository groups available yet.', 'No results': 'No results', + 'No tags available yet.': 'No tags available yet.', + 'No user groups available yet.': 'No user groups available yet.', + 'No users available yet.': 'No users available yet.', 'One result is available, press enter to select it.': 'One result is available, press enter to select it.', 'Open new pull request': 'Создать новый pull запрос', 'Open new pull request for selected commit': 'Open new pull request for selected commit', + 'Please delete {0} character': 'Please delete {0} character', + 'Please delete {0} characters': 'Please delete {0} characters', + 'Please enter {0} or more character': 'Please enter {0} or more character', + 'Please enter {0} or more characters': 'Please enter {0} or more characters', 'Searching...': 'Searching...', - 'Select changeset': 'Select changeset', - 'Select commit': 'Select commit', 'Selection link': 'Selection link', 'Set status to Approved': 'Set status to Approved', 'Set status to Rejected': 'Set status to Rejected', + 'Show more': 'Show more', 'Show selected commit __S': 'Show selected commit __S', 'Show selected commits __S ... __E': 'Show selected commits __S ... __E', + 'Start following this repository': 'Start following this repository', + 'Status Review': 'Status Review', + 'Stop following this repository': 'Stop following this repository', + 'Submitting...': 'Submitting...', + 'Unfollow': 'Unfollow', + 'Updating...': 'Updating...', + 'You can only select {0} item': 'You can only select {0} item', + 'You can only select {0} items': 'You can only select {0} items', + 'disabled': 'отключено', + 'enabled': 'enabled', + 'file': 'file', + 'files': 'files', + 'in {0}': 'in {0}', + 'in {0} and {1}': 'in {0} and {1}', + 'in {0}, {1}': 'in {0}, {1}', + 'just now': 'прямо сейчас', 'specify commit': 'specify commit', - 'Start following this repository': 'Start following this repository', - 'Stop following this repository': 'Stop following this repository', 'truncated result': 'truncated result', 'truncated results': 'truncated results', - 'Unfollow': 'Unfollow', - 'Updating...': 'Updating...' + '{0} active out of {1} users': '{0} active out of {1} users', + '{0} ago': '{0} ago', + '{0} and {1}': '{0} and {1}', + '{0} and {1} ago': '{0} and {1} ago', + '{0} day': '{0} day', + '{0} days': '{0} days', + '{0} hour': '{0} hour', + '{0} hours': '{0} hours', + '{0} min': '{0} min', + '{0} month': '{0} month', + '{0} months': '{0} months', + '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', + '{0} sec': '{0} sec', + '{0} year': '{0} year', + '{0} years': '{0} years', + '{0}, {1} ago': '{0}, {1} ago' }; \ No newline at end of file diff --git a/rhodecode/public/js/rhodecode/i18n/zh.js b/rhodecode/public/js/rhodecode/i18n/zh.js --- a/rhodecode/public/js/rhodecode/i18n/zh.js +++ b/rhodecode/public/js/rhodecode/i18n/zh.js @@ -5,35 +5,71 @@ */ //JS translations map var _TM = { - '{0} active out of {1} users': '{0} active out of {1} users', - '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', 'Add another comment': 'Add another comment', - 'disabled': '禁用', - 'enabled': 'enabled', + 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', 'Follow': 'Follow', - 'loading ...': 'loading ...', 'Loading ...': 'Loading ...', 'Loading failed': 'Loading failed', 'Loading more results...': 'Loading more results...', + 'No bookmarks available yet.': 'No bookmarks available yet.', + 'No branches available yet.': 'No branches available yet.', + 'No gists available yet.': 'No gists available yet.', 'No matches found': 'No matches found', 'No matching files': 'No matching files', + 'No pull requests available yet.': 'No pull requests available yet.', + 'No repositories available yet.': 'No repositories available yet.', + 'No repository groups available yet.': 'No repository groups available yet.', 'No results': 'No results', + 'No tags available yet.': 'No tags available yet.', + 'No user groups available yet.': 'No user groups available yet.', + 'No users available yet.': 'No users available yet.', 'One result is available, press enter to select it.': 'One result is available, press enter to select it.', 'Open new pull request': '新建拉取请求', 'Open new pull request for selected commit': 'Open new pull request for selected commit', + 'Please delete {0} character': 'Please delete {0} character', + 'Please delete {0} characters': 'Please delete {0} characters', + 'Please enter {0} or more character': 'Please enter {0} or more character', + 'Please enter {0} or more characters': 'Please enter {0} or more characters', 'Searching...': 'Searching...', - 'Select changeset': 'Select changeset', - 'Select commit': 'Select commit', 'Selection link': 'Selection link', 'Set status to Approved': 'Set status to Approved', 'Set status to Rejected': 'Set status to Rejected', + 'Show more': 'Show more', 'Show selected commit __S': 'Show selected commit __S', 'Show selected commits __S ... __E': 'Show selected commits __S ... __E', + 'Start following this repository': 'Start following this repository', + 'Status Review': 'Status Review', + 'Stop following this repository': 'Stop following this repository', + 'Submitting...': 'Submitting...', + 'Unfollow': 'Unfollow', + 'Updating...': 'Updating...', + 'You can only select {0} item': 'You can only select {0} item', + 'You can only select {0} items': 'You can only select {0} items', + 'disabled': '禁用', + 'enabled': 'enabled', + 'file': 'file', + 'files': 'files', + 'in {0}': 'in {0}', + 'in {0} and {1}': 'in {0} and {1}', + 'in {0}, {1}': 'in {0}, {1}', + 'just now': '刚才', 'specify commit': 'specify commit', - 'Start following this repository': 'Start following this repository', - 'Stop following this repository': 'Stop following this repository', 'truncated result': 'truncated result', 'truncated results': 'truncated results', - 'Unfollow': 'Unfollow', - 'Updating...': 'Updating...' + '{0} active out of {1} users': '{0} active out of {1} users', + '{0} ago': '{0} ago', + '{0} and {1}': '{0} and {1}', + '{0} and {1} ago': '{0} and {1} ago', + '{0} day': '{0} day', + '{0} days': '{0} days', + '{0} hour': '{0} hour', + '{0} hours': '{0} hours', + '{0} min': '{0} min', + '{0} month': '{0} month', + '{0} months': '{0} months', + '{0} results are available, use up and down arrow keys to navigate.': '{0} results are available, use up and down arrow keys to navigate.', + '{0} sec': '{0} sec', + '{0} year': '{0} year', + '{0} years': '{0} years', + '{0}, {1} ago': '{0}, {1} ago' }; \ No newline at end of file diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -4,7 +4,8 @@ * DO NOT CHANGE THIS FILE MANUALLY * * * * * - * This file is automatically generated when the app starts up. * + * This file is automatically generated when the app starts up with * + * generate_js_files = true * * * * To add a route here pass jsroute=True to the route definition in the app * * * @@ -27,7 +28,7 @@ function registerRCRoutes() { pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']); pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']); pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']); - pyroutes.register('changeset_info', '/changeset_info/%(repo_name)s/%(revision)s', ['repo_name', 'revision']); + pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']); pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']); pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']); pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']); @@ -44,7 +45,7 @@ function registerRCRoutes() { pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']); pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); - pyroutes.register('files_metadata_list_home', '/%(repo_name)s/metadata_list/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']); + pyroutes.register('files_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']); pyroutes.register('summary_home_slash', '/%(repo_name)s/', ['repo_name']); pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']); } diff --git a/rhodecode/public/js/src/codemirror/codemirror.js b/rhodecode/public/js/src/codemirror/codemirror.js --- a/rhodecode/public/js/src/codemirror/codemirror.js +++ b/rhodecode/public/js/src/codemirror/codemirror.js @@ -13,7 +13,7 @@ else if (typeof define == "function" && define.amd) // AMD return define([], mod); else // Plain browser env - this.CodeMirror = mod(); + (this || window).CodeMirror = mod(); })(function() { "use strict"; @@ -21,27 +21,29 @@ // Kludges for bugs and behavior differences that can't be feature // detected are enabled based on userAgent etc sniffing. - - var gecko = /gecko\/\d/i.test(navigator.userAgent); - var ie_upto10 = /MSIE \d/.test(navigator.userAgent); - var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent); + var userAgent = navigator.userAgent; + var platform = navigator.platform; + + var gecko = /gecko\/\d/i.test(userAgent); + var ie_upto10 = /MSIE \d/.test(userAgent); + var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent); var ie = ie_upto10 || ie_11up; var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]); - var webkit = /WebKit\//.test(navigator.userAgent); - var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); - var chrome = /Chrome\//.test(navigator.userAgent); - var presto = /Opera\//.test(navigator.userAgent); + var webkit = /WebKit\//.test(userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent); + var chrome = /Chrome\//.test(userAgent); + var presto = /Opera\//.test(userAgent); var safari = /Apple Computer/.test(navigator.vendor); - var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent); - var phantom = /PhantomJS/.test(navigator.userAgent); - - var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent); + var phantom = /PhantomJS/.test(userAgent); + + var ios = /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent); // This is woefully incomplete. Suggestions for alternative methods welcome. - var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent); - var mac = ios || /Mac/.test(navigator.platform); - var windows = /win/i.test(navigator.platform); - - var presto_version = presto && navigator.userAgent.match(/Version\/(\d*\.\d*)/); + var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent); + var mac = ios || /Mac/.test(platform); + var windows = /win/i.test(platform); + + var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/); if (presto_version) presto_version = Number(presto_version[1]); if (presto_version && presto_version >= 15) { presto = false; webkit = true; } // Some browsers use the wrong event properties to signal cmd/ctrl on OS X @@ -65,7 +67,7 @@ setGuttersForLineNumbers(options); var doc = options.value; - if (typeof doc == "string") doc = new Doc(doc, options.mode); + if (typeof doc == "string") doc = new Doc(doc, options.mode, null, options.lineSeparator); this.doc = doc; var input = new CodeMirror.inputStyles[options.inputStyle](this); @@ -87,6 +89,7 @@ focused: false, suppressEdits: false, // used to disable editing during key handlers when in readOnly mode pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll + selectingText: false, draggingText: false, highlight: new Delayed(), // stores highlight worker timeout keySeq: null, // Unfinished key sequence @@ -407,7 +410,7 @@ if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal"); }); - this.checkedOverlay = false; + this.checkedZeroWidth = false; // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; } @@ -442,29 +445,43 @@ this.horiz.firstChild.style.width = "0"; } - if (!this.checkedOverlay && measure.clientHeight > 0) { - if (sWidth == 0) this.overlayHack(); - this.checkedOverlay = true; + if (!this.checkedZeroWidth && measure.clientHeight > 0) { + if (sWidth == 0) this.zeroWidthHack(); + this.checkedZeroWidth = true; } return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0}; }, setScrollLeft: function(pos) { if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos; + if (this.disableHoriz) this.enableZeroWidthBar(this.horiz, this.disableHoriz); }, setScrollTop: function(pos) { if (this.vert.scrollTop != pos) this.vert.scrollTop = pos; - }, - overlayHack: function() { + if (this.disableVert) this.enableZeroWidthBar(this.vert, this.disableVert); + }, + zeroWidthHack: function() { var w = mac && !mac_geMountainLion ? "12px" : "18px"; - this.horiz.style.minHeight = this.vert.style.minWidth = w; - var self = this; - var barMouseDown = function(e) { - if (e_target(e) != self.vert && e_target(e) != self.horiz) - operation(self.cm, onMouseDown)(e); - }; - on(this.vert, "mousedown", barMouseDown); - on(this.horiz, "mousedown", barMouseDown); + this.horiz.style.height = this.vert.style.width = w; + this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none"; + this.disableHoriz = new Delayed; + this.disableVert = new Delayed; + }, + enableZeroWidthBar: function(bar, delay) { + bar.style.pointerEvents = "auto"; + function maybeDisable() { + // To find out whether the scrollbar is still visible, we + // check whether the element under the pixel in the bottom + // left corner of the scrollbar box is the scrollbar box + // itself (when the bar is still visible) or its filler child + // (when the bar is hidden). If it is still visible, we keep + // it enabled, if it's hidden, we disable pointer events. + var box = bar.getBoundingClientRect(); + var elt = document.elementFromPoint(box.left + 1, box.bottom - 1); + if (elt != bar) bar.style.pointerEvents = "none"; + else delay.set(1000, maybeDisable); + } + delay.set(1000, maybeDisable); }, clear: function() { var parent = this.horiz.parentNode; @@ -714,7 +731,7 @@ // width and height. removeChildren(display.cursorDiv); removeChildren(display.selectionDiv); - display.gutters.style.height = 0; + display.gutters.style.height = display.sizer.style.minHeight = 0; if (different) { display.lastWrapHeight = update.wrapperHeight; @@ -806,7 +823,7 @@ // given line. function updateWidgetHeight(line) { if (line.widgets) for (var i = 0; i < line.widgets.length; ++i) - line.widgets[i].height = line.widgets[i].node.offsetHeight; + line.widgets[i].height = line.widgets[i].node.parentNode.offsetHeight; } // Do a bulk-read of the DOM positions and sizes needed to draw the @@ -955,12 +972,22 @@ lineView.node.removeChild(lineView.gutter); lineView.gutter = null; } + if (lineView.gutterBackground) { + lineView.node.removeChild(lineView.gutterBackground); + lineView.gutterBackground = null; + } + if (lineView.line.gutterClass) { + var wrap = ensureLineWrapped(lineView); + lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass, + "left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + + "px; width: " + dims.gutterTotalWidth + "px"); + wrap.insertBefore(lineView.gutterBackground, lineView.text); + } var markers = lineView.line.gutterMarkers; if (cm.options.lineNumbers || markers) { var wrap = ensureLineWrapped(lineView); var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " + - (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + - "px; width: " + dims.gutterTotalWidth + "px"); + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"); cm.display.input.setUneditable(gutterWrap); wrap.insertBefore(gutterWrap, lineView.text); if (lineView.line.gutterClass) @@ -1067,10 +1094,6 @@ if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); } } - function isReadOnly(cm) { - return cm.options.readOnly || cm.doc.cantEdit; - } - // This will be set to an array of strings when copying, so that, // when pasting, we know what kind of selections the copied text // was made out of. @@ -1082,13 +1105,18 @@ if (!sel) sel = doc.sel; var paste = cm.state.pasteIncoming || origin == "paste"; - var textLines = splitLines(inserted), multiPaste = null; + var textLines = doc.splitLines(inserted), multiPaste = null; // When pasing N lines into N selections, insert one line per selection if (paste && sel.ranges.length > 1) { - if (lastCopied && lastCopied.join("\n") == inserted) - multiPaste = sel.ranges.length % lastCopied.length == 0 && map(lastCopied, splitLines); - else if (textLines.length == sel.ranges.length) + if (lastCopied && lastCopied.join("\n") == inserted) { + if (sel.ranges.length % lastCopied.length == 0) { + multiPaste = []; + for (var i = 0; i < lastCopied.length; i++) + multiPaste.push(doc.splitLines(lastCopied[i])); + } + } else if (textLines.length == sel.ranges.length) { multiPaste = map(textLines, function(l) { return [l]; }); + } } // Normal behavior is to insert the new text into every selection @@ -1120,7 +1148,8 @@ var pasted = e.clipboardData && e.clipboardData.getData("text/plain"); if (pasted) { e.preventDefault(); - runInOp(cm, function() { applyTextInput(cm, pasted, 0, null, "paste"); }); + if (!cm.isReadOnly() && !cm.options.disableInput) + runInOp(cm, function() { applyTextInput(cm, pasted, 0, null, "paste"); }); return true; } } @@ -1222,13 +1251,14 @@ }); on(te, "paste", function(e) { - if (handlePaste(e, cm)) return true; + if (signalDOMEvent(cm, e) || handlePaste(e, cm)) return cm.state.pasteIncoming = true; input.fastPoll(); }); function prepareCopyCut(e) { + if (signalDOMEvent(cm, e)) return if (cm.somethingSelected()) { lastCopied = cm.getSelections(); if (input.inaccurateSelection) { @@ -1256,7 +1286,7 @@ on(te, "copy", prepareCopyCut); on(display.scroller, "paste", function(e) { - if (eventInWidget(display, e)) return; + if (eventInWidget(display, e) || signalDOMEvent(cm, e)) return; cm.state.pasteIncoming = true; input.focus(); }); @@ -1268,6 +1298,7 @@ on(te, "compositionstart", function() { var start = cm.getCursor("from"); + if (input.composing) input.composing.range.clear() input.composing = { start: start, range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"}) @@ -1388,8 +1419,8 @@ // will be the case when there is a lot of text in the textarea, // in which case reading its value would be expensive. if (this.contextMenuPending || !cm.state.focused || - (hasSelection(input) && !prevInput) || - isReadOnly(cm) || cm.options.disableInput || cm.state.keySeq) + (hasSelection(input) && !prevInput && !this.composing) || + cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq) return false; var text = input.value; @@ -1516,6 +1547,10 @@ } }, + readOnlyChanged: function(val) { + if (!val) this.reset(); + }, + setUneditable: nothing, needsContentAttribute: false @@ -1534,10 +1569,11 @@ init: function(display) { var input = this, cm = input.cm; var div = input.div = display.lineDiv; - div.contentEditable = "true"; disableBrowserMagic(div); - on(div, "paste", function(e) { handlePaste(e, cm); }) + on(div, "paste", function(e) { + if (!signalDOMEvent(cm, e)) handlePaste(e, cm); + }) on(div, "compositionstart", function(e) { var data = e.data; @@ -1575,11 +1611,12 @@ on(div, "input", function() { if (input.composing) return; - if (!input.pollContent()) + if (cm.isReadOnly() || !input.pollContent()) runInOp(input.cm, function() {regChange(cm);}); }); function onCopyCut(e) { + if (signalDOMEvent(cm, e)) return if (cm.somethingSelected()) { lastCopied = cm.getSelections(); if (e.type == "cut") cm.replaceSelection("", null, "cut"); @@ -1655,8 +1692,13 @@ try { var rng = range(start.node, start.offset, end.offset, end.node); } catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible if (rng) { - sel.removeAllRanges(); - sel.addRange(rng); + if (!gecko && this.cm.state.focused) { + sel.collapse(start.node, start.offset); + if (!rng.collapsed) sel.addRange(rng); + } else { + sel.removeAllRanges(); + sel.addRange(rng); + } if (old && sel.anchorNode == null) sel.addRange(old); else if (gecko) this.startGracePeriod(); } @@ -1756,7 +1798,7 @@ var toNode = display.view[toIndex + 1].node.previousSibling; } - var newText = splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); + var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); while (newText.length > 1 && oldText.length > 1) { if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } @@ -1800,17 +1842,24 @@ this.div.focus(); }, applyComposition: function(composing) { - if (composing.data && composing.data != composing.startData) + if (this.cm.isReadOnly()) + operation(this.cm, regChange)(this.cm) + else if (composing.data && composing.data != composing.startData) operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel); }, setUneditable: function(node) { - node.setAttribute("contenteditable", "false"); + node.contentEditable = "false" }, onKeyPress: function(e) { e.preventDefault(); - operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); + if (!this.cm.isReadOnly()) + operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); + }, + + readOnlyChanged: function(val) { + this.div.contentEditable = String(val != "nocursor") }, onContextMenu: nothing, @@ -1912,7 +1961,7 @@ } function domTextBetween(cm, from, to, fromLine, toLine) { - var text = "", closing = false; + var text = "", closing = false, lineSep = cm.doc.lineSeparator(); function recognizeMarker(id) { return function(marker) { return marker.id == id; }; } function walk(node) { if (node.nodeType == 1) { @@ -1926,7 +1975,7 @@ if (markerID) { var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); if (found.length && (range = found[0].find())) - text += getBetween(cm.doc, range.from, range.to).join("\n"); + text += getBetween(cm.doc, range.from, range.to).join(lineSep); return; } if (node.getAttribute("contenteditable") == "false") return; @@ -1938,7 +1987,7 @@ var val = node.nodeValue; if (!val) return; if (closing) { - text += "\n"; + text += lineSep; closing = false; } text += val; @@ -2110,7 +2159,7 @@ // Give beforeSelectionChange handlers a change to influence a // selection update. - function filterSelectionChange(doc, sel) { + function filterSelectionChange(doc, sel, options) { var obj = { ranges: sel.ranges, update: function(ranges) { @@ -2118,7 +2167,8 @@ for (var i = 0; i < ranges.length; i++) this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), clipPos(doc, ranges[i].head)); - } + }, + origin: options && options.origin }; signal(doc, "beforeSelectionChange", doc, obj); if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); @@ -2144,7 +2194,7 @@ function setSelectionNoUndo(doc, sel, options) { if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) - sel = filterSelectionChange(doc, sel); + sel = filterSelectionChange(doc, sel, options); var bias = options && options.bias || (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); @@ -2178,8 +2228,9 @@ var out; for (var i = 0; i < sel.ranges.length; i++) { var range = sel.ranges[i]; - var newAnchor = skipAtomic(doc, range.anchor, bias, mayClear); - var newHead = skipAtomic(doc, range.head, bias, mayClear); + var old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i]; + var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear); + var newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear); if (out || newAnchor != range.anchor || newHead != range.head) { if (!out) out = sel.ranges.slice(0, i); out[i] = new Range(newAnchor, newHead); @@ -2188,54 +2239,59 @@ return out ? normalizeSelection(out, sel.primIndex) : sel; } - // Ensure a given position is not inside an atomic range. - function skipAtomic(doc, pos, bias, mayClear) { - var flipped = false, curPos = pos; - var dir = bias || 1; - doc.cantEdit = false; - search: for (;;) { - var line = getLine(doc, curPos.line); - if (line.markedSpans) { - for (var i = 0; i < line.markedSpans.length; ++i) { - var sp = line.markedSpans[i], m = sp.marker; - if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && - (sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { - if (mayClear) { - signal(m, "beforeCursorEnter"); - if (m.explicitlyCleared) { - if (!line.markedSpans) break; - else {--i; continue;} - } - } - if (!m.atomic) continue; - var newPos = m.find(dir < 0 ? -1 : 1); - if (cmp(newPos, curPos) == 0) { - newPos.ch += dir; - if (newPos.ch < 0) { - if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); - else newPos = null; - } else if (newPos.ch > line.text.length) { - if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); - else newPos = null; - } - if (!newPos) { - if (flipped) { - // Driven in a corner -- no valid cursor position found at all - // -- try again *with* clearing, if we didn't already - if (!mayClear) return skipAtomic(doc, pos, bias, true); - // Otherwise, turn off editing until further notice, and return the start of the doc - doc.cantEdit = true; - return Pos(doc.first, 0); - } - flipped = true; newPos = pos; dir = -dir; - } - } - curPos = newPos; - continue search; + function skipAtomicInner(doc, pos, oldPos, dir, mayClear) { + var line = getLine(doc, pos.line); + if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + if ((sp.from == null || (m.inclusiveLeft ? sp.from <= pos.ch : sp.from < pos.ch)) && + (sp.to == null || (m.inclusiveRight ? sp.to >= pos.ch : sp.to > pos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) break; + else {--i; continue;} } } - } - return curPos; + if (!m.atomic) continue; + + if (oldPos) { + var near = m.find(dir < 0 ? 1 : -1), diff; + if (dir < 0 ? m.inclusiveRight : m.inclusiveLeft) near = movePos(doc, near, -dir, line); + if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0)) + return skipAtomicInner(doc, near, pos, dir, mayClear); + } + + var far = m.find(dir < 0 ? -1 : 1); + if (dir < 0 ? m.inclusiveLeft : m.inclusiveRight) far = movePos(doc, far, dir, line); + return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null; + } + } + return pos; + } + + // Ensure a given position is not inside an atomic range. + function skipAtomic(doc, pos, oldPos, bias, mayClear) { + var dir = bias || 1; + var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) || + (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) || + skipAtomicInner(doc, pos, oldPos, -dir, mayClear) || + (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true)); + if (!found) { + doc.cantEdit = true; + return Pos(doc.first, 0); + } + return found; + } + + function movePos(doc, pos, dir, line) { + if (dir < 0 && pos.ch == 0) { + if (pos.line > doc.first) return clipPos(doc, Pos(pos.line - 1)); + else return null; + } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) { + if (pos.line < doc.first + doc.size - 1) return Pos(pos.line + 1, 0); + else return null; + } else { + return new Pos(pos.line, pos.ch + dir); } } @@ -2255,7 +2311,7 @@ var range = doc.sel.ranges[i]; var collapsed = range.empty(); if (collapsed || cm.options.showCursorWhenSelecting) - drawSelectionCursor(cm, range, curFragment); + drawSelectionCursor(cm, range.head, curFragment); if (!collapsed) drawSelectionRange(cm, range, selFragment); } @@ -2263,8 +2319,8 @@ } // Draws a cursor for the given range - function drawSelectionCursor(cm, range, output) { - var pos = cursorCoords(cm, range.head, "div", null, null, !cm.options.singleCursorHeightPerLine); + function drawSelectionCursor(cm, head, output) { + var pos = cursorCoords(cm, head, "div", null, null, !cm.options.singleCursorHeightPerLine); var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); cursor.style.left = pos.left + "px"; @@ -2388,8 +2444,8 @@ doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) { if (doc.frontier >= cm.display.viewFrom) { // Visible - var oldStyles = line.styles; - var highlighted = highlightLine(cm, line, state, true); + var oldStyles = line.styles, tooLong = line.text.length > cm.options.maxHighlightLength; + var highlighted = highlightLine(cm, line, tooLong ? copyState(doc.mode, state) : state, true); line.styles = highlighted.styles; var oldCls = line.styleClasses, newCls = highlighted.classes; if (newCls) line.styleClasses = newCls; @@ -2398,9 +2454,10 @@ oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; if (ischange) changedLines.push(doc.frontier); - line.stateAfter = copyState(doc.mode, state); + line.stateAfter = tooLong ? state : copyState(doc.mode, state); } else { - processLine(cm, line.text, state); + if (line.text.length <= cm.options.maxHighlightLength) + processLine(cm, line.text, state); line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; } ++doc.frontier; @@ -2545,10 +2602,12 @@ function prepareMeasureForLine(cm, line) { var lineN = lineNo(line); var view = findViewForLine(cm, lineN); - if (view && !view.text) + if (view && !view.text) { view = null; - else if (view && view.changes) + } else if (view && view.changes) { updateLineForChanges(cm, view, lineN, getDimensions(cm)); + cm.curOp.forceUpdate = true; + } if (!view) view = updateExternalMeasurement(cm, line); @@ -2961,12 +3020,12 @@ var callbacks = group.delayedCallbacks, i = 0; do { for (; i < callbacks.length; i++) - callbacks[i](); + callbacks[i].call(null); for (var j = 0; j < group.ops.length; j++) { var op = group.ops[j]; if (op.cursorActivityHandlers) while (op.cursorActivityCalled < op.cursorActivityHandlers.length) - op.cursorActivityHandlers[op.cursorActivityCalled++](op.cm); + op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm); } } while (i < callbacks.length); } @@ -3060,7 +3119,8 @@ if (cm.state.focused && op.updateInput) cm.display.input.reset(op.typing); - if (op.focus && op.focus == activeElt()) ensureFocus(op.cm); + if (op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus())) + ensureFocus(op.cm); } function endOperation_finish(op) { @@ -3375,7 +3435,7 @@ return dx * dx + dy * dy > 20 * 20; } on(d.scroller, "touchstart", function(e) { - if (!isMouseLikeTouchEvent(e)) { + if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e)) { clearTimeout(touchFinished); var now = +new Date; d.activeTouch = {start: now, moved: false, @@ -3426,9 +3486,11 @@ on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); d.dragFunctions = { - simple: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);}, + enter: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);}, + over: function(e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e); }}, start: function(e){onDragStart(cm, e);}, - drop: operation(cm, onDrop) + drop: operation(cm, onDrop), + leave: function() {clearDragCursor(cm);} }; var inp = d.input.getField(); @@ -3445,8 +3507,9 @@ var funcs = cm.display.dragFunctions; var toggle = value ? on : off; toggle(cm.display.scroller, "dragstart", funcs.start); - toggle(cm.display.scroller, "dragenter", funcs.simple); - toggle(cm.display.scroller, "dragover", funcs.simple); + toggle(cm.display.scroller, "dragenter", funcs.enter); + toggle(cm.display.scroller, "dragover", funcs.over); + toggle(cm.display.scroller, "dragleave", funcs.leave); toggle(cm.display.scroller, "drop", funcs.drop); } } @@ -3501,7 +3564,7 @@ // not interfere with, such as a scrollbar or widget. function onMouseDown(e) { var cm = this, display = cm.display; - if (display.activeTouch && display.input.supportsTouch() || signalDOMEvent(cm, e)) return; + if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) return; display.shift = e.shiftKey; if (eventInWidget(display, e)) { @@ -3519,7 +3582,10 @@ switch (e_button(e)) { case 1: - if (start) + // #3261: make sure, that we're not starting a second selection + if (cm.state.selectingText) + cm.state.selectingText(e); + else if (start) leftButtonDown(cm, e, start); else if (e_target(e) == display.scroller) e_preventDefault(e); @@ -3554,7 +3620,7 @@ } var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained; - if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && + if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() && type == "single" && (contained = sel.contains(start)) > -1 && (cmp((contained = sel.ranges[contained]).from(), start) < 0 || start.xRel > 0) && (cmp(contained.to(), start) > 0 || start.xRel < 0)) @@ -3639,7 +3705,8 @@ setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex), {scroll: false, origin: "*mouse"}); } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) { - setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0)); + setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0), + {scroll: false, origin: "*mouse"}); startSel = doc.sel; } else { replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); @@ -3717,6 +3784,7 @@ } function done(e) { + cm.state.selectingText = false; counter = Infinity; e_preventDefault(e); display.input.focus(); @@ -3730,13 +3798,14 @@ else extend(e); }); var up = operation(cm, done); + cm.state.selectingText = up; on(document, "mousemove", move); on(document, "mouseup", up); } // Determines whether an event happened in the gutter, and fires the // handlers for the corresponding event. - function gutterEvent(cm, e, type, prevent, signalfn) { + function gutterEvent(cm, e, type, prevent) { try { var mX = e.clientX, mY = e.clientY; } catch(e) { return false; } if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false; @@ -3753,14 +3822,14 @@ if (g && g.getBoundingClientRect().right >= mX) { var line = lineAtHeight(cm.doc, mY); var gutter = cm.options.gutters[i]; - signalfn(cm, type, cm, line, gutter, e); + signal(cm, type, cm, line, gutter, e); return e_defaultPrevented(e); } } } function clickInGutter(cm, e) { - return gutterEvent(cm, e, "gutterClick", true, signalLater); + return gutterEvent(cm, e, "gutterClick", true); } // Kludge to work around strange IE behavior where it'll sometimes @@ -3769,23 +3838,32 @@ function onDrop(e) { var cm = this; + clearDragCursor(cm); if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; e_preventDefault(e); if (ie) lastDrop = +new Date; var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; - if (!pos || isReadOnly(cm)) return; + if (!pos || cm.isReadOnly()) return; // Might be a file drop, in which case we simply extract the text // and insert it. if (files && files.length && window.FileReader && window.File) { var n = files.length, text = Array(n), read = 0; var loadFile = function(file, i) { + if (cm.options.allowDropFileTypes && + indexOf(cm.options.allowDropFileTypes, file.type) == -1) + return; + var reader = new FileReader; reader.onload = operation(cm, function() { - text[i] = reader.result; + var content = reader.result; + if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) content = ""; + text[i] = content; if (++read == n) { pos = clipPos(cm.doc, pos); - var change = {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}; + var change = {from: pos, to: pos, + text: cm.doc.splitLines(text.join(cm.doc.lineSeparator())), + origin: "paste"}; makeChange(cm.doc, change); setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))); } @@ -3839,6 +3917,25 @@ } } + function onDragOver(cm, e) { + var pos = posFromMouse(cm, e); + if (!pos) return; + var frag = document.createDocumentFragment(); + drawSelectionCursor(cm, pos, frag); + if (!cm.display.dragCursor) { + cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors"); + cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv); + } + removeChildrenAndAdd(cm.display.dragCursor, frag); + } + + function clearDragCursor(cm) { + if (cm.display.dragCursor) { + cm.display.lineSpace.removeChild(cm.display.dragCursor); + cm.display.dragCursor = null; + } + } + // SCROLL EVENTS // Sync the scrollable area and scrollbars, ensure the viewport @@ -3903,8 +4000,9 @@ var display = cm.display, scroll = display.scroller; // Quit if there's nothing to scroll here - if (!(dx && scroll.scrollWidth > scroll.clientWidth || - dy && scroll.scrollHeight > scroll.clientHeight)) return; + var canScrollX = scroll.scrollWidth > scroll.clientWidth; + var canScrollY = scroll.scrollHeight > scroll.clientHeight; + if (!(dx && canScrollX || dy && canScrollY)) return; // Webkit browsers on OS X abort momentum scrolls when the target // of the scroll event is removed from the scrollable element. @@ -3928,10 +4026,15 @@ // scrolling entirely here. It'll be slightly off from native, but // better than glitching out. if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { - if (dy) + if (dy && canScrollY) setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); - e_preventDefault(e); + // Only prevent default scrolling if vertical scrolling is + // actually possible. Otherwise, it causes vertical scroll + // jitter on OSX trackpads when deltaX is small and deltaY + // is large (issue #3579) + if (!dy || (dy && canScrollY)) + e_preventDefault(e); display.wheelStartX = null; // Abort measurement, if in progress return; } @@ -3980,7 +4083,7 @@ cm.display.input.ensurePolled(); var prevShift = cm.display.shift, done = false; try { - if (isReadOnly(cm)) cm.state.suppressEdits = true; + if (cm.isReadOnly()) cm.state.suppressEdits = true; if (dropShift) cm.display.shift = false; done = bound(cm) != Pass; } finally { @@ -4159,12 +4262,13 @@ // right-click take effect on it. function onContextMenu(cm, e) { if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return; + if (signalDOMEvent(cm, e, "contextmenu")) return; cm.display.input.onContextMenu(e); } function contextMenuInGutter(cm, e) { if (!hasHandler(cm, "gutterContextMenu")) return false; - return gutterEvent(cm, e, "gutterContextMenu", false, signal); + return gutterEvent(cm, e, "gutterContextMenu", false); } // UPDATING @@ -4468,7 +4572,7 @@ function replaceRange(doc, code, from, to, origin) { if (!to) to = from; if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; } - if (typeof code == "string") code = splitLines(code); + if (typeof code == "string") code = doc.splitLines(code); makeChange(doc, {from: from, to: to, text: code, origin: origin}); } @@ -4712,10 +4816,9 @@ function findPosH(doc, pos, dir, unit, visually) { var line = pos.line, ch = pos.ch, origDir = dir; var lineObj = getLine(doc, line); - var possible = true; function findNextLine() { var l = line + dir; - if (l < doc.first || l >= doc.first + doc.size) return (possible = false); + if (l < doc.first || l >= doc.first + doc.size) return false line = l; return lineObj = getLine(doc, l); } @@ -4725,14 +4828,16 @@ if (!boundToLine && findNextLine()) { if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj); else ch = dir < 0 ? lineObj.text.length : 0; - } else return (possible = false); + } else return false } else ch = next; return true; } - if (unit == "char") moveOnce(); - else if (unit == "column") moveOnce(true); - else if (unit == "word" || unit == "group") { + if (unit == "char") { + moveOnce() + } else if (unit == "column") { + moveOnce(true) + } else if (unit == "word" || unit == "group") { var sawType = null, group = unit == "group"; var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); for (var first = true;; first = false) { @@ -4752,8 +4857,8 @@ if (dir > 0 && !moveOnce(!first)) break; } } - var result = skipAtomic(doc, Pos(line, ch), origDir, true); - if (!possible) result.hitSide = true; + var result = skipAtomic(doc, Pos(line, ch), pos, origDir, true); + if (!cmp(pos, result)) result.hitSide = true; return result; } @@ -5045,7 +5150,7 @@ execCommand: function(cmd) { if (commands.hasOwnProperty(cmd)) - return commands[cmd](this); + return commands[cmd].call(null, this); }, triggerElectric: methodOp(function(text) { triggerElectric(this, text); }), @@ -5140,6 +5245,7 @@ signal(this, "overwriteToggle", this, this.state.overwrite); }, hasFocus: function() { return this.display.input.getField() == activeElt(); }, + isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit); }, scrollTo: methodOp(function(x, y) { if (x != null || y != null) resolveScrollToPos(this); @@ -5263,6 +5369,22 @@ clearCaches(cm); regChange(cm); }, true); + option("lineSeparator", null, function(cm, val) { + cm.doc.lineSep = val; + if (!val) return; + var newBreaks = [], lineNo = cm.doc.first; + cm.doc.iter(function(line) { + for (var pos = 0;;) { + var found = line.text.indexOf(val, pos); + if (found == -1) break; + pos = found + val.length; + newBreaks.push(Pos(lineNo, found)); + } + lineNo++; + }); + for (var i = newBreaks.length - 1; i >= 0; i--) + replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)) + }); option("specialChars", /[\t\u0000-\u0019\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val, old) { cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); if (old != CodeMirror.Init) cm.refresh(); @@ -5321,11 +5443,12 @@ cm.display.disabled = true; } else { cm.display.disabled = false; - if (!val) cm.display.input.reset(); - } + } + cm.display.input.readOnlyChanged(val) }); option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true); option("dragDrop", true, dragDropChanged); + option("allowDropFileTypes", null); option("cursorBlinkRate", 530); option("cursorScrollMargin", 0); @@ -5613,7 +5736,8 @@ } else if (cur.line > cm.doc.first) { var prev = getLine(cm.doc, cur.line - 1).text; if (prev) - cm.replaceRange(line.charAt(0) + "\n" + prev.charAt(prev.length - 1), + cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() + + prev.charAt(prev.length - 1), Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose"); } } @@ -5627,10 +5751,10 @@ var len = cm.listSelections().length; for (var i = 0; i < len; i++) { var range = cm.listSelections()[i]; - cm.replaceRange("\n", range.anchor, range.head, "+input"); + cm.replaceRange(cm.doc.lineSeparator(), range.anchor, range.head, "+input"); cm.indentLine(range.from().line + 1, null, true); - ensureCursorVisible(cm); } + ensureCursorVisible(cm); }); }, toggleOverwrite: function(cm) {cm.toggleOverwrite();} @@ -6558,7 +6682,7 @@ parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); } - return widget.height = widget.node.offsetHeight; + return widget.height = widget.node.parentNode.offsetHeight; } function addLineWidget(doc, handle, node, options) { @@ -6747,7 +6871,9 @@ function getLineStyles(cm, line, updateFrontier) { if (!line.styles || line.styles[0] != cm.state.modeGen) { - var result = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line))); + var state = getStateBefore(cm, lineNo(line)); + var result = highlightLine(cm, line, line.text.length > cm.options.maxHighlightLength ? copyState(cm.doc.mode, state) : state); + line.stateAfter = state; line.styles = result.styles; if (result.classes) line.styleClasses = result.classes; else if (line.styleClasses) line.styleClasses = null; @@ -6764,7 +6890,7 @@ var stream = new StringStream(text, cm.options.tabSize); stream.start = stream.pos = startAt || 0; if (text == "") callBlankLine(mode, state); - while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { + while (!stream.eol()) { readToken(mode, stream, state); stream.start = stream.pos; } @@ -6791,7 +6917,7 @@ // is needed on Webkit to be able to get line-level bounding // rectangles for it (in measureChar). var content = elt("span", null, null, webkit ? "padding-right: .1px" : null); - var builder = {pre: elt("pre", [content]), content: content, + var builder = {pre: elt("pre", [content], "CodeMirror-line"), content: content, col: 0, pos: 0, cm: cm, splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")}; lineView.measure = {}; @@ -6881,6 +7007,10 @@ txt.setAttribute("role", "presentation"); txt.setAttribute("cm-text", "\t"); builder.col += tabWidth; + } else if (m[0] == "\r" || m[0] == "\n") { + var txt = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar")); + txt.setAttribute("cm-text", m[0]); + builder.col += 1; } else { var txt = builder.cm.options.specialCharPlaceholder(m[0]); txt.setAttribute("cm-text", m[0]); @@ -6962,7 +7092,7 @@ if (nextChange == pos) { // Update current marker set spanStyle = spanEndStyle = spanStartStyle = title = css = ""; collapsed = null; nextChange = Infinity; - var foundBookmarks = []; + var foundBookmarks = [], endStyles for (var j = 0; j < spans.length; ++j) { var sp = spans[j], m = sp.marker; if (m.type == "bookmark" && sp.from == pos && m.widgetNode) { @@ -6973,9 +7103,9 @@ spanEndStyle = ""; } if (m.className) spanStyle += " " + m.className; - if (m.css) css = m.css; + if (m.css) css = (css ? css + ";" : "") + m.css; if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle; - if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle; + if (m.endStyle && sp.to == nextChange) (endStyles || (endStyles = [])).push(m.endStyle, sp.to) if (m.title && !title) title = m.title; if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) collapsed = sp; @@ -6983,14 +7113,17 @@ nextChange = sp.from; } } + if (endStyles) for (var j = 0; j < endStyles.length; j += 2) + if (endStyles[j + 1] == nextChange) spanEndStyle += " " + endStyles[j] + + if (!collapsed || collapsed.from == pos) for (var j = 0; j < foundBookmarks.length; ++j) + buildCollapsedSpan(builder, 0, foundBookmarks[j]); if (collapsed && (collapsed.from || 0) == pos) { buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, collapsed.marker, collapsed.from == null); if (collapsed.to == null) return; if (collapsed.to == pos) collapsed = false; } - if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j) - buildCollapsedSpan(builder, 0, foundBookmarks[j]); } if (pos >= len) break; @@ -7226,8 +7359,8 @@ }; var nextDocId = 0; - var Doc = CodeMirror.Doc = function(text, mode, firstLine) { - if (!(this instanceof Doc)) return new Doc(text, mode, firstLine); + var Doc = CodeMirror.Doc = function(text, mode, firstLine, lineSep) { + if (!(this instanceof Doc)) return new Doc(text, mode, firstLine, lineSep); if (firstLine == null) firstLine = 0; BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); @@ -7241,8 +7374,10 @@ this.history = new History(null); this.id = ++nextDocId; this.modeOption = mode; - - if (typeof text == "string") text = splitLines(text); + this.lineSep = lineSep; + this.extend = false; + + if (typeof text == "string") text = this.splitLines(text); updateDoc(this, {from: start, to: start, text: text}); setSelection(this, simpleSelection(start), sel_dontScroll); }; @@ -7272,12 +7407,12 @@ getValue: function(lineSep) { var lines = getLines(this, this.first, this.first + this.size); if (lineSep === false) return lines; - return lines.join(lineSep || "\n"); + return lines.join(lineSep || this.lineSeparator()); }, setValue: docMethodOp(function(code) { var top = Pos(this.first, 0), last = this.first + this.size - 1; makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), - text: splitLines(code), origin: "setValue", full: true}, true); + text: this.splitLines(code), origin: "setValue", full: true}, true); setSelection(this, simpleSelection(top)); }), replaceRange: function(code, from, to, origin) { @@ -7288,7 +7423,7 @@ getRange: function(from, to, lineSep) { var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); if (lineSep === false) return lines; - return lines.join(lineSep || "\n"); + return lines.join(lineSep || this.lineSeparator()); }, getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, @@ -7328,10 +7463,11 @@ extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); }), extendSelections: docMethodOp(function(heads, options) { - extendSelections(this, clipPosArray(this, heads, options)); + extendSelections(this, clipPosArray(this, heads), options); }), extendSelectionsBy: docMethodOp(function(f, options) { - extendSelections(this, map(this.sel.ranges, f), options); + var heads = map(this.sel.ranges, f); + extendSelections(this, clipPosArray(this, heads), options); }), setSelections: docMethodOp(function(ranges, primary, options) { if (!ranges.length) return; @@ -7354,13 +7490,13 @@ lines = lines ? lines.concat(sel) : sel; } if (lineSep === false) return lines; - else return lines.join(lineSep || "\n"); + else return lines.join(lineSep || this.lineSeparator()); }, getSelections: function(lineSep) { var parts = [], ranges = this.sel.ranges; for (var i = 0; i < ranges.length; i++) { var sel = getBetween(this, ranges[i].from(), ranges[i].to()); - if (lineSep !== false) sel = sel.join(lineSep || "\n"); + if (lineSep !== false) sel = sel.join(lineSep || this.lineSeparator()); parts[i] = sel; } return parts; @@ -7375,7 +7511,7 @@ var changes = [], sel = this.sel; for (var i = 0; i < sel.ranges.length; i++) { var range = sel.ranges[i]; - changes[i] = {from: range.from(), to: range.to(), text: splitLines(code[i]), origin: origin}; + changes[i] = {from: range.from(), to: range.to(), text: this.splitLines(code[i]), origin: origin}; } var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); for (var i = changes.length - 1; i >= 0; i--) @@ -7456,7 +7592,7 @@ removeLineWidget: function(widget) { widget.clear(); }, markText: function(from, to, options) { - return markText(this, clipPos(this, from), clipPos(this, to), options, "range"); + return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range"); }, setBookmark: function(pos, options) { var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), @@ -7525,7 +7661,8 @@ }, copy: function(copyHistory) { - var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first); + var doc = new Doc(getLines(this, this.first, this.first + this.size), + this.modeOption, this.first, this.lineSep); doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; doc.sel = this.sel; doc.extend = false; @@ -7541,7 +7678,7 @@ var from = this.first, to = this.first + this.size; if (options.from != null && options.from > from) from = options.from; if (options.to != null && options.to < to) to = options.to; - var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from); + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep); if (options.sharedHist) copy.history = this.history; (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; @@ -7570,7 +7707,13 @@ iterLinkedDocs: function(f) {linkedDocs(this, f);}, getMode: function() {return this.mode;}, - getEditor: function() {return this.cm;} + getEditor: function() {return this.cm;}, + + splitLines: function(str) { + if (this.lineSep) return str.split(this.lineSep); + return splitLinesAuto(str); + }, + lineSeparator: function() { return this.lineSep || "\n"; } }); // Public alias. @@ -8010,24 +8153,30 @@ } }; + var noHandlers = [] + function getHandlers(emitter, type, copy) { + var arr = emitter._handlers && emitter._handlers[type] + if (copy) return arr && arr.length > 0 ? arr.slice() : noHandlers + else return arr || noHandlers + } + var off = CodeMirror.off = function(emitter, type, f) { if (emitter.removeEventListener) emitter.removeEventListener(type, f, false); else if (emitter.detachEvent) emitter.detachEvent("on" + type, f); else { - var arr = emitter._handlers && emitter._handlers[type]; - if (!arr) return; - for (var i = 0; i < arr.length; ++i) - if (arr[i] == f) { arr.splice(i, 1); break; } + var handlers = getHandlers(emitter, type, false) + for (var i = 0; i < handlers.length; ++i) + if (handlers[i] == f) { handlers.splice(i, 1); break; } } }; var signal = CodeMirror.signal = function(emitter, type /*, values...*/) { - var arr = emitter._handlers && emitter._handlers[type]; - if (!arr) return; + var handlers = getHandlers(emitter, type, true) + if (!handlers.length) return; var args = Array.prototype.slice.call(arguments, 2); - for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args); + for (var i = 0; i < handlers.length; ++i) handlers[i].apply(null, args); }; var orphanDelayedCallbacks = null; @@ -8040,8 +8189,8 @@ // them to be executed when the last operation ends, or, if no // operation is active, when a timeout fires. function signalLater(emitter, type /*, values...*/) { - var arr = emitter._handlers && emitter._handlers[type]; - if (!arr) return; + var arr = getHandlers(emitter, type, false) + if (!arr.length) return; var args = Array.prototype.slice.call(arguments, 2), list; if (operationGroup) { list = operationGroup.delayedCallbacks; @@ -8081,8 +8230,7 @@ } function hasHandler(emitter, type) { - var arr = emitter._handlers && emitter._handlers[type]; - return arr && arr.length > 0; + return getHandlers(emitter, type).length > 0 } // Add on and off methods to a constructor's prototype, to make @@ -8129,7 +8277,7 @@ // The inverse of countColumn -- find the offset that corresponds to // a particular column. - function findColumn(string, goal, tabSize) { + var findColumn = CodeMirror.findColumn = function(string, goal, tabSize) { for (var pos = 0, col = 0;;) { var nextTab = string.indexOf("\t", pos); if (nextTab == -1) nextTab = string.length; @@ -8269,7 +8417,12 @@ } while (child = child.parentNode); }; - function activeElt() { return document.activeElement; } + function activeElt() { + var activeElement = document.activeElement; + while (activeElement && activeElement.root && activeElement.root.activeElement) + activeElement = activeElement.root.activeElement; + return activeElement; + } // Older versions of IE throws unspecified error when touching // document.activeElement in some cases (during loading, in iframe) if (ie && ie_version < 11) activeElt = function() { @@ -8371,7 +8524,7 @@ // See if "".split is the broken IE version, if so, provide an // alternative way to split lines. - var splitLines = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var splitLinesAuto = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { var pos = 0, result = [], l = string.length; while (pos <= l) { var nl = string.indexOf("\n", pos); @@ -8417,14 +8570,16 @@ // KEY NAMES - var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", - 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", - 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", - 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete", - 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", - 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", - 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"}; - CodeMirror.keyNames = keyNames; + var keyNames = CodeMirror.keyNames = { + 3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", + 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 127: "Delete", + 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", + 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert" + }; (function() { // Number keys for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i); @@ -8729,7 +8884,7 @@ // THE END - CodeMirror.version = "5.4.0"; + CodeMirror.version = "5.11.0"; return CodeMirror; }); diff --git a/rhodecode/public/js/src/codemirror/codemirror_hint.js b/rhodecode/public/js/src/codemirror/codemirror_hint.js --- a/rhodecode/public/js/src/codemirror/codemirror_hint.js +++ b/rhodecode/public/js/src/codemirror/codemirror_hint.js @@ -25,8 +25,18 @@ }; CodeMirror.defineExtension("showHint", function(options) { - // We want a single cursor position. - if (this.listSelections().length > 1 || this.somethingSelected()) return; + options = parseOptions(this, this.getCursor("start"), options); + var selections = this.listSelections() + if (selections.length > 1) return; + // By default, don't allow completion when something is selected. + // A hint function can have a `supportsSelection` property to + // indicate that it can handle selections. + if (this.somethingSelected()) { + if (!options.hint.supportsSelection) return; + // Don't try with cross-line selections + for (var i = 0; i < selections.length; i++) + if (selections[i].head.line != selections[i].anchor.line) return; + } if (this.state.completionActive) this.state.completionActive.close(); var completion = this.state.completionActive = new Completion(this, options); @@ -38,12 +48,12 @@ function Completion(cm, options) { this.cm = cm; - this.options = this.buildOptions(options); + this.options = options; this.widget = null; this.debounce = 0; this.tick = 0; - this.startPos = this.cm.getCursor(); - this.startLen = this.cm.getLine(this.startPos.line).length; + this.startPos = this.cm.getCursor("start"); + this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length; var self = this; cm.on("cursorActivity", this.activityFunc = function() { self.cursorActivity(); }); @@ -99,7 +109,6 @@ update: function(first) { if (this.tick == null) return; - if (this.data) CodeMirror.signal(this.data, "update"); if (!this.options.hint.async) { this.finishUpdate(this.options.hint(this.cm, this.options), first); } else { @@ -111,6 +120,8 @@ }, finishUpdate: function(data, first) { + if (this.data) CodeMirror.signal(this.data, "update"); + if (data && this.data && CodeMirror.cmpPos(data.from, this.data.from)) data = null; this.data = data; var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle); @@ -123,20 +134,21 @@ CodeMirror.signal(data, "shown"); } } - }, - - buildOptions: function(options) { - var editor = this.cm.options.hintOptions; - var out = {}; - for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; - if (editor) for (var prop in editor) - if (editor[prop] !== undefined) out[prop] = editor[prop]; - if (options) for (var prop in options) - if (options[prop] !== undefined) out[prop] = options[prop]; - return out; } }; + function parseOptions(cm, pos, options) { + var editor = cm.options.hintOptions; + var out = {}; + for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; + if (editor) for (var prop in editor) + if (editor[prop] !== undefined) out[prop] = editor[prop]; + if (options) for (var prop in options) + if (options[prop] !== undefined) out[prop] = options[prop]; + if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos) + return out; + } + function getText(completion) { if (typeof completion == "string") return completion; else return completion.text; @@ -335,34 +347,79 @@ } }; - CodeMirror.registerHelper("hint", "auto", function(cm, options) { - var helpers = cm.getHelpers(cm.getCursor(), "hint"), words; + function applicableHelpers(cm, helpers) { + if (!cm.somethingSelected()) return helpers + var result = [] + for (var i = 0; i < helpers.length; i++) + if (helpers[i].supportsSelection) result.push(helpers[i]) + return result + } + + function resolveAutoHints(cm, pos) { + var helpers = cm.getHelpers(pos, "hint"), words if (helpers.length) { - for (var i = 0; i < helpers.length; i++) { - var cur = helpers[i](cm, options); - if (cur && cur.list.length) return cur; + var async = false, resolved + for (var i = 0; i < helpers.length; i++) if (helpers[i].async) async = true + if (async) { + resolved = function(cm, callback, options) { + var app = applicableHelpers(cm, helpers) + function run(i, result) { + if (i == app.length) return callback(null) + var helper = app[i] + if (helper.async) { + helper(cm, function(result) { + if (result) callback(result) + else run(i + 1) + }, options) + } else { + var result = helper(cm, options) + if (result) callback(result) + else run(i + 1) + } + } + run(0) + } + resolved.async = true + } else { + resolved = function(cm, options) { + var app = applicableHelpers(cm, helpers) + for (var i = 0; i < app.length; i++) { + var cur = app[i](cm, options) + if (cur && cur.list.length) return cur + } + } } + resolved.supportsSelection = true + return resolved } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) { - if (words) return CodeMirror.hint.fromList(cm, {words: words}); + return function(cm) { return CodeMirror.hint.fromList(cm, {words: words}) } } else if (CodeMirror.hint.anyword) { - return CodeMirror.hint.anyword(cm, options); + return function(cm, options) { return CodeMirror.hint.anyword(cm, options) } + } else { + return function() {} } + } + + CodeMirror.registerHelper("hint", "auto", { + resolve: resolveAutoHints }); CodeMirror.registerHelper("hint", "fromList", function(cm, options) { var cur = cm.getCursor(), token = cm.getTokenAt(cur); + var to = CodeMirror.Pos(cur.line, token.end); + if (token.string && /\w/.test(token.string[token.string.length - 1])) { + var term = token.string, from = CodeMirror.Pos(cur.line, token.start); + } else { + var term = "", from = to; + } var found = []; for (var i = 0; i < options.words.length; i++) { var word = options.words[i]; - if (word.slice(0, token.string.length) == token.string) + if (word.slice(0, term.length) == term) found.push(word); } - if (found.length) return { - list: found, - from: CodeMirror.Pos(cur.line, token.start), - to: CodeMirror.Pos(cur.line, token.end) - }; + if (found.length) return {list: found, from: from, to: to}; }); CodeMirror.commands.autocomplete = CodeMirror.showHint; @@ -373,7 +430,7 @@ alignWithWord: true, closeCharacters: /[\s()\[\]{};:>,]/, closeOnUnfocus: true, - completeOnSingleClick: false, + completeOnSingleClick: true, container: null, customKeys: null, extraKeys: null diff --git a/rhodecode/public/js/src/codemirror/codemirror_placeholder.js b/rhodecode/public/js/src/codemirror/codemirror_placeholder.js --- a/rhodecode/public/js/src/codemirror/codemirror_placeholder.js +++ b/rhodecode/public/js/src/codemirror/codemirror_placeholder.js @@ -37,7 +37,9 @@ var elt = cm.state.placeholder = document.createElement("pre"); elt.style.cssText = "height: 0; overflow: visible"; elt.className = "CodeMirror-placeholder"; - elt.appendChild(document.createTextNode(cm.getOption("placeholder"))); + var placeHolder = cm.getOption("placeholder") + if (typeof placeHolder == "string") placeHolder = document.createTextNode(placeHolder) + elt.appendChild(placeHolder) cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild); } diff --git a/rhodecode/public/js/src/deform.js b/rhodecode/public/js/src/deform.js new file mode 100644 --- /dev/null +++ b/rhodecode/public/js/src/deform.js @@ -0,0 +1,194 @@ +/* + * Register a top-level callback to the deform.load() function + * this will be called when the DOM has finished loading. No need + * to include the call at the end of the page. + */ + +$(document).ready(function(){ + deform.load(); +}); + + +var deform_loaded = false; + +var deform = { + callbacks: [], + + addCallback: function (oid, callback) { + deform.callbacks.push([oid, callback]); + }, + + clearCallbacks: function () { + deform.callbacks = []; + }, + + load: function() { + $(function() { + if (!deform_loaded) { + deform.processCallbacks(); + deform.focusFirstInput(); + deform_loaded = true; + }}); + }, + + + processCallbacks: function () { + $(deform.callbacks).each(function(num, item) { + var oid = item[0]; + var callback = item[1]; + callback(oid); + } + ); + deform.clearCallbacks(); + }, + + addSequenceItem: function (protonode, before) { + // - Clone the prototype node and add it before the "before" node. + // Also ensure any callbacks are run for the widget. + + // In order to avoid breaking accessibility: + // + // - Find each tag within the prototype node with an id + // that has the string ``deformField(\d+)`` within it, and modify + // its id to have a random component. + // - For each label referencing an change id, change the label's + // for attribute to the new id. + + var fieldmatch = /deformField(\d+)/; + var namematch = /(.+)?-[#]{3}/; + var code = protonode.attr('prototype'); + var html = decodeURIComponent(code); + var $htmlnode = $(html); + var $idnodes = $htmlnode.find('[id]'); + var $namednodes = $htmlnode.find('[name]'); + var genid = deform.randomString(6); + var idmap = {}; + + // replace ids containing ``deformField`` and associated label for= + // items which point at them + + $idnodes.each(function(idx, node) { + var $node = $(node); + var oldid = $node.attr('id'); + var newid = oldid.replace(fieldmatch, "deformField$1-" + genid); + $node.attr('id', newid); + idmap[oldid] = newid; + var labelselector = 'label[for=' + oldid + ']'; + var $fornodes = $htmlnode.find(labelselector); + $fornodes.attr('for', newid); + }); + + // replace names a containing ```deformField`` like we do for ids + + $namednodes.each(function(idx, node) { + var $node = $(node); + var oldname = $node.attr('name'); + var newname = oldname.replace(fieldmatch, "deformField$1-" + genid); + $node.attr('name', newname); + }); + + $htmlnode.insertBefore(before); + + $(deform.callbacks).each(function(num, item) { + var oid = item[0]; + var callback = item[1]; + var newid = idmap[oid]; + if (newid) { + callback(newid); + } + }); + + deform.clearCallbacks(); + var old_len = parseInt(before.attr('now_len')||'0', 10); + before.attr('now_len', old_len + 1); + // we added something to the dom, trigger a change event + var e = jQuery.Event("change"); + $('#deform').trigger(e); + }, + + appendSequenceItem: function(node) { + var $oid_node = $(node).closest('.deform-seq'); + var $proto_node = $oid_node.find('.deform-proto').first(); + var $before_node = $oid_node.find('.deform-insert-before').last(); + var min_len = parseInt($before_node.attr('min_len')||'0', 10); + var max_len = parseInt($before_node.attr('max_len')||'9999', 10); + var now_len = parseInt($before_node.attr('now_len')||'0', 10); + var orderable = parseInt($before_node.attr('orderable')||'0', 10); + + if (now_len < max_len) { + deform.addSequenceItem($proto_node, $before_node); + deform.processSequenceButtons($oid_node, min_len, max_len, + now_len + 1, orderable); + } + return false; + }, + + removeSequenceItem: function(clicked) { + var $item_node = $(clicked).closest('.deform-seq-item'); + var $oid_node = $item_node.closest('.deform-seq'); + var $before_node = $oid_node.find('.deform-insert-before').last(); + var min_len = parseInt($before_node.attr('min_len')||'0', 10); + var max_len = parseInt($before_node.attr('max_len')||'9999', 10); + var now_len = parseInt($before_node.attr('now_len')||'0', 10); + var orderable = parseInt($before_node.attr('orderable')||'0', 10); + if (now_len > min_len) { + $before_node.attr('now_len', now_len - 1); + $item_node.remove(); + deform.processSequenceButtons($oid_node, min_len, max_len, + now_len-1, orderable); + } + // we removed something from the dom, trigger a change event + var e = jQuery.Event("change"); + $('#deform').trigger(e); + return false; + }, + + processSequenceButtons: function(oid_node, min_len, max_len, now_len, + orderable) { + orderable = !!orderable; // convert to bool + var has_multiple = now_len > 1; + var $ul = oid_node.find('.deform-seq-container').not(oid_node.find('.deform-seq-container .deform-seq-container')); + var $lis = $ul.find('.deform-seq-item').not($ul.find('.deform-seq-container .deform-seq-item')); + var show_closebutton = now_len > min_len; + var show_addbutton = now_len < max_len; + $lis.find('.deform-close-button').not($lis.find('.deform-seq-container .deform-close-button')).toggle(show_closebutton); + oid_node.find('.deform-seq-add').not(oid_node.find('.deform-seq-container .deform-seq-add')).toggle(show_addbutton); + $lis.find('.deform-order-button').not($lis.find('.deform-seq-container .deform-order-button')).toggle(orderable && has_multiple); + }, + + focusFirstInput: function (el) { + el = el || document.body; + var input = $(el).find(':input') + .filter('[id ^= deformField]') + .filter('[type != hidden]') + .first(); + if (input) { + var raw = input.get(0); + if (raw) { + if (raw.type === 'text' || raw.type === 'file' || + raw.type == 'password' || raw.type == 'text' || + raw.type == 'textarea') { + if (!input.hasClass("hasDatepicker")) { + input.focus(); + } + } + } + } + }, + + randomString: function (length) { + var chr='0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'; + chr = chr.split(''); + + if (! length) { + length = Math.floor(Math.random() * chr.length); + } + + var str = ''; + for (var i = 0; i < length; i++) { + str += chr[Math.floor(Math.random() * chr.length)]; + } + return str; + } + +}; diff --git a/rhodecode/public/js/src/plugins/toastr.js b/rhodecode/public/js/src/plugins/toastr.js new file mode 100644 --- /dev/null +++ b/rhodecode/public/js/src/plugins/toastr.js @@ -0,0 +1,435 @@ +/* + * Toastr + * Copyright 2012-2015 + * Authors: John Papa, Hans Fjällemark, and Tim Ferrell. + * All Rights Reserved. + * Use, reproduction, distribution, and modification of this code is subject to the terms and + * conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php + * + * ARIA Support: Greta Krafsig + * + * Project: https://github.com/CodeSeven/toastr + */ +/* global define */ +(function (define) { + define(['jquery'], function ($) { + return (function () { + var $container; + var listener; + var toastId = 0; + var toastType = { + error: 'error', + info: 'info', + success: 'success', + warning: 'warning' + }; + + var toastr = { + clear: clear, + remove: remove, + error: error, + getContainer: getContainer, + info: info, + options: {}, + subscribe: subscribe, + success: success, + version: '2.1.2', + warning: warning + }; + + var previousToast; + + return toastr; + + //////////////// + + function error(message, title, optionsOverride) { + return notify({ + type: toastType.error, + iconClass: getOptions().iconClasses.error, + message: message, + optionsOverride: optionsOverride, + title: title + }); + } + + function getContainer(options, create) { + if (!options) { options = getOptions(); } + $container = $('#' + options.containerId); + if ($container.length) { + return $container; + } + if (create) { + $container = createContainer(options); + } + return $container; + } + + function info(message, title, optionsOverride) { + return notify({ + type: toastType.info, + iconClass: getOptions().iconClasses.info, + message: message, + optionsOverride: optionsOverride, + title: title + }); + } + + function subscribe(callback) { + listener = callback; + } + + function success(message, title, optionsOverride) { + return notify({ + type: toastType.success, + iconClass: getOptions().iconClasses.success, + message: message, + optionsOverride: optionsOverride, + title: title + }); + } + + function warning(message, title, optionsOverride) { + return notify({ + type: toastType.warning, + iconClass: getOptions().iconClasses.warning, + message: message, + optionsOverride: optionsOverride, + title: title + }); + } + + function clear($toastElement, clearOptions) { + var options = getOptions(); + if (!$container) { getContainer(options); } + if (!clearToast($toastElement, options, clearOptions)) { + clearContainer(options); + } + } + + function remove($toastElement) { + var options = getOptions(); + if (!$container) { getContainer(options); } + if ($toastElement && $(':focus', $toastElement).length === 0) { + removeToast($toastElement); + return; + } + if ($container.children().length) { + $container.remove(); + } + } + + // internal functions + + function clearContainer (options) { + var toastsToClear = $container.children(); + for (var i = toastsToClear.length - 1; i >= 0; i--) { + clearToast($(toastsToClear[i]), options); + } + } + + function clearToast ($toastElement, options, clearOptions) { + var force = clearOptions && clearOptions.force ? clearOptions.force : false; + if ($toastElement && (force || $(':focus', $toastElement).length === 0)) { + $toastElement[options.hideMethod]({ + duration: options.hideDuration, + easing: options.hideEasing, + complete: function () { removeToast($toastElement); } + }); + return true; + } + return false; + } + + function createContainer(options) { + $container = $('
') + .attr('id', options.containerId) + .addClass(options.positionClass) + .attr('aria-live', 'polite') + .attr('role', 'alert'); + + $container.appendTo($(options.target)); + return $container; + } + + function getDefaults() { + return { + tapToDismiss: true, + toastClass: 'toast', + containerId: 'toast-container', + debug: false, + + showMethod: 'fadeIn', //fadeIn, slideDown, and show are built into jQuery + showDuration: 300, + showEasing: 'swing', //swing and linear are built into jQuery + onShown: undefined, + hideMethod: 'fadeOut', + hideDuration: 1000, + hideEasing: 'swing', + onHidden: undefined, + closeMethod: false, + closeDuration: false, + closeEasing: false, + + extendedTimeOut: 1000, + iconClasses: { + error: 'toast-error', + info: 'toast-info', + success: 'toast-success', + warning: 'toast-warning' + }, + iconClass: 'toast-info', + positionClass: 'toast-top-right', + timeOut: 5000, // Set timeOut and extendedTimeOut to 0 to make it sticky + titleClass: 'toast-title', + messageClass: 'toast-message', + escapeHtml: false, + target: 'body', + closeHtml: '', + newestOnTop: true, + preventDuplicates: false, + progressBar: false + }; + } + + function publish(args) { + if (!listener) { return; } + listener(args); + } + + function notify(map) { + var options = getOptions(); + var iconClass = map.iconClass || options.iconClass; + + if (typeof (map.optionsOverride) !== 'undefined') { + options = $.extend(options, map.optionsOverride); + iconClass = map.optionsOverride.iconClass || iconClass; + } + + if (shouldExit(options, map)) { return; } + + toastId++; + + $container = getContainer(options, true); + + var intervalId = null; + var $toastElement = $('
'); + var $titleElement = $('
'); + var $messageElement = $('
'); + var $progressElement = $('
'); + var $closeElement = $(options.closeHtml); + var progressBar = { + intervalId: null, + hideEta: null, + maxHideTime: null + }; + var response = { + toastId: toastId, + state: 'visible', + startTime: new Date(), + options: options, + map: map + }; + + personalizeToast(); + + displayToast(); + + handleEvents(); + + publish(response); + + if (options.debug && console) { + console.log(response); + } + + return $toastElement; + + function escapeHtml(source) { + if (source == null) + source = ""; + + return new String(source) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + } + + function personalizeToast() { + setIcon(); + setTitle(); + setMessage(); + setCloseButton(); + setProgressBar(); + setSequence(); + } + + function handleEvents() { + $toastElement.hover(stickAround, delayedHideToast); + if (!options.onclick && options.tapToDismiss) { + $toastElement.click(hideToast); + } + + if (options.closeButton && $closeElement) { + $closeElement.click(function (event) { + if (event.stopPropagation) { + event.stopPropagation(); + } else if (event.cancelBubble !== undefined && event.cancelBubble !== true) { + event.cancelBubble = true; + } + hideToast(true); + }); + } + + if (options.onclick) { + $toastElement.click(function (event) { + options.onclick(event); + hideToast(); + }); + } + } + + function displayToast() { + $toastElement.hide(); + + $toastElement[options.showMethod]( + {duration: options.showDuration, easing: options.showEasing, complete: options.onShown} + ); + + if (options.timeOut > 0) { + intervalId = setTimeout(hideToast, options.timeOut); + progressBar.maxHideTime = parseFloat(options.timeOut); + progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime; + if (options.progressBar) { + progressBar.intervalId = setInterval(updateProgress, 10); + } + } + } + + function setIcon() { + if (map.iconClass) { + $toastElement.addClass(options.toastClass).addClass(iconClass); + } + } + + function setSequence() { + if (options.newestOnTop) { + $container.prepend($toastElement); + } else { + $container.append($toastElement); + } + } + + function setTitle() { + if (map.title) { + $titleElement.append(!options.escapeHtml ? map.title : escapeHtml(map.title)).addClass(options.titleClass); + $toastElement.append($titleElement); + } + } + + function setMessage() { + if (map.message) { + $messageElement.append(!options.escapeHtml ? map.message : escapeHtml(map.message)).addClass(options.messageClass); + $toastElement.append($messageElement); + } + } + + function setCloseButton() { + if (options.closeButton) { + $closeElement.addClass('toast-close-button').attr('role', 'button'); + $toastElement.prepend($closeElement); + } + } + + function setProgressBar() { + if (options.progressBar) { + $progressElement.addClass('toast-progress'); + $toastElement.prepend($progressElement); + } + } + + function shouldExit(options, map) { + if (options.preventDuplicates) { + if (map.message === previousToast) { + return true; + } else { + previousToast = map.message; + } + } + return false; + } + + function hideToast(override) { + var method = override && options.closeMethod !== false ? options.closeMethod : options.hideMethod; + var duration = override && options.closeDuration !== false ? + options.closeDuration : options.hideDuration; + var easing = override && options.closeEasing !== false ? options.closeEasing : options.hideEasing; + if ($(':focus', $toastElement).length && !override) { + return; + } + clearTimeout(progressBar.intervalId); + return $toastElement[method]({ + duration: duration, + easing: easing, + complete: function () { + removeToast($toastElement); + if (options.onHidden && response.state !== 'hidden') { + options.onHidden(); + } + response.state = 'hidden'; + response.endTime = new Date(); + publish(response); + } + }); + } + + function delayedHideToast() { + if (options.timeOut > 0 || options.extendedTimeOut > 0) { + intervalId = setTimeout(hideToast, options.extendedTimeOut); + progressBar.maxHideTime = parseFloat(options.extendedTimeOut); + progressBar.hideEta = new Date().getTime() + progressBar.maxHideTime; + } + } + + function stickAround() { + clearTimeout(intervalId); + progressBar.hideEta = 0; + $toastElement.stop(true, true)[options.showMethod]( + {duration: options.showDuration, easing: options.showEasing} + ); + } + + function updateProgress() { + var percentage = ((progressBar.hideEta - (new Date().getTime())) / progressBar.maxHideTime) * 100; + $progressElement.width(percentage + '%'); + } + } + + function getOptions() { + return $.extend({}, getDefaults(), toastr.options); + } + + function removeToast($toastElement) { + if (!$container) { $container = getContainer(); } + if ($toastElement.is(':visible')) { + return; + } + $toastElement.remove(); + $toastElement = null; + if ($container.children().length === 0) { + $container.remove(); + previousToast = undefined; + } + } + + })(); + }); +}(typeof define === 'function' && define.amd ? define : function (deps, factory) { + if (typeof module !== 'undefined' && module.exports) { //Node + module.exports = factory(require('jquery')); + } else { + window.toastr = factory(window.jQuery); + } +})); diff --git a/rhodecode/public/js/src/rhodecode.js b/rhodecode/public/js/src/rhodecode.js --- a/rhodecode/public/js/src/rhodecode.js +++ b/rhodecode/public/js/src/rhodecode.js @@ -159,7 +159,7 @@ var showRepoStats = function(target, dat lnk = document.createElement('a'); lnk.href = '#'; - lnk.innerHTML = _ngettext('Show more'); + lnk.innerHTML = _gettext('Show more'); lnk.id = 'code_stats_show_more'; td.appendChild(lnk); @@ -223,7 +223,6 @@ var formatSelect2SelectionRefs = functio // takes a given html element and scrolls it down offset pixels function offsetScroll(element, offset){ setTimeout(function(){ - console.log(element); var location = element.offset().top; // some browsers use body, some use html $('html, body').animate({ scrollTop: (location - offset) }); @@ -249,21 +248,26 @@ function offsetScroll(element, offset){ }); } }); - // Add tooltips - $('tr.line .lineno a').attr("title","Click to select line").addClass('tooltip'); - $('tr.line .add-comment-line a').attr("title","Click to comment").addClass('tooltip'); + $('.compare_view_files').on( + 'mouseenter mouseleave', 'tr.line .lineno a',function(event) { + if (event.type === "mouseenter") { + $(this).parents('tr.line').addClass('hover'); + } else { + $(this).parents('tr.line').removeClass('hover'); + } + }); - // Set colors and styles - $('tr.line .lineno a').hover( - function(){ - $(this).parents('tr.line').addClass('hover'); - }, function(){ - $(this).parents('tr.line').removeClass('hover'); - } - ); + $('.compare_view_files').on( + 'mouseenter mouseleave', 'tr.line .add-comment-line a',function(event){ + if (event.type === "mouseenter") { + $(this).parents('tr.line').addClass('commenting'); + } else { + $(this).parents('tr.line').removeClass('commenting'); + } + }); - $('tr.line .lineno a').click( - function(){ + $('.compare_view_files').on( + 'click', 'tr.line .lineno a',function(event) { if ($(this).text() != ""){ $('tr.line').removeClass('selected'); $(this).parents("tr.line").addClass('selected'); @@ -271,7 +275,7 @@ function offsetScroll(element, offset){ // Replace URL without jumping to it if browser supports. // Default otherwise if (history.pushState) { - var new_location = location.href + var new_location = location.href; if (location.hash){ new_location = new_location.replace(location.hash, ""); } @@ -283,23 +287,14 @@ function offsetScroll(element, offset){ return false; } } - } - ); + }); - $('tr.line .add-comment-line a').hover( - function(){ - $(this).parents('tr.line').addClass('commenting'); - }, function(){ - $(this).parents('tr.line').removeClass('commenting'); - } - ); - - $('tr.line .add-comment-line a').on('click', function(e){ - var tr = $(e.currentTarget).parents('tr.line')[0]; - injectInlineForm(tr); - return false; - }); - + $('.compare_view_files').on( + 'click', 'tr.line .add-comment-line a',function(event) { + var tr = $(event.currentTarget).parents('tr.line')[0]; + injectInlineForm(tr); + return false; + }); $('.collapse_file').on('click', function(e) { e.stopPropagation(); @@ -386,28 +381,14 @@ function offsetScroll(element, offset){ var tr = lineno.parents('tr.line'); tr.addClass('selected'); - // once we scrolled into our line, trigger chat app - if (remainder){ - tr.find('.add-comment-line a').trigger( "click" ); - setTimeout(function(){ - var nextNode = $(tr).next(); - if(nextNode.hasClass('inline-comments')){ - nextNode.next().find('.switch-to-chat').trigger( "click" ); - } - else{ - nextNode.find('.switch-to-chat').trigger( "click" ); - } - // trigger scroll into, later so all elements are already loaded - tr[0].scrollIntoView(); - }, 250); + tr[0].scrollIntoView(); - } - else{ - tr[0].scrollIntoView(); - } + $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({ + tr:tr, + remainder:remainder}); } } - }; + } collapsableContent(); }); diff --git a/rhodecode/public/js/src/rhodecode/comments.js b/rhodecode/public/js/src/rhodecode/comments.js --- a/rhodecode/public/js/src/rhodecode/comments.js +++ b/rhodecode/public/js/src/rhodecode/comments.js @@ -150,19 +150,6 @@ var injectInlineForm = function(tr){ var _form = $(f).find('.inline-form').get(0); - $('.switch-to-chat', _form).on('click', function(evt){ - var fParent = $(_parent).closest('.injected_diff').parent().prev('*[fid]'); - var fid = fParent.attr('fid'); - - // activate chat and trigger subscription to channels - $.Topic('/chat_controller').publish({ - action:'subscribe_to_channels', - data: ['/chat${0}$/fid/{1}/{2}'.format(templateContext.repo_name, fid, lineno)] - }); - $(_form).closest('td').find('.comment-inline-form').addClass('hidden'); - $(_form).closest('td').find('.chat-holder').removeClass('hidden'); - }); - var pullRequestId = templateContext.pull_request_data.pull_request_id; var commitId = templateContext.commit_data.commit_id; @@ -205,7 +192,6 @@ var injectInlineForm = function(tr){ // re trigger the linkification of next/prev navigation linkifyComments($('.inline-comment-injected')); timeagoActivate(); - tooltip_activate(); bindDeleteCommentButtons(); commentForm.setActionButtonsDisabled(false); @@ -224,6 +210,12 @@ var injectInlineForm = function(tr){ } }, 10); + $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({ + form:_form, + parent:_parent, + lineno: lineno, + f_path: f_path} + ); }; var deleteComment = function(comment_id) { @@ -545,7 +537,6 @@ var CommentForm = (function() { self.resetCommentFormState(); bindDeleteCommentButtons(); timeagoActivate(); - tooltip_activate(); } }; var submitFailCallback = function(){ diff --git a/rhodecode/public/js/src/rhodecode/connection_controller.js b/rhodecode/public/js/src/rhodecode/connection_controller.js new file mode 100644 --- /dev/null +++ b/rhodecode/public/js/src/rhodecode/connection_controller.js @@ -0,0 +1,219 @@ +"use strict"; +/** leak object to top level scope **/ +var ccLog = undefined; +// global code-mirror logger;, to enable run +// Logger.get('ConnectionController').setLevel(Logger.DEBUG) +ccLog = Logger.get('ConnectionController'); +ccLog.setLevel(Logger.OFF); + +var ConnectionController; +var connCtrlr; +var registerViewChannels; + +(function () { + ConnectionController = function (webappUrl, serverUrl, urls) { + var self = this; + + var channels = ['broadcast']; + this.state = { + open: false, + webappUrl: webappUrl, + serverUrl: serverUrl, + connId: null, + socket: null, + channels: channels, + heartbeat: null, + channelsInfo: {}, + urls: urls + }; + this.channelNameParsers = []; + + this.addChannelNameParser = function (fn) { + if (this.channelNameParsers.indexOf(fn) === -1) { + this.channelNameParsers.push(fn); + } + }; + + this.listen = function () { + if (window.WebSocket) { + ccLog.debug('attempting to create socket'); + var socket_url = self.state.serverUrl + "/ws?conn_id=" + self.state.connId; + var socket_conf = { + url: socket_url, + handleAs: 'json', + headers: { + "Accept": "application/json", + "Content-Type": "application/json" + } + }; + self.state.socket = new WebSocket(socket_conf.url); + + self.state.socket.onopen = function (event) { + ccLog.debug('open event', event); + if (self.state.heartbeat === null) { + self.state.heartbeat = setInterval(function () { + if (self.state.socket.readyState === WebSocket.OPEN) { + self.state.socket.send('heartbeat'); + } + }, 10000) + } + }; + self.state.socket.onmessage = function (event) { + var data = $.parseJSON(event.data); + for (var i = 0; i < data.length; i++) { + if (data[i].message.topic) { + ccLog.debug('publishing', + data[i].message.topic, data[i]); + $.Topic(data[i].message.topic).publish(data[i]) + } + else { + ccLog.warn('unhandled message', data); + } + } + }; + self.state.socket.onclose = function (event) { + ccLog.debug('closed event', event); + setTimeout(function () { + self.connect(true); + }, 5000); + }; + + self.state.socket.onerror = function (event) { + ccLog.debug('error event', event); + }; + } + else { + ccLog.debug('attempting to create long polling connection'); + var poolUrl = self.state.serverUrl + "/listen?conn_id=" + self.state.connId; + self.state.socket = $.ajax({ + url: poolUrl + }).done(function (data) { + ccLog.debug('data', data); + var data = $.parseJSON(data); + for (var i = 0; i < data.length; i++) { + if (data[i].message.topic) { + ccLog.info('publishing', + data[i].message.topic, data[i]); + $.Topic(data[i].message.topic).publish(data[i]) + } + else { + ccLog.warn('unhandled message', data); + } + } + self.listen(); + }).fail(function () { + ccLog.debug('longpoll error'); + setTimeout(function () { + self.connect(true); + }, 5000); + }); + } + + }; + + this.connect = function (create_new_socket) { + var connReq = {'channels': self.state.channels}; + ccLog.debug('try obtaining connection info', connReq); + $.ajax({ + url: self.state.urls.connect, + type: "POST", + contentType: "application/json", + data: JSON.stringify(connReq), + dataType: "json" + }).done(function (data) { + ccLog.debug('Got connection:', data.conn_id); + self.state.channels = data.channels; + self.state.channelsInfo = data.channels_info; + self.state.connId = data.conn_id; + if (create_new_socket) { + self.listen(); + } + self.update(); + }).fail(function () { + setTimeout(function () { + self.connect(create_new_socket); + }, 5000); + }); + self.update(); + }; + + this.subscribeToChannels = function (channels) { + var new_channels = []; + for (var i = 0; i < channels.length; i++) { + var channel = channels[i]; + if (self.state.channels.indexOf(channel)) { + self.state.channels.push(channel); + new_channels.push(channel) + } + } + /** + * only execute the request if socket is present because subscribe + * can actually add channels before initial app connection + **/ + if (new_channels && self.state.socket !== null) { + var connReq = { + 'channels': self.state.channels, + 'conn_id': self.state.connId + }; + $.ajax({ + url: self.state.urls.subscribe, + type: "POST", + contentType: "application/json", + data: JSON.stringify(connReq), + dataType: "json" + }).done(function (data) { + self.state.channels = data.channels; + self.state.channelsInfo = data.channels_info; + self.update(); + }); + } + self.update(); + }; + + this.update = function () { + for (var key in this.state.channelsInfo) { + if (this.state.channelsInfo.hasOwnProperty(key)) { + // update channels with latest info + $.Topic('/connection_controller/channel_update').publish( + {channel: key, state: this.state.channelsInfo[key]}); + } + } + /** + * checks current channel list in state and if channel is not present + * converts them into executable "commands" and pushes them on topics + */ + for (var i = 0; i < this.state.channels.length; i++) { + var channel = this.state.channels[i]; + for (var j = 0; j < this.channelNameParsers.length; j++) { + this.channelNameParsers[j](channel); + } + } + }; + + this.run = function () { + this.connect(true); + }; + + $.Topic('/connection_controller/subscribe').subscribe( + self.subscribeToChannels); + }; + + $.Topic('/plugins/__REGISTER__').subscribe(function (data) { + // enable chat controller + if (window.CHANNELSTREAM_SETTINGS && window.CHANNELSTREAM_SETTINGS.enabled) { + $(document).ready(function () { + connCtrlr.run(); + }); + } + }); + +registerViewChannels = function (){ + // subscribe to PR repo channel for PR's' + if (templateContext.pull_request_data.pull_request_id) { + var channelName = '/repo$' + templateContext.repo_name + '$/pr/' + + String(templateContext.pull_request_data.pull_request_id); + connCtrlr.state.channels.push(channelName); + } +} + +})(); diff --git a/rhodecode/public/js/src/rhodecode/tooltips.js b/rhodecode/public/js/src/rhodecode/tooltips.js --- a/rhodecode/public/js/src/rhodecode/tooltips.js +++ b/rhodecode/public/js/src/rhodecode/tooltips.js @@ -26,10 +26,10 @@ TTIP.main = { offset: [15,15], maxWidth: 600, - set_listeners: function(tt){ - $(tt).mouseover(tt, yt.show_tip); - $(tt).mousemove(tt, yt.move_tip); - $(tt).mouseout(tt, yt.close_tip); + setDeferredListeners: function(){ + $('body').on('mouseover', '.tooltip', yt.show_tip); + $('body').on('mousemove', '.tooltip', yt.move_tip); + $('body').on('mouseout', '.tooltip', yt.close_tip); }, init: function(){ @@ -43,19 +43,13 @@ TTIP.main = { if(yt.maxWidth !== null){ $(yt.tipBox).css('max-width', yt.maxWidth+'px'); } - - var tooltips = $('.tooltip'); - var ttLen = tooltips.length; - - for(i=0;i GET for (var param in params){ if (route[1].indexOf(param) === -1){ - ret.push(encodeURIComponent(param) + "=" + + ret.push(encodeURIComponent(param) + "=" + encodeURIComponent(params[param])); } } @@ -202,7 +210,7 @@ var pyroutes = (function() { result = APPLICATION_URL + result; } } - + return result; }, 'register': function(route_name, route_tmpl, req_params) { diff --git a/rhodecode/public/js/src/rhodecode/utils/topics.js b/rhodecode/public/js/src/rhodecode/utils/topics.js new file mode 100644 --- /dev/null +++ b/rhodecode/public/js/src/rhodecode/utils/topics.js @@ -0,0 +1,59 @@ +// # Copyright (C) 2010-2016 RhodeCode GmbH +// # +// # This program is free software: you can redistribute it and/or modify +// # it under the terms of the GNU Affero General Public License, version 3 +// # (only), as published by the Free Software Foundation. +// # +// # This program is distributed in the hope that it will be useful, +// # but WITHOUT ANY WARRANTY; without even the implied warranty of +// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// # GNU General Public License for more details. +// # +// # You should have received a copy of the GNU Affero General Public License +// # along with this program. If not, see . +// # +// # This program is dual-licensed. If you wish to learn more about the +// # RhodeCode Enterprise Edition, including its added features, Support services, +// # and proprietary license terms, please see https://rhodecode.com/licenses/ + + +var topics = {}; +jQuery.Topic = function (id) { + var callbacks, method, + topic = id && topics[id]; + + if (!topic) { + callbacks = jQuery.Callbacks(); + topic = { + unhandledData: [], + publish: callbacks.fire, + prepare: function(){ + for(var i=0; i< arguments.length; i++){ + this.unhandledData.push(arguments[i]); + } + }, + prepareOrPublish: function(){ + if (callbacks.has() === true){ + this.publish.apply(this, arguments); + } + else{ + this.prepare.apply(this, arguments); + } + }, + processPrepared: function(){ + var data = this.unhandledData; + this.unhandledData = []; + for(var i=0; i< data.length; i++){ + this.publish(data[i]); + } + }, + subscribe: callbacks.add, + unsubscribe: callbacks.remove, + callbacks: callbacks + }; + if (id) { + topics[id] = topic; + } + } + return topic; +}; diff --git a/rhodecode/public/js/topics_list.txt b/rhodecode/public/js/topics_list.txt new file mode 100644 --- /dev/null +++ b/rhodecode/public/js/topics_list.txt @@ -0,0 +1,4 @@ +/plugins/__REGISTER__ - launched after the onDomReady() code from rhodecode.js is executed +/ui/plugins/code/anchor_focus - launched when rc starts to scroll on load to anchor on PR/Codeview +/ui/plugins/code/comment_form_built - launched when injectInlineForm() is executed and the form object is created +/notifications - shows new event notifications \ No newline at end of file diff --git a/rhodecode/subscribers.py b/rhodecode/subscribers.py --- a/rhodecode/subscribers.py +++ b/rhodecode/subscribers.py @@ -53,3 +53,18 @@ def add_localizer(event): request.localizer = localizer request.translate = auto_translate + + +def scan_repositories_if_enabled(event): + """ + This is subscribed to the `pyramid.events.ApplicationCreated` event. It + does a repository scan if enabled in the settings. + """ + from rhodecode.model.scm import ScmModel + from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path + settings = event.app.registry.settings + vcs_server_enabled = settings['vcs.server.enable'] + import_on_startup = settings['startup.import_repos'] + if vcs_server_enabled and import_on_startup: + repositories = ScmModel().repo_scan(get_rhodecode_base_path()) + repo2db_mapper(repositories, remove_obsolete=False) diff --git a/rhodecode/svn_support/__init__.py b/rhodecode/svn_support/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/svn_support/__init__.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging +import os + +from rhodecode import events +from rhodecode.lib.utils2 import str2bool + +from .subscribers import generate_config_subscriber +from . import config_keys + + +log = logging.getLogger(__name__) + + +def includeme(config): + settings = config.registry.settings + _sanitize_settings_and_apply_defaults(settings) + + if settings[config_keys.generate_config]: + config.add_subscriber( + generate_config_subscriber, events.RepoGroupEvent) + + +def _sanitize_settings_and_apply_defaults(settings): + """ + Set defaults, convert to python types and validate settings. + """ + # Convert bool settings from string to bool. + settings[config_keys.generate_config] = str2bool( + settings.get(config_keys.generate_config, 'false')) + settings[config_keys.list_parent_path] = str2bool( + settings.get(config_keys.list_parent_path, 'true')) + + # Set defaults if key not present. + settings.setdefault(config_keys.config_file_path, None) + settings.setdefault(config_keys.location_root, '/') + settings.setdefault(config_keys.parent_path_root, None) + + # Append path separator to paths. + settings[config_keys.location_root] = _append_path_sep( + settings[config_keys.location_root]) + settings[config_keys.parent_path_root] = _append_path_sep( + settings[config_keys.parent_path_root]) + + # Validate settings. + if settings[config_keys.generate_config]: + assert settings[config_keys.config_file_path] is not None + + +def _append_path_sep(path): + """ + Append the path separator if missing. + """ + if isinstance(path, basestring) and not path.endswith(os.path.sep): + path += os.path.sep + return path diff --git a/rhodecode/svn_support/config_keys.py b/rhodecode/svn_support/config_keys.py new file mode 100644 --- /dev/null +++ b/rhodecode/svn_support/config_keys.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +# Definition of setting keys used to configure this module. Defined here to +# avoid repetition of keys throughout the module. +config_file_path = 'svn.proxy.config_file_path' +generate_config = 'svn.proxy.generate_config' +list_parent_path = 'svn.proxy.list_parent_path' +location_root = 'svn.proxy.location_root' +parent_path_root = 'svn.proxy.parent_path_root' diff --git a/rhodecode/svn_support/subscribers.py b/rhodecode/svn_support/subscribers.py new file mode 100644 --- /dev/null +++ b/rhodecode/svn_support/subscribers.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +from .utils import generate_mod_dav_svn_config + + +def generate_config_subscriber(event): + """ + Subscriber to the `rhodcode.events.RepoGroupEvent`. This triggers the + automatic generation of mod_dav_svn config file on repository group + changes. + """ + generate_mod_dav_svn_config(event.request.registry.settings) diff --git a/rhodecode/svn_support/templates/mod-dav-svn.conf.mako b/rhodecode/svn_support/templates/mod-dav-svn.conf.mako new file mode 100644 --- /dev/null +++ b/rhodecode/svn_support/templates/mod-dav-svn.conf.mako @@ -0,0 +1,65 @@ +# Auto generated configuration for use with the Apache mod_dav_svn module. +# +# WARNING: Make sure your Apache instance which runs the mod_dav_svn module is +# only accessible by RhodeCode. Otherwise everyone is able to browse +# the repositories or run subversion operations (checkout/commit/etc.). +# +# The mod_dav_svn module does not support subversion repositories which are +# organized in subfolders. To support the repository groups of RhodeCode it is +# required to provide a block for each group pointing to the +# repository group sub folder. +# +# To ease the configuration RhodeCode auto generates this file whenever a +# repository group is created/changed/deleted. Auto generation can be configured +# in the ini file. +# +# To include this configuration into your apache config you can use the +# `Include` directive. See the following example snippet of a virtual host how +# to include this configuration file. +# +# +# ServerAdmin webmaster@localhost +# DocumentRoot /var/www/html +# ErrorLog ${'${APACHE_LOG_DIR}'}/error.log +# CustomLog ${'${APACHE_LOG_DIR}'}/access.log combined +# Include /path/to/generated/mod_dav_svn.conf +# + + + + # The mod_dav_svn module takes the username from the apache request object. + # Without authorization this will be empty and no username is logged for the + # transactions. This will result in "(no author)" for each revision. The + # following directives implement a fake authentication that allows every + # username/password combination. + AuthType Basic + AuthName ${rhodecode_realm} + AuthBasicProvider anon + Anonymous * + Require valid-user + + DAV svn + SVNParentPath ${parent_path_root} + SVNListParentPath ${'On' if svn_list_parent_path else 'Off'} + + Allow from all + Order allow,deny + + +% for location, parent_path in repo_group_paths: + + + AuthType Basic + AuthName ${rhodecode_realm} + AuthBasicProvider anon + Anonymous * + Require valid-user + + DAV svn + SVNParentPath ${parent_path} + SVNListParentPath ${'On' if svn_list_parent_path else 'Off'} + + Allow from all + Order allow,deny + +% endfor diff --git a/rhodecode/svn_support/tests/test_mod_dav_svn_config.py b/rhodecode/svn_support/tests/test_mod_dav_svn_config.py new file mode 100644 --- /dev/null +++ b/rhodecode/svn_support/tests/test_mod_dav_svn_config.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + + +import mock +import re +import shutil +import tempfile + +from pyramid import testing + +from rhodecode.svn_support import config_keys, utils + + +class TestModDavSvnConfig(object): + @classmethod + def setup_class(cls): + # Make mako renderer available in tests. + config = testing.setUp() + config.include('pyramid_mako') + + # Temporary directory holding the generated config files. + cls.tempdir = tempfile.mkdtemp(suffix='pytest-mod-dav-svn') + + cls.location_root = '/location/root' + cls.parent_path_root = '/parent/path/root' + + @classmethod + def teardown_class(cls): + testing.tearDown() + shutil.rmtree(cls.tempdir, ignore_errors=True) + + @classmethod + def get_settings(cls): + config_file_path = tempfile.mkstemp( + suffix='mod-dav-svn.conf', dir=cls.tempdir)[1] + return { + config_keys.config_file_path: config_file_path, + config_keys.location_root: cls.location_root, + config_keys.parent_path_root: cls.parent_path_root, + config_keys.list_parent_path: True, + } + + @classmethod + def get_repo_groups(cls, count=1): + repo_groups = [] + for num in range(0, count): + full_path = '/path/to/RepoGroup{}'.format(num) + repo_group_mock = mock.MagicMock() + repo_group_mock.full_path = full_path + repo_group_mock.full_path_splitted = full_path.split('/') + repo_groups.append(repo_group_mock) + return repo_groups + + def assert_root_location_directive(self, config): + pattern = ''.format(location=self.location_root) + assert len(re.findall(pattern, config)) == 1 + + def assert_group_location_directive(self, config, group_path): + pattern = ''.format( + location=self.location_root, group_path=group_path) + assert len(re.findall(pattern, config)) == 1 + + @mock.patch('rhodecode.svn_support.utils.RepoGroup') + def test_generate_mod_dav_svn_config(self, RepoGroupMock): + num_groups = 3 + RepoGroupMock.get_all_repo_groups.return_value = self.get_repo_groups( + count=num_groups) + + # Execute the method under test. + settings = self.get_settings() + utils.generate_mod_dav_svn_config(settings) + + # Read generated file. + with open(settings[config_keys.config_file_path], 'r') as file_: + content = file_.read() + + # Assert that one location directive exists for each repository group. + for group in self.get_repo_groups(count=num_groups): + self.assert_group_location_directive(content, group.full_path) + + # Assert that the root location directive exists. + self.assert_root_location_directive(content) + + @mock.patch('rhodecode.svn_support.utils.RepoGroup') + def test_list_parent_path_on(self, RepoGroupMock): + RepoGroupMock.get_all_repo_groups.return_value = self.get_repo_groups() + + # Execute the method under test. + settings = self.get_settings() + settings[config_keys.list_parent_path] = True + utils.generate_mod_dav_svn_config(settings) + + # Read generated file. + with open(settings[config_keys.config_file_path], 'r') as file_: + content = file_.read() + + # Make assertions. + assert not re.search('SVNListParentPath\s+Off', content) + assert re.search('SVNListParentPath\s+On', content) + + @mock.patch('rhodecode.svn_support.utils.RepoGroup') + def test_list_parent_path_off(self, RepoGroupMock): + RepoGroupMock.get_all_repo_groups.return_value = self.get_repo_groups() + + # Execute the method under test. + settings = self.get_settings() + settings[config_keys.list_parent_path] = False + utils.generate_mod_dav_svn_config(settings) + + # Read generated file. + with open(settings[config_keys.config_file_path], 'r') as file_: + content = file_.read() + + # Make assertions. + assert re.search('SVNListParentPath\s+Off', content) + assert not re.search('SVNListParentPath\s+On', content) + + @mock.patch('rhodecode.svn_support.utils.log') + def test_write_does_not_raise_on_error(self, LogMock): + """ + Writing the configuration to file should never raise exceptions. + If e.g. path points to a place without write permissions. + """ + utils._write_mod_dav_svn_config( + 'content', '/dev/null/not/existing/path') + + # Assert that we log the exception. + assert LogMock.exception.called diff --git a/rhodecode/svn_support/utils.py b/rhodecode/svn_support/utils.py new file mode 100644 --- /dev/null +++ b/rhodecode/svn_support/utils.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2016 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# This program is dual-licensed. If you wish to learn more about the +# RhodeCode Enterprise Edition, including its added features, Support services, +# and proprietary license terms, please see https://rhodecode.com/licenses/ + +import logging +import os + +from pyramid.renderers import render + +from rhodecode.lib.utils import get_rhodecode_realm +from rhodecode.model.db import RepoGroup +from . import config_keys + + +log = logging.getLogger(__name__) + + +def generate_mod_dav_svn_config(settings): + """ + Generate the configuration file for use with subversion's mod_dav_svn + module. The configuration has to contain a block for each + available repository group because the mod_dav_svn module does not support + repositories organized in sub folders. + """ + config = _render_mod_dav_svn_config( + settings[config_keys.parent_path_root], + settings[config_keys.list_parent_path], + settings[config_keys.location_root], + RepoGroup.get_all_repo_groups()) + _write_mod_dav_svn_config(config, settings[config_keys.config_file_path]) + + +def _render_mod_dav_svn_config( + parent_path_root, list_parent_path, location_root, repo_groups): + """ + Render mod_dav_svn configuration to string. + """ + repo_group_paths = [] + for repo_group in repo_groups: + group_path = repo_group.full_path_splitted + location = os.path.join(location_root, *group_path) + parent_path = os.path.join(parent_path_root, *group_path) + repo_group_paths.append((location, parent_path)) + + context = { + 'location_root': location_root, + 'parent_path_root': parent_path_root, + 'repo_group_paths': repo_group_paths, + 'svn_list_parent_path': list_parent_path, + 'rhodecode_realm': get_rhodecode_realm(), + } + + # Render the configuration template to string. + template = 'rhodecode:svn_support/templates/mod-dav-svn.conf.mako' + return render(template, context) + + +def _write_mod_dav_svn_config(config, filepath): + """ + Write mod_dav_svn config to file. Log on exceptions but do not raise. + """ + try: + with open(filepath, 'w') as file_: + file_.write(config) + except Exception: + log.exception( + 'Can not write mod_dav_svn configuration to "%s"', filepath) diff --git a/rhodecode/templates/admin/admin_log.html b/rhodecode/templates/admin/admin_log.html --- a/rhodecode/templates/admin/admin_log.html +++ b/rhodecode/templates/admin/admin_log.html @@ -1,4 +1,6 @@ ## -*- coding: utf-8 -*- +<%namespace name="base" file="/base/base.html"/> + %if c.users_log: @@ -12,11 +14,11 @@ %for cnt,l in enumerate(c.users_log): - - diff --git a/rhodecode/templates/files/file_authors_box.html b/rhodecode/templates/files/file_authors_box.html --- a/rhodecode/templates/files/file_authors_box.html +++ b/rhodecode/templates/files/file_authors_box.html @@ -12,24 +12,18 @@ % if c.authors: -
    + diff --git a/rhodecode/templates/files/file_tree_author_box.html b/rhodecode/templates/files/file_tree_author_box.html --- a/rhodecode/templates/files/file_tree_author_box.html +++ b/rhodecode/templates/files/file_tree_author_box.html @@ -6,9 +6,7 @@ diff --git a/rhodecode/templates/files/files.html b/rhodecode/templates/files/files.html --- a/rhodecode/templates/files/files.html +++ b/rhodecode/templates/files/files.html @@ -91,37 +91,26 @@ if (source_page) { return false; } + + if ($('#file-tree-wrapper').hasClass('full-load')) { + // in case our HTML wrapper has full-load class we don't + // trigger the async load of metadata + return false; + } + var state = getState('metadata'); var url_data = { 'repo_name': templateContext.repo_name, - 'revision': state.commit_id, + 'commit_id': state.commit_id, 'f_path': state.f_path }; - var url = pyroutes.url('files_metadata_list_home', url_data); + var url = pyroutes.url('files_nodetree_full', url_data); metadataRequest = $.ajax({url: url}); metadataRequest.done(function(data) { - var data = data.metadata; - var dataLength = data.length; - for (var i = 0; i < dataLength; i++) { - var rowData = data[i]; - var name = rowData.name.replace('\\', '\\\\'); - - $('td[title="size-' + name + '"]').html(rowData.size); - var timeComponent = AgeModule.createTimeComponent( - rowData.modified_ts, rowData.modified_at); - $('td[title="modified_at-' + name + '"]').html(timeComponent); - - $('td[title="revision-' + name + '"]').html( - '
    r{1}:{2}
    '.format( - data[i].message, data[i].revision, data[i].short_id)); - $('td[title="author-' + name + '"]').html( - '{1}'.format( - data[i].author, data[i].user_profile)); - } - tooltip_activate(); + $('#file-tree').html(data); timeagoActivate(); }); metadataRequest.fail(function (data, textStatus, errorThrown) { @@ -134,13 +123,12 @@ var callbacks = function() { var state = getState('callbacks'); - tooltip_activate(); timeagoActivate(); // used for history, and switch to var initialCommitData = { id: null, - text: "${_("Switch To Commit")}", + text: '${_("Switch To Commit")}', type: 'sha', raw_id: null, files_url: null @@ -316,7 +304,6 @@ $('#file_history_overview').hide(); $('#file_history_overview_full').show(); timeagoActivate(); - tooltip_activate(); } else { callbacks(); } @@ -332,5 +319,4 @@ - diff --git a/rhodecode/templates/files/files_browser.html b/rhodecode/templates/files/files_browser.html --- a/rhodecode/templates/files/files_browser.html +++ b/rhodecode/templates/files/files_browser.html @@ -41,67 +41,11 @@ - -
    -
- %if l.user is not None: - ${h.link_to(l.user.username,h.url('edit_user', user_id=l.user.user_id))} - %else: - ${l.username} - %endif + %if l.user is not None: + ${base.gravatar_with_user(l.user.email)} + %else: + ${l.username} + %endif ${h.action_parser(l)[0]()}
@@ -50,8 +52,6 @@ //therefore the .one method $(document).on('pjax:complete',function(){ show_more_event(); - tooltip_activate(); - show_changeset_tooltip(); }); $(document).pjax('#user_log .pager_link', '#user_log'); diff --git a/rhodecode/templates/admin/auth/plugin_settings.html b/rhodecode/templates/admin/auth/plugin_settings.html --- a/rhodecode/templates/admin/auth/plugin_settings.html +++ b/rhodecode/templates/admin/auth/plugin_settings.html @@ -97,7 +97,6 @@
- ## TODO: Ugly hack to get ldap select elements to work. ## Find a solution to integrate this nicely. @@ -114,3 +113,4 @@ $("#search_scope").select2(select2Options); }); + diff --git a/rhodecode/templates/admin/gists/edit.html b/rhodecode/templates/admin/gists/edit.html --- a/rhodecode/templates/admin/gists/edit.html +++ b/rhodecode/templates/admin/gists/edit.html @@ -44,27 +44,31 @@ ${h.dropdownmenu('lifetime', '0', c.lifetime_options)} - - ${h.dropdownmenu('acl_level', c.gist.acl_level, c.acl_options)} + + ${h.dropdownmenu('gist_acl_level', c.gist.acl_level, c.acl_options)} + ## peppercorn schema + % for cnt, file in enumerate(c.files): +
- - - ${h.dropdownmenu('mimetypes' ,'plain',[('plain',_('plain'))],enable_filter=True, id='mimetype_'+h.FID('f',file.path))} + + + ${h.dropdownmenu('mimetype' ,'plain',[('plain',_('plain'))],enable_filter=True, id='mimetype_'+h.FID('f',file.path))}

-                      
+                      
                   
+ ## dynamic edit box. - %endfor +
${h.submit('update',_('Update Gist'),class_="btn btn-success")} diff --git a/rhodecode/templates/admin/gists/index.html b/rhodecode/templates/admin/gists/index.html --- a/rhodecode/templates/admin/gists/index.html +++ b/rhodecode/templates/admin/gists/index.html @@ -118,11 +118,11 @@ "sort": "expires"}, title: "${_("Expires")}", className: "td-exp" } ], language: { - paginate: DEFAULT_GRID_PAGINATION + paginate: DEFAULT_GRID_PAGINATION, + emptyTable: _gettext("No gists available yet.") }, "initComplete": function( settings, json ) { timeagoActivate(); - tooltip_activate(); get_datatable_count(); } }); @@ -130,7 +130,6 @@ // update the counter when things change $('#gist_list_table').on('draw.dt', function() { timeagoActivate(); - tooltip_activate(); get_datatable_count(); }); diff --git a/rhodecode/templates/admin/gists/new.html b/rhodecode/templates/admin/gists/new.html --- a/rhodecode/templates/admin/gists/new.html +++ b/rhodecode/templates/admin/gists/new.html @@ -39,7 +39,7 @@ ${h.dropdownmenu('lifetime', '', c.lifetime_options)} - ${h.dropdownmenu('acl_level', '', c.acl_options)} + ${h.dropdownmenu('gist_acl_level', '', c.acl_options)}
diff --git a/rhodecode/templates/admin/integrations/base.html b/rhodecode/templates/admin/integrations/base.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/admin/integrations/base.html @@ -0,0 +1,40 @@ +## -*- coding: utf-8 -*- +<%! + def inherit(context): + if context['c'].repo: + return "/admin/repos/repo_edit.html" + else: + return "/admin/settings/settings.html" +%> +<%inherit file="${inherit(context)}" /> + +<%def name="title()"> + ${_('Integrations Settings')} + %if c.rhodecode_name: + · ${h.branding(c.rhodecode_name)} + %endif + + +<%def name="breadcrumbs_links()"> + ${h.link_to(_('Admin'),h.url('admin_home'))} + » + ${_('Integrations')} + + +<%def name="menu_bar_nav()"> + %if c.repo: + ${self.menu_items(active='repositories')} + %else: + ${self.menu_items(active='admin')} + %endif + + +<%def name="menu_bar_subnav()"> + %if c.repo: + ${self.repo_menu(active='options')} + %endif + + +<%def name="main_content()"> + ${next.body()} + diff --git a/rhodecode/templates/admin/integrations/edit.html b/rhodecode/templates/admin/integrations/edit.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/admin/integrations/edit.html @@ -0,0 +1,43 @@ +## -*- coding: utf-8 -*- +<%inherit file="base.html"/> + +<%def name="breadcrumbs_links()"> + %if c.repo: + ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))} + » + ${h.link_to(_('Integrations'),request.route_url(route_name='repo_integrations_home', repo_name=c.repo.repo_name))} + » + ${h.link_to(current_IntegrationType.display_name, + request.route_url(route_name='repo_integrations_list', + repo_name=c.repo.repo_name, + integration=current_IntegrationType.key))} + %else: + ${h.link_to(_('Admin'),h.url('admin_home'))} + » + ${h.link_to(_('Settings'),h.url('admin_settings'))} + » + ${h.link_to(_('Integrations'),request.route_url(route_name='global_integrations_home'))} + » + ${h.link_to(current_IntegrationType.display_name, + request.route_url(route_name='global_integrations_list', + integration=current_IntegrationType.key))} + %endif + %if integration: + » + ${integration.name} + %endif + +
+
+

+ %if integration: + ${current_IntegrationType.display_name} - ${integration.name} + %else: + ${_('Create New %(integration_type)s Integration') % {'integration_type': current_IntegrationType.display_name}} + %endif +

+
+
+ ${form.render() | n} +
+
diff --git a/rhodecode/templates/admin/integrations/list.html b/rhodecode/templates/admin/integrations/list.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/admin/integrations/list.html @@ -0,0 +1,147 @@ +## -*- coding: utf-8 -*- +<%inherit file="base.html"/> + +<%def name="breadcrumbs_links()"> + %if c.repo: + ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))} + %else: + ${h.link_to(_('Admin'),h.url('admin_home'))} + » + ${h.link_to(_('Settings'),h.url('admin_settings'))} + %endif + %if current_IntegrationType: + » + %if c.repo: + ${h.link_to(_('Integrations'), + request.route_url(route_name='repo_integrations_home', + repo_name=c.repo.repo_name))} + %else: + ${h.link_to(_('Integrations'), + request.route_url(route_name='global_integrations_home'))} + %endif + » + ${current_IntegrationType.display_name} + %else: + » + ${_('Integrations')} + %endif + +
+
+

${_('Create New Integration')}

+
+
+ %if not available_integrations: + ${_('No integrations available.')} + %else: + %for integration in available_integrations: + <% + if c.repo: + create_url = request.route_url('repo_integrations_create', + repo_name=c.repo.repo_name, + integration=integration) + else: + create_url = request.route_url('global_integrations_create', + integration=integration) + %> + + ${integration} + + %endfor + %endif +
+
+
+
+

${_('Current Integrations')}

+
+
+ + + + + + + + + + + + + %for integration_type, integrations in sorted(current_integrations.items()): + %for integration in sorted(integrations, key=lambda x: x.name): + + + + + + + %endfor + %endfor + + +
${_('Enabled')}${_('Description')}${_('Type')}${_('Actions')}
+ %if integration.enabled: +
+ %else: +
+ %endif +
+ ${integration.name} + + ${integration.integration_type} + + %if integration_type not in available_integrations: + ${_('unknown integration')} + %else: + <% + if c.repo: + edit_url = request.route_url('repo_integrations_edit', + repo_name=c.repo.repo_name, + integration=integration.integration_type, + integration_id=integration.integration_id) + else: + edit_url = request.route_url('global_integrations_edit', + integration=integration.integration_type, + integration_id=integration.integration_id) + %> + + + %endif +
+
+
+ \ No newline at end of file diff --git a/rhodecode/templates/admin/my_account/my_account.html b/rhodecode/templates/admin/my_account/my_account.html --- a/rhodecode/templates/admin/my_account/my_account.html +++ b/rhodecode/templates/admin/my_account/my_account.html @@ -39,6 +39,7 @@
  • ${_('Watched')}
  • ${_('Pull Requests')}
  • ${_('My Permissions')}
  • +
  • ${_('My Live Notifications')}
  • diff --git a/rhodecode/templates/admin/my_account/my_account_auth_tokens.html b/rhodecode/templates/admin/my_account/my_account_auth_tokens.html --- a/rhodecode/templates/admin/my_account/my_account_auth_tokens.html +++ b/rhodecode/templates/admin/my_account/my_account_auth_tokens.html @@ -5,7 +5,7 @@

    ${_('Built-in tokens can be used to authenticate with all possible options.')}
    - ${_('Each token can have a role. VCS tokens can be used together with the authtoken auth plugin for git/hg operations.')} + ${_('Each token can have a role. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations.')}

    diff --git a/rhodecode/templates/admin/my_account/my_account_emails.html b/rhodecode/templates/admin/my_account/my_account_notifications.html copy from rhodecode/templates/admin/my_account/my_account_emails.html copy to rhodecode/templates/admin/my_account/my_account_notifications.html --- a/rhodecode/templates/admin/my_account/my_account_emails.html +++ b/rhodecode/templates/admin/my_account/my_account_notifications.html @@ -1,72 +1,56 @@ -<%namespace name="base" file="/base/base.html"/> -
    -

    ${_('Account Emails')}

    +

    ${_('Your live notification settings')}

    -
    -
    - - - - - %if c.user_email_map: - %for em in c.user_email_map: - - - - - %endfor - %else: - - - - %endif - -
    + +

    IMPORTANT: This feature requires enabled channelstream websocket server to function correctly.

    + + -
    - ${h.secure_form(url('my_account_emails'), method='post')} -
    - -
    -
    -
    - -
    -
    - ${h.text('new_email', class_='medium')} -
    -
    -
    - ${h.submit('save',_('Add'),class_="btn")} - ${h.reset('reset',_('Reset'),class_="btn")} -
    -
    -
    + ${h.secure_form(url('my_account_notifications_toggle_visibility'), method='post', id='notification-status')} + ${h.end_form()} -
    + + Test notification + + + diff --git a/rhodecode/templates/admin/my_account/my_account_pullrequests.html b/rhodecode/templates/admin/my_account/my_account_pullrequests.html --- a/rhodecode/templates/admin/my_account/my_account_pullrequests.html +++ b/rhodecode/templates/admin/my_account/my_account_pullrequests.html @@ -32,8 +32,10 @@
    - ${h.link_to(pull_request.target_repo.repo_name,h.url('summary_home',repo_name=pull_request.target_repo.repo_name))} + +
    + ${h.link_to(pull_request.target_repo.repo_name,h.url('summary_home',repo_name=pull_request.target_repo.repo_name))} +
    ${base.gravatar_with_user(pull_request.author.email, 16)} @@ -94,8 +96,10 @@
    - ${h.link_to(pull_request.target_repo.repo_name,h.url('summary_home',repo_name=pull_request.target_repo.repo_name))} + +
    + ${h.link_to(pull_request.target_repo.repo_name,h.url('summary_home',repo_name=pull_request.target_repo.repo_name))} +
    ${base.gravatar_with_user(pull_request.author.email, 16)} diff --git a/rhodecode/templates/admin/my_account/my_account_repos.html b/rhodecode/templates/admin/my_account/my_account_repos.html --- a/rhodecode/templates/admin/my_account/my_account_repos.html +++ b/rhodecode/templates/admin/my_account/my_account_repos.html @@ -37,11 +37,11 @@ "sort": "action"}, title: "${_('Action')}", className: "td-action" } ], language: { - paginate: DEFAULT_GRID_PAGINATION + paginate: DEFAULT_GRID_PAGINATION, + emptyTable: _gettext("No repositories available yet.") }, "initComplete": function( settings, json ) { get_datatable_count(); - tooltip_activate(); quick_repo_menu(); } }); diff --git a/rhodecode/templates/admin/my_account/my_account_watched.html b/rhodecode/templates/admin/my_account/my_account_watched.html --- a/rhodecode/templates/admin/my_account/my_account_watched.html +++ b/rhodecode/templates/admin/my_account/my_account_watched.html @@ -35,11 +35,11 @@ "type": Number}, title: "${_('Commit')}", className: "td-hash" } ], language: { - paginate: DEFAULT_GRID_PAGINATION + paginate: DEFAULT_GRID_PAGINATION, + emptyTable: _gettext("No repositories available yet.") }, "initComplete": function( settings, json ) { get_datatable_count(); - tooltip_activate(); quick_repo_menu(); } }); diff --git a/rhodecode/templates/admin/repo_groups/repo_groups.html b/rhodecode/templates/admin/repo_groups/repo_groups.html --- a/rhodecode/templates/admin/repo_groups/repo_groups.html +++ b/rhodecode/templates/admin/repo_groups/repo_groups.html @@ -62,11 +62,11 @@ "sort": "action"}, title: "${_('Action')}", className: "td-action" } ], language: { - paginate: DEFAULT_GRID_PAGINATION + paginate: DEFAULT_GRID_PAGINATION, + emptyTable: _gettext("No repository groups available yet.") }, "initComplete": function( settings, json ) { get_datatable_count(); - tooltip_activate(); quick_repo_menu(); } }); diff --git a/rhodecode/templates/admin/repos/repo_creating.html b/rhodecode/templates/admin/repos/repo_creating.html --- a/rhodecode/templates/admin/repos/repo_creating.html +++ b/rhodecode/templates/admin/repos/repo_creating.html @@ -38,7 +38,6 @@ - + \ No newline at end of file diff --git a/rhodecode/templates/admin/repos/repo_edit.html b/rhodecode/templates/admin/repos/repo_edit.html --- a/rhodecode/templates/admin/repos/repo_edit.html +++ b/rhodecode/templates/admin/repos/repo_edit.html @@ -23,6 +23,10 @@ ${self.repo_menu(active='options')} +<%def name="main_content()"> + <%include file="/admin/repos/repo_edit_${c.active}.html"/> + + <%def name="main()">
    @@ -64,14 +68,17 @@
  • ${_('Statistics')}
  • +
  • + ${_('Integrations')} +
  • - <%include file="/admin/repos/repo_edit_${c.active}.html"/> + ${self.main_content()}
    - + \ No newline at end of file diff --git a/rhodecode/templates/admin/repos/repos.html b/rhodecode/templates/admin/repos/repos.html --- a/rhodecode/templates/admin/repos/repos.html +++ b/rhodecode/templates/admin/repos/repos.html @@ -69,11 +69,11 @@ "sort": "action"}, title: "${_('Action')}", className: "td-action" } ], language: { - paginate: DEFAULT_GRID_PAGINATION + paginate: DEFAULT_GRID_PAGINATION, + emptyTable:_gettext("No repositories available yet.") }, "initComplete": function( settings, json ) { get_datatable_count(); - tooltip_activate(); quick_repo_menu(); } }); diff --git a/rhodecode/templates/admin/settings/settings.html b/rhodecode/templates/admin/settings/settings.html --- a/rhodecode/templates/admin/settings/settings.html +++ b/rhodecode/templates/admin/settings/settings.html @@ -18,6 +18,18 @@ ${self.menu_items(active='admin')} +<%def name="side_bar_nav()"> + % for navitem in c.navlist: +
  • + ${navitem.name} +
  • + % endfor + + +<%def name="main_content()"> + <%include file="/admin/settings/settings_${c.active}.html"/> + + <%def name="main()">
    @@ -28,18 +40,14 @@
    - + \ No newline at end of file diff --git a/rhodecode/templates/admin/settings/settings_global.html b/rhodecode/templates/admin/settings/settings_global.html --- a/rhodecode/templates/admin/settings/settings_global.html +++ b/rhodecode/templates/admin/settings/settings_global.html @@ -77,8 +77,8 @@
    ${h.textarea('rhodecode_pre_code',cols=23,rows=5,class_="medium")} - ${_('Custom js/css code added at the end of the
    tag.')} - ${_('Use diff --git a/rhodecode/templates/admin/user_groups/user_groups.html b/rhodecode/templates/admin/user_groups/user_groups.html --- a/rhodecode/templates/admin/user_groups/user_groups.html +++ b/rhodecode/templates/admin/user_groups/user_groups.html @@ -66,11 +66,11 @@ "sort": "action"}, title: "${_('Action')}", className: "td-action" } ], language: { - paginate: DEFAULT_GRID_PAGINATION + paginate: DEFAULT_GRID_PAGINATION, + emptyTable: _gettext("No user groups available yet.") }, "initComplete": function( settings, json ) { get_datatable_count(); - tooltip_activate(); } }); diff --git a/rhodecode/templates/admin/users/users.html b/rhodecode/templates/admin/users/users.html --- a/rhodecode/templates/admin/users/users.html +++ b/rhodecode/templates/admin/users/users.html @@ -84,7 +84,6 @@ pageLength: ${c.visual.admin_grid_items}, order: [[ 1, "asc" ]], columns: [ - { data: {"_": "gravatar"}, className: "td-gravatar" }, { data: {"_": "username", "sort": "username_raw"}, title: "${_('Username')}", className: "td-user" }, { data: {"_": "email", @@ -106,11 +105,11 @@ "sort": "action"}, title: "${_('Action')}", className: "td-action" } ], language: { - paginate: DEFAULT_GRID_PAGINATION + paginate: DEFAULT_GRID_PAGINATION, + emptyTable: _gettext("No users available yet.") }, "initComplete": function( settings, json ) { get_datatable_count(); - tooltip_activate(); }, "createdRow": function ( row, data, index ) { if (!data['active_raw']){ diff --git a/rhodecode/templates/base/base.html b/rhodecode/templates/base/base.html --- a/rhodecode/templates/base/base.html +++ b/rhodecode/templates/base/base.html @@ -7,7 +7,7 @@
    ${self.menu_bar_subnav()} @@ -82,6 +81,7 @@
  • ${_('User groups')}
  • ${_('Permissions')}
  • ${_('Authentication')}
  • +
  • ${_('Integrations')}
  • ${_('Defaults')}
  • ${_('Settings')}
  • @@ -135,8 +135,9 @@ <%def name="gravatar_with_user(contact, size=16, show_disabled=False)"> -
    - ${self.gravatar(h.email_or_none(contact), size)} + <% email = h.email_or_none(contact) %> +
    + ${self.gravatar(email, size)} ${h.link_to_user(contact)}
    @@ -579,7 +580,7 @@ % endif - +
    % endif + + ## This condition has to be adapted if we add more labs settings for + ## VCS types other than 'hg' + % if c.labs_active and (display_globals or repo_type in ['hg']): +
    +
    +

    ${_('Labs settings')}: ${_('These features are considered experimental and may not work as expected.')}

    +
    +
    +
    + +
    +
    + +
    +
    +
    + ${h.checkbox('rhodecode_hg_use_rebase_for_merging' + suffix, 'True', **kwargs)} + +
    + +
    +
    + +
    +
    +
    + % endif diff --git a/rhodecode/templates/bookmarks/bookmarks.html b/rhodecode/templates/bookmarks/bookmarks.html --- a/rhodecode/templates/bookmarks/bookmarks.html +++ b/rhodecode/templates/bookmarks/bookmarks.html @@ -68,11 +68,11 @@ "sort": "compare"}, title: "${_('Compare')}", className: "td-compare" } ], language: { - paginate: DEFAULT_GRID_PAGINATION + paginate: DEFAULT_GRID_PAGINATION, + emptyTable: _gettext("No bookmarks available yet.") }, "initComplete": function(settings, json) { get_datatable_count(); - tooltip_activate(); timeagoActivate(); compare_radio_buttons("${c.repo_name}", 'book'); } @@ -81,7 +81,6 @@ // update when things change $('#obj_list_table').on('draw.dt', function() { get_datatable_count(); - tooltip_activate(); timeagoActivate(); }); diff --git a/rhodecode/templates/branches/branches.html b/rhodecode/templates/branches/branches.html --- a/rhodecode/templates/branches/branches.html +++ b/rhodecode/templates/branches/branches.html @@ -67,11 +67,11 @@ "sort": "compare"}, title: "${_('Compare')}", className: "td-compare" } ], language: { - paginate: DEFAULT_GRID_PAGINATION + paginate: DEFAULT_GRID_PAGINATION, + emptyTable: _gettext("No branches available yet.") }, "initComplete": function( settings, json ) { get_datatable_count(); - tooltip_activate(); timeagoActivate(); compare_radio_buttons("${c.repo_name}", 'branch'); } @@ -80,7 +80,6 @@ // update when things change $('#obj_list_table').on('draw.dt', function() { get_datatable_count(); - tooltip_activate(); timeagoActivate(); }); diff --git a/rhodecode/templates/changelog/changelog.html b/rhodecode/templates/changelog/changelog.html --- a/rhodecode/templates/changelog/changelog.html +++ b/rhodecode/templates/changelog/changelog.html @@ -97,14 +97,18 @@
    - + ## checkbox - - + + + + ## commit message expand arrow - - + + + + @@ -128,27 +132,17 @@ %endif + %else: +
    %endif - - - - - - + + - + @@ -365,7 +365,7 @@ $('.expand_commit').on('click',function(e){ var target_expand = $(this); var cid = target_expand.data('commitId'); - + if (target_expand.hasClass('open')){ $('#c-'+cid).css({'height': '1.5em', 'white-space': 'nowrap', 'text-overflow': 'ellipsis', 'overflow':'hidden'}); $('#t-'+cid).css({'height': '1.5em', 'max-height': '1.5em', 'text-overflow': 'ellipsis', 'overflow':'hidden', 'white-space':'nowrap'}); @@ -507,7 +507,7 @@ diff --git a/rhodecode/templates/email_templates/base.mako b/rhodecode/templates/email_templates/base.mako --- a/rhodecode/templates/email_templates/base.mako +++ b/rhodecode/templates/email_templates/base.mako @@ -1,17 +1,131 @@ ## -*- coding: utf-8 -*- +## helpers +<%def name="tag_button(text, tag_type=None)"> + <% + color_scheme = { + 'default': 'border:1px solid #979797;color:#666666;background-color:#f9f9f9', + 'approved': 'border:1px solid #0ac878;color:#0ac878;background-color:#f9f9f9', + 'rejected': 'border:1px solid #e85e4d;color:#e85e4d;background-color:#f9f9f9', + 'under_review': 'border:1px solid #ffc854;color:#ffc854;background-color:#f9f9f9', + } + %> +
    ${text}
    + + +<%def name="status_text(text, tag_type=None)"> + <% + color_scheme = { + 'default': 'color:#666666', + 'approved': 'color:#0ac878', + 'rejected': 'color:#e85e4d', + 'under_review': 'color:#ffc854', + } + %> + ${text} + + ## headers we additionally can set for email <%def name="headers()" filter="n,trim"> -## plain text version of the email. Empty by default -<%def name="body_plaintext()" filter="n,trim"> +<%def name="plaintext_footer()"> +${_('This is a notification from RhodeCode. %(instance_url)s') % {'instance_url': instance_url}} + + +<%def name="body_plaintext()" filter="n,trim"> +## this example is not called itself but overridden in each template +## the plaintext_footer should be at the bottom of both html and text emails +${self.plaintext_footer()} + -${self.body()} + + + + + + ${self.subject()} + + + + + + + + + +
    ${_('Author')}${_('Age')}${_('Commit')} ${_('Commit Message')}${_('Commit')}${_('Age')}${_('Author')}${_('Refs')}
    - ${self.gravatar(h.email_or_none(commit.author))} - ${h.link_to_user(commit.author, length=22)} - - ${h.age_component(commit.date)} + + %if c.comments.get(commit.raw_id): + + ${len(c.comments[commit.raw_id])} + + %endif -
    -   -
    -
    -
    -
    ${h.urlify_commit_message(commit.message, c.repo_name)}
    -
    -
    @@ -156,13 +150,22 @@ +
    +   +
    +
    +
    +
    ${h.urlify_commit_message(commit.message, c.repo_name)}
    +
    +
    - %if c.comments.get(commit.raw_id): - - ${len(c.comments[commit.raw_id])} - - %endif + + ${h.age_component(commit.date)} + + ${self.gravatar_with_user(commit.author)} @@ -203,7 +206,7 @@ ${c.pagination.pager('$link_previous ~2~ $link_next')} - +
    diff --git a/rhodecode/templates/changeset/changeset.html b/rhodecode/templates/changeset/changeset.html --- a/rhodecode/templates/changeset/changeset.html +++ b/rhodecode/templates/changeset/changeset.html @@ -164,12 +164,10 @@ ${_('Author')}
    - +
    diff --git a/rhodecode/templates/channelstream/plugin_init.html b/rhodecode/templates/channelstream/plugin_init.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/channelstream/plugin_init.html @@ -0,0 +1,24 @@ + diff --git a/rhodecode/templates/data_table/_dt_elements.html b/rhodecode/templates/data_table/_dt_elements.html --- a/rhodecode/templates/data_table/_dt_elements.html +++ b/rhodecode/templates/data_table/_dt_elements.html @@ -103,7 +103,9 @@ <%def name="user_gravatar(email, size=16)"> +
    ${base.gravatar(email, 16)} +
    <%def name="repo_actions(repo_name, super_user=True)"> diff --git a/rhodecode/templates/debug_style/code-block.html b/rhodecode/templates/debug_style/code-block.html --- a/rhodecode/templates/debug_style/code-block.html +++ b/rhodecode/templates/debug_style/code-block.html @@ -9,11 +9,11 @@ <%def name="js_extra()"> - + <%def name="css_extra()"> - + diff --git a/rhodecode/templates/debug_style/tables.html b/rhodecode/templates/debug_style/tables.html --- a/rhodecode/templates/debug_style/tables.html +++ b/rhodecode/templates/debug_style/tables.html @@ -351,9 +351,9 @@
    tests: Test echo method on the server object - + This only works for Pyro4 so far, have to extend it still for HTTP to work.
    - + default
    + + + +
    + + + +
    + + ${_('RhodeCode')} + % if rhodecode_instance_name: + - ${rhodecode_instance_name} + % endif + +
    ${self.body()}
    +
    + +

    + ${self.plaintext_footer()} +

    + + diff --git a/rhodecode/templates/email_templates/commit_comment.mako b/rhodecode/templates/email_templates/commit_comment.mako --- a/rhodecode/templates/email_templates/commit_comment.mako +++ b/rhodecode/templates/email_templates/commit_comment.mako @@ -1,14 +1,43 @@ ## -*- coding: utf-8 -*- <%inherit file="base.mako"/> +<%namespace name="base" file="base.mako"/> <%def name="subject()" filter="n,trim"> - ${_('[mention]') if mention else ''} ${_('%(user)s commented on commit of %(repo_name)s') % { - 'user': h.person(user), - 'repo_name': repo_name - } } +<% +data = { + 'user': h.person(user), + 'repo_name': repo_name, + 'commit_id': h.show_id(commit), + 'status': status_change, + 'comment_file': comment_file, + 'comment_line': comment_line, +} +%> +${_('[mention]') if mention else ''} \ + +% if comment_file: + ${_('%(user)s commented on commit `%(commit_id)s` (file: `%(comment_file)s`)') % data} ${_('in the %(repo_name)s repository') % data |n} +% else: + % if status_change: + ${_('%(user)s commented on commit `%(commit_id)s` (status: %(status)s)') % data |n} ${_('in the %(repo_name)s repository') % data |n} + % else: + ${_('%(user)s commented on commit `%(commit_id)s`') % data |n} ${_('in the %(repo_name)s repository') % data |n} + % endif +% endif + <%def name="body_plaintext()" filter="n,trim"> +<% +data = { + 'user': h.person(user), + 'repo_name': repo_name, + 'commit_id': h.show_id(commit), + 'status': status_change, + 'comment_file': comment_file, + 'comment_line': comment_line, +} +%> ${self.subject()} * ${_('Comment link')}: ${commit_comment_url} @@ -21,37 +50,39 @@ --- -${comment_body|n} - - %if status_change: ${_('Commit status was changed to')}: *${status_change}* %endif +${comment_body|n} + +${self.plaintext_footer()} -% if comment_file: -

    ${_('%(user)s commented on a file in commit of %(repo_url)s.') % {'user': h.person(user), 'repo_url': commit_target_repo} |n}

    -% else: -

    ${_('%(user)s commented on a commit of %(repo_url)s.') % {'user': h.person(user), 'repo_url': commit_target_repo} |n}

    -% endif +<% +data = { + 'user': h.person(user), + 'comment_file': comment_file, + 'comment_line': comment_line, + 'repo': commit_target_repo, + 'repo_name': repo_name, + 'commit_id': h.show_id(commit), +} +%> + + + + -
      -
    • ${_('Comment link')}: ${commit_comment_url}
    • - %if comment_file: -
    • ${_('File: %(comment_file)s on line %(comment_line)s') % {'comment_file': comment_file, 'comment_line': comment_line}}
    • - %endif -
    • ${_('Commit')}: ${h.show_id(commit)}
    • -
    • - ${_('Commit Description')}:

      ${h.urlify_commit_message(commit.message, repo_name)}

      -
    • -
    - -
    -

    ${h.render(comment_body, renderer=renderer_type, mentions=True)}

    -
    - -%if status_change: -

    ${_('Commit status was changed to')}: ${status_change}

    -%endif + % if status_change: + + % endif + +
    + % if comment_file: +

    ${_('%(user)s commented on commit `%(commit_id)s` (file:`%(comment_file)s`)') % data} ${_('in the %(repo)s repository') % data |n}

    + % else: +

    ${_('%(user)s commented on commit `%(commit_id)s`') % data |n} ${_('in the %(repo)s repository') % data |n}

    + % endif +
    ${_('Commit')}${h.show_id(commit)}
    ${_('Description')}${h.urlify_commit_message(commit.message, repo_name)}
    ${_('Status')}${_('The commit status was changed to')}: ${base.status_text(status_change, tag_type=status_change_type)}
    ${(_('Comment on line: %(comment_line)s') if comment_file else _('Comment')) % data}${h.render(comment_body, renderer=renderer_type, mentions=True)}
    diff --git a/rhodecode/templates/email_templates/main.mako b/rhodecode/templates/email_templates/main.mako --- a/rhodecode/templates/email_templates/main.mako +++ b/rhodecode/templates/email_templates/main.mako @@ -8,9 +8,14 @@ ## plain text version of the email. Empty by default <%def name="body_plaintext()" filter="n,trim"> ${body} + +${self.plaintext_footer()} ## BODY GOES BELOW -
    -${body_plaintext()} -
    \ No newline at end of file + + +
    ${body}
    + ${self.plaintext_footer()} +

    \ No newline at end of file diff --git a/rhodecode/templates/email_templates/password_reset.mako b/rhodecode/templates/email_templates/password_reset.mako --- a/rhodecode/templates/email_templates/password_reset.mako +++ b/rhodecode/templates/email_templates/password_reset.mako @@ -16,9 +16,16 @@ There was a request to reset your passwo You can continue, and generate new password by clicking following URL: ${password_reset_url} +${self.plaintext_footer()} ## BODY GOES BELOW -
    -${body_plaintext()} -
    \ No newline at end of file +

    +Hello ${user.username}, +

    +There was a request to reset your password using the email address ${email} on ${h.format_date(date)} +
    +If you did not request a password reset, please contact your RhodeCode administrator. +

    +${_('Generate new password here')}. +

    diff --git a/rhodecode/templates/email_templates/password_reset_confirmation.mako b/rhodecode/templates/email_templates/password_reset_confirmation.mako --- a/rhodecode/templates/email_templates/password_reset_confirmation.mako +++ b/rhodecode/templates/email_templates/password_reset_confirmation.mako @@ -9,14 +9,22 @@ Your new RhodeCode password <%def name="body_plaintext()" filter="n,trim"> Hi ${user.username}, -Below is your new access password for RhodeCode. +There was a request to reset your password using the email address ${email} on ${h.format_date(date)} + +*If you didn't do this, please contact your RhodeCode administrator.* -password: ${new_password} +You can continue, and generate new password by clicking following URL: +${password_reset_url} -*If you didn't request a new password, please contact your RhodeCode administrator immediately.* +${self.plaintext_footer()} ## BODY GOES BELOW -
    -${body_plaintext()} -
    \ No newline at end of file +

    +Hello ${user.username}, +

    +Below is your new access password for RhodeCode. +
    +If you didn't request a new password, please contact your RhodeCode administrator. +

    +

    password:

    diff --git a/rhodecode/templates/email_templates/pull_request_comment.mako b/rhodecode/templates/email_templates/pull_request_comment.mako --- a/rhodecode/templates/email_templates/pull_request_comment.mako +++ b/rhodecode/templates/email_templates/pull_request_comment.mako @@ -1,15 +1,44 @@ ## -*- coding: utf-8 -*- <%inherit file="base.mako"/> +<%namespace name="base" file="base.mako"/> + <%def name="subject()" filter="n,trim"> - ${_('[mention]') if mention else ''} ${_('%(user)s commented on pull request #%(pr_id)s: "%(pr_title)s"') % { - 'user': h.person(user), - 'pr_title': pull_request.title, - 'pr_id': pull_request.pull_request_id - } |n} +<% +data = { + 'user': h.person(user), + 'pr_title': pull_request.title, + 'pr_id': pull_request.pull_request_id, + 'status': status_change, + 'comment_file': comment_file, + 'comment_line': comment_line, +} +%> + +${_('[mention]') if mention else ''} \ + +% if comment_file: + ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s" (file: `%(comment_file)s`)') % data |n} +% else: + % if status_change: + ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s" (status: %(status)s)') % data |n} + % else: + ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s"') % data |n} + % endif +% endif <%def name="body_plaintext()" filter="n,trim"> +<% +data = { + 'user': h.person(user), + 'pr_title': pull_request.title, + 'pr_id': pull_request.pull_request_id, + 'status': status_change, + 'comment_file': comment_file, + 'comment_line': comment_line, +} +%> ${self.subject()} * ${_('Comment link')}: ${pr_comment_url} @@ -22,45 +51,48 @@ --- +%if status_change and not closing_pr: + ${_('%(user)s submitted pull request #%(pr_id)s status: *%(status)s*') % data} +%elif status_change and closing_pr: + ${_('%(user)s submitted pull request #%(pr_id)s status: *%(status)s and closed*') % data} +%endif + ${comment_body|n} - -%if status_change and not closing_pr: - ${_('Pull request status was changed to')}: *${status_change}* -%elif status_change and closing_pr: - ${_('Pull request was closed with status')}: *${status_change}* -%endif - +${self.plaintext_footer()} -% if comment_file: -

    ${_('%(user)s commented on a file on pull request #%(pr_id)s: "%(pr_title)s".') % { - 'user': h.person(user), - 'pr_title': pull_request.title, - 'pr_id': pull_request.pull_request_id - } |n}

    -% else: -

    ${_('%(user)s commented on a pull request #%(pr_id)s "%(pr_title)s".') % { - 'user': h.person(user), - 'pr_title': pull_request.title, - 'pr_id': pull_request.pull_request_id - } |n}

    -% endif + +<% +data = { + 'user': h.person(user), + 'pr_title': pull_request.title, + 'pr_id': pull_request.pull_request_id, + 'status': status_change, + 'comment_file': comment_file, + 'comment_line': comment_line, +} +%> + + + + % if status_change: + + % endif + +
    +

    -
      -
    • ${_('Comment link')}: ${pr_comment_url}
    • -
    • ${_('Source repository')}: ${pr_source_repo.repo_name}
    • - %if comment_file: -
    • ${_('File: %(comment_file)s on line %(comment_line)s') % {'comment_file': comment_file, 'comment_line': comment_line}}
    • + % if comment_file: + ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s" (file:`%(comment_file)s`)') % data |n} + % else: + ${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s"') % data |n} + % endif + + %if status_change and not closing_pr: + , ${_('submitted pull request status: %(status)s') % data} + %elif status_change and closing_pr: + , ${_('submitted pull request status: %(status)s and closed') % data} %endif -
    - -
    -

    ${h.render(comment_body, renderer=renderer_type, mentions=True)}

    -
    - -%if status_change and not closing_pr: -

    ${_('Pull request status was changed to')}: ${status_change}

    -%elif status_change and closing_pr: -

    ${_('Pull request was closed with status')}: ${status_change}

    -%endif +

    +
    ${_('Source')}${pr_source_repo.repo_name}
    ${_('Submitted status')}${base.status_text(status_change, tag_type=status_change_type)}
    ${(_('Comment on line: %(comment_line)s') if comment_file else _('Comment')) % data}${h.render(comment_body, renderer=renderer_type, mentions=True)}
    diff --git a/rhodecode/templates/email_templates/pull_request_review.mako b/rhodecode/templates/email_templates/pull_request_review.mako --- a/rhodecode/templates/email_templates/pull_request_review.mako +++ b/rhodecode/templates/email_templates/pull_request_review.mako @@ -1,26 +1,37 @@ ## -*- coding: utf-8 -*- <%inherit file="base.mako"/> +<%namespace name="base" file="base.mako"/> <%def name="subject()" filter="n,trim"> - ${_('%(user)s wants you to review pull request #%(pr_url)s: "%(pr_title)s"') % { - 'user': h.person(user), - 'pr_title': pull_request.title, - 'pr_url': pull_request.pull_request_id - } |n} +<% +data = { + 'user': h.person(user), + 'pr_id': pull_request.pull_request_id, + 'pr_title': pull_request.title, +} +%> + +${_('%(user)s wants you to review pull request #%(pr_id)s: "%(pr_title)s"') % data |n} <%def name="body_plaintext()" filter="n,trim"> +<% +data = { + 'user': h.person(user), + 'pr_id': pull_request.pull_request_id, + 'pr_title': pull_request.title, + 'source_ref_type': pull_request.source_ref_parts.type, + 'source_ref_name': pull_request.source_ref_parts.name, + 'target_ref_type': pull_request.target_ref_parts.type, + 'target_ref_name': pull_request.target_ref_parts.name, + 'repo_url': pull_request_source_repo_url +} +%> ${self.subject()} -${h.literal(_('Pull request from %(source_ref_type)s:%(source_ref_name)s of %(repo_url)s into %(target_ref_type)s:%(target_ref_name)s') % { - 'source_ref_type': pull_request.source_ref_parts.type, - 'source_ref_name': pull_request.source_ref_parts.name, - 'target_ref_type': pull_request.target_ref_parts.type, - 'target_ref_name': pull_request.target_ref_parts.name, - 'repo_url': pull_request_source_repo_url -})} +${h.literal(_('Pull request from %(source_ref_type)s:%(source_ref_name)s of %(repo_url)s into %(target_ref_type)s:%(target_ref_name)s') % data)} * ${_('Link')}: ${pull_request_url} @@ -29,53 +40,46 @@ * ${_('Description')}: - ${pull_request.description} +${pull_request.description} * ${ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}: % for commit_id, message in pull_request_commits: - ${h.short_id(commit_id)} + ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')} - ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')} % endfor +${self.plaintext_footer()} - - -

    -${_('%(user)s wants you to review pull request #%(pr_id)s: "%(pr_title)s".') % { - 'user': h.person(user), - 'pr_title': pull_request.title, - 'pr_id': pull_request.pull_request_id - } } -

    - -

    ${h.literal(_('Pull request from %(source_ref_type)s:%(source_ref_name)s of %(repo_url)s into %(target_ref_type)s:%(target_ref_name)s') % { - 'source_ref_type': pull_request.source_ref_parts.type, - 'source_ref_name': pull_request.source_ref_parts.name, - 'target_ref_type': pull_request.target_ref_parts.type, - 'target_ref_name': pull_request.target_ref_parts.name, - 'repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url) - })} -

    - -

    ${_('Link')}: ${h.link_to(pull_request_url, pull_request_url)}

    - -

    ${_('Title')}: ${pull_request.title}

    -

    - ${_('Description')}:
    - ${pull_request.description} -

    - -

    - ${ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}: -

      - % for commit_id, message in pull_request_commits: -
    1. -
      ${h.short_id(commit_id)}
      - ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')} -
    2. - % endfor -
    -

    +<% +data = { + 'user': h.person(user), + 'pr_id': pull_request.pull_request_id, + 'pr_title': pull_request.title, + 'source_ref_type': pull_request.source_ref_parts.type, + 'source_ref_name': pull_request.source_ref_parts.name, + 'target_ref_type': pull_request.target_ref_parts.type, + 'target_ref_name': pull_request.target_ref_parts.name, + 'repo_url': pull_request_source_repo_url, + 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url), + 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url) +} +%> + + + + + + + + + +

    ${_('%(user)s wants you to review pull request #%(pr_id)s: "%(pr_title)s".') % data }

    ${_('Title')}${pull_request.title}
    ${_('Source')}${base.tag_button(pull_request.source_ref_parts.name)} ${h.literal(_('%(source_ref_type)s of %(source_repo_url)s') % data)}
    ${_('Target')}${base.tag_button(pull_request.target_ref_parts.name)} ${h.literal(_('%(target_ref_type)s of %(target_repo_url)s') % data)}
    ${_('Description')}${pull_request.description}
    ${ungettext('%(num)s Commit', '%(num)s Commits', len(pull_request_commits)) % {'num': len(pull_request_commits)}}
      + % for commit_id, message in pull_request_commits: +
    1. ${h.short_id(commit_id)}
      + ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')} +
    2. + % endfor +
    diff --git a/rhodecode/templates/email_templates/user_registration.mako b/rhodecode/templates/email_templates/user_registration.mako --- a/rhodecode/templates/email_templates/user_registration.mako +++ b/rhodecode/templates/email_templates/user_registration.mako @@ -2,10 +2,9 @@ <%inherit file="base.mako"/> <%def name="subject()" filter="n,trim"> -RhodeCode new user registration +RhodeCode new user registration: ${user.username} -## plain text version of the email. Empty by default <%def name="body_plaintext()" filter="n,trim"> A new user `${user.username}` has registered on ${h.format_date(date)} @@ -14,9 +13,15 @@ A new user `${user.username}` has regist - Full Name: ${user.firstname} ${user.lastname} - Email: ${user.email} - Profile link: ${h.url('user_profile', username=user.username, qualified=True)} + +${self.plaintext_footer()} ## BODY GOES BELOW -
    -${body_plaintext()} -
    + + + + + + +

    ${_('New user %(user)s has registered on %(date)s') % {'user': user.username, 'date': h.format_date(date)}}

    ${_('Username')} ${user.username}
    ${_('Full Name')}${user.firstname} ${user.lastname}
    ${_('Email')}${user.email}
    ${_('Profile')}${h.url('user_profile', username=user.username, qualified=True)}
    \ No newline at end of file diff --git a/rhodecode/templates/errors/error_document.html b/rhodecode/templates/errors/error_document.html --- a/rhodecode/templates/errors/error_document.html +++ b/rhodecode/templates/errors/error_document.html @@ -5,7 +5,7 @@ Error - ${c.error_message} - + @@ -13,20 +13,20 @@ %endif - + - + <%include file="/base/flash_msg.html"/>

    diff --git a/rhodecode/templates/files/diff_2way.html b/rhodecode/templates/files/diff_2way.html --- a/rhodecode/templates/files/diff_2way.html +++ b/rhodecode/templates/files/diff_2way.html @@ -4,11 +4,11 @@ <%namespace name="diff_block" file="/changeset/diff_block.html"/> <%def name="js_extra()"> - + <%def name="css_extra()"> - + <%def name="title()"> @@ -58,7 +58,7 @@

    ${h.fancy_file_stats(c.diff_data['stats'])}
    -
    +
    - - - - - - - - - + ## file tree is computed from caches, and filled in +
    + ${c.file_tree} +
    - - %if c.file.parent: - - - - - - - - %endif - %for cnt,node in enumerate(c.file): - - - %if node.is_file(): - - - - - %else: - - - - - %endif - - %endfor - - -
    ${_('Name')}${_('Size')}${_('Modified')}${_('Last Commit')}${_('Author')}
    - - .. - -
    - %if node.is_submodule(): - - ${h.link_to_if( - node.url.startswith('http://') or node.url.startswith('https://'), - node.name,node.url)} - - %else: - - ${node.name} - - %endif - - ${_('Loading...')} -
    -
    ${c.followers_pager.pager('$link_previous ~2~ $link_next')} diff --git a/rhodecode/templates/forks/forks_data.html b/rhodecode/templates/forks/forks_data.html --- a/rhodecode/templates/forks/forks_data.html +++ b/rhodecode/templates/forks/forks_data.html @@ -38,8 +38,6 @@ $(document).on('pjax:success',function(){ show_more_event(); timeagoActivate(); - tooltip_activate(); - show_changeset_tooltip(); }); ${c.forks_pager.pager('$link_previous ~2~ $link_next')} diff --git a/rhodecode/templates/forms/checkbox.pt b/rhodecode/templates/forms/checkbox.pt new file mode 100644 --- /dev/null +++ b/rhodecode/templates/forms/checkbox.pt @@ -0,0 +1,20 @@ +
    + + + +
    \ No newline at end of file diff --git a/rhodecode/templates/forms/checkbox_choice.pt b/rhodecode/templates/forms/checkbox_choice.pt new file mode 100644 --- /dev/null +++ b/rhodecode/templates/forms/checkbox_choice.pt @@ -0,0 +1,25 @@ +
    + ${field.start_sequence()} +
    +
    + + +
    +
    + ${field.end_sequence()} +
    \ No newline at end of file diff --git a/rhodecode/templates/forms/form.pt b/rhodecode/templates/forms/form.pt new file mode 100644 --- /dev/null +++ b/rhodecode/templates/forms/form.pt @@ -0,0 +1,100 @@ +
    + +
    + + ${title} + + + + + + + +

    + ${description} +

    + +
    + +
    + + + +
    + +
    + + + +
    \ No newline at end of file diff --git a/rhodecode/templates/forms/mapping.pt b/rhodecode/templates/forms/mapping.pt new file mode 100644 --- /dev/null +++ b/rhodecode/templates/forms/mapping.pt @@ -0,0 +1,33 @@ + + +
    +
    ${title}
    +
    + +
    +

    + There was a problem with this section +

    +

    ${errormsg}

    +
    + +
    + ${description} +
    + + ${field.start_mapping()} +
    +
    + ${field.end_mapping()} + +
    +
    +
    + +
    \ No newline at end of file diff --git a/rhodecode/templates/forms/mapping_item.pt b/rhodecode/templates/forms/mapping_item.pt new file mode 100644 --- /dev/null +++ b/rhodecode/templates/forms/mapping_item.pt @@ -0,0 +1,47 @@ +
    + + +
    +
    + ${input_prepend}${input_append} +
    +

    + ${msg} +

    + +

    + ${field.description} +

    +
    +
    \ No newline at end of file diff --git a/rhodecode/templates/forms/readonly/sequence.pt b/rhodecode/templates/forms/readonly/sequence.pt new file mode 100644 --- /dev/null +++ b/rhodecode/templates/forms/readonly/sequence.pt @@ -0,0 +1,24 @@ +
    + +
    +
    ${title}
    +
    + +
    +
    +
    + +
    +
    + +
    +
    \ No newline at end of file diff --git a/rhodecode/templates/forms/readonly/sequence_item.pt b/rhodecode/templates/forms/readonly/sequence_item.pt new file mode 100644 --- /dev/null +++ b/rhodecode/templates/forms/readonly/sequence_item.pt @@ -0,0 +1,11 @@ +
    +
    + +
    +
    diff --git a/rhodecode/templates/forms/select2.pt b/rhodecode/templates/forms/select2.pt new file mode 100644 --- /dev/null +++ b/rhodecode/templates/forms/select2.pt @@ -0,0 +1,55 @@ +
    + + + + + + + +
    \ No newline at end of file diff --git a/rhodecode/templates/forms/sequence.pt b/rhodecode/templates/forms/sequence.pt new file mode 100644 --- /dev/null +++ b/rhodecode/templates/forms/sequence.pt @@ -0,0 +1,105 @@ +
    + + + + + + +
    +
    ${title}
    +
    + +
    +
    + +
    + +
    +
    + + + +
    +
    \ No newline at end of file diff --git a/rhodecode/templates/forms/sequence_item.pt b/rhodecode/templates/forms/sequence_item.pt new file mode 100644 --- /dev/null +++ b/rhodecode/templates/forms/sequence_item.pt @@ -0,0 +1,35 @@ +
    +
    + + +

    ${msg}

    +
    +
    +
    + + + × +
    + +
    diff --git a/rhodecode/templates/forms/textinput.pt b/rhodecode/templates/forms/textinput.pt new file mode 100644 --- /dev/null +++ b/rhodecode/templates/forms/textinput.pt @@ -0,0 +1,23 @@ + + + + \ No newline at end of file diff --git a/rhodecode/templates/index_base.html b/rhodecode/templates/index_base.html --- a/rhodecode/templates/index_base.html +++ b/rhodecode/templates/index_base.html @@ -102,11 +102,11 @@ "sort": "owner"}, title: "${_('Owner')}", className: "td-user" } ], language: { - paginate: DEFAULT_GRID_PAGINATION + paginate: DEFAULT_GRID_PAGINATION, + emptyTable: _gettext("No repository groups available yet.") }, "drawCallback": function( settings, json ) { timeagoActivate(); - tooltip_activate(); quick_repo_menu(); } }); @@ -135,11 +135,11 @@ "sort": "rss"}, title: "rss", className: "td-rss" } ], language: { - paginate: DEFAULT_GRID_PAGINATION + paginate: DEFAULT_GRID_PAGINATION, + emptyTable: _gettext("No repositories available yet.") }, "drawCallback": function( settings, json ) { timeagoActivate(); - tooltip_activate(); quick_repo_menu(); } }); diff --git a/rhodecode/templates/journal/journal.html b/rhodecode/templates/journal/journal.html --- a/rhodecode/templates/journal/journal.html +++ b/rhodecode/templates/journal/journal.html @@ -46,8 +46,6 @@ $('#j_filter').autoGrowInput(); $(document).on('pjax:success',function(){ show_more_event(); - tooltip_activate(); - show_changeset_tooltip(); }); $(document).pjax('#refresh', '#journal', {url: "${h.url.current(filter=c.search_term)}", push: false}); diff --git a/rhodecode/templates/journal/journal_data.html b/rhodecode/templates/journal/journal_data.html --- a/rhodecode/templates/journal/journal_data.html +++ b/rhodecode/templates/journal/journal_data.html @@ -44,8 +44,6 @@ $(document).on('pjax:success',function(){ show_more_event(); timeagoActivate(); - tooltip_activate(); - show_changeset_tooltip(); }); %else: diff --git a/rhodecode/templates/login.html b/rhodecode/templates/login.html --- a/rhodecode/templates/login.html +++ b/rhodecode/templates/login.html @@ -14,7 +14,7 @@