release: Merge default into stable for release preparation

This commit is contained in:
Marcin Kuzminski 2016-09-15 14:25:45 +00:00
commit 34ef01a0c0
177 changed files with 17865 additions and 5827 deletions

View file

@ -1,5 +1,5 @@
[bumpversion]
current_version = 4.3.1
current_version = 4.4.0
message = release: Bump version {current_version} to {new_version}
[bumpversion:file:rhodecode/VERSION]

View file

@ -8,6 +8,7 @@ syntax: glob
*.swp
*.tox
*.DS_Store*
rhodecode/public/js/src/components/**/*.css
syntax: regexp
@ -23,6 +24,7 @@ syntax: regexp
^_dev
^._dev
^build/
^bower_components/
^coverage\.xml$
^data$
^\.eggs/
@ -38,7 +40,11 @@ syntax: regexp
^rcextensions/
^result$
^rhodecode/public/css/style.css$
^rhodecode/public/css/style-polymer.css$
^rhodecode/public/js/rhodecode-components.html$
^rhodecode/public/js/scripts.js$
^rhodecode/public/js/src/components/root-styles.gen.html$
^rhodecode/public/js/vendors/webcomponentsjs/
^rhodecode\.db$
^rhodecode\.log$
^rhodecode_dev\.log$

View file

@ -4,27 +4,22 @@ done = false
[task:bump_version]
done = true
[task:rc_tools_pinned]
done = true
[task:fixes_on_stable]
done = true
[task:pip2nix_generated]
done = true
[task:changelog_updated]
done = true
[task:generate_api_docs]
done = true
[release]
state = prepared
version = 4.3.1
[task:updated_translation]
[release]
state = in_progress
version = 4.4.0
[task:rc_tools_pinned]
[task:generate_js_routes]
[task:updated_trial_license]

View file

@ -1,144 +1,15 @@
var gruntConfig = require('./grunt_config.json');
module.exports = function(grunt) {
grunt.initConfig({
dirs: {
css: "rhodecode/public/css",
js: {
"src": "rhodecode/public/js/src",
"dest": "rhodecode/public/js"
}
},
concat: {
dist: {
src: [
// Base libraries
'<%= dirs.js.src %>/jquery-1.11.1.min.js',
'<%= dirs.js.src %>/logging.js',
'<%= dirs.js.src %>/bootstrap.js',
'<%= dirs.js.src %>/mousetrap.js',
'<%= dirs.js.src %>/moment.js',
'<%= dirs.js.src %>/appenlight-client-0.4.1.min.js',
'<%= dirs.js.src %>/i18n_utils.js',
'<%= dirs.js.src %>/deform.js',
// Plugins
'<%= dirs.js.src %>/plugins/jquery.pjax.js',
'<%= dirs.js.src %>/plugins/jquery.dataTables.js',
'<%= dirs.js.src %>/plugins/flavoured_checkbox.js',
'<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js',
'<%= dirs.js.src %>/plugins/jquery.autocomplete.js',
'<%= dirs.js.src %>/plugins/jquery.debounce.js',
'<%= dirs.js.src %>/plugins/jquery.mark.js',
'<%= dirs.js.src %>/plugins/jquery.timeago.js',
'<%= dirs.js.src %>/plugins/jquery.timeago-extension.js',
'<%= dirs.js.src %>/plugins/toastr.js',
// Select2
'<%= dirs.js.src %>/select2/select2.js',
// Code-mirror
'<%= dirs.js.src %>/codemirror/codemirror.js',
'<%= dirs.js.src %>/codemirror/codemirror_loadmode.js',
'<%= dirs.js.src %>/codemirror/codemirror_hint.js',
'<%= dirs.js.src %>/codemirror/codemirror_overlay.js',
'<%= dirs.js.src %>/codemirror/codemirror_placeholder.js',
// TODO: mikhail: this is an exception. Since the code mirror modes
// are loaded "on the fly", we need to keep them in a public folder
'<%= dirs.js.dest %>/mode/meta.js',
'<%= dirs.js.dest %>/mode/meta_ext.js',
'<%= dirs.js.dest %>/rhodecode/i18n/select2/translations.js',
// Rhodecode utilities
'<%= dirs.js.src %>/rhodecode/utils/array.js',
'<%= dirs.js.src %>/rhodecode/utils/string.js',
'<%= dirs.js.src %>/rhodecode/utils/pyroutes.js',
'<%= dirs.js.src %>/rhodecode/utils/ajax.js',
'<%= dirs.js.src %>/rhodecode/utils/autocomplete.js',
'<%= dirs.js.src %>/rhodecode/utils/colorgenerator.js',
'<%= dirs.js.src %>/rhodecode/utils/ie.js',
'<%= dirs.js.src %>/rhodecode/utils/os.js',
'<%= dirs.js.src %>/rhodecode/utils/topics.js',
// Rhodecode widgets
'<%= dirs.js.src %>/rhodecode/widgets/multiselect.js',
// Rhodecode components
'<%= dirs.js.src %>/rhodecode/init.js',
'<%= dirs.js.src %>/rhodecode/connection_controller.js',
'<%= dirs.js.src %>/rhodecode/codemirror.js',
'<%= dirs.js.src %>/rhodecode/comments.js',
'<%= dirs.js.src %>/rhodecode/constants.js',
'<%= dirs.js.src %>/rhodecode/files.js',
'<%= dirs.js.src %>/rhodecode/followers.js',
'<%= dirs.js.src %>/rhodecode/menus.js',
'<%= dirs.js.src %>/rhodecode/notifications.js',
'<%= dirs.js.src %>/rhodecode/permissions.js',
'<%= dirs.js.src %>/rhodecode/pjax.js',
'<%= dirs.js.src %>/rhodecode/pullrequests.js',
'<%= dirs.js.src %>/rhodecode/settings.js',
'<%= dirs.js.src %>/rhodecode/select2_widgets.js',
'<%= dirs.js.src %>/rhodecode/tooltips.js',
'<%= dirs.js.src %>/rhodecode/users.js',
'<%= dirs.js.src %>/rhodecode/utils/notifications.js',
'<%= dirs.js.src %>/rhodecode/appenlight.js',
// Rhodecode main module
'<%= dirs.js.src %>/rhodecode.js'
],
dest: '<%= dirs.js.dest %>/scripts.js',
nonull: true
}
},
less: {
development: {
options: {
compress: false,
yuicompress: false,
optimization: 0
},
files: {
"<%= dirs.css %>/style.css": "<%= dirs.css %>/main.less"
}
},
production: {
options: {
compress: true,
yuicompress: true,
optimization: 2
},
files: {
"<%= dirs.css %>/style.css": "<%= dirs.css %>/main.less"
}
}
},
watch: {
less: {
files: ["<%= dirs.css %>/*.less"],
tasks: ["less:production"]
},
js: {
files: ["<%= dirs.js.src %>/**/*.js"],
tasks: ["concat:dist"]
}
},
jshint: {
rhodecode: {
src: '<%= dirs.js.src %>/rhodecode/**/*.js',
options: {
jshintrc: '.jshintrc'
}
}
}
});
grunt.initConfig(gruntConfig);
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-vulcanize');
grunt.loadNpmTasks('grunt-crisper');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.registerTask('default', ['less:production', 'concat:dist']);
grunt.registerTask('default', ['less:production', 'less:components', 'concat:polymercss', 'copy','vulcanize', 'crisper', 'concat:dist']);
};

View file

@ -29,6 +29,9 @@ recursive-include rhodecode *.mako
# 502 page
include rhodecode/public/502.html
# 502 page
include rhodecode/public/502.html
# images, css
include rhodecode/public/css/*.css
include rhodecode/public/images/*.*

15
bower.json Normal file
View file

@ -0,0 +1,15 @@
{
"name": "rhodecode-elements",
"description": "User interface for elements for rhodecode",
"main": "index.html",
"dependencies": {
"webcomponentsjs": "^0.7.22",
"polymer": "Polymer/polymer#^1.6.1",
"paper-button": "PolymerElements/paper-button#^1.0.13",
"paper-spinner": "PolymerElements/paper-spinner#^1.2.0",
"paper-tooltip": "PolymerElements/paper-tooltip#^1.1.2",
"paper-toast": "PolymerElements/paper-toast#^1.3.0",
"paper-toggle-button": "PolymerElements/paper-toggle-button#^1.2.0",
"iron-ajax": "PolymerElements/iron-ajax#^1.4.3"
}
}

View file

@ -414,7 +414,7 @@ search.location = %(here)s/data/index
## channelstream enables persistent connections and live notification
## in the system. It's also used by the chat system
channelstream.enabled = true
channelstream.enabled = false
## location of channelstream server on the backend
channelstream.server = 127.0.0.1:9800
## location of the channelstream server from outside world

View file

@ -388,7 +388,7 @@ search.location = %(here)s/data/index
## channelstream enables persistent connections and live notification
## in the system. It's also used by the chat system
channelstream.enabled = true
channelstream.enabled = false
## location of channelstream server on the backend
channelstream.server = 127.0.0.1:9800
## location of the channelstream server from outside world

View file

@ -30,6 +30,10 @@ let
then pythonPackages
else getAttr pythonPackages pkgs;
buildBowerComponents =
pkgs.buildBowerComponents or
(import ./pkgs/backport-16.03-build-bower-components.nix { inherit pkgs; });
elem = builtins.elem;
basename = path: with pkgs.lib; last (splitString "/" path);
startsWith = prefix: full: let
@ -41,31 +45,28 @@ let
ext = last (splitString "." path);
in
!elem (basename path) [
".git" ".hg" "__pycache__" ".eggs" "node_modules"
"build" "data" "tmp"] &&
".git" ".hg" "__pycache__" ".eggs"
"bower_components" "node_modules"
"build" "data" "result" "tmp"] &&
!elem ext ["egg-info" "pyc"] &&
# TODO: johbo: This check is wrong, since "path" contains an absolute path,
# it would still be good to restore it since we want to ignore "result-*".
!startsWith "result" path;
sources = pkgs.config.rc.sources or {};
version = builtins.readFile ./rhodecode/VERSION;
rhodecode-enterprise-ce-src = builtins.filterSource src-filter ./.;
# Load the generated node packages
nodePackages = pkgs.callPackage "${pkgs.path}/pkgs/top-level/node-packages.nix" rec {
self = nodePackages;
generated = pkgs.callPackage ./pkgs/node-packages.nix { inherit self; };
nodeEnv = import ./pkgs/node-default.nix {
inherit pkgs;
};
nodeDependencies = nodeEnv.shell.nodeDependencies;
# TODO: Should be taken automatically out of the generates packages.
# apps.nix has one solution for this, although I'd prefer to have the deps
# from package.json mapped in here.
nodeDependencies = with nodePackages; [
grunt
grunt-contrib-concat
grunt-contrib-jshint
grunt-contrib-less
grunt-contrib-watch
jshint
];
bowerComponents = buildBowerComponents {
name = "enterprise-ce-${version}";
generated = ./pkgs/bower-packages.nix;
src = rhodecode-enterprise-ce-src;
};
pythonGeneratedPackages = self: basePythonPackages.override (a: {
inherit self;
@ -86,16 +87,25 @@ let
pythonLocalOverrides = self: super: {
rhodecode-enterprise-ce =
let
version = builtins.readFile ./rhodecode/VERSION;
linkNodeModules = ''
linkNodeAndBowerPackages = ''
echo "Export RhodeCode CE path"
export RHODECODE_CE_PATH=${rhodecode-enterprise-ce-src}
echo "Link node packages"
# TODO: check if this adds stuff as a dependency, closure size
rm -fr node_modules
mkdir -p node_modules
${pkgs.lib.concatMapStrings (dep: ''
ln -sfv ${dep}/lib/node_modules/${dep.pkgName} node_modules/
'') nodeDependencies}
mkdir node_modules
# johbo: Linking individual packages allows us to run "npm install"
# inside of a shell to try things out. Re-entering the shell will
# restore a clean environment.
ln -s ${nodeDependencies}/lib/node_modules/* node_modules/
echo "DONE: Link node packages"
echo "Link bower packages"
rm -fr bower_components
mkdir bower_components
ln -s ${bowerComponents}/bower_components/* bower_components/
echo "DONE: Link bower packages"
'';
in super.rhodecode-enterprise-ce.override (attrs: {
@ -109,6 +119,7 @@ let
buildInputs =
attrs.buildInputs ++
(with self; [
pkgs.nodePackages.bower
pkgs.nodePackages.grunt-cli
pkgs.subversion
pytest-catchlog
@ -123,7 +134,8 @@ let
# pkgs/default.nix?
passthru = {
inherit
linkNodeModules
bowerComponents
linkNodeAndBowerPackages
myPythonPackagesUnfix
pythonLocalOverrides;
pythonPackages = self;
@ -145,7 +157,7 @@ let
export PYTHONPATH="$tmp_path/${self.python.sitePackages}:$PYTHONPATH"
mkdir -p $tmp_path/${self.python.sitePackages}
python setup.py develop --prefix $tmp_path --allow-hosts ""
'' + linkNodeModules;
'' + linkNodeAndBowerPackages;
preCheck = ''
export PATH="$out/bin:$PATH"
@ -156,7 +168,7 @@ let
rm -rf $out/lib/${self.python.libPrefix}/site-packages/rhodecode/tests
'';
preBuild = linkNodeModules + ''
preBuild = linkNodeAndBowerPackages + ''
grunt
rm -fr node_modules
'';

View file

@ -29,13 +29,3 @@ use the following instructions:
:menuselection:`Admin --> Settings --> labs` page.
.. image:: ../images/lab-setting.png
Available Lab Extras
--------------------
Once lab settings are enabled, the following features are available.
.. toctree::
:maxdepth: 1
svn-http

View file

@ -1,130 +0,0 @@
.. _svn-http:
|svn| With Write Over HTTP
--------------------------
To use |svn| with read/write support over the |svn| protocol, you have to
configure HTTP |svn| backend.
Prerequisites
^^^^^^^^^^^^^
- Enable HTTP support inside labs setting on your |RCE| instance,
see :ref:`lab-settings`.
- You need to install the following tools on the machine that is running an
instance of |RCE|:
``Apache HTTP Server`` and
``mod_dav_svn``.
Using Ubuntu Distribution as an example you can run:
.. code-block:: bash
$ sudo apt-get install apache2 libapache2-mod-svn
Once installed you need to enable ``dav_svn`` and ``anon``:
.. code-block:: bash
$ sudo a2enmod dav_svn
$ sudo a2enmod authn_anon
Configuring Apache Setup
^^^^^^^^^^^^^^^^^^^^^^^^
.. tip::
It is recommended to run Apache on a port other than 80, due to possible
conflicts with other HTTP servers like nginx. To do this, set the
``Listen`` parameter in the ``/etc/apache2/ports.conf`` file, for example
``Listen 8090``.
.. warning::
Make sure your Apache instance which runs the mod_dav_svn module is
only accessible by RhodeCode. Otherwise everyone is able to browse
the repositories or run subversion operations (checkout/commit/etc.).
It is also recommended to run apache as the same user as |RCE|, otherwise
permission issues could occur. To do this edit the ``/etc/apache2/envvars``
.. code-block:: apache
export APACHE_RUN_USER=rhodecode
export APACHE_RUN_GROUP=rhodecode
1. To configure Apache, create and edit a virtual hosts file, for example
:file:`/etc/apache2/sites-available/default.conf`. Below is an example
how to use one with auto-generated config ```mod_dav_svn.conf```
from configured |RCE| instance.
.. code-block:: apache
<VirtualHost *:8080>
ServerAdmin rhodecode-admin@localhost
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
Include /home/user/.rccontrol/enterprise-1/mod_dav_svn.conf
</VirtualHost>
2. Go to the :menuselection:`Admin --> Settings --> Labs` page, and
enable :guilabel:`Proxy Subversion HTTP requests`, and specify the
:guilabel:`Subversion HTTP Server URL`.
3. Open the |RCE| configuration file,
:file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
4. Add the following configuration option in the ``[app:main]``
section if you don't have it yet.
This enable mapping of created |RCE| repo groups into special |svn| paths.
Each time a new repository group will be created the system will update
the template file, and create new mapping. Apache web server needs to be
reloaded to pick up the changes on this file.
It's recommended to add reload into a crontab so the changes can be picked
automatically once someone creates an repository group inside RhodeCode.
.. code-block:: ini
##############################################
### Subversion proxy support (mod_dav_svn) ###
##############################################
## Enable or disable the config file generation.
svn.proxy.generate_config = true
## Generate config file with `SVNListParentPath` set to `On`.
svn.proxy.list_parent_path = true
## Set location and file name of generated config file.
svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf
## File system path to the directory containing the repositories served by
## RhodeCode.
svn.proxy.parent_path_root = /path/to/repo_store
## Used as a prefix to the <Location> block in the generated config file. In
## most cases it should be set to `/`.
svn.proxy.location_root = /
This would create a special template file called ```mod_dav_svn.conf```. We
used that file path in the apache config above inside the Include statement.
Using |svn|
^^^^^^^^^^^
Once |svn| has been enabled on your instance, you can use it using the
following examples. For more |svn| information, see the `Subversion Red Book`_
.. code-block:: bash
# To clone a repository
svn checkout http://my-svn-server.example.com/my-svn-repo
# svn commit
svn commit
.. _Subversion Red Book: http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.ref.svn

View file

@ -26,6 +26,7 @@ For more information, see the following sections:
* :ref:`vcs-server-versions`
* :ref:`vcs-server-maintain`
* :ref:`vcs-server-config-file`
* :ref:`svn-http`
.. _install-vcs:
@ -297,5 +298,133 @@ For a more detailed explanation of the logger levers, see :ref:`debug-mode`.
format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %Y-%m-%d %H:%M:%S
.. _svn-http:
.. _Ask Ubuntu: http://askubuntu.com/questions/162391/how-do-i-fix-my-locale-issue
|svn| With Write Over HTTP
^^^^^^^^^^^^^^^^^^^^^^^^^^
To use |svn| with read/write support over the |svn| HTTP protocol, you have to
configure the HTTP |svn| backend.
Prerequisites
=============
- Enable HTTP support inside the admin VCS settings on your |RCE| instance
- You need to install the following tools on the machine that is running an
instance of |RCE|:
``Apache HTTP Server`` and
``mod_dav_svn``.
Using Ubuntu Distribution as an example you can run:
.. code-block:: bash
$ sudo apt-get install apache2 libapache2-mod-svn
Once installed you need to enable ``dav_svn``:
.. code-block:: bash
$ sudo a2enmod dav_svn
Configuring Apache Setup
========================
.. tip::
It is recommended to run Apache on a port other than 80, due to possible
conflicts with other HTTP servers like nginx. To do this, set the
``Listen`` parameter in the ``/etc/apache2/ports.conf`` file, for example
``Listen 8090``.
.. warning::
Make sure your Apache instance which runs the mod_dav_svn module is
only accessible by RhodeCode. Otherwise everyone is able to browse
the repositories or run subversion operations (checkout/commit/etc.).
It is also recommended to run apache as the same user as |RCE|, otherwise
permission issues could occur. To do this edit the ``/etc/apache2/envvars``
.. code-block:: apache
export APACHE_RUN_USER=rhodecode
export APACHE_RUN_GROUP=rhodecode
1. To configure Apache, create and edit a virtual hosts file, for example
:file:`/etc/apache2/sites-available/default.conf`. Below is an example
how to use one with auto-generated config ```mod_dav_svn.conf```
from configured |RCE| instance.
.. code-block:: apache
<VirtualHost *:8080>
ServerAdmin rhodecode-admin@localhost
DocumentRoot /var/www/html
ErrorLog ${'${APACHE_LOG_DIR}'}/error.log
CustomLog ${'${APACHE_LOG_DIR}'}/access.log combined
Include /home/user/.rccontrol/enterprise-1/mod_dav_svn.conf
</VirtualHost>
2. Go to the :menuselection:`Admin --> Settings --> VCS` page, and
enable :guilabel:`Proxy Subversion HTTP requests`, and specify the
:guilabel:`Subversion HTTP Server URL`.
3. Open the |RCE| configuration file,
:file:`/home/{user}/.rccontrol/{instance-id}/rhodecode.ini`
4. Add the following configuration option in the ``[app:main]``
section if you don't have it yet.
This enables mapping of the created |RCE| repo groups into special |svn| paths.
Each time a new repository group is created, the system will update
the template file and create new mapping. Apache web server needs to be
reloaded to pick up the changes on this file.
It's recommended to add reload into a crontab so the changes can be picked
automatically once someone creates a repository group inside RhodeCode.
.. code-block:: ini
##############################################
### Subversion proxy support (mod_dav_svn) ###
##############################################
## Enable or disable the config file generation.
svn.proxy.generate_config = true
## Generate config file with `SVNListParentPath` set to `On`.
svn.proxy.list_parent_path = true
## Set location and file name of generated config file.
svn.proxy.config_file_path = %(here)s/mod_dav_svn.conf
## File system path to the directory containing the repositories served by
## RhodeCode.
svn.proxy.parent_path_root = /path/to/repo_store
## Used as a prefix to the <Location> block in the generated config file. In
## most cases it should be set to `/`.
svn.proxy.location_root = /
This would create a special template file called ```mod_dav_svn.conf```. We
used that file path in the apache config above inside the Include statement.
Using |svn|
===========
Once |svn| has been enabled on your instance, you can use it with the
following examples. For more |svn| information, see the `Subversion Red Book`_
.. code-block:: bash
# To clone a repository
svn checkout http://my-svn-server.example.com/my-svn-repo
# svn commit
svn commit
.. _Subversion Red Book: http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.ref.svn
.. _Ask Ubuntu: http://askubuntu.com/questions/162391/how-do-i-fix-my-locale-issue

View file

@ -18,3 +18,4 @@ Welcome to the contribution guides and development docs of RhodeCode.
db-schema
dev-settings
api
dependencies

View file

@ -0,0 +1,60 @@
=======================
Dependency management
=======================
Overview
========
We use the Nix package manager to handle our dependencies. In general we use the
packages out of the package collection `nixpkgs`. For frequently changing
dependencies for Python and JavaScript we use the tools which are described in
this section to generate the needed Nix derivations.
Python dependencies
===================
We use the tool `pip2nix` to generate the Nix derivations for our Python
dependencies.
Generating the dependencies should be done with the following command:
.. code:: shell
pip2nix generate --license
.. note::
License extraction support is still experimental, use the version from the
following pull request: https://github.com/ktosiek/pip2nix/pull/30
Node dependencies
=================
After adding new dependencies via ``npm install --save``, use `node2nix` to
update the corresponding Nix derivations:
.. code:: shell
cd pkgs
node2nix --input ../package.json \
-o node-packages.nix \
-e node-env.nix \
-c node-default.nix \
-d --flatten
Bower dependencies
==================
Frontend dependencies are managed based on `bower`, with `bower2nix` a tool
exists which can generate the needed Nix derivations:
.. code:: shell
bower2nix bower.json pkgs/bower-packages.nix

View file

@ -111,15 +111,18 @@ time operation::
Compile CSS and JavaScript
^^^^^^^^^^^^^^^^^^^^^^^^^^
To use the application's frontend, you will need to compile the CSS and
JavaScript with Grunt. This is easily done from within the nix-shell using the
following command::
To use the application's frontend and prepare it for production deployment,
you will need to compile the CSS and JavaScript with Grunt.
This is easily done from within the nix-shell using the following command::
make web-build
grunt
You will need to recompile following any changes made to the CSS or JavaScript
files.
When developing new features you will need to recompile following any
changes made to the CSS or JavaScript files when developing the code::
grunt watch
This prepares the development (with comments/whitespace) versions of files.
Start the Development Server
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View file

@ -45,6 +45,11 @@ JavaScript
----------
This currently remains undefined. Suggestions welcome!
However, we have decided to go forward with W3C standards and picked
WebComponents as the foundation of user interface. New functionality should
be implemented as components using the
`Polymer Project` <https://www.polymer-project.org>`_ library
and should avoid external dependencies like `jquery`.
HTML
----

View file

@ -14,9 +14,6 @@ py.test based test suite
The test suite is in the folder :file:`rhodecode/tests/` and should be run with
the test runner `py.test` inside of your `nix-shell` environment::
# In case you need the cythonized version
CYTHONIZE=1 python setup.py develop --prefix=$tmp_path
py.test rhodecode
@ -26,20 +23,28 @@ py.test integration
The integration with the test runner is based on the following three parts:
- `pytest_pylons` is a py.test plugin which does the integration with the
Pylons web framework. It sets up the Pylons environment based on the given ini
file.
- :file:`rhodecode/tests/pylons_plugin.py` is a py.test plugin which does the
integration with the Pylons web framework. It sets up the Pylons environment
based on the given ini file.
Tests which depend on the Pylons environment to be set up must request the
fixture `pylonsapp`.
- :file:`rhodecode/tests/plugin.py` contains the integration of py.test with
RhodeCode Enterprise itself.
RhodeCode Enterprise itself and it takes care of setting up the needed parts
of the Pyramid framework.
- :file:`conftest.py` plugins are used to provide a special integration for
certain groups of tests based on the directory location.
.. note::
We are migrating from Pylons to its successor Pyramid. Eventually the role of
the file `pylons_plugin.py` will change to provide only a Pyramid
integration.
VCS backend selection
---------------------

View file

@ -19,8 +19,7 @@ Quick Start Installation Guide
To get |RCE| up and running, run through the below steps:
1. Download the latest |RCC| installer from your `rhodecode.com`_ profile
or main page.
1. Download the latest |RCC| installer from `rhodecode.com/download`_.
If you don't have an account, sign up at `rhodecode.com/register`_.
2. Run the |RCC| installer and accept the End User Licence using the
@ -107,3 +106,5 @@ To get |RCE| up and running, run through the below steps:
.. _rhodecode.com/download/: https://rhodecode.com/download/
.. _rhodecode.com: https://rhodecode.com/
.. _rhodecode.com/register: https://rhodecode.com/register/
.. _rhodecode.com/download: https://rhodecode.com/download/

View file

@ -3,20 +3,20 @@
PostgreSQL
----------
To use a PostgreSQL database you should install and configurevthe database
before installing |RCV|. This is becausevduring |RCV| installation you will
setup a connection to your PostgreSQL database. To work with PostgreSQL,
To use a PostgreSQL database, you should install and configure the database
before installing |RCV|. This is because during |RCV| installation you will
setup the connection to your PostgreSQL database. To work with PostgreSQL,
use the following steps:
1. Depending on your |os|, install avPostgreSQL database following the
1. Depending on your |os|, install a PostgreSQL database following the
appropriate instructions from the `PostgreSQL website`_.
2. Configure the database with a username and password which you will use
2. Configure the database with a username and password, which you will use
with |RCV|.
3. Install |RCV|, and during installation select PostgreSQL as your database.
4. Enter the following information to during the database setup:
4. Enter the following information during the database setup:
* Your network IP Address
* The port number for MySQL access. The default MySQL port is ``5434``
* The port number for PostgreSQL access; the default port is ``5434``
* Your database username
* Your database password
* A new database name

View file

@ -0,0 +1,78 @@
|RCE| 4.4.0 |RNS|
-----------------
Release Date
^^^^^^^^^^^^
- 2016-09-16
General
^^^^^^^
- UI: introduced Polymer webcomponents into core application. RhodeCode will
be now shipped together with Polymer framework webcomponents. Most of
dynamic UI components that require large amounts of interaction
will be done now with Polymer.
- live-notifications: use rhodecode-toast for live notifications instead of
toastr jquery plugin.
- Svn: moved svn http support out of labs settings. It's tested and stable now.
New Features
^^^^^^^^^^^^
- Integrations: integrations can now be configure on whole repo group to apply
same integrations on multiple projects/groups at once.
- Integrations: added scopes on integrations, scopes are: Global,
Repository Group (with/without children), Repositories, Root Repositories Only.
It will allow to configure exactly which projects use which integrations.
- Integrations: show branches/commits separately when posting push events
to hipchat/slack, fixes #4192.
- Pull-requests: summary page now shows update dates for pull request to
easier see which one were receantly updated.
- UI: hidden inline comments will be shown in side view when browsing the diffs
- Diffs: added inline comments toggle into pull requests diff view. #2884
- Live-chat: added summon reviewers functionality. You can now request
presence from online users into a chat for collaborative code-review.
This requires channelstream to be enabled.
- UX: added a static 502 page for gateway error. Once configured via
Nginx or Apache it will present a custom RhodeCode page while
backend servers are offline. Fixes #4202.
Security
^^^^^^^^
- Passwords: forced password change will not allow users to put in the
old password as new one.
Performance
^^^^^^^^^^^
- Vcs: refactor vcs-middleware to handle order of .ini file backends in
detection of vcs protocol. Detection ends now on first match and speeds
overall transaction speed.
- Summary: Improve the algorithm and performance of detection README files
inside summary page. In some cases we reduced cold-cache time from 50s to 1s.
- Safari: improved speed of large diffs on Safari browser.
- UX: remove position relative on diff td as it causes very slow
rendering in browsers.
Fixes
^^^^^
- UX: change confirm password widget to have spacing between the fields to
match rest of ui, fixes: #4200.
- UX: show multiple tags/branches in changelog/summary instead of
truncating them.
- My-account: fix test notifications for IE10+
- Vcs: change way refs are retrieved for git so same name branch/tags and
remotes can be supported, fixes #298.
- Lexers: added small extensions table to extend syntax highlighting for file
sources. Fixes #4227.
- Search: fix bug where file path link was wrong when the repository name was
in the file path, fixes #4228
- Fixed INT overflow bug
- Events: send pushed commits always in the correct in order.

View file

@ -9,6 +9,7 @@ Release Notes
.. toctree::
:maxdepth: 1
release-notes-4.4.0.rst
release-notes-4.3.1.rst
release-notes-4.3.0.rst
release-notes-4.2.1.rst

186
grunt_config.json Normal file
View file

@ -0,0 +1,186 @@
{
"dirs": {
"css": {
"src":"rhodecode/public/css",
"dest":"rhodecode/public/css"
},
"js": {
"src": "rhodecode/public/js/src",
"dest": "rhodecode/public/js"
}
},
"copy": {
"main": {
"expand": true,
"cwd": "bower_components",
"src": "webcomponentsjs/webcomponents-lite.js",
"dest": "<%= dirs.js.dest %>/vendors"
}
},
"concat": {
"polymercss": {
"src": [
"<%= dirs.js.src %>/components/root-styles-prefix.html",
"<%= dirs.css.src %>/style-polymer.css",
"<%= dirs.js.src %>/components/root-styles-suffix.html"
],
"dest": "<%= dirs.js.dest %>/src/components/root-styles.gen.html",
"nonull": true
},
"dist": {
"src": [
"<%= dirs.js.src %>/jquery-1.11.1.min.js",
"<%= dirs.js.src %>/logging.js",
"<%= dirs.js.src %>/bootstrap.js",
"<%= dirs.js.src %>/mousetrap.js",
"<%= dirs.js.src %>/moment.js",
"<%= dirs.js.src %>/appenlight-client-0.4.1.min.js",
"<%= dirs.js.src %>/i18n_utils.js",
"<%= dirs.js.src %>/deform.js",
"<%= dirs.js.src %>/plugins/jquery.pjax.js",
"<%= dirs.js.src %>/plugins/jquery.dataTables.js",
"<%= dirs.js.src %>/plugins/flavoured_checkbox.js",
"<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js",
"<%= dirs.js.src %>/plugins/jquery.autocomplete.js",
"<%= dirs.js.src %>/plugins/jquery.debounce.js",
"<%= dirs.js.src %>/plugins/jquery.mark.js",
"<%= dirs.js.src %>/plugins/jquery.timeago.js",
"<%= dirs.js.src %>/plugins/jquery.timeago-extension.js",
"<%= dirs.js.src %>/select2/select2.js",
"<%= dirs.js.src %>/codemirror/codemirror.js",
"<%= dirs.js.src %>/codemirror/codemirror_loadmode.js",
"<%= dirs.js.src %>/codemirror/codemirror_hint.js",
"<%= dirs.js.src %>/codemirror/codemirror_overlay.js",
"<%= dirs.js.src %>/codemirror/codemirror_placeholder.js",
"<%= dirs.js.dest %>/mode/meta.js",
"<%= dirs.js.dest %>/mode/meta_ext.js",
"<%= dirs.js.dest %>/rhodecode/i18n/select2/translations.js",
"<%= dirs.js.src %>/rhodecode/utils/array.js",
"<%= dirs.js.src %>/rhodecode/utils/string.js",
"<%= dirs.js.src %>/rhodecode/utils/pyroutes.js",
"<%= dirs.js.src %>/rhodecode/utils/ajax.js",
"<%= dirs.js.src %>/rhodecode/utils/autocomplete.js",
"<%= dirs.js.src %>/rhodecode/utils/colorgenerator.js",
"<%= dirs.js.src %>/rhodecode/utils/ie.js",
"<%= dirs.js.src %>/rhodecode/utils/os.js",
"<%= dirs.js.src %>/rhodecode/utils/topics.js",
"<%= dirs.js.src %>/rhodecode/widgets/multiselect.js",
"<%= dirs.js.src %>/rhodecode/init.js",
"<%= dirs.js.src %>/rhodecode/codemirror.js",
"<%= dirs.js.src %>/rhodecode/comments.js",
"<%= dirs.js.src %>/rhodecode/constants.js",
"<%= dirs.js.src %>/rhodecode/files.js",
"<%= dirs.js.src %>/rhodecode/followers.js",
"<%= dirs.js.src %>/rhodecode/menus.js",
"<%= dirs.js.src %>/rhodecode/notifications.js",
"<%= dirs.js.src %>/rhodecode/permissions.js",
"<%= dirs.js.src %>/rhodecode/pjax.js",
"<%= dirs.js.src %>/rhodecode/pullrequests.js",
"<%= dirs.js.src %>/rhodecode/settings.js",
"<%= dirs.js.src %>/rhodecode/select2_widgets.js",
"<%= dirs.js.src %>/rhodecode/tooltips.js",
"<%= dirs.js.src %>/rhodecode/users.js",
"<%= dirs.js.src %>/rhodecode/appenlight.js",
"<%= dirs.js.src %>/rhodecode.js"
],
"dest": "<%= dirs.js.dest %>/scripts.js",
"nonull": true
}
},
"crisper": {
"dist": {
"options": {
"cleanup": false,
"onlySplit": true
},
"src": "<%= dirs.js.dest %>/rhodecode-components.html",
"dest": "<%= dirs.js.dest %>/rhodecode-components.js"
}
},
"less": {
"development": {
"options": {
"compress": false,
"yuicompress": false,
"optimization": 0
},
"files": {
"<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less",
"<%= dirs.css.dest %>/style-polymer.css": "<%= dirs.css.src %>/polymer.less"
}
},
"production": {
"options": {
"compress": true,
"yuicompress": true,
"optimization": 2
},
"files": {
"<%= dirs.css.dest %>/style.css": "<%= dirs.css.src %>/main.less",
"<%= dirs.css.dest %>/style-polymer.css": "<%= dirs.css.src %>/polymer.less"
}
},
"components": {
"files": [
{
"cwd": "<%= dirs.js.src %>/components/",
"dest": "<%= dirs.js.src %>/components/",
"src": [
"**/*.less"
],
"expand": true,
"ext": ".css"
}
]
}
},
"watch": {
"less": {
"files": [
"<%= dirs.css.src %>/**/*.less",
"<%= dirs.js.src %>/components/**/*.less"
],
"tasks": [
"less:development",
"less:components",
"concat:polymercss",
"vulcanize"
]
},
"js": {
"files": [
"!<%= dirs.js.src %>/components/root-styles.gen.html",
"<%= dirs.js.src %>/**/*.js",
"<%= dirs.js.src %>/components/**/*.html"
],
"tasks": [
"less:components",
"concat:polymercss",
"vulcanize",
"crisper",
"concat:dist"
]
}
},
"jshint": {
"rhodecode": {
"src": "<%= dirs.js.src %>/rhodecode/**/*.js",
"options": {
"jshintrc": ".jshintrc"
}
}
},
"vulcanize": {
"default": {
"options": {
"abspath": "",
"inlineScripts": true,
"inlineCss": true,
"stripComments": true
},
"files": {
"<%= dirs.js.dest %>/rhodecode-components.html": "<%= dirs.js.src %>/components/shared-components.html"
}
}
}
}

