diff --git a/.bumpversion.cfg b/.bumpversion.cfg --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 4.7.2 +current_version = 4.8.0 message = release: Bump version {current_version} to {new_version} [bumpversion:file:rhodecode/VERSION] diff --git a/.release.cfg b/.release.cfg --- a/.release.cfg +++ b/.release.cfg @@ -5,25 +5,20 @@ done = false 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.7.2 - -[task:updated_translation] +state = in_progress +version = 4.8.0 [task:generate_js_routes] diff --git a/MANIFEST.in b/MANIFEST.in --- a/MANIFEST.in +++ b/MANIFEST.in @@ -33,6 +33,7 @@ include rhodecode/public/502.html # images, css include rhodecode/public/css/*.css include rhodecode/public/images/*.* +include rhodecode/public/images/ee_features/*.* # sound files include rhodecode/public/sounds/*.mp3 diff --git a/default.nix b/default.nix --- a/default.nix +++ b/default.nix @@ -174,32 +174,33 @@ let ''; postInstall = '' + echo "Writing meta information for rccontrol to nix-support/rccontrol" + mkdir -p $out/nix-support/rccontrol + cp -v rhodecode/VERSION $out/nix-support/rccontrol/version + echo "DONE: Meta information for rccontrol written" + # python based programs need to be wrapped + ln -s ${self.pyramid}/bin/* $out/bin/ + ln -s ${self.gunicorn}/bin/gunicorn $out/bin/ 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 - # TODO: johbo: re-think this. Do the tools import anything from enterprise? ln -s ${self.rhodecode-tools}/bin/rhodecode-* $out/bin/ # note that condition should be restricted when adding further tools - for file in $out/bin/*; do #*/ + for file in $out/bin/*; + do wrapProgram $file \ + --prefix PATH : $PATH \ --prefix PYTHONPATH : $PYTHONPATH \ - --prefix PATH : $PATH \ --set PYTHONHASHSEED random done mkdir $out/etc cp configs/production.ini $out/etc - echo "Writing meta information for rccontrol to nix-support/rccontrol" - mkdir -p $out/nix-support/rccontrol - cp -v rhodecode/VERSION $out/nix-support/rccontrol/version - echo "DONE: Meta information for rccontrol written" # TODO: johbo: Make part of ac-tests if [ ! -f rhodecode/public/js/scripts.js ]; then diff --git a/docs/admin/apache-conf-example.rst b/docs/admin/apache-conf-example.rst --- a/docs/admin/apache-conf-example.rst +++ b/docs/admin/apache-conf-example.rst @@ -62,6 +62,9 @@ Below config if for an Apache Reverse Pr ProxyPass / http://127.0.0.1:10002/ timeout=7200 Keepalive=On ProxyPassReverse / http://127.0.0.1:10002/ + # Increase headers for large Mercurial headers + LimitRequestLine 16380 + # strict http prevents from https -> http downgrade Header always set Strict-Transport-Security "max-age=63072000; includeSubdomains; preload" 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,7 +5,10 @@ Use the following example to configure N .. code-block:: nginx + ## rate limiter for certain pages to prevent brute force attacks + limit_req_zone $binary_remote_addr zone=dl_limit:10m rate=1r/s; + ## custom log format log_format log_custom '$remote_addr - $remote_user [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" ' @@ -109,6 +112,12 @@ Use the following example to configure N proxy_set_header Connection "upgrade"; } + location /_admin/login { + ## rate limit this endpoint + limit_req zone=dl_limit burst=10 nodelay; + try_files $uri @rhode; + } + location / { try_files $uri @rhode; } 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 @@ -3,8 +3,12 @@ Repository Extra Fields ======================= -Extra fields attached to a |repo| allow you to configure additional actions for -|RCX|. To install and read more about |RCX|, see the :ref:`install-rcx` section. +Extra fields attached to a |repo| allow you to configure additional fields for +each repository. This allows storing custom data per-repository. + +It can be used in :ref:`integrations-webhook` or in |RCX|. +To install and read more about |RCX|, see the :ref:`install-rcx` section. + Enabling Extra Fields --------------------- @@ -27,9 +31,14 @@ 2. On the |repo| settings page, select t .. image:: ../images/extra-repo-fields.png +The most important is the `New field key` variable which under the value will +be stored. It needs to be unique for each repository. The label and description +will be generated in repository settings where users can actually save some +values inside generated extra fields. -Example Usage -------------- + +Example Usage in extensions +--------------------------- To use the extra fields in an extension, see the example below. For more information and examples, see the :ref:`extensions-hooks-ref` section. diff --git a/docs/admin/vcs-server.rst b/docs/admin/vcs-server.rst --- a/docs/admin/vcs-server.rst +++ b/docs/admin/vcs-server.rst @@ -7,7 +7,7 @@ The VCS Server handles |RCM| backend fun a VCS Server to run with a |RCM| instance. If you do not, you will be missing the connection between |RCM| and its |repos|. This will cause error messages on the web interface. You can run your setup in the following configurations, -currently the best performance is one VCS Server per |RCM| instance: +currently the best performance is one of following: * One VCS Server per |RCM| instance. * One VCS Server handling multiple instances. @@ -59,7 +59,8 @@ instance in the \vcs.backends Set a comma-separated list of the |repo| options available from the web interface. The default is ``hg, git, svn``, - which is all |repo| types available. + which is all |repo| types available. The order of backends is also the + order backend will try to detect requests type. \vcs.connection_timeout Set the length of time in seconds that the VCS Server waits for @@ -159,9 +160,10 @@ for full details see the :ref:`RhodeCode - NAME: vcsserver-1 - STATUS: RUNNING - - TYPE: VCSServer - - VERSION: 1.0.0 - - URL: http://127.0.0.1:10001 + logs:/home/ubuntu/.rccontrol/vcsserver-1/vcsserver.log + - VERSION: 4.7.2 VCSServer + - URL: http://127.0.0.1:10008 + - CONFIG: /home/ubuntu/.rccontrol/vcsserver-1/vcsserver.ini $ rccontrol restart vcsserver-1 Instance "vcsserver-1" successfully stopped. @@ -181,7 +183,9 @@ For a more detailed explanation of the l .. rst-class:: dl-horizontal \host - Set the host on which the VCS Server will run. + Set the host on which the VCS Server will run. VCSServer is not + protected by any authentication, so we *highly* recommend running it + under localhost ip that is `127.0.0.1` \port Set the port number on which the VCS Server will be available. @@ -189,13 +193,22 @@ For a more detailed explanation of the l \locale Set the locale the VCS Server expects. - \threadpool_size - Set the size of the threadpool used to communicate - with the WSGI workers. This should be at least 6 times the number of - WSGI worker processes. + \workers + Set the number of process workers.Recommended + value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers - \timeout - Set the timeout for RPC communication in seconds. + \max_requests + The maximum number of requests a worker will process before restarting. + Any value greater than zero will limit the number of requests a work + will process before automatically restarting. This is a simple method + to help limit the damage of memory leaks. + + \max_requests_jitter + The maximum jitter to add to the max_requests setting. + The jitter causes the restart per worker to be randomized by + randint(0, max_requests_jitter). This is intended to stagger worker + restarts to avoid all workers restarting at the same time. + .. note:: @@ -204,27 +217,54 @@ For a more detailed explanation of the l .. code-block:: ini ################################################################################ - # RhodeCode VCSServer - configuration # + # RhodeCode VCSServer with HTTP Backend - configuration # # # ################################################################################ - [DEFAULT] + + [server:main] + ## COMMON ## host = 127.0.0.1 - port = 9900 + port = 10002 + + ########################## + ## GUNICORN WSGI SERVER ## + ########################## + ## run with gunicorn --log-config vcsserver.ini --paste vcsserver.ini + use = egg:gunicorn#main + ## Sets the number of process workers. Recommended + ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers + workers = 3 + ## process name + proc_name = rhodecode_vcsserver + ## type of worker class, one of sync, gevent + ## recommended for bigger setup is using of of other than sync one + worker_class = sync + ## The maximum number of simultaneous clients. Valid only for Gevent + #worker_connections = 10 + ## max number of requests that worker will handle before being gracefully + ## restarted, could prevent memory leaks + max_requests = 1000 + max_requests_jitter = 30 + ## amount of time a worker can spend with handling a request before it + ## gets killed and restarted. Set to 6hrs + timeout = 21600 + + [app:main] + use = egg:rhodecode-vcsserver + + pyramid.default_locale_name = en + pyramid.includes = + + ## default locale used by VCS systems locale = en_US.UTF-8 - # number of worker threads, this should be set based on a formula threadpool=N*6 - # where N is number of RhodeCode Enterprise workers, eg. running 2 instances - # 8 gunicorn workers each would be 2 * 8 * 6 = 96, threadpool_size = 96 - threadpool_size = 16 - timeout = 0 # cache regions, please don't change beaker.cache.regions = repo_object beaker.cache.repo_object.type = memorylru - beaker.cache.repo_object.max_items = 1000 - + beaker.cache.repo_object.max_items = 100 # cache auto-expires after N seconds - beaker.cache.repo_object.expire = 10 + beaker.cache.repo_object.expire = 300 beaker.cache.repo_object.enabled = true @@ -270,20 +310,6 @@ For a more detailed explanation of the l level = DEBUG formatter = generic - [handler_file] - class = FileHandler - args = ('vcsserver.log', 'a',) - level = DEBUG - formatter = generic - - [handler_file_rotating] - class = logging.handlers.TimedRotatingFileHandler - # 'D', 5 - rotate every 5days - # you can set 'h', 'midnight' - args = ('vcsserver.log', 'D', 5, 10,) - level = DEBUG - formatter = generic - ################ ## FORMATTERS ## ################ diff --git a/docs/api/methods/pull-request-methods.rst b/docs/api/methods/pull-request-methods.rst --- a/docs/api/methods/pull-request-methods.rst +++ b/docs/api/methods/pull-request-methods.rst @@ -6,7 +6,7 @@ pull_request methods close_pull_request ------------------ -.. py:function:: close_pull_request(apiuser, repoid, pullrequestid, userid=>) +.. py:function:: close_pull_request(apiuser, repoid, pullrequestid, userid=>, message=) Close the pull request specified by `pullrequestid`. @@ -19,6 +19,9 @@ close_pull_request :type pullrequestid: int :param userid: Close the pull request as this user. :type userid: Optional(str or int) + :param message: Optional message to close the Pull Request with. If not + specified it will be generated automatically. + :type message: Optional(str) Example output: @@ -27,6 +30,7 @@ close_pull_request "id": , "result": { "pull_request_id": "", + "close_status": ", "closed": "" }, "error": null @@ -105,10 +109,12 @@ create_pull_request :param description: Set the pull request description. :type description: Optional(str) :param reviewers: Set the new pull request reviewers list. + Reviewer defined by review rules will be added automatically to the + defined list. :type reviewers: Optional(list) Accepts username strings or objects of the format: - {'username': 'nick', 'reasons': ['original author']} + [{'username': 'nick', 'reasons': ['original author'], 'mandatory': }] get_pull_request @@ -320,7 +326,7 @@ merge_pull_request update_pull_request ------------------- -.. py:function:: update_pull_request(apiuser, repoid, pullrequestid, title=, description=, reviewers=, update_commits=, close_pull_request=) +.. py:function:: update_pull_request(apiuser, repoid, pullrequestid, title=, description=, reviewers=, update_commits=) Updates a pull request. @@ -336,10 +342,12 @@ update_pull_request :type description: Optional(str) :param reviewers: Update pull request reviewers list with new value. :type reviewers: Optional(list) + Accepts username strings or objects of the format: + + [{'username': 'nick', 'reasons': ['original author'], 'mandatory': }] + :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: diff --git a/docs/api/methods/repo-methods.rst b/docs/api/methods/repo-methods.rst --- a/docs/api/methods/repo-methods.rst +++ b/docs/api/methods/repo-methods.rst @@ -527,6 +527,7 @@ get_repo_settings "id": 237, "result": { "extensions_largefiles": true, + "extensions_evolve": true, "hooks_changegroup_push_logger": true, "hooks_changegroup_repo_size": false, "hooks_outgoing_pull_logger": true, @@ -762,6 +763,49 @@ lock } +maintenance +----------- + +.. py:function:: maintenance(apiuser, repoid) + + Triggers a maintenance on the given repository. + + 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": "executed maintenance command", + "executed_actions": [ + , ... + ], + "repository": "" + } + error : null + + Example error output: + + .. code-block:: bash + + id : + result : null + error : { + "Unable to execute maintenance on ``" + } + + pull ---- 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 @@ -66,7 +66,7 @@ RhodeCode VCSServer repositories into th RhodeCode currently is using Mercurial Version Control System, please make sure you have it installed before continuing. -To obtain the required sources, use the following commands: +To obtain the required sources, use the following commands:: mkdir rhodecode-develop && cd rhodecode-develop hg clone https://code.rhodecode.com/rhodecode-enterprise-ce @@ -80,9 +80,9 @@ To obtain the required sources, use the Install some required libraries ------------------------------- -There are some required drivers that we need to install to test RhodeCode -under different types of databases. For example in Ubuntu we need to install -the following. +There are some required drivers and dev libraries that we need to install to +test RhodeCode under different types of databases. For example in Ubuntu we +need to install the following. required libraries:: diff --git a/docs/index.rst b/docs/index.rst --- a/docs/index.rst +++ b/docs/index.rst @@ -20,7 +20,7 @@ and commit files and |repos| while manag * Migration from existing databases. * |RCM| SDK. * Built-in analytics -* Built in integrations including: Slack, Jenkins, Webhooks, Jira, Redmine, Hipchat +* Built in integrations including: Slack, Webhooks (used for Jenkins/TeamCity and other CIs), Jira, Redmine, Hipchat * Pluggable authentication system. * Support for AD, |LDAP|, Crowd, CAS, PAM. * Support for external authentication via Oauth Google, Github, Bitbucket, Twitter. diff --git a/docs/integrations/ci.rst b/docs/integrations/ci.rst new file mode 100644 --- /dev/null +++ b/docs/integrations/ci.rst @@ -0,0 +1,97 @@ +.. _integrations-ci: + +CI Server integration +===================== + + +RhodeCode :ref:`integrations-webhook` integration is a powerfull tool to allow +interaction with systems like Jenkin, Bamboo, TeamCity, CircleCi or any other +CI server that allows triggering a build using HTTP call. + +Below are few examples on how to use :ref:`integrations-webhook` to trigger +a CI build. + + +General Webhook ++++++++++++++++ + +:ref:`integrations-webhook` allows sending a JSON payload information to specified +url with GET or POST methods. There are several variables that could be used +in the URL greatly extending the flexibility of this type of integration. + +Most of the modern CI systems such as Jenkins, TeamCity, Bamboo or CircleCi +allows triggering builds via GET or POST calls. + +:ref:`integrations-webhook` can be either specified per each repository or +globally, if your CI maps directly to all your projects a global +:ref:`integrations-webhook` integration can be created and will trigger builds +for each change in projects. If only some projects allow triggering builds a +global integration will also work because mostly a CI system will ignore a +call for unspecified builds. + + +.. note:: + + A quick note on security. It's recommended to allow IP restrictions + to only allow RhodeCode server to trigger builds. If you need to + specify username and password this could be done by embedding it into a + trigger URL, e.g. `http://user:password@server.com/job/${project_id} + + +If users require to provide any custom parameters, they can be stored for each +project inside the :ref:`repo-xtra`. For example to migrate a current job that +has a numeric build id, storing this as `jenkins_build_id` key extra field +the url would look like that:: + + http://server/job/${extra:jenkins_build_id}/ + + +.. note:: + + Please note that some variables will result in multiple calls. + e.g. for |HG| specifying `${branch}` will trigger as many builds as how + many branches the suer actually pushed. Same applies to `${commit_id}` + This will trigger many builds if many commits are pushed. This allows + triggering individual builds for each pushed commit. + + +Jenkins ++++++++ + +To use Jenkins CI with RhodeCode, a Jenkins Build with Parameters should be used. +Plugin details are available here: https://wiki.jenkins.io/display/JENKINS/Build+With+Parameters+Plugin + +If the plugin is configured, RhodeCode can trigger builds automatically by +calling such example url provided in :ref:`integrations-webhook` integration:: + + http://server/job/${project_id}/build-branch-${branch}/buildWithParameters?token=TOKEN&PARAMETER=value&PARAMETER2=value2 + + +Team City ++++++++++ + +To use TeamCity CI it's enough to call the API and provide a buildId. +Example url after configuring :ref:`repo-xtra` would look like that:: + + http://teacmtiyserver/viewType.html?buildTypeId=${extra:tc_build_id} + + +Each project can have many build configurations. +buildTypeId which is a unique ID for each build configuration (job). + + +CircleCi +++++++++ + +To use CircleCi, a POST call needs to be triggered. Example build url would +look like this:: + + http://cicleCiServer/project/${repo_type}/${username}/${repo_id}/tree/${branch} + + +Circle Ci expects format of:: + + POST: /project/:vcs-type/:username/:project/tree/:branch + + +CircleCi API documentation can be found here: https://circleci.com/docs/api/v1-reference/ diff --git a/docs/integrations/integrations.rst b/docs/integrations/integrations.rst --- a/docs/integrations/integrations.rst +++ b/docs/integrations/integrations.rst @@ -17,7 +17,8 @@ Type/Name |RC| Edi :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-ci` |RCCEshort| Trigger Builds for Common CI Systems +:ref:`integrations-email` |RCCEshort| Send repo push commits by email :ref:`integrations-redmine` |RCEEshort| Close/Resolve/Reference redmine issues :ref:`integrations-jira` |RCEEshort| Close/Resolve/Reference JIRA issues ============================ ============ ===================================== diff --git a/docs/integrations/webhook.rst b/docs/integrations/webhook.rst --- a/docs/integrations/webhook.rst +++ b/docs/integrations/webhook.rst @@ -3,9 +3,9 @@ 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. +The :ref:`creating-integrations` 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. Starting from 4.5.0 release, webhook integration allows to use variables inside the URL. For example in URL `https://server-example.com/${repo_name}` @@ -14,8 +14,10 @@ triggered from. Some of the variables li `${branch}` will result in webhook be called multiple times when multiple branches are pushed. -Some of the variables like `${pull_request_id}` will be replaced only in -the pull request related events. +Starting from 4.8.0 also repository extra fields can be used. A format to use +them is `${extra:field_key}`. It's usefull to use them to specify custom +repo only parameters. Some of the variables like `${pull_request_id}` +will be replaced only in the pull request related events. To create a webhook integration, select "webhook" in the integration settings and use the URL and key from your any previous custom webhook created. See diff --git a/docs/release-notes/release-notes-4.8.0.rst b/docs/release-notes/release-notes-4.8.0.rst new file mode 100644 --- /dev/null +++ b/docs/release-notes/release-notes-4.8.0.rst @@ -0,0 +1,103 @@ +|RCE| 4.8.0 |RNS| +----------------- + +Release Date +^^^^^^^^^^^^ + +- 2017-06-30 + + +New Features +^^^^^^^^^^^^ + +- Code Review: added new reviewers logic. This features now is Common Criteria + compatible and allows to define Mandatory (non-removable) reviewers. + In addition new options were added to forbid adding new reviewers or forbid + author of commits or the pull request itself to be a reviewer of the code. +- Audit logs: introducing new audit logs tracking most important actions in + the system. Admins can track important events such as deletion of resources, + permissions changes, user groups changes. Each event tracks users with his + IP and user agent. +- Mercurial: enabled evolve extensions. Each repository can be now configured + to support evolve, commit phases, and evolve state are also shown in + commit and changelog views. +- VCS: expose newly pushed bookmarks or branches as quick links to open a + pull request on client output. Allows easier pull request creation via CLI. + + +General +^^^^^^^ + +- Core: ported many views into pure pyramid code with python3.6 compatibility. + Now almost 80% of the code is ported, and future ready. It's our ongoing + effort to allow support for modern python version. +- Comments: show author tag in pull request comments to easily + discover the author of changes in discussions. +- Files: allow specifying custom filename for uploaded files via web interface. +- Pull requests: changed who is allowed to close a pull request. Now it's only + super-admin, owner or person who can merge. + Before it was every reviewer can close. Which really doesn't make sense. +- Users: show that user is disabled when editing his properties. +- Integrations: expose user_id, and username in Webhook integration + templates arguments. +- Integrations: exposed extra repo variables in template arguments of + Webhook integration. +- Login: add link when using external auth to make it easier to login + using oauth providers, such as Google or Github. +- Maintenance: added svn verify command to tasks to be able to verify the + filesystem and repo formats from web interface. Allows much easier tracking + of incompatible filesystem storage of subversion repositories. +- Events: expose permalink urls for pull requests, and repositories. + Permalink url should provide a non-changeable url that can be used in + external system. +- Svn: increase possibility to specify compatibility to pre 1.9 version. + + +Security +^^^^^^^^ + +- security(high): fixed possibility to delete other users inline comments + for users who were repository admins. +- security(med): fixed XSS inside the tooltip for author string. +- security(med): fixed stored XSS in notifications inbox. +- security(med): use custom writer for RST rendering to prevent injection of javascript: tags. +- security(med): escape flash messaged VCS errors to prevent reflected XSS attacks. +- security(low): use 404 instead of 403 code on permission decorator to + prevent brute force resource discovery attacks. +- security(low): fixed self XSS inside autocomplete files view. +- security(low): fixed self Xss inside repo strip view. +- security(low): fixed self Xss inside the email add functionality. +- security(none): use new safe escaped user attributes across the application. + Will prevent all possible XSS attack vectors from user stored attributes. + This specially can come from external authentication systems which doesn't + validate the data. + + +Performance +^^^^^^^^^^^ + + + + +Fixes +^^^^^ + +- Pull requests: make sure we process comments in the order of IDS when + linking them. In some edge cases it could lead to comments not displaying + correctly. +- Emails: fixed newlines in email templates that can break email sending code. +- Markdown: fixed hr and strong tags styling. +- Notifications: fixed problem with 500 errors on non-numeric entries in url. +- API: use simple schema validator to be consistent how we validate between + API and web views for create user and create user_group calls. +- Users: fixed problem with personal repo group wasn't shown for disabled users. +- Oauth: improve Google extraction of first/last name from returned data. + + +Upgrade notes +^^^^^^^^^^^^^ + + +- API: the `update_pull_request` method will no longer support a close action. + Users should use the existing `close_pull_request` method which allows + specifying a message and status while closing a pull request. \ No newline at end of file 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.8.0.rst release-notes-4.7.2.rst release-notes-4.7.1.rst release-notes-4.7.0.rst diff --git a/docs/usage/basic-vcs-commands.rst b/docs/usage/basic-vcs-commands.rst --- a/docs/usage/basic-vcs-commands.rst +++ b/docs/usage/basic-vcs-commands.rst @@ -3,8 +3,13 @@ Getting Started with VCS ------------------------ -When using |RCM|, you will be working with |git| or |hg| |repos| from the -command line. +When using |RCM|, you will be working with |git|, |svn| or |hg| |repos| from the +command line or using a GUI client such as Tortoise, Tower or SourceTree. + +|RCM| uses a standard |git|, |svn| and |hg| protocols. So all tools that +can interact with there protocols are supported, including Eclipse or PyCharm +plugins. + If you have never used either before, the following information should help you set up your local machine so that you can sync changes with the diff --git a/pkgs/bower-packages.nix b/pkgs/bower-packages.nix --- a/pkgs/bower-packages.nix +++ b/pkgs/bower-packages.nix @@ -7,7 +7,7 @@ buildEnv { name = "bower-env"; ignoreCol (fetchbower "paper-tooltip" "PolymerElements/paper-tooltip#1.1.3" "PolymerElements/paper-tooltip#^1.1.2" "0vmrm1n8k9sk9nvqy03q177axy22pia6i3j1gxbk72j3pqiqvg6k") (fetchbower "paper-toast" "PolymerElements/paper-toast#1.3.0" "PolymerElements/paper-toast#^1.3.0" "0x9rqxsks5455s8pk4aikpp99ijdn6kxr9gvhwh99nbcqdzcxq1m") (fetchbower "paper-toggle-button" "PolymerElements/paper-toggle-button#1.2.0" "PolymerElements/paper-toggle-button#^1.2.0" "0mphcng3ngspbpg4jjn0mb91nvr4xc1phq3qswib15h6sfww1b2w") - (fetchbower "iron-ajax" "PolymerElements/iron-ajax#1.4.3" "PolymerElements/iron-ajax#^1.4.3" "0m3dx27arwmlcp00b7n516sc5a51f40p9vapr1nvd57l3i3z0pzm") + (fetchbower "iron-ajax" "PolymerElements/iron-ajax#1.4.3" "PolymerElements/iron-ajax#^1.4.3" "1b1z3112ggjdflgrwbpmnbsh3kgcm4hn255wshvrlzds4w069gja") (fetchbower "iron-autogrow-textarea" "PolymerElements/iron-autogrow-textarea#1.0.13" "PolymerElements/iron-autogrow-textarea#^1.0.13" "0zwhpl97vii1s8k0lgain8i9dnw29b0mxc5ixdscx9las13n2lqq") (fetchbower "iron-a11y-keys" "PolymerElements/iron-a11y-keys#1.0.6" "PolymerElements/iron-a11y-keys#^1.0.6" "1xz3mgghfcxixq28sdb654iaxj4nyi1bzcwf77ydkms6fviqs9mv") (fetchbower "iron-flex-layout" "PolymerElements/iron-flex-layout#1.3.1" "PolymerElements/iron-flex-layout#^1.0.0" "0nswv3ih3bhflgcd2wjfmddqswzgqxb2xbq65jk9w3rkj26hplbl") diff --git a/pkgs/python-packages.nix b/pkgs/python-packages.nix --- a/pkgs/python-packages.nix +++ b/pkgs/python-packages.nix @@ -601,13 +601,13 @@ }; }; deform = super.buildPythonPackage { - name = "deform-2.0a2"; + name = "deform-2.0.4"; buildInputs = with self; []; doCheck = false; - propagatedBuildInputs = with self; [Chameleon colander peppercorn translationstring zope.deprecation]; + propagatedBuildInputs = with self; [Chameleon colander iso8601 peppercorn translationstring zope.deprecation]; src = fetchurl { - url = "https://pypi.python.org/packages/8d/b3/aab57e81da974a806dc9c5fa024a6404720f890a6dcf2e80885e3cb4609a/deform-2.0a2.tar.gz"; - md5 = "7a90d41f7fbc18002ce74f39bd90a5e4"; + url = "https://pypi.python.org/packages/66/3b/eefcb07abcab7a97f6665aa2d0cf1af741d9d6e78a2e4657fd2b89f89880/deform-2.0.4.tar.gz"; + md5 = "34756e42cf50dd4b4430809116c4ec0a"; }; meta = { license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ]; @@ -887,13 +887,13 @@ }; }; ipython-genutils = super.buildPythonPackage { - name = "ipython-genutils-0.1.0"; + name = "ipython-genutils-0.2.0"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/71/b7/a64c71578521606edbbce15151358598f3dfb72a3431763edc2baf19e71f/ipython_genutils-0.1.0.tar.gz"; - md5 = "9a8afbe0978adbcbfcb3b35b2d015a56"; + url = "https://pypi.python.org/packages/e8/69/fbeffffc05236398ebfcfb512b6d2511c622871dca1746361006da310399/ipython_genutils-0.2.0.tar.gz"; + md5 = "5a4f9781f78466da0ea1a648f3e1f79f"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal ]; @@ -1004,13 +1004,13 @@ }; }; mistune = super.buildPythonPackage { - name = "mistune-0.7.3"; + name = "mistune-0.7.4"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/88/1e/be99791262b3a794332fda598a07c2749a433b9378586361ba9d8e824607/mistune-0.7.3.tar.gz"; - md5 = "4eba50bd121b83716fa4be6a4049004b"; + url = "https://pypi.python.org/packages/25/a4/12a584c0c59c9fed529f8b3c47ca8217c0cf8bcc5e1089d3256410cfbdbc/mistune-0.7.4.tar.gz"; + md5 = "92d01cb717e9e74429e9bde9d29ac43b"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal ]; @@ -1082,13 +1082,13 @@ }; }; objgraph = super.buildPythonPackage { - name = "objgraph-2.0.0"; + name = "objgraph-3.1.0"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; []; src = fetchurl { - url = "https://pypi.python.org/packages/d7/33/ace750b59247496ed769b170586c5def7202683f3d98e737b75b767ff29e/objgraph-2.0.0.tar.gz"; - md5 = "25b0d5e5adc74aa63ead15699614159c"; + url = "https://pypi.python.org/packages/f4/b3/082e54e62094cb2ec84f8d5a49e0142cef99016491cecba83309cff920ae/objgraph-3.1.0.tar.gz"; + md5 = "eddbd96039796bfbd13eee403701e64a"; }; meta = { license = [ pkgs.lib.licenses.mit ]; @@ -1186,13 +1186,13 @@ }; }; prompt-toolkit = super.buildPythonPackage { - name = "prompt-toolkit-1.0.13"; + name = "prompt-toolkit-1.0.14"; buildInputs = with self; []; doCheck = false; propagatedBuildInputs = with self; [six wcwidth]; src = fetchurl { - url = "https://pypi.python.org/packages/23/be/4876b52d5cc159cbd4b0ff6e7aa419a26470849a43a8f647857a4a24467b/prompt_toolkit-1.0.13.tar.gz"; - md5 = "427b496d2c147bd3819bc3a7f6e0d493"; + url = "https://pypi.python.org/packages/55/56/8c39509b614bda53e638b7500f12577d663ac1b868aef53426fc6a26c3f5/prompt_toolkit-1.0.14.tar.gz"; + md5 = "f24061ae133ed32c6b764e92bd48c496"; }; meta = { license = [ pkgs.lib.licenses.bsdOriginal ]; @@ -1641,7 +1641,7 @@ }; }; rhodecode-enterprise-ce = super.buildPythonPackage { - name = "rhodecode-enterprise-ce-4.7.2"; + name = "rhodecode-enterprise-ce-4.8.0"; buildInputs = with self; [pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage configobj]; doCheck = true; propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments pygments-markdown-lexer Pylons Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress cssselect celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu lxml msgpack-python nbconvert 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 subprocess32 waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt]; diff --git a/requirements.txt b/requirements.txt --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ colander==1.2 configobj==5.0.6 cssselect==1.0.1 decorator==4.0.11 -deform==2.0a2 +deform==2.0.4 docutils==0.12 dogpile.cache==0.6.1 dogpile.core==0.4.1 @@ -38,7 +38,7 @@ meld3==1.0.2 msgpack-python==0.4.8 MySQL-python==1.2.5 nose==1.3.6 -objgraph==2.0.0 +objgraph==3.1.0 packaging==15.2 paramiko==1.15.1 Paste==2.0.3 diff --git a/rhodecode/VERSION b/rhodecode/VERSION --- a/rhodecode/VERSION +++ b/rhodecode/VERSION @@ -1,1 +1,1 @@ -4.7.2 \ No newline at end of file +4.8.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 @@ -51,7 +51,7 @@ PYRAMID_SETTINGS = {} EXTENSIONS = {} __version__ = ('.'.join((str(each) for each in VERSION[:3]))) -__dbversion__ = 71 # defines current db version for migrations +__dbversion__ = 78 # defines current db version for migrations __platform__ = platform.system() __license__ = 'AGPLv3, and Commercial License' __author__ = 'RhodeCode GmbH' diff --git a/rhodecode/api/__init__.py b/rhodecode/api/__init__.py --- a/rhodecode/api/__init__.py +++ b/rhodecode/api/__init__.py @@ -35,8 +35,9 @@ from pyramid.httpexceptions import HTTPN from rhodecode.api.exc import ( JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError) +from rhodecode.apps._base import TemplateArgs from rhodecode.lib.auth import AuthUser -from rhodecode.lib.base import get_ip_addr +from rhodecode.lib.base import get_ip_addr, attach_context_attributes from rhodecode.lib.ext_json import json from rhodecode.lib.utils2 import safe_str from rhodecode.lib.plugins.utils import get_plugin_settings @@ -278,6 +279,11 @@ def request_view(request): 'request': request, 'apiuser': auth_u }) + + # register some common functions for usage + attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id, + attach_to_request=True) + try: ret_value = func(**call_params) return jsonrpc_response(request, ret_value) diff --git a/rhodecode/api/tests/test_close_pull_request.py b/rhodecode/api/tests/test_close_pull_request.py --- a/rhodecode/api/tests/test_close_pull_request.py +++ b/rhodecode/api/tests/test_close_pull_request.py @@ -43,16 +43,16 @@ class TestClosePullRequest(object): response = api_call(self.app, params) expected = { 'pull_request_id': pull_request_id, + 'close_status': 'Rejected', 'closed': True, } assert_ok(id_, expected, response.body) - action = 'user_closed_pull_request:%d' % pull_request_id journal = UserLog.query()\ - .filter(UserLog.user_id == author)\ + .filter(UserLog.user_id == author) \ + .order_by('user_log_id') \ .filter(UserLog.repository_id == repo)\ - .filter(UserLog.action == action)\ .all() - assert len(journal) == 1 + assert journal[-1].action == 'repo.pull_request.close' @pytest.mark.backends("git", "hg") def test_api_close_pull_request_already_closed_error(self, pr_util): diff --git a/rhodecode/api/tests/test_comment_pull_request.py b/rhodecode/api/tests/test_comment_pull_request.py --- a/rhodecode/api/tests/test_comment_pull_request.py +++ b/rhodecode/api/tests/test_comment_pull_request.py @@ -62,13 +62,12 @@ class TestCommentPullRequest(object): } assert_ok(id_, expected, response.body) - action = 'user_commented_pull_request:%d' % pull_request_id journal = UserLog.query()\ .filter(UserLog.user_id == author)\ - .filter(UserLog.repository_id == repo)\ - .filter(UserLog.action == action)\ + .filter(UserLog.repository_id == repo) \ + .order_by('user_log_id') \ .all() - assert len(journal) == 2 + assert journal[-1].action == 'repo.pull_request.comment.create' @pytest.mark.backends("git", "hg") def test_api_comment_pull_request_change_status( diff --git a/rhodecode/api/tests/test_create_pull_request.py b/rhodecode/api/tests/test_create_pull_request.py --- a/rhodecode/api/tests/test_create_pull_request.py +++ b/rhodecode/api/tests/test_create_pull_request.py @@ -77,7 +77,7 @@ class TestCreatePullRequestApi(object): assert pull_request.source_repo.repo_name == data['source_repo'] assert pull_request.target_repo.repo_name == data['target_repo'] assert pull_request.revisions == [self.commit_ids['change']] - assert pull_request.reviewers == [] + assert len(pull_request.reviewers) == 1 @pytest.mark.backends("git", "hg") def test_create_with_empty_description(self, backend): @@ -98,7 +98,12 @@ class TestCreatePullRequestApi(object): def test_create_with_reviewers_specified_by_names( self, backend, no_notifications): data = self._prepare_data(backend) - reviewers = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN] + reviewers = [ + {'username': TEST_USER_REGULAR_LOGIN, + 'reasons': ['added manually']}, + {'username': TEST_USER_ADMIN_LOGIN, + 'reasons': ['added manually']}, + ] data['reviewers'] = reviewers id_, params = build_data( self.apikey_regular, 'create_pull_request', **data) @@ -110,16 +115,26 @@ class TestCreatePullRequestApi(object): assert result['result']['msg'] == expected_message pull_request_id = result['result']['pull_request_id'] pull_request = PullRequestModel().get(pull_request_id) - actual_reviewers = [r.user.username for r in pull_request.reviewers] + actual_reviewers = [ + {'username': r.user.username, + 'reasons': ['added manually'], + } for r in pull_request.reviewers + ] assert sorted(actual_reviewers) == sorted(reviewers) @pytest.mark.backends("git", "hg") def test_create_with_reviewers_specified_by_ids( self, backend, no_notifications): data = self._prepare_data(backend) - reviewer_names = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN] reviewers = [ - UserModel().get_by_username(n).user_id for n in reviewer_names] + {'username': UserModel().get_by_username( + TEST_USER_REGULAR_LOGIN).user_id, + 'reasons': ['added manually']}, + {'username': UserModel().get_by_username( + TEST_USER_ADMIN_LOGIN).user_id, + 'reasons': ['added manually']}, + ] + data['reviewers'] = reviewers id_, params = build_data( self.apikey_regular, 'create_pull_request', **data) @@ -131,14 +146,17 @@ class TestCreatePullRequestApi(object): assert result['result']['msg'] == expected_message pull_request_id = result['result']['pull_request_id'] pull_request = PullRequestModel().get(pull_request_id) - actual_reviewers = [r.user.username for r in pull_request.reviewers] - assert sorted(actual_reviewers) == sorted(reviewer_names) + actual_reviewers = [ + {'username': r.user.user_id, + 'reasons': ['added manually'], + } for r in pull_request.reviewers + ] + assert sorted(actual_reviewers) == sorted(reviewers) @pytest.mark.backends("git", "hg") def test_create_fails_when_the_reviewer_is_not_found(self, backend): data = self._prepare_data(backend) - reviewers = ['somebody'] - data['reviewers'] = reviewers + data['reviewers'] = [{'username': 'somebody'}] id_, params = build_data( self.apikey_regular, 'create_pull_request', **data) response = api_call(self.app, params) @@ -153,7 +171,7 @@ class TestCreatePullRequestApi(object): id_, params = build_data( self.apikey_regular, 'create_pull_request', **data) response = api_call(self.app, params) - expected_message = 'reviewers should be specified as a list' + expected_message = {u'': '"test_regular,test_admin" is not iterable'} assert_error(id_, expected_message, given=response.body) @pytest.mark.backends("git", "hg") diff --git a/rhodecode/api/tests/test_create_user.py b/rhodecode/api/tests/test_create_user.py --- a/rhodecode/api/tests/test_create_user.py +++ b/rhodecode/api/tests/test_create_user.py @@ -59,6 +59,21 @@ class TestCreateUser(object): expected = "email `%s` already exist" % (TEST_USER_REGULAR_EMAIL,) assert_error(id_, expected, given=response.body) + def test_api_create_user_with_wrong_username(self): + bad_username = '<> HELLO WORLD <>' + id_, params = build_data( + self.apikey, 'create_user', + username=bad_username, + email='new@email.com', + password='trololo') + response = api_call(self.app, params) + + expected = {'username': + "Username may only contain alphanumeric characters " + "underscores, periods or dashes and must begin with " + "alphanumeric character or underscore"} + assert_error(id_, expected, given=response.body) + def test_api_create_user(self): username = 'test_new_api_user' email = username + "@foo.com" @@ -175,7 +190,6 @@ class TestCreateUser(object): fixture.destroy_repo_group(username) fixture.destroy_user(usr.user_id) - @mock.patch.object(UserModel, 'create_or_update', crash) def test_api_create_user_when_exception_happened(self): diff --git a/rhodecode/api/tests/test_create_user_group.py b/rhodecode/api/tests/test_create_user_group.py --- a/rhodecode/api/tests/test_create_user_group.py +++ b/rhodecode/api/tests/test_create_user_group.py @@ -112,3 +112,16 @@ class TestCreateUserGroup(object): expected = 'failed to create group `%s`' % (group_name,) assert_error(id_, expected, given=response.body) + + def test_api_create_user_group_with_wrong_name(self, user_util): + + group_name = 'wrong NAME <>' + id_, params = build_data( + self.apikey, 'create_user_group', group_name=group_name) + response = api_call(self.app, params) + + expected = {"user_group_name": + "Allowed in name are letters, numbers, and `-`, `_`, " + "`.` Name must start with a letter or number. " + "Got `{}`".format(group_name)} + assert_error(id_, expected, given=response.body) diff --git a/rhodecode/api/tests/test_delete_repo.py b/rhodecode/api/tests/test_delete_repo.py --- a/rhodecode/api/tests/test_delete_repo.py +++ b/rhodecode/api/tests/test_delete_repo.py @@ -30,43 +30,45 @@ from rhodecode.api.tests.utils import ( class TestApiDeleteRepo(object): def test_api_delete_repo(self, backend): repo = backend.create_repo() - + repo_name = repo.repo_name id_, params = build_data( self.apikey, 'delete_repo', repoid=repo.repo_name, ) response = api_call(self.app, params) expected = { - 'msg': 'Deleted repository `%s`' % (repo.repo_name,), + 'msg': 'Deleted repository `%s`' % (repo_name,), 'success': True } assert_ok(id_, expected, given=response.body) def test_api_delete_repo_by_non_admin(self, backend, user_regular): repo = backend.create_repo(cur_user=user_regular.username) + repo_name = repo.repo_name id_, params = build_data( user_regular.api_key, 'delete_repo', repoid=repo.repo_name, ) response = api_call(self.app, params) expected = { - 'msg': 'Deleted repository `%s`' % (repo.repo_name,), + 'msg': 'Deleted repository `%s`' % (repo_name,), 'success': True } assert_ok(id_, expected, given=response.body) def test_api_delete_repo_by_non_admin_no_permission(self, backend): repo = backend.create_repo() + repo_name = repo.repo_name id_, params = build_data( self.apikey_regular, 'delete_repo', repoid=repo.repo_name, ) response = api_call(self.app, params) - expected = 'repository `%s` does not exist' % (repo.repo_name) + expected = 'repository `%s` does not exist' % (repo_name) assert_error(id_, expected, given=response.body) def test_api_delete_repo_exception_occurred(self, backend): repo = backend.create_repo() + repo_name = repo.repo_name id_, params = build_data( self.apikey, 'delete_repo', repoid=repo.repo_name, ) with mock.patch.object(RepoModel, 'delete', crash): response = api_call(self.app, params) - expected = 'failed to delete repository `%s`' % ( - repo.repo_name,) + expected = 'failed to delete repository `%s`' % (repo_name,) assert_error(id_, expected, given=response.body) diff --git a/rhodecode/api/tests/test_get_gist.py b/rhodecode/api/tests/test_get_gist.py --- a/rhodecode/api/tests/test_get_gist.py +++ b/rhodecode/api/tests/test_get_gist.py @@ -28,7 +28,7 @@ from rhodecode.api.tests.utils import ( @pytest.mark.usefixtures("testuser_api", "app") class TestApiGetGist(object): - def test_api_get_gist(self, gist_util): + def test_api_get_gist(self, gist_util, http_host_stub): gist = gist_util.create_gist() gist_id = gist.gist_access_id gist_created_on = gist.created_on @@ -45,14 +45,14 @@ class TestApiGetGist(object): 'expires': -1.0, 'gist_id': int(gist_id), 'type': 'public', - 'url': 'http://test.example.com:80/_admin/gists/%s' % (gist_id,), + 'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,), 'acl_level': Gist.ACL_LEVEL_PUBLIC, 'content': None, } assert_ok(id_, expected, given=response.body) - def test_api_get_gist_with_content(self, gist_util): + def test_api_get_gist_with_content(self, gist_util, http_host_stub): mapping = { u'filename1.txt': {'content': u'hello world'}, u'filename1ą.txt': {'content': u'hello worldę'} @@ -73,7 +73,7 @@ class TestApiGetGist(object): 'expires': -1.0, 'gist_id': int(gist_id), 'type': 'public', - 'url': 'http://test.example.com:80/_admin/gists/%s' % (gist_id,), + 'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,), 'acl_level': Gist.ACL_LEVEL_PUBLIC, 'content': { u'filename1.txt': u'hello world', diff --git a/rhodecode/api/tests/test_get_pull_request.py b/rhodecode/api/tests/test_get_pull_request.py --- a/rhodecode/api/tests/test_get_pull_request.py +++ b/rhodecode/api/tests/test_get_pull_request.py @@ -19,13 +19,13 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ -import mock import pytest import urlobject from pylons import url from rhodecode.api.tests.utils import ( build_data, api_call, assert_error, assert_ok) +from rhodecode.lib.utils2 import safe_unicode pytestmark = pytest.mark.backends("git", "hg") @@ -33,7 +33,7 @@ pytestmark = pytest.mark.backends("git", @pytest.mark.usefixtures("testuser_api", "app") class TestGetPullRequest(object): - def test_api_get_pull_request(self, pr_util): + def test_api_get_pull_request(self, pr_util, http_host_only_stub): from rhodecode.model.pull_request import PullRequestModel pull_request = pr_util.create_pull_request(mergeable=True) id_, params = build_data( @@ -50,16 +50,16 @@ class TestGetPullRequest(object): 'pullrequest_show', repo_name=pull_request.target_repo.repo_name, pull_request_id=pull_request.pull_request_id, qualified=True)) - pr_url = unicode( - url_obj.with_netloc('test.example.com:80')) - source_url = unicode( - pull_request.source_repo.clone_url() - .with_netloc('test.example.com:80')) - target_url = unicode( - pull_request.target_repo.clone_url() - .with_netloc('test.example.com:80')) - shadow_url = unicode( + + pr_url = safe_unicode( + url_obj.with_netloc(http_host_only_stub)) + source_url = safe_unicode( + pull_request.source_repo.clone_url().with_netloc(http_host_only_stub)) + target_url = safe_unicode( + pull_request.target_repo.clone_url().with_netloc(http_host_only_stub)) + shadow_url = safe_unicode( PullRequestModel().get_shadow_clone_url(pull_request)) + expected = { 'pull_request_id': pull_request.pull_request_id, 'url': pr_url, @@ -109,7 +109,8 @@ class TestGetPullRequest(object): 'reasons': reasons, 'review_status': st[0][1].status if st else 'not_reviewed', } - for reviewer, reasons, st in pull_request.reviewers_statuses() + for reviewer, reasons, mandatory, st in + pull_request.reviewers_statuses() ] } assert_ok(id_, expected, response.body) diff --git a/rhodecode/api/tests/test_merge_pull_request.py b/rhodecode/api/tests/test_merge_pull_request.py --- a/rhodecode/api/tests/test_merge_pull_request.py +++ b/rhodecode/api/tests/test_merge_pull_request.py @@ -95,13 +95,13 @@ class TestMergePullRequest(object): assert_ok(id_, expected, response.body) - action = 'user_merged_pull_request:%d' % (pull_request_id, ) journal = UserLog.query()\ .filter(UserLog.user_id == author)\ - .filter(UserLog.repository_id == repo)\ - .filter(UserLog.action == action)\ + .filter(UserLog.repository_id == repo) \ + .order_by('user_log_id') \ .all() - assert len(journal) == 1 + assert journal[-2].action == 'repo.pull_request.merge' + assert journal[-1].action == 'repo.pull_request.close' id_, params = build_data( self.apikey, 'merge_pull_request', diff --git a/rhodecode/api/tests/test_update_pull_request.py b/rhodecode/api/tests/test_update_pull_request.py --- a/rhodecode/api/tests/test_update_pull_request.py +++ b/rhodecode/api/tests/test_update_pull_request.py @@ -33,7 +33,7 @@ class TestUpdatePullRequest(object): @pytest.mark.backends("git", "hg") def test_api_update_pull_request_title_or_description( - self, pr_util, silence_action_logger, no_notifications): + self, pr_util, no_notifications): pull_request = pr_util.create_pull_request() id_, params = build_data( @@ -61,7 +61,7 @@ class TestUpdatePullRequest(object): @pytest.mark.backends("git", "hg") def test_api_try_update_closed_pull_request( - self, pr_util, silence_action_logger, no_notifications): + self, pr_util, no_notifications): pull_request = pr_util.create_pull_request() PullRequestModel().close_pull_request( pull_request, TEST_USER_ADMIN_LOGIN) @@ -78,8 +78,7 @@ class TestUpdatePullRequest(object): assert_error(id_, expected, response.body) @pytest.mark.backends("git", "hg") - def test_api_update_update_commits( - self, pr_util, silence_action_logger, no_notifications): + def test_api_update_update_commits(self, pr_util, no_notifications): commits = [ {'message': 'a'}, {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]}, @@ -119,20 +118,28 @@ class TestUpdatePullRequest(object): @pytest.mark.backends("git", "hg") def test_api_update_change_reviewers( - self, pr_util, silence_action_logger, no_notifications): + self, user_util, pr_util, no_notifications): + a = user_util.create_user() + b = user_util.create_user() + c = user_util.create_user() + new_reviewers = [ + {'username': b.username,'reasons': ['updated via API'], + 'mandatory':False}, + {'username': c.username, 'reasons': ['updated via API'], + 'mandatory':False}, + ] - users = [x.username for x in User.get_all()] - new = [users.pop(0)] - removed = sorted(new) - added = sorted(users) + added = [b.username, c.username] + removed = [a.username] - pull_request = pr_util.create_pull_request(reviewers=new) + pull_request = pr_util.create_pull_request( + reviewers=[(a.username, ['added via API'], False)]) id_, params = build_data( self.apikey, 'update_pull_request', repoid=pull_request.target_repo.repo_name, pullrequestid=pull_request.pull_request_id, - reviewers=added) + reviewers=new_reviewers) response = api_call(self.app, params) expected = { "msg": "Updated pull request `{}`".format( @@ -152,7 +159,7 @@ class TestUpdatePullRequest(object): self.apikey, 'update_pull_request', repoid=pull_request.target_repo.repo_name, pullrequestid=pull_request.pull_request_id, - reviewers=['bad_name']) + reviewers=[{'username': 'bad_name'}]) response = api_call(self.app, params) expected = 'user `bad_name` does not exist' @@ -165,7 +172,7 @@ class TestUpdatePullRequest(object): self.apikey, 'update_pull_request', repoid='fake', pullrequestid='fake', - reviewers=['bad_name']) + reviewers=[{'username': 'bad_name'}]) response = api_call(self.app, params) expected = 'repository `fake` does not exist' @@ -181,7 +188,7 @@ class TestUpdatePullRequest(object): self.apikey, 'update_pull_request', repoid=pull_request.target_repo.repo_name, pullrequestid=999999, - reviewers=['bad_name']) + reviewers=[{'username': 'bad_name'}]) response = api_call(self.app, params) expected = 'pull request `999999` does not exist' 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 @@ -26,7 +26,7 @@ from rhodecode.tests import TEST_USER_AD from rhodecode.api.tests.utils import ( build_data, api_call, assert_error, assert_ok, crash, jsonify) from rhodecode.tests.fixture import Fixture - +from rhodecode.tests.plugin import http_host_stub, http_host_only_stub fixture = Fixture() @@ -71,14 +71,15 @@ class TestApiUpdateRepo(object): ({'repo_name': 'new_repo_name'}, { 'repo_name': 'new_repo_name', - 'url': 'http://test.example.com:80/new_repo_name' + 'url': 'http://{}/new_repo_name'.format(http_host_only_stub()) }), ({'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME), '_group': 'test_group_for_update'}, { 'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME), - 'url': 'http://test.example.com:80/test_group_for_update/{}'.format(UPDATE_REPO_NAME) + 'url': 'http://{}/test_group_for_update/{}'.format( + http_host_only_stub(), UPDATE_REPO_NAME) }), ]) def test_api_update_repo(self, updates, expected, backend): @@ -115,7 +116,8 @@ class TestApiUpdateRepo(object): master_repo = backend.create_repo() repo = backend.create_repo() updates = { - 'fork_of': master_repo.repo_name + 'fork_of': master_repo.repo_name, + 'fork_of_id': master_repo.repo_id } expected_api_data = repo.get_api_data(include_secrets=True) expected_api_data.update(updates) @@ -130,6 +132,7 @@ class TestApiUpdateRepo(object): assert_ok(id_, expected, given=response.body) result = response.json['result']['repository'] assert result['fork_of'] == master_repo.repo_name + assert result['fork_of_id'] == master_repo.repo_id def test_api_update_repo_fork_of_not_found(self, backend): master_repo_name = 'fake-parent-repo' 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 @@ -21,7 +21,8 @@ import logging -from rhodecode.api import jsonrpc_method, JSONRPCError +from rhodecode import events +from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError from rhodecode.api.utils import ( has_superadmin_permission, Optional, OAttr, get_repo_or_error, get_pull_request_or_error, get_commit_or_error, get_user_or_error, @@ -34,6 +35,9 @@ from rhodecode.model.comment import Comm from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment from rhodecode.model.pull_request import PullRequestModel, MergeCheck from rhodecode.model.settings import SettingsModel +from rhodecode.model.validation_schema import Invalid +from rhodecode.model.validation_schema.schemas.reviewer_schema import( + ReviewerListSchema) log = logging.getLogger(__name__) @@ -224,8 +228,9 @@ def get_pull_requests(request, apiuser, @jsonrpc_method() -def merge_pull_request(request, apiuser, repoid, pullrequestid, - userid=Optional(OAttr('apiuser'))): +def merge_pull_request( + request, apiuser, repoid, pullrequestid, + userid=Optional(OAttr('apiuser'))): """ Merge the pull request specified by `pullrequestid` into its target repository. @@ -273,7 +278,12 @@ def merge_pull_request(request, apiuser, merge_possible = not check.failed if not merge_possible: - reasons = ','.join([msg for _e, msg in check.errors]) + error_messages = [] + for err_type, error_msg in check.errors: + error_msg = request.translate(error_msg) + error_messages.append(error_msg) + + reasons = ','.join(error_messages) raise JSONRPCError( 'merge not possible for following reasons: {}'.format(reasons)) @@ -300,63 +310,6 @@ def merge_pull_request(request, apiuser, @jsonrpc_method() -def close_pull_request(request, apiuser, repoid, pullrequestid, - userid=Optional(OAttr('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": , - "result": { - "pull_request_id": "", - "closed": "" - }, - "error": null - - """ - repo = get_repo_or_error(repoid) - if not isinstance(userid, Optional): - if (has_superadmin_permission(apiuser) or - HasRepoPermissionAnyApi('repository.admin')( - user=apiuser, repo_name=repo.repo_name)): - apiuser = get_user_or_error(userid) - else: - raise JSONRPCError('userid is not the same as your user') - - pull_request = get_pull_request_or_error(pullrequestid) - if not PullRequestModel().check_user_update( - pull_request, apiuser, api=True): - raise JSONRPCError( - 'pull request `%s` close failed, no permission to close.' % ( - pullrequestid,)) - if pull_request.is_closed(): - raise JSONRPCError( - 'pull request `%s` is already closed' % (pullrequestid,)) - - PullRequestModel().close_pull_request( - pull_request.pull_request_id, apiuser) - Session().commit() - data = { - 'pull_request_id': pull_request.pull_request_id, - 'closed': True, - } - return data - - -@jsonrpc_method() def comment_pull_request( request, apiuser, repoid, pullrequestid, message=Optional(None), commit_id=Optional(None), status=Optional(None), @@ -529,24 +482,26 @@ def create_pull_request( :param description: Set the pull request description. :type description: Optional(str) :param reviewers: Set the new pull request reviewers list. + Reviewer defined by review rules will be added automatically to the + defined list. :type reviewers: Optional(list) Accepts username strings or objects of the format: - {'username': 'nick', 'reasons': ['original author']} + [{'username': 'nick', 'reasons': ['original author'], 'mandatory': }] """ - source = get_repo_or_error(source_repo) - target = get_repo_or_error(target_repo) + source_db_repo = get_repo_or_error(source_repo) + target_db_repo = get_repo_or_error(target_repo) if not has_superadmin_permission(apiuser): _perms = ('repository.admin', 'repository.write', 'repository.read',) - validate_repo_permissions(apiuser, source_repo, source, _perms) + validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms) - full_source_ref = resolve_ref_or_error(source_ref, source) - full_target_ref = resolve_ref_or_error(target_ref, target) - source_commit = get_commit_or_error(full_source_ref, source) - target_commit = get_commit_or_error(full_target_ref, target) - source_scm = source.scm_instance() - target_scm = target.scm_instance() + full_source_ref = resolve_ref_or_error(source_ref, source_db_repo) + full_target_ref = resolve_ref_or_error(target_ref, target_db_repo) + source_commit = get_commit_or_error(full_source_ref, source_db_repo) + target_commit = get_commit_or_error(full_target_ref, target_db_repo) + source_scm = source_db_repo.scm_instance() + target_scm = target_db_repo.scm_instance() commit_ranges = target_scm.compare( target_commit.raw_id, source_commit.raw_id, source_scm, @@ -562,20 +517,36 @@ def create_pull_request( raise JSONRPCError('no common ancestor found') reviewer_objects = Optional.extract(reviewers) or [] - if not isinstance(reviewer_objects, list): - raise JSONRPCError('reviewers should be specified as a list') + + if reviewer_objects: + schema = ReviewerListSchema() + try: + reviewer_objects = schema.deserialize(reviewer_objects) + except Invalid as err: + raise JSONRPCValidationError(colander_exc=err) + + # validate users + for reviewer_object in reviewer_objects: + user = get_user_or_error(reviewer_object['username']) + reviewer_object['user_id'] = user.user_id - reviewers_reasons = [] - for reviewer_object in reviewer_objects: - reviewer_reasons = [] - if isinstance(reviewer_object, (basestring, int)): - reviewer_username = reviewer_object - else: - reviewer_username = reviewer_object['username'] - reviewer_reasons = reviewer_object.get('reasons', []) + get_default_reviewers_data, get_validated_reviewers = \ + PullRequestModel().get_reviewer_functions() + + reviewer_rules = get_default_reviewers_data( + apiuser.get_instance(), source_db_repo, + source_commit, target_db_repo, target_commit) - user = get_user_or_error(reviewer_username) - reviewers_reasons.append((user.user_id, reviewer_reasons)) + # specified rules are later re-validated, thus we can assume users will + # eventually provide those that meet the reviewer criteria. + if not reviewer_objects: + reviewer_objects = reviewer_rules['reviewers'] + + try: + reviewers = get_validated_reviewers( + reviewer_objects, reviewer_rules) + except ValueError as e: + raise JSONRPCError('Reviewers Validation: {}'.format(e)) pull_request_model = PullRequestModel() pull_request = pull_request_model.create( @@ -586,7 +557,7 @@ def create_pull_request( target_ref=full_target_ref, revisions=reversed( [commit.raw_id for commit in reversed(commit_ranges)]), - reviewers=reviewers_reasons, + reviewers=reviewers, title=title, description=Optional.extract(description) ) @@ -603,7 +574,7 @@ def create_pull_request( def update_pull_request( request, apiuser, repoid, pullrequestid, title=Optional(''), description=Optional(''), reviewers=Optional(None), - update_commits=Optional(None), close_pull_request=Optional(None)): + update_commits=Optional(None)): """ Updates a pull request. @@ -619,10 +590,12 @@ def update_pull_request( :type description: Optional(str) :param reviewers: Update pull request reviewers list with new value. :type reviewers: Optional(list) + Accepts username strings or objects of the format: + + [{'username': 'nick', 'reasons': ['original author'], 'mandatory': }] + :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: @@ -665,29 +638,38 @@ def update_pull_request( pullrequestid,)) reviewer_objects = Optional.extract(reviewers) or [] - if not isinstance(reviewer_objects, list): - raise JSONRPCError('reviewers should be specified as a list') + + if reviewer_objects: + schema = ReviewerListSchema() + try: + reviewer_objects = schema.deserialize(reviewer_objects) + except Invalid as err: + raise JSONRPCValidationError(colander_exc=err) + + # validate users + for reviewer_object in reviewer_objects: + user = get_user_or_error(reviewer_object['username']) + reviewer_object['user_id'] = user.user_id - reviewers_reasons = [] - reviewer_ids = set() - for reviewer_object in reviewer_objects: - reviewer_reasons = [] - if isinstance(reviewer_object, (int, basestring)): - reviewer_username = reviewer_object - else: - reviewer_username = reviewer_object['username'] - reviewer_reasons = reviewer_object.get('reasons', []) + get_default_reviewers_data, get_validated_reviewers = \ + PullRequestModel().get_reviewer_functions() - user = get_user_or_error(reviewer_username) - reviewer_ids.add(user.user_id) - reviewers_reasons.append((user.user_id, reviewer_reasons)) + # re-use stored rules + reviewer_rules = pull_request.reviewer_data + try: + reviewers = get_validated_reviewers( + reviewer_objects, reviewer_rules) + except ValueError as e: + raise JSONRPCError('Reviewers Validation: {}'.format(e)) + else: + reviewers = [] title = Optional.extract(title) description = Optional.extract(description) if title or description: PullRequestModel().edit( pull_request, title or pull_request.title, - description or pull_request.description) + description or pull_request.description, apiuser) Session().commit() commit_changes = {"added": [], "common": [], "removed": []} @@ -699,9 +681,9 @@ def update_pull_request( Session().commit() reviewers_changes = {"added": [], "removed": []} - if reviewer_ids: + if reviewers: added_reviewers, removed_reviewers = \ - PullRequestModel().update_reviewers(pull_request, reviewers_reasons) + PullRequestModel().update_reviewers(pull_request, reviewers, apiuser) reviewers_changes['added'] = sorted( [get_user_or_error(n).username for n in added_reviewers]) @@ -709,11 +691,6 @@ def update_pull_request( [get_user_or_error(n).username for n in removed_reviewers]) Session().commit() - if str2bool(Optional.extract(close_pull_request)): - PullRequestModel().close_pull_request_with_comment( - pull_request, apiuser, repo) - Session().commit() - data = { 'msg': 'Updated pull request `{}`'.format( pull_request.pull_request_id), @@ -723,3 +700,80 @@ def update_pull_request( } return data + + +@jsonrpc_method() +def close_pull_request( + request, apiuser, repoid, pullrequestid, + userid=Optional(OAttr('apiuser')), message=Optional('')): + """ + 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) + :param message: Optional message to close the Pull Request with. If not + specified it will be generated automatically. + :type message: Optional(str) + + Example output: + + .. code-block:: bash + + "id": , + "result": { + "pull_request_id": "", + "close_status": ", + "closed": "" + }, + "error": null + + """ + _ = request.translate + + repo = get_repo_or_error(repoid) + if not isinstance(userid, Optional): + if (has_superadmin_permission(apiuser) or + HasRepoPermissionAnyApi('repository.admin')( + user=apiuser, repo_name=repo.repo_name)): + apiuser = get_user_or_error(userid) + else: + raise JSONRPCError('userid is not the same as your user') + + pull_request = get_pull_request_or_error(pullrequestid) + + if pull_request.is_closed(): + raise JSONRPCError( + 'pull request `%s` is already closed' % (pullrequestid,)) + + # only owner or admin or person with write permissions + allowed_to_close = PullRequestModel().check_user_update( + pull_request, apiuser, api=True) + + if not allowed_to_close: + raise JSONRPCError( + 'pull request `%s` close failed, no permission to close.' % ( + pullrequestid,)) + + # message we're using to close the PR, else it's automatically generated + message = Optional.extract(message) + + # finally close the PR, with proper message comment + comment, status = PullRequestModel().close_pull_request_with_comment( + pull_request, apiuser, repo, message=message) + status_lbl = ChangesetStatus.get_status_lbl(status) + + Session().commit() + + data = { + 'pull_request_id': pull_request.pull_request_id, + 'close_status': status_lbl, + 'closed': True, + } + return data 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 @@ -29,6 +29,8 @@ from rhodecode.api.utils import ( get_user_group_or_error, get_user_or_error, validate_repo_permissions, get_perm_or_error, parse_args, get_origin, build_commit_data, validate_set_owner_permissions) +from rhodecode.lib import audit_logger +from rhodecode.lib import repo_maintenance from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi from rhodecode.lib.utils2 import str2bool, time_to_datetime from rhodecode.lib.ext_json import json @@ -915,12 +917,13 @@ def update_repo( ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo) + old_values = repo.get_api_data() schema = repo_schema.RepoSchema().bind( repo_type_options=rhodecode.BACKENDS.keys(), repo_ref_options=ref_choices, # user caller user=apiuser, - old_values=repo.get_api_data()) + old_values=old_values) try: schema_data = schema.deserialize(dict( # we save old value, users cannot change type @@ -965,6 +968,9 @@ def update_repo( try: RepoModel().update(repo, **validated_updates) + audit_logger.store_api( + 'repo.edit', action_data={'old_data': old_values}, + user=apiuser, repo=repo) Session().commit() return { 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name), @@ -1153,6 +1159,7 @@ def delete_repo(request, apiuser, repoid """ repo = get_repo_or_error(repoid) + repo_name = repo.repo_name if not has_superadmin_permission(apiuser): _perms = ('repository.admin',) validate_repo_permissions(apiuser, repoid, repo, _perms) @@ -1170,18 +1177,26 @@ def delete_repo(request, apiuser, repoid 'Cannot delete `%s` it still contains attached forks' % (repo.repo_name,) ) + old_data = repo.get_api_data() + RepoModel().delete(repo, forks=forks) - RepoModel().delete(repo, forks=forks) + repo = audit_logger.RepoWrap(repo_id=None, + repo_name=repo.repo_name) + + audit_logger.store_api( + 'repo.delete', action_data={'old_data': old_data}, + user=apiuser, repo=repo) + + ScmModel().mark_for_invalidation(repo_name, delete=True) Session().commit() return { - 'msg': 'Deleted repository `%s`%s' % ( - repo.repo_name, _forks_msg), + 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg), 'success': True } except Exception: log.exception("Exception occurred while trying to delete repo") raise JSONRPCError( - 'failed to delete repository `%s`' % (repo.repo_name,) + 'failed to delete repository `%s`' % (repo_name,) ) @@ -1460,7 +1475,7 @@ def comment_commit( rc_config = SettingsModel().get_all_settings() renderer = rc_config.get('rhodecode_markup_renderer', 'rst') status_change_label = ChangesetStatus.get_status_lbl(status) - comm = CommentsModel().create( + comment = CommentsModel().create( message, repo, user, commit_id=commit_id, status_change=status_change_label, status_change_type=status, @@ -1472,7 +1487,7 @@ def comment_commit( # also do a status change try: ChangesetStatusModel().set_status( - repo, status, user, comm, revision=commit_id, + repo, status, user, comment, revision=commit_id, dont_allow_on_closed_pull_request=True ) except StatusChangeOnClosedPullRequestError: @@ -1486,7 +1501,7 @@ def comment_commit( return { 'msg': ( 'Commented on commit `%s` for repository `%s`' % ( - comm.revision, repo.repo_name)), + comment.revision, repo.repo_name)), 'status_change': status, 'success': True, } @@ -1867,6 +1882,11 @@ def strip(request, apiuser, repoid, revi try: ScmModel().strip(repo, revision, branch) + audit_logger.store_api( + 'repo.commit.strip', action_data={'commit_id': revision}, + repo=repo, + user=apiuser, commit=True) + return { 'msg': 'Stripped commit %s from repo `%s`' % ( revision, repo.repo_name), @@ -1902,6 +1922,7 @@ def get_repo_settings(request, apiuser, "id": 237, "result": { "extensions_largefiles": true, + "extensions_evolve": true, "hooks_changegroup_push_logger": true, "hooks_changegroup_repo_size": false, "hooks_outgoing_pull_logger": true, @@ -1985,3 +2006,65 @@ def set_repo_settings(request, apiuser, # Indicate success. return True + + +@jsonrpc_method() +def maintenance(request, apiuser, repoid): + """ + Triggers a maintenance on the given repository. + + 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": "executed maintenance command", + "executed_actions": [ + , ... + ], + "repository": "" + } + error : null + + Example error output: + + .. code-block:: bash + + id : + result : null + error : { + "Unable to execute maintenance on ``" + } + + """ + + repo = get_repo_or_error(repoid) + if not has_superadmin_permission(apiuser): + _perms = ('repository.admin',) + validate_repo_permissions(apiuser, repoid, repo, _perms) + + try: + maintenance = repo_maintenance.RepoMaintenance() + executed_actions = maintenance.execute(repo) + + return { + 'msg': 'executed maintenance command', + 'executed_actions': executed_actions, + 'repository': repo.repo_name + } + except Exception: + log.exception("Exception occurred while trying to run maintenance") + raise JSONRPCError( + 'Unable to execute maintenance on `%s`' % repo.repo_name) 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 @@ -27,6 +27,7 @@ from rhodecode.api.utils import ( has_superadmin_permission, Optional, OAttr, get_user_or_error, get_repo_group_or_error, get_perm_or_error, get_user_group_or_error, get_origin, validate_repo_group_permissions, validate_set_owner_permissions) +from rhodecode.lib import audit_logger from rhodecode.lib.auth import ( HasRepoGroupPermissionAnyApi, HasUserGroupPermissionAnyApi) from rhodecode.model.db import Session @@ -222,6 +223,13 @@ def create_repo_group( group_name=validated_group_name, group_description=schema_data['repo_group_name'], copy_permissions=schema_data['repo_group_copy_permissions']) + Session().flush() + + repo_group_data = repo_group.get_api_data() + audit_logger.store_api( + 'repo_group.create', action_data={'data': repo_group_data}, + user=apiuser) + Session().commit() return { 'msg': 'Created new repo group `%s`' % validated_group_name, @@ -310,8 +318,13 @@ def update_repo_group( enable_locking=schema_data['repo_group_enable_locking'], ) + old_data = repo_group.get_api_data() try: RepoGroupModel().update(repo_group, validated_updates) + audit_logger.store_api( + 'repo_group.edit', action_data={'old_data': old_data}, + user=apiuser) + Session().commit() return { 'msg': 'updated repository group ID:%s %s' % ( @@ -365,8 +378,12 @@ def delete_repo_group(request, apiuser, validate_repo_group_permissions( apiuser, repogroupid, repo_group, ('group.admin',)) + old_data = repo_group.get_api_data() try: RepoGroupModel().delete(repo_group) + audit_logger.store_api( + 'repo_group.delete', action_data={'old_data': old_data}, + user=apiuser) Session().commit() return { 'msg': 'deleted repo group ID:%s %s' % diff --git a/rhodecode/api/views/user_api.py b/rhodecode/api/views/user_api.py --- a/rhodecode/api/views/user_api.py +++ b/rhodecode/api/views/user_api.py @@ -20,14 +20,18 @@ import logging -from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden +from rhodecode.api import ( + jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError) from rhodecode.api.utils import ( Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update) +from rhodecode.lib import audit_logger from rhodecode.lib.auth import AuthUser, PasswordGenerator from rhodecode.lib.exceptions import DefaultUserException from rhodecode.lib.utils2 import safe_int, str2bool from rhodecode.model.db import Session, User, Repository from rhodecode.model.user import UserModel +from rhodecode.model import validation_schema +from rhodecode.model.validation_schema.schemas import user_schema log = logging.getLogger(__name__) @@ -237,20 +241,54 @@ def create_user(request, apiuser, userna if isinstance(create_repo_group, basestring): create_repo_group = str2bool(create_repo_group) + username = Optional.extract(username) + password = Optional.extract(password) + email = Optional.extract(email) + first_name = Optional.extract(firstname) + last_name = Optional.extract(lastname) + active = Optional.extract(active) + admin = Optional.extract(admin) + extern_type = Optional.extract(extern_type) + extern_name = Optional.extract(extern_name) + + schema = user_schema.UserSchema().bind( + # user caller + user=apiuser) + try: + schema_data = schema.deserialize(dict( + username=username, + email=email, + password=password, + first_name=first_name, + last_name=last_name, + active=active, + admin=admin, + extern_type=extern_type, + extern_name=extern_name, + )) + except validation_schema.Invalid as err: + raise JSONRPCValidationError(colander_exc=err) + try: user = UserModel().create_or_update( - username=Optional.extract(username), - password=Optional.extract(password), - email=Optional.extract(email), - firstname=Optional.extract(firstname), - lastname=Optional.extract(lastname), - active=Optional.extract(active), - admin=Optional.extract(admin), - extern_type=Optional.extract(extern_type), - extern_name=Optional.extract(extern_name), + username=schema_data['username'], + password=schema_data['password'], + email=schema_data['email'], + firstname=schema_data['first_name'], + lastname=schema_data['last_name'], + active=schema_data['active'], + admin=schema_data['admin'], + extern_type=schema_data['extern_type'], + extern_name=schema_data['extern_name'], force_password_change=Optional.extract(force_password_change), create_repo_group=create_repo_group ) + Session().flush() + creation_data = user.get_api_data() + audit_logger.store_api( + 'user.create', action_data={'data': creation_data}, + user=apiuser) + Session().commit() return { 'msg': 'created new user `%s`' % username, @@ -326,7 +364,7 @@ def update_user(request, apiuser, userid raise JSONRPCForbidden() user = get_user_or_error(userid) - + old_data = user.get_api_data() # only non optional arguments will be stored in updates updates = {} @@ -343,6 +381,9 @@ def update_user(request, apiuser, userid store_update(updates, extern_type, 'extern_type') user = UserModel().update_user(user, **updates) + audit_logger.store_api( + 'user.edit', action_data={'old_data': old_data}, + user=apiuser) Session().commit() return { 'msg': 'updated user ID:%s %s' % (user.user_id, user.username), @@ -405,9 +446,13 @@ def delete_user(request, apiuser, userid raise JSONRPCForbidden() user = get_user_or_error(userid) - + old_data = user.get_api_data() try: UserModel().delete(userid) + audit_logger.store_api( + 'user.delete', action_data={'old_data': old_data}, + user=apiuser) + Session().commit() return { 'msg': 'deleted user ID:%s %s' % (user.user_id, user.username), diff --git a/rhodecode/api/views/user_group_api.py b/rhodecode/api/views/user_group_api.py --- a/rhodecode/api/views/user_group_api.py +++ b/rhodecode/api/views/user_group_api.py @@ -20,15 +20,19 @@ import logging -from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden +from rhodecode.api import ( + jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError) from rhodecode.api.utils import ( Optional, OAttr, store_update, has_superadmin_permission, get_origin, get_user_or_error, get_user_group_or_error, get_perm_or_error) +from rhodecode.lib import audit_logger from rhodecode.lib.auth import HasUserGroupPermissionAnyApi, HasPermissionAnyApi from rhodecode.lib.exceptions import UserGroupAssignedException from rhodecode.model.db import Session from rhodecode.model.scm import UserGroupList from rhodecode.model.user_group import UserGroupModel +from rhodecode.model import validation_schema +from rhodecode.model.validation_schema.schemas import user_group_schema log = logging.getLogger(__name__) @@ -210,20 +214,41 @@ def create_user_group( if UserGroupModel().get_by_name(group_name): raise JSONRPCError("user group `%s` already exist" % (group_name,)) - try: - if isinstance(owner, Optional): - owner = apiuser.user_id + if isinstance(owner, Optional): + owner = apiuser.user_id + + owner = get_user_or_error(owner) + active = Optional.extract(active) + description = Optional.extract(description) - owner = get_user_or_error(owner) - active = Optional.extract(active) - description = Optional.extract(description) - ug = UserGroupModel().create( - name=group_name, description=description, owner=owner, - active=active) + schema = user_group_schema.UserGroupSchema().bind( + # user caller + user=apiuser) + try: + schema_data = schema.deserialize(dict( + user_group_name=group_name, + user_group_description=description, + user_group_owner=owner.username, + user_group_active=active, + )) + except validation_schema.Invalid as err: + raise JSONRPCValidationError(colander_exc=err) + + try: + user_group = UserGroupModel().create( + name=schema_data['user_group_name'], + description=schema_data['user_group_description'], + owner=owner, + active=schema_data['user_group_active']) + Session().flush() + creation_data = user_group.get_api_data() + audit_logger.store_api( + 'user_group.create', action_data={'data': creation_data}, + user=apiuser) Session().commit() return { 'msg': 'created new user group `%s`' % group_name, - 'user_group': ug.get_api_data() + 'user_group': creation_data } except Exception: log.exception("Error occurred during creation of user group") @@ -291,6 +316,7 @@ def update_user_group(request, apiuser, if not isinstance(owner, Optional): owner = get_user_or_error(owner) + old_data = user_group.get_api_data() updates = {} store_update(updates, group_name, 'users_group_name') store_update(updates, description, 'user_group_description') @@ -298,6 +324,9 @@ def update_user_group(request, apiuser, store_update(updates, active, 'users_group_active') try: UserGroupModel().update(user_group, updates) + audit_logger.store_api( + 'user_group.edit', action_data={'old_data': old_data}, + user=apiuser) Session().commit() return { 'msg': 'updated user group ID:%s %s' % ( @@ -359,8 +388,12 @@ def delete_user_group(request, apiuser, raise JSONRPCError( 'user group `%s` does not exist' % (usergroupid,)) + old_data = user_group.get_api_data() try: UserGroupModel().delete(user_group) + audit_logger.store_api( + 'user_group.delete', action_data={'old_data': old_data}, + user=apiuser) Session().commit() return { 'msg': 'deleted user group ID:%s %s' % ( @@ -438,6 +471,12 @@ def add_user_to_user_group(request, apiu user.username, user_group.users_group_name ) msg = msg if success else 'User is already in that group' + if success: + user_data = user.get_api_data() + audit_logger.store_api( + 'user_group.edit.member.add', action_data={'user': user_data}, + user=apiuser) + Session().commit() return { @@ -501,6 +540,12 @@ def remove_user_from_user_group(request, user.username, user_group.users_group_name ) msg = msg if success else "User wasn't in group" + if success: + user_data = user.get_api_data() + audit_logger.store_api( + 'user_group.edit.member.delete', action_data={'user': user_data}, + user=apiuser) + Session().commit() return {'success': success, 'msg': msg} except Exception: diff --git a/rhodecode/apps/__init__.py b/rhodecode/apps/__init__.py --- a/rhodecode/apps/__init__.py +++ b/rhodecode/apps/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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/ diff --git a/rhodecode/apps/_base/__init__.py b/rhodecode/apps/_base/__init__.py --- a/rhodecode/apps/_base/__init__.py +++ b/rhodecode/apps/_base/__init__.py @@ -24,8 +24,12 @@ from pylons import tmpl_context as c from pyramid.httpexceptions import HTTPFound from rhodecode.lib import helpers as h -from rhodecode.lib.utils2 import StrictAttributeDict, safe_int +from rhodecode.lib.utils import PartialRenderer +from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time +from rhodecode.lib.vcs.exceptions import RepositoryRequirementError +from rhodecode.lib.ext_json import json from rhodecode.model import repo +from rhodecode.model import repo_group from rhodecode.model.db import User from rhodecode.model.scm import ScmModel @@ -36,6 +40,30 @@ ADMIN_PREFIX = '/_admin' STATIC_FILE_PREFIX = '/_static' +def add_route_with_slash(config,name, pattern, **kw): + config.add_route(name, pattern, **kw) + if not pattern.endswith('/'): + config.add_route(name + '_slash', pattern + '/', **kw) + + +def get_format_ref_id(repo): + """Returns a `repo` specific reference formatter function""" + if h.is_svn(repo): + return _format_ref_id_svn + else: + return _format_ref_id + + +def _format_ref_id(name, raw_id): + """Default formatting of a given reference `name`""" + return name + + +def _format_ref_id_svn(name, raw_id): + """Special way of formatting a reference for Subversion including path""" + return '%s@%s' % (name, raw_id) + + class TemplateArgs(StrictAttributeDict): pass @@ -77,9 +105,14 @@ class BaseAppView(object): raise HTTPFound( self.request.route_path('my_account_password')) - def _get_local_tmpl_context(self): + def _get_local_tmpl_context(self, include_app_defaults=False): c = TemplateArgs() c.auth_user = self.request.user + if include_app_defaults: + # NOTE(marcink): after full pyramid migration include_app_defaults + # should be turned on by default + from rhodecode.lib.base import attach_context_attributes + attach_context_attributes(c, self.request, self.request.user.user_id) return c def _register_global_c(self, tmpl_args): @@ -121,15 +154,106 @@ class RepoAppView(BaseAppView): self.db_repo_name = self.db_repo.repo_name self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo) - def _get_local_tmpl_context(self): - c = super(RepoAppView, self)._get_local_tmpl_context() + def _handle_missing_requirements(self, error): + log.error( + 'Requirements are missing for repository %s: %s', + self.db_repo_name, error.message) + + def _get_local_tmpl_context(self, include_app_defaults=False): + c = super(RepoAppView, self)._get_local_tmpl_context( + include_app_defaults=include_app_defaults) + # register common vars for this type of view c.rhodecode_db_repo = self.db_repo c.repo_name = self.db_repo_name c.repository_pull_requests = self.db_repo_pull_requests + + c.repository_requirements_missing = False + try: + self.rhodecode_vcs_repo = self.db_repo.scm_instance() + except RepositoryRequirementError as e: + c.repository_requirements_missing = True + self._handle_missing_requirements(e) + return c +class DataGridAppView(object): + """ + Common class to have re-usable grid rendering components + """ + + def _extract_ordering(self, request, column_map=None): + column_map = column_map or {} + column_index = safe_int(request.GET.get('order[0][column]')) + order_dir = request.GET.get( + 'order[0][dir]', 'desc') + order_by = request.GET.get( + 'columns[%s][data][sort]' % column_index, 'name_raw') + + # translate datatable to DB columns + order_by = column_map.get(order_by) or order_by + + search_q = request.GET.get('search[value]') + return search_q, order_by, order_dir + + def _extract_chunk(self, request): + start = safe_int(request.GET.get('start'), 0) + length = safe_int(request.GET.get('length'), 25) + draw = safe_int(request.GET.get('draw')) + return draw, start, length + + +class BaseReferencesView(RepoAppView): + """ + Base for reference view for branches, tags and bookmarks. + """ + def load_default_context(self): + c = self._get_local_tmpl_context() + + # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead + c.repo_info = self.db_repo + + self._register_global_c(c) + return c + + def load_refs_context(self, ref_items, partials_template): + _render = PartialRenderer(partials_template) + _data = [] + pre_load = ["author", "date", "message"] + + is_svn = h.is_svn(self.rhodecode_vcs_repo) + format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo) + + for ref_name, commit_id in ref_items: + commit = self.rhodecode_vcs_repo.get_commit( + commit_id=commit_id, pre_load=pre_load) + + # TODO: johbo: Unify generation of reference links + use_commit_id = '/' in ref_name or is_svn + files_url = h.url( + 'files_home', + repo_name=c.repo_name, + f_path=ref_name if is_svn else '', + revision=commit_id if use_commit_id else ref_name, + at=ref_name) + + _data.append({ + "name": _render('name', ref_name, files_url), + "name_raw": ref_name, + "date": _render('date', commit.date), + "date_raw": datetime_to_time(commit.date), + "author": _render('author', commit.author), + "commit": _render( + 'commit', commit.message, commit.raw_id, commit.idx), + "commit_raw": commit.idx, + "compare": _render( + 'compare', format_ref_id(ref_name, commit.raw_id)), + }) + c.has_references = bool(_data) + c.data = json.dumps(_data) + + class RepoRoutePredicate(object): def __init__(self, val, config): self.val = val @@ -140,11 +264,15 @@ class RepoRoutePredicate(object): phash = text def __call__(self, info, request): + + if hasattr(request, 'vcs_call'): + # skip vcs calls + return + repo_name = info['match']['repo_name'] repo_model = repo.RepoModel() by_name_match = repo_model.get_by_repo_name(repo_name, cache=True) - # if we match quickly from database, short circuit the operation, - # and validate repo based on the type. + if by_name_match: # register this as request object we can re-use later request.db_repo = by_name_match @@ -158,6 +286,72 @@ class RepoRoutePredicate(object): return False +class RepoTypeRoutePredicate(object): + def __init__(self, val, config): + self.val = val or ['hg', 'git', 'svn'] + + def text(self): + return 'repo_accepted_type = %s' % self.val + + phash = text + + def __call__(self, info, request): + if hasattr(request, 'vcs_call'): + # skip vcs calls + return + + rhodecode_db_repo = request.db_repo + + log.debug( + '%s checking repo type for %s in %s', + self.__class__.__name__, rhodecode_db_repo.repo_type, self.val) + + if rhodecode_db_repo.repo_type in self.val: + return True + else: + log.warning('Current view is not supported for repo type:%s', + rhodecode_db_repo.repo_type) + # + # h.flash(h.literal( + # _('Action not supported for %s.' % rhodecode_repo.alias)), + # category='warning') + # return redirect( + # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name)) + + return False + + +class RepoGroupRoutePredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'repo_group_route = %s' % self.val + + phash = text + + def __call__(self, info, request): + if hasattr(request, 'vcs_call'): + # skip vcs calls + return + + repo_group_name = info['match']['repo_group_name'] + repo_group_model = repo_group.RepoGroupModel() + by_name_match = repo_group_model.get_by_group_name( + repo_group_name, cache=True) + + if by_name_match: + # register this as request object we can re-use later + request.db_repo_group = by_name_match + return True + + return False + + def includeme(config): config.add_route_predicate( 'repo_route', RepoRoutePredicate) + config.add_route_predicate( + 'repo_accepted_types', RepoTypeRoutePredicate) + config.add_route_predicate( + 'repo_group_route', RepoGroupRoutePredicate) diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py --- a/rhodecode/apps/admin/__init__.py +++ b/rhodecode/apps/admin/__init__.py @@ -30,6 +30,20 @@ def admin_routes(config): """ config.add_route( + name='admin_audit_logs', + pattern='/audit_logs') + + config.add_route( + name='pull_requests_global_0', # backward compat + pattern='/pull_requests/{pull_request_id:[0-9]+}') + config.add_route( + name='pull_requests_global_1', # backward compat + pattern='/pull-requests/{pull_request_id:[0-9]+}') + config.add_route( + name='pull_requests_global', + pattern='/pull-request/{pull_request_id:[0-9]+}') + + config.add_route( name='admin_settings_open_source', pattern='/settings/open_source') config.add_route( @@ -50,6 +64,11 @@ def admin_routes(config): name='admin_settings_sessions_cleanup', pattern='/settings/sessions/cleanup') + # global permissions + config.add_route( + name='admin_permissions_ips', + pattern='/permissions/ips') + # users admin config.add_route( name='users', @@ -70,6 +89,28 @@ def admin_routes(config): name='edit_user_auth_tokens_delete', pattern='/users/{user_id:\d+}/edit/auth_tokens/delete') + # user emails + config.add_route( + name='edit_user_emails', + pattern='/users/{user_id:\d+}/edit/emails') + config.add_route( + name='edit_user_emails_add', + pattern='/users/{user_id:\d+}/edit/emails/new') + config.add_route( + name='edit_user_emails_delete', + pattern='/users/{user_id:\d+}/edit/emails/delete') + + # user IPs + config.add_route( + name='edit_user_ips', + pattern='/users/{user_id:\d+}/edit/ips') + config.add_route( + name='edit_user_ips_add', + pattern='/users/{user_id:\d+}/edit/ips/new') + config.add_route( + name='edit_user_ips_delete', + pattern='/users/{user_id:\d+}/edit/ips/delete') + # user groups management config.add_route( name='edit_user_groups_management', @@ -93,6 +134,8 @@ def includeme(config): navigation_registry = NavigationRegistry(labs_active=labs_active) config.registry.registerUtility(navigation_registry) + # main admin routes + config.add_route(name='admin_home', pattern=ADMIN_PREFIX) config.include(admin_routes, route_prefix=ADMIN_PREFIX) # Scan module for configuration decorators. diff --git a/rhodecode/apps/admin/tests/test_admin_audit_logs.py b/rhodecode/apps/admin/tests/test_admin_audit_logs.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/tests/test_admin_audit_logs.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 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 csv +import datetime + +import pytest + +from rhodecode.tests import * +from rhodecode.tests.fixture import FIXTURES +from rhodecode.model.db import UserLog +from rhodecode.model.meta import Session +from rhodecode.lib.utils2 import safe_unicode + + +def route_path(name, params=None, **kwargs): + import urllib + from rhodecode.apps._base import ADMIN_PREFIX + + base_url = { + 'admin_home': ADMIN_PREFIX, + 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs', + + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +class TestAdminController(TestController): + + @pytest.fixture(scope='class', autouse=True) + def prepare(self, request, pylonsapp): + UserLog.query().delete() + Session().commit() + + def strptime(val): + fmt = '%Y-%m-%d %H:%M:%S' + if '.' not in val: + return datetime.datetime.strptime(val, fmt) + + nofrag, frag = val.split(".") + date = datetime.datetime.strptime(nofrag, fmt) + + frag = frag[:6] # truncate to microseconds + frag += (6 - len(frag)) * '0' # add 0s + return date.replace(microsecond=int(frag)) + + with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f: + for row in csv.DictReader(f): + ul = UserLog() + for k, v in row.iteritems(): + v = safe_unicode(v) + if k == 'action_date': + v = strptime(v) + if k in ['user_id', 'repository_id']: + # nullable due to FK problems + v = None + setattr(ul, k, v) + Session().add(ul) + Session().commit() + + @request.addfinalizer + def cleanup(): + UserLog.query().delete() + Session().commit() + + def test_index(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs')) + response.mustcontain('Admin audit logs') + + def test_filter_all_entries(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs')) + all_count = UserLog.query().count() + response.mustcontain('%s entries' % all_count) + + def test_filter_journal_filter_exact_match_on_repository(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='repository:rhodecode'))) + response.mustcontain('3 entries') + + def test_filter_journal_filter_exact_match_on_repository_CamelCase(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='repository:RhodeCode'))) + response.mustcontain('3 entries') + + def test_filter_journal_filter_wildcard_on_repository(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='repository:*test*'))) + response.mustcontain('862 entries') + + def test_filter_journal_filter_prefix_on_repository(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='repository:test*'))) + response.mustcontain('257 entries') + + def test_filter_journal_filter_prefix_on_repository_CamelCase(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='repository:Test*'))) + response.mustcontain('257 entries') + + def test_filter_journal_filter_prefix_on_repository_and_user(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='repository:test* AND username:demo'))) + response.mustcontain('130 entries') + + def test_filter_journal_filter_prefix_on_repository_or_target_repo(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='repository:test* OR repository:rhodecode'))) + response.mustcontain('260 entries') # 257 + 3 + + def test_filter_journal_filter_exact_match_on_username(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='username:demo'))) + response.mustcontain('1087 entries') + + def test_filter_journal_filter_exact_match_on_username_camelCase(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='username:DemO'))) + response.mustcontain('1087 entries') + + def test_filter_journal_filter_wildcard_on_username(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='username:*test*'))) + entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count() + response.mustcontain('{} entries'.format(entries_count)) + + def test_filter_journal_filter_prefix_on_username(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='username:demo*'))) + response.mustcontain('1101 entries') + + def test_filter_journal_filter_prefix_on_user_or_other_user(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='username:demo OR username:volcan'))) + response.mustcontain('1095 entries') # 1087 + 8 + + def test_filter_journal_filter_wildcard_on_action(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='action:*pull_request*'))) + response.mustcontain('187 entries') + + def test_filter_journal_filter_on_date(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='date:20121010'))) + response.mustcontain('47 entries') + + def test_filter_journal_filter_on_date_2(self): + self.log_user() + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='date:20121020'))) + response.mustcontain('17 entries') diff --git a/rhodecode/apps/admin/tests/test_admin_main_views.py b/rhodecode/apps/admin/tests/test_admin_main_views.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/tests/test_admin_main_views.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 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 pytest + +from rhodecode.tests import TestController +from rhodecode.tests.fixture import Fixture + +fixture = Fixture() + + +def route_path(name, params=None, **kwargs): + import urllib + from rhodecode.apps._base import ADMIN_PREFIX + + base_url = { + 'admin_home': ADMIN_PREFIX, + 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}', + 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}', + 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}', + 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}', + + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +class TestAdminMainView(TestController): + + def test_redirect_admin_home(self): + self.log_user() + response = self.app.get(route_path('admin_home'), status=302) + assert response.location.endswith('/audit_logs') + + def test_redirect_pull_request_view(self, view): + self.log_user() + self.app.get( + route_path(view, pull_request_id='xxxx'), + status=404) + + @pytest.mark.backends("git", "hg") + @pytest.mark.parametrize('view', [ + 'pull_requests_global', + 'pull_requests_global_0', + 'pull_requests_global_1', + ]) + def test_redirect_pull_request_view(self, view, pr_util): + self.log_user() + pull_request = pr_util.create_pull_request() + pull_request_id = pull_request.pull_request_id + + response = self.app.get( + route_path(view, pull_request_id=pull_request_id), + status=302) + assert response.location.endswith( + 'pull-request/{}'.format(pull_request_id)) + + repo_name = pull_request.target_repo.repo_name + redirect_url = route_path( + 'pullrequest_show', repo_name=repo_name, + pull_request_id=pull_request.pull_request_id) + + assert redirect_url in response.location diff --git a/rhodecode/apps/admin/tests/test_admin_users.py b/rhodecode/apps/admin/tests/test_admin_users.py --- a/rhodecode/apps/admin/tests/test_admin_users.py +++ b/rhodecode/apps/admin/tests/test_admin_users.py @@ -20,7 +20,9 @@ import pytest -from rhodecode.model.db import User, UserApiKeys +from rhodecode.model.db import User, UserApiKeys, UserEmailMap +from rhodecode.model.meta import Session +from rhodecode.model.user import UserModel from rhodecode.tests import ( TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash) @@ -44,6 +46,20 @@ def route_path(name, params=None, **kwar ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new', 'edit_user_auth_tokens_delete': ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete', + + 'edit_user_emails': + ADMIN_PREFIX + '/users/{user_id}/edit/emails', + 'edit_user_emails_add': + ADMIN_PREFIX + '/users/{user_id}/edit/emails/new', + 'edit_user_emails_delete': + ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete', + + 'edit_user_ips': + ADMIN_PREFIX + '/users/{user_id}/edit/ips', + 'edit_user_ips_add': + ADMIN_PREFIX + '/users/{user_id}/edit/ips/new', + 'edit_user_ips_delete': + ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete', }[name].format(**kwargs) if params: @@ -135,8 +151,131 @@ class TestAdminUsersView(TestController) response = self.app.post( route_path('edit_user_auth_tokens_delete', user_id=user_id), - {'del_auth_token': keys[0].api_key, 'csrf_token': self.csrf_token}) + {'del_auth_token': keys[0].user_api_key_id, + 'csrf_token': self.csrf_token}) assert_session_flash(response, 'Auth token successfully deleted') keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all() assert 2 == len(keys) + + def test_ips(self): + self.log_user() + user = User.get_by_username(TEST_USER_REGULAR_LOGIN) + response = self.app.get(route_path('edit_user_ips', user_id=user.user_id)) + response.mustcontain('All IP addresses are allowed') + + @pytest.mark.parametrize("test_name, ip, ip_range, failure", [ + ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False), + ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False), + ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False), + ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False), + ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True), + ('127_bad_ip', 'foobar', 'foobar', True), + ]) + def test_ips_add(self, user_util, test_name, ip, ip_range, failure): + self.log_user() + user = user_util.create_user(username=test_name) + user_id = user.user_id + + response = self.app.post( + route_path('edit_user_ips_add', user_id=user_id), + params={'new_ip': ip, 'csrf_token': self.csrf_token}) + + if failure: + assert_session_flash( + response, 'Please enter a valid IPv4 or IpV6 address') + response = self.app.get(route_path('edit_user_ips', user_id=user_id)) + + response.mustcontain(no=[ip]) + response.mustcontain(no=[ip_range]) + + else: + response = self.app.get(route_path('edit_user_ips', user_id=user_id)) + response.mustcontain(ip) + response.mustcontain(ip_range) + + def test_ips_delete(self, user_util): + self.log_user() + user = user_util.create_user() + user_id = user.user_id + ip = '127.0.0.1/32' + ip_range = '127.0.0.1 - 127.0.0.1' + new_ip = UserModel().add_extra_ip(user_id, ip) + Session().commit() + new_ip_id = new_ip.ip_id + + response = self.app.get(route_path('edit_user_ips', user_id=user_id)) + response.mustcontain(ip) + response.mustcontain(ip_range) + + self.app.post( + route_path('edit_user_ips_delete', user_id=user_id), + params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token}) + + response = self.app.get(route_path('edit_user_ips', user_id=user_id)) + response.mustcontain('All IP addresses are allowed') + response.mustcontain(no=[ip]) + response.mustcontain(no=[ip_range]) + + def test_emails(self): + self.log_user() + user = User.get_by_username(TEST_USER_REGULAR_LOGIN) + response = self.app.get(route_path('edit_user_emails', user_id=user.user_id)) + response.mustcontain('No additional emails specified') + + def test_emails_add(self, user_util): + self.log_user() + user = user_util.create_user() + user_id = user.user_id + + self.app.post( + route_path('edit_user_emails_add', user_id=user_id), + params={'new_email': 'example@rhodecode.com', + 'csrf_token': self.csrf_token}) + + response = self.app.get(route_path('edit_user_emails', user_id=user_id)) + response.mustcontain('example@rhodecode.com') + + def test_emails_add_existing_email(self, user_util, user_regular): + existing_email = user_regular.email + + self.log_user() + user = user_util.create_user() + user_id = user.user_id + + response = self.app.post( + route_path('edit_user_emails_add', user_id=user_id), + params={'new_email': existing_email, + 'csrf_token': self.csrf_token}) + assert_session_flash( + response, 'This e-mail address is already taken') + + response = self.app.get(route_path('edit_user_emails', user_id=user_id)) + response.mustcontain(no=[existing_email]) + + def test_emails_delete(self, user_util): + self.log_user() + user = user_util.create_user() + user_id = user.user_id + + self.app.post( + route_path('edit_user_emails_add', user_id=user_id), + params={'new_email': 'example@rhodecode.com', + 'csrf_token': self.csrf_token}) + + response = self.app.get(route_path('edit_user_emails', user_id=user_id)) + response.mustcontain('example@rhodecode.com') + + user_email = UserEmailMap.query()\ + .filter(UserEmailMap.email == 'example@rhodecode.com') \ + .filter(UserEmailMap.user_id == user_id)\ + .one() + + del_email_id = user_email.email_id + self.app.post( + route_path('edit_user_emails_delete', user_id=user_id), + params={'del_email_id': del_email_id, + 'csrf_token': self.csrf_token}) + + response = self.app.get(route_path('edit_user_emails', user_id=user_id)) + response.mustcontain(no=['example@rhodecode.com']) \ No newline at end of file diff --git a/rhodecode/apps/admin/views/audit_logs.py b/rhodecode/apps/admin/views/audit_logs.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/views/audit_logs.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 + +from pyramid.view import view_config +from sqlalchemy.orm import joinedload + +from rhodecode.apps._base import BaseAppView +from rhodecode.model.db import UserLog +from rhodecode.lib.user_log_filter import user_log_filter +from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator +from rhodecode.lib.utils2 import safe_int +from rhodecode.lib.helpers import Page + +log = logging.getLogger(__name__) + + +class AdminAuditLogsView(BaseAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + self._register_global_c(c) + return c + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='admin_audit_logs', request_method='GET', + renderer='rhodecode:templates/admin/admin_audit_logs.mako') + def admin_audit_logs(self): + c = self.load_default_context() + + users_log = UserLog.query()\ + .options(joinedload(UserLog.user))\ + .options(joinedload(UserLog.repository)) + + # FILTERING + c.search_term = self.request.GET.get('filter') + try: + users_log = user_log_filter(users_log, c.search_term) + except Exception: + # we want this to crash for now + raise + + users_log = users_log.order_by(UserLog.action_date.desc()) + + p = safe_int(self.request.GET.get('page', 1), 1) + + def url_generator(**kw): + if c.search_term: + kw['filter'] = c.search_term + return self.request.current_route_path(_query=kw) + + c.audit_logs = Page(users_log, page=p, items_per_page=10, + url=url_generator) + return self._get_template_context(c) diff --git a/rhodecode/apps/admin/views/main_views.py b/rhodecode/apps/admin/views/main_views.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/admin/views/main_views.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 + + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +from rhodecode.apps._base import BaseAppView +from rhodecode.lib import helpers as h +from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator) +from rhodecode.model.db import PullRequest + + +log = logging.getLogger(__name__) + + +class AdminMainView(BaseAppView): + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='admin_home', request_method='GET') + def admin_main(self): + # redirect _admin to audit logs... + raise HTTPFound(h.route_path('admin_audit_logs')) + + @LoginRequired() + @view_config(route_name='pull_requests_global_0', request_method='GET') + @view_config(route_name='pull_requests_global_1', request_method='GET') + @view_config(route_name='pull_requests_global', request_method='GET') + def pull_requests(self): + """ + Global redirect for Pull Requests + + :param pull_request_id: id of pull requests in the system + """ + + pull_request_id = self.request.matchdict.get('pull_request_id') + pull_request = PullRequest.get_or_404(pull_request_id, pyramid_exc=True) + repo_name = pull_request.target_repo.repo_name + + raise HTTPFound( + h.route_path('pullrequest_show', repo_name=repo_name, + pull_request_id=pull_request_id)) diff --git a/rhodecode/apps/admin/views/open_source_licenses.py b/rhodecode/apps/admin/views/open_source_licenses.py --- a/rhodecode/apps/admin/views/open_source_licenses.py +++ b/rhodecode/apps/admin/views/open_source_licenses.py @@ -21,7 +21,7 @@ import collections import logging -from pylons import tmpl_context as c + from pyramid.view import view_config from rhodecode.apps._base import BaseAppView @@ -34,15 +34,21 @@ log = logging.getLogger(__name__) class OpenSourceLicensesAdminSettingsView(BaseAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + self._register_global_c(c) + return c + @LoginRequired() @HasPermissionAllDecorator('hg.admin') @view_config( route_name='admin_settings_open_source', request_method='GET', renderer='rhodecode:templates/admin/settings/settings.mako') def open_source_licenses(self): + c = self.load_default_context() c.active = 'open_source' c.navlist = navigation_list(self.request) c.opensource_licenses = collections.OrderedDict( sorted(read_opensource_licenses().items(), key=lambda t: t[0])) - return {} + return self._get_template_context(c) diff --git a/rhodecode/apps/admin/views/sessions.py b/rhodecode/apps/admin/views/sessions.py --- a/rhodecode/apps/admin/views/sessions.py +++ b/rhodecode/apps/admin/views/sessions.py @@ -20,7 +20,6 @@ import logging -from pylons import tmpl_context as c from pyramid.view import view_config from pyramid.httpexceptions import HTTPFound @@ -37,6 +36,11 @@ log = logging.getLogger(__name__) class AdminSessionSettingsView(BaseAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + + self._register_global_c(c) + return c @LoginRequired() @HasPermissionAllDecorator('hg.admin') @@ -44,6 +48,8 @@ class AdminSessionSettingsView(BaseAppVi route_name='admin_settings_sessions', request_method='GET', renderer='rhodecode:templates/admin/settings/settings.mako') def settings_sessions(self): + c = self.load_default_context() + c.active = 'sessions' c.navlist = navigation_list(self.request) @@ -59,11 +65,11 @@ class AdminSessionSettingsView(BaseAppVi c.session_expired_count = c.session_model.get_expired_count( older_than_seconds) - return {} + return self._get_template_context(c) @LoginRequired() + @HasPermissionAllDecorator('hg.admin') @CSRFRequired() - @HasPermissionAllDecorator('hg.admin') @view_config( route_name='admin_settings_sessions_cleanup', request_method='POST') def settings_sessions_cleanup(self): diff --git a/rhodecode/apps/admin/views/svn_config.py b/rhodecode/apps/admin/views/svn_config.py --- a/rhodecode/apps/admin/views/svn_config.py +++ b/rhodecode/apps/admin/views/svn_config.py @@ -33,8 +33,8 @@ log = logging.getLogger(__name__) class SvnConfigAdminSettingsView(BaseAppView): @LoginRequired() + @HasPermissionAllDecorator('hg.admin') @CSRFRequired() - @HasPermissionAllDecorator('hg.admin') @view_config( route_name='admin_settings_vcs_svn_generate_cfg', request_method='POST', renderer='json') diff --git a/rhodecode/apps/admin/views/system_info.py b/rhodecode/apps/admin/views/system_info.py --- a/rhodecode/apps/admin/views/system_info.py +++ b/rhodecode/apps/admin/views/system_info.py @@ -22,7 +22,6 @@ import logging import urllib2 import packaging.version -from pylons import tmpl_context as c from pyramid.view import view_config import rhodecode @@ -39,6 +38,10 @@ log = logging.getLogger(__name__) class AdminSystemInfoSettingsView(BaseAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + self._register_global_c(c) + return c @staticmethod def get_update_data(update_url): @@ -64,6 +67,7 @@ class AdminSystemInfoSettingsView(BaseAp renderer='rhodecode:templates/admin/settings/settings.mako') def settings_system_info(self): _ = self.request.translate + c = self.load_default_context() c.active = 'system' c.navlist = navigation_list(self.request) @@ -106,6 +110,7 @@ class AdminSystemInfoSettingsView(BaseAp (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')), (_('RhodeCode Server ID'), val('server')['server_id'], state('server')), (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')), + (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')), (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')), (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')), ('', '', ''), # spacer @@ -163,7 +168,7 @@ class AdminSystemInfoSettingsView(BaseAp else: self.request.session.flash( 'You are not allowed to do this', queue='warning') - return {} + return self._get_template_context(c) @LoginRequired() @HasPermissionAllDecorator('hg.admin') @@ -172,6 +177,7 @@ class AdminSystemInfoSettingsView(BaseAp renderer='rhodecode:templates/admin/settings/settings_system_update.mako') def settings_system_info_check_update(self): _ = self.request.translate + c = self.load_default_context() update_url = self.get_update_url() @@ -200,4 +206,4 @@ class AdminSystemInfoSettingsView(BaseAp c.should_upgrade = True c.important_notices = latest['general'] - return {} + return self._get_template_context(c) diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py --- a/rhodecode/apps/admin/views/users.py +++ b/rhodecode/apps/admin/views/users.py @@ -20,15 +20,16 @@ import logging import datetime +import formencode from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config from sqlalchemy.sql.functions import coalesce -from rhodecode.lib.helpers import Page -from rhodecode_tools.lib.ext_json import json +from rhodecode.apps._base import BaseAppView, DataGridAppView -from rhodecode.apps._base import BaseAppView +from rhodecode.lib import audit_logger +from rhodecode.lib.ext_json import json from rhodecode.lib.auth import ( LoginRequired, HasPermissionAllDecorator, CSRFRequired) from rhodecode.lib import helpers as h @@ -37,13 +38,13 @@ from rhodecode.lib.utils2 import safe_in from rhodecode.model.auth_token import AuthTokenModel from rhodecode.model.user import UserModel from rhodecode.model.user_group import UserGroupModel -from rhodecode.model.db import User, or_ +from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys from rhodecode.model.meta import Session log = logging.getLogger(__name__) -class AdminUsersView(BaseAppView): +class AdminUsersView(BaseAppView, DataGridAppView): ALLOW_SCOPED_TOKENS = False """ This view has alternative version inside EE, if modified please take a look @@ -64,28 +65,6 @@ class AdminUsersView(BaseAppView): # is a pyramid view raise HTTPFound('/') - def _extract_ordering(self, request): - column_index = safe_int(request.GET.get('order[0][column]')) - order_dir = request.GET.get( - 'order[0][dir]', 'desc') - order_by = request.GET.get( - 'columns[%s][data][sort]' % column_index, 'name_raw') - - # translate datatable to DB columns - order_by = { - 'first_name': 'name', - 'last_name': 'lastname', - }.get(order_by) or order_by - - search_q = request.GET.get('search[value]') - return search_q, order_by, order_dir - - def _extract_chunk(self, request): - start = safe_int(request.GET.get('start'), 0) - length = safe_int(request.GET.get('length'), 25) - draw = safe_int(request.GET.get('draw')) - return draw, start, length - @HasPermissionAllDecorator('hg.admin') @view_config( route_name='users', request_method='GET', @@ -97,8 +76,8 @@ class AdminUsersView(BaseAppView): @HasPermissionAllDecorator('hg.admin') @view_config( # renderer defined below - route_name='users_data', request_method='GET', renderer='json', - xhr=True) + route_name='users_data', request_method='GET', + renderer='json_ext', xhr=True) def users_list_data(self): draw, start, limit = self._extract_chunk(self.request) search_q, order_by, order_dir = self._extract_ordering(self.request) @@ -149,8 +128,8 @@ class AdminUsersView(BaseAppView): users_data.append({ "username": h.gravatar_with_user(user.username), "email": user.email, - "first_name": h.escape(user.name), - "last_name": h.escape(user.lastname), + "first_name": user.first_name, + "last_name": user.last_name, "last_login": h.format_date(user.last_login), "last_activity": h.format_date(user.last_activity), "active": h.bool2icon(user.active), @@ -216,15 +195,23 @@ class AdminUsersView(BaseAppView): user_id = self.request.matchdict.get('user_id') c.user = User.get_or_404(user_id, pyramid_exc=True) + self._redirect_for_default_user(c.user.username) + user_data = c.user.get_api_data() lifetime = safe_int(self.request.POST.get('lifetime'), -1) description = self.request.POST.get('description') role = self.request.POST.get('role') token = AuthTokenModel().create( c.user.user_id, description, lifetime, role) + token_data = token.get_api_data() + self.maybe_attach_token_scope(token) + audit_logger.store_web( + 'user.edit.token.add', action_data={ + 'data': {'token': token_data, 'user': user_data}}, + user=self._rhodecode_user, ) Session().commit() h.flash(_("Auth token successfully created"), category='success') @@ -242,11 +229,19 @@ class AdminUsersView(BaseAppView): user_id = self.request.matchdict.get('user_id') c.user = User.get_or_404(user_id, pyramid_exc=True) self._redirect_for_default_user(c.user.username) + user_data = c.user.get_api_data() del_auth_token = self.request.POST.get('del_auth_token') if del_auth_token: + token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True) + token_data = token.get_api_data() + AuthTokenModel().delete(del_auth_token, c.user.user_id) + audit_logger.store_web( + 'user.edit.token.delete', action_data={ + 'data': {'token': token_data, 'user': user_data}}, + user=self._rhodecode_user,) Session().commit() h.flash(_("Auth token successfully deleted"), category='success') @@ -255,6 +250,186 @@ class AdminUsersView(BaseAppView): @LoginRequired() @HasPermissionAllDecorator('hg.admin') @view_config( + route_name='edit_user_emails', request_method='GET', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def emails(self): + _ = self.request.translate + c = self.load_default_context() + + user_id = self.request.matchdict.get('user_id') + c.user = User.get_or_404(user_id, pyramid_exc=True) + self._redirect_for_default_user(c.user.username) + + c.active = 'emails' + c.user_email_map = UserEmailMap.query() \ + .filter(UserEmailMap.user == c.user).all() + + return self._get_template_context(c) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='edit_user_emails_add', request_method='POST') + def emails_add(self): + _ = self.request.translate + c = self.load_default_context() + + user_id = self.request.matchdict.get('user_id') + c.user = User.get_or_404(user_id, pyramid_exc=True) + self._redirect_for_default_user(c.user.username) + + email = self.request.POST.get('new_email') + user_data = c.user.get_api_data() + try: + UserModel().add_extra_email(c.user.user_id, email) + audit_logger.store_web( + 'user.edit.email.add', action_data={'email': email, 'user': user_data}, + user=self._rhodecode_user) + Session().commit() + h.flash(_("Added new email address `%s` for user account") % email, + category='success') + except formencode.Invalid as error: + h.flash(h.escape(error.error_dict['email']), category='error') + except Exception: + log.exception("Exception during email saving") + h.flash(_('An error occurred during email saving'), + category='error') + raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id)) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='edit_user_emails_delete', request_method='POST') + def emails_delete(self): + _ = self.request.translate + c = self.load_default_context() + + user_id = self.request.matchdict.get('user_id') + c.user = User.get_or_404(user_id, pyramid_exc=True) + self._redirect_for_default_user(c.user.username) + + email_id = self.request.POST.get('del_email_id') + user_model = UserModel() + + email = UserEmailMap.query().get(email_id).email + user_data = c.user.get_api_data() + user_model.delete_extra_email(c.user.user_id, email_id) + audit_logger.store_web( + 'user.edit.email.delete', action_data={'email': email, 'user': user_data}, + user=self._rhodecode_user) + Session().commit() + h.flash(_("Removed email address from user account"), + category='success') + raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id)) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='edit_user_ips', request_method='GET', + renderer='rhodecode:templates/admin/users/user_edit.mako') + def ips(self): + _ = self.request.translate + c = self.load_default_context() + + user_id = self.request.matchdict.get('user_id') + c.user = User.get_or_404(user_id, pyramid_exc=True) + self._redirect_for_default_user(c.user.username) + + c.active = 'ips' + c.user_ip_map = UserIpMap.query() \ + .filter(UserIpMap.user == c.user).all() + + c.inherit_default_ips = c.user.inherit_default_permissions + c.default_user_ip_map = UserIpMap.query() \ + .filter(UserIpMap.user == User.get_default_user()).all() + + return self._get_template_context(c) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='edit_user_ips_add', request_method='POST') + def ips_add(self): + _ = self.request.translate + c = self.load_default_context() + + user_id = self.request.matchdict.get('user_id') + c.user = User.get_or_404(user_id, pyramid_exc=True) + # NOTE(marcink): this view is allowed for default users, as we can + # edit their IP white list + + user_model = UserModel() + desc = self.request.POST.get('description') + try: + ip_list = user_model.parse_ip_range( + self.request.POST.get('new_ip')) + except Exception as e: + ip_list = [] + log.exception("Exception during ip saving") + h.flash(_('An error occurred during ip saving:%s' % (e,)), + category='error') + added = [] + user_data = c.user.get_api_data() + for ip in ip_list: + try: + user_model.add_extra_ip(c.user.user_id, ip, desc) + audit_logger.store_web( + 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data}, + user=self._rhodecode_user) + Session().commit() + added.append(ip) + except formencode.Invalid as error: + msg = error.error_dict['ip'] + h.flash(msg, category='error') + except Exception: + log.exception("Exception during ip saving") + h.flash(_('An error occurred during ip saving'), + category='error') + if added: + h.flash( + _("Added ips %s to user whitelist") % (', '.join(ip_list), ), + category='success') + if 'default_user' in self.request.POST: + # case for editing global IP list we do it for 'DEFAULT' user + raise HTTPFound(h.route_path('admin_permissions_ips')) + raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id)) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() + @view_config( + route_name='edit_user_ips_delete', request_method='POST') + def ips_delete(self): + _ = self.request.translate + c = self.load_default_context() + + user_id = self.request.matchdict.get('user_id') + c.user = User.get_or_404(user_id, pyramid_exc=True) + # NOTE(marcink): this view is allowed for default users, as we can + # edit their IP white list + + ip_id = self.request.POST.get('del_ip_id') + user_model = UserModel() + user_data = c.user.get_api_data() + ip = UserIpMap.query().get(ip_id).ip_addr + user_model.delete_extra_ip(c.user.user_id, ip_id) + audit_logger.store_web( + 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data}, + user=self._rhodecode_user) + Session().commit() + h.flash(_("Removed ip address from user whitelist"), category='success') + + if 'default_user' in self.request.POST: + # case for editing global IP list we do it for 'DEFAULT' user + raise HTTPFound(h.route_path('admin_permissions_ips')) + raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id)) + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( route_name='edit_user_groups_management', request_method='GET', renderer='rhodecode:templates/admin/users/user_edit.mako') def groups_management(self): @@ -264,7 +439,8 @@ class AdminUsersView(BaseAppView): c.user = User.get_or_404(user_id, pyramid_exc=True) c.data = c.user.group_member self._redirect_for_default_user(c.user.username) - groups = [UserGroupModel.get_user_groups_as_dict(group.users_group) for group in c.user.group_member] + groups = [UserGroupModel.get_user_groups_as_dict(group.users_group) + for group in c.user.group_member] c.groups = json.dumps(groups) c.active = 'groups' @@ -272,6 +448,7 @@ class AdminUsersView(BaseAppView): @LoginRequired() @HasPermissionAllDecorator('hg.admin') + @CSRFRequired() @view_config( route_name='edit_user_groups_management_updates', request_method='POST') def groups_management_updates(self): @@ -314,15 +491,15 @@ class AdminUsersView(BaseAppView): p = safe_int(self.request.GET.get('page', 1), 1) filter_term = self.request.GET.get('filter') - c.user_log = UserModel().get_user_log(c.user, filter_term) + user_log = UserModel().get_user_log(c.user, filter_term) def url_generator(**kw): if filter_term: kw['filter'] = filter_term return self.request.current_route_path(_query=kw) - c.user_log = Page(c.user_log, page=p, items_per_page=10, - url=url_generator) + c.audit_logs = h.Page( + user_log, page=p, items_per_page=10, url=url_generator) c.filter_term = filter_term return self._get_template_context(c) diff --git a/rhodecode/apps/channelstream/views.py b/rhodecode/apps/channelstream/views.py --- a/rhodecode/apps/channelstream/views.py +++ b/rhodecode/apps/channelstream/views.py @@ -30,8 +30,6 @@ Channel Stream controller for rhodecode 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 @@ -46,7 +44,6 @@ from rhodecode.lib.channelstream import update_history_from_logs, STATE_PUBLIC_KEYS) from rhodecode.lib.auth import NotAnonymous -from rhodecode.lib.utils2 import str2bool log = logging.getLogger(__name__) @@ -82,7 +79,7 @@ class ChannelstreamView(object): log.error('Incorrect permissions for requested channels') raise HTTPForbidden() - user = c.rhodecode_user + user = self._rhodecode_user if user.user_id: user_data = get_user_data(user.user_id) else: @@ -95,7 +92,7 @@ class ChannelstreamView(object): 'display_name': None, 'display_link': None, } - user_data['permissions'] = c.rhodecode_user.permissions + user_data['permissions'] = self._rhodecode_user.permissions payload = { 'username': user.username, 'user_state': user_data, diff --git a/rhodecode/apps/home/__init__.py b/rhodecode/apps/home/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/home/__init__.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 rhodecode.config import routing_links + + +def includeme(config): + + config.add_route( + name='home', + pattern='/') + + config.add_route( + name='user_autocomplete_data', + pattern='/_users') + + config.add_route( + name='user_group_autocomplete_data', + pattern='/_user_groups') + + config.add_route( + name='repo_list_data', + pattern='/_repos') + + config.add_route( + name='goto_switcher_data', + pattern='/_goto_data') + + # register our static links via redirection mechanism + routing_links.connect_redirection_links(config) + + # Scan module for configuration decorators. + config.scan() diff --git a/rhodecode/apps/home/tests/__init__.py b/rhodecode/apps/home/tests/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/home/tests/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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/ + + +def assert_and_get_content(result): + repos = [] + groups = [] + commits = [] + for data in result: + for data_item in data['children']: + assert data_item['id'] + assert data_item['text'] + assert data_item['url'] + if data_item['type'] == 'repo': + repos.append(data_item) + elif data_item['type'] == 'group': + groups.append(data_item) + elif data_item['type'] == 'commit': + commits.append(data_item) + else: + raise Exception('invalid type %s' % data_item['type']) + + return repos, groups, commits \ No newline at end of file diff --git a/rhodecode/apps/home/tests/test_get_goto_switched_data.py b/rhodecode/apps/home/tests/test_get_goto_switched_data.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/home/tests/test_get_goto_switched_data.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 json + +import pytest + +from . import assert_and_get_content +from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN +from rhodecode.tests.fixture import Fixture + +from rhodecode.lib.utils import map_groups +from rhodecode.model.repo import RepoModel +from rhodecode.model.repo_group import RepoGroupModel +from rhodecode.model.db import Session, Repository, RepoGroup + +fixture = Fixture() + + +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'goto_switcher_data': '/_goto_data', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +class TestGotoSwitcherData(TestController): + + required_repos_with_groups = [ + 'abc', + 'abc-fork', + 'forks/abcd', + 'abcd', + 'abcde', + 'a/abc', + 'aa/abc', + 'aaa/abc', + 'aaaa/abc', + 'repos_abc/aaa/abc', + 'abc_repos/abc', + 'abc_repos/abcd', + 'xxx/xyz', + 'forked-abc/a/abc' + ] + + @pytest.fixture(autouse=True, scope='class') + def prepare(self, request, pylonsapp): + for repo_and_group in self.required_repos_with_groups: + # create structure of groups and return the last group + + repo_group = map_groups(repo_and_group) + + RepoModel()._create_repo( + repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN, + repo_group=getattr(repo_group, 'group_id', None)) + + Session().commit() + + request.addfinalizer(self.cleanup) + + def cleanup(self): + # first delete all repos + for repo_and_groups in self.required_repos_with_groups: + repo = Repository.get_by_repo_name(repo_and_groups) + if repo: + RepoModel().delete(repo) + Session().commit() + + # then delete all empty groups + for repo_and_groups in self.required_repos_with_groups: + if '/' in repo_and_groups: + r_group = repo_and_groups.rsplit('/', 1)[0] + repo_group = RepoGroup.get_by_group_name(r_group) + if not repo_group: + continue + parents = repo_group.parents + RepoGroupModel().delete(repo_group, force_delete=True) + Session().commit() + + for el in reversed(parents): + RepoGroupModel().delete(el, force_delete=True) + Session().commit() + + def test_returns_list_of_repos_and_groups(self, xhr_header): + self.log_user() + + response = self.app.get( + route_path('goto_switcher_data'), + extra_environ=xhr_header, status=200) + result = json.loads(response.body)['results'] + + repos, groups, commits = assert_and_get_content(result) + + assert len(repos) == len(Repository.get_all()) + assert len(groups) == len(RepoGroup.get_all()) + assert len(commits) == 0 + + def test_returns_list_of_repos_and_groups_filtered(self, xhr_header): + self.log_user() + + response = self.app.get( + route_path('goto_switcher_data'), + params={'query': 'abc'}, + extra_environ=xhr_header, status=200) + result = json.loads(response.body)['results'] + + repos, groups, commits = assert_and_get_content(result) + + assert len(repos) == 13 + assert len(groups) == 5 + assert len(commits) == 0 + + def test_returns_list_of_properly_sorted_and_filtered(self, xhr_header): + self.log_user() + + response = self.app.get( + route_path('goto_switcher_data'), + params={'query': 'abc'}, + extra_environ=xhr_header, status=200) + result = json.loads(response.body)['results'] + + repos, groups, commits = assert_and_get_content(result) + + test_repos = [x['text'] for x in repos[:4]] + assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos + + test_groups = [x['text'] for x in groups[:4]] + assert ['abc_repos', 'repos_abc', + 'forked-abc', 'forked-abc/a'] == test_groups diff --git a/rhodecode/apps/home/tests/test_get_repo_list_data.py b/rhodecode/apps/home/tests/test_get_repo_list_data.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/home/tests/test_get_repo_list_data.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 json + +from . import assert_and_get_content +from rhodecode.tests import TestController +from rhodecode.tests.fixture import Fixture +from rhodecode.model.db import Repository + +fixture = Fixture() + + +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'repo_list_data': '/_repos', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +class TestRepoListData(TestController): + + def test_returns_list_of_repos_and_groups(self, xhr_header): + self.log_user() + + response = self.app.get( + route_path('repo_list_data'), + extra_environ=xhr_header, status=200) + result = json.loads(response.body)['results'] + + repos, groups, commits = assert_and_get_content(result) + + assert len(repos) == len(Repository.get_all()) + assert len(groups) == 0 + assert len(commits) == 0 + + def test_returns_list_of_repos_and_groups_filtered(self, xhr_header): + self.log_user() + + response = self.app.get( + route_path('repo_list_data'), + params={'query': 'vcs_test_git'}, + extra_environ=xhr_header, status=200) + result = json.loads(response.body)['results'] + + repos, groups, commits = assert_and_get_content(result) + + assert len(repos) == len(Repository.query().filter( + Repository.repo_name.ilike('%vcs_test_git%')).all()) + assert len(groups) == 0 + assert len(commits) == 0 + + def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header): + self.log_user() + + response = self.app.get( + route_path('repo_list_data'), + params={'query': 'vcs_test_git', 'repo_type': 'git'}, + extra_environ=xhr_header, status=200) + result = json.loads(response.body)['results'] + + repos, groups, commits = assert_and_get_content(result) + + assert len(repos) == len(Repository.query().filter( + Repository.repo_name.ilike('%vcs_test_git%')).all()) + assert len(groups) == 0 + assert len(commits) == 0 + + def test_returns_list_of_repos_non_ascii_query(self, xhr_header): + self.log_user() + response = self.app.get( + route_path('repo_list_data'), + params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'}, + extra_environ=xhr_header, status=200) + result = json.loads(response.body)['results'] + + repos, groups, commits = assert_and_get_content(result) + + assert len(repos) == 0 + assert len(groups) == 0 + assert len(commits) == 0 diff --git a/rhodecode/apps/home/tests/test_get_user_data.py b/rhodecode/apps/home/tests/test_get_user_data.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/home/tests/test_get_user_data.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 json +import pytest + +from rhodecode.tests import TestController +from rhodecode.tests.fixture import Fixture + + +fixture = Fixture() + + +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'user_autocomplete_data': '/_users', + 'user_group_autocomplete_data': '/_user_groups' + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +class TestUserAutocompleteData(TestController): + + def test_returns_list_of_users(self, user_util, xhr_header): + self.log_user() + user = user_util.create_user(active=True) + user_name = user.username + response = self.app.get( + route_path('user_autocomplete_data'), + extra_environ=xhr_header, status=200) + result = json.loads(response.body) + values = [suggestion['value'] for suggestion in result['suggestions']] + assert user_name in values + + def test_returns_inactive_users_when_active_flag_sent( + self, user_util, xhr_header): + self.log_user() + user = user_util.create_user(active=False) + user_name = user.username + + response = self.app.get( + route_path('user_autocomplete_data', + params=dict(user_groups='true', active='0')), + extra_environ=xhr_header, status=200) + result = json.loads(response.body) + values = [suggestion['value'] for suggestion in result['suggestions']] + assert user_name in values + + response = self.app.get( + route_path('user_autocomplete_data', + params=dict(user_groups='true', active='1')), + extra_environ=xhr_header, status=200) + result = json.loads(response.body) + values = [suggestion['value'] for suggestion in result['suggestions']] + assert user_name not in values + + def test_returns_groups_when_user_groups_flag_sent( + self, user_util, xhr_header): + self.log_user() + group = user_util.create_user_group(user_groups_active=True) + group_name = group.users_group_name + response = self.app.get( + route_path('user_autocomplete_data', + params=dict(user_groups='true')), + extra_environ=xhr_header, status=200) + result = json.loads(response.body) + values = [suggestion['value'] for suggestion in result['suggestions']] + assert group_name in values + + @pytest.mark.parametrize('query, count', [ + ('hello1', 0), + ('dev', 2), + ]) + def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, + query, count): + self.log_user() + + user_util._test_name = 'dev-test' + user_util.create_user() + + user_util._test_name = 'dev-group-test' + user_util.create_user_group() + + response = self.app.get( + route_path('user_autocomplete_data', + params=dict(user_groups='true', query=query)), + extra_environ=xhr_header, status=200) + + result = json.loads(response.body) + assert len(result['suggestions']) == count diff --git a/rhodecode/apps/home/tests/test_get_user_group_data.py b/rhodecode/apps/home/tests/test_get_user_group_data.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/home/tests/test_get_user_group_data.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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/ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 json + +import pytest + +from rhodecode.tests import TestController +from rhodecode.tests.fixture import Fixture + + +fixture = Fixture() + + +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'user_autocomplete_data': '/_users', + 'user_group_autocomplete_data': '/_user_groups' + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +class TestUserGroupAutocompleteData(TestController): + + def test_returns_list_of_user_groups(self, user_util, xhr_header): + self.log_user() + user_group = user_util.create_user_group(active=True) + user_group_name = user_group.users_group_name + response = self.app.get( + route_path('user_group_autocomplete_data'), + extra_environ=xhr_header, status=200) + result = json.loads(response.body) + values = [suggestion['value'] for suggestion in result['suggestions']] + assert user_group_name in values + + def test_returns_inactive_user_groups_when_active_flag_sent( + self, user_util, xhr_header): + self.log_user() + user_group = user_util.create_user_group(active=False) + user_group_name = user_group.users_group_name + + response = self.app.get( + route_path('user_group_autocomplete_data', + params=dict(active='0')), + extra_environ=xhr_header, status=200) + result = json.loads(response.body) + values = [suggestion['value'] for suggestion in result['suggestions']] + assert user_group_name in values + + response = self.app.get( + route_path('user_group_autocomplete_data', + params=dict(active='1')), + extra_environ=xhr_header, status=200) + result = json.loads(response.body) + values = [suggestion['value'] for suggestion in result['suggestions']] + assert user_group_name not in values + + @pytest.mark.parametrize('query, count', [ + ('hello1', 0), + ('dev', 1), + ]) + def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count): + self.log_user() + + user_util._test_name = 'dev-test' + user_util.create_user_group() + + response = self.app.get( + route_path('user_group_autocomplete_data', + params=dict(user_groups='true', + query=query)), + extra_environ=xhr_header, status=200) + + result = json.loads(response.body) + + assert len(result['suggestions']) == count diff --git a/rhodecode/tests/functional/test_home.py b/rhodecode/apps/home/tests/test_home.py rename from rhodecode/tests/functional/test_home.py rename to rhodecode/apps/home/tests/test_home.py --- a/rhodecode/tests/functional/test_home.py +++ b/rhodecode/apps/home/tests/test_home.py @@ -18,31 +18,34 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ -import json -from mock import patch import pytest -from pylons import tmpl_context as c import rhodecode -from rhodecode.lib.utils import map_groups -from rhodecode.model.db import Repository, User, RepoGroup +from rhodecode.model.db import Repository from rhodecode.model.meta import Session from rhodecode.model.repo import RepoModel from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.settings import SettingsModel -from rhodecode.tests import TestController, url, TEST_USER_ADMIN_LOGIN +from rhodecode.tests import TestController from rhodecode.tests.fixture import Fixture +from rhodecode.lib import helpers as h + +fixture = Fixture() -fixture = Fixture() +def route_path(name, **kwargs): + return { + 'home': '/', + 'repo_group_home': '/{repo_group_name}' + }[name].format(**kwargs) class TestHomeController(TestController): def test_index(self): self.log_user() - response = self.app.get(url(controller='home', action='index')) + response = self.app.get(route_path('home')) # if global permission is set response.mustcontain('Add Repository') @@ -51,8 +54,10 @@ class TestHomeController(TestController) response.mustcontain('"name_raw": "%s"' % repo.repo_name) def test_index_contains_statics_with_ver(self): + from pylons import tmpl_context as c + self.log_user() - response = self.app.get(url(controller='home', action='index')) + response = self.app.get(route_path('home')) rhodecode_version_hash = c.rhodecode_version_hash response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash)) @@ -60,7 +65,7 @@ class TestHomeController(TestController) def test_index_contains_backend_specific_details(self, backend): self.log_user() - response = self.app.get(url(controller='home', action='index')) + response = self.app.get(route_path('home')) tip = backend.repo.get_commit().raw_id # html in javascript variable: @@ -72,17 +77,16 @@ class TestHomeController(TestController) def test_index_with_anonymous_access_disabled(self): with fixture.anon_access(False): - response = self.app.get(url(controller='home', action='index'), - status=302) + response = self.app.get(route_path('home'), status=302) assert 'login' in response.location def test_index_page_on_groups(self, autologin_user, repo_group): - response = self.app.get(url('repo_group_home', group_name='gr1')) + response = self.app.get(route_path('repo_group_home', repo_group_name='gr1')) response.mustcontain("gr1/repo_in_group") def test_index_page_on_group_with_trailing_slash( self, autologin_user, repo_group): - response = self.app.get(url('repo_group_home', group_name='gr1') + '/') + response = self.app.get(route_path('repo_group_home', repo_group_name='gr1') + '/') response.mustcontain("gr1/repo_in_group") @pytest.fixture(scope='class') @@ -96,21 +100,19 @@ class TestHomeController(TestController) RepoGroupModel().delete(repo_group='gr1', force_delete=True) Session().commit() - def test_index_with_name_with_tags(self, autologin_user): - user = User.get_by_username('test_admin') + def test_index_with_name_with_tags(self, user_util, autologin_user): + user = user_util.create_user() + username = user.username user.name = '' - user.lastname = ( - '') + user.lastname = '#">' + Session().add(user) Session().commit() + user_util.create_repo(owner=username) - response = self.app.get(url(controller='home', action='index')) - response.mustcontain( - '<img src="/image1" onload="' - 'alert('Hello, World!');">') - response.mustcontain( - '<img src="/image2" onload="' - 'alert('Hello, World!');">') + response = self.app.get(route_path('home')) + response.mustcontain(h.html_escape(user.first_name)) + response.mustcontain(h.html_escape(user.last_name)) @pytest.mark.parametrize("name, state", [ ('Disabled', False), @@ -125,266 +127,8 @@ class TestHomeController(TestController) Session().commit() SettingsModel().invalidate_settings_cache() - response = self.app.get(url(controller='home', action='index')) + response = self.app.get(route_path('home')) if state is True: response.mustcontain(version_string) if state is False: response.mustcontain(no=[version_string]) - - -class TestUserAutocompleteData(TestController): - def test_returns_list_of_users(self, user_util): - self.log_user() - user = user_util.create_user(is_active=True) - user_name = user.username - response = self.app.get( - url(controller='home', action='user_autocomplete_data'), - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) - result = json.loads(response.body) - values = [suggestion['value'] for suggestion in result['suggestions']] - assert user_name in values - - def test_returns_inactive_users_when_active_flag_sent(self, user_util): - self.log_user() - user = user_util.create_user(is_active=False) - user_name = user.username - response = self.app.get( - url(controller='home', action='user_autocomplete_data', - user_groups='true', active='0'), - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) - result = json.loads(response.body) - values = [suggestion['value'] for suggestion in result['suggestions']] - assert user_name in values - - def test_returns_groups_when_user_groups_sent(self, user_util): - self.log_user() - group = user_util.create_user_group(user_groups_active=True) - group_name = group.users_group_name - response = self.app.get( - url(controller='home', action='user_autocomplete_data', - user_groups='true'), - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) - result = json.loads(response.body) - values = [suggestion['value'] for suggestion in result['suggestions']] - assert group_name in values - - def test_result_is_limited_when_query_is_sent(self): - self.log_user() - fake_result = [ - { - 'first_name': 'John', - 'value_display': 'hello{} (John Smith)'.format(i), - 'icon_link': '/images/user14.png', - 'value': 'hello{}'.format(i), - 'last_name': 'Smith', - 'username': 'hello{}'.format(i), - 'id': i, - 'value_type': u'user' - } - for i in range(10) - ] - users_patcher = patch.object( - RepoModel, 'get_users', return_value=fake_result) - groups_patcher = patch.object( - RepoModel, 'get_user_groups', return_value=fake_result) - - query = 'hello' - with users_patcher as users_mock, groups_patcher as groups_mock: - response = self.app.get( - url(controller='home', action='user_autocomplete_data', - user_groups='true', query=query), - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) - - result = json.loads(response.body) - users_mock.assert_called_once_with( - name_contains=query, only_active=True) - groups_mock.assert_called_once_with( - name_contains=query, only_active=True) - assert len(result['suggestions']) == 20 - - -def assert_and_get_content(result): - repos = [] - groups = [] - commits = [] - for data in result: - for data_item in data['children']: - assert data_item['id'] - assert data_item['text'] - assert data_item['url'] - if data_item['type'] == 'repo': - repos.append(data_item) - elif data_item['type'] == 'group': - groups.append(data_item) - elif data_item['type'] == 'commit': - commits.append(data_item) - else: - raise Exception('invalid type %s' % data_item['type']) - - return repos, groups, commits - - -class TestGotoSwitcherData(TestController): - required_repos_with_groups = [ - 'abc', - 'abc-fork', - 'forks/abcd', - 'abcd', - 'abcde', - 'a/abc', - 'aa/abc', - 'aaa/abc', - 'aaaa/abc', - 'repos_abc/aaa/abc', - 'abc_repos/abc', - 'abc_repos/abcd', - 'xxx/xyz', - 'forked-abc/a/abc' - ] - - @pytest.fixture(autouse=True, scope='class') - def prepare(self, request, pylonsapp): - for repo_and_group in self.required_repos_with_groups: - # create structure of groups and return the last group - - repo_group = map_groups(repo_and_group) - - RepoModel()._create_repo( - repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN, - repo_group=getattr(repo_group, 'group_id', None)) - - Session().commit() - - request.addfinalizer(self.cleanup) - - def cleanup(self): - # first delete all repos - for repo_and_groups in self.required_repos_with_groups: - repo = Repository.get_by_repo_name(repo_and_groups) - if repo: - RepoModel().delete(repo) - Session().commit() - - # then delete all empty groups - for repo_and_groups in self.required_repos_with_groups: - if '/' in repo_and_groups: - r_group = repo_and_groups.rsplit('/', 1)[0] - repo_group = RepoGroup.get_by_group_name(r_group) - if not repo_group: - continue - parents = repo_group.parents - RepoGroupModel().delete(repo_group, force_delete=True) - Session().commit() - - for el in reversed(parents): - RepoGroupModel().delete(el, force_delete=True) - Session().commit() - - def test_returns_list_of_repos_and_groups(self): - self.log_user() - - response = self.app.get( - url(controller='home', action='goto_switcher_data'), - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) - result = json.loads(response.body)['results'] - - repos, groups, commits = assert_and_get_content(result) - - assert len(repos) == len(Repository.get_all()) - assert len(groups) == len(RepoGroup.get_all()) - assert len(commits) == 0 - - def test_returns_list_of_repos_and_groups_filtered(self): - self.log_user() - - response = self.app.get( - url(controller='home', action='goto_switcher_data'), - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, - params={'query': 'abc'}, status=200) - result = json.loads(response.body)['results'] - - repos, groups, commits = assert_and_get_content(result) - - assert len(repos) == 13 - assert len(groups) == 5 - assert len(commits) == 0 - - def test_returns_list_of_properly_sorted_and_filtered(self): - self.log_user() - - response = self.app.get( - url(controller='home', action='goto_switcher_data'), - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, - params={'query': 'abc'}, status=200) - result = json.loads(response.body)['results'] - - repos, groups, commits = assert_and_get_content(result) - - test_repos = [x['text'] for x in repos[:4]] - assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos - - test_groups = [x['text'] for x in groups[:4]] - assert ['abc_repos', 'repos_abc', - 'forked-abc', 'forked-abc/a'] == test_groups - - -class TestRepoListData(TestController): - def test_returns_list_of_repos_and_groups(self, user_util): - self.log_user() - - response = self.app.get( - url(controller='home', action='repo_list_data'), - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200) - result = json.loads(response.body)['results'] - - repos, groups, commits = assert_and_get_content(result) - - assert len(repos) == len(Repository.get_all()) - assert len(groups) == 0 - assert len(commits) == 0 - - def test_returns_list_of_repos_and_groups_filtered(self): - self.log_user() - - response = self.app.get( - url(controller='home', action='repo_list_data'), - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, - params={'query': 'vcs_test_git'}, status=200) - result = json.loads(response.body)['results'] - - repos, groups, commits = assert_and_get_content(result) - - assert len(repos) == len(Repository.query().filter( - Repository.repo_name.ilike('%vcs_test_git%')).all()) - assert len(groups) == 0 - assert len(commits) == 0 - - def test_returns_list_of_repos_and_groups_filtered_with_type(self): - self.log_user() - - response = self.app.get( - url(controller='home', action='repo_list_data'), - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, - params={'query': 'vcs_test_git', 'repo_type': 'git'}, status=200) - result = json.loads(response.body)['results'] - - repos, groups, commits = assert_and_get_content(result) - - assert len(repos) == len(Repository.query().filter( - Repository.repo_name.ilike('%vcs_test_git%')).all()) - assert len(groups) == 0 - assert len(commits) == 0 - - def test_returns_list_of_repos_non_ascii_query(self): - self.log_user() - response = self.app.get( - url(controller='home', action='repo_list_data'), - headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, - params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'}, status=200) - result = json.loads(response.body)['results'] - - repos, groups, commits = assert_and_get_content(result) - - assert len(repos) == 0 - assert len(groups) == 0 - assert len(commits) == 0 diff --git a/rhodecode/apps/home/views.py b/rhodecode/apps/home/views.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/home/views.py @@ -0,0 +1,304 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 re +import logging + +from pyramid.view import view_config + +from rhodecode.apps._base import BaseAppView +from rhodecode.lib import helpers as h +from rhodecode.lib.auth import LoginRequired, NotAnonymous, \ + HasRepoGroupPermissionAnyDecorator +from rhodecode.lib.index import searcher_from_config +from rhodecode.lib.utils2 import safe_unicode, str2bool +from rhodecode.lib.ext_json import json +from rhodecode.model.db import func, Repository, RepoGroup +from rhodecode.model.repo import RepoModel +from rhodecode.model.repo_group import RepoGroupModel +from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList +from rhodecode.model.user import UserModel +from rhodecode.model.user_group import UserGroupModel + +log = logging.getLogger(__name__) + + +class HomeView(BaseAppView): + + def load_default_context(self): + c = self._get_local_tmpl_context() + c.user = c.auth_user.get_instance() + self._register_global_c(c) + return c + + @LoginRequired() + @view_config( + route_name='user_autocomplete_data', request_method='GET', + renderer='json_ext', xhr=True) + def user_autocomplete_data(self): + query = self.request.GET.get('query') + active = str2bool(self.request.GET.get('active') or True) + include_groups = str2bool(self.request.GET.get('user_groups')) + expand_groups = str2bool(self.request.GET.get('user_groups_expand')) + skip_default_user = str2bool(self.request.GET.get('skip_default_user')) + + log.debug('generating user list, query:%s, active:%s, with_groups:%s', + query, active, include_groups) + + _users = UserModel().get_users( + name_contains=query, only_active=active) + + def maybe_skip_default_user(usr): + if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER: + return False + return True + _users = filter(maybe_skip_default_user, _users) + + if include_groups: + # extend with user groups + _user_groups = UserGroupModel().get_user_groups( + name_contains=query, only_active=active, + expand_groups=expand_groups) + _users = _users + _user_groups + + return {'suggestions': _users} + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='user_group_autocomplete_data', request_method='GET', + renderer='json_ext', xhr=True) + def user_group_autocomplete_data(self): + query = self.request.GET.get('query') + active = str2bool(self.request.GET.get('active') or True) + expand_groups = str2bool(self.request.GET.get('user_groups_expand')) + + log.debug('generating user group list, query:%s, active:%s', + query, active) + + _user_groups = UserGroupModel().get_user_groups( + name_contains=query, only_active=active, + expand_groups=expand_groups) + _user_groups = _user_groups + + return {'suggestions': _user_groups} + + def _get_repo_list(self, name_contains=None, repo_type=None, limit=20): + query = Repository.query()\ + .order_by(func.length(Repository.repo_name))\ + .order_by(Repository.repo_name) + + if repo_type: + query = query.filter(Repository.repo_type == repo_type) + + if name_contains: + ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) + query = query.filter( + Repository.repo_name.ilike(ilike_expression)) + query = query.limit(limit) + + all_repos = query.all() + # permission checks are inside this function + repo_iter = ScmModel().get_repos(all_repos) + return [ + { + 'id': obj['name'], + 'text': obj['name'], + 'type': 'repo', + 'obj': obj['dbrepo'], + 'url': h.route_path('repo_summary', repo_name=obj['name']) + } + for obj in repo_iter] + + def _get_repo_group_list(self, name_contains=None, limit=20): + query = RepoGroup.query()\ + .order_by(func.length(RepoGroup.group_name))\ + .order_by(RepoGroup.group_name) + + if name_contains: + ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) + query = query.filter( + RepoGroup.group_name.ilike(ilike_expression)) + query = query.limit(limit) + + all_groups = query.all() + repo_groups_iter = ScmModel().get_repo_groups(all_groups) + return [ + { + 'id': obj.group_name, + 'text': obj.group_name, + 'type': 'group', + 'obj': {}, + 'url': h.route_path('repo_group_home', repo_group_name=obj.group_name) + } + for obj in repo_groups_iter] + + def _get_hash_commit_list(self, auth_user, hash_starts_with=None): + if not hash_starts_with or len(hash_starts_with) < 3: + return [] + + commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with) + + if len(commit_hashes) != 1: + return [] + + commit_hash_prefix = commit_hashes[0] + + searcher = searcher_from_config(self.request.registry.settings) + result = searcher.search( + 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user, + raise_on_exc=False) + + return [ + { + 'id': entry['commit_id'], + 'text': entry['commit_id'], + 'type': 'commit', + 'obj': {'repo': entry['repository']}, + 'url': h.url('changeset_home', + repo_name=entry['repository'], + revision=entry['commit_id']) + } + for entry in result['results']] + + @LoginRequired() + @view_config( + route_name='repo_list_data', request_method='GET', + renderer='json_ext', xhr=True) + def repo_list_data(self): + _ = self.request.translate + + query = self.request.GET.get('query') + repo_type = self.request.GET.get('repo_type') + log.debug('generating repo list, query:%s, repo_type:%s', + query, repo_type) + + res = [] + repos = self._get_repo_list(query, repo_type=repo_type) + if repos: + res.append({ + 'text': _('Repositories'), + 'children': repos + }) + + data = { + 'more': False, + 'results': res + } + return data + + @LoginRequired() + @view_config( + route_name='goto_switcher_data', request_method='GET', + renderer='json_ext', xhr=True) + def goto_switcher_data(self): + c = self.load_default_context() + + _ = self.request.translate + + query = self.request.GET.get('query') + log.debug('generating goto switcher list, query %s', query) + + res = [] + repo_groups = self._get_repo_group_list(query) + if repo_groups: + res.append({ + 'text': _('Groups'), + 'children': repo_groups + }) + + repos = self._get_repo_list(query) + if repos: + res.append({ + 'text': _('Repositories'), + 'children': repos + }) + + commits = self._get_hash_commit_list(c.auth_user, query) + if commits: + unique_repos = {} + for commit in commits: + unique_repos.setdefault(commit['obj']['repo'], [] + ).append(commit) + + for repo in unique_repos: + res.append({ + 'text': _('Commits in %(repo)s') % {'repo': repo}, + 'children': unique_repos[repo] + }) + + data = { + 'more': False, + 'results': res + } + return data + + def _get_groups_and_repos(self, repo_group_id=None): + # repo groups groups + repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id) + _perms = ['group.read', 'group.write', 'group.admin'] + repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms) + repo_group_data = RepoGroupModel().get_repo_groups_as_dict( + repo_group_list=repo_group_list_acl, admin=False) + + # repositories + repo_list = Repository.get_all_repos(group_id=repo_group_id) + _perms = ['repository.read', 'repository.write', 'repository.admin'] + repo_list_acl = RepoList(repo_list, perm_set=_perms) + repo_data = RepoModel().get_repos_as_dict( + repo_list=repo_list_acl, admin=False) + + return repo_data, repo_group_data + + @LoginRequired() + @view_config( + route_name='home', request_method='GET', + renderer='rhodecode:templates/index.mako') + def main_page(self): + c = self.load_default_context() + c.repo_group = None + + repo_data, repo_group_data = self._get_groups_and_repos() + # json used to render the grids + c.repos_data = json.dumps(repo_data) + c.repo_groups_data = json.dumps(repo_group_data) + + return self._get_template_context(c) + + @LoginRequired() + @HasRepoGroupPermissionAnyDecorator( + 'group.read', 'group.write', 'group.admin') + @view_config( + route_name='repo_group_home', request_method='GET', + renderer='rhodecode:templates/index_repo_group.mako') + @view_config( + route_name='repo_group_home_slash', request_method='GET', + renderer='rhodecode:templates/index_repo_group.mako') + def repo_group_main_page(self): + c = self.load_default_context() + c.repo_group = self.request.db_repo_group + repo_data, repo_group_data = self._get_groups_and_repos( + c.repo_group.group_id) + + # json used to render the grids + c.repos_data = json.dumps(repo_data) + c.repo_groups_data = json.dumps(repo_group_data) + + return self._get_template_context(c) diff --git a/rhodecode/apps/login/views.py b/rhodecode/apps/login/views.py --- a/rhodecode/apps/login/views.py +++ b/rhodecode/apps/login/views.py @@ -25,7 +25,6 @@ import formencode import logging import urlparse -from pylons import url from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config from recaptcha.client.captcha import submit @@ -34,6 +33,7 @@ from rhodecode.apps._base import BaseApp from rhodecode.authentication.base import authenticate, HTTP_TYPE from rhodecode.events import UserRegistered from rhodecode.lib import helpers as h +from rhodecode.lib import audit_logger from rhodecode.lib.auth import ( AuthUser, HasPermissionAnyDecorator, CSRFRequired) from rhodecode.lib.base import get_ip_addr @@ -90,20 +90,21 @@ def get_came_from(request): came_from = safe_str(request.GET.get('came_from', '')) parsed = urlparse.urlparse(came_from) allowed_schemes = ['http', 'https'] + default_came_from = h.route_path('home') if parsed.scheme and parsed.scheme not in allowed_schemes: log.error('Suspicious URL scheme detected %s for url %s' % (parsed.scheme, parsed)) - came_from = url('home') + came_from = default_came_from elif parsed.netloc and request.host != parsed.netloc: log.error('Suspicious NETLOC detected %s for url %s server url ' 'is: %s' % (parsed.netloc, parsed, request.host)) - came_from = url('home') + came_from = default_came_from elif any(bad_str in parsed.path for bad_str in ('\r', '\n')): log.error('Header injection detected `%s` for url %s server url ' % (parsed.path, parsed)) - came_from = url('home') + came_from = default_came_from - return came_from or url('home') + return came_from or default_came_from class LoginView(BaseAppView): @@ -166,6 +167,15 @@ class LoginView(BaseAppView): username=form_result['username'], remember=form_result['remember']) log.debug('Redirecting to "%s" after login.', c.came_from) + + audit_user = audit_logger.UserWrap( + username=self.request.params.get('username'), + ip_addr=self.request.remote_addr) + action_data = {'user_agent': self.request.user_agent} + audit_logger.store_web( + 'user.login.success', action_data=action_data, + user=audit_user, commit=True) + raise HTTPFound(c.came_from, headers=headers) except formencode.Invalid as errors: defaults = errors.value @@ -176,6 +186,14 @@ class LoginView(BaseAppView): 'errors': errors.error_dict, 'defaults': defaults, }) + + audit_user = audit_logger.UserWrap( + username=self.request.params.get('username'), + ip_addr=self.request.remote_addr) + action_data = {'user_agent': self.request.user_agent} + audit_logger.store_web( + 'user.login.failure', action_data=action_data, + user=audit_user, commit=True) return render_ctx except UserCreationError as e: @@ -191,8 +209,13 @@ class LoginView(BaseAppView): def logout(self): auth_user = self._rhodecode_user log.info('Deleting session for user: `%s`', auth_user) + + action_data = {'user_agent': self.request.user_agent} + audit_logger.store_web( + 'user.logout', action_data=action_data, + user=auth_user, commit=True) self.session.delete() - return HTTPFound(url('home')) + return HTTPFound(h.route_path('home')) @HasPermissionAnyDecorator( 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate') @@ -338,6 +361,12 @@ class LoginView(BaseAppView): form_result, password_reset_url) # Display success message and redirect. self.session.flash(msg, queue='success') + + action_data = {'email': user_email, + 'user_agent': self.request.user_agent} + audit_logger.store_web( + 'user.password.reset_request', action_data=action_data, + user=self._rhodecode_user, commit=True) return HTTPFound(self.request.route_path('reset_password')) except formencode.Invalid as errors: diff --git a/rhodecode/apps/my_account/__init__.py b/rhodecode/apps/my_account/__init__.py --- a/rhodecode/apps/my_account/__init__.py +++ b/rhodecode/apps/my_account/__init__.py @@ -41,12 +41,45 @@ def includeme(config): pattern=ADMIN_PREFIX + '/my_account/auth_tokens') config.add_route( name='my_account_auth_tokens_add', - pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new', - ) + pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new') config.add_route( name='my_account_auth_tokens_delete', - pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete', - ) + pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete') + + config.add_route( + name='my_account_emails', + pattern=ADMIN_PREFIX + '/my_account/emails') + config.add_route( + name='my_account_emails_add', + pattern=ADMIN_PREFIX + '/my_account/emails/new') + config.add_route( + name='my_account_emails_delete', + pattern=ADMIN_PREFIX + '/my_account/emails/delete') + + config.add_route( + name='my_account_repos', + pattern=ADMIN_PREFIX + '/my_account/repos') + + config.add_route( + name='my_account_watched', + pattern=ADMIN_PREFIX + '/my_account/watched') + + config.add_route( + name='my_account_perms', + pattern=ADMIN_PREFIX + '/my_account/perms') + + config.add_route( + name='my_account_notifications', + pattern=ADMIN_PREFIX + '/my_account/notifications') + + config.add_route( + name='my_account_notifications_toggle_visibility', + pattern=ADMIN_PREFIX + '/my_account/toggle_visibility') + + # channelstream test + config.add_route( + name='my_account_notifications_test_channelstream', + pattern=ADMIN_PREFIX + '/my_account/test_channelstream') # Scan module for configuration decorators. config.scan() diff --git a/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py b/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py --- a/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py +++ b/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py @@ -103,7 +103,7 @@ class TestMyAccountAuthTokens(TestContro response = self.app.post( route_path('my_account_auth_tokens_delete'), - {'del_auth_token': keys[0].api_key, 'csrf_token': self.csrf_token}) + {'del_auth_token': keys[0].user_api_key_id, 'csrf_token': self.csrf_token}) assert_session_flash(response, 'Auth token successfully deleted') user = User.get(user_id) diff --git a/rhodecode/apps/my_account/tests/test_my_account_emails.py b/rhodecode/apps/my_account/tests/test_my_account_emails.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/my_account/tests/test_my_account_emails.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 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 pytest + +from rhodecode.apps._base import ADMIN_PREFIX +from rhodecode.model.db import User, UserEmailMap +from rhodecode.tests import ( + TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL, + assert_session_flash) +from rhodecode.tests.fixture import Fixture + +fixture = Fixture() + + +def route_path(name, **kwargs): + return { + 'my_account_emails': + ADMIN_PREFIX + '/my_account/emails', + 'my_account_emails_add': + ADMIN_PREFIX + '/my_account/emails/new', + 'my_account_emails_delete': + ADMIN_PREFIX + '/my_account/emails/delete', + }[name].format(**kwargs) + + +class TestMyAccountEmails(TestController): + def test_my_account_my_emails(self): + self.log_user() + response = self.app.get(route_path('my_account_emails')) + response.mustcontain('No additional emails specified') + + def test_my_account_my_emails_add_existing_email(self): + self.log_user() + response = self.app.get(route_path('my_account_emails')) + response.mustcontain('No additional emails specified') + response = self.app.post(route_path('my_account_emails_add'), + {'new_email': TEST_USER_REGULAR_EMAIL, + 'csrf_token': self.csrf_token}) + assert_session_flash(response, 'This e-mail address is already taken') + + def test_my_account_my_emails_add_mising_email_in_form(self): + self.log_user() + response = self.app.get(route_path('my_account_emails')) + response.mustcontain('No additional emails specified') + response = self.app.post(route_path('my_account_emails_add'), + {'csrf_token': self.csrf_token}) + assert_session_flash(response, 'Please enter an email address') + + def test_my_account_my_emails_add_remove(self): + self.log_user() + response = self.app.get(route_path('my_account_emails')) + response.mustcontain('No additional emails specified') + + response = self.app.post(route_path('my_account_emails_add'), + {'new_email': 'foo@barz.com', + 'csrf_token': self.csrf_token}) + + response = self.app.get(route_path('my_account_emails')) + + email_id = UserEmailMap.query().filter( + UserEmailMap.user == User.get_by_username( + TEST_USER_ADMIN_LOGIN)).filter( + UserEmailMap.email == 'foo@barz.com').one().email_id + + response.mustcontain('foo@barz.com') + response.mustcontain('' % email_id) + + response = self.app.post( + route_path('my_account_emails_delete'), { + 'del_email_id': email_id, + 'csrf_token': self.csrf_token}) + assert_session_flash(response, 'Email successfully deleted') + response = self.app.get(route_path('my_account_emails')) + response.mustcontain('No additional emails specified') diff --git a/rhodecode/apps/my_account/tests/test_my_account_password.py b/rhodecode/apps/my_account/tests/test_my_account_password.py --- a/rhodecode/apps/my_account/tests/test_my_account_password.py +++ b/rhodecode/apps/my_account/tests/test_my_account_password.py @@ -132,6 +132,7 @@ class TestMyAccountPassword(TestControll self.app.post(route_path('my_account_password'), form_data) response = self.app.get(route_path('home')) - new_password_hash = response.session['rhodecode_user']['password'] + session = response.get_session_from_response() + new_password_hash = session['rhodecode_user']['password'] assert old_password_hash != new_password_hash \ No newline at end of file diff --git a/rhodecode/apps/my_account/tests/test_my_account_simple_views.py b/rhodecode/apps/my_account/tests/test_my_account_simple_views.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/my_account/tests/test_my_account_simple_views.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 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 pytest + +from rhodecode.apps._base import ADMIN_PREFIX +from rhodecode.model.db import User, UserEmailMap, Repository, UserFollowing +from rhodecode.tests import ( + TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL, + assert_session_flash) +from rhodecode.tests.fixture import Fixture + +fixture = Fixture() + + +def route_path(name, **kwargs): + return { + 'my_account_repos': + ADMIN_PREFIX + '/my_account/repos', + 'my_account_watched': + ADMIN_PREFIX + '/my_account/watched', + 'my_account_perms': + ADMIN_PREFIX + '/my_account/perms', + 'my_account_notifications': + ADMIN_PREFIX + '/my_account/notifications', + }[name].format(**kwargs) + + +class TestMyAccountSimpleViews(TestController): + + def test_my_account_my_repos(self, autologin_user): + response = self.app.get(route_path('my_account_repos')) + repos = Repository.query().filter( + Repository.user == User.get_by_username( + TEST_USER_ADMIN_LOGIN)).all() + for repo in repos: + response.mustcontain('"name_raw": "%s"' % repo.repo_name) + + def test_my_account_my_watched(self, autologin_user): + response = self.app.get(route_path('my_account_watched')) + + repos = UserFollowing.query().filter( + UserFollowing.user == User.get_by_username( + TEST_USER_ADMIN_LOGIN)).all() + for repo in repos: + response.mustcontain( + '"name_raw": "%s"' % repo.follows_repository.repo_name) + + def test_my_account_perms(self, autologin_user): + response = self.app.get(route_path('my_account_perms')) + assert_response = response.assert_response() + assert assert_response.get_elements('.perm_tag.none') + assert assert_response.get_elements('.perm_tag.read') + assert assert_response.get_elements('.perm_tag.write') + assert assert_response.get_elements('.perm_tag.admin') + + def test_my_account_notifications(self, autologin_user): + response = self.app.get(route_path('my_account_notifications')) + response.mustcontain('Test flash message') diff --git a/rhodecode/apps/my_account/views.py b/rhodecode/apps/my_account/views.py --- a/rhodecode/apps/my_account/views.py +++ b/rhodecode/apps/my_account/views.py @@ -19,18 +19,28 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import logging +import datetime +import formencode from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config from rhodecode.apps._base import BaseAppView from rhodecode import forms +from rhodecode.lib import helpers as h +from rhodecode.lib import audit_logger +from rhodecode.lib.ext_json import json from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired -from rhodecode.lib import helpers as h +from rhodecode.lib.channelstream import channelstream_request, \ + ChannelstreamException from rhodecode.lib.utils2 import safe_int, md5 from rhodecode.model.auth_token import AuthTokenModel +from rhodecode.model.db import ( + Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload) from rhodecode.model.meta import Session +from rhodecode.model.scm import RepoList from rhodecode.model.user import UserModel +from rhodecode.model.repo import RepoModel from rhodecode.model.validation_schema.schemas import user_schema log = logging.getLogger(__name__) @@ -158,7 +168,7 @@ class MyAccountView(BaseAppView): @NotAnonymous() @CSRFRequired() @view_config( - route_name='my_account_auth_tokens_add', request_method='POST') + route_name='my_account_auth_tokens_add', request_method='POST',) def my_account_auth_tokens_add(self): _ = self.request.translate c = self.load_default_context() @@ -169,7 +179,13 @@ class MyAccountView(BaseAppView): token = AuthTokenModel().create( c.user.user_id, description, lifetime, role) + token_data = token.get_api_data() + self.maybe_attach_token_scope(token) + audit_logger.store_web( + 'user.edit.token.add', action_data={ + 'data': {'token': token_data, 'user': 'self'}}, + user=self._rhodecode_user, ) Session().commit() h.flash(_("Auth token successfully created"), category='success') @@ -187,8 +203,197 @@ class MyAccountView(BaseAppView): del_auth_token = self.request.POST.get('del_auth_token') if del_auth_token: + token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True) + token_data = token.get_api_data() + AuthTokenModel().delete(del_auth_token, c.user.user_id) + audit_logger.store_web( + 'user.edit.token.delete', action_data={ + 'data': {'token': token_data, 'user': 'self'}}, + user=self._rhodecode_user,) Session().commit() h.flash(_("Auth token successfully deleted"), category='success') return HTTPFound(h.route_path('my_account_auth_tokens')) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='my_account_emails', request_method='GET', + renderer='rhodecode:templates/admin/my_account/my_account.mako') + def my_account_emails(self): + _ = self.request.translate + + c = self.load_default_context() + c.active = 'emails' + + c.user_email_map = UserEmailMap.query()\ + .filter(UserEmailMap.user == c.user).all() + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + @view_config( + route_name='my_account_emails_add', request_method='POST') + def my_account_emails_add(self): + _ = self.request.translate + c = self.load_default_context() + + email = self.request.POST.get('new_email') + + try: + UserModel().add_extra_email(c.user.user_id, email) + audit_logger.store_web( + 'user.edit.email.add', action_data={ + 'data': {'email': email, 'user': 'self'}}, + user=self._rhodecode_user,) + + Session().commit() + h.flash(_("Added new email address `%s` for user account") % email, + category='success') + except formencode.Invalid as error: + h.flash(h.escape(error.error_dict['email']), category='error') + except Exception: + log.exception("Exception in my_account_emails") + h.flash(_('An error occurred during email saving'), + category='error') + return HTTPFound(h.route_path('my_account_emails')) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + @view_config( + route_name='my_account_emails_delete', request_method='POST') + def my_account_emails_delete(self): + _ = self.request.translate + c = self.load_default_context() + + del_email_id = self.request.POST.get('del_email_id') + if del_email_id: + email = UserEmailMap.get_or_404(del_email_id, pyramid_exc=True).email + UserModel().delete_extra_email(c.user.user_id, del_email_id) + audit_logger.store_web( + 'user.edit.email.delete', action_data={ + 'data': {'email': email, 'user': 'self'}}, + user=self._rhodecode_user,) + Session().commit() + h.flash(_("Email successfully deleted"), + category='success') + return HTTPFound(h.route_path('my_account_emails')) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + @view_config( + route_name='my_account_notifications_test_channelstream', + request_method='POST', renderer='json_ext') + def my_account_notifications_test_channelstream(self): + message = 'Test message sent via Channelstream by user: {}, on {}'.format( + self._rhodecode_user.username, datetime.datetime.now()) + payload = { + # 'channel': 'broadcast', + 'type': 'message', + 'timestamp': datetime.datetime.utcnow(), + 'user': 'system', + 'pm_users': [self._rhodecode_user.username], + 'message': { + 'message': message, + 'level': 'info', + 'topic': '/notifications' + } + } + + registry = self.request.registry + rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {}) + channelstream_config = rhodecode_plugins.get('channelstream', {}) + + try: + channelstream_request(channelstream_config, [payload], '/message') + except ChannelstreamException as e: + log.exception('Failed to send channelstream data') + return {"response": 'ERROR: {}'.format(e.__class__.__name__)} + return {"response": 'Channelstream data sent. ' + 'You should see a new live message now.'} + + def _load_my_repos_data(self, watched=False): + if watched: + admin = False + follows_repos = Session().query(UserFollowing)\ + .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\ + .options(joinedload(UserFollowing.follows_repository))\ + .all() + repo_list = [x.follows_repository for x in follows_repos] + else: + admin = True + repo_list = Repository.get_all_repos( + user_id=self._rhodecode_user.user_id) + repo_list = RepoList(repo_list, perm_set=[ + 'repository.read', 'repository.write', 'repository.admin']) + + repos_data = RepoModel().get_repos_as_dict( + repo_list=repo_list, admin=admin) + # json used to render the grid + return json.dumps(repos_data) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='my_account_repos', request_method='GET', + renderer='rhodecode:templates/admin/my_account/my_account.mako') + def my_account_repos(self): + c = self.load_default_context() + c.active = 'repos' + + # json used to render the grid + c.data = self._load_my_repos_data() + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='my_account_watched', request_method='GET', + renderer='rhodecode:templates/admin/my_account/my_account.mako') + def my_account_watched(self): + c = self.load_default_context() + c.active = 'watched' + + # json used to render the grid + c.data = self._load_my_repos_data(watched=True) + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='my_account_perms', request_method='GET', + renderer='rhodecode:templates/admin/my_account/my_account.mako') + def my_account_perms(self): + c = self.load_default_context() + c.active = 'perms' + + c.perm_user = c.auth_user + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='my_account_notifications', request_method='GET', + renderer='rhodecode:templates/admin/my_account/my_account.mako') + def my_notifications(self): + c = self.load_default_context() + c.active = 'notifications' + + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + @view_config( + route_name='my_account_notifications_toggle_visibility', + request_method='POST', renderer='json_ext') + def my_notifications_toggle_visibility(self): + user = self._rhodecode_db_user + new_status = not user.user_data.get('notification_status', True) + user.update_userdata(notification_status=new_status) + Session().commit() + return user.user_data['notification_status'] \ No newline at end of file diff --git a/rhodecode/apps/ops/__init__.py b/rhodecode/apps/ops/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/ops/__init__.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 rhodecode.config.routing import ADMIN_PREFIX + + +def admin_routes(config): + config.add_route( + name='ops_ping', + pattern='/ping') + + +def includeme(config): + + config.include(admin_routes, route_prefix=ADMIN_PREFIX + '/ops') + + # Scan module for configuration decorators. + config.scan() diff --git a/rhodecode/apps/ops/views.py b/rhodecode/apps/ops/views.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/ops/views.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 + +from pyramid.view import view_config + +from rhodecode.apps._base import BaseAppView + + +log = logging.getLogger(__name__) + + +class OpsView(BaseAppView): + + def load_default_context(self): + c = self._get_local_tmpl_context() + c.user = c.auth_user.get_instance() + self._register_global_c(c) + return c + + @view_config( + route_name='ops_ping', request_method='GET', + renderer='json_ext') + def ops_ping(self): + data = { + 'instance': self.request.registry.settings.get('instance_id'), + } + if getattr(self.request, 'user'): + data.update({ + 'caller_ip': self.request.user.ip_addr, + 'caller_name': self.request.user.username, + }) + return {'ok': data} + + + diff --git a/rhodecode/apps/repo_group/__init__.py b/rhodecode/apps/repo_group/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repo_group/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 rhodecode.apps._base import add_route_with_slash + + +def includeme(config): + + # Summary + add_route_with_slash( + config, + name='repo_group_home', + pattern='/{repo_group_name:.*?[^/]}', repo_group_route=True) + + # Scan module for configuration decorators. + config.scan() + diff --git a/rhodecode/apps/repository/__init__.py b/rhodecode/apps/repository/__init__.py --- a/rhodecode/apps/repository/__init__.py +++ b/rhodecode/apps/repository/__init__.py @@ -17,30 +17,137 @@ # 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.apps._base import add_route_with_slash def includeme(config): + # Summary + # NOTE(marcink): one additional route is defined in very bottom, catch + # all pattern + config.add_route( + name='repo_summary_explicit', + pattern='/{repo_name:.*?[^/]}/summary', repo_route=True) + config.add_route( + name='repo_summary_commits', + pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True) + + # repo commits + config.add_route( + name='repo_commit', + pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True) + + # refs data + config.add_route( + name='repo_refs_data', + pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True) + + config.add_route( + name='repo_refs_changelog_data', + pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True) + + config.add_route( + name='repo_stats', + pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True) + + # Tags + config.add_route( + name='tags_home', + pattern='/{repo_name:.*?[^/]}/tags', repo_route=True) + + # Branches + config.add_route( + name='branches_home', + pattern='/{repo_name:.*?[^/]}/branches', repo_route=True) + + config.add_route( + name='bookmarks_home', + pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True) + + # Pull Requests + config.add_route( + name='pullrequest_show', + pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}', + repo_route=True) + + config.add_route( + name='pullrequest_show_all', + pattern='/{repo_name:.*?[^/]}/pull-request', + repo_route=True, repo_accepted_types=['hg', 'git']) + + config.add_route( + name='pullrequest_show_all_data', + pattern='/{repo_name:.*?[^/]}/pull-request-data', + repo_route=True, repo_accepted_types=['hg', 'git']) + + # Settings + config.add_route( + name='edit_repo', + pattern='/{repo_name:.*?[^/]}/settings', repo_route=True) + + # Settings advanced + config.add_route( + name='edit_repo_advanced', + pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True) + config.add_route( + name='edit_repo_advanced_delete', + pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True) + config.add_route( + name='edit_repo_advanced_locking', + pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True) + config.add_route( + name='edit_repo_advanced_journal', + pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True) + config.add_route( + name='edit_repo_advanced_fork', + pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True) + + # Caches + config.add_route( + name='edit_repo_caches', + pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True) + + # Permissions + config.add_route( + name='edit_repo_perms', + pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True) + + # Repo Review Rules + config.add_route( + name='repo_reviewers', + pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True) + + config.add_route( + name='repo_default_reviewers_data', + pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True) + + # Maintenance config.add_route( name='repo_maintenance', - pattern='/{repo_name:.*?[^/]}/maintenance', repo_route=True) + pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True) config.add_route( name='repo_maintenance_execute', - pattern='/{repo_name:.*?[^/]}/maintenance/execute', repo_route=True) - + pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True) # Strip config.add_route( name='strip', - pattern='/{repo_name:.*?[^/]}/strip', repo_route=True) + pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True) config.add_route( name='strip_check', - pattern='/{repo_name:.*?[^/]}/strip_check', repo_route=True) + pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True) config.add_route( name='strip_execute', - pattern='/{repo_name:.*?[^/]}/strip_execute', repo_route=True) + pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True) + + # NOTE(marcink): needs to be at the end for catch-all + add_route_with_slash( + config, + name='repo_summary', + pattern='/{repo_name:.*?[^/]}', repo_route=True) + # Scan module for configuration decorators. config.scan() diff --git a/rhodecode/apps/repository/tests/__init__.py b/rhodecode/apps/repository/tests/__init__.py new file mode 100644 diff --git a/rhodecode/apps/repository/tests/test_pull_requests_list.py b/rhodecode/apps/repository/tests/test_pull_requests_list.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/tests/test_pull_requests_list.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 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 pytest +from rhodecode.model.db import Repository + + +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'pullrequest_show_all': '/{repo_name}/pull-request', + 'pullrequest_show_all_data': '/{repo_name}/pull-request-data', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +@pytest.mark.backends("git", "hg") +@pytest.mark.usefixtures('autologin_user', 'app') +class TestPullRequestList(object): + + @pytest.mark.parametrize('params, expected_title', [ + ({'source': 0, 'closed': 1}, 'Closed Pull Requests'), + ({'source': 0, 'my': 1}, 'opened by me'), + ({'source': 0, 'awaiting_review': 1}, 'awaiting review'), + ({'source': 0, 'awaiting_my_review': 1}, 'awaiting my review'), + ({'source': 1}, 'Pull Requests from'), + ]) + def test_showing_list_page(self, backend, pr_util, params, expected_title): + pull_request = pr_util.create_pull_request() + + response = self.app.get( + route_path('pullrequest_show_all', + repo_name=pull_request.target_repo.repo_name, + params=params)) + + assert_response = response.assert_response() + assert_response.element_equals_to('.panel-title', expected_title) + element = assert_response.get_element('.panel-title') + element_text = assert_response._element_to_string(element) + + def test_showing_list_page_data(self, backend, pr_util, xhr_header): + pull_request = pr_util.create_pull_request() + response = self.app.get( + route_path('pullrequest_show_all_data', + repo_name=pull_request.target_repo.repo_name), + extra_environ=xhr_header) + + assert response.json['recordsTotal'] == 1 + assert response.json['data'][0]['description'] == 'Description' + + def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header): + xss_description = "" + pull_request = pr_util.create_pull_request(description=xss_description) + + response = self.app.get( + route_path('pullrequest_show_all_data', + repo_name=pull_request.target_repo.repo_name), + extra_environ=xhr_header) + + assert response.json['recordsTotal'] == 1 + assert response.json['data'][0]['description'] == \ + "<script>alert('Hi!')</script>" diff --git a/rhodecode/tests/functional/test_bookmarks.py b/rhodecode/apps/repository/tests/test_repo_bookmarks.py rename from rhodecode/tests/functional/test_bookmarks.py rename to rhodecode/apps/repository/tests/test_repo_bookmarks.py --- a/rhodecode/tests/functional/test_bookmarks.py +++ b/rhodecode/apps/repository/tests/test_repo_bookmarks.py @@ -18,24 +18,35 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ +import pytest from rhodecode.model.db import Repository -from rhodecode.tests import * -class TestBookmarksController(TestController): +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'bookmarks_home': '/{repo_name}/bookmarks', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +@pytest.mark.usefixtures('autologin_user', 'app') +class TestBookmarks(object): def test_index(self, backend): - self.log_user() if backend.alias == 'hg': - response = self.app.get(url(controller='bookmarks', - action='index', - repo_name=backend.repo_name)) + response = self.app.get( + route_path('bookmarks_home', repo_name=backend.repo_name)) repo = Repository.get_by_repo_name(backend.repo_name) for commit_id, obj_name in repo.scm_instance().bookmarks.items(): assert commit_id in response assert obj_name in response else: - self.app.get(url(controller='bookmarks', - action='index', - repo_name=backend.repo_name), status=404) \ No newline at end of file + self.app.get( + route_path('bookmarks_home', repo_name=backend.repo_name), + status=404) diff --git a/rhodecode/tests/functional/test_branches.py b/rhodecode/apps/repository/tests/test_repo_branches.py rename from rhodecode/tests/functional/test_branches.py rename to rhodecode/apps/repository/tests/test_repo_branches.py --- a/rhodecode/tests/functional/test_branches.py +++ b/rhodecode/apps/repository/tests/test_repo_branches.py @@ -18,17 +18,28 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ +import pytest from rhodecode.model.db import Repository -from rhodecode.tests import * -class TestBranchesController(TestController): +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'branches_home': '/{repo_name}/branches', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +@pytest.mark.usefixtures('autologin_user', 'app') +class TestBranchesController(object): def test_index(self, backend): - self.log_user() - response = self.app.get(url(controller='branches', - action='index', - repo_name=backend.repo_name)) + response = self.app.get( + route_path('branches_home', repo_name=backend.repo_name)) repo = Repository.get_by_repo_name(backend.repo_name) diff --git a/rhodecode/apps/repository/tests/test_repo_settings.py b/rhodecode/apps/repository/tests/test_repo_settings.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/tests/test_repo_settings.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 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 pytest + +from rhodecode.lib.utils2 import str2bool +from rhodecode.lib.vcs.exceptions import RepositoryRequirementError +from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User +from rhodecode.model.meta import Session +from rhodecode.tests import ( + url, HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, + assert_session_flash) +from rhodecode.tests.fixture import Fixture + +fixture = Fixture() + + +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'edit_repo': '/{repo_name}/settings', + 'edit_repo_advanced': '/{repo_name}/settings/advanced', + 'edit_repo_caches': '/{repo_name}/settings/caches', + 'edit_repo_perms': '/{repo_name}/settings/permissions', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +def _get_permission_for_user(user, repo): + perm = UserRepoToPerm.query()\ + .filter(UserRepoToPerm.repository == + Repository.get_by_repo_name(repo))\ + .filter(UserRepoToPerm.user == User.get_by_username(user))\ + .all() + return perm + + +@pytest.mark.usefixtures('autologin_user', 'app') +class TestAdminRepoSettings(object): + @pytest.mark.parametrize('urlname', [ + 'edit_repo', + 'edit_repo_caches', + 'edit_repo_perms', + 'edit_repo_advanced', + ]) + def test_show_page(self, urlname, app, backend): + app.get(route_path(urlname, repo_name=backend.repo_name), status=200) + + def test_edit_accessible_when_missing_requirements( + self, backend_hg, autologin_user): + scm_patcher = mock.patch.object( + Repository, 'scm_instance', side_effect=RepositoryRequirementError) + with scm_patcher: + self.app.get(route_path('edit_repo', repo_name=backend_hg.repo_name)) + + @pytest.mark.parametrize('urlname', [ + 'repo_vcs_settings', + 'repo_settings_issuetracker', + 'edit_repo_fields', + 'edit_repo_remote', + 'edit_repo_statistics', + ]) + def test_show_page_pylons(self, urlname, app): + app.get(url(urlname, repo_name=HG_REPO)) + + @pytest.mark.parametrize('update_settings', [ + {'repo_description': 'alter-desc'}, + {'repo_owner': TEST_USER_REGULAR_LOGIN}, + {'repo_private': 'true'}, + {'repo_enable_locking': 'true'}, + {'repo_enable_downloads': 'true'}, + ]) + def test_update_repo_settings(self, update_settings, csrf_token, backend, user_util): + repo = user_util.create_repo(repo_type=backend.alias) + repo_name = repo.repo_name + + params = fixture._get_repo_create_params( + csrf_token=csrf_token, + repo_name=repo_name, + repo_type=backend.alias, + repo_owner=TEST_USER_ADMIN_LOGIN, + repo_description='DESC', + + repo_private='false', + repo_enable_locking='false', + repo_enable_downloads='false') + params.update(update_settings) + self.app.post( + route_path('edit_repo', repo_name=repo_name), + params=params, status=302) + + repo = Repository.get_by_repo_name(repo_name) + assert repo.user.username == \ + update_settings.get('repo_owner', repo.user.username) + + assert repo.description == \ + update_settings.get('repo_description', repo.description) + + assert repo.private == \ + str2bool(update_settings.get( + 'repo_private', repo.private)) + + assert repo.enable_locking == \ + str2bool(update_settings.get( + 'repo_enable_locking', repo.enable_locking)) + + assert repo.enable_downloads == \ + str2bool(update_settings.get( + 'repo_enable_downloads', repo.enable_downloads)) + + def test_update_repo_name_via_settings(self, csrf_token, user_util, backend): + repo = user_util.create_repo(repo_type=backend.alias) + repo_name = repo.repo_name + + repo_group = user_util.create_repo_group() + repo_group_name = repo_group.group_name + new_name = repo_group_name + '_' + repo_name + + params = fixture._get_repo_create_params( + csrf_token=csrf_token, + repo_name=new_name, + repo_type=backend.alias, + repo_owner=TEST_USER_ADMIN_LOGIN, + repo_description='DESC', + repo_private='false', + repo_enable_locking='false', + repo_enable_downloads='false') + self.app.post( + route_path('edit_repo', repo_name=repo_name), + params=params, status=302) + repo = Repository.get_by_repo_name(new_name) + assert repo.repo_name == new_name + + def test_update_repo_group_via_settings(self, csrf_token, user_util, backend): + repo = user_util.create_repo(repo_type=backend.alias) + repo_name = repo.repo_name + + repo_group = user_util.create_repo_group() + repo_group_name = repo_group.group_name + repo_group_id = repo_group.group_id + + new_name = repo_group_name + '/' + repo_name + params = fixture._get_repo_create_params( + csrf_token=csrf_token, + repo_name=repo_name, + repo_type=backend.alias, + repo_owner=TEST_USER_ADMIN_LOGIN, + repo_description='DESC', + repo_group=repo_group_id, + repo_private='false', + repo_enable_locking='false', + repo_enable_downloads='false') + self.app.post( + route_path('edit_repo', repo_name=repo_name), + params=params, status=302) + repo = Repository.get_by_repo_name(new_name) + assert repo.repo_name == new_name + + def test_set_private_flag_sets_default_user_permissions_to_none( + self, autologin_user, backend, csrf_token): + + # initially repository perm should be read + perm = _get_permission_for_user(user='default', repo=backend.repo_name) + assert len(perm) == 1 + assert perm[0].permission.permission_name == 'repository.read' + assert not backend.repo.private + + response = self.app.post( + route_path('edit_repo', repo_name=backend.repo_name), + params=fixture._get_repo_create_params( + repo_private='true', + repo_name=backend.repo_name, + repo_type=backend.alias, + repo_owner=TEST_USER_ADMIN_LOGIN, + csrf_token=csrf_token), status=302) + + assert_session_flash( + response, + msg='Repository %s updated successfully' % (backend.repo_name)) + + repo = Repository.get_by_repo_name(backend.repo_name) + assert repo.private is True + + # now the repo default permission should be None + perm = _get_permission_for_user(user='default', repo=backend.repo_name) + assert len(perm) == 1 + assert perm[0].permission.permission_name == 'repository.none' + + response = self.app.post( + route_path('edit_repo', repo_name=backend.repo_name), + params=fixture._get_repo_create_params( + repo_private='false', + repo_name=backend.repo_name, + repo_type=backend.alias, + repo_owner=TEST_USER_ADMIN_LOGIN, + csrf_token=csrf_token), status=302) + + assert_session_flash( + response, + msg='Repository %s updated successfully' % (backend.repo_name)) + assert backend.repo.private is False + + # we turn off private now the repo default permission should stay None + perm = _get_permission_for_user(user='default', repo=backend.repo_name) + assert len(perm) == 1 + assert perm[0].permission.permission_name == 'repository.none' + + # update this permission back + perm[0].permission = Permission.get_by_key('repository.read') + Session().add(perm[0]) + Session().commit() diff --git a/rhodecode/apps/repository/tests/test_repo_settings_advanced.py b/rhodecode/apps/repository/tests/test_repo_settings_advanced.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/tests/test_repo_settings_advanced.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 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 pytest + +from rhodecode.lib.utils2 import safe_unicode, safe_str +from rhodecode.model.db import Repository +from rhodecode.model.repo import RepoModel +from rhodecode.tests import ( + HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator) +from rhodecode.tests.fixture import Fixture +from rhodecode.tests.utils import repo_on_filesystem + +fixture = Fixture() + + +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'repo_summary_explicit': '/{repo_name}/summary', + 'repo_summary': '/{repo_name}', + 'edit_repo_advanced': '/{repo_name}/settings/advanced', + 'edit_repo_advanced_delete': '/{repo_name}/settings/advanced/delete', + 'edit_repo_advanced_fork': '/{repo_name}/settings/advanced/fork', + 'edit_repo_advanced_locking': '/{repo_name}/settings/advanced/locking', + 'edit_repo_advanced_journal': '/{repo_name}/settings/advanced/journal', + + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +@pytest.mark.usefixtures('autologin_user', 'app') +class TestAdminRepoSettingsAdvanced(object): + + def test_set_repo_fork_has_no_self_id(self, autologin_user, backend): + repo = backend.repo + response = self.app.get( + route_path('edit_repo_advanced', repo_name=backend.repo_name)) + opt = """""" % repo.repo_id + response.mustcontain(no=[opt]) + + def test_set_fork_of_target_repo( + self, autologin_user, backend, csrf_token): + target_repo = 'target_%s' % backend.alias + fixture.create_repo(target_repo, repo_type=backend.alias) + repo2 = Repository.get_by_repo_name(target_repo) + response = self.app.post( + route_path('edit_repo_advanced_fork', repo_name=backend.repo_name), + params={'id_fork_of': repo2.repo_id, + 'csrf_token': csrf_token}) + repo = Repository.get_by_repo_name(backend.repo_name) + repo2 = Repository.get_by_repo_name(target_repo) + assert_session_flash( + response, + 'Marked repo %s as fork of %s' % (repo.repo_name, repo2.repo_name)) + + assert repo.fork == repo2 + response = response.follow() + # check if given repo is selected + + opt = 'This repository is a fork of %s' % ( + route_path('repo_summary', repo_name=repo2.repo_name), + repo2.repo_name) + + response.mustcontain(opt) + + fixture.destroy_repo(target_repo, forks='detach') + + @pytest.mark.backends("hg", "git") + def test_set_fork_of_other_type_repo( + self, autologin_user, backend, csrf_token): + TARGET_REPO_MAP = { + 'git': { + 'type': 'hg', + 'repo_name': HG_REPO}, + 'hg': { + 'type': 'git', + 'repo_name': GIT_REPO}, + } + target_repo = TARGET_REPO_MAP[backend.alias] + + repo2 = Repository.get_by_repo_name(target_repo['repo_name']) + response = self.app.post( + route_path('edit_repo_advanced_fork', repo_name=backend.repo_name), + params={'id_fork_of': repo2.repo_id, + 'csrf_token': csrf_token}) + assert_session_flash( + response, + 'Cannot set repository as fork of repository with other type') + + def test_set_fork_of_none(self, autologin_user, backend, csrf_token): + # mark it as None + response = self.app.post( + route_path('edit_repo_advanced_fork', repo_name=backend.repo_name), + params={'id_fork_of': None, '_method': 'put', + 'csrf_token': csrf_token}) + assert_session_flash( + response, + 'Marked repo %s as fork of %s' + % (backend.repo_name, "Nothing")) + assert backend.repo.fork is None + + def test_set_fork_of_same_repo(self, autologin_user, backend, csrf_token): + repo = Repository.get_by_repo_name(backend.repo_name) + response = self.app.post( + route_path('edit_repo_advanced_fork', repo_name=backend.repo_name), + params={'id_fork_of': repo.repo_id, 'csrf_token': csrf_token}) + assert_session_flash( + response, 'An error occurred during this operation') + + @pytest.mark.parametrize( + "suffix", + ['', u'ąęł' , '123'], + ids=no_newline_id_generator) + def test_advanced_delete(self, autologin_user, backend, suffix, csrf_token): + repo = backend.create_repo(name_suffix=suffix) + repo_name = repo.repo_name + repo_name_str = safe_str(repo.repo_name) + + response = self.app.post( + route_path('edit_repo_advanced_delete', repo_name=repo_name_str), + params={'csrf_token': csrf_token}) + assert_session_flash(response, + u'Deleted repository `{}`'.format(repo_name)) + response.follow() + + # check if repo was deleted from db + assert RepoModel().get_by_repo_name(repo_name) is None + assert not repo_on_filesystem(repo_name_str) diff --git a/rhodecode/tests/functional/test_summary.py b/rhodecode/apps/repository/tests/test_repo_summary.py rename from rhodecode/tests/functional/test_summary.py rename to rhodecode/apps/repository/tests/test_repo_summary.py --- a/rhodecode/tests/functional/test_summary.py +++ b/rhodecode/apps/repository/tests/test_repo_summary.py @@ -23,16 +23,16 @@ import re import mock import pytest -from rhodecode.controllers import summary +from rhodecode.apps.repository.views.repo_summary import RepoSummaryView from rhodecode.lib import helpers as h from rhodecode.lib.compat import OrderedDict +from rhodecode.lib.utils2 import AttributeDict from rhodecode.lib.vcs.exceptions import RepositoryRequirementError from rhodecode.model.db import Repository from rhodecode.model.meta import Session from rhodecode.model.repo import RepoModel from rhodecode.model.scm import ScmModel -from rhodecode.tests import ( - TestController, url, HG_REPO, assert_session_flash) +from rhodecode.tests import assert_session_flash from rhodecode.tests.fixture import Fixture from rhodecode.tests.utils import AssertResponse, repo_on_filesystem @@ -40,14 +40,31 @@ from rhodecode.tests.utils import Assert fixture = Fixture() -class TestSummaryController(TestController): - def test_index(self, backend): - self.log_user() +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'repo_summary': '/{repo_name}', + 'repo_stats': '/{repo_name}/repo_stats/{commit_id}', + 'repo_refs_data': '/{repo_name}/refs-data', + 'repo_refs_changelog_data': '/{repo_name}/refs-data-changelog' + + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +@pytest.mark.usefixtures('app') +class TestSummaryView(object): + def test_index(self, autologin_user, backend, http_host_only_stub): repo_id = backend.repo.repo_id repo_name = backend.repo_name with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy', return_value=False): - response = self.app.get(url('summary_home', repo_name=repo_name)) + response = self.app.get( + route_path('repo_summary', repo_name=repo_name)) # repo type response.mustcontain( @@ -61,46 +78,47 @@ class TestSummaryController(TestControll # clone url... response.mustcontain( 'id="clone_url" readonly="readonly"' - ' value="http://test_admin@test.example.com:80/%s"' % (repo_name, )) + ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, )) response.mustcontain( 'id="clone_url_id" readonly="readonly"' - ' value="http://test_admin@test.example.com:80/_%s"' % (repo_id, )) + ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, )) - def test_index_svn_without_proxy(self, backend_svn): - self.log_user() + def test_index_svn_without_proxy( + self, autologin_user, backend_svn, http_host_only_stub): repo_id = backend_svn.repo.repo_id repo_name = backend_svn.repo_name - response = self.app.get(url('summary_home', repo_name=repo_name)) + response = self.app.get(route_path('repo_summary', repo_name=repo_name)) # clone url... response.mustcontain( 'id="clone_url" disabled' - ' value="http://test_admin@test.example.com:80/%s"' % (repo_name, )) + ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, )) response.mustcontain( 'id="clone_url_id" disabled' - ' value="http://test_admin@test.example.com:80/_%s"' % (repo_id, )) + ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, )) - def test_index_with_trailing_slash(self, autologin_user, backend): + def test_index_with_trailing_slash( + self, autologin_user, backend, http_host_only_stub): + repo_id = backend.repo.repo_id repo_name = backend.repo_name with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy', return_value=False): response = self.app.get( - url('summary_home', repo_name=repo_name) + '/', + route_path('repo_summary', repo_name=repo_name) + '/', status=200) # clone url... response.mustcontain( 'id="clone_url" readonly="readonly"' - ' value="http://test_admin@test.example.com:80/%s"' % (repo_name, )) + ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, )) response.mustcontain( 'id="clone_url_id" readonly="readonly"' - ' value="http://test_admin@test.example.com:80/_%s"' % (repo_id, )) + ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, )) - def test_index_by_id(self, backend): - self.log_user() + def test_index_by_id(self, autologin_user, backend): repo_id = backend.repo.repo_id - response = self.app.get(url( - 'summary_home', repo_name='_%s' % (repo_id,))) + response = self.app.get( + route_path('repo_summary', repo_name='_%s' % (repo_id,))) # repo type response.mustcontain( @@ -111,10 +129,9 @@ class TestSummaryController(TestControll """""" ) - def test_index_by_repo_having_id_path_in_name_hg(self): - self.log_user() + def test_index_by_repo_having_id_path_in_name_hg(self, autologin_user): fixture.create_repo(name='repo_1') - response = self.app.get(url('summary_home', repo_name='repo_1')) + response = self.app.get(route_path('repo_summary', repo_name='repo_1')) try: response.mustcontain("repo_1") @@ -122,11 +139,11 @@ class TestSummaryController(TestControll RepoModel().delete(Repository.get_by_repo_name('repo_1')) Session().commit() - def test_index_with_anonymous_access_disabled(self): - with fixture.anon_access(False): - response = self.app.get(url('summary_home', repo_name=HG_REPO), - status=302) - assert 'login' in response.location + def test_index_with_anonymous_access_disabled( + self, backend, disable_anonymous_user): + response = self.app.get( + route_path('repo_summary', repo_name=backend.repo_name), status=302) + assert 'login' in response.location def _enable_stats(self, repo): r = Repository.get_by_repo_name(repo) @@ -173,17 +190,15 @@ class TestSummaryController(TestControll }, } - def test_repo_stats(self, backend, xhr_header): - self.log_user() + def test_repo_stats(self, autologin_user, backend, xhr_header): response = self.app.get( - url('repo_stats', - repo_name=backend.repo_name, commit_id='tip'), + route_path( + 'repo_stats', repo_name=backend.repo_name, commit_id='tip'), extra_environ=xhr_header, status=200) assert re.match(r'6[\d\.]+ KiB', response.json['size']) - def test_repo_stats_code_stats_enabled(self, backend, xhr_header): - self.log_user() + def test_repo_stats_code_stats_enabled(self, autologin_user, backend, xhr_header): repo_name = backend.repo_name # codes stats @@ -191,8 +206,8 @@ class TestSummaryController(TestControll ScmModel().mark_for_invalidation(repo_name) response = self.app.get( - url('repo_stats', - repo_name=backend.repo_name, commit_id='tip'), + route_path( + 'repo_stats', repo_name=backend.repo_name, commit_id='tip'), extra_environ=xhr_header, status=200) @@ -203,7 +218,7 @@ class TestSummaryController(TestControll def test_repo_refs_data(self, backend): response = self.app.get( - url('repo_refs_data', repo_name=backend.repo_name), + route_path('repo_refs_data', repo_name=backend.repo_name), status=200) # Ensure that there is the correct amount of items in the result @@ -220,72 +235,68 @@ class TestSummaryController(TestControll Repository, 'scm_instance', side_effect=RepositoryRequirementError) with scm_patcher: - response = self.app.get(url('summary_home', repo_name=repo_name)) + response = self.app.get(route_path('repo_summary', repo_name=repo_name)) assert_response = AssertResponse(response) assert_response.element_contains( '.main .alert-warning strong', 'Missing requirements') assert_response.element_contains( '.main .alert-warning', - 'These commits cannot be displayed, because this repository' - ' uses the Mercurial largefiles extension, which was not enabled.') + 'Commits cannot be displayed, because this repository ' + 'uses one or more extensions, which was not enabled.') def test_missing_requirements_page_does_not_contains_switch_to( - self, backend): - self.log_user() + self, autologin_user, backend): repo_name = backend.repo_name scm_patcher = mock.patch.object( Repository, 'scm_instance', side_effect=RepositoryRequirementError) with scm_patcher: - response = self.app.get(url('summary_home', repo_name=repo_name)) + response = self.app.get(route_path('repo_summary', repo_name=repo_name)) response.mustcontain(no='Switch To') -@pytest.mark.usefixtures('pylonsapp') -class TestSwitcherReferenceData: +@pytest.mark.usefixtures('app') +class TestRepoLocation(object): - def test_creates_reference_urls_based_on_name(self): - references = { - 'name': 'commit_id', - } - controller = summary.SummaryController() - is_svn = False - result = controller._switcher_reference_data( - 'repo_name', references, is_svn) - expected_url = h.url( - 'files_home', repo_name='repo_name', revision='name', - at='name') - assert result[0]['files_url'] == expected_url + @pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii']) + def test_manual_delete(self, autologin_user, backend, suffix, csrf_token): + repo = backend.create_repo(name_suffix=suffix) + repo_name = repo.repo_name + + # delete from file system + RepoModel()._delete_filesystem_repo(repo) - def test_urls_contain_commit_id_if_slash_in_name(self): - references = { - 'name/with/slash': 'commit_id', - } - controller = summary.SummaryController() - is_svn = False - result = controller._switcher_reference_data( - 'repo_name', references, is_svn) - expected_url = h.url( - 'files_home', repo_name='repo_name', revision='commit_id', - at='name/with/slash') - assert result[0]['files_url'] == expected_url + # test if the repo is still in the database + new_repo = RepoModel().get_by_repo_name(repo_name) + assert new_repo.repo_name == repo_name - def test_adds_reference_to_path_for_svn(self): - references = { - 'name/with/slash': 'commit_id', - } - controller = summary.SummaryController() - is_svn = True - result = controller._switcher_reference_data( - 'repo_name', references, is_svn) - expected_url = h.url( - 'files_home', repo_name='repo_name', f_path='name/with/slash', - revision='commit_id', at='name/with/slash') - assert result[0]['files_url'] == expected_url + # check if repo is not in the filesystem + assert not repo_on_filesystem(repo_name) + self.assert_repo_not_found_redirect(repo_name) + + def assert_repo_not_found_redirect(self, repo_name): + # run the check page that triggers the other flash message + response = self.app.get(h.url('repo_check_home', repo_name=repo_name)) + assert_session_flash( + response, 'The repository at %s cannot be located.' % repo_name) -@pytest.mark.usefixtures('pylonsapp') -class TestCreateReferenceData: +@pytest.fixture() +def summary_view(context_stub, request_stub, user_util): + """ + Bootstrap view to test the view functions + """ + request_stub.matched_route = AttributeDict(name='test_view') + + request_stub.user = user_util.create_user().AuthUser + request_stub.db_repo = user_util.create_repo() + + view = RepoSummaryView(context=context_stub, request=request_stub) + return view + + +@pytest.mark.usefixtures('app') +class TestCreateReferenceData(object): @pytest.fixture def example_refs(self): @@ -296,14 +307,13 @@ class TestCreateReferenceData: ] return example_refs - def test_generates_refs_based_on_commit_ids(self, example_refs): + def test_generates_refs_based_on_commit_ids(self, example_refs, summary_view): repo = mock.Mock() repo.name = 'test-repo' repo.alias = 'git' full_repo_name = 'pytest-repo-group/' + repo.name - controller = summary.SummaryController() - result = controller._create_reference_data( + result = summary_view._create_reference_data( repo, full_repo_name, example_refs) expected_files_url = '/{}/files/'.format(full_repo_name) @@ -332,13 +342,13 @@ class TestCreateReferenceData: }] assert result == expected_result - def test_generates_refs_with_path_for_svn(self, example_refs): + def test_generates_refs_with_path_for_svn(self, example_refs, summary_view): repo = mock.Mock() repo.name = 'test-repo' repo.alias = 'svn' full_repo_name = 'pytest-repo-group/' + repo.name - controller = summary.SummaryController() - result = controller._create_reference_data( + + result = summary_view._create_reference_data( repo, full_repo_name, example_refs) expected_files_url = '/{}/files/'.format(full_repo_name) @@ -372,35 +382,9 @@ class TestCreateReferenceData: assert result == expected_result -@pytest.mark.usefixtures("app") -class TestRepoLocation: - - @pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii']) - def test_manual_delete(self, autologin_user, backend, suffix, csrf_token): - repo = backend.create_repo(name_suffix=suffix) - repo_name = repo.repo_name - - # delete from file system - RepoModel()._delete_filesystem_repo(repo) - - # test if the repo is still in the database - new_repo = RepoModel().get_by_repo_name(repo_name) - assert new_repo.repo_name == repo_name +class TestCreateFilesUrl(object): - # check if repo is not in the filesystem - assert not repo_on_filesystem(repo_name) - self.assert_repo_not_found_redirect(repo_name) - - def assert_repo_not_found_redirect(self, repo_name): - # run the check page that triggers the other flash message - response = self.app.get(url('repo_check_home', repo_name=repo_name)) - assert_session_flash( - response, 'The repository at %s cannot be located.' % repo_name) - - -class TestCreateFilesUrl(object): - def test_creates_non_svn_url(self): - controller = summary.SummaryController() + def test_creates_non_svn_url(self, summary_view): repo = mock.Mock() repo.name = 'abcde' full_repo_name = 'test-repo-group/' + repo.name @@ -408,16 +392,15 @@ class TestCreateFilesUrl(object): raw_id = 'deadbeef0123456789' is_svn = False - with mock.patch.object(summary.h, 'url') as url_mock: - result = controller._create_files_url( + with mock.patch('rhodecode.lib.helpers.url') as url_mock: + result = summary_view._create_files_url( repo, full_repo_name, ref_name, raw_id, is_svn) url_mock.assert_called_once_with( 'files_home', repo_name=full_repo_name, f_path='', revision=ref_name, at=ref_name) assert result == url_mock.return_value - def test_creates_svn_url(self): - controller = summary.SummaryController() + def test_creates_svn_url(self, summary_view): repo = mock.Mock() repo.name = 'abcde' full_repo_name = 'test-repo-group/' + repo.name @@ -425,16 +408,15 @@ class TestCreateFilesUrl(object): raw_id = 'deadbeef0123456789' is_svn = True - with mock.patch.object(summary.h, 'url') as url_mock: - result = controller._create_files_url( + with mock.patch('rhodecode.lib.helpers.url') as url_mock: + result = summary_view._create_files_url( repo, full_repo_name, ref_name, raw_id, is_svn) url_mock.assert_called_once_with( 'files_home', repo_name=full_repo_name, f_path=ref_name, revision=raw_id, at=ref_name) assert result == url_mock.return_value - def test_name_has_slashes(self): - controller = summary.SummaryController() + def test_name_has_slashes(self, summary_view): repo = mock.Mock() repo.name = 'abcde' full_repo_name = 'test-repo-group/' + repo.name @@ -442,8 +424,8 @@ class TestCreateFilesUrl(object): raw_id = 'deadbeef0123456789' is_svn = False - with mock.patch.object(summary.h, 'url') as url_mock: - result = controller._create_files_url( + with mock.patch('rhodecode.lib.helpers.url') as url_mock: + result = summary_view._create_files_url( repo, full_repo_name, ref_name, raw_id, is_svn) url_mock.assert_called_once_with( 'files_home', repo_name=full_repo_name, f_path='', revision=raw_id, @@ -462,42 +444,39 @@ class TestReferenceItems(object): def _format_function(name, id_): return 'format_function_{}_{}'.format(name, id_) - def test_creates_required_amount_of_items(self): + def test_creates_required_amount_of_items(self, summary_view): amount = 100 refs = { 'ref{}'.format(i): '{0:040d}'.format(i) for i in range(amount) } - controller = summary.SummaryController() - - url_patcher = mock.patch.object( - controller, '_create_files_url') - svn_patcher = mock.patch.object( - summary.h, 'is_svn', return_value=False) + url_patcher = mock.patch.object(summary_view, '_create_files_url') + svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn', + return_value=False) with url_patcher as url_mock, svn_patcher: - result = controller._create_reference_items( + result = summary_view._create_reference_items( self.repo, self.repo_full_name, refs, self.ref_type, self._format_function) assert len(result) == amount assert url_mock.call_count == amount - def test_single_item_details(self): + def test_single_item_details(self, summary_view): ref_name = 'ref1' ref_id = 'deadbeef' refs = { ref_name: ref_id } - controller = summary.SummaryController() + svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn', + return_value=False) + url_patcher = mock.patch.object( - controller, '_create_files_url', return_value=self.fake_url) - svn_patcher = mock.patch.object( - summary.h, 'is_svn', return_value=False) + summary_view, '_create_files_url', return_value=self.fake_url) with url_patcher as url_mock, svn_patcher: - result = controller._create_reference_items( + result = summary_view._create_reference_items( self.repo, self.repo_full_name, refs, self.ref_type, self._format_function) diff --git a/rhodecode/tests/functional/test_tags.py b/rhodecode/apps/repository/tests/test_repo_tags.py rename from rhodecode/tests/functional/test_tags.py rename to rhodecode/apps/repository/tests/test_repo_tags.py --- a/rhodecode/tests/functional/test_tags.py +++ b/rhodecode/apps/repository/tests/test_repo_tags.py @@ -18,16 +18,27 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ +import pytest from rhodecode.model.db import Repository -from rhodecode.tests import * -class TestTagsController(TestController): +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'tags_home': '/{repo_name}/tags', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +@pytest.mark.usefixtures('autologin_user', 'app') +class TestTagsController(object): def test_index(self, backend): - self.log_user() - response = self.app.get(url(controller='tags', - action='index', - repo_name=backend.repo_name)) + response = self.app.get( + route_path('tags_home', repo_name=backend.repo_name)) repo = Repository.get_by_repo_name(backend.repo_name) diff --git a/rhodecode/apps/repository/tests/test_vcs_settings.py b/rhodecode/apps/repository/tests/test_vcs_settings.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/tests/test_vcs_settings.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 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 pytest + +import rhodecode +from rhodecode.model.settings import SettingsModel +from rhodecode.tests import url +from rhodecode.tests.utils import AssertResponse + + +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'edit_repo': '/{repo_name}/settings', + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +@pytest.mark.usefixtures('autologin_user', 'app') +class TestAdminRepoVcsSettings(object): + + @pytest.mark.parametrize('setting_name, setting_backends', [ + ('hg_use_rebase_for_merging', ['hg']), + ]) + def test_labs_settings_visible_if_enabled( + self, setting_name, setting_backends, backend): + if backend.alias not in setting_backends: + pytest.skip('Setting not available for backend {}'.format(backend)) + + vcs_settings_url = url( + 'repo_vcs_settings', repo_name=backend.repo.repo_name) + + with mock.patch.dict( + rhodecode.CONFIG, {'labs_settings_active': 'true'}): + response = self.app.get(vcs_settings_url) + + assertr = AssertResponse(response) + assertr.one_element_exists('#rhodecode_{}'.format(setting_name)) + + @pytest.mark.parametrize('setting_name, setting_backends', [ + ('hg_use_rebase_for_merging', ['hg']), + ]) + def test_labs_settings_not_visible_if_disabled( + self, setting_name, setting_backends, backend): + if backend.alias not in setting_backends: + pytest.skip('Setting not available for backend {}'.format(backend)) + + vcs_settings_url = url( + 'repo_vcs_settings', repo_name=backend.repo.repo_name) + + with mock.patch.dict( + rhodecode.CONFIG, {'labs_settings_active': 'false'}): + response = self.app.get(vcs_settings_url) + + assertr = AssertResponse(response) + assertr.no_element_exists('#rhodecode_{}'.format(setting_name)) + + @pytest.mark.parametrize('setting_name, setting_backends', [ + ('hg_use_rebase_for_merging', ['hg']), + ]) + def test_update_boolean_settings( + self, csrf_token, setting_name, setting_backends, backend): + if backend.alias not in setting_backends: + pytest.skip('Setting not available for backend {}'.format(backend)) + + repo = backend.create_repo() + + settings_model = SettingsModel(repo=repo) + vcs_settings_url = url( + 'repo_vcs_settings', repo_name=repo.repo_name) + + self.app.post( + vcs_settings_url, + params={ + 'inherit_global_settings': False, + 'new_svn_branch': 'dummy-value-for-testing', + 'new_svn_tag': 'dummy-value-for-testing', + 'rhodecode_{}'.format(setting_name): 'true', + 'csrf_token': csrf_token, + }) + setting = settings_model.get_setting_by_name(setting_name) + assert setting.app_settings_value + + self.app.post( + vcs_settings_url, + params={ + 'inherit_global_settings': False, + 'new_svn_branch': 'dummy-value-for-testing', + 'new_svn_tag': 'dummy-value-for-testing', + 'rhodecode_{}'.format(setting_name): 'false', + 'csrf_token': csrf_token, + }) + setting = settings_model.get_setting_by_name(setting_name) + assert not setting.app_settings_value diff --git a/rhodecode/apps/repository/utils.py b/rhodecode/apps/repository/utils.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/utils.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 rhodecode.lib import helpers as h +from rhodecode.lib.utils2 import safe_int + + +def reviewer_as_json(user, reasons=None, mandatory=False): + """ + Returns json struct of a reviewer for frontend + + :param user: the reviewer + :param reasons: list of strings of why they are reviewers + :param mandatory: bool, to set user as mandatory + """ + + return { + 'user_id': user.user_id, + 'reasons': reasons or [], + 'mandatory': mandatory, + 'username': user.username, + 'first_name': user.first_name, + 'last_name': user.last_name, + 'gravatar_link': h.gravatar_url(user.email, 14), + } + + +def get_default_reviewers_data( + current_user, source_repo, source_commit, target_repo, target_commit): + + """ Return json for default reviewers of a repository """ + + reasons = ['Default reviewer', 'Repository owner'] + default = reviewer_as_json( + user=current_user, reasons=reasons, mandatory=False) + + return { + 'api_ver': 'v1', # define version for later possible schema upgrade + 'reviewers': [default], + 'rules': {}, + 'rules_data': {}, + } + + +def validate_default_reviewers(review_members, reviewer_rules): + """ + Function to validate submitted reviewers against the saved rules + + """ + reviewers = [] + reviewer_by_id = {} + for r in review_members: + reviewer_user_id = safe_int(r['user_id']) + entry = (reviewer_user_id, r['reasons'], r['mandatory']) + + reviewer_by_id[reviewer_user_id] = entry + reviewers.append(entry) + + return reviewers diff --git a/rhodecode/apps/repository/views/repo_bookmarks.py b/rhodecode/apps/repository/views/repo_bookmarks.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/views/repo_bookmarks.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2017 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 + +from pyramid.httpexceptions import HTTPNotFound +from pyramid.view import view_config + +from rhodecode.apps._base import BaseReferencesView +from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator) +from rhodecode.lib import helpers as h + +log = logging.getLogger(__name__) + + +class RepoBookmarksView(BaseReferencesView): + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @view_config( + route_name='bookmarks_home', request_method='GET', + renderer='rhodecode:templates/bookmarks/bookmarks.mako') + def bookmarks(self): + c = self.load_default_context() + + if not h.is_hg(self.db_repo): + raise HTTPNotFound() + + ref_items = self.rhodecode_vcs_repo.bookmarks.items() + self.load_refs_context( + ref_items=ref_items, partials_template='bookmarks/bookmarks_data.mako') + + return self._get_template_context(c) diff --git a/rhodecode/apps/repository/views/repo_branches.py b/rhodecode/apps/repository/views/repo_branches.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/views/repo_branches.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2017 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 +from pyramid.view import view_config + +from rhodecode.apps._base import BaseReferencesView +from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator) + + +log = logging.getLogger(__name__) + + +class RepoBranchesView(BaseReferencesView): + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @view_config( + route_name='branches_home', request_method='GET', + renderer='rhodecode:templates/branches/branches.mako') + def branches(self): + c = self.load_default_context() + c.closed_branches = self.rhodecode_vcs_repo.branches_closed + # NOTE(marcink): + # we need this trick because of PartialRenderer still uses the + # global 'c', we might not need this after full pylons migration + self._register_global_c(c) + + ref_items = self.rhodecode_vcs_repo.branches_all.items() + self.load_refs_context( + ref_items=ref_items, partials_template='branches/branches_data.mako') + + return self._get_template_context(c) diff --git a/rhodecode/apps/repository/views/repo_caches.py b/rhodecode/apps/repository/views/repo_caches.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/views/repo_caches.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2017 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 + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +from rhodecode.apps._base import RepoAppView +from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \ + CSRFRequired +from rhodecode.lib import helpers as h +from rhodecode.model.meta import Session +from rhodecode.model.scm import ScmModel + +log = logging.getLogger(__name__) + + +class RepoCachesView(RepoAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + + # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead + c.repo_info = self.db_repo + + self._register_global_c(c) + return c + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @view_config( + route_name='edit_repo_caches', request_method='GET', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def repo_caches(self): + c = self.load_default_context() + c.active = 'caches' + + return self._get_template_context(c) + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @CSRFRequired() + @view_config( + route_name='edit_repo_caches', request_method='POST') + def repo_caches_purge(self): + _ = self.request.translate + c = self.load_default_context() + c.active = 'caches' + + try: + ScmModel().mark_for_invalidation(self.db_repo_name, delete=True) + Session().commit() + h.flash(_('Cache invalidation successful'), + category='success') + except Exception: + log.exception("Exception during cache invalidation") + h.flash(_('An error occurred during cache invalidation'), + category='error') + + raise HTTPFound(h.route_path( + 'edit_repo_caches', repo_name=self.db_repo_name)) \ No newline at end of file diff --git a/rhodecode/apps/repository/views/repo_maintainance.py b/rhodecode/apps/repository/views/repo_maintainance.py --- a/rhodecode/apps/repository/views/repo_maintainance.py +++ b/rhodecode/apps/repository/views/repo_maintainance.py @@ -41,7 +41,6 @@ class RepoMaintenanceView(RepoAppView): return c @LoginRequired() - @NotAnonymous() @HasRepoPermissionAnyDecorator('repository.admin') @view_config( route_name='repo_maintenance', request_method='GET', @@ -54,7 +53,6 @@ class RepoMaintenanceView(RepoAppView): return self._get_template_context(c) @LoginRequired() - @NotAnonymous() @HasRepoPermissionAnyDecorator('repository.admin') @view_config( route_name='repo_maintenance_execute', request_method='GET', diff --git a/rhodecode/apps/repository/views/repo_permissions.py b/rhodecode/apps/repository/views/repo_permissions.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/views/repo_permissions.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2017 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 deform +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +from rhodecode.apps._base import RepoAppView +from rhodecode.forms import RcForm +from rhodecode.lib import helpers as h +from rhodecode.lib import audit_logger +from rhodecode.lib.auth import ( + LoginRequired, HasRepoPermissionAnyDecorator, + HasRepoPermissionAllDecorator, CSRFRequired) +from rhodecode.model.db import RepositoryField, RepoGroup +from rhodecode.model.forms import RepoPermsForm +from rhodecode.model.meta import Session +from rhodecode.model.repo import RepoModel +from rhodecode.model.scm import RepoGroupList, ScmModel +from rhodecode.model.validation_schema.schemas import repo_schema + +log = logging.getLogger(__name__) + + +class RepoSettingsPermissionsView(RepoAppView): + + def load_default_context(self): + c = self._get_local_tmpl_context() + + # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead + c.repo_info = self.db_repo + + self._register_global_c(c) + return c + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @view_config( + route_name='edit_repo_perms', request_method='GET', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def edit_permissions(self): + c = self.load_default_context() + c.active = 'permissions' + return self._get_template_context(c) + + @LoginRequired() + @HasRepoPermissionAllDecorator('repository.admin') + @CSRFRequired() + @view_config( + route_name='edit_repo_perms', request_method='POST', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def edit_permissions_update(self): + _ = self.request.translate + c = self.load_default_context() + c.active = 'permissions' + data = self.request.POST + # store private flag outside of HTML to verify if we can modify + # default user permissions, prevents submition of FAKE post data + # into the form for private repos + data['repo_private'] = self.db_repo.private + form = RepoPermsForm()().to_python(data) + changes = RepoModel().update_permissions( + self.db_repo_name, form['perm_additions'], form['perm_updates'], + form['perm_deletions']) + + action_data = { + 'added': changes['added'], + 'updated': changes['updated'], + 'deleted': changes['deleted'], + } + audit_logger.store_web( + 'repo.edit.permissions', action_data=action_data, + user=self._rhodecode_user, repo=self.db_repo) + + Session().commit() + h.flash(_('Repository permissions updated'), category='success') + + raise HTTPFound( + self.request.route_path('edit_repo_perms', repo_name=self.db_repo_name)) diff --git a/rhodecode/apps/repository/views/repo_pull_requests.py b/rhodecode/apps/repository/views/repo_pull_requests.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/views/repo_pull_requests.py @@ -0,0 +1,584 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2017 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 collections +from pyramid.httpexceptions import HTTPFound, HTTPNotFound +from pyramid.view import view_config + +from rhodecode.apps._base import RepoAppView, DataGridAppView +from rhodecode.lib import helpers as h, diffs, codeblocks +from rhodecode.lib.auth import ( + LoginRequired, HasRepoPermissionAnyDecorator) +from rhodecode.lib.utils import PartialRenderer +from rhodecode.lib.utils2 import str2bool, safe_int, safe_str +from rhodecode.lib.vcs.backends.base import EmptyCommit +from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, \ + RepositoryRequirementError, NodeDoesNotExistError +from rhodecode.model.comment import CommentsModel +from rhodecode.model.db import PullRequest, PullRequestVersion, \ + ChangesetComment, ChangesetStatus +from rhodecode.model.pull_request import PullRequestModel, MergeCheck + +log = logging.getLogger(__name__) + + +class RepoPullRequestsView(RepoAppView, DataGridAppView): + + def load_default_context(self): + c = self._get_local_tmpl_context(include_app_defaults=True) + # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead + c.repo_info = self.db_repo + c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED + c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED + self._register_global_c(c) + return c + + def _get_pull_requests_list( + self, repo_name, source, filter_type, opened_by, statuses): + + draw, start, limit = self._extract_chunk(self.request) + search_q, order_by, order_dir = self._extract_ordering(self.request) + _render = PartialRenderer('data_table/_dt_elements.mako') + + # pagination + + if filter_type == 'awaiting_review': + pull_requests = PullRequestModel().get_awaiting_review( + repo_name, source=source, opened_by=opened_by, + statuses=statuses, offset=start, length=limit, + order_by=order_by, order_dir=order_dir) + pull_requests_total_count = PullRequestModel().count_awaiting_review( + repo_name, source=source, statuses=statuses, + opened_by=opened_by) + elif filter_type == 'awaiting_my_review': + pull_requests = PullRequestModel().get_awaiting_my_review( + repo_name, source=source, opened_by=opened_by, + user_id=self._rhodecode_user.user_id, statuses=statuses, + offset=start, length=limit, order_by=order_by, + order_dir=order_dir) + pull_requests_total_count = PullRequestModel().count_awaiting_my_review( + repo_name, source=source, user_id=self._rhodecode_user.user_id, + statuses=statuses, opened_by=opened_by) + else: + pull_requests = PullRequestModel().get_all( + repo_name, source=source, opened_by=opened_by, + statuses=statuses, offset=start, length=limit, + order_by=order_by, order_dir=order_dir) + pull_requests_total_count = PullRequestModel().count_all( + repo_name, source=source, statuses=statuses, + opened_by=opened_by) + + data = [] + comments_model = CommentsModel() + for pr in pull_requests: + comments = comments_model.get_all_comments( + self.db_repo.repo_id, pull_request=pr) + + data.append({ + 'name': _render('pullrequest_name', + pr.pull_request_id, pr.target_repo.repo_name), + 'name_raw': pr.pull_request_id, + 'status': _render('pullrequest_status', + pr.calculated_review_status()), + 'title': _render( + 'pullrequest_title', pr.title, pr.description), + 'description': h.escape(pr.description), + 'updated_on': _render('pullrequest_updated_on', + h.datetime_to_time(pr.updated_on)), + 'updated_on_raw': h.datetime_to_time(pr.updated_on), + 'created_on': _render('pullrequest_updated_on', + h.datetime_to_time(pr.created_on)), + 'created_on_raw': h.datetime_to_time(pr.created_on), + 'author': _render('pullrequest_author', + pr.author.full_contact, ), + 'author_raw': pr.author.full_name, + 'comments': _render('pullrequest_comments', len(comments)), + 'comments_raw': len(comments), + 'closed': pr.is_closed(), + }) + + data = ({ + 'draw': draw, + 'data': data, + 'recordsTotal': pull_requests_total_count, + 'recordsFiltered': pull_requests_total_count, + }) + return data + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @view_config( + route_name='pullrequest_show_all', request_method='GET', + renderer='rhodecode:templates/pullrequests/pullrequests.mako') + def pull_request_list(self): + c = self.load_default_context() + + req_get = self.request.GET + c.source = str2bool(req_get.get('source')) + c.closed = str2bool(req_get.get('closed')) + c.my = str2bool(req_get.get('my')) + c.awaiting_review = str2bool(req_get.get('awaiting_review')) + c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review')) + + c.active = 'open' + if c.my: + c.active = 'my' + if c.closed: + c.active = 'closed' + if c.awaiting_review and not c.source: + c.active = 'awaiting' + if c.source and not c.awaiting_review: + c.active = 'source' + if c.awaiting_my_review: + c.active = 'awaiting_my' + + return self._get_template_context(c) + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @view_config( + route_name='pullrequest_show_all_data', request_method='GET', + renderer='json_ext', xhr=True) + def pull_request_list_data(self): + + # additional filters + req_get = self.request.GET + source = str2bool(req_get.get('source')) + closed = str2bool(req_get.get('closed')) + my = str2bool(req_get.get('my')) + awaiting_review = str2bool(req_get.get('awaiting_review')) + awaiting_my_review = str2bool(req_get.get('awaiting_my_review')) + + filter_type = 'awaiting_review' if awaiting_review \ + else 'awaiting_my_review' if awaiting_my_review \ + else None + + opened_by = None + if my: + opened_by = [self._rhodecode_user.user_id] + + statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN] + if closed: + statuses = [PullRequest.STATUS_CLOSED] + + data = self._get_pull_requests_list( + repo_name=self.db_repo_name, source=source, + filter_type=filter_type, opened_by=opened_by, statuses=statuses) + + return data + + def _get_pr_version(self, pull_request_id, version=None): + pull_request_id = safe_int(pull_request_id) + at_version = None + + if version and version == 'latest': + pull_request_ver = PullRequest.get(pull_request_id) + pull_request_obj = pull_request_ver + _org_pull_request_obj = pull_request_obj + at_version = 'latest' + elif version: + pull_request_ver = PullRequestVersion.get_or_404(version) + pull_request_obj = pull_request_ver + _org_pull_request_obj = pull_request_ver.pull_request + at_version = pull_request_ver.pull_request_version_id + else: + _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404( + pull_request_id) + + pull_request_display_obj = PullRequest.get_pr_display_object( + pull_request_obj, _org_pull_request_obj) + + return _org_pull_request_obj, pull_request_obj, \ + pull_request_display_obj, at_version + + def _get_diffset(self, source_repo_name, source_repo, + source_ref_id, target_ref_id, + target_commit, source_commit, diff_limit, fulldiff, + file_limit, display_inline_comments): + + vcs_diff = PullRequestModel().get_diff( + source_repo, source_ref_id, target_ref_id) + + diff_processor = diffs.DiffProcessor( + vcs_diff, format='newdiff', diff_limit=diff_limit, + file_limit=file_limit, show_full_diff=fulldiff) + + _parsed = diff_processor.prepare() + + def _node_getter(commit): + def get_node(fname): + try: + return commit.get_node(fname) + except NodeDoesNotExistError: + return None + + return get_node + + diffset = codeblocks.DiffSet( + repo_name=self.db_repo_name, + source_repo_name=source_repo_name, + source_node_getter=_node_getter(target_commit), + target_node_getter=_node_getter(source_commit), + comments=display_inline_comments + ) + diffset = diffset.render_patchset( + _parsed, target_commit.raw_id, source_commit.raw_id) + + return diffset + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + # @view_config( + # route_name='pullrequest_show', request_method='GET', + # renderer='rhodecode:templates/pullrequests/pullrequest_show.mako') + def pull_request_show(self): + pull_request_id = safe_int( + self.request.matchdict.get('pull_request_id')) + c = self.load_default_context() + + version = self.request.GET.get('version') + from_version = self.request.GET.get('from_version') or version + merge_checks = self.request.GET.get('merge_checks') + c.fulldiff = str2bool(self.request.GET.get('fulldiff')) + + (pull_request_latest, + pull_request_at_ver, + pull_request_display_obj, + at_version) = self._get_pr_version( + pull_request_id, version=version) + pr_closed = pull_request_latest.is_closed() + + if pr_closed and (version or from_version): + # not allow to browse versions + raise HTTPFound(h.route_path( + 'pullrequest_show', repo_name=self.db_repo_name, + pull_request_id=pull_request_id)) + + versions = pull_request_display_obj.versions() + + c.at_version = at_version + c.at_version_num = (at_version + if at_version and at_version != 'latest' + else None) + c.at_version_pos = ChangesetComment.get_index_from_version( + c.at_version_num, versions) + + (prev_pull_request_latest, + prev_pull_request_at_ver, + prev_pull_request_display_obj, + prev_at_version) = self._get_pr_version( + pull_request_id, version=from_version) + + c.from_version = prev_at_version + c.from_version_num = (prev_at_version + if prev_at_version and prev_at_version != 'latest' + else None) + c.from_version_pos = ChangesetComment.get_index_from_version( + c.from_version_num, versions) + + # define if we're in COMPARE mode or VIEW at version mode + compare = at_version != prev_at_version + + # pull_requests repo_name we opened it against + # ie. target_repo must match + if self.db_repo_name != pull_request_at_ver.target_repo.repo_name: + raise HTTPNotFound() + + c.shadow_clone_url = PullRequestModel().get_shadow_clone_url( + pull_request_at_ver) + + c.pull_request = pull_request_display_obj + c.pull_request_latest = pull_request_latest + + if compare or (at_version and not at_version == 'latest'): + c.allowed_to_change_status = False + c.allowed_to_update = False + c.allowed_to_merge = False + c.allowed_to_delete = False + c.allowed_to_comment = False + c.allowed_to_close = False + else: + can_change_status = PullRequestModel().check_user_change_status( + pull_request_at_ver, self._rhodecode_user) + c.allowed_to_change_status = can_change_status and not pr_closed + + c.allowed_to_update = PullRequestModel().check_user_update( + pull_request_latest, self._rhodecode_user) and not pr_closed + c.allowed_to_merge = PullRequestModel().check_user_merge( + pull_request_latest, self._rhodecode_user) and not pr_closed + c.allowed_to_delete = PullRequestModel().check_user_delete( + pull_request_latest, self._rhodecode_user) and not pr_closed + c.allowed_to_comment = not pr_closed + c.allowed_to_close = c.allowed_to_merge and not pr_closed + + c.forbid_adding_reviewers = False + c.forbid_author_to_review = False + c.forbid_commit_author_to_review = False + + if pull_request_latest.reviewer_data and \ + 'rules' in pull_request_latest.reviewer_data: + rules = pull_request_latest.reviewer_data['rules'] or {} + try: + c.forbid_adding_reviewers = rules.get( + 'forbid_adding_reviewers') + c.forbid_author_to_review = rules.get( + 'forbid_author_to_review') + c.forbid_commit_author_to_review = rules.get( + 'forbid_commit_author_to_review') + except Exception: + pass + + # check merge capabilities + _merge_check = MergeCheck.validate( + pull_request_latest, user=self._rhodecode_user) + c.pr_merge_errors = _merge_check.error_details + c.pr_merge_possible = not _merge_check.failed + c.pr_merge_message = _merge_check.merge_msg + + c.pull_request_review_status = _merge_check.review_status + if merge_checks: + self.request.override_renderer = \ + 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako' + return self._get_template_context(c) + + comments_model = CommentsModel() + + # reviewers and statuses + c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses() + allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers] + + # GENERAL COMMENTS with versions # + q = comments_model._all_general_comments_of_pull_request(pull_request_latest) + q = q.order_by(ChangesetComment.comment_id.asc()) + general_comments = q + + # pick comments we want to render at current version + c.comment_versions = comments_model.aggregate_comments( + general_comments, versions, c.at_version_num) + c.comments = c.comment_versions[c.at_version_num]['until'] + + # INLINE COMMENTS with versions # + q = comments_model._all_inline_comments_of_pull_request(pull_request_latest) + q = q.order_by(ChangesetComment.comment_id.asc()) + inline_comments = q + + c.inline_versions = comments_model.aggregate_comments( + inline_comments, versions, c.at_version_num, inline=True) + + # inject latest version + latest_ver = PullRequest.get_pr_display_object( + pull_request_latest, pull_request_latest) + + c.versions = versions + [latest_ver] + + # if we use version, then do not show later comments + # than current version + display_inline_comments = collections.defaultdict( + lambda: collections.defaultdict(list)) + for co in inline_comments: + if c.at_version_num: + # pick comments that are at least UPTO given version, so we + # don't render comments for higher version + should_render = co.pull_request_version_id and \ + co.pull_request_version_id <= c.at_version_num + else: + # showing all, for 'latest' + should_render = True + + if should_render: + display_inline_comments[co.f_path][co.line_no].append(co) + + # load diff data into template context, if we use compare mode then + # diff is calculated based on changes between versions of PR + + source_repo = pull_request_at_ver.source_repo + source_ref_id = pull_request_at_ver.source_ref_parts.commit_id + + target_repo = pull_request_at_ver.target_repo + target_ref_id = pull_request_at_ver.target_ref_parts.commit_id + + if compare: + # in compare switch the diff base to latest commit from prev version + target_ref_id = prev_pull_request_display_obj.revisions[0] + + # despite opening commits for bookmarks/branches/tags, we always + # convert this to rev to prevent changes after bookmark or branch change + c.source_ref_type = 'rev' + c.source_ref = source_ref_id + + c.target_ref_type = 'rev' + c.target_ref = target_ref_id + + c.source_repo = source_repo + c.target_repo = target_repo + + c.commit_ranges = [] + source_commit = EmptyCommit() + target_commit = EmptyCommit() + c.missing_requirements = False + + source_scm = source_repo.scm_instance() + target_scm = target_repo.scm_instance() + + # try first shadow repo, fallback to regular repo + try: + commits_source_repo = pull_request_latest.get_shadow_repo() + except Exception: + log.debug('Failed to get shadow repo', exc_info=True) + commits_source_repo = source_scm + + c.commits_source_repo = commits_source_repo + commit_cache = {} + try: + pre_load = ["author", "branch", "date", "message"] + show_revs = pull_request_at_ver.revisions + for rev in show_revs: + comm = commits_source_repo.get_commit( + commit_id=rev, pre_load=pre_load) + c.commit_ranges.append(comm) + commit_cache[comm.raw_id] = comm + + # Order here matters, we first need to get target, and then + # the source + target_commit = commits_source_repo.get_commit( + commit_id=safe_str(target_ref_id)) + + source_commit = commits_source_repo.get_commit( + commit_id=safe_str(source_ref_id)) + + except CommitDoesNotExistError: + log.warning( + 'Failed to get commit from `{}` repo'.format( + commits_source_repo), exc_info=True) + except RepositoryRequirementError: + log.warning( + 'Failed to get all required data from repo', exc_info=True) + c.missing_requirements = True + + c.ancestor = None # set it to None, to hide it from PR view + + try: + ancestor_id = source_scm.get_common_ancestor( + source_commit.raw_id, target_commit.raw_id, target_scm) + c.ancestor_commit = source_scm.get_commit(ancestor_id) + except Exception: + c.ancestor_commit = None + + c.statuses = source_repo.statuses( + [x.raw_id for x in c.commit_ranges]) + + # auto collapse if we have more than limit + collapse_limit = diffs.DiffProcessor._collapse_commits_over + c.collapse_all_commits = len(c.commit_ranges) > collapse_limit + c.compare_mode = compare + + # diff_limit is the old behavior, will cut off the whole diff + # if the limit is applied otherwise will just hide the + # big files from the front-end + diff_limit = c.visual.cut_off_limit_diff + file_limit = c.visual.cut_off_limit_file + + c.missing_commits = False + if (c.missing_requirements + or isinstance(source_commit, EmptyCommit) + or source_commit == target_commit): + + c.missing_commits = True + else: + + c.diffset = self._get_diffset( + c.source_repo.repo_name, commits_source_repo, + source_ref_id, target_ref_id, + target_commit, source_commit, + diff_limit, c.fulldiff, file_limit, display_inline_comments) + + c.limited_diff = c.diffset.limited_diff + + # calculate removed files that are bound to comments + comment_deleted_files = [ + fname for fname in display_inline_comments + if fname not in c.diffset.file_stats] + + c.deleted_files_comments = collections.defaultdict(dict) + for fname, per_line_comments in display_inline_comments.items(): + if fname in comment_deleted_files: + c.deleted_files_comments[fname]['stats'] = 0 + c.deleted_files_comments[fname]['comments'] = list() + for lno, comments in per_line_comments.items(): + c.deleted_files_comments[fname]['comments'].extend( + comments) + + # this is a hack to properly display links, when creating PR, the + # compare view and others uses different notation, and + # compare_commits.mako renders links based on the target_repo. + # We need to swap that here to generate it properly on the html side + c.target_repo = c.source_repo + + c.commit_statuses = ChangesetStatus.STATUSES + + c.show_version_changes = not pr_closed + if c.show_version_changes: + cur_obj = pull_request_at_ver + prev_obj = prev_pull_request_at_ver + + old_commit_ids = prev_obj.revisions + new_commit_ids = cur_obj.revisions + commit_changes = PullRequestModel()._calculate_commit_id_changes( + old_commit_ids, new_commit_ids) + c.commit_changes_summary = commit_changes + + # calculate the diff for commits between versions + c.commit_changes = [] + mark = lambda cs, fw: list( + h.itertools.izip_longest([], cs, fillvalue=fw)) + for c_type, raw_id in mark(commit_changes.added, 'a') \ + + mark(commit_changes.removed, 'r') \ + + mark(commit_changes.common, 'c'): + + if raw_id in commit_cache: + commit = commit_cache[raw_id] + else: + try: + commit = commits_source_repo.get_commit(raw_id) + except CommitDoesNotExistError: + # in case we fail extracting still use "dummy" commit + # for display in commit diff + commit = h.AttributeDict( + {'raw_id': raw_id, + 'message': 'EMPTY or MISSING COMMIT'}) + c.commit_changes.append([c_type, commit]) + + # current user review statuses for each version + c.review_versions = {} + if self._rhodecode_user.user_id in allowed_reviewers: + for co in general_comments: + if co.author.user_id == self._rhodecode_user.user_id: + # each comment has a status change + status = co.status_change + if status: + _ver_pr = status[0].comment.pull_request_version_id + c.review_versions[_ver_pr] = status[0] + + return self._get_template_context(c) diff --git a/rhodecode/apps/repository/views/repo_review_rules.py b/rhodecode/apps/repository/views/repo_review_rules.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/views/repo_review_rules.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 + +from pyramid.view import view_config + +from rhodecode.apps._base import RepoAppView +from rhodecode.apps.repository.utils import get_default_reviewers_data +from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator + +log = logging.getLogger(__name__) + + +class RepoReviewRulesView(RepoAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + + # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead + c.repo_info = self.db_repo + + self._register_global_c(c) + return c + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @view_config( + route_name='repo_reviewers', request_method='GET', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def repo_review_rules(self): + c = self.load_default_context() + c.active = 'reviewers' + + return self._get_template_context(c) + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @view_config( + route_name='repo_default_reviewers_data', request_method='GET', + renderer='json_ext') + def repo_default_reviewers_data(self): + review_data = get_default_reviewers_data( + self.db_repo.user, None, None, None, None) + return review_data + + diff --git a/rhodecode/apps/repository/views/repo_settings.py b/rhodecode/apps/repository/views/repo_settings.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/views/repo_settings.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2017 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 deform +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +from rhodecode.apps._base import RepoAppView +from rhodecode.forms import RcForm +from rhodecode.lib import helpers as h +from rhodecode.lib import audit_logger +from rhodecode.lib.auth import ( + LoginRequired, HasRepoPermissionAnyDecorator, + HasRepoPermissionAllDecorator, CSRFRequired) +from rhodecode.model.db import RepositoryField, RepoGroup +from rhodecode.model.meta import Session +from rhodecode.model.repo import RepoModel +from rhodecode.model.scm import RepoGroupList, ScmModel +from rhodecode.model.validation_schema.schemas import repo_schema + +log = logging.getLogger(__name__) + + +class RepoSettingsView(RepoAppView): + + def load_default_context(self): + c = self._get_local_tmpl_context() + + # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead + c.repo_info = self.db_repo + + acl_groups = RepoGroupList( + RepoGroup.query().all(), + perm_set=['group.write', 'group.admin']) + c.repo_groups = RepoGroup.groups_choices(groups=acl_groups) + c.repo_groups_choices = map(lambda k: k[0], c.repo_groups) + + # in case someone no longer have a group.write access to a repository + # pre fill the list with this entry, we don't care if this is the same + # but it will allow saving repo data properly. + repo_group = self.db_repo.group + if repo_group and repo_group.group_id not in c.repo_groups_choices: + c.repo_groups_choices.append(repo_group.group_id) + c.repo_groups.append(RepoGroup._generate_choice(repo_group)) + + if c.repository_requirements_missing or self.rhodecode_vcs_repo is None: + # we might be in missing requirement state, so we load things + # without touching scm_instance() + c.landing_revs_choices, c.landing_revs = \ + ScmModel().get_repo_landing_revs() + else: + c.landing_revs_choices, c.landing_revs = \ + ScmModel().get_repo_landing_revs(self.db_repo) + + c.personal_repo_group = c.auth_user.personal_repo_group + c.repo_fields = RepositoryField.query()\ + .filter(RepositoryField.repository == self.db_repo).all() + + self._register_global_c(c) + return c + + def _get_schema(self, c, old_values=None): + return repo_schema.RepoSettingsSchema().bind( + repo_type=self.db_repo.repo_type, + repo_type_options=[self.db_repo.repo_type], + repo_ref_options=c.landing_revs_choices, + repo_ref_items=c.landing_revs, + repo_repo_group_options=c.repo_groups_choices, + repo_repo_group_items=c.repo_groups, + # user caller + user=self._rhodecode_user, + old_values=old_values + ) + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @view_config( + route_name='edit_repo', request_method='GET', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def edit_settings(self): + c = self.load_default_context() + c.active = 'settings' + + defaults = RepoModel()._get_defaults(self.db_repo_name) + defaults['repo_owner'] = defaults['user'] + defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev'] + + schema = self._get_schema(c) + c.form = RcForm(schema, appstruct=defaults) + return self._get_template_context(c) + + @LoginRequired() + @HasRepoPermissionAllDecorator('repository.admin') + @CSRFRequired() + @view_config( + route_name='edit_repo', request_method='POST', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def edit_settings_update(self): + _ = self.request.translate + c = self.load_default_context() + c.active = 'settings' + old_repo_name = self.db_repo_name + + old_values = self.db_repo.get_api_data() + schema = self._get_schema(c, old_values=old_values) + + c.form = RcForm(schema) + pstruct = self.request.POST.items() + pstruct.append(('repo_type', self.db_repo.repo_type)) + try: + schema_data = c.form.validate(pstruct) + except deform.ValidationFailure as err_form: + return self._get_template_context(c) + + # data is now VALID, proceed with updates + # save validated data back into the updates dict + validated_updates = dict( + repo_name=schema_data['repo_group']['repo_name_without_group'], + repo_group=schema_data['repo_group']['repo_group_id'], + + user=schema_data['repo_owner'], + repo_description=schema_data['repo_description'], + repo_private=schema_data['repo_private'], + clone_uri=schema_data['repo_clone_uri'], + repo_landing_rev=schema_data['repo_landing_commit_ref'], + repo_enable_statistics=schema_data['repo_enable_statistics'], + repo_enable_locking=schema_data['repo_enable_locking'], + repo_enable_downloads=schema_data['repo_enable_downloads'], + ) + # detect if CLONE URI changed, if we get OLD means we keep old values + if schema_data['repo_clone_uri_change'] == 'OLD': + validated_updates['clone_uri'] = self.db_repo.clone_uri + + # use the new full name for redirect + new_repo_name = schema_data['repo_group']['repo_name_with_group'] + + # save extra fields into our validated data + for key, value in pstruct: + if key.startswith(RepositoryField.PREFIX): + validated_updates[key] = value + + try: + RepoModel().update(self.db_repo, **validated_updates) + ScmModel().mark_for_invalidation(new_repo_name) + + audit_logger.store_web( + 'repo.edit', action_data={'old_data': old_values}, + user=self._rhodecode_user, repo=self.db_repo) + + Session().commit() + + h.flash(_('Repository {} updated successfully').format( + old_repo_name), category='success') + except Exception: + log.exception("Exception during update of repository") + h.flash(_('Error occurred during update of repository {}').format( + old_repo_name), category='error') + + raise HTTPFound( + self.request.route_path('edit_repo', repo_name=new_repo_name)) diff --git a/rhodecode/apps/repository/views/repo_settings_advanced.py b/rhodecode/apps/repository/views/repo_settings_advanced.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/views/repo_settings_advanced.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2017 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 + +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPFound + +from rhodecode.apps._base import RepoAppView +from rhodecode.lib import helpers as h +from rhodecode.lib import audit_logger +from rhodecode.lib.auth import ( + LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired) +from rhodecode.lib.exceptions import AttachedForksError +from rhodecode.lib.utils2 import safe_int +from rhodecode.lib.vcs import RepositoryError +from rhodecode.model.db import Session, UserFollowing, User, Repository +from rhodecode.model.repo import RepoModel +from rhodecode.model.scm import ScmModel + +log = logging.getLogger(__name__) + + +class RepoSettingsView(RepoAppView): + + def load_default_context(self): + c = self._get_local_tmpl_context() + + # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead + c.repo_info = self.db_repo + + self._register_global_c(c) + return c + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @view_config( + route_name='edit_repo_advanced', request_method='GET', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def edit_advanced(self): + c = self.load_default_context() + c.active = 'advanced' + + c.default_user_id = User.get_default_user().user_id + c.in_public_journal = UserFollowing.query() \ + .filter(UserFollowing.user_id == c.default_user_id) \ + .filter(UserFollowing.follows_repository == c.repo_info).scalar() + + c.has_origin_repo_read_perm = False + if self.db_repo.fork: + c.has_origin_repo_read_perm = h.HasRepoPermissionAny( + 'repository.write', 'repository.read', 'repository.admin')( + self.db_repo.fork.repo_name, 'repo set as fork page') + + return self._get_template_context(c) + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @CSRFRequired() + @view_config( + route_name='edit_repo_advanced_delete', request_method='POST', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def edit_advanced_delete(self): + """ + Deletes the repository, or shows warnings if deletion is not possible + because of attached forks or other errors. + """ + _ = self.request.translate + handle_forks = self.request.POST.get('forks', None) + + try: + _forks = self.db_repo.forks.count() + if _forks and handle_forks: + if handle_forks == 'detach_forks': + handle_forks = 'detach' + h.flash(_('Detached %s forks') % _forks, category='success') + elif handle_forks == 'delete_forks': + handle_forks = 'delete' + h.flash(_('Deleted %s forks') % _forks, category='success') + + old_data = self.db_repo.get_api_data() + RepoModel().delete(self.db_repo, forks=handle_forks) + + repo = audit_logger.RepoWrap(repo_id=None, + repo_name=self.db_repo.repo_name) + audit_logger.store_web( + 'repo.delete', action_data={'old_data': old_data}, + user=self._rhodecode_user, repo=repo) + + ScmModel().mark_for_invalidation(self.db_repo_name, delete=True) + h.flash( + _('Deleted repository `%s`') % self.db_repo_name, + category='success') + Session().commit() + except AttachedForksError: + repo_advanced_url = h.route_path( + 'edit_repo_advanced', repo_name=self.db_repo_name, + _anchor='advanced-delete') + delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url) + h.flash(_('Cannot delete `{repo}` it still contains attached forks. ' + 'Try using {delete_or_detach} option.') + .format(repo=self.db_repo_name, delete_or_detach=delete_anchor), + category='warning') + + # redirect to advanced for forks handle action ? + raise HTTPFound(repo_advanced_url) + + except Exception: + log.exception("Exception during deletion of repository") + h.flash(_('An error occurred during deletion of `%s`') + % self.db_repo_name, category='error') + # redirect to advanced for more deletion options + raise HTTPFound( + h.route_path('edit_repo_advanced', repo_name=self.db_repo_name), + _anchor='advanced-delete') + + raise HTTPFound(h.route_path('home')) + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @CSRFRequired() + @view_config( + route_name='edit_repo_advanced_journal', request_method='POST', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def edit_advanced_journal(self): + """ + Set's this repository to be visible in public journal, + in other words making default user to follow this repo + """ + _ = self.request.translate + + try: + user_id = User.get_default_user().user_id + ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id) + h.flash(_('Updated repository visibility in public journal'), + category='success') + Session().commit() + except Exception: + h.flash(_('An error occurred during setting this ' + 'repository in public journal'), + category='error') + + raise HTTPFound( + h.route_path('edit_repo_advanced', repo_name=self.db_repo_name)) + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @CSRFRequired() + @view_config( + route_name='edit_repo_advanced_fork', request_method='POST', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def edit_advanced_fork(self): + """ + Mark given repository as a fork of another + """ + _ = self.request.translate + + new_fork_id = self.request.POST.get('id_fork_of') + try: + + if new_fork_id and not new_fork_id.isdigit(): + log.error('Given fork id %s is not an INT', new_fork_id) + + fork_id = safe_int(new_fork_id) + repo = ScmModel().mark_as_fork( + self.db_repo_name, fork_id, self._rhodecode_user.user_id) + fork = repo.fork.repo_name if repo.fork else _('Nothing') + Session().commit() + h.flash(_('Marked repo %s as fork of %s') % (self.db_repo_name, fork), + category='success') + except RepositoryError as e: + log.exception("Repository Error occurred") + h.flash(str(e), category='error') + except Exception as e: + log.exception("Exception while editing fork") + h.flash(_('An error occurred during this operation'), + category='error') + + raise HTTPFound( + h.route_path('edit_repo_advanced', repo_name=self.db_repo_name)) + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @CSRFRequired() + @view_config( + route_name='edit_repo_advanced_locking', request_method='POST', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def edit_advanced_locking(self): + """ + Toggle locking of repository + """ + _ = self.request.translate + set_lock = self.request.POST.get('set_lock') + set_unlock = self.request.POST.get('set_unlock') + + try: + if set_lock: + Repository.lock(self.db_repo, self._rhodecode_user.user_id, + lock_reason=Repository.LOCK_WEB) + h.flash(_('Locked repository'), category='success') + elif set_unlock: + Repository.unlock(self.db_repo) + h.flash(_('Unlocked repository'), category='success') + except Exception as e: + log.exception("Exception during unlocking") + h.flash(_('An error occurred during unlocking'), category='error') + + raise HTTPFound( + h.route_path('edit_repo_advanced', repo_name=self.db_repo_name)) diff --git a/rhodecode/apps/repository/views/strip.py b/rhodecode/apps/repository/views/repo_strip.py rename from rhodecode/apps/repository/views/strip.py rename to rhodecode/apps/repository/views/repo_strip.py --- a/rhodecode/apps/repository/views/strip.py +++ b/rhodecode/apps/repository/views/repo_strip.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2011-2017 RhodeCode GmbH +# Copyright (C) 2017-2017 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 @@ -22,9 +22,11 @@ import logging from pyramid.view import view_config from rhodecode.apps._base import RepoAppView +from rhodecode.lib import audit_logger +from rhodecode.lib import helpers as h from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator, - NotAnonymous) - + NotAnonymous, CSRFRequired) +from rhodecode.lib.ext_json import json log = logging.getLogger(__name__) @@ -40,7 +42,6 @@ class StripView(RepoAppView): return c @LoginRequired() - @NotAnonymous() @HasRepoPermissionAnyDecorator('repository.admin') @view_config( route_name='strip', request_method='GET', @@ -53,41 +54,39 @@ class StripView(RepoAppView): return self._get_template_context(c) @LoginRequired() - @NotAnonymous() @HasRepoPermissionAnyDecorator('repository.admin') + @CSRFRequired() @view_config( route_name='strip_check', request_method='POST', - renderer='json', xhr=True - ) + renderer='json', xhr=True) def strip_check(self): from rhodecode.lib.vcs.backends.base import EmptyCommit data = {} rp = self.request.POST for i in range(1, 11): - chset = 'changeset_id-%d'%(i,) + chset = 'changeset_id-%d' % (i,) check = rp.get(chset) + if check: data[i] = self.db_repo.get_changeset(rp[chset]) if isinstance(data[i], EmptyCommit): - data[i] = {'rev': None, 'commit': rp[chset]} + data[i] = {'rev': None, 'commit': h.escape(rp[chset])} else: - data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch, 'author': data[i].author, + data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch, + 'author': data[i].author, 'comment': data[i].message} else: break return data @LoginRequired() - @NotAnonymous() @HasRepoPermissionAnyDecorator('repository.admin') + @CSRFRequired() @view_config( route_name='strip_execute', request_method='POST', - renderer='json', xhr=True - ) + renderer='json', xhr=True) def strip_execute(self): - from rhodecode.model.scm import ScmModel - from rhodecode.lib.ext_json import json c = self.load_default_context() user = self._rhodecode_user @@ -95,16 +94,23 @@ class StripView(RepoAppView): data = {} for idx in rp: commit = json.loads(rp[idx]) - #If someone put two times the same branch + # If someone put two times the same branch if commit['branch'] in data.keys(): continue try: - ScmModel().strip(repo=c.repo_info, - commit_id=commit['rev'], branch=commit['branch']) - log.info('Stripped commit %s from repo `%s` by %s' % (commit['rev'], c.repo_info.repo_name, user)) + ScmModel().strip( + repo=c.repo_info, + commit_id=commit['rev'], branch=commit['branch']) + log.info('Stripped commit %s from repo `%s` by %s' % ( + commit['rev'], c.repo_info.repo_name, user)) data[commit['rev']] = True - except Exception, e: + + audit_logger.store_web( + 'repo.commit.strip', action_data={'commit_id': commit['rev']}, + repo=self.db_repo, user=self._rhodecode_user, commit=True) + + except Exception as e: data[commit['rev']] = False - log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s' % (commit['rev'], - c.repo_info.repo_name, user, e.message)) + log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s' % ( + commit['rev'], self.db_repo_name, user, e.message)) return data diff --git a/rhodecode/apps/repository/views/repo_summary.py b/rhodecode/apps/repository/views/repo_summary.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/views/repo_summary.py @@ -0,0 +1,368 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2017 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 string + +from pyramid.view import view_config + +from beaker.cache import cache_region + + +from rhodecode.controllers import utils + +from rhodecode.apps._base import RepoAppView +from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP) +from rhodecode.lib import caches, helpers as h +from rhodecode.lib.helpers import RepoPage +from rhodecode.lib.utils2 import safe_str, safe_int +from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator +from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links +from rhodecode.lib.ext_json import json +from rhodecode.lib.vcs.backends.base import EmptyCommit +from rhodecode.lib.vcs.exceptions import CommitError, EmptyRepositoryError +from rhodecode.model.db import Statistics, CacheKey, User +from rhodecode.model.meta import Session +from rhodecode.model.repo import ReadmeFinder +from rhodecode.model.scm import ScmModel + +log = logging.getLogger(__name__) + + +class RepoSummaryView(RepoAppView): + + def load_default_context(self): + c = self._get_local_tmpl_context(include_app_defaults=True) + + # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead + c.repo_info = self.db_repo + c.rhodecode_repo = None + if not c.repository_requirements_missing: + c.rhodecode_repo = self.rhodecode_vcs_repo + + self._register_global_c(c) + return c + + def _get_readme_data(self, db_repo, default_renderer): + repo_name = db_repo.repo_name + log.debug('Looking for README file') + + @cache_region('long_term') + def _generate_readme(cache_key): + readme_data = None + readme_node = None + readme_filename = None + commit = self._get_landing_commit_or_none(db_repo) + if commit: + log.debug("Searching for a README file.") + readme_node = ReadmeFinder(default_renderer).search(commit) + if readme_node: + relative_url = h.url('files_raw_home', + repo_name=repo_name, + revision=commit.raw_id, + f_path=readme_node.path) + readme_data = self._render_readme_or_none( + commit, readme_node, relative_url) + readme_filename = readme_node.path + return readme_data, readme_filename + + invalidator_context = CacheKey.repo_context_cache( + _generate_readme, repo_name, CacheKey.CACHE_TYPE_README) + + with invalidator_context as context: + context.invalidate() + computed = context.compute() + + return computed + + def _get_landing_commit_or_none(self, db_repo): + log.debug("Getting the landing commit.") + try: + commit = db_repo.get_landing_commit() + if not isinstance(commit, EmptyCommit): + return commit + else: + log.debug("Repository is empty, no README to render.") + except CommitError: + log.exception( + "Problem getting commit when trying to render the README.") + + def _render_readme_or_none(self, commit, readme_node, relative_url): + log.debug( + 'Found README file `%s` rendering...', readme_node.path) + renderer = MarkupRenderer() + try: + html_source = renderer.render( + readme_node.content, filename=readme_node.path) + if relative_url: + return relative_links(html_source, relative_url) + return html_source + except Exception: + log.exception( + "Exception while trying to render the README") + + def _load_commits_context(self, c): + p = safe_int(self.request.GET.get('page'), 1) + size = safe_int(self.request.GET.get('size'), 10) + + def url_generator(**kw): + query_params = { + 'size': size + } + query_params.update(kw) + return h.route_path( + 'repo_summary_commits', + repo_name=c.rhodecode_db_repo.repo_name, _query=query_params) + + pre_load = ['author', 'branch', 'date', 'message'] + try: + collection = self.rhodecode_vcs_repo.get_commits(pre_load=pre_load) + except EmptyRepositoryError: + collection = self.rhodecode_vcs_repo + + c.repo_commits = RepoPage( + collection, page=p, items_per_page=size, url=url_generator) + page_ids = [x.raw_id for x in c.repo_commits] + c.comments = self.db_repo.get_comments(page_ids) + c.statuses = self.db_repo.statuses(page_ids) + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @view_config( + route_name='repo_summary_commits', request_method='GET', + renderer='rhodecode:templates/summary/summary_commits.mako') + def summary_commits(self): + c = self.load_default_context() + self._load_commits_context(c) + return self._get_template_context(c) + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @view_config( + route_name='repo_summary', request_method='GET', + renderer='rhodecode:templates/summary/summary.mako') + @view_config( + route_name='repo_summary_slash', request_method='GET', + renderer='rhodecode:templates/summary/summary.mako') + def summary(self): + c = self.load_default_context() + + # Prepare the clone URL + username = '' + if self._rhodecode_user.username != User.DEFAULT_USER: + username = safe_str(self._rhodecode_user.username) + + _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl + if '{repo}' in _def_clone_uri: + _def_clone_uri_by_id = _def_clone_uri.replace( + '{repo}', '_{repoid}') + elif '{repoid}' in _def_clone_uri: + _def_clone_uri_by_id = _def_clone_uri.replace( + '_{repoid}', '{repo}') + + c.clone_repo_url = self.db_repo.clone_url( + user=username, uri_tmpl=_def_clone_uri) + c.clone_repo_url_id = self.db_repo.clone_url( + user=username, uri_tmpl=_def_clone_uri_by_id) + + # If enabled, get statistics data + + c.show_stats = bool(self.db_repo.enable_statistics) + + stats = Session().query(Statistics) \ + .filter(Statistics.repository == self.db_repo) \ + .scalar() + + c.stats_percentage = 0 + + if stats and stats.languages: + c.no_data = False is self.db_repo.enable_statistics + lang_stats_d = json.loads(stats.languages) + + # Sort first by decreasing count and second by the file extension, + # so we have a consistent output. + lang_stats_items = sorted(lang_stats_d.iteritems(), + key=lambda k: (-k[1], k[0]))[:10] + lang_stats = [(x, {"count": y, + "desc": LANGUAGES_EXTENSIONS_MAP.get(x)}) + for x, y in lang_stats_items] + + c.trending_languages = json.dumps(lang_stats) + else: + c.no_data = True + c.trending_languages = json.dumps({}) + + scm_model = ScmModel() + c.enable_downloads = self.db_repo.enable_downloads + c.repository_followers = scm_model.get_followers(self.db_repo) + c.repository_forks = scm_model.get_forks(self.db_repo) + c.repository_is_user_following = scm_model.is_following_repo( + self.db_repo_name, self._rhodecode_user.user_id) + + # first interaction with the VCS instance after here... + if c.repository_requirements_missing: + self.request.override_renderer = \ + 'rhodecode:templates/summary/missing_requirements.mako' + return self._get_template_context(c) + + c.readme_data, c.readme_file = \ + self._get_readme_data(self.db_repo, c.visual.default_renderer) + + # loads the summary commits template context + self._load_commits_context(c) + + return self._get_template_context(c) + + def get_request_commit_id(self): + return self.request.matchdict['commit_id'] + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @view_config( + route_name='repo_stats', request_method='GET', + renderer='json_ext') + def repo_stats(self): + commit_id = self.get_request_commit_id() + + _namespace = caches.get_repo_namespace_key( + caches.SUMMARY_STATS, self.db_repo_name) + show_stats = bool(self.db_repo.enable_statistics) + cache_manager = caches.get_cache_manager( + 'repo_cache_long', _namespace) + _cache_key = caches.compute_key_from_params( + self.db_repo_name, commit_id, show_stats) + + def compute_stats(): + code_stats = {} + size = 0 + try: + scm_instance = self.db_repo.scm_instance() + commit = scm_instance.get_commit(commit_id) + + for node in commit.get_filenodes_generator(): + size += node.size + if not show_stats: + continue + ext = string.lower(node.extension) + ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext) + if ext_info: + if ext in code_stats: + code_stats[ext]['count'] += 1 + else: + code_stats[ext] = {"count": 1, "desc": ext_info} + except EmptyRepositoryError: + pass + return {'size': h.format_byte_size_binary(size), + 'code_stats': code_stats} + + stats = cache_manager.get(_cache_key, createfunc=compute_stats) + return stats + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @view_config( + route_name='repo_refs_data', request_method='GET', + renderer='json_ext') + def repo_refs_data(self): + _ = self.request.translate + self.load_default_context() + + repo = self.rhodecode_vcs_repo + refs_to_create = [ + (_("Branch"), repo.branches, 'branch'), + (_("Tag"), repo.tags, 'tag'), + (_("Bookmark"), repo.bookmarks, 'book'), + ] + res = self._create_reference_data( + repo, self.db_repo_name, refs_to_create) + data = { + 'more': False, + 'results': res + } + return data + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @view_config( + route_name='repo_refs_changelog_data', request_method='GET', + renderer='json_ext') + def repo_refs_changelog_data(self): + _ = self.request.translate + self.load_default_context() + + repo = self.rhodecode_vcs_repo + + refs_to_create = [ + (_("Branches"), repo.branches, 'branch'), + (_("Closed branches"), repo.branches_closed, 'branch_closed'), + # TODO: enable when vcs can handle bookmarks filters + # (_("Bookmarks"), repo.bookmarks, "book"), + ] + res = self._create_reference_data( + repo, self.db_repo_name, refs_to_create) + data = { + 'more': False, + 'results': res + } + return data + + def _create_reference_data(self, repo, full_repo_name, refs_to_create): + format_ref_id = utils.get_format_ref_id(repo) + + result = [] + for title, refs, ref_type in refs_to_create: + if refs: + result.append({ + 'text': title, + 'children': self._create_reference_items( + repo, full_repo_name, refs, ref_type, + format_ref_id), + }) + return result + + def _create_reference_items(self, repo, full_repo_name, refs, ref_type, + format_ref_id): + result = [] + is_svn = h.is_svn(repo) + 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': ref_name, + 'id': format_ref_id(ref_name, raw_id), + 'raw_id': raw_id, + 'type': ref_type, + 'files_url': files_url, + }) + return result + + 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=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/apps/repository/views/repo_tags.py b/rhodecode/apps/repository/views/repo_tags.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/repository/views/repo_tags.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2017 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 +from pyramid.view import view_config + +from rhodecode.apps._base import BaseReferencesView +from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator) + +log = logging.getLogger(__name__) + + +class RepoTagsView(BaseReferencesView): + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + @view_config( + route_name='tags_home', request_method='GET', + renderer='rhodecode:templates/tags/tags.mako') + def tags(self): + c = self.load_default_context() + + ref_items = self.rhodecode_vcs_repo.tags.items() + self.load_refs_context( + ref_items=ref_items, partials_template='tags/tags_data.mako') + + return self._get_template_context(c) diff --git a/rhodecode/apps/search/__init__.py b/rhodecode/apps/search/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/search/__init__.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 rhodecode.apps._base import ADMIN_PREFIX + + +def includeme(config): + + config.add_route( + name='search', + pattern=ADMIN_PREFIX + '/search') + + config.add_route( + name='search_repo', + pattern='/{repo_name:.*?[^/]}/search', repo_route=True) + + # Scan module for configuration decorators. + config.scan() + + + # # FULL TEXT SEARCH + # rmap.connect('search', '%s/search' % (ADMIN_PREFIX,), + # controller='search') + # rmap.connect('search_repo_home', '/{repo_name}/search', + # controller='search', + # action='index', + # conditions={'function': check_repo}, + # requirements=URL_NAME_REQUIREMENTS) \ No newline at end of file diff --git a/rhodecode/apps/search/tests/__init__.py b/rhodecode/apps/search/tests/__init__.py new file mode 100644 diff --git a/rhodecode/apps/search/tests/test_search.py b/rhodecode/apps/search/tests/test_search.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/search/tests/test_search.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 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 mock +import pytest +from whoosh import query + +from rhodecode.tests import ( + TestController, SkipTest, HG_REPO, + TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) +from rhodecode.tests.utils import AssertResponse + + +def route_path(name, **kwargs): + from rhodecode.apps._base import ADMIN_PREFIX + return { + 'search': + ADMIN_PREFIX + '/search', + 'search_repo': + '/{repo_name}/search', + + }[name].format(**kwargs) + + +class TestSearchController(TestController): + + def test_index(self): + self.log_user() + response = self.app.get(route_path('search')) + assert_response = AssertResponse(response) + assert_response.one_element_exists('input#q') + + def test_search_files_empty_search(self): + if os.path.isdir(self.index_location): + raise SkipTest('skipped due to existing index') + else: + self.log_user() + response = self.app.get(route_path('search'), + {'q': HG_REPO}) + response.mustcontain('There is no index to search in. ' + 'Please run whoosh indexer') + + def test_search_validation(self): + self.log_user() + response = self.app.get(route_path('search'), + {'q': query, 'type': 'content', 'page_limit': 1000}) + + response.mustcontain( + 'page_limit - 1000 is greater than maximum value 500') + + @pytest.mark.parametrize("query, expected_hits, expected_paths", [ + ('todo', 23, [ + 'vcs/backends/hg/inmemory.py', + 'vcs/tests/test_git.py']), + ('extension:rst installation', 6, [ + 'docs/index.rst', + 'docs/installation.rst']), + ('def repo', 87, [ + 'vcs/tests/test_git.py', + 'vcs/tests/test_changesets.py']), + ('repository:%s def test' % HG_REPO, 18, [ + 'vcs/tests/test_git.py', + 'vcs/tests/test_changesets.py']), + ('"def main"', 9, [ + 'vcs/__init__.py', + 'vcs/tests/__init__.py', + 'vcs/utils/progressbar.py']), + ('owner:test_admin', 358, [ + 'vcs/tests/base.py', + 'MANIFEST.in', + 'vcs/utils/termcolors.py', + 'docs/theme/ADC/static/documentation.png']), + ('owner:test_admin def main', 72, [ + 'vcs/__init__.py', + 'vcs/tests/test_utils_filesize.py', + 'vcs/tests/test_cli.py']), + ('owner:michał test', 0, []), + ]) + def test_search_files(self, query, expected_hits, expected_paths): + self.log_user() + response = self.app.get(route_path('search'), + {'q': query, 'type': 'content', 'page_limit': 500}) + + response.mustcontain('%s results' % expected_hits) + for path in expected_paths: + response.mustcontain(path) + + @pytest.mark.parametrize("query, expected_hits, expected_commits", [ + ('bother to ask where to fetch repo during tests', 3, [ + ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1'), + ('git', 'c6eb379775c578a95dad8ddab53f963b80894850'), + ('svn', '98')]), + ('michał', 0, []), + ('changed:tests/utils.py', 36, [ + ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1')]), + ('changed:vcs/utils/archivers.py', 11, [ + ('hg', '25213a5fbb048dff8ba65d21e466a835536e5b70'), + ('hg', '47aedd538bf616eedcb0e7d630ea476df0e159c7'), + ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'), + ('hg', '04ad456aefd6461aea24f90b63954b6b1ce07b3e'), + ('git', 'c994f0de03b2a0aa848a04fc2c0d7e737dba31fc'), + ('git', 'd1f898326327e20524fe22417c22d71064fe54a1'), + ('git', 'fe568b4081755c12abf6ba673ba777fc02a415f3'), + ('git', 'bafe786f0d8c2ff7da5c1dcfcfa577de0b5e92f1')]), + ('added:README.rst', 3, [ + ('hg', '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'), + ('git', 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'), + ('svn', '8')]), + ('changed:lazy.py', 15, [ + ('hg', 'eaa291c5e6ae6126a203059de9854ccf7b5baa12'), + ('git', '17438a11f72b93f56d0e08e7d1fa79a378578a82'), + ('svn', '82'), + ('svn', '262'), + ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'), + ('git', '33fa3223355104431402a888fa77a4e9956feb3e') + ]), + ('author:marcin@python-blog.com ' + 'commit_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [ + ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]), + ('b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [ + ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]), + ('b986218b', 1, [ + ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]), + ]) + def test_search_commit_messages( + self, query, expected_hits, expected_commits, enabled_backends): + self.log_user() + response = self.app.get(route_path('search'), + {'q': query, 'type': 'commit', 'page_limit': 500}) + + response.mustcontain('%s results' % expected_hits) + for backend, commit_id in expected_commits: + if backend in enabled_backends: + response.mustcontain(commit_id) + + @pytest.mark.parametrize("query, expected_hits, expected_paths", [ + ('readme.rst', 3, []), + ('test*', 75, []), + ('*model*', 1, []), + ('extension:rst', 48, []), + ('extension:rst api', 24, []), + ]) + def test_search_file_paths(self, query, expected_hits, expected_paths): + self.log_user() + response = self.app.get(route_path('search'), + {'q': query, 'type': 'path', 'page_limit': 500}) + + response.mustcontain('%s results' % expected_hits) + for path in expected_paths: + response.mustcontain(path) + + def test_search_commit_message_specific_repo(self, backend): + self.log_user() + response = self.app.get( + route_path('search_repo',repo_name=backend.repo_name), + {'q': 'bother to ask where to fetch repo during tests', + 'type': 'commit'}) + + response.mustcontain('1 results') + + def test_filters_are_not_applied_for_admin_user(self): + self.log_user() + with mock.patch('whoosh.searching.Searcher.search') as search_mock: + self.app.get(route_path('search'), + {'q': 'test query', 'type': 'commit'}) + assert search_mock.call_count == 1 + _, kwargs = search_mock.call_args + assert kwargs['filter'] is None + + def test_filters_are_applied_for_normal_user(self, enabled_backends): + self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) + with mock.patch('whoosh.searching.Searcher.search') as search_mock: + self.app.get(route_path('search'), + {'q': 'test query', 'type': 'commit'}) + assert search_mock.call_count == 1 + _, kwargs = search_mock.call_args + assert isinstance(kwargs['filter'], query.Or) + expected_repositories = [ + 'vcs_test_{}'.format(b) for b in enabled_backends] + queried_repositories = [ + name for type_, name in kwargs['filter'].all_terms()] + for repository in expected_repositories: + assert repository in queried_repositories diff --git a/rhodecode/apps/search/views.py b/rhodecode/apps/search/views.py new file mode 100644 --- /dev/null +++ b/rhodecode/apps/search/views.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2017 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 urllib +from pyramid.view import view_config +from webhelpers.util import update_params + +from rhodecode.apps._base import BaseAppView, RepoAppView +from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator) +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__) + + +def search(request, tmpl_context, repo_name): + searcher = searcher_from_config(request.registry.settings) + formatted_results = [] + execution_time = '' + + schema = search_schema.SearchParamsSchema() + + search_params = {} + errors = [] + try: + search_params = schema.deserialize( + dict(search_query=request.GET.get('q'), + search_type=request.GET.get('type'), + search_sort=request.GET.get('sort'), + page_limit=request.GET.get('page_limit'), + requested_page=request.GET.get('page')) + ) + except validation_schema.Invalid as e: + errors = e.children + + def url_generator(**kw): + q = urllib.quote(safe_str(search_query)) + return update_params( + "?q=%s&type=%s" % (q, safe_str(search_type)), **kw) + + c = tmpl_context + search_query = search_params.get('search_query') + search_type = search_params.get('search_type') + search_sort = search_params.get('search_sort') + if search_params.get('search_query'): + page_limit = search_params['page_limit'] + requested_page = search_params['requested_page'] + + try: + search_result = searcher.search( + search_query, search_type, c.auth_user, repo_name, + requested_page, page_limit, search_sort) + + formatted_results = Page( + search_result['results'], page=requested_page, + item_count=search_result['count'], + items_per_page=page_limit, url=url_generator) + finally: + searcher.cleanup() + + if not search_result['error']: + execution_time = '%s results (%.3f seconds)' % ( + search_result['count'], + search_result['runtime']) + elif not errors: + node = schema['search_query'] + errors = [ + validation_schema.Invalid(node, search_result['error'])] + + c.perm_user = c.auth_user + c.repo_name = repo_name + c.sort = search_sort + c.url_generator = url_generator + c.errors = errors + c.formatted_results = formatted_results + c.runtime = execution_time + c.cur_query = search_query + c.search_type = search_type + c.searcher = searcher + + +class SearchView(BaseAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + self._register_global_c(c) + return c + + @LoginRequired() + @view_config( + route_name='search', request_method='GET', + renderer='rhodecode:templates/search/search.mako') + def search(self): + c = self.load_default_context() + search(self.request, c, repo_name=None) + return self._get_template_context(c) + + +class SearchRepoView(RepoAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + self._register_global_c(c) + return c + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @view_config( + route_name='search_repo', request_method='GET', + renderer='rhodecode:templates/search/search.mako') + def search_repo(self): + c = self.load_default_context() + search(self.request, c, repo_name=self.db_repo_name) + return self._get_template_context(c) diff --git a/rhodecode/authentication/base.py b/rhodecode/authentication/base.py --- a/rhodecode/authentication/base.py +++ b/rhodecode/authentication/base.py @@ -564,8 +564,6 @@ def authenticate(username, password, env for plugin in authn_registry.get_plugins_for_authentication(): plugin.set_auth_type(auth_type) plugin.set_calling_scope_repo(acl_repo_name) - user = plugin.get_user(username) - display_user = user.username if user else username if headers_only and not plugin.is_headers_auth: log.debug('Auth type is for headers only and plugin `%s` is not ' 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 @@ -71,15 +71,17 @@ class LdapSettingsSchema(AuthnPluginSett host = colander.SchemaNode( colander.String(), default='', - description=_('Host of the LDAP Server \n' - '(e.g., 192.168.2.154, or ldap-server.domain.com'), + description=_('Host[s] of the LDAP Server \n' + '(e.g., 192.168.2.154, or ldap-server.domain.com.\n ' + 'Multiple servers can be specified using commas'), preparer=strip_whitespace, title=_('LDAP Host'), widget='string') port = colander.SchemaNode( colander.Int(), default=389, - description=_('Custom port that the LDAP server is listening on. Default: 389'), + description=_('Custom port that the LDAP server is listening on. ' + 'Default value is: 389'), preparer=strip_whitespace, title=_('Port'), validator=colander.Range(min=0, max=65536), @@ -112,7 +114,9 @@ class LdapSettingsSchema(AuthnPluginSett tls_reqcert = colander.SchemaNode( colander.String(), default=tls_reqcert_choices[0], - description=_('Require Cert over TLS?'), + description=_('Require Cert over TLS?. Self-signed and custom ' + 'certificates can be used when\n `RhodeCode Certificate` ' + 'found in admin > settings > system info page is extended.'), title=_('Certificate Checks'), validator=colander.OneOf(tls_reqcert_choices), widget='select') diff --git a/rhodecode/config/licenses.json b/rhodecode/config/licenses.json --- a/rhodecode/config/licenses.json +++ b/rhodecode/config/licenses.json @@ -1,11 +1,11 @@ { - "nodejs-4.3.1": { + "libnghttp2-1.7.1": { "MIT License": "http://spdx.org/licenses/MIT" }, - "postgresql-9.5.1": { - "PostgreSQL License": "http://spdx.org/licenses/PostgreSQL" - }, - "python-2.7.11": { + "nodejs-4.3.1": { + "MIT License": "http://spdx.org/licenses/MIT" + }, + "python-2.7.12": { "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" }, "python2.7-Babel-1.3": { @@ -14,19 +14,25 @@ "python2.7-Beaker-1.7.0": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, + "python2.7-Chameleon-2.24": { + "BSD-like": "http://repoze.org/license.html" + }, "python2.7-FormEncode-1.2.4": { "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" }, - "python2.7-Mako-1.0.1": { + "python2.7-Jinja2-2.7.3": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, + "python2.7-Mako-1.0.6": { "MIT License": "http://spdx.org/licenses/MIT" }, - "python2.7-Markdown-2.6.2": { + "python2.7-Markdown-2.6.7": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, "python2.7-MarkupSafe-0.23": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, - "python2.7-Paste-2.0.2": { + "python2.7-Paste-2.0.3": { "MIT License": "http://spdx.org/licenses/MIT" }, "python2.7-PasteDeploy-1.5.2": { @@ -35,12 +41,12 @@ "python2.7-PasteScript-1.7.5": { "MIT License": "http://spdx.org/licenses/MIT" }, - "python2.7-Pygments-2.0.2": { + "python2.7-Pygments-2.2.0": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, - "python2.7-Pylons-1.0.1-patch1": { + "python2.7-Pylons-1.0.2.rhodecode-patch1": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" - }, + }, "python2.7-Routes-1.13": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, @@ -65,7 +71,7 @@ "python2.7-WebOb-1.3.1": { "MIT License": "http://spdx.org/licenses/MIT" }, - "python2.7-Whoosh-2.7.0": { + "python2.7-Whoosh-2.7.4": { "BSD 2-clause \"Simplified\" License": "http://spdx.org/licenses/BSD-2-Clause", "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, @@ -87,9 +93,18 @@ "python2.7-backport-ipaddress-0.1": { "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" }, + "python2.7-backports.shutil-get-terminal-size-1.0.0": { + "MIT License": "http://spdx.org/licenses/MIT" + }, + "python2.7-bleach-1.5.0": { + "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" + }, "python2.7-celery-2.2.10": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, + "python2.7-channelstream-0.5.2": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, "python2.7-click-5.1": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, @@ -99,82 +114,169 @@ "python2.7-configobj-5.0.6": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, - "python2.7-cssselect-0.9.1": { + "python2.7-configparser-3.5.0": { + "MIT License": "http://spdx.org/licenses/MIT" + }, + "python2.7-cssselect-1.0.1": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, - "python2.7-decorator-3.4.2": { + "python2.7-decorator-4.0.11": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, + "python2.7-deform-2.0a2": { + "BSD-derived": "http://www.repoze.org/LICENSE.txt" + }, "python2.7-docutils-0.12": { "BSD 2-clause \"Simplified\" License": "http://spdx.org/licenses/BSD-2-Clause" }, + "python2.7-dogpile.cache-0.6.1": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, + "python2.7-dogpile.core-0.4.1": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, "python2.7-elasticsearch-2.3.0": { "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" }, - "python2.7-elasticsearch-dsl-2.0.0": { + "python2.7-elasticsearch-dsl-2.2.0": { "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" }, + "python2.7-entrypoints-0.2.2": { + "MIT License": "http://spdx.org/licenses/MIT" + }, + "python2.7-enum34-1.1.6": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, + "python2.7-functools32-3.2.3.post2": { + "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" + }, "python2.7-future-0.14.3": { "MIT License": "http://spdx.org/licenses/MIT" }, "python2.7-futures-3.0.2": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, + "python2.7-gevent-1.1.2": { + "MIT License": "http://spdx.org/licenses/MIT" + }, "python2.7-gnureadline-6.3.3": { "GNU General Public License v1.0 only": "http://spdx.org/licenses/GPL-1.0" }, + "python2.7-gprof2dot-2016.10.13": { + "GNU Lesser General Public License v3.0 or later": "http://spdx.org/licenses/LGPL-3.0+" + }, + "python2.7-greenlet-0.4.10": { + "MIT License": "http://spdx.org/licenses/MIT" + }, "python2.7-gunicorn-19.6.0": { "MIT License": "http://spdx.org/licenses/MIT" }, + "python2.7-html5lib-0.9999999": { + "MIT License": "http://spdx.org/licenses/MIT" + }, "python2.7-infrae.cache-1.0.1": { "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1" }, - "python2.7-ipython-3.1.0": { + "python2.7-ipython-5.1.0": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, + "python2.7-ipython-genutils-0.2.0": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, "python2.7-iso8601-0.1.11": { "MIT License": "http://spdx.org/licenses/MIT" }, + "python2.7-itsdangerous-0.24": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, + "python2.7-jsonschema-2.6.0": { + "MIT License": "http://spdx.org/licenses/MIT" + }, + "python2.7-jupyter-client-5.0.0": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, + "python2.7-jupyter-core-4.3.0": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, "python2.7-kombu-1.5.1": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, - "python2.7-msgpack-python-0.4.6": { + "python2.7-mistune-0.7.4": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, + "python2.7-msgpack-python-0.4.8": { "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" }, + "python2.7-nbconvert-5.1.1": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, + "python2.7-nbformat-4.3.0": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, "python2.7-packaging-15.2": { "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" }, - "python2.7-psutil-2.2.1": { + "python2.7-pandocfilters-1.4.1": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, - "python2.7-psycopg2-2.6": { + "python2.7-pathlib2-2.1.0": { + "MIT License": "http://spdx.org/licenses/MIT" + }, + "python2.7-peppercorn-0.5": { + "BSD-derived": "http://www.repoze.org/LICENSE.txt" + }, + "python2.7-pexpect-4.2.1": { + "ISC License": "http://spdx.org/licenses/ISC" + }, + "python2.7-pickleshare-0.7.4": { + "MIT License": "http://spdx.org/licenses/MIT" + }, + "python2.7-prompt-toolkit-1.0.14": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, + "python2.7-psutil-4.3.1": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, + "python2.7-psycopg2-2.6.1": { "GNU Lesser General Public License v3.0 or later": "http://spdx.org/licenses/LGPL-3.0+" }, - "python2.7-py-1.4.29": { + "python2.7-ptyprocess-0.5.1": { + "ISC License": "http://opensource.org/licenses/ISC" + }, + "python2.7-py-1.4.31": { "MIT License": "http://spdx.org/licenses/MIT" }, "python2.7-py-bcrypt-0.4": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" }, + "python2.7-py-gfm-0.1.3.rhodecode-upstream1": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, "python2.7-pycrypto-2.6.1": { "Public Domain": null }, "python2.7-pycurl-7.19.5": { "MIT License": "http://spdx.org/licenses/MIT" }, + "python2.7-pygments-markdown-lexer-0.1.0.dev39": { + "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" + }, "python2.7-pyparsing-1.5.7": { "MIT License": "http://spdx.org/licenses/MIT" }, - "python2.7-pyramid-1.6.1": { + "python2.7-pyramid-1.7.4": { "Repoze License": "http://www.repoze.org/LICENSE.txt" }, "python2.7-pyramid-beaker-0.8": { "Repoze License": "http://www.repoze.org/LICENSE.txt" }, - "python2.7-pyramid-debugtoolbar-2.4.2": { + "python2.7-pyramid-debugtoolbar-3.0.5": { "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause", "Repoze License": "http://www.repoze.org/LICENSE.txt" }, + "python2.7-pyramid-jinja2-2.5": { + "BSD-derived": "http://www.repoze.org/LICENSE.txt" + }, "python2.7-pyramid-mako-1.0.2": { "Repoze License": "http://www.repoze.org/LICENSE.txt" }, @@ -182,16 +284,25 @@ "libpng License": "http://spdx.org/licenses/Libpng", "zlib License": "http://spdx.org/licenses/Zlib" }, - "python2.7-pytest-2.8.5": { + "python2.7-pytest-3.0.5": { + "MIT License": "http://spdx.org/licenses/MIT" + }, + "python2.7-pytest-profiling-1.2.2": { + "MIT License": "http://spdx.org/licenses/MIT" + }, + "python2.7-pytest-runner-2.9": { "MIT License": "http://spdx.org/licenses/MIT" }, - "python2.7-pytest-runner-2.7.1": { + "python2.7-pytest-sugar-0.7.1": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, + "python2.7-pytest-timeout-1.2.0": { "MIT License": "http://spdx.org/licenses/MIT" }, - "python2.7-python-dateutil-1.5": { - "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" + "python2.7-python-dateutil-2.1": { + "Simplified BSD": null }, - "python2.7-python-editor-1.0.1": { + "python2.7-python-editor-1.0.3": { "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" }, "python2.7-python-ldap-2.4.19": { @@ -203,6 +314,9 @@ "python2.7-pytz-2015.4": { "MIT License": "http://spdx.org/licenses/MIT" }, + "python2.7-pyzmq-14.6.0": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, "python2.7-recaptcha-client-1.0.6": { "MIT License": "http://spdx.org/licenses/MIT" }, @@ -211,21 +325,35 @@ }, "python2.7-requests-2.9.1": { "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0" - }, + }, "python2.7-setuptools-19.4": { "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0", "Zope Public License 2.0": "http://spdx.org/licenses/ZPL-2.0" }, - "python2.7-setuptools-scm-1.11.0": { + "python2.7-setuptools-scm-1.15.0": { "MIT License": "http://spdx.org/licenses/MIT" }, + "python2.7-simplegeneric-0.8.1": { + "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1" + }, "python2.7-simplejson-3.7.2": { - "Academic Free License": "http://spdx.org/licenses/AFL-2.1", "MIT License": "http://spdx.org/licenses/MIT" }, "python2.7-six-1.9.0": { "MIT License": "http://spdx.org/licenses/MIT" }, + "python2.7-subprocess32-3.2.6": { + "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0" + }, + "python2.7-termcolor-1.1.0": { + "MIT License": "http://spdx.org/licenses/MIT" + }, + "python2.7-testpath-0.1": { + "MIT License": "http://spdx.org/licenses/MIT" + }, + "python2.7-traitlets-4.3.2": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, "python2.7-translationstring-1.3": { "Repoze License": "http://www.repoze.org/LICENSE.txt" }, @@ -235,9 +363,15 @@ "python2.7-venusian-1.0": { "Repoze License": "http://www.repoze.org/LICENSE.txt" }, - "python2.7-waitress-0.8.9": { + "python2.7-waitress-1.0.1": { "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1" }, + "python2.7-wcwidth-0.1.7": { + "MIT License": "http://spdx.org/licenses/MIT" + }, + "python2.7-ws4py-0.3.5": { + "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause" + }, "python2.7-zope.cachedescriptors-4.0.0": { "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1" }, @@ -246,5 +380,9 @@ }, "python2.7-zope.interface-4.1.3": { "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1" + }, + "xz-5.2.2": { + "GNU General Public License v2.0 or later": "http://spdx.org/licenses/GPL-2.0+", + "GNU Library General Public License v2.1 or later": "http://spdx.org/licenses/LGPL-2.1+" } } \ No newline at end of file diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -39,11 +39,15 @@ from routes.middleware import RoutesMidd import routes.util import rhodecode + from rhodecode.model import meta 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.vcs import VCSCommunicationError +from rhodecode.lib.exceptions import VCSServerUnavailable from rhodecode.lib.middleware import csrf from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled from rhodecode.lib.middleware.error_handling import ( @@ -51,10 +55,10 @@ from rhodecode.lib.middleware.error_hand 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.lib.utils2 import aslist as rhodecode_aslist, AttributeDict from rhodecode.subscribers import ( - scan_repositories_if_enabled, write_metadata_if_needed, - write_js_routes_if_enabled) + scan_repositories_if_enabled, write_js_routes_if_enabled, + write_metadata_if_needed) log = logging.getLogger(__name__) @@ -221,7 +225,7 @@ def add_pylons_compat_data(registry, glo def error_handler(exception, request): import rhodecode - from rhodecode.lib.utils2 import AttributeDict + from rhodecode.lib import helpers rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode' @@ -229,6 +233,8 @@ def error_handler(exception, request): # prefer original exception for the response since it may have headers set if isinstance(exception, HTTPException): base_response = exception + elif isinstance(exception, VCSCommunicationError): + base_response = VCSServerUnavailable() def is_http_error(response): # error which should have traceback @@ -255,9 +261,10 @@ def error_handler(exception, request): c.causes = [] if hasattr(base_response, 'causes'): c.causes = base_response.causes + c.messages = helpers.flash.pop_messages() response = render_to_response( - '/errors/error_document.mako', {'c': c}, request=request, + '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request, response=base_response) return response @@ -284,11 +291,15 @@ def includeme(config): # apps config.include('rhodecode.apps._base') + config.include('rhodecode.apps.ops') config.include('rhodecode.apps.admin') config.include('rhodecode.apps.channelstream') config.include('rhodecode.apps.login') + config.include('rhodecode.apps.home') config.include('rhodecode.apps.repository') + config.include('rhodecode.apps.repo_group') + config.include('rhodecode.apps.search') config.include('rhodecode.apps.user_profile') config.include('rhodecode.apps.my_account') config.include('rhodecode.apps.svn_support') @@ -307,6 +318,12 @@ def includeme(config): config.add_subscriber(write_metadata_if_needed, ApplicationCreated) config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated) + # events + # TODO(marcink): this should be done when pyramid migration is finished + # config.add_subscriber( + # 'rhodecode.integrations.integrations_event_handler', + # 'rhodecode.events.RhodecodeEvent') + # Set the authorization policy. authz_policy = ACLAuthorizationPolicy() config.set_authorization_policy(authz_policy) @@ -314,6 +331,10 @@ def includeme(config): # Set the default renderer for HTML templates to mako. config.add_mako_renderer('.html') + config.add_renderer( + name='json_ext', + factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json') + # include RhodeCode plugins includes = aslist(settings.get('rhodecode.includes', [])) for inc in includes: @@ -395,7 +416,6 @@ def wrap_app_in_wsgi_middlewares(pyramid pool = meta.Base.metadata.bind.engine.pool log.debug('sa pool status: %s', pool.status()) - return pyramid_app_with_cleanup diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -32,8 +32,6 @@ import os import re from routes import Mapper -from rhodecode.config import routing_links - # prefix for non repository related links needs to be prefixed with `/` ADMIN_PREFIX = '/_admin' STATIC_FILE_PREFIX = '/_static' @@ -119,8 +117,9 @@ class JSRoutesMapper(Mapper): def make_map(config): """Create, configure and return the routes Mapper""" - rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'], - always_scan=config['debug']) + rmap = JSRoutesMapper( + directory=config['pylons.paths']['controllers'], + always_scan=config['debug']) rmap.minimization = False rmap.explicit = False @@ -186,36 +185,7 @@ def make_map(config): # CUSTOM ROUTES HERE #========================================================================== - # MAIN PAGE - rmap.connect('home', '/', controller='home', action='index', jsroute=True) - rmap.connect('goto_switcher_data', '/_goto_data', controller='home', - action='goto_switcher_data') - rmap.connect('repo_list_data', '/_repos', controller='home', - action='repo_list_data') - - rmap.connect('user_autocomplete_data', '/_users', controller='home', - action='user_autocomplete_data', jsroute=True) - rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home', - action='user_group_autocomplete_data', jsroute=True) - - # TODO: johbo: Static links, to be replaced by our redirection mechanism - rmap.connect('rst_help', - 'http://docutils.sourceforge.net/docs/user/rst/quickref.html', - _static=True) - rmap.connect('markdown_help', - 'http://daringfireball.net/projects/markdown/syntax', - _static=True) - rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True) - rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True) - rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True) - # TODO: anderson - making this a static link since redirect won't play - # nice with POST requests - rmap.connect('enterprise_license_convert_from_old', - 'https://rhodecode.com/u/license-upgrade', - _static=True) - - routing_links.connect_redirection_links(rmap) - + # ping and pylons error test rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping') rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test') @@ -228,10 +198,6 @@ def make_map(config): action='index', conditions={'method': ['GET']}) m.connect('new_repo', '/create_repository', jsroute=True, action='create_repository', conditions={'method': ['GET']}) - m.connect('/repos/{repo_name}', - action='update', conditions={'method': ['PUT'], - 'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) m.connect('delete_repo', '/repos/{repo_name}', action='delete', conditions={'method': ['DELETE']}, requirements=URL_NAME_REQUIREMENTS) @@ -321,19 +287,6 @@ def make_map(config): m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary', action='edit_perms_summary', conditions={'method': ['GET']}) - m.connect('edit_user_emails', '/users/{user_id}/edit/emails', - action='edit_emails', conditions={'method': ['GET']}) - m.connect('edit_user_emails', '/users/{user_id}/edit/emails', - action='add_email', conditions={'method': ['PUT']}) - m.connect('edit_user_emails', '/users/{user_id}/edit/emails', - action='delete_email', conditions={'method': ['DELETE']}) - - m.connect('edit_user_ips', '/users/{user_id}/edit/ips', - action='edit_ips', conditions={'method': ['GET']}) - m.connect('edit_user_ips', '/users/{user_id}/edit/ips', - action='add_ip', conditions={'method': ['PUT']}) - m.connect('edit_user_ips', '/users/{user_id}/edit/ips', - action='delete_ip', conditions={'method': ['DELETE']}) # ADMIN USER GROUPS REST ROUTES with rmap.submapper(path_prefix=ADMIN_PREFIX, @@ -519,37 +472,9 @@ def make_map(config): m.connect('my_account_password', '/my_account/password', action='my_account_password', conditions={'method': ['GET']}) - m.connect('my_account_repos', '/my_account/repos', - action='my_account_repos', conditions={'method': ['GET']}) - - m.connect('my_account_watched', '/my_account/watched', - action='my_account_watched', conditions={'method': ['GET']}) - m.connect('my_account_pullrequests', '/my_account/pull_requests', action='my_account_pullrequests', conditions={'method': ['GET']}) - m.connect('my_account_perms', '/my_account/perms', - action='my_account_perms', conditions={'method': ['GET']}) - - m.connect('my_account_emails', '/my_account/emails', - action='my_account_emails', conditions={'method': ['GET']}) - m.connect('my_account_emails', '/my_account/emails', - action='my_account_emails_add', conditions={'method': ['POST']}) - m.connect('my_account_emails', '/my_account/emails', - action='my_account_emails_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']}) - m.connect('my_account_notifications_test_channelstream', - '/my_account/test_channelstream', - action='my_account_notifications_test_channelstream', - conditions={'method': ['POST']}) - # NOTIFICATION REST ROUTES with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='admin/notifications') as m: @@ -597,22 +522,6 @@ def make_map(config): action='show', conditions={'method': ['GET']}, requirements=URL_NAME_REQUIREMENTS) - # ADMIN MAIN PAGES - with rmap.submapper(path_prefix=ADMIN_PREFIX, - controller='admin/admin') as m: - m.connect('admin_home', '', action='index') - m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}', - action='add_repo') - m.connect( - 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}', - action='pull_requests') - m.connect( - 'pull_requests_global_1', '/pull-requests/{pull_request_id:[0-9]+}', - action='pull_requests') - m.connect( - 'pull_requests_global', '/pull-request/{pull_request_id:[0-9]+}', - action='pull_requests') - # USER JOURNAL rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,), controller='journal', action='index') @@ -642,15 +551,6 @@ def make_map(config): controller='journal', action='toggle_following', jsroute=True, conditions={'method': ['POST']}) - # FULL TEXT SEARCH - rmap.connect('search', '%s/search' % (ADMIN_PREFIX,), - controller='search') - rmap.connect('search_repo_home', '/{repo_name}/search', - controller='search', - action='index', - conditions={'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - # FEEDS rmap.connect('rss_feed_home', '/{repo_name}/feed/rss', controller='feed', action='rss', @@ -673,21 +573,6 @@ def make_map(config): controller='admin/repos', action='repo_check', requirements=URL_NAME_REQUIREMENTS) - rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}', - controller='summary', action='repo_stats', - conditions={'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS, jsroute=True) - - rmap.connect('repo_refs_data', '/{repo_name}/refs-data', - controller='summary', action='repo_refs_data', - requirements=URL_NAME_REQUIREMENTS, jsroute=True) - rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog', - controller='summary', action='repo_refs_changelog_data', - requirements=URL_NAME_REQUIREMENTS, jsroute=True) - rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers', - controller='summary', action='repo_default_reviewers_data', - jsroute=True, requirements=URL_NAME_REQUIREMENTS) - rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}', controller='changeset', revision='tip', conditions={'function': check_repo}, @@ -702,21 +587,6 @@ def make_map(config): requirements=URL_NAME_REQUIREMENTS) # repo edit options - rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True, - controller='admin/repos', action='edit', - conditions={'method': ['GET'], 'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - - rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions', - jsroute=True, - controller='admin/repos', action='edit_permissions', - conditions={'method': ['GET'], 'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions', - controller='admin/repos', action='edit_permissions_update', - conditions={'method': ['PUT'], 'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields', controller='admin/repos', action='edit_fields', conditions={'method': ['GET'], 'function': check_repo}, @@ -730,39 +600,11 @@ def make_map(config): conditions={'method': ['DELETE'], 'function': check_repo}, requirements=URL_NAME_REQUIREMENTS) - rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced', - controller='admin/repos', action='edit_advanced', - conditions={'method': ['GET'], 'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - - rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking', - controller='admin/repos', action='edit_advanced_locking', - conditions={'method': ['PUT'], 'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle', controller='admin/repos', action='toggle_locking', conditions={'method': ['GET'], 'function': check_repo}, requirements=URL_NAME_REQUIREMENTS) - rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal', - controller='admin/repos', action='edit_advanced_journal', - conditions={'method': ['PUT'], 'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - - rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork', - controller='admin/repos', action='edit_advanced_fork', - conditions={'method': ['PUT'], 'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - - rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches', - controller='admin/repos', action='edit_caches_form', - conditions={'method': ['GET'], 'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches', - controller='admin/repos', action='edit_caches', - conditions={'method': ['PUT'], 'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote', controller='admin/repos', action='edit_remote_form', conditions={'method': ['GET'], 'function': check_repo}, @@ -931,13 +773,6 @@ def make_map(config): 'method': ['DELETE']}, requirements=URL_NAME_REQUIREMENTS) - rmap.connect('pullrequest_show_all', - '/{repo_name}/pull-request', - controller='pullrequests', - action='show_all', conditions={'function': check_repo, - 'method': ['GET']}, - requirements=URL_NAME_REQUIREMENTS, jsroute=True) - rmap.connect('pullrequest_comment', '/{repo_name}/pull-request-comment/{pull_request_id}', controller='pullrequests', @@ -951,31 +786,10 @@ def make_map(config): conditions={'function': check_repo, 'method': ['DELETE']}, requirements=URL_NAME_REQUIREMENTS, jsroute=True) - rmap.connect('summary_home_explicit', '/{repo_name}/summary', - controller='summary', conditions={'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - - rmap.connect('branches_home', '/{repo_name}/branches', - controller='branches', conditions={'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - - rmap.connect('tags_home', '/{repo_name}/tags', - controller='tags', conditions={'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - - rmap.connect('bookmarks_home', '/{repo_name}/bookmarks', - controller='bookmarks', conditions={'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True, controller='changelog', conditions={'function': check_repo}, requirements=URL_NAME_REQUIREMENTS) - rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary', - controller='changelog', action='changelog_summary', - conditions={'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - rmap.connect('changelog_file_home', '/{repo_name}/changelog/{revision}/{f_path}', controller='changelog', f_path=None, @@ -1128,26 +942,4 @@ def make_map(config): conditions={'function': check_repo}, requirements=URL_NAME_REQUIREMENTS) - # must be here for proper group/repo catching pattern - _connect_with_slash( - rmap, 'repo_group_home', '/{group_name}', - controller='home', action='index_repo_group', - conditions={'function': check_group}, - requirements=URL_NAME_REQUIREMENTS) - - # catch all, at the end - _connect_with_slash( - rmap, 'summary_home', '/{repo_name}', jsroute=True, - controller='summary', action='index', - conditions={'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - return rmap - - -def _connect_with_slash(mapper, name, path, *args, **kwargs): - """ - Connect a route with an optional trailing slash in `path`. - """ - mapper.connect(name + '_slash', path + '/', *args, **kwargs) - mapper.connect(name, path, *args, **kwargs) diff --git a/rhodecode/config/routing_links.py b/rhodecode/config/routing_links.py --- a/rhodecode/config/routing_links.py +++ b/rhodecode/config/routing_links.py @@ -46,27 +46,56 @@ you can see it working. # flake8: noqa from __future__ import unicode_literals +link_config = [ + { + "name": "enterprise_docs", + "target": "https://rhodecode.com/r1/enterprise/docs/", + "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/", + }, + { + "name": "enterprise_log_file_locations", + "target": "https://rhodecode.com/r1/enterprise/docs/admin-system-overview/", + "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/system-overview.html#log-files", + }, + { + "name": "enterprise_issue_tracker_settings", + "target": "https://rhodecode.com/r1/enterprise/docs/issue-trackers-overview/", + "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/issue-trackers/issue-trackers.html", + }, + { + "name": "enterprise_svn_setup", + "target": "https://rhodecode.com/r1/enterprise/docs/svn-setup/", + "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/svn-http.html", + }, + { + "name": "rst_help", + "target": "http://docutils.sourceforge.net/docs/user/rst/quickref.html", + "external_target": "http://docutils.sourceforge.net/docs/user/rst/quickref.html", + }, + { + "name": "markdown_help", + "target": "https://daringfireball.net/projects/markdown/syntax", + "external_target": "https://daringfireball.net/projects/markdown/syntax", + }, + { + "name": "rhodecode_official", + "target": "https://rhodecode.com", + "external_target": "https://rhodecode.com/", + }, + { + "name": "rhodecode_support", + "target": "https://rhodecode.com/help/", + "external_target": "https://rhodecode.com/support", + }, + { + "name": "rhodecode_translations", + "target": "https://rhodecode.com/translate/enterprise", + "external_target": "https://www.transifex.com/rhodecode/RhodeCode/", + }, -link_config = [ - {"name": "enterprise_docs", - "target": "https://rhodecode.com/r1/enterprise/docs/", - "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/", - }, - {"name": "enterprise_log_file_locations", - "target": "https://rhodecode.com/r1/enterprise/docs/admin-system-overview/", - "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/system-overview.html#log-files", - }, - {"name": "enterprise_issue_tracker_settings", - "target": "https://rhodecode.com/r1/enterprise/docs/issue-trackers-overview/", - "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/issue-trackers/issue-trackers.html", - }, - {"name": "enterprise_svn_setup", - "target": "https://rhodecode.com/r1/enterprise/docs/svn-setup/", - "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/svn-http.html", - }, ] -def connect_redirection_links(rmap): +def connect_redirection_links(config): for link in link_config: - rmap.connect(link['name'], link['target'], _static=True) + config.add_route(link['name'], link['target'], static=True) diff --git a/rhodecode/controllers/admin/admin.py b/rhodecode/controllers/admin/admin.py deleted file mode 100644 --- a/rhodecode/controllers/admin/admin.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2010-2017 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/ - -""" -Controller for Admin panel of RhodeCode Enterprise -""" - - -import logging - -from pylons import request, tmpl_context as c, url -from pylons.controllers.util import redirect -from sqlalchemy.orm import joinedload - -from rhodecode.model.db import UserLog, PullRequest -from rhodecode.lib.user_log_filter import user_log_filter -from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator -from rhodecode.lib.base import BaseController, render -from rhodecode.lib.utils2 import safe_int -from rhodecode.lib.helpers import Page - - -log = logging.getLogger(__name__) - - -class AdminController(BaseController): - - @LoginRequired() - def __before__(self): - super(AdminController, self).__before__() - - @HasPermissionAllDecorator('hg.admin') - def index(self): - users_log = UserLog.query()\ - .options(joinedload(UserLog.user))\ - .options(joinedload(UserLog.repository)) - - # FILTERING - c.search_term = request.GET.get('filter') - try: - users_log = user_log_filter(users_log, c.search_term) - except Exception: - # we want this to crash for now - raise - - users_log = users_log.order_by(UserLog.action_date.desc()) - - p = safe_int(request.GET.get('page', 1), 1) - - def url_generator(**kw): - return url.current(filter=c.search_term, **kw) - - c.users_log = Page(users_log, page=p, items_per_page=10, - url=url_generator) - c.log_data = render('admin/admin_log.mako') - - if request.is_xhr: - return c.log_data - return render('admin/admin.mako') - - # global redirect doesn't need permissions - def pull_requests(self, pull_request_id): - """ - Global redirect for Pull Requests - - :param pull_request_id: id of pull requests in the system - """ - pull_request = PullRequest.get_or_404(pull_request_id) - repo_name = pull_request.target_repo.repo_name - return redirect(url( - 'pullrequest_show', repo_name=repo_name, - pull_request_id=pull_request_id)) 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 @@ -24,35 +24,27 @@ my account controller for RhodeCode admi """ import logging -import datetime import formencode from formencode import htmlfill -from pyramid.threadlocal import get_current_registry from pyramid.httpexceptions import HTTPFound -from pylons import request, tmpl_context as c, url +from pylons import request, tmpl_context as c from pylons.controllers.util import redirect from pylons.i18n.translation import _ -from sqlalchemy.orm import joinedload from rhodecode.lib import helpers as h from rhodecode.lib import auth from rhodecode.lib.auth import ( LoginRequired, NotAnonymous, AuthUser) from rhodecode.lib.base import BaseController, render -from rhodecode.lib.utils import jsonify from rhodecode.lib.utils2 import safe_int, str2bool from rhodecode.lib.ext_json import json -from rhodecode.lib.channelstream import channelstream_request, \ - ChannelstreamException from rhodecode.model.db import ( Repository, PullRequest, UserEmailMap, User, UserFollowing) from rhodecode.model.forms import UserForm -from rhodecode.model.scm import RepoList from rhodecode.model.user import UserModel -from rhodecode.model.repo import RepoModel from rhodecode.model.meta import Session from rhodecode.model.pull_request import PullRequestModel from rhodecode.model.comment import CommentsModel @@ -82,26 +74,6 @@ class MyAccountController(BaseController c.auth_user = AuthUser( user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr) - def _load_my_repos_data(self, watched=False): - if watched: - admin = False - follows_repos = Session().query(UserFollowing)\ - .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\ - .options(joinedload(UserFollowing.follows_repository))\ - .all() - repo_list = [x.follows_repository for x in follows_repos] - else: - admin = True - repo_list = Repository.get_all_repos( - user_id=c.rhodecode_user.user_id) - repo_list = RepoList(repo_list, perm_set=[ - 'repository.read', 'repository.write', 'repository.admin']) - - repos_data = RepoModel().get_repos_as_dict( - repo_list=repo_list, admin=admin) - # json used to render the grid - return json.dumps(repos_data) - @auth.CSRFRequired() def my_account_update(self): """ @@ -181,65 +153,6 @@ class MyAccountController(BaseController force_defaults=False ) - def my_account_repos(self): - c.active = 'repos' - self.__load_data() - - # json used to render the grid - c.data = self._load_my_repos_data() - return render('admin/my_account/my_account.mako') - - def my_account_watched(self): - c.active = 'watched' - self.__load_data() - - # json used to render the grid - c.data = self._load_my_repos_data(watched=True) - return render('admin/my_account/my_account.mako') - - def my_account_perms(self): - c.active = 'perms' - self.__load_data() - c.perm_user = c.auth_user - - return render('admin/my_account/my_account.mako') - - def my_account_emails(self): - c.active = 'emails' - self.__load_data() - - c.user_email_map = UserEmailMap.query()\ - .filter(UserEmailMap.user == c.user).all() - return render('admin/my_account/my_account.mako') - - @auth.CSRFRequired() - def my_account_emails_add(self): - email = request.POST.get('new_email') - - try: - UserModel().add_extra_email(c.rhodecode_user.user_id, email) - Session().commit() - h.flash(_("Added new email address `%s` for user account") % email, - category='success') - except formencode.Invalid as error: - msg = error.error_dict['email'] - h.flash(msg, category='error') - except Exception: - log.exception("Exception in my_account_emails") - h.flash(_('An error occurred during email saving'), - category='error') - return redirect(url('my_account_emails')) - - @auth.CSRFRequired() - def my_account_emails_delete(self): - email_id = request.POST.get('del_email_id') - user_model = UserModel() - user_model.delete_extra_email(c.rhodecode_user.user_id, email_id) - Session().commit() - h.flash(_("Removed email address from user account"), - category='success') - return redirect(url('my_account_emails')) - def _extract_ordering(self, request): column_index = safe_int(request.GET.get('order[0][column]')) order_dir = request.GET.get('order[0][dir]', 'desc') @@ -320,45 +233,4 @@ class MyAccountController(BaseController else: return json.dumps(data) - def my_notifications(self): - c.active = 'notifications' - return render('admin/my_account/my_account.mako') - @auth.CSRFRequired() - @jsonify - def my_notifications_toggle_visibility(self): - user = c.rhodecode_user.get_instance() - new_status = not user.user_data.get('notification_status', True) - user.update_userdata(notification_status=new_status) - Session().commit() - return user.user_data['notification_status'] - - @auth.CSRFRequired() - @jsonify - def my_account_notifications_test_channelstream(self): - message = 'Test message sent via Channelstream by user: {}, on {}'.format( - c.rhodecode_user.username, datetime.datetime.now()) - payload = { - 'type': 'message', - 'timestamp': datetime.datetime.utcnow(), - 'user': 'system', - #'channel': 'broadcast', - 'pm_users': [c.rhodecode_user.username], - 'message': { - 'message': message, - 'level': 'info', - 'topic': '/notifications' - } - } - - registry = get_current_registry() - rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {}) - channelstream_config = rhodecode_plugins.get('channelstream', {}) - - try: - channelstream_request(channelstream_config, [payload], '/message') - except ChannelstreamException as e: - log.exception('Failed to send channelstream data') - return {"response": 'ERROR: {}'.format(e.__class__.__name__)} - return {"response": 'Channelstream data sent. ' - 'You should see a new live message now.'} 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 @@ -48,10 +48,6 @@ log = logging.getLogger(__name__) class NotificationsController(BaseController): """REST Controller styled on the Atom Publishing Protocol""" - # To properly map this controller, ensure your config/routing.py - # file has a resource setup: - # map.resource('notification', 'notifications', controller='_admin/notifications', - # path_prefix='/_admin', name_prefix='_admin_') @LoginRequired() @NotAnonymous() @@ -62,8 +58,8 @@ class NotificationsController(BaseContro """GET /_admin/notifications: All items in the collection""" # url('notifications') c.user = c.rhodecode_user - notif = NotificationModel().get_for_user(c.rhodecode_user.user_id, - filter_=request.GET.getall('type')) + notif = NotificationModel().get_for_user( + c.rhodecode_user.user_id, filter_=request.GET.getall('type')) p = safe_int(request.GET.get('page', 1), 1) notifications_url = webhelpers.paginate.PageURL( @@ -86,7 +82,6 @@ class NotificationsController(BaseContro return render('admin/notifications/notifications.mako') - @auth.CSRFRequired() def mark_all_read(self): if request.is_xhr: @@ -115,15 +110,8 @@ class NotificationsController(BaseContro @auth.CSRFRequired() def update(self, notification_id): - """PUT /_admin/notifications/id: Update an existing item""" - # Forms posted to this method should contain a hidden field: - # - # Or using helpers: - # h.form(url('notification', notification_id=ID), - # method='put') - # url('notification', notification_id=ID) + no = Notification.get_or_404(notification_id) try: - no = Notification.get(notification_id) if self._has_permissions(no): # deletes only notification2user NotificationModel().mark_read(c.rhodecode_user.user_id, no) @@ -136,15 +124,8 @@ class NotificationsController(BaseContro @auth.CSRFRequired() def delete(self, notification_id): - """DELETE /_admin/notifications/id: Delete an existing item""" - # Forms posted to this method should contain a hidden field: - # - # Or using helpers: - # h.form(url('notification', notification_id=ID), - # method='delete') - # url('notification', notification_id=ID) + no = Notification.get_or_404(notification_id) try: - no = Notification.get(notification_id) if self._has_permissions(no): # deletes only notification2user NotificationModel().delete(c.rhodecode_user.user_id, no) @@ -156,10 +137,8 @@ class NotificationsController(BaseContro raise HTTPBadRequest() def show(self, notification_id): - """GET /_admin/notifications/id: Show a specific item""" - # url('notification', notification_id=ID) c.user = c.rhodecode_user - no = Notification.get(notification_id) + no = Notification.get_or_404(notification_id) if no and self._has_permissions(no): unotification = NotificationModel()\ diff --git a/rhodecode/controllers/admin/permissions.py b/rhodecode/controllers/admin/permissions.py --- a/rhodecode/controllers/admin/permissions.py +++ b/rhodecode/controllers/admin/permissions.py @@ -64,12 +64,7 @@ class PermissionsController(BaseControll c.active = 'application' self.__load_data() - c.user = User.get_default_user() - - # TODO: johbo: The default user might be based on outdated state which - # has been loaded from the cache. A call to refresh() ensures that the - # latest state from the database is used. - Session().refresh(c.user) + c.user = User.get_default_user(refresh=True) app_settings = SettingsModel().get_all_settings() defaults = { diff --git a/rhodecode/controllers/admin/repo_groups.py b/rhodecode/controllers/admin/repo_groups.py --- a/rhodecode/controllers/admin/repo_groups.py +++ b/rhodecode/controllers/admin/repo_groups.py @@ -34,17 +34,18 @@ from pylons.i18n.translation import _, u from rhodecode.lib import auth from rhodecode.lib import helpers as h +from rhodecode.lib import audit_logger from rhodecode.lib.ext_json import json from rhodecode.lib.auth import ( LoginRequired, NotAnonymous, HasPermissionAll, HasRepoGroupPermissionAll, HasRepoGroupPermissionAnyDecorator) from rhodecode.lib.base import BaseController, render +from rhodecode.lib.utils2 import safe_int from rhodecode.model.db import RepoGroup, User from rhodecode.model.scm import RepoGroupList from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.forms import RepoGroupForm, RepoGroupPermsForm from rhodecode.model.meta import Session -from rhodecode.lib.utils2 import safe_int log = logging.getLogger(__name__) @@ -153,9 +154,6 @@ class RepoGroupsController(BaseControlle @NotAnonymous() def index(self): - """GET /repo_groups: All items in the collection""" - # url('repo_groups') - repo_group_list = RepoGroup.get_all_repo_groups() _perms = ['group.admin'] repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms) @@ -168,8 +166,6 @@ class RepoGroupsController(BaseControlle @NotAnonymous() @auth.CSRFRequired() def create(self): - """POST /repo_groups: Create a new item""" - # url('repo_groups') parent_group_id = safe_int(request.POST.get('group_parent_id')) can_create = self._can_create_repo_group(parent_group_id) @@ -183,20 +179,29 @@ class RepoGroupsController(BaseControlle try: owner = c.rhodecode_user form_result = repo_group_form.to_python(dict(request.POST)) - RepoGroupModel().create( + repo_group = RepoGroupModel().create( group_name=form_result['group_name_full'], group_description=form_result['group_description'], owner=owner.user_id, copy_permissions=form_result['group_copy_permissions'] ) + Session().flush() + + repo_group_data = repo_group.get_api_data() + audit_logger.store_web( + 'repo_group.create', action_data={'data': repo_group_data}, + user=c.rhodecode_user) + Session().commit() + _new_group_name = form_result['group_name_full'] + repo_group_url = h.link_to( _new_group_name, - h.url('repo_group_home', group_name=_new_group_name)) + h.route_path('repo_group_home', repo_group_name=_new_group_name)) h.flash(h.literal(_('Created repository group %s') % repo_group_url), category='success') - # TODO: in futureaction_logger(, '', '', '', self.sa) + except formencode.Invalid as errors: return htmlfill.render( render('admin/repo_groups/repo_group_add.mako'), @@ -216,8 +221,6 @@ class RepoGroupsController(BaseControlle # perm checks inside @NotAnonymous() def new(self): - """GET /repo_groups/new: Form to create a new item""" - # url('new_repo_group') # perm check for admin, create_group perm or admin of parent_group parent_group_id = safe_int(request.GET.get('parent_group')) if not self._can_create_repo_group(parent_group_id): @@ -229,12 +232,6 @@ class RepoGroupsController(BaseControlle @HasRepoGroupPermissionAnyDecorator('group.admin') @auth.CSRFRequired() def update(self, group_name): - """PUT /repo_groups/group_name: Update an existing item""" - # Forms posted to this method should contain a hidden field: - # - # Or using helpers: - # h.form(url('repos_group', group_name=GROUP_NAME), method='put') - # url('repo_group_home', group_name=GROUP_NAME) c.repo_group = RepoGroupModel()._get_repo_group(group_name) can_create_in_root = self._can_create_repo_group() @@ -250,16 +247,21 @@ class RepoGroupsController(BaseControlle available_groups=c.repo_groups_choices, can_create_in_root=can_create_in_root, allow_disabled=True)() + old_values = c.repo_group.get_api_data() try: form_result = repo_group_form.to_python(dict(request.POST)) gr_name = form_result['group_name'] new_gr = RepoGroupModel().update(group_name, form_result) + + audit_logger.store_web( + 'repo_group.edit', action_data={'old_data': old_values}, + user=c.rhodecode_user) + Session().commit() h.flash(_('Updated repository group %s') % (gr_name,), category='success') # we now have new name ! group_name = new_gr.group_name - # TODO: in future action_logger(, '', '', '', self.sa) except formencode.Invalid as errors: c.active = 'settings' return htmlfill.render( @@ -279,13 +281,6 @@ class RepoGroupsController(BaseControlle @HasRepoGroupPermissionAnyDecorator('group.admin') @auth.CSRFRequired() def delete(self, group_name): - """DELETE /repo_groups/group_name: Delete an existing item""" - # Forms posted to this method should contain a hidden field: - # - # Or using helpers: - # h.form(url('repos_group', group_name=GROUP_NAME), method='delete') - # url('repo_group_home', group_name=GROUP_NAME) - gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name) repos = gr.repositories.all() if repos: @@ -307,11 +302,16 @@ class RepoGroupsController(BaseControlle return redirect(url('repo_groups')) try: + old_values = gr.get_api_data() RepoGroupModel().delete(group_name) + + audit_logger.store_web( + 'repo_group.delete', action_data={'old_data': old_values}, + user=c.rhodecode_user) + Session().commit() h.flash(_('Removed repository group %s') % group_name, category='success') - # TODO: in future action_logger(, '', '', '', self.sa) except Exception: log.exception("Exception during deletion of repository group") h.flash(_('Error occurred during deletion of repository group %s') @@ -321,8 +321,7 @@ class RepoGroupsController(BaseControlle @HasRepoGroupPermissionAnyDecorator('group.admin') def edit(self, group_name): - """GET /repo_groups/group_name/edit: Form to edit an existing item""" - # url('edit_repo_group', group_name=GROUP_NAME) + c.active = 'settings' c.repo_group = RepoGroupModel()._get_repo_group(group_name) @@ -346,8 +345,6 @@ class RepoGroupsController(BaseControlle @HasRepoGroupPermissionAnyDecorator('group.admin') def edit_repo_group_advanced(self, group_name): - """GET /repo_groups/group_name/edit: Form to edit an existing item""" - # url('edit_repo_group', group_name=GROUP_NAME) c.active = 'advanced' c.repo_group = RepoGroupModel()._get_repo_group(group_name) @@ -355,8 +352,6 @@ class RepoGroupsController(BaseControlle @HasRepoGroupPermissionAnyDecorator('group.admin') def edit_repo_group_perms(self, group_name): - """GET /repo_groups/group_name/edit: Form to edit an existing item""" - # url('edit_repo_group', group_name=GROUP_NAME) c.active = 'perms' c.repo_group = RepoGroupModel()._get_repo_group(group_name) self.__load_defaults() @@ -374,8 +369,6 @@ class RepoGroupsController(BaseControlle def update_perms(self, group_name): """ Update permissions for given repository group - - :param group_name: """ c.repo_group = RepoGroupModel()._get_repo_group(group_name) @@ -393,14 +386,20 @@ class RepoGroupsController(BaseControlle # iterate over all members(if in recursive mode) of this groups and # set the permissions ! # this can be potentially heavy operation - RepoGroupModel().update_permissions( + changes = RepoGroupModel().update_permissions( c.repo_group, - form['perm_additions'], form['perm_updates'], - form['perm_deletions'], form['recursive']) + form['perm_additions'], form['perm_updates'], form['perm_deletions'], + form['recursive']) - # TODO: implement this - # action_logger(c.rhodecode_user, 'admin_changed_repo_permissions', - # repo_name, self.ip_addr, self.sa) + action_data = { + 'added': changes['added'], + 'updated': changes['updated'], + 'deleted': changes['deleted'], + } + audit_logger.store_web( + 'repo_group.edit.permissions', action_data=action_data, + user=c.rhodecode_user) + Session().commit() h.flash(_('Repository Group permissions updated'), category='success') return redirect(url('edit_repo_group_perms', group_name=group_name)) 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 @@ -41,15 +41,11 @@ from rhodecode.lib.auth import ( HasRepoGroupPermissionAny, HasRepoPermissionAnyDecorator) from rhodecode.lib.base import BaseRepoController, render 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.utils import repo_name_slug, jsonify from rhodecode.lib.utils2 import safe_int, str2bool -from rhodecode.lib.vcs import RepositoryError -from rhodecode.model.db import ( - User, Repository, UserFollowing, RepoGroup, RepositoryField) +from rhodecode.model.db import (Repository, RepoGroup, RepositoryField) from rhodecode.model.forms import ( - RepoForm, RepoFieldForm, RepoPermsForm, RepoVcsSettingsForm, - IssueTrackerPatternsForm) + RepoForm, RepoFieldForm, RepoVcsSettingsForm, IssueTrackerPatternsForm) from rhodecode.model.meta import Session from rhodecode.model.repo import RepoModel from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList @@ -185,7 +181,7 @@ class ReposController(BaseRepoController except Exception as e: msg = self._log_creation_exception(e, form_result.get('repo_name')) h.flash(msg, category='error') - return redirect(url('home')) + return redirect(h.route_path('home')) return redirect(h.url('repo_creating_home', repo_name=form_result['repo_name_full'], @@ -265,7 +261,7 @@ class ReposController(BaseRepoController if task.failed(): msg = self._log_creation_exception(task.result, c.repo) h.flash(msg, category='error') - return redirect(url('home'), code=501) + return redirect(h.route_path('home'), code=501) repo = Repository.get_by_repo_name(repo_name) if repo and repo.repo_state == Repository.STATE_CREATED: @@ -274,9 +270,9 @@ class ReposController(BaseRepoController h.flash(_('Created repository %s from %s') % (repo.repo_name, clone_uri), category='success') else: - repo_url = h.link_to(repo.repo_name, - h.url('summary_home', - repo_name=repo.repo_name)) + repo_url = h.link_to( + repo.repo_name, + h.route_path('repo_summary',repo_name=repo.repo_name)) fork = repo.fork if fork: fork_name = fork.repo_name @@ -288,165 +284,14 @@ class ReposController(BaseRepoController return {'result': True} return {'result': False} - @HasRepoPermissionAllDecorator('repository.admin') - @auth.CSRFRequired() - def update(self, repo_name): - """ - PUT /repos/repo_name: Update an existing item""" - # Forms posted to this method should contain a hidden field: - # - # Or using helpers: - # h.form(url('repo', repo_name=ID), - # method='put') - # url('repo', repo_name=ID) - - self.__load_data(repo_name) - c.active = 'settings' - c.repo_fields = RepositoryField.query()\ - .filter(RepositoryField.repository == c.repo_info).all() - - repo_model = RepoModel() - changed_name = repo_name - - c.personal_repo_group = c.rhodecode_user.personal_repo_group - # override the choices with extracted revisions ! - repo = Repository.get_by_repo_name(repo_name) - old_data = { - 'repo_name': repo_name, - 'repo_group': repo.group.get_dict() if repo.group else {}, - 'repo_type': repo.repo_type, - } - _form = RepoForm( - edit=True, old_data=old_data, repo_groups=c.repo_groups_choices, - landing_revs=c.landing_revs_choices, allow_disabled=True)() - - try: - form_result = _form.to_python(dict(request.POST)) - repo = repo_model.update(repo_name, **form_result) - ScmModel().mark_for_invalidation(repo_name) - h.flash(_('Repository %s updated successfully') % repo_name, - category='success') - changed_name = repo.repo_name - action_logger(c.rhodecode_user, 'admin_updated_repo', - changed_name, self.ip_addr, self.sa) - Session().commit() - except formencode.Invalid as errors: - defaults = self.__load_data(repo_name) - defaults.update(errors.value) - return htmlfill.render( - render('admin/repos/repo_edit.mako'), - defaults=defaults, - errors=errors.error_dict or {}, - prefix_error=False, - encoding="UTF-8", - force_defaults=False) - - except Exception: - log.exception("Exception during update of repository") - h.flash(_('Error occurred during update of repository %s') \ - % repo_name, category='error') - return redirect(url('edit_repo', repo_name=changed_name)) - - @HasRepoPermissionAllDecorator('repository.admin') - @auth.CSRFRequired() - def delete(self, repo_name): - """ - DELETE /repos/repo_name: Delete an existing item""" - # Forms posted to this method should contain a hidden field: - # - # Or using helpers: - # h.form(url('repo', repo_name=ID), - # method='delete') - # url('repo', repo_name=ID) - - repo_model = RepoModel() - repo = repo_model.get_by_repo_name(repo_name) - if not repo: - h.not_mapped_error(repo_name) - return redirect(url('repos')) - try: - _forks = repo.forks.count() - handle_forks = None - if _forks and request.POST.get('forks'): - do = request.POST['forks'] - if do == 'detach_forks': - handle_forks = 'detach' - h.flash(_('Detached %s forks') % _forks, category='success') - elif do == 'delete_forks': - handle_forks = 'delete' - h.flash(_('Deleted %s forks') % _forks, category='success') - repo_model.delete(repo, forks=handle_forks) - action_logger(c.rhodecode_user, 'admin_deleted_repo', - repo_name, self.ip_addr, self.sa) - ScmModel().mark_for_invalidation(repo_name) - h.flash(_('Deleted repository %s') % repo_name, category='success') - Session().commit() - except AttachedForksError: - h.flash(_('Cannot delete %s it still contains attached forks') - % repo_name, category='warning') - - except Exception: - log.exception("Exception during deletion of repository") - h.flash(_('An error occurred during deletion of %s') % repo_name, - category='error') - - return redirect(url('repos')) - @HasPermissionAllDecorator('hg.admin') def show(self, repo_name, format='html'): """GET /repos/repo_name: Show a specific item""" # url('repo', repo_name=ID) @HasRepoPermissionAllDecorator('repository.admin') - def edit(self, repo_name): - """GET /repo_name/settings: Form to edit an existing item""" - # url('edit_repo', repo_name=ID) - defaults = self.__load_data(repo_name) - if 'clone_uri' in defaults: - del defaults['clone_uri'] - - c.repo_fields = RepositoryField.query()\ - .filter(RepositoryField.repository == c.repo_info).all() - c.personal_repo_group = c.rhodecode_user.personal_repo_group - c.active = 'settings' - return htmlfill.render( - render('admin/repos/repo_edit.mako'), - defaults=defaults, - encoding="UTF-8", - force_defaults=False) - - @HasRepoPermissionAllDecorator('repository.admin') - def edit_permissions(self, repo_name): - """GET /repo_name/settings: Form to edit an existing item""" - # url('edit_repo', repo_name=ID) - c.repo_info = self._load_repo(repo_name) - c.active = 'permissions' - defaults = RepoModel()._get_defaults(repo_name) - - return htmlfill.render( - render('admin/repos/repo_edit.mako'), - defaults=defaults, - encoding="UTF-8", - force_defaults=False) - - @HasRepoPermissionAllDecorator('repository.admin') - @auth.CSRFRequired() - def edit_permissions_update(self, repo_name): - form = RepoPermsForm()().to_python(request.POST) - RepoModel().update_permissions(repo_name, - form['perm_additions'], form['perm_updates'], form['perm_deletions']) - - #TODO: implement this - #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions', - # repo_name, self.ip_addr, self.sa) - Session().commit() - h.flash(_('Repository permissions updated'), category='success') - return redirect(url('edit_repo_perms', repo_name=repo_name)) - - @HasRepoPermissionAllDecorator('repository.admin') def edit_fields(self, repo_name): """GET /repo_name/settings: Form to edit an existing item""" - # url('edit_repo', repo_name=ID) c.repo_info = self._load_repo(repo_name) c.repo_fields = RepositoryField.query()\ .filter(RepositoryField.repository == c.repo_info).all() @@ -490,106 +335,6 @@ class ReposController(BaseRepoController h.flash(msg, category='error') return redirect(url('edit_repo_fields', repo_name=repo_name)) - @HasRepoPermissionAllDecorator('repository.admin') - def edit_advanced(self, repo_name): - """GET /repo_name/settings: Form to edit an existing item""" - # url('edit_repo', repo_name=ID) - c.repo_info = self._load_repo(repo_name) - c.default_user_id = User.get_default_user().user_id - c.in_public_journal = UserFollowing.query()\ - .filter(UserFollowing.user_id == c.default_user_id)\ - .filter(UserFollowing.follows_repository == c.repo_info).scalar() - - c.active = 'advanced' - c.has_origin_repo_read_perm = False - if c.repo_info.fork: - c.has_origin_repo_read_perm = h.HasRepoPermissionAny( - 'repository.write', 'repository.read', 'repository.admin')( - c.repo_info.fork.repo_name, 'repo set as fork page') - - if request.POST: - return redirect(url('repo_edit_advanced')) - return render('admin/repos/repo_edit.mako') - - @HasRepoPermissionAllDecorator('repository.admin') - @auth.CSRFRequired() - def edit_advanced_journal(self, repo_name): - """ - Set's this repository to be visible in public journal, - in other words assing default user to follow this repo - - :param repo_name: - """ - - try: - repo_id = Repository.get_by_repo_name(repo_name).repo_id - user_id = User.get_default_user().user_id - self.scm_model.toggle_following_repo(repo_id, user_id) - h.flash(_('Updated repository visibility in public journal'), - category='success') - Session().commit() - except Exception: - h.flash(_('An error occurred during setting this' - ' repository in public journal'), - category='error') - - return redirect(url('edit_repo_advanced', repo_name=repo_name)) - - @HasRepoPermissionAllDecorator('repository.admin') - @auth.CSRFRequired() - def edit_advanced_fork(self, repo_name): - """ - Mark given repository as a fork of another - - :param repo_name: - """ - - new_fork_id = request.POST.get('id_fork_of') - try: - - if new_fork_id and not new_fork_id.isdigit(): - log.error('Given fork id %s is not an INT', new_fork_id) - - fork_id = safe_int(new_fork_id) - repo = ScmModel().mark_as_fork(repo_name, fork_id, - c.rhodecode_user.username) - fork = repo.fork.repo_name if repo.fork else _('Nothing') - Session().commit() - h.flash(_('Marked repo %s as fork of %s') % (repo_name, fork), - category='success') - except RepositoryError as e: - log.exception("Repository Error occurred") - h.flash(str(e), category='error') - except Exception as e: - log.exception("Exception while editing fork") - h.flash(_('An error occurred during this operation'), - category='error') - - return redirect(url('edit_repo_advanced', repo_name=repo_name)) - - @HasRepoPermissionAllDecorator('repository.admin') - @auth.CSRFRequired() - def edit_advanced_locking(self, repo_name): - """ - Unlock repository when it is locked ! - - :param repo_name: - """ - try: - repo = Repository.get_by_repo_name(repo_name) - if request.POST.get('set_lock'): - Repository.lock(repo, c.rhodecode_user.user_id, - lock_reason=Repository.LOCK_WEB) - h.flash(_('Locked repository'), category='success') - elif request.POST.get('set_unlock'): - Repository.unlock(repo) - h.flash(_('Unlocked repository'), category='success') - except Exception as e: - log.exception("Exception during unlocking") - h.flash(_('An error occurred during unlocking'), - category='error') - return redirect(url('edit_repo_advanced', repo_name=repo_name)) - @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') @auth.CSRFRequired() def toggle_locking(self, repo_name): @@ -617,32 +362,7 @@ class ReposController(BaseRepoController log.exception("Exception during unlocking") h.flash(_('An error occurred during unlocking'), category='error') - return redirect(url('summary_home', repo_name=repo_name)) - - @HasRepoPermissionAllDecorator('repository.admin') - @auth.CSRFRequired() - def edit_caches(self, repo_name): - """PUT /{repo_name}/settings/caches: invalidate the repo caches.""" - try: - ScmModel().mark_for_invalidation(repo_name, delete=True) - Session().commit() - h.flash(_('Cache invalidation successful'), - category='success') - except Exception: - log.exception("Exception during cache invalidation") - h.flash(_('An error occurred during cache invalidation'), - category='error') - - return redirect(url('edit_repo_caches', repo_name=c.repo_name)) - - @HasRepoPermissionAllDecorator('repository.admin') - def edit_caches_form(self, repo_name): - """GET /repo_name/settings: Form to edit an existing item""" - # url('edit_repo', repo_name=ID) - c.repo_info = self._load_repo(repo_name) - c.active = 'caches' - - return render('admin/repos/repo_edit.mako') + return redirect(h.route_path('repo_summary', repo_name=repo_name)) @HasRepoPermissionAllDecorator('repository.admin') @auth.CSRFRequired() @@ -660,7 +380,6 @@ class ReposController(BaseRepoController @HasRepoPermissionAllDecorator('repository.admin') def edit_remote_form(self, repo_name): """GET /repo_name/settings: Form to edit an existing item""" - # url('edit_repo', repo_name=ID) c.repo_info = self._load_repo(repo_name) c.active = 'remote' @@ -682,7 +401,6 @@ class ReposController(BaseRepoController @HasRepoPermissionAllDecorator('repository.admin') def edit_statistics_form(self, repo_name): """GET /repo_name/settings: Form to edit an existing item""" - # url('edit_repo', repo_name=ID) c.repo_info = self._load_repo(repo_name) repo = c.repo_info.scm_instance() 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 @@ -195,9 +195,12 @@ class SettingsController(BaseController) pyramid_settings = get_current_registry().settings c.svn_proxy_generate_config = pyramid_settings[generate_config] + defaults = self._form_defaults() + + model.create_largeobjects_dirs_if_needed(defaults['paths_root_path']) return htmlfill.render( render('admin/settings/settings.mako'), - defaults=self._form_defaults(), + defaults=defaults, encoding="UTF-8", force_defaults=False) diff --git a/rhodecode/controllers/admin/user_groups.py b/rhodecode/controllers/admin/user_groups.py --- a/rhodecode/controllers/admin/user_groups.py +++ b/rhodecode/controllers/admin/user_groups.py @@ -35,9 +35,11 @@ from sqlalchemy.orm import joinedload from rhodecode.lib import auth from rhodecode.lib import helpers as h +from rhodecode.lib import audit_logger +from rhodecode.lib.ext_json import json from rhodecode.lib.exceptions import UserGroupAssignedException,\ RepoGroupAssignmentError -from rhodecode.lib.utils import jsonify, action_logger +from rhodecode.lib.utils import jsonify from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int from rhodecode.lib.auth import ( LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator, @@ -52,8 +54,7 @@ from rhodecode.model.forms import ( UserGroupForm, UserGroupPermsForm, UserIndividualPermissionsForm, UserPermissionsForm) from rhodecode.model.meta import Session -from rhodecode.lib.utils import action_logger -from rhodecode.lib.ext_json import json + log = logging.getLogger(__name__) @@ -105,8 +106,6 @@ class UserGroupsController(BaseControlle # permission check inside @NotAnonymous() def index(self): - """GET /users_groups: All items in the collection""" - # url('users_groups') from rhodecode.lib.utils import PartialRenderer _render = PartialRenderer('data_table/_dt_elements.mako') @@ -142,8 +141,6 @@ class UserGroupsController(BaseControlle @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true') @auth.CSRFRequired() def create(self): - """POST /users_groups: Create a new item""" - # url('users_groups') users_group_form = UserGroupForm()() try: @@ -154,14 +151,16 @@ class UserGroupsController(BaseControlle owner=c.rhodecode_user.user_id, active=form_result['users_group_active']) Session().flush() - + creation_data = user_group.get_api_data() user_group_name = form_result['users_group_name'] - action_logger(c.rhodecode_user, - 'admin_created_users_group:%s' % user_group_name, - None, self.ip_addr, self.sa) - user_group_link = h.link_to(h.escape(user_group_name), - url('edit_users_group', - user_group_id=user_group.users_group_id)) + + audit_logger.store_web( + 'user_group.create', action_data={'data': creation_data}, + user=c.rhodecode_user) + + user_group_link = h.link_to( + h.escape(user_group_name), + url('edit_users_group', user_group_id=user_group.users_group_id)) h.flash(h.literal(_('Created user group %(user_group_link)s') % {'user_group_link': user_group_link}), category='success') @@ -191,13 +190,6 @@ class UserGroupsController(BaseControlle @HasUserGroupPermissionAnyDecorator('usergroup.admin') @auth.CSRFRequired() def update(self, user_group_id): - """PUT /user_groups/user_group_id: Update an existing item""" - # Forms posted to this method should contain a hidden field: - # - # Or using helpers: - # h.form(url('users_group', user_group_id=ID), - # method='put') - # url('users_group', user_group_id=ID) user_group_id = safe_int(user_group_id) c.user_group = UserGroup.get_or_404(user_group_id) @@ -207,16 +199,22 @@ class UserGroupsController(BaseControlle users_group_form = UserGroupForm( edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)() + old_values = c.user_group.get_api_data() try: form_result = users_group_form.to_python(request.POST) pstruct = peppercorn.parse(request.POST.items()) form_result['users_group_members'] = pstruct['user_group_members'] - UserGroupModel().update(c.user_group, form_result) + user_group, added_members, removed_members = \ + UserGroupModel().update(c.user_group, form_result) updated_user_group = form_result['users_group_name'] - action_logger(c.rhodecode_user, - 'admin_updated_users_group:%s' % updated_user_group, - None, self.ip_addr, self.sa) + + audit_logger.store_web( + 'user_group.edit', action_data={'old_data': old_values}, + user=c.rhodecode_user) + + # TODO(marcink): use added/removed to set user_group.edit.member.add + h.flash(_('Updated user group %s') % updated_user_group, category='success') Session().commit() @@ -241,19 +239,16 @@ class UserGroupsController(BaseControlle @HasUserGroupPermissionAnyDecorator('usergroup.admin') @auth.CSRFRequired() def delete(self, user_group_id): - """DELETE /user_groups/user_group_id: Delete an existing item""" - # Forms posted to this method should contain a hidden field: - # - # Or using helpers: - # h.form(url('users_group', user_group_id=ID), - # method='delete') - # url('users_group', user_group_id=ID) user_group_id = safe_int(user_group_id) c.user_group = UserGroup.get_or_404(user_group_id) force = str2bool(request.POST.get('force')) + old_values = c.user_group.get_api_data() try: UserGroupModel().delete(c.user_group, force=force) + audit_logger.store_web( + 'user.delete', action_data={'old_data': old_values}, + user=c.rhodecode_user) Session().commit() h.flash(_('Successfully deleted user group'), category='success') except UserGroupAssignedException as e: @@ -330,9 +325,9 @@ class UserGroupsController(BaseControlle except RepoGroupAssignmentError: h.flash(_('Target group cannot be the same'), category='error') return redirect(url('edit_user_group_perms', user_group_id=user_group_id)) - #TODO: implement this - #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions', - # repo_name, self.ip_addr, self.sa) + + # TODO(marcink): implement global permissions + # audit_log.store_web('user_group.edit.permissions') Session().commit() h.flash(_('User Group permissions updated'), category='success') return redirect(url('edit_user_group_perms', user_group_id=user_group_id)) @@ -389,8 +384,6 @@ class UserGroupsController(BaseControlle @HasUserGroupPermissionAnyDecorator('usergroup.admin') @auth.CSRFRequired() def update_global_perms(self, user_group_id): - """PUT /users_perm/user_group_id: Update an existing item""" - # url('users_group_perm', user_group_id=ID, method='put') user_group_id = safe_int(user_group_id) user_group = UserGroup.get_or_404(user_group_id) c.active = 'global_perms' @@ -492,6 +485,9 @@ class UserGroupsController(BaseControlle @XHRRequired() @jsonify def user_group_members(self, user_group_id): + """ + Return members of given user group + """ user_group_id = safe_int(user_group_id) user_group = UserGroup.get_or_404(user_group_id) group_members_obj = sorted((x.user for x in user_group.members), @@ -500,8 +496,8 @@ class UserGroupsController(BaseControlle group_members = [ { 'id': user.user_id, - 'first_name': user.name, - 'last_name': user.lastname, + 'first_name': user.first_name, + 'last_name': user.last_name, 'username': user.username, 'icon_link': h.gravatar_url(user.email, 30), 'value_display': h.person(user.email), 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 @@ -31,15 +31,17 @@ from pylons.controllers.util import redi from pylons.i18n.translation import _ from rhodecode.authentication.plugins import auth_rhodecode + +from rhodecode.lib import helpers as h +from rhodecode.lib import auth +from rhodecode.lib import audit_logger +from rhodecode.lib.auth import ( + LoginRequired, HasPermissionAllDecorator, AuthUser) +from rhodecode.lib.base import BaseController, render from rhodecode.lib.exceptions import ( DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException, UserOwnsUserGroupsException, UserCreationError) -from rhodecode.lib import helpers as h -from rhodecode.lib import auth -from rhodecode.lib.auth import ( - LoginRequired, HasPermissionAllDecorator, AuthUser, generate_auth_token) -from rhodecode.lib.base import BaseController, render -from rhodecode.model.auth_token import AuthTokenModel +from rhodecode.lib.utils2 import safe_int, AttributeDict from rhodecode.model.db import ( PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup) @@ -49,8 +51,6 @@ from rhodecode.model.repo_group import R from rhodecode.model.user import UserModel from rhodecode.model.meta import Session from rhodecode.model.permission import PermissionModel -from rhodecode.lib.utils import action_logger -from rhodecode.lib.utils2 import datetime_to_time, safe_int, AttributeDict log = logging.getLogger(__name__) @@ -88,7 +88,6 @@ class UsersController(BaseController): @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() def create(self): - """POST /users: Create a new item""" c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name user_model = UserModel() user_form = UserForm()() @@ -96,9 +95,12 @@ class UsersController(BaseController): form_result = user_form.to_python(dict(request.POST)) user = user_model.create(form_result) Session().flush() + creation_data = user.get_api_data() username = form_result['username'] - action_logger(c.rhodecode_user, 'admin_created_user:%s' % username, - None, self.ip_addr, self.sa) + + audit_logger.store_web( + 'user.create', action_data={'data': creation_data}, + user=c.rhodecode_user) user_link = h.link_to(h.escape(username), url('edit_user', @@ -125,8 +127,6 @@ class UsersController(BaseController): @HasPermissionAllDecorator('hg.admin') def new(self): - """GET /users/new: Form to create a new item""" - # url('new_user') c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name self._get_personal_repo_group_template_vars() return render('admin/users/user_add.mako') @@ -134,13 +134,7 @@ class UsersController(BaseController): @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() def update(self, user_id): - """PUT /users/user_id: Update an existing item""" - # Forms posted to this method should contain a hidden field: - # - # Or using helpers: - # h.form(url('update_user', user_id=ID), - # method='put') - # url('user', user_id=ID) + user_id = safe_int(user_id) c.user = User.get_or_404(user_id) c.active = 'profile' @@ -152,6 +146,7 @@ class UsersController(BaseController): old_data={'user_id': user_id, 'email': c.user.email})() form_result = {} + old_values = c.user.get_api_data() try: form_result = _form.to_python(dict(request.POST)) skip_attrs = ['extern_type', 'extern_name'] @@ -160,12 +155,15 @@ class UsersController(BaseController): # forbid updating username for external accounts skip_attrs.append('username') - UserModel().update_user(user_id, skip_attrs=skip_attrs, **form_result) - usr = form_result['username'] - action_logger(c.rhodecode_user, 'admin_updated_user:%s' % usr, - None, self.ip_addr, self.sa) + UserModel().update_user( + user_id, skip_attrs=skip_attrs, **form_result) + + audit_logger.store_web( + 'user.edit', action_data={'old_data': old_values}, + user=c.rhodecode_user) + + Session().commit() h.flash(_('User updated successfully'), category='success') - Session().commit() except formencode.Invalid as errors: defaults = errors.value e = errors.error_dict or {} @@ -188,13 +186,6 @@ class UsersController(BaseController): @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() def delete(self, user_id): - """DELETE /users/user_id: Delete an existing item""" - # Forms posted to this method should contain a hidden field: - # - # Or using helpers: - # h.form(url('delete_user', user_id=ID), - # method='delete') - # url('user', user_id=ID) user_id = safe_int(user_id) c.user = User.get_or_404(user_id) @@ -249,10 +240,16 @@ class UsersController(BaseController): _('Deleted %s user groups') % len(_user_groups), category='success') + old_values = c.user.get_api_data() try: UserModel().delete(c.user, handle_repos=handle_repos, handle_repo_groups=handle_repo_groups, handle_user_groups=handle_user_groups) + + audit_logger.store_web( + 'user.delete', action_data={'old_data': old_values}, + user=c.rhodecode_user) + Session().commit() set_handle_flash_repos() set_handle_flash_repo_groups() @@ -272,19 +269,25 @@ class UsersController(BaseController): def reset_password(self, user_id): """ toggle reset password flag for this user - - :param user_id: """ user_id = safe_int(user_id) c.user = User.get_or_404(user_id) try: old_value = c.user.user_data.get('force_password_change') c.user.update_userdata(force_password_change=not old_value) - Session().commit() + if old_value: msg = _('Force password change disabled for user') + audit_logger.store_web( + 'user.edit.password_reset.disabled', + user=c.rhodecode_user) else: msg = _('Force password change enabled for user') + audit_logger.store_web( + 'user.edit.password_reset.enabled', + user=c.rhodecode_user) + + Session().commit() h.flash(msg, category='success') except Exception: log.exception("Exception during password reset for user") @@ -298,8 +301,6 @@ class UsersController(BaseController): def create_personal_repo_group(self, user_id): """ Create personal repository group for this user - - :param user_id: """ from rhodecode.model.repo_group import RepoGroupModel @@ -381,8 +382,7 @@ class UsersController(BaseController): return redirect(h.route_path('users')) c.active = 'advanced' - c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr) - c.personal_repo_group = c.perm_user.personal_repo_group + c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id) c.personal_repo_group_name = RepoGroupModel()\ .get_personal_group_name(user) c.first_admin = User.get_first_super_admin() @@ -429,8 +429,6 @@ class UsersController(BaseController): @HasPermissionAllDecorator('hg.admin') @auth.CSRFRequired() def update_global_perms(self, user_id): - """PUT /users_perm/user_id: Update an existing item""" - # url('user_perm', user_id=ID, method='put') user_id = safe_int(user_id) user = User.get_or_404(user_id) c.active = 'global_perms' @@ -457,11 +455,13 @@ class UsersController(BaseController): PermissionModel().update_user_permissions(form_result) + # TODO(marcink): implement global permissions + # audit_log.store_web('user.edit.permissions') + Session().commit() h.flash(_('User global permissions updated successfully'), category='success') - Session().commit() except formencode.Invalid as errors: defaults = errors.value c.user = user @@ -491,140 +491,3 @@ class UsersController(BaseController): return render('admin/users/user_edit.mako') - @HasPermissionAllDecorator('hg.admin') - def edit_emails(self, user_id): - user_id = safe_int(user_id) - c.user = User.get_or_404(user_id) - if c.user.username == User.DEFAULT_USER: - h.flash(_("You can't edit this user"), category='warning') - return redirect(h.route_path('users')) - - c.active = 'emails' - c.user_email_map = UserEmailMap.query() \ - .filter(UserEmailMap.user == c.user).all() - - defaults = c.user.get_dict() - return htmlfill.render( - render('admin/users/user_edit.mako'), - defaults=defaults, - encoding="UTF-8", - force_defaults=False) - - @HasPermissionAllDecorator('hg.admin') - @auth.CSRFRequired() - def add_email(self, user_id): - """POST /user_emails:Add an existing item""" - # url('user_emails', user_id=ID, method='put') - user_id = safe_int(user_id) - c.user = User.get_or_404(user_id) - - email = request.POST.get('new_email') - user_model = UserModel() - - try: - user_model.add_extra_email(user_id, email) - Session().commit() - h.flash(_("Added new email address `%s` for user account") % email, - category='success') - except formencode.Invalid as error: - msg = error.error_dict['email'] - h.flash(msg, category='error') - except Exception: - log.exception("Exception during email saving") - h.flash(_('An error occurred during email saving'), - category='error') - return redirect(url('edit_user_emails', user_id=user_id)) - - @HasPermissionAllDecorator('hg.admin') - @auth.CSRFRequired() - def delete_email(self, user_id): - """DELETE /user_emails_delete/user_id: Delete an existing item""" - # url('user_emails_delete', user_id=ID, method='delete') - user_id = safe_int(user_id) - c.user = User.get_or_404(user_id) - email_id = request.POST.get('del_email_id') - user_model = UserModel() - user_model.delete_extra_email(user_id, email_id) - Session().commit() - h.flash(_("Removed email address from user account"), category='success') - return redirect(url('edit_user_emails', user_id=user_id)) - - @HasPermissionAllDecorator('hg.admin') - def edit_ips(self, user_id): - user_id = safe_int(user_id) - c.user = User.get_or_404(user_id) - if c.user.username == User.DEFAULT_USER: - h.flash(_("You can't edit this user"), category='warning') - return redirect(h.route_path('users')) - - c.active = 'ips' - c.user_ip_map = UserIpMap.query() \ - .filter(UserIpMap.user == c.user).all() - - c.inherit_default_ips = c.user.inherit_default_permissions - c.default_user_ip_map = UserIpMap.query() \ - .filter(UserIpMap.user == User.get_default_user()).all() - - defaults = c.user.get_dict() - return htmlfill.render( - render('admin/users/user_edit.mako'), - defaults=defaults, - encoding="UTF-8", - force_defaults=False) - - @HasPermissionAllDecorator('hg.admin') - @auth.CSRFRequired() - def add_ip(self, user_id): - """POST /user_ips:Add an existing item""" - # url('user_ips', user_id=ID, method='put') - - user_id = safe_int(user_id) - c.user = User.get_or_404(user_id) - user_model = UserModel() - try: - ip_list = user_model.parse_ip_range(request.POST.get('new_ip')) - except Exception as e: - ip_list = [] - log.exception("Exception during ip saving") - h.flash(_('An error occurred during ip saving:%s' % (e,)), - category='error') - - desc = request.POST.get('description') - added = [] - for ip in ip_list: - try: - user_model.add_extra_ip(user_id, ip, desc) - Session().commit() - added.append(ip) - except formencode.Invalid as error: - msg = error.error_dict['ip'] - h.flash(msg, category='error') - except Exception: - log.exception("Exception during ip saving") - h.flash(_('An error occurred during ip saving'), - category='error') - if added: - h.flash( - _("Added ips %s to user whitelist") % (', '.join(ip_list), ), - category='success') - if 'default_user' in request.POST: - return redirect(url('admin_permissions_ips')) - return redirect(url('edit_user_ips', user_id=user_id)) - - @HasPermissionAllDecorator('hg.admin') - @auth.CSRFRequired() - def delete_ip(self, user_id): - """DELETE /user_ips_delete/user_id: Delete an existing item""" - # url('user_ips_delete', user_id=ID, method='delete') - user_id = safe_int(user_id) - c.user = User.get_or_404(user_id) - - ip_id = request.POST.get('del_ip_id') - user_model = UserModel() - user_model.delete_extra_ip(user_id, ip_id) - Session().commit() - h.flash(_("Removed ip address from user whitelist"), category='success') - - if 'default_user' in request.POST: - return redirect(url('admin_permissions_ips')) - return redirect(url('edit_user_ips', user_id=user_id)) diff --git a/rhodecode/controllers/base_references.py b/rhodecode/controllers/base_references.py deleted file mode 100644 --- a/rhodecode/controllers/base_references.py +++ /dev/null @@ -1,85 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2010-2017 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 pylons import tmpl_context as c - -from rhodecode.controllers import utils -from rhodecode.lib import helpers as h -from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator -from rhodecode.lib.base import BaseRepoController, render -from rhodecode.lib.ext_json import json -from rhodecode.lib.utils import PartialRenderer -from rhodecode.lib.utils2 import datetime_to_time - - -class BaseReferencesController(BaseRepoController): - """ - Base for reference controllers for branches, tags and bookmarks. - - Implement and set the following things: - - - `partials_template` is the source for the partials to use. - - - `template` is the template to render in the end. - - - `_get_reference_items(repo)` should return a sequence of tuples which - map from `name` to `commit_id`. - - """ - - @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', - 'repository.admin') - def index(self): - _render = PartialRenderer(self.partials_template) - _data = [] - pre_load = ["author", "date", "message"] - repo = c.rhodecode_repo - is_svn = h.is_svn(repo) - format_ref_id = utils.get_format_ref_id(repo) - - for ref_name, commit_id in self._get_reference_items(repo): - commit = repo.get_commit( - commit_id=commit_id, pre_load=pre_load) - - # TODO: johbo: Unify generation of reference links - use_commit_id = '/' in ref_name or is_svn - files_url = h.url( - 'files_home', - repo_name=c.repo_name, - f_path=ref_name if is_svn else '', - revision=commit_id if use_commit_id else ref_name, - at=ref_name) - - _data.append({ - "name": _render('name', ref_name, files_url), - "name_raw": ref_name, - "date": _render('date', commit.date), - "date_raw": datetime_to_time(commit.date), - "author": _render('author', commit.author), - "commit": _render( - 'commit', commit.message, commit.raw_id, commit.idx), - "commit_raw": commit.idx, - "compare": _render( - 'compare', format_ref_id(ref_name, commit.raw_id)), - }) - c.has_references = bool(_data) - c.data = json.dumps(_data) - return render(self.template) diff --git a/rhodecode/controllers/bookmarks.py b/rhodecode/controllers/bookmarks.py deleted file mode 100644 --- a/rhodecode/controllers/bookmarks.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2011-2017 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/ -""" -Bookmarks controller for rhodecode -""" - -import logging - -from pylons import tmpl_context as c -from webob.exc import HTTPNotFound - -from rhodecode.controllers.base_references import BaseReferencesController -from rhodecode.lib import helpers as h - -log = logging.getLogger(__name__) - - -class BookmarksController(BaseReferencesController): - - partials_template = 'bookmarks/bookmarks_data.mako' - template = 'bookmarks/bookmarks.mako' - - def __before__(self): - super(BookmarksController, self).__before__() - if not h.is_hg(c.rhodecode_repo): - raise HTTPNotFound() - - def _get_reference_items(self, repo): - return repo.bookmarks.items() diff --git a/rhodecode/controllers/branches.py b/rhodecode/controllers/branches.py deleted file mode 100644 --- a/rhodecode/controllers/branches.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2010-2017 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/ - -""" -branches controller for rhodecode -""" - -import logging - -from pylons import tmpl_context as c - -from rhodecode.controllers.base_references import BaseReferencesController - - -log = logging.getLogger(__name__) - - -class BranchesController(BaseReferencesController): - - partials_template = 'branches/branches_data.mako' - template = 'branches/branches.mako' - - def __before__(self): - super(BranchesController, self).__before__() - c.closed_branches = c.rhodecode_repo.branches_closed - - def _get_reference_items(self, repo): - return repo.branches_all.items() diff --git a/rhodecode/controllers/changelog.py b/rhodecode/controllers/changelog.py --- a/rhodecode/controllers/changelog.py +++ b/rhodecode/controllers/changelog.py @@ -46,27 +46,6 @@ log = logging.getLogger(__name__) DEFAULT_CHANGELOG_SIZE = 20 -def _load_changelog_summary(): - p = safe_int(request.GET.get('page'), 1) - size = safe_int(request.GET.get('size'), 10) - - def url_generator(**kw): - return url('summary_home', - repo_name=c.rhodecode_db_repo.repo_name, size=size, **kw) - - pre_load = ['author', 'branch', 'date', 'message'] - try: - collection = c.rhodecode_repo.get_commits(pre_load=pre_load) - except EmptyRepositoryError: - collection = c.rhodecode_repo - - c.repo_commits = RepoPage( - collection, page=p, items_per_page=size, url=url_generator) - page_ids = [x.raw_id for x in c.repo_commits] - c.comments = c.rhodecode_db_repo.get_comments(page_ids) - c.statuses = c.rhodecode_db_repo.statuses(page_ids) - - class ChangelogController(BaseRepoController): def __before__(self): @@ -88,13 +67,11 @@ class ChangelogController(BaseRepoContro except EmptyRepositoryError: if not redirect_after: return None - h.flash(h.literal(_('There are no commits yet')), - category='warning') + h.flash(_('There are no commits yet'), category='warning') redirect(url('changelog_home', repo_name=repo.repo_name)) except RepositoryError as e: - msg = safe_str(e) - log.exception(msg) - h.flash(msg, category='warning') + log.exception(safe_str(e)) + h.flash(safe_str(h.escape(e)), category='warning') if not partial: redirect(h.url('changelog_home', repo_name=repo.repo_name)) raise HTTPBadRequest() @@ -134,7 +111,7 @@ class ChangelogController(BaseRepoContro def _check_if_valid_branch(self, branch_name, repo_name, f_path): if branch_name not in c.rhodecode_repo.branches_all: - h.flash('Branch {} is not found.'.format(branch_name), + h.flash('Branch {} is not found.'.format(h.escape(branch_name)), category='warning') redirect(url('changelog_file_home', repo_name=repo_name, revision=branch_name, f_path=f_path or '')) @@ -210,12 +187,11 @@ class ChangelogController(BaseRepoContro collection, p, chunk_size, c.branch_name, dynamic=f_path) except EmptyRepositoryError as e: - h.flash(safe_str(e), category='warning') - return redirect(url('summary_home', repo_name=repo_name)) + h.flash(safe_str(h.escape(e)), category='warning') + return redirect(h.route_path('repo_summary', repo_name=repo_name)) except (RepositoryError, CommitDoesNotExistError, Exception) as e: - msg = safe_str(e) - log.exception(msg) - h.flash(msg, category='error') + log.exception(safe_str(e)) + h.flash(safe_str(h.escape(e)), category='error') return redirect(url('changelog_home', repo_name=repo_name)) if (request.environ.get('HTTP_X_PARTIAL_XHR') @@ -279,12 +255,3 @@ class ChangelogController(BaseRepoContro c.rhodecode_repo, c.pagination, prev_data=prev_data, next_data=next_data) return render('changelog/changelog_elements.mako') - - @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', - 'repository.admin') - def changelog_summary(self, repo_name): - if request.environ.get('HTTP_X_PJAX'): - _load_changelog_summary() - return render('changelog/changelog_summary_data.mako') - raise HTTPNotFound() diff --git a/rhodecode/controllers/changeset.py b/rhodecode/controllers/changeset.py --- a/rhodecode/controllers/changeset.py +++ b/rhodecode/controllers/changeset.py @@ -39,8 +39,8 @@ from rhodecode.lib.base import BaseRepoC from rhodecode.lib.compat import OrderedDict from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError import rhodecode.lib.helpers as h -from rhodecode.lib.utils import action_logger, jsonify -from rhodecode.lib.utils2 import safe_unicode +from rhodecode.lib.utils import jsonify +from rhodecode.lib.utils2 import safe_unicode, safe_int from rhodecode.lib.vcs.backends.base import EmptyCommit from rhodecode.lib.vcs.exceptions import ( RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError) @@ -48,7 +48,6 @@ from rhodecode.model.db import Changeset from rhodecode.model.changeset_status import ChangesetStatusModel from rhodecode.model.comment import CommentsModel from rhodecode.model.meta import Session -from rhodecode.model.repo import RepoModel log = logging.getLogger(__name__) @@ -268,8 +267,10 @@ class ChangesetController(BaseRepoContro repo_name=c.repo_name, source_node_getter=_node_getter(commit1), target_node_getter=_node_getter(commit2), - comments=inline_comments - ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id) + comments=inline_comments) + diffset = diffset.render_patchset( + _parsed, commit1.raw_id, commit2.raw_id) + c.changes[commit.raw_id] = diffset else: # downloads/raw we only need RAW diff nothing else @@ -368,7 +369,6 @@ class ChangesetController(BaseRepoContro comment_type=comment_type, resolves_comment_id=resolves_comment_id ) - c.inline_comment = True if comment.line_no else False # get status if set ! if status: @@ -433,20 +433,26 @@ class ChangesetController(BaseRepoContro @auth.CSRFRequired() @jsonify def delete_comment(self, repo_name, comment_id): - comment = ChangesetComment.get(comment_id) + comment = ChangesetComment.get_or_404(safe_int(comment_id)) if not comment: log.debug('Comment with id:%s not found, skipping', comment_id) # comment already deleted in another call probably return True - owner = (comment.author.user_id == c.rhodecode_user.user_id) is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name) - if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner: - CommentsModel().delete(comment=comment) + super_admin = h.HasPermissionAny('hg.admin')() + comment_owner = (comment.author.user_id == c.rhodecode_user.user_id) + is_repo_comment = comment.repo.repo_name == c.repo_name + comment_repo_admin = is_repo_admin and is_repo_comment + + if super_admin or comment_owner or comment_repo_admin: + CommentsModel().delete(comment=comment, user=c.rhodecode_user) Session().commit() return True else: - raise HTTPForbidden() + log.warning('No permissions for user %s to delete comment_id: %s', + c.rhodecode_user, comment_id) + raise HTTPNotFound() @LoginRequired() @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', diff --git a/rhodecode/controllers/compare.py b/rhodecode/controllers/compare.py --- a/rhodecode/controllers/compare.py +++ b/rhodecode/controllers/compare.py @@ -24,7 +24,7 @@ Compare controller for showing differenc import logging -from webob.exc import HTTPBadRequest +from webob.exc import HTTPBadRequest, HTTPNotFound from pylons import request, tmpl_context as c, url from pylons.controllers.util import redirect from pylons.i18n.translation import _ @@ -63,14 +63,13 @@ class CompareController(BaseRepoControll return repo.scm_instance().EMPTY_COMMIT h.flash(h.literal(_('There are no commits yet')), category='warning') - redirect(url('summary_home', repo_name=repo.repo_name)) + redirect(h.route_path('repo_summary', repo_name=repo.repo_name)) except RepositoryError as e: - msg = safe_str(e) - log.exception(msg) - h.flash(msg, category='warning') + log.exception(safe_str(e)) + h.flash(safe_str(h.escape(e)), category='warning') if not partial: - redirect(h.url('summary_home', repo_name=repo.repo_name)) + redirect(h.route_path('repo_summary', repo_name=repo.repo_name)) raise HTTPBadRequest() @LoginRequired() @@ -86,6 +85,10 @@ class CompareController(BaseRepoControll target_repo = request.GET.get('target_repo', source_repo) c.source_repo = Repository.get_by_repo_name(source_repo) c.target_repo = Repository.get_by_repo_name(target_repo) + + if c.source_repo is None or c.target_repo is None: + raise HTTPNotFound() + c.source_ref = c.target_ref = _('Select commit') c.source_ref_type = "" c.target_ref_type = "" @@ -141,18 +144,17 @@ class CompareController(BaseRepoControll target_repo = Repository.get_by_repo_name(target_repo_name) if source_repo is None: - msg = _('Could not find the original repo: %(repo)s') % { - 'repo': source_repo} - - log.error(msg) - h.flash(msg, category='error') + log.error('Could not find the source repo: {}' + .format(source_repo_name)) + h.flash(_('Could not find the source repo: `{}`') + .format(h.escape(source_repo_name)), category='error') return redirect(url('compare_home', repo_name=c.repo_name)) if target_repo is None: - msg = _('Could not find the other repo: %(repo)s') % { - 'repo': target_repo_name} - log.error(msg) - h.flash(msg, category='error') + log.error('Could not find the target repo: {}' + .format(source_repo_name)) + h.flash(_('Could not find the target repo: `{}`') + .format(h.escape(target_repo_name)), category='error') return redirect(url('compare_home', repo_name=c.repo_name)) source_scm = source_repo.scm_instance() @@ -269,11 +271,13 @@ class CompareController(BaseRepoControll return None return get_node - c.diffset = codeblocks.DiffSet( + diffset = codeblocks.DiffSet( repo_name=source_repo.repo_name, source_node_getter=_node_getter(source_commit), target_node_getter=_node_getter(target_commit), - ).render_patchset(_parsed, source_ref, target_ref) + ) + c.diffset = diffset.render_patchset( + _parsed, source_ref, target_ref) c.preview_mode = merge c.source_commit = source_commit diff --git a/rhodecode/controllers/feed.py b/rhodecode/controllers/feed.py --- a/rhodecode/controllers/feed.py +++ b/rhodecode/controllers/feed.py @@ -113,7 +113,7 @@ class FeedController(BaseRepoController) def _generate_feed(cache_key): feed = Atom1Feed( title=self.title % repo_name, - link=url('summary_home', repo_name=repo_name, qualified=True), + link=h.route_url('repo_summary', repo_name=repo_name), description=self.description % repo_name, language=self.language, ttl=self.ttl @@ -150,8 +150,7 @@ class FeedController(BaseRepoController) def _generate_feed(cache_key): feed = Rss201rev2Feed( title=self.title % repo_name, - link=url('summary_home', repo_name=repo_name, - qualified=True), + link=h.route_url('repo_summary', repo_name=repo_name), description=self.description % repo_name, language=self.language, ttl=self.ttl diff --git a/rhodecode/controllers/files.py b/rhodecode/controllers/files.py --- a/rhodecode/controllers/files.py +++ b/rhodecode/controllers/files.py @@ -35,10 +35,10 @@ from webob.exc import HTTPNotFound, HTTP from rhodecode.controllers.utils import parse_path_ref from rhodecode.lib import diffs, helpers as h, caches -from rhodecode.lib.compat import OrderedDict +from rhodecode.lib import audit_logger from rhodecode.lib.codeblocks import ( filenode_as_lines_tokens, filenode_as_annotated_lines_tokens) -from rhodecode.lib.utils import jsonify, action_logger +from rhodecode.lib.utils import jsonify from rhodecode.lib.utils2 import ( convert_line_endings, detect_mode, safe_str, str2bool) from rhodecode.lib.auth import ( @@ -101,13 +101,13 @@ class FilesController(BaseRepoController add_new = "" h.flash(h.literal( _('There are no files yet. %s') % add_new), category='warning') - redirect(h.url('summary_home', repo_name=repo_name)) + redirect(h.route_path('repo_summary', repo_name=repo_name)) except (CommitDoesNotExistError, LookupError): msg = _('No such commit exists for this repository') h.flash(msg, category='error') raise HTTPNotFound() except RepositoryError as e: - h.flash(safe_str(e), category='error') + h.flash(safe_str(h.escape(e)), category='error') raise HTTPNotFound() def __get_filenode_or_redirect(self, repo_name, commit, path): @@ -124,12 +124,11 @@ class FilesController(BaseRepoController if file_node.is_dir(): raise RepositoryError('The given path is a directory') except CommitDoesNotExistError: - msg = _('No such commit exists for this repository') - log.exception(msg) - h.flash(msg, category='error') + log.exception('No such commit exists for this repository') + h.flash(_('No such commit exists for this repository'), category='error') raise HTTPNotFound() except RepositoryError as e: - h.flash(safe_str(e), category='error') + h.flash(safe_str(h.escape(e)), category='error') raise HTTPNotFound() return file_node @@ -257,7 +256,7 @@ class FilesController(BaseRepoController repo_name, c.commit.raw_id, f_path) except RepositoryError as e: - h.flash(safe_str(e), category='error') + h.flash(safe_str(h.escape(e)), category='error') raise HTTPNotFound() if request.environ.get('HTTP_X_PJAX'): @@ -450,7 +449,7 @@ class FilesController(BaseRepoController c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path) c.default_message = _( - 'Deleted file %s via RhodeCode Enterprise') % (f_path) + 'Deleted file {} via RhodeCode Enterprise').format(f_path) c.f_path = f_path node_path = f_path author = c.rhodecode_user.full_contact @@ -469,12 +468,12 @@ class FilesController(BaseRepoController author=author, ) - h.flash(_('Successfully deleted file %s') % f_path, - category='success') + h.flash( + _('Successfully deleted file `{}`').format( + h.escape(f_path)), category='success') except Exception: - msg = _('Error occurred during commit') - log.exception(msg) - h.flash(msg, category='error') + log.exception('Error during commit operation') + h.flash(_('Error occurred during commit'), category='error') return redirect(url('changeset_home', repo_name=c.repo_name, revision='tip')) @@ -503,7 +502,7 @@ class FilesController(BaseRepoController c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path) c.default_message = _( - 'Deleted file %s via RhodeCode Enterprise') % (f_path) + 'Deleted file {} via RhodeCode Enterprise').format(f_path) c.f_path = f_path return render('files/files_delete.mako') @@ -537,7 +536,7 @@ class FilesController(BaseRepoController return redirect(url('files_home', repo_name=c.repo_name, revision=c.commit.raw_id, f_path=f_path)) c.default_message = _( - 'Edited file %s via RhodeCode Enterprise') % (f_path) + 'Edited file {} via RhodeCode Enterprise').format(f_path) c.f_path = f_path old_content = c.file.content sl = old_content.splitlines(1) @@ -575,12 +574,12 @@ class FilesController(BaseRepoController parent_commit=c.commit, ) - h.flash(_('Successfully committed to %s') % f_path, - category='success') + h.flash( + _('Successfully committed changes to file `{}`').format( + h.escape(f_path)), category='success') except Exception: - msg = _('Error occurred during commit') - log.exception(msg) - h.flash(msg, category='error') + log.exception('Error occurred during commit') + h.flash(_('Error occurred during commit'), category='error') return redirect(url('changeset_home', repo_name=c.repo_name, revision='tip')) @@ -612,7 +611,7 @@ class FilesController(BaseRepoController return redirect(url('files_home', repo_name=c.repo_name, revision=c.commit.raw_id, f_path=f_path)) c.default_message = _( - 'Edited file %s via RhodeCode Enterprise') % (f_path) + 'Edited file {} via RhodeCode Enterprise').format(f_path) c.f_path = f_path return render('files/files_edit.mako') @@ -660,7 +659,7 @@ class FilesController(BaseRepoController file_obj = r_post.get('upload_file', None) if file_obj is not None and hasattr(file_obj, 'filename'): - filename = file_obj.filename + filename = r_post.get('filename_upload') content = file_obj.file if hasattr(content, 'file'): @@ -669,14 +668,14 @@ class FilesController(BaseRepoController # If there's no commit, redirect to repo summary if type(c.commit) is EmptyCommit: - redirect_url = "summary_home" + redirect_url = h.route_path('repo_summary', repo_name=c.repo_name) else: - redirect_url = "changeset_home" + redirect_url = url("changeset_home", repo_name=c.repo_name, + revision='tip') if not filename: h.flash(_('No filename'), category='warning') - return redirect(url(redirect_url, repo_name=c.repo_name, - revision='tip')) + return redirect(redirect_url) # extract the location from filename, # allows using foo/bar.txt syntax to create subdirectories @@ -704,8 +703,9 @@ class FilesController(BaseRepoController author=author, ) - h.flash(_('Successfully committed to %s') % node_path, - category='success') + h.flash( + _('Successfully committed new file `{}`').format( + h.escape(node_path)), category='success') except NonRelativePathError as e: h.flash(_( 'The location specified must be a relative path and must not ' @@ -713,11 +713,10 @@ class FilesController(BaseRepoController return redirect(url('changeset_home', repo_name=c.repo_name, revision='tip')) except (NodeError, NodeAlreadyExistsError) as e: - h.flash(_(e), category='error') + h.flash(_(h.escape(e)), category='error') except Exception: - msg = _('Error occurred during commit') - log.exception(msg) - h.flash(msg, category='error') + log.exception('Error occurred during commit') + h.flash(_('Error occurred during commit'), category='error') return redirect(url('changeset_home', repo_name=c.repo_name, revision='tip')) @@ -801,7 +800,7 @@ class FilesController(BaseRepoController if not use_cached_archive: # generate new archive fd, archive = tempfile.mkstemp() - log.debug('Creating new temp archive in %s' % (archive,)) + log.debug('Creating new temp archive in %s', archive) try: commit.archive_repo(archive, kind=fileformat, subrepos=subrepos) except ImproperArchiveTypeError: @@ -809,10 +808,26 @@ class FilesController(BaseRepoController if archive_cache_enabled: # if we generated the archive and we have cache enabled # let's use this for future - log.debug('Storing new archive in %s' % (cached_archive_path,)) + log.debug('Storing new archive in %s', cached_archive_path) shutil.move(archive, cached_archive_path) archive = cached_archive_path + # store download action + audit_logger.store_web( + 'repo.archive.download', action_data={ + 'user_agent': request.user_agent, + 'archive_name': archive_name, + 'archive_spec': fname, + 'archive_cached': use_cached_archive}, + user=c.rhodecode_user, + repo=dbrepo, + commit=True + ) + + response.content_disposition = str( + 'attachment; filename=%s' % archive_name) + response.content_type = str(content_type) + def get_chunked_archive(archive): with open(archive, 'rb') as stream: while True: @@ -826,14 +841,6 @@ class FilesController(BaseRepoController break yield data - # store download action - action_logger(user=c.rhodecode_user, - action='user_downloaded_archive:%s' % archive_name, - repo=repo_name, ipaddr=self.ip_addr, commit=True) - response.content_disposition = str( - 'attachment; filename=%s' % archive_name) - response.content_type = str(content_type) - return get_chunked_archive(archive) @LoginRequired() diff --git a/rhodecode/controllers/forks.py b/rhodecode/controllers/forks.py --- a/rhodecode/controllers/forks.py +++ b/rhodecode/controllers/forks.py @@ -139,7 +139,7 @@ class ForksController(BaseRepoController c.repo_info = Repository.get_by_repo_name(repo_name) if not c.repo_info: h.not_mapped_error(repo_name) - return redirect(url('home')) + return redirect(h.route_path('home')) defaults = self.__load_data(repo_name) diff --git a/rhodecode/controllers/home.py b/rhodecode/controllers/home.py --- a/rhodecode/controllers/home.py +++ b/rhodecode/controllers/home.py @@ -24,20 +24,15 @@ Home controller for RhodeCode Enterprise import logging import time -import re -from pylons import tmpl_context as c, request, url, config -from pylons.i18n.translation import _ -from sqlalchemy.sql import func +from pylons import tmpl_context as c from rhodecode.lib.auth import ( - LoginRequired, HasPermissionAllDecorator, AuthUser, - HasRepoGroupPermissionAnyDecorator, XHRRequired) + LoginRequired, HasPermissionAllDecorator, + HasRepoGroupPermissionAnyDecorator) from rhodecode.lib.base import BaseController, render -from rhodecode.lib.index import searcher_from_config + from rhodecode.lib.ext_json import json -from rhodecode.lib.utils import jsonify -from rhodecode.lib.utils2 import safe_unicode, str2bool from rhodecode.model.db import Repository, RepoGroup from rhodecode.model.repo import RepoModel from rhodecode.model.repo_group import RepoGroupModel @@ -70,221 +65,3 @@ class HomeController(BaseController): msg = ('RhodeCode Enterprise %s test exception. Generation time: %s' % (c.rhodecode_name, time.time())) raise TestException(msg) - - def _get_groups_and_repos(self, repo_group_id=None): - # repo groups groups - repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id) - _perms = ['group.read', 'group.write', 'group.admin'] - repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms) - repo_group_data = RepoGroupModel().get_repo_groups_as_dict( - repo_group_list=repo_group_list_acl, admin=False) - - # repositories - repo_list = Repository.get_all_repos(group_id=repo_group_id) - _perms = ['repository.read', 'repository.write', 'repository.admin'] - repo_list_acl = RepoList(repo_list, perm_set=_perms) - repo_data = RepoModel().get_repos_as_dict( - repo_list=repo_list_acl, admin=False) - - return repo_data, repo_group_data - - @LoginRequired() - def index(self): - c.repo_group = None - - repo_data, repo_group_data = self._get_groups_and_repos() - # json used to render the grids - c.repos_data = json.dumps(repo_data) - c.repo_groups_data = json.dumps(repo_group_data) - - return render('/index.mako') - - @LoginRequired() - @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write', - 'group.admin') - def index_repo_group(self, group_name): - """GET /repo_group_name: Show a specific item""" - c.repo_group = RepoGroupModel()._get_repo_group(group_name) - repo_data, repo_group_data = self._get_groups_and_repos( - c.repo_group.group_id) - - # json used to render the grids - c.repos_data = json.dumps(repo_data) - c.repo_groups_data = json.dumps(repo_group_data) - - return render('index_repo_group.mako') - - def _get_repo_list(self, name_contains=None, repo_type=None, limit=20): - query = Repository.query()\ - .order_by(func.length(Repository.repo_name))\ - .order_by(Repository.repo_name) - - if repo_type: - query = query.filter(Repository.repo_type == repo_type) - - if name_contains: - ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) - query = query.filter( - Repository.repo_name.ilike(ilike_expression)) - query = query.limit(limit) - - all_repos = query.all() - repo_iter = self.scm_model.get_repos(all_repos) - return [ - { - 'id': obj['name'], - 'text': obj['name'], - 'type': 'repo', - 'obj': obj['dbrepo'], - 'url': url('summary_home', repo_name=obj['name']) - } - for obj in repo_iter] - - def _get_repo_group_list(self, name_contains=None, limit=20): - query = RepoGroup.query()\ - .order_by(func.length(RepoGroup.group_name))\ - .order_by(RepoGroup.group_name) - - if name_contains: - ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) - query = query.filter( - RepoGroup.group_name.ilike(ilike_expression)) - query = query.limit(limit) - - all_groups = query.all() - repo_groups_iter = self.scm_model.get_repo_groups(all_groups) - return [ - { - 'id': obj.group_name, - 'text': obj.group_name, - 'type': 'group', - 'obj': {}, - 'url': url('repo_group_home', group_name=obj.group_name) - } - for obj in repo_groups_iter] - - def _get_hash_commit_list(self, hash_starts_with=None, limit=20): - if not hash_starts_with or len(hash_starts_with) < 3: - return [] - - commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with) - - if len(commit_hashes) != 1: - return [] - - commit_hash_prefix = commit_hashes[0] - - auth_user = AuthUser( - user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr) - searcher = searcher_from_config(config) - result = searcher.search( - 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user, - raise_on_exc=False) - - return [ - { - 'id': entry['commit_id'], - 'text': entry['commit_id'], - 'type': 'commit', - 'obj': {'repo': entry['repository']}, - 'url': url('changeset_home', - repo_name=entry['repository'], - revision=entry['commit_id']) - } - for entry in result['results']] - - @LoginRequired() - @XHRRequired() - @jsonify - def goto_switcher_data(self): - query = request.GET.get('query') - log.debug('generating goto switcher list, query %s', query) - - res = [] - repo_groups = self._get_repo_group_list(query) - if repo_groups: - res.append({ - 'text': _('Groups'), - 'children': repo_groups - }) - - repos = self._get_repo_list(query) - if repos: - res.append({ - 'text': _('Repositories'), - 'children': repos - }) - - commits = self._get_hash_commit_list(query) - if commits: - unique_repos = {} - for commit in commits: - unique_repos.setdefault(commit['obj']['repo'], [] - ).append(commit) - - for repo in unique_repos: - res.append({ - 'text': _('Commits in %(repo)s') % {'repo': repo}, - 'children': unique_repos[repo] - }) - - data = { - 'more': False, - 'results': res - } - return data - - @LoginRequired() - @XHRRequired() - @jsonify - def repo_list_data(self): - query = request.GET.get('query') - repo_type = request.GET.get('repo_type') - log.debug('generating repo list, query:%s', query) - - res = [] - repos = self._get_repo_list(query, repo_type=repo_type) - if repos: - res.append({ - 'text': _('Repositories'), - 'children': repos - }) - - data = { - 'more': False, - 'results': res - } - return data - - @LoginRequired() - @XHRRequired() - @jsonify - def user_autocomplete_data(self): - query = request.GET.get('query') - active = str2bool(request.GET.get('active') or True) - - repo_model = RepoModel() - _users = repo_model.get_users( - name_contains=query, only_active=active) - - if request.GET.get('user_groups'): - # extend with user groups - _user_groups = repo_model.get_user_groups( - name_contains=query, only_active=active) - _users = _users + _user_groups - - return {'suggestions': _users} - - @LoginRequired() - @XHRRequired() - @jsonify - def user_group_autocomplete_data(self): - query = request.GET.get('query') - active = str2bool(request.GET.get('active') or True) - - repo_model = RepoModel() - _user_groups = repo_model.get_user_groups( - name_contains=query, only_active=active) - _user_groups = _user_groups - - return {'suggestions': _user_groups} diff --git a/rhodecode/controllers/journal.py b/rhodecode/controllers/journal.py --- a/rhodecode/controllers/journal.py +++ b/rhodecode/controllers/journal.py @@ -34,11 +34,11 @@ from webob.exc import HTTPBadRequest from pylons import request, tmpl_context as c, response, url from pylons.i18n.translation import _ -from rhodecode.controllers.admin.admin import user_log_filter from rhodecode.model.db import UserLog, UserFollowing, User, UserApiKeys from rhodecode.model.meta import Session import rhodecode.lib.helpers as h from rhodecode.lib.helpers import Page +from rhodecode.lib.user_log_filter import user_log_filter from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired from rhodecode.lib.base import BaseController, render from rhodecode.lib.utils2 import safe_int, AttributeDict diff --git a/rhodecode/controllers/pullrequests.py b/rhodecode/controllers/pullrequests.py --- a/rhodecode/controllers/pullrequests.py +++ b/rhodecode/controllers/pullrequests.py @@ -21,8 +21,6 @@ """ pull requests controller for rhodecode for initializing pull requests """ -import types - import peppercorn import formencode import logging @@ -33,6 +31,7 @@ from pylons import request, tmpl_context from pylons.controllers.util import redirect from pylons.i18n.translation import _ from pyramid.threadlocal import get_current_registry +from pyramid.httpexceptions import HTTPFound from sqlalchemy.sql import func from sqlalchemy.sql.expression import or_ @@ -72,124 +71,6 @@ class PullrequestsController(BaseRepoCon c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED - def _extract_ordering(self, request): - column_index = safe_int(request.GET.get('order[0][column]')) - order_dir = request.GET.get('order[0][dir]', 'desc') - order_by = request.GET.get( - 'columns[%s][data][sort]' % column_index, 'name_raw') - return order_by, order_dir - - @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', - 'repository.admin') - @HasAcceptedRepoType('git', 'hg') - def show_all(self, repo_name): - # filter types - c.active = 'open' - c.source = str2bool(request.GET.get('source')) - c.closed = str2bool(request.GET.get('closed')) - c.my = str2bool(request.GET.get('my')) - c.awaiting_review = str2bool(request.GET.get('awaiting_review')) - c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review')) - c.repo_name = repo_name - - opened_by = None - if c.my: - c.active = 'my' - opened_by = [c.rhodecode_user.user_id] - - statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN] - if c.closed: - c.active = 'closed' - statuses = [PullRequest.STATUS_CLOSED] - - if c.awaiting_review and not c.source: - c.active = 'awaiting' - if c.source and not c.awaiting_review: - c.active = 'source' - if c.awaiting_my_review: - c.active = 'awaiting_my' - - data = self._get_pull_requests_list( - repo_name=repo_name, opened_by=opened_by, statuses=statuses) - if not request.is_xhr: - c.data = json.dumps(data['data']) - c.records_total = data['recordsTotal'] - return render('/pullrequests/pullrequests.mako') - else: - return json.dumps(data) - - def _get_pull_requests_list(self, repo_name, opened_by, statuses): - # pagination - start = safe_int(request.GET.get('start'), 0) - length = safe_int(request.GET.get('length'), c.visual.dashboard_items) - order_by, order_dir = self._extract_ordering(request) - - if c.awaiting_review: - pull_requests = PullRequestModel().get_awaiting_review( - repo_name, source=c.source, opened_by=opened_by, - statuses=statuses, offset=start, length=length, - order_by=order_by, order_dir=order_dir) - pull_requests_total_count = PullRequestModel( - ).count_awaiting_review( - repo_name, source=c.source, statuses=statuses, - opened_by=opened_by) - elif c.awaiting_my_review: - pull_requests = PullRequestModel().get_awaiting_my_review( - repo_name, source=c.source, opened_by=opened_by, - user_id=c.rhodecode_user.user_id, statuses=statuses, - offset=start, length=length, order_by=order_by, - order_dir=order_dir) - pull_requests_total_count = PullRequestModel( - ).count_awaiting_my_review( - repo_name, source=c.source, user_id=c.rhodecode_user.user_id, - statuses=statuses, opened_by=opened_by) - else: - pull_requests = PullRequestModel().get_all( - repo_name, source=c.source, opened_by=opened_by, - statuses=statuses, offset=start, length=length, - order_by=order_by, order_dir=order_dir) - pull_requests_total_count = PullRequestModel().count_all( - repo_name, source=c.source, statuses=statuses, - opened_by=opened_by) - - from rhodecode.lib.utils import PartialRenderer - _render = PartialRenderer('data_table/_dt_elements.mako') - data = [] - for pr in pull_requests: - comments = CommentsModel().get_all_comments( - c.rhodecode_db_repo.repo_id, pull_request=pr) - - data.append({ - 'name': _render('pullrequest_name', - pr.pull_request_id, pr.target_repo.repo_name), - 'name_raw': pr.pull_request_id, - 'status': _render('pullrequest_status', - pr.calculated_review_status()), - 'title': _render( - 'pullrequest_title', pr.title, pr.description), - 'description': h.escape(pr.description), - 'updated_on': _render('pullrequest_updated_on', - h.datetime_to_time(pr.updated_on)), - 'updated_on_raw': h.datetime_to_time(pr.updated_on), - 'created_on': _render('pullrequest_updated_on', - h.datetime_to_time(pr.created_on)), - 'created_on_raw': h.datetime_to_time(pr.created_on), - 'author': _render('pullrequest_author', - pr.author.full_contact, ), - 'author_raw': pr.author.full_name, - 'comments': _render('pullrequest_comments', len(comments)), - 'comments_raw': len(comments), - 'closed': pr.is_closed(), - }) - # json used to render the grid - data = ({ - 'data': data, - 'recordsTotal': pull_requests_total_count, - 'recordsFiltered': pull_requests_total_count, - }) - return data - @LoginRequired() @NotAnonymous() @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', @@ -203,7 +84,7 @@ class PullrequestsController(BaseRepoCon except EmptyRepositoryError: h.flash(h.literal(_('There are no commits yet')), category='warning') - redirect(url('summary_home', repo_name=source_repo.repo_name)) + redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name)) commit_id = request.GET.get('commit') branch_ref = request.GET.get('branch') @@ -346,8 +227,6 @@ class PullrequestsController(BaseRepoCon target_repo = _form['target_repo'] target_ref = _form['target_ref'] commit_ids = _form['revisions'][::-1] - reviewers = [ - (r['user_id'], r['reasons']) for r in _form['review_members']] # find the ancestor for this pr source_db_repo = Repository.get_by_repo_name(_form['source_repo']) @@ -375,23 +254,36 @@ class PullrequestsController(BaseRepoCon ) description = _form['pullrequest_desc'] + + get_default_reviewers_data, validate_default_reviewers = \ + PullRequestModel().get_reviewer_functions() + + # recalculate reviewers logic, to make sure we can validate this + reviewer_rules = get_default_reviewers_data( + c.rhodecode_user.get_instance(), source_db_repo, + source_commit, target_db_repo, target_commit) + + given_reviewers = _form['review_members'] + reviewers = validate_default_reviewers(given_reviewers, reviewer_rules) + try: pull_request = PullRequestModel().create( c.rhodecode_user.user_id, source_repo, source_ref, target_repo, target_ref, commit_ids, reviewers, pullrequest_title, - description + description, reviewer_rules ) Session().commit() h.flash(_('Successfully opened new pull request'), category='success') except Exception as e: - msg = _('Error occurred during sending pull request') + msg = _('Error occurred during creation of this pull request.') log.exception(msg) h.flash(msg, category='error') return redirect(url('pullrequest_home', repo_name=repo_name)) - return redirect(url('pullrequest_show', repo_name=target_repo, - pull_request_id=pull_request.pull_request_id)) + raise HTTPFound( + h.route_path('pullrequest_show', repo_name=target_repo, + pull_request_id=pull_request.pull_request_id)) @LoginRequired() @NotAnonymous() @@ -410,11 +302,10 @@ class PullrequestsController(BaseRepoCon if 'review_members' in controls: self._update_reviewers( - pull_request_id, controls['review_members']) + pull_request_id, controls['review_members'], + pull_request.reviewer_data) elif str2bool(request.POST.get('update_commits', 'false')): self._update_commits(pull_request) - elif str2bool(request.POST.get('close_pull_request', 'false')): - self._reject_close(pull_request) elif str2bool(request.POST.get('edit_pull_request', 'false')): self._edit_pull_request(pull_request) else: @@ -426,7 +317,7 @@ class PullrequestsController(BaseRepoCon try: PullRequestModel().edit( pull_request, request.POST.get('title'), - request.POST.get('description')) + request.POST.get('description'), c.rhodecode_user) except ValueError: msg = _(u'Cannot update closed pull requests.') h.flash(msg, category='error') @@ -492,7 +383,7 @@ class PullrequestsController(BaseRepoCon msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason] warning_reasons = [ UpdateFailureReason.NO_CHANGE, - UpdateFailureReason.WRONG_REF_TPYE, + UpdateFailureReason.WRONG_REF_TYPE, ] category = 'warning' if resp.reason in warning_reasons else 'error' h.flash(msg, category=category) @@ -529,10 +420,10 @@ class PullrequestsController(BaseRepoCon scm=pull_request.target_repo.repo_type) self._merge_pull_request(pull_request, user, extras) - return redirect(url( - 'pullrequest_show', - repo_name=pull_request.target_repo.repo_name, - pull_request_id=pull_request.pull_request_id)) + raise HTTPFound( + h.route_path('pullrequest_show', + repo_name=pull_request.target_repo.repo_name, + pull_request_id=pull_request.pull_request_id)) def _merge_pull_request(self, pull_request, user, extras): merge_resp = PullRequestModel().merge( @@ -553,18 +444,21 @@ class PullrequestsController(BaseRepoCon merge_resp.failure_reason) h.flash(msg, category='error') - def _update_reviewers(self, pull_request_id, review_members): - reviewers = [ - (int(r['user_id']), r['reasons']) for r in review_members] - PullRequestModel().update_reviewers(pull_request_id, reviewers) - Session().commit() + def _update_reviewers(self, pull_request_id, review_members, reviewer_rules): + + get_default_reviewers_data, validate_default_reviewers = \ + PullRequestModel().get_reviewer_functions() - def _reject_close(self, pull_request): - if pull_request.is_closed(): - raise HTTPForbidden() + try: + reviewers = validate_default_reviewers(review_members, reviewer_rules) + except ValueError as e: + log.error('Reviewers Validation: {}'.format(e)) + h.flash(e, category='error') + return - PullRequestModel().close_pull_request_with_comment( - pull_request, c.rhodecode_user, c.rhodecode_db_repo) + PullRequestModel().update_reviewers( + pull_request_id, reviewers, c.rhodecode_user) + h.flash(_('Pull request reviewers updated.'), category='success') Session().commit() @LoginRequired() @@ -583,7 +477,7 @@ class PullrequestsController(BaseRepoCon # only owner can delete it ! if allowed_to_delete: - PullRequestModel().delete(pull_request) + PullRequestModel().delete(pull_request, c.rhodecode_user) Session().commit() h.flash(_('Successfully deleted pull request'), category='success') @@ -608,7 +502,8 @@ class PullrequestsController(BaseRepoCon _org_pull_request_obj = pull_request_ver.pull_request at_version = pull_request_ver.pull_request_version_id else: - _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id) + _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404( + pull_request_id) pull_request_display_obj = PullRequest.get_pr_display_object( pull_request_obj, _org_pull_request_obj) @@ -715,9 +610,9 @@ class PullrequestsController(BaseRepoCon c.allowed_to_comment = False c.allowed_to_close = False else: - c.allowed_to_change_status = PullRequestModel(). \ - check_user_change_status(pull_request_at_ver, c.rhodecode_user) \ - and not pr_closed + can_change_status = PullRequestModel().check_user_change_status( + pull_request_at_ver, c.rhodecode_user) + c.allowed_to_change_status = can_change_status and not pr_closed c.allowed_to_update = PullRequestModel().check_user_update( pull_request_latest, c.rhodecode_user) and not pr_closed @@ -726,7 +621,24 @@ class PullrequestsController(BaseRepoCon c.allowed_to_delete = PullRequestModel().check_user_delete( pull_request_latest, c.rhodecode_user) and not pr_closed c.allowed_to_comment = not pr_closed - c.allowed_to_close = c.allowed_to_change_status and not pr_closed + c.allowed_to_close = c.allowed_to_merge and not pr_closed + + c.forbid_adding_reviewers = False + c.forbid_author_to_review = False + c.forbid_commit_author_to_review = False + + if pull_request_latest.reviewer_data and \ + 'rules' in pull_request_latest.reviewer_data: + rules = pull_request_latest.reviewer_data['rules'] or {} + try: + c.forbid_adding_reviewers = rules.get( + 'forbid_adding_reviewers') + c.forbid_author_to_review = rules.get( + 'forbid_author_to_review') + c.forbid_commit_author_to_review = rules.get( + 'forbid_commit_author_to_review') + except Exception: + pass # check merge capabilities _merge_check = MergeCheck.validate( @@ -748,7 +660,7 @@ class PullrequestsController(BaseRepoCon # GENERAL COMMENTS with versions # q = comments_model._all_general_comments_of_pull_request(pull_request_latest) q = q.order_by(ChangesetComment.comment_id.asc()) - general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc()) + general_comments = q # pick comments we want to render at current version c.comment_versions = comments_model.aggregate_comments( @@ -758,7 +670,8 @@ class PullrequestsController(BaseRepoCon # INLINE COMMENTS with versions # q = comments_model._all_inline_comments_of_pull_request(pull_request_latest) q = q.order_by(ChangesetComment.comment_id.asc()) - inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc()) + inline_comments = q + c.inline_versions = comments_model.aggregate_comments( inline_comments, versions, c.at_version_num, inline=True) @@ -841,12 +754,18 @@ class PullrequestsController(BaseRepoCon c.commit_ranges.append(comm) commit_cache[comm.raw_id] = comm + # Order here matters, we first need to get target, and then + # the source target_commit = commits_source_repo.get_commit( commit_id=safe_str(target_ref_id)) + source_commit = commits_source_repo.get_commit( commit_id=safe_str(source_ref_id)) + except CommitDoesNotExistError: - pass + log.warning( + 'Failed to get commit from `{}` repo'.format( + commits_source_repo), exc_info=True) except RepositoryRequirementError: log.warning( 'Failed to get all required data from repo', exc_info=True) @@ -960,6 +879,7 @@ class PullrequestsController(BaseRepoCon pull_request_id = safe_int(pull_request_id) pull_request = PullRequest.get_or_404(pull_request_id) if pull_request.is_closed(): + log.debug('comment: forbidden because pull request is closed') raise HTTPForbidden() status = request.POST.get('changeset_status', None) @@ -968,95 +888,95 @@ class PullrequestsController(BaseRepoCon resolves_comment_id = request.POST.get('resolves_comment_id', None) close_pull_request = request.POST.get('close_pull_request') - close_pr = False - if close_pull_request: - close_pr = True - pull_request_review_status = pull_request.calculated_review_status() - if pull_request_review_status == ChangesetStatus.STATUS_APPROVED: - # approved only if we have voting consent - status = ChangesetStatus.STATUS_APPROVED - else: - status = ChangesetStatus.STATUS_REJECTED - - allowed_to_change_status = PullRequestModel().check_user_change_status( - pull_request, c.rhodecode_user) + # the logic here should work like following, if we submit close + # pr comment, use `close_pull_request_with_comment` function + # else handle regular comment logic + user = c.rhodecode_user + repo = c.rhodecode_db_repo - if status and allowed_to_change_status: - message = (_('Status change %(transition_icon)s %(status)s') - % {'transition_icon': '>', - 'status': ChangesetStatus.get_status_lbl(status)}) - if close_pr: - message = _('Closing with') + ' ' + message - text = text or message - comm = CommentsModel().create( - text=text, - repo=c.rhodecode_db_repo.repo_id, - user=c.rhodecode_user.user_id, - pull_request=pull_request_id, - f_path=request.POST.get('f_path'), - 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, - comment_type=comment_type, - resolves_comment_id=resolves_comment_id - ) + if close_pull_request: + # only owner or admin or person with write permissions + allowed_to_close = PullRequestModel().check_user_update( + pull_request, c.rhodecode_user) + if not allowed_to_close: + log.debug('comment: forbidden because not allowed to close ' + 'pull request %s', pull_request_id) + raise HTTPForbidden() + comment, status = PullRequestModel().close_pull_request_with_comment( + pull_request, user, repo, message=text) + Session().flush() + events.trigger( + events.PullRequestCommentEvent(pull_request, comment)) + + else: + # regular comment case, could be inline, or one with status. + # for that one we check also permissions + + allowed_to_change_status = PullRequestModel().check_user_change_status( + pull_request, c.rhodecode_user) + + if status and allowed_to_change_status: + message = (_('Status change %(transition_icon)s %(status)s') + % {'transition_icon': '>', + 'status': ChangesetStatus.get_status_lbl(status)}) + text = text or message - if allowed_to_change_status: - old_calculated_status = pull_request.calculated_review_status() - # get status if set ! - if status: - ChangesetStatusModel().set_status( - c.rhodecode_db_repo.repo_id, - status, - c.rhodecode_user.user_id, - comm, - pull_request=pull_request_id - ) + comment = CommentsModel().create( + text=text, + repo=c.rhodecode_db_repo.repo_id, + user=c.rhodecode_user.user_id, + pull_request=pull_request_id, + f_path=request.POST.get('f_path'), + 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), + comment_type=comment_type, + resolves_comment_id=resolves_comment_id + ) + + if allowed_to_change_status: + # calculate old status before we change it + old_calculated_status = pull_request.calculated_review_status() - 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() - if old_calculated_status != calculated_status: - PullRequestModel()._trigger_pull_request_hook( - pull_request, c.rhodecode_user, 'review_status_change') - - calculated_status_lbl = ChangesetStatus.get_status_lbl( - calculated_status) + # get status if set ! + if status: + ChangesetStatusModel().set_status( + c.rhodecode_db_repo.repo_id, + status, + c.rhodecode_user.user_id, + comment, + pull_request=pull_request_id + ) - if close_pr: - status_completed = ( - calculated_status in [ChangesetStatus.STATUS_APPROVED, - ChangesetStatus.STATUS_REJECTED]) - if close_pull_request or status_completed: - PullRequestModel().close_pull_request( - pull_request_id, c.rhodecode_user) - else: - h.flash(_('Closing pull request on other statuses than ' - 'rejected or approved is forbidden. ' - 'Calculated status from all reviewers ' - 'is currently: %s') % calculated_status_lbl, - category='warning') + Session().flush() + events.trigger( + events.PullRequestCommentEvent(pull_request, comment)) + + # 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() + if old_calculated_status != calculated_status: + PullRequestModel()._trigger_pull_request_hook( + pull_request, c.rhodecode_user, 'review_status_change') Session().commit() if not request.is_xhr: - return redirect(h.url('pullrequest_show', repo_name=repo_name, - pull_request_id=pull_request_id)) + raise HTTPFound( + h.route_path('pullrequest_show', + repo_name=repo_name, + pull_request_id=pull_request_id)) data = { 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))), } - if comm: - c.co = comm - c.inline_comment = True if comm.line_no else False - data.update(comm.get_dict()) - data.update({'rendered_text': - render('changeset/changeset_comment_block.mako')}) + if comment: + c.co = comment + rendered_comment = render('changeset/changeset_comment_block.mako') + data.update(comment.get_dict()) + data.update({'rendered_text': rendered_comment}) return data @@ -1067,25 +987,32 @@ class PullrequestsController(BaseRepoCon @auth.CSRFRequired() @jsonify def delete_comment(self, repo_name, comment_id): - return self._delete_comment(comment_id) + comment = ChangesetComment.get_or_404(safe_int(comment_id)) + if not comment: + log.debug('Comment with id:%s not found, skipping', comment_id) + # comment already deleted in another call probably + return True - def _delete_comment(self, comment_id): - comment_id = safe_int(comment_id) - co = ChangesetComment.get_or_404(comment_id) - if co.pull_request.is_closed(): + if comment.pull_request.is_closed(): # don't allow deleting comments on closed pull request raise HTTPForbidden() - is_owner = co.author.user_id == c.rhodecode_user.user_id is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name) - if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner: - old_calculated_status = co.pull_request.calculated_review_status() - CommentsModel().delete(comment=co) + super_admin = h.HasPermissionAny('hg.admin')() + comment_owner = comment.author.user_id == c.rhodecode_user.user_id + is_repo_comment = comment.repo.repo_name == c.repo_name + comment_repo_admin = is_repo_admin and is_repo_comment + + if super_admin or comment_owner or comment_repo_admin: + old_calculated_status = comment.pull_request.calculated_review_status() + CommentsModel().delete(comment=comment, user=c.rhodecode_user) Session().commit() - calculated_status = co.pull_request.calculated_review_status() + calculated_status = comment.pull_request.calculated_review_status() if old_calculated_status != calculated_status: PullRequestModel()._trigger_pull_request_hook( - co.pull_request, c.rhodecode_user, 'review_status_change') + comment.pull_request, c.rhodecode_user, 'review_status_change') return True else: - raise HTTPForbidden() + log.warning('No permissions for user %s to delete comment_id: %s', + c.rhodecode_user, comment_id) + raise HTTPNotFound() diff --git a/rhodecode/controllers/search.py b/rhodecode/controllers/search.py deleted file mode 100644 --- a/rhodecode/controllers/search.py +++ /dev/null @@ -1,111 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2010-2017 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/ - -""" -Search controller for RhodeCode -""" - -import logging -import urllib - -from pylons import request, config, tmpl_context as c - -from webhelpers.util import update_params - -from rhodecode.lib.auth import LoginRequired, AuthUser -from rhodecode.lib.base import BaseRepoController, render -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__) - - -class SearchController(BaseRepoController): - - @LoginRequired() - def index(self, repo_name=None): - - searcher = searcher_from_config(config) - formatted_results = [] - execution_time = '' - - schema = search_schema.SearchParamsSchema() - - search_params = {} - errors = [] - try: - search_params = schema.deserialize( - dict(search_query=request.GET.get('q'), - search_type=request.GET.get('type'), - search_sort=request.GET.get('sort'), - page_limit=request.GET.get('page_limit'), - requested_page=request.GET.get('page')) - ) - except validation_schema.Invalid as e: - errors = e.children - - def url_generator(**kw): - q = urllib.quote(safe_str(search_query)) - return update_params( - "?q=%s&type=%s" % (q, safe_str(search_type)), **kw) - - search_query = search_params.get('search_query') - search_type = search_params.get('search_type') - search_sort = search_params.get('search_sort') - if search_params.get('search_query'): - 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) - - try: - search_result = searcher.search( - search_query, search_type, c.perm_user, repo_name, - requested_page, page_limit, search_sort) - - formatted_results = Page( - search_result['results'], page=requested_page, - item_count=search_result['count'], - items_per_page=page_limit, url=url_generator) - finally: - searcher.cleanup() - - if not search_result['error']: - execution_time = '%s results (%.3f seconds)' % ( - search_result['count'], - search_result['runtime']) - elif not errors: - node = schema['search_query'] - errors = [ - validation_schema.Invalid(node, search_result['error'])] - - c.sort = search_sort - c.url_generator = url_generator - c.errors = errors - c.formatted_results = formatted_results - c.runtime = execution_time - c.cur_query = search_query - c.search_type = search_type - # Return a rendered template - return render('/search/search.mako') diff --git a/rhodecode/controllers/summary.py b/rhodecode/controllers/summary.py deleted file mode 100644 --- a/rhodecode/controllers/summary.py +++ /dev/null @@ -1,326 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2010-2017 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/ - -""" -Summary controller for RhodeCode Enterprise -""" - -import logging -from string import lower - -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 (LANGUAGES_EXTENSIONS_MAP) -from rhodecode.controllers import utils -from rhodecode.controllers.changelog import _load_changelog_summary -from rhodecode.lib import caches, helpers as h -from rhodecode.lib.utils import jsonify -from rhodecode.lib.utils2 import safe_str -from rhodecode.lib.auth import ( - LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, XHRRequired) -from rhodecode.lib.base import BaseRepoController, render -from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links -from rhodecode.lib.ext_json import json -from rhodecode.lib.vcs.backends.base import EmptyCommit -from rhodecode.lib.vcs.exceptions import ( - CommitError, EmptyRepositoryError, NodeDoesNotExistError) -from rhodecode.model.db import Statistics, CacheKey, User -from rhodecode.model.repo import ReadmeFinder - - -log = logging.getLogger(__name__) - - -class SummaryController(BaseRepoController): - - def __before__(self): - super(SummaryController, self).__before__() - - 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): - readme_data = None - readme_node = None - readme_filename = None - commit = self._get_landing_commit_or_none(db_repo) - if commit: - log.debug("Searching for a README file.") - readme_node = ReadmeFinder(default_renderer).search(commit) - if readme_node: - relative_url = h.url('files_raw_home', - repo_name=repo_name, - revision=commit.raw_id, - f_path=readme_node.path) - readme_data = self._render_readme_or_none( - commit, readme_node, relative_url) - readme_filename = readme_node.path - return readme_data, readme_filename - - invalidator_context = CacheKey.repo_context_cache( - _generate_readme, repo_name, CacheKey.CACHE_TYPE_README) - - with invalidator_context as context: - context.invalidate() - computed = context.compute() - - return computed - - def _get_landing_commit_or_none(self, db_repo): - log.debug("Getting the landing commit.") - try: - commit = db_repo.get_landing_commit() - if not isinstance(commit, EmptyCommit): - return commit - else: - log.debug("Repository is empty, no README to render.") - except CommitError: - log.exception( - "Problem getting commit when trying to render the README.") - - def _render_readme_or_none(self, commit, readme_node, relative_url): - log.debug( - 'Found README file `%s` rendering...', readme_node.path) - renderer = MarkupRenderer() - try: - html_source = renderer.render( - readme_node.content, filename=readme_node.path) - if relative_url: - return relative_links(html_source, relative_url) - return html_source - except Exception: - log.exception( - "Exception while trying to render the README") - - @LoginRequired() - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') - def index(self, repo_name): - - # Prepare the clone URL - - username = '' - if c.rhodecode_user.username != User.DEFAULT_USER: - username = safe_str(c.rhodecode_user.username) - - _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl - if '{repo}' in _def_clone_uri: - _def_clone_uri_by_id = _def_clone_uri.replace( - '{repo}', '_{repoid}') - elif '{repoid}' in _def_clone_uri: - _def_clone_uri_by_id = _def_clone_uri.replace( - '_{repoid}', '{repo}') - - c.clone_repo_url = c.rhodecode_db_repo.clone_url( - user=username, uri_tmpl=_def_clone_uri) - c.clone_repo_url_id = c.rhodecode_db_repo.clone_url( - user=username, uri_tmpl=_def_clone_uri_by_id) - - # If enabled, get statistics data - - c.show_stats = bool(c.rhodecode_db_repo.enable_statistics) - - stats = self.sa.query(Statistics)\ - .filter(Statistics.repository == c.rhodecode_db_repo)\ - .scalar() - - c.stats_percentage = 0 - - if stats and stats.languages: - c.no_data = False is c.rhodecode_db_repo.enable_statistics - lang_stats_d = json.loads(stats.languages) - - # Sort first by decreasing count and second by the file extension, - # so we have a consistent output. - lang_stats_items = sorted(lang_stats_d.iteritems(), - key=lambda k: (-k[1], k[0]))[:10] - lang_stats = [(x, {"count": y, - "desc": LANGUAGES_EXTENSIONS_MAP.get(x)}) - for x, y in lang_stats_items] - - c.trending_languages = json.dumps(lang_stats) - else: - c.no_data = True - c.trending_languages = json.dumps({}) - - c.enable_downloads = c.rhodecode_db_repo.enable_downloads - c.repository_followers = self.scm_model.get_followers( - c.rhodecode_db_repo) - c.repository_forks = self.scm_model.get_forks(c.rhodecode_db_repo) - c.repository_is_user_following = self.scm_model.is_following_repo( - c.repo_name, c.rhodecode_user.user_id) - - if c.repository_requirements_missing: - return render('summary/missing_requirements.mako') - - c.readme_data, c.readme_file = \ - self.__get_readme_data(c.rhodecode_db_repo) - - _load_changelog_summary() - - if request.is_xhr: - return render('changelog/changelog_summary_data.mako') - - return render('summary/summary.mako') - - @LoginRequired() - @XHRRequired() - @HasRepoPermissionAnyDecorator( - 'repository.read', 'repository.write', 'repository.admin') - @jsonify - def repo_stats(self, repo_name, commit_id): - _namespace = caches.get_repo_namespace_key( - caches.SUMMARY_STATS, repo_name) - show_stats = bool(c.rhodecode_db_repo.enable_statistics) - cache_manager = caches.get_cache_manager('repo_cache_long', _namespace) - _cache_key = caches.compute_key_from_params( - repo_name, commit_id, show_stats) - - def compute_stats(): - code_stats = {} - size = 0 - try: - scm_instance = c.rhodecode_db_repo.scm_instance() - commit = scm_instance.get_commit(commit_id) - - for node in commit.get_filenodes_generator(): - size += node.size - if not show_stats: - continue - ext = lower(node.extension) - ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext) - if ext_info: - if ext in code_stats: - code_stats[ext]['count'] += 1 - else: - code_stats[ext] = {"count": 1, "desc": ext_info} - except EmptyRepositoryError: - pass - return {'size': h.format_byte_size_binary(size), - 'code_stats': code_stats} - - stats = cache_manager.get(_cache_key, createfunc=compute_stats) - return stats - - def _switcher_reference_data(self, repo_name, references, is_svn): - """Prepare reference data for given `references`""" - items = [] - for name, commit_id in references.items(): - use_commit_id = '/' in name or is_svn - items.append({ - 'name': name, - 'commit_id': commit_id, - 'files_url': h.url( - 'files_home', - repo_name=repo_name, - f_path=name if is_svn else '', - revision=commit_id if use_commit_id else name, - at=name) - }) - return items - - @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', - 'repository.admin') - @jsonify - def repo_refs_data(self, repo_name): - repo = c.rhodecode_repo - refs_to_create = [ - (_("Branch"), repo.branches, 'branch'), - (_("Tag"), repo.tags, 'tag'), - (_("Bookmark"), repo.bookmarks, 'book'), - ] - res = self._create_reference_data(repo, repo_name, refs_to_create) - data = { - 'more': False, - 'results': res - } - return data - - @LoginRequired() - @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', - 'repository.admin') - @jsonify - def repo_default_reviewers_data(self, repo_name): - return { - 'reviewers': [utils.reviewer_as_json( - user=c.rhodecode_db_repo.user, reasons=None)] - } - - @jsonify - def repo_refs_changelog_data(self, repo_name): - repo = c.rhodecode_repo - - refs_to_create = [ - (_("Branches"), repo.branches, 'branch'), - (_("Closed branches"), repo.branches_closed, 'branch_closed'), - # TODO: enable when vcs can handle bookmarks filters - # (_("Bookmarks"), repo.bookmarks, "book"), - ] - res = self._create_reference_data(repo, repo_name, refs_to_create) - data = { - 'more': False, - 'results': res - } - return data - - def _create_reference_data(self, repo, full_repo_name, refs_to_create): - format_ref_id = utils.get_format_ref_id(repo) - - result = [] - for title, refs, ref_type in refs_to_create: - if refs: - result.append({ - 'text': title, - 'children': self._create_reference_items( - repo, full_repo_name, refs, ref_type, format_ref_id), - }) - return result - - def _create_reference_items(self, repo, full_repo_name, refs, ref_type, - format_ref_id): - result = [] - is_svn = h.is_svn(repo) - 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': ref_name, - 'id': format_ref_id(ref_name, raw_id), - 'raw_id': raw_id, - 'type': ref_type, - 'files_url': files_url, - }) - return result - - 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=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/controllers/tags.py b/rhodecode/controllers/tags.py deleted file mode 100644 --- a/rhodecode/controllers/tags.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (C) 2010-2017 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/ - -""" -Tags controller for rhodecode -""" - -import logging - -from rhodecode.controllers.base_references import BaseReferencesController - -log = logging.getLogger(__name__) - - -class TagsController(BaseReferencesController): - - partials_template = 'tags/tags_data.mako' - template = 'tags/tags.mako' - - def _get_reference_items(self, repo): - return repo.tags.items() diff --git a/rhodecode/controllers/utils.py b/rhodecode/controllers/utils.py --- a/rhodecode/controllers/utils.py +++ b/rhodecode/controllers/utils.py @@ -87,21 +87,3 @@ def get_commit_from_ref_name(repo, ref_n '%s "%s" does not exist' % (ref_type, ref_name)) return repo_scm.get_commit(commit_id) - - -def reviewer_as_json(user, reasons): - """ - Returns json struct of a reviewer for frontend - - :param user: the reviewer - :param reasons: list of strings of why they are reviewers - """ - - return { - 'user_id': user.user_id, - 'reasons': reasons, - 'username': user.username, - 'firstname': user.firstname, - 'lastname': user.lastname, - 'gravatar_link': h.gravatar_url(user.email, 14), - } diff --git a/rhodecode/events/__init__.py b/rhodecode/events/__init__.py --- a/rhodecode/events/__init__.py +++ b/rhodecode/events/__init__.py @@ -18,6 +18,8 @@ import logging from pyramid.threadlocal import get_current_registry +from rhodecode.events.base import RhodecodeEvent + log = logging.getLogger(__name__) @@ -32,20 +34,21 @@ def trigger(event, registry=None): # 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) + log.debug('event %s triggered using registry %s', event, registry) # 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) + # config.add_subscriber( + # 'rhodecode.integrations.integrations_event_handler', + # 'rhodecode.events.RhodecodeEvent') + # trigger(event, request.registry) + 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, UserPostCreate, diff --git a/rhodecode/events/base.py b/rhodecode/events/base.py --- a/rhodecode/events/base.py +++ b/rhodecode/events/base.py @@ -24,7 +24,8 @@ from rhodecode.lib.utils2 import Attribu # this is a user object to be used for events caused by the system (eg. shell) SYSTEM_USER = AttributeDict(dict( - username='__SYSTEM__' + username='__SYSTEM__', + user_id='__SYSTEM_ID__' )) log = logging.getLogger(__name__) @@ -32,12 +33,12 @@ log = logging.getLogger(__name__) class RhodecodeEvent(object): """ - Base event class for all Rhodecode events + Base event class for all RhodeCode events """ name = "RhodeCodeEvent" - def __init__(self): - self.request = get_current_request() + def __init__(self, request=None): + self.request = request or get_current_request() self.utc_timestamp = datetime.utcnow() @property @@ -61,7 +62,8 @@ class RhodecodeEvent(object): instance = auth_user.get_instance() if not instance: return AttributeDict(dict( - username=auth_user.username + username=auth_user.username, + user_id=auth_user.user_id, )) return instance @@ -78,9 +80,8 @@ class RhodecodeEvent(object): def server_url(self): default = '' if self.request: - from rhodecode.lib import helpers as h try: - return h.url('home', qualified=True) + return self.request.route_url('home') except Exception: log.exception('Failed to fetch URL for server') return default @@ -93,7 +94,8 @@ class RhodecodeEvent(object): 'utc_timestamp': self.utc_timestamp, 'actor_ip': self.actor_ip, 'actor': { - 'username': self.actor.username + 'username': self.actor.username, + 'user_id': self.actor.user_id }, 'server_url': self.server_url } diff --git a/rhodecode/events/pullrequest.py b/rhodecode/events/pullrequest.py --- a/rhodecode/events/pullrequest.py +++ b/rhodecode/events/pullrequest.py @@ -41,6 +41,7 @@ class PullRequestEvent(RepoEvent): data = super(PullRequestEvent, self).as_dict() commits = _commits_as_dict( + self, commit_ids=self.pullrequest.revisions, repos=[self.pullrequest.source_repo] ) @@ -52,6 +53,8 @@ class PullRequestEvent(RepoEvent): 'issues': issues, 'pull_request_id': self.pullrequest.pull_request_id, 'url': PullRequestModel().get_url(self.pullrequest), + 'permalink_url': PullRequestModel().get_url( + self.pullrequest, permalink=True), 'status': self.pullrequest.calculated_review_status(), 'commits': commits, } @@ -131,7 +134,9 @@ class PullRequestCommentEvent(PullReques 'type': self.comment.comment_type, 'file': self.comment.f_path, 'line': self.comment.line_no, - 'url': CommentsModel().get_url(self.comment) + 'url': CommentsModel().get_url(self.comment), + 'permalink_url': CommentsModel().get_url( + self.comment, permalink=True), } }) return data diff --git a/rhodecode/events/repo.py b/rhodecode/events/repo.py --- a/rhodecode/events/repo.py +++ b/rhodecode/events/repo.py @@ -16,6 +16,7 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ +import collections import logging from rhodecode.translation import lazy_ugettext @@ -26,17 +27,18 @@ from rhodecode.lib.vcs.exceptions import log = logging.getLogger(__name__) -def _commits_as_dict(commit_ids, repos): +def _commits_as_dict(event, commit_ids, repos): """ Helper function to serialize commit_ids + :param event: class calling this method :param commit_ids: commits to get :param repos: list of repos to check """ from rhodecode.lib.utils2 import extract_mentioned_users - from rhodecode.lib import helpers as h from rhodecode.lib.helpers import ( urlify_commit_message, process_patterns, chop_at_smart) + from rhodecode.model.repo import RepoModel if not repos: raise Exception('no repo defined') @@ -53,7 +55,7 @@ def _commits_as_dict(commit_ids, repos): reviewers = [] for repo in repos: if not needed_commits: - return commits # return early if we have the commits we need + return commits # return early if we have the commits we need vcs_repo = repo.scm_instance(cache=False) try: @@ -62,22 +64,22 @@ def _commits_as_dict(commit_ids, repos): try: cs = vcs_repo.get_changeset(commit_id) except CommitDoesNotExistError: - continue # maybe its in next repo + 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 - ) + cs_data['url'] = RepoModel().get_commit_url( + repo, cs_data['raw_id'], request=event.request) + cs_data['permalink_url'] = RepoModel().get_commit_url( + repo, cs_data['raw_id'], request=event.request, permalink=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) - cs_data['message_html_title'] = chop_at_smart(cs_data['message'], '\n', suffix_if_chopped='...') + cs_data['message_html'] = urlify_commit_message( + cs_data['message'], repo.repo_name) + cs_data['message_html_title'] = chop_at_smart( + cs_data['message'], '\n', suffix_if_chopped='...') commits.append(cs_data) needed_commits.remove(commit_id) @@ -118,12 +120,20 @@ class RepoEvent(RhodecodeEvent): def as_dict(self): from rhodecode.model.repo import RepoModel data = super(RepoEvent, self).as_dict() + extra_fields = collections.OrderedDict() + for field in self.repo.extra_fields: + extra_fields[field.field_key] = field.field_value + 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) + 'url': RepoModel().get_url( + self.repo, request=self.request), + 'permalink_url': RepoModel().get_url( + self.repo, request=self.request, permalink=True), + 'extra_fields': extra_fields } }) return data @@ -235,10 +245,13 @@ class RepoPushEvent(RepoVCSEvent): def as_dict(self): data = super(RepoPushEvent, self).as_dict() - branch_url = repo_url = data['repo']['url'] + + def branch_url(branch_name): + return '{}/changelog?branch={}'.format( + data['repo']['url'], branch_name) commits = _commits_as_dict( - commit_ids=self.pushed_commit_ids, repos=[self.repo]) + self, commit_ids=self.pushed_commit_ids, repos=[self.repo]) last_branch = None for commit in reversed(commits): @@ -251,8 +264,7 @@ class RepoPushEvent(RepoVCSEvent): branches = [ { 'name': branch, - 'url': '{}/changelog?branch={}'.format( - data['repo']['url'], branch) + 'url': branch_url(branch) } for branch in branches ] diff --git a/rhodecode/forms/__init__.py b/rhodecode/forms/__init__.py --- a/rhodecode/forms/__init__.py +++ b/rhodecode/forms/__init__.py @@ -24,6 +24,9 @@ deform - later can be replaced with some """ from rhodecode.translation import _ +from rhodecode.translation import TranslationString + +from mako.template import Template from deform import Button, Form, widget, ValidationFailure @@ -31,3 +34,16 @@ class buttons: save = Button(name='Save', type='submit') reset = Button(name=_('Reset'), type='reset') delete = Button(name=_('Delete'), type='submit') + + +class RcForm(Form): + def render_error(self, request, field): + html = '' + if field.error: + for err in field.error.messages(): + if isinstance(err, TranslationString): + err = request.translate(err) + html = Template( + '${err}').render(err=err) + + return html 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.7.0\n" +"Project-Id-Version: rhodecode-enterprise-ce 4.8.0\n" "Report-Msgid-Bugs-To: marcin@rhodecode.com\n" -"POT-Creation-Date: 2017-04-07 13:01+0200\n" +"POT-Creation-Date: 2017-06-27 17:25+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -24,7 +24,7 @@ msgid "Global" msgstr "" #: rhodecode/apps/admin/navigation.py:84 -#: rhodecode/templates/admin/repos/repo_edit.mako:52 +#: rhodecode/templates/admin/repos/repo_edit.mako:55 msgid "VCS" msgstr "" @@ -37,7 +37,7 @@ msgid "Remap and Rescan" msgstr "" #: rhodecode/apps/admin/navigation.py:87 -#: rhodecode/templates/admin/repos/repo_edit.mako:58 +#: rhodecode/templates/admin/repos/repo_edit.mako:61 msgid "Issue Tracker" msgstr "" @@ -48,7 +48,7 @@ msgstr "" #: rhodecode/templates/admin/my_account/my_account_profile_edit.mako:97 #: rhodecode/templates/admin/users/user_add.mako:86 #: rhodecode/templates/admin/users/user_edit_profile.mako:65 -#: rhodecode/templates/admin/users/users.mako:64 +#: rhodecode/templates/admin/users/users.mako:65 #: rhodecode/templates/email_templates/user_registration.mako:25 #: rhodecode/templates/users/user_profile.mako:51 msgid "Email" @@ -75,7 +75,7 @@ msgstr "" #: rhodecode/templates/admin/integrations/new.mako:17 #: rhodecode/templates/admin/integrations/new.mako:23 #: rhodecode/templates/admin/repo_groups/repo_group_edit.mako:51 -#: rhodecode/templates/admin/repos/repo_edit.mako:72 +#: rhodecode/templates/admin/repos/repo_edit.mako:75 #: rhodecode/templates/base/base.mako:82 msgid "Integrations" msgstr "" @@ -97,11 +97,11 @@ msgstr "" msgid "Labs" msgstr "" -#: rhodecode/apps/admin/views/sessions.py:86 +#: rhodecode/apps/admin/views/sessions.py:92 msgid "Cleaned up old sessions" msgstr "" -#: rhodecode/apps/admin/views/sessions.py:92 +#: rhodecode/apps/admin/views/sessions.py:98 msgid "Failed to cleanup up old sessions" msgstr "" @@ -113,245 +113,409 @@ msgstr "" msgid "Failed to generate the Apache configuration for Subversion." msgstr "" -#: rhodecode/apps/admin/views/system_info.py:95 +#: rhodecode/apps/admin/views/system_info.py:99 msgid "Note: please make sure this server can access `${url}` for the update link to work" msgstr "" -#: rhodecode/apps/admin/views/system_info.py:98 +#: rhodecode/apps/admin/views/system_info.py:102 msgid "Update info" msgstr "" -#: rhodecode/apps/admin/views/system_info.py:100 +#: rhodecode/apps/admin/views/system_info.py:104 msgid "Check for updates" msgstr "" -#: rhodecode/apps/admin/views/system_info.py:105 -msgid "RhodeCode Version" -msgstr "" - -#: rhodecode/apps/admin/views/system_info.py:106 -msgid "RhodeCode Server IP" -msgstr "" - -#: rhodecode/apps/admin/views/system_info.py:107 -msgid "RhodeCode Server ID" -msgstr "" - -#: rhodecode/apps/admin/views/system_info.py:108 -msgid "RhodeCode Configuration" -msgstr "" - #: rhodecode/apps/admin/views/system_info.py:109 -msgid "Workers" +msgid "RhodeCode Version" msgstr "" #: rhodecode/apps/admin/views/system_info.py:110 -msgid "Worker Type" +msgid "RhodeCode Server IP" +msgstr "" + +#: rhodecode/apps/admin/views/system_info.py:111 +msgid "RhodeCode Server ID" +msgstr "" + +#: rhodecode/apps/admin/views/system_info.py:112 +msgid "RhodeCode Configuration" +msgstr "" + +#: rhodecode/apps/admin/views/system_info.py:113 +msgid "RhodeCode Certificate" msgstr "" #: rhodecode/apps/admin/views/system_info.py:114 -msgid "Database" +msgid "Workers" msgstr "" #: rhodecode/apps/admin/views/system_info.py:115 -msgid "Database version" +msgid "Worker Type" msgstr "" #: rhodecode/apps/admin/views/system_info.py:119 -msgid "Platform" +msgid "Database" msgstr "" #: rhodecode/apps/admin/views/system_info.py:120 +msgid "Database version" +msgstr "" + +#: rhodecode/apps/admin/views/system_info.py:124 +msgid "Platform" +msgstr "" + +#: rhodecode/apps/admin/views/system_info.py:125 msgid "Platform UUID" msgstr "" -#: rhodecode/apps/admin/views/system_info.py:121 -msgid "Python version" -msgstr "" - -#: rhodecode/apps/admin/views/system_info.py:122 -msgid "Python path" -msgstr "" - #: rhodecode/apps/admin/views/system_info.py:126 -msgid "CPU" +msgid "Python version" msgstr "" #: rhodecode/apps/admin/views/system_info.py:127 +msgid "Python path" +msgstr "" + +#: rhodecode/apps/admin/views/system_info.py:131 +msgid "CPU" +msgstr "" + +#: rhodecode/apps/admin/views/system_info.py:132 msgid "Load" msgstr "" -#: rhodecode/apps/admin/views/system_info.py:128 -msgid "Memory" -msgstr "" - -#: rhodecode/apps/admin/views/system_info.py:129 -msgid "Uptime" -msgstr "" - #: rhodecode/apps/admin/views/system_info.py:133 -msgid "Storage location" +msgid "Memory" msgstr "" #: rhodecode/apps/admin/views/system_info.py:134 -msgid "Storage info" -msgstr "" - -#: rhodecode/apps/admin/views/system_info.py:135 -msgid "Storage inodes" -msgstr "" - -#: rhodecode/apps/admin/views/system_info.py:137 -msgid "Gist storage location" +msgid "Uptime" msgstr "" #: rhodecode/apps/admin/views/system_info.py:138 -msgid "Gist storage info" +msgid "Storage location" +msgstr "" + +#: rhodecode/apps/admin/views/system_info.py:139 +msgid "Storage info" msgstr "" #: rhodecode/apps/admin/views/system_info.py:140 -msgid "Archive cache storage location" -msgstr "" - -#: rhodecode/apps/admin/views/system_info.py:141 -msgid "Archive cache info" +msgid "Storage inodes" +msgstr "" + +#: rhodecode/apps/admin/views/system_info.py:142 +msgid "Gist storage location" msgstr "" #: rhodecode/apps/admin/views/system_info.py:143 -msgid "Temp storage location" -msgstr "" - -#: rhodecode/apps/admin/views/system_info.py:144 -msgid "Temp storage info" +msgid "Gist storage info" +msgstr "" + +#: rhodecode/apps/admin/views/system_info.py:145 +msgid "Archive cache storage location" msgstr "" #: rhodecode/apps/admin/views/system_info.py:146 -msgid "Search info" -msgstr "" - -#: rhodecode/apps/admin/views/system_info.py:147 -msgid "Search location" +msgid "Archive cache info" +msgstr "" + +#: rhodecode/apps/admin/views/system_info.py:148 +msgid "Temp storage location" +msgstr "" + +#: rhodecode/apps/admin/views/system_info.py:149 +msgid "Temp storage info" msgstr "" #: rhodecode/apps/admin/views/system_info.py:151 -msgid "VCS Backends" +msgid "Search info" msgstr "" #: rhodecode/apps/admin/views/system_info.py:152 +msgid "Search location" +msgstr "" + +#: rhodecode/apps/admin/views/system_info.py:156 +msgid "VCS Backends" +msgstr "" + +#: rhodecode/apps/admin/views/system_info.py:157 msgid "VCS Server" msgstr "" -#: rhodecode/apps/admin/views/system_info.py:153 +#: rhodecode/apps/admin/views/system_info.py:158 msgid "GIT" msgstr "" -#: rhodecode/apps/admin/views/system_info.py:154 +#: rhodecode/apps/admin/views/system_info.py:159 msgid "HG" msgstr "" -#: rhodecode/apps/admin/views/system_info.py:155 +#: rhodecode/apps/admin/views/system_info.py:160 msgid "SVN" msgstr "" -#: rhodecode/apps/admin/views/users.py:60 -#: rhodecode/controllers/admin/users.py:359 -#: rhodecode/controllers/admin/users.py:380 +#: rhodecode/apps/admin/views/users.py:63 +#: rhodecode/controllers/admin/users.py:360 +#: rhodecode/controllers/admin/users.py:381 #: rhodecode/controllers/admin/users.py:412 #: rhodecode/controllers/admin/users.py:486 -#: rhodecode/controllers/admin/users.py:499 -#: rhodecode/controllers/admin/users.py:557 msgid "You can't edit this user" msgstr "" +#: rhodecode/apps/admin/views/users.py:168 +#: rhodecode/apps/my_account/views.py:148 +#: rhodecode/controllers/admin/gists.py:62 +msgid "forever" +msgstr "" + +#: rhodecode/apps/admin/views/users.py:169 +#: rhodecode/apps/my_account/views.py:149 +#: rhodecode/controllers/admin/gists.py:63 +msgid "5 minutes" +msgstr "" + +#: rhodecode/apps/admin/views/users.py:170 +#: rhodecode/apps/my_account/views.py:150 +#: rhodecode/controllers/admin/gists.py:64 +msgid "1 hour" +msgstr "" + +#: rhodecode/apps/admin/views/users.py:171 +#: rhodecode/apps/my_account/views.py:151 +#: rhodecode/controllers/admin/gists.py:65 +msgid "1 day" +msgstr "" + +#: rhodecode/apps/admin/views/users.py:172 +#: rhodecode/apps/my_account/views.py:152 +#: rhodecode/controllers/admin/gists.py:66 +msgid "1 month" +msgstr "" + +#: rhodecode/apps/admin/views/users.py:174 +#: rhodecode/apps/my_account/views.py:154 +#: rhodecode/controllers/admin/gists.py:70 +msgid "Lifetime" +msgstr "" + #: rhodecode/apps/admin/views/users.py:178 -#: rhodecode/apps/my_account/views.py:138 -#: rhodecode/controllers/admin/gists.py:62 -msgid "forever" -msgstr "" - -#: rhodecode/apps/admin/views/users.py:179 -#: rhodecode/apps/my_account/views.py:139 -#: rhodecode/controllers/admin/gists.py:63 -msgid "5 minutes" -msgstr "" - -#: rhodecode/apps/admin/views/users.py:180 -#: rhodecode/apps/my_account/views.py:140 -#: rhodecode/controllers/admin/gists.py:64 -msgid "1 hour" -msgstr "" - -#: rhodecode/apps/admin/views/users.py:181 -#: rhodecode/apps/my_account/views.py:141 -#: rhodecode/controllers/admin/gists.py:65 -msgid "1 day" -msgstr "" - -#: rhodecode/apps/admin/views/users.py:182 -#: rhodecode/apps/my_account/views.py:142 -#: rhodecode/controllers/admin/gists.py:66 -msgid "1 month" -msgstr "" - -#: rhodecode/apps/admin/views/users.py:184 -#: rhodecode/apps/my_account/views.py:144 -#: rhodecode/controllers/admin/gists.py:70 -msgid "Lifetime" -msgstr "" - -#: rhodecode/apps/admin/views/users.py:188 -#: rhodecode/apps/my_account/views.py:148 +#: rhodecode/apps/my_account/views.py:158 #: rhodecode/templates/admin/my_account/my_account_auth_tokens.mako:16 #: rhodecode/templates/admin/users/user_edit_auth_tokens.mako:16 msgid "Role" msgstr "" -#: rhodecode/apps/admin/views/users.py:219 -#: rhodecode/apps/my_account/views.py:175 +#: rhodecode/apps/admin/views/users.py:217 +#: rhodecode/apps/my_account/views.py:191 msgid "Auth token successfully created" msgstr "" -#: rhodecode/apps/admin/views/users.py:240 -#: rhodecode/apps/my_account/views.py:192 +#: rhodecode/apps/admin/views/users.py:246 +#: rhodecode/apps/my_account/views.py:215 msgid "Auth token successfully deleted" msgstr "" -#: rhodecode/apps/admin/views/users.py:284 +#: rhodecode/apps/admin/views/users.py:290 +#: rhodecode/apps/my_account/views.py:253 +#, python-format +msgid "Added new email address `%s` for user account" +msgstr "" + +#: rhodecode/apps/admin/views/users.py:296 +#: rhodecode/apps/my_account/views.py:259 +msgid "An error occurred during email saving" +msgstr "" + +#: rhodecode/apps/admin/views/users.py:323 +msgid "Removed email address from user account" +msgstr "" + +#: rhodecode/apps/admin/views/users.py:372 +#, python-format +msgid "An error occurred during ip saving:%s" +msgstr "" + +#: rhodecode/apps/admin/views/users.py:389 +msgid "An error occurred during ip saving" +msgstr "" + +#: rhodecode/apps/admin/views/users.py:393 +#, python-format +msgid "Added ips %s to user whitelist" +msgstr "" + +#: rhodecode/apps/admin/views/users.py:423 +msgid "Removed ip address from user whitelist" +msgstr "" + +#: rhodecode/apps/admin/views/users.py:472 msgid "Groups successfully changed" msgstr "" -#: rhodecode/apps/login/views.py:247 rhodecode/apps/login/views.py:316 +#: rhodecode/apps/home/views.py:197 rhodecode/apps/home/views.py:230 +#: rhodecode/controllers/pullrequests.py:191 +#: rhodecode/templates/admin/my_account/my_account.mako:38 +#: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako:128 +#: rhodecode/templates/admin/repos/repo_add.mako:15 +#: rhodecode/templates/admin/repos/repo_add.mako:19 +#: rhodecode/templates/admin/users/user_edit_advanced.mako:11 +#: rhodecode/templates/base/base.mako:76 rhodecode/templates/base/base.mako:148 +#: rhodecode/templates/base/base.mako:575 +msgid "Repositories" +msgstr "" + +#: rhodecode/apps/home/views.py:223 +msgid "Groups" +msgstr "" + +#: rhodecode/apps/home/views.py:243 +#, python-format +msgid "Commits in %(repo)s" +msgstr "" + +#: rhodecode/apps/login/views.py:270 rhodecode/apps/login/views.py:339 msgid "Bad captcha" msgstr "" -#: rhodecode/apps/login/views.py:256 +#: rhodecode/apps/login/views.py:279 msgid "You have successfully registered with RhodeCode" msgstr "" -#: rhodecode/apps/login/views.py:292 +#: rhodecode/apps/login/views.py:315 msgid "If such email exists, a password reset link was sent to it." msgstr "" -#: rhodecode/apps/login/views.py:298 +#: rhodecode/apps/login/views.py:321 msgid "Password reset has been disabled." msgstr "" -#: rhodecode/apps/login/views.py:381 +#: rhodecode/apps/login/views.py:410 msgid "Given reset token is invalid" msgstr "" -#: rhodecode/apps/login/views.py:389 +#: rhodecode/apps/login/views.py:418 msgid "Your password reset was successful, a new password has been sent to your email" msgstr "" -#: rhodecode/apps/my_account/views.py:115 +#: rhodecode/apps/my_account/views.py:125 msgid "Error occurred during update of user password" msgstr "" -#: rhodecode/apps/my_account/views.py:122 +#: rhodecode/apps/my_account/views.py:132 msgid "Successfully updated password" msgstr "" +#: rhodecode/apps/my_account/views.py:281 +msgid "Email successfully deleted" +msgstr "" + +#: rhodecode/apps/repository/views/repo_caches.py:70 +msgid "Cache invalidation successful" +msgstr "" + +#: rhodecode/apps/repository/views/repo_caches.py:74 +msgid "An error occurred during cache invalidation" +msgstr "" + +#: rhodecode/apps/repository/views/repo_permissions.py:95 +msgid "Repository permissions updated" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings.py:171 +msgid "Repository {} updated successfully" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings.py:175 +msgid "Error occurred during update of repository {}" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings_advanced.py:93 +#, python-format +msgid "Detached %s forks" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings_advanced.py:96 +#, python-format +msgid "Deleted %s forks" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings_advanced.py:109 +#, python-format +msgid "Deleted repository `%s`" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings_advanced.py:116 +msgid "detach or delete" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings_advanced.py:117 +msgid "Cannot delete `{repo}` it still contains attached forks. Try using {delete_or_detach} option." +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings_advanced.py:127 +#, python-format +msgid "An error occurred during deletion of `%s`" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings_advanced.py:152 +msgid "Updated repository visibility in public journal" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings_advanced.py:156 +msgid "An error occurred during setting this repository in public journal" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings_advanced.py:184 +msgid "Nothing" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings_advanced.py:186 +#, python-format +msgid "Marked repo %s as fork of %s" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings_advanced.py:193 +msgid "An error occurred during this operation" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings_advanced.py:217 +msgid "Locked repository" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings_advanced.py:220 +msgid "Unlocked repository" +msgstr "" + +#: rhodecode/apps/repository/views/repo_settings_advanced.py:223 +#: rhodecode/controllers/admin/repos.py:363 +msgid "An error occurred during unlocking" +msgstr "" + +#: rhodecode/apps/repository/views/repo_summary.py:293 +msgid "Branch" +msgstr "" + +#: rhodecode/apps/repository/views/repo_summary.py:294 +msgid "Tag" +msgstr "" + +#: rhodecode/apps/repository/views/repo_summary.py:295 +msgid "Bookmark" +msgstr "" + +#: rhodecode/apps/repository/views/repo_summary.py:318 +#: rhodecode/controllers/files.py:1021 rhodecode/model/pull_request.py:1345 +#: rhodecode/model/scm.py:775 rhodecode/templates/base/vcs_settings.mako:255 +msgid "Branches" +msgstr "" + +#: rhodecode/apps/repository/views/repo_summary.py:319 +msgid "Closed branches" +msgstr "" + #: rhodecode/apps/svn_support/events.py:30 msgid "Configuration for Apaache mad_dav_svn changed." msgstr "" @@ -408,7 +572,7 @@ msgid "The Port in use by the Atlassian msgstr "" #: rhodecode/authentication/plugins/auth_crowd.py:69 -#: rhodecode/authentication/plugins/auth_ldap.py:84 +#: rhodecode/authentication/plugins/auth_ldap.py:86 msgid "Port" msgstr "" @@ -436,7 +600,7 @@ msgstr "" msgid "Admin Groups" msgstr "" -#: rhodecode/authentication/plugins/auth_crowd.py:215 +#: rhodecode/authentication/plugins/auth_crowd.py:216 msgid "CROWD" msgstr "" @@ -483,126 +647,129 @@ msgstr "" #: rhodecode/authentication/plugins/auth_ldap.py:74 msgid "" -"Host of the LDAP Server \n" -"(e.g., 192.168.2.154, or ldap-server.domain.com" -msgstr "" - -#: rhodecode/authentication/plugins/auth_ldap.py:77 +"Host[s] of the LDAP Server \n" +"(e.g., 192.168.2.154, or ldap-server.domain.com.\n" +" Multiple servers can be specified using commas" +msgstr "" + +#: rhodecode/authentication/plugins/auth_ldap.py:78 msgid "LDAP Host" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:82 -msgid "Custom port that the LDAP server is listening on. Default: 389" -msgstr "" - -#: rhodecode/authentication/plugins/auth_ldap.py:90 +#: rhodecode/authentication/plugins/auth_ldap.py:83 +msgid "Custom port that the LDAP server is listening on. Default value is: 389" +msgstr "" + +#: rhodecode/authentication/plugins/auth_ldap.py:92 msgid "" "Optional user DN/account to connect to LDAP if authentication is required. \n" "e.g., cn=admin,dc=mydomain,dc=com, or uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:95 +#: rhodecode/authentication/plugins/auth_ldap.py:97 msgid "Account" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:100 +#: rhodecode/authentication/plugins/auth_ldap.py:102 msgid "Password to authenticate for given user DN." msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:103 +#: rhodecode/authentication/plugins/auth_ldap.py:105 #: rhodecode/templates/login.mako:50 rhodecode/templates/register.mako:48 #: rhodecode/templates/admin/my_account/my_account.mako:30 #: rhodecode/templates/admin/users/user_add.mako:44 -#: rhodecode/templates/base/base.mako:313 +#: rhodecode/templates/base/base.mako:315 #: rhodecode/templates/debug_style/login.html:45 msgid "Password" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:108 +#: rhodecode/authentication/plugins/auth_ldap.py:110 msgid "TLS Type" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:109 +#: rhodecode/authentication/plugins/auth_ldap.py:111 msgid "Connection Security" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:115 -msgid "Require Cert over TLS?" -msgstr "" - -#: rhodecode/authentication/plugins/auth_ldap.py:116 +#: rhodecode/authentication/plugins/auth_ldap.py:117 +msgid "" +"Require Cert over TLS?. Self-signed and custom certificates can be used when\n" +" `RhodeCode Certificate` found in admin > settings > system info page is extended." +msgstr "" + +#: rhodecode/authentication/plugins/auth_ldap.py:120 msgid "Certificate Checks" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:122 +#: rhodecode/authentication/plugins/auth_ldap.py:126 msgid "" "Base DN to search. Dynamic bind is supported. Add `$login` marker in it to be replaced with current user credentials \n" "(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:127 +#: rhodecode/authentication/plugins/auth_ldap.py:131 msgid "Base DN" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:132 +#: rhodecode/authentication/plugins/auth_ldap.py:136 msgid "" "Filter to narrow results \n" "(e.g., (&(objectCategory=Person)(objectClass=user)), or \n" "(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:137 +#: rhodecode/authentication/plugins/auth_ldap.py:141 msgid "LDAP Search Filter" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:143 +#: rhodecode/authentication/plugins/auth_ldap.py:147 msgid "How deep to search LDAP. If unsure set to SUBTREE" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:144 +#: rhodecode/authentication/plugins/auth_ldap.py:148 msgid "LDAP Search Scope" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:150 +#: rhodecode/authentication/plugins/auth_ldap.py:154 msgid "LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:152 +#: rhodecode/authentication/plugins/auth_ldap.py:156 msgid "Login Attribute" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:153 +#: rhodecode/authentication/plugins/auth_ldap.py:157 msgid "The LDAP Login attribute of the CN must be specified" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:158 +#: rhodecode/authentication/plugins/auth_ldap.py:162 msgid "LDAP Attribute to map to first name (e.g., givenName)" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:161 +#: rhodecode/authentication/plugins/auth_ldap.py:165 msgid "First Name Attribute" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:166 +#: rhodecode/authentication/plugins/auth_ldap.py:170 msgid "LDAP Attribute to map to last name (e.g., sn)" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:169 +#: rhodecode/authentication/plugins/auth_ldap.py:173 msgid "Last Name Attribute" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:174 +#: rhodecode/authentication/plugins/auth_ldap.py:178 msgid "" "LDAP Attribute to map to email address (e.g., mail).\n" "Emails are a crucial part of RhodeCode. \n" "If possible add a valid email attribute to ldap users." msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:179 +#: rhodecode/authentication/plugins/auth_ldap.py:183 msgid "Email Attribute" msgstr "" -#: rhodecode/authentication/plugins/auth_ldap.py:360 +#: rhodecode/authentication/plugins/auth_ldap.py:365 msgid "LDAP" msgstr "" @@ -634,74 +801,71 @@ msgstr "" msgid "Rhodecode Token Auth" msgstr "" -#: rhodecode/controllers/changelog.py:91 rhodecode/controllers/compare.py:64 -#: rhodecode/controllers/pullrequests.py:204 +#: rhodecode/controllers/changelog.py:70 rhodecode/controllers/compare.py:64 +#: rhodecode/controllers/pullrequests.py:85 msgid "There are no commits yet" msgstr "" +#: rhodecode/controllers/changeset.py:76 +msgid "Show whitespace" +msgstr "" + #: rhodecode/controllers/changeset.py:77 -msgid "Show whitespace" -msgstr "" - -#: rhodecode/controllers/changeset.py:78 msgid "Show whitespace for all diffs" msgstr "" +#: rhodecode/controllers/changeset.py:83 +msgid "Ignore whitespace" +msgstr "" + #: rhodecode/controllers/changeset.py:84 -msgid "Ignore whitespace" -msgstr "" - -#: rhodecode/controllers/changeset.py:85 msgid "Ignore whitespace for all diffs" msgstr "" +#: rhodecode/controllers/changeset.py:140 +msgid "Increase context" +msgstr "" + #: rhodecode/controllers/changeset.py:141 -msgid "Increase context" -msgstr "" - -#: rhodecode/controllers/changeset.py:142 msgid "Increase context for all diffs" msgstr "" -#: rhodecode/controllers/changeset.py:190 rhodecode/controllers/files.py:106 -#: rhodecode/controllers/files.py:127 +#: rhodecode/controllers/changeset.py:189 rhodecode/controllers/files.py:106 +#: rhodecode/controllers/files.py:128 msgid "No such commit exists for this repository" msgstr "" -#: rhodecode/controllers/changeset.py:344 -#: rhodecode/controllers/pullrequests.py:985 -#: rhodecode/model/pull_request.py:1055 +#: rhodecode/controllers/changeset.py:343 +#: rhodecode/controllers/pullrequests.py:919 #, python-format msgid "Status change %(transition_icon)s %(status)s" msgstr "" -#: rhodecode/controllers/changeset.py:389 +#: rhodecode/controllers/changeset.py:387 msgid "Changing the status of a commit associated with a closed pull request is not allowed" msgstr "" -#: rhodecode/controllers/compare.py:89 +#: rhodecode/controllers/compare.py:92 msgid "Select commit" msgstr "" -#: rhodecode/controllers/compare.py:144 -#, python-format -msgid "Could not find the original repo: %(repo)s" -msgstr "" - -#: rhodecode/controllers/compare.py:152 -#, python-format -msgid "Could not find the other repo: %(repo)s" -msgstr "" - -#: rhodecode/controllers/compare.py:164 +#: rhodecode/controllers/compare.py:149 +msgid "Could not find the source repo: `{}`" +msgstr "" + +#: rhodecode/controllers/compare.py:156 +msgid "Could not find the target repo: `{}`" +msgstr "" + +#: rhodecode/controllers/compare.py:166 msgid "The comparison of two different kinds of remote repos is not available" msgstr "" -#: rhodecode/controllers/compare.py:202 +#: rhodecode/controllers/compare.py:204 msgid "Could not compare repos with different large file settings" msgstr "" -#: rhodecode/controllers/compare.py:242 +#: rhodecode/controllers/compare.py:244 #, python-format msgid "Repositories unrelated. Cannot compare commit %(commit1)s from repository %(repo1)s with commit %(commit2)s from repository %(repo2)s." msgstr "" @@ -725,51 +889,47 @@ msgstr "" msgid "There are no files yet. %s" msgstr "" -#: rhodecode/controllers/files.py:435 rhodecode/controllers/files.py:488 -#: rhodecode/controllers/files.py:519 rhodecode/controllers/files.py:594 -#: rhodecode/controllers/files.py:639 rhodecode/controllers/files.py:730 +#: rhodecode/controllers/files.py:434 rhodecode/controllers/files.py:487 +#: rhodecode/controllers/files.py:518 rhodecode/controllers/files.py:593 +#: rhodecode/controllers/files.py:638 rhodecode/controllers/files.py:729 #, python-format msgid "This repository has been locked by %s on %s" msgstr "" -#: rhodecode/controllers/files.py:443 rhodecode/controllers/files.py:496 +#: rhodecode/controllers/files.py:442 rhodecode/controllers/files.py:495 msgid "You can only delete files with revision being a valid branch " msgstr "" -#: rhodecode/controllers/files.py:452 rhodecode/controllers/files.py:505 -#, python-format -msgid "Deleted file %s via RhodeCode Enterprise" +#: rhodecode/controllers/files.py:451 rhodecode/controllers/files.py:504 +msgid "Deleted file {} via RhodeCode Enterprise" msgstr "" #: rhodecode/controllers/files.py:472 -#, python-format -msgid "Successfully deleted file %s" -msgstr "" - -#: rhodecode/controllers/files.py:475 rhodecode/controllers/files.py:581 -#: rhodecode/controllers/files.py:718 +msgid "Successfully deleted file `{}`" +msgstr "" + +#: rhodecode/controllers/files.py:476 rhodecode/controllers/files.py:582 +#: rhodecode/controllers/files.py:719 msgid "Error occurred during commit" msgstr "" -#: rhodecode/controllers/files.py:527 rhodecode/controllers/files.py:602 +#: rhodecode/controllers/files.py:526 rhodecode/controllers/files.py:601 msgid "You can only edit files with revision being a valid branch " msgstr "" -#: rhodecode/controllers/files.py:539 rhodecode/controllers/files.py:614 -#, python-format -msgid "Edited file %s via RhodeCode Enterprise" -msgstr "" - -#: rhodecode/controllers/files.py:556 +#: rhodecode/controllers/files.py:538 rhodecode/controllers/files.py:613 +msgid "Edited file {} via RhodeCode Enterprise" +msgstr "" + +#: rhodecode/controllers/files.py:555 msgid "No changes" msgstr "" -#: rhodecode/controllers/files.py:578 rhodecode/controllers/files.py:707 -#, python-format -msgid "Successfully committed to %s" -msgstr "" - -#: rhodecode/controllers/files.py:652 rhodecode/controllers/files.py:741 +#: rhodecode/controllers/files.py:578 +msgid "Successfully committed changes to file `{}`" +msgstr "" + +#: rhodecode/controllers/files.py:651 rhodecode/controllers/files.py:740 msgid "Added file via RhodeCode Enterprise" msgstr "" @@ -777,39 +937,37 @@ msgstr "" msgid "No filename" msgstr "" +#: rhodecode/controllers/files.py:707 +msgid "Successfully committed new file `{}`" +msgstr "" + #: rhodecode/controllers/files.py:710 msgid "The location specified must be a relative path and must not contain .. in the path" msgstr "" -#: rhodecode/controllers/files.py:764 +#: rhodecode/controllers/files.py:763 msgid "Downloads disabled" msgstr "" -#: rhodecode/controllers/files.py:770 +#: rhodecode/controllers/files.py:769 #, python-format msgid "Unknown revision %s" msgstr "" -#: rhodecode/controllers/files.py:772 +#: rhodecode/controllers/files.py:771 msgid "Empty repository" msgstr "" -#: rhodecode/controllers/files.py:774 rhodecode/controllers/files.py:808 +#: rhodecode/controllers/files.py:773 rhodecode/controllers/files.py:807 msgid "Unknown archive type" msgstr "" -#: rhodecode/controllers/files.py:993 +#: rhodecode/controllers/files.py:1000 msgid "Changesets" msgstr "" -#: rhodecode/controllers/files.py:1014 rhodecode/controllers/summary.py:277 -#: rhodecode/model/pull_request.py:1280 rhodecode/model/scm.py:782 -#: rhodecode/templates/base/vcs_settings.mako:242 -msgid "Branches" -msgstr "" - -#: rhodecode/controllers/files.py:1018 rhodecode/model/scm.py:797 -#: rhodecode/templates/base/vcs_settings.mako:267 +#: rhodecode/controllers/files.py:1025 rhodecode/model/scm.py:790 +#: rhodecode/templates/base/vcs_settings.mako:280 msgid "Tags" msgstr "" @@ -818,27 +976,6 @@ msgstr "" msgid "An error occurred during repository forking %s" msgstr "" -#: rhodecode/controllers/home.py:207 -msgid "Groups" -msgstr "" - -#: rhodecode/controllers/home.py:214 rhodecode/controllers/home.py:249 -#: rhodecode/controllers/pullrequests.py:310 -#: rhodecode/templates/admin/my_account/my_account.mako:38 -#: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako:128 -#: rhodecode/templates/admin/repos/repo_add.mako:15 -#: rhodecode/templates/admin/repos/repo_add.mako:19 -#: rhodecode/templates/admin/users/user_edit_advanced.mako:11 -#: rhodecode/templates/base/base.mako:76 rhodecode/templates/base/base.mako:148 -#: rhodecode/templates/base/base.mako:572 -msgid "Repositories" -msgstr "" - -#: rhodecode/controllers/home.py:227 -#, python-format -msgid "Commits in %(repo)s" -msgstr "" - #: rhodecode/controllers/journal.py:107 rhodecode/controllers/journal.py:150 msgid "public journal" msgstr "" @@ -847,80 +984,58 @@ msgstr "" msgid "journal" msgstr "" -#: rhodecode/controllers/pullrequests.py:218 +#: rhodecode/controllers/pullrequests.py:99 msgid "Commit does not exist" msgstr "" -#: rhodecode/controllers/pullrequests.py:335 +#: rhodecode/controllers/pullrequests.py:216 msgid "Pull request requires a title with min. 3 chars" msgstr "" -#: rhodecode/controllers/pullrequests.py:337 +#: rhodecode/controllers/pullrequests.py:218 msgid "Error creating pull request: {}" msgstr "" -#: rhodecode/controllers/pullrequests.py:385 +#: rhodecode/controllers/pullrequests.py:276 msgid "Successfully opened new pull request" msgstr "" -#: rhodecode/controllers/pullrequests.py:388 -msgid "Error occurred during sending pull request" -msgstr "" - -#: rhodecode/controllers/pullrequests.py:431 +#: rhodecode/controllers/pullrequests.py:279 +msgid "Error occurred during creation of this pull request." +msgstr "" + +#: rhodecode/controllers/pullrequests.py:322 msgid "Cannot update closed pull requests." msgstr "" +#: rhodecode/controllers/pullrequests.py:328 +msgid "Pull request title & description updated." +msgstr "" + +#: rhodecode/controllers/pullrequests.py:346 +msgid "Pull request updated to \"{source_commit_id}\" with {count_added} added, {count_removed} removed commits. Source of changes: {change_source}" +msgstr "" + +#: rhodecode/controllers/pullrequests.py:363 +msgid "Reload page" +msgstr "" + #: rhodecode/controllers/pullrequests.py:437 -msgid "Pull request title & description updated." -msgstr "" - -#: rhodecode/controllers/pullrequests.py:455 -msgid "Pull request updated to \"{source_commit_id}\" with {count_added} added, {count_removed} removed commits. Source of changes: {change_source}" -msgstr "" - -#: rhodecode/controllers/pullrequests.py:472 -msgid "Reload page" -msgstr "" - -#: rhodecode/controllers/pullrequests.py:546 msgid "Pull request was successfully merged and closed." msgstr "" -#: rhodecode/controllers/pullrequests.py:588 +#: rhodecode/controllers/pullrequests.py:461 +msgid "Pull request reviewers updated." +msgstr "" + +#: rhodecode/controllers/pullrequests.py:482 msgid "Successfully deleted pull request" msgstr "" -#: rhodecode/controllers/pullrequests.py:592 +#: rhodecode/controllers/pullrequests.py:486 msgid "Your are not allowed to delete this pull request" msgstr "" -#: rhodecode/controllers/pullrequests.py:989 -#: rhodecode/model/pull_request.py:1059 -msgid "Closing with" -msgstr "" - -#: rhodecode/controllers/pullrequests.py:1039 -#, 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:251 -msgid "Branch" -msgstr "" - -#: rhodecode/controllers/summary.py:252 -msgid "Tag" -msgstr "" - -#: rhodecode/controllers/summary.py:253 -msgid "Bookmark" -msgstr "" - -#: rhodecode/controllers/summary.py:278 -msgid "Closed branches" -msgstr "" - #: rhodecode/controllers/admin/defaults.py:84 msgid "Default settings updated successfully" msgstr "" @@ -976,89 +1091,73 @@ msgstr "" msgid "%(expiry)s - current value" msgstr "" -#: rhodecode/controllers/admin/my_account.py:78 +#: rhodecode/controllers/admin/my_account.py:70 msgid "You can't edit this user since it's crucial for entire application" msgstr "" -#: rhodecode/controllers/admin/my_account.py:138 +#: rhodecode/controllers/admin/my_account.py:110 msgid "Your account was updated successfully" msgstr "" -#: rhodecode/controllers/admin/my_account.py:153 -#: rhodecode/controllers/admin/users.py:184 +#: rhodecode/controllers/admin/my_account.py:125 +#: rhodecode/controllers/admin/users.py:182 #, python-format msgid "Error occurred during update of user %s" msgstr "" -#: rhodecode/controllers/admin/my_account.py:222 -#: rhodecode/controllers/admin/users.py:527 -#, python-format -msgid "Added new email address `%s` for user account" -msgstr "" - -#: rhodecode/controllers/admin/my_account.py:229 -#: rhodecode/controllers/admin/users.py:534 -msgid "An error occurred during email saving" -msgstr "" - -#: rhodecode/controllers/admin/my_account.py:239 -#: rhodecode/controllers/admin/users.py:549 -msgid "Removed email address from user account" -msgstr "" - -#: rhodecode/controllers/admin/permissions.py:112 +#: rhodecode/controllers/admin/permissions.py:107 msgid "Application permissions updated successfully" msgstr "" -#: rhodecode/controllers/admin/permissions.py:127 -#: rhodecode/controllers/admin/permissions.py:176 -#: rhodecode/controllers/admin/permissions.py:230 +#: rhodecode/controllers/admin/permissions.py:122 +#: rhodecode/controllers/admin/permissions.py:171 +#: rhodecode/controllers/admin/permissions.py:225 msgid "Error occurred during update of permissions" msgstr "" -#: rhodecode/controllers/admin/permissions.py:161 +#: rhodecode/controllers/admin/permissions.py:156 msgid "Object permissions updated successfully" msgstr "" -#: rhodecode/controllers/admin/permissions.py:215 +#: rhodecode/controllers/admin/permissions.py:210 msgid "Global permissions updated successfully" msgstr "" -#: rhodecode/controllers/admin/repo_groups.py:197 +#: rhodecode/controllers/admin/repo_groups.py:202 #, python-format msgid "Created repository group %s" msgstr "" -#: rhodecode/controllers/admin/repo_groups.py:210 +#: rhodecode/controllers/admin/repo_groups.py:215 #, python-format msgid "Error occurred during creation of repository group %s" msgstr "" -#: rhodecode/controllers/admin/repo_groups.py:258 +#: rhodecode/controllers/admin/repo_groups.py:261 #, python-format msgid "Updated repository group %s" msgstr "" -#: rhodecode/controllers/admin/repo_groups.py:274 +#: rhodecode/controllers/admin/repo_groups.py:276 #, python-format msgid "Error occurred during update of repository group %s" msgstr "" -#: rhodecode/controllers/admin/repo_groups.py:296 +#: rhodecode/controllers/admin/repo_groups.py:291 #, python-format msgid "This group contains %(num)d repository and cannot be deleted" msgid_plural "This group contains %(num)d repositories and cannot be deleted" msgstr[0] "" msgstr[1] "" -#: rhodecode/controllers/admin/repo_groups.py:305 +#: rhodecode/controllers/admin/repo_groups.py:300 #, python-format msgid "This group contains %(num)d subgroup and cannot be deleted" msgid_plural "This group contains %(num)d subgroups and cannot be deleted" msgstr[0] "" msgstr[1] "" -#: rhodecode/controllers/admin/repo_groups.py:312 +#: rhodecode/controllers/admin/repo_groups.py:313 #, python-format msgid "Removed repository group %s" msgstr "" @@ -1068,366 +1167,285 @@ msgstr "" msgid "Error occurred during deletion of repository group %s" msgstr "" -#: rhodecode/controllers/admin/repo_groups.py:388 -#: rhodecode/controllers/admin/user_groups.py:323 +#: rhodecode/controllers/admin/repo_groups.py:381 +#: rhodecode/controllers/admin/user_groups.py:318 msgid "Cannot change permission for yourself as admin" msgstr "" -#: rhodecode/controllers/admin/repo_groups.py:405 +#: rhodecode/controllers/admin/repo_groups.py:404 msgid "Repository Group permissions updated" msgstr "" +#: rhodecode/controllers/admin/repos.py:125 +#, python-format +msgid "Error creating repository %s: invalid certificate" +msgstr "" + #: rhodecode/controllers/admin/repos.py:129 #, python-format -msgid "Error creating repository %s: invalid certificate" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:133 -#, python-format msgid "Error creating repository %s" msgstr "" -#: rhodecode/controllers/admin/repos.py:274 +#: rhodecode/controllers/admin/repos.py:270 #, python-format msgid "Created repository %s from %s" msgstr "" -#: rhodecode/controllers/admin/repos.py:283 +#: rhodecode/controllers/admin/repos.py:279 #, python-format msgid "Forked repository %s as %s" msgstr "" -#: rhodecode/controllers/admin/repos.py:286 +#: rhodecode/controllers/admin/repos.py:282 #, python-format msgid "Created repository %s" msgstr "" -#: rhodecode/controllers/admin/repos.py:327 -#, python-format -msgid "Repository %s updated successfully" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:346 -#, python-format -msgid "Error occurred during update of repository %s" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:374 -#, python-format -msgid "Detached %s forks" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:377 -#, python-format -msgid "Deleted %s forks" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:382 -#, python-format -msgid "Deleted repository %s" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:385 -#, python-format -msgid "Cannot delete %s it still contains attached forks" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:390 -#, python-format -msgid "An error occurred during deletion of %s" +#: rhodecode/controllers/admin/repos.py:319 +msgid "An error occurred during creation of field" +msgstr "" + +#: rhodecode/controllers/admin/repos.py:334 +msgid "An error occurred during removal of field" +msgstr "" + +#: rhodecode/controllers/admin/repos.py:353 +msgid "Unlocked" +msgstr "" + +#: rhodecode/controllers/admin/repos.py:357 +msgid "Locked" +msgstr "" + +#: rhodecode/controllers/admin/repos.py:359 +#, python-format +msgid "Repository has been %s" +msgstr "" + +#: rhodecode/controllers/admin/repos.py:373 +msgid "Pulled from remote location" +msgstr "" + +#: rhodecode/controllers/admin/repos.py:376 +msgid "An error occurred during pull from remote location" +msgstr "" + +#: rhodecode/controllers/admin/repos.py:397 +msgid "An error occurred during deletion of repository stats" msgstr "" #: rhodecode/controllers/admin/repos.py:443 -msgid "Repository permissions updated" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:474 -msgid "An error occurred during creation of field" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:489 -msgid "An error occurred during removal of field" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:528 -msgid "Updated repository visibility in public journal" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:532 -msgid "An error occurred during setting this repository in public journal" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:556 -msgid "Nothing" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:558 -#, python-format -msgid "Marked repo %s as fork of %s" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:565 -msgid "An error occurred during this operation" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:583 -msgid "Locked repository" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:586 -msgid "Unlocked repository" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:589 -#: rhodecode/controllers/admin/repos.py:618 -msgid "An error occurred during unlocking" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:608 -msgid "Unlocked" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:612 -msgid "Locked" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:614 -#, python-format -msgid "Repository has been %s" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:629 -msgid "Cache invalidation successful" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:633 -msgid "An error occurred during cache invalidation" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:653 -msgid "Pulled from remote location" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:656 -msgid "An error occurred during pull from remote location" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:678 -msgid "An error occurred during deletion of repository stats" -msgstr "" - -#: rhodecode/controllers/admin/repos.py:725 msgid "Error occurred during deleting issue tracker entry" msgstr "" -#: rhodecode/controllers/admin/repos.py:728 -#: rhodecode/controllers/admin/settings.py:381 +#: rhodecode/controllers/admin/repos.py:446 +#: rhodecode/controllers/admin/settings.py:384 msgid "Removed issue tracker entry" msgstr "" -#: rhodecode/controllers/admin/repos.py:758 -#: rhodecode/controllers/admin/settings.py:428 +#: rhodecode/controllers/admin/repos.py:476 +#: rhodecode/controllers/admin/settings.py:431 msgid "Updated issue tracker entries" msgstr "" -#: rhodecode/controllers/admin/repos.py:819 +#: rhodecode/controllers/admin/repos.py:537 #: rhodecode/controllers/admin/settings.py:147 -#: rhodecode/controllers/admin/settings.py:619 +#: rhodecode/controllers/admin/settings.py:622 msgid "Some form inputs contain invalid data." msgstr "" -#: rhodecode/controllers/admin/repos.py:837 +#: rhodecode/controllers/admin/repos.py:555 msgid "Error occurred during updating repository VCS settings" msgstr "" -#: rhodecode/controllers/admin/repos.py:841 +#: rhodecode/controllers/admin/repos.py:559 #: rhodecode/controllers/admin/settings.py:176 msgid "Updated VCS settings" msgstr "" #: rhodecode/controllers/admin/settings.py:172 -#: rhodecode/controllers/admin/settings.py:283 +#: rhodecode/controllers/admin/settings.py:286 msgid "Error occurred during updating application settings" msgstr "" -#: rhodecode/controllers/admin/settings.py:223 +#: rhodecode/controllers/admin/settings.py:226 #, python-format msgid "Repositories successfully rescanned added: %s ; removed: %s" msgstr "" -#: rhodecode/controllers/admin/settings.py:279 +#: rhodecode/controllers/admin/settings.py:282 msgid "Updated application settings" msgstr "" -#: rhodecode/controllers/admin/settings.py:345 -msgid "Updated visualisation settings" -msgstr "" - #: rhodecode/controllers/admin/settings.py:348 +msgid "Updated visualisation settings" +msgstr "" + +#: rhodecode/controllers/admin/settings.py:351 msgid "Error occurred during updating visualisation settings" msgstr "" -#: rhodecode/controllers/admin/settings.py:441 +#: rhodecode/controllers/admin/settings.py:444 msgid "Please enter email address" msgstr "" -#: rhodecode/controllers/admin/settings.py:459 +#: rhodecode/controllers/admin/settings.py:462 msgid "Send email task created" msgstr "" -#: rhodecode/controllers/admin/settings.py:492 +#: rhodecode/controllers/admin/settings.py:495 msgid "Added new hook" msgstr "" -#: rhodecode/controllers/admin/settings.py:507 +#: rhodecode/controllers/admin/settings.py:510 msgid "Updated hooks" msgstr "" -#: rhodecode/controllers/admin/settings.py:511 +#: rhodecode/controllers/admin/settings.py:514 msgid "Error occurred during hook creation" msgstr "" -#: rhodecode/controllers/admin/settings.py:640 +#: rhodecode/controllers/admin/settings.py:643 msgid "Error occurred during updating labs settings" msgstr "" -#: rhodecode/controllers/admin/settings.py:645 +#: rhodecode/controllers/admin/settings.py:648 msgid "Updated Labs settings" msgstr "" -#: rhodecode/controllers/admin/user_groups.py:165 +#: rhodecode/controllers/admin/user_groups.py:164 #, python-format msgid "Created user group %(user_group_link)s" msgstr "" -#: rhodecode/controllers/admin/user_groups.py:179 +#: rhodecode/controllers/admin/user_groups.py:178 #, python-format msgid "Error occurred during creation of user group %s" msgstr "" -#: rhodecode/controllers/admin/user_groups.py:220 +#: rhodecode/controllers/admin/user_groups.py:218 #, python-format msgid "Updated user group %s" msgstr "" -#: rhodecode/controllers/admin/user_groups.py:236 +#: rhodecode/controllers/admin/user_groups.py:234 #, python-format msgid "Error occurred during update of user group %s" msgstr "" +#: rhodecode/controllers/admin/user_groups.py:253 +msgid "Successfully deleted user group" +msgstr "" + #: rhodecode/controllers/admin/user_groups.py:258 -msgid "Successfully deleted user group" -msgstr "" - -#: rhodecode/controllers/admin/user_groups.py:263 msgid "An error occurred during deletion of user group" msgstr "" -#: rhodecode/controllers/admin/user_groups.py:331 +#: rhodecode/controllers/admin/user_groups.py:326 msgid "Target group cannot be the same" msgstr "" -#: rhodecode/controllers/admin/user_groups.py:337 +#: rhodecode/controllers/admin/user_groups.py:332 msgid "User Group permissions updated" msgstr "" -#: rhodecode/controllers/admin/user_groups.py:422 +#: rhodecode/controllers/admin/user_groups.py:415 msgid "User Group global permissions updated successfully" msgstr "" -#: rhodecode/controllers/admin/user_groups.py:437 +#: rhodecode/controllers/admin/user_groups.py:430 #: rhodecode/controllers/admin/users.py:477 msgid "An error occurred during permissions saving" msgstr "" -#: rhodecode/controllers/admin/user_groups.py:481 +#: rhodecode/controllers/admin/user_groups.py:474 msgid "User Group synchronization updated successfully" msgstr "" -#: rhodecode/controllers/admin/user_groups.py:485 +#: rhodecode/controllers/admin/user_groups.py:478 msgid "An error occurred during synchronization update" msgstr "" -#: rhodecode/controllers/admin/users.py:106 +#: rhodecode/controllers/admin/users.py:108 #, python-format msgid "Created user %(user_link)s" msgstr "" -#: rhodecode/controllers/admin/users.py:122 +#: rhodecode/controllers/admin/users.py:124 #, python-format msgid "Error occurred during creation of user %s" msgstr "" -#: rhodecode/controllers/admin/users.py:167 +#: rhodecode/controllers/admin/users.py:166 msgid "User updated successfully" msgstr "" -#: rhodecode/controllers/admin/users.py:218 +#: rhodecode/controllers/admin/users.py:209 #, python-format msgid "Detached %s repositories" msgstr "" -#: rhodecode/controllers/admin/users.py:223 +#: rhodecode/controllers/admin/users.py:214 #, python-format msgid "Deleted %s repositories" msgstr "" -#: rhodecode/controllers/admin/users.py:231 +#: rhodecode/controllers/admin/users.py:222 #, python-format msgid "Detached %s repository groups" msgstr "" -#: rhodecode/controllers/admin/users.py:236 +#: rhodecode/controllers/admin/users.py:227 #, python-format msgid "Deleted %s repository groups" msgstr "" -#: rhodecode/controllers/admin/users.py:244 +#: rhodecode/controllers/admin/users.py:235 #, python-format msgid "Detached %s user groups" msgstr "" -#: rhodecode/controllers/admin/users.py:249 +#: rhodecode/controllers/admin/users.py:240 #, python-format msgid "Deleted %s user groups" msgstr "" -#: rhodecode/controllers/admin/users.py:260 +#: rhodecode/controllers/admin/users.py:257 msgid "Successfully deleted user" msgstr "" -#: rhodecode/controllers/admin/users.py:266 +#: rhodecode/controllers/admin/users.py:263 msgid "An error occurred during deletion of user" msgstr "" +#: rhodecode/controllers/admin/users.py:280 +msgid "Force password change disabled for user" +msgstr "" + #: rhodecode/controllers/admin/users.py:285 -msgid "Force password change disabled for user" -msgstr "" - -#: rhodecode/controllers/admin/users.py:287 msgid "Force password change enabled for user" msgstr "" -#: rhodecode/controllers/admin/users.py:291 +#: rhodecode/controllers/admin/users.py:294 msgid "An error occurred during password reset for user" msgstr "" -#: rhodecode/controllers/admin/users.py:324 +#: rhodecode/controllers/admin/users.py:325 #, python-format msgid "Linked repository group `%s` as personal" msgstr "" -#: rhodecode/controllers/admin/users.py:330 +#: rhodecode/controllers/admin/users.py:331 #, python-format msgid "Created repository group `%s`" msgstr "" -#: rhodecode/controllers/admin/users.py:334 +#: rhodecode/controllers/admin/users.py:335 #, python-format msgid "Repository group `%s` is already taken" msgstr "" -#: rhodecode/controllers/admin/users.py:339 +#: rhodecode/controllers/admin/users.py:340 msgid "An error occurred during repository group creation for user" msgstr "" @@ -1435,81 +1453,63 @@ msgstr "" 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:461 +#: rhodecode/controllers/admin/users.py:462 msgid "User global permissions updated successfully" msgstr "" -#: rhodecode/controllers/admin/users.py:589 -#, python-format -msgid "An error occurred during ip saving:%s" -msgstr "" - -#: rhodecode/controllers/admin/users.py:604 -msgid "An error occurred during ip saving" -msgstr "" - -#: rhodecode/controllers/admin/users.py:608 -#, python-format -msgid "Added ips %s to user whitelist" -msgstr "" - -#: rhodecode/controllers/admin/users.py:626 -msgid "Removed ip address from user whitelist" -msgstr "" - -#: rhodecode/events/pullrequest.py:68 +#: rhodecode/events/pullrequest.py:71 msgid "pullrequest created" msgstr "" -#: rhodecode/events/pullrequest.py:77 +#: rhodecode/events/pullrequest.py:80 msgid "pullrequest closed" msgstr "" -#: rhodecode/events/pullrequest.py:86 +#: rhodecode/events/pullrequest.py:89 msgid "pullrequest commits updated" msgstr "" -#: rhodecode/events/pullrequest.py:95 +#: rhodecode/events/pullrequest.py:98 msgid "pullrequest review changed" msgstr "" -#: rhodecode/events/pullrequest.py:104 +#: rhodecode/events/pullrequest.py:107 msgid "pullrequest merged" msgstr "" -#: rhodecode/events/pullrequest.py:113 +#: rhodecode/events/pullrequest.py:116 msgid "pullrequest commented" msgstr "" -#: rhodecode/events/repo.py:138 +#: rhodecode/events/repo.py:148 msgid "repository pre create" msgstr "" -#: rhodecode/events/repo.py:147 +#: rhodecode/events/repo.py:157 msgid "repository created" msgstr "" -#: rhodecode/events/repo.py:156 +#: rhodecode/events/repo.py:166 msgid "repository pre delete" msgstr "" -#: rhodecode/events/repo.py:165 +#: rhodecode/events/repo.py:175 msgid "repository deleted" msgstr "" -#: rhodecode/events/repo.py:201 +#: rhodecode/events/repo.py:211 msgid "repository pre pull" msgstr "" -#: rhodecode/events/repo.py:210 +#: rhodecode/events/repo.py:220 msgid "repository pull" msgstr "" -#: rhodecode/events/repo.py:219 +#: rhodecode/events/repo.py:229 msgid "repository pre push" msgstr "" -#: rhodecode/events/repo.py:230 +#: rhodecode/events/repo.py:240 msgid "repository push" msgstr "" @@ -1541,7 +1541,7 @@ msgstr "" msgid "user pre update" msgstr "" -#: rhodecode/forms/__init__.py:32 rhodecode/templates/admin/gists/new.mako:62 +#: rhodecode/forms/__init__.py:35 rhodecode/templates/admin/gists/new.mako:62 #: rhodecode/templates/admin/my_account/my_account_auth_tokens.mako:87 #: rhodecode/templates/admin/my_account/my_account_emails.mako:65 #: rhodecode/templates/admin/my_account/my_account_profile_edit.mako:107 @@ -1552,8 +1552,8 @@ msgstr "" #: rhodecode/templates/admin/repo_groups/repo_group_edit_settings.mako:68 #: rhodecode/templates/admin/repos/repo_edit_fields.mako:66 #: rhodecode/templates/admin/repos/repo_edit_issuetracker.mako:80 -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:111 -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:161 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:110 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:195 #: rhodecode/templates/admin/repos/repo_edit_vcs.mako:44 #: rhodecode/templates/admin/settings/settings_global.mako:140 #: rhodecode/templates/admin/settings/settings_issuetracker.mako:16 @@ -1563,13 +1563,13 @@ msgstr "" #: rhodecode/templates/admin/user_groups/user_group_edit_perms.mako:121 #: rhodecode/templates/admin/users/user_edit_auth_tokens.mako:83 #: rhodecode/templates/admin/users/user_edit_emails.mako:63 -#: rhodecode/templates/admin/users/user_edit_ips.mako:70 +#: rhodecode/templates/admin/users/user_edit_ips.mako:71 #: rhodecode/templates/admin/users/user_edit_profile.mako:135 #: rhodecode/templates/base/default_perms_box.mako:89 msgid "Reset" msgstr "" -#: rhodecode/forms/__init__.py:33 rhodecode/templates/admin/gists/show.mako:49 +#: rhodecode/forms/__init__.py:36 rhodecode/templates/admin/gists/show.mako:49 #: rhodecode/templates/admin/integrations/list.mako:211 #: rhodecode/templates/admin/my_account/my_account_auth_tokens.mako:49 #: rhodecode/templates/admin/my_account/my_account_emails.mako:32 @@ -1578,13 +1578,13 @@ msgstr "" #: rhodecode/templates/admin/settings/settings_hooks.mako:46 #: rhodecode/templates/admin/users/user_edit_auth_tokens.mako:45 #: rhodecode/templates/admin/users/user_edit_emails.mako:31 -#: rhodecode/templates/admin/users/user_edit_ips.mako:34 +#: rhodecode/templates/admin/users/user_edit_ips.mako:35 #: rhodecode/templates/base/issue_tracker_settings.mako:69 -#: rhodecode/templates/base/vcs_settings.mako:251 -#: rhodecode/templates/base/vcs_settings.mako:276 -#: rhodecode/templates/changeset/changeset_file_comment.mako:137 -#: rhodecode/templates/changeset/changeset_file_comment.mako:139 +#: rhodecode/templates/base/vcs_settings.mako:264 +#: rhodecode/templates/base/vcs_settings.mako:289 #: rhodecode/templates/changeset/changeset_file_comment.mako:142 +#: rhodecode/templates/changeset/changeset_file_comment.mako:144 +#: rhodecode/templates/changeset/changeset_file_comment.mako:147 #: rhodecode/templates/data_table/_dt_elements.mako:123 #: rhodecode/templates/data_table/_dt_elements.mako:184 #: rhodecode/templates/data_table/_dt_elements.mako:198 @@ -1732,16 +1732,15 @@ msgstr "" #: rhodecode/integrations/types/slack.py:60 rhodecode/templates/login.mako:43 #: rhodecode/templates/register.mako:41 -#: rhodecode/templates/admin/admin_log.mako:7 +#: rhodecode/templates/admin/admin_log_base.mako:6 #: rhodecode/templates/admin/my_account/my_account_profile.mako:24 #: rhodecode/templates/admin/my_account/my_account_profile_edit.mako:24 #: rhodecode/templates/admin/my_account/my_account_profile_edit.mako:69 #: rhodecode/templates/admin/user_groups/user_group_edit_settings.mako:70 #: rhodecode/templates/admin/users/user_add.mako:35 -#: rhodecode/templates/admin/users/user_edit_audit.mako:22 #: rhodecode/templates/admin/users/user_edit_profile.mako:39 -#: rhodecode/templates/admin/users/users.mako:62 -#: rhodecode/templates/base/base.mako:304 +#: rhodecode/templates/admin/users/users.mako:63 +#: rhodecode/templates/base/base.mako:306 #: rhodecode/templates/debug_style/login.html:36 #: rhodecode/templates/email_templates/user_registration.mako:23 #: rhodecode/templates/users/user_profile.mako:27 @@ -1776,180 +1775,180 @@ msgstr "" msgid "Send events such as repo pushes and pull requests to your slack channel." msgstr "" -#: rhodecode/integrations/types/webhook.py:152 +#: rhodecode/integrations/types/webhook.py:164 msgid "Webhook URL" msgstr "" -#: rhodecode/integrations/types/webhook.py:154 +#: rhodecode/integrations/types/webhook.py:166 msgid "URL of the webhook to receive POST event. Following variables are allowed to be used: {vars}. Some of the variables would trigger multiple calls, like ${{branch}} or ${{commit_id}}. Webhook will be called as many times as unique objects in data in such cases." msgstr "" -#: rhodecode/integrations/types/webhook.py:168 +#: rhodecode/integrations/types/webhook.py:180 msgid "Secret Token" msgstr "" -#: rhodecode/integrations/types/webhook.py:169 +#: rhodecode/integrations/types/webhook.py:181 msgid "String used to validate received payloads." msgstr "" -#: rhodecode/integrations/types/webhook.py:178 +#: rhodecode/integrations/types/webhook.py:190 msgid "Call Method" msgstr "" -#: rhodecode/integrations/types/webhook.py:179 +#: rhodecode/integrations/types/webhook.py:191 msgid "Select if the webhook call should be made with POST or GET." msgstr "" -#: rhodecode/integrations/types/webhook.py:192 +#: rhodecode/integrations/types/webhook.py:204 msgid "Webhook" msgstr "" -#: rhodecode/integrations/types/webhook.py:193 +#: rhodecode/integrations/types/webhook.py:205 msgid "Post json events to a webhook endpoint" msgstr "" -#: rhodecode/lib/action_parser.py:89 +#: rhodecode/lib/action_parser.py:94 msgid "[deleted] repository" msgstr "" -#: rhodecode/lib/action_parser.py:92 rhodecode/lib/action_parser.py:110 +#: rhodecode/lib/action_parser.py:97 rhodecode/lib/action_parser.py:115 msgid "[created] repository" msgstr "" -#: rhodecode/lib/action_parser.py:95 +#: rhodecode/lib/action_parser.py:100 msgid "[created] repository as fork" msgstr "" -#: rhodecode/lib/action_parser.py:98 rhodecode/lib/action_parser.py:113 +#: rhodecode/lib/action_parser.py:103 rhodecode/lib/action_parser.py:118 msgid "[forked] repository" msgstr "" -#: rhodecode/lib/action_parser.py:101 rhodecode/lib/action_parser.py:116 +#: rhodecode/lib/action_parser.py:106 rhodecode/lib/action_parser.py:121 msgid "[updated] repository" msgstr "" -#: rhodecode/lib/action_parser.py:104 +#: rhodecode/lib/action_parser.py:109 msgid "[downloaded] archive from repository" msgstr "" -#: rhodecode/lib/action_parser.py:107 +#: rhodecode/lib/action_parser.py:112 msgid "[delete] repository" msgstr "" -#: rhodecode/lib/action_parser.py:119 +#: rhodecode/lib/action_parser.py:124 msgid "[created] user" msgstr "" -#: rhodecode/lib/action_parser.py:122 +#: rhodecode/lib/action_parser.py:127 msgid "[updated] user" msgstr "" -#: rhodecode/lib/action_parser.py:125 +#: rhodecode/lib/action_parser.py:130 msgid "[created] user group" msgstr "" -#: rhodecode/lib/action_parser.py:128 +#: rhodecode/lib/action_parser.py:133 msgid "[updated] user group" msgstr "" -#: rhodecode/lib/action_parser.py:131 +#: rhodecode/lib/action_parser.py:136 msgid "[commented] on commit in repository" msgstr "" -#: rhodecode/lib/action_parser.py:134 +#: rhodecode/lib/action_parser.py:139 msgid "[commented] on pull request for" msgstr "" -#: rhodecode/lib/action_parser.py:137 +#: rhodecode/lib/action_parser.py:142 msgid "[closed] pull request for" msgstr "" -#: rhodecode/lib/action_parser.py:140 +#: rhodecode/lib/action_parser.py:145 msgid "[merged] pull request for" msgstr "" -#: rhodecode/lib/action_parser.py:143 +#: rhodecode/lib/action_parser.py:148 msgid "[pushed] into" msgstr "" -#: rhodecode/lib/action_parser.py:146 +#: rhodecode/lib/action_parser.py:151 msgid "[committed via RhodeCode] into repository" msgstr "" -#: rhodecode/lib/action_parser.py:149 +#: rhodecode/lib/action_parser.py:154 msgid "[pulled from remote] into repository" msgstr "" -#: rhodecode/lib/action_parser.py:152 +#: rhodecode/lib/action_parser.py:157 msgid "[pulled] from" msgstr "" -#: rhodecode/lib/action_parser.py:155 +#: rhodecode/lib/action_parser.py:160 msgid "[started following] repository" msgstr "" -#: rhodecode/lib/action_parser.py:158 +#: rhodecode/lib/action_parser.py:163 msgid "[stopped following] repository" msgstr "" -#: rhodecode/lib/action_parser.py:166 +#: rhodecode/lib/action_parser.py:172 #, python-format msgid "fork name %s" msgstr "" -#: rhodecode/lib/action_parser.py:183 +#: rhodecode/lib/action_parser.py:190 #: rhodecode/templates/pullrequests/pullrequest_show.mako:51 #, python-format msgid "Pull request #%s" msgstr "" -#: rhodecode/lib/action_parser.py:216 +#: rhodecode/lib/action_parser.py:223 #, python-format msgid "Show all combined commits %s->%s" msgstr "" -#: rhodecode/lib/action_parser.py:220 -msgid "compare view" -msgstr "" - #: rhodecode/lib/action_parser.py:227 +msgid "compare view" +msgstr "" + +#: rhodecode/lib/action_parser.py:234 #, python-format msgid " and %(num)s more commits" msgstr "" -#: rhodecode/lib/action_parser.py:279 +#: rhodecode/lib/action_parser.py:286 #, python-format msgid "Deleted branch: %s" msgstr "" -#: rhodecode/lib/action_parser.py:282 +#: rhodecode/lib/action_parser.py:289 #, python-format msgid "Created tag: %s" msgstr "" -#: rhodecode/lib/action_parser.py:295 +#: rhodecode/lib/action_parser.py:302 msgid "Commit not found" msgstr "" -#: rhodecode/lib/auth.py:1197 +#: rhodecode/lib/auth.py:1220 #, python-format msgid "IP %s not allowed" msgstr "" -#: rhodecode/lib/auth.py:1281 +#: rhodecode/lib/auth.py:1309 msgid "You need to be a registered user to perform this action" msgstr "" -#: rhodecode/lib/auth.py:1329 +#: rhodecode/lib/auth.py:1366 #, python-format msgid "Action not supported for %s." msgstr "" -#: rhodecode/lib/auth.py:1379 +#: rhodecode/lib/auth.py:1412 msgid "You need to be signed in to view this page" msgstr "" -#: rhodecode/lib/base.py:549 +#: rhodecode/lib/base.py:561 #, python-format msgid "The repository at %(repo_name)s cannot be located." msgstr "" @@ -1974,20 +1973,21 @@ msgstr "" msgid "Click to select line" msgstr "" -#: rhodecode/lib/helpers.py:1517 +#: rhodecode/lib/helpers.py:1527 #, python-format msgid " and %s more" msgstr "" -#: rhodecode/lib/helpers.py:1521 +#: rhodecode/lib/helpers.py:1531 msgid "No Files" msgstr "" -#: rhodecode/lib/helpers.py:1800 +#: rhodecode/lib/helpers.py:1836 msgid "" "Example filter terms:\n" " repository:vcs\n" " username:marcin\n" +" username:(NOT marcin)\n" " action:*push*\n" " ip:127.0.0.1\n" " date:20120101\n" @@ -2002,7 +2002,21 @@ msgid "" " \"username:test AND repository:test*\"\n" msgstr "" -#: rhodecode/lib/helpers.py:1820 +#: rhodecode/lib/helpers.py:1859 +msgid "" +"Example filter terms for `{searcher}` search:\n" +"{terms}\n" +"Generate wildcards using '*' character:\n" +" \"repo_name:vcs*\" - search everything starting with 'vcs'\n" +" \"repo_name:*vcs*\" - search for repository containing 'vcs'\n" +"\n" +"Optional AND / OR operators in queries\n" +" \"repo_name:vcs OR repo_name:test\"\n" +" \"owner:test AND repo_name:test*\"\n" +"More: {search_doc}" +msgstr "" + +#: rhodecode/lib/helpers.py:1875 #, 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 "" @@ -2042,7 +2056,7 @@ msgstr "" #: rhodecode/lib/utils2.py:515 #: rhodecode/public/js/rhodecode-components.js:33659 #: rhodecode/public/js/scripts.js:25507 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:66 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:74 #: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:174 msgid "just now" msgstr "" @@ -2076,7 +2090,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2289 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2289 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2339 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2340 rhodecode/model/db.py:2410 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2340 rhodecode/model/db.py:2482 msgid "Repository no access" msgstr "" @@ -2109,7 +2123,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2290 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2290 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2340 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2341 rhodecode/model/db.py:2411 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2341 rhodecode/model/db.py:2483 msgid "Repository read access" msgstr "" @@ -2142,7 +2156,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2291 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2291 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2341 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2342 rhodecode/model/db.py:2412 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2342 rhodecode/model/db.py:2484 msgid "Repository write access" msgstr "" @@ -2175,7 +2189,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2292 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2292 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2342 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2343 rhodecode/model/db.py:2413 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2343 rhodecode/model/db.py:2485 msgid "Repository admin access" msgstr "" @@ -2248,7 +2262,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2310 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2310 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2360 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2361 rhodecode/model/db.py:2431 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2361 rhodecode/model/db.py:2503 msgid "Repository creation disabled" msgstr "" @@ -2281,7 +2295,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2311 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2311 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2361 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2362 rhodecode/model/db.py:2432 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2362 rhodecode/model/db.py:2504 msgid "Repository creation enabled" msgstr "" @@ -2314,7 +2328,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2315 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2315 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2365 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2366 rhodecode/model/db.py:2436 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2366 rhodecode/model/db.py:2508 msgid "Repository forking disabled" msgstr "" @@ -2347,7 +2361,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2316 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2316 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2366 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2367 rhodecode/model/db.py:2437 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2367 rhodecode/model/db.py:2509 msgid "Repository forking enabled" msgstr "" @@ -2401,7 +2415,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2950 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2950 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:3050 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:3051 rhodecode/model/db.py:3121 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:3051 rhodecode/model/db.py:3212 msgid "Not Reviewed" msgstr "" @@ -2434,7 +2448,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2951 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2951 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:3051 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:3052 rhodecode/model/db.py:3122 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:3052 rhodecode/model/db.py:3213 msgid "Approved" msgstr "" @@ -2467,7 +2481,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2952 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2952 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:3052 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:3053 rhodecode/model/db.py:3123 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:3053 rhodecode/model/db.py:3214 msgid "Rejected" msgstr "" @@ -2500,7 +2514,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2953 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2953 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:3053 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:3054 rhodecode/model/db.py:3124 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:3054 rhodecode/model/db.py:3215 msgid "Under Review" msgstr "" @@ -2530,7 +2544,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2294 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2294 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2344 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2345 rhodecode/model/db.py:2415 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2345 rhodecode/model/db.py:2487 msgid "Repository group no access" msgstr "" @@ -2560,7 +2574,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2295 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2295 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2345 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2346 rhodecode/model/db.py:2416 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2346 rhodecode/model/db.py:2488 msgid "Repository group read access" msgstr "" @@ -2590,7 +2604,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2296 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2296 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2346 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2347 rhodecode/model/db.py:2417 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2347 rhodecode/model/db.py:2489 msgid "Repository group write access" msgstr "" @@ -2620,7 +2634,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2297 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2297 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2347 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2348 rhodecode/model/db.py:2418 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2348 rhodecode/model/db.py:2490 msgid "Repository group admin access" msgstr "" @@ -2649,7 +2663,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2299 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2299 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2349 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2350 rhodecode/model/db.py:2420 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2350 rhodecode/model/db.py:2492 msgid "User group no access" msgstr "" @@ -2678,7 +2692,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2300 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2300 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2350 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2351 rhodecode/model/db.py:2421 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2351 rhodecode/model/db.py:2493 msgid "User group read access" msgstr "" @@ -2707,7 +2721,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2301 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2301 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2351 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2352 rhodecode/model/db.py:2422 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2352 rhodecode/model/db.py:2494 msgid "User group write access" msgstr "" @@ -2736,7 +2750,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2302 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2302 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2352 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2353 rhodecode/model/db.py:2423 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2353 rhodecode/model/db.py:2495 msgid "User group admin access" msgstr "" @@ -2765,7 +2779,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2304 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2304 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2354 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2355 rhodecode/model/db.py:2425 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2355 rhodecode/model/db.py:2497 msgid "Repository Group creation disabled" msgstr "" @@ -2794,7 +2808,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2305 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2305 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2355 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2356 rhodecode/model/db.py:2426 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2356 rhodecode/model/db.py:2498 msgid "Repository Group creation enabled" msgstr "" @@ -2823,7 +2837,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2307 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2307 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2357 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2358 rhodecode/model/db.py:2428 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2358 rhodecode/model/db.py:2500 msgid "User Group creation disabled" msgstr "" @@ -2852,7 +2866,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2308 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2308 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2358 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2359 rhodecode/model/db.py:2429 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2359 rhodecode/model/db.py:2501 msgid "User Group creation enabled" msgstr "" @@ -2881,7 +2895,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2318 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2318 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2368 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2369 rhodecode/model/db.py:2439 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2369 rhodecode/model/db.py:2511 msgid "Registration disabled" msgstr "" @@ -2910,7 +2924,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2319 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2319 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2369 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2370 rhodecode/model/db.py:2440 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2370 rhodecode/model/db.py:2512 msgid "User Registration with manual account activation" msgstr "" @@ -2939,7 +2953,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2320 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2320 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2370 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2371 rhodecode/model/db.py:2441 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2371 rhodecode/model/db.py:2513 msgid "User Registration with automatic account activation" msgstr "" @@ -2968,7 +2982,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2322 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2322 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2376 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2377 rhodecode/model/db.py:2447 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2377 rhodecode/model/db.py:2519 #: rhodecode/model/permission.py:95 msgid "Manual activation of external account" msgstr "" @@ -2998,7 +3012,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2323 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2323 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2377 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2378 rhodecode/model/db.py:2448 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2378 rhodecode/model/db.py:2520 #: rhodecode/model/permission.py:96 msgid "Automatic activation of external account" msgstr "" @@ -3022,7 +3036,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2312 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2312 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2362 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2363 rhodecode/model/db.py:2433 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2363 rhodecode/model/db.py:2505 msgid "Repository creation enabled with write permission to a repository group" msgstr "" @@ -3045,7 +3059,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2313 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2313 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2363 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2364 rhodecode/model/db.py:2434 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2364 rhodecode/model/db.py:2506 msgid "Repository creation disabled with write permission to a repository group" msgstr "" @@ -3065,7 +3079,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2287 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2287 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2337 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2338 rhodecode/model/db.py:2408 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2338 rhodecode/model/db.py:2480 msgid "RhodeCode Super Administrator" msgstr "" @@ -3083,7 +3097,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2325 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2325 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2379 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2380 rhodecode/model/db.py:2450 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2380 rhodecode/model/db.py:2522 msgid "Inherit object permissions from default user disabled" msgstr "" @@ -3101,7 +3115,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2326 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2326 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2380 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2381 rhodecode/model/db.py:2451 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2381 rhodecode/model/db.py:2523 msgid "Inherit object permissions from default user enabled" msgstr "" @@ -3111,7 +3125,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:912 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:912 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:954 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:955 rhodecode/model/db.py:1008 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:955 rhodecode/model/db.py:1043 msgid "all" msgstr "" @@ -3121,7 +3135,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:913 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:913 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:955 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:956 rhodecode/model/db.py:1009 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:956 rhodecode/model/db.py:1044 msgid "http/web interface" msgstr "" @@ -3131,7 +3145,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:914 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:914 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:956 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:957 rhodecode/model/db.py:1010 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:957 rhodecode/model/db.py:1045 msgid "vcs (git/hg/svn protocol)" msgstr "" @@ -3141,7 +3155,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:915 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:915 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:957 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:958 rhodecode/model/db.py:1011 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:958 rhodecode/model/db.py:1046 msgid "api calls" msgstr "" @@ -3151,7 +3165,7 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:916 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:916 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:958 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:959 rhodecode/model/db.py:1012 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:959 rhodecode/model/db.py:1047 msgid "feed access" msgstr "" @@ -3161,62 +3175,62 @@ msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_4_0_2.py:2065 #: rhodecode/lib/dbmigrate/schema/db_4_5_0_0.py:2065 #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2108 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2109 rhodecode/model/db.py:2179 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2109 rhodecode/model/db.py:2248 msgid "No parent" msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2372 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2373 rhodecode/model/db.py:2443 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2373 rhodecode/model/db.py:2515 msgid "Password reset enabled" msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2373 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2374 rhodecode/model/db.py:2444 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2374 rhodecode/model/db.py:2516 msgid "Password reset hidden" msgstr "" #: rhodecode/lib/dbmigrate/schema/db_4_7_0_0.py:2374 -#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2375 rhodecode/model/db.py:2445 +#: rhodecode/lib/dbmigrate/schema/db_4_7_0_1.py:2375 rhodecode/model/db.py:2517 msgid "Password reset disabled" msgstr "" -#: rhodecode/lib/index/whoosh.py:149 +#: rhodecode/lib/index/whoosh.py:150 msgid "Invalid search query. Try quoting it." msgstr "" -#: rhodecode/lib/index/whoosh.py:151 +#: rhodecode/lib/index/whoosh.py:152 msgid "There is no index to search in. Please run whoosh indexer" msgstr "" -#: rhodecode/lib/index/whoosh.py:156 +#: rhodecode/lib/index/whoosh.py:157 msgid "An error occurred during this search operation" msgstr "" -#: rhodecode/lib/index/whoosh.py:164 -msgid "Index Type" -msgstr "" - #: rhodecode/lib/index/whoosh.py:165 +msgid "Index Type" +msgstr "" + +#: rhodecode/lib/index/whoosh.py:166 msgid "File Index" msgstr "" -#: rhodecode/lib/index/whoosh.py:166 rhodecode/lib/index/whoosh.py:171 +#: rhodecode/lib/index/whoosh.py:167 rhodecode/lib/index/whoosh.py:172 msgid "Indexed documents" msgstr "" -#: rhodecode/lib/index/whoosh.py:168 rhodecode/lib/index/whoosh.py:173 +#: rhodecode/lib/index/whoosh.py:169 rhodecode/lib/index/whoosh.py:174 msgid "Last update" msgstr "" -#: rhodecode/lib/index/whoosh.py:170 +#: rhodecode/lib/index/whoosh.py:171 msgid "Commit index" msgstr "" -#: rhodecode/model/comment.py:368 +#: rhodecode/model/comment.py:374 msgid "made a comment" msgstr "" -#: rhodecode/model/comment.py:369 +#: rhodecode/model/comment.py:375 msgid "Show it now" msgstr "" @@ -3302,7 +3316,7 @@ msgstr "" #: rhodecode/model/permission.py:79 #: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako:11 #: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako:126 -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:12 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:11 #: rhodecode/templates/admin/user_groups/user_group_edit_perms.mako:11 msgid "None" msgstr "" @@ -3310,7 +3324,7 @@ msgstr "" #: rhodecode/model/permission.py:68 rhodecode/model/permission.py:74 #: rhodecode/model/permission.py:80 #: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako:12 -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:13 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:12 #: rhodecode/templates/admin/user_groups/user_group_edit_perms.mako:12 msgid "Read" msgstr "" @@ -3318,10 +3332,10 @@ msgstr "" #: rhodecode/model/permission.py:69 rhodecode/model/permission.py:75 #: rhodecode/model/permission.py:81 #: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako:13 -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:14 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:13 #: rhodecode/templates/admin/user_groups/user_group_edit_perms.mako:13 -#: rhodecode/templates/changeset/changeset_file_comment.mako:266 -#: rhodecode/templates/changeset/changeset_file_comment.mako:316 +#: rhodecode/templates/changeset/changeset_file_comment.mako:271 +#: rhodecode/templates/changeset/changeset_file_comment.mako:321 msgid "Write" msgstr "" @@ -3344,7 +3358,7 @@ msgstr "" #: rhodecode/templates/admin/repo_groups/repo_groups.mako:13 #: rhodecode/templates/admin/repos/repo_add.mako:13 #: rhodecode/templates/admin/repos/repo_add.mako:17 -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:15 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:14 #: rhodecode/templates/admin/repos/repos.mako:13 #: rhodecode/templates/admin/settings/settings.mako:12 #: rhodecode/templates/admin/user_groups/user_group_add.mako:11 @@ -3354,9 +3368,9 @@ msgstr "" #: rhodecode/templates/admin/users/user_add.mako:11 #: rhodecode/templates/admin/users/user_edit.mako:12 #: rhodecode/templates/admin/users/users.mako:13 -#: rhodecode/templates/admin/users/users.mako:75 -#: rhodecode/templates/base/base.mako:409 -#: rhodecode/templates/base/base.mako:416 +#: rhodecode/templates/admin/users/users.mako:76 +#: rhodecode/templates/base/base.mako:412 +#: rhodecode/templates/base/base.mako:419 msgid "Admin" msgstr "" @@ -3387,79 +3401,79 @@ msgstr "" msgid "Disable password recovery" msgstr "" -#: rhodecode/model/pull_request.py:78 +#: rhodecode/model/pull_request.py:80 msgid "This pull request can be automatically merged." msgstr "" -#: rhodecode/model/pull_request.py:80 +#: rhodecode/model/pull_request.py:82 msgid "This pull request cannot be merged because of an unhandled exception." msgstr "" -#: rhodecode/model/pull_request.py:83 -msgid "This pull request cannot be merged because of merge conflicts." -msgstr "" - #: rhodecode/model/pull_request.py:85 +msgid "This pull request cannot be merged because of merge conflicts." +msgstr "" + +#: rhodecode/model/pull_request.py:87 msgid "This pull request could not be merged because push to target failed." msgstr "" -#: rhodecode/model/pull_request.py:88 +#: rhodecode/model/pull_request.py:90 msgid "This pull request cannot be merged because the target is not a head." msgstr "" -#: rhodecode/model/pull_request.py:91 +#: rhodecode/model/pull_request.py:93 msgid "This pull request cannot be merged because the source contains more branches than the target." msgstr "" -#: rhodecode/model/pull_request.py:94 +#: rhodecode/model/pull_request.py:96 msgid "This pull request cannot be merged because the target has multiple heads." msgstr "" -#: rhodecode/model/pull_request.py:97 +#: rhodecode/model/pull_request.py:99 msgid "This pull request cannot be merged because the target repository is locked." msgstr "" -#: rhodecode/model/pull_request.py:100 +#: rhodecode/model/pull_request.py:102 msgid "This pull request cannot be merged because the target or the source reference is missing." msgstr "" -#: rhodecode/model/pull_request.py:103 +#: rhodecode/model/pull_request.py:105 msgid "This pull request cannot be merged because the target reference is missing." msgstr "" -#: rhodecode/model/pull_request.py:106 +#: rhodecode/model/pull_request.py:108 msgid "This pull request cannot be merged because the source reference is missing." msgstr "" -#: rhodecode/model/pull_request.py:109 +#: rhodecode/model/pull_request.py:111 msgid "This pull request cannot be merged because of conflicts related to sub repositories." msgstr "" -#: rhodecode/model/pull_request.py:115 -msgid "Pull request update successful." -msgstr "" - #: rhodecode/model/pull_request.py:117 -msgid "Pull request update failed because of an unknown error." +msgid "Pull request update successful." msgstr "" #: rhodecode/model/pull_request.py:119 -msgid "No update needed because the source and target have not changed." +msgid "Pull request update failed because of an unknown error." msgstr "" #: rhodecode/model/pull_request.py:121 -msgid "Pull request cannot be updated because the reference type is not supported for an update." -msgstr "" - -#: rhodecode/model/pull_request.py:124 +msgid "No update needed because the source and target have not changed." +msgstr "" + +#: rhodecode/model/pull_request.py:123 +msgid "Pull request cannot be updated because the reference type is not supported for an update. Only Branch, Tag or Bookmark is allowed." +msgstr "" + +#: rhodecode/model/pull_request.py:126 msgid "This pull request cannot be updated because the target reference is missing." msgstr "" -#: rhodecode/model/pull_request.py:127 +#: rhodecode/model/pull_request.py:129 msgid "This pull request cannot be updated because the source reference is missing." msgstr "" -#: rhodecode/model/pull_request.py:524 +#: rhodecode/model/pull_request.py:544 #, python-format msgid "" "Merge pull request #%(pr_id)s from %(source_repo)s %(source_ref_name)s\n" @@ -3467,86 +3481,90 @@ msgid "" " %(pr_title)s" msgstr "" -#: rhodecode/model/pull_request.py:556 +#: rhodecode/model/pull_request.py:576 msgid "Pull request merged and closed" msgstr "" -#: rhodecode/model/pull_request.py:1087 +#: rhodecode/model/pull_request.py:1108 +msgid "Closing with status change {transition_icon} {status}." +msgstr "" + +#: rhodecode/model/pull_request.py:1152 msgid "Server-side pull request merging is disabled." msgstr "" -#: rhodecode/model/pull_request.py:1089 +#: rhodecode/model/pull_request.py:1154 msgid "This pull request is closed." msgstr "" -#: rhodecode/model/pull_request.py:1101 +#: rhodecode/model/pull_request.py:1166 msgid "Pull request merging is not supported." msgstr "" -#: rhodecode/model/pull_request.py:1119 +#: rhodecode/model/pull_request.py:1184 msgid "Target repository large files support is disabled." msgstr "" -#: rhodecode/model/pull_request.py:1122 +#: rhodecode/model/pull_request.py:1187 msgid "Source repository large files support is disabled." msgstr "" -#: rhodecode/model/pull_request.py:1279 rhodecode/model/scm.py:790 +#: rhodecode/model/pull_request.py:1344 rhodecode/model/scm.py:783 msgid "Bookmarks" msgstr "" -#: rhodecode/model/pull_request.py:1284 +#: rhodecode/model/pull_request.py:1349 msgid "Commit IDs" msgstr "" -#: rhodecode/model/pull_request.py:1287 +#: rhodecode/model/pull_request.py:1352 msgid "Closed Branches" msgstr "" -#: rhodecode/model/pull_request.py:1411 +#: rhodecode/model/pull_request.py:1493 msgid "User `{}` not allowed to perform merge." msgstr "" -#: rhodecode/model/pull_request.py:1424 +#: rhodecode/model/pull_request.py:1506 msgid "Pull request reviewer approval is pending." msgstr "" -#: rhodecode/model/pull_request.py:1439 +#: rhodecode/model/pull_request.py:1521 msgid "Cannot merge, {} TODO still not resolved." msgstr "" -#: rhodecode/model/pull_request.py:1442 +#: rhodecode/model/pull_request.py:1524 msgid "Cannot merge, {} TODOs still not resolved." msgstr "" -#: rhodecode/model/scm.py:768 +#: rhodecode/model/scm.py:761 msgid "latest tip" msgstr "" -#: rhodecode/model/user.py:126 +#: rhodecode/model/user.py:166 msgid "You can't Edit this user since it's crucial for entire application" msgstr "" -#: rhodecode/model/user.py:292 +#: rhodecode/model/user.py:332 #, python-format msgid "You can't edit this user (`%(username)s`) since it's crucial for entire application" msgstr "" -#: rhodecode/model/user.py:462 +#: rhodecode/model/user.py:502 msgid "You can't remove this user since it's crucial for entire application" msgstr "" -#: rhodecode/model/user.py:470 +#: rhodecode/model/user.py:510 #, python-format msgid "user \"%s\" still owns %s repositories and cannot be removed. Switch owners or remove those repositories:%s" msgstr "" -#: rhodecode/model/user.py:479 +#: rhodecode/model/user.py:519 #, python-format msgid "user \"%s\" still owns %s repository groups and cannot be removed. Switch owners or remove those repository groups:%s" msgstr "" -#: rhodecode/model/user.py:488 +#: rhodecode/model/user.py:528 #, python-format msgid "user \"%s\" still owns %s user groups and cannot be removed. Switch owners or remove those user groups:%s" msgstr "" @@ -3570,6 +3588,7 @@ msgid "Username \"%(username)s\" is forb msgstr "" #: rhodecode/model/validators.py:164 +#: rhodecode/model/validation_schema/schemas/user_schema.py:69 msgid "Username may only contain alphanumeric characters underscores, periods or dashes and must begin with alphanumeric character or underscore" msgstr "" @@ -3675,94 +3694,107 @@ msgstr "" msgid "Repository group with name \"%(repo)s\" exists in group \"%(group)s\"" msgstr "" -#: rhodecode/model/validators.py:620 +#: rhodecode/model/validators.py:581 +#: rhodecode/model/validation_schema/schemas/repo_schema.py:219 +msgid "Repository name cannot end with .git" +msgstr "" + +#: rhodecode/model/validators.py:640 #, python-format msgid "invalid clone url for %(rtype)s repository" msgstr "" -#: rhodecode/model/validators.py:621 +#: rhodecode/model/validators.py:641 #, python-format msgid "Invalid clone url, provide a valid clone url starting with one of %(allowed_prefixes)s" msgstr "" -#: rhodecode/model/validators.py:650 +#: rhodecode/model/validators.py:670 msgid "Fork have to be the same type as parent" msgstr "" -#: rhodecode/model/validators.py:665 +#: rhodecode/model/validators.py:685 msgid "You do not have the permission to create repositories in this group." msgstr "" -#: rhodecode/model/validators.py:668 -#: rhodecode/model/validation_schema/schemas/repo_schema.py:102 +#: rhodecode/model/validators.py:688 +#: rhodecode/model/validation_schema/schemas/repo_schema.py:125 msgid "You do not have the permission to store repositories in the root location." msgstr "" -#: rhodecode/model/validators.py:728 +#: rhodecode/model/validators.py:748 msgid "This username or user group name is not valid" msgstr "" -#: rhodecode/model/validators.py:846 +#: rhodecode/model/validators.py:879 msgid "This is not a valid path" msgstr "" -#: rhodecode/model/validators.py:861 +#: rhodecode/model/validators.py:894 msgid "This e-mail address is already taken" msgstr "" -#: rhodecode/model/validators.py:881 +#: rhodecode/model/validators.py:914 #, python-format msgid "e-mail \"%(email)s\" does not exist." msgstr "" -#: rhodecode/model/validators.py:902 +#: rhodecode/model/validators.py:935 #, python-format msgid "Revisions %(revs)s are already part of pull request or have set status" msgstr "" -#: rhodecode/model/validators.py:933 -#: rhodecode/model/validation_schema/validators.py:16 -#: rhodecode/model/validation_schema/validators.py:29 +#: rhodecode/model/validators.py:966 +#: rhodecode/model/validation_schema/validators.py:40 +#: rhodecode/model/validation_schema/validators.py:53 msgid "Please enter a valid IPv4 or IpV6 address" msgstr "" -#: rhodecode/model/validators.py:934 +#: rhodecode/model/validators.py:967 #, python-format msgid "The network size (bits) must be within the range of 0-32 (not %(bits)r)" msgstr "" -#: rhodecode/model/validators.py:961 +#: rhodecode/model/validators.py:994 msgid "Key name can only consist of letters, underscore, dash or numbers" msgstr "" -#: rhodecode/model/validators.py:976 +#: rhodecode/model/validators.py:1009 #, python-format msgid "Plugins %(loaded)s and %(next_to_load)s both export the same name" msgstr "" -#: rhodecode/model/validators.py:979 +#: rhodecode/model/validators.py:1012 #, python-format msgid "The plugin \"%(plugin_id)s\" is missing an includeme function." msgstr "" -#: rhodecode/model/validators.py:982 +#: rhodecode/model/validators.py:1015 #, python-format msgid "Can not load plugin \"%(plugin_id)s\"" msgstr "" -#: rhodecode/model/validators.py:984 +#: rhodecode/model/validators.py:1017 #, python-format msgid "No plugin available with ID \"%(plugin_id)s\"" msgstr "" -#: rhodecode/model/validation_schema/validators.py:37 +#: rhodecode/model/validation_schema/validators.py:61 msgid "Invalid glob pattern" msgstr "" -#: rhodecode/model/validation_schema/validators.py:46 +#: rhodecode/model/validation_schema/validators.py:70 msgid "Name must start with a letter or number. Got `{}`" msgstr "" +#: rhodecode/model/validation_schema/validators.py:132 +msgid "Invalid clone url, provide a valid clone url starting with one of {allowed_prefixes}" +msgstr "" + +#: rhodecode/model/validation_schema/validators.py:138 +msgid "invalid clone url for {repo_type} repository" +msgstr "" + #: rhodecode/model/validation_schema/schemas/comment_schema.py:42 #: rhodecode/model/validation_schema/schemas/gist_schema.py:89 msgid "Gist with name {} already exists" @@ -3831,253 +3863,261 @@ msgid "Repo group owner with id `{}` doe msgstr "" #: rhodecode/model/validation_schema/schemas/repo_group_schema.py:130 -#: rhodecode/model/validation_schema/schemas/repo_schema.py:181 +#: rhodecode/model/validation_schema/schemas/repo_schema.py:204 msgid "Repository with name `{}` already exists" msgstr "" #: rhodecode/model/validation_schema/schemas/repo_group_schema.py:135 -#: rhodecode/model/validation_schema/schemas/repo_schema.py:186 +#: rhodecode/model/validation_schema/schemas/repo_schema.py:209 msgid "Repository group with name `{}` already exists" msgstr "" -#: rhodecode/model/validation_schema/schemas/repo_schema.py:48 +#: rhodecode/model/validation_schema/schemas/repo_schema.py:55 msgid "Repo owner with id `{}` does not exists" msgstr "" -#: rhodecode/model/validation_schema/schemas/repo_schema.py:68 +#: rhodecode/model/validation_schema/schemas/repo_schema.py:91 msgid "Fork with id `{}` does not exists" msgstr "" -#: rhodecode/model/validation_schema/schemas/repo_schema.py:71 +#: rhodecode/model/validation_schema/schemas/repo_schema.py:94 msgid "Cannot set fork of parameter of this repository to itself" msgstr "" -#: rhodecode/model/validation_schema/schemas/repo_schema.py:96 -#: rhodecode/model/validation_schema/schemas/repo_schema.py:100 +#: rhodecode/model/validation_schema/schemas/repo_schema.py:119 +#: rhodecode/model/validation_schema/schemas/repo_schema.py:123 msgid "Repository group `{}` does not exist" msgstr "" -#: rhodecode/model/validation_schema/schemas/user_schema.py:36 +#: rhodecode/model/validation_schema/schemas/user_group_schema.py:32 +msgid "Allowed in name are letters, numbers, and `-`, `_`, `.` Name must start with a letter or number. Got `{}`" +msgstr "" + +#: rhodecode/model/validation_schema/schemas/user_group_schema.py:48 +msgid "User group owner with id `{}` does not exists" +msgstr "" + +#: rhodecode/model/validation_schema/schemas/user_schema.py:38 msgid "Password is incorrect" msgstr "" -#: rhodecode/model/validation_schema/schemas/user_schema.py:59 +#: rhodecode/model/validation_schema/schemas/user_schema.py:60 msgid "New password must be different to old password" msgstr "" #: rhodecode/public/js/rhodecode-components.js:31663 #: rhodecode/public/js/scripts.js:23511 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:23 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:29 #: rhodecode/public/js/src/plugins/jquery.autocomplete.js:87 msgid "No results" msgstr "" #: rhodecode/public/js/rhodecode-components.js:33594 #: rhodecode/public/js/scripts.js:25442 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:88 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:97 #: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:109 msgid "{0} year" msgstr "" #: rhodecode/public/js/rhodecode-components.js:33595 #: rhodecode/public/js/scripts.js:25443 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:83 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:92 #: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:110 msgid "{0} month" msgstr "" #: rhodecode/public/js/rhodecode-components.js:33596 #: rhodecode/public/js/scripts.js:25444 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:78 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:87 #: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:111 msgid "{0} day" msgstr "" #: rhodecode/public/js/rhodecode-components.js:33597 #: rhodecode/public/js/scripts.js:25445 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:80 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:89 #: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:112 msgid "{0} hour" msgstr "" #: rhodecode/public/js/rhodecode-components.js:33598 #: rhodecode/public/js/scripts.js:25446 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:82 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:91 #: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:113 msgid "{0} min" msgstr "" #: rhodecode/public/js/rhodecode-components.js:33599 #: rhodecode/public/js/scripts.js:25447 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:87 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:96 #: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:114 msgid "{0} sec" msgstr "" #: rhodecode/public/js/rhodecode-components.js:33619 #: rhodecode/public/js/scripts.js:25467 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:63 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:71 #: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:134 msgid "in {0}" msgstr "" #: rhodecode/public/js/rhodecode-components.js:33627 #: rhodecode/public/js/scripts.js:25475 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:75 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:84 #: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:142 msgid "{0} ago" msgstr "" #: rhodecode/public/js/rhodecode-components.js:33639 #: rhodecode/public/js/scripts.js:25487 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:90 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:99 #: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:154 msgid "{0}, {1} ago" msgstr "" #: rhodecode/public/js/rhodecode-components.js:33641 #: rhodecode/public/js/scripts.js:25489 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:65 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:73 #: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:156 msgid "in {0}, {1}" msgstr "" #: rhodecode/public/js/rhodecode-components.js:33645 #: rhodecode/public/js/scripts.js:25493 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:76 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:85 #: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:160 msgid "{0} and {1}" msgstr "" #: rhodecode/public/js/rhodecode-components.js:33647 #: rhodecode/public/js/scripts.js:25495 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:77 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:86 #: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:162 msgid "{0} and {1} ago" msgstr "" #: rhodecode/public/js/rhodecode-components.js:33649 #: rhodecode/public/js/scripts.js:25497 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:64 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:72 #: rhodecode/public/js/src/plugins/jquery.timeago-extension.js:164 msgid "in {0} and {1}" msgstr "" #: rhodecode/public/js/rhodecode-components.js:47492 #: rhodecode/public/js/scripts.js:39340 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:14 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:20 #: rhodecode/public/js/rhodecode/i18n/select2/translations.js:4 msgid "Loading more results..." msgstr "" #: rhodecode/public/js/rhodecode-components.js:47495 #: rhodecode/public/js/scripts.js:39343 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:36 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:43 #: rhodecode/public/js/rhodecode/i18n/select2/translations.js:7 msgid "Searching..." msgstr "" #: rhodecode/public/js/rhodecode-components.js:47498 #: rhodecode/public/js/scripts.js:39346 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:18 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:24 #: rhodecode/public/js/rhodecode/i18n/select2/translations.js:10 msgid "No matches found" msgstr "" #: rhodecode/public/js/rhodecode-components.js:47501 #: rhodecode/public/js/scripts.js:39349 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:13 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:19 #: rhodecode/public/js/rhodecode/i18n/select2/translations.js:13 msgid "Loading failed" msgstr "" #: rhodecode/public/js/rhodecode-components.js:47505 #: rhodecode/public/js/scripts.js:39353 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:28 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:34 #: rhodecode/public/js/rhodecode/i18n/select2/translations.js:17 msgid "One result is available, press enter to select it." msgstr "" #: rhodecode/public/js/rhodecode-components.js:47507 #: rhodecode/public/js/scripts.js:39355 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:86 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:95 #: 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/rhodecode-components.js:47512 #: rhodecode/public/js/scripts.js:39360 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:33 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:39 #: rhodecode/public/js/rhodecode/i18n/select2/translations.js:24 msgid "Please enter {0} or more character" msgstr "" #: rhodecode/public/js/rhodecode-components.js:47514 #: rhodecode/public/js/scripts.js:39362 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:34 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:40 #: rhodecode/public/js/rhodecode/i18n/select2/translations.js:26 msgid "Please enter {0} or more characters" msgstr "" #: rhodecode/public/js/rhodecode-components.js:47519 #: rhodecode/public/js/scripts.js:39367 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:31 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:37 #: rhodecode/public/js/rhodecode/i18n/select2/translations.js:31 msgid "Please delete {0} character" msgstr "" #: rhodecode/public/js/rhodecode-components.js:47521 #: rhodecode/public/js/scripts.js:39369 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:32 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:38 #: rhodecode/public/js/rhodecode/i18n/select2/translations.js:33 msgid "Please delete {0} characters" msgstr "" #: rhodecode/public/js/rhodecode-components.js:47525 #: rhodecode/public/js/scripts.js:39373 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:56 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:64 #: rhodecode/public/js/rhodecode/i18n/select2/translations.js:37 msgid "You can only select {0} item" msgstr "" #: rhodecode/public/js/rhodecode-components.js:47527 #: rhodecode/public/js/scripts.js:39375 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:57 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:65 #: rhodecode/public/js/rhodecode/i18n/select2/translations.js:39 msgid "You can only select {0} items" msgstr "" #: rhodecode/public/js/rhodecode-components.js:48456 #: rhodecode/public/js/scripts.js:40304 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:69 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:78 #: rhodecode/public/js/src/rhodecode/changelog.js:35 msgid "showing {0} out of {1} commit" msgstr "" #: rhodecode/public/js/rhodecode-components.js:48458 #: rhodecode/public/js/scripts.js:40306 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:70 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:79 #: rhodecode/public/js/src/rhodecode/changelog.js:37 msgid "showing {0} out of {1} commits" msgstr "" #: rhodecode/public/js/rhodecode-components.js:48891 #: rhodecode/public/js/scripts.js:40739 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:39 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:46 #: rhodecode/public/js/src/rhodecode/codemirror.js:296 msgid "Set status to Approved" msgstr "" #: rhodecode/public/js/rhodecode-components.js:48910 #: rhodecode/public/js/scripts.js:40758 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:40 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:47 #: rhodecode/public/js/src/rhodecode/codemirror.js:315 msgid "Set status to Rejected" msgstr "" #: rhodecode/public/js/rhodecode-components.js:48929 #: rhodecode/public/js/scripts.js:40777 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:51 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:58 #: rhodecode/public/js/src/rhodecode/codemirror.js:334 #: rhodecode/templates/email_templates/commit_comment.mako:99 #: rhodecode/templates/email_templates/pull_request_comment.mako:107 @@ -4086,42 +4126,42 @@ msgstr "" #: rhodecode/public/js/rhodecode-components.js:48949 #: rhodecode/public/js/scripts.js:40797 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:27 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:33 #: rhodecode/public/js/src/rhodecode/codemirror.js:354 msgid "Note Comment" msgstr "" #: rhodecode/public/js/rhodecode-components.js:49315 #: rhodecode/public/js/scripts.js:41163 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:68 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:77 #: rhodecode/public/js/src/rhodecode/comments.js:125 msgid "resolve comment" msgstr "" #: rhodecode/public/js/rhodecode-components.js:49399 #: rhodecode/public/js/scripts.js:41247 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:46 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:53 #: rhodecode/public/js/src/rhodecode/comments.js:209 msgid "Status Review" msgstr "" #: rhodecode/public/js/rhodecode-components.js:49414 #: rhodecode/public/js/scripts.js:41262 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:5 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:10 #: rhodecode/public/js/src/rhodecode/comments.js:224 msgid "Comment text will be set automatically based on currently selected status ({0}) ..." msgstr "" #: rhodecode/public/js/rhodecode-components.js:49571 #: rhodecode/public/js/scripts.js:41419 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:48 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:55 #: rhodecode/public/js/src/rhodecode/comments.js:381 msgid "Submitting..." msgstr "" #: rhodecode/public/js/rhodecode-components.js:49622 #: rhodecode/public/js/scripts.js:41470 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:12 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:18 #: rhodecode/public/js/src/rhodecode/comments.js:432 #: rhodecode/templates/files/files_browser_tree.mako:51 msgid "Loading ..." @@ -4129,105 +4169,170 @@ msgstr "" #: rhodecode/public/js/rhodecode-components.js:49727 #: rhodecode/public/js/scripts.js:41575 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:6 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:12 #: rhodecode/public/js/src/rhodecode/comments.js:537 msgid "Delete this comment?" msgstr "" #: rhodecode/public/js/rhodecode-components.js:49798 #: rhodecode/public/js/scripts.js:41646 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:11 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:17 #: rhodecode/public/js/src/rhodecode/comments.js:608 msgid "Leave a comment, or click resolve button to resolve TODO comment #{0}" msgstr "" #: rhodecode/public/js/rhodecode-components.js:49875 #: rhodecode/public/js/scripts.js:41723 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:10 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:16 #: rhodecode/public/js/src/rhodecode/comments.js:685 msgid "Leave a comment on line {0}." msgstr "" #: rhodecode/public/js/rhodecode-components.js:49989 #: rhodecode/public/js/scripts.js:41837 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:52 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:59 #: rhodecode/public/js/src/rhodecode/comments.js:799 msgid "TODO from comment {0} was fixed." msgstr "" #: rhodecode/public/js/rhodecode-components.js:50195 #: rhodecode/public/js/scripts.js:42043 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:72 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:81 #: rhodecode/public/js/src/rhodecode/files.js:150 msgid "truncated result" msgstr "" #: rhodecode/public/js/rhodecode-components.js:50197 #: rhodecode/public/js/scripts.js:42045 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:73 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:82 #: rhodecode/public/js/src/rhodecode/files.js:152 msgid "truncated results" msgstr "" #: rhodecode/public/js/rhodecode-components.js:50206 #: rhodecode/public/js/scripts.js:42054 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:19 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:25 #: rhodecode/public/js/src/rhodecode/files.js:161 msgid "No matching files" msgstr "" #: rhodecode/public/js/rhodecode-components.js:50341 #: rhodecode/public/js/scripts.js:42189 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:37 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:44 #: rhodecode/public/js/src/rhodecode/files.js:296 msgid "Selection link" msgstr "" #: rhodecode/public/js/rhodecode-components.js:50381 #: rhodecode/public/js/scripts.js:42229 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:47 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:54 #: rhodecode/public/js/src/rhodecode/followers.js:26 msgid "Stop following this repository" msgstr "" #: rhodecode/public/js/rhodecode-components.js:50382 #: rhodecode/public/js/scripts.js:42230 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:54 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:61 #: rhodecode/public/js/src/rhodecode/followers.js:27 msgid "Unfollow" msgstr "" #: rhodecode/public/js/rhodecode-components.js:50391 #: rhodecode/public/js/scripts.js:42239 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:45 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:52 #: rhodecode/public/js/src/rhodecode/followers.js:36 msgid "Start following this repository" msgstr "" #: rhodecode/public/js/rhodecode-components.js:50392 #: rhodecode/public/js/scripts.js:42240 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:8 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:14 #: rhodecode/public/js/src/rhodecode/followers.js:37 msgid "Follow" msgstr "" -#: rhodecode/public/js/rhodecode-components.js:50849 -#: rhodecode/public/js/scripts.js:42697 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:58 -#: rhodecode/public/js/src/rhodecode/pullrequests.js:213 +#: rhodecode/public/js/rhodecode-components.js:50770 +#: rhodecode/public/js/rhodecode-components.js:50779 +#: rhodecode/public/js/scripts.js:42618 rhodecode/public/js/scripts.js:42627 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:5 +#: rhodecode/public/js/src/rhodecode/pullrequests.js:134 +#: rhodecode/public/js/src/rhodecode/pullrequests.js:143 +msgid "All reviewers must vote." +msgstr "" + +#: rhodecode/public/js/rhodecode-components.js:50784 +#: rhodecode/public/js/scripts.js:42632 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:6 +#: rhodecode/public/js/src/rhodecode/pullrequests.js:148 +msgid "At least {0} reviewer must vote." +msgstr "" + +#: rhodecode/public/js/rhodecode-components.js:50790 +#: rhodecode/public/js/scripts.js:42638 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:7 +#: rhodecode/public/js/src/rhodecode/pullrequests.js:154 +msgid "At least {0} reviewers must vote." +msgstr "" + +#: rhodecode/public/js/rhodecode-components.js:50797 +#: rhodecode/public/js/scripts.js:42645 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:41 +#: rhodecode/public/js/src/rhodecode/pullrequests.js:161 +msgid "Reviewers picked from source code changes." +msgstr "" + +#: rhodecode/public/js/rhodecode-components.js:50804 +#: rhodecode/public/js/scripts.js:42652 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:4 +#: rhodecode/public/js/src/rhodecode/pullrequests.js:168 +msgid "Adding new reviewers is forbidden." +msgstr "" + +#: rhodecode/public/js/rhodecode-components.js:50811 +#: rhodecode/public/js/scripts.js:42659 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:8 +#: rhodecode/public/js/src/rhodecode/pullrequests.js:175 +msgid "Author is not allowed to be a reviewer." +msgstr "" + +#: rhodecode/public/js/rhodecode-components.js:50825 +#: rhodecode/public/js/scripts.js:42673 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:11 +#: rhodecode/public/js/src/rhodecode/pullrequests.js:189 +msgid "Commit Authors are not allowed to be a reviewer." +msgstr "" + +#: rhodecode/public/js/rhodecode-components.js:50972 +#: rhodecode/public/js/scripts.js:42820 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:63 +#: rhodecode/public/js/src/rhodecode/pullrequests.js:336 +msgid "User `{0}` not allowed to be a reviewer" +msgstr "" + +#: rhodecode/public/js/rhodecode-components.js:51099 +#: rhodecode/public/js/scripts.js:42947 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:66 +#: rhodecode/public/js/src/rhodecode/pullrequests.js:463 msgid "added manually by \"{0}\"" msgstr "" -#: rhodecode/public/js/rhodecode-components.js:51420 -#: rhodecode/public/js/scripts.js:43268 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:61 +#: rhodecode/public/js/rhodecode-components.js:51101 +#: rhodecode/public/js/scripts.js:42949 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:76 +#: rhodecode/public/js/src/rhodecode/pullrequests.js:465 +msgid "member of \"{0}\"" +msgstr "" + +#: rhodecode/public/js/rhodecode-components.js:51682 +#: rhodecode/public/js/scripts.js:43530 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:69 #: rhodecode/public/js/src/rhodecode.js:142 msgid "file" msgstr "" -#: rhodecode/public/js/rhodecode-components.js:51440 -#: rhodecode/public/js/scripts.js:43288 -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:42 +#: rhodecode/public/js/rhodecode-components.js:51702 +#: rhodecode/public/js/scripts.js:43550 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:49 #: rhodecode/public/js/src/rhodecode.js:162 msgid "Show more" msgstr "" @@ -4242,150 +4347,150 @@ msgstr "" msgid "Add another comment" msgstr "" -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:4 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:9 #: rhodecode/public/js/src/i18n_messages.js:5 #: rhodecode/templates/pullrequests/pullrequest_show.mako:325 msgid "Close" msgstr "" -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:7 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:13 msgid "Diff to Commit " msgstr "" -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:9 -#: rhodecode/public/js/src/i18n_messages.js:4 -msgid "Invite reviewers to this discussion" -msgstr "" - #: rhodecode/public/js/rhodecode/i18n/js_translations.js:15 -msgid "No bookmarks available yet." -msgstr "" - -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:16 -msgid "No branches available yet." -msgstr "" - -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:17 -msgid "No gists available yet." -msgstr "" - -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:20 -msgid "No pull requests available yet." +#: rhodecode/public/js/src/i18n_messages.js:4 +msgid "Invite reviewers to this discussion" msgstr "" #: rhodecode/public/js/rhodecode/i18n/js_translations.js:21 -msgid "No repositories available yet." +msgid "No bookmarks available yet." msgstr "" #: rhodecode/public/js/rhodecode/i18n/js_translations.js:22 -msgid "No repository groups available yet." -msgstr "" - -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:24 -msgid "No tags available yet." -msgstr "" - -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:25 -msgid "No user groups available yet." +msgid "No branches available yet." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:23 +msgid "No gists available yet." msgstr "" #: rhodecode/public/js/rhodecode/i18n/js_translations.js:26 -msgid "No users available yet." -msgstr "" - -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:29 -#: rhodecode/templates/changelog/changelog.mako:61 -msgid "Open new pull request" +msgid "No pull requests available yet." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:27 +msgid "No repositories available yet." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:28 +msgid "No repository groups available yet." msgstr "" #: rhodecode/public/js/rhodecode/i18n/js_translations.js:30 -msgid "Open new pull request for selected commit" +msgid "No tags available yet." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:31 +msgid "No user groups available yet." +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:32 +msgid "No users available yet." msgstr "" #: rhodecode/public/js/rhodecode/i18n/js_translations.js:35 +#: rhodecode/templates/changelog/changelog.mako:61 +msgid "Open new pull request" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:36 +msgid "Open new pull request for selected commit" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:42 msgid "Saving..." msgstr "" -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:38 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:45 #: rhodecode/public/js/src/i18n_messages.js:6 #: rhodecode/templates/admin/settings/settings_email.mako:48 msgid "Send" msgstr "" -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:41 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:48 msgid "Show at Commit " msgstr "" -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:43 -msgid "Show selected commit __S" -msgstr "" - -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:44 -msgid "Show selected commits __S ... __E" -msgstr "" - -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:49 -#: rhodecode/public/js/src/i18n_messages.js:7 -msgid "Switch to chat" -msgstr "" - #: rhodecode/public/js/rhodecode/i18n/js_translations.js:50 +msgid "Show selected commit __S" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:51 +msgid "Show selected commits __S ... __E" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:56 +#: rhodecode/public/js/src/i18n_messages.js:7 +msgid "Switch to chat" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:57 #: rhodecode/public/js/src/i18n_messages.js:8 msgid "Switch to comment" msgstr "" -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:53 -msgid "There are currently no open pull requests requiring your participation." -msgstr "" - -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:55 -msgid "Updating..." -msgstr "" - -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:59 -#: rhodecode/templates/admin/auth/auth_settings.mako:71 -msgid "disabled" -msgstr "" - #: rhodecode/public/js/rhodecode/i18n/js_translations.js:60 -#: rhodecode/templates/admin/auth/auth_settings.mako:71 -msgid "enabled" +msgid "There are currently no open pull requests requiring your participation." msgstr "" #: rhodecode/public/js/rhodecode/i18n/js_translations.js:62 -msgid "files" +msgid "Updating..." msgstr "" #: rhodecode/public/js/rhodecode/i18n/js_translations.js:67 -#: rhodecode/templates/pullrequests/pullrequest.mako:108 +#: rhodecode/templates/admin/auth/auth_settings.mako:71 +msgid "disabled" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:68 +#: rhodecode/templates/admin/auth/auth_settings.mako:71 +msgid "enabled" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:70 +msgid "files" +msgstr "" + +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:75 +#: rhodecode/templates/pullrequests/pullrequest.mako:140 msgid "loading..." msgstr "" -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:71 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:80 msgid "specify commit" msgstr "" -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:74 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:83 msgid "{0} active out of {1} users" msgstr "" -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:79 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:88 msgid "{0} days" msgstr "" -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:81 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:90 msgid "{0} hours" msgstr "" -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:84 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:93 msgid "{0} months" msgstr "" -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:85 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:94 msgid "{0} out of {1} users" msgstr "" -#: rhodecode/public/js/rhodecode/i18n/js_translations.js:89 +#: rhodecode/public/js/rhodecode/i18n/js_translations.js:98 msgid "{0} years" msgstr "" @@ -4467,7 +4572,7 @@ msgstr "" #: rhodecode/templates/admin/my_account/my_account_watched.mako:31 #: rhodecode/templates/admin/repo_groups/repo_groups.mako:53 #: rhodecode/templates/admin/repos/repo_add_base.mako:9 -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:15 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:16 #: rhodecode/templates/admin/repos/repos.mako:54 #: rhodecode/templates/admin/user_groups/user_groups.mako:55 #: rhodecode/templates/admin/users/user_edit_groups.mako:54 @@ -4475,7 +4580,7 @@ msgstr "" #: rhodecode/templates/bookmarks/bookmarks.mako:59 #: rhodecode/templates/branches/branches.mako:58 #: rhodecode/templates/files/files_browser_tree.mako:5 -#: rhodecode/templates/pullrequests/pullrequests.mako:100 +#: rhodecode/templates/pullrequests/pullrequests.mako:110 #: rhodecode/templates/tags/tags.mako:59 msgid "Name" msgstr "" @@ -4490,7 +4595,7 @@ msgstr "" #: rhodecode/templates/admin/repo_groups/repo_groups.mako:56 #: rhodecode/templates/admin/repos/repo_add_base.mako:43 #: rhodecode/templates/admin/repos/repo_edit_issuetracker.mako:29 -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:98 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:127 #: rhodecode/templates/admin/repos/repos.mako:57 #: rhodecode/templates/admin/user_groups/user_group_add.mako:43 #: rhodecode/templates/admin/user_groups/user_group_edit_settings.mako:42 @@ -4498,9 +4603,10 @@ msgstr "" #: rhodecode/templates/admin/users/user_edit_auth_tokens.mako:15 #: rhodecode/templates/admin/users/user_edit_auth_tokens.mako:67 #: rhodecode/templates/admin/users/user_edit_groups.mako:59 +#: rhodecode/templates/admin/users/user_edit_ips.mako:12 #: rhodecode/templates/base/issue_tracker_settings.mako:10 -#: rhodecode/templates/changeset/changeset.mako:53 -#: rhodecode/templates/compare/compare_commits.mako:20 +#: rhodecode/templates/changeset/changeset.mako:73 +#: rhodecode/templates/compare/compare_commits.mako:21 #: rhodecode/templates/email_templates/commit_comment.mako:89 #: rhodecode/templates/email_templates/pull_request_review.mako:41 #: rhodecode/templates/email_templates/pull_request_review.mako:75 @@ -4508,9 +4614,9 @@ msgstr "" #: rhodecode/templates/files/file_tree_detail.mako:12 #: rhodecode/templates/forks/fork.mako:48 #: rhodecode/templates/forks/forks_data.mako:9 -#: rhodecode/templates/pullrequests/pullrequest.mako:47 +#: rhodecode/templates/pullrequests/pullrequest.mako:54 #: rhodecode/templates/pullrequests/pullrequest_show.mako:163 -#: rhodecode/templates/pullrequests/pullrequest_show.mako:460 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:487 #: rhodecode/templates/summary/components.mako:73 msgid "Description" msgstr "" @@ -4521,7 +4627,7 @@ msgstr "" #: rhodecode/templates/admin/repo_groups/repo_group_edit_settings.mako:24 #: rhodecode/templates/admin/repo_groups/repo_groups.mako:60 #: rhodecode/templates/admin/repos/repo_edit_advanced.mako:5 -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:80 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:109 #: rhodecode/templates/admin/repos/repos.mako:65 #: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:5 #: rhodecode/templates/admin/user_groups/user_group_edit_settings.mako:24 @@ -4544,13 +4650,13 @@ msgstr "" #: rhodecode/templates/bookmarks/bookmarks.mako:66 #: rhodecode/templates/branches/branches.mako:65 #: rhodecode/templates/changelog/changelog.mako:107 -#: rhodecode/templates/changelog/changelog_summary_data.mako:8 #: rhodecode/templates/changeset/changeset.mako:36 -#: rhodecode/templates/compare/compare_commits.mako:18 +#: rhodecode/templates/compare/compare_commits.mako:19 #: rhodecode/templates/email_templates/commit_comment.mako:49 #: rhodecode/templates/email_templates/commit_comment.mako:88 -#: rhodecode/templates/pullrequests/pullrequest_show.mako:458 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:485 #: rhodecode/templates/search/search_commit.mako:6 +#: rhodecode/templates/summary/summary_commits.mako:8 #: rhodecode/templates/tags/tags.mako:66 msgid "Commit" msgstr "" @@ -4565,7 +4671,7 @@ msgid "Home" msgstr "" #: rhodecode/templates/login.mako:5 rhodecode/templates/login.mako:35 -#: rhodecode/templates/login.mako:75 rhodecode/templates/base/base.mako:329 +#: rhodecode/templates/login.mako:75 rhodecode/templates/base/base.mako:332 #: rhodecode/templates/debug_style/login.html:60 msgid "Sign In" msgstr "" @@ -4588,13 +4694,13 @@ msgstr "" #: rhodecode/templates/login.mako:68 rhodecode/templates/password_reset.mako:37 #: rhodecode/templates/base/base.mako:46 -#: rhodecode/templates/errors/error_document.mako:64 +#: rhodecode/templates/errors/error_document.mako:63 msgid "Support" msgstr "" #: rhodecode/templates/login.mako:69 rhodecode/templates/password_reset.mako:38 #: rhodecode/templates/files/files_add.mako:54 -#: rhodecode/templates/files/files_add.mako:65 +#: rhodecode/templates/files/files_add.mako:71 msgid "or" msgstr "" @@ -4653,7 +4759,7 @@ msgstr "" #: rhodecode/templates/admin/my_account/my_account_profile_edit.mako:79 #: rhodecode/templates/admin/users/user_add.mako:68 #: rhodecode/templates/admin/users/user_edit_profile.mako:47 -#: rhodecode/templates/admin/users/users.mako:66 +#: rhodecode/templates/admin/users/users.mako:67 msgid "First Name" msgstr "" @@ -4663,7 +4769,7 @@ msgstr "" #: rhodecode/templates/admin/my_account/my_account_profile_edit.mako:88 #: rhodecode/templates/admin/users/user_add.mako:77 #: rhodecode/templates/admin/users/user_edit_profile.mako:56 -#: rhodecode/templates/admin/users/users.mako:68 +#: rhodecode/templates/admin/users/users.mako:69 msgid "Last Name" msgstr "" @@ -4675,36 +4781,32 @@ msgstr "" msgid "Create Account" msgstr "" -#: rhodecode/templates/admin/admin.mako:5 -#: rhodecode/templates/admin/admin.mako:15 +#: rhodecode/templates/admin/admin_audit_logs.mako:5 #: rhodecode/templates/base/base.mako:75 -msgid "Admin journal" -msgstr "" - -#: rhodecode/templates/admin/admin.mako:13 -msgid "journal filter..." -msgstr "" - -#: rhodecode/templates/admin/admin.mako:14 +msgid "Admin audit logs" +msgstr "" + +#: rhodecode/templates/admin/admin_audit_logs.mako:13 +msgid "filter..." +msgstr "" + +#: rhodecode/templates/admin/admin_audit_logs.mako:14 #: rhodecode/templates/admin/users/user_edit_audit.mako:15 msgid "filter" msgstr "" -#: rhodecode/templates/admin/admin.mako:15 -#: rhodecode/templates/journal/journal.mako:14 -#, python-format -msgid "%s entry" -msgid_plural "%s entries" -msgstr[0] "" -msgstr[1] "" - -#: rhodecode/templates/admin/admin.mako:17 +#: rhodecode/templates/admin/admin_audit_logs.mako:15 +msgid "Audit logs" +msgstr "" + +#: rhodecode/templates/admin/admin_audit_logs.mako:17 #: rhodecode/templates/admin/users/user_edit_audit.mako:17 #: rhodecode/templates/journal/journal.mako:17 +#: rhodecode/templates/search/search.mako:76 msgid "Example Queries" msgstr "" -#: rhodecode/templates/admin/admin_log.mako:8 +#: rhodecode/templates/admin/admin_log_base.mako:7 #: rhodecode/templates/admin/my_account/my_account_auth_tokens.mako:18 #: rhodecode/templates/admin/my_account/my_account_repos.mako:37 #: rhodecode/templates/admin/repo_groups/repo_groups.mako:62 @@ -4712,38 +4814,45 @@ msgstr "" #: rhodecode/templates/admin/repos/repos.mako:69 #: rhodecode/templates/admin/user_groups/user_group_edit_settings.mako:71 #: rhodecode/templates/admin/user_groups/user_groups.mako:68 -#: rhodecode/templates/admin/users/user_edit_audit.mako:23 #: rhodecode/templates/admin/users/user_edit_auth_tokens.mako:18 #: rhodecode/templates/admin/users/user_edit_groups.mako:73 -#: rhodecode/templates/admin/users/users.mako:79 +#: rhodecode/templates/admin/users/users.mako:80 #: rhodecode/templates/files/files_detail.mako:58 msgid "Action" msgstr "" -#: rhodecode/templates/admin/admin_log.mako:9 +#: rhodecode/templates/admin/admin_log_base.mako:8 +msgid "Action Data" +msgstr "" + +#: rhodecode/templates/admin/admin_log_base.mako:9 #: rhodecode/templates/admin/defaults/defaults.mako:31 #: rhodecode/templates/admin/permissions/permissions_objects.mako:13 -#: rhodecode/templates/admin/users/user_edit_audit.mako:24 #: rhodecode/templates/search/search_commit.mako:5 #: rhodecode/templates/search/search_path.mako:3 msgid "Repository" msgstr "" -#: rhodecode/templates/admin/admin_log.mako:10 -#: rhodecode/templates/admin/users/user_edit_audit.mako:25 +#: rhodecode/templates/admin/admin_log_base.mako:10 #: rhodecode/templates/bookmarks/bookmarks.mako:61 #: rhodecode/templates/branches/branches.mako:60 #: rhodecode/templates/tags/tags.mako:61 msgid "Date" msgstr "" -#: rhodecode/templates/admin/admin_log.mako:11 -#: rhodecode/templates/admin/users/user_edit_audit.mako:26 -msgid "From IP" -msgstr "" - -#: rhodecode/templates/admin/admin_log.mako:46 -#: rhodecode/templates/admin/users/user_edit_audit.mako:61 +#: rhodecode/templates/admin/admin_log_base.mako:11 +msgid "IP" +msgstr "" + +#: rhodecode/templates/admin/admin_log_base.mako:38 +msgid "toggle" +msgstr "" + +#: rhodecode/templates/admin/admin_log_base.mako:43 +msgid "data not available for v1 entries type" +msgstr "" + +#: rhodecode/templates/admin/admin_log_base.mako:64 msgid "No actions yet" msgstr "" @@ -4784,8 +4893,8 @@ msgstr "" #: rhodecode/templates/admin/repo_groups/repo_group_edit_settings.mako:67 #: rhodecode/templates/admin/repos/repo_add_base.mako:101 #: rhodecode/templates/admin/repos/repo_edit_issuetracker.mako:79 -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:110 -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:160 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:109 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:194 #: rhodecode/templates/admin/settings/settings_hooks.mako:63 #: rhodecode/templates/admin/settings/settings_issuetracker.mako:15 #: rhodecode/templates/admin/user_groups/user_group_add.mako:60 @@ -4826,7 +4935,7 @@ msgstr "" #: rhodecode/templates/admin/defaults/defaults_repositories.mako:27 #: rhodecode/templates/admin/repos/repo_add_base.mako:97 -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:112 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:143 #: rhodecode/templates/forks/fork.mako:87 msgid "Private repositories are only visible to people explicitly added as collaborators." msgstr "" @@ -4877,7 +4986,7 @@ msgstr "" #: rhodecode/templates/admin/gists/edit.mako:56 #: rhodecode/templates/admin/gists/new.mako:50 -#: rhodecode/templates/files/files_add.mako:74 +#: rhodecode/templates/files/files_add.mako:80 #: rhodecode/templates/files/files_edit.mako:78 msgid "plain" msgstr "" @@ -4888,9 +4997,9 @@ msgstr "" #: rhodecode/templates/admin/gists/edit.mako:102 #: rhodecode/templates/base/issue_tracker_settings.mako:73 -#: rhodecode/templates/changeset/changeset_file_comment.mako:385 +#: rhodecode/templates/changeset/changeset_file_comment.mako:390 #: rhodecode/templates/codeblocks/diffs.mako:76 -#: rhodecode/templates/files/files_add.mako:102 +#: rhodecode/templates/files/files_add.mako:108 #: rhodecode/templates/files/files_delete.mako:69 #: rhodecode/templates/files/files_edit.mako:105 #: rhodecode/templates/pullrequests/pullrequest_show.mako:64 @@ -4957,14 +5066,13 @@ msgstr "" #: rhodecode/templates/bookmarks/bookmarks.mako:63 #: rhodecode/templates/branches/branches.mako:62 #: rhodecode/templates/changelog/changelog.mako:113 -#: rhodecode/templates/changelog/changelog_summary_data.mako:11 -#: rhodecode/templates/changeset/changeset.mako:180 -#: rhodecode/templates/compare/compare_commits.mako:17 +#: rhodecode/templates/changeset/changeset.mako:200 +#: rhodecode/templates/compare/compare_commits.mako:18 #: rhodecode/templates/files/files_browser_tree.mako:9 -#: rhodecode/templates/pullrequests/pullrequest_show.mako:309 -#: rhodecode/templates/pullrequests/pullrequest_show.mako:457 -#: rhodecode/templates/pullrequests/pullrequests.mako:102 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:484 +#: rhodecode/templates/pullrequests/pullrequests.mako:112 #: rhodecode/templates/search/search_commit.mako:16 +#: rhodecode/templates/summary/summary_commits.mako:11 #: rhodecode/templates/tags/tags.mako:63 msgid "Author" msgstr "" @@ -5025,12 +5133,12 @@ msgstr "" #: rhodecode/templates/data_table/_dt_elements.mako:193 #: rhodecode/templates/data_table/_dt_elements.mako:206 #: rhodecode/templates/debug_style/buttons.html:128 -#: rhodecode/templates/files/files_add.mako:204 +#: rhodecode/templates/files/files_add.mako:208 #: rhodecode/templates/files/files_edit.mako:165 #: rhodecode/templates/files/files_source.mako:48 #: rhodecode/templates/files/files_source.mako:51 #: rhodecode/templates/pullrequests/pullrequest_show.mako:63 -#: rhodecode/templates/pullrequests/pullrequest_show.mako:324 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:339 #: rhodecode/templates/users/user_profile.mako:7 msgid "Edit" msgstr "" @@ -5071,10 +5179,10 @@ msgstr "" #: rhodecode/templates/admin/integrations/new.mako:21 #: rhodecode/templates/admin/repo_groups/repo_group_edit.mako:48 #: rhodecode/templates/admin/repos/repo_edit.mako:15 -#: rhodecode/templates/admin/repos/repo_edit.mako:43 +#: rhodecode/templates/admin/repos/repo_edit.mako:46 #: rhodecode/templates/admin/settings/settings.mako:14 #: rhodecode/templates/admin/user_groups/user_group_edit.mako:33 -#: rhodecode/templates/base/base.mako:84 rhodecode/templates/base/base.mako:249 +#: rhodecode/templates/base/base.mako:84 rhodecode/templates/base/base.mako:251 msgid "Settings" msgstr "" @@ -5177,7 +5285,7 @@ msgid "No description available" msgstr "" #: rhodecode/templates/admin/my_account/my_account.mako:5 -#: rhodecode/templates/base/base.mako:343 +#: rhodecode/templates/base/base.mako:346 msgid "My account" msgstr "" @@ -5201,7 +5309,7 @@ msgid "OAuth Identities" msgstr "" #: rhodecode/templates/admin/my_account/my_account.mako:37 -#: rhodecode/templates/admin/users/user_edit.mako:38 +#: rhodecode/templates/admin/users/user_edit.mako:43 msgid "Emails" msgstr "" @@ -5218,7 +5326,7 @@ msgstr "" #: rhodecode/templates/admin/my_account/my_account.mako:41 #: rhodecode/templates/admin/permissions/permissions.mako:14 #: rhodecode/templates/admin/repo_groups/repo_group_edit.mako:49 -#: rhodecode/templates/admin/repos/repo_edit.mako:46 +#: rhodecode/templates/admin/repos/repo_edit.mako:49 #: rhodecode/templates/admin/user_groups/user_group_edit.mako:34 #: rhodecode/templates/base/base.mako:80 msgid "Permissions" @@ -5275,7 +5383,7 @@ msgstr "" #: rhodecode/templates/admin/repos/repo_edit_fields.mako:65 #: rhodecode/templates/admin/users/user_edit_auth_tokens.mako:82 #: rhodecode/templates/admin/users/user_edit_emails.mako:62 -#: rhodecode/templates/admin/users/user_edit_ips.mako:69 +#: rhodecode/templates/admin/users/user_edit_ips.mako:70 msgid "Add" msgstr "" @@ -5294,9 +5402,7 @@ msgid "Primary" msgstr "" #: rhodecode/templates/admin/my_account/my_account_emails.mako:31 -#: rhodecode/templates/admin/users/user_edit_emails.mako:30 -#, python-format -msgid "Confirm to delete this email: %s" +msgid "Confirm to delete this email: {}" msgstr "" #: rhodecode/templates/admin/my_account/my_account_emails.mako:42 @@ -5380,13 +5486,13 @@ msgstr "" #: rhodecode/templates/admin/settings/settings_global.mako:9 #: rhodecode/templates/email_templates/pull_request_review.mako:39 #: rhodecode/templates/email_templates/pull_request_review.mako:72 -#: rhodecode/templates/pullrequests/pullrequest.mako:38 -#: rhodecode/templates/pullrequests/pullrequests.mako:104 +#: rhodecode/templates/pullrequests/pullrequest.mako:45 +#: rhodecode/templates/pullrequests/pullrequests.mako:114 msgid "Title" msgstr "" #: rhodecode/templates/admin/my_account/my_account_pullrequests.mako:47 -#: rhodecode/templates/pullrequests/pullrequests.mako:108 +#: rhodecode/templates/pullrequests/pullrequests.mako:118 msgid "Last Update" msgstr "" @@ -5404,7 +5510,7 @@ msgid "My Notifications" msgstr "" #: rhodecode/templates/admin/notifications/notifications.mako:32 -#: rhodecode/templates/changeset/changeset.mako:140 +#: rhodecode/templates/changeset/changeset.mako:160 msgid "Comments" msgstr "" @@ -5425,6 +5531,10 @@ msgstr "" msgid "Notifications" msgstr "" +#: rhodecode/templates/admin/notifications/show_notification.mako:40 +msgid "Subject" +msgstr "" + #: rhodecode/templates/admin/permissions/permissions.mako:5 msgid "Permissions Administration" msgstr "" @@ -5483,23 +5593,23 @@ msgid "Default IP Whitelist For All User msgstr "" #: rhodecode/templates/admin/permissions/permissions_ips.mako:27 -#: rhodecode/templates/admin/users/user_edit_ips.mako:35 +#: rhodecode/templates/admin/users/user_edit_ips.mako:36 #, python-format msgid "Confirm to delete this ip: %s" msgstr "" #: rhodecode/templates/admin/permissions/permissions_ips.mako:34 -#: rhodecode/templates/admin/users/user_edit_ips.mako:43 +#: rhodecode/templates/admin/users/user_edit_ips.mako:44 msgid "All IP addresses are allowed" msgstr "" #: rhodecode/templates/admin/permissions/permissions_ips.mako:49 -#: rhodecode/templates/admin/users/user_edit_ips.mako:59 +#: rhodecode/templates/admin/users/user_edit_ips.mako:60 msgid "New IP Address" msgstr "" #: rhodecode/templates/admin/permissions/permissions_ips.mako:53 -#: rhodecode/templates/admin/users/user_edit_ips.mako:62 +#: rhodecode/templates/admin/users/user_edit_ips.mako:63 msgid "Description..." msgstr "" @@ -5583,9 +5693,9 @@ msgid "Add Child Group" msgstr "" #: rhodecode/templates/admin/repo_groups/repo_group_edit.mako:50 -#: rhodecode/templates/admin/repos/repo_edit.mako:49 +#: rhodecode/templates/admin/repos/repo_edit.mako:52 #: rhodecode/templates/admin/user_groups/user_group_edit.mako:35 -#: rhodecode/templates/admin/users/user_edit.mako:35 +#: rhodecode/templates/admin/users/user_edit.mako:40 msgid "Advanced" msgstr "" @@ -5642,26 +5752,26 @@ msgid "Repository Group Permissions" msgstr "" #: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako:15 -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:16 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:15 #: rhodecode/templates/admin/user_groups/user_group_edit_perms.mako:15 msgid "User/User Group" msgstr "" #: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako:31 -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:31 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:30 #: rhodecode/templates/admin/user_groups/user_group_edit_perms.mako:31 msgid "super admin" msgstr "" #: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako:34 -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:34 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:33 #: rhodecode/templates/admin/user_groups/user_group_edit_perms.mako:34 msgid "owner" msgstr "" #: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako:52 #: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako:76 -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:61 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:60 #: rhodecode/templates/admin/user_groups/user_group_edit_perms.mako:52 #: rhodecode/templates/admin/user_groups/user_group_edit_perms.mako:76 msgid "permission for all other users" @@ -5669,8 +5779,8 @@ msgstr "" #: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako:62 #: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako:109 -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:71 -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:99 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:70 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:98 msgid "Revoke" msgstr "" @@ -5680,7 +5790,7 @@ msgid "delegated admin" msgstr "" #: rhodecode/templates/admin/repo_groups/repo_group_edit_perms.mako:118 -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:107 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:106 #: rhodecode/templates/admin/user_groups/user_group_edit_perms.mako:117 #: rhodecode/templates/base/issue_tracker_settings.mako:83 msgid "Add new" @@ -5745,7 +5855,7 @@ msgid "Clone from" msgstr "" #: rhodecode/templates/admin/repos/repo_add_base.mako:47 -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:102 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:132 #: rhodecode/templates/forks/fork.mako:52 msgid "Keep it short and to the point. Use a README file for longer descriptions." msgstr "" @@ -5755,7 +5865,6 @@ msgid "Repository Group" msgstr "" #: rhodecode/templates/admin/repos/repo_add_base.mako:58 -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:62 #: rhodecode/templates/forks/fork.mako:64 #, python-format msgid "Select my personal group (%(repo_group_name)s)" @@ -5775,7 +5884,7 @@ msgid "Set the type of repository to cre msgstr "" #: rhodecode/templates/admin/repos/repo_add_base.mako:84 -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:70 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:98 #: rhodecode/templates/forks/fork.mako:73 msgid "Landing commit" msgstr "" @@ -5803,36 +5912,36 @@ msgstr "" msgid "%s repository settings" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit.mako:55 +#: rhodecode/templates/admin/repos/repo_edit.mako:58 msgid "Extra Fields" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit.mako:61 +#: rhodecode/templates/admin/repos/repo_edit.mako:64 msgid "Caches" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit.mako:65 +#: rhodecode/templates/admin/repos/repo_edit.mako:68 msgid "Remote" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit.mako:69 +#: rhodecode/templates/admin/repos/repo_edit.mako:72 #: rhodecode/templates/summary/components.mako:135 msgid "Statistics" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit.mako:75 +#: rhodecode/templates/admin/repos/repo_edit.mako:79 +msgid "Reviewer Rules" +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit.mako:83 #: rhodecode/templates/admin/repos/repo_edit_maintenance.mako:3 msgid "Maintenance" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit.mako:78 +#: rhodecode/templates/admin/repos/repo_edit.mako:86 msgid "Strip" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit.mako:93 -msgid "Reviewers" -msgstr "" - #: rhodecode/templates/admin/repos/repo_edit_advanced.mako:7 msgid "Updated on" msgstr "" @@ -5867,84 +5976,77 @@ msgstr "" msgid "Public Journal Visibility" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:56 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:55 msgid "Remove from Public Journal" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:60 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:59 msgid "Add to Public Journal" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:65 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:64 msgid "All actions made on this repository will be visible to everyone following the public journal." msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:74 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:73 msgid "Locking state" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:83 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:82 msgid "This Repository is not currently locked." msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:90 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:89 msgid "Confirm to unlock repository." msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:92 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:91 msgid "Unlock repository" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:97 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:96 msgid "Confirm to lock repository." msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:99 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:98 msgid "Lock Repository" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:105 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:104 msgid "Force repository locking. This only works when anonymous access is disabled. Pulling from the repository locks the repository to that user until the same user pushes to that repository again." msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:114 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:113 msgid "Delete repository" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:121 -#, python-format -msgid "This repository has %s fork." -msgid_plural "This repository has %s forks." -msgstr[0] "" -msgstr[1] "" - -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:125 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:124 msgid "Detach forks" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:130 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:129 msgid "Delete forks" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:139 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:138 #: rhodecode/templates/data_table/_dt_elements.mako:124 #, python-format msgid "Confirm to delete this repository: %s" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:141 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:140 msgid "Delete This Repository" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:146 -msgid "This repository will be renamed in a special way in order to make it inaccessible to RhodeCode Enterprise and its VCS systems. If you need to fully delete it from the file system, please do it manually, or with rhodecode-cleanup-repos command." -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:180 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:145 +msgid "This repository will be renamed in a special way in order to make it inaccessible to RhodeCode Enterprise and its VCS systems. If you need to fully delete it from the file system, please do it manually, or with rhodecode-cleanup-repos command available in rhodecode-tools." +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:179 msgid "Change repository" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:180 +#: rhodecode/templates/admin/repos/repo_edit_advanced.mako:179 msgid "Pick repository" msgstr "" @@ -5952,44 +6054,41 @@ msgstr "" msgid "Invalidate Cache for Repository" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_caches.mako:10 -msgid "Invalidate repository cache" +#: rhodecode/templates/admin/repos/repo_edit_caches.mako:7 +msgid "Manually invalidate the repository cache. On the next access a repository cache will be recreated." msgstr "" #: rhodecode/templates/admin/repos/repo_edit_caches.mako:10 +msgid "Cache purge can be automated by such api call. Can be called periodically in crontab etc." +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_caches.mako:20 +msgid "Invalidate repository cache" +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_caches.mako:20 msgid "Confirm to invalidate repository cache" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_caches.mako:14 -msgid "Manually invalidate the repository cache. On the next access a repository cache will be recreated." -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_caches.mako:28 -#, python-format -msgid "List of repository caches (%(count)s entry)" -msgid_plural "List of repository caches (%(count)s entries)" -msgstr[0] "" -msgstr[1] "" - -#: rhodecode/templates/admin/repos/repo_edit_caches.mako:35 +#: rhodecode/templates/admin/repos/repo_edit_caches.mako:39 #: rhodecode/templates/admin/repos/repo_edit_issuetracker.mako:32 #: rhodecode/templates/base/issue_tracker_settings.mako:13 msgid "Prefix" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_caches.mako:36 +#: rhodecode/templates/admin/repos/repo_edit_caches.mako:40 #: rhodecode/templates/admin/repos/repo_edit_fields.mako:11 msgid "Key" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_caches.mako:37 +#: rhodecode/templates/admin/repos/repo_edit_caches.mako:41 #: rhodecode/templates/admin/user_groups/user_group_add.mako:52 #: rhodecode/templates/admin/user_groups/user_group_edit_settings.mako:51 #: rhodecode/templates/admin/user_groups/user_groups.mako:64 #: rhodecode/templates/admin/users/user_add.mako:97 #: rhodecode/templates/admin/users/user_edit_groups.mako:64 #: rhodecode/templates/admin/users/user_edit_profile.mako:90 -#: rhodecode/templates/admin/users/users.mako:73 +#: rhodecode/templates/admin/users/users.mako:74 msgid "Active" msgstr "" @@ -6063,19 +6162,27 @@ msgstr "" msgid "Test Patterns" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_maintenance.mako:9 -msgid "Perform maintenance tasks for this repo, following tasks will be performed" -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_maintenance.mako:16 +#: rhodecode/templates/admin/repos/repo_edit_maintenance.mako:8 +msgid "Perform maintenance tasks for this repo" +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_maintenance.mako:10 +msgid "Following tasks will be performed" +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_maintenance.mako:17 +msgid "Maintenance can be automated by such api call. Can be called periodically in crontab etc." +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_maintenance.mako:25 msgid "No maintenance tasks for this repo available" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_maintenance.mako:26 +#: rhodecode/templates/admin/repos/repo_edit_maintenance.mako:34 msgid "Run Maintenance" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_maintenance.mako:41 +#: rhodecode/templates/admin/repos/repo_edit_maintenance.mako:49 msgid "Performing Maintenance" msgstr "" @@ -6083,11 +6190,11 @@ msgstr "" msgid "Repository Permissions" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:43 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:42 msgid "private repository" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:48 +#: rhodecode/templates/admin/repos/repo_edit_permissions.mako:47 msgid "only users/user groups explicitly added here will have access" msgstr "" @@ -6095,56 +6202,87 @@ msgstr "" msgid "Remote url" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_remote.mako:9 +#: rhodecode/templates/admin/repos/repo_edit_remote.mako:7 +msgid "Manually pull changes from external repository." +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_remote.mako:11 msgid "Remote mirror url" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_remote.mako:12 -msgid "Pull can be automated by such api call called periodically (in crontab etc)" -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_remote.mako:22 -#: rhodecode/templates/admin/repos/repo_edit_remote.mako:30 +#: rhodecode/templates/admin/repos/repo_edit_remote.mako:15 +msgid "Pull can be automated by such api call. Can be called periodically in crontab etc." +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_remote.mako:25 +#: rhodecode/templates/admin/repos/repo_edit_remote.mako:36 msgid "Pull changes from remote location" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_remote.mako:22 +#: rhodecode/templates/admin/repos/repo_edit_remote.mako:25 msgid "Confirm to pull changes from remote side" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_remote.mako:27 +#: rhodecode/templates/admin/repos/repo_edit_remote.mako:31 msgid "This repository does not have any remote mirror url set." msgstr "" +#: rhodecode/templates/admin/repos/repo_edit_remote.mako:32 +msgid "Set remote url." +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_reviewers.mako:3 +msgid "Default Reviewer Rules" +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_reviewers.mako:6 +msgid "This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license." +msgstr "" + #: rhodecode/templates/admin/repos/repo_edit_settings.mako:6 #, python-format msgid "Settings for Repository: %s" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:19 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:22 msgid "Non-changeable id" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:19 -msgid "what is that ?" -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:21 -msgid "URL by id" -msgstr "" - #: rhodecode/templates/admin/repos/repo_edit_settings.mako:22 +msgid "what is that ?" +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:24 +msgid "URL by id" +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:25 msgid "" "In case this repository is renamed or moved into another group the repository url changes.\n" " Using above url guarantees that this repository will always be accessible under such url.\n" " Useful for CI systems, or any other cases that you need to hardcode the url into 3rd party service." msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:30 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:33 +#: rhodecode/templates/data_table/_dt_elements.mako:164 +#: rhodecode/templates/forks/fork.mako:58 +msgid "Repository group" +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:41 +#, python-format +msgid "Select my personal group (`%(repo_group_name)s`)" +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:44 +msgid "Optional select a group to put this repository into." +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:51 msgid "Remote uri" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:36 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:59 #: rhodecode/templates/base/perms_summary.mako:79 #: rhodecode/templates/base/perms_summary.mako:149 #: rhodecode/templates/base/perms_summary.mako:151 @@ -6152,63 +6290,56 @@ msgstr "" msgid "edit" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:39 -msgid "new value, leave empty to remove" -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:41 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:65 +msgid "enter new value, or leave empty to remove" +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:75 msgid "cancel" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:48 -msgid "http[s] url where from repository was imported, also used for doing remote pulls." -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:56 -#: rhodecode/templates/data_table/_dt_elements.mako:164 -#: rhodecode/templates/forks/fork.mako:58 -msgid "Repository group" -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:65 -msgid "Optional select a group to put this repository into." -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:74 -#: rhodecode/templates/forks/fork.mako:77 -msgid "Default commit for files page, downloads, whoosh and readme" -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:92 -msgid "Change owner of this repository." -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:108 -#: rhodecode/templates/data_table/_dt_elements.mako:58 -msgid "Private repository" -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:117 -msgid "Enable statistics" +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:87 +msgid "http[s] url where from repository was imported, this field can used for doing {pull_link}." +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:88 +msgid "This field is stored encrypted inside Database, a format of http://user:password@server.com/repo_name can be used and will be hidden from display." +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:103 +msgid "Default commit for files page, downloads, full text search index and readme" msgstr "" #: rhodecode/templates/admin/repos/repo_edit_settings.mako:121 +msgid "Change owner of this repository." +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:138 +#: rhodecode/templates/data_table/_dt_elements.mako:58 +msgid "Private repository" +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:148 +msgid "Enable statistics" +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:153 msgid "Enable statistics window on summary page." msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:126 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:158 msgid "Enable downloads" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:130 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:163 msgid "Enable download menu on summary page." msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:135 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:168 msgid "Enable automatic locking" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_settings.mako:139 +#: rhodecode/templates/admin/repos/repo_edit_settings.mako:173 msgid "Enable automatic locking on repository. Pulling from this repository creates a lock that can be released by pushing back by the same user" msgstr "" @@ -6272,32 +6403,24 @@ msgstr "" msgid "Remove" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_strip.mako:112 +#: rhodecode/templates/admin/repos/repo_edit_strip.mako:114 msgid "Checking commits" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_strip.mako:127 -msgid "author" -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_strip.mako:127 -msgid "comment" -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_strip.mako:128 +#: rhodecode/templates/admin/repos/repo_edit_strip.mako:142 msgid " commit verified positive" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_strip.mako:131 +#: rhodecode/templates/admin/repos/repo_edit_strip.mako:154 msgid " commit verified negative" msgstr "" -#: rhodecode/templates/admin/repos/repo_edit_strip.mako:153 -msgid " commit striped successful" -msgstr "" - -#: rhodecode/templates/admin/repos/repo_edit_strip.mako:156 -msgid " commit striped failed" +#: rhodecode/templates/admin/repos/repo_edit_strip.mako:179 +msgid " commit striped successfully" +msgstr "" + +#: rhodecode/templates/admin/repos/repo_edit_strip.mako:182 +msgid " commit strip failed" msgstr "" #: rhodecode/templates/admin/repos/repo_edit_vcs.mako:13 @@ -6835,12 +6958,12 @@ msgid "%s user group settings" msgstr "" #: rhodecode/templates/admin/user_groups/user_group_edit.mako:36 -#: rhodecode/templates/admin/users/user_edit.mako:36 +#: rhodecode/templates/admin/users/user_edit.mako:41 msgid "Global permissions" msgstr "" #: rhodecode/templates/admin/user_groups/user_group_edit.mako:37 -#: rhodecode/templates/admin/users/user_edit.mako:37 +#: rhodecode/templates/admin/users/user_edit.mako:42 msgid "Permissions summary" msgstr "" @@ -6877,43 +7000,35 @@ msgid "This group is set to be automatic msgstr "" #: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:37 -msgid "Each member will be added or removed from this groups once they interact with RhodeCode system." -msgstr "" - -#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:38 msgid "This group synchronization was set by" msgstr "" -#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:42 +#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:41 msgid "This group is not set to be automatically synchronised" msgstr "" -#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:51 +#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:50 msgid "Disable synchronization" msgstr "" -#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:53 +#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:52 msgid "Enable synchronization" msgstr "" -#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:60 -msgid "User group will no longer synchronize membership" -msgstr "" - -#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:62 -msgid "User group will start to synchronize membership" -msgstr "" - -#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:75 +#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:58 +msgid "Users will be added or removed from this group when they authenticate with RhodeCode system, based on LDAP group membership. This requires `LDAP+User group` authentication plugin to be configured and enabled. (EE only feature)" +msgstr "" + +#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:71 msgid "Delete User Group" msgstr "" -#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:81 +#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:77 #, python-format msgid "Confirm to delete user group `%(ugroup)s` with all permission assignments" msgstr "" -#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:83 +#: rhodecode/templates/admin/user_groups/user_group_edit_advanced.mako:79 msgid "Delete This User Group" msgstr "" @@ -7005,24 +7120,28 @@ msgstr "" msgid "%s user settings" msgstr "" -#: rhodecode/templates/admin/users/user_edit.mako:33 +#: rhodecode/templates/admin/users/user_edit.mako:19 +msgid "This user is set as disabled" +msgstr "" + +#: rhodecode/templates/admin/users/user_edit.mako:38 #: rhodecode/templates/admin/users/user_edit_profile.mako:5 msgid "User Profile" msgstr "" -#: rhodecode/templates/admin/users/user_edit.mako:34 -msgid "Auth tokens" -msgstr "" - #: rhodecode/templates/admin/users/user_edit.mako:39 +msgid "Auth tokens" +msgstr "" + +#: rhodecode/templates/admin/users/user_edit.mako:44 msgid "Ip Whitelist" msgstr "" -#: rhodecode/templates/admin/users/user_edit.mako:40 +#: rhodecode/templates/admin/users/user_edit.mako:45 msgid "User Groups Management" msgstr "" -#: rhodecode/templates/admin/users/user_edit.mako:41 +#: rhodecode/templates/admin/users/user_edit.mako:46 msgid "User audit" msgstr "" @@ -7036,7 +7155,7 @@ msgid "Last login" msgstr "" #: rhodecode/templates/admin/users/user_edit_advanced.mako:9 -#: rhodecode/templates/admin/users/users.mako:71 +#: rhodecode/templates/admin/users/users.mako:72 msgid "Last activity" msgstr "" @@ -7151,6 +7270,11 @@ msgstr "" msgid "Additional Email Addresses" msgstr "" +#: rhodecode/templates/admin/users/user_edit_emails.mako:30 +#, python-format +msgid "Confirm to delete this email: %s" +msgstr "" + #: rhodecode/templates/admin/users/user_edit_groups.mako:12 #, python-format msgid "Add `%s` to user group" @@ -7160,12 +7284,24 @@ msgstr "" msgid "Custom IP Whitelist" msgstr "" -#: rhodecode/templates/admin/users/user_edit_ips.mako:19 +#: rhodecode/templates/admin/users/user_edit_ips.mako:7 +msgid "Current IP address" +msgstr "" + +#: rhodecode/templates/admin/users/user_edit_ips.mako:10 +msgid "IP Address" +msgstr "" + +#: rhodecode/templates/admin/users/user_edit_ips.mako:11 +msgid "IP Range" +msgstr "" + +#: rhodecode/templates/admin/users/user_edit_ips.mako:20 #, python-format msgid "Inherited from %s" msgstr "" -#: rhodecode/templates/admin/users/user_edit_ips.mako:63 +#: rhodecode/templates/admin/users/user_edit_ips.mako:64 msgid "" "Enter comma separated list of ip addresses like 127.0.0.1,\n" "or use a ip address with a mask 127.0.0.1/24, to create a network range.\n" @@ -7210,7 +7346,7 @@ msgstr "" msgid "Users administration" msgstr "" -#: rhodecode/templates/admin/users/users.mako:77 +#: rhodecode/templates/admin/users/users.mako:78 msgid "Auth type" msgstr "" @@ -7278,110 +7414,110 @@ msgstr "" msgid "Show Pull Requests for %s" msgstr "" -#: rhodecode/templates/base/base.mako:246 +#: rhodecode/templates/base/base.mako:247 msgid "Options" msgstr "" -#: rhodecode/templates/base/base.mako:253 +#: rhodecode/templates/base/base.mako:255 #: rhodecode/templates/forks/forks_data.mako:30 msgid "Compare fork" msgstr "" -#: rhodecode/templates/base/base.mako:256 -#: rhodecode/templates/base/base.mako:403 +#: rhodecode/templates/base/base.mako:258 +#: rhodecode/templates/base/base.mako:406 #: rhodecode/templates/search/search.mako:64 msgid "Search" msgstr "" -#: rhodecode/templates/base/base.mako:260 -msgid "Unlock" -msgstr "" - #: rhodecode/templates/base/base.mako:262 +msgid "Unlock" +msgstr "" + +#: rhodecode/templates/base/base.mako:264 msgid "Lock" msgstr "" -#: rhodecode/templates/base/base.mako:267 +#: rhodecode/templates/base/base.mako:269 #: rhodecode/templates/data_table/_dt_elements.mako:27 #: rhodecode/templates/data_table/_dt_elements.mako:28 #: rhodecode/templates/forks/forks_data.mako:8 -#: rhodecode/templates/summary/components.mako:103 msgid "Fork" -msgid_plural "Forks" -msgstr[0] "" -msgstr[1] "" - -#: rhodecode/templates/base/base.mako:268 +msgstr "" + +#: rhodecode/templates/base/base.mako:270 msgid "Create Pull Request" msgstr "" -#: rhodecode/templates/base/base.mako:290 +#: rhodecode/templates/base/base.mako:292 msgid "Sign in" msgstr "" -#: rhodecode/templates/base/base.mako:298 +#: rhodecode/templates/base/base.mako:300 #: rhodecode/templates/debug_style/login.html:28 msgid "Sign in to your account" msgstr "" -#: rhodecode/templates/base/base.mako:315 +#: rhodecode/templates/base/base.mako:317 #: rhodecode/templates/debug_style/login.html:46 msgid "(Forgot password?)" msgstr "" -#: rhodecode/templates/base/base.mako:325 -#: rhodecode/templates/debug_style/login.html:56 -msgid "Don't have an account ?" -msgstr "" - -#: rhodecode/templates/base/base.mako:345 +#: rhodecode/templates/base/base.mako:327 +msgid "Don't have an account?" +msgstr "" + +#: rhodecode/templates/base/base.mako:329 +msgid "Using external auth? Sign In here." +msgstr "" + +#: rhodecode/templates/base/base.mako:348 msgid "My personal group" msgstr "" -#: rhodecode/templates/base/base.mako:349 +#: rhodecode/templates/base/base.mako:352 msgid "Sign Out" msgstr "" -#: rhodecode/templates/base/base.mako:385 +#: rhodecode/templates/base/base.mako:388 msgid "Show activity journal" msgstr "" -#: rhodecode/templates/base/base.mako:386 +#: rhodecode/templates/base/base.mako:389 #: rhodecode/templates/journal/journal.mako:4 #: rhodecode/templates/journal/journal.mako:14 msgid "Journal" msgstr "" -#: rhodecode/templates/base/base.mako:391 +#: rhodecode/templates/base/base.mako:394 msgid "Show Public activity journal" msgstr "" -#: rhodecode/templates/base/base.mako:392 +#: rhodecode/templates/base/base.mako:395 msgid "Public journal" msgstr "" -#: rhodecode/templates/base/base.mako:397 +#: rhodecode/templates/base/base.mako:400 msgid "Show Gists" msgstr "" -#: rhodecode/templates/base/base.mako:398 +#: rhodecode/templates/base/base.mako:401 msgid "Gists" msgstr "" -#: rhodecode/templates/base/base.mako:402 +#: rhodecode/templates/base/base.mako:405 msgid "Search in repositories you have access to" msgstr "" -#: rhodecode/templates/base/base.mako:408 +#: rhodecode/templates/base/base.mako:411 msgid "Admin settings" msgstr "" -#: rhodecode/templates/base/base.mako:415 +#: rhodecode/templates/base/base.mako:418 msgid "Delegated Admin settings" msgstr "" -#: rhodecode/templates/base/base.mako:425 -#: rhodecode/templates/base/base.mako:426 +#: rhodecode/templates/base/base.mako:428 +#: rhodecode/templates/base/base.mako:429 #: rhodecode/templates/debug_style/alerts.html:5 #: rhodecode/templates/debug_style/buttons.html:5 #: rhodecode/templates/debug_style/code-block.html:6 @@ -7403,15 +7539,15 @@ msgstr "" msgid "Style" msgstr "" -#: rhodecode/templates/base/base.mako:483 +#: rhodecode/templates/base/base.mako:486 msgid "Go to" msgstr "" -#: rhodecode/templates/base/base.mako:536 +#: rhodecode/templates/base/base.mako:539 msgid "Keyboard shortcuts" msgstr "" -#: rhodecode/templates/base/base.mako:544 +#: rhodecode/templates/base/base.mako:547 msgid "Site-wide shortcuts" msgstr "" @@ -7503,10 +7639,10 @@ msgid "Confirm to remove this pattern:" msgstr "" #: rhodecode/templates/base/issue_tracker_settings.mako:191 -#: rhodecode/templates/changeset/changeset_file_comment.mako:269 -#: rhodecode/templates/changeset/changeset_file_comment.mako:319 -#: rhodecode/templates/files/files_add.mako:78 -#: rhodecode/templates/files/files_add.mako:224 +#: rhodecode/templates/changeset/changeset_file_comment.mako:274 +#: rhodecode/templates/changeset/changeset_file_comment.mako:324 +#: rhodecode/templates/files/files_add.mako:84 +#: rhodecode/templates/files/files_add.mako:228 #: rhodecode/templates/files/files_edit.mako:82 #: rhodecode/templates/files/files_edit.mako:185 msgid "Preview" @@ -7586,7 +7722,7 @@ msgstr "" msgid "No permission defined" msgstr "" -#: rhodecode/templates/base/root.mako:150 +#: rhodecode/templates/base/root.mako:155 msgid "Please enable JavaScript to use RhodeCode Enterprise" msgstr "" @@ -7682,95 +7818,107 @@ msgstr "" msgid "Requires hgsubversion library to be installed. Allows cloning remote SVN repositories and migrates them to Mercurial type." msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:139 +#: rhodecode/templates/base/vcs_settings.mako:136 +msgid "Enable evolve extension" +msgstr "" + +#: rhodecode/templates/base/vcs_settings.mako:140 +msgid "Enable evolve extension for all repositories." +msgstr "" + +#: rhodecode/templates/base/vcs_settings.mako:142 +msgid "Enable evolve extension for this repository." +msgstr "" + +#: rhodecode/templates/base/vcs_settings.mako:152 msgid "Mercurial Labs Settings" msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:139 +#: rhodecode/templates/base/vcs_settings.mako:152 msgid "These features are considered experimental and may not work as expected." msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:145 +#: rhodecode/templates/base/vcs_settings.mako:158 msgid "Use rebase as merge strategy" msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:148 +#: rhodecode/templates/base/vcs_settings.mako:161 msgid "Use rebase instead of creating a merge commit when merging via web interface." msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:160 +#: rhodecode/templates/base/vcs_settings.mako:173 msgid "Git Settings" msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:165 +#: rhodecode/templates/base/vcs_settings.mako:178 msgid "Enable lfs extension" msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:169 -msgid "Enable lfs extensions for all repositories." -msgstr "" - -#: rhodecode/templates/base/vcs_settings.mako:171 -msgid "Enable lfs extensions for this repository." -msgstr "" - #: rhodecode/templates/base/vcs_settings.mako:182 +msgid "Enable lfs extensions for all repositories." +msgstr "" + +#: rhodecode/templates/base/vcs_settings.mako:184 +msgid "Enable lfs extensions for this repository." +msgstr "" + +#: rhodecode/templates/base/vcs_settings.mako:195 msgid "Filesystem location where Git lfs objects should be stored." msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:193 +#: rhodecode/templates/base/vcs_settings.mako:206 msgid "Global Subversion Settings" msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:199 +#: rhodecode/templates/base/vcs_settings.mako:212 msgid "Proxy subversion HTTP requests" msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:203 -msgid "Subversion HTTP Support. Enables communication with SVN over HTTP protocol." -msgstr "" - -#: rhodecode/templates/base/vcs_settings.mako:204 -msgid "SVN Protocol setup Documentation" -msgstr "" - -#: rhodecode/templates/base/vcs_settings.mako:210 -msgid "Subversion HTTP Server URL" -msgstr "" - #: rhodecode/templates/base/vcs_settings.mako:216 +msgid "Subversion HTTP Support. Enables communication with SVN over HTTP protocol." +msgstr "" + +#: rhodecode/templates/base/vcs_settings.mako:217 +msgid "SVN Protocol setup Documentation" +msgstr "" + +#: rhodecode/templates/base/vcs_settings.mako:223 +msgid "Subversion HTTP Server URL" +msgstr "" + +#: rhodecode/templates/base/vcs_settings.mako:229 msgid "Generate Apache Config" msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:228 +#: rhodecode/templates/base/vcs_settings.mako:241 msgid "Subversion Settings" msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:233 +#: rhodecode/templates/base/vcs_settings.mako:246 msgid "Repository patterns" msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:237 +#: rhodecode/templates/base/vcs_settings.mako:250 msgid "Patterns for identifying SVN branches and tags. For recursive search, use \"*\". Eg.: \"/branches/*\"" msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:301 +#: rhodecode/templates/base/vcs_settings.mako:314 msgid "Pull Request Settings" msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:306 +#: rhodecode/templates/base/vcs_settings.mako:319 msgid "Enable server-side merge for pull requests" msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:309 +#: rhodecode/templates/base/vcs_settings.mako:322 msgid "Note: when this feature is enabled, it only runs hooks defined in the rcextension package. Custom hooks added on the Admin -> Settings -> Hooks page will not be run when pull requests are automatically merged from the web interface." msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:313 +#: rhodecode/templates/base/vcs_settings.mako:326 msgid "Invalidate and relocate inline comments during update" msgstr "" -#: rhodecode/templates/base/vcs_settings.mako:316 +#: rhodecode/templates/base/vcs_settings.mako:329 msgid "During the update of a pull request, the position of inline comments will be updated and outdated inline comments will be hidden." msgstr "" @@ -7788,10 +7936,10 @@ msgid "Compare Selected Bookmarks" msgstr "" #: rhodecode/templates/bookmarks/bookmarks_data.mako:13 -#: rhodecode/templates/changelog/changelog_elements.mako:91 -#: rhodecode/templates/changelog/changelog_summary_data.mako:62 -#: rhodecode/templates/changeset/changeset.mako:92 +#: rhodecode/templates/changelog/changelog_elements.mako:111 +#: rhodecode/templates/changeset/changeset.mako:112 #: rhodecode/templates/files/base.mako:10 +#: rhodecode/templates/summary/summary_commits.mako:62 #, python-format msgid "Bookmark %s" msgstr "" @@ -7810,10 +7958,10 @@ msgid "Compare Selected Branches" msgstr "" #: rhodecode/templates/branches/branches_data.mako:12 -#: rhodecode/templates/changelog/changelog_elements.mako:83 -#: rhodecode/templates/changelog/changelog_summary_data.mako:76 -#: rhodecode/templates/changeset/changeset.mako:105 +#: rhodecode/templates/changelog/changelog_elements.mako:103 +#: rhodecode/templates/changeset/changeset.mako:125 #: rhodecode/templates/files/base.mako:23 +#: rhodecode/templates/summary/summary_commits.mako:76 #, python-format msgid "Branch %s" msgstr "" @@ -7851,19 +7999,19 @@ msgstr[0] "" msgstr[1] "" #: rhodecode/templates/changelog/changelog.mako:110 -#: rhodecode/templates/files/files_add.mako:93 +#: rhodecode/templates/files/files_add.mako:99 #: rhodecode/templates/files/files_delete.mako:60 #: rhodecode/templates/files/files_edit.mako:96 msgid "Commit Message" msgstr "" #: rhodecode/templates/changelog/changelog.mako:112 -#: rhodecode/templates/changelog/changelog_summary_data.mako:10 +#: rhodecode/templates/summary/summary_commits.mako:10 msgid "Age" msgstr "" #: rhodecode/templates/changelog/changelog.mako:115 -#: rhodecode/templates/changelog/changelog_summary_data.mako:12 +#: rhodecode/templates/summary/summary_commits.mako:12 msgid "Refs" msgstr "" @@ -7880,7 +8028,7 @@ msgid "load previous" msgstr "" #: rhodecode/templates/changelog/changelog_elements.mako:26 -#: rhodecode/templates/changelog/changelog_summary_data.mako:21 +#: rhodecode/templates/summary/summary_commits.mako:21 #, python-format msgid "" "Commit status: %s\n" @@ -7888,44 +8036,66 @@ msgid "" msgstr "" #: rhodecode/templates/changelog/changelog_elements.mako:30 -#: rhodecode/templates/changelog/changelog_summary_data.mako:25 +#: rhodecode/templates/summary/summary_commits.mako:25 #, python-format msgid "Commit status: %s" msgstr "" #: rhodecode/templates/changelog/changelog_elements.mako:36 -#: rhodecode/templates/changelog/changelog_summary_data.mako:31 +#: rhodecode/templates/summary/summary_commits.mako:31 msgid "Commit status: Not Reviewed" msgstr "" #: rhodecode/templates/changelog/changelog_elements.mako:41 -#: rhodecode/templates/changelog/changelog_summary_data.mako:36 +#: rhodecode/templates/summary/summary_commits.mako:36 msgid "Commit has comments" msgstr "" #: rhodecode/templates/changelog/changelog_elements.mako:53 -#: rhodecode/templates/compare/compare_commits.mako:46 -#: rhodecode/templates/pullrequests/pullrequest_show.mako:495 +#: rhodecode/templates/changeset/changeset.mako:40 +msgid "Commit phase" +msgstr "" + +#: rhodecode/templates/changelog/changelog_elements.mako:60 +#: rhodecode/templates/changelog/changelog_elements.mako:67 +#: rhodecode/templates/changeset/changeset.mako:46 +#: rhodecode/templates/changeset/changeset.mako:53 +msgid "Evolve State" +msgstr "" + +#: rhodecode/templates/changelog/changelog_elements.mako:60 +#: rhodecode/templates/changeset/changeset.mako:46 +msgid "obsolete" +msgstr "" + +#: rhodecode/templates/changelog/changelog_elements.mako:67 +#: rhodecode/templates/changeset/changeset.mako:53 +msgid "hidden" +msgstr "" + +#: rhodecode/templates/changelog/changelog_elements.mako:73 +#: rhodecode/templates/compare/compare_commits.mako:47 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:522 #: rhodecode/templates/search/search_commit.mako:36 msgid "Expand commit message" msgstr "" -#: rhodecode/templates/changelog/changelog_elements.mako:77 -#: rhodecode/templates/changeset/changeset.mako:86 +#: rhodecode/templates/changelog/changelog_elements.mako:97 +#: rhodecode/templates/changeset/changeset.mako:106 #: rhodecode/templates/files/base.mako:4 msgid "merge" msgstr "" -#: rhodecode/templates/changelog/changelog_elements.mako:99 -#: rhodecode/templates/changelog/changelog_summary_data.mako:69 -#: rhodecode/templates/changeset/changeset.mako:99 +#: rhodecode/templates/changelog/changelog_elements.mako:119 +#: rhodecode/templates/changeset/changeset.mako:119 #: rhodecode/templates/files/base.mako:17 +#: rhodecode/templates/summary/summary_commits.mako:69 #: rhodecode/templates/tags/tags_data.mako:12 #, python-format msgid "Tag %s" msgstr "" -#: rhodecode/templates/changelog/changelog_elements.mako:113 +#: rhodecode/templates/changelog/changelog_elements.mako:133 msgid "load next" msgstr "" @@ -7933,100 +8103,78 @@ msgstr "" msgid "Show File" msgstr "" -#: rhodecode/templates/changelog/changelog_summary_data.mako:9 -#: rhodecode/templates/search/search_commit.mako:8 -msgid "Commit message" -msgstr "" - -#: rhodecode/templates/changelog/changelog_summary_data.mako:100 -msgid "Add or upload files directly via RhodeCode:" -msgstr "" - -#: rhodecode/templates/changelog/changelog_summary_data.mako:103 -#: rhodecode/templates/files/files_browser.mako:25 -msgid "Add New File" -msgstr "" - -#: rhodecode/templates/changelog/changelog_summary_data.mako:111 -msgid "Push new repo:" -msgstr "" - -#: rhodecode/templates/changelog/changelog_summary_data.mako:122 -msgid "Existing repository?" -msgstr "" - #: rhodecode/templates/changeset/changeset.mako:7 #, python-format msgid "%s Commit" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:43 +#: rhodecode/templates/changeset/changeset.mako:62 msgid "Parent Commit" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:43 +#: rhodecode/templates/changeset/changeset.mako:62 msgid "Parent" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:47 +#: rhodecode/templates/changeset/changeset.mako:66 msgid "Child Commit" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:47 -msgid "Child" -msgstr "" - -#: rhodecode/templates/changeset/changeset.mako:58 -msgid "Expand" -msgstr "" - #: rhodecode/templates/changeset/changeset.mako:66 -#: rhodecode/templates/changeset/changeset.mako:72 +msgid "Child" +msgstr "" + +#: rhodecode/templates/changeset/changeset.mako:78 +msgid "Expand" +msgstr "" + +#: rhodecode/templates/changeset/changeset.mako:86 +#: rhodecode/templates/changeset/changeset.mako:92 #: rhodecode/templates/changeset/changeset_file_comment.mako:81 #: rhodecode/templates/compare/compare_diff.mako:159 msgid "Commit status" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:79 +#: rhodecode/templates/changeset/changeset.mako:99 #: rhodecode/templates/files/file_tree_detail.mako:21 #: rhodecode/templates/files/files_detail.mako:20 msgid "References" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:115 +#: rhodecode/templates/changeset/changeset.mako:135 msgid "Diff options" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:119 +#: rhodecode/templates/changeset/changeset.mako:139 #: rhodecode/templates/codeblocks/diffs.mako:445 #: rhodecode/templates/codeblocks/diffs.mako:448 msgid "Raw diff" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:120 +#: rhodecode/templates/changeset/changeset.mako:140 msgid "Raw Diff" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:123 +#: rhodecode/templates/changeset/changeset.mako:143 msgid "Patch diff" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:124 +#: rhodecode/templates/changeset/changeset.mako:144 msgid "Patch Diff" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:127 +#: rhodecode/templates/changeset/changeset.mako:147 #: rhodecode/templates/codeblocks/diffs.mako:452 #: rhodecode/templates/codeblocks/diffs.mako:455 msgid "Download diff" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:128 +#: rhodecode/templates/changeset/changeset.mako:148 msgid "Download Diff" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:145 -#: rhodecode/templates/changeset/changeset.mako:147 +#: rhodecode/templates/changeset/changeset.mako:165 +#: rhodecode/templates/changeset/changeset.mako:167 #: rhodecode/tests/functional/test_commit_comments.py:275 #, python-format msgid "%d Commit comment" @@ -8034,8 +8182,8 @@ msgid_plural "%d Commit comments" msgstr[0] "" msgstr[1] "" -#: rhodecode/templates/changeset/changeset.mako:150 -#: rhodecode/templates/changeset/changeset.mako:152 +#: rhodecode/templates/changeset/changeset.mako:170 +#: rhodecode/templates/changeset/changeset.mako:172 #: rhodecode/tests/functional/test_commit_comments.py:282 #, python-format msgid "%d Inline Comment" @@ -8043,19 +8191,19 @@ msgid_plural "%d Inline Comments" msgstr[0] "" msgstr[1] "" -#: rhodecode/templates/changeset/changeset.mako:160 +#: rhodecode/templates/changeset/changeset.mako:180 msgid "Unresolved TODOs" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:169 +#: rhodecode/templates/changeset/changeset.mako:189 msgid "There are no unresolved TODOs" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:249 +#: rhodecode/templates/changeset/changeset.mako:269 msgid "No Child Commits" msgstr "" -#: rhodecode/templates/changeset/changeset.mako:285 +#: rhodecode/templates/changeset/changeset.mako:305 msgid "No Parent Commits" msgstr "" @@ -8081,72 +8229,80 @@ msgstr "" msgid "resolves comment #{}" msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:100 +#: rhodecode/templates/changeset/changeset_file_comment.mako:96 +msgid "Pull request author" +msgstr "" + +#: rhodecode/templates/changeset/changeset_file_comment.mako:97 +msgid "author" +msgstr "" + +#: rhodecode/templates/changeset/changeset_file_comment.mako:105 msgid "Outdated comment from pull request version {0}" msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:104 -#: rhodecode/templates/changeset/changeset_file_comment.mako:119 +#: rhodecode/templates/changeset/changeset_file_comment.mako:109 +#: rhodecode/templates/changeset/changeset_file_comment.mako:124 msgid "Comment from pull request version {0}" msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:116 +#: rhodecode/templates/changeset/changeset_file_comment.mako:121 msgid "Outdated comment from pull request version {}" msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:146 -#: rhodecode/templates/changeset/changeset_file_comment.mako:149 +#: rhodecode/templates/changeset/changeset_file_comment.mako:151 +#: rhodecode/templates/changeset/changeset_file_comment.mako:154 msgid "Prev" msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:147 -#: rhodecode/templates/changeset/changeset_file_comment.mako:150 +#: rhodecode/templates/changeset/changeset_file_comment.mako:152 +#: rhodecode/templates/changeset/changeset_file_comment.mako:155 msgid "Next" msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:185 +#: rhodecode/templates/changeset/changeset_file_comment.mako:190 msgid "Leave a comment on this Pull Request." msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:187 +#: rhodecode/templates/changeset/changeset_file_comment.mako:192 msgid "Leave a comment on {} commits in this range." msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:189 +#: rhodecode/templates/changeset/changeset_file_comment.mako:194 msgid "Leave a comment on this Commit." msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:277 +#: rhodecode/templates/changeset/changeset_file_comment.mako:282 #: rhodecode/templates/codeblocks/diffs.mako:71 msgid "You need to be logged in to leave comments." msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:278 +#: rhodecode/templates/changeset/changeset_file_comment.mako:283 #: rhodecode/templates/codeblocks/diffs.mako:71 msgid "Login now" msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:343 +#: rhodecode/templates/changeset/changeset_file_comment.mako:348 #, python-format msgid "Comments parsed using %s syntax with %s, and %s actions support." msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:345 +#: rhodecode/templates/changeset/changeset_file_comment.mako:350 msgid "Use @username inside this text to send notification to this RhodeCode user" msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:346 +#: rhodecode/templates/changeset/changeset_file_comment.mako:351 msgid "Start typing with / for certain actions to be triggered via text box." msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:363 +#: rhodecode/templates/changeset/changeset_file_comment.mako:368 #: rhodecode/templates/pullrequests/pullrequest_show.mako:15 #: rhodecode/templates/pullrequests/pullrequest_show.mako:153 #: rhodecode/templates/pullrequests/pullrequests.mako:52 msgid "Closed" msgstr "" -#: rhodecode/templates/changeset/changeset_file_comment.mako:393 +#: rhodecode/templates/changeset/changeset_file_comment.mako:398 #: rhodecode/templates/compare/compare_diff.mako:104 #: rhodecode/templates/compare/compare_diff.mako:112 #: rhodecode/templates/compare/compare_diff.mako:120 @@ -8185,7 +8341,7 @@ msgstr "" #: rhodecode/templates/changeset/changeset_range.mako:99 #: rhodecode/templates/compare/compare_diff.mako:312 -#: rhodecode/templates/pullrequests/pullrequest_show.mako:419 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:446 #, python-format msgid "Expand %s commit" msgid_plural "Expand %s commits" @@ -8194,7 +8350,7 @@ msgstr[1] "" #: rhodecode/templates/changeset/changeset_range.mako:105 #: rhodecode/templates/compare/compare_diff.mako:318 -#: rhodecode/templates/pullrequests/pullrequest_show.mako:425 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:452 #, python-format msgid "Collapse %s commit" msgid_plural "Collapse %s commits" @@ -8363,26 +8519,26 @@ msgstr "" msgid "Compare was calculated based on this shared commit." msgstr "" -#: rhodecode/templates/compare/compare_commits.mako:16 -#: rhodecode/templates/pullrequests/pullrequest_show.mako:456 +#: rhodecode/templates/compare/compare_commits.mako:17 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:483 msgid "Time" msgstr "" -#: rhodecode/templates/compare/compare_commits.mako:67 +#: rhodecode/templates/compare/compare_commits.mako:68 #, python-format msgid "%s commit hidden" msgid_plural "%s commits hidden" msgstr[0] "" msgstr[1] "" -#: rhodecode/templates/compare/compare_commits.mako:68 -#: rhodecode/templates/pullrequests/pullrequest_show.mako:573 +#: rhodecode/templates/compare/compare_commits.mako:69 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:600 msgid "show it" msgid_plural "show them" msgstr[0] "" msgstr[1] "" -#: rhodecode/templates/compare/compare_commits.mako:74 +#: rhodecode/templates/compare/compare_commits.mako:75 msgid "No commits in this compare" msgstr "" @@ -8420,6 +8576,7 @@ msgstr "" #: rhodecode/templates/email_templates/pull_request_comment.mako:90 #: rhodecode/templates/email_templates/pull_request_review.mako:73 #: rhodecode/templates/files/files_source.mako:23 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:71 msgid "Source" msgstr "" @@ -8633,6 +8790,10 @@ msgstr "" msgid "Form vertical" msgstr "" +#: rhodecode/templates/debug_style/login.html:56 +msgid "Don't have an account ?" +msgstr "" + #: rhodecode/templates/email_templates/base.mako:32 #, python-format msgid "This is a notification from RhodeCode. %(instance_url)s" @@ -8746,6 +8907,7 @@ msgid "%(user)s left %(comment_type)s on msgstr "" #: rhodecode/templates/email_templates/pull_request_comment.mako:49 +#: rhodecode/templates/pullrequests/pullrequest.mako:72 msgid "Source repository" msgstr "" @@ -8814,8 +8976,6 @@ msgid "%(target_ref_type)s of %(target_r msgstr "" #: rhodecode/templates/email_templates/pull_request_review.mako:76 -#: rhodecode/templates/summary/components.mako:95 -#: rhodecode/templates/summary/components.mako:98 #, python-format msgid "%(num)s Commit" msgid_plural "%(num)s Commits" @@ -8839,7 +8999,7 @@ msgstr "" msgid "Full Name" msgstr "" -#: rhodecode/templates/errors/error_document.mako:46 +#: rhodecode/templates/errors/error_document.mako:45 #, python-format msgid "You will be redirected to %s in %s seconds" msgstr "" @@ -8910,6 +9070,7 @@ msgid "Remove Custom Path" msgstr "" #: rhodecode/templates/files/files_add.mako:50 +#: rhodecode/templates/files/files_add.mako:59 msgid "Filename" msgstr "" @@ -8917,34 +9078,34 @@ msgstr "" msgid "Upload File" msgstr "" -#: rhodecode/templates/files/files_add.mako:59 -msgid "Upload file" -msgstr "" - -#: rhodecode/templates/files/files_add.mako:63 +#: rhodecode/templates/files/files_add.mako:62 msgid "No file selected" msgstr "" #: rhodecode/templates/files/files_add.mako:65 +msgid "Upload file" +msgstr "" + +#: rhodecode/templates/files/files_add.mako:71 msgid "Create New File" msgstr "" -#: rhodecode/templates/files/files_add.mako:75 +#: rhodecode/templates/files/files_add.mako:81 #: rhodecode/templates/files/files_edit.mako:79 msgid "line wraps" msgstr "" -#: rhodecode/templates/files/files_add.mako:76 +#: rhodecode/templates/files/files_add.mako:82 #: rhodecode/templates/files/files_edit.mako:80 msgid "on" msgstr "" -#: rhodecode/templates/files/files_add.mako:76 +#: rhodecode/templates/files/files_add.mako:82 #: rhodecode/templates/files/files_edit.mako:80 msgid "off" msgstr "" -#: rhodecode/templates/files/files_add.mako:103 +#: rhodecode/templates/files/files_add.mako:109 #: rhodecode/templates/files/files_edit.mako:106 msgid "Commit changes" msgstr "" @@ -8965,6 +9126,11 @@ msgstr "" msgid "Close File List" msgstr "" +#: rhodecode/templates/files/files_browser.mako:25 +#: rhodecode/templates/summary/summary_commits.mako:103 +msgid "Add New File" +msgstr "" + #: rhodecode/templates/files/files_browser.mako:27 msgid "Add File" msgstr "" @@ -9077,7 +9243,6 @@ msgid "LargeFile" msgstr "" #: rhodecode/templates/files/files_source.mako:10 -#: rhodecode/templates/search/search_content.mako:57 msgid "line" msgid_plural "lines" msgstr[0] "" @@ -9141,6 +9306,10 @@ msgstr "" msgid "Fork name" msgstr "" +#: rhodecode/templates/forks/fork.mako:77 +msgid "Default commit for files page, downloads, whoosh and readme" +msgstr "" + #: rhodecode/templates/forks/fork.mako:93 msgid "Copy permissions" msgstr "" @@ -9178,6 +9347,13 @@ msgstr "" msgid "Filter" msgstr "" +#: rhodecode/templates/journal/journal.mako:14 +#, python-format +msgid "%s entry" +msgid_plural "%s entries" +msgstr[0] "" +msgstr[1] "" + #: rhodecode/templates/journal/journal.mako:23 msgid "ATOM journal feed" msgstr "" @@ -9208,65 +9384,75 @@ msgstr "" msgid "New pull request" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest.mako:51 +#: rhodecode/templates/pullrequests/pullrequest.mako:35 +msgid "Pull request summary" +msgstr "" + +#: rhodecode/templates/pullrequests/pullrequest.mako:58 msgid "Write a short description on this pull request" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest.mako:57 +#: rhodecode/templates/pullrequests/pullrequest.mako:64 msgid "Commit flow" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest.mako:65 -msgid "Origin repository" -msgstr "" - -#: rhodecode/templates/pullrequests/pullrequest.mako:83 +#: rhodecode/templates/pullrequests/pullrequest.mako:90 msgid "Loading refs..." msgstr "" -#: rhodecode/templates/pullrequests/pullrequest.mako:94 +#: rhodecode/templates/pullrequests/pullrequest.mako:101 msgid "Submit Pull Request" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest.mako:107 -#: rhodecode/templates/pullrequests/pullrequest_show.mako:322 +#: rhodecode/templates/pullrequests/pullrequest.mako:115 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:309 +msgid "Author of this pull request" +msgstr "" + +#: rhodecode/templates/pullrequests/pullrequest.mako:129 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:323 +msgid "Reviewer rules" +msgstr "" + +#: rhodecode/templates/pullrequests/pullrequest.mako:139 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:337 msgid "Pull request reviewers" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest.mako:118 -#: rhodecode/templates/pullrequests/pullrequest_show.mako:366 -msgid "Add reviewer" -msgstr "" - -#: rhodecode/templates/pullrequests/pullrequest.mako:297 -#: rhodecode/templates/pullrequests/pullrequest.mako:570 -msgid "Please select origin and destination" -msgstr "" - -#: rhodecode/templates/pullrequests/pullrequest.mako:303 +#: rhodecode/templates/pullrequests/pullrequest.mako:150 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:392 +msgid "Add reviewer or reviewer group" +msgstr "" + +#: rhodecode/templates/pullrequests/pullrequest.mako:302 +#: rhodecode/templates/pullrequests/pullrequest.mako:504 +msgid "Please select source and target" +msgstr "" + +#: rhodecode/templates/pullrequests/pullrequest.mako:308 msgid "Loading compare ..." msgstr "" -#: rhodecode/templates/pullrequests/pullrequest.mako:350 -#: rhodecode/templates/pullrequests/pullrequest.mako:352 +#: rhodecode/templates/pullrequests/pullrequest.mako:356 +#: rhodecode/templates/pullrequests/pullrequest.mako:358 msgid "This pull request will consist of __COMMITS__ commit." msgid_plural "This pull request will consist of __COMMITS__ commits." msgstr[0] "" msgstr[1] "" -#: rhodecode/templates/pullrequests/pullrequest.mako:355 +#: rhodecode/templates/pullrequests/pullrequest.mako:361 msgid "Show detailed compare." msgstr "" -#: rhodecode/templates/pullrequests/pullrequest.mako:362 +#: rhodecode/templates/pullrequests/pullrequest.mako:368 msgid "There are no commits to merge." msgstr "" -#: rhodecode/templates/pullrequests/pullrequest.mako:462 -msgid "Destination repository" -msgstr "" - -#: rhodecode/templates/pullrequests/pullrequest.mako:473 +#: rhodecode/templates/pullrequests/pullrequest.mako:431 +msgid "Target repository" +msgstr "" + +#: rhodecode/templates/pullrequests/pullrequest.mako:441 msgid "Select commit reference" msgstr "" @@ -9314,10 +9500,6 @@ msgstr "" msgid "Confirm to delete this pull request" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:71 -msgid "Origin" -msgstr "" - #: rhodecode/templates/pullrequests/pullrequest_show.mako:88 msgid "Common ancestor" msgstr "" @@ -9416,69 +9598,69 @@ msgid "Pull request versions not availab msgstr "" #: rhodecode/templates/pullrequests/pullrequest_show.mako:300 -#: rhodecode/templates/pullrequests/pullrequest_show.mako:370 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:397 msgid "Save Changes" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:387 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:414 msgid "Missing requirements:" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:388 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:415 msgid "These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled." msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:396 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:423 msgid "Missing commits" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:397 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:424 msgid "This pull request cannot be displayed, because one or more commits no longer exist in the source repository." msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:398 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:425 msgid "Please update this pull request, push the commits back into the source repository, or consider closing this pull request." msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:409 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:436 #, python-format msgid "Showing changes at v%d, commenting is disabled." msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:432 -#: rhodecode/templates/pullrequests/pullrequest_show.mako:434 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:459 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:461 msgid "Update commits" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:434 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:461 msgid "Update is disabled for current view" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:445 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:472 msgid "Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:449 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:476 msgid "commits added: {}, removed: {}" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:467 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:494 msgid "Commit added in displayed changes" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:469 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:496 msgid "Commit removed in displayed changes" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:572 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:599 msgid "there is {num} general comment from older versions" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:575 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:602 msgid "there are {num} general comments from older versions" msgstr "" -#: rhodecode/templates/pullrequests/pullrequest_show.mako:576 +#: rhodecode/templates/pullrequests/pullrequest_show.mako:603 msgid "show them" msgstr "" @@ -9568,6 +9750,11 @@ msgstr "" msgid "File names" msgstr "" +#: rhodecode/templates/search/search_commit.mako:8 +#: rhodecode/templates/summary/summary_commits.mako:9 +msgid "Commit message" +msgstr "" + #: rhodecode/templates/search/search_commit.mako:11 msgid "Age (new first)" msgstr "" @@ -9599,34 +9786,6 @@ msgstr "" msgid "%s RSS feed" msgstr "" -#: rhodecode/templates/summary/components.mako:5 -#, python-format -msgid "%(num)s Branch" -msgid_plural "%(num)s Branches" -msgstr[0] "" -msgstr[1] "" - -#: rhodecode/templates/summary/components.mako:12 -#, python-format -msgid "%(num)s Closed Branch" -msgid_plural "%(num)s Closed Branches" -msgstr[0] "" -msgstr[1] "" - -#: rhodecode/templates/summary/components.mako:19 -#, python-format -msgid "%(num)s Tag" -msgid_plural "%(num)s Tags" -msgstr[0] "" -msgstr[1] "" - -#: rhodecode/templates/summary/components.mako:26 -#, python-format -msgid "%(num)s Bookmark" -msgid_plural "%(num)s Bookmarks" -msgstr[0] "" -msgstr[1] "" - #: rhodecode/templates/summary/components.mako:49 msgid "Read-only url" msgstr "" @@ -9707,6 +9866,18 @@ msgstr "" msgid "Readme file from commit %s:%s" msgstr "" +#: rhodecode/templates/summary/summary_commits.mako:100 +msgid "Add or upload files directly via RhodeCode:" +msgstr "" + +#: rhodecode/templates/summary/summary_commits.mako:111 +msgid "Push new repo:" +msgstr "" + +#: rhodecode/templates/summary/summary_commits.mako:122 +msgid "Existing repository?" +msgstr "" + #: rhodecode/templates/tags/tags.mako:5 #, python-format msgid "%s Tags" diff --git a/rhodecode/integrations/types/webhook.py b/rhodecode/integrations/types/webhook.py --- a/rhodecode/integrations/types/webhook.py +++ b/rhodecode/integrations/types/webhook.py @@ -35,12 +35,14 @@ from rhodecode.integrations.types.base i log = logging.getLogger(__name__) -# updating this required to update the `base_vars` passed in url calling func +# updating this required to update the `common_vars` passed in url calling func WEBHOOK_URL_VARS = [ 'repo_name', 'repo_type', 'repo_id', 'repo_url', + # extra repo fields + 'extra:', # special attrs below that we handle, using multi-call 'branch', @@ -50,6 +52,10 @@ WEBHOOK_URL_VARS = [ 'pull_request_id', 'pull_request_url', + # user who triggers the call + 'username', + 'user_id', + ] URL_VARS = ', '.join('${' + x + '}' for x in WEBHOOK_URL_VARS) @@ -70,7 +76,13 @@ class WebhookHandler(object): 'repo_type': data['repo']['repo_type'], 'repo_id': data['repo']['repo_id'], 'repo_url': data['repo']['url'], + 'username': data['actor']['username'], + 'user_id': data['actor']['user_id'] } + extra_vars = {} + for extra_key, extra_val in data['repo']['extra_fields'].items(): + extra_vars['extra:{}'.format(extra_key)] = extra_val + common_vars.update(extra_vars) return string.Template( self.template_url).safe_substitute(**common_vars) diff --git a/rhodecode/lib/action_parser.py b/rhodecode/lib/action_parser.py --- a/rhodecode/lib/action_parser.py +++ b/rhodecode/lib/action_parser.py @@ -42,8 +42,13 @@ def action_parser(user_log, feed=False, :param feed: use output for feeds (no html and fancy icons) :param parse_cs: parse Changesets into VCS instances """ - ap = ActionParser(user_log, feed=False, parse_commits=False) - return ap.callbacks() + if user_log.version == 'v2': + ap = AuditLogParser(user_log) + return ap.callbacks() + else: + # old style + ap = ActionParser(user_log, feed=False, parse_commits=False) + return ap.callbacks() class ActionParser(object): @@ -161,8 +166,9 @@ class ActionParser(object): return action_map def get_fork_name(self): + from rhodecode.lib import helpers as h repo_name = self.action_params - _url = url('summary_home', repo_name=repo_name) + _url = h.route_path('repo_summary', repo_name=repo_name) return _('fork name %s') % link_to(self.action_params, _url) def get_user_name(self): @@ -174,6 +180,7 @@ class ActionParser(object): return group_name def get_pull_request(self): + from rhodecode.lib import helpers as h pull_request_id = self.action_params if self.is_deleted(): repo_name = self.user_log.repository_name @@ -181,8 +188,8 @@ class ActionParser(object): repo_name = self.user_log.repository.repo_name return link_to( _('Pull request #%s') % pull_request_id, - url('pullrequest_show', repo_name=repo_name, - pull_request_id=pull_request_id)) + h.route_path('pullrequest_show', repo_name=repo_name, + pull_request_id=pull_request_id)) def get_archive_name(self): archive_name = self.action_params @@ -302,6 +309,34 @@ class ActionParser(object): return self.user_log.repository is None +class AuditLogParser(object): + def __init__(self, audit_log_entry): + self.audit_log_entry = audit_log_entry + + def get_icon(self, action): + return 'icon-rhodecode' + + def callbacks(self): + action_str = self.audit_log_entry.action + + def callback(): + # returned callbacks we need to call to get + action = action_str \ + .replace('[', '')\ + .replace(']', '') + return literal(action) + + def icon(): + tmpl = """""" + ico = self.get_icon(action_str) + return literal(tmpl % (ico, action_str)) + + action_params_func = _no_params_func + + return [ + callback, action_params_func, icon] + + def _no_params_func(): return "" diff --git a/rhodecode/lib/audit_logger.py b/rhodecode/lib/audit_logger.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/audit_logger.py @@ -0,0 +1,255 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2017-2017 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 datetime + +from rhodecode.model import meta +from rhodecode.model.db import User, UserLog, Repository + + +log = logging.getLogger(__name__) + +# action as key, and expected action_data as value +ACTIONS_V1 = { + 'user.login.success': {'user_agent': ''}, + 'user.login.failure': {'user_agent': ''}, + 'user.logout': {'user_agent': ''}, + 'user.password.reset_request': {}, + 'user.push': {'user_agent': '', 'commit_ids': []}, + 'user.pull': {'user_agent': ''}, + + 'user.create': {'data': {}}, + 'user.delete': {'old_data': {}}, + 'user.edit': {'old_data': {}}, + 'user.edit.permissions': {}, + 'user.edit.ip.add': {'ip': {}, 'user': {}}, + 'user.edit.ip.delete': {'ip': {}, 'user': {}}, + 'user.edit.token.add': {'token': {}, 'user': {}}, + 'user.edit.token.delete': {'token': {}, 'user': {}}, + 'user.edit.email.add': {'email': ''}, + 'user.edit.email.delete': {'email': ''}, + 'user.edit.password_reset.enabled': {}, + 'user.edit.password_reset.disabled': {}, + + 'user_group.create': {'data': {}}, + 'user_group.delete': {'old_data': {}}, + 'user_group.edit': {'old_data': {}}, + 'user_group.edit.permissions': {}, + 'user_group.edit.member.add': {'user': {}}, + 'user_group.edit.member.delete': {'user': {}}, + + 'repo.create': {'data': {}}, + 'repo.fork': {'data': {}}, + 'repo.edit': {'old_data': {}}, + 'repo.edit.permissions': {}, + 'repo.delete': {'old_data': {}}, + 'repo.commit.strip': {'commit_id': ''}, + 'repo.archive.download': {'user_agent': '', 'archive_name': '', + 'archive_spec': '', 'archive_cached': ''}, + 'repo.pull_request.create': '', + 'repo.pull_request.edit': '', + 'repo.pull_request.delete': '', + 'repo.pull_request.close': '', + 'repo.pull_request.merge': '', + 'repo.pull_request.vote': '', + 'repo.pull_request.comment.create': '', + 'repo.pull_request.comment.delete': '', + + 'repo.pull_request.reviewer.add': '', + 'repo.pull_request.reviewer.delete': '', + + 'repo.commit.comment.create': {'data': {}}, + 'repo.commit.comment.delete': {'data': {}}, + 'repo.commit.vote': '', + + 'repo_group.create': {'data': {}}, + 'repo_group.edit': {'old_data': {}}, + 'repo_group.edit.permissions': {}, + 'repo_group.delete': {'old_data': {}}, +} +ACTIONS = ACTIONS_V1 + +SOURCE_WEB = 'source_web' +SOURCE_API = 'source_api' + + +class UserWrap(object): + """ + Fake object used to imitate AuthUser + """ + + def __init__(self, user_id=None, username=None, ip_addr=None): + self.user_id = user_id + self.username = username + self.ip_addr = ip_addr + + +class RepoWrap(object): + """ + Fake object used to imitate RepoObject that audit logger requires + """ + + def __init__(self, repo_id=None, repo_name=None): + self.repo_id = repo_id + self.repo_name = repo_name + + +def _store_log(action_name, action_data, user_id, username, user_data, + ip_address, repository_id, repository_name): + user_log = UserLog() + user_log.version = UserLog.VERSION_2 + + user_log.action = action_name + user_log.action_data = action_data + + user_log.user_ip = ip_address + + user_log.user_id = user_id + user_log.username = username + user_log.user_data = user_data + + user_log.repository_id = repository_id + user_log.repository_name = repository_name + + user_log.action_date = datetime.datetime.now() + + log.info('AUDIT: Logging action: `%s` by user:id:%s[%s] ip:%s', + action_name, user_id, username, ip_address) + + return user_log + + +def store_web(*args, **kwargs): + if 'action_data' not in kwargs: + kwargs['action_data'] = {} + kwargs['action_data'].update({ + 'source': SOURCE_WEB + }) + return store(*args, **kwargs) + + +def store_api(*args, **kwargs): + if 'action_data' not in kwargs: + kwargs['action_data'] = {} + kwargs['action_data'].update({ + 'source': SOURCE_API + }) + return store(*args, **kwargs) + + +def store(action, user, action_data=None, user_data=None, ip_addr=None, + repo=None, sa_session=None, commit=False): + """ + Audit logger for various actions made by users, typically this + results in a call such:: + + from rhodecode.lib import audit_logger + + audit_logger.store( + 'repo.edit', user=self._rhodecode_user) + audit_logger.store( + 'repo.delete', action_data={'data': repo_data}, + user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8')) + + # repo action + audit_logger.store( + 'repo.delete', + user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'), + repo=audit_logger.RepoWrap(repo_name='some-repo')) + + # repo action, when we know and have the repository object already + audit_logger.store( + 'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, }, + user=self._rhodecode_user, + repo=repo_object) + + # alternative wrapper to the above + audit_logger.store_web( + 'repo.delete', action_data={}, + user=self._rhodecode_user, + repo=repo_object) + + # without an user ? + audit_logger.store( + 'user.login.failure', + user=audit_logger.UserWrap( + username=self.request.params.get('username'), + ip_addr=self.request.remote_addr)) + + """ + from rhodecode.lib.utils2 import safe_unicode + from rhodecode.lib.auth import AuthUser + + action_spec = ACTIONS.get(action, None) + if action_spec is None: + raise ValueError('Action `{}` is not supported'.format(action)) + + if not sa_session: + sa_session = meta.Session() + + try: + username = getattr(user, 'username', None) + if not username: + pass + + user_id = getattr(user, 'user_id', None) + if not user_id: + # maybe we have username ? Try to figure user_id from username + if username: + user_id = getattr( + User.get_by_username(username), 'user_id', None) + + ip_addr = ip_addr or getattr(user, 'ip_addr', None) + if not ip_addr: + pass + + if not user_data: + # try to get this from the auth user + if isinstance(user, AuthUser): + user_data = { + 'username': user.username, + 'email': user.email, + } + + repository_name = getattr(repo, 'repo_name', None) + repository_id = getattr(repo, 'repo_id', None) + if not repository_id: + # maybe we have repo_name ? Try to figure repo_id from repo_name + if repository_name: + repository_id = getattr( + Repository.get_by_repo_name(repository_name), 'repo_id', None) + + user_log = _store_log( + action_name=safe_unicode(action), + action_data=action_data or {}, + user_id=user_id, + username=username, + user_data=user_data or {}, + ip_address=safe_unicode(ip_addr), + repository_id=repository_id, + repository_name=repository_name + ) + sa_session.add(user_log) + if commit: + sa_session.commit() + + except Exception: + log.exception('AUDIT: failed to store audit log') diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -34,10 +34,10 @@ import traceback from functools import wraps import ipaddress -from pyramid.httpexceptions import HTTPForbidden, HTTPFound -from pylons import url, request -from pylons.controllers.util import abort, redirect +from pyramid.httpexceptions import HTTPForbidden, HTTPFound, HTTPNotFound from pylons.i18n.translation import _ +# NOTE(marcink): this has to be removed only after pyramid migration, +# replace with _ = request.translate from sqlalchemy.orm.exc import ObjectDeletedError from sqlalchemy.orm import joinedload from zope.cachedescriptors.property import Lazy as LazyProperty @@ -138,7 +138,7 @@ class _RhodeCodeCryptoBase(object): class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase): - ENC_PREF = '$2a$10' + ENC_PREF = ('$2a$10', '$2b$10') def hash_create(self, str_): self._assert_bytes(str_) @@ -302,7 +302,8 @@ def _cached_perms_data(user_id, scope, u explicit, algo) return permissions.calculate() -class PermOrigin: + +class PermOrigin(object): ADMIN = 'superadmin' REPO_USER = 'user:%s' @@ -341,7 +342,6 @@ class PermOriginDict(dict): {'resource': [('read', 'default'), ('write', 'admin')]} """ - def __init__(self, *args, **kw): dict.__init__(self, *args, **kw) self.perm_origin_stack = {} @@ -807,6 +807,8 @@ class AuthUser(object): self.ip_addr = ip_addr self.name = '' self.lastname = '' + self.first_name = '' + self.last_name = '' self.email = '' self.is_authenticated = False self.admin = False @@ -1045,8 +1047,8 @@ class AuthUser(object): default_ips = UserIpMap.query().filter( UserIpMap.user == User.get_default_user(cache=True)) if cache: - default_ips = default_ips.options(FromCache("sql_cache_short", - "get_user_ips_default")) + default_ips = default_ips.options( + FromCache("sql_cache_short", "get_user_ips_default")) # populate from default user for ip in default_ips: @@ -1059,8 +1061,8 @@ class AuthUser(object): user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id) if cache: - user_ips = user_ips.options(FromCache("sql_cache_short", - "get_user_ips_%s" % user_id)) + user_ips = user_ips.options( + FromCache("sql_cache_short", "get_user_ips_%s" % user_id)) for ip in user_ips: try: @@ -1114,6 +1116,17 @@ def get_csrf_token(session=None, force_n return session.get(csrf_token_key) +def get_request(perm_class): + from pyramid.threadlocal import get_current_request + pyramid_request = get_current_request() + if not pyramid_request: + # return global request of pylons in case pyramid isn't available + # NOTE(marcink): this should be removed after migration to pyramid + from pylons import request + return request + return pyramid_request + + # CHECK DECORATORS class CSRFRequired(object): """ @@ -1144,7 +1157,12 @@ class CSRFRequired(object): supplied_token = self._get_csrf(_request) return supplied_token and supplied_token == cur_token + def _get_request(self): + return get_request(self) + def __wrapper(self, func, *fargs, **fkwargs): + request = self._get_request() + if request.method in self.except_methods: return func(*fargs, **fkwargs) @@ -1157,8 +1175,8 @@ class CSRFRequired(object): reason = 'token-missing' supplied_token = self._get_csrf(request) if supplied_token and cur_token != supplied_token: - reason = 'token-mismatch [%s:%s]' % (cur_token or ''[:6], - supplied_token or ''[:6]) + reason = 'token-mismatch [%s:%s]' % ( + cur_token or ''[:6], supplied_token or ''[:6]) csrf_message = \ ("Cross-site request forgery detected, request denied. See " @@ -1185,10 +1203,15 @@ class LoginRequired(object): def __call__(self, func): return get_cython_compat_decorator(self.__wrapper, func) + def _get_request(self): + return get_request(self) + def __wrapper(self, func, *fargs, **fkwargs): from rhodecode.lib import helpers as h cls = fargs[0] user = cls._rhodecode_user + request = self._get_request() + loc = "%s:%s" % (cls.__class__.__name__, func.__name__) log.debug('Starting login restriction checks for user: %s' % (user,)) # check if our IP is allowed @@ -1255,22 +1278,27 @@ class LoginRequired(object): # we preserve the get PARAM came_from = request.path_qs log.debug('redirecting to login page with %s' % (came_from,)) - return redirect( + raise HTTPFound( h.route_path('login', _query={'came_from': came_from})) class NotAnonymous(object): """ Must be logged in to execute this function else - redirect to login page""" + redirect to login page + """ def __call__(self, func): return get_cython_compat_decorator(self.__wrapper, func) + def _get_request(self): + return get_request(self) + def __wrapper(self, func, *fargs, **fkwargs): import rhodecode.lib.helpers as h cls = fargs[0] self.user = cls._rhodecode_user + request = self._get_request() log.debug('Checking if user is not anonymous @%s' % cls) @@ -1281,19 +1309,28 @@ class NotAnonymous(object): h.flash(_('You need to be a registered user to ' 'perform this action'), category='warning') - return redirect( + raise HTTPFound( h.route_path('login', _query={'came_from': came_from})) else: return func(*fargs, **fkwargs) class XHRRequired(object): + # TODO(marcink): remove this in favor of the predicates in pyramid routes + def __call__(self, func): return get_cython_compat_decorator(self.__wrapper, func) + def _get_request(self): + return get_request(self) + def __wrapper(self, func, *fargs, **fkwargs): + from pylons.controllers.util import abort + request = self._get_request() + log.debug('Checking if request is XMLHttpRequest (XHR)') xhr_message = 'This is not a valid XMLHttpRequest (XHR) request' + if not request.is_xhr: abort(400, detail=xhr_message) @@ -1303,9 +1340,9 @@ class XHRRequired(object): class HasAcceptedRepoType(object): """ Check if requested repo is within given repo type aliases + """ - TODO: anderson: not sure where to put this decorator - """ + # TODO(marcink): remove this in favor of the predicates in pyramid routes def __init__(self, *repo_type_list): self.repo_type_list = set(repo_type_list) @@ -1328,8 +1365,9 @@ class HasAcceptedRepoType(object): h.flash(h.literal( _('Action not supported for %s.' % rhodecode_repo.alias)), category='warning') - return redirect( - url('summary_home', repo_name=cls.rhodecode_db_repo.repo_name)) + raise HTTPFound( + h.route_path('repo_summary', + repo_name=cls.rhodecode_db_repo.repo_name)) class PermsDecorator(object): @@ -1345,12 +1383,7 @@ class PermsDecorator(object): return get_cython_compat_decorator(self.__wrapper, func) def _get_request(self): - from pyramid.threadlocal import get_current_request - pyramid_request = get_current_request() - if not pyramid_request: - # return global request of pylons in case pyramid isn't available - return request - return pyramid_request + return get_request(self) def _get_came_from(self): _request = self._get_request() @@ -1377,13 +1410,13 @@ class PermsDecorator(object): if anonymous: came_from = self._get_came_from() h.flash(_('You need to be signed in to view this page'), - category='warning') + category='warning') raise HTTPFound( h.route_path('login', _query={'came_from': came_from})) else: - # redirect with forbidden ret code - raise HTTPForbidden() + # redirect with 404 to prevent resource discovery + raise HTTPNotFound() def check_permissions(self, user): """Dummy function for overriding""" @@ -1429,10 +1462,16 @@ class HasRepoPermissionAllDecorator(Perm def check_permissions(self, user): perms = user.permissions repo_name = self._get_repo_name() + try: user_perms = set([perms['repositories'][repo_name]]) except KeyError: + log.debug('cannot locate repo with name: `%s` in permissions defs', + repo_name) return False + + log.debug('checking `%s` permissions for repo `%s`', + user_perms, repo_name) if self.required_perms.issubset(user_perms): return True return False @@ -1450,11 +1489,16 @@ class HasRepoPermissionAnyDecorator(Perm def check_permissions(self, user): perms = user.permissions repo_name = self._get_repo_name() + try: user_perms = set([perms['repositories'][repo_name]]) except KeyError: + log.debug('cannot locate repo with name: `%s` in permissions defs', + repo_name) return False + log.debug('checking `%s` permissions for repo `%s`', + user_perms, repo_name) if self.required_perms.intersection(user_perms): return True return False @@ -1476,8 +1520,12 @@ class HasRepoGroupPermissionAllDecorator try: user_perms = set([perms['repositories_groups'][group_name]]) except KeyError: + log.debug('cannot locate repo group with name: `%s` in permissions defs', + group_name) return False + log.debug('checking `%s` permissions for repo group `%s`', + user_perms, group_name) if self.required_perms.issubset(user_perms): return True return False @@ -1496,11 +1544,16 @@ class HasRepoGroupPermissionAnyDecorator def check_permissions(self, user): perms = user.permissions group_name = self._get_repo_group_name() + try: user_perms = set([perms['repositories_groups'][group_name]]) except KeyError: + log.debug('cannot locate repo group with name: `%s` in permissions defs', + group_name) return False + log.debug('checking `%s` permissions for repo group `%s`', + user_perms, group_name) if self.required_perms.intersection(user_perms): return True return False @@ -1575,6 +1628,7 @@ class PermsFunction(object): if not user: log.debug('Using user attribute from global request') # TODO: remove this someday,put as user as attribute here + request = self._get_request() user = request.user # init auth user if not already given @@ -1603,12 +1657,7 @@ class PermsFunction(object): return False def _get_request(self): - from pyramid.threadlocal import get_current_request - pyramid_request = get_current_request() - if not pyramid_request: - # return global request of pylons incase pyramid one isn't available - return request - return pyramid_request + return get_request(self) def _get_check_scope(self, cls_name): return { @@ -1673,7 +1722,8 @@ class HasRepoPermissionAny(PermsFunction def _get_repo_name(self): if not self.repo_name: - self.repo_name = get_repo_slug(request) + _request = self._get_request() + self.repo_name = get_repo_slug(_request) return self.repo_name def check_permissions(self, user): diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -162,6 +162,10 @@ def get_access_path(environ): return path +def get_user_agent(environ): + return environ.get('HTTP_USER_AGENT') + + def vcs_operation_context( environ, repo_name, username, action, scm, check_locking=True, is_shadow_repo=False): @@ -200,6 +204,7 @@ def vcs_operation_context( 'make_lock': make_lock, 'locked_by': locked_by, 'server_url': utils2.get_server_url(environ), + 'user_agent': get_user_agent(environ), 'hooks': get_enabled_hook_classes(ui_settings), 'is_shadow_repo': is_shadow_repo, } @@ -260,7 +265,7 @@ class BasicAuth(AuthBasicAuthenticator): __call__ = authenticate -def attach_context_attributes(context, request): +def attach_context_attributes(context, request, user_id, attach_to_request=False): """ Attach variables into template context called `c`, please note that request could be pylons or pyramid request in here. @@ -302,7 +307,7 @@ def attach_context_attributes(context, r 'rhodecode_markup_renderer', 'rst') context.visual.comment_types = ChangesetComment.COMMENT_TYPES context.visual.rhodecode_support_url = \ - rc_config.get('rhodecode_support_url') or url('rhodecode_support') + rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support') context.pre_code = rc_config.get('rhodecode_pre_code') context.post_code = rc_config.get('rhodecode_post_code') @@ -325,6 +330,11 @@ def attach_context_attributes(context, r context.rhodecode_instanceid = config.get('instance_id') + context.visual.cut_off_limit_diff = safe_int( + config.get('cut_off_limit_diff')) + context.visual.cut_off_limit_file = safe_int( + config.get('cut_off_limit_file')) + # AppEnlight context.appenlight_enabled = str2bool(config.get('appenlight', 'false')) context.appenlight_api_public_key = config.get( @@ -382,10 +392,12 @@ def attach_context_attributes(context, r context.csrf_token = auth.get_csrf_token() context.backends = rhodecode.BACKENDS.keys() context.backends.sort() - context.unread_notifications = NotificationModel().get_unread_cnt_for_user( - context.rhodecode_user.user_id) + context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id) + if attach_to_request: + request.call_context = context + else: + context.pyramid_request = pyramid.threadlocal.get_current_request() - context.pyramid_request = pyramid.threadlocal.get_current_request() def get_auth_user(environ): @@ -435,7 +447,7 @@ class BaseController(WSGIController): """ # on each call propagate settings calls into global settings. set_rhodecode_config(config) - attach_context_attributes(c, request) + attach_context_attributes(c, request, c.rhodecode_user.user_id) # TODO: Remove this when fixed in attach_context_attributes() c.repo_name = get_repo_slug(request) # can be empty @@ -550,7 +562,7 @@ class BaseRepoController(BaseController) "The repository at %(repo_name)s cannot be located.") % {'repo_name': c.repo_name}, category='error', ignore_duplicate=True) - redirect(url('home')) + redirect(h.route_path('home')) # update last change according to VCS data if not missing_requirements: @@ -577,7 +589,7 @@ class BaseRepoController(BaseController) 'Requirements are missing for repository %s: %s', c.repo_name, error.message) - summary_url = url('summary_home', repo_name=c.repo_name) + summary_url = h.route_path('repo_summary', repo_name=c.repo_name) statistics_url = url('edit_repo_statistics', repo_name=c.repo_name) settings_update_url = url('repo', repo_name=c.repo_name) path = request.path 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 @@ -31,12 +31,13 @@ from celery.task import task from pylons import config import rhodecode +from rhodecode.lib import audit_logger from rhodecode.lib.celerylib import ( run_task, dbsession, __get_lockkey, LockHeld, DaemonLock, 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 +from rhodecode.lib.utils import add_cache from rhodecode.lib.utils2 import safe_int, str2bool from rhodecode.model.db import Repository, User @@ -141,7 +142,7 @@ def create_repo(form_data, cur_user): 'enable_downloads', defs.get('repo_enable_downloads')) try: - RepoModel(DBS)._create_repo( + repo = RepoModel(DBS)._create_repo( repo_name=repo_name_full, repo_type=repo_type, description=description, @@ -158,8 +159,6 @@ def create_repo(form_data, cur_user): enable_downloads=enable_downloads, state=state ) - - action_logger(cur_user, 'user_created_repo', repo_name_full, '', DBS) DBS.commit() # now create this repo on Filesystem @@ -177,6 +176,14 @@ def create_repo(form_data, cur_user): # set new created state repo.set_state(Repository.STATE_CREATED) + repo_id = repo.repo_id + repo_data = repo.get_api_data() + + audit_logger.store( + 'repo.create', action_data={'data': repo_data}, + user=cur_user, + repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id)) + DBS.commit() except Exception: log.warning('Exception occurred when creating repository, ' @@ -240,8 +247,7 @@ def create_repo_fork(form_data, cur_user fork_of=fork_of, copy_fork_permissions=copy_fork_permissions ) - action_logger(cur_user, 'user_forked_repo:%s' % repo_name_full, - fork_of.repo_name, '', DBS) + DBS.commit() base_path = Repository.base_path() @@ -264,6 +270,14 @@ def create_repo_fork(form_data, cur_user # set new created state repo.set_state(Repository.STATE_CREATED) + + repo_id = repo.repo_id + repo_data = repo.get_api_data() + audit_logger.store( + 'repo.fork', action_data={'data': repo_data}, + user=cur_user, + repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id)) + DBS.commit() except Exception as e: log.warning('Exception %s occurred when forking repository, ' diff --git a/rhodecode/lib/channelstream.py b/rhodecode/lib/channelstream.py --- a/rhodecode/lib/channelstream.py +++ b/rhodecode/lib/channelstream.py @@ -77,8 +77,8 @@ def get_user_data(user_id): return { 'id': user.user_id, 'username': user.username, - 'first_name': user.name, - 'last_name': user.lastname, + 'first_name': user.first_name, + 'last_name': user.last_name, 'icon_link': h.gravatar_url(user.email, 60), 'display_name': h.person(user, 'username_or_name_or_email'), 'display_link': h.link_to_user(user), diff --git a/rhodecode/lib/codeblocks.py b/rhodecode/lib/codeblocks.py --- a/rhodecode/lib/codeblocks.py +++ b/rhodecode/lib/codeblocks.py @@ -471,31 +471,29 @@ class DiffSet(object): source_file_type = source_lexer.name target_file_type = target_lexer.name - op_hunks = patch['chunks'][0] - hunks = patch['chunks'][1:] - filediff = AttributeDict({ 'source_file_path': source_file_path, 'target_file_path': target_file_path, 'source_filenode': source_filenode, 'target_filenode': target_filenode, - 'hunks': [], 'source_file_type': target_file_type, 'target_file_type': source_file_type, - 'patch': patch, + 'patch': {'filename': patch['filename'], 'stats': patch['stats']}, + 'operation': patch['operation'], 'source_mode': patch['stats']['old_mode'], 'target_mode': patch['stats']['new_mode'], 'limited_diff': isinstance(patch, LimitedDiffContainer), + 'hunks': [], 'diffset': self, }) - for hunk in hunks: + for hunk in patch['chunks'][1:]: hunkbit = self.parse_hunk(hunk, source_file, target_file) - hunkbit.filediff = filediff + hunkbit.source_file_path = source_file_path + hunkbit.target_file_path = target_file_path filediff.hunks.append(hunkbit) left_comments = {} - if source_file_path in self.comments_store: for lineno, comments in self.comments_store[source_file_path].items(): left_comments[lineno] = comments @@ -503,8 +501,8 @@ class DiffSet(object): if target_file_path in self.comments_store: for lineno, comments in self.comments_store[target_file_path].items(): left_comments[lineno] = comments + filediff.left_comments = left_comments - filediff.left_comments = left_comments return filediff def parse_hunk(self, hunk, source_file, target_file): @@ -519,6 +517,7 @@ class DiffSet(object): before, after = [], [] for line in hunk['lines']: + if line['action'] == 'unmod': result.lines.extend( self.parse_lines(before, after, source_file, target_file)) @@ -567,7 +566,8 @@ class DiffSet(object): before_tokens = [('nonl', before['line'])] else: before_tokens = self.get_line_tokens( - line_text=before['line'], line_number=before['old_lineno'], + line_text=before['line'], + line_number=before['old_lineno'], file=source_file) original.lineno = before['old_lineno'] original.content = before['line'] diff --git a/rhodecode/lib/db_manage.py b/rhodecode/lib/db_manage.py --- a/rhodecode/lib/db_manage.py +++ b/rhodecode/lib/db_manage.py @@ -319,6 +319,7 @@ class DbManage(object): (RhodeCodeUi.HOOK_PRE_PUSH, 'python:vcsserver.hooks.pre_push'), (RhodeCodeUi.HOOK_PRETX_PUSH, 'python:vcsserver.hooks.pre_push'), (RhodeCodeUi.HOOK_PUSH, 'python:vcsserver.hooks.log_push_action'), + (RhodeCodeUi.HOOK_PUSH_KEY, 'python:vcsserver.hooks.key_push'), ] @@ -363,6 +364,14 @@ class DbManage(object): hgsubversion.ui_active = False self.sa.add(hgsubversion) + # enable hgevolve disabled by default + hgevolve = RhodeCodeUi() + hgevolve.ui_section = 'extensions' + hgevolve.ui_key = 'evolve' + hgevolve.ui_value = '' + hgevolve.ui_active = False + self.sa.add(hgevolve) + # enable hggit disabled by default hggit = RhodeCodeUi() hggit.ui_section = 'extensions' diff --git a/rhodecode/lib/dbmigrate/versions/072_version_4_8_0.py b/rhodecode/lib/dbmigrate/versions/072_version_4_8_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/072_version_4_8_0.py @@ -0,0 +1,61 @@ +import os +import logging +import datetime + +from sqlalchemy import * +from sqlalchemy.exc import DatabaseError +from sqlalchemy.orm import relation, backref, class_mapper, joinedload +from sqlalchemy.orm.session import Session +from sqlalchemy.ext.declarative import declarative_base + +from rhodecode.lib.dbmigrate.migrate import * +from rhodecode.lib.dbmigrate.migrate.changeset import * +from rhodecode.lib.utils2 import str2bool + +from rhodecode.model.meta import Base +from rhodecode.model import meta +from rhodecode.lib.dbmigrate.versions import _reset_base, notify + +log = logging.getLogger(__name__) + + +def get_by_key(cls, key): + return cls.query().filter(cls.ui_key == key).scalar() + + +def get_repos_location(cls): + return get_by_key(cls, '/').ui_value + + +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_7_0_1 as db + + # add last_activity + user_log_table = db.UserLog.__table__ + + user_data = Column('user_data_json', db.JsonType(dialect_map=dict(mysql=UnicodeText(16384)))) + user_data.create(table=user_log_table) + + version = Column("version", String(255), nullable=True, default='v2') + version.create(table=user_log_table) + + action_data = Column('action_data_json', db.JsonType(dialect_map=dict(mysql=UnicodeText(16384)))) + action_data.create(table=user_log_table) + + # issue fixups + fixups(db, meta.Session) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + +def fixups(models, _SESSION): + pass + diff --git a/rhodecode/lib/dbmigrate/versions/073_version_4_8_0.py b/rhodecode/lib/dbmigrate/versions/073_version_4_8_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/073_version_4_8_0.py @@ -0,0 +1,63 @@ +import logging + +from sqlalchemy import * +from rhodecode.model import meta +from rhodecode.lib.dbmigrate.versions import _reset_base, notify + +log = logging.getLogger(__name__) + + +def get_by_key(cls, key): + return cls.query().filter(cls.ui_key == key).scalar() + + +def create_or_update_hook(cls, key, val, SESSION): + new_ui = get_by_key(cls, key) or cls() + new_ui.ui_section = 'hooks' + new_ui.ui_active = True + new_ui.ui_key = key + new_ui.ui_value = val + + SESSION().add(new_ui) + + +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_7_0_0 as db + + # issue fixups + fixups(db, meta.Session) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + +def fixups(models, _SESSION): + + cleanup_if_present = ( + 'pushkey.key_push', + ) + + for hook in cleanup_if_present: + ui_cfg = models.RhodeCodeUi.query().filter( + models.RhodeCodeUi.ui_key == hook).scalar() + if ui_cfg is not None: + log.info('Removing RhodeCodeUI for hook "%s".', hook) + _SESSION().delete(ui_cfg) + + to_add = [ + ('pushkey.key_push', + 'python:vcsserver.hooks.key_push'), + ] + + for hook, value in to_add: + log.info('Adding RhodeCodeUI for hook "%s".', hook) + create_or_update_hook(models.RhodeCodeUi, hook, value, _SESSION) + + _SESSION().commit() diff --git a/rhodecode/lib/dbmigrate/versions/074_version_4_8_0.py b/rhodecode/lib/dbmigrate/versions/074_version_4_8_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/074_version_4_8_0.py @@ -0,0 +1,37 @@ +import logging + +from sqlalchemy import * +from rhodecode.model import meta +from rhodecode.lib.dbmigrate.versions import _reset_base, notify + +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_7_0_1 as db + + repo_review_rule_table = db.RepoReviewRule.__table__ + + forbid_author_to_review = Column( + "forbid_author_to_review", Boolean(), nullable=True, default=False) + forbid_author_to_review.create(table=repo_review_rule_table) + + forbid_adding_reviewers = Column( + "forbid_adding_reviewers", Boolean(), nullable=True, default=False) + forbid_adding_reviewers.create(table=repo_review_rule_table) + + fixups(db, meta.Session) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + +def fixups(models, _SESSION): + pass diff --git a/rhodecode/lib/dbmigrate/versions/075_version_4_8_0.py b/rhodecode/lib/dbmigrate/versions/075_version_4_8_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/075_version_4_8_0.py @@ -0,0 +1,38 @@ +import logging + +from sqlalchemy import * +from rhodecode.model import meta +from rhodecode.lib.dbmigrate.versions import _reset_base, notify + +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_7_0_1 as db + + repo_review_rule_user_table = db.RepoReviewRuleUser.__table__ + repo_review_rule_user_group_table = db.RepoReviewRuleUserGroup.__table__ + + mandatory_user = Column( + "mandatory", Boolean(), nullable=True, default=False) + mandatory_user.create(table=repo_review_rule_user_table) + + mandatory_user_group = Column( + "mandatory", Boolean(), nullable=True, default=False) + mandatory_user_group.create(table=repo_review_rule_user_group_table) + + fixups(db, meta.Session) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + +def fixups(models, _SESSION): + pass diff --git a/rhodecode/lib/dbmigrate/versions/076_version_4_8_0.py b/rhodecode/lib/dbmigrate/versions/076_version_4_8_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/076_version_4_8_0.py @@ -0,0 +1,33 @@ +import logging + +from sqlalchemy import * +from rhodecode.model import meta +from rhodecode.lib.dbmigrate.versions import _reset_base, notify + +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_7_0_1 as db + + pull_request_reviewers = db.PullRequestReviewers.__table__ + + mandatory = Column( + "mandatory", Boolean(), nullable=True, default=False) + mandatory.create(table=pull_request_reviewers) + + fixups(db, meta.Session) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + +def fixups(models, _SESSION): + pass diff --git a/rhodecode/lib/dbmigrate/versions/077_version_4_8_0.py b/rhodecode/lib/dbmigrate/versions/077_version_4_8_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/077_version_4_8_0.py @@ -0,0 +1,40 @@ +import logging + +from sqlalchemy import * +from rhodecode.model import meta +from rhodecode.lib.dbmigrate.versions import _reset_base, notify + +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_7_0_1 as db + + pull_request = db.PullRequest.__table__ + pull_request_version = db.PullRequestVersion.__table__ + + reviewer_data_1 = Column( + 'reviewer_data_json', + db.JsonType(dialect_map=dict(mysql=UnicodeText(16384)))) + reviewer_data_1.create(table=pull_request) + + reviewer_data_2 = Column( + 'reviewer_data_json', + db.JsonType(dialect_map=dict(mysql=UnicodeText(16384)))) + reviewer_data_2.create(table=pull_request_version) + + fixups(db, meta.Session) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + +def fixups(models, _SESSION): + pass diff --git a/rhodecode/lib/dbmigrate/versions/078_version_4_8_0.py b/rhodecode/lib/dbmigrate/versions/078_version_4_8_0.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/078_version_4_8_0.py @@ -0,0 +1,33 @@ +import logging + +from sqlalchemy import * +from rhodecode.model import meta +from rhodecode.lib.dbmigrate.versions import _reset_base, notify + +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_7_0_1 as db + + repo_review_rule_table = db.RepoReviewRule.__table__ + + forbid_commit_author_to_review = Column( + "forbid_commit_author_to_review", Boolean(), nullable=True, default=False) + forbid_commit_author_to_review.create(table=repo_review_rule_table) + + fixups(db, meta.Session) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + +def fixups(models, _SESSION): + pass diff --git a/rhodecode/lib/ext_json_renderer.py b/rhodecode/lib/ext_json_renderer.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/ext_json_renderer.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 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 rhodecode.lib.ext_json import json + + +def pyramid_ext_json(info): + """ + Custom json renderer for pyramid to use our ext_json lib + """ + def _render(value, system): + request = system.get('request') + if request is not None: + response = request.response + ct = response.content_type + if ct == response.default_content_type: + response.content_type = 'application/json' + return json.dumps(value) + + return _render diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -36,6 +36,8 @@ import urlparse import time import string import hashlib +from collections import OrderedDict + import pygments import itertools import fnmatch @@ -891,8 +893,9 @@ 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) + if user.first_name or user.last_name: + return '%s %s <%s>' % ( + user.first_name, user.last_name, email) else: return email else: @@ -1141,14 +1144,14 @@ class InitialsGravatar(object): # first push the email initials prefix, server = email_address.split('@', 1) - # check if prefix is maybe a 'firstname.lastname' syntax + # check if prefix is maybe a 'first_name.last_name' syntax _dot_split = prefix.rsplit('.', 1) if len(_dot_split) == 2: initials = [_dot_split[0][0], _dot_split[1][0]] else: initials = [prefix[0], server[0]] - # then try to replace either firtname or lastname + # then try to replace either first_name or last_name fn_letter = (first_name or " ")[0].strip() ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip() @@ -1245,12 +1248,19 @@ def initials_gravatar(email_address, fir return klass.generate_svg(svg_type=svg_type) -def gravatar_url(email_address, size=30): - # doh, we need to re-import those to mock it later - from pylons import tmpl_context as c +def gravatar_url(email_address, size=30, request=None): + request = get_current_request() + if request and hasattr(request, 'call_context'): + _use_gravatar = request.call_context.visual.use_gravatar + _gravatar_url = request.call_context.visual.gravatar_url + else: + # doh, we need to re-import those to mock it later + from pylons import tmpl_context as c - _use_gravatar = c.visual.use_gravatar - _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL + _use_gravatar = c.visual.use_gravatar + _gravatar_url = c.visual.gravatar_url + + _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL email_address = email_address or User.DEFAULT_USER_EMAIL if isinstance(email_address, unicode): @@ -1532,10 +1542,10 @@ def breadcrumb_repo_link(repo): """ path = [ - link_to(group.name, url('repo_group_home', group_name=group.group_name)) + link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name)) for group in repo.groups_with_parents ] + [ - link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name)) + link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name)) ] return literal(' » '.join(path)) @@ -1602,16 +1612,24 @@ def urlify_commits(text_, repository): def _process_url_func(match_obj, repo_name, uid, entry, - return_raw_data=False): + return_raw_data=False, link_format='html'): pref = '' if match_obj.group().startswith(' '): pref = ' ' issue_id = ''.join(match_obj.groups()) - tmpl = ( - '%(pref)s' - '%(issue-prefix)s%(id-repr)s' - '') + + if link_format == 'html': + tmpl = ( + '%(pref)s' + '%(issue-prefix)s%(id-repr)s' + '') + elif link_format == 'rst': + tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_' + elif link_format == 'markdown': + tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)' + else: + raise ValueError('Bad link_format:{}'.format(link_format)) (repo_name_cleaned, parent_group_name) = RepoGroupModel().\ @@ -1644,7 +1662,12 @@ def _process_url_func(match_obj, repo_na return tmpl % data -def process_patterns(text_string, repo_name, config=None): +def process_patterns(text_string, repo_name, link_format='html'): + allowed_formats = ['html', 'rst', 'markdown'] + if link_format not in allowed_formats: + raise ValueError('Link format can be only one of:{} got {}'.format( + allowed_formats, link_format)) + repo = None if repo_name: # Retrieving repo_name to avoid invalid repo_name to explode on @@ -1656,6 +1679,7 @@ def process_patterns(text_string, repo_n issues_data = [] newtext = text_string + for uid, entry in active_entries.items(): log.debug('found issue tracker entry with uid %s' % (uid,)) @@ -1682,7 +1706,8 @@ def process_patterns(text_string, repo_n issues_data.append(data_func(match_obj)) url_func = partial( - _process_url_func, repo_name=repo_name, entry=entry, uid=uid) + _process_url_func, repo_name=repo_name, entry=entry, uid=uid, + link_format=link_format) newtext = pattern.sub(url_func, newtext) log.debug('processed prefix:uid `%s`' % (uid,)) @@ -1750,7 +1775,8 @@ def renderer_from_filename(filename, exc return None -def render(source, renderer='rst', mentions=False, relative_url=None): +def render(source, renderer='rst', mentions=False, relative_url=None, + repo_name=None): def maybe_convert_relative_links(html_source): if relative_url: @@ -1758,11 +1784,21 @@ def render(source, renderer='rst', menti return html_source if renderer == 'rst': + if repo_name: + # process patterns on comments if we pass in repo name + source, issues = process_patterns( + source, repo_name, link_format='rst') + return literal( '
%s
' % maybe_convert_relative_links( MarkupRenderer.rst(source, mentions=mentions))) elif renderer == 'markdown': + if repo_name: + # process patterns on comments if we pass in repo name + source, issues = process_patterns( + source, repo_name, link_format='markdown') + return literal( '
%s
' % maybe_convert_relative_links( @@ -1801,6 +1837,7 @@ def journal_filter_help(): 'Example filter terms:\n' + ' repository:vcs\n' + ' username:marcin\n' + + ' username:(NOT marcin)\n' + ' action:*push*\n' + ' ip:127.0.0.1\n' + ' date:20120101\n' + @@ -1816,6 +1853,24 @@ def journal_filter_help(): ) +def search_filter_help(searcher): + + terms = '' + return _( + 'Example filter terms for `{searcher}` search:\n' + + '{terms}\n' + + 'Generate wildcards using \'*\' character:\n' + + ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' + + ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' + + '\n' + + 'Optional AND / OR operators in queries\n' + + ' "repo_name:vcs OR repo_name:test"\n' + + ' "owner:test AND repo_name:test*"\n' + + 'More: {search_doc}' + ).format(searcher=searcher.name, + terms=terms, search_doc=searcher.query_lang_doc) + + def not_mapped_error(repo_name): flash(_('%s repository is not mapped to db perhaps' ' it was created or renamed from the filesystem' @@ -1914,24 +1969,24 @@ def get_last_path_part(file_node): return u'../' + path -def route_url(*args, **kwds): +def route_url(*args, **kwargs): """ - Wrapper around pyramids `route_url` function. It is used to generate - URLs from within pylons views or templates. This will be removed when - pyramid migration if finished. + Wrapper around pyramids `route_url` (fully qualified url) 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.route_url(*args, **kwds) + return req.route_url(*args, **kwargs) -def route_path(*args, **kwds): +def route_path(*args, **kwargs): """ 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.route_path(*args, **kwds) + return req.route_path(*args, **kwargs) def route_path_or_none(*args, **kwargs): @@ -1959,3 +2014,23 @@ def resource_path(*args, **kwds): """ req = get_current_request() return req.resource_path(*args, **kwds) + + +def api_call_example(method, args): + """ + Generates an API call example via CURL + """ + args_json = json.dumps(OrderedDict([ + ('id', 1), + ('auth_token', 'SECRET'), + ('method', method), + ('args', args) + ])) + return literal( + "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'" + "

SECRET can be found in auth-tokens page, " + "and needs to be of `api calls` role." + .format( + api_url=route_url('apiv2'), + token_url=route_url('my_account_auth_tokens'), + data=args_json)) 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 @@ -30,7 +30,7 @@ import logging import rhodecode from rhodecode import events from rhodecode.lib import helpers as h -from rhodecode.lib.utils import action_logger +from rhodecode.lib import audit_logger from rhodecode.lib.utils2 import safe_str from rhodecode.lib.exceptions import HTTPLockedRC, UserCreationError from rhodecode.model.db import Repository, User @@ -152,9 +152,15 @@ def pre_pull(extras): def post_pull(extras): """Hook executed after client pulls the code.""" - user = User.get_by_username(extras.username) - action = 'pull' - action_logger(user, action, extras.repository, extras.ip, commit=True) + + audit_user = audit_logger.UserWrap( + username=extras.username, + ip_addr=extras.ip) + repo = audit_logger.RepoWrap(repo_name=extras.repository) + audit_logger.store( + 'user.pull', action_data={ + 'user_agent': extras.user_agent}, + user=audit_user, repo=repo, commit=True) # Propagate to external components. if not is_shadow_repo(extras): @@ -165,6 +171,7 @@ def post_pull(extras): output = '' # make lock is a tri state False, True, None. We only make lock on True if extras.make_lock is True and not is_shadow_repo(extras): + user = User.get_by_username(extras.username) Repository.lock(Repository.get_by_repo_name(extras.repository), user.user_id, lock_reason=Repository.LOCK_PULL) @@ -185,12 +192,17 @@ def post_pull(extras): def post_push(extras): """Hook executed after user pushes to the repository.""" - action_tmpl = extras.action + ':%s' - commit_ids = extras.commit_ids[:29000] + commit_ids = extras.commit_ids - action = action_tmpl % ','.join(commit_ids) - action_logger( - extras.username, action, extras.repository, extras.ip, commit=True) + # log the push call + audit_user = audit_logger.UserWrap( + username=extras.username, ip_addr=extras.ip) + repo = audit_logger.RepoWrap(repo_name=extras.repository) + audit_logger.store( + 'user.push', action_data={ + 'user_agent': extras.user_agent, + 'commit_ids': commit_ids[:10000]}, + user=audit_user, repo=repo, commit=True) # Propagate to external components. if not is_shadow_repo(extras): @@ -220,8 +232,20 @@ def post_push(extras): # 2xx Codes don't raise exceptions output += _http_ret.title + if extras.new_refs: + tmpl = \ + extras.server_url + '/' + \ + extras.repository + \ + "/pull-request/new?{ref_type}={ref_name}" + for branch_name in extras.new_refs['branches']: + output += 'RhodeCode: open pull request link: {}\n'.format( + tmpl.format(ref_type='branch', ref_name=branch_name)) + + for book_name in extras.new_refs['bookmarks']: + output += 'RhodeCode: open pull request link: {}\n'.format( + tmpl.format(ref_type='bookmark', ref_name=book_name)) + output += 'RhodeCode: push completed\n' - return HookResponse(0, output) diff --git a/rhodecode/lib/index/__init__.py b/rhodecode/lib/index/__init__.py --- a/rhodecode/lib/index/__init__.py +++ b/rhodecode/lib/index/__init__.py @@ -33,6 +33,8 @@ default_location = '%(here)s/data/index' class BaseSearch(object): + query_lang_doc = '' + def __init__(self): pass diff --git a/rhodecode/lib/index/whoosh.py b/rhodecode/lib/index/whoosh.py --- a/rhodecode/lib/index/whoosh.py +++ b/rhodecode/lib/index/whoosh.py @@ -61,7 +61,8 @@ log = logging.getLogger(__name__) class Search(BaseSearch): - + # this also shows in UI + query_lang_doc = 'http://whoosh.readthedocs.io/en/latest/querylang.html' name = 'whoosh' def __init__(self, config): 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 @@ -34,6 +34,8 @@ from mako.template import Template as Ma from docutils.core import publish_parts from docutils.parsers.rst import directives +from docutils import writers +from docutils.writers import html4css1 import markdown from rhodecode.lib.markdown_ext import GithubFlavoredMarkdownExtension @@ -46,6 +48,31 @@ log = logging.getLogger(__name__) DEFAULT_COMMENTS_RENDERER = 'rst' +class CustomHTMLTranslator(writers.html4css1.HTMLTranslator): + """ + Custom HTML Translator used for sandboxing potential + JS injections in ref links + """ + + def visit_reference(self, node): + if 'refuri' in node.attributes: + refuri = node['refuri'] + if ':' in refuri: + prefix, link = refuri.lstrip().split(':', 1) + if prefix == 'javascript': + # we don't allow javascript type of refs... + node['refuri'] = 'javascript:alert("SandBoxedJavascript")' + + # old style class requires this... + return html4css1.HTMLTranslator.visit_reference(self, node) + + +class RhodeCodeWriter(writers.html4css1.Writer): + def __init__(self): + writers.Writer.__init__(self) + self.translator_class = CustomHTMLTranslator + + def relative_links(html_source, server_path): if not html_source: return html_source @@ -56,12 +83,12 @@ def relative_links(html_source, server_p return html_source for el in doc.cssselect('img, video'): - src = el.attrib['src'] + src = el.attrib.get('src') if src: el.attrib['src'] = relative_path(src, server_path) for el in doc.cssselect('a:not(.gfm)'): - src = el.attrib['href'] + src = el.attrib.get('href') if src: el.attrib['href'] = relative_path(src, server_path) @@ -341,7 +368,7 @@ class MarkupRenderer(object): directives.register_directive(k, v) parts = publish_parts(source=source, - writer_name="html4css1", + writer=RhodeCodeWriter(), settings_overrides=docutils_settings) return parts['html_title'] + parts["fragment"] 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 @@ -36,7 +36,8 @@ from webob.exc import ( import rhodecode from rhodecode.authentication.base import authenticate, VCS_TYPE from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware -from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context +from rhodecode.lib.base import ( + BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context) from rhodecode.lib.exceptions import ( HTTPLockedRC, HTTPRequirementError, UserCreationError, NotAllowedToCreateUserError) @@ -310,6 +311,7 @@ class SimpleVCS(object): log.debug('Extracted repo name is %s', self.url_repo_name) ip_addr = get_ip_addr(environ) + user_agent = get_user_agent(environ) username = None # skip passing error to error controller @@ -429,9 +431,9 @@ class SimpleVCS(object): fix_PATH() log.info( - '%s action on %s repo "%s" by "%s" from %s', + '%s action on %s repo "%s" by "%s" from %s %s', action, self.SCM, safe_str(self.url_repo_name), - safe_str(username), ip_addr) + safe_str(username), ip_addr, user_agent) return self._generate_vcs_response( environ, start_response, repo_path, extras, action) diff --git a/rhodecode/lib/rcmail/message.py b/rhodecode/lib/rcmail/message.py --- a/rhodecode/lib/rcmail/message.py +++ b/rhodecode/lib/rcmail/message.py @@ -114,14 +114,18 @@ class Message(object): return response + def _get_headers(self): + headers = [self.subject, self.sender] + headers += list(self.send_to) + headers += self.extra_headers.values() + return headers + def is_bad_headers(self): """ Checks for bad headers i.e. newlines in subject, sender or recipients. """ - headers = [self.subject, self.sender] - headers += list(self.send_to) - headers += self.extra_headers.values() + headers = self._get_headers() for val in headers: for c in '\r\n': @@ -144,7 +148,8 @@ class Message(object): raise InvalidMessage("No sender address has been set") if self.is_bad_headers(): - raise BadHeaders + headers = self._get_headers() + raise BadHeaders(headers) def add_recipient(self, recipient): """ diff --git a/rhodecode/lib/repo_maintenance.py b/rhodecode/lib/repo_maintenance.py --- a/rhodecode/lib/repo_maintenance.py +++ b/rhodecode/lib/repo_maintenance.py @@ -83,6 +83,15 @@ class HGVerify(MaintenanceTask): return res +class SVNVerify(MaintenanceTask): + human_name = 'SVN Verify repo' + + def run(self): + instance = self.db_repo.scm_instance() + res = instance.verify() + return res + + class RepoMaintenance(object): """ Performs maintenance of repository based on it's type @@ -90,7 +99,7 @@ class RepoMaintenance(object): tasks = { 'hg': [HGVerify], 'git': [GitGC], - 'svn': [], + 'svn': [SVNVerify], } def get_tasks_for_repo(self, db_repo): diff --git a/rhodecode/lib/system_info.py b/rhodecode/lib/system_info.py --- a/rhodecode/lib/system_info.py +++ b/rhodecode/lib/system_info.py @@ -570,6 +570,10 @@ def rhodecode_config(): log.exception('Failed to read .ini file for display') parsed_ini = {} + cert_path = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(path)))), + '.rccontrol-profile/etc/ca-bundle.crt') + rhodecode_ini_safe['server:main'] = parsed_ini blacklist = [ @@ -608,7 +612,8 @@ def rhodecode_config(): rhodecode_ini_safe.pop(k, None) # TODO: maybe put some CONFIG checks here ? - return SysInfoRes(value={'config': rhodecode_ini_safe, 'path': path}) + return SysInfoRes(value={'config': rhodecode_ini_safe, + 'path': path, 'cert_path': cert_path}) def database_info(): diff --git a/rhodecode/lib/user_log_filter.py b/rhodecode/lib/user_log_filter.py --- a/rhodecode/lib/user_log_filter.py +++ b/rhodecode/lib/user_log_filter.py @@ -23,10 +23,10 @@ import logging from whoosh.qparser.default import QueryParser, query from whoosh.qparser.dateparse import DateParserPlugin from whoosh.fields import (TEXT, Schema, DATETIME) -from sqlalchemy.sql.expression import or_, and_, func +from sqlalchemy.sql.expression import or_, and_, not_, func from rhodecode.model.db import UserLog -from rhodecode.lib.utils2 import remove_prefix, remove_suffix +from rhodecode.lib.utils2 import remove_prefix, remove_suffix, safe_unicode # JOURNAL SCHEMA used only to generate queries in journal. We use whoosh # querylang to build sql queries and filter journals @@ -54,7 +54,7 @@ def user_log_filter(user_log, search_ter if search_term: qp = QueryParser('repository', schema=JOURNAL_SCHEMA) qp.add_plugin(DateParserPlugin()) - qry = qp.parse(unicode(search_term)) + qry = qp.parse(safe_unicode(search_term)) log.debug('Filtering using parsed query %r' % qry) def wildcard_handler(col, wc_term): @@ -89,16 +89,27 @@ def user_log_filter(user_log, search_ter return func.lower(field).startswith(func.lower(val)) elif isinstance(term, query.DateRange): return and_(field >= val[0], field <= val[1]) + elif isinstance(term, query.Not): + return not_(field == val) return func.lower(field) == func.lower(val) - if isinstance(qry, (query.And, query.Term, query.Prefix, query.Wildcard, - query.DateRange)): + if isinstance(qry, (query.And, query.Not, query.Term, query.Prefix, + query.Wildcard, query.DateRange)): if not isinstance(qry, query.And): qry = [qry] + for term in qry: - field = term.fieldname - val = (term.text if not isinstance(term, query.DateRange) - else [term.startdate, term.enddate]) + if isinstance(term, query.Not): + not_term = [z for z in term.leaves()][0] + field = not_term.fieldname + val = not_term.text + elif isinstance(term, query.DateRange): + field = term.fieldname + val = [term.startdate, term.enddate] + else: + field = term.fieldname + val = term.text + user_log = user_log.filter(get_filterion(field, val, term)) elif isinstance(qry, query.Or): filters = [] diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -96,10 +96,11 @@ def repo_name_slug(value): # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS #============================================================================== def get_repo_slug(request): - if isinstance(request, Request) and getattr(request, 'matchdict', None): + if isinstance(request, Request) and getattr(request, 'db_repo', None): # pyramid - _repo = request.matchdict.get('repo_name') + _repo = request.db_repo.repo_name else: + # TODO(marcink): remove after pylons migration... _repo = request.environ['pylons.routes_dict'].get('repo_name') if _repo: @@ -110,7 +111,7 @@ def get_repo_slug(request): def get_repo_group_slug(request): if isinstance(request, Request) and getattr(request, 'matchdict', None): # pyramid - _group = request.matchdict.get('group_name') + _group = request.matchdict.get('repo_group_name') else: _group = request.environ['pylons.routes_dict'].get('group_name') @@ -138,68 +139,6 @@ def get_user_group_slug(request): return _group -def action_logger(user, action, repo, ipaddr='', sa=None, commit=False): - """ - Action logger for various actions made by users - - :param user: user that made this action, can be a unique username string or - object containing user_id attribute - :param action: action to log, should be on of predefined unique actions for - easy translations - :param repo: string name of repository or object containing repo_id, - that action was made on - :param ipaddr: optional ip address from what the action was made - :param sa: optional sqlalchemy session - - """ - - if not sa: - sa = meta.Session() - # if we don't get explicit IP address try to get one from registered user - # in tmpl context var - if not ipaddr: - ipaddr = getattr(get_current_rhodecode_user(), 'ip_addr', '') - - try: - if getattr(user, 'user_id', None): - user_obj = User.get(user.user_id) - elif isinstance(user, basestring): - user_obj = User.get_by_username(user) - else: - raise Exception('You have to provide a user object or a username') - - if getattr(repo, 'repo_id', None): - repo_obj = Repository.get(repo.repo_id) - repo_name = repo_obj.repo_name - elif isinstance(repo, basestring): - repo_name = repo.lstrip('/') - repo_obj = Repository.get_by_repo_name(repo_name) - else: - repo_obj = None - repo_name = '' - - user_log = UserLog() - user_log.user_id = user_obj.user_id - user_log.username = user_obj.username - action = safe_unicode(action) - user_log.action = action[:1200000] - - user_log.repository = repo_obj - user_log.repository_name = repo_name - - user_log.action_date = datetime.datetime.now() - user_log.user_ip = ipaddr - sa.add(user_log) - - log.info('Logging action:`%s` on repo:`%s` by user:%s ip:%s', - action, safe_unicode(repo), user_obj, ipaddr) - if commit: - sa.commit() - except Exception: - log.error(traceback.format_exc()) - raise - - def get_filesystem_repos(path, recursive=False, skip_removed_repos=True): """ Scans given path for repos and return (name,(type,path)) tuple @@ -428,6 +367,7 @@ def config_data_from_db(clear_session=Tr if 'push' not in enabled_hook_classes: skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH)) skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH)) + skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY)) config = [entry for entry in config if entry[:2] not in skip_entries] diff --git a/rhodecode/lib/utils2.py b/rhodecode/lib/utils2.py --- a/rhodecode/lib/utils2.py +++ b/rhodecode/lib/utils2.py @@ -572,7 +572,8 @@ def credentials_filter(uri): return ''.join(uri) -def get_clone_url(uri_tmpl, qualifed_home_url, repo_name, repo_id, **override): +def get_clone_url(request, uri_tmpl, repo_name, repo_id, **override): + qualifed_home_url = request.route_url('home') parsed_url = urlobject.URLObject(qualifed_home_url) decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/'))) args = { 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 @@ -130,7 +130,7 @@ class UpdateFailureReason(object): NO_CHANGE = 2 # The pull request has a reference type that is not supported for update. - WRONG_REF_TPYE = 3 + WRONG_REF_TYPE = 3 # Update failed because the target reference is missing. MISSING_TARGET_REF = 4 @@ -1065,8 +1065,8 @@ class BaseCommit(object): def no_node_at_path(self, path): return NodeDoesNotExistError( - "There is no file nor directory at the given path: " - "'%s' at commit %s" % (path, self.short_id)) + u"There is no file nor directory at the given path: " + u"`%s` at commit %s" % (safe_unicode(path), self.short_id)) def _fix_path(self, path): """ diff --git a/rhodecode/lib/vcs/backends/hg/commit.py b/rhodecode/lib/vcs/backends/hg/commit.py --- a/rhodecode/lib/vcs/backends/hg/commit.py +++ b/rhodecode/lib/vcs/backends/hg/commit.py @@ -160,6 +160,27 @@ class MercurialCommit(base.BaseCommit): return self._make_commits(parents) @LazyProperty + def phase(self): + phase_id = self._remote.ctx_phase(self.idx) + phase_text = { + 0: 'public', + 1: 'draft', + 2: 'secret', + }.get(phase_id) or '' + + return safe_unicode(phase_text) + + @LazyProperty + def obsolete(self): + obsolete = self._remote.ctx_obsolete(self.idx) + return obsolete + + @LazyProperty + def hidden(self): + hidden = self._remote.ctx_hidden(self.idx) + return hidden + + @LazyProperty def children(self): """ Returns list of child commits. diff --git a/rhodecode/lib/vcs/backends/svn/repository.py b/rhodecode/lib/vcs/backends/svn/repository.py --- a/rhodecode/lib/vcs/backends/svn/repository.py +++ b/rhodecode/lib/vcs/backends/svn/repository.py @@ -158,6 +158,12 @@ class SubversionRepository(base.BaseRepo return commit_id1 return commit_id2 + def verify(self): + verify = self._remote.verify() + + self._remote.invalidate_vcs_cache() + return verify + def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None): # TODO: johbo: Implement better comparison, this is a very naive # version which does not allow to compare branches, tags or folders diff --git a/rhodecode/model/auth_token.py b/rhodecode/model/auth_token.py --- a/rhodecode/model/auth_token.py +++ b/rhodecode/model/auth_token.py @@ -59,23 +59,25 @@ class AuthTokenModel(BaseModel): return new_auth_token - def delete(self, api_key, user=None): + def delete(self, auth_token_id, user=None): """ Deletes given api_key, if user is set it also filters the object for deletion by given user. """ - api_key = UserApiKeys.query().filter(UserApiKeys.api_key == api_key) + auth_token = UserApiKeys.query().filter( + UserApiKeys.user_api_key_id == auth_token_id) if user: user = self._get_user(user) - api_key = api_key.filter(UserApiKeys.user_id == user.user_id) + auth_token = auth_token.filter(UserApiKeys.user_id == user.user_id) + auth_token = auth_token.scalar() - api_key = api_key.scalar() - try: - Session().delete(api_key) - except Exception: - log.error(traceback.format_exc()) - raise + if auth_token: + try: + Session().delete(auth_token) + except Exception: + log.error(traceback.format_exc()) + raise def get_auth_tokens(self, user, show_expired=True): user = self._get_user(user) diff --git a/rhodecode/model/changeset_status.py b/rhodecode/model/changeset_status.py --- a/rhodecode/model/changeset_status.py +++ b/rhodecode/model/changeset_status.py @@ -80,7 +80,7 @@ class ChangesetStatusModel(BaseModel): """ votes = defaultdict(int) reviewers_number = len(statuses_by_reviewers) - for user, reasons, statuses in statuses_by_reviewers: + for user, reasons, mandatory, statuses in statuses_by_reviewers: if statuses: ver, latest = statuses[0] votes[latest.status] += 1 @@ -248,13 +248,14 @@ class ChangesetStatusModel(BaseModel): for o in pull_request.reviewers: if not o.user: continue - st = commit_statuses.get(o.user.username, None) - if st: - st = [(x, list(y)[0]) - for x, y in (itertools.groupby(sorted(st, key=version), - version))] + statuses = commit_statuses.get(o.user.username, None) + if statuses: + statuses = [(x, list(y)[0]) + for x, y in (itertools.groupby( + sorted(statuses, key=version),version))] - pull_request_reviewers.append((o.user, o.reasons, st)) + pull_request_reviewers.append( + (o.user, o.reasons, o.mandatory, statuses)) return pull_request_reviewers def calculated_review_status(self, pull_request, reviewers_statuses=None): diff --git a/rhodecode/model/comment.py b/rhodecode/model/comment.py --- a/rhodecode/model/comment.py +++ b/rhodecode/model/comment.py @@ -29,14 +29,14 @@ import collections from datetime import datetime from pylons.i18n.translation import _ -from pyramid.threadlocal import get_current_registry +from pyramid.threadlocal import get_current_registry, get_current_request from sqlalchemy.sql.expression import null from sqlalchemy.sql.functions import coalesce from rhodecode.lib import helpers as h, diffs +from rhodecode.lib import audit_logger from rhodecode.lib.channelstream import channelstream_request -from rhodecode.lib.utils import action_logger -from rhodecode.lib.utils2 import extract_mentioned_users +from rhodecode.lib.utils2 import extract_mentioned_users, safe_str from rhodecode.model import BaseModel from rhodecode.model.db import ( ChangesetComment, User, Notification, PullRequest, AttributeDict) @@ -163,6 +163,13 @@ class CommentsModel(BaseModel): return todos + def _log_audit_action(self, action, action_data, user, comment): + audit_logger.store( + action=action, + action_data=action_data, + user=user, + repo=comment.repo) + def create(self, text, repo, user, commit_id=None, pull_request=None, f_path=None, line_no=None, status_change=None, status_change_type=None, comment_type=None, @@ -268,8 +275,7 @@ class CommentsModel(BaseModel): target_repo_url = h.link_to( repo.repo_name, - h.url('summary_home', - repo_name=repo.repo_name, qualified=True)) + h.route_url('repo_summary', repo_name=repo.repo_name)) # commit specifics kwargs.update({ @@ -300,13 +306,11 @@ class CommentsModel(BaseModel): 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_target_repo_url = h.route_url( + 'repo_summary', repo_name=pr_target_repo.repo_name) - pr_source_repo_url = h.url( - 'summary_home', repo_name=pr_source_repo.repo_name, - qualified=True) + pr_source_repo_url = h.route_url( + 'repo_summary', repo_name=pr_source_repo.repo_name) # pull request specifics kwargs.update({ @@ -340,13 +344,15 @@ class CommentsModel(BaseModel): email_kwargs=kwargs, ) - action = ( - 'user_commented_pull_request:{}'.format( - comment.pull_request.pull_request_id) - if comment.pull_request - else 'user_commented_revision:{}'.format(comment.revision) - ) - action_logger(user, action, comment.repo) + Session().flush() + if comment.pull_request: + action = 'repo.pull_request.comment.create' + else: + action = 'repo.commit.comment.create' + + comment_data = comment.get_api_data() + self._log_audit_action( + action, {'data': comment_data}, user, comment) registry = get_current_registry() rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {}) @@ -388,15 +394,22 @@ class CommentsModel(BaseModel): return comment - def delete(self, comment): + def delete(self, comment, user): """ Deletes given comment - - :param comment_id: """ comment = self.__get_commit_comment(comment) + old_data = comment.get_api_data() Session().delete(comment) + if comment.pull_request: + action = 'repo.pull_request.comment.delete' + else: + action = 'repo.commit.comment.delete' + + self._log_audit_action( + action, {'old_data': old_data}, user, comment) + return comment def get_all_comments(self, repo_id, revision=None, pull_request=None): @@ -412,22 +425,39 @@ class CommentsModel(BaseModel): q = q.order_by(ChangesetComment.created_on) return q.all() - def get_url(self, comment): + def get_url(self, comment, request=None, permalink=False): + if not request: + request = get_current_request() + 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,) + pull_request = comment.pull_request + if permalink: + return request.route_url( + 'pull_requests_global', + pull_request_id=pull_request.pull_request_id, + _anchor='comment-%s' % comment.comment_id) + else: + return request.route_url('pullrequest_show', + repo_name=safe_str(pull_request.target_repo.repo_name), + pull_request_id=pull_request.pull_request_id, + _anchor='comment-%s' % comment.comment_id) + else: - return h.url( - 'changeset_home', - repo_name=comment.repo.repo_name, - revision=comment.revision, - anchor='comment-%s' % comment.comment_id, - qualified=True,) + repo = comment.repo + commit_id = comment.revision + + if permalink: + return request.route_url( + 'repo_commit', repo_name=safe_str(repo.repo_id), + commit_id=commit_id, + _anchor='comment-%s' % comment.comment_id) + + else: + return request.route_url( + 'repo_commit', repo_name=safe_str(repo.repo_name), + commit_id=commit_id, + _anchor='comment-%s' % comment.comment_id) def get_comments(self, repo_id, revision=None, pull_request=None): """ diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -44,8 +44,8 @@ from sqlalchemy.sql.expression import tr from beaker.cache import cache_region from zope.cachedescriptors.property import Lazy as LazyProperty -from pylons import url from pylons.i18n.translation import lazy_ugettext as _ +from pyramid.threadlocal import get_current_request from rhodecode.lib.vcs import get_vcs_instance from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference @@ -358,6 +358,7 @@ class RhodeCodeUi(Base, BaseModel): HOOK_PRE_PUSH = 'prechangegroup.pre_push' HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push' HOOK_PUSH = 'changegroup.push_logger' + HOOK_PUSH_KEY = 'pushkey.key_push' # TODO: johbo: Unify way how hooks are configured for git and hg, # git part is currently hardcoded. @@ -571,6 +572,20 @@ class User(Base, BaseModel): self._email = val.lower() if val else None @hybrid_property + def first_name(self): + from rhodecode.lib import helpers as h + if self.name: + return h.escape(self.name) + return self.name + + @hybrid_property + def last_name(self): + from rhodecode.lib import helpers as h + if self.lastname: + return h.escape(self.lastname) + return self.lastname + + @hybrid_property def api_key(self): """ Fetch if exist an auth-token with role ALL connected to this user @@ -689,7 +704,7 @@ class User(Base, BaseModel): @property def username_and_name(self): - return '%s (%s %s)' % (self.username, self.firstname, self.lastname) + return '%s (%s %s)' % (self.username, self.first_name, self.last_name) @property def username_or_name_or_email(self): @@ -698,20 +713,20 @@ class User(Base, BaseModel): @property def full_name(self): - return '%s %s' % (self.firstname, self.lastname) + return '%s %s' % (self.first_name, self.last_name) @property def full_name_or_username(self): - return ('%s %s' % (self.firstname, self.lastname) - if (self.firstname and self.lastname) else self.username) + return ('%s %s' % (self.first_name, self.last_name) + if (self.first_name and self.last_name) else self.username) @property def full_contact(self): - return '%s %s <%s>' % (self.firstname, self.lastname, self.email) + return '%s %s <%s>' % (self.first_name, self.last_name, self.email) @property def short_contact(self): - return '%s %s' % (self.firstname, self.lastname) + return '%s %s' % (self.first_name, self.last_name) @property def is_admin(self): @@ -761,9 +776,9 @@ class User(Base, BaseModel): if val: return val else: + cache_key = "get_user_by_name_%s" % _hash_key(username) q = q.options( - FromCache("sql_cache_short", - "get_user_by_name_%s" % _hash_key(username))) + FromCache("sql_cache_short", cache_key)) return q.scalar() @@ -774,8 +789,8 @@ class User(Base, BaseModel): .filter(or_(UserApiKeys.expires == -1, UserApiKeys.expires >= time.time())) if cache: - q = q.options(FromCache("sql_cache_short", - "get_auth_token_%s" % auth_token)) + q = q.options( + FromCache("sql_cache_short", "get_auth_token_%s" % auth_token)) match = q.first() if match: @@ -790,9 +805,10 @@ class User(Base, BaseModel): else: q = cls.query().filter(cls.email == email) + email_key = _hash_key(email) if cache: - q = q.options(FromCache("sql_cache_short", - "get_email_key_%s" % _hash_key(email))) + q = q.options( + FromCache("sql_cache_short", "get_email_key_%s" % email_key)) ret = q.scalar() if ret is None: @@ -804,8 +820,8 @@ class User(Base, BaseModel): 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)) + q = q.options( + FromCache("sql_cache_short", "get_email_map_key_%s" % email_key)) ret = getattr(q.scalar(), 'user', None) return ret @@ -872,10 +888,16 @@ class User(Base, BaseModel): .order_by(User.username.asc()).all() @classmethod - def get_default_user(cls, cache=False): + def get_default_user(cls, cache=False, refresh=False): user = User.get_by_username(User.DEFAULT_USER, cache=cache) if user is None: raise Exception('FATAL: Missing default account!') + if refresh: + # The default user might be based on outdated state which + # has been loaded from the cache. + # A call to refresh() ensures that the + # latest state from the database is used. + Session().refresh(user) return user def _get_default_perms(self, user, suffix=''): @@ -996,6 +1018,19 @@ class UserApiKeys(Base, BaseModel): } return data + def get_api_data(self, include_secrets=False): + data = self.__json__() + if include_secrets: + return data + else: + data['auth_token'] = self.token_obfuscated + return data + + @hybrid_property + def description_safe(self): + from rhodecode.lib import helpers as h + return h.escape(self.description) + @property def expired(self): if self.expires == -1: @@ -1027,6 +1062,11 @@ class UserApiKeys(Base, BaseModel): def scope_humanized(self): return self._get_scope() + @property + def token_obfuscated(self): + if self.api_key: + return self.api_key[:4] + "****" + class UserEmailMap(Base, BaseModel): __tablename__ = 'user_email_map' @@ -1076,6 +1116,11 @@ class UserIpMap(Base, BaseModel): description = Column("description", String(10000), nullable=True, unique=None, default=None) user = relationship('User', lazy='joined') + @hybrid_property + def description_safe(self): + from rhodecode.lib import helpers as h + return h.escape(self.description) + @classmethod def _get_ip_range(cls, ip_addr): net = ipaddress.ip_network(ip_addr, strict=False) @@ -1098,6 +1143,10 @@ class UserLog(Base, BaseModel): {'extend_existing': True, 'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, ) + VERSION_1 = 'v1' + VERSION_2 = 'v2' + VERSIONS = [VERSION_1, VERSION_2] + 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) @@ -1107,6 +1156,10 @@ class UserLog(Base, BaseModel): 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) + version = Column("version", String(255), nullable=True, default=VERSION_1) + user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384))))) + action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384))))) + def __unicode__(self): return u"<%s('id:%s:%s')>" % ( self.__class__.__name__, self.repository_name, self.action) @@ -1156,6 +1209,11 @@ class UserGroup(Base, BaseModel): user = relationship('User') @hybrid_property + def description_safe(self): + from rhodecode.lib import helpers as h + return h.escape(self.description) + + @hybrid_property def group_data(self): if not self._group_data: return {} @@ -1187,17 +1245,16 @@ class UserGroup(Base, BaseModel): 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))) + 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)) + 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): @@ -1454,6 +1511,11 @@ class Repository(Base, BaseModel): safe_unicode(self.repo_name)) @hybrid_property + def description_safe(self): + from rhodecode.lib import helpers as h + return h.escape(self.description) + + @hybrid_property def landing_rev(self): # always should return [rev_type, rev] if self._landing_revision: @@ -1538,9 +1600,9 @@ class Repository(Base, BaseModel): if val: return val else: + cache_key = "get_repo_by_name_%s" % _hash_key(repo_name) q = q.options( - FromCache("sql_cache_short", - "get_repo_by_name_%s" % _hash_key(repo_name))) + FromCache("sql_cache_short", cache_key)) return q.scalar() @@ -1750,6 +1812,7 @@ class Repository(Base, BaseModel): # TODO: mikhail: Here there is an anti-pattern, we probably need to # move this methods on models level. from rhodecode.model.settings import SettingsModel + from rhodecode.model.repo import RepoModel repo = self _user_id, _time, _reason = self.locked @@ -1759,13 +1822,14 @@ 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), + 'url': RepoModel().get_url(self), 'private': repo.private, 'created_on': repo.created_on, - 'description': repo.description, + 'description': repo.description_safe, 'landing_rev': repo.landing_rev, 'owner': repo.user.username, 'fork_of': repo.fork.repo_name if repo.fork else None, + 'fork_of_id': repo.fork.repo_id if repo.fork else None, 'enable_statistics': repo.enable_statistics, 'enable_locking': repo.enable_locking, 'enable_downloads': repo.enable_downloads, @@ -1893,7 +1957,6 @@ class Repository(Base, BaseModel): return clone_uri def clone_url(self, **override): - qualified_home_url = url('home', qualified=True) uri_tmpl = None if 'with_id' in override: @@ -1915,8 +1978,9 @@ class Repository(Base, BaseModel): # ie, not having tmpl_context set up pass - return get_clone_url(uri_tmpl=uri_tmpl, - qualifed_home_url=qualified_home_url, + request = get_current_request() + return get_clone_url(request=request, + uri_tmpl=uri_tmpl, repo_name=self.repo_name, repo_id=self.repo_id, **override) @@ -2160,8 +2224,13 @@ class RepoGroup(Base, BaseModel): self.parent_group = parent_group def __unicode__(self): - return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id, - self.group_name) + return u"<%s('id:%s:%s')>" % ( + self.__class__.__name__, self.group_id, self.group_name) + + @hybrid_property + def description_safe(self): + from rhodecode.lib import helpers as h + return h.escape(self.group_description) @classmethod def _generate_choice(cls, repo_group): @@ -2176,7 +2245,7 @@ class RepoGroup(Base, BaseModel): repo_groups = [] if show_empty_group: - repo_groups = [('-1', u'-- %s --' % _('No parent'))] + repo_groups = [(-1, u'-- %s --' % _('No parent'))] repo_groups.extend([cls._generate_choice(x) for x in groups]) @@ -2196,16 +2265,19 @@ class RepoGroup(Base, BaseModel): 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))) + name_key = _hash_key(group_name) + gr = gr.options( + FromCache("sql_cache_short", "get_group_%s" % name_key)) return gr.scalar() @classmethod def get_user_personal_repo_group(cls, user_id): user = User.get(user_id) + if user.username == User.DEFAULT_USER: + return None + return cls.query()\ - .filter(cls.personal == true())\ + .filter(cls.personal == true()) \ .filter(cls.user == user).scalar() @classmethod @@ -2389,7 +2461,7 @@ class RepoGroup(Base, BaseModel): data = { 'group_id': group.group_id, 'group_name': group.group_name, - 'group_description': group.group_description, + 'group_description': group.description_safe, '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, @@ -3088,20 +3160,39 @@ class ChangesetComment(Base, BaseModel): def is_todo(self): return self.comment_type == self.COMMENT_TYPE_TODO + @property + def is_inline(self): + return self.line_no and self.f_path + def get_index_version(self, versions): return self.get_index_from_version( self.pull_request_version_id, versions) - 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) + def get_api_data(self): + comment = self + data = { + 'comment_id': comment.comment_id, + 'comment_type': comment.comment_type, + 'comment_text': comment.text, + 'comment_status': comment.status_change, + 'comment_f_path': comment.f_path, + 'comment_lineno': comment.line_no, + 'comment_author': comment.author, + 'comment_created_on': comment.created_on + } + return data + + def __json__(self): + data = dict() + data.update(self.get_api_data()) + return data + class ChangesetStatus(Base, BaseModel): __tablename__ = 'changeset_statuses' @@ -3153,6 +3244,19 @@ class ChangesetStatus(Base, BaseModel): def status_lbl(self): return ChangesetStatus.get_status_lbl(self.status) + def get_api_data(self): + status = self + data = { + 'status_id': status.changeset_status_id, + 'status': status.status, + } + return data + + def __json__(self): + data = dict() + data.update(self.get_api_data()) + return data + class _PullRequestBase(BaseModel): """ @@ -3215,6 +3319,19 @@ class _PullRequestBase(BaseModel): _last_merge_status = Column('merge_status', Integer(), nullable=True) merge_rev = Column('merge_rev', String(40), nullable=True) + reviewer_data = Column( + 'reviewer_data_json', MutationObj.as_mutable( + JsonType(dialect_map=dict(mysql=UnicodeText(16384))))) + + @property + def reviewer_data_json(self): + return json.dumps(self.reviewer_data) + + @hybrid_property + def description_safe(self): + from rhodecode.lib import helpers as h + return h.escape(self.description) + @hybrid_property def revisions(self): return self._revisions.split(':') if self._revisions else [] @@ -3276,14 +3393,19 @@ class _PullRequestBase(BaseModel): else: return None - def get_api_data(self): + def get_api_data(self, with_merge_state=True): from rhodecode.model.pull_request import PullRequestModel + pull_request = self - merge_status = PullRequestModel().merge_status(pull_request) - - pull_request_url = url( - 'pullrequest_show', repo_name=self.target_repo.repo_name, - pull_request_id=self.pull_request_id, qualified=True) + if with_merge_state: + merge_status = PullRequestModel().merge_status(pull_request) + merge_state = { + 'status': merge_status[0], + 'message': safe_unicode(merge_status[1]), + } + else: + merge_state = {'status': 'not_available', + 'message': 'not_available'} merge_data = { 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request), @@ -3294,7 +3416,7 @@ class _PullRequestBase(BaseModel): data = { 'pull_request_id': pull_request.pull_request_id, - 'url': pull_request_url, + 'url': PullRequestModel().get_url(pull_request), 'title': pull_request.title, 'description': pull_request.description, 'status': pull_request.status, @@ -3302,10 +3424,7 @@ class _PullRequestBase(BaseModel): '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]), - }, + 'mergeable': merge_state, 'source': { 'clone_url': pull_request.source_repo.clone_url(), 'repository': pull_request.source_repo.repo_name, @@ -3334,7 +3453,8 @@ class _PullRequestBase(BaseModel): 'reasons': reasons, 'review_status': st[0][1].status if st else 'not_reviewed', } - for reviewer, reasons, st in pull_request.reviewers_statuses() + for reviewer, reasons, mandatory, st in + pull_request.reviewers_statuses() ] } @@ -3359,7 +3479,8 @@ class PullRequest(Base, _PullRequestBase reviewers = relationship('PullRequestReviewers', cascade="all, delete, delete-orphan") - statuses = relationship('ChangesetStatus') + statuses = relationship('ChangesetStatus', + cascade="all, delete, delete-orphan") comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan") versions = relationship('PullRequestVersion', @@ -3424,6 +3545,8 @@ class PullRequest(Base, _PullRequestBase attrs.revisions = pull_request_obj.revisions attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref + attrs.reviewer_data = org_pull_request_obj.reviewer_data + attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json return PullRequestDisplay(attrs, internal=internal_methods) @@ -3502,11 +3625,6 @@ class PullRequestReviewers(Base, BaseMod 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}, ) - def __init__(self, user=None, pull_request=None, reasons=None): - self.user = user - self.pull_request = pull_request - self.reasons = reasons or [] - @hybrid_property def reasons(self): if not self._reasons: @@ -3531,7 +3649,7 @@ class PullRequestReviewers(Base, BaseMod _reasons = Column( 'reason', MutationList.as_mutable( JsonType('list', dialect_map=dict(mysql=UnicodeText(16384))))) - + mandatory = Column("mandatory", Boolean(), nullable=False, default=False) user = relationship('User') pull_request = relationship('PullRequest') @@ -3651,6 +3769,11 @@ class Gist(Base, BaseModel): def __repr__(self): return '' % (self.gist_type, self.gist_access_id) + @hybrid_property + def description_safe(self): + from rhodecode.lib import helpers as h + return h.escape(self.gist_description) + @classmethod def get_or_404(cls, id_, pyramid_exc=False): @@ -3670,6 +3793,8 @@ class Gist(Base, BaseModel): def gist_url(self): import rhodecode + from pylons import url + alias_url = rhodecode.CONFIG.get('gist_alias_url') if alias_url: return alias_url.replace('{gistid}', self.gist_access_id) @@ -3835,14 +3960,17 @@ class RepoReviewRuleUser(Base, BaseModel {'extend_existing': True, 'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,} ) - repo_review_rule_user_id = Column( - 'repo_review_rule_user_id', Integer(), primary_key=True) - repo_review_rule_id = Column("repo_review_rule_id", - Integer(), ForeignKey('repo_review_rules.repo_review_rule_id')) - user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), - nullable=False) + repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True) + repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id')) + user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False) + mandatory = Column("mandatory", Boolean(), nullable=False, default=False) user = relationship('User') + def rule_data(self): + return { + 'mandatory': self.mandatory + } + class RepoReviewRuleUserGroup(Base, BaseModel): __tablename__ = 'repo_review_rules_users_groups' @@ -3850,14 +3978,17 @@ class RepoReviewRuleUserGroup(Base, Base {'extend_existing': True, 'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,} ) - repo_review_rule_users_group_id = Column( - 'repo_review_rule_users_group_id', Integer(), primary_key=True) - repo_review_rule_id = Column("repo_review_rule_id", - Integer(), ForeignKey('repo_review_rules.repo_review_rule_id')) - users_group_id = Column("users_group_id", Integer(), - ForeignKey('users_groups.users_group_id'), nullable=False) + repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True) + repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id')) + users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False) + mandatory = Column("mandatory", Boolean(), nullable=False, default=False) users_group = relationship('UserGroup') + def rule_data(self): + return { + 'mandatory': self.mandatory + } + class RepoReviewRule(Base, BaseModel): __tablename__ = 'repo_review_rules' @@ -3872,13 +4003,14 @@ class RepoReviewRule(Base, BaseModel): "repo_id", Integer(), ForeignKey('repositories.repo_id')) repo = relationship('Repository', backref='review_rules') - _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), - default=u'*') # glob - _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), - default=u'*') # glob - - use_authors_for_review = Column("use_authors_for_review", Boolean(), - nullable=False, default=False) + _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob + _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob + + use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False) + forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False) + forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False) + forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False) + rule_users = relationship('RepoReviewRuleUser') rule_user_groups = relationship('RepoReviewRuleUserGroup') @@ -3934,16 +4066,32 @@ class RepoReviewRule(Base, BaseModel): def review_users(self): """ Returns the users which this rule applies to """ - users = set() - users |= set([ - rule_user.user for rule_user in self.rule_users - if rule_user.user.active]) - users |= set( - member.user - for rule_user_group in self.rule_user_groups - for member in rule_user_group.users_group.members - if member.user.active - ) + users = collections.OrderedDict() + + for rule_user in self.rule_users: + if rule_user.user.active: + if rule_user.user not in users: + users[rule_user.user.username] = { + 'user': rule_user.user, + 'source': 'user', + 'source_data': {}, + 'data': rule_user.rule_data() + } + + for rule_user_group in self.rule_user_groups: + source_data = { + 'name': rule_user_group.users_group.users_group_name, + 'members': len(rule_user_group.users_group.members) + } + for member in rule_user_group.users_group.members: + if member.user.active: + users[member.user.username] = { + 'user': member.user, + 'source': 'user_group', + 'source_data': source_data, + 'data': rule_user_group.rule_data() + } + return users def __repr__(self): diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -242,7 +242,7 @@ def RepoForm(edit=False, old_data=None, allow_extra_fields = True filter_extra_fields = False repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True), - v.SlugifyName()) + v.SlugifyName(), v.CannotHaveGitSuffix()) repo_group = All(v.CanWriteGroup(old_data), v.OneOf(repo_groups, hideList=True)) repo_type = v.OneOf(supported_backends, required=False, @@ -384,6 +384,7 @@ class _BaseVcsSettingsForm(formencode.Sc # hg extensions_largefiles = v.StringBoolean(if_missing=False) + extensions_evolve = v.StringBoolean(if_missing=False) phases_publish = v.StringBoolean(if_missing=False) rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False) @@ -534,12 +535,13 @@ def PullRequestForm(repo_id): class ReviewerForm(formencode.Schema): user_id = v.Int(not_empty=True) reasons = All() + mandatory = v.StringBoolean() class _PullRequestForm(formencode.Schema): allow_extra_fields = True filter_extra_fields = True - user = v.UnicodeString(strip=True, required=True) + common_ancestor = v.UnicodeString(strip=True, required=True) source_repo = v.UnicodeString(strip=True, required=True) source_ref = v.UnicodeString(strip=True, required=True) target_repo = v.UnicodeString(strip=True, required=True) diff --git a/rhodecode/model/integration.py b/rhodecode/model/integration.py --- a/rhodecode/model/integration.py +++ b/rhodecode/model/integration.py @@ -96,6 +96,9 @@ class IntegrationModel(BaseModel): """ Send an event to an integration """ handler = self.get_integration_handler(integration) if handler: + log.debug( + 'events: sending event %s on integration %s using handler %s', + event, integration, handler) handler.send_event(event) def get_integrations(self, scope, IntegrationType=None): @@ -200,14 +203,14 @@ class IntegrationModel(BaseModel): query = query.filter(or_(*clauses)) if cache: - query = query.options(FromCache( - "sql_cache_short", - "get_enabled_repo_integrations_%i" % event.repo.repo_id)) + cache_key = "get_enabled_repo_integrations_%i" % event.repo.repo_id + query = query.options( + FromCache("sql_cache_short", cache_key)) else: # only global integrations query = query.filter(global_integrations_filter) if cache: - query = query.options(FromCache( - "sql_cache_short", "get_enabled_global_integrations")) + query = query.options( + FromCache("sql_cache_short", "get_enabled_global_integrations")) result = query.all() return result \ No newline at end of file diff --git a/rhodecode/model/notification.py b/rhodecode/model/notification.py --- a/rhodecode/model/notification.py +++ b/rhodecode/model/notification.py @@ -332,14 +332,19 @@ class EmailNotificationModel(BaseModel): :param kwargs: """ + kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name - + instance_url = h.route_url('home') _kwargs = { - 'instance_url': h.url('home', qualified=True), + 'instance_url': instance_url, + 'whitespace_filter': self.whitespace_filter } _kwargs.update(kwargs) return _kwargs + def whitespace_filter(self, text): + return text.replace('\n', '').replace('\t', '') + def get_renderer(self, type_): template_name = self.email_types[type_] return PartialRenderer(template_name) 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,14 +31,16 @@ import urllib from pylons.i18n.translation import _ from pylons.i18n.translation import lazy_ugettext +from pyramid.threadlocal import get_current_request from sqlalchemy import or_ +from rhodecode import events from rhodecode.lib import helpers as h, hooks_utils, diffs +from rhodecode.lib import audit_logger from rhodecode.lib.compat import OrderedDict from rhodecode.lib.hooks_daemon import prepare_callback_daemon from rhodecode.lib.markup_renderer import ( DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer) -from rhodecode.lib.utils import action_logger from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe from rhodecode.lib.vcs.backends.base import ( Reference, MergeResponse, MergeFailureReason, UpdateFailureReason) @@ -118,9 +120,9 @@ class PullRequestModel(BaseModel): 'Pull request update failed because of an unknown error.'), UpdateFailureReason.NO_CHANGE: lazy_ugettext( 'No update needed because the source and target have not changed.'), - UpdateFailureReason.WRONG_REF_TPYE: lazy_ugettext( + UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext( 'Pull request cannot be updated because the reference type is ' - 'not supported for an update.'), + 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'), UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext( 'This pull request cannot be updated because the target ' 'reference is missing.'), @@ -415,7 +417,9 @@ class PullRequestModel(BaseModel): .all() def create(self, created_by, source_repo, source_ref, target_repo, - target_ref, revisions, reviewers, title, description=None): + target_ref, revisions, reviewers, title, description=None, + reviewer_data=None): + created_by_user = self._get_user(created_by) source_repo = self._get_repo(source_repo) target_repo = self._get_repo(target_repo) @@ -429,6 +433,7 @@ class PullRequestModel(BaseModel): pull_request.title = title pull_request.description = description pull_request.author = created_by_user + pull_request.reviewer_data = reviewer_data Session().add(pull_request) Session().flush() @@ -436,15 +441,20 @@ class PullRequestModel(BaseModel): reviewer_ids = set() # members / reviewers for reviewer_object in reviewers: - if isinstance(reviewer_object, tuple): - user_id, reasons = reviewer_object - else: - user_id, reasons = reviewer_object, [] + user_id, reasons, mandatory = reviewer_object + user = self._get_user(user_id) - user = self._get_user(user_id) + # skip duplicates + if user.user_id in reviewer_ids: + continue + reviewer_ids.add(user.user_id) - reviewer = PullRequestReviewers(user, pull_request, reasons) + reviewer = PullRequestReviewers() + reviewer.user = user + reviewer.pull_request = pull_request + reviewer.reasons = reasons + reviewer.mandatory = mandatory Session().add(reviewer) # Set approval status to "Under Review" for all commits which are @@ -460,6 +470,11 @@ class PullRequestModel(BaseModel): self._trigger_pull_request_hook( pull_request, created_by_user, 'create') + creation_data = pull_request.get_api_data(with_merge_state=False) + self._log_audit_action( + 'repo.pull_request.create', {'data': creation_data}, + created_by_user, pull_request) + return pull_request def _trigger_pull_request_hook(self, pull_request, user, action): @@ -510,7 +525,12 @@ class PullRequestModel(BaseModel): log.debug( "Merge was successful, updating the pull request comments.") self._comment_and_close_pr(pull_request, user, merge_state) - self._log_action('user_merged_pull_request', user, pull_request) + + self._log_audit_action( + 'repo.pull_request.merge', + {'merge_state': merge_state.__dict__}, + user, pull_request) + else: log.warn("Merge failed, not updating the pull request.") return merge_state @@ -594,7 +614,7 @@ class PullRequestModel(BaseModel): pull_request, source_ref_type) return UpdateResponse( executed=False, - reason=UpdateFailureReason.WRONG_REF_TPYE, + reason=UpdateFailureReason.WRONG_REF_TYPE, old=pull_request, new=None, changes=None, source_changed=False, target_changed=False) @@ -763,6 +783,7 @@ class PullRequestModel(BaseModel): version._last_merge_status = pull_request._last_merge_status version.shadow_merge_ref = pull_request.shadow_merge_ref version.merge_rev = pull_request.merge_rev + version.reviewer_data = pull_request.reviewer_data version.revisions = pull_request.revisions version.pull_request = pull_request @@ -806,13 +827,15 @@ class PullRequestModel(BaseModel): """ pull_request = pull_request_version.pull_request - comments = ChangesetComment.query().filter( - # TODO: johbo: Should we query for the repo at all here? - # Pending decision on how comments of PRs are to be related - # to either the source repo, the target repo or no repo at all. - ChangesetComment.repo_id == pull_request.target_repo.repo_id, - ChangesetComment.pull_request == pull_request, - ChangesetComment.pull_request_version == None) + comments = ChangesetComment.query()\ + .filter( + # TODO: johbo: Should we query for the repo at all here? + # Pending decision on how comments of PRs are to be related + # to either the source repo, the target repo or no repo at all. + ChangesetComment.repo_id == pull_request.target_repo.repo_id, + ChangesetComment.pull_request == pull_request, + ChangesetComment.pull_request_version == None)\ + .order_by(ChangesetComment.comment_id.asc()) # TODO: johbo: Find out why this breaks if it is done in a bulk # operation. @@ -886,8 +909,9 @@ class PullRequestModel(BaseModel): renderer = RstTemplateRenderer() return renderer.render('pull_request_update.mako', **params) - def edit(self, pull_request, title, description): + def edit(self, pull_request, title, description, user): pull_request = self.__get_pull_request(pull_request) + old_data = pull_request.get_api_data(with_merge_state=False) if pull_request.is_closed(): raise ValueError('This pull request is closed') if title: @@ -895,22 +919,27 @@ class PullRequestModel(BaseModel): pull_request.description = description pull_request.updated_on = datetime.datetime.now() Session().add(pull_request) + self._log_audit_action( + 'repo.pull_request.edit', {'old_data': old_data}, + user, pull_request) - def update_reviewers(self, pull_request, reviewer_data): + def update_reviewers(self, pull_request, reviewer_data, user): """ Update the reviewers in the pull request :param pull_request: the pr to update - :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])] + :param reviewer_data: list of tuples + [(user, ['reason1', 'reason2'], mandatory_flag)] """ - reviewers_reasons = {} - for user_id, reasons in reviewer_data: + reviewers = {} + for user_id, reasons, mandatory in reviewer_data: if isinstance(user_id, (int, basestring)): user_id = self._get_user(user_id).user_id - reviewers_reasons[user_id] = reasons + reviewers[user_id] = { + 'reasons': reasons, 'mandatory': mandatory} - reviewers_ids = set(reviewers_reasons.keys()) + reviewers_ids = set(reviewers.keys()) pull_request = self.__get_pull_request(pull_request) current_reviewers = PullRequestReviewers.query()\ .filter(PullRequestReviewers.pull_request == @@ -926,9 +955,16 @@ class PullRequestModel(BaseModel): for uid in ids_to_add: changed = True _usr = self._get_user(uid) - reasons = reviewers_reasons[uid] - reviewer = PullRequestReviewers(_usr, pull_request, reasons) + reviewer = PullRequestReviewers() + reviewer.user = _usr + reviewer.pull_request = pull_request + reviewer.reasons = reviewers[uid]['reasons'] + # NOTE(marcink): mandatory shouldn't be changed now + # reviewer.mandatory = reviewers[uid]['reasons'] Session().add(reviewer) + self._log_audit_action( + 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()}, + user, pull_request) for uid in ids_to_remove: changed = True @@ -939,7 +975,11 @@ class PullRequestModel(BaseModel): # use .all() in case we accidentally added the same person twice # this CAN happen due to the lack of DB checks for obj in reviewers: + old_data = obj.get_dict() Session().delete(obj) + self._log_audit_action( + 'repo.pull_request.reviewer.delete', + {'old_data': old_data}, user, pull_request) if changed: pull_request.updated_on = datetime.datetime.now() @@ -948,11 +988,18 @@ class PullRequestModel(BaseModel): self.notify_reviewers(pull_request, ids_to_add) 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 get_url(self, pull_request, request=None, permalink=False): + if not request: + request = get_current_request() + + if permalink: + return request.route_url( + 'pull_requests_global', + pull_request_id=pull_request.pull_request_id,) + else: + return request.route_url('pullrequest_show', + repo_name=safe_str(pull_request.target_repo.repo_name), + pull_request_id=pull_request.pull_request_id,) def get_shadow_clone_url(self, pull_request): """ @@ -979,22 +1026,16 @@ class PullRequestModel(BaseModel): pr_source_repo = pull_request_obj.source_repo pr_target_repo = pull_request_obj.target_repo - pr_url = h.url( - 'pullrequest_show', + pr_url = h.route_url('pullrequest_show', repo_name=pr_target_repo.repo_name, - pull_request_id=pull_request_obj.pull_request_id, - qualified=True,) + pull_request_id=pull_request_obj.pull_request_id,) # set some variables for email notification - pr_target_repo_url = h.url( - 'summary_home', - repo_name=pr_target_repo.repo_name, - qualified=True) + pr_target_repo_url = h.route_url( + 'repo_summary', repo_name=pr_target_repo.repo_name) - pr_source_repo_url = h.url( - 'summary_home', - repo_name=pr_source_repo.repo_name, - qualified=True) + pr_source_repo_url = h.route_url( + 'repo_summary', repo_name=pr_source_repo.repo_name) # pull request specifics pull_request_commits = [ @@ -1031,9 +1072,13 @@ class PullRequestModel(BaseModel): email_kwargs=kwargs, ) - def delete(self, pull_request): + def delete(self, pull_request, user): pull_request = self.__get_pull_request(pull_request) + old_data = pull_request.get_api_data(with_merge_state=False) self._cleanup_merge_workspace(pull_request) + self._log_audit_action( + 'repo.pull_request.delete', {'old_data': old_data}, + user, pull_request) Session().delete(pull_request) def close_pull_request(self, pull_request, user): @@ -1044,44 +1089,64 @@ class PullRequestModel(BaseModel): Session().add(pull_request) self._trigger_pull_request_hook( pull_request, pull_request.author, 'close') - self._log_action('user_closed_pull_request', user, pull_request) + self._log_audit_action( + 'repo.pull_request.close', {}, user, pull_request) - def close_pull_request_with_comment(self, pull_request, user, repo, - message=None): - status = ChangesetStatus.STATUS_REJECTED + def close_pull_request_with_comment( + self, pull_request, user, repo, message=None): + + pull_request_review_status = pull_request.calculated_review_status() - if not message: - message = ( - _('Status change %(transition_icon)s %(status)s') % { - 'transition_icon': '>', - 'status': ChangesetStatus.get_status_lbl(status)}) + if pull_request_review_status == ChangesetStatus.STATUS_APPROVED: + # approved only if we have voting consent + status = ChangesetStatus.STATUS_APPROVED + else: + status = ChangesetStatus.STATUS_REJECTED + status_lbl = ChangesetStatus.get_status_lbl(status) - internal_message = _('Closing with') + ' ' + message + default_message = ( + _('Closing with status change {transition_icon} {status}.') + ).format(transition_icon='>', status=status_lbl) + text = message or default_message - comm = CommentsModel().create( - text=internal_message, + # create a comment, and link it to new status + comment = CommentsModel().create( + text=text, repo=repo.repo_id, user=user.user_id, pull_request=pull_request.pull_request_id, - f_path=None, - line_no=None, - status_change=ChangesetStatus.get_status_lbl(status), + status_change=status_lbl, status_change_type=status, closing_pr=True ) + # calculate old status before we change it + old_calculated_status = pull_request.calculated_review_status() ChangesetStatusModel().set_status( repo.repo_id, status, user.user_id, - comm, + comment=comment, pull_request=pull_request.pull_request_id ) + Session().flush() + events.trigger(events.PullRequestCommentEvent(pull_request, comment)) + # we now calculate the status of pull request again, and based on that + # calculation trigger status change. This might happen in cases + # that non-reviewer admin closes a pr, which means his vote doesn't + # change the status, while if he's a reviewer this might change it. + calculated_status = pull_request.calculated_review_status() + if old_calculated_status != calculated_status: + self._trigger_pull_request_hook( + pull_request, user, 'review_status_change') + # finally close the PR PullRequestModel().close_pull_request( pull_request.pull_request_id, user) + return comment, status + def merge_status(self, pull_request): if not self._is_merge_enabled(pull_request): return False, _('Server-side pull request merging is disabled.') @@ -1226,11 +1291,11 @@ class PullRequestModel(BaseModel): 'user': { 'user_id': repo.user.user_id, 'username': repo.user.username, - 'firstname': repo.user.firstname, - 'lastname': repo.user.lastname, + 'firstname': repo.user.first_name, + 'lastname': repo.user.last_name, 'gravatar_link': h.gravatar_url(repo.user.email, 14), }, - 'description': h.chop_at_smart(repo.description, '\n'), + 'description': h.chop_at_smart(repo.description_safe, '\n'), 'refs': { 'all_refs': all_refs, 'selected_ref': selected_ref, @@ -1360,12 +1425,29 @@ class PullRequestModel(BaseModel): 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, - '{action}:{pr_id}'.format( - action=action, pr_id=pull_request.pull_request_id), - pull_request.target_repo) + def _log_audit_action(self, action, action_data, user, pull_request): + audit_logger.store( + action=action, + action_data=action_data, + user=user, + repo=pull_request.target_repo) + + def get_reviewer_functions(self): + """ + Fetches functions for validation and fetching default reviewers. + If available we use the EE package, else we fallback to CE + package functions + """ + try: + from rc_reviewers.utils import get_default_reviewers_data + from rc_reviewers.utils import validate_default_reviewers + except ImportError: + from rhodecode.apps.repository.utils import \ + get_default_reviewers_data + from rhodecode.apps.repository.utils import \ + validate_default_reviewers + + return get_default_reviewers_data, validate_default_reviewers class MergeCheck(object): diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -30,8 +30,7 @@ import time import traceback from datetime import datetime, timedelta -from sqlalchemy.sql import func -from sqlalchemy.sql.expression import true, or_ +from pyramid.threadlocal import get_current_request from zope.cachedescriptors.property import Lazy as LazyProperty from rhodecode import events @@ -40,19 +39,17 @@ from rhodecode.lib.auth import HasUserGr from rhodecode.lib.caching_query import FromCache from rhodecode.lib.exceptions import AttachedForksError from rhodecode.lib.hooks_base import log_delete_repository -from rhodecode.lib.markup_renderer import MarkupRenderer from rhodecode.lib.utils import make_db_config from rhodecode.lib.utils2 import ( safe_str, safe_unicode, remove_prefix, obfuscate_url_pw, get_current_rhodecode_user, safe_int, datetime_to_time, action_logger_generic) from rhodecode.lib.vcs.backends import get_backend -from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError from rhodecode.model import BaseModel -from rhodecode.model.db import ( +from rhodecode.model.db import (_hash_key, Repository, UserRepoToPerm, UserGroupRepoToPerm, UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission, Statistics, UserGroup, RepoGroup, RepositoryField) -from rhodecode.model.scm import UserGroupList + from rhodecode.model.settings import VcsSettingsModel @@ -103,8 +100,8 @@ class RepoModel(BaseModel): .filter(Repository.repo_id == repo_id) if cache: - repo = repo.options(FromCache("sql_cache_short", - "get_repo_%s" % repo_id)) + repo = repo.options( + FromCache("sql_cache_short", "get_repo_%s" % repo_id)) return repo.scalar() def get_repo(self, repository): @@ -115,8 +112,9 @@ class RepoModel(BaseModel): .filter(Repository.repo_name == repo_name) if cache: - repo = repo.options(FromCache("sql_cache_short", - "get_repo_%s" % repo_name)) + name_key = _hash_key(repo_name) + repo = repo.options( + FromCache("sql_cache_short", "get_repo_%s" % name_key)) return repo.scalar() def _extract_id_from_repo_name(self, repo_name): @@ -134,6 +132,7 @@ class RepoModel(BaseModel): :param repo_name: :return: repo object if matched else None """ + try: _repo_id = self._extract_id_from_repo_name(repo_name) if _repo_id: @@ -156,86 +155,36 @@ class RepoModel(BaseModel): repos = Repository.query().filter(Repository.group == root).all() return repos - def get_url(self, repo): - return h.url('summary_home', repo_name=safe_str(repo.repo_name), - qualified=True) + def get_url(self, repo, request=None, permalink=False): + if not request: + request = get_current_request() - 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) - if only_active: - query = query.filter(User.active == true()) + if not request: + return - if name_contains: - ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) - query = query.filter( - or_( - User.name.ilike(ilike_expression), - User.lastname.ilike(ilike_expression), - User.username.ilike(ilike_expression) - ) - ) - query = query.limit(limit) - users = query.all() - - _users = [ - { - 'id': user.user_id, - 'first_name': user.name, - 'last_name': user.lastname, - 'username': user.username, - 'email': user.email, - 'icon_link': h.gravatar_url(user.email, 30), - 'value_display': h.person(user), - 'value': user.username, - 'value_type': 'user', - 'active': user.active, - } - for user in users - ] - return _users + if permalink: + return request.route_url( + 'repo_summary', repo_name=safe_str(repo.repo_id)) + else: + return request.route_url( + 'repo_summary', repo_name=safe_str(repo.repo_name)) - def get_user_groups(self, name_contains=None, limit=20, only_active=True): - - # TODO: mikhail: move this method to the UserGroupModel. - query = self.sa.query(UserGroup) - if only_active: - query = query.filter(UserGroup.users_group_active == true()) + def get_commit_url(self, repo, commit_id, request=None, permalink=False): + if not request: + request = get_current_request() - if name_contains: - ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) - query = query.filter( - UserGroup.users_group_name.ilike(ilike_expression))\ - .order_by(func.length(UserGroup.users_group_name))\ - .order_by(UserGroup.users_group_name) - - query = query.limit(limit) - user_groups = query.all() - perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin'] - user_groups = UserGroupList(user_groups, perm_set=perm_set) + if not request: + return - _groups = [ - { - 'id': group.users_group_id, - # TODO: marcink figure out a way to generate the url for the - # icon - 'icon_link': '', - 'value_display': 'Group: %s (%d members)' % ( - group.users_group_name, len(group.members),), - 'value': group.users_group_name, - 'description': group.user_group_description, - 'owner': group.user.username, + if permalink: + return request.route_url( + 'repo_commit', repo_name=safe_str(repo.repo_id), + commit_id=commit_id) - 'owner_icon': h.gravatar_url(group.user.email, 30), - 'value_display_owner': h.person(group.user.email), - - 'value_type': 'user_group', - 'active': group.users_group_active, - } - for group in user_groups - ] - return _groups + else: + return request.route_url( + 'repo_commit', repo_name=safe_str(repo.repo_name), + commit_id=commit_id) @classmethod def update_repoinfo(cls, repositories=None): @@ -308,7 +257,7 @@ class RepoModel(BaseModel): "last_changeset": last_rev(repo.repo_name, cs_cache), "last_changeset_raw": cs_cache.get('revision'), - "desc": desc(repo.description), + "desc": desc(repo.description_safe), "owner": user_profile(repo.user.username), "state": state(repo.repo_state), @@ -340,7 +289,7 @@ class RepoModel(BaseModel): defaults = repo_info.get_dict() defaults['repo_name'] = repo_info.just_name - groups = repo_info.groups_with_parents + groups = repo_info.groups_with_parents parent_group = groups[-1] if groups else None # we use -1 as this is how in HTML, we mark an empty group @@ -376,16 +325,6 @@ class RepoModel(BaseModel): replacement_user = User.get_first_super_admin().username defaults.update({'user': replacement_user}) - # fill repository users - for p in repo_info.repo_to_perm: - defaults.update({'u_perm_%s' % p.user.user_id: - p.permission.permission_name}) - - # fill repository groups - for p in repo_info.users_group_to_perm: - defaults.update({'g_perm_%s' % p.users_group.users_group_id: - p.permission.permission_name}) - return defaults def update(self, repo, **kwargs): @@ -414,12 +353,6 @@ class RepoModel(BaseModel): val = kwargs[k] if strip: k = remove_prefix(k, 'repo_') - if k == 'clone_uri': - from rhodecode.model.validators import Missing - _change = kwargs.get('clone_uri_change') - if _change in [Missing, 'OLD']: - # we don't change the value, so use original one - val = cur_repo.clone_uri setattr(cur_repo, k, val) @@ -594,10 +527,16 @@ class RepoModel(BaseModel): req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin') + changes = { + 'added': [], + 'updated': [], + 'deleted': [] + } # update permissions for member_id, perm, member_type in perm_updates: member_id = int(member_id) if member_type == 'user': + member_name = User.get(member_id).username # this updates also current one if found self.grant_user_permission( repo=repo, user=member_id, perm=perm) @@ -609,10 +548,14 @@ class RepoModel(BaseModel): self.grant_user_group_permission( repo=repo, group_name=member_id, perm=perm) + changes['updated'].append({'type': member_type, 'id': member_id, + 'name': member_name, 'new_perm': perm}) + # set new permissions for member_id, perm, member_type in perm_additions: member_id = int(member_id) if member_type == 'user': + member_name = User.get(member_id).username self.grant_user_permission( repo=repo, user=member_id, perm=perm) else: # set for user group @@ -622,11 +565,13 @@ class RepoModel(BaseModel): *req_perms)(member_name, user=cur_user): self.grant_user_group_permission( repo=repo, group_name=member_id, perm=perm) - + changes['added'].append({'type': member_type, 'id': member_id, + 'name': member_name, 'new_perm': perm}) # delete permissions for member_id, perm, member_type in perm_deletions: member_id = int(member_id) if member_type == 'user': + member_name = User.get(member_id).username self.revoke_user_permission(repo=repo, user=member_id) else: # set for user group # check if we have permissions to alter this usergroup @@ -636,6 +581,10 @@ class RepoModel(BaseModel): self.revoke_user_group_permission( repo=repo, group_name=member_id) + changes['deleted'].append({'type': member_type, 'id': member_id, + 'name': member_name, 'new_perm': perm}) + return changes + def create_fork(self, form_data, cur_user): """ Simple wrapper into executing celery task for fork creation 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 @@ -35,7 +35,7 @@ from zope.cachedescriptors.property impo from rhodecode import events from rhodecode.model import BaseModel -from rhodecode.model.db import ( +from rhodecode.model.db import (_hash_key, RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm, UserGroup, Repository) from rhodecode.model.settings import VcsSettingsModel, SettingsModel @@ -73,8 +73,9 @@ class RepoGroupModel(BaseModel): .filter(RepoGroup.group_name == repo_group_name) if cache: - repo = repo.options(FromCache( - "sql_cache_short", "get_repo_group_%s" % repo_group_name)) + name_key = _hash_key(repo_group_name) + repo = repo.options( + FromCache("sql_cache_short", "get_repo_group_%s" % name_key)) return repo.scalar() def get_default_create_personal_repo_group(self): @@ -339,6 +340,12 @@ class RepoGroupModel(BaseModel): req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin') + changes = { + 'added': [], + 'updated': [], + 'deleted': [] + } + def _set_perm_user(obj, user, perm): if isinstance(obj, RepoGroup): self.grant_user_permission( @@ -381,7 +388,6 @@ class RepoGroupModel(BaseModel): repo=obj, group_name=user_group) # start updates - updates = [] log.debug('Now updating permissions for %s in recursive mode:%s', repo_group, recursive) @@ -407,10 +413,13 @@ class RepoGroupModel(BaseModel): # in recursive mode obj = repo_group + change_obj = obj.get_api_data() + # update permissions for member_id, perm, member_type in perm_updates: member_id = int(member_id) if member_type == 'user': + member_name = User.get(member_id).username # this updates also current one if found _set_perm_user(obj, user=member_id, perm=perm) else: # set for user group @@ -419,10 +428,15 @@ class RepoGroupModel(BaseModel): user=cur_user): _set_perm_group(obj, users_group=member_id, perm=perm) + changes['updated'].append( + {'change_obj': change_obj, 'type': member_type, + 'id': member_id, 'name': member_name, 'new_perm': perm}) + # set new permissions for member_id, perm, member_type in perm_additions: member_id = int(member_id) if member_type == 'user': + member_name = User.get(member_id).username _set_perm_user(obj, user=member_id, perm=perm) else: # set for user group # check if we have permissions to alter this usergroup @@ -431,10 +445,15 @@ class RepoGroupModel(BaseModel): user=cur_user): _set_perm_group(obj, users_group=member_id, perm=perm) + changes['added'].append( + {'change_obj': change_obj, 'type': member_type, + 'id': member_id, 'name': member_name, 'new_perm': perm}) + # delete permissions for member_id, perm, member_type in perm_deletions: member_id = int(member_id) if member_type == 'user': + member_name = User.get(member_id).username _revoke_perm_user(obj, user=member_id) else: # set for user group # check if we have permissions to alter this usergroup @@ -443,13 +462,16 @@ class RepoGroupModel(BaseModel): user=cur_user): _revoke_perm_group(obj, user_group=member_id) - updates.append(obj) + changes['deleted'].append( + {'change_obj': change_obj, 'type': member_type, + 'id': member_id, 'name': member_name, 'new_perm': perm}) + # if it's not recursive call for all,repos,groups # break the loop and don't proceed with other changes if recursive not in ['all', 'repos', 'groups']: break - return updates + return changes def update(self, repo_group, form_data): try: @@ -689,7 +711,7 @@ class RepoGroupModel(BaseModel): "menu": quick_menu(group.group_name), "name": repo_group_lnk(group.group_name), "name_raw": group.group_name, - "desc": desc(group.group_description, group.personal), + "desc": desc(group.description_safe, group.personal), "top_level_repos": 0, "owner": user_profile(group.user.username) } diff --git a/rhodecode/model/scm.py b/rhodecode/model/scm.py --- a/rhodecode/model/scm.py +++ b/rhodecode/model/scm.py @@ -47,7 +47,7 @@ from rhodecode.lib.auth import ( from rhodecode.lib.exceptions import NonRelativePathError, IMCCommitError from rhodecode.lib import hooks_utils, caches from rhodecode.lib.utils import ( - get_filesystem_repos, action_logger, make_db_config) + get_filesystem_repos, make_db_config) from rhodecode.lib.utils2 import (safe_str, safe_unicode) from rhodecode.lib.system_info import get_system_info from rhodecode.model import BaseModel @@ -289,9 +289,6 @@ class ScmModel(BaseModel): if f is not None: try: self.sa.delete(f) - action_logger(UserTemp(user_id), - 'stopped_following_repo', - RepoTemp(follow_repo_id)) return except Exception: log.error(traceback.format_exc()) @@ -302,10 +299,6 @@ class ScmModel(BaseModel): f.user_id = user_id f.follows_repo_id = follow_repo_id self.sa.add(f) - - action_logger(UserTemp(user_id), - 'started_following_repo', - RepoTemp(follow_repo_id)) except Exception: log.error(traceback.format_exc()) raise @@ -503,7 +496,7 @@ class ScmModel(BaseModel): if not flat: _data = { - "name": f.unicode_path, + "name": h.escape(f.unicode_path), "type": "file", } if extended_info: @@ -529,7 +522,7 @@ class ScmModel(BaseModel): _data = d.unicode_path if not flat: _data = { - "name": d.unicode_path, + "name": h.escape(d.unicode_path), "type": "dir", } if extended_info: diff --git a/rhodecode/model/settings.py b/rhodecode/model/settings.py --- a/rhodecode/model/settings.py +++ b/rhodecode/model/settings.py @@ -18,6 +18,7 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ +import os import hashlib import logging from collections import namedtuple @@ -51,7 +52,8 @@ class SettingsModel(BaseModel): BUILTIN_HOOKS = ( RhodeCodeUi.HOOK_REPO_SIZE, RhodeCodeUi.HOOK_PUSH, RhodeCodeUi.HOOK_PRE_PUSH, RhodeCodeUi.HOOK_PRETX_PUSH, - RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL) + RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL, + RhodeCodeUi.HOOK_PUSH_KEY,) HOOKS_SECTION = 'hooks' def __init__(self, sa=None, repo=None): @@ -207,6 +209,7 @@ class SettingsModel(BaseModel): caches.clear_cache_manager(cache_manager) def get_all_settings(self, cache=False): + def _compute(): q = self._get_settings_query() if not q: @@ -413,15 +416,16 @@ class VcsSettingsModel(object): ('hooks', 'outgoing.pull_logger'),) HG_SETTINGS = ( ('extensions', 'largefiles'), - ('phases', 'publish'),) + ('phases', 'publish'), + ('extensions', 'evolve'),) GIT_SETTINGS = ( ('vcs_git_lfs', 'enabled'),) - GLOBAL_HG_SETTINGS = ( ('extensions', 'largefiles'), ('largefiles', 'usercache'), ('phases', 'publish'), - ('extensions', 'hgsubversion')) + ('extensions', 'hgsubversion'), + ('extensions', 'evolve'),) GLOBAL_GIT_SETTINGS = ( ('vcs_git_lfs', 'enabled'), ('vcs_git_lfs', 'store_location')) @@ -544,22 +548,26 @@ class VcsSettingsModel(object): @assert_repo_settings def create_or_update_repo_hg_settings(self, data): - largefiles, phases = \ + largefiles, phases, evolve = \ self.HG_SETTINGS - largefiles_key, phases_key = \ + largefiles_key, phases_key, evolve_key = \ self._get_settings_keys(self.HG_SETTINGS, data) self._create_or_update_ui( self.repo_settings, *largefiles, value='', active=data[largefiles_key]) self._create_or_update_ui( + self.repo_settings, *evolve, value='', + active=data[evolve_key]) + self._create_or_update_ui( self.repo_settings, *phases, value=safe_str(data[phases_key])) def create_or_update_global_hg_settings(self, data): - largefiles, largefiles_store, phases, hgsubversion \ + largefiles, largefiles_store, phases, hgsubversion, evolve \ = self.GLOBAL_HG_SETTINGS - largefiles_key, largefiles_store_key, phases_key, subversion_key \ + largefiles_key, largefiles_store_key, phases_key, subversion_key, evolve_key \ = self._get_settings_keys(self.GLOBAL_HG_SETTINGS, data) + self._create_or_update_ui( self.global_settings, *largefiles, value='', active=data[largefiles_key]) @@ -570,6 +578,9 @@ class VcsSettingsModel(object): self.global_settings, *phases, value=safe_str(data[phases_key])) self._create_or_update_ui( self.global_settings, *hgsubversion, active=data[subversion_key]) + self._create_or_update_ui( + self.global_settings, *evolve, value='', + active=data[evolve_key]) def create_or_update_repo_git_settings(self, data): # NOTE(marcink): # comma make unpack work properly @@ -774,3 +785,27 @@ class VcsSettingsModel(object): raise ValueError( 'The given data does not contain {} key'.format(data_key)) return data_keys + + def create_largeobjects_dirs_if_needed(self, repo_store_path): + """ + This is subscribed to the `pyramid.events.ApplicationCreated` event. It + does a repository scan if enabled in the settings. + """ + + from rhodecode.lib.vcs.backends.hg import largefiles_store + from rhodecode.lib.vcs.backends.git import lfs_store + + paths = [ + largefiles_store(repo_store_path), + lfs_store(repo_store_path)] + + for path in paths: + if os.path.isdir(path): + continue + if os.path.isfile(path): + continue + # not a file nor dir, we try to create it + try: + os.makedirs(path) + except Exception: + log.warning('Failed to create largefiles dir:%s', path) diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -30,21 +30,21 @@ from pylons.i18n.translation import _ import ipaddress from sqlalchemy.exc import DatabaseError -from sqlalchemy.sql.expression import true, false from rhodecode import events from rhodecode.lib.user_log_filter import user_log_filter from rhodecode.lib.utils2 import ( safe_unicode, get_current_rhodecode_user, action_logger_generic, AttributeDict, str2bool) +from rhodecode.lib.exceptions import ( + DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException, + UserOwnsUserGroupsException, NotAllowedToCreateUserError) from rhodecode.lib.caching_query import FromCache from rhodecode.model import BaseModel from rhodecode.model.auth_token import AuthTokenModel from rhodecode.model.db import ( - or_, joinedload, User, UserToPerm, UserEmailMap, UserIpMap, UserLog) -from rhodecode.lib.exceptions import ( - DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException, - UserOwnsUserGroupsException, NotAllowedToCreateUserError) + _hash_key, true, false, or_, joinedload, User, UserToPerm, + UserEmailMap, UserIpMap, UserLog) from rhodecode.model.meta import Session from rhodecode.model.repo_group import RepoGroupModel @@ -58,13 +58,52 @@ class UserModel(BaseModel): def get(self, user_id, cache=False): user = self.sa.query(User) if cache: - user = user.options(FromCache("sql_cache_short", - "get_user_%s" % user_id)) + user = user.options( + FromCache("sql_cache_short", "get_user_%s" % user_id)) return user.get(user_id) def get_user(self, user): return self._get_user(user) + def _serialize_user(self, user): + import rhodecode.lib.helpers as h + + return { + 'id': user.user_id, + 'first_name': user.first_name, + 'last_name': user.last_name, + 'username': user.username, + 'email': user.email, + 'icon_link': h.gravatar_url(user.email, 30), + 'value_display': h.escape(h.person(user)), + 'value': user.username, + 'value_type': 'user', + 'active': user.active, + } + + def get_users(self, name_contains=None, limit=20, only_active=True): + + query = self.sa.query(User) + if only_active: + query = query.filter(User.active == true()) + + if name_contains: + ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) + query = query.filter( + or_( + User.name.ilike(ilike_expression), + User.lastname.ilike(ilike_expression), + User.username.ilike(ilike_expression) + ) + ) + query = query.limit(limit) + users = query.all() + + _users = [ + self._serialize_user(user) for user in users + ] + return _users + def get_by_username(self, username, cache=False, case_insensitive=False): if case_insensitive: @@ -73,8 +112,9 @@ class UserModel(BaseModel): user = self.sa.query(User)\ .filter(User.username == username) if cache: - user = user.options(FromCache("sql_cache_short", - "get_user_%s" % username)) + name_key = _hash_key(username) + user = user.options( + FromCache("sql_cache_short", "get_user_%s" % name_key)) return user.scalar() def get_by_email(self, email, cache=False, case_insensitive=False): @@ -630,7 +670,8 @@ class UserModel(BaseModel): user_id, api_key, username) return False if not dbuser.active: - log.debug('User `%s` is inactive, skipping fill data', username) + log.debug('User `%s:%s` is inactive, skipping fill data', + username, user_id) return False log.debug('filling user:%s data', dbuser) @@ -638,6 +679,11 @@ class UserModel(BaseModel): # TODO: johbo: Think about this and find a clean solution user_data = dbuser.get_dict() user_data.update(dbuser.get_api_data(include_secrets=True)) + user_data.update({ + # set explicit the safe escaped values + 'first_name': dbuser.first_name, + 'last_name': dbuser.last_name, + }) for k, v in user_data.iteritems(): # properties of auth user we dont update @@ -726,7 +772,7 @@ class UserModel(BaseModel): """ user = self._get_user(user) obj = UserEmailMap.query().get(email_id) - if obj: + if obj and obj.user_id == user.user_id: self.sa.delete(obj) def parse_ip_range(self, ip_range): @@ -783,7 +829,7 @@ class UserModel(BaseModel): """ user = self._get_user(user) obj = UserIpMap.query().get(ip_id) - if obj: + if obj and obj.user_id == user.user_id: self.sa.delete(obj) def get_accounts_in_creation_order(self, current_user=None): diff --git a/rhodecode/model/user_group.py b/rhodecode/model/user_group.py --- a/rhodecode/model/user_group.py +++ b/rhodecode/model/user_group.py @@ -27,14 +27,18 @@ user group model for RhodeCode import logging import traceback -from rhodecode.lib.utils2 import safe_str +from rhodecode.lib.utils2 import safe_str, safe_unicode +from rhodecode.lib.exceptions import ( + UserGroupAssignedException, RepoGroupAssignmentError) +from rhodecode.lib.utils2 import ( + get_current_rhodecode_user, action_logger_generic) from rhodecode.model import BaseModel -from rhodecode.model.db import UserGroupMember, UserGroup,\ - UserGroupRepoToPerm, Permission, UserGroupToPerm, User, UserUserGroupToPerm,\ - UserGroupUserGroupToPerm, UserGroupRepoGroupToPerm -from rhodecode.lib.exceptions import UserGroupAssignedException,\ - RepoGroupAssignmentError -from rhodecode.lib.utils2 import get_current_rhodecode_user, action_logger_generic +from rhodecode.model.scm import UserGroupList +from rhodecode.model.db import ( + true, func, User, UserGroupMember, UserGroup, + UserGroupRepoToPerm, Permission, UserGroupToPerm, UserUserGroupToPerm, + UserGroupUserGroupToPerm, UserGroupRepoGroupToPerm) + log = logging.getLogger(__name__) @@ -112,7 +116,7 @@ class UserGroupModel(BaseModel): if member_type == 'user': self.revoke_user_permission(user_group=user_group, user=member_id) else: - #check if we have permissions to alter this usergroup + # check if we have permissions to alter this usergroup member_name = UserGroup.get(member_id).users_group_name if not check_perms or HasUserGroupPermissionAny(*req_perms)(member_name, user=cur_user): self.revoke_user_group_permission( @@ -171,7 +175,7 @@ class UserGroupModel(BaseModel): user_id for user_id in current_members_ids if user_id not in user_id_list] - return (added_members, deleted_members) + return added_members, deleted_members def _set_users_as_members(self, user_group, user_ids): user_group.members = [] @@ -187,6 +191,7 @@ class UserGroupModel(BaseModel): self._set_users_as_members(user_group, user_ids) self._log_user_changes('added to', user_group, added) self._log_user_changes('removed from', user_group, removed) + return added, removed def _clean_members_data(self, members_data): if not members_data: @@ -221,12 +226,16 @@ class UserGroupModel(BaseModel): user_group.user = owner + added_user_ids = [] + removed_user_ids = [] if 'users_group_members' in form_data: members_id_list = self._clean_members_data( form_data['users_group_members']) - self._update_members_from_user_ids(user_group, members_id_list) + added_user_ids, removed_user_ids = \ + self._update_members_from_user_ids(user_group, members_id_list) self.sa.add(user_group) + return user_group, added_user_ids, removed_user_ids def delete(self, user_group, force=False): """ @@ -539,6 +548,59 @@ class UserGroupModel(BaseModel): log.debug('Adding user %s to user group %s', user.username, gr.users_group_name) UserGroupModel().add_user_to_group(gr.users_group_name, user.username) + def _serialize_user_group(self, user_group): + import rhodecode.lib.helpers as h + return { + 'id': user_group.users_group_id, + # TODO: marcink figure out a way to generate the url for the + # icon + 'icon_link': '', + 'value_display': 'Group: %s (%d members)' % ( + user_group.users_group_name, len(user_group.members),), + 'value': user_group.users_group_name, + 'description': user_group.user_group_description, + 'owner': user_group.user.username, + + 'owner_icon': h.gravatar_url(user_group.user.email, 30), + 'value_display_owner': h.person(user_group.user.email), + + 'value_type': 'user_group', + 'active': user_group.users_group_active, + } + + def get_user_groups(self, name_contains=None, limit=20, only_active=True, + expand_groups=False): + query = self.sa.query(UserGroup) + if only_active: + query = query.filter(UserGroup.users_group_active == true()) + + if name_contains: + ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) + query = query.filter( + UserGroup.users_group_name.ilike(ilike_expression))\ + .order_by(func.length(UserGroup.users_group_name))\ + .order_by(UserGroup.users_group_name) + + query = query.limit(limit) + user_groups = query.all() + perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin'] + user_groups = UserGroupList(user_groups, perm_set=perm_set) + + # store same serialize method to extract data from User + from rhodecode.model.user import UserModel + serialize_user = UserModel()._serialize_user + + _groups = [] + for group in user_groups: + entry = self._serialize_user_group(group) + if expand_groups: + expanded_members = [] + for member in group.members: + expanded_members.append(serialize_user(member.user)) + entry['members'] = expanded_members + _groups.append(entry) + return _groups + @staticmethod def get_user_groups_as_dict(user_group): import rhodecode.lib.helpers as h @@ -550,7 +612,9 @@ class UserGroupModel(BaseModel): 'active': user_group.users_group_active, "owner": user_group.user.username, 'owner_icon': h.gravatar_url(user_group.user.email, 30), - "owner_data": {'owner': user_group.user.username, 'owner_icon': h.gravatar_url(user_group.user.email, 30)} + "owner_data": { + 'owner': user_group.user.username, + 'owner_icon': h.gravatar_url(user_group.user.email, 30)} } return data diff --git a/rhodecode/model/validation_schema/schemas/repo_schema.py b/rhodecode/model/validation_schema/schemas/repo_schema.py --- a/rhodecode/model/validation_schema/schemas/repo_schema.py +++ b/rhodecode/model/validation_schema/schemas/repo_schema.py @@ -19,8 +19,10 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import colander +import deform.widget from rhodecode.translation import _ +from rhodecode.model.validation_schema.utils import convert_to_optgroup from rhodecode.model.validation_schema import validators, preparers, types DEFAULT_LANDING_REF = 'rev:tip' @@ -32,6 +34,11 @@ def get_group_and_repo(repo_name): repo_name, get_object=True) +def get_repo_group(repo_group_id): + from rhodecode.model.repo_group import RepoGroup + return RepoGroup.get(repo_group_id), RepoGroup.CHOICES_SEPARATOR + + @colander.deferred def deferred_repo_type_validator(node, kw): options = kw.get('repo_type_options', []) @@ -53,11 +60,27 @@ def deferred_repo_owner_validator(node, @colander.deferred def deferred_landing_ref_validator(node, kw): - options = kw.get('repo_ref_options', [DEFAULT_LANDING_REF]) + options = kw.get( + 'repo_ref_options', [DEFAULT_LANDING_REF]) return colander.OneOf([x for x in options]) @colander.deferred +def deferred_clone_uri_validator(node, kw): + repo_type = kw.get('repo_type') + validator = validators.CloneUriValidator(repo_type) + return validator + + +@colander.deferred +def deferred_landing_ref_widget(node, kw): + items = kw.get( + 'repo_ref_items', [(DEFAULT_LANDING_REF, DEFAULT_LANDING_REF)]) + items = convert_to_optgroup(items) + return deform.widget.Select2Widget(values=items) + + +@colander.deferred def deferred_fork_of_validator(node, kw): old_values = kw.get('old_values') or {} @@ -191,7 +214,25 @@ def deferred_unique_name_validator(node, @colander.deferred def deferred_repo_name_validator(node, kw): - return validators.valid_name_validator + def no_git_suffix_validator(node, value): + if value.endswith('.git'): + msg = _('Repository name cannot end with .git') + raise colander.Invalid(node, msg) + return colander.All( + no_git_suffix_validator, validators.valid_name_validator) + + +@colander.deferred +def deferred_repo_group_validator(node, kw): + options = kw.get( + 'repo_repo_group_options') + return colander.OneOf([x for x in options]) + + +@colander.deferred +def deferred_repo_group_widget(node, kw): + items = kw.get('repo_repo_group_items') + return deform.widget.Select2Widget(values=items) class GroupType(colander.Mapping): @@ -215,8 +256,10 @@ class GroupType(colander.Mapping): parent_group_name, parent_group) = get_group_and_repo(validated_name) + appstruct['repo_name_with_group'] = validated_name appstruct['repo_name_without_group'] = repo_name_without_group appstruct['repo_group_name'] = parent_group_name or types.RootLocation + if parent_group: appstruct['repo_group_id'] = parent_group.group_id @@ -260,16 +303,19 @@ class RepoSchema(colander.MappingSchema) repo_owner = colander.SchemaNode( colander.String(), - validator=deferred_repo_owner_validator) + validator=deferred_repo_owner_validator, + widget=deform.widget.TextInputWidget()) repo_description = colander.SchemaNode( - colander.String(), missing='') + colander.String(), missing='', + widget=deform.widget.TextAreaWidget()) repo_landing_commit_ref = colander.SchemaNode( colander.String(), validator=deferred_landing_ref_validator, preparers=[preparers.strip_preparer], - missing=DEFAULT_LANDING_REF) + missing=DEFAULT_LANDING_REF, + widget=deferred_landing_ref_widget) repo_clone_uri = colander.SchemaNode( colander.String(), @@ -284,19 +330,19 @@ class RepoSchema(colander.MappingSchema) repo_private = colander.SchemaNode( types.StringBooleanType(), - missing=False) + missing=False, widget=deform.widget.CheckboxWidget()) repo_copy_permissions = colander.SchemaNode( types.StringBooleanType(), - missing=False) + missing=False, widget=deform.widget.CheckboxWidget()) repo_enable_statistics = colander.SchemaNode( types.StringBooleanType(), - missing=False) + missing=False, widget=deform.widget.CheckboxWidget()) repo_enable_downloads = colander.SchemaNode( types.StringBooleanType(), - missing=False) + missing=False, widget=deform.widget.CheckboxWidget()) repo_enable_locking = colander.SchemaNode( types.StringBooleanType(), - missing=False) + missing=False, widget=deform.widget.CheckboxWidget()) def deserialize(self, cstruct): """ @@ -319,3 +365,50 @@ class RepoSchema(colander.MappingSchema) third.deserialize({'unique_repo_name': validated_name}) return appstruct + + +class RepoSettingsSchema(RepoSchema): + repo_group = colander.SchemaNode( + colander.Integer(), + validator=deferred_repo_group_validator, + widget=deferred_repo_group_widget, + missing='') + + repo_clone_uri_change = colander.SchemaNode( + colander.String(), + missing='NEW') + + repo_clone_uri = colander.SchemaNode( + colander.String(), + preparers=[preparers.strip_preparer], + validator=deferred_clone_uri_validator, + missing='') + + def deserialize(self, cstruct): + """ + Custom deserialize that allows to chain validation, and verify + permissions, and as last step uniqueness + """ + + # first pass, to validate given data + appstruct = super(RepoSchema, self).deserialize(cstruct) + validated_name = appstruct['repo_name'] + # because of repoSchema adds repo-group as an ID, we inject it as + # full name here because validators require it, it's unwrapped later + # so it's safe to use and final name is going to be without group anyway + + group, separator = get_repo_group(appstruct['repo_group']) + if group: + validated_name = separator.join([group.group_name, validated_name]) + + # second pass to validate permissions to repo_group + second = RepoGroupAccessSchema().bind(**self.bindings) + appstruct_second = second.deserialize({'repo_group': validated_name}) + # save result + appstruct['repo_group'] = appstruct_second['repo_group'] + + # thirds to validate uniqueness + third = RepoNameUniqueSchema().bind(**self.bindings) + third.deserialize({'unique_repo_name': validated_name}) + + return appstruct diff --git a/rhodecode/model/validation_schema/schemas/reviewer_schema.py b/rhodecode/model/validation_schema/schemas/reviewer_schema.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/validation_schema/schemas/reviewer_schema.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 ReviewerSchema(colander.MappingSchema): + username = colander.SchemaNode(types.StrOrIntType()) + reasons = colander.SchemaNode(colander.List(), missing=['no reason specified']) + mandatory = colander.SchemaNode(colander.Boolean(), missing=False) + + +class ReviewerListSchema(colander.SequenceSchema): + reviewers = ReviewerSchema() + + diff --git a/rhodecode/model/validation_schema/schemas/user_group_schema.py b/rhodecode/model/validation_schema/schemas/user_group_schema.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/validation_schema/schemas/user_group_schema.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 re +import colander + +from rhodecode.model.validation_schema import types, validators +from rhodecode.translation import _ + + +@colander.deferred +def deferred_user_group_name_validator(node, kw): + + def name_validator(node, value): + + msg = _('Allowed in name are letters, numbers, and `-`, `_`, `.` ' + 'Name must start with a letter or number. Got `{}`').format(value) + + if not re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value): + raise colander.Invalid(node, msg) + + return name_validator + + +@colander.deferred +def deferred_user_group_owner_validator(node, kw): + + def owner_validator(node, value): + from rhodecode.model.db import User + existing = User.get_by_username(value) + if not existing: + msg = _(u'User group owner with id `{}` does not exists').format(value) + raise colander.Invalid(node, msg) + + return owner_validator + + +class UserGroupSchema(colander.Schema): + + user_group_name = colander.SchemaNode( + colander.String(), + validator=deferred_user_group_name_validator) + + user_group_description = colander.SchemaNode( + colander.String(), missing='') + + user_group_owner = colander.SchemaNode( + colander.String(), + validator=deferred_user_group_owner_validator) + + user_group_active = colander.SchemaNode( + types.StringBooleanType(), + missing=False) + + def deserialize(self, cstruct): + """ + Custom deserialize that allows to chain validation, and verify + permissions, and as last step uniqueness + """ + + appstruct = super(UserGroupSchema, self).deserialize(cstruct) + return appstruct diff --git a/rhodecode/model/validation_schema/schemas/user_schema.py b/rhodecode/model/validation_schema/schemas/user_schema.py --- a/rhodecode/model/validation_schema/schemas/user_schema.py +++ b/rhodecode/model/validation_schema/schemas/user_schema.py @@ -18,10 +18,12 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ +import re import colander from rhodecode import forms from rhodecode.model.db import User +from rhodecode.model.validation_schema import types, validators from rhodecode.translation import _ from rhodecode.lib.auth import check_password @@ -52,10 +54,72 @@ class ChangePasswordSchema(colander.Sche widget=forms.widget.CheckedPasswordWidget(redisplay=True), validator=colander.Length(min=6)) - def validator(self, form, values): if values['current_password'] == values['new_password']: exc = colander.Invalid(form) exc['new_password'] = _('New password must be different ' 'to old password') raise exc + + +@colander.deferred +def deferred_username_validator(node, kw): + + def name_validator(node, value): + msg = _( + u'Username may only contain alphanumeric characters ' + u'underscores, periods or dashes and must begin with ' + u'alphanumeric character or underscore') + + if not re.match(r'^[\w]{1}[\w\-\.]{0,254}$', value): + raise colander.Invalid(node, msg) + + return name_validator + + +@colander.deferred +def deferred_email_validator(node, kw): + # NOTE(marcink): we might provide uniqueness validation later here... + return colander.Email() + + +class UserSchema(colander.Schema): + username = colander.SchemaNode( + colander.String(), + validator=deferred_username_validator) + + email = colander.SchemaNode( + colander.String(), + validator=deferred_email_validator) + + password = colander.SchemaNode( + colander.String(), missing='') + + first_name = colander.SchemaNode( + colander.String(), missing='') + + last_name = colander.SchemaNode( + colander.String(), missing='') + + active = colander.SchemaNode( + types.StringBooleanType(), + missing=False) + + admin = colander.SchemaNode( + types.StringBooleanType(), + missing=False) + + extern_name = colander.SchemaNode( + colander.String(), missing='') + + extern_type = colander.SchemaNode( + colander.String(), missing='') + + def deserialize(self, cstruct): + """ + Custom deserialize that allows to chain validation, and verify + permissions, and as last step uniqueness + """ + + appstruct = super(UserSchema, self).deserialize(cstruct) + return appstruct diff --git a/rhodecode/model/validation_schema/types.py b/rhodecode/model/validation_schema/types.py --- a/rhodecode/model/validation_schema/types.py +++ b/rhodecode/model/validation_schema/types.py @@ -190,7 +190,7 @@ class UserGroupType(UserOrUserGroupType) class StrOrIntType(colander.String): def deserialize(self, node, cstruct): - if isinstance(node, basestring): + if isinstance(cstruct, basestring): return super(StrOrIntType, self).deserialize(node, cstruct) else: return colander.Integer().deserialize(node, cstruct) diff --git a/rhodecode/model/validation_schema/utils.py b/rhodecode/model/validation_schema/utils.py new file mode 100644 --- /dev/null +++ b/rhodecode/model/validation_schema/utils.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 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 deform.widget + + +def convert_to_optgroup(items): + """ + Convert such format:: + + [ + ['rev:tip', u'latest tip'], + ([(u'branch:default', u'default')], u'Branches'), + ] + + into one used by deform Select widget:: + + ( + ('rev:tip', 'latest tip'), + OptGroup('Branches', + ('branch:default', 'default'), + ) + """ + result = [] + for value, label in items: + # option group + if isinstance(value, (tuple, list)): + result.append(deform.widget.OptGroup(label, *value)) + else: + result.append((value, label)) + + return result diff --git a/rhodecode/model/validation_schema/validators.py b/rhodecode/model/validation_schema/validators.py --- a/rhodecode/model/validation_schema/validators.py +++ b/rhodecode/model/validation_schema/validators.py @@ -1,5 +1,27 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2017 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 re +import logging + import ipaddress import colander @@ -7,6 +29,8 @@ import colander from rhodecode.translation import _ from rhodecode.lib.utils2 import glob2re +log = logging.getLogger(__name__) + def ip_addr_validator(node, value): try: @@ -46,3 +70,71 @@ def valid_name_validator(node, value): msg = _('Name must start with a letter or number. Got `{}`').format(value) if not re.match(r'^[a-zA-z0-9]{1,}', value): raise colander.Invalid(node, msg) + + +class InvalidCloneUrl(Exception): + allowed_prefixes = () + + +def url_validator(url, repo_type, config): + from rhodecode.lib.vcs.backends.hg import MercurialRepository + from rhodecode.lib.vcs.backends.git import GitRepository + from rhodecode.lib.vcs.backends.svn import SubversionRepository + + if repo_type == 'hg': + allowed_prefixes = ('http', 'svn+http', 'git+http') + + if 'http' in url[:4]: + # initially check if it's at least the proper URL + # or does it pass basic auth + + MercurialRepository.check_url(url, config) + elif 'svn+http' in url[:8]: # svn->hg import + SubversionRepository.check_url(url, config) + elif 'git+http' in url[:8]: # git->hg import + raise NotImplementedError() + else: + exc = InvalidCloneUrl('Clone from URI %s not allowed. ' + 'Allowed url must start with one of %s' + % (url, ','.join(allowed_prefixes))) + exc.allowed_prefixes = allowed_prefixes + raise exc + + elif repo_type == 'git': + allowed_prefixes = ('http', 'svn+http', 'hg+http') + if 'http' in url[:4]: + # initially check if it's at least the proper URL + # or does it pass basic auth + GitRepository.check_url(url, config) + elif 'svn+http' in url[:8]: # svn->git import + raise NotImplementedError() + elif 'hg+http' in url[:8]: # hg->git import + raise NotImplementedError() + else: + exc = InvalidCloneUrl('Clone from URI %s not allowed. ' + 'Allowed url must start with one of %s' + % (url, ','.join(allowed_prefixes))) + exc.allowed_prefixes = allowed_prefixes + raise exc + + +class CloneUriValidator(object): + def __init__(self, repo_type): + self.repo_type = repo_type + + def __call__(self, node, value): + from rhodecode.lib.utils import make_db_config + try: + config = make_db_config(clear_session=False) + url_validator(value, self.repo_type, config) + except InvalidCloneUrl as e: + log.warning(e) + msg = _(u'Invalid clone url, provide a valid clone ' + u'url starting with one of {allowed_prefixes}').format( + allowed_prefixes=e.allowed_prefixes) + raise colander.Invalid(node, msg) + except Exception: + log.exception('Url validation failed') + msg = _(u'invalid clone url for {repo_type} repository').format( + repo_type=self.repo_type) + 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 @@ -574,6 +574,26 @@ def SlugifyName(): return _validator +def CannotHaveGitSuffix(): + class _validator(formencode.validators.FancyValidator): + messages = { + 'has_git_suffix': + _(u'Repository name cannot end with .git'), + } + + def _to_python(self, value, state): + return value + + def validate_python(self, value, state): + if value and value.endswith('.git'): + msg = M( + self, 'has_git_suffix', state) + raise formencode.Invalid( + msg, value, state, error_dict={'repo_name': msg}) + + return _validator + + def ValidCloneUri(): class InvalidCloneUrl(Exception): allowed_prefixes = () @@ -764,7 +784,8 @@ def ValidPerms(type_='repo'): del_member = perm_dict.get('id') del_type = perm_dict.get('type') if del_member and del_type: - perm_deletions.add((del_member, None, del_type)) + perm_deletions.add( + (del_member, None, del_type)) # store additions in order of how they were added in web form for k in sorted(new_perms_group.keys()): @@ -773,36 +794,48 @@ def ValidPerms(type_='repo'): new_type = perm_dict.get('type') new_perm = perm_dict.get('perm') if new_member and new_perm and new_type: - perm_additions.add((new_member, new_perm, new_type)) + perm_additions.add( + (new_member, new_perm, new_type)) # get updates of permissions # (read the existing radio button states) + default_user_id = User.get_default_user().user_id for k, update_value in value.iteritems(): if k.startswith('u_perm_') or k.startswith('g_perm_'): member = k[7:] update_type = {'u': 'user', 'g': 'users_group'}[k[0]] - if member == User.DEFAULT_USER: + + if safe_int(member) == default_user_id: if str2bool(value.get('repo_private')): - # set none for default when updating to - # private repo protects agains form manipulation + # prevent from updating default user permissions + # when this repository is marked as private update_value = EMPTY_PERM - perm_updates.add((member, update_value, update_type)) - # check the deletes - value['perm_additions'] = list(perm_additions) + perm_updates.add( + (member, update_value, update_type)) + + value['perm_additions'] = [] # propagated later value['perm_updates'] = list(perm_updates) value['perm_deletions'] = list(perm_deletions) - # validate users they exist and they are active ! - for member_id, _perm, member_type in perm_additions: + updates_map = dict( + (x[0], (x[1], x[2])) for x in value['perm_updates']) + # make sure Additions don't override updates. + for member_id, perm, member_type in list(perm_additions): + if member_id in updates_map: + perm = updates_map[member_id][0] + value['perm_additions'].append((member_id, perm, member_type)) + + # on new entries validate users they exist and they are active ! + # this leaves feedback to the form try: if member_type == 'user': - self.user_db = User.query()\ + User.query()\ .filter(User.active == true())\ .filter(User.user_id == member_id).one() if member_type == 'users_group': - self.user_db = UserGroup.query()\ + UserGroup.query()\ .filter(UserGroup.users_group_active == true())\ .filter(UserGroup.users_group_id == member_id)\ .one() diff --git a/rhodecode/public/css/buttons.less b/rhodecode/public/css/buttons.less --- a/rhodecode/public/css/buttons.less +++ b/rhodecode/public/css/buttons.less @@ -62,6 +62,10 @@ input[type="button"] { text-shadow: none; } + &.no-margin { + margin: 0 0 0 0; + } + } @@ -270,11 +274,17 @@ input[type="button"] { font-size: inherit; color: @rcblue; border: none; - .border-radius (0); + border-radius: 0; background-color: transparent; + &.last-item { + border: none; + padding: 0 0 0 0; + } + &:last-child { border: none; + padding: 0 0 0 0; } &:hover { diff --git a/rhodecode/public/css/deform.less b/rhodecode/public/css/deform.less --- a/rhodecode/public/css/deform.less +++ b/rhodecode/public/css/deform.less @@ -12,7 +12,7 @@ .control-label { width: 200px; - padding: 10px; + padding: 10px 0px; float: left; } .control-inputs { 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 @@ -165,6 +165,7 @@ div.markdown-block h6 { div.markdown-block hr { border: 0; color: #e6e5e5; + background-color: #e6e5e5; height: 3px; margin-bottom: 13px; } @@ -200,6 +201,12 @@ div.markdown-block img { background-color: #fff; } + +div.markdown-block strong { + font-weight: 600; + margin: 0; +} + div.markdown-block ul, div.markdown-block ol { padding-left: 30px !important; @@ -210,7 +217,7 @@ div.markdown-block ol { div.markdown-block ul li, div.markdown-block ol li { list-style: disc !important; - margin: 13px !important; + margin: 6px !important; padding: 0 !important; } 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 @@ -11,6 +11,7 @@ @import 'form-bootstrap'; @import 'codemirror'; @import 'legacy_code_styles'; +@import 'readme-box'; @import 'progress-bar'; @import 'type'; @@ -358,9 +359,26 @@ ul.auth_plugins { } } +.pr-mergeinfo { + clear: both; + margin: .5em 0; + + input { + min-width: 100% !important; + padding: 0 !important; + border: 0; + } +} + .pr-pullinfo { clear: both; margin: .5em 0; + + input { + min-width: 100% !important; + padding: 0 !important; + border: 0; + } } #pr-title-input { @@ -1297,6 +1315,11 @@ table.integrations { width: 100%; margin-bottom: 8px; } + +.reviewer_entry { + min-height: 55px; +} + .reviewers_member { width: 100%; overflow: auto; @@ -1331,6 +1354,8 @@ table.integrations { } } +.reviewer_member_mandatory, +.reviewer_member_mandatory_remove, .reviewer_member_remove { position: absolute; right: 0; @@ -1340,6 +1365,15 @@ table.integrations { padding: 0; color: black; } + +.reviewer_member_mandatory_remove { + color: @grey4; +} + +.reviewer_member_mandatory { + padding-top:20px; +} + .reviewer_member_status { margin-top: 5px; } @@ -1369,6 +1403,11 @@ table.integrations { .pr-description { white-space:pre-wrap; } + +.pr-reviewer-rules { + padding: 10px 0px 20px 0px; +} + .group_members { margin-top: 0; padding: 0; @@ -2008,6 +2047,10 @@ BIN_FILENODE = 7 input { display: none; } + margin-top: 10px; + } + .file-upload-label { + margin-top: 10px; } p { margin-top: 5px; diff --git a/rhodecode/public/css/navigation.less b/rhodecode/public/css/navigation.less --- a/rhodecode/public/css/navigation.less +++ b/rhodecode/public/css/navigation.less @@ -558,6 +558,8 @@ ul#context-pages { .dataTables_processing { text-align: center; font-size: 1.1em; + position: relative; + top: 95px; } .dataTables_paginate, .pagination-wh { diff --git a/rhodecode/public/css/panels.less b/rhodecode/public/css/panels.less --- a/rhodecode/public/css/panels.less +++ b/rhodecode/public/css/panels.less @@ -42,6 +42,10 @@ .panel-body { padding: @panel-padding; + + &.panel-body-min-height { + min-height: 150px + } } .panel-footer { diff --git a/rhodecode/public/css/readme-box.less b/rhodecode/public/css/readme-box.less new file mode 100644 --- /dev/null +++ b/rhodecode/public/css/readme-box.less @@ -0,0 +1,252 @@ +/** README styling **/ +div.readme_box { + clear: both; + overflow: hidden; + margin: 0; + padding: 3px 15px 3px; +} + +div.readme_box h1, +div.readme_box h2, +div.readme_box h3, +div.readme_box h4, +div.readme_box h5, +div.readme_box h6 { + border-bottom: none !important; + padding: 0 !important; + overflow: visible !important; +} + +div.readme_box h1 { + font-size: 32px; + margin: 15px 0 15px 0 !important; + padding-bottom: 5px !important; +} + +div.readme_box h2 { + font-size: 24px !important; + margin: 34px 0 10px 0 !important; + border-top: 3px #e6e5e5 solid !important; + padding-top: 15px !important; + padding-bottom: 8px !important; +} + +div.readme_box h3 { + font-size: 18px !important; + margin: 30px 0 8px 0 !important; + padding-bottom: 2px !important; +} + +div.readme_box h4 { + font-size: 13px !important; + margin: 18px 0 3px 0 !important; +} + +div.readme_box h5 { + font-size: 12px !important; + margin: 15px 0 3px 0 !important; +} + +div.readme_box h6 { + font-size: 12px; + color: #777777; + margin: 15px 0 3px 0 !important; +} + +div.readme_box hr { + border: 0; + color: #e6e5e5; + background-color: #e6e5e5; + height: 3px; + margin-bottom: 13px; +} + +div.readme_box ol, +div.readme_box ul, +div.readme_box p, +div.readme_box blockquote, +div.readme_box dl, +div.readme_box li, +div.readme_box table { + margin: 3px 0px 13px 0px !important; + color: #424242 !important; + font-size: 13px !important; + font-family: "Helvetica" !important; + font-weight: normal !important; + overflow: visible !important; + line-height: 140% !important; +} + +div.readme_box pre { + 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.readme_box img { + border-style: none; + background-color: #fff; +} + + +div.readme_box strong { + font-weight: 600; + margin: 0; +} + +div.readme_box ul, +div.readme_box ol { + padding-left: 30px !important; + margin-top: 0px !important; + margin-bottom: 18px !important; +} + +div.readme_box ul li, +div.readme_box ol li { + list-style: bullet !important; + margin: 6px !important; + padding: 0 !important; +} + +div.readme_box ol li { + list-style: decimal !important; +} + +/* +div.readme_box a, +div.readme_box a:visited { + color: #4183C4 !important; + background-color: inherit; + text-decoration: none; +} +*/ + + +div.readme_box button { + font-size: @basefontsize; + padding: 4px 6px; + .border-radius(@border-radius); + border: @border-thickness solid @grey5; + background-color: @grey6; +} + +div.readme_box code, +div.readme_box pre { + font-family: Monaco; + font-size: 11px; + .border-radius(@border-radius); + background-color: white; + color: @grey3; +} + + +div.readme_box code { + border: @border-thickness solid @grey6; + margin: 0 2px; + padding: 0 5px; +} + +div.readme_box pre { + border: @border-thickness solid @grey5; + overflow: auto; + padding: .5em; + background-color: @grey7; +} + +div.readme_box pre > code { + border: 0; + margin: 0; + padding: 0; +} + +/** RST STYLE **/ +div.rst-block { + clear: both; + overflow: hidden; + margin: 0; + padding: 3px 15px 3px; +} + +div.rst-block h2 { + font-weight: normal; +} + +div.rst-block h1, +div.rst-block h2, +div.rst-block h3, +div.rst-block h4, +div.rst-block h5, +div.rst-block h6 { + border-bottom: 0 !important; + margin: 0 !important; + padding: 0 !important; + line-height: 1.5em !important; +} + + +div.rst-block h1:first-child { + padding-top: .25em !important; +} + +div.rst-block h2, +div.rst-block h3 { + margin: 1em 0 !important; +} + +div.rst-block h2 { + margin-top: 1.5em !important; + border-top: 4px solid #e0e0e0 !important; + padding-top: .5em !important; +} + +div.rst-block p { + color: black !important; + margin: 1em 0 !important; + line-height: 1.5em !important; +} + +div.rst-block ul { + list-style: disc !important; + margin: 1em 0 1em 2em !important; + clear: both; +} + +div.rst-block ol { + list-style: decimal; + margin: 1em 0 1em 2em !important; +} + +div.rst-block pre, +div.rst-block code { + font: 12px "Bitstream Vera Sans Mono","Courier",monospace; +} + +div.rst-block code { + font-size: 12px !important; + background-color: ghostWhite !important; + color: #444 !important; + padding: 0 .2em !important; + border: 1px solid #dedede !important; +} + +div.rst-block pre code { + padding: 0 !important; + font-size: 12px !important; + background-color: #eee !important; + border: none !important; +} + +div.rst-block pre { + margin: 1em 0; + padding: @padding; + border: 1px solid @grey6; + .border-radius(@border-radius); + overflow: auto; + font-size: 12px; + color: #444; + background-color: @grey7; +} \ 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 @@ -57,9 +57,13 @@ white-space: pre-wrap; } - #clone_url, + #clone_url { + width: ~"calc(100% - 96px)"; + padding: @padding/4; + } + #clone_url_id { - min-width: 29em; + width: ~"calc(100% - 118px)"; padding: @padding/4; } @@ -211,6 +215,7 @@ float: left; display: block; position: relative; + width: 100%; // adds some space to make copy and paste easier .left-label, 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 @@ -108,6 +108,11 @@ table.dataTable { &.td-hash { min-width: 80px; width: 200px; + + .obsolete { + text-decoration: line-through; + color: lighten(@grey2,25%); + } } &.td-time { diff --git a/rhodecode/public/css/tags.less b/rhodecode/public/css/tags.less --- a/rhodecode/public/css/tags.less +++ b/rhodecode/public/css/tags.less @@ -98,4 +98,12 @@ &.admin { &:extend(.tag5); } -} \ No newline at end of file +} + +.phase-draft { + color: @color3 +} + +.phase-secret { + color:@grey3 +} diff --git a/rhodecode/public/images/ee_features/default_reviewers.png b/rhodecode/public/images/ee_features/default_reviewers.png new file mode 100644 index 0000000000000000000000000000000000000000..89483e9a4aefb207931d64ab3c2c8d1f53f6906a GIT binary patch literal 160468 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∾R5l*cDI* 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

@cReKOKvhxnW>V><;2w`9KhF{6e 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 z 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 iuzaPb7}!AB^!m~`h*GT9st`rFb~k_NmZ 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 diff --git a/rhodecode/public/js/rhodecode/base/keyboard-bindings.js b/rhodecode/public/js/rhodecode/base/keyboard-bindings.js --- a/rhodecode/public/js/rhodecode/base/keyboard-bindings.js +++ b/rhodecode/public/js/rhodecode/base/keyboard-bindings.js @@ -62,7 +62,7 @@ function setRCMouseBindings(repoName, re // nav in repo context Mousetrap.bind(['g s'], function(e) { window.location = pyroutes.url( - 'summary_home', {'repo_name': repoName}); + 'repo_summary', {'repo_name': repoName}); }); Mousetrap.bind(['g c'], function(e) { window.location = pyroutes.url( 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 @@ -7,8 +7,14 @@ var _TM = { '(from usergroup {0})': '(from usergroup {0})', 'Add another comment': 'Add another comment', + 'Adding new reviewers is forbidden.': 'Adding new reviewers is forbidden.', + 'All reviewers must vote.': 'All reviewers must vote.', + 'At least {0} reviewer must vote.': 'At least {0} reviewer must vote.', + 'At least {0} reviewers must vote.': 'At least {0} reviewers must vote.', + 'Author is not allowed to be a reviewer.': 'Author is not allowed to be a reviewer.', 'Close': 'Close', 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', + 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.', 'Delete this comment?': 'Delete this comment?', 'Diff to Commit ': 'Diff to Commit ', 'Follow': 'Follow', @@ -38,6 +44,7 @@ var _TM = { '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', + 'Reviewers picked from source code changes.': 'Reviewers picked from source code changes.', 'Saving...': 'Saving...', 'Searching...': 'Searching...', 'Selection link': 'Selection link', @@ -59,6 +66,7 @@ var _TM = { 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.', 'Unfollow': 'Unfollow', 'Updating...': 'Updating...', + 'User `{0}` not allowed to be a reviewer': 'User `{0}` not allowed to be a reviewer', 'You can only select {0} item': 'You can only select {0} item', 'You can only select {0} items': 'You can only select {0} items', 'added manually by "{0}"': 'added manually by "{0}"', @@ -71,6 +79,7 @@ var _TM = { 'in {0}, {1}': 'in {0}, {1}', 'just now': 'just now', 'loading...': 'loading...', + 'member of "{0}"': 'member of "{0}"', 'resolve comment': 'resolve comment', 'showing {0} out of {1} commit': 'showing {0} out of {1} commit', 'showing {0} out of {1} commits': 'showing {0} out of {1} commits', 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 @@ -7,8 +7,14 @@ var _TM = { '(from usergroup {0})': '(from usergroup {0})', 'Add another comment': 'Add another comment', + 'Adding new reviewers is forbidden.': 'Adding new reviewers is forbidden.', + 'All reviewers must vote.': 'All reviewers must vote.', + 'At least {0} reviewer must vote.': 'At least {0} reviewer must vote.', + 'At least {0} reviewers must vote.': 'At least {0} reviewers must vote.', + 'Author is not allowed to be a reviewer.': 'Author is not allowed to be a reviewer.', 'Close': 'Close', 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', + 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.', 'Delete this comment?': 'Delete this comment?', 'Diff to Commit ': 'Diff to Commit ', 'Follow': 'Follow', @@ -38,6 +44,7 @@ var _TM = { 'Please delete {0} characters': 'Bitte {0} Zeichen löschen', 'Please enter {0} or more character': 'Bitte {0} oder mehr Zeichen eingeben', 'Please enter {0} or more characters': 'Bitte {0} oder mehr Zeichen eingeben', + 'Reviewers picked from source code changes.': 'Reviewers picked from source code changes.', 'Saving...': 'Saving...', 'Searching...': 'Searching...', 'Selection link': 'Selection link', @@ -59,6 +66,7 @@ var _TM = { 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.', 'Unfollow': 'Unfollow', 'Updating...': 'Updating...', + 'User `{0}` not allowed to be a reviewer': 'User `{0}` not allowed to be a reviewer', 'You can only select {0} item': 'You can only select {0} item', 'You can only select {0} items': 'You can only select {0} items', 'added manually by "{0}"': 'added manually by "{0}"', @@ -71,6 +79,7 @@ var _TM = { 'in {0}, {1}': 'in {0}, {1}', 'just now': 'jetzt gerade', 'loading...': 'loading...', + 'member of "{0}"': 'member of "{0}"', 'resolve comment': 'resolve comment', 'showing {0} out of {1} commit': 'showing {0} out of {1} commit', 'showing {0} out of {1} commits': 'showing {0} out of {1} commits', 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 @@ -7,8 +7,14 @@ var _TM = { '(from usergroup {0})': '(from usergroup {0})', 'Add another comment': 'Add another comment', + 'Adding new reviewers is forbidden.': 'Adding new reviewers is forbidden.', + 'All reviewers must vote.': 'All reviewers must vote.', + 'At least {0} reviewer must vote.': 'At least {0} reviewer must vote.', + 'At least {0} reviewers must vote.': 'At least {0} reviewers must vote.', + 'Author is not allowed to be a reviewer.': 'Author is not allowed to be a reviewer.', 'Close': 'Close', 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', + 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.', 'Delete this comment?': 'Delete this comment?', 'Diff to Commit ': 'Diff to Commit ', 'Follow': 'Follow', @@ -38,6 +44,7 @@ var _TM = { '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', + 'Reviewers picked from source code changes.': 'Reviewers picked from source code changes.', 'Saving...': 'Saving...', 'Searching...': 'Searching...', 'Selection link': 'Selection link', @@ -59,6 +66,7 @@ var _TM = { 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.', 'Unfollow': 'Unfollow', 'Updating...': 'Updating...', + 'User `{0}` not allowed to be a reviewer': 'User `{0}` not allowed to be a reviewer', 'You can only select {0} item': 'You can only select {0} item', 'You can only select {0} items': 'You can only select {0} items', 'added manually by "{0}"': 'added manually by "{0}"', @@ -71,6 +79,7 @@ var _TM = { 'in {0}, {1}': 'in {0}, {1}', 'just now': 'just now', 'loading...': 'loading...', + 'member of "{0}"': 'member of "{0}"', 'resolve comment': 'resolve comment', 'showing {0} out of {1} commit': 'showing {0} out of {1} commit', 'showing {0} out of {1} commits': 'showing {0} out of {1} commits', 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 @@ -7,8 +7,14 @@ var _TM = { '(from usergroup {0})': '(from usergroup {0})', 'Add another comment': 'Add another comment', + 'Adding new reviewers is forbidden.': 'Adding new reviewers is forbidden.', + 'All reviewers must vote.': 'All reviewers must vote.', + 'At least {0} reviewer must vote.': 'At least {0} reviewer must vote.', + 'At least {0} reviewers must vote.': 'At least {0} reviewers must vote.', + 'Author is not allowed to be a reviewer.': 'Author is not allowed to be a reviewer.', 'Close': 'Close', 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', + 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.', 'Delete this comment?': 'Delete this comment?', 'Diff to Commit ': 'Diff to Commit ', 'Follow': 'Follow', @@ -38,6 +44,7 @@ var _TM = { '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', + 'Reviewers picked from source code changes.': 'Reviewers picked from source code changes.', 'Saving...': 'Saving...', 'Searching...': 'Searching...', 'Selection link': 'Selection link', @@ -59,6 +66,7 @@ var _TM = { 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.', 'Unfollow': 'Unfollow', 'Updating...': 'Updating...', + 'User `{0}` not allowed to be a reviewer': 'User `{0}` not allowed to be a reviewer', 'You can only select {0} item': 'You can only select {0} item', 'You can only select {0} items': 'You can only select {0} items', 'added manually by "{0}"': 'added manually by "{0}"', @@ -71,6 +79,7 @@ var _TM = { 'in {0}, {1}': 'in {0}, {1}', 'just now': 'just now', 'loading...': 'loading...', + 'member of "{0}"': 'member of "{0}"', 'resolve comment': 'resolve comment', 'showing {0} out of {1} commit': 'showing {0} out of {1} commit', 'showing {0} out of {1} commits': 'showing {0} out of {1} commits', 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 @@ -7,8 +7,14 @@ var _TM = { '(from usergroup {0})': '(from usergroup {0})', 'Add another comment': 'Add another comment', + 'Adding new reviewers is forbidden.': 'Adding new reviewers is forbidden.', + 'All reviewers must vote.': 'All reviewers must vote.', + 'At least {0} reviewer must vote.': 'At least {0} reviewer must vote.', + 'At least {0} reviewers must vote.': 'At least {0} reviewers must vote.', + 'Author is not allowed to be a reviewer.': 'Author is not allowed to be a reviewer.', 'Close': 'Close', 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', + 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.', 'Delete this comment?': 'Delete this comment?', 'Diff to Commit ': 'Diff to Commit ', 'Follow': 'Follow', @@ -38,6 +44,7 @@ var _TM = { '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', + 'Reviewers picked from source code changes.': 'Reviewers picked from source code changes.', 'Saving...': 'Saving...', 'Searching...': 'Searching...', 'Selection link': 'Lien vers la sélection', @@ -59,6 +66,7 @@ var _TM = { 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.', 'Unfollow': 'Unfollow', 'Updating...': 'Updating...', + 'User `{0}` not allowed to be a reviewer': 'User `{0}` not allowed to be a reviewer', 'You can only select {0} item': 'You can only select {0} item', 'You can only select {0} items': 'You can only select {0} items', 'added manually by "{0}"': 'added manually by "{0}"', @@ -71,6 +79,7 @@ var _TM = { 'in {0}, {1}': 'in {0}, {1}', 'just now': 'à l’instant', 'loading...': 'Chargement…', + 'member of "{0}"': 'member of "{0}"', 'resolve comment': 'resolve comment', 'showing {0} out of {1} commit': 'showing {0} out of {1} commit', 'showing {0} out of {1} commits': 'showing {0} out of {1} commits', 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 @@ -7,8 +7,14 @@ var _TM = { '(from usergroup {0})': '(from usergroup {0})', 'Add another comment': 'Aggiungi un altro commento', + 'Adding new reviewers is forbidden.': 'Adding new reviewers is forbidden.', + 'All reviewers must vote.': 'All reviewers must vote.', + 'At least {0} reviewer must vote.': 'At least {0} reviewer must vote.', + 'At least {0} reviewers must vote.': 'At least {0} reviewers must vote.', + 'Author is not allowed to be a reviewer.': 'Author is not allowed to be a reviewer.', 'Close': 'Close', 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', + 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.', 'Delete this comment?': 'Delete this comment?', 'Diff to Commit ': 'Diff to Commit ', 'Follow': 'Segui', @@ -38,6 +44,7 @@ var _TM = { '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', + 'Reviewers picked from source code changes.': 'Reviewers picked from source code changes.', 'Saving...': 'Saving...', 'Searching...': 'Searching...', 'Selection link': 'Collegamento selezione', @@ -59,6 +66,7 @@ var _TM = { 'There are currently no open pull requests requiring your participation.': 'Al momento non ci sono richieste di PULL che richiedono il tuo intervento', 'Unfollow': 'Smetti di seguire', 'Updating...': 'Updating...', + 'User `{0}` not allowed to be a reviewer': 'User `{0}` not allowed to be a reviewer', 'You can only select {0} item': 'You can only select {0} item', 'You can only select {0} items': 'You can only select {0} items', 'added manually by "{0}"': 'added manually by "{0}"', @@ -71,6 +79,7 @@ var _TM = { 'in {0}, {1}': 'in {0}, {1}', 'just now': 'proprio ora', 'loading...': 'loading...', + 'member of "{0}"': 'member of "{0}"', 'resolve comment': 'resolve comment', 'showing {0} out of {1} commit': 'showing {0} out of {1} commit', 'showing {0} out of {1} commits': 'showing {0} out of {1} commits', 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 @@ -7,8 +7,14 @@ var _TM = { '(from usergroup {0})': '(from usergroup {0})', 'Add another comment': '別のコメントを追加', + 'Adding new reviewers is forbidden.': 'Adding new reviewers is forbidden.', + 'All reviewers must vote.': 'All reviewers must vote.', + 'At least {0} reviewer must vote.': 'At least {0} reviewer must vote.', + 'At least {0} reviewers must vote.': 'At least {0} reviewers must vote.', + 'Author is not allowed to be a reviewer.': 'Author is not allowed to be a reviewer.', 'Close': 'Close', 'Comment text will be set automatically based on currently selected status ({0}) ...': '選択したステータス ({0}) を元にコメントが自動的に設定されます...', + 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.', 'Delete this comment?': 'Delete this comment?', 'Diff to Commit ': 'Diff to Commit ', 'Follow': 'フォロー', @@ -38,6 +44,7 @@ var _TM = { 'Please delete {0} characters': '{0} 文字削除してください', 'Please enter {0} or more character': '{0} 文字以上入力してください', 'Please enter {0} or more characters': '{0} 文字以上入力してください', + 'Reviewers picked from source code changes.': 'Reviewers picked from source code changes.', 'Saving...': 'Saving...', 'Searching...': '検索中...', 'Selection link': 'セレクション・リンク', @@ -59,6 +66,7 @@ var _TM = { 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.', 'Unfollow': 'アンフォロー', 'Updating...': 'Updating...', + 'User `{0}` not allowed to be a reviewer': 'User `{0}` not allowed to be a reviewer', 'You can only select {0} item': '{0} 件のみ選択できます', 'You can only select {0} items': '{0} 件のみ選択できます', 'added manually by "{0}"': 'added manually by "{0}"', @@ -71,6 +79,7 @@ var _TM = { 'in {0}, {1}': 'in {0}, {1}', 'just now': 'たったいま', 'loading...': '読み込み中...', + 'member of "{0}"': 'member of "{0}"', 'resolve comment': 'resolve comment', 'showing {0} out of {1} commit': 'showing {0} out of {1} commit', 'showing {0} out of {1} commits': 'showing {0} out of {1} commits', diff --git a/rhodecode/public/js/rhodecode/i18n/js_translations.js b/rhodecode/public/js/rhodecode/i18n/js_translations.js --- a/rhodecode/public/js/rhodecode/i18n/js_translations.js +++ b/rhodecode/public/js/rhodecode/i18n/js_translations.js @@ -1,8 +1,14 @@ // AUTO GENERATED FILE FOR Babel JS-GETTEXT EXTRACTORS, DO NOT CHANGE _gettext('(from usergroup {0})'); _gettext('Add another comment'); +_gettext('Adding new reviewers is forbidden.'); +_gettext('All reviewers must vote.'); +_gettext('At least {0} reviewer must vote.'); +_gettext('At least {0} reviewers must vote.'); +_gettext('Author is not allowed to be a reviewer.'); _gettext('Close'); _gettext('Comment text will be set automatically based on currently selected status ({0}) ...'); +_gettext('Commit Authors are not allowed to be a reviewer.'); _gettext('Delete this comment?'); _gettext('Diff to Commit '); _gettext('Follow'); @@ -32,6 +38,7 @@ _gettext('Please delete {0} characters'); _gettext('Please enter {0} or more character'); _gettext('Please enter {0} or more characters'); +_gettext('Reviewers picked from source code changes.'); _gettext('Saving...'); _gettext('Searching...'); _gettext('Selection link'); @@ -53,6 +60,7 @@ _gettext('There are currently no open pull requests requiring your participation.'); _gettext('Unfollow'); _gettext('Updating...'); +_gettext('User `{0}` not allowed to be a reviewer'); _gettext('You can only select {0} item'); _gettext('You can only select {0} items'); _gettext('added manually by "{0}"'); @@ -65,6 +73,7 @@ _gettext('in {0}, {1}'); _gettext('just now'); _gettext('loading...'); +_gettext('member of "{0}"'); _gettext('resolve comment'); _gettext('showing {0} out of {1} commit'); _gettext('showing {0} out of {1} commits'); 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 @@ -7,8 +7,14 @@ var _TM = { '(from usergroup {0})': '(from usergroup {0})', 'Add another comment': 'Dodaj kolejny komentarz', + 'Adding new reviewers is forbidden.': 'Adding new reviewers is forbidden.', + 'All reviewers must vote.': 'All reviewers must vote.', + 'At least {0} reviewer must vote.': 'At least {0} reviewer must vote.', + 'At least {0} reviewers must vote.': 'At least {0} reviewers must vote.', + 'Author is not allowed to be a reviewer.': 'Author is not allowed to be a reviewer.', 'Close': 'Zamknij', 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', + 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.', 'Delete this comment?': 'Delete this comment?', 'Diff to Commit ': 'Diff to Commit ', 'Follow': 'Obserwuj', @@ -38,6 +44,7 @@ var _TM = { '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', + 'Reviewers picked from source code changes.': 'Reviewers picked from source code changes.', 'Saving...': 'Saving...', 'Searching...': 'Searching...', 'Selection link': 'Wybór linku', @@ -59,6 +66,7 @@ var _TM = { 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.', 'Unfollow': 'Nie obserwuj', 'Updating...': 'Updating...', + 'User `{0}` not allowed to be a reviewer': 'User `{0}` not allowed to be a reviewer', 'You can only select {0} item': 'You can only select {0} item', 'You can only select {0} items': 'You can only select {0} items', 'added manually by "{0}"': 'added manually by "{0}"', @@ -71,6 +79,7 @@ var _TM = { 'in {0}, {1}': 'in {0}, {1}', 'just now': 'przed chwilą', 'loading...': 'wczytywanie...', + 'member of "{0}"': 'member of "{0}"', 'resolve comment': 'resolve comment', 'showing {0} out of {1} commit': 'showing {0} out of {1} commit', 'showing {0} out of {1} commits': 'showing {0} out of {1} commits', 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 @@ -7,8 +7,14 @@ var _TM = { '(from usergroup {0})': '(from usergroup {0})', 'Add another comment': 'Adicionar outro comentário', + 'Adding new reviewers is forbidden.': 'Adding new reviewers is forbidden.', + 'All reviewers must vote.': 'All reviewers must vote.', + 'At least {0} reviewer must vote.': 'At least {0} reviewer must vote.', + 'At least {0} reviewers must vote.': 'At least {0} reviewers must vote.', + 'Author is not allowed to be a reviewer.': 'Author is not allowed to be a reviewer.', 'Close': 'Close', 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', + 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.', 'Delete this comment?': 'Delete this comment?', 'Diff to Commit ': 'Diff to Commit ', 'Follow': 'Seguir', @@ -38,6 +44,7 @@ var _TM = { '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', + 'Reviewers picked from source code changes.': 'Reviewers picked from source code changes.', 'Saving...': 'Saving...', 'Searching...': 'Searching...', 'Selection link': 'Link da seleção', @@ -59,6 +66,7 @@ var _TM = { 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.', 'Unfollow': 'Parar de seguir', 'Updating...': 'Updating...', + 'User `{0}` not allowed to be a reviewer': 'User `{0}` not allowed to be a reviewer', 'You can only select {0} item': 'You can only select {0} item', 'You can only select {0} items': 'You can only select {0} items', 'added manually by "{0}"': 'added manually by "{0}"', @@ -71,6 +79,7 @@ var _TM = { 'in {0}, {1}': 'in {0}, {1}', 'just now': 'agora há pouco', 'loading...': 'loading...', + 'member of "{0}"': 'member of "{0}"', 'resolve comment': 'resolve comment', 'showing {0} out of {1} commit': 'showing {0} out of {1} commit', 'showing {0} out of {1} commits': 'showing {0} out of {1} commits', 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 @@ -7,8 +7,14 @@ var _TM = { '(from usergroup {0})': '(from usergroup {0})', 'Add another comment': 'Добавить другой комментарий', + 'Adding new reviewers is forbidden.': 'Adding new reviewers is forbidden.', + 'All reviewers must vote.': 'All reviewers must vote.', + 'At least {0} reviewer must vote.': 'At least {0} reviewer must vote.', + 'At least {0} reviewers must vote.': 'At least {0} reviewers must vote.', + 'Author is not allowed to be a reviewer.': 'Author is not allowed to be a reviewer.', 'Close': 'Close', 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', + 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.', 'Delete this comment?': 'Delete this comment?', 'Diff to Commit ': 'Diff to Commit ', 'Follow': 'Наблюдать', @@ -38,6 +44,7 @@ var _TM = { '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', + 'Reviewers picked from source code changes.': 'Reviewers picked from source code changes.', 'Saving...': 'Saving...', 'Searching...': 'Searching...', 'Selection link': 'Ссылка выбора', @@ -59,6 +66,7 @@ var _TM = { 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.', 'Unfollow': 'Не наблюдать', 'Updating...': 'Updating...', + 'User `{0}` not allowed to be a reviewer': 'User `{0}` not allowed to be a reviewer', 'You can only select {0} item': 'You can only select {0} item', 'You can only select {0} items': 'You can only select {0} items', 'added manually by "{0}"': 'added manually by "{0}"', @@ -71,6 +79,7 @@ var _TM = { 'in {0}, {1}': 'in {0}, {1}', 'just now': 'прямо сейчас', 'loading...': 'загрузка...', + 'member of "{0}"': 'member of "{0}"', 'resolve comment': 'resolve comment', 'showing {0} out of {1} commit': 'showing {0} out of {1} commit', 'showing {0} out of {1} commits': 'showing {0} out of {1} commits', 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 @@ -7,8 +7,14 @@ var _TM = { '(from usergroup {0})': '(from usergroup {0})', 'Add another comment': 'Add another comment', + 'Adding new reviewers is forbidden.': 'Adding new reviewers is forbidden.', + 'All reviewers must vote.': 'All reviewers must vote.', + 'At least {0} reviewer must vote.': 'At least {0} reviewer must vote.', + 'At least {0} reviewers must vote.': 'At least {0} reviewers must vote.', + 'Author is not allowed to be a reviewer.': 'Author is not allowed to be a reviewer.', 'Close': 'Close', 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...', + 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.', 'Delete this comment?': 'Delete this comment?', 'Diff to Commit ': 'Diff to Commit ', 'Follow': 'Follow', @@ -38,6 +44,7 @@ var _TM = { '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', + 'Reviewers picked from source code changes.': 'Reviewers picked from source code changes.', 'Saving...': 'Saving...', 'Searching...': 'Searching...', 'Selection link': '选择链接', @@ -59,6 +66,7 @@ var _TM = { 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.', 'Unfollow': 'Unfollow', 'Updating...': 'Updating...', + 'User `{0}` not allowed to be a reviewer': 'User `{0}` not allowed to be a reviewer', 'You can only select {0} item': 'You can only select {0} item', 'You can only select {0} items': 'You can only select {0} items', 'added manually by "{0}"': 'added manually by "{0}"', @@ -71,6 +79,7 @@ var _TM = { 'in {0}, {1}': 'in {0}, {1}', 'just now': '刚才', 'loading...': 'loading...', + 'member of "{0}"': 'member of "{0}"', 'resolve comment': 'resolve comment', 'showing {0} out of {1} commit': 'showing {0} out of {1} commit', 'showing {0} out of {1} commits': 'showing {0} out of {1} commits', 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 @@ -12,22 +12,13 @@ ******************************************************************************/ function registerRCRoutes() { // routes registration - pyroutes.register('home', '/', []); - pyroutes.register('user_autocomplete_data', '/_users', []); - pyroutes.register('user_group_autocomplete_data', '/_user_groups', []); pyroutes.register('new_repo', '/_admin/create_repository', []); pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']); pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']); pyroutes.register('gists', '/_admin/gists', []); pyroutes.register('new_gist', '/_admin/gists/new', []); pyroutes.register('toggle_following', '/_admin/toggle_following', []); - pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']); - pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']); - pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']); - pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/default-reviewers', ['repo_name']); pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']); - pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']); - pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']); 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']); @@ -39,7 +30,6 @@ function registerRCRoutes() { pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']); pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']); pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']); - pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']); pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']); pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']); pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']); @@ -53,8 +43,6 @@ function registerRCRoutes() { 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_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']); pyroutes.register('favicon', '/favicon.ico', []); pyroutes.register('robots', '/robots.txt', []); pyroutes.register('auth_home', '/_admin/auth*traverse', []); @@ -73,17 +61,30 @@ function registerRCRoutes() { pyroutes.register('repo_integrations_new', '%(repo_name)s/settings/integrations/new', ['repo_name']); pyroutes.register('repo_integrations_create', '%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']); pyroutes.register('repo_integrations_edit', '%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']); + pyroutes.register('ops_ping', '_admin/ops/ping', []); + pyroutes.register('admin_home', '/_admin', []); + pyroutes.register('admin_audit_logs', '_admin/audit_logs', []); + pyroutes.register('pull_requests_global_0', '_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']); + pyroutes.register('pull_requests_global_1', '_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']); + pyroutes.register('pull_requests_global', '_admin/pull-request/%(pull_request_id)s', ['pull_request_id']); pyroutes.register('admin_settings_open_source', '_admin/settings/open_source', []); pyroutes.register('admin_settings_vcs_svn_generate_cfg', '_admin/settings/vcs/svn_generate_cfg', []); pyroutes.register('admin_settings_system', '_admin/settings/system', []); pyroutes.register('admin_settings_system_update', '_admin/settings/system/updates', []); pyroutes.register('admin_settings_sessions', '_admin/settings/sessions', []); pyroutes.register('admin_settings_sessions_cleanup', '_admin/settings/sessions/cleanup', []); + pyroutes.register('admin_permissions_ips', '_admin/permissions/ips', []); pyroutes.register('users', '_admin/users', []); pyroutes.register('users_data', '_admin/users_data', []); pyroutes.register('edit_user_auth_tokens', '_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']); pyroutes.register('edit_user_auth_tokens_add', '_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']); pyroutes.register('edit_user_auth_tokens_delete', '_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']); + pyroutes.register('edit_user_emails', '_admin/users/%(user_id)s/edit/emails', ['user_id']); + pyroutes.register('edit_user_emails_add', '_admin/users/%(user_id)s/edit/emails/new', ['user_id']); + pyroutes.register('edit_user_emails_delete', '_admin/users/%(user_id)s/edit/emails/delete', ['user_id']); + pyroutes.register('edit_user_ips', '_admin/users/%(user_id)s/edit/ips', ['user_id']); + pyroutes.register('edit_user_ips_add', '_admin/users/%(user_id)s/edit/ips/new', ['user_id']); + pyroutes.register('edit_user_ips_delete', '_admin/users/%(user_id)s/edit/ips/delete', ['user_id']); pyroutes.register('edit_user_groups_management', '_admin/users/%(user_id)s/edit/groups_management', ['user_id']); pyroutes.register('edit_user_groups_management_updates', '_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']); pyroutes.register('edit_user_audit_logs', '_admin/users/%(user_id)s/edit/audit', ['user_id']); @@ -95,11 +96,44 @@ function registerRCRoutes() { pyroutes.register('register', '/_admin/register', []); pyroutes.register('reset_password', '/_admin/password_reset', []); pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []); - pyroutes.register('repo_maintenance', '/%(repo_name)s/maintenance', ['repo_name']); - pyroutes.register('repo_maintenance_execute', '/%(repo_name)s/maintenance/execute', ['repo_name']); - pyroutes.register('strip', '/%(repo_name)s/strip', ['repo_name']); - pyroutes.register('strip_check', '/%(repo_name)s/strip_check', ['repo_name']); - pyroutes.register('strip_execute', '/%(repo_name)s/strip_execute', ['repo_name']); + pyroutes.register('home', '/', []); + pyroutes.register('user_autocomplete_data', '/_users', []); + pyroutes.register('user_group_autocomplete_data', '/_user_groups', []); + pyroutes.register('repo_list_data', '/_repos', []); + pyroutes.register('goto_switcher_data', '/_goto_data', []); + pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']); + pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']); + pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']); + pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']); + pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']); + pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']); + pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']); + pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']); + pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']); + pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']); + pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']); + pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']); + pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']); + pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']); + pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']); + pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']); + pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']); + pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']); + pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']); + pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']); + pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']); + pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']); + pyroutes.register('repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']); + pyroutes.register('repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']); + pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']); + pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']); + pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']); + pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']); + pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']); + pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']); + pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']); + pyroutes.register('search', '/_admin/search', []); + pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']); pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']); pyroutes.register('my_account_profile', '/_admin/my_account/profile', []); pyroutes.register('my_account_password', '/_admin/my_account/password', []); @@ -107,5 +141,14 @@ function registerRCRoutes() { pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []); pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []); pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []); + pyroutes.register('my_account_emails', '/_admin/my_account/emails', []); + pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []); + pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []); + pyroutes.register('my_account_repos', '/_admin/my_account/repos', []); + pyroutes.register('my_account_watched', '/_admin/my_account/watched', []); + pyroutes.register('my_account_perms', '/_admin/my_account/perms', []); + pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []); + pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []); + pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []); pyroutes.register('apiv2', '/_admin/api', []); } diff --git a/rhodecode/public/js/src/rhodecode/pullrequests.js b/rhodecode/public/js/src/rhodecode/pullrequests.js --- a/rhodecode/public/js/src/rhodecode/pullrequests.js +++ b/rhodecode/public/js/src/rhodecode/pullrequests.js @@ -16,77 +16,342 @@ // # RhodeCode Enterprise Edition, including its added features, Support services, // # and proprietary license terms, please see https://rhodecode.com/licenses/ + +var prButtonLockChecks = { + 'compare': false, + 'reviewers': false +}; + /** - * Pull request reviewers + * lock button until all checks and loads are made. E.g reviewer calculation + * should prevent from submitting a PR + * @param lockEnabled + * @param msg + * @param scope */ -var removeReviewMember = function(reviewer_id, mark_delete){ - var reviewer = $('#reviewer_{0}'.format(reviewer_id)); +var prButtonLock = function(lockEnabled, msg, scope) { + scope = scope || 'all'; + if (scope == 'all'){ + prButtonLockChecks['compare'] = !lockEnabled; + prButtonLockChecks['reviewers'] = !lockEnabled; + } else if (scope == 'compare') { + prButtonLockChecks['compare'] = !lockEnabled; + } else if (scope == 'reviewers'){ + prButtonLockChecks['reviewers'] = !lockEnabled; + } + var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers; + if (lockEnabled) { + $('#save').attr('disabled', 'disabled'); + } + else if (checksMeet) { + $('#save').removeAttr('disabled'); + } - if(typeof(mark_delete) === undefined){ - mark_delete = false; - } + if (msg) { + $('#pr_open_message').html(msg); + } +}; - if(mark_delete === true){ - if (reviewer){ - // now delete the input - $('#reviewer_{0} input'.format(reviewer_id)).remove(); - // mark as to-delete - var obj = $('#reviewer_{0}_name'.format(reviewer_id)); - obj.addClass('to-delete'); - obj.css({"text-decoration":"line-through", "opacity": 0.5}); - } - } - else{ - $('#reviewer_{0}'.format(reviewer_id)).remove(); - } + +/** +Generate Title and Description for a PullRequest. +In case of 1 commits, the title and description is that one commit +in case of multiple commits, we iterate on them with max N number of commits, +and build description in a form +- commitN +- commitN+1 +... + +Title is then constructed from branch names, or other references, +replacing '-' and '_' into spaces + +* @param sourceRef +* @param elements +* @param limit +* @returns {*[]} +*/ +var getTitleAndDescription = function(sourceRef, elements, limit) { + var title = ''; + var desc = ''; + + $.each($(elements).get().reverse().slice(0, limit), function(idx, value) { + var rawMessage = $(value).find('td.td-description .message').data('messageRaw'); + desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n'; + }); + // only 1 commit, use commit message as title + if (elements.length === 1) { + title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0]; + } + else { + // use reference name + title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter(); + } + + return [title, desc] }; -var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) { - var members = $('#review_members').get(0); - var reasons_html = ''; - var reasons_inputs = ''; - var reasons = reasons || []; - if (reasons) { - for (var i = 0; i < reasons.length; i++) { - reasons_html += '

- {0}
'.format(reasons[i]); - reasons_inputs += ''; + + +ReviewersController = function () { + var self = this; + this.$reviewRulesContainer = $('#review_rules'); + this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules'); + this.forbidReviewUsers = undefined; + this.$reviewMembers = $('#review_members'); + this.currentRequest = null; + + this.defaultForbidReviewUsers = function() { + return [ + {'username': 'default', + 'user_id': templateContext.default_user.user_id} + ]; + }; + + this.hideReviewRules = function() { + self.$reviewRulesContainer.hide(); + }; + + this.showReviewRules = function() { + self.$reviewRulesContainer.show(); + }; + + this.addRule = function(ruleText) { + self.showReviewRules(); + return '
- {0}
'.format(ruleText) + }; + + this.loadReviewRules = function(data) { + // reset forbidden Users + this.forbidReviewUsers = self.defaultForbidReviewUsers(); + + // reset state of review rules + self.$rulesList.html(''); + + if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) { + // default rule, case for older repo that don't have any rules stored + self.$rulesList.append( + self.addRule( + _gettext('All reviewers must vote.')) + ); + return self.forbidReviewUsers + } + + if (data.rules.voting !== undefined) { + if (data.rules.voting < 0){ + self.$rulesList.append( + self.addRule( + _gettext('All reviewers must vote.')) + ) + } else if (data.rules.voting === 1) { + self.$rulesList.append( + self.addRule( + _gettext('At least {0} reviewer must vote.').format(data.rules.voting)) + ) + + } else { + self.$rulesList.append( + self.addRule( + _gettext('At least {0} reviewers must vote.').format(data.rules.voting)) + ) + } + } + if (data.rules.use_code_authors_for_review) { + self.$rulesList.append( + self.addRule( + _gettext('Reviewers picked from source code changes.')) + ) + } + if (data.rules.forbid_adding_reviewers) { + $('#add_reviewer_input').remove(); + self.$rulesList.append( + self.addRule( + _gettext('Adding new reviewers is forbidden.')) + ) + } + if (data.rules.forbid_author_to_review) { + self.forbidReviewUsers.push(data.rules_data.pr_author); + self.$rulesList.append( + self.addRule( + _gettext('Author is not allowed to be a reviewer.')) + ) + } + if (data.rules.forbid_commit_author_to_review) { + + if (data.rules_data.forbidden_users) { + $.each(data.rules_data.forbidden_users, function(index, member_data) { + self.forbidReviewUsers.push(member_data) + }); + + } + + self.$rulesList.append( + self.addRule( + _gettext('Commit Authors are not allowed to be a reviewer.')) + ) + } + + return self.forbidReviewUsers + }; + + this.loadDefaultReviewers = function(sourceRepo, sourceRef, targetRepo, targetRef) { + + if (self.currentRequest) { + // make sure we cleanup old running requests before triggering this + // again + self.currentRequest.abort(); } - } - var tmpl = '
  • '+ - ''+ - '
    '+ - '
    '+ - '
    '+ - 'gravatar'+ - '{1}'+ - reasons_html + - ''+ - ''+ - '{3}'+ - ''+ - '
    ' + - ''+ - '
    '+ - ''+ - ''+ - '
  • ' ; + + $('.calculate-reviewers').show(); + // reset reviewer members + self.$reviewMembers.empty(); + + prButtonLock(true, null, 'reviewers'); + $('#user').hide(); // hide user autocomplete before load + + var url = pyroutes.url('repo_default_reviewers_data', + { + 'repo_name': templateContext.repo_name, + 'source_repo': sourceRepo, + 'source_ref': sourceRef[2], + 'target_repo': targetRepo, + 'target_ref': targetRef[2] + }); + + self.currentRequest = $.get(url) + .done(function(data) { + self.currentRequest = null; + + // review rules + self.loadReviewRules(data); + + for (var i = 0; i < data.reviewers.length; i++) { + var reviewer = data.reviewers[i]; + self.addReviewMember( + reviewer.user_id, reviewer.first_name, + reviewer.last_name, reviewer.username, + reviewer.gravatar_link, reviewer.reasons, + reviewer.mandatory); + } + $('.calculate-reviewers').hide(); + prButtonLock(false, null, 'reviewers'); + $('#user').show(); // show user autocomplete after load + }); + }; + + // check those, refactor + this.removeReviewMember = function(reviewer_id, mark_delete) { + var reviewer = $('#reviewer_{0}'.format(reviewer_id)); + + if(typeof(mark_delete) === undefined){ + mark_delete = false; + } + + if(mark_delete === true){ + if (reviewer){ + // now delete the input + $('#reviewer_{0} input'.format(reviewer_id)).remove(); + // mark as to-delete + var obj = $('#reviewer_{0}_name'.format(reviewer_id)); + obj.addClass('to-delete'); + obj.css({"text-decoration":"line-through", "opacity": 0.5}); + } + } + else{ + $('#reviewer_{0}'.format(reviewer_id)).remove(); + } + }; + + this.addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons, mandatory) { + var members = self.$reviewMembers.get(0); + var reasons_html = ''; + var reasons_inputs = ''; + var reasons = reasons || []; + var mandatory = mandatory || false; - var displayname = "{0} ({1} {2})".format( - nname, escapeHtml(fname), escapeHtml(lname)); - var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs); - // check if we don't have this ID already in - var ids = []; - var _els = $('#review_members li').toArray(); - for (el in _els){ - ids.push(_els[el].id) - } - if(ids.indexOf('reviewer_'+id) == -1){ - // only add if it's not there - members.innerHTML += element; - } + if (reasons) { + for (var i = 0; i < reasons.length; i++) { + reasons_html += '
    - {0}
    '.format(reasons[i]); + reasons_inputs += ''; + } + } + var tmpl = '' + + '
  • '+ + ''+ + '
    '+ + '
    '+ + '
    '+ + 'gravatar'+ + '{1}'+ + reasons_html + + ''+ + ''+ + '{3}'+ + ''; + + if (mandatory) { + tmpl += ''+ + '
    ' + + ''+ + '
    ' + + ''+ + '
    ' + + ''+ + '
    '; + + } else { + tmpl += ''+ + ''+ + '
    ' + + ''+ + '
    '; + } + // continue template + tmpl += ''+ + ''+ + '
  • ' ; + + var displayname = "{0} ({1} {2})".format( + nname, escapeHtml(fname), escapeHtml(lname)); + var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs); + // check if we don't have this ID already in + var ids = []; + var _els = self.$reviewMembers.find('li').toArray(); + for (el in _els){ + ids.push(_els[el].id) + } + + var userAllowedReview = function(userId) { + var allowed = true; + $.each(self.forbidReviewUsers, function(index, member_data) { + if (parseInt(userId) === member_data['user_id']) { + allowed = false; + return false // breaks the loop + } + }); + return allowed + }; + + var userAllowed = userAllowedReview(id); + if (!userAllowed){ + alert(_gettext('User `{0}` not allowed to be a reviewer').format(nname)); + } + var shouldAdd = userAllowed && ids.indexOf('reviewer_'+id) == -1; + + if(shouldAdd) { + // only add if it's not there + members.innerHTML += element; + } + + }; + + this.updateReviewers = function(repo_name, pull_request_id){ + var postData = '_method=put&' + $('#reviewers input').serialize(); + _updatePullRequest(repo_name, pull_request_id, postData); + }; }; + var _updatePullRequest = function(repo_name, pull_request_id, postData) { var url = pyroutes.url( 'pullrequest_update', @@ -102,23 +367,6 @@ var _updatePullRequest = function(repo_n ajaxPOST(url, postData, success); }; -var updateReviewers = function(reviewers_ids, repo_name, pull_request_id){ - if (reviewers_ids === undefined){ - var postData = '_method=put&' + $('#reviewers input').serialize(); - _updatePullRequest(repo_name, pull_request_id, postData); - } -}; - -/** - * PULL REQUEST reject & close - */ -var closePullRequest = function(repo_name, pull_request_id) { - var postData = { - '_method': 'put', - 'close_pull_request': true}; - _updatePullRequest(repo_name, pull_request_id, postData); -}; - /** * PULL REQUEST update commits */ @@ -198,8 +446,8 @@ var initPullRequestsCodeMirror = functio /** * Reviewer autocomplete */ -var ReviewerAutoComplete = function(input_id) { - $('#'+input_id).autocomplete({ +var ReviewerAutoComplete = function(inputId) { + $(inputId).autocomplete({ serviceUrl: pyroutes.url('user_autocomplete_data'), minChars:2, maxHeight:400, @@ -207,14 +455,28 @@ var ReviewerAutoComplete = function(inpu showNoSuggestionNotice: true, tabDisabled: true, autoSelectFirst: true, + params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true }, formatResult: autocompleteFormatResult, lookupFilter: autocompleteFilterResult, - onSelect: function(suggestion, data){ - var msg = _gettext('added manually by "{0}"'); - var reasons = [msg.format(templateContext.rhodecode_user.username)]; - addReviewMember(data.id, data.first_name, data.last_name, - data.username, data.icon_link, reasons); - $('#'+input_id).val(''); + onSelect: function(element, data) { + + var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)]; + if (data.value_type == 'user_group') { + reasons.push(_gettext('member of "{0}"').format(data.value_display)); + + $.each(data.members, function(index, member_data) { + reviewersController.addReviewMember( + member_data.id, member_data.first_name, member_data.last_name, + member_data.username, member_data.icon_link, reasons); + }) + + } else { + reviewersController.addReviewMember( + data.id, data.first_name, data.last_name, + data.username, data.icon_link, reasons); + } + + $(inputId).val(''); } }); }; diff --git a/rhodecode/subscribers.py b/rhodecode/subscribers.py --- a/rhodecode/subscribers.py +++ b/rhodecode/subscribers.py @@ -121,7 +121,7 @@ def add_pylons_context(event): # Setup the pylons context object ('c') context = ContextObj() context.rhodecode_user = auth_user - attach_context_attributes(context, request) + attach_context_attributes(context, request, request.user.user_id) pylons.tmpl_context._push_object(context) @@ -130,12 +130,12 @@ 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: + from rhodecode.model.scm import ScmModel + from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path repositories = ScmModel().repo_scan(get_rhodecode_base_path()) repo2db_mapper(repositories, remove_obsolete=False) @@ -172,6 +172,10 @@ def write_metadata_if_needed(event): with open(metadata_destination, 'wb') as f: f.write(ext_json.json.dumps(metadata)) + settings = event.app.registry.settings + if settings.get('metadata.skip'): + return + try: write() except Exception: diff --git a/rhodecode/templates/admin/admin.mako b/rhodecode/templates/admin/admin_audit_logs.mako rename from rhodecode/templates/admin/admin.mako rename to rhodecode/templates/admin/admin_audit_logs.mako --- a/rhodecode/templates/admin/admin.mako +++ b/rhodecode/templates/admin/admin_audit_logs.mako @@ -2,7 +2,7 @@ <%inherit file="/base/base.mako"/> <%def name="title()"> - ${_('Admin journal')} + ${_('Admin audit logs')} %if c.rhodecode_name: · ${h.branding(c.rhodecode_name)} %endif @@ -10,9 +10,9 @@ <%def name="breadcrumbs_links()"> ${h.form(None, id_="filter_form", method="get")} - + - ${_('Admin journal')} - ${ungettext('%s entry', '%s entries', c.users_log.item_count) % (c.users_log.item_count)} + ${_('Audit logs')} - ${_ungettext('%s entry', '%s entries', c.audit_logs.item_count) % (c.audit_logs.item_count)} ${h.end_form()}

    ${_('Example Queries')}

    @@ -29,7 +29,7 @@
    - ${c.log_data} + <%include file="/admin/admin_log_base.mako" />
    diff --git a/rhodecode/templates/admin/admin_log.mako b/rhodecode/templates/admin/admin_log.mako deleted file mode 100644 --- a/rhodecode/templates/admin/admin_log.mako +++ /dev/null @@ -1,60 +0,0 @@ -## -*- coding: utf-8 -*- -<%namespace name="base" file="/base/base.mako"/> - -%if c.users_log: - - - - - - - - - - %for cnt,l in enumerate(c.users_log): - - - - - - - - - %endfor -
    ${_('Username')}${_('Action')}${_('Repository')}${_('Date')}${_('From IP')}
    - %if l.user is not None: - ${base.gravatar_with_user(l.user.email)} - %else: - ${l.username} - %endif - ${h.action_parser(l)[0]()} -
    - ${h.literal(h.action_parser(l)[1]())} -
    -
    - %if l.repository is not None: - ${h.link_to(l.repository.repo_name,h.url('summary_home',repo_name=l.repository.repo_name))} - %else: - ${l.repository_name} - %endif - ${h.format_date(l.action_date)}${l.user_ip}
    - -
    -${c.users_log.pager('$link_previous ~2~ $link_next')} -
    -%else: - ${_('No actions yet')} -%endif - - - diff --git a/rhodecode/templates/admin/admin_log_base.mako b/rhodecode/templates/admin/admin_log_base.mako new file mode 100644 --- /dev/null +++ b/rhodecode/templates/admin/admin_log_base.mako @@ -0,0 +1,65 @@ +<%namespace name="base" file="/base/base.mako"/> + +%if c.audit_logs: + + + + + + + + + + + %for cnt,l in enumerate(c.audit_logs): + + + + + + + + + + %endfor +
    ${_('Username')}${_('Action')}${_('Action Data')}${_('Repository')}${_('Date')}${_('IP')}
    + %if l.user is not None: + ${base.gravatar_with_user(l.user.email)} + %else: + ${l.username} + %endif + + % if l.version == l.VERSION_1: + ${h.action_parser(l)[0]()} + % else: + ${h.literal(l.action)} + % endif + +
    + % if l.version == l.VERSION_1: + ${h.literal(h.action_parser(l)[1]())} + % endif +
    +
    + % if l.version == l.VERSION_2: + ${_('toggle')} + + % else: +
    -
    + % endif +
    + %if l.repository is not None: + ${h.link_to(l.repository.repo_name, h.route_path('repo_summary',repo_name=l.repository.repo_name))} + %else: + ${l.repository_name} + %endif + ${h.format_date(l.action_date)}${l.user_ip}
    + +
    +${c.audit_logs.pager('$link_previous ~2~ $link_next')} +
    +%else: + ${_('No actions yet')} +%endif \ No newline at end of file diff --git a/rhodecode/templates/admin/auth/auth_settings.mako b/rhodecode/templates/admin/auth/auth_settings.mako --- a/rhodecode/templates/admin/auth/auth_settings.mako +++ b/rhodecode/templates/admin/auth/auth_settings.mako @@ -9,7 +9,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${_('Authentication Plugins')} diff --git a/rhodecode/templates/admin/auth/plugin_settings.mako b/rhodecode/templates/admin/auth/plugin_settings.mako --- a/rhodecode/templates/admin/auth/plugin_settings.mako +++ b/rhodecode/templates/admin/auth/plugin_settings.mako @@ -9,7 +9,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Authentication Plugins'),request.resource_path(resource.__parent__, route_name='auth_home'))} » diff --git a/rhodecode/templates/admin/defaults/defaults.mako b/rhodecode/templates/admin/defaults/defaults.mako --- a/rhodecode/templates/admin/defaults/defaults.mako +++ b/rhodecode/templates/admin/defaults/defaults.mako @@ -9,7 +9,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${_('Repositories defaults')} diff --git a/rhodecode/templates/admin/gists/show.mako b/rhodecode/templates/admin/gists/show.mako --- a/rhodecode/templates/admin/gists/show.mako +++ b/rhodecode/templates/admin/gists/show.mako @@ -73,7 +73,7 @@
    -
    +
    ${self.gravatar_with_user(c.file_last_commit.author, 16)} - ${_('created')} ${h.age_component(c.file_last_commit.date)}
    diff --git a/rhodecode/templates/admin/integrations/base.mako b/rhodecode/templates/admin/integrations/base.mako --- a/rhodecode/templates/admin/integrations/base.mako +++ b/rhodecode/templates/admin/integrations/base.mako @@ -18,7 +18,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${_('Integrations')} diff --git a/rhodecode/templates/admin/integrations/form.mako b/rhodecode/templates/admin/integrations/form.mako --- a/rhodecode/templates/admin/integrations/form.mako +++ b/rhodecode/templates/admin/integrations/form.mako @@ -3,7 +3,7 @@ <%def name="breadcrumbs_links()"> %if c.repo: - ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))} + ${h.link_to('Settings',h.route_path('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))} » @@ -12,7 +12,7 @@ repo_name=c.repo.repo_name, integration=current_IntegrationType.key))} %elif c.repo_group: - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Repository Groups'),h.url('repo_groups'))} » @@ -25,7 +25,7 @@ repo_group_name=c.repo_group.group_name, integration=current_IntegrationType.key))} %else: - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Settings'),h.url('admin_settings'))} » diff --git a/rhodecode/templates/admin/integrations/list.mako b/rhodecode/templates/admin/integrations/list.mako --- a/rhodecode/templates/admin/integrations/list.mako +++ b/rhodecode/templates/admin/integrations/list.mako @@ -3,15 +3,15 @@ <%def name="breadcrumbs_links()"> %if c.repo: - ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))} + ${h.link_to('Settings',h.route_path('edit_repo', repo_name=c.repo.repo_name))} %elif c.repo_group: - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Repository Groups'),h.url('repo_groups'))} » ${h.link_to(c.repo_group.group_name,h.url('edit_repo_group', group_name=c.repo_group.group_name))} %else: - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Settings'),h.url('admin_settings'))} %endif @@ -160,11 +160,11 @@ %if integration.repo: - + ${_('repo')}:${integration.repo.repo_name} %elif integration.repo_group: - + ${_('repogroup')}:${integration.repo_group.group_name} %if integration.child_repos_only: ${_('child repos only')} diff --git a/rhodecode/templates/admin/integrations/new.mako b/rhodecode/templates/admin/integrations/new.mako --- a/rhodecode/templates/admin/integrations/new.mako +++ b/rhodecode/templates/admin/integrations/new.mako @@ -4,11 +4,11 @@ <%def name="breadcrumbs_links()"> %if c.repo: - ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))} + ${h.link_to('Settings',h.route_path('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))} %elif c.repo_group: - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Repository Groups'),h.url('repo_groups'))} » @@ -16,7 +16,7 @@ » ${h.link_to(_('Integrations'),request.route_url(route_name='repo_group_integrations_home', repo_group_name=c.repo_group.group_name))} %else: - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Settings'),h.url('admin_settings'))} » diff --git a/rhodecode/templates/admin/my_account/my_account.mako b/rhodecode/templates/admin/my_account/my_account.mako --- a/rhodecode/templates/admin/my_account/my_account.mako +++ b/rhodecode/templates/admin/my_account/my_account.mako @@ -34,12 +34,12 @@ % if my_account_oauth_url:
  • ${_('OAuth Identities')}
  • % endif -
  • ${_('Emails')}
  • -
  • ${_('Repositories')}
  • -
  • ${_('Watched')}
  • +
  • ${_('Emails')}
  • +
  • ${_('Repositories')}
  • +
  • ${_('Watched')}
  • ${_('Pull Requests')}
  • -
  • ${_('Permissions')}
  • -
  • ${_('Live Notifications')}
  • +
  • ${_('Permissions')}
  • +
  • ${_('Live Notifications')}
  • diff --git a/rhodecode/templates/admin/my_account/my_account_auth_tokens.mako b/rhodecode/templates/admin/my_account/my_account_auth_tokens.mako --- a/rhodecode/templates/admin/my_account/my_account_auth_tokens.mako +++ b/rhodecode/templates/admin/my_account/my_account_auth_tokens.mako @@ -43,9 +43,9 @@ ${h.secure_form(h.route_path('my_account_auth_tokens_delete'), method='post')} - ${h.hidden('del_auth_token',auth_token.api_key)} + ${h.hidden('del_auth_token', auth_token.user_api_key_id)} ${h.end_form()} @@ -139,7 +139,7 @@ var repoFilter = function(data) { query.callback({results: cachedData.results}); } else { $.ajax({ - url: "${h.url('repo_list_data')}", + url: pyroutes.url('repo_list_data'), data: {'query': query.term}, dataType: 'json', type: 'GET', diff --git a/rhodecode/templates/admin/my_account/my_account_emails.mako b/rhodecode/templates/admin/my_account/my_account_emails.mako --- a/rhodecode/templates/admin/my_account/my_account_emails.mako +++ b/rhodecode/templates/admin/my_account/my_account_emails.mako @@ -25,10 +25,10 @@ - ${h.secure_form(url('my_account_emails'),method='delete')} + ${h.secure_form(h.route_path('my_account_emails_delete'), method='POST')} ${h.hidden('del_email_id',em.email_id)} - ${h.end_form()} @@ -48,7 +48,7 @@
    - ${h.secure_form(url('my_account_emails'), method='post')} + ${h.secure_form(h.route_path('my_account_emails_add'), method='POST')}
    diff --git a/rhodecode/templates/admin/my_account/my_account_notifications.mako b/rhodecode/templates/admin/my_account/my_account_notifications.mako --- a/rhodecode/templates/admin/my_account/my_account_notifications.mako +++ b/rhodecode/templates/admin/my_account/my_account_notifications.mako @@ -1,7 +1,7 @@