View file

@ -3,10 +3,16 @@
"version": "0.0.1",
"devDependencies": {
"grunt": "^0.4.5",
"grunt-contrib-copy": "^1.0.0",
"grunt-contrib-concat": "^0.5.1",
"grunt-contrib-jshint": "^0.12.0",
"grunt-contrib-less": "^1.1.0",
"grunt-contrib-watch": "^0.6.1",
"jshint": "^2.9.1-rc3"
"crisper": "^2.0.2",
"vulcanize": "^1.14.8",
"grunt-crisper": "^1.0.1",
"grunt-vulcanize": "^1.0.0",
"jshint": "^2.9.1-rc3",
"bower": "^1.7.9"
}
}

View file

@ -0,0 +1,67 @@
# Backported buildBowerComponents so that we can also use it with the version
# 16.03 which is the current stable at the time of this writing.
#
# This file can be removed once building with 16.03 is not needed anymore.
{ pkgs }:
{ buildInputs ? [], generated, ... } @ attrs:
let
bower2nix-src = pkgs.fetchzip {
url = "https://github.com/rvl/bower2nix/archive/v3.0.1.tar.gz";
sha256 = "1zbvz96k2j6g0r4lvm5cgh41a73k9dgayk7x63cmg538dzznxvyb";
};
bower2nix = import "${bower2nix-src}/default.nix" { inherit pkgs; };
fetchbower = import ./backport-16.03-fetchbower.nix {
inherit (pkgs) stdenv lib;
inherit bower2nix;
};
# Fetches the bower packages. `generated` should be the result of a
# `bower2nix` command.
bowerPackages = import generated {
inherit (pkgs) buildEnv;
inherit fetchbower;
};
in pkgs.stdenv.mkDerivation (
attrs
//
{
name = "bower_components-" + attrs.name;
inherit bowerPackages;
builder = builtins.toFile "builder.sh" ''
source $stdenv/setup
# The project's bower.json is required
cp $src/bower.json .
# Dereference symlinks -- bower doesn't like them
cp --recursive --reflink=auto \
--dereference --no-preserve=mode \
$bowerPackages bc
# Bower install in offline mode -- links together the fetched
# bower packages.
HOME=$PWD bower \
--config.storage.packages=bc/packages \
--config.storage.registry=bc/registry \
--offline install
# Sets up a single bower_components directory within
# the output derivation.
mkdir -p $out
mv bower_components $out
'';
buildInputs = buildInputs ++ [
pkgs.git
pkgs.nodePackages.bower
];
}
)

View file

@ -0,0 +1,26 @@
{ stdenv, lib, bower2nix }:
let
bowerVersion = version:
let
components = lib.splitString "#" version;
hash = lib.last components;
ver = if builtins.length components == 1 then version else hash;
in ver;
fetchbower = name: version: target: outputHash: stdenv.mkDerivation {
name = "${name}-${bowerVersion version}";
buildCommand = ''
fetch-bower --quiet --out=$PWD/out "${name}" "${target}" "${version}"
# In some cases, the result of fetchBower is different depending
# on the output directory (e.g. if the bower package contains
# symlinks). So use a local output directory before copying to
# $out.
cp -R out $out
'';
outputHashMode = "recursive";
outputHashAlgo = "sha256";
inherit outputHash;
buildInputs = [ bower2nix ];
};
in fetchbower

31
pkgs/bower-packages.nix Normal file
View file

@ -0,0 +1,31 @@
{ fetchbower, buildEnv }:
buildEnv { name = "bower-env"; ignoreCollisions = true; paths = [
(fetchbower "webcomponentsjs" "0.7.22" "^0.7.22" "0ggh3k8ssafd056ib1m5bvzi7cpz3ry7gr5176d79na1w0c3i7dz")
(fetchbower "polymer" "Polymer/polymer#1.6.1" "Polymer/polymer#^1.6.1" "09mm0jgk457gvwqlc155swch7gjr6fs3g7spnvhi6vh5b6518540")
(fetchbower "paper-button" "PolymerElements/paper-button#1.0.13" "PolymerElements/paper-button#^1.0.13" "0i3y153nqk06pn0gk282vyybnl3g1w3w41d5i9z659cgn27g3fvm")
(fetchbower "paper-spinner" "PolymerElements/paper-spinner#1.2.0" "PolymerElements/paper-spinner#^1.2.0" "1av1m6y81jw3hjhz1yqy3rwcgxarjzl58ldfn4q6sn51pgzngfqb")
(fetchbower "paper-tooltip" "PolymerElements/paper-tooltip#1.1.2" "PolymerElements/paper-tooltip#^1.1.2" "1j64nprcyk2d2bbl3qwjyr0lbjngm4wclpyfwgai1c4y6g6bigd2")
(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-flex-layout" "PolymerElements/iron-flex-layout#1.3.1" "PolymerElements/iron-flex-layout#^1.0.0" "0nswv3ih3bhflgcd2wjfmddqswzgqxb2xbq65jk9w3rkj26hplbl")
(fetchbower "paper-behaviors" "PolymerElements/paper-behaviors#1.0.12" "PolymerElements/paper-behaviors#^1.0.0" "012bqk97awgz55cn7rm9g7cckrdhkqhls3zvp8l6nd4rdwcrdzq8")
(fetchbower "paper-material" "PolymerElements/paper-material#1.0.6" "PolymerElements/paper-material#^1.0.0" "0rljmknfdbm5aabvx9pk77754zckj3l127c3mvnmwkpkkr353xnh")
(fetchbower "paper-styles" "PolymerElements/paper-styles#1.1.4" "PolymerElements/paper-styles#^1.0.0" "0j8vg74xrcxlni8i93dsab3y80f34kk30lv4yblqpkp9c3nrilf7")
(fetchbower "neon-animation" "PolymerElements/neon-animation#1.2.4" "PolymerElements/neon-animation#^1.0.0" "16mz9i2n5w0k5j8d6gha23cnbdgm5syz3fawyh89gdbq97bi2q5j")
(fetchbower "iron-a11y-announcer" "PolymerElements/iron-a11y-announcer#1.0.5" "PolymerElements/iron-a11y-announcer#^1.0.0" "0n7c7j1pwk3835s7s2jd9125wdcsqf216yi5gj07wn5s8h8p7m9d")
(fetchbower "iron-overlay-behavior" "PolymerElements/iron-overlay-behavior#1.8.6" "PolymerElements/iron-overlay-behavior#^1.0.9" "14brn9gz6qqskarg3fxk91xs7vg02vgcsz9a9743kidxr0l0413m")
(fetchbower "iron-fit-behavior" "PolymerElements/iron-fit-behavior#1.2.5" "PolymerElements/iron-fit-behavior#^1.1.0" "1msnlh8lp1xg6v4h6dkjwj9kzac5q5q208ayla3x9hi483ki6rlf")
(fetchbower "iron-checked-element-behavior" "PolymerElements/iron-checked-element-behavior#1.0.5" "PolymerElements/iron-checked-element-behavior#^1.0.0" "0l0yy4ah454s8bzfv076s8by7h67zy9ni6xb932qwyhx8br6c1m7")
(fetchbower "promise-polyfill" "polymerlabs/promise-polyfill#1.0.1" "polymerlabs/promise-polyfill#^1.0.0" "045bj2caav3famr5hhxgs1dx7n08r4s46mlzwb313vdy17is38xb")
(fetchbower "iron-behaviors" "PolymerElements/iron-behaviors#1.0.17" "PolymerElements/iron-behaviors#^1.0.0" "021qvkmbk32jrrmmphpmwgby4bzi5jyf47rh1bxmq2ip07ly4bpr")
(fetchbower "paper-ripple" "PolymerElements/paper-ripple#1.0.8" "PolymerElements/paper-ripple#^1.0.0" "0r9sq8ik7wwrw0qb82c3rw0c030ljwd3s466c9y4qbcrsbvfjnns")
(fetchbower "font-roboto" "PolymerElements/font-roboto#1.0.1" "PolymerElements/font-roboto#^1.0.1" "02jz43r0wkyr3yp7rq2rc08l5cwnsgca9fr54sr4rhsnl7cjpxrj")
(fetchbower "iron-meta" "PolymerElements/iron-meta#1.1.2" "PolymerElements/iron-meta#^1.0.0" "1wl4dx8fnsknw9z9xi8bpc4cy9x70c11x4zxwxnj73hf3smifppl")
(fetchbower "iron-resizable-behavior" "PolymerElements/iron-resizable-behavior#1.0.5" "PolymerElements/iron-resizable-behavior#^1.0.0" "1fd5zmbr2hax42vmcasncvk7lzi38fmb1kyii26nn8pnnjak7zkn")
(fetchbower "iron-selector" "PolymerElements/iron-selector#1.5.2" "PolymerElements/iron-selector#^1.0.0" "1ajv46llqzvahm5g6g75w7nfyjcslp53ji0wm96l2k94j87spv3r")
(fetchbower "web-animations-js" "web-animations/web-animations-js#2.2.2" "web-animations/web-animations-js#^2.2.0" "1izfvm3l67vwys0bqbhidi9rqziw2f8wv289386sc6jsxzgkzhga")
(fetchbower "iron-a11y-keys-behavior" "PolymerElements/iron-a11y-keys-behavior#1.1.7" "PolymerElements/iron-a11y-keys-behavior#^1.0.0" "070z46dbbz242002gmqrgy28x0y1fcqp9hnvbi05r3zphiqfx3l7")
(fetchbower "iron-validatable-behavior" "PolymerElements/iron-validatable-behavior#1.1.1" "PolymerElements/iron-validatable-behavior#^1.0.0" "1yhxlvywhw2klbbgm3f3cmanxfxggagph4ii635zv0c13707wslv")
(fetchbower "iron-form-element-behavior" "PolymerElements/iron-form-element-behavior#1.0.6" "PolymerElements/iron-form-element-behavior#^1.0.0" "0rdhxivgkdhhz2yadgdbjfc70l555p3y83vjh8rfj5hr0asyn6q1")
]; }

15
pkgs/node-default.nix Normal file
View file

@ -0,0 +1,15 @@
# This file has been generated by node2nix 1.0.0. Do not edit!
{pkgs ? import <nixpkgs> {
inherit system;
}, system ? builtins.currentSystem}:
let
nodeEnv = import ./node-env.nix {
inherit (pkgs) stdenv python utillinux runCommand writeTextFile nodejs;
};
in
import ./node-packages.nix {
inherit (pkgs) fetchurl fetchgit;
inherit nodeEnv;
}

292
pkgs/node-env.nix Normal file
View file

@ -0,0 +1,292 @@
# This file originates from node2nix
{stdenv, python, nodejs, utillinux, runCommand, writeTextFile}:
let
# Create a tar wrapper that filters all the 'Ignoring unknown extended header keyword' noise
tarWrapper = runCommand "tarWrapper" {} ''
mkdir -p $out/bin
cat > $out/bin/tar <<EOF
#! ${stdenv.shell} -e
$(type -p tar) "\$@" --warning=no-unknown-keyword
EOF
chmod +x $out/bin/tar
'';
# Function that generates a TGZ file from a NPM project
buildNodeSourceDist =
{ name, version, src, ... }:
stdenv.mkDerivation {
name = "node-tarball-${name}-${version}";
inherit src;
buildInputs = [ nodejs ];
buildPhase = ''
export HOME=$TMPDIR
tgzFile=$(npm pack)
'';
installPhase = ''
mkdir -p $out/tarballs
mv $tgzFile $out/tarballs
mkdir -p $out/nix-support
echo "file source-dist $out/tarballs/$tgzFile" >> $out/nix-support/hydra-build-products
'';
};
includeDependencies = {dependencies}:
stdenv.lib.optionalString (dependencies != [])
(stdenv.lib.concatMapStrings (dependency:
''
# Bundle the dependencies of the package
mkdir -p node_modules
cd node_modules
# Only include dependencies if they don't exist. They may also be bundled in the package.
if [ ! -e "${dependency.name}" ]
then
${composePackage dependency}
fi
cd ..
''
) dependencies);
# Recursively composes the dependencies of a package
composePackage = { name, packageName, src, dependencies ? [], ... }@args:
let
fixImpureDependencies = writeTextFile {
name = "fixDependencies.js";
text = ''
var fs = require('fs');
var url = require('url');
/*
* Replaces an impure version specification by *
*/
function replaceImpureVersionSpec(versionSpec) {
var parsedUrl = url.parse(versionSpec);
if(versionSpec == "latest" || versionSpec == "unstable" ||
versionSpec.substr(0, 2) == ".." || dependency.substr(0, 2) == "./" || dependency.substr(0, 2) == "~/" || dependency.substr(0, 1) == '/')
return '*';
else if(parsedUrl.protocol == "git:" || parsedUrl.protocol == "git+ssh:" || parsedUrl.protocol == "git+http:" || parsedUrl.protocol == "git+https:" ||
parsedUrl.protocol == "http:" || parsedUrl.protocol == "https:")
return '*';
else
return versionSpec;
}
var packageObj = JSON.parse(fs.readFileSync('./package.json'));
/* Replace dependencies */
if(packageObj.dependencies !== undefined) {
for(var dependency in packageObj.dependencies) {
var versionSpec = packageObj.dependencies[dependency];
packageObj.dependencies[dependency] = replaceImpureVersionSpec(versionSpec);
}
}
/* Replace development dependencies */
if(packageObj.devDependencies !== undefined) {
for(var dependency in packageObj.devDependencies) {
var versionSpec = packageObj.devDependencies[dependency];
packageObj.devDependencies[dependency] = replaceImpureVersionSpec(versionSpec);
}
}
/* Replace optional dependencies */
if(packageObj.optionalDependencies !== undefined) {
for(var dependency in packageObj.optionalDependencies) {
var versionSpec = packageObj.optionalDependencies[dependency];
packageObj.optionalDependencies[dependency] = replaceImpureVersionSpec(versionSpec);
}
}
/* Write the fixed JSON file */
fs.writeFileSync("package.json", JSON.stringify(packageObj));
'';
};
in
''
DIR=$(pwd)
cd $TMPDIR
unpackFile ${src}
# Make the base dir in which the target dependency resides first
mkdir -p "$(dirname "$DIR/${packageName}")"
if [ -f "${src}" ]
then
# Figure out what directory has been unpacked
packageDir=$(find . -type d -maxdepth 1 | tail -1)
# Restore write permissions to make building work
chmod -R u+w "$packageDir"
# Move the extracted tarball into the output folder
mv "$packageDir" "$DIR/${packageName}"
elif [ -d "${src}" ]
then
# Restore write permissions to make building work
chmod -R u+w $strippedName
# Move the extracted directory into the output folder
mv $strippedName "$DIR/${packageName}"
fi
# Unset the stripped name to not confuse the next unpack step
unset strippedName
# Some version specifiers (latest, unstable, URLs, file paths) force NPM to make remote connections or consult paths outside the Nix store.
# The following JavaScript replaces these by * to prevent that
cd "$DIR/${packageName}"
node ${fixImpureDependencies}
# Include the dependencies of the package
${includeDependencies { inherit dependencies; }}
cd ..
${stdenv.lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
'';
# Extract the Node.js source code which is used to compile packages with
# native bindings
nodeSources = runCommand "node-sources" {} ''
tar --no-same-owner --no-same-permissions -xf ${nodejs.src}
mv node-* $out
'';
# Builds and composes an NPM package including all its dependencies
buildNodePackage = { name, packageName, version, dependencies ? [], production ? true, npmFlags ? "", dontNpmInstall ? false, preRebuild ? "", ... }@args:
stdenv.lib.makeOverridable stdenv.mkDerivation (builtins.removeAttrs args [ "dependencies" ] // {
name = "node-${name}-${version}";
buildInputs = [ tarWrapper python nodejs ] ++ stdenv.lib.optional (stdenv.isLinux) utillinux ++ args.buildInputs or [];
dontStrip = args.dontStrip or true; # Striping may fail a build for some package deployments
inherit dontNpmInstall preRebuild;
unpackPhase = args.unpackPhase or "true";
buildPhase = args.buildPhase or "true";
compositionScript = composePackage args;
passAsFile = [ "compositionScript" ];
installPhase = args.installPhase or ''
# Create and enter a root node_modules/ folder
mkdir -p $out/lib/node_modules
cd $out/lib/node_modules
# Compose the package and all its dependencies
source $compositionScriptPath
# Patch the shebangs of the bundled modules to prevent them from
# calling executables outside the Nix store as much as possible
patchShebangs .
# Deploy the Node.js package by running npm install. Since the
# dependencies have been provided already by ourselves, it should not
# attempt to install them again, which is good, because we want to make
# it Nix's responsibility. If it needs to install any dependencies
# anyway (e.g. because the dependency parameters are
# incomplete/incorrect), it fails.
#
# The other responsibilities of NPM are kept -- version checks, build
# steps, postprocessing etc.
export HOME=$TMPDIR
cd "${packageName}"
runHook preRebuild
npm --registry http://www.example.com --nodedir=${nodeSources} ${npmFlags} ${stdenv.lib.optionalString production "--production"} rebuild
if [ "$dontNpmInstall" != "1" ]
then
npm --registry http://www.example.com --nodedir=${nodeSources} ${npmFlags} ${stdenv.lib.optionalString production "--production"} install
fi
# Create symlink to the deployed executable folder, if applicable
if [ -d "$out/lib/node_modules/.bin" ]
then
ln -s $out/lib/node_modules/.bin $out/bin
fi
# Create symlinks to the deployed manual page folders, if applicable
if [ -d "$out/lib/node_modules/${packageName}/man" ]
then
mkdir -p $out/share
for dir in "$out/lib/node_modules/${packageName}/man/"*
do
mkdir -p $out/share/man/$(basename "$dir")
for page in "$dir"/*
do
ln -s $page $out/share/man/$(basename "$dir")
done
done
fi
'';
});
# Builds a development shell
buildNodeShell = { name, packageName, version, src, dependencies ? [], production ? true, npmFlags ? "", dontNpmInstall ? false, ... }@args:
let
nodeDependencies = stdenv.mkDerivation {
name = "node-dependencies-${name}-${version}";
buildInputs = [ tarWrapper python nodejs ] ++ stdenv.lib.optional (stdenv.isLinux) utillinux ++ args.buildInputs or [];
includeScript = includeDependencies { inherit dependencies; };
passAsFile = [ "includeScript" ];
buildCommand = ''
mkdir -p $out/lib
cd $out/lib
source $includeScriptPath
# Create fake package.json to make the npm commands work properly
cat > package.json <<EOF
{
"name": "${packageName}",
"version": "${version}"
}
EOF
# Patch the shebangs of the bundled modules to prevent them from
# calling executables outside the Nix store as much as possible
patchShebangs .
export HOME=$TMPDIR
npm --registry http://www.example.com --nodedir=${nodeSources} ${npmFlags} ${stdenv.lib.optionalString production "--production"} rebuild
${stdenv.lib.optionalString (!dontNpmInstall) ''
npm --registry http://www.example.com --nodedir=${nodeSources} ${npmFlags} ${stdenv.lib.optionalString production "--production"} install
''}
ln -s $out/lib/node_modules/.bin $out/bin
'';
};
in
stdenv.lib.makeOverridable stdenv.mkDerivation {
name = "node-shell-${name}-${version}";
buildInputs = [ python nodejs ] ++ stdenv.lib.optional (stdenv.isLinux) utillinux ++ args.buildInputs or [];
buildCommand = ''
mkdir -p $out/bin
cat > $out/bin/shell <<EOF
#! ${stdenv.shell} -e
$shellHook
exec ${stdenv.shell}
EOF
chmod +x $out/bin/shell
'';
# Provide the dependencies in a development shell through the NODE_PATH environment variable
inherit nodeDependencies;
shellHook = stdenv.lib.optionalString (dependencies != []) ''
export NODE_PATH=$nodeDependencies/lib/node_modules
'';
};
in
{ inherit buildNodeSourceDist buildNodePackage buildNodeShell; }

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,20 @@ let
url = http://www.repoze.org/LICENSE.txt;
};
};
# johbo: Interim bridge which allows us to build with the upcoming
# nixos.16.09 branch (unstable at the moment of writing this note) and the
# current stable nixos-16.03.
backwardsCompatibleFetchgit = { ... }@args:
let
origSources = pkgs.fetchgit args;
in
pkgs.lib.overrideDerivation origSources (oldAttrs: {
NIX_PREFETCH_GIT_CHECKOUT_HOOK = ''
find $out -name '.git*' -print0 | xargs -0 rm -rf
'';
});
in
self: super: {
@ -96,7 +110,7 @@ self: super: {
});
py-gfm = super.py-gfm.override {
src = pkgs.fetchgit {
src = backwardsCompatibleFetchgit {
url = "https://code.rhodecode.com/upstream/py-gfm";
rev = "0d66a19bc16e3d49de273c0f797d4e4781e8c0f2";
sha256 = "0ryp74jyihd3ckszq31bml5jr3bciimhfp7va7kw6ld92930ksv3";
@ -120,7 +134,7 @@ self: super: {
Pylons = super.Pylons.override (attrs: {
name = "Pylons-1.0.1-patch1";
src = pkgs.fetchgit {
src = backwardsCompatibleFetchgit {
url = "https://code.rhodecode.com/upstream/pylons";
rev = "707354ee4261b9c10450404fc9852ccea4fd667d";
sha256 = "b2763274c2780523a335f83a1df65be22ebe4ff413a7bc9e9288d23c1f62032e";

View file

@ -51,19 +51,6 @@
license = [ { fullName = "BSD-like (http://repoze.org/license.html)"; } ];
};
};
Fabric = super.buildPythonPackage {
name = "Fabric-1.10.0";
buildInputs = with self; [];
doCheck = false;
propagatedBuildInputs = with self; [paramiko];
src = fetchurl {
url = "https://pypi.python.org/packages/e3/5f/b6ebdb5241d5ec9eab582a5c8a01255c1107da396f849e538801d2fe64a5/Fabric-1.10.0.tar.gz";
md5 = "2cb96473387f0e7aa035210892352f4a";
};
meta = {
license = [ pkgs.lib.licenses.bsdOriginal ];
};
};
FormEncode = super.buildPythonPackage {
name = "FormEncode-1.2.4";
buildInputs = with self; [];
@ -1430,7 +1417,7 @@
};
};
rhodecode-enterprise-ce = super.buildPythonPackage {
name = "rhodecode-enterprise-ce-4.3.1";
name = "rhodecode-enterprise-ce-4.4.0";
buildInputs = with self; [WebTest configobj cssselect flake8 lxml mock pytest pytest-cov pytest-runner];
doCheck = true;
propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments Pylons Pyro4 Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu msgpack-python packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt];

View file

@ -1,7 +1,6 @@
Babel==1.3
Beaker==1.7.0
CProfileV==1.0.6
Fabric==1.10.0
FormEncode==1.2.4
Jinja2==2.7.3
Mako==1.0.1

View file

@ -1 +1 @@
4.3.1
4.4.0

View file

@ -51,7 +51,7 @@ PYRAMID_SETTINGS = {}
EXTENSIONS = {}
__version__ = ('.'.join((str(each) for each in VERSION[:3])))
__dbversion__ = 55 # defines current db version for migrations
__dbversion__ = 58 # defines current db version for migrations
__platform__ = platform.system()
__license__ = 'AGPLv3, and Commercial License'
__author__ = 'RhodeCode GmbH'
@ -60,3 +60,4 @@ __url__ = 'http://rhodecode.com'
is_windows = __platform__ in ['Windows']
is_unix = not is_windows
is_test = False
disable_error_handler = False

View file

@ -29,7 +29,9 @@ from rhodecode.lib.ext_json import json
def url_gen(request):
urls = {
'connect': request.route_url('channelstream_connect'),
'subscribe': request.route_url('channelstream_subscribe')
'subscribe': request.route_url('channelstream_subscribe'),
'longpoll': request.registry.settings.get('channelstream.longpoll_url', ''),
'ws': request.registry.settings.get('channelstream.ws_url', '')
}
return json.dumps(urls)

View file

@ -95,6 +95,7 @@ class ChannelstreamView(object):
'display_name': None,
'display_link': None,
}
user_data['permissions'] = c.rhodecode_user.permissions
payload = {
'username': user.username,
'user_state': user_data,

View file

@ -28,7 +28,11 @@ from rhodecode.lib.utils2 import __get_lem
# language map is also used by whoosh indexer, which for those specified
# extensions will index it's content
LANGUAGES_EXTENSIONS_MAP = __get_lem()
# custom extensions to lexers, format is 'ext': 'LexerClass'
extra = {
'vbs': 'VbNet'
}
LANGUAGES_EXTENSIONS_MAP = __get_lem(extra)
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"

View file

@ -158,6 +158,8 @@ def load_pyramid_environment(global_config, settings):
# This has to be done before the database connection is initialized.
if settings['is_test']:
rhodecode.is_test = True
rhodecode.disable_error_handler = True
utils.initialize_test_environment(settings_merged)
# Initialize the database connection.

View file

@ -44,9 +44,10 @@ 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.exceptions import VCSServerUnavailable
from rhodecode.lib.vcs.exceptions import VCSCommunicationError
from rhodecode.lib.middleware import csrf
from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
from rhodecode.lib.middleware.https_fixup import HttpsFixup
from rhodecode.lib.middleware.vcs import VCSMiddleware
from rhodecode.lib.plugins.utils import register_rhodecode_plugin
@ -193,10 +194,6 @@ def make_not_found_view(config):
pylons_app_as_view = wsgiapp(pylons_app)
# Protect from VCS Server error related pages when server is not available
if not vcs_server_enabled:
pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
def pylons_app_with_error_handler(context, request):
"""
Handle exceptions from rc pylons app:
@ -221,10 +218,18 @@ def make_not_found_view(config):
return error_handler(response, request)
except HTTPError as e: # pyramid type exceptions
return error_handler(e, request)
except Exception:
if settings.get('debugtoolbar.enabled', False):
except Exception as e:
log.exception(e)
if (settings.get('debugtoolbar.enabled', False) or
rhodecode.disable_error_handler):
raise
if isinstance(e, VCSCommunicationError):
return error_handler(VCSServerUnavailable(), request)
return error_handler(HTTPInternalServerError(), request)
return response
return pylons_app_with_error_handler
@ -249,7 +254,6 @@ def webob_to_pyramid_http_response(webob_response):
def error_handler(exception, request):
# TODO: dan: replace the old pylons error controller with this
from rhodecode.model.settings import SettingsModel
from rhodecode.lib.utils2 import AttributeDict
@ -278,6 +282,10 @@ def error_handler(exception, request):
if not c.rhodecode_name:
c.rhodecode_name = 'Rhodecode'
c.causes = []
if hasattr(base_response, 'causes'):
c.causes = base_response.causes
response = render_to_response(
'/errors/error_document.html', {'c': c}, request=request,
response=base_response)

View file

@ -42,6 +42,7 @@ STATIC_FILE_PREFIX = '/_static'
URL_NAME_REQUIREMENTS = {
# group name can have a slash in them, but they must not end with a slash
'group_name': r'.*?[^/]',
'repo_group_name': r'.*?[^/]',
# repo names can have a slash in them, but they must not end with a slash
'repo_name': r'.*?[^/]',
# file path eats up everything at the end
@ -531,9 +532,7 @@ def make_map(config):
action='my_account_update', conditions={'method': ['POST']})
m.connect('my_account_password', '/my_account/password',
action='my_account_password', conditions={'method': ['GET']})
m.connect('my_account_password', '/my_account/password',
action='my_account_password_update', conditions={'method': ['POST']})
action='my_account_password', conditions={'method': ['GET', 'POST']})
m.connect('my_account_repos', '/my_account/repos',
action='my_account_repos', conditions={'method': ['GET']})

View file

@ -32,17 +32,21 @@ from pylons.controllers.util import redirect
from pylons.i18n.translation import _
from sqlalchemy.orm import joinedload
from rhodecode import forms
from rhodecode.lib import helpers as h
from rhodecode.lib import auth
from rhodecode.lib.auth import (
LoginRequired, NotAnonymous, AuthUser, generate_auth_token)
from rhodecode.lib.base import BaseController, render
from rhodecode.lib.utils import jsonify
from rhodecode.lib.utils2 import safe_int, md5
from rhodecode.lib.ext_json import json
from rhodecode.model.validation_schema.schemas import user_schema
from rhodecode.model.db import (
Repository, PullRequest, PullRequestReviewers, UserEmailMap, User,
UserFollowing)
from rhodecode.model.forms import UserForm, PasswordChangeForm
from rhodecode.model.forms import UserForm
from rhodecode.model.scm import RepoList
from rhodecode.model.user import UserModel
from rhodecode.model.repo import RepoModel
@ -185,38 +189,44 @@ class MyAccountController(BaseController):
force_defaults=False
)
@auth.CSRFRequired()
def my_account_password_update(self):
c.active = 'password'
self.__load_data()
_form = PasswordChangeForm(c.rhodecode_user.username)()
try:
form_result = _form.to_python(request.POST)
UserModel().update_user(c.rhodecode_user.user_id, **form_result)
instance = c.rhodecode_user.get_instance()
instance.update_userdata(force_password_change=False)
Session().commit()
session.setdefault('rhodecode_user', {}).update(
{'password': md5(instance.password)})
session.save()
h.flash(_("Successfully updated password"), category='success')
except formencode.Invalid as errors:
return htmlfill.render(
render('admin/my_account/my_account.html'),
defaults=errors.value,
errors=errors.error_dict or {},
prefix_error=False,
encoding="UTF-8",
force_defaults=False)
except Exception:
log.exception("Exception updating password")
h.flash(_('Error occurred during update of user password'),
category='error')
return render('admin/my_account/my_account.html')
@auth.CSRFRequired(except_methods=['GET'])
def my_account_password(self):
c.active = 'password'
self.__load_data()
schema = user_schema.ChangePasswordSchema().bind(
username=c.rhodecode_user.username)
form = forms.Form(schema,
buttons=(forms.buttons.save, forms.buttons.reset))
if request.method == 'POST':
controls = request.POST.items()
try:
valid_data = form.validate(controls)
UserModel().update_user(c.rhodecode_user.user_id, **valid_data)
instance = c.rhodecode_user.get_instance()
instance.update_userdata(force_password_change=False)
Session().commit()
except forms.ValidationFailure as e:
request.session.flash(
_('Error occurred during update of user password'),
queue='error')
form = e
except Exception:
log.exception("Exception updating password")
request.session.flash(
_('Error occurred during update of user password'),
queue='error')
else:
session.setdefault('rhodecode_user', {}).update(
{'password': md5(instance.password)})
session.save()
request.session.flash(
_("Successfully updated password"), queue='success')
return redirect(url('my_account_password'))
c.form = form
return render('admin/my_account/my_account.html')
def my_account_repos(self):
@ -352,11 +362,10 @@ class MyAccountController(BaseController):
return render('admin/my_account/my_account.html')
@auth.CSRFRequired()
@jsonify
def my_notifications_toggle_visibility(self):
user = c.rhodecode_user.get_instance()
user_data = user.user_data
status = user_data.get('notification_status', False)
user_data['notification_status'] = not status
user.user_data = user_data
new_status = not user.user_data.get('notification_status', True)
user.update_userdata(notification_status=new_status)
Session().commit()
return redirect(url('my_account_notifications'))
return user.user_data['notification_status']

View file

@ -135,6 +135,7 @@ class SettingsController(BaseController):
c.svn_tag_patterns = model.get_global_svn_tag_patterns()
application_form = ApplicationUiSettingsForm()()
try:
form_result = application_form.to_python(dict(request.POST))
except formencode.Invalid as errors:
@ -151,12 +152,14 @@ class SettingsController(BaseController):
)
try:
model.update_global_ssl_setting(form_result['web_push_ssl'])
if c.visual.allow_repo_location_change:
model.update_global_path_setting(
form_result['paths_root_path'])
model.update_global_ssl_setting(form_result['web_push_ssl'])
model.update_global_hook_settings(form_result)
model.create_global_svn_settings(form_result)
model.create_or_update_global_svn_settings(form_result)
model.create_or_update_global_hg_settings(form_result)
model.create_or_update_global_pr_settings(form_result)
except Exception:
@ -789,18 +792,5 @@ LabSetting = collections.namedtuple(
# This list has to be kept in sync with the form
# rhodecode.model.forms.LabsSettingsForm.
_LAB_SETTINGS = [
LabSetting(
key='rhodecode_proxy_subversion_http_requests',
type='bool',
group=lazy_ugettext('Subversion HTTP Support'),
label=lazy_ugettext('Proxy subversion HTTP requests'),
help='' # Do not translate the empty string!
),
LabSetting(
key='rhodecode_subversion_http_server_url',
type='str',
group=lazy_ugettext('Subversion HTTP Server URL'),
label='', # Do not translate the empty string!
help=lazy_ugettext('e.g. http://localhost:8080/')
),
]

View file

@ -44,6 +44,8 @@ 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__)
@ -61,37 +63,16 @@ class SummaryController(BaseRepoController):
@cache_region('long_term')
def _generate_readme(cache_key):
readme_data = None
readme_file = None
try:
# gets the landing revision or tip if fails
commit = db_repo.get_landing_commit()
if isinstance(commit, EmptyCommit):
raise EmptyRepositoryError()
renderer = MarkupRenderer()
for f in renderer.pick_readme_order(default_renderer):
try:
node = commit.get_node(f)
except NodeDoesNotExistError:
continue
if not node.is_file():
continue
readme_file = f
log.debug('Found README file `%s` rendering...',
readme_file)
readme_data = renderer.render(node.content,
filename=f)
break
except CommitError:
log.exception("Problem getting commit")
pass
except EmptyRepositoryError:
pass
except Exception:
log.exception("General failure")
return readme_data, readme_file
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:
readme_data = self._render_readme_or_none(commit, readme_node)
readme_filename = readme_node.path
return readme_data, readme_filename
invalidator_context = CacheKey.repo_context_cache(
_generate_readme, repo_name, CacheKey.CACHE_TYPE_README)
@ -102,11 +83,36 @@ class SummaryController(BaseRepoController):
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):
log.debug(
'Found README file `%s` rendering...', readme_node.path)
renderer = MarkupRenderer()
try:
return renderer.render(
readme_node.content, filename=readme_node.path)
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)
@ -124,6 +130,8 @@ class SummaryController(BaseRepoController):
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)\

View file

@ -47,7 +47,7 @@ def _commits_as_dict(commit_ids, repos):
if not commit_ids:
return []
needed_commits = set(commit_ids)
needed_commits = list(commit_ids)
commits = []
reviewers = []
@ -57,6 +57,7 @@ def _commits_as_dict(commit_ids, repos):
vcs_repo = repo.scm_instance(cache=False)
try:
# use copy of needed_commits since we modify it while iterating
for commit_id in list(needed_commits):
try:
cs = vcs_repo.get_changeset(commit_id)
@ -78,7 +79,7 @@ def _commits_as_dict(commit_ids, repos):
repo.repo_name)
commits.append(cs_data)
needed_commits.discard(commit_id)
needed_commits.remove(commit_id)
except Exception as e:
log.exception(e)

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2010-2016 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
"""
Base module for form rendering / validation - currently just a wrapper for
deform - later can be replaced with something custom.
"""
from rhodecode.translation import _
from deform import Button, Form, widget, ValidationFailure
class buttons:
save = Button(name='Save', type='submit')
reset = Button(name=_('Reset'), type='reset')
delete = Button(name=_('Delete'), type='submit')

View file

@ -1,21 +1,22 @@
# English translations for rhodecode.
# Copyright (C) 2015 RhodeCode GmbH
# This file is distributed under the same license as the rhodecode project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2010.
# Translations template for rhodecode-enterprise-ce.
# Copyright (C) 2016 RhodeCode GmbH
# This file is distributed under the same license as the rhodecode-enterprise-ce project.
#
# Translators:
msgid ""
msgstr ""
"Project-Id-Version: rhodecode 0.1\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"Project-Id-Version: RhodeCode\n"
"Report-Msgid-Bugs-To: marcin@rhodecode.com\n"
"POT-Creation-Date: 2013-06-01 18:38+0200\n"
"PO-Revision-Date: 2011-02-25 19:13+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"Last-Translator: Marcin Kuzminski <marcin@rhodecode.com>\n"
"Language-Team: en <admin@rhodecode.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 0.9.6\n"
"Generated-By: Babel 1.3\n"
"Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
#: rhodecode/controllers/changelog.py:149
msgid "All Branches"

View file

@ -20,7 +20,7 @@
import logging
from rhodecode.model.db import Repository, Integration
from rhodecode.model.db import Repository, Integration, RepoGroup
from rhodecode.config.routing import (
ADMIN_PREFIX, add_route_requirements, URL_NAME_REQUIREMENTS)
from rhodecode.integrations import integration_type_registry
@ -29,6 +29,17 @@ log = logging.getLogger(__name__)
def includeme(config):
# global integrations
config.add_route('global_integrations_new',
ADMIN_PREFIX + '/integrations/new')
config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
attr='new_integration',
renderer='rhodecode:templates/admin/integrations/new.html',
request_method='GET',
route_name='global_integrations_new')
config.add_route('global_integrations_home',
ADMIN_PREFIX + '/integrations')
config.add_route('global_integrations_list',
@ -46,18 +57,80 @@ def includeme(config):
config.add_route('global_integrations_edit',
ADMIN_PREFIX + '/integrations/{integration}/{integration_id}',
custom_predicates=(valid_integration,))
for route_name in ['global_integrations_create', 'global_integrations_edit']:
config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
attr='settings_get',
renderer='rhodecode:templates/admin/integrations/edit.html',
renderer='rhodecode:templates/admin/integrations/form.html',
request_method='GET',
route_name=route_name)
config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
attr='settings_post',
renderer='rhodecode:templates/admin/integrations/edit.html',
renderer='rhodecode:templates/admin/integrations/form.html',
request_method='POST',
route_name=route_name)
# repo group integrations
config.add_route('repo_group_integrations_home',
add_route_requirements(
'{repo_group_name}/settings/integrations',
URL_NAME_REQUIREMENTS
),
custom_predicates=(valid_repo_group,)
)
config.add_route('repo_group_integrations_list',
add_route_requirements(
'{repo_group_name}/settings/integrations/{integration}',
URL_NAME_REQUIREMENTS
),
custom_predicates=(valid_repo_group, valid_integration))
for route_name in ['repo_group_integrations_home', 'repo_group_integrations_list']:
config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
attr='index',
renderer='rhodecode:templates/admin/integrations/list.html',
request_method='GET',
route_name=route_name)
config.add_route('repo_group_integrations_new',
add_route_requirements(
'{repo_group_name}/settings/integrations/new',
URL_NAME_REQUIREMENTS
),
custom_predicates=(valid_repo_group,))
config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
attr='new_integration',
renderer='rhodecode:templates/admin/integrations/new.html',
request_method='GET',
route_name='repo_group_integrations_new')
config.add_route('repo_group_integrations_create',
add_route_requirements(
'{repo_group_name}/settings/integrations/{integration}/new',
URL_NAME_REQUIREMENTS
),
custom_predicates=(valid_repo_group, valid_integration))
config.add_route('repo_group_integrations_edit',
add_route_requirements(
'{repo_group_name}/settings/integrations/{integration}/{integration_id}',
URL_NAME_REQUIREMENTS
),
custom_predicates=(valid_repo_group, valid_integration))
for route_name in ['repo_group_integrations_edit', 'repo_group_integrations_create']:
config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
attr='settings_get',
renderer='rhodecode:templates/admin/integrations/form.html',
request_method='GET',
route_name=route_name)
config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
attr='settings_post',
renderer='rhodecode:templates/admin/integrations/form.html',
request_method='POST',
route_name=route_name)
# repo integrations
config.add_route('repo_integrations_home',
add_route_requirements(
'{repo_name}/settings/integrations',
@ -74,8 +147,21 @@ def includeme(config):
config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
attr='index',
request_method='GET',
renderer='rhodecode:templates/admin/integrations/list.html',
route_name=route_name)
config.add_route('repo_integrations_new',
add_route_requirements(
'{repo_name}/settings/integrations/new',
URL_NAME_REQUIREMENTS
),
custom_predicates=(valid_repo,))
config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
attr='new_integration',
renderer='rhodecode:templates/admin/integrations/new.html',
request_method='GET',
route_name='repo_integrations_new')
config.add_route('repo_integrations_create',
add_route_requirements(
'{repo_name}/settings/integrations/{integration}/new',
@ -91,12 +177,12 @@ def includeme(config):
for route_name in ['repo_integrations_edit', 'repo_integrations_create']:
config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
attr='settings_get',
renderer='rhodecode:templates/admin/integrations/edit.html',
renderer='rhodecode:templates/admin/integrations/form.html',
request_method='GET',
route_name=route_name)
config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
attr='settings_post',
renderer='rhodecode:templates/admin/integrations/edit.html',
renderer='rhodecode:templates/admin/integrations/form.html',
request_method='POST',
route_name=route_name)
@ -107,20 +193,37 @@ def valid_repo(info, request):
return True
def valid_repo_group(info, request):
repo_group = RepoGroup.get_by_group_name(info['match']['repo_group_name'])
if repo_group:
return True
return False
def valid_integration(info, request):
integration_type = info['match']['integration']
integration_id = info['match'].get('integration_id')
repo_name = info['match'].get('repo_name')
repo_group_name = info['match'].get('repo_group_name')
if integration_type not in integration_type_registry:
return False
repo = None
repo, repo_group = None, None
if repo_name:
repo = Repository.get_by_repo_name(info['match']['repo_name'])
repo = Repository.get_by_repo_name(repo_name)
if not repo:
return False
if repo_group_name:
repo_group = RepoGroup.get_by_group_name(repo_group_name)
if not repo_group:
return False
if repo_name and repo_group:
raise Exception('Either repo or repo_group can be set, not both')
if integration_id:
integration = Integration.get(integration_id)
if not integration:
@ -129,5 +232,7 @@ def valid_integration(info, request):
return False
if repo and repo.repo_id != integration.repo_id:
return False
if repo_group and repo_group.group_id != integration.repo_group_id:
return False
return True

View file

@ -20,26 +20,52 @@
import colander
from rhodecode.translation import lazy_ugettext
from rhodecode.translation import _
class IntegrationSettingsSchemaBase(colander.MappingSchema):
"""
This base schema is intended for use in integrations.
It adds a few default settings (e.g., "enabled"), so that integration
authors don't have to maintain a bunch of boilerplate.
"""
class IntegrationOptionsSchemaBase(colander.MappingSchema):
enabled = colander.SchemaNode(
colander.Bool(),
default=True,
description=lazy_ugettext('Enable or disable this integration.'),
description=_('Enable or disable this integration.'),
missing=False,
title=lazy_ugettext('Enabled'),
title=_('Enabled'),
)
name = colander.SchemaNode(
colander.String(),
description=lazy_ugettext('Short name for this integration.'),
description=_('Short name for this integration.'),
missing=colander.required,
title=lazy_ugettext('Integration name'),
title=_('Integration name'),
)
class RepoIntegrationOptionsSchema(IntegrationOptionsSchemaBase):
pass
class RepoGroupIntegrationOptionsSchema(IntegrationOptionsSchemaBase):
child_repos_only = colander.SchemaNode(
colander.Bool(),
default=True,
description=_(
'Limit integrations to to work only on the direct children '
'repositories of this repository group (no subgroups)'),
missing=False,
title=_('Limit to childen repos only'),
)
class GlobalIntegrationOptionsSchema(IntegrationOptionsSchemaBase):
child_repos_only = colander.SchemaNode(
colander.Bool(),
default=False,
description=_(
'Limit integrations to to work only on root level repositories'),
missing=False,
title=_('Root repositories only'),
)
class IntegrationSettingsSchemaBase(colander.MappingSchema):
pass

View file

@ -18,25 +18,84 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
import colander
from rhodecode.translation import _
class IntegrationTypeBase(object):
""" Base class for IntegrationType plugins """
description = ''
icon = '''
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 -256 1792 1792"
id="svg3025"
version="1.1"
inkscape:version="0.48.3.1 r9886"
width="100%"
height="100%"
sodipodi:docname="cog_font_awesome.svg">
<metadata
id="metadata3035">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3033" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview3031"
showgrid="false"
inkscape:zoom="0.13169643"
inkscape:cx="896"
inkscape:cy="896"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg3025" />
<g
transform="matrix(1,0,0,-1,121.49153,1285.4237)"
id="g3027">
<path
d="m 1024,640 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 527 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 l -138,108 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-111 621,-98 L 593,86 q -49,16 -90,37 L 362,16 Q 352,7 337,7 323,7 312,18 186,132 147,186 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,495 Q 16,497 8,507.5 0,518 0,531 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1391 915,1378 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"
id="path3029"
inkscape:connector-curvature="0"
style="fill:currentColor" />
</g>
</svg>
'''
def __init__(self, settings):
"""
:param settings: dict of settings to be used for the integration
"""
self.settings = settings
def settings_schema(self):
"""
A colander schema of settings for the integration type
Subclasses can return their own schema but should always
inherit from IntegrationSettingsSchemaBase
"""
return IntegrationSettingsSchemaBase()
return colander.Schema()

View file

@ -26,11 +26,10 @@ import colander
from mako.template import Template
from rhodecode import events
from rhodecode.translation import _, lazy_ugettext
from rhodecode.translation import _
from rhodecode.lib.celerylib import run_task
from rhodecode.lib.celerylib import tasks
from rhodecode.integrations.types.base import IntegrationTypeBase
from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
log = logging.getLogger(__name__)
@ -147,18 +146,79 @@ repo_push_template_html = Template('''
</html>
''')
email_icon = '''
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 -256 1850 1850"
id="svg2989"
version="1.1"
inkscape:version="0.48.3.1 r9886"
width="100%"
height="100%"
sodipodi:docname="envelope_font_awesome.svg">
<metadata
id="metadata2999">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs2997" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview2995"
showgrid="false"
inkscape:zoom="0.13169643"
inkscape:cx="896"
inkscape:cy="896"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg2989" />
<g
transform="matrix(1,0,0,-1,37.966102,1282.678)"
id="g2991">
<path
d="m 1664,32 v 768 q -32,-36 -69,-66 -268,-206 -426,-338 -51,-43 -83,-67 -32,-24 -86.5,-48.5 Q 945,256 897,256 h -1 -1 Q 847,256 792.5,280.5 738,305 706,329 674,353 623,396 465,528 197,734 160,764 128,800 V 32 Q 128,19 137.5,9.5 147,0 160,0 h 1472 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,1051 v 11 13.5 q 0,0 -0.5,13 -0.5,13 -3,12.5 -2.5,-0.5 -5.5,9 -3,9.5 -9,7.5 -6,-2 -14,2.5 H 160 q -13,0 -22.5,-9.5 Q 128,1133 128,1120 128,952 275,836 468,684 676,519 682,514 711,489.5 740,465 757,452 774,439 801.5,420.5 829,402 852,393 q 23,-9 43,-9 h 1 1 q 20,0 43,9 23,9 50.5,27.5 27.5,18.5 44.5,31.5 17,13 46,37.5 29,24.5 35,29.5 208,165 401,317 54,43 100.5,115.5 46.5,72.5 46.5,131.5 z m 128,37 V 32 q 0,-66 -47,-113 -47,-47 -113,-47 H 160 Q 94,-128 47,-81 0,-34 0,32 v 1088 q 0,66 47,113 47,47 113,47 h 1472 q 66,0 113,-47 47,-47 47,-113 z"
id="path2993"
inkscape:connector-curvature="0"
style="fill:currentColor" />
</g>
</svg>
'''
class EmailSettingsSchema(IntegrationSettingsSchemaBase):
class EmailSettingsSchema(colander.Schema):
@colander.instantiate(validator=colander.Length(min=1))
class recipients(colander.SequenceSchema):
title = lazy_ugettext('Recipients')
description = lazy_ugettext('Email addresses to send push events to')
title = _('Recipients')
description = _('Email addresses to send push events to')
widget = deform.widget.SequenceWidget(min_len=1)
recipient = colander.SchemaNode(
colander.String(),
title=lazy_ugettext('Email address'),
description=lazy_ugettext('Email address'),
title=_('Email address'),
description=_('Email address'),
default='',
validator=colander.Email(),
widget=deform.widget.TextInputWidget(
@ -169,8 +229,9 @@ class EmailSettingsSchema(IntegrationSettingsSchemaBase):
class EmailIntegrationType(IntegrationTypeBase):
key = 'email'
display_name = lazy_ugettext('Email')
SettingsSchema = EmailSettingsSchema
display_name = _('Email')
description = _('Send repo push summaries to a list of recipients via email')
icon = email_icon
def settings_schema(self):
schema = EmailSettingsSchema()

View file

@ -29,29 +29,28 @@ from celery.task import task
from mako.template import Template
from rhodecode import events
from rhodecode.translation import lazy_ugettext
from rhodecode.translation import _
from rhodecode.lib import helpers as h
from rhodecode.lib.celerylib import run_task
from rhodecode.lib.colander_utils import strip_whitespace
from rhodecode.integrations.types.base import IntegrationTypeBase
from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
log = logging.getLogger(__name__)
class HipchatSettingsSchema(IntegrationSettingsSchemaBase):
class HipchatSettingsSchema(colander.Schema):
color_choices = [
('yellow', lazy_ugettext('Yellow')),
('red', lazy_ugettext('Red')),
('green', lazy_ugettext('Green')),
('purple', lazy_ugettext('Purple')),
('gray', lazy_ugettext('Gray')),
('yellow', _('Yellow')),
('red', _('Red')),
('green', _('Green')),
('purple', _('Purple')),
('gray', _('Gray')),
]
server_url = colander.SchemaNode(
colander.String(),
title=lazy_ugettext('Hipchat server URL'),
description=lazy_ugettext('Hipchat integration url.'),
title=_('Hipchat server URL'),
description=_('Hipchat integration url.'),
default='',
preparer=strip_whitespace,
validator=colander.url,
@ -61,15 +60,15 @@ class HipchatSettingsSchema(IntegrationSettingsSchemaBase):
)
notify = colander.SchemaNode(
colander.Bool(),
title=lazy_ugettext('Notify'),
description=lazy_ugettext('Make a notification to the users in room.'),
title=_('Notify'),
description=_('Make a notification to the users in room.'),
missing=False,
default=False,
)
color = colander.SchemaNode(
colander.String(),
title=lazy_ugettext('Color'),
description=lazy_ugettext('Background color of message.'),
title=_('Color'),
description=_('Background color of message.'),
missing='',
validator=colander.OneOf([x[0] for x in color_choices]),
widget=deform.widget.Select2Widget(
@ -79,29 +78,28 @@ class HipchatSettingsSchema(IntegrationSettingsSchemaBase):
repo_push_template = Template('''
<b>${data['actor']['username']}</b> pushed to
%if data['push']['branches']:
${len(data['push']['branches']) > 1 and 'branches' or 'branch'}
${', '.join('<a href="%s">%s</a>' % (branch['url'], branch['name']) for branch in data['push']['branches'])}
%else:
unknown branch
%endif
in <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>
<b>${data['actor']['username']}</b> pushed to repo <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>:
<br>
<ul>
%for commit in data['push']['commits']:
%for branch, branch_commits in branches_commits.items():
<li>
<a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}
<a href="${branch_commits['branch']['url']}">branch: ${branch_commits['branch']['name']}</a>
<ul>
%for commit in branch_commits['commits']:
<li><a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}</li>
%endfor
</ul>
</li>
%endfor
</ul>
''')
class HipchatIntegrationType(IntegrationTypeBase):
key = 'hipchat'
display_name = lazy_ugettext('Hipchat')
display_name = _('Hipchat')
description = _('Send events such as repo pushes and pull requests to '
'your hipchat channel.')
icon = '''<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve"><g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path fill="#205281" d="M4197.1,4662.4c-1661.5-260.4-3018-1171.6-3682.6-2473.3C219.9,1613.6,100,1120.3,100,462.6c0-1014,376.8-1918.4,1127-2699.4C2326.7-3377.6,3878.5-3898.3,5701-3730.5l486.5,44.5l208.9-123.3c637.2-373.4,1551.8-640.6,2240.4-650.9c304.9-6.9,335.7,0,417.9,75.4c185,174.7,147.3,411.1-89.1,548.1c-315.2,181.6-620,544.7-733.1,870.1l-51.4,157.6l472.7,472.7c349.4,349.4,520.7,551.5,657.7,774.2c784.5,1281.2,784.5,2788.5,0,4052.6c-236.4,376.8-794.8,966-1178.4,1236.7c-572.1,407.7-1264.1,709.1-1993.7,870.1c-267.2,58.2-479.6,75.4-1038,82.2C4714.4,4686.4,4310.2,4679.6,4197.1,4662.4z M5947.6,3740.9c1856.7-380.3,3127.6-1709.4,3127.6-3275c0-1000.3-534.4-1949.2-1466.2-2600.1c-188.4-133.6-287.8-226.1-301.5-284.4c-41.1-157.6,263.8-938.6,397.4-1020.8c20.5-10.3,34.3-44.5,34.3-75.4c0-167.8-811.9,195.3-1363.4,609.8l-181.6,137l-332.3-58.2c-445.3-78.8-1281.2-78.8-1702.6,0C2796-2569.2,1734.1-1832.6,1220.2-801.5C983.8-318.5,905,51.5,929,613.3c27.4,640.6,243.2,1192.1,685.1,1740.3c620,770.8,1661.5,1305.2,2822.8,1452.5C4806.9,3854,5553.7,3819.7,5947.6,3740.9z"/><path fill="#205281" d="M2381.5-345.9c-75.4-106.2-68.5-167.8,34.3-322c332.3-500.2,1010.6-928.4,1760.8-1120.2c417.9-106.2,1226.4-106.2,1644.3,0c712.5,181.6,1270.9,517.3,1685.4,1014C7681-561.7,7715.3-424.7,7616-325.4c-89.1,89.1-167.9,65.1-431.7-133.6c-835.8-630.3-2028-856.4-3086.5-585.8C3683.3-938.6,3142-685,2830.3-448.7C2576.8-253.4,2463.7-229.4,2381.5-345.9z"/></g></g><!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon --></svg>'''
valid_events = [
events.PullRequestCloseEvent,
events.PullRequestMergeEvent,
@ -217,8 +215,23 @@ class HipchatIntegrationType(IntegrationTypeBase):
)
def format_repo_push_event(self, data):
branch_data = {branch['name']: branch
for branch in data['push']['branches']}
branches_commits = {}
for commit in data['push']['commits']:
log.critical(commit)
if commit['branch'] not in branches_commits:
branch_commits = {'branch': branch_data[commit['branch']],
'commits': []}
branches_commits[commit['branch']] = branch_commits
branch_commits = branches_commits[commit['branch']]
branch_commits['commits'].append(commit)
result = repo_push_template.render(
data=data,
branches_commits=branches_commits,
)
return result

View file

@ -29,21 +29,20 @@ from celery.task import task
from mako.template import Template
from rhodecode import events
from rhodecode.translation import lazy_ugettext
from rhodecode.translation import _
from rhodecode.lib import helpers as h
from rhodecode.lib.celerylib import run_task
from rhodecode.lib.colander_utils import strip_whitespace
from rhodecode.integrations.types.base import IntegrationTypeBase
from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
log = logging.getLogger(__name__)
class SlackSettingsSchema(IntegrationSettingsSchemaBase):
class SlackSettingsSchema(colander.Schema):
service = colander.SchemaNode(
colander.String(),
title=lazy_ugettext('Slack service URL'),
description=h.literal(lazy_ugettext(
title=_('Slack service URL'),
description=h.literal(_(
'This can be setup at the '
'<a href="https://my.slack.com/services/new/incoming-webhook/">'
'slack app manager</a>')),
@ -56,8 +55,8 @@ class SlackSettingsSchema(IntegrationSettingsSchemaBase):
)
username = colander.SchemaNode(
colander.String(),
title=lazy_ugettext('Username'),
description=lazy_ugettext('Username to show notifications coming from.'),
title=_('Username'),
description=_('Username to show notifications coming from.'),
missing='Rhodecode',
preparer=strip_whitespace,
widget=deform.widget.TextInputWidget(
@ -66,8 +65,8 @@ class SlackSettingsSchema(IntegrationSettingsSchemaBase):
)
channel = colander.SchemaNode(
colander.String(),
title=lazy_ugettext('Channel'),
description=lazy_ugettext('Channel to send notifications to.'),
title=_('Channel'),
description=_('Channel to send notifications to.'),
missing='',
preparer=strip_whitespace,
widget=deform.widget.TextInputWidget(
@ -76,8 +75,8 @@ class SlackSettingsSchema(IntegrationSettingsSchemaBase):
)
icon_emoji = colander.SchemaNode(
colander.String(),
title=lazy_ugettext('Emoji'),
description=lazy_ugettext('Emoji to use eg. :studio_microphone:'),
title=_('Emoji'),
description=_('Emoji to use eg. :studio_microphone:'),
missing='',
preparer=strip_whitespace,
widget=deform.widget.TextInputWidget(
@ -87,25 +86,22 @@ class SlackSettingsSchema(IntegrationSettingsSchemaBase):
repo_push_template = Template(r'''
*${data['actor']['username']}* pushed to \
%if data['push']['branches']:
${len(data['push']['branches']) > 1 and 'branches' or 'branch'} \
${', '.join('<%s|%s>' % (branch['url'], branch['name']) for branch in data['push']['branches'])} \
%else:
unknown branch \
%endif
in <${data['repo']['url']}|${data['repo']['repo_name']}>
>>>
%for commit in data['push']['commits']:
<${commit['url']}|${commit['short_id']}> - ${commit['message_html']|html_to_slack_links}
*${data['actor']['username']}* pushed to repo <${data['repo']['url']}|${data['repo']['repo_name']}>:
%for branch, branch_commits in branches_commits.items():
branch: <${branch_commits['branch']['url']}|${branch_commits['branch']['name']}>
%for commit in branch_commits['commits']:
> <${commit['url']}|${commit['short_id']}> - ${commit['message_html']|html_to_slack_links}
%endfor
%endfor
''')
class SlackIntegrationType(IntegrationTypeBase):
key = 'slack'
display_name = lazy_ugettext('Slack')
SettingsSchema = SlackSettingsSchema
display_name = _('Slack')
description = _('Send events such as repo pushes and pull requests to '
'your slack channel.')
icon = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M165.963541,15.8384262 C162.07318,3.86308197 149.212328,-2.69009836 137.239082,1.20236066 C125.263738,5.09272131 118.710557,17.9535738 122.603016,29.9268197 L181.550164,211.292328 C185.597902,222.478689 197.682361,228.765377 209.282098,225.426885 C221.381246,221.943607 228.756984,209.093246 224.896,197.21023 C224.749115,196.756984 165.963541,15.8384262 165.963541,15.8384262" fill="#DFA22F"></path><path d="M74.6260984,45.515541 C70.7336393,33.5422951 57.8727869,26.9891148 45.899541,30.8794754 C33.9241967,34.7698361 27.3710164,47.6306885 31.2634754,59.6060328 L90.210623,240.971541 C94.2583607,252.157902 106.34282,258.44459 117.942557,255.104 C130.041705,251.62282 137.417443,238.772459 133.556459,226.887344 C133.409574,226.436197 74.6260984,45.515541 74.6260984,45.515541" fill="#3CB187"></path><path d="M240.161574,166.045377 C252.136918,162.155016 258.688,149.294164 254.797639,137.31882 C250.907279,125.345574 238.046426,118.792393 226.07318,122.682754 L44.7076721,181.632 C33.5213115,185.677639 27.234623,197.762098 30.5731148,209.361836 C34.0563934,221.460984 46.9067541,228.836721 58.7897705,224.975738 C59.2430164,224.828852 240.161574,166.045377 240.161574,166.045377" fill="#CE1E5B"></path><path d="M82.507541,217.270557 C94.312918,213.434754 109.528131,208.491016 125.855475,203.186361 C122.019672,191.380984 117.075934,176.163672 111.76918,159.83423 L68.4191475,173.924721 L82.507541,217.270557" fill="#392538"></path><path d="M173.847082,187.591344 C190.235279,182.267803 205.467279,177.31777 217.195016,173.507148 C213.359213,161.70177 208.413377,146.480262 203.106623,130.146623 L159.75659,144.237115 L173.847082,187.591344" fill="#BB242A"></path><path d="M210.484459,74.7058361 C222.457705,70.8154754 229.010885,57.954623 225.120525,45.9792787 C221.230164,34.0060328 208.369311,27.4528525 196.393967,31.3432131 L15.028459,90.292459 C3.84209836,94.3380984 -2.44459016,106.422557 0.896,118.022295 C4.37718033,130.121443 17.227541,137.49718 29.1126557,133.636197 C29.5638033,133.489311 210.484459,74.7058361 210.484459,74.7058361" fill="#72C5CD"></path><path d="M52.8220328,125.933115 C64.6274098,122.097311 79.8468197,117.151475 96.1762623,111.84682 C90.8527213,95.4565246 85.9026885,80.2245246 82.0920656,68.4946885 L38.731541,82.5872787 L52.8220328,125.933115" fill="#248C73"></path><path d="M144.159475,96.256 C160.551869,90.9303607 175.785967,85.9803279 187.515803,82.1676066 C182.190164,65.7752131 177.240131,50.5390164 173.42741,38.807082 L130.068984,52.8996721 L144.159475,96.256" fill="#62803A"></path></g></svg>'''
valid_events = [
events.PullRequestCloseEvent,
events.PullRequestMergeEvent,
@ -221,8 +217,23 @@ class SlackIntegrationType(IntegrationTypeBase):
)
def format_repo_push_event(self, data):
branch_data = {branch['name']: branch
for branch in data['push']['branches']}
branches_commits = {}
for commit in data['push']['commits']:
log.critical(commit)
if commit['branch'] not in branches_commits:
branch_commits = {'branch': branch_data[commit['branch']],
'commits': []}
branches_commits[commit['branch']] = branch_commits
branch_commits = branches_commits[commit['branch']]
branch_commits['commits'].append(commit)
result = repo_push_template.render(
data=data,
branches_commits=branches_commits,
html_to_slack_links=html_to_slack_links,
)
return result

View file

@ -28,19 +28,19 @@ from celery.task import task
from mako.template import Template
from rhodecode import events
from rhodecode.translation import lazy_ugettext
from rhodecode.translation import _
from rhodecode.integrations.types.base import IntegrationTypeBase
from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
log = logging.getLogger(__name__)
class WebhookSettingsSchema(IntegrationSettingsSchemaBase):
class WebhookSettingsSchema(colander.Schema):
url = colander.SchemaNode(
colander.String(),
title=lazy_ugettext('Webhook URL'),
description=lazy_ugettext('URL of the webhook to receive POST event.'),
default='',
title=_('Webhook URL'),
description=_('URL of the webhook to receive POST event.'),
missing=colander.required,
required=True,
validator=colander.url,
widget=deform.widget.TextInputWidget(
placeholder='https://www.example.com/webhook'
@ -48,18 +48,24 @@ class WebhookSettingsSchema(IntegrationSettingsSchemaBase):
)
secret_token = colander.SchemaNode(
colander.String(),
title=lazy_ugettext('Secret Token'),
description=lazy_ugettext('String used to validate received payloads.'),
title=_('Secret Token'),
description=_('String used to validate received payloads.'),
default='',
missing='',
widget=deform.widget.TextInputWidget(
placeholder='secret_token'
),
)
class WebhookIntegrationType(IntegrationTypeBase):
key = 'webhook'
display_name = lazy_ugettext('Webhook')
display_name = _('Webhook')
description = _('Post json events to a webhook endpoint')
icon = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 239" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M119.540432,100.502743 C108.930124,118.338815 98.7646301,135.611455 88.3876025,152.753617 C85.7226696,157.154315 84.4040417,160.738531 86.5332204,166.333309 C92.4107024,181.787152 84.1193605,196.825836 68.5350381,200.908244 C53.8383677,204.759349 39.5192953,195.099955 36.6032893,179.365384 C34.0194114,165.437749 44.8274148,151.78491 60.1824106,149.608284 C61.4694072,149.424428 62.7821041,149.402681 64.944891,149.240571 C72.469175,136.623655 80.1773157,123.700312 88.3025935,110.073173 C73.611854,95.4654658 64.8677898,78.3885437 66.803227,57.2292132 C68.1712787,42.2715849 74.0527146,29.3462646 84.8033863,18.7517722 C105.393354,-1.53572199 136.805164,-4.82141828 161.048542,10.7510424 C184.333097,25.7086706 194.996783,54.8450075 185.906752,79.7822957 C179.052655,77.9239597 172.151111,76.049808 164.563565,73.9917997 C167.418285,60.1274266 165.306899,47.6765751 155.95591,37.0109123 C149.777932,29.9690049 141.850349,26.2780332 132.835442,24.9178894 C114.764113,22.1877169 97.0209573,33.7983633 91.7563309,51.5355878 C85.7800012,71.6669027 94.8245623,88.1111998 119.540432,100.502743 L119.540432,100.502743 Z" fill="#C73A63"></path><path d="M149.841194,79.4106285 C157.316054,92.5969067 164.905578,105.982857 172.427885,119.246236 C210.44865,107.483365 239.114472,128.530009 249.398582,151.063322 C261.81978,178.282014 253.328765,210.520191 228.933162,227.312431 C203.893073,244.551464 172.226236,241.605803 150.040866,219.46195 C155.694953,214.729124 161.376716,209.974552 167.44794,204.895759 C189.360489,219.088306 208.525074,218.420096 222.753207,201.614016 C234.885769,187.277151 234.622834,165.900356 222.138374,151.863988 C207.730339,135.66681 188.431321,135.172572 165.103273,150.721309 C155.426087,133.553447 145.58086,116.521995 136.210101,99.2295848 C133.05093,93.4015266 129.561608,90.0209366 122.440622,88.7873178 C110.547271,86.7253555 102.868785,76.5124151 102.408155,65.0698097 C101.955433,53.7537294 108.621719,43.5249733 119.04224,39.5394355 C129.363912,35.5914599 141.476705,38.7783085 148.419765,47.554004 C154.093621,54.7244134 155.896602,62.7943365 152.911402,71.6372484 C152.081082,74.1025091 151.00562,76.4886916 149.841194,79.4106285 L149.841194,79.4106285 Z" fill="#4B4B4B"></path><path d="M167.706921,187.209935 L121.936499,187.209935 C117.54964,205.253587 108.074103,219.821756 91.7464461,229.085759 C79.0544063,236.285822 65.3738898,238.72736 50.8136292,236.376762 C24.0061432,232.053165 2.08568567,207.920497 0.156179306,180.745298 C-2.02835403,149.962159 19.1309765,122.599149 47.3341915,116.452801 C49.2814904,123.524363 51.2485589,130.663141 53.1958579,137.716911 C27.3195169,150.919004 18.3639187,167.553089 25.6054984,188.352614 C31.9811726,206.657224 50.0900643,216.690262 69.7528413,212.809503 C89.8327554,208.847688 99.9567329,192.160226 98.7211371,165.37844 C117.75722,165.37844 136.809118,165.180745 155.847178,165.475311 C163.280522,165.591951 169.019617,164.820939 174.620326,158.267339 C183.840836,147.48306 200.811003,148.455721 210.741239,158.640984 C220.88894,169.049642 220.402609,185.79839 209.663799,195.768166 C199.302587,205.38802 182.933414,204.874012 173.240413,194.508846 C171.247644,192.37176 169.677943,189.835329 167.706921,187.209935 L167.706921,187.209935 Z" fill="#4A4A4A"></path></g></svg>'''
valid_events = [
events.PullRequestCloseEvent,
events.PullRequestMergeEvent,

View file

@ -18,23 +18,29 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import colander
import logging
import pylons
import deform
import logging
import colander
import peppercorn
import webhelpers.paginate
from pyramid.httpexceptions import HTTPFound, HTTPForbidden
from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
from pyramid.renderers import render
from pyramid.response import Response
from rhodecode.lib import auth
from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
from rhodecode.model.db import Repository, Session, Integration
from rhodecode.lib.utils2 import safe_int
from rhodecode.lib.helpers import Page
from rhodecode.model.db import Repository, RepoGroup, Session, Integration
from rhodecode.model.scm import ScmModel
from rhodecode.model.integration import IntegrationModel
from rhodecode.admin.navigation import navigation_list
from rhodecode.translation import _
from rhodecode.integrations import integration_type_registry
from rhodecode.model.validation_schema.schemas.integration_schema import (
make_integration_schema, IntegrationScopeType)
log = logging.getLogger(__name__)
@ -59,28 +65,51 @@ class IntegrationSettingsViewBase(object):
self.IntegrationType = None
self.repo = None
self.repo_group = None
self.integration = None
self.integrations = {}
request = self.request
if 'repo_name' in request.matchdict: # we're in a repo context
if 'repo_name' in request.matchdict: # in repo settings context
repo_name = request.matchdict['repo_name']
self.repo = Repository.get_by_repo_name(repo_name)
if 'integration' in request.matchdict: # we're in integration context
if 'repo_group_name' in request.matchdict: # in group settings context
repo_group_name = request.matchdict['repo_group_name']
self.repo_group = RepoGroup.get_by_group_name(repo_group_name)
if 'integration' in request.matchdict: # integration type context
integration_type = request.matchdict['integration']
self.IntegrationType = integration_type_registry[integration_type]
if 'integration_id' in request.matchdict: # single integration context
integration_id = request.matchdict['integration_id']
self.integration = Integration.get(integration_id)
else: # list integrations context
for integration in IntegrationModel().get_integrations(self.repo):
self.integrations.setdefault(integration.integration_type, []
).append(integration)
# extra perms check just in case
if not self._has_perms_for_integration(self.integration):
raise HTTPForbidden()
self.settings = self.integration and self.integration.settings or {}
self.admin_view = not (self.repo or self.repo_group)
def _has_perms_for_integration(self, integration):
perms = self.request.user.permissions
if 'hg.admin' in perms['global']:
return True
if integration.repo:
return perms['repositories'].get(
integration.repo.repo_name) == 'repository.admin'
if integration.repo_group:
return perms['repositories_groups'].get(
integration.repo_group.group_name) == 'group.admin'
return False
def _template_c_context(self):
# TODO: dan: this is a stopgap in order to inherit from current pylons
@ -91,7 +120,10 @@ class IntegrationSettingsViewBase(object):
c.active = 'integrations'
c.rhodecode_user = self.request.user
c.repo = self.repo
c.repo_group = self.repo_group
c.repo_name = self.repo and self.repo.repo_name or None
c.repo_group_name = self.repo_group and self.repo_group.group_name or None
if self.repo:
c.repo_info = self.repo
c.rhodecode_db_repo = self.repo
@ -102,34 +134,77 @@ class IntegrationSettingsViewBase(object):
return c
def _form_schema(self):
if self.integration:
settings = self.integration.settings
else:
settings = {}
return self.IntegrationType(settings=settings).settings_schema()
schema = make_integration_schema(IntegrationType=self.IntegrationType,
settings=self.settings)
def settings_get(self, defaults=None, errors=None, form=None):
"""
View that displays the plugin settings as a form.
"""
defaults = defaults or {}
errors = errors or {}
# returns a clone, important if mutating the schema later
return schema.bind(
permissions=self.request.user.permissions,
no_scope=not self.admin_view)
def _form_defaults(self):
defaults = {}
if self.integration:
defaults = self.integration.settings or {}
defaults['name'] = self.integration.name
defaults['enabled'] = self.integration.enabled
defaults['settings'] = self.integration.settings or {}
defaults['options'] = {
'name': self.integration.name,
'enabled': self.integration.enabled,
'scope': {
'repo': self.integration.repo,
'repo_group': self.integration.repo_group,
'child_repos_only': self.integration.child_repos_only,
},
}
else:
if self.repo:
scope = self.repo.repo_name
scope = _('{repo_name} repository').format(
repo_name=self.repo.repo_name)
elif self.repo_group:
scope = _('{repo_group_name} repo group').format(
repo_group_name=self.repo_group.group_name)
else:
scope = _('Global')
defaults['name'] = '{} {} integration'.format(scope,
self.IntegrationType.display_name)
defaults['enabled'] = True
defaults['options'] = {
'enabled': True,
'name': _('{name} integration').format(
name=self.IntegrationType.display_name),
}
defaults['options']['scope'] = {
'repo': self.repo,
'repo_group': self.repo_group,
}
schema = self._form_schema().bind(request=self.request)
return defaults
def _delete_integration(self, integration):
Session().delete(self.integration)
Session().commit()
self.request.session.flash(
_('Integration {integration_name} deleted successfully.').format(
integration_name=self.integration.name),
queue='success')
if self.repo:
redirect_to = self.request.route_url(
'repo_integrations_home', repo_name=self.repo.repo_name)
elif self.repo_group:
redirect_to = self.request.route_url(
'repo_group_integrations_home',
repo_group_name=self.repo_group.group_name)
else:
redirect_to = self.request.route_url('global_integrations_home')
raise HTTPFound(redirect_to)
def settings_get(self, defaults=None, form=None):
"""
View that displays the integration settings as a form.
"""
defaults = defaults or self._form_defaults()
schema = self._form_schema()
if self.integration:
buttons = ('submit', 'delete')
@ -138,23 +213,10 @@ class IntegrationSettingsViewBase(object):
form = form or deform.Form(schema, appstruct=defaults, buttons=buttons)
for node in schema:
setting = self.settings.get(node.name)
if setting is not None:
defaults.setdefault(node.name, setting)
else:
if node.default:
defaults.setdefault(node.name, node.default)
template_context = {
'form': form,
'defaults': defaults,
'errors': errors,
'schema': schema,
'current_IntegrationType': self.IntegrationType,
'integration': self.integration,
'settings': self.settings,
'resource': self.context,
'c': self._template_c_context(),
}
@ -163,71 +225,93 @@ class IntegrationSettingsViewBase(object):
@auth.CSRFRequired()
def settings_post(self):
"""
View that validates and stores the plugin settings.
View that validates and stores the integration settings.
"""
if self.request.params.get('delete'):
Session().delete(self.integration)
Session().commit()
self.request.session.flash(
_('Integration {integration_name} deleted successfully.').format(
integration_name=self.integration.name),
queue='success')
if self.repo:
redirect_to = self.request.route_url(
'repo_integrations_home', repo_name=self.repo.repo_name)
else:
redirect_to = self.request.route_url('global_integrations_home')
raise HTTPFound(redirect_to)
schema = self._form_schema().bind(request=self.request)
form = deform.Form(schema, buttons=('submit', 'delete'))
params = {}
for node in schema.children:
if type(node.typ) in (colander.Set, colander.List):
val = self.request.params.getall(node.name)
else:
val = self.request.params.get(node.name)
if val:
params[node.name] = val
controls = self.request.POST.items()
pstruct = peppercorn.parse(controls)
if self.integration and pstruct.get('delete'):
return self._delete_integration(self.integration)
schema = self._form_schema()
skip_settings_validation = False
if self.integration and 'enabled' not in pstruct.get('options', {}):
skip_settings_validation = True
schema['settings'].validator = None
for field in schema['settings'].children:
field.validator = None
field.missing = ''
if self.integration:
buttons = ('submit', 'delete')
else:
buttons = ('submit',)
form = deform.Form(schema, buttons=buttons)
if not self.admin_view:
# scope is read only field in these cases, and has to be added
options = pstruct.setdefault('options', {})
if 'scope' not in options:
options['scope'] = IntegrationScopeType().serialize(None, {
'repo': self.repo,
'repo_group': self.repo_group,
})
try:
valid_data = form.validate(controls)
valid_data = form.validate_pstruct(pstruct)
except deform.ValidationFailure as e:
self.request.session.flash(
_('Errors exist when saving integration settings. '
'Please check the form inputs.'),
queue='error')
return self.settings_get(errors={}, defaults=params, form=e)
return self.settings_get(form=e)
if not self.integration:
self.integration = Integration()
self.integration.integration_type = self.IntegrationType.key
if self.repo:
self.integration.repo = self.repo
Session().add(self.integration)
self.integration.enabled = valid_data.pop('enabled', False)
self.integration.name = valid_data.pop('name')
self.integration.settings = valid_data
scope = valid_data['options']['scope']
IntegrationModel().update_integration(self.integration,
name=valid_data['options']['name'],
enabled=valid_data['options']['enabled'],
settings=valid_data['settings'],
repo=scope['repo'],
repo_group=scope['repo_group'],
child_repos_only=scope['child_repos_only'],
)
self.integration.settings = valid_data['settings']
Session().commit()
# Display success message and redirect.
self.request.session.flash(
_('Integration {integration_name} updated successfully.').format(
integration_name=self.IntegrationType.display_name),
queue='success')
if self.repo:
redirect_to = self.request.route_url(
'repo_integrations_edit', repo_name=self.repo.repo_name,
# if integration scope changes, we must redirect to the right place
# keeping in mind if the original view was for /repo/ or /_admin/
admin_view = not (self.repo or self.repo_group)
if self.integration.repo and not admin_view:
redirect_to = self.request.route_path(
'repo_integrations_edit',
repo_name=self.integration.repo.repo_name,
integration=self.integration.integration_type,
integration_id=self.integration.integration_id)
elif self.integration.repo_group and not admin_view:
redirect_to = self.request.route_path(
'repo_group_integrations_edit',
repo_group_name=self.integration.repo_group.group_name,
integration=self.integration.integration_type,
integration_id=self.integration.integration_id)
else:
redirect_to = self.request.route_url(
redirect_to = self.request.route_path(
'global_integrations_edit',
integration=self.integration.integration_type,
integration_id=self.integration.integration_id)
@ -235,31 +319,60 @@ class IntegrationSettingsViewBase(object):
return HTTPFound(redirect_to)
def index(self):
current_integrations = self.integrations
if self.IntegrationType:
current_integrations = {
self.IntegrationType.key: self.integrations.get(
self.IntegrationType.key, [])
}
""" List integrations """
if self.repo:
scope = self.repo
elif self.repo_group:
scope = self.repo_group
else:
scope = 'all'
integrations = []
for integration in IntegrationModel().get_integrations(
scope=scope, IntegrationType=self.IntegrationType):
# extra permissions check *just in case*
if not self._has_perms_for_integration(integration):
continue
integrations.append(integration)
sort_arg = self.request.GET.get('sort', 'name:asc')
if ':' in sort_arg:
sort_field, sort_dir = sort_arg.split(':')
else:
sort_field = sort_arg, 'asc'
assert sort_field in ('name', 'integration_type', 'enabled', 'scope')
integrations.sort(
key=lambda x: getattr(x[1], sort_field), reverse=(sort_dir=='desc'))
page_url = webhelpers.paginate.PageURL(
self.request.path, self.request.GET)
page = safe_int(self.request.GET.get('page', 1), 1)
integrations = Page(integrations, page=page, items_per_page=10,
url=page_url)
template_context = {
'sort_field': sort_field,
'rev_sort_dir': sort_dir != 'desc' and 'desc' or 'asc',
'current_IntegrationType': self.IntegrationType,
'current_integrations': current_integrations,
'integrations_list': integrations,
'available_integrations': integration_type_registry,
'c': self._template_c_context()
'c': self._template_c_context(),
'request': self.request,
}
return template_context
if self.repo:
html = render('rhodecode:templates/admin/integrations/list.html',
template_context,
request=self.request)
else:
html = render('rhodecode:templates/admin/integrations/list.html',
template_context,
request=self.request)
return Response(html)
def new_integration(self):
template_context = {
'available_integrations': integration_type_registry,
'c': self._template_c_context(),
}
return template_context
class GlobalIntegrationsView(IntegrationSettingsViewBase):
def perm_check(self, user):
@ -270,3 +383,10 @@ class RepoIntegrationsView(IntegrationSettingsViewBase):
def perm_check(self, user):
return auth.HasRepoPermissionAll('repository.admin'
)(repo_name=self.repo.repo_name, user=user)
class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
def perm_check(self, user):
return auth.HasRepoGroupPermissionAll('group.admin'
)(group_name=self.repo_group.group_name, user=user)

View file

@ -48,12 +48,12 @@ def annotate_highlight(
:param headers: dictionary with headers (keys are whats in ``order``
parameter)
"""
from rhodecode.lib.utils import get_custom_lexer
from rhodecode.lib.helpers import get_lexer_for_filenode
options['linenos'] = True
formatter = AnnotateHtmlFormatter(
filenode=filenode, order=order, headers=headers,
annotate_from_commit_func=annotate_from_commit_func, **options)
lexer = get_custom_lexer(filenode.extension) or filenode.lexer
lexer = get_lexer_for_filenode(filenode)
highlighted = highlight(filenode.content, lexer, formatter)
return highlighted

View file

@ -1116,9 +1116,11 @@ class CSRFRequired(object):
For use with the ``webhelpers.secure_form`` helper functions.
"""
def __init__(self, token=csrf_token_key, header='X-CSRF-Token'):
def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
except_methods=None):
self.token = token
self.header = header
self.except_methods = except_methods or []
def __call__(self, func):
return get_cython_compat_decorator(self.__wrapper, func)
@ -1131,6 +1133,9 @@ class CSRFRequired(object):
return supplied_token and supplied_token == cur_token
def __wrapper(self, func, *fargs, **fkwargs):
if request.method in self.except_methods:
return func(*fargs, **fkwargs)
cur_token = get_csrf_token(save_if_missing=False)
if self.check_csrf(request, cur_token):
if request.POST.get(self.token):

View file

@ -28,6 +28,7 @@ import logging
import socket
import ipaddress
import pyramid.threadlocal
from paste.auth.basic import AuthBasicAuthenticator
from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
@ -276,7 +277,7 @@ def attach_context_attributes(context, request):
# Visual options
context.visual = AttributeDict({})
# DB store
# DB stored Visual Items
context.visual.show_public_icon = str2bool(
rc_config.get('rhodecode_show_public_icon'))
context.visual.show_private_icon = str2bool(
@ -368,6 +369,8 @@ def attach_context_attributes(context, request):
context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
context.rhodecode_user.user_id)
context.pyramid_request = pyramid.threadlocal.get_current_request()
def get_auth_user(environ):
ip_addr = get_ip_addr(environ)

View file

@ -84,6 +84,7 @@ def get_user_data(user_id):
'icon_link': h.gravatar_url(user.email, 14),
'display_name': h.person(user, 'username_or_name_or_email'),
'display_link': h.link_to_user(user),
'notifications': user.user_data.get('notification_status', True)
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,36 @@
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 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_4_0_0
tbl = db_4_4_0_0.Integration.__table__
repo_group_id = db_4_4_0_0.Integration.repo_group_id
repo_group_id.create(table=tbl)
def downgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine

View file

@ -0,0 +1,35 @@
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 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_4_0_1
tbl = db_4_4_0_1.Integration.__table__
child_repos_only = db_4_4_0_1.Integration.child_repos_only
child_repos_only.create(table=tbl)
def downgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine

View file

@ -0,0 +1,85 @@
import logging
from sqlalchemy import *
from rhodecode.model import init_model_encryption, meta
from rhodecode.lib.utils2 import safe_str
from rhodecode.lib.dbmigrate.versions import _reset_base, notify
log = logging.getLogger(__name__)
def get_all_settings(models):
settings = {
'rhodecode_' + result.app_settings_name: result.app_settings_value
for result in models.RhodeCodeSetting.query()
}
return settings
def get_ui_by_section_and_key(models, section, key):
q = models.RhodeCodeUi.query()
q = q.filter(models.RhodeCodeUi.ui_section == section)
q = q.filter(models.RhodeCodeUi.ui_key == key)
return q.scalar()
def create_ui_section_value(models, Session, section, val, key=None, active=True):
new_ui = models.RhodeCodeUi()
new_ui.ui_section = section
new_ui.ui_value = val
new_ui.ui_active = active
new_ui.ui_key = key
Session().add(new_ui)
return new_ui
def create_or_update_ui(
models, Session, section, key, value=None, active=None):
ui = get_ui_by_section_and_key(models, section, key)
if not ui:
active = True if active is None else active
create_ui_section_value(
models, Session, section, value, key=key, active=active)
else:
if active is not None:
ui.ui_active = active
if value is not None:
ui.ui_value = value
Session().add(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_4_0_1
init_model_encryption(db_4_4_0_1)
fixups(db_4_4_0_1, meta.Session)
def downgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine
def fixups(models, Session):
current_settings = get_all_settings(models)
svn_proxy_enabled = safe_str(current_settings.get(
'rhodecode_proxy_subversion_http_requests', 'False'))
svn_proxy_url = current_settings.get(
'rhodecode_subversion_http_server_url', '')
create_or_update_ui(
models, Session, 'vcs_svn_proxy', 'http_requests_enabled',
value=svn_proxy_enabled)
create_or_update_ui(
models, Session, 'vcs_svn_proxy', 'http_server_url',
value=svn_proxy_url)
Session().commit()

View file

@ -55,6 +55,7 @@ def wrap_to_table(str_):
return '''<table class="code-difftable">
<tr class="line no-comment">
<td class="add-comment-line tooltip" title="%s"><span class="add-comment-content"></span></td>
<td></td>
<td class="lineno new"></td>
<td class="code no-comment"><pre>%s</pre></td>
</tr>
@ -691,14 +692,14 @@ class DiffProcessor(object):
anchor_link = False
###########################################################
# COMMENT ICON
# COMMENT ICONS
###########################################################
_html.append('''\t<td class="add-comment-line"><span class="add-comment-content">''')
if enable_comments and change['action'] != Action.CONTEXT:
_html.append('''<a href="#"><span class="icon-comment-add"></span></a>''')
_html.append('''</span></td>\n''')
_html.append('''</span></td><td class="comment-toggle tooltip" title="Toggle Comments"><i class="icon-comment"></i></td>\n''')
###########################################################
# OLD LINE NUMBER

View file

@ -23,6 +23,7 @@ Set of custom exceptions used in RhodeCode
"""
from webob.exc import HTTPClientError
from pyramid.httpexceptions import HTTPBadGateway
class LdapUsernameError(Exception):
@ -120,3 +121,19 @@ class NotAllowedToCreateUserError(Exception):
class RepositoryCreationError(Exception):
pass
class VCSServerUnavailable(HTTPBadGateway):
""" HTTP Exception class for VCS Server errors """
code = 502
title = 'VCS Server Error'
causes = [
'VCS Server is not running',
'Incorrect vcs.server=host:port',
'Incorrect vcs.server.protocol',
]
def __init__(self, message=''):
self.explanation = 'Could not connect to VCS Server'
if message:
self.explanation += ': ' + message
super(VCSServerUnavailable, self).__init__()

View file

@ -520,13 +520,18 @@ def get_lexer_safe(mimetype=None, filepath=None):
return lexer
def get_lexer_for_filenode(filenode):
lexer = get_custom_lexer(filenode.extension) or filenode.lexer
return lexer
def pygmentize(filenode, **kwargs):
"""
pygmentize function using pygments
:param filenode:
"""
lexer = get_custom_lexer(filenode.extension) or filenode.lexer
lexer = get_lexer_for_filenode(filenode)
return literal(code_highlight(filenode.content, lexer,
CodeHtmlFormatter(**kwargs)))
@ -772,10 +777,10 @@ def get_repo_type_by_name(repo_name):
def is_svn_without_proxy(repository):
from rhodecode import CONFIG
if is_svn(repository):
if not CONFIG.get('rhodecode_proxy_subversion_http_requests', False):
return True
from rhodecode.model.settings import VcsSettingsModel
conf = VcsSettingsModel().get_ui_settings_as_config_obj()
return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
return False
@ -1946,6 +1951,13 @@ def route_path(*args, **kwds):
return req.route_path(*args, **kwds)
def route_path_or_none(*args, **kwargs):
try:
return route_path(*args, **kwargs)
except KeyError:
return None
def static_url(*args, **kwds):
"""
Wrapper around pyramids `route_path` function. It is used to generate

View file

@ -265,7 +265,7 @@ class WhooshResultWrapper(object):
f_path = '' # noqa
if self.search_type in ['content', 'path']:
f_path = res['path'].split(res['repository'])[-1]
f_path = res['path'][len(res['repository']):]
f_path = f_path.lstrip(os.sep)
if self.search_type == 'content':

View file

@ -51,18 +51,10 @@ class MarkupRenderer(object):
RST_PAT = re.compile(r'\.re?st$', re.IGNORECASE)
PLAIN_PAT = re.compile(r'^readme$', re.IGNORECASE)
# list of readme files to search in file tree and display in summary
# attached weights defines the search order lower is first
ALL_READMES = [
('readme', 0), ('README', 0), ('Readme', 0),
('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1),
('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2),
('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2),
('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2),
]
# extension together with weights. Lower is first means we control how
# extensions are attached to readme names with those.
PLAIN_EXTS = [
# prefer no extension
('', 0), # special case that renders READMES names without extension
('.text', 2), ('.TEXT', 2),
('.txt', 3), ('.TXT', 3)
@ -80,8 +72,6 @@ class MarkupRenderer(object):
('.markdown', 4), ('.MARKDOWN', 4)
]
ALL_EXTS = PLAIN_EXTS + MARKDOWN_EXTS + RST_EXTS
def _detect_renderer(self, source, filename=None):
"""
runs detection of what renderer should be used for generating html
@ -124,29 +114,6 @@ class MarkupRenderer(object):
return None
@classmethod
def generate_readmes(cls, all_readmes, extensions):
combined = itertools.product(all_readmes, extensions)
# sort by filename weight(y[0][1]) + extensions weight(y[1][1])
prioritized_readmes = sorted(combined, key=lambda y: y[0][1] + y[1][1])
# filename, extension
return [''.join([x[0][0], x[1][0]]) for x in prioritized_readmes]
def pick_readme_order(self, default_renderer):
if default_renderer == 'markdown':
markdown = self.generate_readmes(self.ALL_READMES, self.MARKDOWN_EXTS)
readme_order = markdown + self.generate_readmes(
self.ALL_READMES, self.RST_EXTS + self.PLAIN_EXTS)
elif default_renderer == 'rst':
markdown = self.generate_readmes(self.ALL_READMES, self.RST_EXTS)
readme_order = markdown + self.generate_readmes(
self.ALL_READMES, self.MARKDOWN_EXTS + self.PLAIN_EXTS)
else:
readme_order = self.generate_readmes(self.ALL_READMES, self.ALL_EXTS)
return readme_order
def render(self, source, filename=None):
"""
Renders a given filename using detected renderer

View file

@ -1,76 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2015-2016 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
"""
Disable VCS pages when VCS Server is not available
"""
import logging
import re
from pyramid.httpexceptions import HTTPBadGateway
log = logging.getLogger(__name__)
class VCSServerUnavailable(HTTPBadGateway):
""" HTTP Exception class for when VCS Server is unavailable """
code = 502
title = 'VCS Server Required'
explanation = 'A VCS Server is required for this action. There is currently no VCS Server configured.'
class DisableVCSPagesWrapper(object):
"""
Pyramid view wrapper to disable all pages that require VCS Server to be
running, avoiding that errors explode to the user.
This Wrapper should be enabled only in case VCS Server is not available
for the instance.
"""
VCS_NOT_REQUIRED = [
'^/$',
('/_admin(?!/settings/mapping)(?!/my_account/repos)'
'(?!/create_repository)(?!/gists)(?!/notifications/)'
),
]
_REGEX_VCS_NOT_REQUIRED = [re.compile(path) for path in VCS_NOT_REQUIRED]
def _check_vcs_requirement(self, path_info):
"""
Tries to match the current path to one of the safe URLs to be rendered.
Displays an error message in case
"""
for regex in self._REGEX_VCS_NOT_REQUIRED:
safe_url = regex.match(path_info)
if safe_url:
return True
# Url is not safe to be rendered without VCS Server
log.debug('accessing: `%s` with VCS Server disabled', path_info)
return False
def __init__(self, handler):
self.handler = handler
def __call__(self, context, request):
if not self._check_vcs_requirement(request.path):
raise VCSServerUnavailable('VCS Server is not available')
return self.handler(context, request)

View file

@ -23,12 +23,15 @@ SimpleGit middleware for handling git protocol request (push/clone etc.)
It's implemented with basic auth function
"""
import re
import logging
import urlparse
import rhodecode
from rhodecode.lib import utils2
from rhodecode.lib.middleware import simplevcs
log = logging.getLogger(__name__)
GIT_PROTO_PAT = re.compile(
r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')

View file

@ -23,12 +23,15 @@ SimpleHG middleware for handling mercurial protocol request
(push/clone etc.). It's implemented with basic auth function
"""
import logging
import urlparse
from rhodecode.lib import utils
from rhodecode.lib.ext_json import json
from rhodecode.lib.middleware import simplevcs
log = logging.getLogger(__name__)
class SimpleHg(simplevcs.SimpleVCS):

View file

@ -18,13 +18,17 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
from urlparse import urljoin
import requests
from webob.exc import HTTPNotAcceptable
import rhodecode
from rhodecode.lib.middleware import simplevcs
from rhodecode.lib.utils import is_valid_repo
from rhodecode.lib.utils2 import str2bool
log = logging.getLogger(__name__)
class SimpleSvnApp(object):
@ -92,10 +96,21 @@ class SimpleSvnApp(object):
return headers
class DisabledSimpleSvnApp(object):
def __init__(self, config):
self.config = config
def __call__(self, environ, start_response):
reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
log.warning(reason)
return HTTPNotAcceptable(reason)(environ, start_response)
class SimpleSvn(simplevcs.SimpleVCS):
SCM = 'svn'
READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
DEFAULT_HTTP_SERVER = 'http://localhost:8090'
def _get_repository_name(self, environ):
"""
@ -126,11 +141,19 @@ class SimpleSvn(simplevcs.SimpleVCS):
else 'push')
def _create_wsgi_app(self, repo_path, repo_name, config):
return SimpleSvnApp(config)
if self._is_svn_enabled():
return SimpleSvnApp(config)
# we don't have http proxy enabled return dummy request handler
return DisabledSimpleSvnApp(config)
def _is_svn_enabled(self):
conf = self.repo_vcs_config
return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
def _create_config(self, extras, repo_name):
server_url = rhodecode.CONFIG.get(
'rhodecode_subversion_http_server_url', '')
extras['subversion_http_server_url'] = (
server_url or 'http://localhost/')
conf = self.repo_vcs_config
server_url = conf.get('vcs_svn_proxy', 'http_server_url')
server_url = server_url or self.DEFAULT_HTTP_SERVER
extras['subversion_http_server_url'] = server_url
return extras

View file

@ -46,10 +46,11 @@ from rhodecode.lib.utils import (
is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
from rhodecode.lib.vcs.conf import settings as vcs_settings
from rhodecode.lib.vcs.backends import base
from rhodecode.model import meta
from rhodecode.model.db import User, Repository
from rhodecode.model.scm import ScmModel
from rhodecode.model.settings import SettingsModel
log = logging.getLogger(__name__)
@ -86,6 +87,10 @@ class SimpleVCS(object):
self.registry = registry
self.application = application
self.config = config
# re-populated by specialized middleware
self.repo_name = None
self.repo_vcs_config = base.Config()
# base path of repo locations
self.basepath = get_rhodecode_base_path()
# authenticate this VCS request using authfunc
@ -111,9 +116,7 @@ class SimpleVCS(object):
def _get_by_id(self, repo_name):
"""
Gets a special pattern _<ID> from clone url and tries to replace it
with a repository_name for support of _<ID> non changable urls
:param repo_name:
with a repository_name for support of _<ID> non changeable urls
"""
data = repo_name.split('/')
@ -205,8 +208,7 @@ class SimpleVCS(object):
"""
org_proto = environ['wsgi._org_proto']
# check if we have SSL required ! if not it's a bad request !
require_ssl = str2bool(
SettingsModel().get_ui_by_key('push_ssl').ui_value)
require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
if require_ssl and org_proto == 'http':
log.debug('proto is %s and SSL is required BAD REQUEST !',
org_proto)
@ -231,25 +233,18 @@ class SimpleVCS(object):
log.debug('User not allowed to proceed, %s', reason)
return HTTPNotAcceptable(reason)(environ, start_response)
if not self.repo_name:
log.warning('Repository name is empty: %s', self.repo_name)
# failed to get repo name, we fail now
return HTTPNotFound()(environ, start_response)
log.debug('Extracted repo name is %s', self.repo_name)
ip_addr = get_ip_addr(environ)
username = None
# skip passing error to error controller
environ['pylons.status_code_redirect'] = True
# ======================================================================
# EXTRACT REPOSITORY NAME FROM ENV
# ======================================================================
environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
repo_name = self._get_repository_name(environ)
environ['REPO_NAME'] = repo_name
log.debug('Extracted repo name is %s', repo_name)
# check for type, presence in database and on filesystem
if not self.is_valid_and_existing_repo(
repo_name, self.basepath, self.SCM):
return HTTPNotFound()(environ, start_response)
# ======================================================================
# GET ACTION PULL or PUSH
# ======================================================================
@ -264,7 +259,7 @@ class SimpleVCS(object):
if anonymous_user.active:
# ONLY check permissions if the user is activated
anonymous_perm = self._check_permission(
action, anonymous_user, repo_name, ip_addr)
action, anonymous_user, self.repo_name, ip_addr)
else:
anonymous_perm = False
@ -328,7 +323,8 @@ class SimpleVCS(object):
return HTTPNotAcceptable(reason)(environ, start_response)
# check permissions for this repository
perm = self._check_permission(action, user, repo_name, ip_addr)
perm = self._check_permission(
action, user, self.repo_name, ip_addr)
if not perm:
return HTTPForbidden()(environ, start_response)
@ -336,14 +332,14 @@ class SimpleVCS(object):
# in hooks executed by rhodecode
check_locking = _should_check_locking(environ.get('QUERY_STRING'))
extras = vcs_operation_context(
environ, repo_name=repo_name, username=username,
environ, repo_name=self.repo_name, username=username,
action=action, scm=self.SCM,
check_locking=check_locking)
# ======================================================================
# REQUEST HANDLING
# ======================================================================
str_repo_name = safe_str(repo_name)
str_repo_name = safe_str(self.repo_name)
repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
log.debug('Repository path is %s', repo_path)
@ -354,7 +350,7 @@ class SimpleVCS(object):
action, self.SCM, str_repo_name, safe_str(username), ip_addr)
return self._generate_vcs_response(
environ, start_response, repo_path, repo_name, extras, action)
environ, start_response, repo_path, self.repo_name, extras, action)
@initialize_generator
def _generate_vcs_response(

View file

@ -24,12 +24,14 @@ import logging
import tempfile
import urlparse
from webob.exc import HTTPNotFound
import rhodecode
from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
from rhodecode.lib.middleware.simplehg import SimpleHg
from rhodecode.lib.middleware.simplesvn import SimpleSvn
from rhodecode.model.settings import VcsSettingsModel
log = logging.getLogger(__name__)
@ -131,31 +133,66 @@ class VCSMiddleware(object):
self.config = config
self.appenlight_client = appenlight_client
self.registry = registry
self.use_gzip = True
# order in which we check the middlewares, based on vcs.backends config
self.check_middlewares = config['vcs.backends']
self.checks = {
'hg': (is_hg, SimpleHg),
'git': (is_git, SimpleGit),
'svn': (is_svn, SimpleSvn),
}
def vcs_config(self, repo_name=None):
"""
returns serialized VcsSettings
"""
return VcsSettingsModel(repo=repo_name).get_ui_settings_as_config_obj()
def wrap_in_gzip_if_enabled(self, app, config):
if self.use_gzip:
app = GunzipMiddleware(app)
return app
def _get_handler_app(self, environ):
app = None
if is_hg(environ):
app = SimpleHg(self.application, self.config, self.registry)
if is_git(environ):
app = SimpleGit(self.application, self.config, self.registry)
proxy_svn = rhodecode.CONFIG.get(
'rhodecode_proxy_subversion_http_requests', False)
if proxy_svn and is_svn(environ):
app = SimpleSvn(self.application, self.config, self.registry)
if app:
app = GunzipMiddleware(app)
app, _ = wrap_in_appenlight_if_enabled(
app, self.config, self.appenlight_client)
log.debug('Checking vcs types in order: %r', self.check_middlewares)
for vcs_type in self.check_middlewares:
vcs_check, handler = self.checks[vcs_type]
if vcs_check(environ):
log.debug(
'Found VCS Middleware to handle the request %s', handler)
app = handler(self.application, self.config, self.registry)
break
return app
def __call__(self, environ, start_response):
# check if we handle one of interesting protocols ?
# check if we handle one of interesting protocols, optionally extract
# specific vcsSettings and allow changes of how things are wrapped
vcs_handler = self._get_handler_app(environ)
if vcs_handler:
# translate the _REPO_ID into real repo NAME for usage
# in middleware
environ['PATH_INFO'] = vcs_handler._get_by_id(environ['PATH_INFO'])
repo_name = vcs_handler._get_repository_name(environ)
# check for type, presence in database and on filesystem
if not vcs_handler.is_valid_and_existing_repo(
repo_name, vcs_handler.basepath, vcs_handler.SCM):
return HTTPNotFound()(environ, start_response)
# TODO: johbo: Needed for the Pyro4 backend and Mercurial only.
# Remove once we fully switched to the HTTP backend.
environ['REPO_NAME'] = repo_name
# register repo_name and it's config back to the handler
vcs_handler.repo_name = repo_name
vcs_handler.repo_vcs_config = self.vcs_config(repo_name)
vcs_handler = self.wrap_in_gzip_if_enabled(
vcs_handler, self.config)
vcs_handler, _ = wrap_in_appenlight_if_enabled(
vcs_handler, self.config, self.appenlight_client)
return vcs_handler(environ, start_response)
return self.application(environ, start_response)

View file

@ -54,7 +54,7 @@ def md5_safe(s):
return md5(safe_str(s))
def __get_lem():
def __get_lem(extra_mapping=None):
"""
Get language extension map based on what's inside pygments lexers
"""
@ -82,7 +82,16 @@ def __get_lem():
desc = lx.replace('Lexer', '')
d[ext].append(desc)
return dict(d)
data = dict(d)
extra_mapping = extra_mapping or {}
if extra_mapping:
for k, v in extra_mapping.items():
if k not in data:
# register new mapping2lexer
data[k] = [v]
return data
def str2bool(_str):
@ -110,7 +119,7 @@ def aslist(obj, sep=None, strip=True):
:param sep:
:param strip:
"""
if isinstance(obj, (basestring)):
if isinstance(obj, (basestring,)):
lst = obj.split(sep)
if strip:
lst = [v.strip() for v in lst]

View file

@ -1438,7 +1438,8 @@ class Config(object):
return clone
def __repr__(self):
return '<Config(%s values) at %s>' % (len(self._values), hex(id(self)))
return '<Config(%s sections) at %s>' % (
len(self._values), hex(id(self)))
def items(self, section):
return self._values.get(section, {}).iteritems()

View file

@ -98,7 +98,7 @@ class GitInMemoryCommit(base.BaseInMemoryCommit):
self.repository._rebuild_cache(self.repository.commit_ids)
# invalidate parsed refs after commit
self.repository._parsed_refs = self.repository._get_parsed_refs()
self.repository._refs = self.repository._get_refs()
self.repository.branches = self.repository._get_branches()
tip = self.repository.get_commit()
self.reset()

View file

@ -205,12 +205,6 @@ class GitRepository(BaseRepository):
return []
return output.splitlines()
def _get_all_commit_ids2(self):
# alternate implementation
includes = [x[1][0] for x in self._parsed_refs.iteritems()
if x[1][1] != 'T']
return [c.commit.id for c in self._remote.get_walker(include=includes)]
def _get_commit_id(self, commit_id_or_idx):
def is_null(value):
return len(value) == commit_id_or_idx.count('0')
@ -232,17 +226,23 @@ class GitRepository(BaseRepository):
raise CommitDoesNotExistError(msg)
elif is_bstr:
# get by branch/tag name
ref_id = self._parsed_refs.get(commit_id_or_idx)
if ref_id: # and ref_id[1] in ['H', 'RH', 'T']:
return ref_id[0]
# check full path ref, eg. refs/heads/master
ref_id = self._refs.get(commit_id_or_idx)
if ref_id:
return ref_id
tag_ids = self.tags.values()
# maybe it's a tag ? we don't have them in self.commit_ids
if commit_id_or_idx in tag_ids:
return commit_id_or_idx
# check branch name
branch_ids = self.branches.values()
ref_id = self._refs.get('refs/heads/%s' % commit_id_or_idx)
if ref_id:
return ref_id
elif (not SHA_PATTERN.match(commit_id_or_idx) or
# check tag name
ref_id = self._refs.get('refs/tags/%s' % commit_id_or_idx)
if ref_id:
return ref_id
if (not SHA_PATTERN.match(commit_id_or_idx) or
commit_id_or_idx not in self.commit_ids):
msg = "Commit %s does not exist for %s" % (
commit_id_or_idx, self)
@ -289,20 +289,25 @@ class GitRepository(BaseRepository):
description = self._remote.get_description()
return safe_unicode(description or self.DEFAULT_DESCRIPTION)
def _get_refs_entry(self, value, reverse):
def _get_refs_entries(self, prefix='', reverse=False, strip_prefix=True):
if self.is_empty():
return {}
return OrderedDict()
def get_name(ctx):
return ctx[0]
result = []
for ref, sha in self._refs.iteritems():
if ref.startswith(prefix):
ref_name = ref
if strip_prefix:
ref_name = ref[len(prefix):]
result.append((safe_unicode(ref_name), sha))
_branches = [
(safe_unicode(x[0]), x[1][0])
for x in self._parsed_refs.iteritems() if x[1][1] == value]
return OrderedDict(sorted(_branches, key=get_name, reverse=reverse))
def get_name(entry):
return entry[0]
return OrderedDict(sorted(result, key=get_name, reverse=reverse))
def _get_branches(self):
return self._get_refs_entry('H', False)
return self._get_refs_entries(prefix='refs/heads/', strip_prefix=True)
@LazyProperty
def branches(self):
@ -324,10 +329,12 @@ class GitRepository(BaseRepository):
return self._get_tags()
def _get_tags(self):
return self._get_refs_entry('T', True)
return self._get_refs_entries(
prefix='refs/tags/', strip_prefix=True, reverse=True)
def tag(self, name, user, commit_id=None, message=None, date=None,
**kwargs):
# TODO: fix this method to apply annotated tags correct with message
"""
Creates and returns a tag for the given ``commit_id``.
@ -346,7 +353,7 @@ class GitRepository(BaseRepository):
name, commit.raw_id)
self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
self._parsed_refs = self._get_parsed_refs()
self._refs = self._get_refs()
self.tags = self._get_tags()
return commit
@ -367,24 +374,28 @@ class GitRepository(BaseRepository):
self._remote.get_refs_path(), 'refs', 'tags', name)
try:
os.remove(tagpath)
self._parsed_refs = self._get_parsed_refs()
self._refs = self._get_refs()
self.tags = self._get_tags()
except OSError as e:
raise RepositoryError(e.strerror)
@LazyProperty
def _parsed_refs(self):
return self._get_parsed_refs()
def _get_refs(self):
return self._remote.get_refs()
def _get_parsed_refs(self):
# TODO: (oliver) who needs RH; branches?
# Remote Heads were commented out, as they may overwrite local branches
# See the TODO note in rhodecode.lib.vcs.remote.git:get_refs for more
# details.
keys = [('refs/heads/', 'H'),
#('refs/remotes/origin/', 'RH'),
('refs/tags/', 'T')]
return self._remote.get_refs(keys=keys)
@LazyProperty
def _refs(self):
return self._get_refs()
@property
def _ref_tree(self):
node = tree = {}
for ref, sha in self._refs.iteritems():
path = ref.split('/')
for bit in path[:-1]:
node = node.setdefault(bit, {})
node[path[-1]] = sha
node = tree
return tree
def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
"""

View file

@ -305,7 +305,7 @@ def _get_proxy_method(proxy, name):
try:
return getattr(proxy, name)
except CommunicationError:
raise CommunicationError(
raise exceptions.PyroVCSCommunicationError(
'Unable to connect to remote pyro server %s' % proxy)

View file

@ -36,6 +36,7 @@ import urllib2
import urlparse
import uuid
import pycurl
import msgpack
import requests
@ -172,7 +173,11 @@ class RemoteObject(object):
def _remote_call(url, payload, exceptions_map, session):
response = session.post(url, data=msgpack.packb(payload))
try:
response = session.post(url, data=msgpack.packb(payload))
except pycurl.error as e:
raise exceptions.HttpVCSCommunicationError(e)
response = msgpack.unpackb(response.content)
error = response.get('error')
if error:

View file

@ -24,6 +24,19 @@ Custom vcs exceptions module.
import functools
import urllib2
import pycurl
from Pyro4.errors import CommunicationError
class VCSCommunicationError(Exception):
pass
class PyroVCSCommunicationError(VCSCommunicationError):
pass
class HttpVCSCommunicationError(VCSCommunicationError):
pass
class VCSError(Exception):
@ -161,7 +174,6 @@ def map_vcs_exceptions(func):
try:
return func(*args, **kwargs)
except Exception as e:
# The error middleware adds information if it finds
# __traceback_info__ in a frame object. This way the remote
# traceback information is made available in error reports.
@ -182,5 +194,4 @@ def map_vcs_exceptions(func):
raise _EXCEPTION_MAP[kind](*e.args)
else:
raise
return wrapper

View file

@ -27,6 +27,7 @@ import stat
from zope.cachedescriptors.property import Lazy as LazyProperty
from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
from rhodecode.lib.utils import safe_unicode, safe_str
from rhodecode.lib.utils2 import md5
from rhodecode.lib.vcs import path as vcspath
@ -435,11 +436,26 @@ class FileNode(Node):
content, name and mimetype.
"""
from pygments import lexers
lexer = None
try:
lexer = lexers.guess_lexer_for_filename(self.name, self.content, stripnl=False)
lexer = lexers.guess_lexer_for_filename(
self.name, self.content, stripnl=False)
except lexers.ClassNotFound:
lexer = None
# try our EXTENSION_MAP
if not lexer:
try:
lexer_class = LANGUAGES_EXTENSIONS_MAP.get(self.extension)
if lexer_class:
lexer = lexers.get_lexer_by_name(lexer_class[0])
except lexers.ClassNotFound:
lexer = None
if not lexer:
lexer = lexers.TextLexer(stripnl=False)
# returns first alias
return lexer
@LazyProperty

View file

@ -2036,6 +2036,8 @@ class RepoGroup(Base, BaseModel):
users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
parent_group = relationship('RepoGroup', remote_side=group_id)
user = relationship('User')
integrations = relationship('Integration',
cascade="all, delete, delete-orphan")
def __init__(self, group_name='', parent_group=None):
self.group_name = group_name
@ -3481,6 +3483,8 @@ class Integration(Base, BaseModel):
integration_type = Column('integration_type', String(255))
enabled = Column('enabled', Boolean(), nullable=False)
name = Column('name', String(255), nullable=False)
child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
default=False)
settings = Column(
'settings_json', MutationObj.as_mutable(
@ -3490,10 +3494,23 @@ class Integration(Base, BaseModel):
nullable=True, unique=None, default=None)
repo = relationship('Repository', lazy='joined')
def __repr__(self):
if self.repo:
scope = 'repo=%r' % self.repo
else:
scope = 'global'
repo_group_id = Column(
'repo_group_id', Integer(), ForeignKey('groups.group_id'),
nullable=True, unique=None, default=None)
repo_group = relationship('RepoGroup', lazy='joined')
return '<Integration(%r, %r)>' % (self.integration_type, scope)
@property
def scope(self):
if self.repo:
return repr(self.repo)
if self.repo_group:
if self.child_repos_only:
return repr(self.repo_group) + ' (child repos only)'
else:
return repr(self.repo_group) + ' (recursive)'
if self.child_repos_only:
return 'root_repos'
return 'global'
def __repr__(self):
return '<Integration(%r, %r)>' % (self.integration_type, self.scope)

View file

@ -102,20 +102,6 @@ def LoginForm():
return _LoginForm
def PasswordChangeForm(username):
class _PasswordChangeForm(formencode.Schema):
allow_extra_fields = True
filter_extra_fields = True
current_password = v.ValidOldPassword(username)(not_empty=True)
new_password = All(v.ValidPassword(), v.UnicodeString(strip=False, min=6))
new_password_confirmation = All(v.ValidPassword(), v.UnicodeString(strip=False, min=6))
chained_validators = [v.ValidPasswordsMatch('new_password',
'new_password_confirmation')]
return _PasswordChangeForm
def UserForm(edit=False, available_languages=[], old_data={}):
class _UserForm(formencode.Schema):
allow_extra_fields = True
@ -403,6 +389,9 @@ class _BaseVcsSettingsForm(formencode.Schema):
rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
def ApplicationUiSettingsForm():
class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
@ -435,11 +424,6 @@ def LabsSettingsForm():
allow_extra_fields = True
filter_extra_fields = False
rhodecode_proxy_subversion_http_requests = v.StringBoolean(
if_missing=False)
rhodecode_subversion_http_server_url = v.UnicodeString(
strip=True, if_missing=None)
return _LabSettingsForm

View file

@ -29,7 +29,7 @@ import traceback
from pylons import tmpl_context as c
from pylons.i18n.translation import _, ungettext
from sqlalchemy import or_
from sqlalchemy import or_, and_
from sqlalchemy.sql.expression import false, true
from mako import exceptions
@ -39,7 +39,7 @@ from rhodecode.lib import helpers as h
from rhodecode.lib.caching_query import FromCache
from rhodecode.lib.utils import PartialRenderer
from rhodecode.model import BaseModel
from rhodecode.model.db import Integration, User
from rhodecode.model.db import Integration, User, Repository, RepoGroup
from rhodecode.model.meta import Session
from rhodecode.integrations import integration_type_registry
from rhodecode.integrations.types.base import IntegrationTypeBase
@ -61,28 +61,35 @@ class IntegrationModel(BaseModel):
raise Exception('integration must be int, long or Instance'
' of Integration got %s' % type(integration))
def create(self, IntegrationType, enabled, name, settings, repo=None):
def create(self, IntegrationType, name, enabled, repo, repo_group,
child_repos_only, settings):
""" Create an IntegrationType integration """
integration = Integration()
integration.integration_type = IntegrationType.key
integration.settings = {}
integration.repo = repo
integration.enabled = enabled
integration.name = name
self.sa.add(integration)
self.update_integration(integration, name, enabled, repo, repo_group,
child_repos_only, settings)
self.sa.commit()
return integration
def update_integration(self, integration, name, enabled, repo, repo_group,
child_repos_only, settings):
integration = self.__get_integration(integration)
integration.repo = repo
integration.repo_group = repo_group
integration.child_repos_only = child_repos_only
integration.name = name
integration.enabled = enabled
integration.settings = settings
return integration
def delete(self, integration):
try:
integration = self.__get_integration(integration)
if integration:
self.sa.delete(integration)
return True
except Exception:
log.error(traceback.format_exc())
raise
integration = self.__get_integration(integration)
if integration:
self.sa.delete(integration)
return True
return False
def get_integration_handler(self, integration):
@ -100,33 +107,116 @@ class IntegrationModel(BaseModel):
if handler:
handler.send_event(event)
def get_integrations(self, repo=None):
if repo:
return self.sa.query(Integration).filter(
Integration.repo_id==repo.repo_id).all()
def get_integrations(self, scope, IntegrationType=None):
"""
Return integrations for a scope, which must be one of:
# global integrations
return self.sa.query(Integration).filter(
Integration.repo_id==None).all()
'all' - every integration, global/repogroup/repo
'global' - global integrations only
<Repository> instance - integrations for this repo only
<RepoGroup> instance - integrations for this repogroup only
"""
if isinstance(scope, Repository):
query = self.sa.query(Integration).filter(
Integration.repo==scope)
elif isinstance(scope, RepoGroup):
query = self.sa.query(Integration).filter(
Integration.repo_group==scope)
elif scope == 'global':
# global integrations
query = self.sa.query(Integration).filter(
and_(Integration.repo_id==None, Integration.repo_group_id==None)
)
elif scope == 'root-repos':
query = self.sa.query(Integration).filter(
and_(Integration.repo_id==None,
Integration.repo_group_id==None,
Integration.child_repos_only==True)
)
elif scope == 'all':
query = self.sa.query(Integration)
else:
raise Exception(
"invalid `scope`, must be one of: "
"['global', 'all', <Repository>, <RepoGroup>]")
if IntegrationType is not None:
query = query.filter(
Integration.integration_type==IntegrationType.key)
result = []
for integration in query.all():
IntType = integration_type_registry.get(integration.integration_type)
result.append((IntType, integration))
return result
def get_for_event(self, event, cache=False):
"""
Get integrations that match an event
"""
query = self.sa.query(Integration).filter(Integration.enabled==True)
query = self.sa.query(
Integration
).filter(
Integration.enabled==True
)
global_integrations_filter = and_(
Integration.repo_id==None,
Integration.repo_group_id==None,
Integration.child_repos_only==False,
)
if isinstance(event, events.RepoEvent):
root_repos_integrations_filter = and_(
Integration.repo_id==None,
Integration.repo_group_id==None,
Integration.child_repos_only==True,
)
clauses = [
global_integrations_filter,
]
# repo integrations
if event.repo.repo_id: # pre create events dont have a repo_id yet
clauses.append(
Integration.repo_id==event.repo.repo_id
)
if event.repo.group:
clauses.append(
and_(
Integration.repo_group_id==event.repo.group.group_id,
Integration.child_repos_only==True
)
)
# repo group cascade to kids
clauses.append(
and_(
Integration.repo_group_id.in_(
[group.group_id for group in
event.repo.groups_with_parents]
),
Integration.child_repos_only==False
)
)
if not event.repo.group: # root repo
clauses.append(root_repos_integrations_filter)
query = query.filter(or_(*clauses))
if isinstance(event, events.RepoEvent): # global + repo integrations
query = query.filter(
or_(Integration.repo_id==None,
Integration.repo_id==event.repo.repo_id))
if cache:
query = query.options(FromCache(
"sql_cache_short",
"get_enabled_repo_integrations_%i" % event.repo.repo_id))
else: # only global integrations
query = query.filter(Integration.repo_id==None)
query = query.filter(global_integrations_filter)
if cache:
query = query.options(FromCache(
"sql_cache_short", "get_enabled_global_integrations"))
return query.all()
result = query.all()
return result

View file

@ -40,11 +40,13 @@ from rhodecode.lib.auth import HasUserGroupPermissionAny
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 (
Repository, UserRepoToPerm, UserGroupRepoToPerm, UserRepoGroupToPerm,
@ -933,3 +935,119 @@ class RepoModel(BaseModel):
if os.path.isdir(rm_path):
shutil.move(rm_path, os.path.join(self.repos_path, _d))
class ReadmeFinder:
"""
Utility which knows how to find a readme for a specific commit.
The main idea is that this is a configurable algorithm. When creating an
instance you can define parameters, currently only the `default_renderer`.
Based on this configuration the method :meth:`search` behaves slightly
different.
"""
readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE)
path_re = re.compile(r'^docs?', re.IGNORECASE)
default_priorities = {
None: 0,
'.text': 2,
'.txt': 3,
'.rst': 1,
'.rest': 2,
'.md': 1,
'.mkdn': 2,
'.mdown': 3,
'.markdown': 4,
}
path_priority = {
'doc': 0,
'docs': 1,
}
FALLBACK_PRIORITY = 99
RENDERER_TO_EXTENSION = {
'rst': ['.rst', '.rest'],
'markdown': ['.md', 'mkdn', '.mdown', '.markdown'],
}
def __init__(self, default_renderer=None):
self._default_renderer = default_renderer
self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(
default_renderer, [])
def search(self, commit, path='/'):
"""
Find a readme in the given `commit`.
"""
nodes = commit.get_nodes(path)
matches = self._match_readmes(nodes)
matches = self._sort_according_to_priority(matches)
if matches:
return matches[0].node
paths = self._match_paths(nodes)
paths = self._sort_paths_according_to_priority(paths)
for path in paths:
match = self.search(commit, path=path)
if match:
return match
return None
def _match_readmes(self, nodes):
for node in nodes:
if not node.is_file():
continue
path = node.path.rsplit('/', 1)[-1]
match = self.readme_re.match(path)
if match:
extension = match.group(1)
yield ReadmeMatch(node, match, self._priority(extension))
def _match_paths(self, nodes):
for node in nodes:
if not node.is_dir():
continue
match = self.path_re.match(node.path)
if match:
yield node.path
def _priority(self, extension):
renderer_priority = (
0 if extension in self._renderer_extensions else 1)
extension_priority = self.default_priorities.get(
extension, self.FALLBACK_PRIORITY)
return (renderer_priority, extension_priority)
def _sort_according_to_priority(self, matches):
def priority_and_path(match):
return (match.priority, match.path)
return sorted(matches, key=priority_and_path)
def _sort_paths_according_to_priority(self, paths):
def priority_and_path(path):
return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path)
return sorted(paths, key=priority_and_path)
class ReadmeMatch:
def __init__(self, node, match, priority):
self.node = node
self._match = match
self.priority = priority
@property
def path(self):
return self.node.path
def __repr__(self):
return '<ReadmeMatch {} priority={}'.format(self.path, self.priority)

View file

@ -469,6 +469,8 @@ class RepoGroupModel(BaseModel):
def delete(self, repo_group, force_delete=False, fs_remove=True):
repo_group = self._get_repo_group(repo_group)
if not repo_group:
return False
try:
self.sa.delete(repo_group)
if fs_remove:
@ -478,6 +480,7 @@ class RepoGroupModel(BaseModel):
# Trigger delete event.
events.trigger(events.RepoGroupDeleteEvent(repo_group))
return True
except Exception:
log.error('Error removing repo_group %s', repo_group)

View file

@ -24,9 +24,9 @@ from collections import namedtuple
from functools import wraps
from rhodecode.lib import caches
from rhodecode.lib.caching_query import FromCache
from rhodecode.lib.utils2 import (
Optional, AttributeDict, safe_str, remove_prefix, str2bool)
from rhodecode.lib.vcs.backends import base
from rhodecode.model import BaseModel
from rhodecode.model.db import (
RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, RhodeCodeSetting)
@ -402,15 +402,25 @@ class VcsSettingsModel(object):
INHERIT_SETTINGS = 'inherit_vcs_settings'
GENERAL_SETTINGS = (
'use_outdated_comments', 'pr_merge_enabled',
'use_outdated_comments',
'pr_merge_enabled',
'hg_use_rebase_for_merging')
HOOKS_SETTINGS = (
('hooks', 'changegroup.repo_size'),
('hooks', 'changegroup.push_logger'),
('hooks', 'outgoing.pull_logger'))
HG_SETTINGS = (
('extensions', 'largefiles'), ('phases', 'publish'))
GLOBAL_HG_SETTINGS = HG_SETTINGS + (('extensions', 'hgsubversion'), )
('extensions', 'largefiles'),
('phases', 'publish'))
GLOBAL_HG_SETTINGS = (
('extensions', 'largefiles'),
('phases', 'publish'),
('extensions', 'hgsubversion'))
GLOBAL_SVN_SETTINGS = (
('vcs_svn_proxy', 'http_requests_enabled'),
('vcs_svn_proxy', 'http_server_url'))
SVN_BRANCH_SECTION = 'vcs_svn_branch'
SVN_TAG_SECTION = 'vcs_svn_tag'
SSL_SETTING = ('web', 'push_ssl')
@ -520,13 +530,10 @@ class VcsSettingsModel(object):
def create_repo_svn_settings(self, data):
return self._create_svn_settings(self.repo_settings, data)
def create_global_svn_settings(self, data):
return self._create_svn_settings(self.global_settings, data)
@assert_repo_settings
def create_or_update_repo_hg_settings(self, data):
largefiles, phases = self.HG_SETTINGS
largefiles_key, phases_key = self._get_hg_settings(
largefiles_key, phases_key = self._get_settings_keys(
self.HG_SETTINGS, data)
self._create_or_update_ui(
self.repo_settings, *largefiles, value='',
@ -535,8 +542,8 @@ class VcsSettingsModel(object):
self.repo_settings, *phases, value=safe_str(data[phases_key]))
def create_or_update_global_hg_settings(self, data):
largefiles, phases, subversion = self.GLOBAL_HG_SETTINGS
largefiles_key, phases_key, subversion_key = self._get_hg_settings(
largefiles, phases, hgsubversion = self.GLOBAL_HG_SETTINGS
largefiles_key, phases_key, subversion_key = self._get_settings_keys(
self.GLOBAL_HG_SETTINGS, data)
self._create_or_update_ui(
self.global_settings, *largefiles, value='',
@ -544,7 +551,22 @@ class VcsSettingsModel(object):
self._create_or_update_ui(
self.global_settings, *phases, value=safe_str(data[phases_key]))
self._create_or_update_ui(
self.global_settings, *subversion, active=data[subversion_key])
self.global_settings, *hgsubversion, active=data[subversion_key])
def create_or_update_global_svn_settings(self, data):
# branch/tags patterns
self._create_svn_settings(self.global_settings, data)
http_requests_enabled, http_server_url = self.GLOBAL_SVN_SETTINGS
http_requests_enabled_key, http_server_url_key = self._get_settings_keys(
self.GLOBAL_SVN_SETTINGS, data)
self._create_or_update_ui(
self.global_settings, *http_requests_enabled,
value=safe_str(data[http_requests_enabled_key]))
self._create_or_update_ui(
self.global_settings, *http_server_url,
value=data[http_server_url_key])
def update_global_ssl_setting(self, value):
self._create_or_update_ui(
@ -582,6 +604,16 @@ class VcsSettingsModel(object):
def get_global_ui_settings(self, section=None, key=None):
return self.global_settings.get_ui(section, key)
def get_ui_settings_as_config_obj(self, section=None, key=None):
config = base.Config()
ui_settings = self.get_ui_settings(section=section, key=key)
for entry in ui_settings:
config.set(entry.section, entry.key, entry.value)
return config
def get_ui_settings(self, section=None, key=None):
if not self.repo_settings or self.inherit_global_settings:
return self.get_global_ui_settings(section, key)
@ -689,7 +721,7 @@ class VcsSettingsModel(object):
name, data[data_key], 'bool')
Session().add(setting)
def _get_hg_settings(self, settings, data):
def _get_settings_keys(self, settings, data):
data_keys = [self._get_form_ui_key(*s) for s in settings]
for data_key in data_keys:
if data_key not in data:

View file

@ -147,7 +147,6 @@ class UserModel(BaseModel):
# cleanups, my_account password change form
kwargs.pop('current_password', None)
kwargs.pop('new_password', None)
kwargs.pop('new_password_confirmation', None)
# cleanups, user edit password change form
kwargs.pop('password_confirmation', None)
@ -315,6 +314,7 @@ class UserModel(BaseModel):
new_user.update_userdata(force_password_change=True)
if language:
new_user.update_userdata(language=language)
new_user.update_userdata(notification_status=True)
self.sa.add(new_user)

View file

@ -0,0 +1,226 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2016 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import os
import deform
import colander
from rhodecode.translation import _
from rhodecode.model.db import Repository, RepoGroup
from rhodecode.model.validation_schema import validators, preparers
def integration_scope_choices(permissions):
"""
Return list of (value, label) choices for integration scopes depending on
the permissions
"""
result = [('', _('Pick a scope:'))]
if 'hg.admin' in permissions['global']:
result.extend([
('global', _('Global (all repositories)')),
('root-repos', _('Top level repositories only')),
])
repo_choices = [
('repo:%s' % repo_name, '/' + repo_name)
for repo_name, repo_perm
in permissions['repositories'].items()
if repo_perm == 'repository.admin'
]
repogroup_choices = [
('repogroup:%s' % repo_group_name, '/' + repo_group_name + '/ (child repos only)')
for repo_group_name, repo_group_perm
in permissions['repositories_groups'].items()
if repo_group_perm == 'group.admin'
]
repogroup_recursive_choices = [
('repogroup-recursive:%s' % repo_group_name, '/' + repo_group_name + '/ (recursive)')
for repo_group_name, repo_group_perm
in permissions['repositories_groups'].items()
if repo_group_perm == 'group.admin'
]
result.extend(
sorted(repogroup_recursive_choices + repogroup_choices + repo_choices,
key=lambda (choice, label): choice.split(':', 1)[1]
)
)
return result
@colander.deferred
def deferred_integration_scopes_validator(node, kw):
perms = kw.get('permissions')
def _scope_validator(_node, scope):
is_super_admin = 'hg.admin' in perms['global']
if scope.get('repo'):
if (is_super_admin or perms['repositories'].get(
scope['repo'].repo_name) == 'repository.admin'):
return True
msg = _('Only repo admins can create integrations')
raise colander.Invalid(_node, msg)
elif scope.get('repo_group'):
if (is_super_admin or perms['repositories_groups'].get(
scope['repo_group'].group_name) == 'group.admin'):
return True
msg = _('Only repogroup admins can create integrations')
raise colander.Invalid(_node, msg)
else:
if is_super_admin:
return True
msg = _('Only superadmins can create global integrations')
raise colander.Invalid(_node, msg)
return _scope_validator
@colander.deferred
def deferred_integration_scopes_widget(node, kw):
if kw.get('no_scope'):
return deform.widget.TextInputWidget(readonly=True)
choices = integration_scope_choices(kw.get('permissions'))
widget = deform.widget.Select2Widget(values=choices)
return widget
class IntegrationScopeType(colander.SchemaType):
def serialize(self, node, appstruct):
if appstruct is colander.null:
return colander.null
if appstruct.get('repo'):
return 'repo:%s' % appstruct['repo'].repo_name
elif appstruct.get('repo_group'):
if appstruct.get('child_repos_only'):
return 'repogroup:%s' % appstruct['repo_group'].group_name
else:
return 'repogroup-recursive:%s' % (
appstruct['repo_group'].group_name)
else:
if appstruct.get('child_repos_only'):
return 'root-repos'
else:
return 'global'
raise colander.Invalid(node, '%r is not a valid scope' % appstruct)
def deserialize(self, node, cstruct):
if cstruct is colander.null:
return colander.null
if cstruct.startswith('repo:'):
repo = Repository.get_by_repo_name(cstruct.split(':')[1])
if repo:
return {
'repo': repo,
'repo_group': None,
'child_repos_only': None,
}
elif cstruct.startswith('repogroup-recursive:'):
repo_group = RepoGroup.get_by_group_name(cstruct.split(':')[1])
if repo_group:
return {
'repo': None,
'repo_group': repo_group,
'child_repos_only': False
}
elif cstruct.startswith('repogroup:'):
repo_group = RepoGroup.get_by_group_name(cstruct.split(':')[1])
if repo_group:
return {
'repo': None,
'repo_group': repo_group,
'child_repos_only': True
}
elif cstruct == 'global':
return {
'repo': None,
'repo_group': None,
'child_repos_only': False
}
elif cstruct == 'root-repos':
return {
'repo': None,
'repo_group': None,
'child_repos_only': True
}
raise colander.Invalid(node, '%r is not a valid scope' % cstruct)
class IntegrationOptionsSchemaBase(colander.MappingSchema):
name = colander.SchemaNode(
colander.String(),
description=_('Short name for this integration.'),
missing=colander.required,
title=_('Integration name'),
)
scope = colander.SchemaNode(
IntegrationScopeType(),
description=_(
'Scope of the integration. Recursive means the integration '
' runs on all repos of that group and children recursively.'),
title=_('Integration scope'),
validator=deferred_integration_scopes_validator,
widget=deferred_integration_scopes_widget,
missing=colander.required,
)
enabled = colander.SchemaNode(
colander.Bool(),
default=True,
description=_('Enable or disable this integration.'),
missing=False,
title=_('Enabled'),
)
def make_integration_schema(IntegrationType, settings=None):
"""
Return a colander schema for an integration type
:param IntegrationType: the integration type class
:param settings: existing integration settings dict (optional)
"""
settings = settings or {}
settings_schema = IntegrationType(settings=settings).settings_schema()
class IntegrationSchema(colander.Schema):
options = IntegrationOptionsSchemaBase()
schema = IntegrationSchema()
schema['options'].title = _('General integration options')
settings_schema.name = 'settings'
settings_schema.title = _('{integration_type} settings').format(
integration_type=IntegrationType.display_name)
schema.add(settings_schema)
return schema

View file

@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016-2016 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import colander
from rhodecode import forms
from rhodecode.model.db import User
from rhodecode.translation import _
from rhodecode.lib.auth import check_password
@colander.deferred
def deferred_user_password_validator(node, kw):
username = kw.get('username')
user = User.get_by_username(username)
def _user_password_validator(node, value):
if not check_password(value, user.password):
msg = _('Password is incorrect')
raise colander.Invalid(node, msg)
return _user_password_validator
class ChangePasswordSchema(colander.Schema):
current_password = colander.SchemaNode(
colander.String(),
missing=colander.required,
widget=forms.widget.PasswordWidget(redisplay=True),
validator=deferred_user_password_validator)
new_password = colander.SchemaNode(
colander.String(),
missing=colander.required,
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

View file

@ -20,6 +20,8 @@
import colander
from rhodecode.model.db import User, UserGroup
class GroupNameType(colander.String):
SEPARATOR = '/'
@ -32,3 +34,72 @@ class GroupNameType(colander.String):
path = path.split(self.SEPARATOR)
path = [item for item in path if item]
return self.SEPARATOR.join(path)
class UserOrUserGroupType(colander.SchemaType):
""" colander Schema type for valid rhodecode user and/or usergroup """
scopes = ('user', 'usergroup')
def __init__(self):
self.users = 'user' in self.scopes
self.usergroups = 'usergroup' in self.scopes
def serialize(self, node, appstruct):
if appstruct is colander.null:
return colander.null
if self.users:
if isinstance(appstruct, User):
if self.usergroups:
return 'user:%s' % appstruct.username
return appstruct.username
if self.usergroups:
if isinstance(appstruct, UserGroup):
if self.users:
return 'usergroup:%s' % appstruct.users_group_name
return appstruct.users_group_name
raise colander.Invalid(
node, '%s is not a valid %s' % (appstruct, ' or '.join(self.scopes)))
def deserialize(self, node, cstruct):
if cstruct is colander.null:
return colander.null
user, usergroup = None, None
if self.users:
if cstruct.startswith('user:'):
user = User.get_by_username(cstruct.split(':')[1])
else:
user = User.get_by_username(cstruct)
if self.usergroups:
if cstruct.startswith('usergroup:'):
usergroup = UserGroup.get_by_group_name(cstruct.split(':')[1])
else:
usergroup = UserGroup.get_by_group_name(cstruct)
if self.users and self.usergroups:
if user and usergroup:
raise colander.Invalid(node, (
'%s is both a user and usergroup, specify which '
'one was wanted by prepending user: or usergroup: to the '
'name') % cstruct)
if self.users and user:
return user
if self.usergroups and usergroup:
return usergroup
raise colander.Invalid(
node, '%s is not a valid %s' % (cstruct, ' or '.join(self.scopes)))
class UserType(UserOrUserGroupType):
scopes = ('user',)
class UserGroupType(UserOrUserGroupType):
scopes = ('usergroup',)

View file

@ -13,7 +13,3 @@ def ip_addr_validator(node, value):
except ValueError:
msg = _(u'Please enter a valid IPv4 or IpV6 address')
raise colander.Invalid(node, msg)

File diff suppressed because one or more lines are too long

View file

@ -255,7 +255,7 @@ table.code-difftable {
/** LINE NUMBERS **/
.lineno {
padding-left: 2px;
padding-left: 2px !important;
padding-right: 2px;
text-align: right;
width: 32px;

View file

@ -12,6 +12,7 @@
.control-label {
width: 200px;
padding: 10px;
float: left;
}
.control-inputs {
@ -26,14 +27,37 @@
.form-group {
clear: left;
margin-bottom: 20px;
&:after { /* clear fix */
content: " ";
display: block;
clear: left;
}
}
.form-control {
width: 100%;
padding: 0.9em;
border: 1px solid #979797;
border-radius: 2px;
}
.form-control.select2-container {
padding: 0; /* padding already applied in .drop-menu a */
}
.form-control.readonly {
background: #eeeeee;
cursor: not-allowed;
}
.error-block {
color: red;
margin: 0;
}
.help-block {
margin: 0;
}
.deform-seq-container .control-inputs {
@ -62,7 +86,9 @@
}
}
.form-control.select2-container { height: 40px; }
.form-control.select2-container {
height: 40px;
}
.deform-two-field-sequence .deform-seq-container .deform-seq-item label {
display: none;
@ -74,7 +100,7 @@
display: none;
}
.deform-two-field-sequence .deform-seq-container .deform-seq-item.form-group {
background: red;
margin: 0;
}
.deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group .form-group {
width: 45%; padding: 0 2px; float: left; clear: none;

View file

@ -6,14 +6,15 @@ div.diffblock .code-header .changeset_header > div {
// Line select and comment
div.diffblock.margined.comm tr {
td {
position: relative;
// IMPORTANT - never position:relative this as it causes insanely
// slow rendering
}
.add-comment-line {
// Force td width for Firefox
width: 20px;
// TODO: anderson: fixing mouse-over bug.
// TODO: anderson: fixing mouse-over bug.
// why was it vertical-align baseline in first place??
vertical-align: top !important;
// Force width and display for IE 9
@ -23,14 +24,35 @@ div.diffblock.margined.comm tr {
a {
display: none;
position: absolute;
top: 2px;
left: 2px;
margin-top: 2px;
margin-left: 2px;
color: @grey3;
}
}
}
.comment-toggle {
position: relative;
min-width: 20px;
width: 20px;
color: @rcblue;
.icon-comment {
position: absolute;
top: 2px;
left: 0;
z-index: 100;
visibility: hidden;
}
&.active {
.icon-comment{
visibility: visible;
}
cursor: pointer;
}
}
&.line {
&:hover, &.hover{
.add-comment-line a{
@ -47,7 +69,7 @@ div.diffblock.margined.comm tr {
&.commenting {
&, del, ins {
background-image: none !important;
background-color: lighten(@alert4, 10%) !important;
background-color: lighten(@alert4, 10%) !important;
}
}
}
@ -75,4 +97,4 @@ div.diffblock.margined.comm tr {
clear: both;
font-family: @text-semibold;
}
}
}

View file

@ -25,10 +25,8 @@
@import 'comments';
@import 'panels-bootstrap';
@import 'panels';
@import 'toastr';
@import 'deform';
//--- BASE ------------------//
.noscript-error {
top: 0;
@ -1101,6 +1099,44 @@ table.issuetracker {
}
}
table.integrations {
.td-icon {
width: 20px;
.integration-icon {
height: 20px;
width: 20px;
}
}
}
.integrations {
a.integration-box {
color: @text-color;
&:hover {
.panel {
background: #fbfbfb;
}
}
.integration-icon {
width: 30px;
height: 30px;
margin-right: 20px;
float: left;
}
.panel-body {
padding: 10px;
}
.panel {
margin-bottom: 10px;
}
h2 {
display: inline-block;
margin: 0;
min-width: 140px;
}
}
}
//Permissions Settings
#add_perm {
@ -2103,3 +2139,8 @@ input[type=radio] {
padding: 0;
border: none;
}
.toggle-ajax-spinner{
height: 16px;
width: 16px;
}

View file

@ -0,0 +1,24 @@
//Primary CSS
//--- IMPORTS ------------------//
@import 'helpers';
@import 'mixins';
@import 'variables';
@import 'buttons';
@import 'alerts';
:root {
--primary-color: @rcblue;
--light-primary-color: @rclightblue;
--dark-primary-color: @rcdarkblue;
--primary-text-color: @text-color;
--paper-spinner-layer-1-color: @grey6;
--paper-spinner-layer-2-color: @grey5;
--paper-spinner-layer-3-color: @grey4;
--paper-spinner-layer-4-color: @grey3;
}
.paper-toggle-button {
display: inline;
}

View file

@ -117,7 +117,7 @@ table.dataTable {
&.annotate{
padding-right: 0;
div.annotatediv{
margin: 0 0.7em;
}
@ -138,7 +138,7 @@ table.dataTable {
&.td-journalaction {
min-width: 300px;
.journal_action_params {
.journal_action_params {
// waiting for feedback
}
}
@ -202,9 +202,11 @@ table.dataTable {
&.td-tags {
padding: .5em 1em .5em 0;
width: 140px;
.tag {
margin: 1px;
float: left;
}
}
@ -262,11 +264,11 @@ table.dataTable {
width: 150px;
height: 22px;
overflow: hidden;
.tag {
display: inline-block;
}
&.truncate {
height: 22px;
max-height:2em;
@ -428,7 +430,7 @@ table.trending_language_tbl {
}
}
// Compare
// Compare
table.compare_view_commits {
margin-top: @space;
@ -486,7 +488,7 @@ table.compare_view_commits {
td {
padding-top: @space;
}
&:first-child td {
padding-top: 0;
}

Some files were not shown because too many files have changed in this diff Show more